Atrax tachograph driving-time compliance — operations
Daily compliance alert on EU driving/rest-time regulation breaches (tachograph), for the Ecotrans fleet. Implemented in #7045.
This is not statistical analytics. Unlike the sibling Atrax jobs (atrax_route_anomaly_daily,
atrax_ecodriving_anomaly_daily — median/MAD, modified-Z, per-route norms), this job reads the
driving/rest counters Atrax has already computed from the tachograph (used-allowance counts and
driving/rest minutes) and applies simple EU 561/2006 regulatory threshold rules — a breach is only an
exceeded allowance or limit, never mere usage (#7458). There is no model, no norm, no distribution to study. It IS registered in
dev_r_ai_systems (tachograph-compliance, #7153) because it is inherently per-driver — see
§EU AI Act.
Delivery changed in #7153 (plan-7107 PR-1). The old bespoke delivery — one aggregate Discord embed + one GitHub
complianceissue — is REMOVED. NEW violations now escalate into the sharedp24_fleet_anomaly_escalationstable (with a shared Discord digest); fleet managers triage in the et-operational-platform escalation panel. Every escalated tachograph row carriesrequires_human_review=true. Sections below describing the Discord-embed/GitHub-issue delivery are historical. Seedocs/atrax-fleet-anomaly-escalation-operations.md.
Why it matters
Driving/rest-time breaches are a real legal risk for the fleet: administrative fines at a roadside or ITD inspection. Atrax already surfaces the breach counters per driver; nothing consumed them until this alert.
Data flow
Atrax /api/rtdrivers
→ n8n atrax-drivers-daily-snapshot (existing, live)
→ p24_atrax_drivers_daily (one row per driver per stat_date, Europe/Berlin)
→ atrax_tachograph_compliance_daily (nightly, this job)
→ p24_tachograph_violations (persist all current violations, is_new flag — audit history)
→ p24_fleet_anomaly_escalations (upsert NEW violations, #7153) + shared Discord digest
Known limitation (shared, #7153): a rerun that recomputes a day as no longer in violation does not delete an existing open escalation row — it must be dismissed by a human in the panel.
Source columns consumed (p24_atrax_drivers_daily)
driver_id, stat_date, first_name, last_name, this_week_driving_min,
this_week_remaining_min, this_week_limit_min (default 3360 = 56 h), last_2w_driving_min,
last_2w_remaining_min, weekly_rest_min, extended_driving_periods (default 0),
shortened_rest_periods (default 0). Idempotency key of the source table is (driver_id, stat_date).
vehicle_id / occurred_at on the escalation row (#7168, PR-2)
p24_atrax_drivers_daily is keyed only by (driver_id, stat_date), so it carries neither the vehicle
nor the exact time of a violation. Etap 1 (verified at code level, no live call needed): the Atrax
/api/rtdrivers payload has no per-violation vehicle or timestamp field either — the n8n
map_drivers node (atrax-drivers-daily-snapshot) maps every field the payload exposes and none is a
vehicle; /api/rtdrivers is a one-row-per-driver realtime snapshot of daily/weekly counters, not a
per-violation event log. So vehicle_id/occurred_at cannot come directly from the tachograph feed.
Fallback — inferred from p24_ride_events (name join, low confidence): for each NEW violation the
job pulls the driver’s DRIVING segments for the same Berlin day from p24_ride_events, matches the
driver by name (first_name + last_name ↔ driver_name, case- and whitespace-folded — not
transliterated), and takes the representative segment (longest duration, earliest event_start
as tie-break) as the vehicle_id (atrax_car_id) and occurred_at (event_start).
Because there is no stable driver id shared between the two feeds, and driver_name in
p24_ride_events may differ by Polish-character encoding (unverified), the name join is uncertain
and every result is flagged in the escalation row’s detail jsonb:
detail->>'vehicle_id_confidence' | vehicle_id / occurred_at | meaning |
|---|---|---|
inferred | filled from the representative segment | name join matched — treat as a hint, not ground truth |
null | NULL | no matching DRIVING segment that day — left NULL rather than guessed |
direct | (reserved) | reserved for a future real per-violation Atrax field; not emitted today |
detail->>'vehicle_inference_note' carries a human-readable explanation of which case applied. The
run summary reports escalations_vehicle_inferred (count of rows that got an inferred vehicle). The
ride-events fetch runs inside the escalation step’s best-effort try, so a p24_ride_events failure
never blocks the p24_tachograph_violations audit write.
Note on
dedup_key: the escalation identity includesvehicle_id, so an inferred vehicle becomes part of the dedup key for a tachograph row. Ride events for a past day are stable, so reruns are deterministic; a genuine change in the underlyingp24_ride_eventsdata could produce a new escalation row rather than updating the old one (acceptable — the freeze trigger still protects any row a human has begun triaging).
Rules
violation_type | condition | severity | meaning |
|---|---|---|---|
extended_driving | extended_driving_periods > EXTENDED_DRIVING_MAX_ALLOWED (2) | violation | used more than the 2 extended daily-driving periods/week EU 561/2006 allows |
shortened_rest | shortened_rest_periods > SHORTENED_REST_MAX_ALLOWED (3) | violation | used more than the 3 reduced daily-rest periods allowed between weekly rests |
weekly_rest_short | weekly_rest_min < WEEKLY_REST_MIN_REQUIRED_MIN (1440) | violation | weekly rest below the 24 h reduced-weekly-rest minimum |
weekly_rest_reduced | WEEKLY_REST_MIN_REQUIRED_MIN <= weekly_rest_min < WEEKLY_REST_REGULAR_MIN (1440–2699) | warning | reduced weekly rest (legal, but requires later compensation — below the 45 h regular rest). Not escalated (#7727) — persisted as a warning counter only, see the cutover note below |
weekly_limit_exceeded | this_week_remaining_min <= 0 | violation | weekly driving limit exceeded (56 h) |
weekly_limit_near | 0 < this_week_remaining_min <= WEEKLY_REMAINING_WARN_MIN (60) | warning | approaching the weekly limit |
fortnight_limit_exceeded | last_2w_remaining_min <= 0 | violation | two-week driving limit exceeded (90 h) |
fortnight_limit_near | 0 < last_2w_remaining_min <= FORTNIGHT_REMAINING_WARN_MIN (120) | warning | approaching the two-week limit |
*_exceeded/*_shorttakes precedence over*_near/*_reducedfor the same counter — a driver over the weekly limit produces only theweekly_limit_exceededrow, and a driver below the 24 h rest minimum produces only theweekly_rest_shortrow, never both.- Allowances are flagged only when EXCEEDED, not merely used (#7458).
extended_driving_periods(0–2/week) andshortened_rest_periods(0–3 between weekly rests) are counts of EU 561/2006 allowances a driver has used; using them is legal, so a violation fires only above the allowance (> 2/> 3). Likewise a weekly rest of 24 h–<45 h is a legal reduced rest (warning, needs compensation); only a rest below 24 h is a breach. - The allowance limits + warning bands are tunable module constants in
atrax_tachograph_compliance_daily.py:EXTENDED_DRIVING_MAX_ALLOWED=2,SHORTENED_REST_MAX_ALLOWED=3,WEEKLY_REST_MIN_REQUIRED_MIN=1440,WEEKLY_REST_REGULAR_MIN=2700,WEEKLY_REMAINING_WARN_MIN=60,FORTNIGHT_REMAINING_WARN_MIN=120(minutes). The allowance counts and the 24 h / 45 h / 56 h / 90 h figures are the EU legal limits; the*_neardriving bands are round, human-tunable starting values (same convention as the #6194mad_floor/ severity bands). Recalibrate the*_nearbands against the observed*_remaining_mindistribution once there is enough live data (a sys-admin live-data pass).
Rollup — alert only on NEW violations
Mirrors weekly-supabase-advisor-audit / nightly-devops-triage: diff the target day against the
previous day’s snapshot and alert only on new violations, as ONE aggregate report grouped by
driver — never one alert per driver.
- Counter violations (
extended_driving,shortened_rest): new when the counter increased vs the previous day (absent the previous day ⇒ baseline 0). An ongoing over-allowance count is not re-alerted; every newly-added period beyond the allowance is. - Limit violations (weekly / fortnight): escalation-aware.
exceededis new unless the driver was alreadyexceededyesterday;nearis new unless the driver was alreadynearorexceededyesterday (crossing into a worse band re-alerts). - Weekly-rest violations (
weekly_rest_short,weekly_rest_reduced): escalation-aware the same way, with inverted bands.weekly_rest_shortis new unless the driver was alreadyshortyesterday;weekly_rest_reducedis new only when the driver wasok(full regular rest) yesterday. - All current-day violations (new and ongoing) are persisted to
p24_tachograph_violations— the table is both the diff source and the audit history. - Zero new violations ⇒ no Discord message, no GitHub issue, exit 0.
Tables
p24_tachograph_violations
One row per (driver_id, stat_date, violation_type) (unique index
ux_tachograph_violations_driver_date_type). Columns: the three key columns plus driver_name,
severity, is_new, detail, the copied source counters (this_week_*, last_2w_*,
weekly_rest_min, extended_driving_periods, shortened_rest_periods), and detected_at. RLS:
service_role FOR ALL + grafana_readonly SELECT. Idempotent: the job does DELETE stat_date=D →
INSERT, so a same-day re-run is safe.
2026-10-05 rule/mapping cutover + invalidation of pre-cutover rows (#7727)
Before 2026-10-05 the detector + the n8n map_drivers mapping were wrong: extended_driving was
flagged with a > 0 rule on the wrong source field (used raw counts instead of …Left/limit). Every
p24_tachograph_violations row dated 2026-09-17 … 2026-10-04 (489 rows — all extended_driving,
the entire pre-cutover content) and the 50 open extended_driving escalations were therefore false.
The 2026-10-05 cutover (n8n map_drivers fix, PR #7428 / #7460, plus the exceeded-allowance rules
EXTENDED_DRIVING_MAX_ALLOWED=2 etc.) corrected the detector; from that date no extended_driving
rows are produced.
Migration 20261010094733_purge_false_tachograph_violations_7727.sql cleaned this up reversibly:
- The 489 false
extended_drivingrows were copied intop24_tachograph_violations_archive(full row shape +original_id,invalidated_at,invalidation_reason; same RLS as the live table) and then deleted fromp24_tachograph_violations. The pop-up //api/reports/ driver-compliance-violationsread the live table, so they no longer show the false history; the archive preserves it for audit and makes the move undoable. - The 50 open pre-cutover
extended_drivingescalations and the 54 openweekly_rest_reducedescalations were soft-dismissed (status='dismissed',resolution_notereferencing #7727) — reversible by setting them back toopen. weekly_rest_reducedno longer escalates (ESCALATION_SUPPRESSED_TYPESin the ETL): a reduced weekly rest (24–45 h) is legal under EU 561/2006, a warning band not a breach, so it is kept as a warning counter inp24_tachograph_violationsbut never raised into the human-review escalation panel.weekly_rest_short(<24 h, a real breach) and all*_exceededbreaches still escalate.- Backup of the invalidated ids/counts:
scripts/nightly-etl/backups/7727-tachograph-purge-backup.json.
The nightly job (scripts/nightly-etl/atrax_tachograph_compliance_daily.py)
# default = yesterday (Europe/Berlin); --date to backfill a specific day; --dry-run writes/alerts nothing
python3 scripts/nightly-etl/atrax_tachograph_compliance_daily.py --env-file /opt/p24-infra/bms-4/.env
python3 scripts/nightly-etl/atrax_tachograph_compliance_daily.py --date 2026-09-14 --dry-run --env-file /opt/p24-infra/bms-4/.env- Sync Python, reuses
atrax_norm_common(env loading, PostgREST read/write, Europe/Berlin day helpers, and the Error Notification Standard — Discord webhook + GH issue on failure). - Credentials from env only (
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY); never logged. - Alert Discord webhook:
P24_DISCORD_COMPLIANCE_WEBHOOK_URLif set, otherwise falls back toP24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL(the only Discord webhook currently deployed to bms-4). A dedicated compliance channel is an optional future secret-manager task. - Aggregate GitHub issue: labelled
compliance, title[Compliance] Tachograph driving-time violations — <date> (…). Idempotent per day — it will not re-file if an open issue with that date already exists. - Exit code: 0 on success, 1 on failure (failure also fires the Discord + GH
bugerror notification).
Cron wiring (live on bms-4)
Scheduled and live on bms-4 since #7045 (infra-task). The essential constraint is ordering: the
job reads p24_atrax_drivers_daily for yesterday (Berlin), and those rows are written by the n8n
atrax-drivers-daily-snapshot workflow at 04:00 UTC (0 6 Berlin). So the job must run after
04:00 UTC — the existing norm-control ETL wrapper (run-atrax-norm-nightly.sh, 02:15 UTC, under
the claude-runner crontab) runs before the snapshot and therefore cannot host this job.
It runs from the claude-runner-2 user crontab at 04:45 UTC (06:45 Berlin) — 45 min after the
snapshot, verified to have complete rows by then:
# claude-runner-2 crontab (crontab -l -u claude-runner-2) — 04:45 UTC, after the 04:00 UTC
# atrax-drivers-daily-snapshot. python3.12 pinned (3.10 mis-parses Atrax timestamptz).
45 4 * * * /usr/bin/python3.12 /opt/p24-infra/scripts/nightly-etl/atrax_tachograph_compliance_daily.py --env-file /opt/p24-infra/bms-4/.env >> /var/log/p24-infra-workers/atrax-tachograph-compliance.log 2>&1- Why
claude-runner-2, notclaude-runner: the infra-task worker that wired this runs underclaude-runner-2and cannot edit another user’s crontab without root;claude-runner-2already hosts infra crons (check-sub-usage.py) on this host, so this is consistent. Consolidating it underclaude-runneralongside the norm-control wrapper is an optional future cleanup — it needs a root/claude-runnercrontab edit and a later run slot (the wrapper’s own 02:15 UTC is too early for this job); functionally the current wiring is complete. - Log:
/var/log/p24-infra-workers/atrax-tachograph-compliance.log. - Manual / backfill: run any time with
--env-fileas shown above (add--date YYYY-MM-DDto target a specific Berlin day,--dry-runto compute without writing/alerting). - Empty-day safety: if the snapshot did not write the target day’s rows (upstream failure), the job reads 0 driver rows → 0 violations → no alert, exit 0. It never emits false “resolved” alerts.
EU AI Act
Registered in dev_r_ai_systems as tachograph-compliance (#7153, plan-7107 §4). The #7045 scope
had deliberately deferred this (“no ML, no statistical scoring, no performance ranking”). The plan-7107
audit revised that call: although the logic is rule-based threshold checks over counters Atrax already
computed — no ML, no median/MAD — the job is inherently per-driver (it evaluates a named driver’s
regulatory compliance), which is adjacent to Annex III.4(a). It is registered at limited risk,
not high-risk, because it is advisory / investigatory only: a human verifies every finding against
the tachograph record and no output is wired into any automated disciplinary / ranking / allocation
system. Every escalation row it creates carries requires_human_review=true, and the panel blocks
bulk / automatic resolution on those rows. Wiring this output into any automated employment decision
would require a fresh conformity assessment first — the same guardrail the ecodriving and route-norm
jobs carry.
Failure handling
Any unhandled exception → atrax_norm_common.notify_failure: a RED ERROR Discord embed
(P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL) + a GitHub issue labelled bug. Per-day idempotency
means a failed run can simply be re-run once the cause is fixed.