mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [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>
This commit is contained in:
co-authored by
roboco-app[bot] <302741806+roboco-app[bot]@users.noreply.github.com>
Frontend Developer 2
Frontend Documenter
Frontend PM
parent
80ccf415cb
commit
b3dda00e41
@@ -125,6 +125,7 @@ The Next.js 16 control panel (`panel/`, package `roboco-panel` v0.25.0) is the s
|
|||||||
| `WorkSessionsView` | comp | `components/work-sessions/work-sessions-view.tsx` | Git page's "Work Sessions" tab body; search/status filters are LOCAL `useState`, not URL params |
|
| `WorkSessionsView` | comp | `components/work-sessions/work-sessions-view.tsx` | Git page's "Work Sessions" tab body; search/status filters are LOCAL `useState`, not URL params |
|
||||||
| `SessionTrendChart` | comp | `components/work-sessions/session-trend-chart.tsx` | Active-session start-time histogram (hourly/daily bucketing); honestly labeled active-only, no history beyond `GET /work-sessions` |
|
| `SessionTrendChart` | comp | `components/work-sessions/session-trend-chart.tsx` | Active-session start-time histogram (hourly/daily bucketing); honestly labeled active-only, no history beyond `GET /work-sessions` |
|
||||||
| `CostTrendChart` / `SpendTrendChart` | comp | `components/dashboard/cost-trend-chart.tsx` / `components/business/spend-trend-chart.tsx` | Daily-spend area charts off `GET /usage/time-series`; 7d on Overview (`CommandCenter`), 30d on the Business scorecard (`CompanyScorecardCard`) |
|
| `CostTrendChart` / `SpendTrendChart` | comp | `components/dashboard/cost-trend-chart.tsx` / `components/business/spend-trend-chart.tsx` | Daily-spend area charts off `GET /usage/time-series`; 7d on Overview (`CommandCenter`), 30d on the Business scorecard (`CompanyScorecardCard`) |
|
||||||
|
| `CompanyScorecardCard` / `ObjectivesSection` | comp | `components/business/company-scorecard-card.tsx` | Business page Scorecard tab body; four sections off one `cockpitApi.summary()` call. `ObjectivesSection` renders three positional charter objective cards (`first_pass_yield` 90%, `median_lead_time_hours` <24h, `escaped_defects` 0) — positional-by-convention mapping documented in a `ponytail:` code comment; "No data yet" fallback for null/undefined metrics (mirrors `SpeedSection`). `CockpitSummary` gained optional `first_pass_yield?: number\|null` and `escaped_defects?: number\|null` (backend companion item not yet shipped — see `panel/docs/frontend/company-scorecard-card.md`). |
|
||||||
| `ProductCardGrid` | comp | `components/products/product-card-grid.tsx` | Workstation Products card-grid view; reuses `CellsList`/`ProgressCell` (exported from `product-table.tsx`) |
|
| `ProductCardGrid` | comp | `components/products/product-card-grid.tsx` | Workstation Products card-grid view; reuses `CellsList`/`ProgressCell` (exported from `product-table.tsx`) |
|
||||||
| `ProjectCardGrid` | comp | `components/projects/project-card-grid.tsx` | Workstation Projects card-grid view; reuses `getExternalUrl`/badge renderers (exported from `project-table.tsx`) |
|
| `ProjectCardGrid` | comp | `components/projects/project-card-grid.tsx` | Workstation Projects card-grid view; reuses `getExternalUrl`/badge renderers (exported from `project-table.tsx`) |
|
||||||
| `sortProducts` | fn | `components/products/products-view.tsx` | Pure client-side sort (name/cell count) for the Products card grid; direction rides a comparator multiplier, not sort-then-reverse, so ties keep their relative order |
|
| `sortProducts` | fn | `components/products/products-view.tsx` | Pure client-side sort (name/cell count) for the Products card grid; direction rides a comparator multiplier, not sort-then-reverse, so ties keep their relative order |
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 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 (0–1).
|
||||||
|
// 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 0–1 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.
|
||||||
@@ -68,6 +68,8 @@ function buildSummary(overrides: Partial<CockpitSummary> = {}): CockpitSummary {
|
|||||||
pending_pitches: 0,
|
pending_pitches: 0,
|
||||||
signals: [],
|
signals: [],
|
||||||
median_lead_time_hours: null,
|
median_lead_time_hours: null,
|
||||||
|
first_pass_yield: null,
|
||||||
|
escaped_defects: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -237,7 +239,9 @@ describe("CompanyScorecardCard", () => {
|
|||||||
|
|
||||||
render(<CompanyScorecardCard />);
|
render(<CompanyScorecardCard />);
|
||||||
|
|
||||||
expect(screen.getByText("No data yet")).toBeInTheDocument();
|
// Speed section renders 'No data yet' (and so do the objective cards
|
||||||
|
// for the still-absent first_pass_yield / escaped_defects metrics).
|
||||||
|
expect(screen.getAllByText("No data yet").length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
@@ -245,15 +249,105 @@ describe("CompanyScorecardCard", () => {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
it("shows formatted lead time when median_lead_time_hours is present", () => {
|
it("shows formatted lead time when median_lead_time_hours is present", () => {
|
||||||
setQueryState({
|
setQueryState({
|
||||||
data: buildSummary({ median_lead_time_hours: 18.7 }),
|
data: buildSummary({
|
||||||
|
median_lead_time_hours: 18.7,
|
||||||
|
first_pass_yield: 0.9,
|
||||||
|
escaped_defects: 0,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<CompanyScorecardCard />);
|
render(<CompanyScorecardCard />);
|
||||||
|
|
||||||
// Component renders `{value.toFixed(1)}h median — target: < 24h`
|
// Lead time renders in both the Speed section and the Objectives section
|
||||||
expect(screen.getByText(/18\.7h/)).toBeInTheDocument();
|
expect(screen.getAllByText(/18\.7h/).length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
// 'No data yet' must NOT appear
|
// 'No data yet' must NOT appear when all metrics are present
|
||||||
expect(screen.queryByText("No data yet")).not.toBeInTheDocument();
|
expect(screen.queryByText("No data yet")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Objectives section: three charter objective cards against live metrics
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
it("renders three objective cards with target values when all metrics are present", () => {
|
||||||
|
setQueryState({
|
||||||
|
data: buildSummary({
|
||||||
|
first_pass_yield: 0.92,
|
||||||
|
median_lead_time_hours: 18.7,
|
||||||
|
escaped_defects: 0,
|
||||||
|
objectives: [
|
||||||
|
{ metric: "Tasks shipped to merge with no human code edits", target: "90%", status: "Active" },
|
||||||
|
{ metric: "Median lead time, intake → merged", target: "< 24h", status: "Active" },
|
||||||
|
{ metric: "Critical escaped defects per release", target: "0", status: "Active" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<CompanyScorecardCard />);
|
||||||
|
|
||||||
|
// Three objective labels from the charter objectives array
|
||||||
|
expect(
|
||||||
|
screen.getByText("Tasks shipped to merge with no human code edits"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Median lead time, intake → merged"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByText("Critical escaped defects per release"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Live metric values: first_pass_yield as a percentage (0.92 → 92%),
|
||||||
|
// median_lead_time_hours as {value}h, escaped_defects as a count.
|
||||||
|
expect(screen.getByText("92%")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("18.7h")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("0")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// All three target values render. The '< 24h' target appears in both the
|
||||||
|
// Speed section and the Objectives section, so use getAllByText there.
|
||||||
|
expect(screen.getByText(/target: 90%/)).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText(/target: < 24h/).length).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(screen.getByText(/target: 0/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders 'No data yet' for missing first_pass_yield and escaped_defects, not fabricated labels", () => {
|
||||||
|
setQueryState({
|
||||||
|
data: buildSummary({
|
||||||
|
first_pass_yield: null,
|
||||||
|
median_lead_time_hours: 18.7,
|
||||||
|
escaped_defects: undefined,
|
||||||
|
objectives: [
|
||||||
|
{ metric: "Tasks shipped to merge with no human code edits", target: "90%", status: "Active" },
|
||||||
|
{ metric: "Median lead time, intake → merged", target: "< 24h", status: "Active" },
|
||||||
|
{ metric: "Critical escaped defects per release", target: "0", status: "Active" },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<CompanyScorecardCard />);
|
||||||
|
|
||||||
|
// Two 'No data yet' fallbacks: one for first_pass_yield, one for escaped_defects.
|
||||||
|
// median_lead_time_hours is present so its card shows the value, not the fallback.
|
||||||
|
expect(screen.getAllByText("No data yet").length).toBe(2);
|
||||||
|
|
||||||
|
// The fake stub labels must NOT appear anywhere
|
||||||
|
expect(screen.queryByText("Revenue growth")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Customer retention")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Not tracked yet")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render the fake 'Revenue growth' or 'Customer retention' stub labels", () => {
|
||||||
|
setQueryState({
|
||||||
|
data: buildSummary({
|
||||||
|
first_pass_yield: 0.9,
|
||||||
|
median_lead_time_hours: 12,
|
||||||
|
escaped_defects: 0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<CompanyScorecardCard />);
|
||||||
|
|
||||||
|
expect(screen.queryByText("Revenue growth")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Customer retention")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("Not tracked yet")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -239,32 +239,112 @@ function SpeedSection({ medianLeadTimeHours }: SpeedSectionProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Stub objectives section
|
// Objectives section — three charter objective cards, each showing a live
|
||||||
|
// metric against its target.
|
||||||
|
//
|
||||||
|
// ponytail: The charter `objectives` field is free text
|
||||||
|
// (Record<string, unknown>[]) while the three metrics are hardcoded
|
||||||
|
// (first_pass_yield, median_lead_time_hours, escaped_defects). The mapping
|
||||||
|
// is positional-by-convention, not derived — one card per charter objective
|
||||||
|
// only holds until you edit the Goals tab. Stated assumption, not
|
||||||
|
// discovered later.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function StubObjectivesSection() {
|
const OBJECTIVE_FALLBACK_LABELS = [
|
||||||
const stubs = [
|
"Tasks shipped to merge with no human code edits",
|
||||||
{ id: "obj-1", label: "Revenue growth" },
|
"Median lead time, intake → merged",
|
||||||
{ id: "obj-2", label: "Customer retention" },
|
"Critical escaped defects per release",
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
|
interface ObjectivesSectionProps {
|
||||||
|
objectives: Record<string, unknown>[];
|
||||||
|
firstPassYield: number | null | undefined;
|
||||||
|
medianLeadTimeHours: number | null | undefined;
|
||||||
|
escapedDefects: number | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectiveLabel(
|
||||||
|
objectives: Record<string, unknown>[],
|
||||||
|
index: number,
|
||||||
|
): string {
|
||||||
|
const raw = objectives[index]?.metric;
|
||||||
|
return typeof raw === "string" && raw.length > 0
|
||||||
|
? raw
|
||||||
|
: OBJECTIVE_FALLBACK_LABELS[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ObjectiveCardProps {
|
||||||
|
label: string;
|
||||||
|
hasData: boolean;
|
||||||
|
formattedValue: string;
|
||||||
|
targetText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ObjectiveCard({
|
||||||
|
label,
|
||||||
|
hasData,
|
||||||
|
formattedValue,
|
||||||
|
targetText,
|
||||||
|
}: ObjectiveCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border bg-card p-3 space-y-1">
|
||||||
|
<div className="text-xs text-muted-foreground">{label}</div>
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
{hasData ? (
|
||||||
|
<span className="font-medium tabular-nums">{formattedValue}</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground italic">No data yet</span>
|
||||||
|
)}
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
target: {targetText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ObjectivesSection({
|
||||||
|
objectives,
|
||||||
|
firstPassYield,
|
||||||
|
medianLeadTimeHours,
|
||||||
|
escapedDefects,
|
||||||
|
}: ObjectivesSectionProps) {
|
||||||
|
// Show 'No data yet' when a metric is null or undefined — never a fabricated
|
||||||
|
// value. Mirrors SpeedSection's hasData guard.
|
||||||
|
const fpyHasData = firstPassYield != null;
|
||||||
|
const ltHasData = medianLeadTimeHours != null;
|
||||||
|
const edHasData = escapedDefects != null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HelpTip label="Placeholder — not yet wired to the Goals tab's Objectives list">
|
|
||||||
<SectionLabel>Objectives</SectionLabel>
|
<SectionLabel>Objectives</SectionLabel>
|
||||||
</HelpTip>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{stubs.map((stub) => (
|
<ObjectiveCard
|
||||||
<div
|
label={objectiveLabel(objectives, 0)}
|
||||||
key={stub.id}
|
hasData={fpyHasData}
|
||||||
className="rounded-lg border border-dashed p-3 flex items-center justify-between"
|
formattedValue={
|
||||||
>
|
fpyHasData
|
||||||
<span className="text-sm text-muted-foreground">{stub.label}</span>
|
? `${((firstPassYield as number) * 100).toFixed(0)}%`
|
||||||
<span className="text-xs text-muted-foreground italic">
|
: ""
|
||||||
Not tracked yet
|
}
|
||||||
</span>
|
targetText="90%"
|
||||||
</div>
|
/>
|
||||||
))}
|
<ObjectiveCard
|
||||||
|
label={objectiveLabel(objectives, 1)}
|
||||||
|
hasData={ltHasData}
|
||||||
|
formattedValue={
|
||||||
|
ltHasData
|
||||||
|
? `${(medianLeadTimeHours as number).toFixed(1)}h`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
targetText="< 24h"
|
||||||
|
/>
|
||||||
|
<ObjectiveCard
|
||||||
|
label={objectiveLabel(objectives, 2)}
|
||||||
|
hasData={edHasData}
|
||||||
|
formattedValue={edHasData ? `${escapedDefects as number}` : ""}
|
||||||
|
targetText="0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -299,7 +379,12 @@ function ScorecardBody({
|
|||||||
spendTrendLoading={spendTrendLoading}
|
spendTrendLoading={spendTrendLoading}
|
||||||
/>
|
/>
|
||||||
<SpeedSection medianLeadTimeHours={data.median_lead_time_hours} />
|
<SpeedSection medianLeadTimeHours={data.median_lead_time_hours} />
|
||||||
<StubObjectivesSection />
|
<ObjectivesSection
|
||||||
|
objectives={data.objectives}
|
||||||
|
firstPassYield={data.first_pass_yield}
|
||||||
|
medianLeadTimeHours={data.median_lead_time_hours}
|
||||||
|
escapedDefects={data.escaped_defects}
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ export interface CockpitSummary {
|
|||||||
pending_pitches: number;
|
pending_pitches: number;
|
||||||
signals: CockpitSignal[];
|
signals: CockpitSignal[];
|
||||||
median_lead_time_hours?: number | null;
|
median_lead_time_hours?: number | null;
|
||||||
|
// Fraction of tasks shipped to merge with no human code edits (0–1).
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CockpitSignal {
|
export interface CockpitSignal {
|
||||||
|
|||||||
Reference in New Issue
Block a user