Skip to content

Releases: goldbarth/port-tidewatch

v1.3.0 — Readability

16 Jun 14:35
cfa78be

Choose a tag to compare

port-tidewatch v1.3.0

Release date: 2026-06-16

Readability (M9). v1.2 brought real data; v1.3 makes the displayed values
legible — what each number means and how current it is. This milestone
replaces the dropped M8 observability work (see the ADR-003 amendment): the
OpenTelemetry path stays wired but unsurfaced, and the effort went into honest,
domain-true freshness instead.

Highlights

  • Freshness reflects the measurement, not the poll — the per-gauge indicator
    showed the age of the last successful API poll, so a fresh poll against a
    minutes-old PEGELONLINE reading read as "live". It now shows the age of the
    reading itself (Reading.Timestamp): "Messwert: vor N min".
  • Staleness keys off data arrival, not measurement age — PEGELONLINE
    publishes with a multi-minute lag, so a healthy feed is always minutes-old; an
    age-based threshold would flag a permanent false "stale". A tile now turns
    stale only when new data stops flowing — measured from when the last
    reading arrived, against a threshold derived from the inferred source cadence
    (2× cadence, with a floor for the dashboard's own poll interval). So the
    Simulator (~seconds) and PEGELONLINE (~minutes) get the right threshold
    automatically, with nothing hard-coded.
  • Wall-clock time anchor in the header — a running clock (HH:MM:SS,
    Europe/Berlin) next to the live indicator, so a measurement's age reads against
    the current time: current time → measurement N min old → status. Client-side,
    updated every second, no API call.
  • Local dashboard works on a fresh cloneconfig.json ships the deploy
    placeholder https://<INGESTION_FQDN>, substituted only at deploy time.
    AppConfig now treats an unresolved placeholder as same-origin, so following
    the local runbook reaches the dev-proxied /api instead of a dead host.

What's changed

Freshness (ingestion + dashboard)

  • The gauge API exposes three new fields per gauge: measuredAt (the newest
    reading's measurement time, source clock), cadenceSeconds (the inferred
    source cadence — the median gap between reading timestamps, so a single
    missed poll does not inflate it), and lastReadingAt (when that reading
    arrived, on our clock).
  • The state holder records each gauge's last arrival time; the API mapper derives
    the cadence. State stays raw — view concerns live in the mapper (ADR-002).
  • The dashboard shows a per-tile freshness chip from measuredAt, classifies
    stale from arrival recency vs the cadence-derived threshold, and drives all
    relative times from one shared 1 Hz clock (no per-component timers).
  • The header indicator is now poll-liveness only (live / veraltet /
    verbinde…); all measurement times moved into the tiles.

Config / local dev

  • AppConfig normalizes an empty or unresolved "<…>" apiBaseUrl to
    same-origin /api; a real FQDN is kept (trailing slash trimmed). The deploy
    substitution (sed in the workflow / the azure.yaml hook) is unchanged, so
    production still serves the real ingestion FQDN — the change only affects the
    pre-substitution (local) case.

Tests

  • Mapper: measuredAt, median cadenceSeconds (incl. robustness to a missed
    interval), and lastReadingAt pass-through.
  • Freshness threshold: PEGELONLINE 2× cadence, no flapping at a normal
    interval, Simulator's few-second window, and unknown-cadence handling.
  • AppConfig: placeholder, empty, real FQDN, and fetch-error fallback.

Known limitations (by design)

  • PEGELONLINE readings are inherently minutes old — the public feed publishes
    with a multi-minute lag. The dashboard now shows this honestly: the age can sit
    at several minutes while the tile stays "live", because new data keeps arriving
    on cadence. Stale means the feed has actually stalled.
  • Dashboard remains read-only and polling-based.
  • No notification delivery — alert events are published but acting on them is
    still out of scope.
  • OpenTelemetry is wired but not surfaced — the M8 dashboard observability
    work was attempted and reverted; the trace path still exports to Jaeger/OTLP,
    just without an in-dashboard view.

What's next

The roadmap beyond M9 is open. The core pipeline, real data feed, deploy paths
(k8s + Argo CD, Container Apps), and now legible freshness are in place; see
docs/ISSUES.md
for the milestone history and any new work.

v1.2.0 — Real data

15 Jun 13:52
73592d8

Choose a tag to compare

port-tidewatch v1.2.0

Release date: 2026-06-15

Real data (M7). v1.1 made the pipeline demonstrable with a scripted surge; v1.2
feeds it from the live PEGELONLINE Elbe gauges alongside that simulator,
behind a source-selection switch — without touching the Reading contract or
the consumer path.

Highlights

  • Live PEGELONLINE Elbe feed — a source adapter polls the public WSV REST-API
    for four Hamburg gauges (St. Pauli, Bunthaus, Over, Zollenspieker) and emits the
    same Reading records the simulator does. Centimetres-above-gauge-zero are
    converted to metres NHN in an explicit mapping layer (W/100 + PNP offset);
    conditional GET (If-None-Match) means a 304 emits no duplicate reading, and
    transient API failures are logged and retried without crashing the host.
  • Source-selection switch — a single ReadingSource config value
    (Simulator | Pegelonline) chooses the active source at startup, so the same
    build serves the scripted demo or the real feed without recompiling. A bad or
    empty value fails fast at startup, consistent with the threshold-options
    validation.
  • Producer renamed to Tidewatch.Source — the simulator project became a
    Generic Host that hosts interchangeable reading sources behind IReadingSource;
    the simulator is now one source among others, so the name and the deployed
    workload (reading-source) are source-neutral.

What's seen on the dashboard

Four live Hamburg Elbe gauges, all normal

Live dashboard on real WSV/PEGELONLINE data — the public Hamburg Elbe gauges (St. Pauli, Zollenspieker, Over, Bunthaus), all normal.

The scripted-surge demo (video and screenshots) from
v1.1
still tells the alerting story best — real Elbe levels normally sit well below the
4.50 m NHN warning boundary, so the live feed reads calm normal. The live feed's
value is the real-data integration: cm → PNP → NHN mapping and conditional polling
against the public WSV API.

What's changed

Reading-source host (was Tidewatch.Simulator)

  • Moved to a Generic Host (Microsoft.NET.Sdk.Worker): DI, Options, graceful
    shutdown. The publish path is the IReadingPublisher seam; sources depend on
    intent, not transport.
  • SimulatorSource holds the scripted surge (behaviour unchanged — same Reading,
    exchange, routing key; SURGE_PEAK_M / SURGE_PERIOD_S / RABBITMQ_HOST still
    honoured).
  • PegelonlineSource + PegelonlineClient + PegelMapper: latest value, window
    backfill on start, live gaugeZero lookup, and the cm → m / PNP → NHN mapping.
    Stations are configured by UUID. HPA tidal gauges expose no gaugeZero, so their
    PNP is set explicitly (Hamburg PNP = NHN −5.00 m); the live lookup remains for
    other WSV stations.
  • ReadingSourceOptions + validator select the active source at startup
    (ValidateOnStart); only the chosen source and its dependencies are registered.

Deploy

  • The deployed producer runs the live feed: ReadingSource=Pegelonline is set via
    env var on the k8s reading-source Deployment and the Container Apps app; the
    local appsettings default stays Simulator for the documented demo.
  • The producer workload was renamed simulatorreading-source across the k8s
    manifests, kustomization, azure.yaml, and the bicep (resource, name,
    azd-service-name), plus the runbook.

Tests

  • PegelMapper cm → m / PNP → NHN pinned end to end against a known St. Pauli
    value; ReadingSource validator (known / missing / unknown / numeric).

Known limitations (by design)

  • Live feed reads calm — real Elbe levels stay below the storm-surge
    thresholds, so the live source shows normal; the simulator remains the
    dramatic demo. The two are not run side by side (single source per run).
  • Dashboard remains read-only and polling-based.
  • No notification delivery — alert events are published but acting on them is
    still out of scope.

What's next (v1.3)

Observability made visible — see M8 in
docs/ISSUES.md:
surface the existing OpenTelemetry path in the dashboard (a pipeline latency pulse,
a Jaeger deep-link, and an optional self-rendered trace waterfall).

v1.1.0 — Demo & polish

12 Jun 15:02
d2ad734

Choose a tag to compare

port-tidewatch v1.1.0

Release date: 2026-06-12

Demo & polish (M6). v1.0 was a complete, observable pipeline; v1.1 makes it
demonstrable and richer — a scripted storm surge so severe actually happens,
a presentable dashboard, more to watch per gauge, and alert events published off
the stage-change chokepoint.

Demo

The simulator drives one gauge (CUX) through a scripted surge while the others
hold normal, so the full normal → warning → severe → recede cascade is
visible end to end:

If the player does not load, download the clip
(≈ 60 s).

All gauges normal CUX entering severe at the surge peak CUX sustained severe with the peak marker near the top band
Calm — all normal. Peak — CUX severe (5.65 m). Held severe (5.75 m).

What you're seeing. Cards are gauges on a fixed 0–6 m NHN scale; the
shaded bands are the WADI thresholds (warning 4.50 m, severe 5.50 m).
The header summarises gauges per stage, the highest current level, and overall
status, and the live / last-updated indicator makes stale data obvious.

Highlights

  • Scripted storm-surge scenario — the simulator now drives a tide baseline
    plus a smooth raised-cosine surge (peak/period parameterisable via
    SURGE_PEAK_M / SURGE_PERIOD_S), so severe is reachable and the cascade
    tells a story instead of random noise.
  • Tidal operations dashboard — banded area charts on a fixed 0–6 m domain
    (the bands are the thresholds), a header summary, relative time-in-stage
    ("warning for 3 min"), a live / stale / offline indicator, self-hosted fonts,
    and animated stage transitions.
  • Richer per-gauge signals — the API DTO gains rate-of-change (m/min),
    time-in-current-stage, and window min/max, computed in the mapper (state stays
    raw, ADR-002) and surfaced as a trend arrow.
  • Alert-event publishing — a genuine stage change publishes an AlertEvent
    (gauge, previous → new stage, level, timestamp) to a fanout exchange from the
    single ApplyStageChange chokepoint (ADR-001), traced consistently with the
    existing OpenTelemetry path.

What's changed

Simulator

  • Composite level model (baseline + tide + scripted surge + small noise) with
    per-gauge roles; one gauge surges, the rest stay normal. The Reading
    contract and the publish path are unchanged — only level generation differs.

API & dashboard

  • GaugeDto gains rateMetersPerMin, timeInStageSeconds, windowMin,
    windowMax; the endpoint passes a single UtcNow so a response is internally
    consistent.
  • Dashboard reworked: banded charts with a peak marker, header summary, relative
    stage time, connection indicator (last good snapshot held across a failed
    poll — ADR-002 addendum), dark "tidal operations" theme with self-hosted
    Fraunces + IBM Plex Mono.

Ingestion

  • New AlertEvent contract and an IAlertPublisher seam; RabbitMqTransport
    declares a fanout alert exchange plus a durable audit queue and owns a
    dedicated publish channel. The initial normal establishment is suppressed,
    so a held stage publishes nothing.

Tests

  • Unit coverage for the computed DTO fields; integration tests that a single
    transition yields exactly one alert event and a held stage yields none.

Known limitations (by design)

  • No notification delivery — the chokepoint publishes alert events, but
    acting on them (email / push, a notification consumer) is out of scope.
  • Simulator is still the sole source — a real PEGELONLINE Elbe feed is the
    v1.2 (M7) step.
  • Dashboard remains read-only and polling-based.

What's next (v1.2)

Real data — see M7 in docs/ISSUES.md: a PEGELONLINE source
adapter (cm → m, PNP → NHN) alongside the simulator, a source-selection switch,
and a threshold "what-if" panel.

v1.0.0

11 Jun 17:01
adb3274

Choose a tag to compare

port-tidewatch v1.0.0

Release date: 2026-06-11

First release. A small, focused storm-surge ingestion service modelled on the
Hamburg warning service (WADI): it ingests water-level readings, evaluates them
against staged thresholds in metres above NHN, and surfaces per-gauge alert
state on a read-only dashboard — observable end to end and deployable two ways.

Highlights

  • End-to-end ingestion pipeline — simulator → RabbitMQ → ingestion → state,
    with a dead-letter path for poison messages.
  • Staged, trend-aware surge evaluationnormal / warning (4.50 m) /
    severe (5.50 m), with a rising-trend nudge and de-escalation hysteresis so
    the stage does not flap on single outliers.
  • Distributed tracing — OpenTelemetry across the whole path (simulator →
    broker → ingestion) with W3C context propagation through the AMQP headers.
  • Read-only dashboard — Angular (standalone + signals), per-gauge level,
    stage, and a recent-history sparkline, polling a small HTTP API.
  • Two deployment targets — Kubernetes + Argo CD (GitOps) and Azure Container
    Apps, both verified.

What's included

Service & domain

  • Tidewatch.ContractsReading (decimal metres, DateTimeOffset) and
    AlertState.
  • Tidewatch.Ingestion — RabbitMQ transport with exchange/queue/dead-letter
    topology; ReadingConsumer (dead-letters malformed/invalid messages);
    per-gauge GaugeStateHolder with a single isolated stage-change point;
    SurgeEvaluator (median base stage + trend pre-escalation + hysteresis).
  • SurgeThresholdOptions bound from configuration and validated at startup
    (a bad threshold config fails fast).
  • Tidewatch.Simulator — emits readings for four gauges (CUX, HEL, STP, BHV).

API & dashboard

  • Read-only HTTP API on the ingestion host: GET /api/gauges (snapshot + a
    trend downsampled to a fixed bucket count) and /healthz.
  • Angular dashboard: gauge cards (level, stage badge, sparkline), 4 s polling,
    runtime-configurable API base URL, same-origin in dev/k8s and cross-origin
    (CORS) on the cloud stack.

Observability & tests

  • OpenTelemetry tracing with OTLP export; spans for receipt → evaluation →
    stage change, dead-letter outcomes marked as errors.
  • Unit tests for the evaluator (boundary, trend, hysteresis, outlier damping)
    and Testcontainers integration tests (boundary and rising-trend, end to end
    against a real broker).

Deployment

  • Kubernetes + Argo CD (primary) — Kustomize manifests for rabbitmq,
    ingestion, simulator, dashboard, and Jaeger, with an Ingress for same-origin
    /api routing; an Argo CD Application syncs from the repo. Runs on a local
    kind cluster (€0); portable to AKS.
  • Azure Container Apps — azd + Bicep (ACR, Container Apps environment, the
    three apps) plus Azure Static Web Apps (Free) for the dashboard. GitHub
    Actions: CI on push/PR, manual deploy via federated OIDC.

Decisions

Recorded as ADRs 001–004 under docs/adrs/ (threshold placement, dashboard
state, deploy target, evaluator algorithm). Runbooks for all three run modes are
under docs/.

Known limitations (by design)

  • No alert-event publishing yet — the stage-change chokepoint updates state
    only; publishing to a dedicated exchange is the v1.1 step (ADR-001).
  • Dashboard is intentionally minimal and read-only.
  • No cloud tracing backend in the Container Apps stack; tracing is exercised
    locally (and in the Kubernetes stack via an in-cluster Jaeger).
  • Kubernetes target is a local cluster (kind), chosen to keep cost at €0;
    AKS is a later swap.

What's next (v1.1)

Demo and polish — see M6 in docs/ISSUES.md: a storm-surge
scenario in the simulator (so severe is demonstrable), dashboard visual polish,
richer per-gauge signals, demo assets, and alert-event publishing.