The ltv account fails to authenticate from the VPS specifically
(works fine from Rocky with the same credentials, network path to
ftp.meteo.lv:21 confirmed fine from the VPS too) — narrowed to either
a password transcription error in .env.staging or LVGMC IP-allowlisting
the account, neither confirmable without inspecting the password.
Caught via the overnight watch this was set up for rather than
assumed safe; reverted ENABLE_LVGMC_FTP_JOBS to false on the VPS
immediately once the failure was confirmed real, restoring stable
operation. FTP stays enabled on Rocky, where it works.
Changelog rows for the COALESCE upsert fix, both timezone fixes, the
ENABLE_LVGMC_FTP_JOBS/ENABLE_HARMONIE_JOBS split, and the VPS compose
LVGMC templating fix — none had one yet. Updated the stale "currently
deployed" references (were still e446f1ff) to the actual final
release (3eddf95). Recorded the DMI Harmonie research (no API key
needed, new domain, verified live) and the 5-step verification plan
for actually wiring it up, deferred to a dedicated session. One line
about the VPS's first FTP fetch is marked pending — a background
watch is still confirming it as of this commit; will follow up once
it resolves.
deploy/vps/compose.yml hardcoded LVGMC_USER/PASSWORD/URL to inert
placeholder strings directly in the file, unlike POSTGRES_* which
already read from .env.staging — so simply adding real values to
.env.staging would have had no effect. Switched to the same
${VAR} substitution pattern, and enabled ENABLE_LVGMC_FTP_JOBS now
that real credentials exist. HARMONIE stays on placeholders pending
real DMI credentials.
Real LVGMC FTP credentials arrived today; real DMI HARMONIE
credentials haven't. The combined flag would have enabled both the
moment FTP's were ready, crash-looping the app on HARMONIE's still-
placeholder values via parMapN — caught this before it happened
(the Grib job was ~15 min from its first scheduled run). Split into
ENABLE_LVGMC_FTP_JOBS and ENABLE_HARMONIE_JOBS so each provider can
be enabled independently as its own credentials become real.
Same root cause as the station-observation fix (this uses the same
open-data portal, same UTC DATETIME field): the raw UTC string was
passed straight through to the frontend as a display label, which
JS's Date parser then reads as local time for a string with no
timezone suffix — silently showing observation times 2-3h behind
the newsroom's actual clock. Internal recency filtering (isRecent)
was already self-consistent either way; this only affects display.
Found while wiring up real FTP credentials: the open-data portal's
DATETIME field is UTC, but was being stored into weather.dateTime
as-is with no conversion — while the private FTP feed's "Laiks"
column is already Latvia local time and also stored as-is. Both
paths write the same column, so the table has been silently holding
two timelines 2-3h apart since open-data went live, invisible only
because FTP was never actually running with real data until today.
Verified live: at real local time 20:31 EEST, the fix now correctly
produces dateTime=20:00 instead of 17:00, matching what FTP writes
for the same real hour.
ON CONFLICT DO UPDATE was a blind full-row overwrite. Open-data
station rows always carry null visibilityMin/dewPoint/sunDuration
and an empty phenomena array (fields it doesn't publish), so once a
second source (FTP) writes real values for those fields, a later
open-data write for the same (city, dateTime) would silently null
them back out. Switched to COALESCE(excluded.field, weather.field)
so a missing value from one source never erases a real value the
other already wrote; phenomena needs NULLIF against an empty array
specifically, since Scala's List[String] never maps to SQL NULL.
.symbolPalette/.selectedPreview/.currentSymbol are used by both
Faktiskā's IconInputs.tsx and Brīdinājumi's Warnings.tsx, but were
only defined in mapGraphics.css (Faktiskā's own stylesheet, twice,
with a fully dead first version) and then size-patched for both
consumers via !important overrides in weatherIcons.css. Moved the one
real definition into weatherIcons.css, the file both pages actually
import, and removed the dead/duplicate/!important versions.
No visual change intended — verified via headless-browser screenshot
on Faktiskā and computed-style checks on both pages. One incidental
fix: .currentSymbol.empty's Inter font was silently losing to the
!important rule; normal cascade now applies it correctly again.
Found by a fresh-eyes review after today's design pass: WindInputs
had no productionTemplate branching, so it kept showing "Wind
direction"/"Wind speed"/"Gusts" in English right under the freshly-
translated "Temperatūra un vējš" heading. Threaded the same
productionTemplate flag MapView.tsx already uses for its other
labels. Also removed .assignedSymbol, an unreferenced CSS rule the
same review flagged as dead.
Brīdinājumi was tucked in the "Vairāk" overflow menu despite being a
primary newsroom workflow. Moved it into the always-visible nav
alongside Stacijas/Kartes/Faktiskā/Ūdens, and gave every visible nav
item a small icon (reusing the same icons as their Home cards) so the
header matches the icon treatment the overflow menu already had.
canvas textBaseline "middle" centers on font em-box metrics, not
visible ink — digits have no descenders, so they read as sitting too
high in their boxes. Center on the actual glyph bounds instead,
matching the technique already used correctly in Ūdens's drawRanges.
Shared by Faktiskā and the older Kartes comparison map.
Both "Jaunākās temperatūras" and "Kartes noformējums" dumped their
full content on load, forcing a scroll past both just to reach the
map/export on every visit. Both now collapse by default (Faktiskā
only; the older non-production Kartes overlay panel keeps its current
always-open behavior) behind a toggle that still surfaces a manual-
override count so nothing is silently hidden.
Also fixed a real bug found in the process: Faktiskā's resolution
buttons carried both "resolutionChoices" and "faktiskaResolutionChoices"
classes, and the former (defined in waterTemperature.css for a narrow
sidebar layout) was winning the cascade in this wide-content context,
forcing full-width, left-aligned buttons instead of the intended
compact pair.
Restructured "Kartes noformējums" into distinct bordered cards instead
of one continuous flow, translated the still-English "Weather symbols"
/"City assignments" block to Latvian ("Laikapstākļu simboli"/"Mākoņi
pilsētām"), and collapsed the city list to show only active exceptions
by default with a "Rādīt pārējās pilsētas" button to reveal the rest.
Replaced the flat near-white page background with a layered blue-gray
gradient (from a sampled palette) for actual visual depth, while
keeping buttons/icons/links on the original saturated accent blue so
interactive elements still stand out against the calmer backdrop.
Also removed the "Testa dati" badge on Home, a synthetic-data leftover
now that both Rocky and the VPS run on real data only.
Deployed everything since 6185dbf (Brīdinājumi draggable symbols, 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, and wiped the
VPS weather table's synthetic rows after a PostgreSQL backup.
Flips ENABLE_SCHEDULED_JOBS on for the VPS release: the open-data
station and water-temperature paths are proven safe on Rocky.
ENABLE_LEGACY_PROVIDER_JOBS stays off pending real FTP/HARMONIE
credentials.
ARCHITECTURE.md, PRODUCT_WORKFLOWS.md, DEVELOPMENT_AND_STAGING.md, and
README.md still described the removed synthetic-seed staging setup and
Ūdens as pure manual entry. Updates this session (open-data station
ingestion, the scheduler split, water-temperature auto-populate)
weren't reflected outside UPDATE_ROADMAP.md's changelog.
Classify all 65 LVĢMC stations reporting water temperature (56 inland
WTEMD + 9 coastal SEDUT) into the six Ūdens zones by geography, and
report the real min/max across each zone's currently-reporting
stations instead of one hand-picked station's single value shown
twice. Drops readings older than 12h so a stuck sensor can't skew a
zone's range.
Confirmed via git history it's not a separate vendor: the very first
commit (2023-04-13) included real sample CSVs from it with a Latvian
header identical to the LVGMC/open-data fields — same underlying LVĢMC
data, just an earlier delivery mechanism superseded by the FTP feed and
never removed. Never wired into anything that runs (Server.scala's
import was already commented out).