* feat: D82 — dashboard UI Claude.ai-style per-provider rows (Phase 5)
Implements ADR 0012 D82: restructures dashboard.html to render
quota_v2 data (produced by D81 / PR #53) in a Claude.ai-style
per-provider row layout. Closes v1.x roadmap #8.
## What changed
### dashboard.html (A–G)
A. New "Plan Usage" section at top (full-width, above the 2-col grid):
- Per-provider rows rendered from `data.quota_v2`
- Each row: provider badge (colored chip), status dot + chip
(live/stale/unavailable), schema version tag
- Two utilization bars (5h + 7d) with:
- Rounded gradient bar (green <50% / amber 50-80% / red >80%)
- Label "Current 5-hour session: 49%" / "Weekly all-models: 31%"
- Right-side reset countdown (see B)
- Bottom chips: representative-claim badge (purple), overage chip
(amber/green), fallback-percentage chip, last-fresh "Updated N
min ago" tag; stale rows show amber ⚠ stale data chip with tooltip
- Unavailable rows: provider badge + reason text only; no bars
B. formatResetCountdown(epochSeconds):
- < 1 hour: "Resets in 23 min"
- 1–24 hours: "Resets in 12hr 30min"
- < 7 days: "Resets Sun 9:00 PM" (weekday + 12h time)
- >= 7 days: "Resets May 31 9:00 PM" (month day + time)
- past: "Resetting now…"
Uses toLocaleString('en-US', { hour12: true }).
C. 60s auto-refresh (quota_v2 only) with visibilityState guard:
- Separate timer (quotaRefreshTimer); does NOT replace the 30s poll
- Pauses on 'hidden'; resumes + immediate re-fetch on 'visible'
- Other 3 panels (24h, 30d, top fallback) keep 30s cadence unchanged
D. Manual refresh button (↻ Refresh) in Plan Usage header:
- 2-second spam guard (button disables post-click)
- Spinning ⟳ icon during fetch
- Re-enables after fetch completes (success or error)
E. Graceful quota_v2 / legacy quota fallback:
- If data.quota_v2 is present and non-empty → render Plan Usage rows;
hide legacy "Quota (per provider)" panel
- If data.quota_v2 is absent/empty → show note in Plan Usage area;
surface legacy data.quota in the original table panel
- Guards operator running an older OLP build (pre-D81)
F. Visual polish: rounded bars, gradient fills, airy whitespace, mobile-
responsive (bars reflow on narrow viewports via flex-wrap). Color
palette: #10b981 (green), #f59e0b (amber), #ef4444 (red) matching
Tailwind emerald/amber/red-500 per spec.
G. Other 3 panels (24h, 30d, top fallback) and their 30s poll cadence
are IDENTICAL to D51. Only the Quota panel restructures.
### docs/v1x-roadmap.md (H)
Marks entry #8 as "✅ CLOSED (D82, v0.5.0)". Adds closure status,
PR ref, and a brief note inside the entry body. Updates reading-order
header paragraph to include #8 in the closed list.
## Authority + citations
- ADR 0012 D82 — Claude.ai-style restructure D-day spec
(docs/adr/0012-phase-5-charter-quota-probes-dashboard.md § D-day table)
- D81 PR #53 — quota_v2 shape producer (commit 5288493);
ProviderQuotaEntry shape per ADR 0008 Amendment 1 § 3
- Maintainer reference 2026-05-26 — claude.ai/settings/usage screenshot;
"Resets in 1hr 6min" / "Resets Sun 9:00 PM" string format
- v1.x roadmap #8 — closed by this commit
(docs/v1x-roadmap.md #8 — Dashboard enrichment)
## Test impact
npm test: 727 pass / 0 fail (unchanged). dashboard.html is frontend-only;
D83 ships Suite 38/39 (probe unit tests + dashboard smoke tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(dashboard): D82 reviewer Nit #4 — gate overage chip on real status
When entry.overage = { status: null, disabled_reason: null } (audit-query's
default shape when the provider doesn't supply overage info), the chip rendered
as amber "Overage: —" which falsely suggests a warning state.
Now: chip only renders when entry.overage.status is truthy (i.e., Anthropic
actually returned an overage-status header). Truly-missing overage info shows
no chip at all, matching the maintainer's intent.
Identified by D82 fresh-context reviewer (PR #54 thread) as the only
maintainer-visible nit worth folding in pre-merge. 1-line change. All 727
tests continue to pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
17 KiB
OLP v1.x Roadmap — Deferred Work Tracker
Purpose. Single landing page for every Phase-1 deferral that an actual v1.x sprint must pick up. Each entry cross-references its ratifying ADR, its GitHub issue (if any), and the load-bearing code anchor so a future maintainer can resume without spelunking the commit history.
Status: Living document. Add new entries at the top. Each item should answer:
- What is deferred?
- Why was it deferred (link the ratifying ADR amendment).
- Where does the work live in the tree today (file + anchor).
- When does it need to land (trigger: load profile, security event, governance amendment).
Reading order for a v1.x sprint kickoff. As of 2026-05-26, #1 (streaming SF, D57+D58), #2 (multi-key auth, Phase 2), #4 and #7 (closed in D56), and #8 (dashboard enrichment, D82 Phase 5) are CLOSED. Remaining v1.x scope: #3 (soft trigger reactivation), #5 (provider cacheKeyFields mask), #6 (streaming SPAWN_FAILED salvage — unbundled from #1 at #1 close). All three remaining items have explicit "trigger to start" gates that have not fired.
#1 — Streaming-path singleflight + TOCTOU close — ✅ SHIPPED (D57 + D58, 2026-05-25)
- Status. Closed. Trigger (b) fired 2026-05-25 — maintainer "go" after v0.3.1. Shipped across three D-days:
- D57 (PR #36) — cache layer:
cacheStore.getOrComputeStreaming(keyId, cacheKey, sourceFactory, opts) → { stream, isFirst, role }with_streamingInflightMap, tee fan-out, late-joiner replay buffer, per-client backpressure (PER_CLIENT_QUEUE_CAP=1MB), replay cap (ACCUMULATED_REPLAY_CAP=10MB), AbortController propagation, synchronous Map check+insert (closes TOCTOU). Suite 27 = 12 unit tests. - D58 (PR #37) — server.mjs wiring: streaming branch swap;
tryAcquireSpawn/releaseSpawnmoved insidesourceFactoryclosure (D38 §7 coordination);CONCURRENCY_LIMITfallthrough preserved;X-OLP-Streaming-Inflight: source | attachedheader;cache_status: 'streaming_attached'audit value + audit-query gauge reconciliation;res.on('close') → stream.return()for client disconnect; D16 truncated-not-cached invariant preserved viacacheStore.deleteon stop-less exhaustion. Suite 28 = 8 HTTP integration tests. - D59 (this commit) — docs polish: README known-limitations entry inverted; this roadmap entry closed; issue #16 closed.
- D57 (PR #36) — cache layer:
- Design authority.
docs/adr/0005-cache-cross-provider.mdAmendment 8 — implemented per spec §§1–14 across D57+D58. - Tracking issue. GitHub issue #16 — CLOSED at D59 with refs to D57+D58 PRs.
- Final test count delta. 603 (v0.3.1) → 623 (v0.3.2/v0.4.0). +20 tests across the SF arc.
- Deferred sub-items (left here as future-work pointers, NOT blocking #1 closure):
X-OLP-Streaming-Inflight: solovalue not emitted on the wire (Amendment 8 §11). It's observable only post-stream via thestreaming_inflight_source_donelog event'sattached_count: 0. Future ADR amendment may expose via HTTP trailer.streaming_inflight_joinlog event from_attachClientcache-layer path (carries no provider/model context). D58 emits the event from the server-layer wrapper instead; cache-layer emission would need a provider/model plumb (TODO marker atlib/cache/store.mjs:~620).isFirstfield returned bygetOrComputeStreamingis currently unused by server.mjs (rolesupersedes). Could be removed in a future cache-layer API cleanup.
#2 — Multi-key auth (lib/keys.mjs) — PHASE 2 ACTIVE (no longer deferred)
- Status. Phase 2 active as of 2026-05-25. Design ratified at D43-B. This entry stays for cross-reference but is no longer a v1.x deferral; implementation D-days D44+ execute within Phase 2.
- What. Per-API-key identity, namespace scoping for the cache, ownership tier (owner vs guest) for header gating, and audit log of which key issued which request. Detailed scope in ADR 0007.
- Design ADR (ratified).
docs/adr/0007-multi-key-auth.md— Option 2 (filesystem manifest) + opaque token, with explicit forward path to Option 3 hybrid (SQLite-indexed mirror) when Phase 3+ Dashboard / SQL-aggregate quota work justifies. Migratable, manifest-as-SPOT. - Tracking. Not a GitHub issue. Tracked here + via ADR 0007 acceptance criteria (§ 10) which drive the D44+ test surface.
- Resolves.
X-OLP-Fallback-Detailowner-only gating (D40 / ADR 0004 Amendment 5 — currently ungated; Phase 2 re-gates per ADR 0007 § 7)./healthper-key visibility (currently anonymous-only — owner / guest / anonymous tiers per ADR 0007 § 7).
- Code anchors today (unchanged at ADR ratification; replaced by D44+ implementation).
lib/cache/store.mjs:77-79per-keyId namespace Map — wire is in place.lib/cache/store.mjs:287singleflight composition${keyId}:${cacheKey}— wire is in place.server.mjs:502, :531— the twokeyId='__anonymous__'call sites to replace.server.mjs:392—/healthhandler entry (Phase 2 gate).server.mjs:1072, :1101—X-OLP-Fallback-Detailheader-write paths (Phase 2 gate).
- Trigger (already fired). Maintainer opened Phase 2 sprint 2026-05-25.
#3 — Soft trigger reactivation (ADR 0004 Amendment 2)
- What. Per-provider
quotaStatuspolling,softThresholdcomparisons, soft-skip advancement when quota approaches limit. CurrentlyevaluateSoftTriggersalways returnsfalsebecausequotaSnapshotis never populated. - Why deferred. v0.1 hard triggers (SPAWN_FAILED / CLI_NOT_FOUND / SPAWN_TIMEOUT / CONCURRENCY_LIMIT) are sufficient for fallback advancement at personal/family scale. Soft triggers require persistent quota snapshots and a polling mechanism, which adds operational surface (timer drift, snapshot staleness, observability burden).
- Design ADR.
docs/adr/0004-fallback-engine.mdAmendment 2 — explicit v1.x deferral with mitigations (startup warning if user configures soft thresholds without runtime enforcement). - Tracking. Not a GitHub issue. Tracked here + via the startup warning in
server.mjs(the_softTriggersConfiguredwarn emission). - Blocks.
- Issue #8 (
X-OLP-Provider-Usedchain-origin semantics) — Option A (trackfirstAttemptedProvider) becomes preferable once soft triggers can fire. See ADR 0004 Amendment 6 § v1.x re-evaluation. X-OLP-Fallback-Detailtrigger_type: 'soft'path — currently dead code, becomes live with this work.
- Issue #8 (
- Code anchors today.
lib/fallback/engine.mjsevaluateSoftTriggers(returns false unconditionally at v0.1).lib/providers/base.mjsProvider.quotaStatuscontract (declared but unused at v0.1).
- Trigger to start. First quota-rate-limit event in the wild — at which point the operator would want pre-emptive advancement rather than spawn-then-fail.
#4 — /health activeSpawns integration
- What. Surface D38
getActiveSpawnCount(providerName)per-provider on the/healthendpoint at the pathproviders.status.<name>.activeSpawns. - Why deferred. D38 (issue #1) shipped the runtime enforcement and exported
getActiveSpawnCount;/healthintegration was scoped out as forward-looking polish. - Design ADR.
docs/adr/0002-plugin-architecture.mdAmendment 6 — names the target path explicitly: "/healthintegration deferred — when surfaced there will land atproviders.status.<name>.activeSpawns; not wired at D38." - Tracking. Not a GitHub issue. Tracked here.
- Code anchors today.
lib/providers/index.mjsexportsgetActiveSpawnCountalready.server.mjs handleHealth— extension point for the new field.
- Trigger to start. First time the maintainer wants per-provider concurrency visibility for capacity planning.
#5 — Provider-level cacheKeyFields (per-plugin mask)
- What. Per-plugin declaration of which IR fields are actually consumed by the underlying CLI invocation, used by
computeCacheKeyto skip fields that the plugin drops at spawn. Reduces spurious-miss rate from the v0.1 conservative-posture trade-off (Amendment 7). - Why deferred. At personal/family scale the extra spawn cost from spurious misses is negligible. The contract extension adds complexity (per-plugin field set + plumbing through
buildDefaultChain→executeHopFn→computeCacheKey). - Design ADR.
docs/adr/0005-cache-cross-provider.mdAmendment 7 § Forward path. - Tracking. Not a GitHub issue. Tracked here.
- Code anchors today.
- Plugin file headers — each lists its "fields dropped at spawn" table for human reference; the v1.x amendment makes that table machine-readable.
lib/cache/keys.mjs computeCacheKey— would acceptpluginCacheKeyMaskparameter.
- Trigger to start. First time spurious-miss rate becomes a measurable load factor.
#6 — Streaming-path SPAWN_FAILED salvage
- What. Currently the streaming branch does NOT participate in D16 salvage (the salvage-on-SPAWN_FAILED + chunks pattern that the buffered path uses). Streaming SPAWN_FAILED mid-stream → the truncation marker (D35 #10) fires, but no salvage logic captures partial chunks for downstream cache reuse.
- Why deferred. Less impactful than #1 — at most one client benefits per spawn event, and the buffered path already provides salvage for the bulk of requests. Streaming is the minority path.
- Status update post-#1 close (2026-05-25). #1 was originally bundled with #6 in the design ADR (Amendment 8). The tee architecture as implemented does NOT carry salvage semantics — D57's tee writes
accumulatedChunksto cache only on normal source completion (stop chunk seen); on SPAWN_FAILED mid-stream the cache layer rejects all clients with the error and does NOT persist partial chunks. D58 preserves D16's truncated-not-cached invariant via server-layercacheStore.deleteon stop-less exhaustion. #6 therefore remains independently deferrable. - Design ADR. Not yet ratified. The unbundling from #1 means #6 now needs its own ADR amendment when triggered.
- Tracking. Not a GitHub issue. Tracked here.
- Trigger to start. First report of streaming-path SPAWN_FAILED mid-stream where partial-chunk salvage would have helped a downstream caller. Practically unlikely at family scale.
#8 — Dashboard enrichment: per-provider subscription quota + reset times + 1-min refresh + manual refresh (D78 follow-up) — ✅ CLOSED (D82, v0.5.0)
- Status. Closed at D82 (Phase 5).
dashboard.htmlrestructured to Claude.ai-style per-provider rows renderingquota_v2. Closed by PR on branchd82-dashboard-ui-claude-ai-style; ships with v0.5.0. 60s quota auto-refresh + manual refresh button + visibilityState guard implemented. Graceful fallback to legacyquotafield when server runs a pre-D81 build. - What. Phase 3 dashboard (D51
dashboard.html, v0.3.0) shows: per-provider quota (currently always "n/a — no quota api"), last-24h request count + cache hit + fallback rate, 30d request-count sparkline, top fallback chains. Maintainer request 2026-05-26 post-D78: extend to show what each enabled provider's subscription is actually consuming, with reset times visible, refresh once per minute (current 30s is OK but maintainer specified 1min target), and a manual refresh button. Reference design: Claude.ai's ownclaude.ai/settings/usagepage — current session bar with "Resets in 1hr 6min", weekly all-models bar with "Resets Sun 9:00 PM", per-model bar (Sonnet only), additional features (routine runs), usage credits + monthly spend limit + auto-reload toggle. - Why deferred. v0.3.0/v0.4.x ships the dashboard frame but
provider.quotaStatus()returnsnullin all three v0.1 plugins (anthropic / openai / mistral). The ratifying spec in ADR 0004 Amendment 2 puntsquotaStatus()to v1.x ("soft trigger reactivation") — this dashboard ask is the operator-facing reason that work would land. - What this requires. Per-provider plugin work + dashboard.html UI work + audit-query.mjs aggregation:
lib/providers/anthropic.mjs quotaStatus()— discover where the maintainer's Claude.ai subscription quota state is exposed. Candidates: (a)claudeCLI command (e.g.,claude usage) if Anthropic adds one — currently absent; (b) parsing theclaude-codeoutput for rate-limit error messages and caching state from headers; (c) hittingapi.anthropic.com/v1/.../usagedirectly via the OAuth refresh token — not a documented endpoint, primary-source risk. ADR 0002 Rule 1 / Rule 5 require an authority citation before any implementation. Likely path: wait until Anthropic publishes a documented endpoint, OR derive from audit-side request counts only (no real quota truth, just "you sent N requests in the current 5h window").lib/providers/openai.mjs quotaStatus()— codex CLI doesn't expose ChatGPT-subscription quota state. OpenAI rate-limit headers per request might be parseable but ADR 0004 Amendment 2 explicitly says no plugin parses HTTP status at v0.1.lib/providers/mistral.mjs quotaStatus()— Le Chat Pro has/v1/usageendpoint per Mistral docs (verify).dashboard.htmlUI restructure to a Claude.ai-style layout: rows of (label, bar, "Resets in X" / "Resets atlib/audit-query.mjs— extendaggregateRequests/spendTrendDailyto compute "in the current rolling window" (since session/week start) per provider. Today's aggregates are wall-clock windows; subscription resets are per-account-anchored. Need a way to model session windows (e.g., "Anthropic 5h-from-first-request-since-last-reset").
- Reference (maintainer 2026-05-26). Screenshot of
claude.ai/settings/usageshared inline. Key panels: Plan usage limits (current session + resets-in), Weekly limits (All models / Sonnet only / per-feature breakdown, each with resets-on), Additional features (Daily included routine runs N / 15), Usage credits (toggle + spent vs monthly limit + auto-reload + buy-credits link). - Tracking. Not yet a GitHub issue. Track here + cross-reference ADR 0004 Amendment 2 (soft trigger reactivation — same
quotaStatus()data-source work) when this becomes Phase 5 scope. - Code anchors today.
dashboard.html— current 4 panels; needs restructure to Claude.ai-style row layoutlib/providers/anthropic.mjs/openai.mjs/mistral.mjs—quotaStatus()returns null todaylib/audit-query.mjs— currentaggregateRequestsis wall-clock-window; needs session-window variant
- Trigger to start. ANY of: (a) Anthropic publishes a documented
claude usageCLI orapi.anthropic.com/v1/usageendpoint, (b) maintainer hits real "I want to see quota right now" pain often enough to design without per-provider truth (audit-derived only), (c) Phase 5 multi-tenant adds per-key spend limits and the dashboard needs to surface those.
#7 — AUTH_MISSING tuple path test coverage (D40 follow-up)
- What. Dedicated test in
test-features.mjsSuite D40 that asserts thefallbackDetailtuple records the AUTH_MISSING path withtrigger_type: 'auth_missing'. D40 reviewer flagged this as the last gap in the engine-path matrix; code is structurally correct, just lacks an explicit pin. - Why deferred. Low priority — the AUTH_MISSING early-return branch has the tuple push BEFORE it (verified in D40 reviewer pass), so coverage is implicit via the other engine-path tests. A 3-line dedicated test would make the pin explicit.
- Design. No ADR needed. ~5-line test addition.
- Tracking. Not a GitHub issue. Tracked here.
- Trigger to start. Next routine test-suite hardening pass, OR when AUTH_MISSING handling is changed for any reason.
Adding a new entry
When a future D-day defers work, the deferring commit should:
- Always update this file with a new entry at the top.
- Always name the ratifying ADR amendment (or note "no ADR yet — future work needs one").
- Always name the load-bearing code anchor (
file:lineform preferred over symbolic names — the symbolic name can drift). - Always name a concrete trigger to start the work — vague triggers ("when needed") let entries rot.
- If the deferral has a GitHub issue, keep it OPEN and reference it here. If it does NOT, leave a note explaining why (e.g., "tracked here only — no external governance event filed").
The maintainer's session-startup discipline should grep this file at sprint kickoff. If an entry's "trigger to start" condition is met, it leaves this page and becomes a sprint item.