Skip to content

Commit 070a1a7

Browse files
committed
Merge branch 'main' into preview (v2.7.21)
2 parents ee5f6ad + b5e7603 commit 070a1a7

53 files changed

Lines changed: 2195 additions & 130 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Usage surface filter
2+
3+
Date: 2026-07-16
4+
Class: C3 — small but persistent frontend/backend contract change
5+
Loop archetype: spec-satisfaction repair
6+
7+
## Goal
8+
9+
Add a real `All / Codex / Claude` filter to the Usage page. The selected surface must consistently control summary cards, active-day charts, model rows, and provider rows. The control stays compact beside the existing range filter; Codex and Claude labels collapse to their SVG marks at narrow widths.
10+
11+
## Trigger
12+
13+
The Usage page currently filters only by time range. Users who run both Codex and Claude through opencodex cannot separate their traffic.
14+
15+
## Non-goals
16+
17+
- Do not infer the client surface from provider or model names.
18+
- Do not migrate or rewrite existing `usage.jsonl` files.
19+
- Do not add a dependency, global store, new endpoint, or new icon asset.
20+
- Do not alter provider/model aggregation rules, token arithmetic, or the Logs page.
21+
- Do not push or release from this unit.
22+
23+
## Existing contract and baseline
24+
25+
- `RequestLogContext` and `RequestLogEntry` already use `surface?: "claude"`; absence means the Codex surface.
26+
- `addRequestLog` currently drops that field when it writes `usage.jsonl`.
27+
- `/api/usage` currently accepts only `range` and aggregates every persisted row.
28+
- The GUI already ships `/provider-icons/openai.svg` and `/provider-icons/claude.svg`.
29+
- Baseline command: `bun test --isolate tests/request-log.test.ts tests/usage-log.test.ts tests/usage-summary.test.ts tests/api-usage.test.ts`
30+
- Baseline result: 47 pass, 0 fail on 2026-07-16.
31+
32+
## Design read
33+
34+
```yaml
35+
name: opencodex-usage-surface-filter
36+
colors:
37+
primary: existing semantic tokens
38+
accent: existing active segmented-control fill
39+
background: existing surface token
40+
typography:
41+
heading: existing title token
42+
body: existing control token
43+
iconography:
44+
system: existing inline SVG family
45+
weight: regular
46+
domain: existing provider brand SVG assets
47+
```
48+
49+
Reading this as a Korean-first repeated-work dashboard with quiet, dense controls. Keep the current visual grammar: two adjacent pill groups, no new color, no motion beyond existing button feedback.
50+
51+
- DESIGN_VARIANCE: 3
52+
- MOTION_INTENSITY: 1
53+
- Product density: D5
54+
55+
## Work-phase map
56+
57+
This unit is one implementation work-phase and one PABCD cycle:
58+
59+
1. `010_usage_surface_filter.md` — persist the existing surface marker, filter the usage summary contract, wire the responsive segmented control, add regressions, verify the rendered states.
60+
61+
## Scope manifest
62+
63+
### New
64+
65+
- `devlog/_plan/260716_usage_surface_filter/000_plan.md` — objective, scope, baseline, evidence ledger.
66+
- `devlog/_plan/260716_usage_surface_filter/010_usage_surface_filter.md` — diff-level implementation contract.
67+
68+
### Modify
69+
70+
- `src/server/request-log.ts`
71+
- `src/usage/log.ts`
72+
- `src/usage/summary.ts`
73+
- `src/server/management-api.ts`
74+
- `tests/usage-log.test.ts`
75+
- `tests/usage-summary.test.ts`
76+
- `tests/api-usage.test.ts`
77+
- `tests/claude-messages-endpoint.test.ts`
78+
- `tests/claude-native-passthrough.test.ts`
79+
- `tests/server-auth.test.ts`
80+
- `gui/src/pages/Usage.tsx`
81+
- `gui/src/styles.css`
82+
- `docs/design-system/components.md`
83+
84+
## Verifier
85+
86+
The verifier measures both the data boundary and rendered behavior:
87+
88+
1. Focused unit/API regressions prove persistence, legacy fallback, query parsing, and whole-summary filtering.
89+
2. Existing Claude routed/native and Codex Responses integration tests prove the ingress marker survives real request finalization and persistence.
90+
3. Root TypeScript check and GUI build prove contract and frontend integration.
91+
4. Browser QA at desktop, split-screen, mobile, and narrow widths proves both segmented controls fit, icon collapse activates, every surface selection changes the displayed usage dataset, and keyboard/focus semantics remain available.
92+
93+
## Stop condition
94+
95+
DONE requires all focused tests, typecheck, GUI build, and one clean browser observation after exercising All, Codex, and Claude. Stop as BLOCKED if the running proxy cannot be safely restarted or the browser cannot reach the locally served build after three distinct recovery attempts. Return NEEDS_HUMAN only if real product intent conflicts with the legacy-surface fallback.
96+
97+
## Escalation and delegation
98+
99+
- Downward: bounded backend/tests and frontend/UI slices may be delegated only with disjoint write scopes and explicit `cxc-dev-*` skill routing.
100+
- Upward: after two distinct workers fail the same packet, the main agent reclaims that slice and records the failure before editing.
101+
- Any new storage migration, inferred classification, or endpoint expansion is outside scope and requires a P-phase amendment.
102+
103+
## Memory artifact
104+
105+
This folder is the continuity record. C-phase command output and screenshot paths are appended here; D records the terminal outcome and moves the unit to `devlog/_fin/` only after all criteria pass.
106+
107+
## Known historical limitation
108+
109+
Old `usage.jsonl` rows did not persist the request surface. Provider/model values cannot recover it because Claude traffic may route through any provider. Missing markers therefore keep the product's existing convention and count as Codex. `All` remains historically exact; Codex/Claude views become exact for rows written after this change.
110+
111+
## Reuse decisions
112+
113+
- Reuse the existing `surface?: "claude"` marker and the existing four-locale `logs.filter.surface.*` copy.
114+
- Reuse the shipped OpenAI and Claude brand SVG files. Do not call `providerIconSrc`: the control represents client surfaces rather than upstream providers, so a small local surface-to-asset map is clearer than coupling the filter to provider aliases.
115+
116+
## Expected terminal outcomes
117+
118+
- `DONE`: verified surface filter shipped locally and recorded.
119+
- `NOOP`: impossible because the capability is absent at baseline.
120+
- `BLOCKED`: local runtime/browser dependency unavailable after bounded recovery.
121+
- `UNSAFE`: an implementation would require destructive history migration.
122+
- `NEEDS_HUMAN`: legacy-row semantics cannot be resolved from existing product behavior.
123+
124+
## Audit record
125+
126+
- Round 1: `GO-WITH-FIXES (blockers=4)` — explicit accessible names, selection semantics, 44px touch targets, and the historical attribution caveat were underspecified.
127+
- Frontend responsive audit: `FAIL` — the initial radio semantics conflicted with the six-Tab-stop verifier, and moving the filter row alone did not prove 320px containment.
128+
- Synthesis: accepted every blocker. The plan now uses `role="group"` + `aria-pressed`, translated per-button `aria-label`, `var(--control-touch)` at mobile, and complete group stacking at <=360px; it records legacy ambiguity and adds real Codex/routed Claude/native Claude persistence tests.
129+
- Re-audit: `PASS` — no blockers. Non-blocking follow-up only: the untouched Logs page retains its older radiogroup pattern.
130+
131+
## Implementation record
132+
133+
- `57c1893 feat(usage): filter aggregates by client surface`
134+
- Persists the existing Claude surface marker in the usage allowlist.
135+
- Adds typed `all | codex | claude` parsing and filters once before totals, days, models, and providers.
136+
- Threads the normalized surface through normal and catch API responses.
137+
- Activates the contract through focused, routed Claude, native Claude, and Codex Responses tests.
138+
- `49e77f3 feat(gui): add responsive usage surface filter`
139+
- Adds the source control before the range control and sends both query dimensions.
140+
- Uses translated button names, `aria-pressed`, existing OpenAI/Claude SVG marks, 44px mobile targets, and complete group stacking at 320px.
141+
- Documents the segmented-filter contract in the GUI design system.
142+
143+
## Check evidence
144+
145+
- Focused persistence/API: `bun test --isolate tests/request-log.test.ts tests/usage-log.test.ts tests/usage-summary.test.ts tests/api-usage.test.ts` — 52 pass, 0 fail.
146+
- Routed/native request activation: `bun test --isolate tests/claude-messages-endpoint.test.ts tests/claude-native-passthrough.test.ts tests/server-auth.test.ts` — 69 pass, 0 fail.
147+
- Full regression: `bun test --isolate ./tests/` — 2556 pass, 0 fail across 238 files.
148+
- Static/build: `bun run typecheck` and `cd gui && bun run build` — exit 0.
149+
- Lint: `cd gui && bun run lint` — 0 errors; two pre-existing TanStack Virtual compiler warnings in untouched `Debug.tsx` and `Logs.tsx`.
150+
- Patch integrity: `git diff --check` — exit 0; no assertions deleted, skipped, or weakened.
151+
- Final independent diff review: `PASS`; no correctness blocker in marker propagation, fallback, aggregate consistency, fetch cancellation, accessibility, or 320px containment.
152+
153+
## Render evidence
154+
155+
- Desktop: `.codexclaw/evidence/usage-surface-filter/usage-1280.png` — both groups show text and marks without overlap.
156+
- Split-screen/tablet: `usage-css-1024.png` and `usage-css-768.png` — page `scrollWidth` equals viewport width; at 768px both pill groups remain beside the title.
157+
- Mobile: `usage-css-390.png` — `All + Codex SVG + Claude SVG`, two groups on one line, 44px button height, no page overflow.
158+
- Narrow: `usage-css-320.png` — groups stack at 274px each inside the 320px viewport, every button is 44px high, no page overflow.
159+
- Interaction: Codex and Claude clicks swap `aria-pressed` correctly; keyboard focus matches `:focus-visible`; browser console warnings/errors: none.
160+
- Teardown: temporary Vite proxy QA server on port 5173 stopped after browser finalization.
161+
162+
## D closeout
163+
164+
Terminal outcome: `DONE`.
165+
166+
- All acceptance criteria in `010_usage_surface_filter.md` are met.
167+
- No production dependency, icon asset, migration, inference rule, or release action was added.
168+
- No push was performed.
169+
- What did not improve: historical Claude rows remain indistinguishable because the old JSONL format discarded the surface marker. `All` is exact for history; source-specific attribution is exact for new rows only.
170+
- Evidence that would invalidate this direction: a real request finalized outside `addFinalRequestLog`, a provider/model combination that is incorrectly used as source evidence, or browser proof of either segmented group clipping below 320px. None appeared in the audited call graph or verification matrix.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Work-phase 010 — Usage surface filter
2+
3+
Status: completed — 2026-07-16
4+
5+
## Acceptance contract
6+
7+
1. `surface=all` returns the existing aggregate unchanged.
8+
2. `surface=codex` includes rows whose persisted `surface` is absent; this preserves the existing request-log convention and legacy `usage.jsonl` behavior.
9+
3. `surface=claude` includes only rows explicitly persisted with `surface: "claude"`.
10+
4. Unknown or missing surface query values fall back to `all`.
11+
5. Every Usage page section is driven by the same server-filtered response.
12+
6. Desktop shows `All / Codex / Claude` text. Narrow layouts keep `All` and replace Codex/Claude visible labels with their existing SVG marks while preserving full accessible names.
13+
7. The existing time-range selection stays independent and is sent with the surface query.
14+
8. Both segmented controls use native buttons inside labelled `role="group"` containers. Every button remains a Tab stop and exposes selection with `aria-pressed`; no incomplete radio keyboard model is introduced.
15+
16+
## Activation scenarios
17+
18+
- Surface persistence branch: finalize a Claude request log and observe `surface: "claude"` in the appended/read-back usage row.
19+
- Legacy fallback branch: summarize a row without `surface` under `codex` and observe that it remains included.
20+
- Claude filter branch: summarize mixed rows under `claude` and observe that all totals, days, models, and providers exclude Codex rows.
21+
- Invalid query branch: call `/api/usage?surface=unknown` and observe `surface: "all"` with the unfiltered total.
22+
- Responsive branch: render at 390px and 320px; observe hidden Codex/Claude text, visible brand marks, no clipping, and accessible button names.
23+
24+
## Diff map
25+
26+
### MODIFY `src/usage/log.ts`
27+
28+
Before: `PersistedUsageEntry` has no surface marker and `normalizeUsageEntry` cannot retain one.
29+
30+
After:
31+
32+
- Add `surface?: "claude"` to `PersistedUsageEntry`.
33+
- Copy only the known literal through `normalizeUsageEntry`.
34+
- Keep the field optional so existing JSONL rows remain valid without migration.
35+
36+
### MODIFY `src/server/request-log.ts`
37+
38+
Before: `addRequestLog` persists request metadata but drops `RequestLogEntry.surface`.
39+
40+
After:
41+
42+
- Forward `entry.surface` into `appendUsageEntry` when present.
43+
- Preserve the existing rule that Codex is represented by absence and Claude by the literal marker.
44+
45+
### MODIFY `src/usage/summary.ts`
46+
47+
Before: `summarizeUsage(entries, range, now)` filters only by time.
48+
49+
After:
50+
51+
- Add exported `UsageSurface = "all" | "codex" | "claude"`.
52+
- Add `parseSurface(input)` with an `all` fallback.
53+
- Add `surface` to `UsageSummary`.
54+
- Extend `summarizeUsage(entries, range, now, surface = "all")`.
55+
- Filter once before totals/day/model/provider builders: Claude rows require the explicit marker; Codex rows use `surface !== "claude"` so legacy rows remain compatible.
56+
- Do not change downstream aggregation helpers.
57+
58+
### MODIFY `src/server/management-api.ts`
59+
60+
Before: `/api/usage` parses `range` only.
61+
62+
After:
63+
64+
- Parse the `surface` query at the HTTP boundary.
65+
- Pass the typed value into `summarizeUsage`.
66+
- Keep a 200 response and fallback behavior for unknown values, matching existing range handling.
67+
- Include the normalized `surface` in both the normal response and the catch/error fallback.
68+
69+
### MODIFY `tests/usage-log.test.ts`
70+
71+
- Persist and read back a Claude row with `surface: "claude"`.
72+
- Assert unspecified legacy rows remain unchanged and secret-safe normalization still strips unrelated runtime fields.
73+
74+
### MODIFY `tests/usage-summary.test.ts`
75+
76+
- Include `surface` in the fixture builder.
77+
- Test `parseSurface` accepted values and fallback.
78+
- Test mixed Codex/Claude entries under `all`, `codex`, and `claude`.
79+
- Assert summary totals plus day/model/provider rows all follow the selected surface.
80+
81+
### MODIFY `tests/api-usage.test.ts`
82+
83+
- Mark one fixture row as Claude.
84+
- Assert response includes the normalized `surface`.
85+
- Exercise `surface=codex`, `surface=claude`, and an unknown value.
86+
87+
### MODIFY `tests/claude-messages-endpoint.test.ts`
88+
89+
- Extend the existing routed Claude end-to-end case to query `/api/usage?range=all&surface=claude` after consuming the response.
90+
- Assert the persisted row appears in Claude and is absent from Codex.
91+
92+
### MODIFY `tests/claude-native-passthrough.test.ts`
93+
94+
- Extend the existing native Anthropic passthrough case with the same persisted surface assertions.
95+
96+
### MODIFY `tests/server-auth.test.ts`
97+
98+
- Extend the existing Codex `/v1/responses` usage integration to query `surface=codex`, assert the normalized response field, and prove the row stays out of the Claude aggregate.
99+
100+
### MODIFY `gui/src/pages/Usage.tsx`
101+
102+
Before: local state and fetch URL contain only `range`; the header renders one segmented control.
103+
104+
After:
105+
106+
- Add `UsageSurface` state defaulting to `all`.
107+
- Include `surface` in `fetchUsage`, effect dependencies, and the response type.
108+
- Add a labelled source button group before the labelled range button group.
109+
- Reuse `logs.filter.surface.*` translations for All/Codex/Claude and its accessible group label.
110+
- Render existing `/provider-icons/openai.svg` and `/provider-icons/claude.svg` as decorative marks beside visible labels.
111+
- Mark each choice with `aria-pressed`; keep all six buttons in normal Tab order and avoid radio roles that would require roving focus and arrow-key selection.
112+
- Set an explicit translated `aria-label` on every source and range button. Collapsed visible text may use `display: none` because the button name does not depend on that span.
113+
- Keep the title and both filters in a wrapping header container; do not add a new component abstraction.
114+
115+
### MODIFY `gui/src/styles.css`
116+
117+
- Generalize the existing Usage segmented-control classes so the source and range groups share geometry and focus behavior.
118+
- Add `.usage-filters` and source-button label/icon classes.
119+
- At <=640px hide only Codex/Claude visible text and keep the icons; `All` stays text.
120+
- At <=760px add `min-height: var(--control-touch)` so every button reaches the existing 44px touch token.
121+
- At <=360px stack the two pill groups vertically, keeping each three-option group intact within the 284px content width.
122+
- Preserve the existing global visible `:focus-visible` outline.
123+
124+
### MODIFY `docs/design-system/components.md`
125+
126+
- Document segmented filters as adjacent exclusive choices using native buttons plus `aria-pressed`, pill geometry, 44px mobile targets, accessible names for icon-only collapsed labels, and no data-loss collapse.
127+
128+
## Verification commands
129+
130+
```sh
131+
bun test --isolate tests/request-log.test.ts tests/usage-log.test.ts tests/usage-summary.test.ts tests/api-usage.test.ts
132+
bun test --isolate tests/claude-messages-endpoint.test.ts tests/claude-native-passthrough.test.ts tests/server-auth.test.ts
133+
bun run typecheck
134+
bun run build:gui
135+
```
136+
137+
Browser matrix:
138+
139+
- Desktop 1280x900: both groups show text; All/Codex/Claude each refresh the entire dataset.
140+
- Split-screen 768x900: controls stay in the header without overlap.
141+
- Mobile 390x844: All text plus two provider marks; independent range group remains usable.
142+
- Narrow 320x700: filter row wraps intentionally; no clipped title/button or horizontal page overflow.
143+
- Keyboard: tab through six buttons; visible focus; active choices expose `aria-pressed=true`.
144+
145+
## Rollback
146+
147+
Revert this unit's commits. Existing JSONL rows with the additive `surface` field remain readable because the old reader already ignores extra fields; no file migration or cleanup is required.

0 commit comments

Comments
 (0)