CHANGELOG

What's new in SyncTide.

Release notes, required migrations and deployment steps for every public version. For a single page covering everything that ships in v1.0.1, see Features.

v1.2.0 2026-05-30 New web interface

Redesigned web interface — faster, cleaner, same data

Schema version 55 · no new migrations · Drop-in upgrade

A brand-new operator UI

The whole web interface has been rebuilt as a modern single-page application. It loads faster, navigates without full-page reloads, and keeps the dashboard, asset tree, trends, alarms, messaging, reports and equipment configuration you already know — re-laid-out for clarity. The new UI is served directly by the platform at /ui behind the same LAN address; nothing changes about how you reach SyncTide (http://<host> still lands you on the app and signs you in).

Lighter on the network

Live dashboards now refresh every device's latest values in a single request per tick instead of one request per device, via a new batch endpoint. Combined with coalesced screen updates and a bounded trend cache, busy multi-device dashboards stay smooth and put far less load on the backend.

European Portuguese

The interface ships with a full Português (Portugal) translation, switchable from the top bar.

Operator action when upgrading

No database migrations — schema stays at 55, so this is a drop-in upgrade. Windows operators run the v1.2.0 installer or apply the delta package; the old Streamlit interface is retired automatically and the platform serves the new UI on first start. Cloud installs: docker compose pull && docker compose up -d. Data collection from devices is unaffected and continues through the upgrade.

Download v1.2.0 Setup →

v1.0.4 2026-05-12 OPC UA shared session

OPC UA shared session, Reports UX, Messaging Center widgets, per-user Home Layout

Schema version 52 · 4 new migrations vs v1.0.3 · Drop-in upgrade

Loopback HTTP shared-session for OPC UA

The architectural fix the v1.0.3 robustness sweep worked around is now in place. The SyncTideOPCUA worker exposes a loopback HTTP server (port 53600 by default, override via SYNCTIDE_OPCUA_INTERNAL_PORT); the FastAPI backend's /admin/opcua/browse and /admin/opcua/browse-namespaces endpoints proxy through it. Both share the same per-endpoint asyncio.Lock, so a browse-in-progress blocks the next poll cycle and vice versa — no more racing for the PLC's 2–3 OPC UA session slots.

The backend falls back to the legacy spin-up-own-client path automatically when the worker is unreachable (service stopped, port blocked, container restarting), so the install never ends up worse than v1.0.3.

Reports History: per-tab filters, click-to-preview, delete

The Reports History page now applies device / category / date-range filters across all three sub-tabs (Generated, Outputs, Jobs). Each card has a 👁️ button to preview inline (replacing the old dropdown selector) and a 🗑️ delete button gated by operator role. Manual one-off jobs no longer pollute the Scheduled jobs list. Legacy rows without stamped device IDs are shown as wildcards so the filter never silently hides existing reports.

Messaging Center: 8 toggleable Dashboard widgets

The Messaging Center Dashboard has grown to eight widgets — KPIs, gateway status, 7-day trend, ack rate, top firing rules (now labelled device · tag · [category] instead of "Rule #6"), activity timeline, gateway health, and recent failures. Each one can be toggled independently from the new Dashboard Layout sub-tab inside Messaging Center → Configuration. The 10-second messaging scheduler keeps detection-to-send latency under 15s.

Per-user Home Layout

Home Layout moves from System Configurations to a sub-tab of User Configurations, so each operator customises their own home page independently. Admins still set the system-wide default from System Configurations → Home Layout — the "Save as system default" and "Clear system default" buttons are now admin-only. A page-permission audit fixed a viewer leak where restricted users were seeing Messaging Center and User Configurations regardless of their assigned allowlist.

Operator action when upgrading

4 additive migrations apply on first backend startup (048 device session invalidation, 049 devices.updated_at trigger fix, 050 virtual-trigger default, 051 report_outputs.device_ids backfill, 052 messaging dashboard config). docker compose pull && docker compose up -d on cloud installs; Windows operators take the v1.0.4 installer or apply the delta package. The loopback port (53600) is loopback-only on Windows and only exposed across the synctide Docker network on the cloud variant.

Download v1.0.4 Setup →

v1.0.3 2026-05-12 Field-test fixes

Installer completeness, OPC UA robustness, UX polish

Schema version 48 · 1 new migration vs v1.0.2 · Drop-in upgrade

Windows installer now registers all 11 services

v1.0.2's installer was registering only 8 Windows services — the binaries for SyncTideMQTT, SyncTideTelegramInbound, and SyncTideHealthMonitor shipped to disk but the NSSM install calls were missing, so MQTT ingestion, inbound Telegram acknowledgments, and the watchdog-of-watchdogs were silently absent on Windows. v1.0.3 registers all three; clean installs now match the Docker variant feature-for-feature.

OPC UA: no more "needs a service restart"

Several subtle robustness bugs around the OPC UA polling worker and the browse panel converged into a single field-test failure: operator adds a device, hits Browse to import variables, gets contention with the polling worker (S7-1200 has only 2-3 session slots), and ends up needing to restart SyncTideOPCUA manually before values land. v1.0.3 closes the loop:

  • Browse client uses a short 10s session timeout instead of the OPC UA default of 1 hour, so a leaked session is reclaimed in seconds. The default was wedging session slots for an hour at a time on tight-limit PLCs.
  • Session-limit errors surface clearly. When the server returns BadTooManySessions / BadResourceUnavailable, the operator gets a clean message ("Server session limit full — raise poll interval to 60s or pause polling, then retry") instead of the raw asyncua exception.
  • Worker drops the subscription session after browse touched the device (new devices.last_browse_at column). Any browse-displaced session is rebuilt automatically on the next poll cycle.
  • Worker drops the subscription session after a config edit (uses the new devices.updated_at column). Endpoint URL changes, auth changes, and polling toggles now propagate without a service restart.
  • UI warns about session-limit contention on the Browse panel so operators know to raise the poll interval temporarily before hitting Browse on a tight-limit PLC.

Configurations page restructure

Three separate top-level tabs (Data Retention, MQTT Brokers, Health Monitor) folded into the renamed Runtime Configurations tab — fewer tabs to scan, related settings now sit next to each other. Adds a new Ports & Firewall section: a hand-off-able list of every inbound listening port (Caddy LAN, FastAPI loopback, Streamlit loopback, Postgres) and every outbound destination (Telegram API, Twilio, SMTP, GitHub for updates, PLC protocols) so the client's IT team can configure the firewall without guessing. Health Monitor now shows a live per-service status panel when enabled (running / stopped / paused per SyncTide service) so operators don't have to alt-tab to services.msc or docker ps to spot which service is down.

UX polish

  • Health Monitor: license-required state now shows the friendly key-emoji banner that matches every other gated tab, not the raw license_required error string.
  • Equipment Configuration: CSV devices can finally edit their watched folder and switch protocol away from CSV. The Endpoint Configuration sub-tab was short-circuited to a static hint for CSV devices — operators had no UI path to change the folder.
  • Equipment Configuration → Real Time: CSV devices now show latest ingested values (same as protocol devices). Empty-state guidance is protocol-aware — "no tag addresses configured, use Browse" for OPC UA / Modbus / IEC, "no CSV file ingested yet" for CSV.
  • Configurations → Current Runtime: ingestion interval changes now show in the UI immediately after Save (the underlying cache was holding the old value). The value was always persisting correctly; the UI just wasn't reflecting it.
  • Last-ingestion timestamp on Configurations no longer reads an hour ahead of the wall clock. Naive datetime.now() writes were getting re-interpreted as UTC by the local-time formatter.
  • Map page: theme toggle renamed Map → Light (with the existing Dark option) — the previous "Map" label was meaningless against Dark.
  • Installer: three port/URL popups during install collapsed to one. v1.0.2 was showing a pre-install summary popup, a mid-install port-fallback popup, and a final URL popup — all carrying overlapping content. The pre-install popup now only fires when there's a hard blocker (port 8000 or 8501 in use), and the mid-install duplicate is gone.

Operator action when upgrading

Migration 048 is additive and idempotent — it adds two TIMESTAMPTZ columns to devices (updated_at, last_browse_at) plus an auto-update trigger. docker compose pull && docker compose up -d on cloud installs. Windows operators run the v1.0.3 installer or apply the delta package — both register the three previously-missing services on top of an existing v1.0.2 install.

Download v1.0.3 Setup →

v1.0.2 2026-05-11 Robustness sweep

Health monitor, automated backups, security hardening

Schema version 47 · 1 new migration vs v1.0.1 · Drop-in upgrade

Health monitor — watchdog of watchdogs

A new health-monitor Docker service pings the backend and checks every expected container once a minute. After 3 consecutive failures it pages the operator over Telegram (using the existing messaging gateway — no extra bot), with a recovery message on the first clean tick. Configurable from Configurations → Health Monitor: enabled/disabled, Telegram gateway + chat id, check interval, failures-before-alert, cooldown, expected-services override.

Disabled by default — turn it on once a Telegram gateway is wired in Messaging Center.

Automated daily backups

The new backup sidecar service runs backup_cloud.sh daily at 03:00 UTC. Captures a pg_dump of the SyncTide database (TimescaleDB-aware), tarred bind-mounted folders (raw_data, reports, templates), tarred Docker volumes (mosquitto, Caddy TLS certs), plus a passwords-masked copy of .env. Rolling 30-day retention.

Restore procedure verified end-to-end against a fresh ephemeral TimescaleDB container: every reference table matches the source 1:1. Full runbook at RECOVERY.md.

Security hardening (5 audit fixes)

  • POST /license/upload now requires admin role. Previously unauthenticated — the signature check prevented forgery but a LAN attacker could still DoS the platform or downgrade-replace with a different valid license they obtained elsewhere.
  • must_change_password enforced server-side. Earlier only the Streamlit UI redirected; direct API callers with a stolen bearer token bypassed the gate. get_current_user now refuses every path except /auth/me, /auth/logout, /users/me/password when the flag is set.
  • init_admin.py seeds new admins with must_change_password = TRUE. Customers running docker compose up with the unedited template are force-flipped into the reset flow on first login.
  • _DEFAULT_PASSWORDS expanded with the docker template seed so the startup-time scan catches the "never edited the template" case.
  • Bulk alarm endpoints (acknowledge/clear device + all) bumped from viewer → operator role. Industrial SOP: viewers see, operators acknowledge. Single-alarm acknowledge stays at viewer.

Operator action when upgrading

None on Postgres data — migration 047 is additive and idempotent (a single-row config table seeded disabled). docker compose pull && docker compose up -d is the whole upgrade. The race between health-monitor's first poll and migration 047 (caught in dev) is fixed by gating the service on the backend healthcheck.

Raw changelog (.md)

v1.0.1 2026-05-11 Public launch

Public launch — comms resilience, Telegram ack-by-reply, attributed acks

Schema version 46 · 2 new migrations vs v1.15.4 · Drop-in upgrade

Versioning note

The pre-launch 1.15.x stream was internal-only iteration. v1.0.1 is the first numbered release we ship to paying customers; from here on the public version line is monotonic and the full upgrade path (delta packages, migration replay, in-place container swap) is fully supported. Schema version keeps climbing — it counts applied migrations, not platform releases — so downstream upgrade logic Just Works.

Comms resilience — workers self-heal from cable yanks

  • BaseException cleanup in the OPC UA and Modbus drivers — any non-clean exit (including asyncio.CancelledError from the outer timeout) drops the cached client/subscription session so the next cycle reconnects fresh. Previously a single cancellation could leave a dead socket in cache and every subsequent attempt hung on it until manual restart.
  • OPC UA aliveness probe — once per poll cycle the worker reads ServerStatus.State; a failure flips the device to Error within seconds even when the subscription queue is silent.
  • Exponential backoff per device — 5s → 10s → 20s → … → 300s cap, reset on first success. After 10 consecutive failures the device is flagged "quarantined" in the log.
  • Hot drop on disable/delete — workers tear down cached state without needing a container restart.

Cable-yank end-to-end verified: 14 s recovery on OPC UA, ~40 s on Modbus.

Telegram inbound — close the ack loop

  • New telegram-inbound worker — long-polls getUpdates for every active Telegram gateway. One HTTP round-trip per ~25 s when idle, near-zero quota cost.
  • Flexible matching cascade: Telegram swipe-to-reply (matches by provider_message_id, most precise) → ACK-{token} (original format still works) → simple keyword (ack, ok, okay, confirmo, confirmar, sim, yes, ✅, 👍, ✓) matched against the most recent unacked Telegram message for the sender's chat_id within 24 h.
  • Outgoing instruction simplified — Telegram messages now read "💬 Reply with 'ack', 'ok' or 👍 to acknowledge" instead of the unfriendly token. SMS / WhatsApp / email keep the precise token because their webhook parsing relies on it.
  • Confirmation reply — bot replies "✅ Acknowledged. Escalation cancelled." the moment a match lands.

Alarm history attribution (migration 046)

New column alarm_events.acknowledged_by_label populated by every ack path: Platform: Administrator, Telegram: Tiago G., SMS: +351 912 345 678, Token: <contact>. Past in-platform acks backfill from the users join.

Seconds-resolution comm timeout (migration 045)

The "Update data timeout" used to be stored as integer minutes — too coarse for sub-second OPC UA subscriptions and IEC 104. New column devices.comm_timeout_seconds (CHECK ≥ 5 s); UI gains a Seconds box alongside Days/Hours/Minutes. Timer is based on the most recent measurement timestamp the device reported, not on polling attempts — a subscription cycle with no new data does not reset the clock.

UX polish

  • Streak detection — device flips to Error after 3 consecutive failed polls regardless of rolling 20-poll rate.
  • Cooldown = 0 is now valid for messaging rules. Default for new rules stays at 30 minutes; zero means every alarm trigger sends a fresh message with no rate limit.
  • Compact virtual-tag editor — Tag / Operator / Function pickers are dropdowns (scales to hundreds of tags). Site-scope picker shows Device + Tag pair side-by-side.
  • Plain-English virtual-tag errors"Missing operator between ${Ambient Temp} and ${ALC105:Flow}" instead of the previous parser dump.
  • OPC UA browse now lets you pick ns=0 / ns=1 for diagnostics on non-standard servers.
  • Cascade delete on tag-address Save — removing a row from Tag Addresses also drops the corresponding mapping and metadata rows so ghost names stop showing in Real Time + Variable Mapping.
  • Tag Address row delete keys widgets by stable per-row UUID so deleting a row in the middle actually removes the clicked row.
  • Unicode threshold operators — uses ≥ ≤ ≠ glyphs so Streamlit's Markdown parser doesn't swallow the leading > as a blockquote.
  • Reports cross-FS fixPath.replaceshutil.move; PDF generation works under Docker where /tmp and /app/reports_output sit on different filesystems.

Operator action when upgrading

None on Postgres data — migrations 045 + 046 are additive and idempotent. Re-running them on a customer instance is a no-op. The container image tag changes from synctide:1.15.0-test1 to synctide:1.0.1. docker compose up -d after pulling reads the new default from docker-compose.yml.

Raw changelog (.md)