Commit graph

3 commits

Author SHA1 Message Date
le king fu
9084d90609 feat(server): archive closed Claude usage days by month, serve them on /claude-usage/history
POST /claude-usage now merges every day of the sanitized snapshot except
the last one (today, still open) into <dir of CLAUDE_USAGE_PATH>/history/
YYYY-MM.json, after the snapshot itself is written. The latest snapshot
wins, so a day leaving the 30-day window keeps the value the agent froze
at D+2. Each month file is written atomically (same-dir temp, mode 600,
rename). A month file that cannot be read back is never overwritten, and
an archive failure is logged (months and error codes only) without
changing the 204.

GET /claude-usage/history?from=YYYY-MM&to=YYYY-MM (read token) serves the
archived days, defaulting to the 12 months up to the last archived month,
400 on a bad format, from > to or a span over 36 months. Month files are
re-read through the snapshot's per-day whitelist, now exported from
claude-usage.js as sanitizeDay().

The new module also carries the one-shot import command used by the
backfill (`node claude-usage-history.js --import <file|->`), where an
already archived date always wins.

Refs #55

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:41:30 -04:00
le king fu
e2a24d768b fix(server): require a full ISO receivedAt on read, cover the temp-file cleanup
readClaudeUsage() and readHostSnapshot() validated the stored receivedAt
with new Date(), which accepts "1" or a bare date. Both now go through
isIsoTimestamp(), exported by claude-usage.js and built on the sanitizer's
own check, so no regex is duplicated.

The existing 500 tests failed at mkdir, before the temp file existed, so
removing the unlinkSync() cleanup of either writer left the suite green. Two
new cases make the rename fail after the temp write (the target is a
non-empty directory) and require that no temp file is left.

Refs #54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:25:26 -04:00
le king fu
e1224e0df4 feat(claude-usage): add POST and GET /claude-usage behind a third token kind
- checkAuth() reads the expected token from a TOKEN_KINDS table
  (read -> HEALTH_TOKEN, ingest -> HOSTS_INGEST_TOKEN, claude-ingest ->
  CLAUDE_USAGE_INGEST_TOKEN); each ingestion token fails closed to a 503
  naming its own variable. Still one gate, one tokenMatches() call. A route
  naming an unknown kind fails module load.
- readBody() takes its ceiling per route: 4 KiB for /hosts/<id>, 64 KiB for
  /claude-usage.
- claude-usage.js: pure, exported sanitizeClaudeUsage() for contract
  schemaVersion 1 (whitelist rebuild, invalid values dropped, list bounds).
- POST /claude-usage: 400 on invalid JSON or schemaVersion, server-stamped
  receivedAt, atomic write (pid-suffixed temp, mode 600, rename), 204.
- GET /claude-usage: re-sanitized snapshot with ageSeconds, stale and
  staleAfterSeconds (CLAUDE_USAGE_STALE_SECONDS, default 2700), or no_data.
- Dockerfile COPY ships claude-usage.js.
- auth.test.js: the checkAuth() equality allowlist loses the former
  `route.tokenKind === "ingest"` entry, which the table removes. The pin
  only got stricter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 09:16:05 -04:00