This document tracks proposed WeatherTool improvements. Work should be delivered in small, reviewable phases rather than as one large rewrite. Each phase should leave the application runnable and independently testable.
- **Windows source workspace:** source editing, review, and Git operations only; do not install dependencies, compile, build, run, or test here.
- **Rocky development and staging:** the sole compile, build, development-runtime, and test environment, with production-like Docker staging at `http://192.168.1.101:9190`.
- **Git over SSH:** Windows pushes reviewed commits to a private bare repository on Rocky; the Rocky staging checkout pulls those commits and rebuilds.
- **Ubuntu VPS deployment:** release `b0b58d2` is publicly operational behind Cloudflare strict TLS, Nginx, and Authelia, ingesting real LVĢMC open-data station/water-temperature observations on a schedule; the `weather` table was wiped and re-populated fresh once the UTC/local timezone mismatch between sources was fixed. FTP (`ENABLE_LVGMC_FTP_JOBS`) stays `false` on both Rocky and the VPS pending a real answer from the LVGMC contact about the `ltv` account's connection/rate policy — see Phase 7 for the full, still-unresolved diagnosis. The VPS does not compile or build the project.
- [ ] Add security tests for invalid fields, filenames, offsets, lengths, and date ranges. Highest-priority target per an independent review: `ValidateFileName`/`ValidateField`/`ValidateInt` are pure functions with zero I/O and are currently the entire path-traversal/SQL-injection security boundary, verified only by manual curl.
- [ ]`PostgresService.query`'s "list" branch (`byField` match, ~13 hardcoded literal cases, no `case _ =>`) is only safe today because `ValidateField`'s allowlist and `WeatherData`'s case class fields happen to stay in sync with it — nothing enforces that. Add a field to `WeatherData` without updating this match and any request for it with `granularity=hour` throws an uncaught `MatchError` (a real risk given the DMI/open-data provider work in progress). A test iterating every `WeatherData.getKeys` value through this path would catch it before it ships; needs either a test-container Postgres or refactoring SQL-fragment-building apart from execution so it's testable without a DB.
- [x] Add frontend type checking (`npm run typecheck`, part of the dependency-maintenance work in Phase 3) — now also runs automatically in CI (see below). Critical workflow smoke tests remain manual/headless-browser only, not automated.
- [ ] Run tests automatically before staging deployment. Partial: Gitea Actions CI (see `CONTINUOUS_INTEGRATION.md`) now runs `sbt test` and the frontend typecheck/build/audit routine automatically on every push/PR to `codex/staging-baseline` — this is CI, not CD; it isn't yet wired as a required gate before Rocky/VPS deployment, which remains a manual decision independent of CI status.
The first observed frontend install reported 15 vulnerabilities: 1 critical, 10 high, 3 moderate, and 1 low. The frontend dependency tree was reviewed in controlled groups, obsolete packages were removed, and both the complete and production-only npm audits now report zero known vulnerabilities at commit `de6f279`.
- [x] Establish that the production dependency audit is primarily blocked by `solid-js@1.9.4` resolving vulnerable `seroval@1.2.0`; confirm that the full audit also contains development-tool advisories.
- [x] Update direct frontend dependencies in controlled groups.
- [x] Replace or remove obsolete frontend packages where appropriate.
- [x] Add a committed TypeScript typecheck command and validate the updated frontend with a clean `npm ci`, typecheck, and production build.
- [x] Resolve the remaining transitive build-tool advisories after reviewing the proposed `npm audit fix` changes; verify full and production-only audits at zero.
- [x] Build, smoke-test, checksum, transfer, and deploy a commit-addressed VPS release containing `de6f279` or later without restarting PostgreSQL or Authelia. (`ef64895` deployed 2026-08-22.)
- [ ] Visually compare every page and representative exported PNG in the deployed dependency-maintenance release.
- [ ] Rotate and remove the API key exposed in a source comment (`src/main/scala/fetch/dmi/FetchService.scala:56`, a DMI HARMONIE key introduced 2025-02-01; confirmed the repo's GitHub `origin` is public, so this has been externally exposed for roughly 18 months — decision on rotation vs. a git-history rewrite is still open).
- [x] Remove credentials/internal error details from connection-error messages: `/api/fetch/lvgmc/stations` was returning `error.getMessage` straight to the caller; now logs it server-side and returns a generic message.
- [ ] Protect or remove debug and administrative endpoints. Partial: `/api/fetch/lvgmc/stations` (the one that could trigger a real FTP login) is now gated behind `ENABLE_LVGMC_FTP_JOBS`, but `/api/debug/time`, `/api/debug/folder-structure`, and `/api/debug/delete-tmp` remain unauthenticated.
- [ ] Introduce closed, validated weather-field and aggregation types. Partial: the one route that let a raw field string reach SQL now validates against `WeatherData.getKeys` (see below), but this is a route-level allowlist, not a closed type threaded through the codebase.
- [x] Eliminate raw user-controlled SQL identifiers: `/api/query/city/.../{field}/...` let an arbitrary path segment reach `Fragment.const` unescaped; added `ValidateField`, the same allowlist-against-`WeatherData.getKeys` pattern `/query/country` already used.
- [x] Validate and constrain filenames, resolved paths, offsets, and byte lengths: added `ValidateFileName` (allowlist regex, rejects `..` and separators including decoded `%2f`) and applied it to every route that concatenated a raw path segment into a filesystem or FTP path (`/debug/file`, `/show/grib`, `/grib/binary-chunk`, `/show/lvgmc-forecast`); hardened `ValidateInt` to reject negative integers; capped `binary-chunk` read length at 64MB to remove an unbounded allocation.
- [ ] Remove explicit `System.gc()` calls. Confirmed still present in `DataService.getBinaryChunk` (runs on every GRIB binary-chunk request — likely a latency source on what's probably a hot path; a commented-out `logMemory` block next to it suggests leftover debugging code, not an intentional design choice), per a 2026-08-24 independent review.
- [ ] Supervise scheduled jobs independently instead of recursively restarting the application. Confirmed still fully live by a 2026-08-24 independent review: `Scheduler.scheduleTask` has zero `.attempt`/error handling on any task, none of the task bodies wired into it (FTP fetch, open-data fetch, cleanup, GRIB fetch) handle their own errors either, and `(serverTask, scheduledTasks).parMapN(...)` means any one failure cancels the HTTP server itself — this is the same defect that caused the actual VPS/Rocky crash-loop incidents earlier this session, patched around with feature flags rather than closed. `OpenDataStationService` also has no client timeout configured, unlike the DMI fetch service. `WarningService`/`WaterTemperatureService` already show the correct pattern (`.attempt` + stale-cache fallback) the scheduler tasks should follow.
- [ ] Add a pooled JDBC transactor. `DBConnection.scala` uses `Transactor.fromDriverManager`, which opens a brand-new physical connection for every query — doobie's documented behavior for scripts/tests, not production services. Not urgent at current traffic, but an easy, well-known swap (`HikariTransactor`) worth doing before real load.
- [x] Harden `WarningService`: bound LVĢMC retry attempts to once per cache window during an upstream outage, isolate one warning's polygon-fetch failure from the rest instead of failing the whole response, verify the CKAN polygon filter against real column-type ambiguity rather than guessing, and log (rather than silently swallow) a defaulted vertex-order value.
- [x] Add a separate Latvian-named Brīdinājumi workspace that renders current LVĢMC warning polygons without manual tracing.
- [x] Add production border overlay artwork and feathered warning-polygon fills to Brīdinājumi.
- [x] Replace Brīdinājumi's 4-corner bounding-box projection with an affine fit calibrated against the same validated Kartes/Faktiskā city pixel positions.
- [x] Limit the Brīdinājumi canvas to one operator-selected warning (superseded below by multi-warning selection), remove the on-canvas legend, collapse card detail to phenomenon/severity by default with full detail on click, and pre-fill the title from the selected warning's phenomenon.
- [x] Redesign Brīdinājumi warning cards as compact phenomenon-icon chips and move full warning text into a dialog popup over a dimmed, blurred backdrop.
- [x] Give Brīdinājumi chips a hover state where the severity-colored icon capsule expands to fill the chip, adapted from a reviewed external Uiverse.io reference component.
- [x] Split Brīdinājumi's warning selection (checkbox, drives the map) from viewing a warning's detail (clicking the chip, opens the popup) so choosing a map warning no longer forces the popup open; widen the detail popup for readability.
- [x] Replace single-warning selection with multi-select checkboxes so combined-severity newsroom maps (e.g. yellow + orange wind together) can be produced; re-add severity-ordered compositing of every checked warning's polygons. Restyle chips as narrower, fully severity-colored, white-icon/white-text buttons, and port a reviewed Uiverse.io checkbox component for the selection control.
- [x] Add per-warning editorial weather-symbol placement to Brīdinājumi, reusing Faktiskā's existing symbol set: drag to move, drag a corner handle to resize, stored as canvas-fraction coordinates so placement holds across both export resolutions; the drag/resize affordance is preview-only and never reaches the exported PNG. This is the app's first direct-manipulation canvas control (everywhere else is plain form inputs).
- [ ] Decide whether Brīdinājumi should actively prevent checking warnings of different phenomena together, or continue leaving that as an operator responsibility.
- [x] Build and verify commit-addressed images on Rocky, transfer and checksum-verify them on the VPS, and update only the application service. Current full-SHA image: `weathertool:138f57c808631dc17396b22ac0305b670fbcf776`; PostgreSQL and Authelia were not restarted.
- The fixed production PNG bases, browser/social metadata, reviewed frontend dependency updates, stabilized header navigation, authenticated API routing, the full Brīdinājumi warning-map workspace (including draggable per-warning symbol placement), hardened LVĢMC warning fetching, real open-data station/water-temperature ingestion, and the scheduler safety split are deployed in UAT release `138f57c8`; newsroom workflow validation is in progress.
- Link-preview crawlers cannot authenticate through Authelia; the final Nginx policy must deliberately expose only the minimum preview metadata/assets if WhatsApp previews are required.
- Both the VPS and Rocky `weather` tables now hold only real open-data station observations; their original synthetic rows were backed up and wiped 2026-08-23.
- The private LVGMC FTP feed has real credentials as of 2026-08-23, but `ENABLE_LVGMC_FTP_JOBS` stays `false` on both Rocky and the VPS pending a real answer from LVGMC about the `ltv` account's connection/rate policy (see Phase 7). The DMI HARMONIE forecast feed remains gated (`ENABLE_HARMONIE_JOBS`, default off) pending real credentials — turned out not to need any (DMI dropped its key requirement), but wiring it up is deferred to a dedicated verification session (see Phase 7).
| 2026-08-20 | `16a8c68` | Deploy approved production map bases, normalized branding assets, favicon, Apple icon, and social metadata to public UAT | Yes — Rocky build and VPS/browser smoke checks |
| 2026-08-22 | `ef64895` | Deploy the reviewed zero-advisory frontend dependency state as an immutable full-SHA VPS release while leaving PostgreSQL, Authelia, and HOP uninterrupted | Yes — isolated Rocky smoke test, dual-host checksum, matching image ID, container health, loopback, authentication-gate, and HOP checks |
| 2026-08-22 | `36d094e` | Stabilize the header, keep the primary workspaces visible, and move secondary workspaces into an icon-labelled menu | Yes — Rocky browser and Jam navigation checks |
| 2026-08-22 | `df911f1` | Preserve API 401 responses through Authelia/Nginx so frontend queries do not follow cross-origin login redirects | Yes — Rocky and VPS health checks; authenticated browser query verified after refreshing synthetic data |
| 2026-08-22 | `f3197bf` | Return query API payloads with an explicit JSON content type | Yes — Rocky API and browser verification; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `0608865` | Localize the operator-facing workspace names and concise workflow copy in Latvian | Yes — Rocky browser verification; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `7f09181` | Independently calibrate both Ūdens templates and optically center Monda values using visible glyph bounds | Yes — both native-resolution PNG exports visually validated on Rocky; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `bc5bee1` | Add the Brīdinājumi LVĢMC warning-map workflow with metadata/polygon ingestion, day/phenomenon filtering, and manual title entry | Yes — Rocky browser verification; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `13115f0` | Add production border overlay artwork and feathered warning-polygon fills to Brīdinājumi | Yes — Rocky browser verification; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `4395935` | Replace Brīdinājumi's 4-corner bounding-box projection, which drifted up to ~200px on the 3840 canvas, with an affine fit calibrated against the validated Kartes/Faktiskā city pixel positions | Yes — Rocky typecheck, build, and headless-browser screenshot comparison at both native export resolutions; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `924e410` | Limit Brīdinājumi to one selected warning per export, remove the on-canvas legend, collapse card detail until clicked, and pre-fill the title from the selected warning's phenomenon | Yes — Rocky typecheck, build, and headless-browser screenshot verification of selection switching and card detail; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `cafc75a` | Redesign Brīdinājumi warning cards as compact phenomenon-icon chips and move full warning text into a dialog popup over a dimmed, blurred backdrop | Yes — Rocky typecheck, build, and headless-browser screenshot verification of the popup open/close flow; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `70fd24c` | Give Brīdinājumi chips a hover-expanding severity capsule, adapted from a reviewed Uiverse.io reference component | Yes — Rocky typecheck, build, and headless-browser screenshot verification of rest/hover/active chip states; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `6fbf672` | Split Brīdinājumi warning selection (checkbox) from viewing detail (chip click) and widen the detail popup | Yes — Rocky typecheck, build, and headless-browser verification that checkbox clicks change the map selection without opening the popup and chip clicks open the correct warning's detail without changing the selection; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `56b8716` | Replace single-warning Brīdinājumi selection with multi-select checkboxes and severity-ordered polygon compositing, and restyle chips as narrower, fully severity-colored, white-icon/white-text buttons with a ported Uiverse.io checkbox component | Yes — Rocky typecheck, build, and headless-browser screenshot verification of multiple simultaneous checked warnings compositing correctly on the map; deployed to VPS in release `b5150ab` on 2026-08-22 |
| 2026-08-22 | `6185dbf` | Harden `WarningService` against partial upstream failures: bounded outage retry, per-warning fetch isolation, a live-API-verified CKAN polygon filter, and visible logging for a previously-silent vertex-order default | Yes — Scala tests, Rocky full-stack rebuild verified against real live LVĢMC data (including a genuinely severe 52,345-point wind-warning polygon rendering correctly); deployed to VPS in release `6185dbf` on 2026-08-22, verified against live data on the VPS itself |
| 2026-08-23 | `c97875d` | Add draggable, resizable per-warning weather-symbol placement to Brīdinājumi, reusing Faktiskā's symbol set and image cache | Yes — Rocky typecheck, build, and headless-browser mouse-drag/resize verification against fixture warning data (the live LVĢMC feed had zero active warnings at test time); confirmed the exported PNG excludes the preview-only drag/resize handles; deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `893a09a` | Add `fetch.lvgmc.OpenDataStationService`: real, free, keyless current station observations from LVĢMC's open-data portal, scheduled alongside (not replacing) the private FTP feed | Yes — Scala tests, manual `sbt runMain` fetch+write against the real feed on Rocky staging (1,577 rows, 33 cities), confirmed in `weather` via direct SQL, Faktiskā's 13 fixed stations rendering real temperatures with correct stale-fallback for the one uncovered city (Valmiera), and Kartes' aggregate query API returning sane blended values; deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `cb25316` | Stop generating synthetic weather data (remove `dev/seed_weather.sql`, the `seed` Compose service, and the README section describing it) now that real data is verified and this tool may plausibly be used against live broadcasts | Yes — `docker compose config` valid, running Rocky stack unaffected (profile-gated service, no running containers touched); deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `6ddfe73` | Split scheduled jobs (`ENABLE_SCHEDULED_JOBS` vs. new `ENABLE_LEGACY_PROVIDER_JOBS`) so the working open-data station fetch runs without the FTP/Harmonie jobs' placeholder credentials crash-looping the whole app via `parMapN` | Yes — Scala tests, Rocky rebuild deployed with the flag on: logs confirm only the safe jobs scheduled, FTP/Harmonie never attempted, app stayed up; deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `39fcb3e` | Remove the dead `METEO_*` fetch path (`fetch/csv/FetchService.scala`, its empty test, and its env vars everywhere) — confirmed via git history it's the same underlying LVĢMC data as the FTP/open-data paths, just an earlier, superseded delivery mechanism that was never wired into anything running | Yes — Scala tests (2 suites now, down from 3, the deleted one was empty); deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `4036d24`–`4b92272` | Add `fetch.lvgmc.WaterTemperatureService`: real, free, keyless water temperatures for all 6 Ūdens zones (2 coastal `SEDUT` stations for Jūra/Līcis, 4 inland `WTEMD` stations for Kurzeme/Zemgale/Vidzeme/Latgale), fetch-on-demand with a 15-minute in-memory cache mirroring `WarningService` rather than the persisted station-observation path, since Ūdens has never stored history | Yes — Scala tests, Rocky rebuild, direct API curl confirming real values for all 6 zones; caught and fixed one station-selection error (Daugavpils only reports water level, not temperature — swapped for Ludza) via direct verification before committing; deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `5a92465` | Auto-populate Ūdens's min/max fields from `/api/water-temperatures` on load, mirroring Faktiskā's fetch/override/reset pattern, with per-zone manual-override tracking and an "Atiestatīt" reset button | Yes — Rocky typecheck, build, and headless-browser verification (Playwright in Docker, since `chromium-cli` wasn't available in this environment) confirming auto-populated values match the API, manual edit correctly flips a zone to "Manuāli" and enables its reset button, and reset correctly restores the fetched value; noted along the way that `/udens-temperatura` 404s on a direct hit (no SPA fallback yet, consistent with the known limitation already listed) but works via client-side nav; deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `9eca9eb` | Fix the one-station-per-zone design catching a real problem after deploy: every zone showed the same value duplicated as both min and max ("19...19"). Classified all 65 LVĢMC stations that report water temperature (56 inland WTEMD, 9 coastal SEDUT) into the 6 zones by geography and report the real min/max across each zone's currently-reporting stations, dropping readings over 12h stale so one stuck sensor can't skew a range | Yes — Scala tests, Rocky rebuild, direct API curl confirming a genuine spread per zone (e.g. Vidzeme 11.4–18.7°C, traced the low point to a real fresh reading from a colder headwater station near Cēsis, not a stale-sensor artifact), and headless-browser screenshot confirming the UI renders the real spread (e.g. "11...19") with no console errors; deployed to VPS in release `138f57c8` on 2026-08-23 |
| 2026-08-23 | `69a5260` | Bring architecture/workflow docs current with real-data ingestion (removed synthetic-seed staging descriptions, documented Ūdens auto-populate, the scheduler split) | Yes — doc-only change, no rebuild needed |
| 2026-08-23 | `138f57c8` | **Full go-live VPS release**: deployed image `weathertool:138f57c808631dc17396b22ac0305b670fbcf776` (everything from `c97875d` through `69a5260` — Brīdinājumi draggable symbol placement, real open-data station/water-temperature ingestion, synthetic-data removal, scheduler split, `METEO_*` deletion, real per-zone water-temperature ranges), flipped `ENABLE_SCHEDULED_JOBS=true` on the VPS now that the open-data path is proven, and wiped the VPS `weather` table's original 12,784 synthetic rows down to 0 (Rocky's own table was left untouched — separate decision, not requested for this release) | Yes — exact release image smoke-tested on Rocky (healthy, real warnings/water-temperature data) before transfer; VPS PostgreSQL backed up to `/srv/weathertool/backups/pre-real-data-release/` before the wipe; checksum verified on both ends; `app` service recreated without touching PostgreSQL/Authelia; confirmed healthy container, working loopback and public HTTPS (302 unauthenticated page, 401 unauthenticated API — matching established behavior), real warnings/water-temperature API responses, the scheduled open-data job firing on schedule and landing 1,577 real rows within ~2 minutes, and a real (non-Valmiera) city query returning genuine current temperatures with Valmiera correctly absent rather than falling back to wiped synthetic data |
| 2026-08-23 | `744d706` | Rework the visual palette: layered blue-gray background gradient (sampled tones, iterated visually against Home/Brīdinājumi/Kartes) replacing the near-flat pale tint, while keeping buttons/icons/links on the original saturated accent blue; removed the "Testa dati" badge, a synthetic-data leftover | Yes — Rocky build and headless-browser screenshot comparison across Home, Brīdinājumi, and Kartes; deployed to VPS in release `f78b0a26` on 2026-08-23 |
| 2026-08-23 | `a1c2fd5` | Declutter Faktiskā: collapse "Jaunākās temperatūras" and "Kartes noformējums" by default behind toggles (with a manual-override-count badge so nothing's silently hidden), fix a real cascade bug forcing Faktiskā's resolution buttons into full-width left-aligned rows instead of a compact pair, restructure "Kartes noformējums" into distinct bordered cards, translate its still-English symbol/city-assignment block to Latvian ("Mākoņi pilsētām"), and collapse its city list to exceptions-only by default | Yes — Rocky typecheck, build, and headless-browser screenshot verification of the collapse/expand toggles, the exceptions-only city list, and the button fix; deployed to VPS in release `f78b0a26` on 2026-08-23 |
| 2026-08-23 | `5c00e48` | Fix temperature numbers rendering visually too high in their map badges: canvas `textBaseline: "middle"` centers on font em-box metrics, not visible glyph ink, and digits have no descenders — centered on actual glyph bounds instead, matching the technique already used correctly in Ūdens | Yes — Rocky build and headless-browser screenshot comparison of the map badges before/after; shared by Faktiskā and the older Kartes comparison map; deployed to VPS in release `f78b0a26` on 2026-08-23 |
| 2026-08-23 | `59bb499` | Promote Brīdinājumi from the "Vairāk" overflow menu into the always-visible header nav, and add a small icon to every visible nav item (reusing the same icons as their Home cards) | Yes — Rocky build and headless-browser screenshot verification at 1900/1440/1200px widths, confirming no overflow and correct active/overflow-menu state; deployed to VPS in release `f78b0a26` on 2026-08-23 |
| 2026-08-23 | `f78b0a26` | Deploy the full frontend design pass (background/accent rework, Faktiskā decluttering, temperature-badge centering fix, header nav) to the VPS — no database or scheduler changes, application-only release | Yes — exact release image smoke-tested on Rocky (bundle hash and headless-browser screenshot confirmed against the known-good local build) before transfer; checksum verified on both ends; `app` service recreated without touching PostgreSQL/Authelia; confirmed healthy container, matching bundle hash on the VPS, working loopback and public HTTPS (302 unauthenticated page, 401 unauthenticated API), and real warnings/water-temperature API responses |
| 2026-08-23 | `d7439a2b` | Fresh-eyes review of the whole design pass turned up two real issues, fixed here: `WindInputs` had no Latvian branching and was showing "Wind direction"/"Wind speed"/"Gusts" in English right under the freshly-translated "Temperatūra un vējš" heading on Faktiskā (threaded the same `productionTemplate` flag `MapView.tsx` already uses elsewhere), and a confirmed-unreferenced `.assignedSymbol` CSS rule was removed. The review also flagged two things left alone: a pre-existing (not introduced this session) `.symbolPalette`/`.selectedPreview`/`.currentSymbol` cross-file class collision in `weatherIcons.css` currently masked by `!important` rather than actually resolved, and a naming nit on "Mākoņi pilsētām" (the section assigns arbitrary weather symbols, not just clouds) — both worth a look later, not blocking | Yes — Rocky typecheck, build, and headless-browser screenshot confirming the wind fields now render in Latvian; deployed to VPS in release `d7439a2b`, exact release image smoke-tested (bundle hash matched) before transfer, checksum verified, container healthy, public HTTPS/API checks passing |
| 2026-08-23 | `e446f1f` | Consolidate `.symbolPalette`/`.selectedPreview`/`.currentSymbol` (used by both Faktiskā's `IconInputs.tsx` and Brīdinājumi's `Warnings.tsx`) into one real definition in `weatherIcons.css`, the file both actually import, removing the dead/duplicate versions in `mapGraphics.css` and the `!important` size patches that were masking the collision. "Mākoņi pilsētām" naming confirmed intentional — the newsroom users are non-technical and the same people who'll use this long-term, so intuitive-but-imprecise beats literally-correct-but-jargony | Yes — Rocky typecheck, build, headless-browser screenshot on Faktiskā (pixel-identical to before), and computed-style verification on both Faktiskā and Brīdinājumi confirming identical resolved CSS with no `!important`; caught one incidental fix for free (`.currentSymbol.empty`'s Inter font was silently losing to the `!important` rule, normal cascade now applies it correctly); deployed to VPS in release `e446f1ff`, exact release image smoke-tested (bundle hash matched) before transfer, checksum verified, container healthy, public HTTPS/API checks passing |
| 2026-08-23 | `2b5dff6` | Make the `weather` table upsert non-destructive: `ON CONFLICT DO UPDATE` was a blind full-row overwrite, so open-data's always-null `visibilityMin`/`dewPoint`/`sunDuration` and always-empty `phenomena` would silently erase real FTP values whenever open-data's write landed later for the same row. Switched to `COALESCE(excluded.field, weather.field)`, with `NULLIF` against an empty array specifically for `phenomena` since Scala's `List[String]` never maps to SQL `NULL` | Yes — Scala tests; found while wiring up real FTP credentials, before any real dual-source writes had happened |
| 2026-08-23 | `cab94d0` | Fix a real UTC-vs-local timezone mismatch: the open-data portal's `DATETIME` is UTC, stored into `weather.dateTime` with no conversion, while FTP's "Laiks" column is already local and also stored as-is — silently present since open-data went live (`893a09a`), invisible until a second, correctly-labeled source existed alongside it. Fixed by converting to `Europe/Riga` at ingestion in `OpenDataStationService` | Yes — Scala tests; verified live: at real local time 20:31 EEST the fix produces `dateTime=20:00` instead of the previous `17:00`, matching what FTP writes for the same real hour |
| 2026-08-23 | `3315f00` | Fix the same UTC-vs-local mismatch in Ūdens's `observedAt` display (`WaterTemperatureService`) — same open-data portal, same root cause, display-only (internal recency filtering was already self-consistent either way) | Yes — Scala tests; verified live via `/api/water-temperatures`: `observedAt` now matches real local time |
| 2026-08-23 | `3eddf95` | Split `ENABLE_LEGACY_PROVIDER_JOBS` into independent `ENABLE_LVGMC_FTP_JOBS`/`ENABLE_HARMONIE_JOBS` — real LVGMC FTP credentials arrived today, real DMI HARMONIE credentials haven't, and the combined flag would have enabled both together, crash-looping the app on HARMONIE's still-placeholder values via `parMapN` | Yes — Scala tests; caught before it could happen (the Grib job was ~15 min from its first scheduled run when noticed) and reverted within under a minute; re-verified after the fix that only "Fetch Weather Stations" scheduled, not "Fetch Grib" |
| 2026-08-23 | `dc04f66` | Fix `deploy/vps/compose.yml` hardcoding `LVGMC_USER`/`PASSWORD`/`URL` to inert placeholder strings directly in the file (unlike `POSTGRES_*`, which already read from `.env.staging`) — real credentials added to `.env.staging` alone would have had no effect until this switched to the same `${VAR}` substitution pattern. Enabled `ENABLE_LVGMC_FTP_JOBS` on the VPS | Yes — `docker compose config` syntax valid; VPS `app` container came up healthy with real credentials loaded (would have crashed immediately on missing-config if not) |
| 2026-08-24 | `6b9c7cf` | Fix path traversal, auth-bypass, and DoS findings from an independent fresh-eyes security review (requested by the user specifically to get a second, skeptical pass against the local Rocky version): a live-verified CRITICAL — `/api/debug/file/{fileName}` allowed unauthenticated arbitrary file read, confirmed by reading `/proc/self/environ` and leaking `LVGMC_PASSWORD`/`POSTGRES_PASSWORD` into the review's own output — plus the same raw-filename pattern in `/show/grib`, `/grib/binary-chunk`, and `/show/lvgmc-forecast`. Added `ValidateFileName` (allowlist regex; rejects `..` and separators including decoded `%2f`) and applied it everywhere a path segment reached a filesystem or FTP path; hardened `ValidateInt` to reject negatives; gated `/api/fetch/lvgmc/stations` behind `ENABLE_LVGMC_FTP_JOBS` (it could otherwise trigger a real, unauthenticated FTP login regardless of the flag) and stopped it leaking `error.getMessage`; added an explicit `/api/*` catch-all so an unmatched API route can never fall through to the SPA shell; capped `binary-chunk` read length at 64MB | Yes — Scala tests, Rocky rebuild, live curl verification of `%2f`-encoded and literal traversal payloads (404), the gated FTP route (503 while the flag is off), an unmatched `/api/*` route (404, not `index.html`), and legitimate filenames/queries still returning correct 200s |
| 2026-08-24 | `8c45d8d` | Fix SQL injection surfaced by the same security review: `/api/query/city/.../{field}/...` passed its `field` path segment unvalidated into `PostgresService.query`, which splices it into SQL via `Fragment.const` (unescaped) whenever the aggregate key is min/max/avg/sum/distinct, or whenever granularity is `hour` in the list branch. Added `ValidateField`, reusing the `WeatherData.getKeys` allowlist `AggFieldList` already applied to `/query/country`. `AggregateKey` values reaching `Fragment.const` elsewhere were already safe — they come from a closed ADT (`AggregateKey.fromString`), not raw input | Yes — Scala tests, Rocky rebuild, live curl verification that SQL-injection payloads in the `field` segment 404 and a legitimate query still returns correct data |
| 2026-08-24 | *(operational, no commit)* | Rotated Rocky's `POSTGRES_PASSWORD` following the security review, since the old value had been exposed into this session's context multiple times (two of my own sloppy shell commands, plus the `/proc/self/environ` read the review used as its traversal proof). Changed the role's actual password via `ALTER ROLE` against the running container (editing `.env` alone has no effect on an already-initialized PostgreSQL data directory), then updated `.env` and recreated the `scala` service. The Postgres container itself was also recreated as a side effect (its own `POSTGRES_PASSWORD` env interpolation changed too), which is harmless — that variable only takes effect on a fresh, empty data directory, not an existing one — but is worth knowing about | Yes — clean scala container startup with no auth errors, and a real query (`/api/query/latest-temperatures/Rīga`) returning live data over the new password |
| 2026-08-24 | `0be325f` | Deploy the security-review fixes (`6b9c7cf`, `8c45d8d`) to the VPS as image `weathertool:0be325fbbbf92b03bc8d574dc6f7b9449ef310ea`; release `b0b58d2` remains the immediate application rollback. Application-only release — PostgreSQL and Authelia were not restarted | Yes — exact release image smoke-tested on Rocky against a throwaway local Postgres before transfer (traversal/injection payloads 404, gated FTP route 503, legitimate queries 200); checksum verified on both ends; `app` service recreated cleanly (healthy, no auth/DB errors in logs); loopback re-verification of the same traversal/injection/FTP-gate checks plus real `/api/warnings` and `/api/water-temperatures` responses; public HTTPS confirmed unchanged (302 unauthenticated page, 401 unauthenticated API) |
| 2026-08-24 | `9bab93d` | A follow-up independent fresh-eyes review (requested specifically as a second pass, not a rerun of the same pentest) caught that the `6b9c7cf` FTP auth-bypass fix was incomplete: only `/api/fetch/lvgmc/stations` (the one route the original review named) was gated behind `ENABLE_LVGMC_FTP_JOBS`. A sibling route, `/api/show/lvgmc-forecast/{fileName}`, calls the same `fetch.fetchFile` — a real, unauthenticated LVGMC FTP login — and had only gotten the traversal fix (`ValidateFileName`), not the auth-bypass fix, because the two routes were fixed along different mental categories (traversal batch vs. the one named auth-bypass fix) instead of by tracing every caller of the dangerous method. Gated this route the same way; traced every HTTP-reachable caller of `fetch.fetchFile`/`fetchWeatherStations` this time to confirm no others remain. Deployed to both Rocky and the VPS (image `weathertool:9bab93daa6b3a0fb99165a8fac9accfa3a3ccbf1`) immediately given this was live and exploitable in production | Yes — Scala tests, Rocky rebuild and curl verification (both FTP routes 503 while the flag is off), VPS image smoke-tested against a throwaway local Postgres before transfer, checksum verified, `app` service recreated cleanly, loopback re-verification of both gated routes, public HTTPS unchanged (302/401) |
| 2026-08-24 | *(review, no commit)* | A follow-up independent fresh-eyes review (explicitly scoped as a second, different pass — not a rerun of the original pentest) sanity-checked the `6b9c7cf`/`8c45d8d` fixes and did a broader skeptical pass. Caught the incomplete FTP fix documented under `9bab93d` above. Confirmed `ValidateFileName`'s minor `"."` edge case is harmless (resolves to a directory, not exploitable) and found no other unvalidated `Fragment.const` usages. Surfaced several open architectural/testability findings now tracked in Phase 2/Phase 5 above: the `parMapN` scheduler crash-loop pattern (still fully live), per-request `System.gc()` in `getBinaryChunk`, `PostgresService.query`'s unenforced field-routing invariant, no JDBC connection pooling, and several files under `src/main/scala` (`DataServiceTest`, `FetchServiceTest`×2, `OpenDataStationServiceTest`, `GribParserTest`) that look like tests but are actually `main`-method scripts never run by `sbt test` — compiled straight into the production jar, at least one capable of writing to the real database or hitting live endpoints if run by mistake |
| 2026-08-24 | `5b88e69` | Add Gitea Actions CI: a repository-scoped `weathertool-ci-rocky-01` runner on Rocky (isolated behind its own Docker-in-Docker daemon, mirroring the isolation pattern already proven by HOP's Forgejo runner on the same host — the two coexist without conflict, each an independent client registered to a different server) runs `sbt test` and the frontend typecheck/build/audit routine on every push/PR to `codex/staging-baseline` plus manual dispatch. Verified `sbt test` compiles and passes with no `.env` present at all (via a `git archive HEAD` dry run) before writing the workflow, since CI must never see real credentials. Repo mirrored to Gitea (`bot/WeatherTool`) as an additional remote alongside the existing `rocky`/`origin` ones — deliberately not replacing either, pending an open question about the GitHub `origin` (`guntisdev/WeatherTool`) that won't be resolved until 2026-08-26. See `CONTINUOUS_INTEGRATION.md` | **Correction**: this row originally claimed the first run (`ci.yml #1`) verified green, based on a WebFetch summary of the Gitea Actions page that turned out to be simply wrong — reported as fact without visually checking it first. It actually failed, along with the next three runs, on three different real bugs (see `7e1b492`/`2c78ae8` below and `CONTINUOUS_INTEGRATION.md`). First actual green run was `ci.yml #5`, commit `2c78ae8` |
| 2026-08-24 | `7e1b492`–`2c78ae8` | Fix three real CI bugs surfaced by actually running the new workflow, found via user-provided screenshots of the Gitea Actions UI after the WebFetch-based "success" claim above turned out false: (1) the runner's `HOME`/cache-directory permission error, initially misdiagnosed as only disabling the optional `actions/cache` action type, actually blocked resolving `actions/checkout` at all — fixed with `HOME=/data` on the runner container (operational fix, in `compose.yml`, not this repo); (2) `actions/checkout` is a JavaScript action needing Node in the job container, which `backend`'s `hseeberger/scala-sbt` image doesn't have — fixed with an `apt-get install nodejs` step before checkout (`7e1b492`); (3) that apt-installed Node was Debian bullseye's own stale default (Node 12), too old for `actions/checkout@v4`'s modern JS — fixed by installing a real current Node 20 via NodeSource's setup script instead (`2c78ae8`). See `CONTINUOUS_INTEGRATION.md` for the full detail | Yes — `ci.yml #5` (commit `2c78ae8`) verified green in the Gitea Actions UI via user-provided screenshot (2m24s), both jobs passing |
| 2026-08-25 | `464e035` | Add shared side padding (`clamp(16px,3vw,48px)`) to the one truly shared layout wrapper (`.appContent`) so full-bleed pages (Kartes, Stacijas, Apskats — the only three with no CSS of their own) get breathing room on the sides instead of running edge-to-edge, while staying full-width rather than a narrower centered column | Yes — Rocky rebuild, headless-browser screenshot of Kartes before/after; confirmed no double-padding on pages that already set their own margins (Home) |
| 2026-08-25 | `1e83e48` | Fix a real CSS collision in Kartes' results grid (`Result.css`): a newer consolidated `.item` rule used `background:var(--surface)!important` to force out an older duplicate rule instead of removing it, leaving the old rule's `padding`/`text-align`/`max-width` still silently active underneath. Also removed dead frontend code found via a systematic check of every class in every stylesheet against actual `.tsx` usage — undercounted by a prior review by several: a whole leftover "Production layout" block in `mapGraphics.css` (9 classes, including a scoped selector that never matched anything in the current markup), unused `.grid-1-1`/`.grid-1-2`/`.panel` utility classes, dead calendar-navigation CSS, and dead commented-out JSX in `DateList.tsx` | Yes — typecheck clean, production build succeeds, headless-browser screenshots of Kartes (including a real query result), Faktiskā, and Arhīvs show no visual regression |
| 2026-08-25 | `e62895a` | Narrow Ūdens's six temperature cards so all fit on one row instead of wrapping — switched `grid-template-columns` to `repeat(auto-fit, minmax(165px,1fr))`. Caught a real bug while tuning the floor value: an earlier hardcoded `repeat(6, minmax(130px,1fr))` pass produced 6 columns at every width from 1050–1900px, but at some in-between widths (e.g. 1300px) the resulting input boxes came out to ~38px — narrow enough that the native number-spinner arrows ate nearly all of it, clipping digits that were genuinely present in the DOM (confirmed via `inputValue()`) to nothing visible. Fixed by hiding the native spinner arrows and switching to `auto-fit`, which can't produce that in-between too-narrow zone the way a hand-rolled breakpoint ladder can | Yes — a programmatic sweep of 14 viewport widths (375–1900px) reading each input's real computed width and value, not just screenshots, confirmed every width keeps a comfortably legible input |
| 2026-08-25 | `e1ce153` | Give Ūdens's resolution-choice buttons the same visual language as the temperature cards next to them (border, radius, soft glass background) instead of the app's generic flat button style, after measuring (via Playwright bounding boxes, not eyeballing) that the perceived "buttons sit higher" was a visual-weight mismatch, not an actual coordinate offset — the tops were already pixel-identical | Yes — Rocky rebuild, headless-browser screenshot confirmation |
| 2026-08-25 | `07b16ac` | Source Valmiera's Faktiskā temperature from Priekuļi as a temporary substitute, since LVĢMC's open-data station list has no station in or near Valmiera at all. Priekuļi chosen by measuring against the app's own calibrated map coordinates (~56px away vs. the next-closest candidate's ~81px), not a guess — same pattern as Stende/Zīlāni substituting for Talsi/Jēkabpils. Implemented client-side in Faktiskā's fetch only; Valmiera's marker position and on-map label are untouched. Also fixes a second symptom for free: the weather-symbol picker only lists cities with a value, so Valmiera never appeared there either. Provisional pending direct confirmation from LVĢMC | Yes — typecheck clean, production build succeeds, headless-browser checks confirm Valmiera's field shows Priekuļi's real live value (fresh, not flagged manual/stale) and now appears in the weather-symbol exceptions list (13 cities, was 12) |
| 2026-08-25 | `4c40c24` | Add click-to-edit temperature badges directly on the Faktiskā map, as a fast path for the common "one or two values need a tweak" case instead of always opening the 13-field "Rādīt stacijas" panel. Mirrors Brīdinājumi's existing canvas-coordinate pattern exactly (`getBoundingClientRect` scaling) rather than inventing a new one, hit-testing against Faktiskā's already-fixed per-resolution marker positions. The floating input is a plain DOM element positioned over the canvas, never drawn onto it, so it stays preview-only like Brīdinājumi's drag handles. Reuses the existing `updateValue` function via a new `onEditValue` callback prop, so the "Rādīt stacijas" panel's override tracking/reset/manual-count badge all stay in sync automatically. Kept the panel rather than removing it after this shipped — it's the only place that surfaces missing/stale data and offers a reset, neither of which click-to-edit does | Yes — end-to-end Playwright test: hover shows a pointer cursor, click shows a correctly positioned input prefilled with the real value, Enter commits and the canvas redraws, the change appears in the panel as a manual override, Escape cancels without committing, works correctly at both export resolutions, no console errors, Kartes (a different `MapView` mode) unaffected |
| 2026-08-25 | `001b014` | Deploy everything accumulated since the last VPS push (`9bab93d`) as image `weathertool:001b014932a1c5e28080dd057613f8bb807a3b81`: the frontend padding/cleanup/Ūdens-layout/button-styling pass, the Valmiera→Priekuļi substitution, and click-to-edit on Faktiskā. The Gitea CI setup itself doesn't affect the deployed app. Release `b0b58d2` remains the rollback target | Yes — exact release image smoke-tested on Rocky against a throwaway local Postgres before transfer (traversal/injection payloads 404, gated FTP routes 503, `/faktiska` and `/udens-temperatura` direct hits 200); confirmed the built image's CSS bundle genuinely contains tonight's changes (not a stale cache hit) before shipping; checksum verified on both ends; `app` service recreated cleanly (healthy, schedulers registered, no errors in logs); loopback re-verification of the same security/routing checks plus confirmed Priekuļi has real live data server-side; public HTTPS unchanged (302/401). Browser-level verification of Valmiera/click-to-edit specifically relied on Rocky's already-passing headless-browser test of the identical code, since the VPS app port is loopback-only by design and not reachable for a direct browser check from this session |
| 2026-08-24 | `2c44888`–`b0b58d2` | Replace the hardcoded per-route static-file list (`/station`, `/cities`, `/latvia`, `/database`, `/harmonie`, `/lvgmc-forecast`, `/bridinajumi`) with a general SPA fallback: real files serve as-is, anything else falls back to `index.html` so the SolidJS router handles it client-side — fixes a real, user-reported bug where a direct hit (e.g. a browser refresh) on `/faktiska` or `/udens-temperatura` 404ed instead of loading the app, since those two routes were never added to the old list. A missing file under `/assets` specifically still 404s properly rather than silently serving HTML, so a stale tab after a future deploy gets a clean error instead of a confusing JS parse failure | Yes — first attempt silently no-op'd because the fix was built into a `git archive HEAD` image before being committed (classic mistake, caught immediately by re-testing and finding identical old behavior); after committing, verified via curl on all previously-working routes, both previously-broken routes, a missing asset (404), a real asset (200), and `/api` (200), then a full headless-browser render check (zero console errors, real data, correct nav state) on a genuine direct hit — not just HTTP status codes; deployed to VPS in release `b0b58d2`, verified the same way through both the loopback port and the public domain (Authelia gate still correctly redirects unauthenticated requests) |