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>
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>
- __tests__/claude-usage.test.js (119 cases): sanitizer contract and
idempotence, dropped values (unknown fields, negative, past 2^53,
detail outside its regex, status outside the enum, bad dates, model
keys), list bounds, token isolation both ways, 503 naming each variable,
413 at 65 537 bytes and /hosts/<id> still 413 at 4 097, atomic mode-600
write, POST then GET round trip, exact staleness boundary under a frozen
clock, no_data degradation, rejection logs, COPY line.
- writes.test.js: POST /claude-usage may write only in the directory of
CLAUDE_USAGE_PATH, its rejections write nothing, GET /claude-usage joins
the read routes.
- README.md and CLAUDE.md: both routes, the three env vars, the
/data/claude-usage read-write mount owned by uid 1000, the token table.
Resolves#48
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>