feat+test+docs: D51 — dashboard.html full multi-panel UI (Phase 3) (#28)

* feat+test+docs: D51 — dashboard.html full multi-panel UI (Phase 3)

Fourth Phase 3 D-day. Replaces the D50 dashboard.html placeholder with
the full 4-panel UI per ADR 0008 § 6. Vanilla HTML + JS + fetch — no
build step, no framework, no CDN (Lane 1 = A). 30s page poll with
document.visibilityState pause/resume (Lane 4 = A).

4 PANELS (all rendered from /v0/management/dashboard-data — single
backing endpoint per Lane 2 in-memory query model):

  Panel 1 — Per-provider quota
    Table: { Provider | Available | Status }. Null available → "n/a"
    pill; provider.quotaStatus() error → red status pill (graceful
    degradation per ADR § 9).

  Panel 2 — Last 24h: request count + cache hit + fallback rate
    Per-provider row: { Requests | Cache hit % | Fallback rate % }.
    Cache hit from cache_hit_24h.by_provider[p].hit_rate; fallback
    rate computed from window_24h.by_provider[p].fallback_count/count.

  Panel 3 — Request count last 30 days (SVG sparkline)
    Vanilla SVG bar chart with <title> tooltips showing per-day per-
    provider breakdown. Y-axis: requests per day (max-scaled). X-axis:
    30 daily UTC buckets.

  Panel 4 — Top fallback chains (last 24h)
    Numbered table: { # | Chain (monospace, arrow-joined) | Count |
    First seen | Last seen }.

POLL + VISIBILITYCHANGE PAUSE (ADR 0008 § 6.5):

  - setInterval(refresh, 30000) after initial fetch.
  - document.addEventListener('visibilitychange') → stopPolling() on
    hidden / refresh()+startPolling() on visible.
  - Prevents 2880 background polls/day per owner when tab hidden.

ERROR HANDLING:

  - 401 from dashboard-data → in-page error banner explains owner-tier
    requirement + suggests SSH-tunnel + header-injection workaround.
  - Other HTTP errors → generic "HTTP <code>" banner; console.warn
    for operator debugging.
  - Per-panel empty states ("Loading…", "No requests in window.",
    "No fallback chains triggered.").

CRITICAL CORRECTNESS INVARIANTS (ADR 0008 § 6 + Lane 1 = A):

  - No <script src> — entire JS inline (Suite 25d asserts).
  - No <link rel="stylesheet" href> — all CSS in <style> (25d).
  - Only one backing endpoint hit: /v0/management/dashboard-data
    (Suite 25e asserts).
  - 401 path keeps panels in last-good state rather than clearing —
    operator sees the error banner + can debug.

TESTS — Suite 25, +6 (582 → 588):

  25a: owner /dashboard response contains all 4 panel container IDs
  25b: JS declares POLL_INTERVAL_MS = 30000 + setInterval/clearInterval
  25c: visibilitychange listener + document.visibilityState check
  25d: NO external script src / NO external stylesheet href (Lane 1 = A
    pinning)
  25e: dashboard JS fetches /v0/management/dashboard-data only
  25f: 401 in-page error banner mentions owner-tier guidance

MANUAL SMOKE (ADR 0008 § 10 #12):

  Dashboard renders without console errors in a real browser when
  served by a running OLP instance + owner-tier Bearer via SSH-tunnel
  + header-injection extension. Not automated at Phase 3.

DOCUMENTATION:

  - AGENTS.md: dashboard.html marker promoted 🟡 D50 placeholder → 
    D51 full UI.
  - CHANGELOG.md: D51 entry under Unreleased per release_kit overlay.

NOT IN D51:

  - Daily audit rotation (D52)
  - tried_providers schema fix (D53; D45 P2 deferral)
  - Phase 3 close (D55; v0.3.0; maintainer-triggered)

Test count: 582 → 588 (+6). Verified locally via npm test.

AUTHORITY:

  - ADR 0008 § 6 (panels + refresh + localhost) + § 6.5 (poll +
    visibilityState pause) + Lane 1 = A (no build step) + Lane 4 = A
    (30s poll) + Lane 5 = B (full 4-panel scope).
  - ADR 0008 § 9 (graceful degradation surfaced in Panel 1).
  - ADR 0008 § 10 #12 (HTML smoke criterion satisfied at server-side
    level).
  - CLAUDE.md release_kit overlay phase_rolling_mode — under
    Unreleased.
  - Standing autopilot grant.

ALIGNMENT.md scope check: this PR replaces an existing entry-surface
static file (dashboard.html). Per Rule 5: management surface, not
OpenAI-spec-compatible — outside /v1/* spec scope. No code change in
server.mjs / lib/ / providers / IR / models-registry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: D51 fold-in — dashboard.html cosmetic polish (opus P3)

Fresh-context opus reviewer (PR #28) flagged 4 P3 cosmetic findings;
addressing the 2 trivial ones inline. The other 2 (visibilitychange
race + defensive date null-check) are negligible at family-scale per
reviewer; deferred to Phase 4 if UX feedback warrants.

- Line ~193: deleted orphan empty <text> SVG element (no textContent,
  rendered nothing — debris from initial pass).
- Line ~200 comment: was "Date labels (first / mid / last)" but only
  first + last rendered. Tightened to clarify intent + note mid label
  deferred to Phase 4 if needed.

No behavior change. 588/588 tests pass.

Authority: PR #28 fresh-context opus reviewer P3 findings (2 of 4
addressed; remaining 2 documented as negligible).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-05-25 16:11:40 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.7
parent f9f2eaa059
commit 251b578114
4 changed files with 453 additions and 39 deletions
+125
View File
@@ -11601,3 +11601,128 @@ describe('Suite 24 — D50 management endpoints (Phase 3, ADR 0008 §§ 7-8)', (
});
});
});
// ── Suite 25: D51 dashboard.html UI smoke (Phase 3, ADR 0008 § 6) ─────────
//
// HTML-level smoke tests for the D51 dashboard. Per ADR 0008 § 10 criterion
// #12 the "no JS console errors in a real browser" sub-claim is manual or
// playwright; this suite covers the server-observable claims:
// - The owner-served HTML contains the 4 panel container IDs.
// - The polling script references the 30s interval + visibilitychange
// pause hook.
// - No external script/style src (no build step, no framework, no CDN).
describe('Suite 25 — D51 dashboard.html UI smoke (Phase 3, ADR 0008 § 6)', () => {
const GLOBAL_OLP_HOME = process.env.OLP_HOME;
let _suite25SavedOAuth;
function ensureSuite25FakeOAuth() {
_suite25SavedOAuth = process.env.CLAUDE_CODE_OAUTH_TOKEN;
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'suite25-fake-oauth-token';
}
function restoreSuite25OAuth() {
if (_suite25SavedOAuth !== undefined) process.env.CLAUDE_CODE_OAUTH_TOKEN = _suite25SavedOAuth;
else delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
}
let TMP, server, port;
before(async () => {
TMP = _mkdtempSyncForSetup(_pathJoinForSetup(_tmpdirForSetup(), 'olp-test-25-'));
process.env.OLP_HOME = TMP;
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
__setProvidersEnabled({ anthropic: true });
__setSpawnImpl(makeMockSpawn(['suite25-response']));
ensureSuite25FakeOAuth();
server = createOlpServer();
await new Promise(resolve => {
server.listen(0, '127.0.0.1', () => resolve());
});
port = server.address().port;
});
after(async () => {
__resetSpawnImpl();
__setProvidersEnabled({});
__clearCache();
restoreSuite25OAuth();
if (server) await new Promise(resolve => server.close(() => resolve()));
process.env.OLP_HOME = GLOBAL_OLP_HOME;
__setAuthConfig({ allow_anonymous: true, owner_only_endpoints: [], fallback_detail_header_policy: 'all' });
rmSync(TMP, { recursive: true, force: true });
});
it('25a: owner /dashboard response contains all 4 panel container IDs', async () => {
const { plaintext_token } = createKey({ name: '25a-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
const r = await fetch({
port, method: 'GET', path: '/dashboard',
headers: { Authorization: `Bearer ${plaintext_token}` },
});
assert.equal(r.status, 200);
assert.match(r.body, /id="panel-quota"/);
assert.match(r.body, /id="panel-24h"/);
assert.match(r.body, /id="panel-trend"/);
assert.match(r.body, /id="panel-chains"/);
});
it('25b: dashboard JS references 30s polling interval + setInterval/clearInterval', async () => {
const { plaintext_token } = createKey({ name: '25b-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
const r = await fetch({
port, method: 'GET', path: '/dashboard',
headers: { Authorization: `Bearer ${plaintext_token}` },
});
assert.equal(r.status, 200);
assert.match(r.body, /POLL_INTERVAL_MS\s*=\s*30000/, 'must declare 30s POLL_INTERVAL_MS constant');
assert.match(r.body, /setInterval\(/, 'must call setInterval');
assert.match(r.body, /clearInterval\(/, 'must call clearInterval (for visibilitychange pause)');
});
it('25c: dashboard JS wires visibilitychange listener for tab-hidden pause', async () => {
const { plaintext_token } = createKey({ name: '25c-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
const r = await fetch({
port, method: 'GET', path: '/dashboard',
headers: { Authorization: `Bearer ${plaintext_token}` },
});
assert.equal(r.status, 200);
assert.match(r.body, /addEventListener\(\s*['"]visibilitychange['"]/, 'must register visibilitychange listener');
assert.match(r.body, /document\.visibilityState\s*===\s*['"]hidden['"]/, 'must check hidden state');
});
it('25d: dashboard has NO external script/style src (no build step, no framework, no CDN)', async () => {
const { plaintext_token } = createKey({ name: '25d-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
const r = await fetch({
port, method: 'GET', path: '/dashboard',
headers: { Authorization: `Bearer ${plaintext_token}` },
});
assert.equal(r.status, 200);
// ADR 0008 Lane 1 = A: static HTML + vanilla JS + fetch; no framework.
// Strict: no <script src=> and no <link rel="stylesheet" href=>.
assert.ok(!/<script\s+[^>]*src\s*=/i.test(r.body),
'dashboard must NOT include any <script src="..."> (no external JS — ADR 0008 Lane 1 A)');
assert.ok(!/<link\s+[^>]*rel\s*=\s*['"]stylesheet['"][^>]*href\s*=/i.test(r.body),
'dashboard must NOT include any external <link rel="stylesheet" href="..."> (no external CSS)');
});
it('25e: dashboard fetches /v0/management/dashboard-data (the only backing endpoint hit by JS)', async () => {
const { plaintext_token } = createKey({ name: '25e-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
const r = await fetch({
port, method: 'GET', path: '/dashboard',
headers: { Authorization: `Bearer ${plaintext_token}` },
});
assert.equal(r.status, 200);
assert.match(r.body, /fetch\(\s*['"]\/v0\/management\/dashboard-data['"]/,
'dashboard JS must fetch /v0/management/dashboard-data (consolidated D50 endpoint)');
});
it('25f: dashboard 401 surface for non-owner shows actionable error banner instructions', async () => {
// The dashboard HTML itself is served owner-only_block at handleDashboard
// (Suite 24 covers that). This test verifies the IN-PAGE error banner
// string mentions owner-token guidance so a future maintainer who lands
// on a 401 from the in-page JS knows what to do.
const { plaintext_token } = createKey({ name: '25f-owner', owner_tier: 'owner', providers_enabled: '*', olpHome: TMP });
const r = await fetch({
port, method: 'GET', path: '/dashboard',
headers: { Authorization: `Bearer ${plaintext_token}` },
});
assert.equal(r.status, 200);
assert.match(r.body, /401.*owner-tier/i, 'dashboard error banner must mention owner-tier in 401 case');
});
});