feat+test+docs: D50 — server.mjs management endpoints (Phase 3 dashboard wire-up) (#27)

* feat+test+docs: D50 — server.mjs management endpoints (Phase 3 dashboard wire-up)

Third Phase 3 D-day. Wires the D49 lib/audit-query.mjs aggregate query
layer into 4 owner_only_block HTTP endpoints per ADR 0008 §§ 7-8.
Ships a placeholder dashboard.html at repo root (D51 lands the full
multi-panel UI). All endpoints follow the Phase 2 / D45 auth + audit
+ touchLastUsed pattern.

4 NEW ENDPOINTS — all owner_only_block per ADR 0008 § 8:

  GET /dashboard
    Serves dashboard.html (text/html). D50 stub explains state +
    lists backing endpoints. D51 replaces with full UI.

  GET /v0/management/dashboard-data
    Full aggregate per § 7.2:
      { generated_at, window_24h, cache_hit_24h, quota,
        spend_trend_30d, top_fallback_chains_24h, cache_stats }

  GET /v0/management/quota
    Quota subset only (per-provider provider.quotaStatus + error
    capture per § 9 graceful degradation).

  GET /cache/stats
    Live in-memory cacheStore.stats() with generated_at wrapper.

HELPER:

  _runOwnerOnlyManagementEndpoint(req, res, method, path, inner)
    Factors common auth + audit ctx + owner-block + res.on('finish')
    wire. inner is async (req, res, olpIdentity, auditCtx) → void.
    Eliminates 4× boilerplate.

OWNER_ONLY_BLOCK MODE (ADR 0008 § 8 D48-fold-in):

  authenticate → if owner_tier !== 'owner' → 401 owner_required.
  Distinct from owner_only_trim (Phase 2 /health pattern). Anonymous
  identity (when allow_anonymous: true) REACHES the handler and is
  401'd by the owner check (Suite 24c). Allow_anonymous: false + no
  header → 401 auth_required at middleware (Suite 24d).

PROVIDER QUOTASTATUS ERROR CAPTURE:

  Dashboard-data + quota endpoints catch per-provider throws and
  surface { provider, error, available: null } so one bad provider
  doesn't fail the whole panel (ADR 0008 § 9 graceful degradation).

DASHBOARD.HTML PLACEHOLDER (~50 lines at repo root):

  Explains D50 state, lists backing endpoints with curl example.
  Cached in memory at first /dashboard request via _loadDashboardHtml
  with module-scope _dashboardHtmlCache; falls back to in-memory stub
  if file missing (defensive for test imports from non-repo cwd).

AUDIT ON MANAGEMENT ENDPOINTS (ADR 0008 § 7.5):

  Every management request appends audit row including 401 paths
  (verified by Suite 24j). Touch wire skips anonymous + env-owner
  identities (matches Phase 2 pattern).

TESTS — Suite 24, +11 (571 → 582):

  24a-d: /dashboard owner_only_block matrix (owner 200 / guest 401
    / anonymous-with-allow_anonymous=true 401 / no-auth-with-
    allow_anonymous=false 401)
  24e: dashboard-data owner → 200 JSON with all § 7.2 fields
    (asserts spend_trend_30d.length === 30)
  24f: dashboard-data guest → 401 owner_required
  24g: quota owner → 200 JSON with quota array
  24h: cache/stats owner → 200 JSON shape
  24h-401: cache/stats guest → 401
  24i: successful dashboard-data appends audit row with status 200
    + key_id + correct path
  24j: 401 (guest blocked) dashboard-data appends audit row with
    error_code: 'owner_required' + owner_tier: 'guest'

DOCUMENTATION:

  - AGENTS.md: dashboard.html new entry (D50 placeholder); lib/audit-
    query.mjs marker note unchanged.
  - CHANGELOG.md: D50 entry under Unreleased per release_kit overlay.

NOT IN D50 scope:

  - Full dashboard UI (D51 — replaces dashboard.html with the real
    4-panel layout + 30s poll JS)
  - Daily audit rotation (D52)
  - tried_providers schema fix (D53)
  - Phase 3 close (D55; v0.3.0; maintainer-triggered)

Test count: 571 → 582 (+11). Verified locally via npm test.

AUTHORITY:

  - ADR 0008 § 7 (endpoint definitions) + § 8 (owner_only_block
    mode) + § 9 (graceful degradation) + § 7.5 (audit on management
    endpoints).
  - ADR 0007 § 7 (auth model reused).
  - ADR 0002 § Provider contract (quotaStatus).
  - ADR 0005 (cacheStore.stats source of truth).
  - CLAUDE.md release_kit overlay phase_rolling_mode — under
    Unreleased.
  - Standing autopilot grant.

ALIGNMENT.md scope check: this PR adds 4 new entry-surface endpoints
under owner-only_block gating + a new lib/audit-query consumer surface.
Per Rule 5: management endpoints are owner-only operational surface,
not OpenAI-spec-compatible — they exist outside the /v1/chat/completions
+ /v1/models spec scope. No provider plugin / IR change.

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

* docs: D50 fold-in — AGENTS.md dashboard.html duplicate (opus P3)

Fresh-context opus reviewer (PR #27) flagged a duplicate dashboard.html
entry: my D50 addition was added directly above a stale
"Planned (Phase 6) — not yet authored" line that should have been
removed. The file contradicted itself.

Fix: merge into single entry — keep the original line phrasing and
attach the D50 status update.

No code change, no test change.

Authority: PR #27 fresh-context opus reviewer P3 finding.

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:02:47 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.7
parent 686794e316
commit f9f2eaa059
5 changed files with 529 additions and 1 deletions
+28
View File
@@ -4,6 +4,34 @@ All notable changes to OLP land here. Per `CLAUDE.md` release_kit overlay, this
## Unreleased
### D50 — `server.mjs` management endpoints (Phase 3 dashboard wire-up)
Third Phase 3 D-day. Wires the D49 `lib/audit-query.mjs` aggregate query layer into 4 owner_only_block HTTP endpoints per ADR 0008 §§ 7-8. Ships a placeholder `dashboard.html` at repo root (D51 lands the full multi-panel UI). All endpoints follow the Phase 2 / D45 auth + audit + touchLastUsed pattern.
- **4 new endpoints** (all owner_only_block per ADR 0008 § 8 — anonymous + guest + missing-key all → 401):
- `GET /dashboard` — serves `dashboard.html` (Content-Type text/html; charset=utf-8). D50 stub explains the state + lists backing endpoints; D51 replaces with full UI.
- `GET /v0/management/dashboard-data` — full aggregate per ADR 0008 § 7.2: `{ generated_at, window_24h (auditAggregateRequests), cache_hit_24h (auditCacheHitRateWindow), quota (per-provider provider.quotaStatus + error capture), spend_trend_30d (auditSpendTrendDaily — exactly 30 entries), top_fallback_chains_24h (auditTopFallbackChains limit 10), cache_stats (live cacheStore.stats()) }`.
- `GET /v0/management/quota` — quota subset only (subset of dashboard-data; useful for scripted monitoring).
- `GET /cache/stats` — live in-memory `cacheStore.stats()` shape (`{ hits, misses, size, inflightCount }` + `generated_at` wrapper).
- **`_runOwnerOnlyManagementEndpoint(req, res, method, path, inner)` helper** factors the common auth + audit ctx + owner-block + res.on('finish') wire. inner is async (req, res, olpIdentity, auditCtx) → returns void. Eliminates 4× boilerplate.
- **`owner_only_block` mode** (ADR 0008 § 8): authenticate → if not owner → 401 `owner_required`. Distinct from `owner_only_trim` (Phase 2 /health pattern). Anonymous identity (when `allow_anonymous: true`) reaches the handler and is 401'd by the owner check — verified by Suite 24c.
- **Provider quotaStatus error capture**: dashboard-data + quota endpoints catch per-provider throws and surface `{ provider, error, available: null }` so one bad provider doesn't fail the whole panel (ADR 0008 § 9 graceful degradation).
- **`dashboard.html` placeholder** (~50 lines at repo root): explains the D50 state, lists backing endpoints with curl example. Cached in memory at first /dashboard request (`_loadDashboardHtml` with module-scope `_dashboardHtmlCache`); falls back to an in-memory stub if the file is missing (e.g., test imports from non-repo cwd).
- **Audit on management endpoints** (ADR 0008 § 7.5): every management request appends an audit row including 401 paths (verified by Suite 24j). Touch wire skips anonymous + env-owner identities (matches Phase 2 pattern).
- **Router**: 4 new GET branches added between /v1/chat/completions and the 404 fallback.
- **Test surface (Suite 24, +11 tests — 571 → 582):**
- 24a-d: /dashboard owner_only_block (owner 200 / guest 401 / anonymous-with-allow_anonymous=true 401 / no-auth-with-allow_anonymous=false 401)
- 24e: dashboard-data owner → 200 JSON with all required ADR § 7.2 fields (asserts `spend_trend_30d.length === 30`)
- 24f: dashboard-data guest → 401 owner_required
- 24g: quota owner → 200 JSON with quota array
- 24h: cache/stats owner → 200 JSON with `{ hits, misses, size, inflightCount, generated_at }`
- 24h-401: cache/stats guest → 401
- 24i: successful dashboard-data appends audit row with `status_code: 200` + `key_id` + `path: '/v0/management/dashboard-data'`
- 24j: 401 (guest blocked) dashboard-data appends audit row with `error_code: 'owner_required'` + `owner_tier: 'guest'`
- **Documentation:** AGENTS.md `lib/audit-query.mjs` D49 marker note added + new `dashboard.html` entry (D50 placeholder).
- **Test count:** 571 → 582 (+11 D50 tests in Suite 24).
- **Authority:** ADR 0008 § 7 (endpoints) + § 8 (owner_only_block mode) + § 9 (graceful degradation) + § 7.5 (audit on management endpoints); ADR 0007 § 7 (auth model reused); ADR 0002 § Provider contract (quotaStatus); ADR 0005 (cacheStore.stats); CLAUDE.md `release_kit overlay phase_rolling_mode` — under Unreleased; standing autopilot grant.
### D49 — `lib/audit-query.mjs` audit aggregate query layer (Phase 3)
Second Phase 3 D-day. Implements ADR 0008 § 4 query API. Pure in-memory ndjson scan; cross-file walk over `audit.ndjson` (live) + `audit-YYYY-MM-DD.ndjson` (rotated). No server.mjs integration in this D-day (D50 wires the consuming endpoints).