feat(insights): port unique files from closed bundle-PRs (#41)

Re-introduce the 3 unique files from closed PRs #37 and #40 that
overlap-merged with already-landed sub-PRs (#34, #35, #36, #38, #39):

- app/(app)/insights/page.tsx — Server Component dat alle helpers
  parallel aanroept en de 5 sectie-Cards rendert (Sprint Health,
  Plan-quality, Agent throughput, Velocity, Backlog health)
- app/(app)/insights/components/sprint-info-strip.tsx — chips per
  active sprint met productname + goal + dagen-over + taakcount
- app/(app)/insights/components/alignment-trend.tsx — Recharts
  LineChart die % ALIGNED jobs per sprint over laatste 5 sprints toont
- lib/insights/verify-stats.ts — TrendPoint type + getAlignmentTrend
  helper (uitgebreid van PR #38)

Plus dependency: recharts (was in package.json van #37/#40 die we
sloten).

Tests: 290/290 groen, tsc clean, lint clean.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-01 16:44:53 +02:00 committed by GitHub
parent 8c0941804c
commit 0454eede74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 269 additions and 0 deletions

View file

@ -1,4 +1,5 @@
import { prisma } from '@/lib/prisma'
import { productAccessFilter } from '@/lib/product-access'
export type VerifyResultKey = 'ALIGNED' | 'PARTIAL' | 'EMPTY' | 'DIVERGENT'
@ -17,6 +18,14 @@ export interface VerifyResultStats {
topDivergent: TopJob[]
}
export interface TrendPoint {
sprintId: string
sprintGoal: string
productName: string
alignedRatio: number
total: number
}
const RESULT_ORDER: VerifyResultKey[] = ['ALIGNED', 'PARTIAL', 'EMPTY', 'DIVERGENT']
export async function getVerifyResultStats(
@ -90,3 +99,48 @@ export async function getVerifyResultStats(
topDivergent: rawDivergent.map(toTopJob),
}
}
export async function getAlignmentTrend(
userId: string,
sprintsBack = 5,
): Promise<TrendPoint[]> {
const sprints = await prisma.sprint.findMany({
where: {
status: 'COMPLETED',
product: productAccessFilter(userId),
},
orderBy: { completed_at: 'desc' },
take: sprintsBack,
select: {
id: true,
sprint_goal: true,
completed_at: true,
product: { select: { name: true } },
},
})
const points = await Promise.all(
sprints.map(async sprint => {
const jobs = await prisma.claudeJob.findMany({
where: {
user_id: userId,
status: 'DONE',
verify_result: { not: null },
task: { story: { sprint_id: sprint.id } },
},
select: { verify_result: true },
})
const aligned = jobs.filter(j => j.verify_result === 'ALIGNED').length
return {
sprintId: sprint.id,
sprintGoal: sprint.sprint_goal,
productName: sprint.product.name,
alignedRatio: jobs.length > 0 ? Math.round((aligned / jobs.length) * 100) : 0,
total: jobs.length,
}
}),
)
// chronologisch oplopend (we fetched desc, so reverse)
return points.reverse()
}