docs(03-operator-experience): create phase plan
This commit is contained in:
345
.planning/phases/03-operator-experience/03-01-PLAN.md
Normal file
345
.planning/phases/03-operator-experience/03-01-PLAN.md
Normal file
@@ -0,0 +1,345 @@
|
||||
---
|
||||
phase: 03-operator-experience
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- packages/shared/pyproject.toml
|
||||
- packages/portal/package.json
|
||||
- packages/shared/shared/config.py
|
||||
- packages/shared/shared/models/billing.py
|
||||
- packages/shared/shared/models/tenant.py
|
||||
- packages/shared/shared/api/billing.py
|
||||
- packages/shared/shared/api/channels.py
|
||||
- packages/shared/shared/api/usage.py
|
||||
- packages/shared/shared/crypto.py
|
||||
- packages/orchestrator/orchestrator/agents/runner.py
|
||||
- packages/orchestrator/orchestrator/audit/logger.py
|
||||
- migrations/versions/005_billing_and_usage.py
|
||||
- tests/unit/test_key_encryption.py
|
||||
- tests/unit/test_slack_oauth.py
|
||||
- tests/unit/test_stripe_webhooks.py
|
||||
- tests/unit/test_usage_aggregation.py
|
||||
- tests/unit/test_budget_alerts.py
|
||||
autonomous: true
|
||||
requirements:
|
||||
- AGNT-07
|
||||
- LLM-03
|
||||
- PRTA-03
|
||||
- PRTA-05
|
||||
- PRTA-06
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Audit events for LLM calls include prompt_tokens, completion_tokens, cost_usd, and provider in metadata"
|
||||
- "BYO API keys can be encrypted and decrypted without data loss using Fernet"
|
||||
- "Slack OAuth state can be HMAC-signed and verified for CSRF protection"
|
||||
- "Stripe webhook events can be processed idempotently"
|
||||
- "Token usage can be aggregated per agent and per provider from audit events"
|
||||
- "Budget alerts trigger at 80% and 100% thresholds"
|
||||
artifacts:
|
||||
- path: "packages/shared/shared/models/billing.py"
|
||||
provides: "TenantLlmKey model, StripeEvent model, billing field mixins"
|
||||
- path: "packages/shared/shared/crypto.py"
|
||||
provides: "KeyEncryptionService with Fernet encrypt/decrypt/rotate"
|
||||
- path: "packages/shared/shared/api/channels.py"
|
||||
provides: "Slack OAuth state generation/verification, OAuth callback endpoint"
|
||||
- path: "packages/shared/shared/api/billing.py"
|
||||
provides: "Stripe webhook handler, checkout session creation, billing portal session"
|
||||
- path: "packages/shared/shared/api/usage.py"
|
||||
provides: "Usage aggregation endpoints (per-agent tokens, per-provider cost, budget alerts)"
|
||||
- path: "migrations/versions/005_billing_and_usage.py"
|
||||
provides: "DB migration for billing fields, tenant_llm_keys, stripe_events, audit index, agent budget_limit_usd"
|
||||
key_links:
|
||||
- from: "packages/orchestrator/orchestrator/agents/runner.py"
|
||||
to: "packages/shared/shared/models/audit.py"
|
||||
via: "log_llm_call metadata includes token counts and cost"
|
||||
pattern: "prompt_tokens.*completion_tokens.*cost_usd"
|
||||
- from: "packages/shared/shared/api/usage.py"
|
||||
to: "audit_events table"
|
||||
via: "JSONB aggregate queries on metadata fields"
|
||||
pattern: "metadata.*prompt_tokens.*cost_usd"
|
||||
- from: "packages/shared/shared/crypto.py"
|
||||
to: "PLATFORM_ENCRYPTION_KEY env var"
|
||||
via: "Fernet key loaded at init"
|
||||
pattern: "MultiFernet"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Backend foundation for Phase 3: database migrations, dependency installs, audit trail token metadata, encryption service, and all backend API endpoints for billing, channel connection, and usage aggregation.
|
||||
|
||||
Purpose: Every portal UI feature in Phase 3 depends on backend APIs and database schema. This plan ships all backend infrastructure so Plans 02-04 can focus on frontend.
|
||||
Output: New DB tables/fields, billing + channel + usage API endpoints, encryption service, enhanced audit logger, and comprehensive test scaffolds.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-operator-experience/03-CONTEXT.md
|
||||
@.planning/phases/03-operator-experience/03-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing models the executor needs -->
|
||||
|
||||
From packages/shared/shared/models/tenant.py:
|
||||
```python
|
||||
class Tenant(Base):
|
||||
__tablename__ = "tenants"
|
||||
id: Mapped[uuid.UUID]
|
||||
name: Mapped[str]
|
||||
slug: Mapped[str]
|
||||
settings: Mapped[dict[str, Any]]
|
||||
created_at: Mapped[datetime]
|
||||
updated_at: Mapped[datetime]
|
||||
agents: Mapped[list[Agent]] = relationship(...)
|
||||
channel_connections: Mapped[list[ChannelConnection]] = relationship(...)
|
||||
|
||||
class Agent(Base):
|
||||
__tablename__ = "agents"
|
||||
id: Mapped[uuid.UUID]
|
||||
tenant_id: Mapped[uuid.UUID]
|
||||
name: Mapped[str]
|
||||
role: Mapped[str]
|
||||
is_active: Mapped[bool]
|
||||
# ... other fields
|
||||
|
||||
class ChannelConnection(Base):
|
||||
__tablename__ = "channel_connections"
|
||||
# channel_type, workspace_id, tenant_id, config (JSON), is_active
|
||||
```
|
||||
|
||||
From packages/shared/shared/models/audit.py:
|
||||
```python
|
||||
class AuditEvent(AuditBase):
|
||||
__tablename__ = "audit_events"
|
||||
id: Mapped[uuid.UUID]
|
||||
tenant_id: Mapped[uuid.UUID]
|
||||
agent_id: Mapped[uuid.UUID | None]
|
||||
action_type: Mapped[str] # "llm_call" | "tool_invocation" | "escalation"
|
||||
metadata: Mapped[dict[str, Any]] # JSONB
|
||||
created_at: Mapped[datetime]
|
||||
```
|
||||
|
||||
From packages/shared/shared/config.py:
|
||||
```python
|
||||
class Settings(BaseSettings):
|
||||
# existing: database_url, redis_url, slack_bot_token, slack_signing_secret, ...
|
||||
```
|
||||
|
||||
From packages/orchestrator/orchestrator/audit/logger.py:
|
||||
```python
|
||||
class AuditLogger:
|
||||
async def log_llm_call(self, tenant_id, agent_id, user_id, input_summary, output_summary, latency_ms, metadata=None)
|
||||
async def log_tool_call(self, tool_name, args, result, tenant_id, agent_id, latency_ms, error=None)
|
||||
async def log_escalation(self, ...)
|
||||
```
|
||||
|
||||
From packages/shared/shared/api/portal.py:
|
||||
```python
|
||||
portal_router = APIRouter(prefix="/api/portal")
|
||||
# Existing: /auth/verify, /auth/register, /tenants CRUD, /tenants/{id}/agents CRUD
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Database migrations, models, encryption service, and test scaffolds</name>
|
||||
<files>
|
||||
packages/shared/pyproject.toml,
|
||||
packages/portal/package.json,
|
||||
packages/shared/shared/config.py,
|
||||
packages/shared/shared/models/billing.py,
|
||||
packages/shared/shared/models/tenant.py,
|
||||
packages/shared/shared/crypto.py,
|
||||
migrations/versions/005_billing_and_usage.py,
|
||||
tests/unit/test_key_encryption.py,
|
||||
tests/unit/test_budget_alerts.py
|
||||
</files>
|
||||
<behavior>
|
||||
- test_encrypt_decrypt_roundtrip: KeyEncryptionService.encrypt(plaintext) -> decrypt -> returns original plaintext
|
||||
- test_encrypt_produces_different_ciphertext: encrypt("key") != encrypt("key") (Fernet uses random IV)
|
||||
- test_decrypt_invalid_raises: decrypt("garbage") raises InvalidToken
|
||||
- test_multifernet_rotation: rotate(old_ciphertext) produces new ciphertext decryptable by current key
|
||||
- test_budget_alert_no_limit: agent with budget_limit_usd=None -> no alert
|
||||
- test_budget_alert_under_threshold: usage at 50% -> status "ok"
|
||||
- test_budget_alert_warning: usage at 80% -> status "warning"
|
||||
- test_budget_alert_exceeded: usage at 100%+ -> status "exceeded"
|
||||
</behavior>
|
||||
<action>
|
||||
1. Install Python dependencies: `uv add stripe cryptography` in packages/shared/pyproject.toml
|
||||
2. Install Node dependencies: `npm install recharts @stripe/stripe-js stripe` in packages/portal/
|
||||
|
||||
3. Create `packages/shared/shared/models/billing.py`:
|
||||
- `TenantLlmKey` model: id (UUID PK), tenant_id (FK tenants.id CASCADE), provider (TEXT NOT NULL), label (TEXT NOT NULL), encrypted_key (TEXT NOT NULL), key_version (INT DEFAULT 1), created_at. UNIQUE(tenant_id, provider). Use AuditBase (same as audit_events — separate declarative base).
|
||||
- `StripeEvent` model: event_id (TEXT PK), processed_at (TIMESTAMPTZ DEFAULT now()). Use Base from tenant.py.
|
||||
- Note: tenant_llm_keys needs RLS enabled (same pattern as agents table).
|
||||
|
||||
4. Add billing fields to `Tenant` model in tenant.py:
|
||||
- stripe_customer_id: Mapped[str | None] (String(255), nullable=True)
|
||||
- stripe_subscription_id: Mapped[str | None] (String(255), nullable=True)
|
||||
- stripe_subscription_item_id: Mapped[str | None] (String(255), nullable=True)
|
||||
- subscription_status: Mapped[str] (String(50), default="none") — values: none, trialing, active, past_due, canceled, unpaid
|
||||
- trial_ends_at: Mapped[datetime | None] (DateTime(timezone=True), nullable=True)
|
||||
- agent_quota: Mapped[int] (Integer, default=0)
|
||||
|
||||
5. Add budget field to `Agent` model in tenant.py:
|
||||
- budget_limit_usd: Mapped[float | None] (Float, nullable=True, default=None) — NULL means no limit
|
||||
|
||||
6. Create `packages/shared/shared/crypto.py` — KeyEncryptionService:
|
||||
- Uses MultiFernet with PLATFORM_ENCRYPTION_KEY (required) and PLATFORM_ENCRYPTION_KEY_PREVIOUS (optional)
|
||||
- Methods: encrypt(plaintext: str) -> str, decrypt(ciphertext: str) -> str, rotate(ciphertext: str) -> str
|
||||
- See research Pattern 4 for exact implementation
|
||||
|
||||
7. Add to `packages/shared/shared/config.py`:
|
||||
- platform_encryption_key: str = Field(default="", description="Fernet key for BYO API key encryption")
|
||||
- platform_encryption_key_previous: str = Field(default="", description="Previous Fernet key for rotation")
|
||||
- stripe_secret_key: str = Field(default="", description="Stripe secret API key")
|
||||
- stripe_webhook_secret: str = Field(default="", description="Stripe webhook endpoint secret")
|
||||
- stripe_per_agent_price_id: str = Field(default="", description="Stripe Price ID for per-agent monthly plan")
|
||||
- portal_url: str = Field(default="http://localhost:3000", description="Portal base URL for Stripe redirects")
|
||||
- slack_client_id: str = Field(default="", description="Slack OAuth app client ID")
|
||||
- slack_client_secret: str = Field(default="", description="Slack OAuth app client secret")
|
||||
- slack_oauth_redirect_uri: str = Field(default="http://localhost:3000/api/slack/callback", description="Slack OAuth redirect URI")
|
||||
- oauth_state_secret: str = Field(default="", description="HMAC secret for OAuth state parameter signing")
|
||||
|
||||
8. Create Alembic migration `005_billing_and_usage.py`:
|
||||
- ADD COLUMNS to tenants: stripe_customer_id, stripe_subscription_id, stripe_subscription_item_id, subscription_status, trial_ends_at, agent_quota
|
||||
- ADD COLUMN to agents: budget_limit_usd
|
||||
- CREATE TABLE tenant_llm_keys with RLS enabled (same FORCE ROW LEVEL SECURITY pattern as agents)
|
||||
- CREATE TABLE stripe_events (event_id TEXT PK, processed_at TIMESTAMPTZ DEFAULT now())
|
||||
- CREATE INDEX idx_audit_events_tenant_type_created ON audit_events (tenant_id, action_type, created_at DESC)
|
||||
- GRANT SELECT, INSERT on tenant_llm_keys to konstruct_app
|
||||
- GRANT SELECT, INSERT on stripe_events to konstruct_app
|
||||
|
||||
9. Write test scaffolds:
|
||||
- tests/unit/test_key_encryption.py — test encrypt/decrypt roundtrip, rotation, invalid ciphertext
|
||||
- tests/unit/test_budget_alerts.py — test threshold logic (no limit, under 80%, at 80%, at 100%+)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/unit/test_key_encryption.py tests/unit/test_budget_alerts.py -x -v</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- stripe and cryptography in shared pyproject.toml, recharts and @stripe/stripe-js in portal package.json
|
||||
- Tenant model has billing fields, Agent model has budget_limit_usd
|
||||
- TenantLlmKey and StripeEvent models exist in billing.py
|
||||
- KeyEncryptionService passes encrypt/decrypt/rotate tests
|
||||
- Budget alert threshold logic passes at all levels
|
||||
- Alembic migration 005 exists with all schema changes
|
||||
- Config has all new settings fields
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Backend API endpoints — channels, billing, usage aggregation, and audit logger enhancement</name>
|
||||
<files>
|
||||
packages/shared/shared/api/channels.py,
|
||||
packages/shared/shared/api/billing.py,
|
||||
packages/shared/shared/api/usage.py,
|
||||
packages/shared/shared/api/portal.py,
|
||||
packages/orchestrator/orchestrator/agents/runner.py,
|
||||
tests/unit/test_slack_oauth.py,
|
||||
tests/unit/test_stripe_webhooks.py,
|
||||
tests/unit/test_usage_aggregation.py
|
||||
</files>
|
||||
<behavior>
|
||||
- test_generate_oauth_state: generate_oauth_state(tenant_id, secret) produces base64-encoded string containing tenant_id
|
||||
- test_verify_oauth_state_valid: verify_oauth_state(valid_state, secret) returns correct tenant_id
|
||||
- test_verify_oauth_state_tampered: verify_oauth_state(tampered_state, secret) raises ValueError
|
||||
- test_stripe_webhook_idempotency: processing same event_id twice returns "already_processed" on second call
|
||||
- test_stripe_subscription_updated: customer.subscription.updated event updates tenant subscription_status
|
||||
- test_stripe_cancellation: customer.subscription.deleted event sets status=canceled and deactivates agents
|
||||
- test_usage_group_by_agent: aggregation query groups prompt_tokens, completion_tokens, cost_usd by agent_id
|
||||
- test_usage_group_by_provider: aggregation query groups cost_usd by provider
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `packages/shared/shared/api/channels.py`:
|
||||
- `generate_oauth_state(tenant_id: str, secret: str) -> str` — HMAC-SHA256 signed state with nonce (see research Pattern 1)
|
||||
- `verify_oauth_state(state: str, secret: str) -> str` — returns tenant_id or raises ValueError
|
||||
- `GET /api/portal/channels/slack/install?tenant_id={id}` — generates state, returns Slack OAuth authorize URL with scopes: app_mentions:read,channels:read,channels:history,chat:write,im:read,im:write,im:history
|
||||
- `GET /api/portal/channels/slack/callback?code={code}&state={state}` — verifies state, exchanges code via POST to https://slack.com/api/oauth.v2.access, encrypts bot_token with KeyEncryptionService, stores in channel_connections (channel_type="slack", workspace_id=team.id, config={bot_token, bot_user_id, team_name})
|
||||
- `POST /api/portal/channels/whatsapp/connect` — accepts {tenant_id, phone_number_id, waba_id, system_user_token}, validates by calling GET https://graph.facebook.com/v22.0/{phone_number_id} with token, encrypts token, stores in channel_connections
|
||||
- `POST /api/portal/channels/{tenant_id}/test` — accepts {channel_type}, loads channel_connection for tenant, sends test message ("Konstruct connected successfully") via the appropriate channel SDK, returns success/failure
|
||||
|
||||
2. Create `packages/shared/shared/api/billing.py`:
|
||||
- `POST /api/portal/billing/checkout` — accepts {tenant_id, agent_count}, creates Stripe Customer if none exists (lazy creation per research recommendation), creates Checkout Session with mode="subscription", trial_period_days=14, quantity=agent_count, returns session.url
|
||||
- `POST /api/portal/billing/portal` — accepts {tenant_id}, creates Stripe Billing Portal session, returns portal_session.url
|
||||
- `POST /api/webhooks/stripe` — Stripe webhook handler: reads raw body with request.body(), verifies signature with stripe.Webhook.construct_event(), checks idempotency via StripeEvent table (INSERT ON CONFLICT DO NOTHING), dispatches to handler per event type:
|
||||
- checkout.session.completed: store subscription_id, subscription_item_id, set status
|
||||
- customer.subscription.updated: update subscription_status, agent_quota, trial_ends_at
|
||||
- customer.subscription.deleted: set status=canceled, set Agent.is_active=False for all tenant agents
|
||||
- invoice.paid: set status=active, re-enable agents
|
||||
- invoice.payment_failed: set status=past_due
|
||||
- Use StripeClient pattern (not legacy stripe.api_key): `client = stripe.StripeClient(api_key=settings.stripe_secret_key)`
|
||||
|
||||
3. Create `packages/shared/shared/api/usage.py`:
|
||||
- `GET /api/portal/usage/{tenant_id}/summary?start_date={}&end_date={}` — returns per-agent token usage and cost (SQL aggregate on audit_events WHERE action_type='llm_call', GROUP BY agent_id). Use CAST(:metadata AS jsonb) pattern for asyncpg.
|
||||
- `GET /api/portal/usage/{tenant_id}/by-provider?start_date={}&end_date={}` — returns cost grouped by provider
|
||||
- `GET /api/portal/usage/{tenant_id}/message-volume?start_date={}&end_date={}` — returns message count grouped by channel
|
||||
- `GET /api/portal/usage/{tenant_id}/budget-alerts` — for each agent with budget_limit_usd, compare current month cost_usd sum against limit. Return status: "ok" (<80%), "warning" (80-99%), "exceeded" (>=100%).
|
||||
- Include the composite index from migration 005 for performance.
|
||||
|
||||
4. Register new routers in the appropriate main.py files. Add channels_router, billing_router, and usage_router to the FastAPI app. The stripe webhook route should be on a separate prefix (/api/webhooks/stripe) without auth.
|
||||
|
||||
5. Enhance audit logger — in `packages/orchestrator/orchestrator/agents/runner.py`, extend the metadata dict passed to `log_llm_call()` to include:
|
||||
- prompt_tokens: extracted from LiteLLM response usage object
|
||||
- completion_tokens: extracted from LiteLLM response usage object
|
||||
- total_tokens: prompt + completion
|
||||
- cost_usd: use litellm.completion_cost() if available, otherwise estimate from model pricing table
|
||||
- provider: extract from model string (e.g., "anthropic/claude-sonnet-4" -> "anthropic")
|
||||
These fields are CRITICAL — the cost dashboard (Plan 04) queries them from audit_events.metadata JSONB.
|
||||
|
||||
6. Write test files:
|
||||
- tests/unit/test_slack_oauth.py — test state generation, verification, and tampered state rejection
|
||||
- tests/unit/test_stripe_webhooks.py — test idempotency (duplicate event skipped), subscription update, cancellation with agent deactivation
|
||||
- tests/unit/test_usage_aggregation.py — test per-agent grouping and per-provider grouping with mock audit data
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct && pytest tests/unit/test_slack_oauth.py tests/unit/test_stripe_webhooks.py tests/unit/test_usage_aggregation.py -x -v</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Slack OAuth install URL generation and callback exchange work (state HMAC verified)
|
||||
- WhatsApp manual connect endpoint validates token and stores encrypted
|
||||
- Test message endpoint sends via appropriate channel
|
||||
- Stripe checkout session creation, billing portal session, and webhook handler all functional
|
||||
- Webhook idempotency prevents duplicate processing
|
||||
- Subscription cancellation deactivates all tenant agents
|
||||
- Usage aggregation returns per-agent and per-provider data from audit_events
|
||||
- Budget alerts return correct status for each threshold level
|
||||
- Audit logger now includes token counts and cost in LLM call metadata
|
||||
- All unit tests pass
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
All Wave 0 test scaffolds created and passing:
|
||||
- `pytest tests/unit/test_key_encryption.py -x` — Fernet encrypt/decrypt/rotate
|
||||
- `pytest tests/unit/test_budget_alerts.py -x` — threshold logic
|
||||
- `pytest tests/unit/test_slack_oauth.py -x` — OAuth state HMAC
|
||||
- `pytest tests/unit/test_stripe_webhooks.py -x` — idempotency, status updates, cancellation
|
||||
- `pytest tests/unit/test_usage_aggregation.py -x` — SQL aggregates
|
||||
- `pytest tests/unit -x -q` — full unit suite still green
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All 5 test files pass with 0 failures
|
||||
- Alembic migration 005 exists and is syntactically valid
|
||||
- New API routers registered and importable
|
||||
- KeyEncryptionService encrypt/decrypt roundtrip works
|
||||
- Audit logger metadata includes prompt_tokens, completion_tokens, cost_usd, provider
|
||||
- Existing test suite remains green
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-operator-experience/03-01-SUMMARY.md`
|
||||
</output>
|
||||
273
.planning/phases/03-operator-experience/03-02-PLAN.md
Normal file
273
.planning/phases/03-operator-experience/03-02-PLAN.md
Normal file
@@ -0,0 +1,273 @@
|
||||
---
|
||||
phase: 03-operator-experience
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["03-01"]
|
||||
files_modified:
|
||||
- packages/portal/app/api/slack/callback/route.ts
|
||||
- packages/portal/app/(dashboard)/onboarding/page.tsx
|
||||
- packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx
|
||||
- packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx
|
||||
- packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx
|
||||
- packages/portal/app/(dashboard)/settings/api-keys/page.tsx
|
||||
- packages/portal/lib/queries.ts
|
||||
- packages/portal/components/onboarding-stepper.tsx
|
||||
autonomous: false
|
||||
requirements:
|
||||
- PRTA-03
|
||||
- PRTA-04
|
||||
- LLM-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Operator can click Add to Slack and complete OAuth flow to connect their workspace"
|
||||
- "Operator can paste WhatsApp credentials and have them validated and stored"
|
||||
- "After connecting a channel, operator can send a test message that verifies end-to-end connectivity"
|
||||
- "Agent goes live automatically after test message succeeds"
|
||||
- "Operator completes the full onboarding sequence (connect -> configure -> test) in a guided wizard"
|
||||
- "Operator can add, view, and delete BYO LLM API keys from a settings page"
|
||||
artifacts:
|
||||
- path: "packages/portal/app/api/slack/callback/route.ts"
|
||||
provides: "Next.js Route Handler for Slack OAuth redirect"
|
||||
- path: "packages/portal/app/(dashboard)/onboarding/page.tsx"
|
||||
provides: "Onboarding wizard with 3-step stepper"
|
||||
- path: "packages/portal/app/(dashboard)/settings/api-keys/page.tsx"
|
||||
provides: "BYO API key management page"
|
||||
key_links:
|
||||
- from: "packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx"
|
||||
to: "/api/portal/channels/slack/install"
|
||||
via: "fetch to get OAuth URL, then window.location redirect"
|
||||
pattern: "channels/slack/install"
|
||||
- from: "packages/portal/app/api/slack/callback/route.ts"
|
||||
to: "/api/portal/channels/slack/callback"
|
||||
via: "proxy the OAuth callback to FastAPI backend"
|
||||
pattern: "channels/slack/callback"
|
||||
- from: "packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx"
|
||||
to: "/api/portal/channels/{tenant_id}/test"
|
||||
via: "POST to send test message"
|
||||
pattern: "channels.*test"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Channel connection wizard (Slack OAuth + WhatsApp manual setup), onboarding flow with 3-step stepper, and BYO API key management page.
|
||||
|
||||
Purpose: Operators can connect their messaging channels and onboard their AI employee through a guided wizard, plus manage their own LLM API keys -- all from the portal UI.
|
||||
Output: Onboarding wizard (connect channel -> configure agent -> test message), Slack OAuth callback handler, WhatsApp manual connect form, BYO key settings page.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-operator-experience/03-CONTEXT.md
|
||||
@.planning/phases/03-operator-experience/03-RESEARCH.md
|
||||
@.planning/phases/03-operator-experience/03-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 backend APIs -->
|
||||
|
||||
From packages/shared/shared/api/channels.py (created in Plan 01):
|
||||
```python
|
||||
# GET /api/portal/channels/slack/install?tenant_id={id} -> { "authorize_url": "https://slack.com/oauth/v2/authorize?..." }
|
||||
# GET /api/portal/channels/slack/callback?code={code}&state={state} -> { "success": true, "workspace_name": "..." }
|
||||
# POST /api/portal/channels/whatsapp/connect -> { "success": true } (body: { tenant_id, phone_number_id, waba_id, system_user_token })
|
||||
# POST /api/portal/channels/{tenant_id}/test -> { "success": true, "message": "Test message sent" } (body: { channel_type })
|
||||
```
|
||||
|
||||
From packages/shared/shared/api/billing.py (created in Plan 01):
|
||||
```python
|
||||
# POST /api/portal/billing/checkout -> { "checkout_url": "https://checkout.stripe.com/..." }
|
||||
# POST /api/portal/billing/portal -> { "portal_url": "https://billing.stripe.com/..." }
|
||||
```
|
||||
|
||||
From packages/shared/shared/crypto.py (created in Plan 01):
|
||||
```python
|
||||
class KeyEncryptionService:
|
||||
def encrypt(self, plaintext: str) -> str: ...
|
||||
def decrypt(self, ciphertext: str) -> str: ...
|
||||
def rotate(self, ciphertext: str) -> str: ...
|
||||
```
|
||||
|
||||
<!-- Existing portal patterns -->
|
||||
|
||||
From packages/portal/lib/api.ts:
|
||||
```typescript
|
||||
// API client for FastAPI backend — use this for all portal API calls
|
||||
```
|
||||
|
||||
From packages/portal/lib/queries.ts:
|
||||
```typescript
|
||||
// TanStack Query hooks — add new hooks for channels, billing, usage here
|
||||
```
|
||||
|
||||
Established patterns:
|
||||
- shadcn/ui components
|
||||
- react-hook-form + zod v4 + standardSchemaResolver
|
||||
- TanStack Query for data fetching
|
||||
- App Router with (dashboard) route group
|
||||
- proxy.ts for auth protection (Next.js 16 pattern)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Slack OAuth callback route, onboarding wizard with 3-step stepper, and channel connection forms</name>
|
||||
<files>
|
||||
packages/portal/app/api/slack/callback/route.ts,
|
||||
packages/portal/app/(dashboard)/onboarding/page.tsx,
|
||||
packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx,
|
||||
packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx,
|
||||
packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx,
|
||||
packages/portal/components/onboarding-stepper.tsx,
|
||||
packages/portal/lib/queries.ts
|
||||
</files>
|
||||
<action>
|
||||
1. Create `packages/portal/app/api/slack/callback/route.ts`:
|
||||
- Next.js Route Handler (GET) that receives the OAuth redirect from Slack
|
||||
- Extract `code` and `state` from searchParams
|
||||
- Forward to FastAPI backend: GET /api/portal/channels/slack/callback?code={code}&state={state}
|
||||
- On success: redirect to /onboarding?step=2&channel=slack&connected=true
|
||||
- On error: redirect to /onboarding?step=1&error=slack_auth_failed
|
||||
|
||||
2. Create `packages/portal/components/onboarding-stepper.tsx`:
|
||||
- 3-step stepper component using shadcn/ui: "Connect Channel" -> "Configure Agent" -> "Test Message"
|
||||
- Visual progress indicator (numbered steps with active/complete/pending states)
|
||||
- Step state managed via URL searchParams (step=1|2|3) for shareable/refreshable URLs
|
||||
- Each step renders the corresponding step component
|
||||
|
||||
3. Create `packages/portal/app/(dashboard)/onboarding/page.tsx`:
|
||||
- Reads `step` from searchParams (default 1)
|
||||
- Reads `tenant_id` from session or query param
|
||||
- Renders OnboardingStepper with the appropriate step component
|
||||
- Guards: if no tenant selected, redirect to tenant selection
|
||||
|
||||
4. Create `packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx`:
|
||||
- Two cards: "Add to Slack" and "Connect WhatsApp"
|
||||
- **Slack card:** Button that fetches /api/portal/channels/slack/install?tenant_id={id}, then does window.location.href = authorize_url (external redirect to Slack)
|
||||
- **WhatsApp card:** Expandable form with 3 fields (Phone Number ID, WhatsApp Business Account ID, System User Token) + step-by-step instructions text
|
||||
- On submit: POST /api/portal/channels/whatsapp/connect
|
||||
- Validation with zod + react-hook-form
|
||||
- On successful connection (either channel): advance to step 2 automatically
|
||||
- If returning from Slack OAuth (connected=true in searchParams): show success toast and advance
|
||||
|
||||
5. Create `packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx`:
|
||||
- If tenant already has an agent: show existing agent summary with "Edit" link to Agent Designer
|
||||
- If no agent: redirect to Agent Designer page with return URL back to onboarding?step=3
|
||||
- Minimal step — the Agent Designer (from Phase 1) handles all agent configuration
|
||||
- "Next" button enabled only when at least one active agent exists for the tenant
|
||||
|
||||
6. Create `packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx`:
|
||||
- Shows connected channel(s) with a "Send Test Message" button per channel
|
||||
- On click: POST /api/portal/channels/{tenant_id}/test with {channel_type}
|
||||
- Shows loading state while test runs, then success/failure result
|
||||
- On success: show "Your AI employee is live!" celebration message
|
||||
- Agent goes live automatically (is_active already true by default) — NO separate "Go Live" button per user decision
|
||||
- Per user decision: test message step is REQUIRED, not skippable
|
||||
|
||||
7. Add TanStack Query hooks to `packages/portal/lib/queries.ts`:
|
||||
- useSlackInstallUrl(tenantId) — GET /channels/slack/install
|
||||
- useConnectWhatsApp() — mutation POST /channels/whatsapp/connect
|
||||
- useSendTestMessage() — mutation POST /channels/{tenantId}/test
|
||||
- useChannelConnections(tenantId) — GET to list existing connections
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Slack OAuth callback route handler proxies to FastAPI and redirects appropriately
|
||||
- Onboarding page renders 3-step stepper with progress indicator
|
||||
- Connect Channel step shows Slack OAuth button and WhatsApp manual form
|
||||
- Configure Agent step links to existing Agent Designer
|
||||
- Test Message step sends test and shows result
|
||||
- Agent goes live automatically after successful test (no Go Live button)
|
||||
- All TanStack Query hooks defined
|
||||
- Portal builds without errors
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: BYO API key management settings page</name>
|
||||
<files>
|
||||
packages/portal/app/(dashboard)/settings/api-keys/page.tsx,
|
||||
packages/portal/lib/queries.ts
|
||||
</files>
|
||||
<action>
|
||||
1. Create `packages/portal/app/(dashboard)/settings/api-keys/page.tsx`:
|
||||
- Tenant-level settings page (per user decision — simpler than per-agent for v1)
|
||||
- List existing BYO keys: show provider name, label, created date (NOT the key itself — never display decrypted keys)
|
||||
- "Add API Key" button opens a form:
|
||||
- Provider: select dropdown (OpenAI, Anthropic, Custom)
|
||||
- Label: text input (human-readable name, e.g., "Production OpenAI key")
|
||||
- API Key: password input (masked by default)
|
||||
- Submit: POST to /api/portal/tenants/{tenant_id}/llm-keys (needs endpoint added to Plan 01's channels.py or a new file)
|
||||
- Delete button per key with confirmation dialog
|
||||
- Use shadcn/ui Card, Table, Dialog, Button, Input, Select components
|
||||
- react-hook-form + zod for validation (provider required, label 3-100 chars, key not empty)
|
||||
|
||||
2. Add TanStack Query hooks to `packages/portal/lib/queries.ts`:
|
||||
- useLlmKeys(tenantId) — GET /api/portal/tenants/{tenant_id}/llm-keys
|
||||
- useAddLlmKey() — mutation POST /api/portal/tenants/{tenant_id}/llm-keys
|
||||
- useDeleteLlmKey() — mutation DELETE /api/portal/tenants/{tenant_id}/llm-keys/{keyId}
|
||||
|
||||
3. Add navigation link to settings/api-keys in the dashboard layout sidebar (if sidebar exists) or in the tenant detail page.
|
||||
|
||||
Note: The backend endpoints for LLM key CRUD may need to be added to Plan 01's API if not already included. If the endpoint doesn't exist yet, create it in this task: add routes to packages/shared/shared/api/channels.py or create a separate packages/shared/shared/api/llm_keys.py with GET (list, redacted), POST (encrypt + store), DELETE.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- BYO API key settings page renders with list of existing keys (redacted)
|
||||
- Add key form validates and submits to backend
|
||||
- Delete key with confirmation dialog works
|
||||
- Portal builds without errors
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify onboarding wizard and BYO key management</name>
|
||||
<files>n/a</files>
|
||||
<action>
|
||||
Human verifies the onboarding wizard and BYO key management UI:
|
||||
1. Start the portal: `cd packages/portal && npm run dev`
|
||||
2. Navigate to /onboarding — verify 3-step stepper displays
|
||||
3. Step 1: Verify "Add to Slack" button and WhatsApp form are present
|
||||
4. Step 2: Verify it links to Agent Designer or shows existing agent
|
||||
5. Step 3: Verify "Send Test Message" button is present
|
||||
6. Navigate to /settings/api-keys — verify key list and add form render
|
||||
7. Try adding a BYO key with the form — verify it submits without JS errors
|
||||
8. Confirm there is NO separate "Go Live" button — agent goes live after test
|
||||
</action>
|
||||
<verify>Human visual inspection of onboarding flow and settings page</verify>
|
||||
<done>Operator confirms onboarding wizard works through all 3 steps and BYO key page renders correctly</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- Portal builds successfully: `cd packages/portal && npx next build`
|
||||
- Onboarding wizard navigable through all 3 steps
|
||||
- BYO API key page renders and accepts input
|
||||
- No separate "Go Live" button exists (per user decision)
|
||||
- Test message step is required (not skippable)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Operator can initiate Slack OAuth from the portal
|
||||
- Operator can paste WhatsApp credentials via guided form
|
||||
- Onboarding wizard completes in 3 steps: connect -> configure -> test
|
||||
- Agent goes live automatically after successful test message
|
||||
- Operator can manage BYO API keys from settings page
|
||||
- Portal builds without errors
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-operator-experience/03-02-SUMMARY.md`
|
||||
</output>
|
||||
192
.planning/phases/03-operator-experience/03-03-PLAN.md
Normal file
192
.planning/phases/03-operator-experience/03-03-PLAN.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
phase: 03-operator-experience
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["03-01"]
|
||||
files_modified:
|
||||
- packages/portal/app/(dashboard)/billing/page.tsx
|
||||
- packages/portal/components/subscription-card.tsx
|
||||
- packages/portal/components/billing-status.tsx
|
||||
- packages/portal/lib/queries.ts
|
||||
autonomous: false
|
||||
requirements:
|
||||
- PRTA-05
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Operator can subscribe to a per-agent monthly plan via Stripe Checkout"
|
||||
- "Operator can upgrade (add agents) and downgrade (remove agents) their subscription"
|
||||
- "Operator can cancel their subscription"
|
||||
- "Operator can manage payment methods and view invoices via Stripe Billing Portal"
|
||||
- "Feature limits are enforced based on subscription state (agents deactivated on cancellation)"
|
||||
- "14-day free trial with full access is available"
|
||||
artifacts:
|
||||
- path: "packages/portal/app/(dashboard)/billing/page.tsx"
|
||||
provides: "Billing management page with subscription status and actions"
|
||||
- path: "packages/portal/components/subscription-card.tsx"
|
||||
provides: "Card showing current plan, agent count, status, trial info"
|
||||
- path: "packages/portal/components/billing-status.tsx"
|
||||
provides: "Status badge for subscription state (trialing, active, past_due, canceled)"
|
||||
key_links:
|
||||
- from: "packages/portal/app/(dashboard)/billing/page.tsx"
|
||||
to: "/api/portal/billing/checkout"
|
||||
via: "POST to create Checkout Session, then redirect to Stripe"
|
||||
pattern: "billing/checkout"
|
||||
- from: "packages/portal/app/(dashboard)/billing/page.tsx"
|
||||
to: "/api/portal/billing/portal"
|
||||
via: "POST to create Billing Portal session, then redirect"
|
||||
pattern: "billing/portal"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Billing management page with Stripe subscription integration -- subscribe, upgrade, downgrade, cancel, and manage payment via Stripe Billing Portal.
|
||||
|
||||
Purpose: Operators can self-serve their subscription lifecycle through the portal, with per-agent monthly pricing that matches the "hire an employee" metaphor.
|
||||
Output: Billing page with subscription card, Checkout redirect, Billing Portal redirect, status display.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-operator-experience/03-CONTEXT.md
|
||||
@.planning/phases/03-operator-experience/03-RESEARCH.md
|
||||
@.planning/phases/03-operator-experience/03-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 backend APIs -->
|
||||
|
||||
From packages/shared/shared/api/billing.py (created in Plan 01):
|
||||
```python
|
||||
# POST /api/portal/billing/checkout -> { "checkout_url": "https://checkout.stripe.com/..." }
|
||||
# Body: { "tenant_id": str, "agent_count": int }
|
||||
# POST /api/portal/billing/portal -> { "portal_url": "https://billing.stripe.com/..." }
|
||||
# Body: { "tenant_id": str }
|
||||
# POST /api/webhooks/stripe -> webhook handler (no portal UI interaction)
|
||||
```
|
||||
|
||||
From packages/shared/shared/models/tenant.py (updated in Plan 01):
|
||||
```python
|
||||
class Tenant(Base):
|
||||
# New billing fields:
|
||||
stripe_customer_id: Mapped[str | None]
|
||||
stripe_subscription_id: Mapped[str | None]
|
||||
stripe_subscription_item_id: Mapped[str | None]
|
||||
subscription_status: Mapped[str] # "none" | "trialing" | "active" | "past_due" | "canceled" | "unpaid"
|
||||
trial_ends_at: Mapped[datetime | None]
|
||||
agent_quota: Mapped[int]
|
||||
```
|
||||
|
||||
Established portal patterns:
|
||||
- shadcn/ui components (Card, Badge, Button, Dialog)
|
||||
- TanStack Query for data fetching
|
||||
- API client in lib/api.ts
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Billing page with subscription management</name>
|
||||
<files>
|
||||
packages/portal/app/(dashboard)/billing/page.tsx,
|
||||
packages/portal/components/subscription-card.tsx,
|
||||
packages/portal/components/billing-status.tsx,
|
||||
packages/portal/lib/queries.ts
|
||||
</files>
|
||||
<action>
|
||||
1. Create `packages/portal/components/billing-status.tsx`:
|
||||
- Badge component showing subscription status with color coding:
|
||||
- "trialing" -> blue badge with trial end date
|
||||
- "active" -> green badge
|
||||
- "past_due" -> amber badge with "Payment required" text
|
||||
- "canceled" -> red badge
|
||||
- "none" -> gray badge "No subscription"
|
||||
- Uses shadcn/ui Badge component
|
||||
|
||||
2. Create `packages/portal/components/subscription-card.tsx`:
|
||||
- shadcn/ui Card displaying:
|
||||
- BillingStatus badge (top right)
|
||||
- Plan name: "AI Employee Plan" (per-agent monthly)
|
||||
- Price: "$49/agent/month" (from user decision)
|
||||
- Current agent count vs quota
|
||||
- Trial info: if trialing, show "Trial ends {date}" with days remaining
|
||||
- Agent count adjuster: +/- buttons to change quantity (triggers Stripe subscription item update)
|
||||
- Action buttons:
|
||||
- If status="none": "Subscribe" button -> creates Checkout Session -> redirects to Stripe
|
||||
- If status="trialing" or "active": "Manage Billing" button -> creates Billing Portal session -> redirects to Stripe hosted portal
|
||||
- If status="past_due": "Update Payment" button -> Billing Portal redirect
|
||||
- If status="canceled": "Resubscribe" button -> new Checkout Session
|
||||
|
||||
3. Create `packages/portal/app/(dashboard)/billing/page.tsx`:
|
||||
- Reads tenant_id from query or session
|
||||
- Fetches tenant data (includes billing fields) via existing useTenant() hook
|
||||
- Renders SubscriptionCard
|
||||
- Handles ?session_id= searchParam (Stripe Checkout success redirect): show success toast, refetch tenant
|
||||
- If subscription_status is "past_due": show a top banner warning about payment failure
|
||||
- Per user decision: per-agent monthly pricing ($49/agent/month), 14-day free trial with full access, credit card required upfront
|
||||
|
||||
4. Add TanStack Query hooks to `packages/portal/lib/queries.ts`:
|
||||
- useCreateCheckoutSession() — mutation POST /billing/checkout, returns { checkout_url }
|
||||
- useCreateBillingPortalSession() — mutation POST /billing/portal, returns { portal_url }
|
||||
- useUpdateSubscriptionQuantity() — mutation (if needed for +/- agent count)
|
||||
|
||||
5. Add "Billing" link to dashboard navigation/sidebar.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Billing page renders subscription card with current status
|
||||
- Subscribe button creates Checkout Session and redirects to Stripe
|
||||
- Manage Billing button redirects to Stripe Billing Portal
|
||||
- Status badges show correct colors for all subscription states
|
||||
- Trial info displayed when status is "trialing"
|
||||
- Past due banner shown when payment has failed
|
||||
- Portal builds without errors
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: Verify billing page and subscription UI</name>
|
||||
<files>n/a</files>
|
||||
<action>
|
||||
Human verifies the billing management page:
|
||||
1. Start the portal: `cd packages/portal && npm run dev`
|
||||
2. Navigate to /billing — verify subscription card renders
|
||||
3. Verify status badge shows "No subscription" for new tenant
|
||||
4. Verify "Subscribe" button is present
|
||||
5. Verify pricing shows "$49/agent/month" and "14-day free trial"
|
||||
6. Verify "Billing" link appears in dashboard navigation
|
||||
7. Check that agent count +/- controls are present
|
||||
</action>
|
||||
<verify>Human visual inspection of billing page</verify>
|
||||
<done>Operator confirms billing page renders correctly with subscription card, pricing, and action buttons</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- Portal builds: `cd packages/portal && npx next build`
|
||||
- Billing page renders without errors
|
||||
- All subscription states display correctly
|
||||
- Navigation to billing page works
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Operator can see subscription status and pricing on billing page
|
||||
- Subscribe flow initiates Stripe Checkout redirect
|
||||
- Billing Portal accessible for managing payment/invoices
|
||||
- Status badges accurately reflect subscription_status field
|
||||
- Portal builds successfully
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-operator-experience/03-03-SUMMARY.md`
|
||||
</output>
|
||||
215
.planning/phases/03-operator-experience/03-04-PLAN.md
Normal file
215
.planning/phases/03-operator-experience/03-04-PLAN.md
Normal file
@@ -0,0 +1,215 @@
|
||||
---
|
||||
phase: 03-operator-experience
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["03-01", "03-03"]
|
||||
files_modified:
|
||||
- packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx
|
||||
- packages/portal/components/usage-chart.tsx
|
||||
- packages/portal/components/provider-cost-chart.tsx
|
||||
- packages/portal/components/budget-alert-badge.tsx
|
||||
- packages/portal/components/message-volume-chart.tsx
|
||||
- packages/portal/lib/queries.ts
|
||||
autonomous: false
|
||||
requirements:
|
||||
- AGNT-07
|
||||
- PRTA-06
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Operator can see token usage per agent in a chart"
|
||||
- "Operator can see cost breakdown by LLM provider"
|
||||
- "Operator can see message volume per channel"
|
||||
- "Operator can filter usage data by time range"
|
||||
- "Budget alerts are visible when an agent approaches or exceeds its budget limit"
|
||||
- "Dashboard answers 'how much is this employee costing me?' at a glance"
|
||||
artifacts:
|
||||
- path: "packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx"
|
||||
provides: "Cost tracking and usage metrics dashboard page"
|
||||
- path: "packages/portal/components/usage-chart.tsx"
|
||||
provides: "Bar chart of token usage per agent (Recharts)"
|
||||
- path: "packages/portal/components/provider-cost-chart.tsx"
|
||||
provides: "Pie or bar chart of cost breakdown by LLM provider (Recharts)"
|
||||
- path: "packages/portal/components/budget-alert-badge.tsx"
|
||||
provides: "Colored badge showing budget status (ok/warning/exceeded)"
|
||||
- path: "packages/portal/components/message-volume-chart.tsx"
|
||||
provides: "Bar chart of message volume per channel (Recharts)"
|
||||
key_links:
|
||||
- from: "packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx"
|
||||
to: "/api/portal/usage/{tenantId}/summary"
|
||||
via: "TanStack Query hook fetching usage data"
|
||||
pattern: "usage.*summary"
|
||||
- from: "packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx"
|
||||
to: "/api/portal/usage/{tenantId}/budget-alerts"
|
||||
via: "TanStack Query hook fetching budget alert statuses"
|
||||
pattern: "usage.*budget-alerts"
|
||||
- from: "packages/portal/components/usage-chart.tsx"
|
||||
to: "recharts"
|
||||
via: "BarChart, ResponsiveContainer components"
|
||||
pattern: "import.*recharts"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Cost tracking dashboard with per-agent token usage, provider cost breakdown, message volume, time range filtering, and budget alert indicators.
|
||||
|
||||
Purpose: Operators can see "how much is this employee costing me?" at a glance -- like a payroll view. Budget alerts warn when agents approach or exceed their configured limits.
|
||||
Output: Usage dashboard page with Recharts visualizations, budget alert badges, and time range selector.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/03-operator-experience/03-CONTEXT.md
|
||||
@.planning/phases/03-operator-experience/03-RESEARCH.md
|
||||
@.planning/phases/03-operator-experience/03-01-SUMMARY.md
|
||||
@.planning/phases/03-operator-experience/03-03-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 01 backend APIs -->
|
||||
|
||||
From packages/shared/shared/api/usage.py (created in Plan 01):
|
||||
```python
|
||||
# GET /api/portal/usage/{tenant_id}/summary?start_date={}&end_date={}
|
||||
# Returns: [{ agent_id, agent_name, prompt_tokens, completion_tokens, total_tokens, cost_usd, llm_call_count }]
|
||||
|
||||
# GET /api/portal/usage/{tenant_id}/by-provider?start_date={}&end_date={}
|
||||
# Returns: [{ provider, cost_usd, call_count }]
|
||||
|
||||
# GET /api/portal/usage/{tenant_id}/message-volume?start_date={}&end_date={}
|
||||
# Returns: [{ channel, message_count }]
|
||||
|
||||
# GET /api/portal/usage/{tenant_id}/budget-alerts
|
||||
# Returns: [{ agent_id, agent_name, budget_limit_usd, current_cost_usd, status: "ok"|"warning"|"exceeded" }]
|
||||
```
|
||||
|
||||
Established portal patterns:
|
||||
- shadcn/ui components (Card, Badge, Select)
|
||||
- TanStack Query for data fetching
|
||||
- Recharts installed in Plan 01 (npm install recharts)
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Cost dashboard page with Recharts visualizations and budget alerts</name>
|
||||
<files>
|
||||
packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx,
|
||||
packages/portal/components/usage-chart.tsx,
|
||||
packages/portal/components/provider-cost-chart.tsx,
|
||||
packages/portal/components/message-volume-chart.tsx,
|
||||
packages/portal/components/budget-alert-badge.tsx,
|
||||
packages/portal/lib/queries.ts
|
||||
</files>
|
||||
<action>
|
||||
1. Create `packages/portal/components/budget-alert-badge.tsx`:
|
||||
- Colored badge based on status:
|
||||
- "ok" -> green (or no badge if usage is low)
|
||||
- "warning" -> amber with "80%+ budget used" text
|
||||
- "exceeded" -> red with "Budget exceeded" text
|
||||
- Null budget_limit_usd -> gray "No limit set" text (not a warning)
|
||||
- Uses shadcn/ui Badge
|
||||
|
||||
2. Create `packages/portal/components/usage-chart.tsx`:
|
||||
- Recharts BarChart showing total_tokens per agent
|
||||
- ResponsiveContainer for adaptive sizing
|
||||
- XAxis: agent name, YAxis: token count
|
||||
- Tooltip showing prompt_tokens, completion_tokens, total_tokens, cost_usd
|
||||
- Color: indigo-500 (#6366f1) for consistency
|
||||
- Handles empty state: "No usage data for this period" message
|
||||
|
||||
3. Create `packages/portal/components/provider-cost-chart.tsx`:
|
||||
- Recharts BarChart (horizontal) showing cost_usd per provider
|
||||
- Each provider gets a distinct color (openai=green, anthropic=orange, ollama=blue)
|
||||
- Tooltip: provider name, cost, call count
|
||||
- Handles empty state
|
||||
|
||||
4. Create `packages/portal/components/message-volume-chart.tsx`:
|
||||
- Recharts BarChart showing message_count per channel (slack, whatsapp)
|
||||
- Channel-specific colors (slack=purple, whatsapp=green)
|
||||
- Handles empty state
|
||||
|
||||
5. Create `packages/portal/app/(dashboard)/usage/[tenantId]/page.tsx`:
|
||||
- Time range selector at top: simple <select> with options: "Last 7 days", "Last 30 days", "This month", "Last 3 months". Default: "Last 30 days". Drives start_date/end_date query params.
|
||||
- Layout (responsive grid, 1 col mobile, 2 cols desktop):
|
||||
- **Top row:** Summary cards — total cost, total tokens, total messages (large numbers, shadcn Card)
|
||||
- **Row 2 left:** UsageChart (tokens per agent)
|
||||
- **Row 2 right:** ProviderCostChart (cost by provider)
|
||||
- **Row 3 left:** MessageVolumeChart (messages by channel)
|
||||
- **Row 3 right:** Budget Alerts table — list each agent with BudgetAlertBadge, current cost, limit. Only show agents that have a budget_limit_usd set.
|
||||
- All data fetched via TanStack Query hooks with start_date/end_date params
|
||||
- Loading skeletons while data loads
|
||||
- Per user decision: this should feel like a "payroll view" — answer "how much is this employee costing me?" at a glance
|
||||
|
||||
6. Add TanStack Query hooks to `packages/portal/lib/queries.ts`:
|
||||
- useUsageSummary(tenantId, startDate, endDate)
|
||||
- useUsageByProvider(tenantId, startDate, endDate)
|
||||
- useMessageVolume(tenantId, startDate, endDate)
|
||||
- useBudgetAlerts(tenantId)
|
||||
|
||||
7. Add "Usage" link to dashboard navigation/sidebar, linking to /usage/{tenantId}.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- Usage page renders with time range selector defaulting to "Last 30 days"
|
||||
- Token usage bar chart renders per agent (or empty state)
|
||||
- Provider cost chart renders per provider
|
||||
- Message volume chart renders per channel
|
||||
- Budget alert badges show correct colors (ok/warning/exceeded)
|
||||
- Summary cards show totals at the top
|
||||
- Responsive layout works on mobile and desktop
|
||||
- Portal builds without errors
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 2: Verify cost dashboard and budget alerts</name>
|
||||
<files>n/a</files>
|
||||
<action>
|
||||
Human verifies the cost tracking dashboard:
|
||||
1. Start the portal: `cd packages/portal && npm run dev`
|
||||
2. Navigate to /usage/{tenantId} — verify dashboard renders
|
||||
3. Verify time range selector shows options (Last 7 days, Last 30 days, This month, Last 3 months)
|
||||
4. Verify chart areas render (may show empty state if no audit data)
|
||||
5. Verify budget alerts section is visible
|
||||
6. Verify summary cards at top show totals
|
||||
7. Verify responsive layout (resize browser to mobile width)
|
||||
8. Verify "Usage" link in dashboard navigation
|
||||
9. Confirm it feels like a "payroll view" — quick answer to "how much is this employee costing me?"
|
||||
</action>
|
||||
<verify>Human visual inspection of cost dashboard</verify>
|
||||
<done>Operator confirms cost dashboard renders with charts, budget alerts, and time range selector</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- Portal builds: `cd packages/portal && npx next build`
|
||||
- Usage page accessible at /usage/{tenantId}
|
||||
- Charts render with Recharts (or show empty state gracefully)
|
||||
- Budget alerts display with correct color coding
|
||||
- Time range selector changes data display
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Operator sees token usage per agent in a bar chart
|
||||
- Operator sees cost breakdown by LLM provider
|
||||
- Operator sees message volume per channel
|
||||
- Time range selector filters displayed data
|
||||
- Budget alerts show amber at 80%, red at 100%+ of configured limit
|
||||
- Dashboard layout is clean and answers "what is this costing me?" at a glance
|
||||
- Portal builds successfully
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/03-operator-experience/03-04-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user