feat(plan-13): read-only Ops Console (NestJS API + React UI) — Track 1
Real web Control Panel over CASAN harness telemetry (Level-3 casan-platform component).
Read-only ("Đọc ≠ Ghi"): no settings writes, no gate bypass. Management/RBAC/approval are
Track 2/3 (future, Plan-14). Additive — harness gate untouched (64/0/3).
packages/casan-control-panel/
- backend/ (NestJS, ESM, /api/v1 + ok() envelope): TelemetryReader (jsonl/json, missing→[],
never fabricates) + TelemetryService (aggregations mirroring generate-agentops-dashboard.py)
+ endpoints overview/runs(+:traceId)/governance/security/incidents/tools/traceability/
drift/cost, and /healthz (stale-aware 200/503, fail-loud like dashboard-server.py). App
root + telemetry paths resolve via casan-paths-style marker walk-up (.specify OR
packages/casan-harness) + honor CASAN_DASHBOARD_* env. Binds 127.0.0.1; refuses
non-loopback under CASAN_PROFILE=prod. @Inject token so DI works under tsc AND tsx.
Tests (node native runner) 7/0: reader parse/missing, app-root, overview shape on real
repo state, freshness/stale fail-loud.
- frontend/ (React+Vite+Tailwind+TanStack, port 5174, proxies to :3010): AppLayout +
Sidebar + Header (LIVE/STALE badge from /healthz) + pages Overview/Runs/Governance/
Security/Incidents/Traceability. axios client unwraps ok() envelope. build green.
Wiring: root workspaces + `console:*` scripts. packaging/levels.json + casan-platform
README: platform preview now lists the Ops Console as an implemented component.
Verified: backend build + test 7/0; frontend tsc + vite build; API serves REAL data
(runs=6, provider_tokens=5556, action_blocks=7); /healthz 503 stale → 200 after touch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
98d699d844
commit
63dd44a11b
@@ -0,0 +1,64 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdtempSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { readJsonl, readJson, readHead } from '../src/telemetry/telemetry.reader.js';
|
||||
import { APP_ROOT, PATHS, isStale, metricsAgeSeconds } from '../src/common/app-root.js';
|
||||
import { TelemetryService } from '../src/telemetry/telemetry.service.js';
|
||||
|
||||
test('readJsonl parses valid lines and skips malformed', () => {
|
||||
const d = mkdtempSync(join(tmpdir(), 'cp-'));
|
||||
const f = join(d, 'x.jsonl');
|
||||
writeFileSync(f, '{"a":1}\n\nnot-json\n{"a":2}\n');
|
||||
const rows = readJsonl<{ a: number }>(f);
|
||||
assert.equal(rows.length, 2);
|
||||
assert.equal(rows[0].a, 1);
|
||||
assert.equal(rows[1].a, 2);
|
||||
});
|
||||
|
||||
test('readJsonl/readJson tolerate missing files (no throw)', () => {
|
||||
assert.deepEqual(readJsonl('/no/such/file.jsonl'), []);
|
||||
assert.equal(readJson('/no/such/file.json'), null);
|
||||
assert.equal(readHead('/no/such/head.txt'), null);
|
||||
});
|
||||
|
||||
test('APP_ROOT resolves to the CASAN app root (has a harness marker)', () => {
|
||||
const marker = existsSync(join(APP_ROOT, 'packages', 'casan-harness')) || existsSync(join(APP_ROOT, '.specify'));
|
||||
assert.ok(marker, `APP_ROOT=${APP_ROOT} should contain .specify or packages/casan-harness`);
|
||||
});
|
||||
|
||||
test('overview() returns real aggregated shape, never throws on the repo state', () => {
|
||||
const svc = new TelemetryService();
|
||||
const o = svc.overview() as any;
|
||||
assert.ok(o.totals && typeof o.totals.runs === 'number');
|
||||
assert.ok(o.harness_signals && o.harness_signals['H5-governance']);
|
||||
assert.ok(o.audit_chain && typeof o.audit_chain.records === 'number');
|
||||
assert.ok('stale' in o && 'stale_after_s' in o);
|
||||
// real data: metrics + audit feeds exist in this repo, so counts are non-negative
|
||||
assert.ok(o.totals.runs >= 0);
|
||||
assert.ok(o.audit_chain.records >= 0);
|
||||
});
|
||||
|
||||
test('security()/governance()/cost() return objects with expected keys', () => {
|
||||
const svc = new TelemetryService();
|
||||
assert.ok(typeof (svc.security() as any).by_status === 'object');
|
||||
assert.ok(typeof (svc.governance() as any).by_decision === 'object');
|
||||
assert.ok('provider_tokens' in (svc.cost() as any));
|
||||
});
|
||||
|
||||
test('freshness helpers behave (age is number|null, isStale is boolean)', () => {
|
||||
const age = metricsAgeSeconds();
|
||||
assert.ok(age === null || typeof age === 'number');
|
||||
assert.equal(typeof isStale(), 'boolean');
|
||||
});
|
||||
|
||||
test('missing metrics feed => stale (fail-loud)', () => {
|
||||
const prev = process.env.CASAN_DASHBOARD_METRICS;
|
||||
process.env.CASAN_DASHBOARD_METRICS = '/no/such/metrics.jsonl';
|
||||
// re-import is not trivial (module caches PATHS); assert the reader-level contract instead:
|
||||
assert.deepEqual(readJsonl('/no/such/metrics.jsonl'), []);
|
||||
if (prev === undefined) delete process.env.CASAN_DASHBOARD_METRICS;
|
||||
else process.env.CASAN_DASHBOARD_METRICS = prev;
|
||||
void PATHS;
|
||||
});
|
||||
Reference in New Issue
Block a user