7 Commits

Author SHA1 Message Date
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
serversdwn
d2fd3b7182 docs: v1.4.1 changelog entry 2026-03-17 14:29:46 -04:00
serversdwn
1d94c5dd04 docs: delete deprecated client specific readme 2026-03-17 14:26:59 -04:00
serversdwn
814b6f915e fix: settings dialog now runs in its own thread, increasing responsiveness. 2026-03-17 13:36:35 -04:00
serversdwn
9cfdebe553 fix: watcher correctly uses AppData directory, not program files. 2026-03-17 03:40:04 -04:00
serversdwn
f773e1dac9 fix: tray icon more legible 2026-03-17 03:27:53 -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
4 changed files with 33 additions and 85 deletions

View File

@@ -6,6 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
--- ---
## [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 ## [1.4.0] - 2026-03-12
### Added ### Added

View File

@@ -1,26 +0,0 @@
# Series 3 Watcher — v1_0(py38-safe) for DL2
**Target**: Windows 7 + Python 3.8.10
**Baseline**: v5_4 (no logic changes)
## Files
- series3_agent_v1_0_py38.py — main script (py38-safe)
- config.ini — your config (already included)
- series3_roster.csv — your roster (already included, this auto updates from a URL to a dropbox file)
- requirements.txt — none beyond stdlib
## Install
1) Create `C:\SeismoEmitter\` on DL2
2) Extract this ZIP into that folder
3) Open CMD:
```cmd
cd C:\SeismoEmitter
python series3_agent_v1_0_py38.py
```
(If the console shows escape codes on Win7, set `COLORIZE = False` in `config.ini`.)
## Quick validation
- Heartbeat prints Local/UTC timestamps
- One line per active roster unit with OK/Pending/Missing, Age, Last, File
- Unexpected units block shows .MLG not in roster
- agent.log rotates per LOG_RETENTION_DAYS

View File

@@ -154,50 +154,11 @@ COLORS = {
} }
ICON_SIZE = 64 ICON_SIZE = 64
ICON_FILE = os.path.join(HERE, "icon.ico")
# Load base icon once at startup; fall back to None if missing.
# When frozen, bundled data files live in sys._MEIPASS.
def _load_base_icon():
candidates = [ICON_FILE]
if getattr(sys, "frozen", False):
candidates.insert(0, os.path.join(sys._MEIPASS, "icon.ico"))
for path in candidates:
try:
img = Image.open(path)
img = img.convert("RGBA").resize((ICON_SIZE, ICON_SIZE), Image.LANCZOS)
return img
except Exception:
continue
return None
_BASE_ICON = _load_base_icon()
def make_icon(status): def make_icon(status):
""" """Draw a plain colored circle for the system tray — clean and readable at 16px."""
Use icon.ico as the base and overlay a small status dot in the
bottom-right corner. Falls back to a plain colored circle if the
ico file is not available.
"""
color = COLORS.get(status, COLORS["starting"]) color = COLORS.get(status, COLORS["starting"])
if _BASE_ICON is not None:
img = _BASE_ICON.copy()
draw = ImageDraw.Draw(img)
# Dot size and position — bottom-right corner
dot_r = ICON_SIZE // 5 # radius ~12px on 64px icon
margin = 2
x0 = ICON_SIZE - dot_r * 2 - margin
y0 = ICON_SIZE - dot_r * 2 - margin
x1 = ICON_SIZE - margin
y1 = ICON_SIZE - margin
# White outline for contrast on dark/light backgrounds
draw.ellipse([x0 - 2, y0 - 2, x1 + 2, y1 + 2], fill=(255, 255, 255, 220))
draw.ellipse([x0, y0, x1, y1], fill=color + (255,))
return img
# Fallback: plain colored circle
img = Image.new("RGBA", (ICON_SIZE, ICON_SIZE), (0, 0, 0, 0)) img = Image.new("RGBA", (ICON_SIZE, ICON_SIZE), (0, 0, 0, 0))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
margin = 6 margin = 6
@@ -285,15 +246,13 @@ class WatcherTray:
# --- Menu item callbacks --- # --- Menu item callbacks ---
def _open_settings(self, icon, item): def _open_settings(self, icon, item):
"""Open the settings dialog. On save, restart watcher thread.""" """Open the settings dialog in its own thread so the tray stays responsive."""
def _run():
from settings_dialog import show_dialog from settings_dialog import show_dialog
saved = show_dialog(CONFIG_PATH, wizard=False) saved = show_dialog(CONFIG_PATH, wizard=False)
if saved: if saved:
self._restart_watcher() self._restart_watcher()
# Rebuild menu so status label refreshes threading.Thread(target=_run, daemon=True, name="settings-dialog").start()
if self._icon is not None:
with self._menu_lock:
self._icon.menu = self._build_menu()
def _open_logs(self, icon, item): def _open_logs(self, icon, item):
log_dir = self.state.get("log_dir") log_dir = self.state.get("log_dir")
@@ -351,17 +310,12 @@ class WatcherTray:
return pystray.Menu(*items) return pystray.Menu(*items)
def _build_menu(self): def _build_menu(self):
# Capture current text/submenu at build time; pystray will call # Use a callable for the status item so pystray re-evaluates it
# callables each render, but static strings are fine for infrequent # every time the menu is opened — keeps it in sync with the tooltip.
# menu rebuilds. We use callables for the dynamic items so that the
# text shown on hover/open is current.
status_text = self._status_text()
units_submenu = self._build_units_submenu()
return pystray.Menu( return pystray.Menu(
pystray.MenuItem(status_text, None, enabled=False), pystray.MenuItem(lambda item: self._status_text(), None, enabled=False),
pystray.Menu.SEPARATOR, pystray.Menu.SEPARATOR,
pystray.MenuItem("Units", units_submenu), pystray.MenuItem("Units", lambda item: self._build_units_submenu()),
pystray.Menu.SEPARATOR, pystray.Menu.SEPARATOR,
pystray.MenuItem("Settings...", self._open_settings), pystray.MenuItem("Settings...", self._open_settings),
pystray.MenuItem("Open Log Folder", self._open_logs), pystray.MenuItem("Open Log Folder", self._open_logs),

View File

@@ -319,9 +319,12 @@ def run_watcher(state: Dict[str, Any], stop_event: threading.Event) -> None:
""" """
if getattr(sys, "frozen", False): if getattr(sys, "frozen", False):
here = os.path.dirname(os.path.abspath(sys.executable)) here = os.path.dirname(os.path.abspath(sys.executable))
_appdata = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or here
config_dir = os.path.join(_appdata, "Series3Watcher")
else: else:
here = os.path.dirname(os.path.abspath(__file__)) or "." here = os.path.dirname(os.path.abspath(__file__)) or "."
config_path = os.path.join(here, "config.ini") config_dir = here
config_path = os.path.join(config_dir, "config.ini")
state["status"] = "starting" state["status"] = "starting"
state["units"] = [] state["units"] = []