Przejdź do treści

Etap 29 — Offline hardening (pełny zakres, Fazy 0→5)

Cel

Doprowadzić kiosk do pełnej pracy offline dla wszystkich funkcji (odbiór, nadanie, wydanie, uzupełnianie, inwentaryzacja) — z zachowaniem integralności stanu i limitów, oraz załatać dziury już istniejące w obecnym offline. Bazuje na audycie → Audyt offline. Zasada nadrzędna: backend = źródło prawdy; offline = lokalna aproksymacja + reconcyliacja przez chokepoint. Zapis stanu offline jest dopuszczalny tylko gdy jest relatywny + idempotentny + re-walidowany serwerowo lub ograniczony twardym budżetem + audytowany.

Status 2026-06-17 — WSZYSTKIE FAZY 0→5 WDROŻONE + rozszerzenie (over-limit/flagi)

Kolejność realizacji: 0 → 1 → 4 → 2 → 5.1 → 5.2 → 3 (najgroźniejsze — wydanie z budżetem — na końcu). Wdrożone na produkcji: backend przebudowany (2 migracje zaaplikowane), web przebudowany, kiosk 1.5.69 opublikowany jako MachineRelease STABLE → ota.auto-rollout rozdał na wszystkie 5 maszyn (instalują się na cyklu agenta). Doszło rozszerzenie (sekcja niżej): instancyjne tryby przekroczenia limitu (RESET/CARRY_OVER) + flagi offline na wszystkich zdarzeniach (transakcje/inwentaryzacje/paczki) z odznakami w panelu. Lokalna SQLite kiosku = v5. Pozostaje: test offline na żywej maszynie (wyjmij sieć → każdy flow [odbiór/nadanie/wydanie/uzup./inwent.] działa → wepnij → replay → stan zgodny + flagi/konflikty w panelu).

Prymitywy wspólne (raz, używane we wszystkich fazach)

P1. Koperta zdarzenia (outbox) — ujednolicenie

Każdy wiersz outboxa = { eventId: uuid(klient), kind, occurredAt, actorUserId?, payload }. - Kiosk: local_db.dart — kolumna eventId w outbox (migracja lokalnej SQLite, addytywna). offline_repo.dart — helper enqueue(kind, payload, {actorUserId}) generujący eventId raz przy zapisie (NIE przy wysyłce → stabilny klucz mimo retry). - Backend: machine-sync.dto.tsReplayEventDto zyskuje eventId (uuid, wymagany) + actorUserId?. Walidacja occurredAt w rozsądnym zakresie (nie przyszłość > 5 min, nie starsze niż 30 dni) → poza zakresem = REJECT z reasonem.

P2. Serwerowy rejestr dedup ReplayedEvent (klucz, nie tylko status)

  • schema.prisma: nowy model ReplayedEvent { eventId String @id, machineId, kind, outcome, appliedAt }. Migracja addytywna.
  • machine-sync.service.ts replay(): na starcie obsługi eventu INSERT ReplayedEvent(eventId); kolizja (już jest) → zwróć poprzedni outcome jako idempotentny no-op, nie aplikuj ponownie. Zastępuje kruche poleganie na WHERE status=....

P3. Rozróżnienie łączności w kiosku (A-conflation)

  • api_client.dart: typ ApiException { int? statusCode; bool isNetwork; }. Timeout (domyślnie 8 s) na wszystkich wywołaniach (dziś tylko login). isNetwork=true tylko dla timeout/socket; 4xx/5xx → isNetwork=false + statusCode.
  • sync_service.dart: flip na OFFLINE tylko gdy isNetwork; 401 → osobny stan „klucz odrzucony" (nie offline → nie podstawiaj cicho stale-auth); 5xx → „błąd serwera" (nie offline).

P4. Chokepoint pozostaje jedyną drogą zapisu stanu

Każdy replay piszący stan (dispense.confirmed, replenish.confirmed, inventory.saved) MUSI iść przez InventoryService.createMovement / applyStockTake z klientowym idempotencyKey (uuid generowany w kiosku, zapisany w payloadzie przy enqueue). Żadnych bespoke ścieżek omijających walidację stanu.


Faza 0 — Załatać istniejące dziury (LOW ryzyko, robić i tak)

Zaimplementowane 2026-06-17 (kiosk 1.5.63) — BEZ migracji

Faza 0 weszła migration-free: pickup/deposit/pickup-failed są status-guarded (idempotentne przez przejście stanu), więc tabela ReplayedEvent (klucz-dedup) przesunięta do Fazy 3 (wydanie, gdzie ruchy nie są status-guarded). Koperta eventId/actorUserId jedzie tylko jako pole (autoryzacja + tracing). Konflikty offline (0.5) logowane do istniejącego MachineApiLog (kind:'offline-replay') → widoczne od ręki w panelowym podglądzie logów /v1 (chipy kinds budowane dynamicznie). Commity: 4bec0ed (0.1), 375e954 (backend 0.4/0.3), c1fb7cf (kiosk 0.2/0.3/0.4).

29-0.1 — Łączność + timeouty (P3)

  • Kiosk: api_client.dart (ApiException+timeouty), sync_service.dart (flip tylko na network), banner statusu czyta realny sygnał.
  • Invariant: 500/401 NIE wygląda już jak offline → koniec cichego stale-auth na zrotowanym kluczu.
  • Bez backendu. Bump kiosku.

29-0.2 — Nadanie nie ginie (A2) — deposit.completed na żywo

  • Kiosk: offline_repo.dart +markDepositedLocally (flip cached_shipments → DEPOSITED + enqueue deposit.completed{shipmentId}). deposit_size_screen.dart:160 / deposit_lookup_screen.dart:318POST /shipments/:id/deposited zmienione na local-first (próbuj online; przy isNetwork → buforuj).
  • Backend: replayDepositCompleted (już istnieje, ASSIGNED→DEPOSITED) + dedup P2. Dotyczy paczek rozpoczętych online (ASSIGNED), którym zginął mark — pełne offline-nadanie to Faza 2.
  • Invariant: paczka fizycznie w skrytce ⇒ shipment zawsze dojdzie do DEPOSITED (nie osierocona).

29-0.3 — Odbiór offline utwardzony (A3/A4/A5)

  • Kiosk offline_repo.dart shipmentsForUser: odfiltruj expiresAt < now; dołącz cached_lockers.status → ustaw lockerBroken z cache (nie otwieraj BROKEN offline). pickup_screen.dart: report-failed → outbox pickup.failed (nowy kind) zamiast gubić.
  • Backend: handler replay pickup.failed (shipment→PICKUP_FAILED + locker BROKEN), dedup P2.
  • Invariant: offline nie oferuje przeterminowanych/na zepsutej skrytce; nieudany odbiór nie ginie.

29-0.4 — Autoryzacja replay + dedup + zegar (A1/A6/A7)

  • Backend: P1 (eventId+occurredAt walidacja) + P2 (ReplayedEvent). Autoryzacja: replayPickupCompleted waliduje shipment.recipientId == event.actorUserId (inaczej REJECT) → kiosk nie może oznaczyć cudzej paczki odebraną dla dowolnego usera. actorUserId = user, który uwierzytelnił się offline (kiosk dokłada do koperty).
  • Kiosk: dołącz actorUserId do pickup.completed/pickup.failed.
  • Invariant: replay związany z faktycznym odbiorcą; sam klucz maszyny nie wystarcza do dowolnej zmiany.

29-0.5 — Panel: konflikty offline (A3)

  • Backend: zapisuj outcome CONFLICT/REJECT replay do MachineApiLog (lub mały MachineSyncConflict) z {shipmentId, kind, reason, at}.
  • Web: w detalu maszyny / raportach sekcja „Konflikty offline" (ghost-pickup, utknięte nadania).
  • Invariant: rozjazdy offline są widoczne dla operatora, nie znikają cicho.

Deploy Fazy 0: backend+web razem (migracja ReplayedEvent + ewent. konflikt-log), OTA kiosku. Migracje addytywne.


Faza 1 — Fundament łączności

Zaimplementowane 2026-06-17 (kiosk 1.5.64, commit 3319726)

ConnectivityService wpięty w app.dart obok SyncService. Persisted „offline since" jako pole (in-memory, nie przez reboot — wystarczające dla sygnału). Konsument: sync.flush() na reconnect (reszta — Fazy 2–5).

29-1.1 — ConnectivityService (kiosk)

  • Lekki health-ping (GET /health, istnieje) co ~15 s + lastSuccessAt; strumień online. Flow konsultują go świadomie zamiast przypadkowego try/catch. Persisted „offline since". Banner = realny sygnał.
  • Invariant: deterministyczna bramka online/offline dla Faz 2–5.
  • Bez backendu (poza istniejącym /health). Bump kiosku.

Faza 2 — Nadanie offline (deposit) — MED

Zaimplementowane 2026-06-17 (kiosk 1.5.67) — BEZ leasingu (migration-free)

Zamiast tabeli LockerLease poszliśmy optymistycznie: kiosk bierze wolną PARCEL skrytkę rozmiaru z cache (cached_lockers, lokalnie RESERVED), mintuje finalny trackingId OFF-<hex> i buforuje deposit.offline. Replay (replayDepositOffline) tworzy Shipment DEPOSITED idempotentnie po unikalnym trackingId i waliduje, że skrytka wciąż FREE — jeśli online ją zajął, CONFLICT do operatora (paczka fizycznie w skrytce). Recipient search offline z cached_users. Commit 2f974fc (kiosk), 4cfebea (backend).

29-2.1 — Backend: leasing skrytek + replay nadania

  • schema.prisma: LockerLease { id, machineId, lockerId, size, function, leasedUntil, consumedEventId? } — pula wstępnie zarezerwowanych FREE skrytek wydzielona maszynie online (cron/endpoint odnawia). Migracja addytywna.
  • machines.controller/service: GET /machines/me/locker-leases (kiosk pobiera pulę online), replayDepositOffline — tworzy Shipment (DEPOSITED) wiązany z recipient+locker z leasingu; waliduje że lease ważny/niespożyty; kolizja → CONFLICT (parcel fizycznie w skrytce → flaga do operatora).
  • Tracking: kiosk mintuje provisional uuid; backend przy replay nadaje realny tracking lub akceptuje provisional (deterministyczne mapowanie po eventId).

29-2.2 — Kiosk: offline branch nadania

  • Cache: leasing w nowej tabeli cached_leases. deposit_recipient_screen — szukaj odbiorcy z cached_users offline. deposit_size_screen — wybór skrytki z leasingu, otwarcie lokalnym agentem, enqueue deposit.offline{provisionalTracking, recipientId, lockerId, actorUserId}.
  • Invariant: offline nadaje tylko w skrytkę z ważnego leasingu; replay waliduje serwerowo; konflikt = operator.
  • Bump kiosku. Deploy backend+web (panel: widok leasingu/konfliktów).

Faza 3 — Wydanie offline z twardym budżetem — HIGH

Zaimplementowane 2026-06-17 (kiosk 1.5.69, backend migracja 20260617_etap29_movement_offline_flags)

Snapshot zamiast osobnego endpointu: kiosk po każdym udanym katalogu ONLINE (/dispense/me/catalog) zapisuje per-user snapshot do cached_user_dispense (remaining/decyzja/requiresAcceptance z silnika limitów). Reuse cached_items/cached_stock z Fazy 4.1 — żaden nowy endpoint nie był potrzebny. Offline dispenseCatalogOffline liczy efektywny budżet = cached.remaining − Σ offline_usage_ledger (trwały, keyed po eventId; przeżywa reboot). Fail-closed: brak snapshotu / decyzja≠ALLOW / requiresAcceptance → produkt nie pojawia się offline (baner informuje). Po zamknięciu drzwi dispense.confirmed{idempotencyKey} → ledger + outbox; net-blip PO zamknięciu też buforuje. Bez ReplayedEventcreateMovement jest już idempotentny po idempotencyKey, więc tabela-dedup z P2 okazała się zbędna; ledger kasowany na drenażu eventu (świeży katalog nie odejmuje 2×). Backend replayDispenseConfirmed: re-ewaluacja limitu (log:false) → WITHDRAWAL przez chokepoint; nadwyżka (stale snapshot/wyścig) → Movement.flaggedOverLimit=true (+ offlineReplayedAt) zamiast utraty rekordu; konflikt stanu → CONFLICT w logu offline-replay. Commity: c0baf01 (backend 3.1), 8e61b96 (kiosk 3.2).

29-3.1 — Backend: catalog/limit snapshot dla kiosku

  • Nowy GET /machines/me/dispense-snapshot?userId= (machine-auth): zwraca katalog (itemy+ceny+dokumenty+flagi), stan per-slot, defaultDeny, oraz snapshot limitów zalogowanego usera (remaining/limitQty/resetAt per item/grupa) policzony silnikiem. Per-user przy logowaniu online (nie wszyscy naraz).
  • Nowy handler replayDispenseConfirmed: przez createMovement (reject stanu + idempotencja) i re-run limit.evaluate; over-limit → ruch zapisany ale oflagowany (Movement.flaggedOverLimit lub LimitDecisionLog BLOCK po fakcie) — bo fizyczne nieodwracalne, więc audyt zamiast utraty rekordu.
  • schema.prisma: Movement.flaggedOverLimit Boolean @default(false) (+ ewent. offlineReplayedAt). Migracja addytywna.

29-3.2 — Kiosk: cache + budżet + offline branch wydania

  • Nowe tabele SQLite: cached_items, cached_stock, cached_dispense_policy(defaultDeny), cached_user_limits(userId,itemKey,remaining,limitQty,resetAt), offline_usage_ledger(userId,itemId,qty,window,eventId).
  • Logika: offline remaining_efektywne = cached.remaining − Σ offline_usage. Wydaj tylko do remaining_efektywne i cached_stock. Fail-closed: brak snapshotu limitu dla usera/itemu i defaultDeny (cache) → BLOK. dispense_screen offline branch: czyta cache, egzekwuje budżet, otwiera lokalnie, dekrementuje offline_usage_ledger (przeżywa reboot), enqueue dispense.confirmed{idempotencyKey, userId, itemId, qty, slot, actorUserId}.
  • Dokumenty: cache cached_documents+cached_acceptances; bramka offline; akceptacja → outbox.
  • Invariant: offline NIGDY nie wyda ponad remaining z ostatniego synca (twardy budżet, trwały licznik); brak polityki = fail-closed; nadwyżki (rozjazd stanu) oflagowane przy replay. Ryzyko resztkowe: cofnięte uprawnienie ograniczone świeżością synca (akceptowane, jak revoked-user przy odbiorze dziś).
  • Bump kiosku. Deploy backend+web (panel: flaga over-limit w raportach).

Faza 4 — Uzupełnianie offline — LOW-MED

Zaimplementowane 2026-06-17 (kiosk 1.5.66)

Fundament cache (4.1, commit 63eba02): /machines/me/sync zwraca items + per-slot stock; local_db v4 (cached_items/cached_stock, full-mirror + tombstone). 4.2 (kiosk e687415, backend f906287): lista do uzupełnienia z cache offline, otwarcie lokalnym agentem, replenish.confirmed{idempotencyKey="restock-<job>-<slot>"}createMovement RESTOCK (relatywny +delta, clamp do headroom, idempotentny). MachinesModule→DispensingModule. Optymistyczny bump cached_stock.

29-4.1 — Kiosk + backend: replenish offline

  • Cache: reuse cached_items/cached_stock (z Fazy 3). replenish_screen/replenish_run_screen offline branch: lista z cache, otwarcie lokalne, enqueue replenish.confirmed{idempotencyKey ZAPISANY przy enqueue, slotId, qty, actorUserId}.
  • Backend replayReplenishConfirmedcreateMovement RESTOCK (relatywny +delta, capacity re-clamp, idempotencja).
  • Invariant: RESTOCK relatywny + idempotentny + re-clamp serwerowy → komponuje się bezpiecznie z przeplotem; klucz stabilny (nie regenerowany).
  • Bump kiosku. Deploy backend.

Faza 5 — Inwentaryzacja offline (z guardem OCC) — MED-HIGH

Zaimplementowane 2026-06-17 (kiosk 1.5.68, backend 831524f + replay 5.2)

5.1 (commit 831524f): applyStockTake przyjmuje baselineQty per linia (= expectedQty ze snapshotu); gdy slot.quantity != baselineQty → linia pomijana jako DRIFT (+ licznik drift), bez forsowania absolutu — naprawia minę C1 także dla online replay-after-delay. 5.2 (kiosk): lista produktów/skrytek z cache offline, otwarcie lokalne, inventory.saved{clientDocumentId(UUID v4), lines:[…,expectedQty]}; replay → kioskDispense.inventorySave idempotentnie po clientDocumentId (ten sam dokument = brak podwójnego zapisu) → applyStockTake z OCC.

29-5.1 — Backend: OCC guard w applyStockTake (fix C1, korzysta też online)

  • inventory.service.ts applyStockTake: przyjmuje baselineQty per linia (= expectedQty ze snapshotu operatora). Przy aplikacji: jeśli slot.quantity != baselineQtyNIE forsuj absolutu; oznacz linię DRIFT/CONFLICT (skip + raport do operatora). Dokument inwent. zapisuje baseline+delta+drift per linia.
  • kiosk-dispense.service.ts inventorySave + DTO: przenosi baselineQty per linia.
  • Invariant: spis nie nadpisuje świeższego stanu — dryf między snapshotem a replay jest wykrywany, nie clobberowany. (Naprawia minę nawet dla online replay-after-delay.)

29-5.2 — Kiosk: inwentaryzacja offline

  • Cache produktów/lockerów inwent. (reuse cached_items/cached_stock + expectedQty jako baseline). inventory_screen/inventory_column_screen offline branch: otwarcie lokalne, enqueue inventory.saved{documentId, lines:[{slot, baselineQty, foundQty}], actorUserId}.
  • Backend replayInventorySaved → InventoryDocument + applyStockTake z OCC.
  • Invariant: buforowanie inwent. bezpieczne dopiero PO 29-5.1; linie z dryfem → konflikt do operatora, nie ciche nadpisanie.
  • Bump kiosku. Deploy backend+web (panel: dryf/konflikty inwent.).

Rozszerzenie po fazach (2026-06-17): tryby przekroczenia limitu + flagi offline wszędzie

Zaimplementowane + WDROŻONE 2026-06-17 (migracje 20260617_etap29_movement_offline_flags + 20260617_etap29_overlimit_modes)

Po domknięciu Faz 0→5 doszły dwie rzeczy żądane przez operatora: (1) konfigurowalna instancyjnie polityka traktowania przekroczeń limitu powstałych offline, w dwóch trybach; (2) jednolita flaga „offline" na KAŻDYM rekordzie zmaterializowanym przez replay (transakcje, inwentaryzacje, paczki).

A. Przekroczenie limitu — 2 tryby (konfiguracja całej instancji)

Przekroczenie limitu może powstać wyłącznie offline — kiosk online pilnuje twardego budżetu (Faza 3); nadwyżka wpada tylko gdy snapshot uprawnień ze synca był nieaktualny albo był wyścig między urządzeniami. Polityka jest instancyjna: DispenseSetting.overLimitMode ∈ {RESET, CARRY_OVER}, przełączana w panelu Wydawanie → Limity (pod „Domyślnie zabronione").

Tryb Zachowanie Kara w następnym okresie
RESET (domyślny) Nadużycie wygasa naturalnie wraz z oknem kroczącym silnika. Brak — tylko flaga audytowa Movement.flaggedOverLimit.
CARRY_OVER Nadwyżka zapisana jako dług (LimitOverage) odejmowany od przysługującej ilości. Tak — jednorazowo, potem dług auto-wygasa.

Dlaczego dług ma okno [at+okres, at+2·okres) — bez podwójnego karania

Silnik liczy zużycie w oknie kroczącym (usedQty = suma WITHDRAWAL z ostatnich periodSeconds). Ruch nadużycia sam blokuje usera przez periodSeconds (naturalnie „przenosi" przekroczenie w bieżącym oknie). Gdyby dług działał OD RAZU, user byłby ukarany podwójnie (ruchy + dług). Dlatego dług staje się aktywny dopiero gdy ruchy nadużycia wypadną z okna (appliesFrom = at + periodSeconds) i trwa jeden okres (appliesUntil = at + 2·periodSeconds), po czym auto-wygasa → kara jednorazowa, bez kumulacji w nieskończoność.

Ścieżka: replayDispenseConfirmed re-ewaluuje limit (log:false), zapisuje WITHDRAWAL przez chokepoint createMovement z flaggedOverLimit, a w trybie CARRY_OVER woła limits.recordOverLimit()LimitOverage per przekroczona reguła. evaluate() odejmuje sumę aktywnych długów per reguła od limitQty (clamp do 0). MatchedRuleResult niesie periodSeconds. setPolicy({defaultDeny?, overLimitMode?}) aplikuje tylko przysłane pole (toggle nie rusza drugiego).

B. Flagi „offline" na każdym zreplayowanym rekordzie

Każdy rekord sfinalizowany przez replay offline dostaje audytowy offlineReplayedAt (null = online), nakładany przez chokepoint, nie ad-hoc:

Rekord Pole Kind(y) replay Chokepoint
Movement (wydanie + uzupełnianie) offlineReplayedAt (+ flaggedOverLimit dla wydania) dispense.confirmed, replenish.confirmed createMovement (DTO)
InventoryDocument (inwentaryzacja) offlineReplayedAt inventory.saved inventorySave
Shipment (paczki) offlineReplayedAt pickup.completed, pickup.failed, deposit.completed, deposit.offline replay tx

Panel — wspólne odznaki OfflineBadge (bursztyn „offline") + OverLimitBadge (różowy „ponad limit", i18n common.offlineFlags) w: tabeli ruchów (Raporty), liście + detalu przesyłek, liście inwentaryzacji. Listy zwracają te skalary bez zmian backendu (zapytania używają include/braku select, więc skalary jadą same).


Macierz outbox kinds (po etapie 29)

kind faza replay → invariant
pickup.completed istnieje/0.4 flip PICKED_UP, recipient==actor status-guard + actor + dedup
pickup.failed 0.3 PICKUP_FAILED + locker BROKEN dedup
deposit.completed 0.2 ASSIGNED→DEPOSITED status-guard + dedup
deposit.offline 2 utwórz Shipment z leasingu lease-valid + dedup
dispense.confirmed 3 createMovement WITHDRAWAL + re-limit (flag) budżet kiosk + chokepoint + flaga
replenish.confirmed 4 createMovement RESTOCK relatywny + re-clamp + idempotent
inventory.saved 5 applyStockTake (OCC) baseline-guard + dedup

Każdy kind oznacza rekord jako offline

Po rozszerzeniu z 2026-06-17 każdy replay piszący stan ustawia offlineReplayedAt na docelowym rekordzie (Shipment / Movement / InventoryDocument), a dispense.confirmed dodatkowo Movement.flaggedOverLimit przy przekroczeniu. Patrz sekcja „Rozszerzenie po fazach" wyżej.

Migracje DB — STAN FAKTYCZNY (as-built, wszystkie addytywne)

Spec zakładał kilka tabel, których NIE zbudowano (prostsze, sprawdzone wzorce wystarczyły):

  • ReplayedEvent (dedup po kluczu) — niepotrzebny: pickup/deposit są status-guarded (idempotentne przez przejście stanu), a dispense/replenish dedupują się po Movement.idempotencyKey (chokepoint createMovement jest idempotentny). Inventory — po clientDocumentId.
  • LockerLease (pula rezerwacji pod nadanie offline) — zastąpione optymistycznym pickiem z cache + walidacją „skrytka wciąż FREE" przy replayu (replayDepositOffline).
  • MachineSyncConflict — konflikty/odrzucenia replay logowane do istniejącego MachineApiLog (kind:'offline-replay'), widoczne w panelowym podglądzie logów /v1.

Realnie wdrożone migracje serwera (2):

  1. 20260617_etap29_movement_offline_flagsMovement.flaggedOverLimit + Movement.offlineReplayedAt (Faza 3).
  2. 20260617_etap29_overlimit_modesenum OverLimitMode, DispenseSetting.overLimitMode, Shipment.offlineReplayedAt, InventoryDocument.offlineReplayedAt, tabela LimitOverage (rozszerzenie po fazach).

Fazy 0/1/2/4/5 weszły migration-free. Lokalna SQLite kiosku doszła do v5 (bez migracji serwera).

Ryzyko i kolejność

0 (LOW, robić i tak) → 1 (LOW) → 2 (MED) → 4 (LOW-MED) → 3 (HIGH) → 5 (MED-HIGH, gated na 5.1). Sugerowana kolejność realizacji: 0 → 1 → 4 → 2 → 5.1 → 5.2 → 3 (najgroźniejsze, wydanie, na końcu, gdy fundament i wzorce replay są sprawdzone). Każdy chunk: tsc/analyze → commit etap-29-X.Y: → deploy backend+web / OTA kiosk → weryfikacja (w tym test offline: wyjmij sieć → flow działa → wepnij → replay → stan zgodny).

Powiązane