From 2dd76967ad9b95bf6f7b0f14014cd318777e2d4a Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:34:27 +0200 Subject: [PATCH] fix(dashboard): preserve cancelled request count --- .../src/features/dashboard/schemas.test.ts | 2 + frontend/src/features/dashboard/schemas.ts | 1 + .../design.md | 44 +++++++++++++++++++ .../proposal.md | 28 ++++++++++++ .../specs/usage-error-metrics/spec.md | 20 +++++++++ .../tasks.md | 11 +++++ 6 files changed, 106 insertions(+) create mode 100644 openspec/changes/preserve-dashboard-cancelled-count/design.md create mode 100644 openspec/changes/preserve-dashboard-cancelled-count/proposal.md create mode 100644 openspec/changes/preserve-dashboard-cancelled-count/specs/usage-error-metrics/spec.md create mode 100644 openspec/changes/preserve-dashboard-cancelled-count/tasks.md diff --git a/frontend/src/features/dashboard/schemas.test.ts b/frontend/src/features/dashboard/schemas.test.ts index 7d57df7a7..d3bf64dc9 100644 --- a/frontend/src/features/dashboard/schemas.test.ts +++ b/frontend/src/features/dashboard/schemas.test.ts @@ -57,6 +57,7 @@ describe("DashboardOverviewSchema", () => { cachedInputTokens: 300, errorRate: 0.02, errorCount: 10, + cancelledCount: 3, topError: null, }, comparison: { @@ -81,6 +82,7 @@ describe("DashboardOverviewSchema", () => { expect(parsed.accounts).toHaveLength(0); expect(parsed.summary.comparison?.previous.requests).toBe(250); + expect(parsed.summary.metrics?.cancelledCount).toBe(3); }); it("drops legacy request_logs field from parse result", () => { diff --git a/frontend/src/features/dashboard/schemas.ts b/frontend/src/features/dashboard/schemas.ts index ec4708861..be44af3e9 100644 --- a/frontend/src/features/dashboard/schemas.ts +++ b/frontend/src/features/dashboard/schemas.ts @@ -64,6 +64,7 @@ const DashboardMetricsSchema = z.object({ cachedInputTokens: z.number().nullable(), errorRate: z.number().nullable(), errorCount: z.number().nullable(), + cancelledCount: z.number().int().nonnegative().nullable().optional(), topError: z.string().nullable(), conversations: z.number().int().nullable().optional().default(null), conversationRequests: z.number().int().nonnegative().optional().default(0), diff --git a/openspec/changes/preserve-dashboard-cancelled-count/design.md b/openspec/changes/preserve-dashboard-cancelled-count/design.md new file mode 100644 index 000000000..678cf611d --- /dev/null +++ b/openspec/changes/preserve-dashboard-cancelled-count/design.md @@ -0,0 +1,44 @@ +## Context + +The backend overview response already carries a nullable `cancelledCount`. +`DashboardOverviewSchema` is the frontend trust boundary, and Zod strips +unknown object keys there. The omission is therefore isolated to the typed +consumer contract; no calculation or transport change is required. + +## Goals / Non-Goals + +**Goals:** + +- Keep the frontend overview contract aligned with the backend response. +- Lock the documented requests/error/cancelled breakdown with a focused test. + +**Non-Goals:** + +- Change cancellation classification or aggregation. +- Add a new dashboard card or navigation surface. +- Change backward compatibility for payloads that omit the field. + +## Decisions + +- Declare `cancelledCount` as nullable and optional, matching the additive + backend response and preserving compatibility with older servers. +- Test the field through `DashboardOverviewSchema.parse`, the actual API + boundary, rather than testing the nested schema in isolation. + +Alternative considered: configure the metrics object with `.passthrough()`. +That would weaken the trust boundary for every unknown metric, so the explicit +field is the smaller and safer change. + +## Risks / Trade-offs + +- [Risk] Frontend and backend optionality drift → Mirror the existing additive + metric pattern and cover a payload that includes the field. + +## Migration Plan + +Ship as an additive frontend contract change. Rollback is removal of the field; +backend responses remain compatible in either direction. + +## Open Questions + +None. diff --git a/openspec/changes/preserve-dashboard-cancelled-count/proposal.md b/openspec/changes/preserve-dashboard-cancelled-count/proposal.md new file mode 100644 index 000000000..32a6c5939 --- /dev/null +++ b/openspec/changes/preserve-dashboard-cancelled-count/proposal.md @@ -0,0 +1,28 @@ +## Why + +The dashboard overview API emits `cancelledCount`, but the frontend Zod +boundary omits that field and silently strips it from otherwise valid metrics +payloads. Operators therefore cannot distinguish a window with no +cancellations from one whose cancellation count was discarded client-side. + +## What Changes + +- Preserve `cancelledCount` when the dashboard parses overview metrics. +- Add a frontend contract regression covering the backend's documented + requests/error/cancelled breakdown. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `usage-error-metrics`: The dashboard frontend preserves the overview + cancellation count emitted by the API. + +## Impact + +Frontend dashboard schema and its focused tests only. No backend, database, or +navigation changes. diff --git a/openspec/changes/preserve-dashboard-cancelled-count/specs/usage-error-metrics/spec.md b/openspec/changes/preserve-dashboard-cancelled-count/specs/usage-error-metrics/spec.md new file mode 100644 index 000000000..46b338d1f --- /dev/null +++ b/openspec/changes/preserve-dashboard-cancelled-count/specs/usage-error-metrics/spec.md @@ -0,0 +1,20 @@ +## MODIFIED Requirements + +### Requirement: Cancelled counts surface alongside error counts + +Metric surfaces that expose an error count MUST also expose the window's +cancelled count as an additive field: the dashboard overview metrics +(`cancelledCount`), the usage summary metrics (`cancelled7d`), the reports +daily rows (`cancelled_count`) and summary (`total_cancelled`), and the fleet +pressure metrics (`cancelledCount`). The dashboard overview cancelled total +MUST be sourced from the demand quarter rollup (status grain) for the folded +segment plus the raw tail, so it stays accurate across history already folded +without the hourly `cancelled_count` measure. The dashboard frontend MUST +preserve `cancelledCount` when parsing the overview response. + +#### Scenario: Dashboard overview preserves the status breakdown + +- **GIVEN** the dashboard overview API returns `requests=4`, `errorCount=1`, + and `cancelledCount=2` +- **WHEN** the frontend parses the overview response +- **THEN** the parsed metrics expose all three values unchanged diff --git a/openspec/changes/preserve-dashboard-cancelled-count/tasks.md b/openspec/changes/preserve-dashboard-cancelled-count/tasks.md new file mode 100644 index 000000000..a6b8e110c --- /dev/null +++ b/openspec/changes/preserve-dashboard-cancelled-count/tasks.md @@ -0,0 +1,11 @@ +## 1. Frontend contract + +- [x] 1.1 Add a failing dashboard schema test that requires `cancelledCount` + to survive parsing +- [x] 1.2 Add `cancelledCount` to the dashboard metrics Zod schema + +## 2. Validation + +- [x] 2.1 Run the focused dashboard schema tests +- [x] 2.2 Run frontend typecheck and build +- [x] 2.3 Validate OpenSpec