feat(reports): scan archive/ subdir as fallback to handle post-07:30 UTC window #8

Merged
maximus merged 1 commit from feat/reports-scans-archive-fallback into main 2026-05-11 00:44:26 +00:00
Owner

Summary

Extends GET /reports/scans to scan ${REPORTS_DIR}/archive/ as automatic fallback.

Bug constate en prod : le handler ne lit que REPORTS_DIR top-level. Or cote VPS, le Sergent fait renameSync(reports/X.json, reports/archive/X.json) quotidien a 07:30 UTC (defenseurs/src/sergent.ts:680). Donc :

  • 05:40-07:30 UTC : reports DANS reports/ direct (~2h window)
  • 07:30 -> 05:00 J+1 : reports UNIQUEMENT dans reports/archive/ (~22h/jour)

Un run manuel de defenseur-auto apres 07:30 UTC trouvait count=0. Le cron a 06:00 UTC marchait par chance.

Initial endpoint introduced in #6.

Implementation

  • Refactor : extract collectScanReportsFromDir(dir, date) (returns Map<filename, report> to enable filename-keyed dedupe).
  • readScanReportsForDate(date) calls it twice (top-level + archive), merges with top-level priority on filename collision (defensive — top-level is the more recent copy by definition), then sorts asc by timestamp (unchanged contract).
  • archive/ missing -> silent skip (existsSync guard, like REPORTS_DIR).
  • No new query param, no API surface change. Caller code is untouched.

Conserve : auth bearer, regex date validation, isScanReport filter, response shape { date, count, reports: Report[] }.

Test coverage

test-curl.sh extended : 17/17 cases pass locally (11 existing + 6 new).

New cases :

  • archive-only date 2026-05-04 -> count=1 (the 22h window)
  • archive report agent matches (right report returned)
  • top-level priority over archive (no STALE) (dedupe regression)
  • no dedupe duplication on 2026-05-07 (count stays at 3)
  • missing archive/ -> still count=3 from top-level (silent skip)
  • missing archive/ + archive-only date -> count=0 (silent skip end-to-end)

Coolify changes (manual, post-merge)

  1. Modifier custom_docker_run_options via UI ou API : -v /home/defenseur/defenseurs/reports:/data/scans:ro
  2. Add env var Coolify : REPORTS_DIR=/data/scans (is_runtime=true, is_buildtime=false)
  3. Force Deploy (pas Restart — Restart ne re-applique pas les nouvelles options Docker run)

Validation post-deploy

curl -H "Authorization: Bearer $HEALTH_TOKEN" \
  "https://health.lacompagniemaximus.com/reports/scans?date=$(date -u +%Y-%m-%d)" \
  | jq '.count'

Doit retourner > 0 (peu importe l'heure UTC).

Test plan

  • test-curl.sh passes locally (17/17)
  • Max merges + applies the 3 Coolify actions above
  • Force Deploy
  • Curl validation returns count > 0 outside the 05:40-07:30 UTC window
## Summary Extends `GET /reports/scans` to scan `${REPORTS_DIR}/archive/` as automatic fallback. **Bug constate en prod** : le handler ne lit que `REPORTS_DIR` top-level. Or cote VPS, le Sergent fait `renameSync(reports/X.json, reports/archive/X.json)` quotidien a 07:30 UTC (`defenseurs/src/sergent.ts:680`). Donc : - 05:40-07:30 UTC : reports DANS `reports/` direct (~2h window) - 07:30 -> 05:00 J+1 : reports UNIQUEMENT dans `reports/archive/` (~22h/jour) Un run manuel de defenseur-auto apres 07:30 UTC trouvait `count=0`. Le cron a 06:00 UTC marchait par chance. Initial endpoint introduced in #6. ## Implementation - Refactor : extract `collectScanReportsFromDir(dir, date)` (returns `Map<filename, report>` to enable filename-keyed dedupe). - `readScanReportsForDate(date)` calls it twice (top-level + archive), merges with **top-level priority** on filename collision (defensive — top-level is the more recent copy by definition), then sorts asc by timestamp (unchanged contract). - `archive/` missing -> silent skip (`existsSync` guard, like REPORTS_DIR). - No new query param, no API surface change. Caller code is untouched. Conserve : auth bearer, regex date validation, `isScanReport` filter, response shape `{ date, count, reports: Report[] }`. ## Test coverage `test-curl.sh` extended : 17/17 cases pass locally (11 existing + 6 new). New cases : - `archive-only date 2026-05-04 -> count=1` (the 22h window) - `archive report agent matches` (right report returned) - `top-level priority over archive (no STALE)` (dedupe regression) - `no dedupe duplication on 2026-05-07` (count stays at 3) - `missing archive/ -> still count=3 from top-level` (silent skip) - `missing archive/ + archive-only date -> count=0` (silent skip end-to-end) ## Coolify changes (manual, post-merge) 1. Modifier `custom_docker_run_options` via UI ou API : `-v /home/defenseur/defenseurs/reports:/data/scans:ro` 2. Add env var Coolify : `REPORTS_DIR=/data/scans` (`is_runtime=true`, `is_buildtime=false`) 3. **Force Deploy** (pas Restart — Restart ne re-applique pas les nouvelles options Docker run) ## Validation post-deploy ```bash curl -H "Authorization: Bearer $HEALTH_TOKEN" \ "https://health.lacompagniemaximus.com/reports/scans?date=$(date -u +%Y-%m-%d)" \ | jq '.count' ``` Doit retourner > 0 (peu importe l'heure UTC). ## Test plan - [x] `test-curl.sh` passes locally (17/17) - [ ] Max merges + applies the 3 Coolify actions above - [ ] Force Deploy - [ ] Curl validation returns count > 0 outside the 05:40-07:30 UTC window
maximus added 1 commit 2026-05-10 20:54:35 +00:00
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>
maximus merged commit ac21fd7f4b into main 2026-05-11 00:44:26 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#8
No description provided.