…ntry
The ingestion module had no way in. Adds the route, the read layer it
needed, and the nav entry.
Read layer (lib/queries/board-flow-data.ts) loads items and events and
hands them to the existing pure functions, so every calculation stays
unit-testable without a database. Reads paginate explicitly: PostgREST
caps responses at the project's max-rows, and a board above that would
come back silently truncated — the failure mode behind #121.
The route degrades instead of erroring. A missing schema (migration 023
not applied) and an org with no board both render an unconfigured state,
so the nav entry can ship before the migration lands rather than 500ing
on a deployment that hasn't migrated.
Section order is the spec's and it is deliberate: quality gates before
any number, so the reader knows what the figures can carry before
reading them. Then durations, time per column, WIP aging, throughput,
CFD, stalled items, Little's Law.
Two visualization decisions:
- The CFD groups by lifecycle bucket, not by column. The live board has
17 columns; stacking that many bands is unreadable, while five buckets
make accumulation obvious. Needed the resolved bucket per column, so
summarizeBoard now returns `statusBuckets`.
- Ran the product's categorical ramp through a palette validator. It
passes colour-vision separation (worst adjacent pair ΔE 18.6, target
8) but four of five slots fall below 3:1 contrast against the page
surface. That obligates relief, so every mark is paired with a visible
label or rendered as a table — identity is never colour alone. Same
reason the gates lead with an icon and the severity word, not a dot.
Nav entry lives in tenantNavItems, which the sidebar and the mobile
sheet share, so one entry covers both. Translations in en-US and pt-BR;
es-ES falls back to en-US per the existing convention.
Not verified: the page has not been rendered against real data. The
schema is not applied anywhere yet and this machine has no Supabase
credentials, so only the empty and unconfigured states are reachable
locally. Build and types pass; visual confirmation is still owed.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem
Repos with real analysis runs rendered "0 runs" in
/repos(42 repos for theclickbusorg).Root cause:
getOrgReposSummaryfetched rawmetricsorg-wide with a global.limit(repos.length * 15)and grouped client-side. PostgREST silently caps every response at the project "Max rows" (default 1000), so once an org had > 1000 metric rows in a window only the newest 1000 came back. Repos whose latest run fell outside that slice received zero rows → "0 runs" despite having metrics.Ruled out via SQL (not a data problem, not the #120 usage leak): all 192 repos have metrics@90d; the worst "newest-run rank" was 1546 < 1776 total metrics — pure read-side truncation.
Fix
022_repo_metric_summaries.sql: view returning one pre-aggregated row per(repository, window_days)—runs_count, latest values, previous stabilization (delta), sparkline.getOrgReposSummaryreads the view instead of raw metrics. Result set ≈ repo count, well under any max-rows cap. Fixes /repos, /dashboard, /compare, /ai-exposure (all call this function).Verification
tsc✓ ·eslint✓ · 213 tests ✓repos_zero_runs = 0.Follow-up (not in this PR)
getAvailableWindowDays(temporal.ts:47,.limit(2000)) shares the same org-wide cap pattern — low impact (window selector only).🤖 Generated with Claude Code