# Architecture ## Shape of the system ``` DPC radar API ──┐ │ (worker only: origin header, presigned S3, 5-min cadence) ARPA CAP feed ──┤ ▼ Python worker ──► object storage / CDN (crop, reproject, manifest.json colourise, render) frames/*.png alerts.json cells.json │ ▼ Flutter app ◄── api.met.no (direct, identifying User-Agent) ``` **The app never talks to DPC or ARPA directly.** Both would be rate-limited by thousands of clients, DPC presigned URLs expire in minutes, and the rasters are whole-Italy GeoTIFFs that a phone should not decode. The worker is the only client of those services, and it fans out through a CDN. MET Norway is the exception: it is designed for per-client access, its license permits it, and forecasts are per-location so a shared cache would not help. ## Monorepo ``` Nuvolari/ ├─ app/ Flutter application (Dart package "nuvolari") ├─ backend/ Python worker ├─ docs/ this documentation └─ tool/ verification scripts ``` ## App layers ``` lib/ ├─ core/ cross-cutting, no feature knowledge │ ├─ config/ Env (dart-define), feature flags │ ├─ region/ RegionConfig + asset loader │ ├─ net/ Dio client, User-Agent and retry interceptors │ ├─ cache/ FrameCache (disk + memory LRU) │ └─ l10n/ localisation plumbing ├─ data/ one folder per domain, each exposing an interface │ ├─ radar/ RadarSource + Dpc/Arpa/Mock implementations + models │ ├─ forecast/ ForecastSource + MetNo/IconIt2 implementations │ └─ alerts/ AlertSource + ArpaCap implementation ├─ features/ one folder per screen or coherent UI area │ ├─ map/ timeline/ forecast/ alerts/ sources/ consent/ ads/ └─ l10n/ app_it.arb (template) ``` Dependencies point inwards: `features` depends on `data`, `data` depends on `core`, `core` depends on nothing in the app. A feature never imports another feature. ## The adapter seam ```dart abstract interface class RadarSource { Future getLatestManifest(); Future> getFrames(); } ``` Three implementations: | Implementation | Status | Purpose | |---|---|---| | `MockRadarSource` | active | Synthetic frames from assets. Runs with no network and no credentials — the default in tests and in demo mode. | | `DpcRadarSource` | active | Reads `manifest.json` and PNG frames from our CDN. | | `ArpaRadarSource` | **disabled stub** | Placeholder until ARPA authorization exists. Throws if constructed while its feature flag is off. | The active source is resolved from the region config plus a runtime flag, so switching sources is configuration, never a code change. `ForecastSource` and `AlertSource` follow the same pattern. Because `MockRadarSource` is a first-class implementation rather than test scaffolding, the whole UI — timeline, scrubbing, prefetch, cache eviction, degraded states — is exercisable offline. ## Data contract `manifest.json`, published by the worker and consumed by the app: ```json { "region": "piemonte", "product": "VMI", "generatedAt": 1758706260000, "bbox": [6.55, 43.95, 9.30, 46.55], "crs": "EPSG:3857", "frames": [ { "ts": 1758706200000, "url": "frames/VMI/1758706200000.png" } ], "legend": { "unit": "dBZ", "stops": [{ "value": 5, "color": "#4FA3D1" }] }, "attribution": "Radar-DPC — CC BY-SA" } ``` Frame URLs are relative to the manifest so the whole tree can be moved between hosts. The legend travels with the data: the app draws whatever the worker produced rather than hardcoding a palette that could drift from the rendering. ## Degradation Failure is normal here — the worker can be behind, a frame can be missing, the phone can be offline. The rules: - Manifest unreachable → keep the last good manifest from cache, show a "dati non disponibili" banner with the age of the newest frame. - Individual frame missing → hold the previous frame in the timeline; never a blank map. - No frames at all → the map and the forecast still work; only the radar layer is empty. - Animation stops when the app leaves the foreground (`AppLifecycleState`) so a backgrounded app never burns battery prefetching. The banner always states **when** the data is from. Stale radar shown as if current is worse than no radar. ## Privacy by construction No user location ever reaches a server. Forecast requests go from the device straight to MET Norway. Rain notifications work by the device subscribing to FCM topics named after geographic cells — the subscription happens on the device, so the backend holds no user records at all.