Commit graph

10 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
75ed6dd324 fix(test-curl): fix the two tokens, refuse bad ports on both variables (#46)
All checks were successful
CI / verification (pull_request) Successful in 27s
Non-blocking points of the review of PR #47.

- TOKEN and INGEST_TOKEN are no longer read from the environment, like
  the URLs: two inherited values that happened to be equal broke the
  compartmentalisation cases. Pinned in the proxy case, which now runs
  with one inherited value for both.
- The refusal cases run each bad value (not a number, 0, above 65535)
  on each of the two variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:06:01 -04:00
le king fu
2c7886dbfe fix(test-curl): take the two ports from the environment (#46)
All checks were successful
CI / verification (pull_request) Successful in 28s
test-curl.sh booted its servers on 3099 and 3098, hardcoded, and
ci.test.js runs it whole on every `npm test`: two runs side by side got
in each other's way, at least one of them failing every time.

- TEST_CURL_PORT and TEST_CURL_NOINGEST_PORT choose the ports, 3099 and
  3098 staying the defaults. Names of their own rather than PORT, which
  a dev shell often exports already.
- BASE_URL and NOINGEST_URL are derived from the ports and no longer
  read from the environment: overridden alone, they sent every curl away
  from the servers the script had started.
- A value outside 1-65535 and two identical ports are refused up front,
  with the variable named.
- ci.test.js gives every case free ports of its own (freePorts() keeps
  its probes open together so they are distinct), and no case listens on
  3099 or 3098 any more. New cases: two passes at once, the four
  refusals, source pins on the defaults and on the derived URLs, and a
  first-try connection check that pins the `wait` of the trap, which the
  fixed ports used to pin by accident.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:41:37 -04:00
le king fu
7427577053 test(ci): pin the non-HTTP occupant and the wait deadline of test-curl.sh (#41)
All checks were successful
CI / verification (pull_request) Successful in 27s
CI / verification (push) Successful in 25s
Second review of PR #45, non-blocking: two behaviours were right by hand
and held by no test.

- a listener on 3099 that closes at once, then one that accepts and stalls:
  the port rule is "anything but connection refused", not "an HTTP answer"
- a server that stays alive and never listens: the wait ends on its
  deadline, with a message and exit 1

Also moves the port check above the "Fixtures" comment it had split from
its mkdir lines, and counts four protections in CLAUDE.md, not three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:18:25 -04:00
le king fu
193635a68e fix(test-curl): refuse a port already taken before starting (#41)
All checks were successful
CI / verification (pull_request) Successful in 21s
Review of PR #45: wait_for did not keep a neighbour out. With a listener
already on 3099, the server started by the script only dies of EADDRINUSE
after the wait loop has reached the neighbour, and the cases then ran
against it.

- require_free checks both ports before anything starts: free means
  "connection refused" (curl exit 7), as in scripts/boot-from-copy.sh
- wait_for runs on a clock deadline, and checks the process again after a
  successful probe
- the EXIT trap waits for the servers it killed, so a run started right
  after finds the two ports free

ci.test.js adds a real neighbour on each port, and bounds the two
failed-start cases to 3 s: a message that does not stop the run is now red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:51:42 -04:00
le king fu
096f95da05 fix(test-curl): keep the socket pass off the environment (#41)
All checks were successful
CI / verification (pull_request) Successful in 20s
Behind a proxy from the environment, test-curl.sh sent its 18 curl lines to
the proxy instead of the servers it boots: with a dead proxy it exited 7
after 11 s without a line of output.

- export NO_PROXY and no_proxy, as scripts/boot-from-copy.sh does
- unset FORCE_COLOR: node colours a number even into a pipe, and 8 cases
  compared "3" against a coloured "3"
- wait_for checks the server process before the port and stops the run with
  a message when a server died or never answered
- the EXIT trap guards each kill: under set -e, a failed kill on the first
  server ended the trap and left the second one running on 3098

ci.test.js runs the whole script three times: behind a fake proxy that
answers everything, and with a fake node that fails for each port in turn.

Fixes #41

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:13:18 -04:00
le king fu
7c14e581cb test(defenseurs): cover GET /defenseurs/state in the socket-level pass
Four cases on top of the 41 from #26: the file served byte for byte
(non-canonical fixture, compared with cmp so the trailing newline counts),
no_data when absent, 401 without a token and 401 with the ingest token.

Refs #27

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:42:56 -04:00
le king fu
50a829beb3 test(hosts): extend the socket-level pass to the two workstation routes
test-curl.sh predates vitest and still claimed to be the authoritative suite
for one endpoint. It is now the socket-level pass, and its header says so:
vitest covers the logic, this script covers what only a real client and a
real TCP connection can show.

Twenty-four cases for POST /hosts/<id> and GET /hosts, chosen for what they
prove rather than for coverage: the 413 answered before the connection is
cut, the two tokens refusing each other's routes in both directions, 401
landing ahead of 403 so an unauthenticated caller cannot probe the allowlist,
the route regex turning a malformed id and a traversal attempt into 404
before any handler runs, and the ingest-persist-read round trip closing on a
GET that shows the pushed hostname online.

Two cases needed care to be worth anything:

The hostile payload splices "__proto__" into the JSON as text. Written as
__proto__: in an object literal it would set the prototype and JSON.stringify
would emit nothing, leaving the case asserting against a payload that never
carried the key. It now checks the response and the persisted file, whose key
set must be exactly the whitelist.

The 413 case runs under set -e while the server destroys the socket right
after flushing the response, so curl can exit 55/56 having already read the
status line. Every probe goes through an http_code helper that absorbs that;
a curl which truly got nothing reports 000, which fails the case rather than
passing it quietly. -H "Expect:" also suppresses the 100-continue handshake,
which otherwise changes which side notices the reset first.

The fail-closed 503 needs a server booted without HOSTS_INGEST_TOKEN, so the
script now runs a second instance on 3098 for it, and confirms reads still
answer 200 there — the two tokens are independent, including in absence.

The EXIT trap dereferenced an unset SERVER_PID under set -u, which made it
error out instead of cleaning up when anything failed before the boot. Both
PIDs are initialised and the kills guarded.

CLAUDE.md: the vitest count was stale (244 -> 251, auth.test.js 19 -> 26) and
test-curl.sh was undocumented.

Refs #16

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:18:53 -04:00
le king fu
2e756557ff feat(reports): scan archive/ subdir as fallback to handle post-07:30 UTC window
Sergent renameSync() rotates reports/ -> reports/archive/ at 07:30 UTC daily,
so for ~22h per day the only copy of a fresh scan lives in archive/. The
handler now scans both directories and concatenates with top-level priority
on filename collision. archive/ missing is a silent skip.

Tests : 17/17 in test-curl.sh (11 existing + 6 new for archive coverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:53:14 -04:00
le king fu
6eda076a25 feat(reports): add GET /reports/scans endpoint for defenseur-auto
Replaces the SSH/rsync canal between Max's workstation cron and the VPS
for fetching defenseur scan reports. The defenseur-auto orchestrator now
pulls reports/defenseur-X_<date>*.json over HTTPS, reusing HEALTH_TOKEN.

The handler mirrors the style of index.js (HTTP native, no framework),
includes the same isScanReport guard as defenseurs/src/report.ts (filters
out defenseur-auto_*.json run reports), and validates the date param
against /^\d{4}-\d{2}-\d{2}$/ to short-circuit path traversal before any
filesystem access.

Validated by test-curl.sh — 11 cases covering auth, validation, date
filter, isScanReport filter, sort order, GET-only and 404 paths.

Spike: ~/claude-code/.spikes/archived/endpoint-reports-sur-vps-health-api-pour/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:50:29 -04:00