fix(ui): adopt AnalyticsCardShell in 5 analytics cards that hand-roll their own chrome - #6543
Conversation
cycle-time-card, gate-precision-card, reversal-health-card, gate-outcome-card, and slop-duplicate-trend-card each hand-rolled the same "titled card with loading/empty/ready" chrome AnalyticsCardShell (JSONbored#2200) already provides, matching acceptance-rate-card/queue-health- card. Fixes the real user-visible bug this caused: CycleTimeCard's no-samples branch rendered a bare <p> instead of the shared EmptyState every other adopted card gets for free. - CycleTimeCard and SlopDuplicateTrendCard are true binary states (nothing else renders when empty) -- both now use the shell's state="empty"/"ready" toggle directly. - GatePrecisionCard keeps its documented "render nothing when there are no evaluated rows" early return unchanged, wrapping only the rows>0 branch in the shell. - ReversalHealthCard and GateOutcomeCard always render their Stat tiles regardless of the list/mix content, so both stay in the shell's "ready" state and keep their own inner list-vs-EmptyState toggle, matching acceptance-rate-card's shape for content that doesn't fit the shell's own binary empty/ready split. AnalyticsCardShell gains an optional `action` header-right slot (status pill / boundary badge / freshness stamp), rendered across every state via the header's existing justify-between layout, so each card keeps its existing header signal. Data-fetching logic and ready-state content are unchanged -- only the chrome/state-handling wrapper moved. Closes JSONbored#6175
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-16 12:52:10 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. Visual preview
Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|


Closes #6175
Summary
apps/loopover-ui/src/components/site/app-panels/analytics-card-shell.tsxis the documented (#2200) shared "titled card with loading/empty/ready" treatment. Five sibling analytics cards never adopted it, instead hand-rolling the same<section className="rounded-token border-hairline...">header/title markup:cycle-time-card.tsx,gate-precision-card.tsx,reversal-health-card.tsx,gate-outcome-card.tsx,slop-duplicate-trend-card.tsx. This is the root cause of a real, user-visible bug:cycle-time-card.tsx's no-data case rendered a bare<p>instead of the sharedEmptyStateevery properly-adopted sibling (acceptance-rate-card.tsx,queue-health-card.tsx) gets for free fromAnalyticsCardShell'sstate="empty".Each of the 5 cards has a different relationship to "empty," so each migrates differently rather than mechanically:
CycleTimeCardandSlopDuplicateTrendCardare true binary states — when there's no signal, nothing else renders (both previously fell back to a bare<p>). Both now use the shell'sstate={hasSignal ? "ready" : "empty"}directly, with the previous<p>copy moved intoemptyTitle/emptyHint.GatePrecisionCardkeeps its documented "render nothing when there are no evaluated rows" early return (if (report.rows.length === 0) return null;) completely unchanged — only therows > 0branch is wrapped in the shell, withstate="ready"always.ReversalHealthCardandGateOutcomeCardalways render theirStattiles regardless of whether the list/mix content has data — using the shell's ownstate="empty"there would also hide those always-present Stats, which isn't the current (or wanted) behavior. Both stay instate="ready"and keep their existing inner list-vs-EmptyStatetoggle exactly as before, the same shapeacceptance-rate-card.tsx/queue-health-card.tsxalready established for content that doesn't cleanly fit the shell's binary split.AnalyticsCardShellgains an optionalactionheader-right slot (status pill / boundary badge / freshness stamp), rendered across every state via the header's existingjustify-betweenlayout, so each card keeps its existing header signal instead of losing it in the move.Data-fetching logic and the shape of ready-state content are unchanged — only the chrome/state-handling wrapper moved.
Note on the prior closed attempt
#6472 attempted this same issue and was auto-closed: CI genuinely failed (
validate,validate-code,validate-tests, and the UI preview build) despite the PR claiming a clean typecheck/lint/test/build. This PR verifies every claim below directly rather than assuming — including deliberately reverting the fix files viagit stashto confirm the new/strengthened tests actually fail without them before restoring.Scope
type(scope): short summaryConventional Commit format.apps/loopover-ui/**) and does not mix unrelated backend, MCP, docs, or deploy changes.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Validation
npm --workspace @loopover/ui run typecheck— clean.npm run ui:lint— 0 errors, only pre-existing warnings in unrelated files.npm --workspace @loopover/ui run test(full workspace) — 47 files, 294 tests, all passing.cycle-time-card.test.tsx's andslop-duplicate-trend-card.test.tsx's empty-state tests to assert the realEmptyStatetitle/description (not just the header pill text, which was the only thing the old assertions checked) — verified both genuinely fail without the fix viagit stashon the three changed component files, then restored and re-confirmed passing.analytics-card-shell.test.tsxfor the newactionslot: renders across all three states, and is omitted entirely when not provided.git diff --checknpm run actionlintnpm run docs:drift-check/manifest:drift-check/command-reference:checknpm audit --audit-level=moderate— 0 vulnerabilities.npm run ui:build— fails in this sandbox on an unrelated, pre-existing issue (@scalar/api-referencefails to resolve from@scalar/api-reference-react's dist output), reproduced identically on a clean, unmodifiedmaincheckout this same session (unrelated to this diff).npm run typecheck/npm run test:coverage— this sandbox's roottsc --noEmitreliably OOMs regardless of diff content (reproduced repeatedly this session); the workspace-scoped@loopover/uitypecheck (clean) plus the full workspace test run (294/294 passing) cover this diff's actual surface.apps/**is excluded from Codecov'scoverage.include, so nocodecov/patchcheck applies to this diff.If any required check was skipped, explain why:
ui:build: pre-existing sandbox environment issue, confirmed unrelated to this diff.typecheck/test:coverage: established OOM pattern in this sandbox, unrelated to diff content; superseded by the scoped checks above.Safety
UI Evidencesection below with screenshots. — See below.CHANGELOG.mduntouched.UI Evidence
The visible change is
CycleTimeCard's andSlopDuplicateTrendCard's empty states now show the sharedEmptyStatetreatment (icon + title + description, matching every sibling card) instead of a bare paragraph. The other 3 cards (GatePrecisionCard,ReversalHealthCard,GateOutcomeCard) render identically to before — only their internal chrome now comes from the shared shell rather than hand-rolled markup.ui:buildcouldn't run in this sandbox (see Validation) to produce a live screenshot; covered instead by the new/strengthened regression tests asserting the exact renderedEmptyStatetitle/description text per card.Notes
None.