Etap 3 — Kiosk UI + OTA pipeline¶
Etap 3 dorzucił dwa duże klocki: (a) Flutter kiosk dla fizycznego paczkomatu (Windows desktop primary, Linux secondary) i (b) end-to-end OTA pipeline który z poziomu panelu pozwala wgrać nową wersję, przypisać ją do maszyny i obserwować live progress instalacji.
| Chunk | Commit | Co wleciało |
|---|---|---|
| 3.1 | 3da4759 · a8bb7e8 |
backend release management — MachineRelease, MachineReleaseAssignment, multipart upload, SHA-256, /releases/:id/file stream |
| 3.2 | 95d5a60 |
panel: <UploadRelease> + <ReleasesTable> + <AssignRelease> + <ReleaseStatusCard> (4 s polling) |
| 3.3 + 3.4 | 1889e72 … |
Flutter kiosk skeleton + OTA updater (kontekst lokalny w aplikacji) |
| 3.5 | 9fbc148 … cebc9b3 |
pickup flow + ErgoFlow dark theme + RFID/PIN tabs + custom PIN pad + 12 polish dropów (v0.35 → v0.47) |
| infra | ada368f · d46a564 · 4019227 |
Windows kiosk bootstrap (PS scripts) + headless OTA agent + auto-logon Scheduled Task |
Najtrudniejsza decyzja etapu 3
Czy aplikacja Flutter sama sobie zarządza OTA, czy outsourcujemy to do zewnętrznego procesu? Pierwotny chunk 3.4 zrobił updater w aplikacji (Dart + Process.run do unzipa). Polish drop 9 (10cd430) usunął cały ten kod i przesunął OTA do PowerShell agenta jako LocalSystem Scheduled Task. Powód: aplikacja nie może zatrzymać samej siebie i zamienić swojego pliku exe — Windows trzyma binary z lock-iem dopóki proces żyje. Agent jest osobnym procesem, który ma prawo pokazać UI splash przez state-file, zatrzymać UI Scheduled Task, podmienić junction, wystartować nową wersję.
Topologia kiosku — Windows desktop¶
graph LR
classDef ui fill:#0AD6E8,stroke:#10F3FF,color:#06101A,stroke-width:2px
classDef agent fill:#F5C16C,stroke:#F5C16C,color:#06101A
classDef hw fill:#5EE6A0,stroke:#5EE6A0,color:#06101A
classDef state fill:#1A2C42,stroke:#0AD6E8,color:#fff
UI["UI · SmartBoxKioskUI<br/>Flutter Windows desktop<br/>Scheduled Task pod kiosk-userem"]:::ui
Agent["Agent · SmartBoxKioskAgent<br/>PowerShell 5.1<br/>Scheduled Task pod LocalSystem"]:::agent
StateFile[("update-state.json<br/>C:/smartbox-kiosk/")]:::state
Junction[("C:/smartbox-kiosk/current<br/>(junction → versions/0.83.0)")]:::state
Versions[("C:/smartbox-kiosk/versions/<br/>0.78.0/ · 0.79.0/ · 0.83.0/")]:::state
Modbus[Modbus TCP<br/>latch + sensor]:::hw
RFIDReader[RFID HID-keyboard<br/>card reader]:::hw
Backend[(Backend API<br/>X-Machine-Api-Key)]
Releases[(release file storage<br/>/data/releases)]
UI --> StateFile
Agent --> StateFile
Agent --> Junction
Agent --> Versions
Junction --> UI
UI --> Modbus
UI --> RFIDReader
UI --> Backend
Agent --> Backend
Agent --> Releases
Dwie Scheduled Task-i w Windowsie:
| Task | User | Trigger | Co robi |
|---|---|---|---|
SmartBoxKioskUI |
kiosk-user (auto-logon) | logon | uruchamia Flutter app fullscreen, always-on-top, borderless |
SmartBoxKioskAgent |
LocalSystem |
system start + co 30 s | poll GET /machines/me/release-assignment, jak PENDING to ściąga + instaluje |
UI nie wie o agencie ani na odwrót — komunikują się przez plik update-state.json na dysku. UI poll-uje co 1 s (UpdateStateWatcher), agent zapisuje stan przy każdej fazie OTA.
OTA — pełny flow¶
sequenceDiagram
autonumber
participant Admin
participant Panel
participant Backend
participant Storage as /data/releases
participant Agent as PS agent (LocalSystem)
participant UI as Flutter UI
participant State as update-state.json
Admin->>Panel: System → Wersje → Wgraj
Panel->>Backend: POST /releases (multipart, max 500 MB)
Backend->>Storage: zapisz, hash SHA-256
Backend-->>Panel: { id, sha256, fileSize }
Admin->>Panel: machine detail → Aktualizuj → wybierz wersję
Panel->>Backend: POST /machines/:id/releases
Backend->>Backend: insert MachineReleaseAssignment (PENDING)
Note over Agent: co 30 s
Agent->>Backend: GET /machines/me/release-assignment
Backend-->>Agent: { id, version, sha256, fileUrl } (PENDING)
Agent->>State: phase: "downloading"
UI->>State: read every 1s
UI->>UI: render <UpdateSplash /> downloading
Agent->>Backend: GET /releases/:id/file (stream)
Backend-->>Agent: binary + X-Release-SHA256 header
loop 256 KB buffer
Agent->>Backend: POST /machines/me/release-assignment/:id/status<br/>DOWNLOADING + progressPct (5 s throttle)
end
Agent->>Agent: weryfikuj SHA-256
alt mismatch
Agent->>State: phase: "failed"
Agent->>Backend: status FAILED
else OK
Agent->>State: phase: "installing"
Agent->>Agent: rozpakuj do versions/<ver>/
Agent->>Agent: atomic junction swap → current
Agent->>State: phase: "restarting"
Agent->>Agent: Stop + Start SmartBoxKioskUI Scheduled Task
end
UI->>State: phase = "installed", lingering 3 s
UI->>UI: fade do login
Agent->>Backend: status INSTALLED + installedVersion
Backend->>Backend: Machine.currentVersion = installedVersion
End-to-end na świeżej ~12 MB release: ~3 s download + ~10 s install + ~3 s "installed" linger.
InactivityGuard pause podczas OTA
UpdateStateWatcher widzi phase != "idle" i podaje sygnał do InactivityScope.pauseWhile(). Inactivity overlay jest wstrzymane przez cały czas trwania OTA — user nie zostaje wylogowany w środku update-u.
Kiosk UI — flow użytkownika¶
Etap 3.5 ustabilizował screens:
graph TB
classDef screen fill:#0AD6E8,stroke:#10F3FF,color:#06101A
classDef offl fill:#5EE6A0,stroke:#5EE6A0,color:#06101A
Login["LoginScreen<br/>RFID tab (default) + PIN tab"]:::screen
Menu["UserMenuScreen<br/>Odbierz · Nadaj · Profil"]:::screen
Pickup["PickupScreen<br/>lista DEPOSITED dla użytkownika"]:::screen
Open["LockerOpenScreen<br/>open / take / close dance"]:::screen
NoP["NoParcelsScreen<br/>sad-face fallback"]:::offl
DepRecip["DepositRecipientScreen<br/>typeahead /deposit/recipients"]:::screen
DepSize["DepositSizeScreen<br/>size + function chips"]:::screen
DepLook["DepositLookupScreen<br/>last-6 tracking lookup"]:::screen
DepOK["DepositSuccessScreen<br/>QR + tracking + recipient"]:::screen
Profile["ProfileScreen<br/>edit phone + locale"]:::screen
Login --> Menu
Menu -->|Odbierz · mam pending| Pickup
Menu -->|Odbierz · brak pending| NoP
Menu -->|Nadaj · employee| DepRecip
Menu -->|Nadaj · courier z trackingiem| DepLook
Menu -->|Profil| Profile
Pickup --> Open
Open --> Login
DepRecip --> DepSize
DepLook --> DepSize
DepSize --> Open
Open --> DepOK
DepOK --> Login
UserMenuScreen gat-uje "Nadaj":
- EMPLOYEE widzi tylko Odbierz.
- ADMIN + COURIER widzą Odbierz + Nadaj.
Login ma 2-flag picker (PL / EN) i live machine name + location z GET /machines/me co 30 s.
InactivityScope — etap 3.5 polish 7¶
Pojedynczy guard na app root zamiast per-screen:
InactivityController— single source of truth.bump()resetuje idle clock;pauseWhile<T>(Future<T>)jest reference-counted (kilka in-flight network calls się nakłada, timer wraca do biegu dopiero gdy wszystko skończy).InactivityNavigatorObserver— wpięty wMaterialApp.navigatorObservers.bump()na każdymdidPush/didPop/didReplace.InactivityGuard— overlay z gradientowym countdown-em (cyan→leaf), "Czy jesteś tam jeszcze?" copy. Idle przezinactivityTimeoutSec(default 5 s) → fade in; countdowninactivityCountdownSec(default 10 s) → bounce do login. Touch dismiss-uje. KiedybusyCount > 0overlay jest ukryty — user czekający na/pickup/by-usernie wyloguje się w środku fetch-u.
Oba timeouts są per-machine, edytowane z panelu (Edit-machine dialog), surfaced przez GET /machines/me.
Brand kiosku — ErgoFlow dark¶
Etap 3.5 (7e0311b) dał kioskowi własny look:
- ink-950 backdrop + 24 ErgoFlow bubbles wolniej niż w panelu (~60–80 s cykle)
- custom PIN pad (3×4 + duże dot indicators) zamiast systemowej klawiatury
- big touch targets (etap-5 polish dorzucił jeszcze większe — etap-3 stał na ~80 px)
- 2-flag picker w header (PL + EN)
- live header chip: machine name + location + sync status
Pełen detal → Kiosk (Flutter).
Deploy kiosku — bootstrap-all.ps1¶
graph LR
A[Goła Win 11<br/>Pro x64] -->|bootstrap-all.ps1| B[Hostname + auto-logon]
B --> C[Pobranie smartbox-kiosk-bundle.zip]
C --> D[Rozpakowanie do<br/>C:/smartbox-kiosk/versions/0.83.0]
D --> E[Junction current → 0.83.0]
E --> F[Scheduled Task<br/>SmartBoxKioskUI]
F --> G[Scheduled Task<br/>SmartBoxKioskAgent]
G --> H[Reboot → kiosk live]
Czas od goła Windows-a do działającego kiosku z UI fullscreen: ~20 minut (głównie pobieranie .NET + reboot). Pełen runbook → infra/scripts/windows-kiosk/RUNTIME.md.
Versioning kiosku¶
lib/core/version.dart exports kAppVersion. OTA agent reportuje to do backendu po udanej instalacji (POST /machines/me/release-assignment/:id/status, installedVersion) — Machine.currentVersion zostaje w sync.
| Etap | kAppVersion |
|---|---|
| 3.0 (skeleton) | 0.30.0 |
| 3.1 (release backend) | 0.31.0 |
| 3.2 (panel UI) | 0.32.0 |
| 3.3 + 3.4 (Flutter scaffold + OTA) | 0.34.0 |
| 3.5 (pickup flow) | 0.35.0 |
| 3.5 polish 1–12 | 0.36.0 … 0.47.0 |
| 3.5 polish 12 final | 0.48.0 |
Aktualny live: 0.83.0 (etap-5 fixes).
Co się zmieniło od etapu 3¶
- Kiosk hardware wszedł w etapie 5 —
ModbusLockerHardwarezamiast mock-a, lifecycle hardening (deposit-into-open guard, PIN clear on fail, defensive close-verify, opening watchdog). Patrz Etap 5. - Offline mode wszedł w etapie 4.5 —
SyncServiceco 2 min ciągniecached_users/cached_credentials/cached_lockers/cached_shipmentsi replay-uje outbox. Patrz Etap 4 · Offline. - Mobile-parity API wszedł w etapie 4.6 —
me-shipmentsJWT-authed endpoints jako foundation pod przyszłe mobilki Android. Patrz Etap 4 · Mobile API. - OTA agent buffer + throttle zoptymalizowany w
bbfefd4(bigger buffer + rare status reports = 140x faster) — z 256 B → 256 KB buffer, status report z każdego packetu → co 5 s. - RFID HID-keyboard auth wszedł w etapie 5 polish (
fc23bf4→d991582) — pięć fallback-ów, ostatecznieWH_KEYBOARD_LLhook w PS bridge. - Cache-first hot paths (etap 5 polish,
59690bf) — login/menu/pickup czyta zcached_*zanim odpyta backend, fallback po 200 ms.
Pełna mapa screens + offline cache + OTA agent → Kiosk (Flutter).