Plan #7107 — Kompletny mechanizm eskalacji anomalii floty (Atrax)

Issue: radieu/p24-infra#7107 Type: New Feature (design/plan) Status: design — dla /review-plan (użytkownik zażądał 4 iteracji, nie domyślnych 3), następnie implementacja rozbita na PR-1…PR-4 + cross-repo issue w et-operational-platform. Repos: radieu/p24-infra (schemat + RLS + digest Discord + kontrakt) oraz radieu/et-operational-platform (panel UI) — wspólny projekt Supabase mwkqmgadqnkkihjdeqsi.

Ten dokument jest trwałą wersją planu z ciała issue #7107. Zakres (#0–#10) to treść JEDNEGO issue projektowego; realizacja jest podzielona na samodzielnie weryfikowalne PR-y (patrz §Sekwencja wykonania). PR persystujący ten plan nie implementuje żadnego z etapów — jest artefaktem projektowym do przeglądu.


Kontekst

W poprzednich sesjach zbudowano 4 niezależne systemy detekcji anomalii floty na danych Atrax:

SystemTabela detekcjiDzisiejsze raportowanie
Kontrola spalania + fraud (#6138/#6194/#6710)p24_route_consumption_anomalies, p24_gps_daily_stats_nativeLane E → dev_r_alert_events → alert-triage-batch → GH issue p24-infra-nc-alert
Ecodriving per-trasa (#6841/#6848/#7020)p24_ecodriving_anomaliesbrak — tylko zapis do tabeli
Vehicle health: silnik/napięcie/AdBlue (#7043/#7044/#7046)p24_vehicle_health_anomaliesbrak — tylko zapis do tabeli
Tachograf compliance (#7045)p24_tachograph_violationsbespoke Discord embed + bezpośredni gh issue create

Decyzja użytkownika (nadpisuje pierwszą wersję tego planu): żaden z 4 systemów nie eskaluje przez GitHub Issues. dev_r_alert_events/alert-triage-batch/p24-infra-nc-alert to mechanizm dla infra/ops (serwery, kontenery) — anomalie floty mają innego odbiorcę (fleet managerowie) i inny kanał: zapis do bazy + notyfikacja Discord, a rzeczywiste zarządzanie eskalacją (przypisanie, potwierdzenie, rozwiązanie) odbywa się w panelu eskalacji w et-operational-platform — osobnej aplikacji/repo, nie tutaj.

Zakres: wszystkie 4 systemy migrują do nowego mechanizmu (w tym już działający fuel/fraud — zdejmujemy go z dev_r_alert_events).

Architektura

0. BLOKUJĄCY Etap 0 — sprawdzić, czy incidents (et-operational-platform) już to robi, zanim tworzymy nową tabelę

Plan-review na żądanie użytkownika (“nie generować duplikacji względem już istniejących tabel w et-operational-platform”) wykrył: we współdzielonym projekcie Supabase (mwkqmgadqnkkihjdeqsi) istnieje tabela incidents (bez prefiksu p24_), opisana w docs/supabase-operations.md:45 jako “Business incident tracking” — własność et-operational-platform, odrębna od dev_r_incidents (to są infra-incydenty p24-infra, potwierdzone jako niezwiązane). Jej kolumny/schemat nie są nigdzie zadokumentowane w tym repo — CREATE TABLE nie istnieje w supabase/migrations/ tego repo (bo to nie jest tabela p24-infra). Nie wiadomo, czy to już jest ogólny mechanizm zgłoszeń biznesowych, który mógł (powinien?) być rozszerzony o anomalie floty, zamiast budować drugą, równoległą tabelę o podobnym celu.

To musi się rozstrzygnąć PRZED napisaniem migracji p24_fleet_anomaly_escalations — nie równolegle, nie “do zweryfikowania w designie” jak inne otwarte punkty niżej, bo zmienia całą przesłankę architektury:

  1. Sprawdzić schemat incidents (read-only information_schema query na współdzielonym projekcie). Nie wymaga nowego sekretu — uzasadnienie to docs/supabase-operations.md:31 (service_role = “Full access, bypasses RLS”) oraz linia 102 tego samego dokumentu, gdzie incidents jest już wprost na liście grantów grafana_readonly (GRANT SELECT). Sprawdzić kolumny, czy ma już coś w stylu source/type/status/assigned_to, czy et-operational-platform faktycznie pisze do niej z kodu aplikacji.
  2. Jeśli incidents jest generyczna i rozszerzalna → rozszerzyć ją (dodać nullable kolumny fleet-specific: vehicle_id, driver_id, attribution, requires_human_review, severity jeśli jej nie ma) zamiast tworzyć p24_fleet_anomaly_escalations od zera. To prawdopodobnie oznacza, że et-operational-platform już ma UI do zarządzania incydentami — panel z #7 mógłby być rozszerzeniem istniejącego ekranu.
  3. Jeśli incidents jest za wąska/niezwiązana → zostać przy nowej, dedykowanej tabeli z jawnie zapisanym uzasadnieniem.
  4. Ten check wykonuje sys-admin/infra-task (read-only, żadnych nowych credentiali) jako pierwszy krok fazy design zdalnego workera — PR-1 nie może ruszyć, zanim to się nie rozstrzygnie.

1. Nowa, wspólna tabela p24_fleet_anomaly_escalations (aktualne TYLKO jeśli Etap 0 potwierdzi, że incidents się nie nadaje)

Jedna tabela, wspólna dla wszystkich 4 systemów (dyskryminator source_system):

KolumnaTypUwagi
iduuidPK
source_systemtext'fuel_consumption' | 'ecodriving' | 'vehicle_health' | 'tachograph'
anomaly_typetextnp. ecodriving_deviation, engine_temperature_deviation, extended_driving
vehicle_idtext, nullablewymagane również dla tachografu (patrz uwaga niżej); nullable tylko gdy naprawdę nieustalalne
driver_id / driver_nametext, nullablebrak dla vehicle_health
occurred_attimestamptz, nullabledokładny czas zdarzenia (nie tylko occurred_date)
attributiontext, nullable'vehicle_route' | 'driver_pattern' — niesie flagę EU AI Act (patrz #4)
severitytextznormalizowane do 'warning' | 'critical'
observed_value, norm_median, modified_znumeric, nullabletylko systemy statystyczne
detailjsonbpełny, systemowo-specyficzny payload
occurred_datedatestat_date/trip_date źródłowej anomalii
statustext'open' | 'acknowledged' | 'resolved' | 'dismissed' — zarządzane przez ludzi w panelu, nigdy automatycznie
requires_human_reviewbooleantrue gdy attribution='driver_pattern' — patrz #4
assigned_to, resolution_note, resolved_atnullablewypełniane przez panel
created_at, updated_attimestamptz

Uwaga — tachograf potrzebuje pojazdu i konkretnego czasu, nie tylko dnia. p24_atrax_drivers_daily (dzisiejsze źródło tachografu) jest kluczowane tylko (driver_id, stat_date) — nie ma tam vehicle_id ani godziny, tylko dobowe liczniki. Wiersz eskalacji tachografu musi zawsze nieść, w jakim pojeździe i o której godzinie doszło do naruszenia:

  • Etap 1 (przed implementacją migracji tachografu): sprawdzić surowy payload /api/rtdrivers — czy Atrax w ogóle podaje vehicleId/timestamp per naruszenie, czego dziś nie mapujemy do p24_atrax_drivers_daily.
  • Fallback, jeśli Atrax nie daje tego per-naruszenie: dociągnąć vehicle_id/occurred_at przez join driver_id/first_name+last_name z p24_atrax_drivers_daily do driver_name w p24_ride_events dla tego samego stat_date, biorąc np. najdłuższy/pierwszy segment DRIVING tego dnia jako reprezentatywny czas i pojazd. Ryzyko, niezweryfikowane: driver_name w p24_ride_events może mieć problem z kodowaniem polskich znaków i nie ma wspólnego stabilnego ID między tabelami — join po nazwisku jest niepewny i wymaga jawnego flagowania niskiej pewności (vehicle_id_confidence='inferred' vs 'direct') albo pozostawienia NULL z komunikatem w detail.
  • Nie implementować na ślepo — jeśli Etap 1 wykaże, że żadna z dwóch ścieżek nie daje wiarygodnego wyniku, udokumentować to jako znane ograniczenie.

Idempotencja. Zweryfikowano w kodzie: wszystkie 4 skrypty dziś robią delete+reinsert całego dnia (atrax_route_anomaly_daily.py:849-851, atrax_tachograph_compliance_daily.py:468-470, atrax_ecodriving_anomaly_daily.py:572-574, atrax_vehicle_health_daily.py:285-292). Czysty ON CONFLICT DO NOTHING zamroziłby observed_value/severity na zawsze po pierwszym zapisie.

Reguła: unique index na (source_system, anomaly_type, coalesce(vehicle_id,''), coalesce(driver_id,''), occurred_date) + INSERT ... ON CONFLICT (...) DO UPDATE SET observed_value=EXCLUDED.observed_value, norm_median=EXCLUDED.norm_median, modified_z=EXCLUDED.modified_z, detail=EXCLUDED.detail, severity=EXCLUDED.severity, updated_at=now() WHERE p24_fleet_anomaly_escalations.status='open'. Efekt: dopóki nikt nie zaczął triażu, rerun odświeża dane detekcji; gdy status już nie jest open, wiersz zamraża się.

To realna zmiana logiki w każdym z 4 skryptów: każdy musi przejść z “usuń i wstaw cały dzień na nowo” na “upsert per-wiersz z klauzulą WHERE”.

Znane, jawnie zaakceptowane ograniczenie: rerun, który przeliczy że anomalia już nie zachodzi, NIE usuwa istniejącego otwartego wiersza — wymaga ręcznego odrzucenia w panelu. Nazwać to w ops docach każdego z 4 skryptów.

RLS: service_role FOR ALL, grafana_readonly SELECT, plus dostęp dla et-operational-platform (odczyt + update status/assigned_to/resolution_note) — dokładny mechanizm grantu do zweryfikowania w fazie design, może wymagać secret-manager/nowej polityki RLS.

2. Normalizacja severity

  • Fuel/ecodriving/vehicle-health: escalate→critical, watch→warning, baseline_drift→critical.
  • Tachograf: *_exceeded / extended_driving>0 / shortened_rest>0 → critical; *_near → warning.

3. Notyfikacja: Discord digest (wzorem tachografu, uogólniony)

Wynieść logikę diff-tylko-NOWE do wspólnej funkcji w atrax_norm_common.py (np. post_fleet_escalation_digest(source_system, new_rows)), używanej przez wszystkie 4 skrypty. Jeden webhook per system. Uogólnić P24_DISCORD_COMPLIANCE_WEBHOOK_URL na P24_DISCORD_FLEET_ESCALATIONS_WEBHOOK_URL (nowy sekret = zadanie dla secret-manager, nie zgadywać wartości).

Ważne: dziś to dwa strukturalnie różne modele dostawy — tachograf woła Discord bezpośrednio, fuel/fraud steguje do dev_r_alert_events (Lane E). Nowa funkcja musi jawnie zastąpić OBA modele.

Po zdjęciu Lane E: wpis 'atrax-route-anomaly' w VALID_SOURCE (scripts/alert-ingest.py, infra-src/meta-dispatcher/src/alert-ingest.ts) staje się martwym kodem — usunąć w tym samym PR. Sprawdzić .github/workflows/alert-source-cutover-guard.yml (pilnuje odwrotnego kierunku) — potwierdzić że zdjęcie Lane E nie koliduje.

4. Zgodność EU AI Act — reguła niesiona w danych

Cytat z docs/eu-ai-act-compliance.md: “advisory/investigatory only, a human interprets every finding… never wired into any automated disciplinary/performance/allocation system… wiring this output into any automated employment decision requires a fresh conformity assessment first.”

  • requires_human_review=true (każdy wiersz z attribution='driver_pattern' + KAŻDY wiersz tachografu, bo tachograf z definicji jest per-kierowca) — panel nie może udostępniać dla takich wierszy żadnej akcji masowej/automatycznego rozwiązania, i nie może zasilać żadnego innego systemu oceny pracownika bez świeżej oceny zgodności.

Rejestracja dev_r_ai_systems to osobny, brakujący krok. Zweryfikowano: rejestruje TYLKO ecodriving-per-route-anomaly. Fuel/fraud, vehicle-health i tachograf NIE MAJĄ wpisu wcale — ten projekt powinien uzupełnić audyt + wpis dla każdego z 4 systemów, z jawną oceną per system czy w ogóle podlega (vehicle-health jest o zasobie, nie o osobie, prawdopodobnie NIE wymaga Annex III — decyzja do jawnego zapisania). Dodatkowo: przejście z ad-hoc triażu GH-issue na dedykowany panel z akcją status→resolved/dismissed to bardziej ukształtowany “decision support” — zrewidować istniejący wpis ecodrivingu (risk_justification/has_human_oversight).

Uwaga: reguła “p24-infra nie powinno wysyłać e-mail/push w imieniu et-operational-platform” (#9) to własne rozumowanie o granicy własności danych (PII/tożsamość użytkownika) — NIE cytat z credential-isolation-policy.md (ten dokument dotyczy wyłącznie credentiali) — nie przypisywać tej reguły do tego dokumentu.

5. Migracja 4 istniejących skryptów

  1. atrax_ecodriving_anomaly_daily.py — dodać zapis do nowej tabeli + wywołanie digestu.

  2. atrax_vehicle_health_daily.py — to samo, rozszerzalne o AdBlue (#7046).

  3. atrax_tachograph_compliance_daily.py — usunąć _post_discord/_file_alert_issue/ send_alert, zastąpić zapisem do nowej tabeli + wspólnym digestem. Istniejąca logika “tylko NOWE” zostaje. p24_tachograph_violations (audyt) zostaje bez zmian.

    Realny gap do naprawienia w tym samym PR-1: ten skrypt NIE jest wpięty do żadnego crona — docs/atrax-tachograph-compliance-operations.md:103-107 nazywa to “separate sys-admin handoff… until then run it manually”, i to nigdy nie zostało zrealizowane (potwierdzone: nieobecny w run-atrax-norm-nightly.sh, brak wpisu w .github/workflows/). Bez tego migracja jest asymetryczna. PR-1 musi również wpiąć atrax_tachograph_compliance_daily.py do run-atrax-norm-nightly.sh.

  4. atrax_route_anomaly_daily.py (fuel/fraud) — zdjąć Lane E (native_fraud_flag/ reconcile_fraud ZOSTAJĄ — to klasyfikacja, nie dostawa alertu), zastąpić zapisem do nowej tabeli

    • digestem.
  5. Nowy atrax_driver_utilization_compute.py (#10) — nowy komponent, niezależny od tabeli eskalacji.

dev_r_alert_events/alert-triage-batch/p24-infra-nc-alert ZOSTAJE dla genuinie infrastrukturalnych alertów — po tej migracji żaden z 4 skryptów Atrax już go nie używa.

6. Kontrakt cross-repo z et-operational-platform

p24-infra buduje: schemat + RLS + digest Discord + dokumentację kontraktu. et-operational-platform buduje: sam panel UI.

Mechanizm: docs/playbooks/cross-project-infra-requests.md działa w ODWROTNYM kierunku (instrukcja dla et-operational-platform jak prosić p24-infra). Właściwy mechanizm to prawdopodobnie docs/playbooks/queue-dispatch-cross-project.md (POST /queue-issue z repo: "radieu/et-operational-platform") — ale myślany do dispatchowania implementacji już istniejącego issue. Potwierdzić przy wykonaniu, nie zgadywać gh issue create --repo radieu/et-operational-platform bez sprawdzenia sankcjonowanego wzorca.

Potwierdzone: oba repo współdzielą ten sam projekt Supabase mwkqmgadqnkkihjdeqsi (docs/infrastructure-overview.md:384).

Gap do jawnego nazwania: et-operational-platform nie ma dziś żadnej rejestracji AI Act pod tego typu panel (sprawdzone: tylko vipo-offer-parser, niezwiązany) — cross-repo issue musi to nazwać jako wymagające ich własnej zgody/oceny.

Rozszerzony zakres

7. Panel UI w et-operational-platform — koordynowane cross-repo issue

Zakłada drugie issue w repo et-operational-platform (mechanizm per #6) opisujące: schemat p24_fleet_anomaly_escalations, semantykę status/requires_human_review/metryk wysycenia (#9), wymóg UI: lista + filtrowanie + przypisanie + zamykanie, z zablokowaną akcją masową/auto-rozwiązania dla requires_human_review=true. Oba issue cross-referencują się bez keywordów zamykających.

8. Korelacja cross-systemowa per pojazd — SQL view, nie nowy ETL

Widok p24_fleet_vehicle_daily_risk grupujący p24_fleet_anomaly_escalations po (vehicle_id, occurred_date): liczba odrębnych source_system, najwyższa severity, lista anomaly_type. Widok SQL nad już zapisywaną tabelą, zero nowej ścieżki zapisu.

9. Notyfikacje rozszerzone — Discord (ops) + Realtime dla et-operational-platform

p24-infra nie powinno trzymać tożsamości/preferencji notyfikacji fleet managerów. Zamiast p24-infra wysyłającego e-mail/push:

  • Discord digest (#3) zostaje.
  • Włączyć Supabase Realtime na p24_fleet_anomaly_escalations (ustawienie konfiguracyjne) — et-operational-platform subskrybuje i sam decyduje o e-mail/push.
  • Udokumentować to rozgraniczenie explicite w cross-repo issue z #7.

10. Miara wysycenia godzin jazdy (utilization) — dzienna/tygodniowa/dwutygodniowa/miesięczna

Cel: obok naruszeń tachografu, pokazywać ile % legalnie dostępnego czasu jazdy kierowca faktycznie wykorzystał — sygnał operacyjny, nie compliance.

Research zakończony: Zmapowane pola z /api/rtdrivers (map_drivers w n8n-workflows/atrax-drivers-daily-snapshot_akgnvueDkDPbLJ49.json): brak jakiegokolwiek pola dziennego limitu — kod nigdy go nie odczytuje, brak zapisanego przykładowego surowego payloadu. this_week_limit_min ma fallback ?? 3360 (56h). Brak stałych regulacji UE (540/600 min) gdziekolwiek w repo. Tabela p24_atrax_drivers_daily jest młoda (dane od 2026-08-08) i nie ma wypełniania dziur. Wzorzec architektoniczny: p24_consumption_rolling_averages (#6777) — dedykowana tabela z window_type, osobny nocny skrypt, zarejestrowana w infra-src/report-query-api/tables.py jako SNAPSHOT TableSpec, z endpointem /api/v1/driver/{id}/rolling-averages. NIE SQL view, NIE agregacja w report-query-api.

Projekt (poziomy pewności):

  • Tygodniowy: this_week_driving_min / this_week_limit_min — wysoka pewność.
  • Dwutygodniowy: last_2w_driving_min / (last_2w_driving_min + last_2w_remaining_min) — wysoka pewność.
  • Dzienny: day_driving_min / (540 lub 600 min stała UE albo realne pole Atrax jeśli Etap 1 je znajdzie) — średnia pewność, wymaga Etap 1.
  • Miesięczny: SUM(day_driving_min) za kalendarzowy miesiąc / (liczba dni z realnym wierszem, NIE dni kalendarzowych, × dzienna stała) — niższa pewność, metryka biznesowa.

Etap 1: jednorazowy live-check surowej odpowiedzi /api/rtdrivers — czy jest tam pole dziennego limitu. Jeśli nie, hardkodować stałą UE jako jawnie oznaczone założenie.

Architektura: nowa tabela p24_driver_utilization (window_type), nowy skrypt atrax_driver_utilization_compute.py wpięty w run-atrax-norm-nightly.sh. Uwaga: atrax_tachograph_compliance_daily.py dziś NIE jest w tym runnerze (patrz #5.3, PR-1 to naprawia) — kotwiczyć nowy krok po atrax_vehicle_health_daily, chyba że PR-1 już wpięło tachograf na koncu. Sprawdzić aktualny stan runnera w chwili implementacji PR-3. Rejestracja w report-query-api/tables.py + nowy endpoint /api/v1/driver/{driver_id}/utilization. To metryka informacyjna/KPI, NIE trafia do p24_fleet_anomaly_escalations.

Sekwencja wykonania

Cały ten zakres (#0–10) to treść JEDNEGO issue projektowego, ale realizacja musi być podzielona, żeby każdy krok był samodzielnie weryfikowalny (panel/Realtime zależą od innego repo):

  1. Etap 0 (blokujący): sprawdzić schemat/użycie tabeli incidents (#0).
  2. PR-1 (rdzeń, samodzielnie weryfikowalny): tabela p24_fleet_anomaly_escalations (lub rozszerzenie incidents) + idempotencja DO UPDATE ... WHERE status='open' + normalizacja severity + wspólna post_fleet_escalation_digest() + migracja logiki zapisu we wszystkich 4 skryptach + wpięcie atrax_tachograph_compliance_daily.py do run-atrax-norm-nightly.sh + audyt/ uzupełnienie dev_r_ai_systems dla fuel/fraud, vehicle-health, tachografu ORAZ rewizja wpisu ecodrivingu + usunięcie martwego VALID_SOURCE wpisu.
  3. PR-2 (tachograf: vehicle_id/occurred_at, Etap 1 first).
  4. PR-3 (osobne issue — miara wysycenia, #10). Nie blokuje PR-1/2.
  5. PR-4 (osobne issue — widok korelacji, #8). Dorzucić dopiero gdy panel faktycznie go zapyta.
  6. Cross-repo issue w et-operational-platform (#6-7). Zależny od PR-1.

WAŻNE — instrukcja dla worker/design fazy: plan-review na tym issue (/review-plan) ma przejść 4 iteracje, nie domyślne 3 z CLAUDE.md — jawne żądanie użytkownika, nadpisuje domyślny limit — przed commitem PR-1.

Weryfikacja po wdrożeniu — per PR

Etap 0 / PR-1:

  1. incidents sprawdzona i decyzja jawnie zapisana z uzasadnieniem.
  2. Migracja aplikuje się czysto, CI zielone.
  3. --dry-run przebieg wszystkich 4 skryptów na dniu ze znaną anomalią zapisuje wiersz z poprawnym source_system/severity/requires_human_review (dla tachografu vehicle_id/occurred_at mogą być jeszcze NULL — dopełnia PR-2).
  4. Rerun nie nadpisuje ręcznie zmienionego status/assigned_to/resolution_note; rerun który przeliczy anomalię jako niewystępującą zostawia stary otwarty wiersz.
  5. Discord digest wysyła się raz per system, tylko dla nowych wierszy.
  6. Regresja: stary bespoke webhook tachografu i Lane E fuel-joba usunięte z kodu.
  7. dev_r_ai_systems: 3 nowe wpisy + zrewidowany wpis ecodrivingu.

PR-2: 8. Wiersze tachografu niosą vehicle_id/occurred_at.

PR-3: 9. Tabela p24_driver_utilization zapełnia się poprawnie; endpoint odpowiada.

PR-4: 10. Widok korelacji zwraca poprawny wynik.

Cross-repo: 11. Panel odczytuje z nowej tabeli i respektuje requires_human_review.


Persisted from issue #7107. Plan reviewed via /review-plan (Co-Authored-By Claude Sonnet 5). Implementation is split into PR-1…PR-4 + a cross-repo et-operational-platform issue per the sequence above; this document is the design artifact, not an implementation.