Files
roboco/panel/docs/frontend/company-scorecard-card.md
T
roboco-app[bot]GitHubroboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>Frontend Developer 2Frontend DocumenterFrontend PM
b3dda00e41 [d700055f] Replace StubObjectivesSection with live charter objective cards (#702) (#704)
* [d700055f] feat(scorecard): replace StubObjectivesSection with live charter objective cards

Delete the StubObjectivesSection placeholder (fake 'Revenue growth'/'Customer
retention' labels) and render a real ObjectivesSection with three positional
charter objective cards, each showing its live metric against its target:
first_pass_yield (90%), median_lead_time_hours (<24h), escaped_defects (0).

- CockpitSummary: add optional first_pass_yield and escaped_defects fields
  (backend companion item not yet shipped; UI renders 'No data yet' until then)
- ObjectivesSection: positional mapping objectives[i].metric -> metric i,
  documented as a positional-by-convention assumption; canonical fallback
  labels when the charter objectives array is empty/shorter than 3; 'No data
  yet' italic muted fallback for null/undefined metrics (SpeedSection pattern);
  DeliveryMetric card styling; first_pass_yield formatted as pctOrDash does
- SpeedSection kept as-is; ObjectivesSection references the same
  median_lead_time_hours value as a peer card alongside the other two
- Tests: buildSummary carries the new fields; cover present/missing metrics
  and absence of the fake stub labels; existing lead-time/null tests adjusted
  for the now-shared value and multi-card 'No data yet'

* [d700055f] docs(scorecard): document live ObjectivesSection and new CockpitSummary fields

Add panel/docs/frontend/company-scorecard-card.md covering the new
ObjectivesSection: the three positional charter objective cards
(first_pass_yield 90%, median_lead_time_hours <24h, escaped_defects 0),
the positional-by-convention mapping, canonical fallback labels, the
'No data yet' fallback pattern, and the two new optional CockpitSummary
fields with the backend companion-item caveat. Add a Key Symbols row for
CompanyScorecardCard/ObjectivesSection in docs/map/panel.md.

---------

Co-authored-by: roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech>
Co-authored-by: Frontend Documenter <fe-doc@roboco.tech>
Co-authored-by: Frontend PM <fe-pm@roboco.tech>
2026-07-26 17:13:22 +00:00

89 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Company Scorecard Card
## Overview
`CompanyScorecardCard` (`panel/src/components/business/company-scorecard-card.tsx`) is the live charter-performance card on the Business page's **Scorecard** tab (`app/(dashboard)/business/page.tsx`, `?tab=scorecard`). Its subtitle reads "Live performance against the charter". It renders four sections off a single `cockpitApi.summary()` call (`GET /api/cockpit/summary`, typed by `CockpitSummary` in `panel/src/lib/api/cockpit.ts`):
1. **Delivery** — live task counts across the pipeline (`DeliverySection`).
2. **Spend** — 30-day spend, projected monthly, monthly cap, plus the `SpendTrendChart` (`SpendSection`).
3. **Speed** — median lead time, intake → merged (`SpeedSection`).
4. **Objectives** — three charter objective cards, each showing a live metric against its target (`ObjectivesSection`).
Loading, error, and empty states are handled at the card level: `ScorecardSkeleton` while the query loads, `OfflineState` with a retry button on error or missing data.
## Objectives section
`ObjectivesSection` replaced the former `StubObjectivesSection`, a placeholder that rendered fake "Revenue growth" / "Customer retention" labels with a "Not tracked yet" badge. The stub actively misrepresented charter performance; the live section renders the three real charter objectives against their live metrics.
### The three objective cards
Each card shows the objective label, the live metric value (or "No data yet"), and the target:
| Position | Charter objective (`objectives[i].metric`) | Target | Metric field | Format |
|---|---|---|---|---|
| 0 | Tasks shipped to merge with no human code edits | `90%` | `first_pass_yield` | percentage, `(v * 100).toFixed(0)%` |
| 1 | Median lead time, intake → merged | `< 24h` | `median_lead_time_hours` | `{value.toFixed(1)}h` |
| 2 | Critical escaped defects per release | `0` | `escaped_defects` | count, `${value}` |
The lead-time metric is the same value `SpeedSection` renders in the Speed section above — `ObjectivesSection` reads `data.median_lead_time_hours` a second time and presents it as an objective card alongside the other two. `SpeedSection` is intentionally kept as-is.
### Positional-by-convention mapping
The charter `objectives` field on `CockpitSummary` is `Record<string, unknown>[]` — free-form text from the Goals tab, each entry carrying `{metric, target, status}`. The three metrics above are hardcoded by contrast. The mapping is **positional by convention, not derived**: `objectives[i].metric` supplies the label for card `i`, and card `i` reads the `i`-th hardcoded metric. This holds only while the charter has exactly three objectives in the canonical order; editing the Goals tab can desync labels from metrics. The assumption is documented in a `ponytail:` code comment at the top of the `ObjectivesSection` block in `company-scorecard-card.tsx` — a stated assumption, not something discovered later.
`objectiveLabel(objectives, index)` returns `objectives[index].metric` when it is a non-empty string, falling back to `OBJECTIVE_FALLBACK_LABELS[index]` (the three canonical labels above) when the array is empty or shorter than three. A fabricated label is never rendered.
### "No data yet" fallback
Each card guards its metric with a `hasData` check (`value != null`, covering both `null` and `undefined`). When the metric is absent the card renders "No data yet" in italic muted text instead of a value — mirroring the `SpeedSection` pattern. This is the contract for the two new fields (`first_pass_yield`, `escaped_defects`): until the backend companion item ships them on the `/cockpit/summary` response, both are absent and both cards show "No data yet". The card never fabricates a value or a label.
## CockpitSummary type changes
`panel/src/lib/api/cockpit.ts` gained two optional fields on `CockpitSummary`:
```typescript
// Fraction of tasks shipped to merge with no human code edits (01).
// Backend companion item adds this; until it ships the field is absent
// and the UI renders 'No data yet'. Formatted as a percentage, matching
// the phone's pctOrDash convention in tg-metrics-tab.tsx.
first_pass_yield?: number | null;
// Count of critical escaped defects per release (backend companion item).
escaped_defects?: number | null;
```
Both are optional and nullable so the UI degrades cleanly while the backend companion item is outstanding. `first_pass_yield` is a 01 fraction formatted as a percentage, matching the phone's `pctOrDash(scorecard.first_pass_yield)` convention in `tg-metrics-tab.tsx`. `median_lead_time_hours` is unchanged — it already existed at the top level of the type and `SpeedSection` reads it from there.
The backend companion item (out of scope for this change) is what adds `first_pass_yield` and `escaped_defects` to the `/api/cockpit/summary` response. Until it lands, the two objective cards for those metrics show "No data yet".
## Card structure
```
CompanyScorecardCard
└── ScorecardBody (data, spendTrend, spendTrendLoading)
├── DeliverySection (data.delivery)
├── SpendSection (data.spend, spendTrend, spendTrendLoading)
├── SpeedSection (data.median_lead_time_hours)
└── ObjectivesSection (data.objectives, first_pass_yield,
median_lead_time_hours, escaped_defects)
└── ObjectiveCard × 3 (label, hasData, formattedValue, targetText)
```
`ObjectiveCard` is a presentational leaf: a `rounded-lg border bg-card p-3` card (matching `DeliveryMetric`'s styling) with the label on top and a `flex justify-between` row holding the value (or "No data yet") and the `target: {targetText}` annotation.
## Tests
`panel/src/components/business/__tests__/company-scorecard-card.test.tsx` covers the Objectives section:
- Three objective cards render with their target values when all metrics are present (labels from `objectives[i].metric`, values formatted as `92%` / `18.7h` / `0`, targets `90%` / `< 24h` / `0`).
- A missing `first_pass_yield` (null) and `escaped_defects` (undefined) each render "No data yet" — two fallbacks — while a present `median_lead_time_hours` still shows its value.
- The fake "Revenue growth" / "Customer retention" / "Not tracked yet" stub labels do not appear in any render path.
The `buildSummary` test helper carries `first_pass_yield: null` and `escaped_defects: null` by default, matching the not-yet-shipped backend state.
## Related
- `docs/map/panel.md` — the agent-facing codebase map entry for `CompanyScorecardCard` / `ObjectivesSection`.
- `panel/src/lib/api/cockpit.ts``CockpitSummary` type and `cockpitApi.summary()` client.
- `panel/src/components/business/spend-trend-chart.tsx` — the 30-day spend chart embedded in `SpendSection`.
- `tg-metrics-tab.tsx` — the phone cockpit's metrics tab, whose `pctOrDash(scorecard.first_pass_yield)` convention the `first_pass_yield` percentage format matches.