Changelog · update of 8 September 2026 · covering 11 Jul – 2 Sep · branch main · 17 commits · 1 merged PR · 2 contributors
Blue — blueberry farm · wp6-blue.spohf.fontysvenlo.devRed — tomato greenhouse · wp6-red.spohf.fontysvenlo.devGrey — public platform demo · wp6-grey.spohf.fontysvenlo.devShared — twin-agnostic platform
Headlines
The twins moved house — production now runs on the new cluster. On 15 July both digital twins were evacuated from the old Kubernetes cluster onto the university's v2 platform: databases dumped, restored and verified (Blue's 5.34 million readings, full history, plus the yearly manual measurements that exist nowhere else), then the production web addresses flipped over. The old cluster is kept switched off as a rollback that a single DNS change can bring back.
The Sync Status page now answers "is it green — and if not, since when, and how bad?" Each data source shows a health badge, a 7-day reliability score and a records-per-run sparkline, and it now distinguishes our pipeline is broken from the upstream source has gone quiet — two problems that used to look identical.
Every page view and every nightly job is now traceable end to end. The twins emit OpenTelemetry traces, and every log line carries its trace ID — so "the sync was slow last night" can be answered by looking at the actual run instead of guessing.
Blue's growth tracker now marks all three harvests. Observed picking dates gave us approximate Growing Degree Day thresholds for the second (1300) and third (1500) harvest, alongside the existing first (1175).
Two failures that had been silent for weeks were found and fixed. An expired certificate at the login provider was crash-looping both dashboards; and the nightly CSV exports had been failing unnoticed for 23 days after the move.
🔗 The Open live buttons (↗) deep-link into the deployed dashboards. Blue and Red require a login on first open; Grey is a public demo (no login).
🚚 Production moved to the new platform
The single biggest piece of work this period — and deliberately invisible if it went well. It went well.
Both twins evacuated onto the v2 cluster
SharedBlueRed
The old Kubernetes cluster had become a cluster running nothing but this project, kept alive purely for us. Rather than maintain it, everything was moved to the university's shared v2 platform so the old one can be switched off entirely.
Because the two clusters use different storage technology, the data could not simply be copied at disk level — it had to be exported and re-imported. That was done and checked record by record:
Blue: 5,340,000+ sensor readings, the full pre-aggregated history, and the yearly long_data manual measurements — the one dataset whose source files exist nowhere but the database.
Red: full greenhouse readings history.
The sync job resumed from exactly where the restore left off, so there is no gap and no duplication at the seam.
The public web addresses were then pointed at the new cluster after being validated against it first. The old cluster still holds a complete second copy and is a one-step rollback.
In production, Blue had been writing into the database server's default administrative database — an accident dating back to its first deploy that every other environment (local development, the setup scripts, the documentation) disagreed with. Blue now has a proper wp6_blue database of its own, created automatically, exactly as Red always had.
Because Blue is the second twin needing this, the "create a twin's database" step became one shared, reusable piece of configuration instead of a copy-paste — so a third twin is a few lines, not a new script.
Commits
2612ce7 feat(helm): give blue its own database (wp6_blue) instead of squatting in postgres
Six infrastructure faults the move exposed — all fixed
Shared
The new cluster stores data on real network block storage rather than plain folders on a machine. That difference is invisible until it isn't: six separate assumptions baked into the old setup broke, each one found and fixed during the move.
The database refused to start — the new storage arrives with a system folder already present, which the database reads as "this directory isn't empty, I won't initialise here". Now it initialises in a sub-folder — but only where explicitly switched on, so the old cluster's live 2.8 GB database was not touched.
The startup deadlocked — the database-creation step ran after the app was declared healthy, but the app could never become healthy without its database. It now runs first.
Deploys hung — the dashboards hold an exclusive lock on their upload/export storage, so starting a replacement before stopping the old one could never work on this storage type. They now stop then start.
Uploads and exports could not be written — the new storage arrives owned by the system administrator, and the twins deliberately run as an unprivileged user. Ownership is now set correctly at mount time.
One deploy wedged indefinitely — a setup job that finished in about a second was being cleaned up so fast that the deployment tool never saw it succeed, and waited forever for a job that no longer existed.
The nightly CSV exports had silently failed for 23 days (see below).
Commits
05320af fix(helm): let TimescaleDB init in a PGDATA subdir on block-device PVCs 6abb706 fix(helm): run the TSDB bootstrap as a PreSync hook, not PostSync 08a01f6 fix(helm): Recreate strategy for dashboards mounting RWO volumes aeaa961 fix(helm): fsGroup so non-root pods can write PVCs on block storage bba7e54 fix(helm): drop hook-succeeded from the TSDB bootstrap delete policy 72bf5be fix(helm): pin export CronJobs to the dashboard node so RWO PVCs attach
A fallback that stays a fallback
Shared
During the changeover, new builds were published to both clusters' image registries — a fallback you cannot ship a fix to is a weak fallback, and feature work continued while the migration ran. Once the new cluster took over production, that second publish was removed on purpose: the old cluster now freezes at its last known-good build instead of quietly rolling forward onto untested code.
Commits
9814cfe ci: push images to both Harbors for the migration window 0b46077 ci: push only to harbor.inf; stop feeding old's registry
🩺 Knowing when the data stops arriving
A green job is not the same as fresh data. The Sync Status page now says which one you're looking at.
Redesigned, source-aware sync health
SharedBlueRedGrey
The Sync Status card was rebuilt to answer one question at a glance: is this source healthy, and if not, since when and how badly?
Automatic sources (the datalake feeds) get a data-freshness budget and show Healthy · Stale · Outage · Failing. The important distinction: a sync that keeps succeeding while the data behind it stops advancing is now flagged as stale/outage — an upstream problem — rather than looking indistinguishable from a healthy source.
Manual sources (uploaded spreadsheets, insect-trap counts) are event-driven, so they show upload recency instead of being wrongly judged against a sync schedule.
Each source keeps a rolling 30-day run history, feeding a 7-day reliability figure ("X of the last Y runs succeeded") and a records-per-run sparkline.
Failing-since and a consecutive-failure count replace the previous all-time totals, so an old, long-recovered incident no longer colours today's picture.
Fixed: a recovered source kept displaying a fossilised old error. Success now clears the error state.
The whole thing is shared platform code, so all three twins got it at once.
c63bb3f feat(status): visual, type-aware sync health with SLA, outage & failing-since
🔭 The platform can now explain itself
Traces and logs, joined up — so slow pages and odd sync runs can be investigated instead of guessed at.
End-to-end tracing across dashboards, sync and CLI jobs
Shared
Every web request and every scheduled job now records a trace: a timeline of what it did, including its database queries and its calls out to the external data APIs. A slow page or a stalled sync run can be opened up and read directly.
The 15-minute sync job produces one trace per run, with a segment per data source — so an upstream API being slow is visibly distinct from our own database being slow.
The nightly export and manual-ingest command-line jobs are traced too, and gained proper structured logging they previously lacked.
Logs are stamped with their trace ID, so a log line links straight to the request that produced it.
Health-check and metrics endpoints are excluded, and per-request internal noise dropped, so each request reads as one clean entry rather than three.
Vendor-neutral by design: the twins emit the open OpenTelemetry standard to whatever collector is configured, and do nothing at all when none is. It switched itself on when the new cluster supplied an endpoint — no application configuration was needed.
Commits
0d287df feat(telemetry): env-gated OpenTelemetry tracing for dashboards and sync job 5755dcb feat(telemetry): trace the CLI jobs and correlate logs with traces ba0c611 feat(telemetry): exclude probe URLs and drop noisy ASGI send/receive spans
🫐 Blue — the full harvest sequence on the growth tracker
Growing Degree Days accumulate heat over the season; crossing a threshold predicts a picking window.
Second and third harvest thresholds
Blue
The GDD tracker previously marked only the first harvest. Observed picking dates from the farm now give us working thresholds for the second (~1300 GDD) and third (~1500 GDD) harvests as well, so the whole picking sequence can be read off one chart.
All three harvest lines share a single light-to-dark purple ramp, so they read as one group and their ordering is visible without reading the labels — and they don't compete with the blue trace of the current year's actual accumulation.
These are approximate and derived from a small number of observed pick dates; they are expected to be refined as more seasons are recorded.
fa5b6af feat(blue): add second and third harvest GDD thresholds
🛡️ Staying up when someone else goes down
Two outages that were caused by, or hidden by, something outside the application.
A login-provider outage no longer takes the dashboards down
SharedBlueRed
On 13 July the central login provider's certificate expired. Both dashboards fetched its configuration during startup, so the failure killed them on boot — they restarted and died 21 times until someone else renewed the certificate. While it was down, nothing served: not the public pages, not even the health check, because the app refused to start over details it wouldn't need until someone clicked "log in".
Two genuinely different failures were being treated the same, and are now separated:
A missing secret is our own misconfiguration — it will never fix itself, so it still fails immediately and loudly.
The identity provider being briefly unhealthy is someone else's service — it does fix itself, so the dashboard now starts anyway, serves everything that doesn't need a login, and picks the provider up automatically once it recovers. No restart needed.
Also: Red now trains its light-prediction model in the background at startup, matching Blue, so a cold start on a fresh cluster can't stall the boot.
Commits
f9d38b3 fix(auth): don't let an OIDC provider outage crash-loop the dashboards
The nightly CSV exports had been failing silently for 23 days
BlueRed
Both twins' nightly data exports stopped working the moment production moved, and nothing said so — the last successful run was 17 July, discovered on 9 August.
The cause is a property of the new storage: an export volume can only be attached to one machine at a time, and the dashboard holds that attachment permanently. On the old cluster there were few enough machines that the export job happened to land on the right one; the new cluster has seven, so it effectively never did. The export would wait, unable to start, until it was timed out and discarded.
Export jobs are now explicitly scheduled onto the same machine as their dashboard, where they share the attachment cleanly. Verified on the live cluster before shipping, and written as one shared rule for both twins.
Commits
72bf5be fix(helm): pin export CronJobs to the dashboard node so RWO PVCs attach
⚙️ Platform & architecture (under the hood)
Less visible, but it keeps the twins trustworthy and the codebase maintainable.
The twins now run correctly on Windows in any language setting
SharedRedBlue
Contributed by Tim Riebner — the first outside contribution to the codebase this period, and a fix for a problem none of us could see.
The twins read their sensor definitions from configuration files that contain degree symbols (°C). Those files were being opened without stating which text encoding to expect, so Python fell back to whatever the operating system's language settings suggest. On macOS and Linux that is always UTF-8 and everything works; on a Windows machine set to almost any language it is not, and the degree symbols came out corrupted — °C rendering as °C on a Western European setup, and worse elsewhere.
Six file-reading points now state UTF-8 explicitly — the sensor metadata for all three twins, the greenhouse risk thresholds and growth stages, the export summaries, and the dashboards' own stylesheets and scripts.
That is every place the application reads a file, so the whole class of problem is closed rather than patched where it happened to show.
Practical effect: a new student or partner on a Windows laptop gets a working dashboard with correct units, instead of one subtly wrong in a way that is easy to mistake for a data problem.
Commits
d08b712 Fix for Windows with different locales to UTF-8 (PR #28)
Test and tooling hardening
Shared
Fixed an intermittent test failure that only ever appeared on the slower CI machines: a background database maintenance task could collide with the test's own cleanup. The fix removes the task first and waits for it, rather than papering over the race with a retry that would resurface later inside some unrelated change.
The database-bootstrap step is now a shared, reusable template across twins rather than duplicated configuration — the same pattern as the export scheduling rule.
Structured logging was extended to the dashboards, which previously had none.
Commits
e524a0e test(e2e): stop the cagg refresh policy racing the red schema teardown