Scrum4Me/docs/old/plans/PBI-78-cost-analysis-widget.md
Janpeter Visser b39c3ec2e1
docs(cleanup): archief verouderde plannen, backlog en root-duplicaten (#191)
* docs(cleanup): archief verouderde plannen, backlog en root-duplicaten

- 6 plans naar docs/old/plans/ (PBI-11/75/78, user-settings-store, Local github setup, lees-de-readme — laatste was verkeerde repo)
- docs/backlog/ naar docs/old/backlog/ (pre-MCP statische registry; live werk loopt via Scrum4Me-MCP)
- 6 root-level duplicaten naar docs/old/ (functional, {pbi,story,task}-dialog, product-backlog, backlog)
- 2 landing plans (niet uitgevoerd) krijgen archived: true frontmatter — blijven op plek maar uit INDEX
- scripts/generate-docs-index.mjs: skip docs/old/** + skip archived: true
- CLAUDE.md: rijen docs/backlog/, docs/plans/<key>-*.md, docs/manual/ weg; Track B-sectie verwijderd
- README.md / CHANGELOG.md / docs/plans/v1-readiness.md: link-fixes naar nieuwe locaties

Verify groen (lint + typecheck + 718 tests). docs/INDEX.md geregenereerd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cleanup): registreer handmatige verplaatsingen en fix referenties

- 4 plans verplaatst naar docs/old/plans/ (M10-qr-pairing-login, auto-pr-deploy-sync, docs-restructure-ai-lookup, v1-readiness)
- 3 archive-plans verplaatst naar docs/old/plans/ (archive-map nu leeg)
- ST-1114-copilot-reviews + 3 research-docs naar nieuwe docs/Ideas/ map
- Duplicaat docs/old/2026-04-27-m8-realtime-solo.md verwijderd (origineel zit in docs/old/plans/)
- Link-fixes naar nieuwe locaties:
  - CHANGELOG.md → docs/old/plans/v1-readiness.md
  - docs/runbooks/deploy-control.md → docs/old/plans/auto-pr-deploy-sync.md (2x)
  - docs/runbooks/worker-idempotency.md → docs/old/plans/auto-pr-deploy-sync.md
  - docs/plans/docs-restructure-pbi-spec.md → docs/old/plans/docs-restructure-ai-lookup.md (4x text + 2x href)
- docs/INDEX.md geregenereerd (96 docs, was 100)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:46:00 +02:00

8.1 KiB
Raw Permalink Blame History

PBI-78 — Cost-analyse widget op Insights-pagina

Context

De insights-pagina heeft al een TokenUsageCard die KPI's + per-job tabel toont, maar alleen voor de actieve sprint van een gefilterd product. Daardoor mis je het globale plaatje: hoeveel geef je deze maand uit, welk model is de grootste kostenpost, hoe goed werkt prompt-caching, en welke job-kinds (IDEA_MAKE_PLAN met Opus vs TASK_IMPLEMENTATION met Sonnet) trekken het budget.

We voegen een nieuwe sectie "Cost analyse" toe (tussen Sprint Health en Plan-quality). Eén shared periode-selector (7d/30d/90d/MTD) stuurt vier visualisaties aan op basis van best practices uit de Anthropic Console + LLM-observability tools (Datadog, Portkey):

  1. Trend-chart over tijd
  2. Breakdown per model
  3. Breakdown per job-kind
  4. Cache efficiency

De bestaande TokenUsageCard blijft staan als sprint-detail (top duurste jobs voor de actieve sprint).

Bestaande infrastructuur (hergebruik)

Reeds aanwezig in DB:

  • prisma/schema.prismaClaudeJob heeft input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, actual_thinking_tokens, model_id, kind, finished_at
  • ModelPrice tabel met prijzen per 1M tokens (input/output/cache_read/cache_write)
  • Prijzen worden gesynced via scripts/sync-model-prices.ts

Hergebruikbare patronen:

Te bouwen

Taak 1 — Data-laag — lib/insights/cost-analysis.ts (nieuw)

Eén bestand met vijf functies, allemaal (userId, period) als parameters. Period wordt naar WHERE cj.finished_at >= NOW() - INTERVAL '<n> days' vertaald (MTD = >= date_trunc('month', NOW())).

export type Period = '7d' | '30d' | '90d' | 'mtd'

export interface CostKpi {
  totalCostUsd: number
  totalTokens: number
  jobCount: number
  avgPerDayUsd: number
  cacheSavingsUsd: number   // (input_price - cache_read_price) × cache_read_tokens
  topModelId: string | null
  topModelCostUsd: number
}

export interface CostByDayRow { day: string; costUsd: number }
export interface CostByModelRow { modelId: string; costUsd: number; jobCount: number }
export interface CostByKindRow { kind: string; costUsd: number; jobCount: number }
export interface CacheEfficiency {
  cacheReadTokens: number
  uncachedInputTokens: number
  cacheHitRatio: number      // cache_read / (cache_read + input)
  savingsUsd: number
  spentOnCacheWriteUsd: number
}

export async function getCostKpi(userId: string, period: Period): Promise<CostKpi>
export async function getCostByDay(userId: string, period: Period): Promise<CostByDayRow[]>
export async function getCostByModel(userId: string, period: Period): Promise<CostByModelRow[]>
export async function getCostByKind(userId: string, period: Period): Promise<CostByKindRow[]>
export async function getCacheEfficiency(userId: string, period: Period): Promise<CacheEfficiency>

Belangrijke details:

  • Alle queries: WHERE cj.user_id = ${userId} AND cj.status = 'DONE' AND cj.finished_at >= <periodStart>
  • Geen productAccessFilter nodig — cj.user_id = ${userId} filtert al op de eigenaar
  • getCostByDay vult ontbrekende dagen op met 0 (anders breekt de chart-x-as) — vul aan client- of server-side, kies één
  • Periode → days mapping inline: 7d→7, 30d→30, 90d→90, mtd→huidige dag-van-maand
  • Cache savings: cache_read_tokens × (input_price - cache_read_price) / 1_000_000 — "wat je betaald zou hebben zonder cache, minus wat je betaalde mét cache"

Taak 2 — UI — app/(app)/insights/components/cost-analysis.tsx (nieuw)

Eén client-component die de hele sectie rendert. Structuur:

[Period selector rechtsboven]
[KPI strip: Totaal | Cache savings | Avg/dag | Top model ($X op claude-opus-4-7)]
[grid grid-cols-1 md:grid-cols-2 gap-4]
  [Daily cost line/bar chart]   [Model breakdown - horizontal bar of donut]
  [Job-kind breakdown - bar]    [Cache efficiency - donut + label "X% hit, $Y bespaard"]

Period selector: kopieer pattern uit agent-throughput.tsx (regels 50-61) — useTransition + router.replace met ?period= in URL. Default tonen als "30d".

Charts: Recharts (al gebruikt in BurndownChart, AgentThroughputCard, VelocityChart):

  • Daily: <BarChart> met één bar (cost in USD), x-as = dag (MM-DD), tooltip toont $X.XXXX
  • Model: <BarChart layout="vertical"> met model_id labels — beperkt tot top 5
  • Kind: <BarChart layout="vertical"> met kind labels — beperkt tot top 5
  • Cache: <PieChart> met twee segmenten (cached / uncached input) + tekst "X% cache hit · $Y bespaard"

Empty state: als kpi.jobCount === 0: render één regel "Geen jobs in deze periode."

Taak 3 — Integratie — app/(app)/insights/page.tsx (edit)

Wijzigingen:

 interface InsightsPageProps {
-  searchParams: Promise<{ product?: string }>
+  searchParams: Promise<{ product?: string; period?: string }>
 }
-  const { product: filterProductId } = await searchParams
+  const { product: filterProductId, period: rawPeriod } = await searchParams
+  const period = (['7d','30d','90d','mtd'].includes(rawPeriod ?? '') ? rawPeriod : '30d') as Period

In de Promise.all, voeg toe:

getCostKpi(userId, period),
getCostByDay(userId, period),
getCostByModel(userId, period),
getCostByKind(userId, period),
getCacheEfficiency(userId, period),

Nieuwe sectie tussen Sprint Health en Plan-quality:

<section className="space-y-3">
  <h2 className="text-lg font-medium text-foreground">Cost analyse</h2>
  <CostAnalysisCard
    period={period}
    kpi={costKpi}
    byDay={costByDay}
    byModel={costByModel}
    byKind={costByKind}
    cache={cacheEff}
  />
</section>

De bestaande "Token gebruik" sectie blijft staan (sprint-detail tabel).

Bestanden

Nieuw:

  • lib/insights/cost-analysis.ts — 5 query-functies + types
  • app/(app)/insights/components/cost-analysis.tsx — client-component met period-selector + 4 charts

Edit:

  • app/(app)/insights/page.tsx — period uit searchParams, parallel-fetch, nieuwe sectie

Geen wijzigingen aan:

  • Prisma schema (alle data is er al)
  • MCP server (token-data wordt al weggeschreven via update_job_status)
  • TokenUsageCard (blijft als sprint-detail tabel)

Verificatie

npm run verify && npm run build

Handmatig:

  1. Open /insights zonder query — period default 30d, sectie toont KPI + 4 charts
  2. Wissel period via selector → URL updatet ?period=7d, charts laden nieuwe data via router.replace
  3. Check empty state: kies periode zonder jobs → "Geen jobs in deze periode."
  4. Sanity-check KPI's tegen ruwe DB-query:
    SELECT SUM(input_tokens * mp.input_price_per_1m / 1e6
             + output_tokens * mp.output_price_per_1m / 1e6
             + cache_read_tokens * mp.cache_read_price_per_1m / 1e6
             + cache_write_tokens * mp.cache_write_price_per_1m / 1e6
             + COALESCE(actual_thinking_tokens, 0) * mp.input_price_per_1m / 1e6)
    FROM claude_jobs cj
    LEFT JOIN model_prices mp ON mp.model_id = cj.model_id
    WHERE cj.user_id = '<id>' AND cj.status = 'DONE'
      AND cj.finished_at >= NOW() - INTERVAL '30 days';
    
  5. Cache savings sanity: cacheSavingsUsd ≈ cache_read_tokens × 0.9 × input_price / 1M (cache_read prijs = 0.1× input prijs, dus savings is 90%)