Etap 28 — Media offline-first (zdjęcia produktów + PDF hostowane u nas)¶
Zaimplementowane + WDROŻONE 2026-06-17 (backend migracja 20260618_etap28_media, kiosk 1.5.70)
Zdjęcia produktów/kategorii i PDF-y (regulaminy/info) są hostowane u nas (S3/MinIO, z fallbackiem na dysk w dev/VM), a kiosk pobiera je raz i trzyma lokalnie (offline-first). Wcześniej były to gołe zewnętrzne URL-e wklejane ręcznie, pobierane per-render bez cache.
Cel¶
- Admin wgrywa plik w kartotece zamiast wklejać URL (zewnętrzny link dalej działa jako opcja).
- Kiosk renderuje z dysku — zdjęcia i PDF działają offline (komplementarne do Offline hardening).
- Storage zgodny z kontraktem przyszłej platformy deploy (BILLvis): MinIO przez S3 API, path-style, bucket=namespace, stream przez backend (MinIO in-cluster only → presigned nie dociera do przeglądarki/kiosku).
Architektura¶
Backend — port MediaStorage (28.1)¶
- Interfejs
MediaStorage(put/getStream/exists/delete) z dwoma adapterami, wybór z ENV przy boot:S3Storage— gdy ustawioneS3_ENDPOINT+S3_BUCKET. Czyta kontraktS3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY/S3_REGION/S3_FORCE_PATH_STYLE(@aws-sdk/client-s3).DiskStorage— fallback (MEDIA_DIR, domyślnie/data/media) dla dev + obecnego single-VM, dopóki nie ma MinIO. Nie dla docelowego k8s (brak PVC → tam S3 obowiązkowe).
- Model
Media— content-addressed:sha256(unique) = klucz obiektu (media/<sha256>), dedup za darmo;contentType/size/kind(IMAGE|PDF|OTHER)/originalName/uploadedById. POST /media(ADMIN, multer, walidacja MIME + rozmiar 25 MB) → hash + dedup + put →{ id, url:"/media/<id>" }.GET /media/:id(PUBLIC — id niezgadywalny uuid, treść jawna;Cache-Control: immutable+ETag=sha) → backend streamuje bajty z magazynu (nie presigned URL).- Nullable FK
Item.photoMediaId/ItemGroup.imageMediaId/ItemDocument.mediaId— zarezerwowane na przyszły manifest/integrity. Legacy*Urlzostają.
URL = relatywne /media/<id> (28.2)¶
Po wgraniu web zapisuje relatywne /media/<id> wprost w istniejących polach photoUrl/imageUrl/url. Dzięki temu wszystkie serializery (catalog, /machines/me/sync, kiosk) działają bez zmian — przepuszczają string dalej. Klienci resolvują /media/<id> → swój API base (kontrakt „nie zaszywaj hosta backendu"). Brak osobnego manifest-endpointu — kiosk czerpie referencje z już synchronizowanego photoUrl.
Web — upload w kartotece (28.3)¶
MediaUploadButton (file picker → server action uploadMedia → POST /media) przy polu zdjęcia produktu i URL dokumentu. Podgląd miniatury. Helper mediaSrc(url): relatywne /media/<id> → API base, zewnętrzne URL bez zmian. Lista + link PDF renderują przez mediaSrc.
Kiosk — cache offline-first (28.4, 1.5.70)¶
MediaCache(singleton,attach(ApiClient)wapp.dart):/media/<id>jest immutable, więc „plik na dysku?" wystarcza za inwalidację. Cache wC:\smartbox-kiosk\media\<id>.resolveUrlrelatywne → apiBase.- Lazy cache przy renderze + precache na każdym syncu (
cached_items.photoUrl LIKE '/media/%'). KioskMediaImagezastępujeImage.network(wydawanie: kafel/karta/hero; inwentaryzacja: kolumna) — cache→Image.file(offline), inaczejImage.network+ dociąg w tle.- PDF viewer: hosted
/media/<id>→ bajty z cache (offline-first); zewn. URL →http.getjak dotąd. ApiClient:getBytes(path)+apiBasegetter.
Deploy (28.5)¶
- Compose: wolumen
media_files:/data/media+ ENVMEDIA_DIRiS3_*(puste → dysk; ustawione → S3 auto-switch) na backendzie. - Wdrożone 2026-06-17: backend (migracja) + web na VM, kiosk 1.5.70 STABLE →
ota.auto-rolloutna 5 maszyn. Backend wystartował na DiskStorage (/data/media) — brak MinIO na tej VM.
Gdy pojawi się MinIO¶
Wystarczy podać backendowi ENV S3_ENDPOINT + S3_BUCKET + S3_ACCESS_KEY + S3_SECRET_KEY (region/path-style mają sensowne domyślne) i zrestartować — przełącza się na S3 automatycznie, zero zmian w kodzie. Nowe pliki lecą do MinIO; stare z dysku można domigrować osobno (opcjonalnie). Pełny kontrakt platformy: C:\Projekty\BILLvis\platform\CONTRACT.md (S3 ENV + reszta wymagań deploy).
Otwarte / opcjonalne¶
- Backfill istniejących zewnętrznych URL-i do
Media(gdyby chcieć wszystko hostować). - Cap/LRU na katalog media kiosku (na razie bez limitu).
- Wykorzystanie FK
*MediaIdna manifest „które media na tej maszynie" + integrity (onDelete: SetNull). - Backup wolumenów (
media_files,release_files,postgres_data) — wspólny follow-up.