65b3af90ae
When a binary is forwarded WITHOUT its paired _ASCII.TXT (because
the TXT wasn't quiescent within the grace period — BW slow to
write, AV scanning, etc.), the old behaviour was to permanently
mark the binary as "done" in the state file, even though the TXT
might land seconds later. Result: that event lived in SFM forever
with broken-codec peak values and no project info.
Fix: state entries now carry a had_report flag. Forwards without
a TXT set had_report=False. On subsequent scans, the watcher
treats had_report=False entries as re-pair candidates — they get
re-forwarded once the TXT appears, and the SFM server's upsert
path (in seismo-relay's insert_events IntegrityError handler)
refreshes the DB row with the report's authoritative values.
Three status states in ForwardState.status(sha256):
None — never forwarded. First-forward path.
True — forwarded successfully WITH report (or legacy entry
without the had_report field). Permanently done.
False — forwarded WITHOUT report. Re-pair if TXT now exists.
Backward compat: legacy state-file entries (no had_report key)
default to True so existing deployments don't unexpectedly
re-forward every entry on upgrade.
Tests cover:
- re-pair when TXT appears after a had_report=False forward
- had_report=True entries stay skipped permanently
- legacy entries (missing field) treated as fully forwarded
- state.status() returns None for unknown sha
- re-marking had_report=False then True promotes to fully-done
36 watcher tests pass (was 31, +5 new).
12 KiB
12 KiB
Changelog
All notable changes to Series 3 Watcher will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased — v1.5.0
First release of the SFM event forwarder.
Added — SFM event forwarder
- Forward Blastware event binaries (+ paired BW ACH ASCII reports) to an SFM server. When
SFM_FORWARD_ENABLED=trueandSFM_URLis set, every event binary in the BW ACH watch folder is POSTed as multipart to/db/import/blastware_filealong with its<stem>_<ext>_ASCII.TXTpartner report (BW ACH convention; manual-export<binary>.TXTis also supported as a fallback). SFM parses the report and indexes the full per-channel stats (PPV, ZC Freq, Time of Peak, Peak Acceleration / Displacement, Peak Vector Sum + time, sensor self-check Pass/Fail, monitor-log timestamps) into a searchable database — no codec decoding required. - Idempotent forwarding. Forwarded files are tracked by sha256 in a JSON state file (default
<log dir>/sfm_forwarded.json, override viaSFM_STATE_FILE). Re-scans don't re-POST and the state survives restarts / auto-updates. - Re-pair on late-arriving TXT. When the watcher forwards a binary alone (
_ASCII.TXTpartner didn't appear withinSFM_MISSING_REPORT_GRACE_SECONDS), the state file recordshad_report: false. On subsequent scans, the watcher re-checks whether the TXT has since arrived. If yes, the event is re-forwarded with the TXT attached — the SFM server's upsert path refreshes the DB row with the report's device-authoritative peak / project values. Without this, slow-disk or AV-interrupted TXT writes would permanently leave that event with broken-codec peaks in the SFM database. Legacy state-file entries (without thehad_reportfield) default tohad_report: trueso an upgrade doesn't unexpectedly re-forward existing entries. - Quiescence + grace-period guards. Files modified within
SFM_QUIESCENCE_SECONDS(default 5s) are skipped to avoid forwarding mid-write. If a binary's report partner hasn't appeared afterSFM_MISSING_REPORT_GRACE_SECONDS(default 60s), the binary is forwarded alone rather than blocking forever. - Per-pass rate cap.
SFM_MAX_FORWARDS_PER_PASS(default 500) drips first-deploy backfill instead of hammering the SFM server in one burst. At 60-secondSFM_FORWARD_INTERVAL_SECONDScadence that's ~30K events/hour throughput. Set to0for unlimited. Scan walks oldest-first so backfill advances chronologically and successive scans reliably progress. event_forwarder.py --seed-stateCLI mode. Walks the watch folder once, sha256s every in-window event, and marks them all as already-forwarded without POSTing anything. Recommended pre-deploy workflow on machines with a large historical archive — flipSFM_FORWARD_ENABLED=trueafter seeding and only events that appear from then on get forwarded.- SFM Forward tab in the Settings dialog with: Forward checkbox, SFM URL + Test button (GETs
/health), Forward Interval / Quiescence / Missing-Report Grace / HTTP Timeout / Max Events Per Pass spinboxes, State File entry with Browse... Save-time guard: enabling forwarding without a URL shows a validation error. - Histogram-aware log clarity. Histogram events (extensions ending in
H) don't get auto-exported reports from BW; the log distinguishes that case ((histogram, no report expected)) from a waveform with unexpectedly missing report (no report ⚠). - README "First-time deployment" section documenting the seed-state workflow + the rate cap as belt-and-suspenders for safe rollout on machines with hundreds of thousands of historical events.
- 31 new unit tests in
test_event_forwarder.pycovering filename matching, state idempotency, scan logic (quiescence / grace period / max age / already-forwarded / TXT pairing), multipart byte shape, rate cap (oldest-first, cap=0 unlimited, cap=N enforcement), seed-state mode (in-window seeding / max-age skip / end-to-end skip-after-seed / idempotent re-runs), histogram classification, and an end-to-end POST against a stdlib fake server.
Configuration
New [agent] keys (all default-off — existing 1.4.x deployments don't change behaviour on auto-update):
| Key | Default | Notes |
|---|---|---|
SFM_FORWARD_ENABLED |
false |
Master toggle for the forwarder |
SFM_URL |
empty | e.g. http://10.0.0.44:8200 |
SFM_FORWARD_INTERVAL_SECONDS |
60 |
Scan-and-forward cadence |
SFM_QUIESCENCE_SECONDS |
5 |
Skip files modified in the last N seconds |
SFM_MISSING_REPORT_GRACE_SECONDS |
60 |
Forward without TXT after this delay |
SFM_HTTP_TIMEOUT |
60 |
Per-request HTTP timeout |
SFM_STATE_FILE |
<log dir>/sfm_forwarded.json |
Override location of the forwarded-sha256 state file |
SFM_MAX_FORWARDS_PER_PASS |
500 |
Per-scan cap (0 = unlimited) |
Compatibility
- Requires SFM server v0.16+ (the
/db/import/blastware_fileendpoint that accepts paired_ASCII.TXTreports + the BW-report label normalisation — released alongside this watcher version on the seismo-relay side).
[1.4.4] - 2026-03-17
Removed
OK_HOURSandMISSING_HOURSconfig keys and Settings dialog fields removed — unit status thresholds are calculated by terra-view from rawage_minutes, not by the watcher. These fields had no effect since v1.4.2.
[1.4.3] - 2026-03-17
Added
- Auto-updater now logs all activity to the watcher log file (
[updater]prefix) — silent failures are now visible. - Configurable update source:
UPDATE_SOURCE = gitea(default),url, ordisabled. Inurlmode the watcher fetchesversion.txtand the.exefrom a custom base URL (e.g. terra-view) instead of the Gitea API — enables updates on isolated networks that cannot reach Gitea.disabledturns off automatic checks while keeping the remote push path (from terra-view) functional. - New Updates tab in the Settings dialog to configure
UPDATE_SOURCEandUPDATE_URL.
Fixed
- Downloaded
.exeis now validated before applying: absolute size floor (100 KB), relative size floor (50% of current exe), and MZ magic bytes check. A corrupt or truncated download is now rejected and logged rather than silently overwriting the live exe. - Swap
.batnow backs up the current exe as<exe>.oldbefore overwriting, providing a manual rollback copy if needed. - Swap
.batretry loop is now capped at 5 attempts — was previously infinite if the file remained locked. - Swap
.batnow cleans up the temp download file on both success and failure.
[1.4.2] - 2026-03-17
Changed
- Tray icon color now reflects watcher + API health rather than unit ages — green=API OK, amber=API disabled, red=API failing, purple=watcher error.
- Status menu text updated to show
Running — API OK | N unit(s) | scan Xm ago. - Units submenu removed from tray — status tracking for individual units is handled by terra-view, not the watcher.
- Unit list still logged to console and log file for debugging, but no OK/Pending/Missing judgement applied.
watcher_statusfield added to heartbeat payload so terra-view receives accurate watcher health data.
[1.4.1] - 2026-03-17
Fixed
config.ininow saves toAppData\Local\Series3Watcher\instead ofProgram Files— fixes permission denied error on first-run wizard save.- Config path resolution in both
series3_tray.pyandseries3_watcher.pyupdated to usesys.frozen+LOCALAPPDATAwhen running as a PyInstaller.exe. - Status menu item now uses a callable so it updates every time the menu opens — was showing stale "Starting..." while tooltip correctly showed current status.
- Settings dialog now opens in its own thread — fixes unresponsive tabs and text fields while the watcher loop is running.
- Tray icon reverted to plain colored dot — custom icon graphic was unreadable at 16px tray size.
.icofile is still used for the.exefile icon.
Changed
- Terra-View URL field in settings wizard now accepts base URL only (e.g.
http://192.168.x.x:8000) —/api/series3/heartbeatendpoint appended automatically. - Test Connection button now hits
/healthendpoint instead of posting a fake heartbeat — no database side effects. - "terra-view URL" label capitalized to "Terra-View URL".
- Default log path updated to
AppData\Local\Series3Watcher\agent_logs\series3_watcher.log. - Installer now creates
agent_logs\folder on install. BUILDING.mdadded — step-by-step guide for building, releasing, and updating.
[1.4.0] - 2026-03-12
Added
series3_tray.py— system tray launcher usingpystray+Pillow. Color-coded icon (green=OK, amber=Pending, red=Missing, purple=Error, grey=Starting). Right-click menu shows live status, unit count, last scan age, Open Log Folder, and Exit.run_watcher(state, stop_event)inseries3_watcher.pyfor background thread use by the tray. Sharedstatedict updated on every scan cycle with status, unit list, last scan time, and last error.- Interruptible sleep in watcher loop — tray exit is immediate, no waiting out the full scan interval.
Changed
main()now callsrun_watcher()— standalone behavior unchanged.requirements.txtupdated to document tray dependencies (pystray,Pillow); watcher itself remains stdlib-only.
[1.3.0] - 2026-03-12
Changed
- Renamed program to "series3-watcher" and main script to
series3_watcher.py— better reflects what it does (watches for activity) rather than implying active data emission. - Default
SOURCE_TYPEupdated toseries3_watcher. - Default log filename updated to
series3_watcher.log.
[1.2.1] - 2026-03-03
Changed
- Changed the name of the program to "series3-agent", this was done to align with the s4/thor agent and because it represents the program's functionality better.
- All instances of "emitter" changed to agent.
- config.ini added to .gitignore, replaced with a template example file.
- README.md updated to reflect changes.
1.2.0 - 2025-12-04
Changed
- Removed roster CSV dependency and all Dropbox refresh/hot-reload logic; heartbeat now only enumerates
.MLGfiles. - Added
MAX_EVENT_AGE_DAYSfilter to ignore stale events and log when no recent activity exists. - Simplified heartbeat output/logging to show detected units only; logging hardened to never crash the agent.
1.1.1 - 2025-12-02
Added
- Example
config.ininow ships with API heartbeat settings enabled (API_ENABLED,API_URL,API_INTERVAL_SECONDS,SOURCE_ID,SOURCE_TYPE).
1.1.0 - 2025-12-01
Added
- Standardized SFM telemetry payload builder and periodic HTTP heartbeat POST via
urllib. - Config support for API heartbeat (
API_ENABLED,API_URL,API_INTERVAL_SECONDS,SOURCE_ID,SOURCE_TYPE); payload includes file path/size metadata.
Changed
- Refactored scanner to retain file paths and header sniff cache; reformatted logging/ANSI handling.
1.0.1 - 2025-11-20
Added
API_URLconfig key andreport_to_serverper-unit POST hook (addsrequestsdependency).
Changed
- Example
config.iniroster URL updated; merged intomain.
1.0.0 - 2025-11-17
Added
- Automatic roster refresh from Dropbox at a configurable interval (
ROSTER_REFRESH_MIN_SECONDS). - Hot-reload of roster file without restarting the script.
- Failsafe reload: if the new roster is missing or invalid, the previous good roster is retained.
- Atomic roster downloads (temp file in-place replace) to avoid partial/corrupted CSVs.
- Startup config echo printing WATCH_PATH, ROSTER_FILE, and ROSTER_URL visibility.
- Active / Bench / Ignored unit categories for clearer fleet status mapping.
Fixed
- Removed stray
note=note_suffixbug in the Unexpected Units section. - Removed duplicate
import time. - Removed duplicate roster load during startup (roster now loads once).
- Cleaned indentation for Python 3.8 compatibility.
Changed
- Reset versioning from legacy
v5.9 betato v1.0.0 (clean semver baseline). - Main script normalized as
series3_emitter.py(later renamed toseries3_agent.pyin v1.2.1).