diff --git a/series3_tray.py b/series3_tray.py index 748125f..9f49daf 100644 --- a/series3_tray.py +++ b/series3_tray.py @@ -1,5 +1,5 @@ """ -Series 3 Watcher — System Tray Launcher v1.4.1 +Series 3 Watcher — System Tray Launcher v1.4.2 Requires: pystray, Pillow, tkinter (stdlib) Run with: pythonw series3_tray.py (no console window) @@ -277,46 +277,49 @@ class WatcherTray: status = self.state.get("status", "starting") last_err = self.state.get("last_error") last_scan = self.state.get("last_scan") + api_status = self.state.get("api_status", "disabled") + unit_count = len(self.state.get("units", [])) if status == "error": - return "Status: Error — {}".format(last_err or "unknown") + return "Error — {}".format(last_err or "unknown") if status == "starting": - return "Status: Starting..." + return "Starting..." + + # Scan age if last_scan is not None: age_secs = int((datetime.now() - last_scan).total_seconds()) - if age_secs < 60: - age_str = "{}s ago".format(age_secs) - else: - age_str = "{}m ago".format(age_secs // 60) - unit_count = len(self.state.get("units", [])) - return "Status: {} | {} unit(s) | scan {}".format( - status.upper(), unit_count, age_str - ) - return "Status: {}".format(status.upper()) - - def _build_units_submenu(self): - units = self.state.get("units", []) - if not units: - items = [pystray.MenuItem("No units detected", None, enabled=False)] + age_str = "{}s ago".format(age_secs) if age_secs < 60 else "{}m ago".format(age_secs // 60) else: - items = [] - for u in units: - label = "{uid} — {status} ({age:.1f}h ago)".format( - uid=u["uid"], - status=u["status"], - age=u["age_hours"], - ) - items.append(pystray.MenuItem(label, None, enabled=False)) - return pystray.Menu(*items) + age_str = "never" + + # API status label + if api_status == "ok": + api_str = "API OK" + elif api_status == "fail": + api_str = "API FAIL" + else: + api_str = "API off" + + return "Running — {} | {} unit(s) | scan {}".format(api_str, unit_count, age_str) + + def _tray_status(self): + """Return the icon status key based on watcher + API health.""" + status = self.state.get("status", "starting") + if status == "error": + return "error" + if status == "starting": + return "starting" + api_status = self.state.get("api_status", "disabled") + if api_status == "fail": + return "missing" # red — API failing + if api_status == "disabled": + return "pending" # amber — running but not reporting + return "ok" # green — running and API good def _build_menu(self): - # Use a callable for the status item so pystray re-evaluates it - # every time the menu is opened — keeps it in sync with the tooltip. return pystray.Menu( pystray.MenuItem(lambda item: self._status_text(), None, enabled=False), pystray.Menu.SEPARATOR, - pystray.MenuItem("Units", lambda item: self._build_units_submenu()), - pystray.Menu.SEPARATOR, pystray.MenuItem("Settings...", self._open_settings), pystray.MenuItem("Open Log Folder", self._open_logs), pystray.Menu.SEPARATOR, @@ -331,17 +334,17 @@ class WatcherTray: update_check_counter = 0 # check for updates every ~5 min (30 * 10s ticks) while not self.stop_event.is_set(): - status = self.state.get("status", "starting") + icon_status = self._tray_status() if self._icon is not None: - # Always rebuild menu every cycle so unit list and scan age stay fresh + # Always rebuild menu every cycle so status text stays fresh with self._menu_lock: self._icon.menu = self._build_menu() - if status != last_status: - self._icon.icon = make_icon(status) - self._icon.title = "Series 3 Watcher — {}".format(status.upper()) - last_status = status + if icon_status != last_status: + self._icon.icon = make_icon(icon_status) + self._icon.title = "Series 3 Watcher — {}".format(self._status_text()) + last_status = icon_status # Check if terra-view signalled an update via heartbeat response if self.state.get("update_available"): diff --git a/series3_watcher.py b/series3_watcher.py index b2f72ca..3358877 100644 --- a/series3_watcher.py +++ b/series3_watcher.py @@ -1,5 +1,5 @@ """ -Series 3 Watcher — v1.4.1 +Series 3 Watcher — v1.4.2 Environment: - Python 3.8 (Windows 7 compatible) @@ -220,7 +220,7 @@ def scan_latest( # --- API heartbeat / SFM telemetry helpers --- -VERSION = "1.4.1" +VERSION = "1.4.2" def _read_log_tail(log_file: str, n: int = 25) -> Optional[list]: @@ -398,45 +398,25 @@ def run_watcher(state: Dict[str, Any], stop_event: threading.Event) -> None: ) now_epoch = time.time() - # Build per-unit status list for the tray + # Log detected units to console and log file (info only, no status judgement) unit_list = [] - worst = "ok" # tracks worst status across all units - if latest: print("\nDetected Units (within last {} days):".format(MAX_EVENT_AGE_DAYS)) for uid in sorted(latest.keys()): info = latest[uid] age_hours = (now_epoch - info["mtime"]) / 3600.0 - if age_hours > MISSING_HOURS: - status, col = "missing", C_MIS - elif age_hours > OK_HOURS: - status, col = "pending", C_PEN - else: - status, col = "ok", C_OK - - # escalate worst status - if status == "missing": - worst = "missing" - elif status == "pending" and worst != "missing": - worst = "pending" - unit_list.append({ "uid": uid, - "status": status, "age_hours": age_hours, "last": fmt_last(info["mtime"]), "fname": info["fname"], }) - line = ( - "{col}{uid:<8} {status:<8} Age: {age:<7} Last: {last} (File: {fname}){rst}".format( - col=col, + "{uid:<8} Age: {age:<7} Last: {last} (File: {fname})".format( uid=uid, - status=status.capitalize(), age=fmt_age(now_epoch, info["mtime"]), last=fmt_last(info["mtime"]), fname=info["fname"], - rst=C_RST, ) ) print(line) @@ -448,10 +428,9 @@ def run_watcher(state: Dict[str, Any], stop_event: threading.Event) -> None: ENABLE_LOGGING, "[info] no recent MLG activity within {} days".format(MAX_EVENT_AGE_DAYS), ) - worst = "missing" - # Update shared state for tray - state["status"] = worst + # Update shared state for tray — status reflects watcher health, not unit ages + state["status"] = "running" state["units"] = unit_list state["last_scan"] = datetime.now() state["last_error"] = None @@ -463,12 +442,19 @@ def run_watcher(state: Dict[str, Any], stop_event: threading.Event) -> None: if now_ts - last_api_ts >= interval: hb_payload = build_sfm_payload(latest, cfg) hb_payload["version"] = VERSION + hb_payload["watcher_status"] = state.get("status", "unknown") hb_payload["log_tail"] = _read_log_tail(cfg.get("LOG_FILE", ""), 25) response = send_api_payload(hb_payload, cfg.get("API_URL", "")) last_api_ts = now_ts - # Surface update signal to tray - if response and response.get("update_available"): - state["update_available"] = True + if response is not None: + state["api_status"] = "ok" + state["last_api"] = datetime.now() + if response.get("update_available"): + state["update_available"] = True + else: + state["api_status"] = "fail" + else: + state["api_status"] = "disabled" except Exception as e: err = "[loop-error] {}".format(e)