• v0.4.0 baf8aade2d

    serversdown released this 2026-06-25 12:42:04 -04:00 | 3 commits to main since this release

    Added — Mirror (dual-send)

    • Optional best-effort mirror server. Each heartbeat and each forwarded event can now be sent to a second ("mirror") server in addition to the primary, so a standby box (e.g. the office NAS) stays a continuous replica during a migration — making the eventual cutover a non-event. Default off — blank mirror_api_url/mirror_sfm_url means existing installs are unchanged after auto-update.
    • Heartbeat mirror (best-effort). After the primary heartbeat POST, the same payload is fired at mirror_api_url when set. Bounded by the existing API timeout and fully wrapped — it can never delay or fail the primary heartbeat; its result is ignored except for a debug log line.
    • Event mirror (reliable, isolated). Events are re-forwarded to mirror_sfm_url using a separate sha256 state file (mirror_sfm_state_file, blank → <log_dir>/thor_forwarded_mirror.json), so the mirror tracks its own delivery independently of the primary. A quick reachability probe (~3 s) skips the mirror pass when the mirror server is down, so a dead mirror never stalls the loop on per-event timeouts; skipped events stay pending in the mirror state and deliver when the mirror returns — no data loss.
    • Isolation invariant (the one rule). Nothing on the mirror path can delay or fail the primary: its own state file, its own try/except, the reachability guard + bounded timeouts, all exceptions swallowed-and-logged. The primary path stays exactly as reliable as before.
    • Mirror tab in the Settings dialog with Mirror API URL + Mirror SFM URL fields (blank = off). The mirror rides along with the primary — there is no separate enable flag (heartbeat-mirror active iff the primary heartbeat is on and mirror_api_url is set; event-mirror active iff primary forwarding is on and mirror_sfm_url is set).
    • New unit tests in test_event_forwarder.py covering mirror reachability, the down-mirror skip, separate-state idempotency, and the isolation invariant (a mirror failure leaves the primary's result + state untouched).

    Configuration

    New config.json keys (all default-empty = off; existing 0.3.x deployments don't change behaviour on auto-update):

    Key Default Notes
    mirror_api_url "" Second heartbeat destination — same URL form as api_url (blank = off)
    mirror_sfm_url "" Second SFM base URL, e.g. http://10.0.0.x:8200 (blank = off)
    mirror_sfm_state_file "" Override the mirror's forwarded-sha256 state file. Blank → <log_dir>\thor_forwarded_mirror.json

    Changed

    • Bumped VERSION to 0.4.0

    Operational note (migration seeding). To make the mirror re-deliver only the gap since a one-time replica snapshot, copy the primary state file to the mirror state file and drop entries whose forwarded_at is after the snapshot. SFM-side dedup covers any overlap. See docs/mirror-dual-send-design.md.

    Downloads