Polish air-quality visualization, end-to-end. Pollmaster fuses three independent data streams onto one Leaflet map:
- GIOŚ ground stations — every active Polish monitoring point, severity-coloured against the WHO 2021 short-term guidelines, with per-pollutant heatmap layers.
- Copernicus CAMS satellite-model — assimilated Sentinel-5P + surface stations over a 10 km European grid; clickable anywhere on the map, no API key needed.
- NASA GIBS WMTS overlays — aerosol optical depth and tropospheric NO₂ as translucent raster tiles, public and unauthenticated.
The backend hides the upstream Polish JSON-LD shape behind clean English contracts, persists a per-station snapshot to disk for cold-start resilience, and orchestrates a Strategy-pattern provider chain for the satellite layer (CAMS by default, OpenWeatherMap as an optional secondary source). A stale-while-revalidate cache plus aggressive client-side rendering keep the UI responsive even when the GIOŚ rate limiter pushes the upstream fetch into the minutes range.
- Quick start
- Architecture
- API surface
- Configuration
- Operations runbook
- Performance characteristics
- Testing
- Deployment
- Troubleshooting
- Versioning
- Roadmap
.\dev-run.ps1 # Debug + https profile, MAUI WinUI client
.\dev-run.ps1 -Configuration Release -BackendProfile lanTwo windows open: the backend (logs streaming in a dedicated PowerShell console) and the MAUI WinUI client (native window).
Two scripts, two scenarios:
# LAN development (phone + PC on the same WiFi, local backend)
.\dev-phone.ps1 # auto-detects LAN IP, USB or paired wireless
.\dev-phone.ps1 -Connect 192.168.0.88:40149 # explicit wireless target
.\dev-phone.ps1 -PairWith 192.168.0.88:41123 -PairCode 123456 # first-time wireless pairing
.\dev-phone.ps1 -SkipBackend # backend already running on the LAN
# Production deploy (phone uses the Railway/Render-hosted backend, works on any network)
.\prod-phone.ps1 # Release build + /healthz preflight + adb deploy
.\prod-phone.ps1 -Connect 192.168.0.88:34667 # explicit wireless target
.\prod-phone.ps1 -SkipHealthcheck # skip /healthz when backend is being swappedprod-phone.ps1 reads the committed Railway URL from appsettings.Android.json,
curls /healthz to fail fast if the backend is down, then builds + deploys without
ever starting a local backend. Use it whenever you want the phone to keep working
outside your LAN (mobile data, holidays, demos).
The script kills lingering build / runtime processes, wipes every bin/ and obj/,
detects the PC's RFC 1918 IPv4 and rewrites Pollmaster/Resources/Raw/appsettings.Android.json
so the client points at the right backend, resolves adb from PATH or the standard
Android SDK locations, optionally pairs / auto-discovers via mDNS, builds backend and
MAUI, then launches both. See Troubleshooting for Wireless
debugging quirks (rotating ports, doze).
# Backend
dotnet run --project Pollmaster.Api --launch-profile https
# MAUI Windows client
dotnet build Pollmaster\Pollmaster.csproj -f net10.0-windows10.0.19041.0
dotnet run --project Pollmaster --framework net10.0-windows10.0.19041.0
# MAUI Android — needs LAN backend (--launch-profile lan) and adb-connected device
dotnet build Pollmaster\Pollmaster.csproj -t:Run -f net10.0-androidThe default deployment ships with the Copernicus CAMS provider enabled — no key, no registration, working immediately. If you also want OpenWeatherMap in the provider chain (richer NH₃ field, OWM's own AQI categorisation), drop your free API key into user secrets:
cd Pollmaster.Api
dotnet user-secrets set "OpenWeatherMap:ApiKey" "your-32-hex-key"Fresh OWM keys take 10–60 min to activate upstream; the chain falls through to CAMS in the meantime so the satellite endpoint stays green.
┌────────────────────────┐
│ GIOŚ public REST │
│ api.gios.gov.pl /v1/ │
└──────────┬─────────────┘
│
┌───────────────┴────────────────┐
│ GiosRateLimitHandler │ sliding window 30 req / 10 s
│ (DelegatingHandler) │
└───────────────┬────────────────┘
│
┌───────────────┴────────────────┐
│ Microsoft.Extensions.Http │ retry + circuit breaker + timeouts
│ .Resilience standard handler │
└───────────────┬────────────────┘
│
┌───────────────┴────────────────┐
│ IGiosApiClient (typed) │ Polish JSON-LD adapter
└───────────────┬────────────────┘
│
┌───────────┬─────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
StationSvc SensorSvc MeasurementSvc AqIndexSvc StationSnapshotSvc
│ │ │ │ │ facade
└───────────┴─────────────┴──────────────┴──────────────┘
│
┌───────────────┴────────────────┐
│ OverviewService │ single-flight gate (Semaphore)
│ + OverviewProjector │ Strategy (WHO ratios)
└───────────────┬────────────────┘
│
┌───────────────┴────────────────┐
│ Stale-while-revalidate cache │
│ IMemoryCache → FileSnapshot │
└───────────────┬────────────────┘
│
┌───────────────┴────────────────┐
│ /api/overview /api/stations │ CORS + OpenAPI
└───────────────┬────────────────┘
│
┌──────────┴──────────┐
│ Pollmaster MAUI │ Blazor Hybrid + Leaflet.js
│ Windows + Android │ + FileOfflineOverviewCache
└─────────────────────┘ (renders cached map < 50 ms,
survives offline / cold-start)
The backend ships in a multi-stage Dockerfile and deploys to Railway (recommended),
Render, Fly.io, Hetzner — any container host. Program.cs binds to the $PORT env var
when present so the same image runs on every PaaS provider unchanged. See
Deployment.
A second pipeline serves satellite / model-assimilated data for arbitrary points on the map. Two layers operate side-by-side:
Raster tiles (Leaflet TileLayer, no auth) Point readings (REST JSON)
┌───────────────────────────────────┐ ┌───────────────────────────────┐
│ NASA GIBS WMTS │ │ Open-Meteo (Copernicus CAMS) │
│ MODIS AOD, OMI NO₂ │ │ /v1/air-quality no API key │
└────────────┬──────────────────────┘ └──────────────┬────────────────┘
│
┌──────────────┴────────────────┐
│ OpenWeatherMap Air Pollution │ optional
│ /data/2.5/air_pollution │ key
└──────────────┬────────────────┘
│
ISatelliteProvider[] (Strategy)
CamsSatelliteProvider (priority 1)
OwmSatelliteProvider (priority 2)
│
SatellitePollutionService
(orchestrator + per-provider cache)
│
/api/satellite/point?lat=…&lon=…
The frontend overlays GIBS as a translucent raster layer for an at-a-glance regional
picture; tapping any point on the map fires /api/satellite/point, which walks the
provider chain (CAMS first, OWM second), returns the first hit, and the UI renders the
same WHO-limit severity bars that the GIOŚ markers use. The DTO carries a source field
so the popup attributes the reading to the upstream that answered.
Pollmaster.slnx
├── Pollmaster.Shared\ Class library — API contracts shared between client and backend
├── Pollmaster.Api\ ASP.NET Core 10 backend (GIOŚ proxy + cache + REST API)
├── Pollmaster.Api.Tests\ xUnit tests (mappers, severity, projector, dedupe, snapshot store)
├── Pollmaster\ .NET MAUI Blazor Hybrid client (Leaflet map UI)
├── Dockerfile Multi-stage Alpine image for the backend (Railway/Fly/Render/Hetzner)
├── railway.toml Railway deployment manifest (Dockerfile builder + /healthz probe)
├── dev-run.ps1 Windows dev loop (kill / clean / build / run backend + MAUI)
├── dev-phone.ps1 Android phone LAN dev loop (mDNS auto-connect + adb deploy)
└── prod-phone.ps1 Android phone production deploy (Railway URL + /healthz preflight)
| Pattern | Where | Why |
|---|---|---|
| Adapter | Pollmaster.Api.Gios.Mapping.* |
Translate Polish JSON-LD to English Pollmaster.Shared.Contracts |
| Gateway | IGiosApiClient + GiosApiClient |
Isolate HTTP / JSON details from business logic |
| Facade | StationSnapshotService |
Compose station + sensors + index + readings behind one call |
| Strategy | IWhoLimitProvider, ISeverityCalculator, IOverviewProjector |
Swappable rule tables / projection |
| Strategy + Chain | ISatelliteProvider + CamsSatelliteProvider + OwmSatelliteProvider |
Try multiple satellite sources in priority order; new providers slot in as one class |
| Single-flight gate | OverviewService static SemaphoreSlim |
Only one GIOŚ fan-out at a time, even under concurrent calls |
| Stale-while-revalidate | OverviewService.GetOverviewAsync + RefreshIfStaleAsync |
Always serve cached data; refresh in the background |
| Background service | OverviewCacheWarmupService |
Hosted BackgroundService that warms the cache every 30 min |
| Composite cache | Memory → disk → GIOŚ rebuild | Each layer is independently testable; cold start uses disk, warm start uses memory |
| Delegating handler chain | GiosRateLimitHandler → resilience handler |
Cross-cutting concerns (throttling, retry, circuit breaker) decorate the HTTP client |
| Discriminated union | Result<T> in Pollmaster.Shared.Common |
Expected failures travel as values, exceptions are reserved for genuine bugs |
- SRP —
OverviewServiceowns caching and concurrency; the pure projection lives inOverviewProjector. Mappers, services, gateways and stores are all separate types. - OCP —
ISeverityCalculator,IWhoLimitProvider,IOverviewProjectorlet you swap rule tables (EEA index, different thresholds, alternative ratios) without touching the orchestration. - LSP — tests substitute concrete
WhoLimitProvider/WhoSeverityCalculatorintoOverviewProjectorand assert behaviour without mocks. - ISP —
IGiosApiClient,IOverviewSnapshotStore,IOverviewProjector,IPollmasterApiClient,IMediaCaptureService— each interface is tightly scoped. - DIP —
Microsoft.Extensions.DependencyInjectioneverywhere. HTTP clients viaIHttpClientFactory; cache viaIMemoryCache; logging via source-generatedLoggerMessageextensions (OverviewServiceLog).
FrozenDictionaryinWhoLimitProvider— built once at type init, faster lookups on the hot path of every projection.Parallel.ForEachAsync+ConcurrentBagfor the station fan-out — bounded concurrency (3) without the manualSemaphoreSlim+Task.WhenAllallocation path.LoggerMessagesource-gen (OverviewServiceLog) — every overview hot-path log call has a static event id and zero-allocation argument formatting unless the level is enabled.- Outbound rate limiter —
SlidingWindowRateLimitershared across HTTP pipelines via the singletonGiosRateLimiter, well under the GIOŚ documented limits. - HTTP 400 ≡ no-data — the gateway treats 400 from
/data/getDataas "retired sensor", caches an empty result, and stops the rate-limit storm. - Single-flight gate — concurrent overview requests do not pile up duplicate GIOŚ fan-outs; later callers see the freshly-populated cache after the gate releases.
- Disk snapshot survives restarts —
cache/overview-<timestamp>.jsonpreserves the last computed overview, served immediately on backend boot (stale-while-revalidate).
| Method | Route | Description |
|---|---|---|
| GET | /healthz |
Liveness — always 200 OK while the process is up |
| GET | /healthz/ready |
Readiness — JSON with per-check status |
| GET | /api/stations |
All GIOŚ stations |
| GET | /api/stations/{id} |
Single station |
| GET | /api/stations/{id}/sensors |
Sensors at the station |
| GET | /api/stations/{id}/index |
Current air-quality index |
| GET | /api/stations/{id}/snapshot |
Combined station + index + latest readings |
| GET | /api/sensors/{id}/readings |
Recent measurement series for one sensor |
| GET | /api/overview |
Lightweight per-station projection (severity, critical pollutant, WHO ratios). Drives marker colours and heatmap layers. |
| GET | /api/satellite/status |
Whether the satellite provider is configured. |
| GET | /api/satellite/point?lat=&lon= |
Satellite/model-assimilated air-pollution reading at a point. 503 when no API key is configured. |
| Dev | /openapi/v1.json |
OpenAPI document (Development environment only) |
/healthz/ready returns a structured JSON breakdown of every registered IHealthCheck,
suitable for Kubernetes / Docker / Azure App Service readiness probes:
{
"status": "Healthy",
"totalDurationMs": 12.4,
"entries": {
"overview-cache": { "status": "Healthy", "description": "Overview in memory (287 stations).", "durationMs": 0.5, "data": { "stations": 287, "source": "memory" } },
"gios-reachability": { "status": "Healthy", "description": "GIOŚ reachable (200).", "durationMs": 89.2, "data": {} }
}
}All settings live in Pollmaster.Api/appsettings.json (or environment-specific
overrides like appsettings.Production.json). Every section binds to a strongly-typed
options class with ValidateOnStart.
{
"Gios": {
"BaseAddress": "https://api.gios.gov.pl/",
"TimeoutSeconds": 30,
"Cache": {
"StationsTtlMinutes": 120,
"SensorsTtlMinutes": 60,
"IndexTtlSeconds": 1800,
"MeasurementsTtlSeconds": 1800,
"SnapshotTtlSeconds": 1800
}
},
"Cors": {
"AllowedOrigins": [ "*" ]
},
"Warmup": {
"Enabled": true,
"InitialDelaySeconds": 5,
"IntervalSeconds": 1800
},
"OverviewPersistence": {
"Directory": "cache",
"FreshnessMinutes": 30,
"RetainCount": 5
},
"OpenWeatherMap": {
"BaseAddress": "https://api.openweathermap.org/data/2.5/",
"ApiKey": "",
"TimeoutSeconds": 10,
"CacheTtlSeconds": 600
},
"Cams": {
"BaseAddress": "https://air-quality-api.open-meteo.com/",
"TimeoutSeconds": 10,
"CacheTtlSeconds": 600,
"Enabled": true
}
}| Section | Knob | Default | What it does |
|---|---|---|---|
Gios.BaseAddress |
string | …gios.gov.pl |
Upstream GIOŚ base URL. Replace for a mock or proxy. |
Gios.TimeoutSeconds |
int | 30 | Per-request HttpClient timeout. |
Gios.Cache.* |
int | varies | Per-resource memory cache TTL. Bumped well over warmup interval so the cache stays hot. |
Cors.AllowedOrigins |
string[] | ["*"] |
CORS origins for the MAUI client. Restrict in production. |
Warmup.Enabled |
bool | true | Master switch for the background cache warmer. |
Warmup.IntervalSeconds |
int | 1800 | Rebuild cadence. Default matches GIOŚ hourly refresh. |
OverviewPersistence.Directory |
string | cache |
Folder for overview-<timestamp>.json files. Relative paths resolve against ContentRoot. |
OverviewPersistence.FreshnessMinutes |
int | 30 | Threshold at which the warmup considers the disk snapshot stale and rebuilds it. |
OverviewPersistence.RetainCount |
int | 5 | How many historical snapshots to keep on disk after each save. |
OpenWeatherMap.ApiKey |
string (optional) | empty | OpenWeatherMap Air Pollution API key. When empty, OWM is skipped in the provider chain; with CAMS enabled the satellite endpoint still answers. Sign up free at openweathermap.org for 1 000 req/day. |
OpenWeatherMap.CacheTtlSeconds |
int | 600 | Per-point cache lifetime for the OWM provider — coordinates are rounded to ~110 m before hashing so neighbouring taps share the hit. |
Cams.Enabled |
bool | true | Master switch for the CAMS (Copernicus, via Open-Meteo) provider. No key required — disable only when forcing OWM-only behaviour in tests. |
Cams.CacheTtlSeconds |
int | 600 | Per-point cache lifetime for the CAMS provider. Same ~110 m grid hashing as OWM, but stored under a separate cache key so OWM failures cannot poison CAMS hits. |
Pollmaster/Resources/Raw/appsettings.json (and appsettings.Android.json override):
{
"PollmasterApi": {
"BaseAddress": "https://localhost:7100/",
"TimeoutSeconds": 120
}
}Android emulator defaults to http://10.0.2.2:5100/ (host loopback alias). A physical
phone needs the PC's LAN IP — dev-phone.ps1 rewrites this automatically.
Structured logs go to Microsoft.Extensions.Logging. In Development the console sink
is the default. Notable event ids (defined in
Pollmaster.Api.Services.OverviewServiceLog):
| Event id | Level | Meaning |
|---|---|---|
| 1001 | Info | Overview rebuilt — payload size and applied TTL |
| 1002 | Info | Disk snapshot served (fresh) |
| 1003 | Info | Stale-but-served disk snapshot — warmup will refresh |
| 1004 | Info | Warmup skipped — existing snapshot already fresh |
| 1005 | Warning | Per-station build failed — empty entry surfaced for that id |
| 1006 | Warning | Disk snapshot load failed |
| 1007 | Warning | Disk snapshot persist failed |
Read it at any time via /healthz/ready. Watch the overview-cache entry — a
Degraded state usually means the disk snapshot is stale and the warmup is still
running. An Unhealthy state means the file does not exist on disk at all and the
warmup has not produced one (cold start, or persistent failure to fetch).
Pollmaster.Api/cache/overview-yyyyMMddTHHmmssfffZ.json. Safe to inspect, copy, or
delete — the next warmup rebuilds. Retention defaults to 5 newest files.
# delete the freshest snapshot — next request rebuilds
Remove-Item Pollmaster.Api\cache\overview-*.jsonOr kill the backend and restart — the warmup tick (after InitialDelaySeconds) will
rebuild if RefreshIfStaleAsync decides the disk copy is too old.
# Watch all GIOŚ outbound traffic in the backend window
$env:Logging__LogLevel__System.Net.Http.HttpClient = "Information"
dotnet run --project Pollmaster.Api --launch-profile lanThe IGiosApiClient chain logs every outgoing request, the Polly attempt, and the
final response. Look for repeated 400 against /data/getData/{id} — that is GIOŚ
flagging retired sensors and the gateway will cache them as no-data.
| Scenario | Latency |
|---|---|
/api/overview — memory cache hit |
< 5 ms |
/api/overview — disk cache hit (cold restart) |
60–150 ms |
/api/overview — full rebuild (cold cache) |
5–7 min |
| Station popup (renders from JS overview state) | < 5 ms, no backend call |
/healthz |
< 1 ms |
/healthz/ready |
50–200 ms (GIOŚ probe) |
| Warmup cycle (every 30 min, cache fresh) | < 10 ms (just disk check) |
| Warmup cycle (cache stale, full rebuild) | 5–7 min |
The single-flight gate guarantees that concurrent first-load requests do not multiply the cold-cache latency — only one rebuild ever runs.
GIOŚ rate limits (documented by the provider):
/data/getData/*— 2 req/min (archive) or 1500 req/min (current data)/aqindex/getIndex/*— 1500 req/min/station/sensors/*— 1500 req/min/station/findAll— 2 and 1500 req/min depending on endpoint
The outbound GiosRateLimitHandler is conservatively configured at 30 req / 10 s
(180 req/min), comfortably under the lower envelope.
dotnet test # runs every test project in the solution
dotnet test --logger "console;verbosity=detailed" # verbose output
dotnet test /p:CollectCoverage=true # generate coverage dataThe xUnit suite (Pollmaster.Api.Tests) covers:
- Mappers —
StationMapperTests,MeasurementMapperTests,AirQualityIndexMapperTests,SensorMapperTests. Verify the Polish-to-English translation, the timestamp parsing, and the JSON-LD field-name quirks of GIOŚ. - WHO + severity —
WhoLimitProviderTests(case-insensitive lookup, unknown returns null),WhoSeverityCalculatorTests(parameterised over each bucket of the six-step palette). - Projector —
OverviewProjectorTests(empty input, official-vs-derived severity, worst-ratio selection, pollutants without values). - Snapshot dedupe —
SnapshotDeduplicationTests(freshness preference per pollutant code, ordering of empty readings, multiple sensors per pollutant). - Result<T> —
ResultTests(success / failure invariants, access semantics). - Disk snapshot store —
FileOverviewSnapshotStoreTests(round-trip, retention, prune behaviour) using a temp directory.
Minimum 80 % on new code, 100 % on critical paths (per the project's
MEMORY.mddevelopment directives). Rundotnet test /p:CollectCoverage=trueto generate thecoverage.cobertura.xmlbaseline; pair withreportgeneratorfor HTML reports.
- End-to-end against the real GIOŚ API. The gateway is intentionally not exercised against the live service in unit tests; the OpenAPI document is the only contract.
- MAUI UI smoke tests. The MAUI client is verified manually via
dev-run.ps1(Windows) anddev-phone.ps1(Android). For automated UI testing add an Appium / Maui.UITest project — not present yet.
Pull requests trigger .github/workflows/tests.yml:
testjob — restore Shared / Api / Tests, build Release, rundotnet test.code-qualityjob — single-author check (must beJakub Syrek <…>), rejects any commit message carrying an AI co-author line.
Both must pass before merge.
dotnet publish Pollmaster.Api -c Release -o publish/ --self-contained falseCopy publish/ to the target host. Run as a Windows Service (sc.exe create) or a
Linux systemd unit. Reverse-proxy with Nginx / IIS for TLS termination — Pollmaster.Api
itself only serves HTTP in production unless you bind a certificate explicitly.
Mandatory production settings:
Cors:AllowedOriginsrestricted to known clients (https://your.app) — never*.Warmup:Enabled=trueso cold-start latency lands on the warmup, not the user.- Persist
OverviewPersistence:Directorysomewhere durable (e.g./var/lib/pollmaster/cache/).
The repo ships with a production Dockerfile at the root and a railway.toml deploy
manifest. Railway auto-detects both and gives you a stable HTTPS URL like
pollmaster-production.up.railway.app.
One-time setup:
- Sign in at railway.app with GitHub (no credit card needed for sign-in — only when you actually subscribe to Hobby).
- New Project → Deploy from GitHub repo →
Pollmaster. - Service settings → Root Directory: leave blank (Dockerfile is at repo root).
- Service settings → Variables:
OpenWeatherMap__ApiKey= your OWM key (optional — CAMS works without it).Cors__AllowedOrigins__0= your MAUI client origin or*for the personal phone.
- Service settings → Networking → Generate Domain (or attach a custom one).
- Copy the URL into
Pollmaster/Resources/Raw/appsettings.Android.json→PollmasterApi.BaseAddress, then redeploy the MAUI client.
Every subsequent git push origin main triggers a redeploy. The
OverviewCacheWarmupService keeps the cache hot continuously — Railway's Hobby plan
does not sleep idle containers, so the warmup actually runs, unlike scale-to-zero free
tiers.
The same Dockerfile runs unchanged on every container host. Build and ship:
docker build -t pollmaster-api .
docker run -d --name pollmaster -p 8080:8080 -v pollmaster-cache:/app/cache `
-e OpenWeatherMap__ApiKey=$env:OWM_KEY pollmaster-apiThe image listens on $PORT when injected (Railway / Render / Heroku style) and falls
back to 8080 otherwise. The /app/cache volume preserves the disk snapshot across
restarts — first hit after a redeploy serves the persisted file immediately instead of
waiting on a fresh GIOŚ fan-out.
livenessProbe:
httpGet:
path: /healthz
port: 5100
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /healthz/ready
port: 5100
initialDelaySeconds: 15
periodSeconds: 30The readiness probe returns Degraded (HTTP 200) when the disk snapshot is stale —
configure your platform to treat that as "send traffic, but flag in monitoring".
The Blazor client persists every successful /api/overview response to
FileSystem.AppDataDirectory (FileOfflineOverviewCache). On startup the map renders
from that snapshot before the network request returns, so the user sees populated
markers within ~50 ms even when:
- the device is offline (no Wi-Fi, no mobile data, plane mode);
- the backend is cold-starting on a sleepy free-tier host;
- the backend is down entirely.
The header status line indicates the state (Offline cache: 290 stations · 12 min old · refreshing…) and is replaced when the network refresh completes. An empty server
response never overwrites a good snapshot — see FileOfflineOverviewCache.SaveAsync.
| Target | Command |
|---|---|
| Windows MSIX | dotnet publish Pollmaster -f net10.0-windows10.0.19041.0 -c Release -p:WindowsPackageType=MSIX |
| Android APK | dotnet publish Pollmaster -f net10.0-android -c Release -p:AndroidPackageFormats=apk |
| Android AAB | dotnet publish Pollmaster -f net10.0-android -c Release -p:AndroidPackageFormats=aab |
For Play Store distribution, sign the AAB with -p:AndroidSigningKeyStore=... and the
matching password / alias options. See the
official MAUI Android publishing guide.
Merging to main triggers version.yml which parses the commit history since the
last tag, decides the bump type from Conventional Commits,
updates every <Version> in the solution's .csproj files, tags the commit, and
creates a GitHub Release. No manual version edits.
| Commit type | Bump |
|---|---|
feat: |
minor |
fix: / docs: / test: / refactor: / perf: |
patch |
BREAKING CHANGE: |
major |
The disk snapshot was never written (cold start, warmup still running) or the GIOŚ
fan-out is rate-limited. Wait 5–7 minutes for the first warmup cycle to complete, or
check /healthz/ready — overview-cache should flip from Unhealthy to Healthy
once the warmup finishes.
Android rotates the Wireless-debugging connect port whenever the screen turns off or
the user leaves the Wireless debugging settings screen. The script retries with
mDNS re-discovery up to 4 times — if you still see refused connections, keep the
Wireless debugging screen open and the phone awake while running dev-phone.ps1.
Also worth checking:
- Phone and PC are on the same Wi-Fi / LAN (Hyper-V virtual switches are excluded by the script's IP auto-detection).
- Windows Firewall has allowed inbound traffic on TCP 5100 for
dotnet.exe.
GiosRateLimitHandler caps outbound calls at 30 req / 10 s, well under the documented
GIOŚ limits. If you still hit 429s, GIOŚ may have lowered the per-IP rate or you have
multiple Pollmaster backends sharing the same egress IP. Reduce
Warmup.IntervalSeconds to 3600 (every hour) so each warmup tick spends less time in
the rate-limit queue.
Pre-1.10 the resilience handler on the client defaulted to a 30 s total request
timeout, which killed the cold /api/overview call. The current configuration
(Pollmaster/MauiProgram.cs) sets a 120 s total timeout that matches the worst-case
rebuild. If you forked an older version, copy the AddStandardResilienceHandler
options block.
The MAUI Android target builds independently; dotnet build -t:Run -f net10.0-windows10.0.19041.0
does not redeploy the Android APK. Run .\dev-phone.ps1 to push the latest build.
Pollmaster follows Semantic Versioning. See
CHANGELOG.md and Releases
for the full history. The MAUI app's display version, the API assembly version and
the shared library version are all synchronised by version.yml.
- OpenTelemetry metrics — counters for cache hits / misses, GIOŚ response times, rate-limit waits, overview rebuild durations.
- Hosted iOS build — the MAUI client targets Android + Windows today; iOS / Mac
Catalyst frameworks are wired into the
.csprojbut never built by CI. getDisplayMediacapture on WebView2 — better recording quality on Windows when available, falling back to the html2canvas snapshot loop on Android.- Distributed cache backend — swap
FileOverviewSnapshotStorefor a Redis-backed layer behind the sameIOverviewSnapshotStoreinterface for multi-instance deployments.
- SECURITY.md — vulnerability reporting
- CHANGELOG.md — release history
- ABOUT.md — short project summary
- .github/BRANCH_PROTECTION.md — required GitHub settings
Air-quality data is provided by GIOŚ under the portal regulations. Pollmaster code is released under the MIT license.