Quality gates / quality (pull_request) Waiting to run
The counters had an endpoint and no way to read them without jq. Adds a pure formatter plus a CLI that prints the app, queue, and model boundaries. The formatter is a function of the snapshot and the three frozen vocabularies, so a caller-supplied failure label cannot reach the terminal: unknown classes are simply not printed. Refusal paths are explicit — 404 means the route is off because no token is configured, 403 means the token was rejected — and the token is never echoed in the report or an error line. RED: tests/model-metrics-report-format.test.js failed with ERR_MODULE_NOT_FOUND before src/model-metrics-report.js existed; the refusal-path test failed with a generic 'answered 404' before the 404 branch was written. Tests: npm test 298/298, npm run test:metrics-report 8/8, syntax and diff gates clean, real-Chromium ui acceptance green with the privacy disclosure visible.
89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Operator report for the privacy-safe model counters (issue #19).
|
|
//
|
|
// It reads one snapshot from a running Timmy server and prints it. The token is a credential for the
|
|
// operator surface, so it is read from the environment or argv and never echoed — not in errors, not
|
|
// in the report, not in a stack trace.
|
|
import { formatOperatorReport } from '../src/model-metrics-report.js';
|
|
|
|
const USAGE = `Usage: node scripts/model_metrics_report.mjs [options]
|
|
|
|
--url <origin> base URL of the Timmy server (default http://127.0.0.1:4173)
|
|
--token <token> operator token (default: $TIMMY_METRICS_TOKEN)
|
|
--json print the raw snapshot instead of the text report
|
|
--timeout-ms <ms> request deadline (default 5000)
|
|
--help show this text
|
|
`;
|
|
|
|
function parseArgs(argv) {
|
|
const options = {
|
|
url: process.env.TIMMY_METRICS_URL || 'http://127.0.0.1:4173',
|
|
token: process.env.TIMMY_METRICS_TOKEN || '',
|
|
json: false,
|
|
timeoutMs: 5_000,
|
|
help: false,
|
|
};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === '--help' || arg === '-h') {
|
|
options.help = true;
|
|
} else if (arg === '--json') {
|
|
options.json = true;
|
|
} else if (arg === '--url') {
|
|
options.url = argv[++index] ?? '';
|
|
} else if (arg === '--token') {
|
|
options.token = argv[++index] ?? '';
|
|
} else if (arg === '--timeout-ms') {
|
|
options.timeoutMs = Number(argv[++index]);
|
|
} else {
|
|
throw new Error(`unknown option: ${arg}`);
|
|
}
|
|
}
|
|
return options;
|
|
}
|
|
|
|
async function main(argv) {
|
|
const options = parseArgs(argv);
|
|
if (options.help) {
|
|
process.stdout.write(USAGE);
|
|
return 0;
|
|
}
|
|
if (!options.token) {
|
|
throw new Error('operator token required: pass --token or set TIMMY_METRICS_TOKEN');
|
|
}
|
|
const endpoint = new URL('api/metrics', options.url.endsWith('/') ? options.url : `${options.url}/`);
|
|
let response;
|
|
try {
|
|
response = await fetch(endpoint, {
|
|
headers: { authorization: `Bearer ${options.token}`, accept: 'application/json' },
|
|
signal: AbortSignal.timeout(Number.isFinite(options.timeoutMs) ? options.timeoutMs : 5_000),
|
|
});
|
|
} catch (error) {
|
|
process.stderr.write(`Could not read ${endpoint.origin}/api/metrics: ${error?.message || 'request failed'}\n`);
|
|
return 1;
|
|
}
|
|
if (!response.ok) {
|
|
if (response.status === 404) {
|
|
// The route is absent rather than guarded: that is a deployment fact, not a credential problem.
|
|
process.stderr.write('Metrics are disabled on this server: the route only exists when TIMMY_METRICS_TOKEN is set.\n');
|
|
return 1;
|
|
}
|
|
if (response.status === 403) {
|
|
process.stderr.write('The server rejected the operator token.\n');
|
|
return 1;
|
|
}
|
|
process.stderr.write(`The server answered ${response.status} for /api/metrics.\n`);
|
|
return 1;
|
|
}
|
|
const snapshot = await response.json();
|
|
process.stdout.write(options.json ? `${JSON.stringify(snapshot, null, 2)}\n` : formatOperatorReport(snapshot));
|
|
return 0;
|
|
}
|
|
|
|
main(process.argv.slice(2)).then(code => {
|
|
process.exitCode = code;
|
|
}).catch(error => {
|
|
process.stderr.write(`${error?.message || 'report failed'}\n`);
|
|
process.exitCode = 2;
|
|
});
|