Atrax exploitation puller — discovery & build handoff (#6197 / #6710)

Daily puller for Atrax GET /api/exploitation, populating p24_gps_daily_stats (core) + p24_gps_daily_stats_native (Phase-7 vendor fraud/consumption signal for #6138 / #6194).

Status: DISCOVERY RESOLVED + puller runnable (#6710, 2026-09-01). The “genuine unknown” in §2 below was a false premise: Atrax’s X-Request-Hash/X-Timestamp signing was already reverse-engineered and verified end-to-end without a browser by #5921 (2026-08-12, docs/playbooks/atrax-request-signing.md), and the live atrax-daily-stats-collector n8n workflow has used that raw signed path in production since #6204. A live signed capture (#6710) confirmed the full request contract and response shape, so the puller now issues the raw signed HTTP request directly — no Playwright, no login-form automation, no UI navigation. loginToAtrax() / captureExploitationJson() (which threw DiscoveryRequiredError) are replaced by getAtraxToken()

  • signedHeaders() + fetchExploitation() in scripts/atrax-playwright-puller.js. Verified live against real vehicles on bms-4 (a moving unit returned populated distance/consumption and a real native frauds/total_fraud_time_s signal). Remaining deferred work: apply the #6720 native-table migration to the live DB, schedule the nightly cron, and (optionally) teach the live n8n map_stats node to also emit native rows — see §4 Ownership.

(The file keeps its historical atrax-playwright-puller.js name — referenced by the #6720 migration + operator docs — but no longer uses a browser.)


1. What is already DONE (shipped in #6197)

ArtefactPathNotes
Pure transform (map_stats port)scripts/lib/atrax-exploitation-transform.jsRaw /api/exploitation → p24_gps_daily_stats row + richer native object. **Corrected in 6710 to faithfully mirror the live map_stats node: summary-based sources (the #6197 port read non-existent top-level keys), no DB-generated columns emitted, correct native field names. Pure, no I/O.
Unit testsscripts/tests/atrax-exploitation-transform.test.mjsnode --test (zero deps). 16 cases against the real response shape; run: node --test scripts/tests/atrax-exploitation-transform.test.mjs.
Puller wiringscripts/atrax-playwright-puller.jsenv load, validateEnv (names only), active-vehicle selection, per-vehicle error isolation, 1 s vendor-politeness delay, transform call, upsert, zero-success sanity check, Discord + GH-issue error notification. #6710: the two browser stubs replaced by the raw signed getAtraxToken/fetchExploitation path (see §2).

2. Discovery — RESOLVED (no browser needed) (#6710, 2026-09-01)

The original §2 asked for browser login-form + UI-navigation capture, on the belief that only a live browser could produce Atrax’s signing headers. That belief was already wrong when #6197 was written: #5921 (2026-08-12) reverse-engineered the signature from Atrax’s own JS bundle and proved it end-to-end with our service-account OAuth token — no browser — and the live n8n collector has run it in production since #6204. #6710 re-verified with a live signed capture on bms-4. So there is no Playwright discovery to do; the puller uses the same raw signed path the n8n workflow uses.

Confirmed request contract (live, #6710)

  • Token: POST /oauth/token, form body grant_type=password + username/password/scope/ client_id/client_secret (the last two are the public literals jatrax/passid), header Authorization: <ATRAX_AUTH_STRING> (already carries its Basic prefix — do NOT re-prefix it).
  • Exploitation: GET /api/exploitation?vehicleId=<atrax_car_id>&start=<epoch_s>&stop=<epoch_s>&useGps=false, headers Authorization: Bearer <token>, tronik-webappinstanceid: <ATRAX_WEBAPP_INSTANCE_ID>, X-Timestamp (ms), X-Request-Hash = SHA256(X-Timestamp + token) (fresh per request).
  • Window: the 02:00-UTC op-day [D 02:00 UTC, D+1 02:00 UTC) (computeDayWindow, #6204 Bug 2) — NOT a CET-midnight window.
  • An invalid/placeholder atrax_car_id (e.g. the TEST-001 row in p24_gps_current_state) returns 400 "Błędny identyfikator pojazdu" — handled by per-vehicle isolation.

Confirmed response shape (live, #6710)

  • Top level keys are ONLY summary, lines, details, daily, name, color. There are no top-level metric keys — every metric is under summary.*. daily[] holds per-day entries with the same field names as summary.
  • The native fraud/consumption fields all live under summary.*: losses, lossFuel, frauds, totalFraudTime, standardConsumption, pouredFuel, and the refuel COUNT as refueling (singular, populated) — refuelings (plural) is null (extractNative coalesces plural→singular).
  • /api/exploitation carries NO fuel counter (fuelCounter* does not exist anywhere in the body). The 100%-null fuel_counter_*/fuel_used_liters columns are therefore a data-availability limit, not a bug — correcting the odometer/fuel-counter-branch misdiagnosis in #6710’s design comment. Available fuel data is firstFuelLevel/lastFuelLevel (level %) and fuelConsumption/fuelConsumptionTotal (litres); the DB’s fuel_level_avg/min/max columns have no matching source and stay null. Distance comes from summary.firstOdometer/lastOdometer (km → m).

Known request contract (from the atrax-daily-stats-collector n8n workflow — do NOT re-derive)

  • Endpoint: GET https://tronik.atrax4.com/api/exploitation?vehicleId=<atrax_car_id>&start=<epoch_s>&stop=<epoch_s>&useGps=false
  • Auth (browser session): the app supplies Authorization: Bearer <token> plus the browser-computed x-request-hash / x-timestamp signing headers — the whole reason for the browser approach (the raw HTTP node cannot compute them → 401, #5921).
  • OAuth token (if ever needed directly): POST /oauth/token, grant_type=password, with username/password/scope/client_id/client_secret from the ATRAX_* env keys.
  • Day window: computeDayWindow(statDate, tzOffsetHours) in the transform module reproduces the legacy CET window (local [00:00, 23:59:59] as epoch seconds). Confirm CET vs CEST at run time.

3. Transform & write contract

  • transformExploitation(raw, { atraxCarId, carId, statDate }) → { core, native }.
  • core upserts into p24_gps_daily_stats — POST .../p24_gps_daily_stats?on_conflict=atrax_car_id,stat_date, Prefer: resolution=merge-duplicates (same pattern the n8n upsert_stats node used; the table exists and is empty today — this puller is what finally populates it).
  • native upserts into p24_gps_daily_stats_native — a new staging table for the richer fields (standardConsumption, losses/lossFuel, frauds/totalFraudTime, refuelings/pouredFuel) that feed #6194’s Phase 7 native fraud signal.
    • **Created by 6720 as a new table (chosen over extending p24_gps_daily_stats — keeps the fraud/native signal isolated for #6194 to reconcile against, and avoids an ALTER on a table with no in-repo migration): supabase/migrations/20260901103919_p24_gps_daily_stats_native_6710.sql, columns atrax_car_id, stat_date (unique / on_conflict) + the seven native columns, RLS matching the sibling p24_gps_* tables. Key nesting confirmed against the #6710 live capture (all under summary.*; refuel count is refueling singular). ⚠️ The migration is merged but not yet applied to the live DB (PGRST205) — the puller’s native upsert 404s until CI applies it.
  • Active-vehicle filter: getActiveVehicles() reads p24_gps_current_state (active=true, atrax_car_id not null) — the live interim source. When #6194’s p24_gps_trips / p24_gps_fuel_snapshots land (404 today), switch to a per-day distance/ignition-delta query on those. Never blind-loop the full ~263-vehicle roster (§1a doctrine, #6194) — costly through a browser.

4. Ownership & SLA

This crosses the dev-coder / sys-admin boundary exactly as #6194 documented:

PhaseOwnerDeliverableStatus
Foundation (transform, tests, wiring, this doc)dev-coder (#6197)merged PR✅ done
Native table p24_gps_daily_stats_native (§3)dev-coder (#6720)migration merged to main✅ merged (⚠️ NOT yet applied to live DB — PGRST205; CI apply pending)
Live discovery (§2) + raw-signed buildinfra-task on bms-4 (#6710)replace browser stubs with getAtraxToken/fetchExploitation, correct the transform, document✅ done
Apply the #6720 native migration to live DBinfra-task / CItable must exist before the puller’s native upsert stops 404’ing⏳ pending
Cron scheduling + first production runinfra-task on bms-4nightly cron (measured ~1 s/vehicle over the active fleet + politeness delay); register in dev_r_services at deploy time⏳ pending
(optional) live n8n map_stats → also emit native rowsinfra-task on bms-4teach the production nightly node to write p24_gps_daily_stats_native too, OR let this puller be the sole native populator⏳ open decision

Why the last three are still deferred (not done in #6710): applying a migration is CI’s job on merge (do not hand-run apply_migration — banned, #5653); the cron cadence + dev_r_services registration is a deploy-time decision; and editing the live n8n workflow has no repo→n8n deploy path (the JSON in n8n-workflows/ is an export/snapshot, not a source) — it is a hand-edit in the n8n UI/API against the currently-working nightly job, which deserves its own supervised change rather than a blind edit inside a light task.

Deferred compliance: dev_r_services registration is done by sys-admin at deployment time (the service is not runnable until discovery lands — registering a non-functional service now would be misleading). Not an AI/ML system → no dev_r_ai_systems entry needed (per #6197).

Error handling (already wired): Discord embed via P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL

  • a bug-labelled GH issue on fatal error or zero successful pulls. Login failures surface distinctly from per-vehicle navigation failures (the latter are isolated and never abort the run).

5. Cross-references

  • #5921 — direct-API signature still technically unsolved; this puller makes it moot in practice but does not close it. This work carries no closing keyword against that root-cause issue — the decision to downgrade/archive #5921 is left to whoever confirms this puller is live and reliable in production.
  • **#6138 / 6194 — this is their “Phase 7” richer Atrax-native source.
  • scripts/rotate/n8n-update-credential-playwright.js — the established headless-Chromium idiom on bms-4.
  • scripts/atrax-archival.py — the vendor-politeness delay + env-load conventions this puller mirrors.