4 Commits

Author SHA1 Message Date
f790b21808 Merge pull request 'merge v1.4.3' (#5) from dev into main
Reviewed-on: #5
2026-03-17 21:11:41 -04:00
0bea6ca4ea Merge pull request 'v1.4.2' (#3) from dev into main
Reviewed-on: #3
2026-03-17 16:15:22 -04:00
2456fd0ee8 Merge pull request 'Merge v1.4.1 from dev' (#2) from dev into main
## [1.4.1] - 2026-03-17

### Fixed
- `config.ini` now saves to `AppData\Local\Series3Watcher\` instead of `Program Files` — fixes permission denied error on first-run wizard save.
- Config path resolution in both `series3_tray.py` and `series3_watcher.py` updated to use `sys.frozen` + `LOCALAPPDATA` when 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. `.ico` file is still used for the `.exe` file icon.

### Changed
- Terra-View URL field in settings wizard now accepts base URL only (e.g. `http://192.168.x.x:8000`) — `/api/series3/heartbeat` endpoint appended automatically.
- Test Connection button now hits `/health` endpoint 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.md` added — step-by-step guide for building, releasing, and updating.

## [1.4.0] - 2026-03-12

### Added
- `series3_tray.py` — system tray launcher using `pystray` + `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)` in `series3_watcher.py` for background thread use by the tray. Shared `state` dict 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 calls `run_watcher()` — standalone behavior unchanged.
- `requirements.txt` updated to document tray dependencies (`pystray`, `Pillow`); watcher itself remains stdlib-only.
2026-03-17 14:31:50 -04:00
c133932b29 Merge pull request 'Merge: dev to main, refactor rename' (#1) from dev into main
Reviewed-on: serversdown/series3-agent#1
2026-03-03 17:12:58 -05:00
8 changed files with 24 additions and 18 deletions

View File

@@ -6,11 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
--- ---
## [1.4.4] - 2026-03-17
### Removed
- `OK_HOURS` and `MISSING_HOURS` config keys and Settings dialog fields removed — unit status thresholds are calculated by terra-view from raw `age_minutes`, not by the watcher. These fields had no effect since v1.4.2.
## [1.4.3] - 2026-03-17 ## [1.4.3] - 2026-03-17
### Added ### Added

View File

@@ -1,4 +1,4 @@
# Series 3 Watcher v1.4.4 # Series 3 Watcher v1.4.3
Monitors Instantel **Series 3 (Minimate)** call-in activity on a Blastware server. Runs as a **system tray app** that starts automatically on login, reports heartbeats to terra-view, and self-updates from Gitea. Monitors Instantel **Series 3 (Minimate)** call-in activity on a Blastware server. Runs as a **system tray app** that starts automatically on login, reports heartbeats to terra-view, and self-updates from Gitea.
@@ -71,6 +71,8 @@ All settings live in `config.ini`. The Setup Wizard covers every field, but here
| Key | Description | | Key | Description |
|-----|-------------| |-----|-------------|
| `SCAN_INTERVAL_SECONDS` | How often to scan the folder (default `300`) | | `SCAN_INTERVAL_SECONDS` | How often to scan the folder (default `300`) |
| `OK_HOURS` | Age threshold for OK status (default `12`) |
| `MISSING_HOURS` | Age threshold for Missing status (default `24`) |
| `MLG_HEADER_BYTES` | Bytes to read from each `.MLG` header for unit ID (default `2048`) | | `MLG_HEADER_BYTES` | Bytes to read from each `.MLG` header for unit ID (default `2048`) |
| `RECENT_WARN_DAYS` | Log unsniffable files newer than this window | | `RECENT_WARN_DAYS` | Log unsniffable files newer than this window |
@@ -121,7 +123,7 @@ To view connected watchers: **Settings → Developer → Watcher Manager**.
## Versioning ## Versioning
Follows **Semantic Versioning**. Current release: **v1.4.4**. Follows **Semantic Versioning**. Current release: **v1.4.3**.
See `CHANGELOG.md` for full history. See `CHANGELOG.md` for full history.
--- ---

View File

@@ -23,11 +23,6 @@ if exist "%~dp0icon.ico" (
pyinstaller --onefile --windowed --name "%EXE_NAME%" series3_tray.py pyinstaller --onefile --windowed --name "%EXE_NAME%" series3_tray.py
) )
REM Copy versioned exe to plain name for Inno Setup
copy /Y "dist\%EXE_NAME%.exe" "dist\series3-watcher.exe"
echo. echo.
echo Done. echo Done. Check dist\%EXE_NAME%.exe
echo Gitea upload: dist\%EXE_NAME%.exe
echo Inno Setup: dist\series3-watcher.exe (copy of above)
pause pause

View File

@@ -14,6 +14,8 @@ MAX_EVENT_AGE_DAYS = 365
# Scanning # Scanning
SCAN_INTERVAL_SECONDS = 30 SCAN_INTERVAL_SECONDS = 30
OK_HOURS = 12
MISSING_HOURS = 24
# Logging # Logging
ENABLE_LOGGING = True ENABLE_LOGGING = True

View File

@@ -3,7 +3,7 @@
[Setup] [Setup]
AppName=Series 3 Watcher AppName=Series 3 Watcher
AppVersion=1.4.4 AppVersion=1.4.2
AppPublisher=Terra-Mechanics Inc. AppPublisher=Terra-Mechanics Inc.
DefaultDirName={pf}\Series3Watcher DefaultDirName={pf}\Series3Watcher
DefaultGroupName=Series 3 Watcher DefaultGroupName=Series 3 Watcher

View File

@@ -1,5 +1,5 @@
""" """
Series 3 Watcher — System Tray Launcher v1.4.4 Series 3 Watcher — System Tray Launcher v1.4.3
Requires: pystray, Pillow, tkinter (stdlib) Requires: pystray, Pillow, tkinter (stdlib)
Run with: pythonw series3_tray.py (no console window) Run with: pythonw series3_tray.py (no console window)

View File

@@ -60,6 +60,8 @@ def load_config(path: str) -> Dict[str, Any]:
return { return {
"WATCH_PATH": get_str("SERIES3_PATH", r"C:\Blastware 10\Event\autocall home"), "WATCH_PATH": get_str("SERIES3_PATH", r"C:\Blastware 10\Event\autocall home"),
"SCAN_INTERVAL": get_int("SCAN_INTERVAL_SECONDS", 300), "SCAN_INTERVAL": get_int("SCAN_INTERVAL_SECONDS", 300),
"OK_HOURS": float(get_int("OK_HOURS", 12)),
"MISSING_HOURS": float(get_int("MISSING_HOURS", 24)),
"ENABLE_LOGGING": get_bool("ENABLE_LOGGING", True), "ENABLE_LOGGING": get_bool("ENABLE_LOGGING", True),
"LOG_FILE": get_str("LOG_FILE", os.path.join( "LOG_FILE": get_str("LOG_FILE", os.path.join(
os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or "C:\\", os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or "C:\\",
@@ -217,7 +219,7 @@ def scan_latest(
# --- API heartbeat / SFM telemetry helpers --- # --- API heartbeat / SFM telemetry helpers ---
VERSION = "1.4.4" VERSION = "1.4.3"
def _read_log_tail(log_file: str, n: int = 25) -> Optional[list]: def _read_log_tail(log_file: str, n: int = 25) -> Optional[list]:

View File

@@ -1,5 +1,5 @@
""" """
Series 3 Watcher — Settings Dialog v1.4.4 Series 3 Watcher — Settings Dialog v1.4.3
Provides a Tkinter settings dialog that doubles as a first-run wizard. Provides a Tkinter settings dialog that doubles as a first-run wizard.
@@ -30,6 +30,8 @@ DEFAULTS = {
"SERIES3_PATH": r"C:\Blastware 10\Event\autocall home", "SERIES3_PATH": r"C:\Blastware 10\Event\autocall home",
"MAX_EVENT_AGE_DAYS": "365", "MAX_EVENT_AGE_DAYS": "365",
"SCAN_INTERVAL_SECONDS":"300", "SCAN_INTERVAL_SECONDS":"300",
"OK_HOURS": "12",
"MISSING_HOURS": "24",
"MLG_HEADER_BYTES": "2048", "MLG_HEADER_BYTES": "2048",
"ENABLE_LOGGING": "true", "ENABLE_LOGGING": "true",
"LOG_FILE": os.path.join( "LOG_FILE": os.path.join(
@@ -227,6 +229,8 @@ class SettingsDialog:
# Scanning # Scanning
self.var_scan_interval = tk.StringVar(value=v["SCAN_INTERVAL_SECONDS"]) self.var_scan_interval = tk.StringVar(value=v["SCAN_INTERVAL_SECONDS"])
self.var_ok_hours = tk.StringVar(value=v["OK_HOURS"])
self.var_missing_hours = tk.StringVar(value=v["MISSING_HOURS"])
self.var_mlg_header_bytes = tk.StringVar(value=v["MLG_HEADER_BYTES"]) self.var_mlg_header_bytes = tk.StringVar(value=v["MLG_HEADER_BYTES"])
# Logging # Logging
@@ -394,7 +398,9 @@ class SettingsDialog:
def _build_tab_scanning(self, nb): def _build_tab_scanning(self, nb):
f = self._tab_frame(nb, "Scanning") f = self._tab_frame(nb, "Scanning")
_add_label_spinbox(f, 0, "Scan Interval (sec)", self.var_scan_interval, 10, 3600) _add_label_spinbox(f, 0, "Scan Interval (sec)", self.var_scan_interval, 10, 3600)
_add_label_spinbox(f, 1, "MLG Header Bytes", self.var_mlg_header_bytes, 256, 65536) _add_label_spinbox(f, 1, "OK Hours", self.var_ok_hours, 1, 168)
_add_label_spinbox(f, 2, "Missing Hours", self.var_missing_hours, 1, 168)
_add_label_spinbox(f, 3, "MLG Header Bytes", self.var_mlg_header_bytes, 256, 65536)
def _build_tab_logging(self, nb): def _build_tab_logging(self, nb):
f = self._tab_frame(nb, "Logging") f = self._tab_frame(nb, "Logging")
@@ -492,6 +498,8 @@ class SettingsDialog:
(self.var_api_interval, "API Interval", 30, 3600, 300), (self.var_api_interval, "API Interval", 30, 3600, 300),
(self.var_max_event_age_days, "Max Event Age Days", 1, 3650, 365), (self.var_max_event_age_days, "Max Event Age Days", 1, 3650, 365),
(self.var_scan_interval, "Scan Interval", 10, 3600, 300), (self.var_scan_interval, "Scan Interval", 10, 3600, 300),
(self.var_ok_hours, "OK Hours", 1, 168, 12),
(self.var_missing_hours, "Missing Hours", 1, 168, 24),
(self.var_mlg_header_bytes, "MLG Header Bytes", 256, 65536, 2048), (self.var_mlg_header_bytes, "MLG Header Bytes", 256, 65536, 2048),
(self.var_log_retention_days, "Log Retention Days", 1, 365, 30), (self.var_log_retention_days, "Log Retention Days", 1, 365, 30),
] ]
@@ -524,6 +532,8 @@ class SettingsDialog:
"SERIES3_PATH": self.var_series3_path.get().strip(), "SERIES3_PATH": self.var_series3_path.get().strip(),
"MAX_EVENT_AGE_DAYS": str(int_values["Max Event Age Days"]), "MAX_EVENT_AGE_DAYS": str(int_values["Max Event Age Days"]),
"SCAN_INTERVAL_SECONDS":str(int_values["Scan Interval"]), "SCAN_INTERVAL_SECONDS":str(int_values["Scan Interval"]),
"OK_HOURS": str(int_values["OK Hours"]),
"MISSING_HOURS": str(int_values["Missing Hours"]),
"MLG_HEADER_BYTES": str(int_values["MLG Header Bytes"]), "MLG_HEADER_BYTES": str(int_values["MLG Header Bytes"]),
"ENABLE_LOGGING": "true" if self.var_enable_logging.get() else "false", "ENABLE_LOGGING": "true" if self.var_enable_logging.get() else "false",
"LOG_FILE": self.var_log_file.get().strip(), "LOG_FILE": self.var_log_file.get().strip(),