GPS fuel snapshots — Phase 1 (Lane B) import + kill switch
Operational runbook for the append-only p24_gps_fuel_snapshots branch added to the
atrax-kravag-scheduled-fleet-updates n8n workflow (Lane B / Phase 1 of #6194). This closes the
“no history” gap: p24_gps_current_state is UPSERT-only, so nothing downstream (trip-builder,
norm-compute, anomaly-daily — Lane D, PR #6274) has a time series to compute from until this branch
is live.
Re-targeted in #6548. The branch was originally designed against
fleet-update-v2-batch(AJ1px9uHIfbsriof, PR #6275), but that workflow is orphaned (active=false, 0 executions — nothing calls it). The real, live GPS producer isatrax-kravag-scheduled-fleet-updates(workflow idCCx9UMdphmGficDX, 57 → 59 nodes, active, every 3 minutes), whoseupsert_gps_current_statenode is the actual writer ofp24_gps_current_state. The snapshot branch now hangs off that workflow’s GPS path.fleet-update-v2-batchis left untouched (no activation, no deletion — that is a separate cleanup decision, out of scope for #6548).
The nightly jobs that consume this table live in a separate PR (#6274) — this doc covers only
producing the raw snapshots. Once #6274 merges, its docs/atrax-route-norm-control-operations.md
is the parent runbook; this file is the Phase-1/import half.
⚠️ Live is source of truth — never a repo-only edit (backup-clobber)
The daily n8n-backup.yml GH Action (cron 0 4 * * *) exports live n8n → repo, treating live
as authoritative. A repo-only edit (JSON committed but never imported to live) is silently
overwritten within 24 h — this already happened once to PR #6275’s nodes. Any change to this
workflow must be imported to the live bms-4 n8n instance in the same work session as the merge — do
not leave a merged-but-not-imported gap overnight. Always pull the current live definition via the
n8n API before editing the repo copy, so you are not reconciling against a stale export.
What was added (in the workflow JSON)
Two nodes on n8n-workflows/atrax_kravag-scheduled-fleet-updates_CCx9UMdphmGficDX.json, wired as a
parallel branch off aggregate_gps_upsert — alongside the existing upsert_gps_current_state,
never in series with it. aggregate_gps_upsert (an Aggregate node, aggregateAllItemData) emits a
single item whose $json.data is the array of GPS rows; both upsert_gps_current_state and the new
kill switch read from it independently.
snapshot_killswitch(IF node) — gates the branch on the env varP24_GPS_SNAPSHOTS_ENABLED. Writes only when it equals the stringtrue. Any other value, or unset, routes to the (empty) false branch and writes nothing.insert_gps_fuel_snapshots(HTTP Request) —POST /rest/v1/p24_gps_fuel_snapshots(PostgREST,Prefer: return=minimal), reshapingaggregate_gps_upsert’s$json.datainto snapshot rows. Uses this workflow’s own Supabase credentialsupabaseKapbaraAuthBearer(idcMpMbkDFAJzC3O4f) — the same one bound onupsert_gps_current_stateand the other Supabase HTTP nodes here. (Note: this is a different credential id than PR #6275’sZNuDXmNs3nT58L6p/supabase-service-role-key, which belonged to thefleet-update-v2-batchworkflow — n8n credential ids are per-credential-object; always reuse whatever the sibling nodes in the same workflow already use.)
Design constraints baked into the node (do not “fix” these — they are deliberate, per #6194 §5a-ops):
- No
retryOnFail. The table is append-only with no unique constraint by design; an automatic retry after a partial-success/timeout would silently duplicate the whole batch and corrupt every downstream median/MAD computation. A failed run simply skips that 3-min snapshot. onError: continueRegularOutput. The branch swallows its own errors so a snapshot hiccup can never fail the execution, block the existing (working)upsert_gps_current_stateupsert, or fire the workflow’s sharederrorWorkflow(gG3SsUPtcqgtsbje) alert path — a snapshot gap is a lower severity than a real GPS-sync outage and must not page on-call.
Handoff 1 (sys-admin / infra-task) — import to live bms-4
A dev-issue worker cannot import to live n8n; a sys-admin/infra-task worker on bms-4 performs
the steps below. n8n runs locally on bms-4 (http://localhost:5678, queue mode — main + 3 workers);
the API key is BMS4_N8N_API_KEY in /opt/p24-infra/bms-4/.env.
- Pull the live definition first and edit from it (backup-clobber guard, above):
curl -H "X-N8N-API-KEY: $KEY" http://localhost:5678/api/v1/workflows/CCx9UMdphmGficDX. - Import to live. The n8n public-API
PUT /workflows/{id}is lossy onsettings(it rejectsmaxConcurrency,binaryMode,callerPolicy,availableInMCPand would silently dropmaxConcurrency: 1). Use the n8n CLI import instead, which preserves the full settings and the versioning layer:docker cp modified-full.json bms-4-n8n-1:/tmp/wf.json docker exec bms-4-n8n-1 n8n import:workflow --input=/tmp/wf.json --activeState=fromJson--activeState=fromJsonrespects the JSONactive: truefield (the default deactivates). Confirm the two new nodes appear,activeis stilltrue, andsettings.maxConcurrencyis still1. - Enable the kill switch. The flag is wired through
bms-4/docker-compose.ymlon all four n8n services (n8n,n8n-worker-1..3) asP24_GPS_SNAPSHOTS_ENABLED=${P24_GPS_SNAPSHOTS_ENABLED:-true}(#6548) — i.e. enabled by default, in git. It is deliberately not inbms-4/.env: that file is regenerated from SOPS bysecrets-sync.ymlon every run, so a manual non-secret line there would be clobbered. The compose default is the durable home. Recreate the n8n containers so the schedule trigger re-registers against the new definition and the workers pick up the env var (queue mode →$envis evaluated on the worker that runs the node, so every worker needs it):cd /opt/p24-infra/bms-4 && docker compose up -d n8n n8n-worker-1 n8n-worker-2 n8n-worker-3N8N_BLOCK_ENV_ACCESS_IN_NODE=falseis already set on every n8n service, so$envaccess in the IF node is not blocked. - Confirm the
supabaseKapbaraAuthBearercredential (idcMpMbkDFAJzC3O4f) is bound on the new HTTP node after import.
Verification (Phase 1)
After import + enable, confirm rows accumulate at roughly one per active vehicle per 3-minute run:
SELECT count(*), min(captured_at), max(captured_at),
count(DISTINCT atrax_car_id) AS vehicles
FROM public.p24_gps_fuel_snapshots
WHERE captured_at > now() - interval '1 hour';Expect ~20 snapshots/hour per active vehicle (every 3 min). If vehicles is ~0 while the workflow is
executing, the kill switch is still closed (env var not true, or the n8n containers were not
recreated after setting it).
Kill switch — incident response (NOT a scheduled handoff)
Overriding P24_GPS_SNAPSHOTS_ENABLED to anything other than true — either by flipping the
:-true default to :-false in bms-4/docker-compose.yml, or by exporting
P24_GPS_SNAPSHOTS_ENABLED=false into the compose process env — and recreating the n8n containers
stops all new snapshot writes without touching the rest of the workflow. This is an
incident-response action owned by the same sys-admin on-call rotation that
handles other live-production n8n issues — not a scheduled task. Reasons to flip it off: unbounded
table growth ahead of the 35-day retention job landing (PR #6274 Lane E), or PostgREST write pressure
on the shared Supabase project.
Retention (35-day prune + Parquet archival) is Lane E in PR #6274 — until that is scheduled, watch table size manually; at 3-min cadence ~263 vehicles × 20/hour × 24 h ≈ 126k rows/day.