Background worker: install/start/stop the kiosk to match config. Handles both modes (autostart .desktop entry on Pi OS Desktop, systemd unit on Pi OS Lite) — the installer picks the right one.
(prev_enabled: bool, new_enabled: bool, settings_changed: bool)
| 2784 | _watchtower_check_once() |
| 2785 | except Exception as exc: |
| 2786 | logger.debug(f"[watchtower] monitor cycle failed: {exc}") |
| 2787 | try: |
| 2788 | interval = max(5, int(shared_data.config.get('watchtower_interval_s', 30))) |
| 2789 | except (TypeError, ValueError): |
| 2790 | interval = 30 |
| 2791 | for _ in range(interval // 5): |
| 2792 | if getattr(shared_data, 'webapp_should_exit', False): |
| 2793 | return |
| 2794 | if not shared_data.config.get('watchtower_enabled', False): |
| 2795 | break |
| 2796 | _time.sleep(5) |
| 2797 | |
| 2798 | |
| 2799 | @app.route('/api/net/watchtower', methods=['GET']) |
| 2800 | def watchtower_status(): |
| 2801 | """Unified alert feed from the standalone watchers, for the Watchtower pane. |
| 2802 | Reflects config even before the monitor has run. `?limit=` and |
| 2803 | `?min_severity=` narrow the returned alert list.""" |
| 2804 | cfg = shared_data.config |
| 2805 | try: |
| 2806 | limit = max(1, min(500, int(request.args.get('limit', 100)))) |
| 2807 | except (TypeError, ValueError): |
| 2808 | limit = 100 |
| 2809 | min_sev = request.args.get('min_severity') or None |
| 2810 | alerts = [] |
| 2811 | try: |
| 2812 | with _watchtower_lock: |
| 2813 | wt = _wt_get() |
| 2814 | alerts = [dict(a, raw=None) for a in wt.recent(limit=limit, |
| 2815 | min_severity=min_sev)] |
| 2816 | summary = dict(_wt_summary) |
| 2817 | if 'sources' not in summary: |
| 2818 | summary = wt.summary() |
| 2819 | except Exception as exc: |
| 2820 | logger.debug(f"[watchtower] status failed: {exc}") |
| 2821 | summary = {} |
| 2822 | return jsonify({ |
| 2823 | 'success': True, |
| 2824 | 'enabled': bool(cfg.get('watchtower_enabled', False)), |
| 2825 | 'interval_s': cfg.get('watchtower_interval_s', 30), |
| 2826 | 'notify_enabled': bool(cfg.get('watchtower_notify_enabled', True)), |
| 2827 | 'notify_min_severity': cfg.get('watchtower_notify_min_severity', 'high'), |
| 2828 | 'summary': summary, |
| 2829 | 'alerts': alerts, |
| 2830 | }) |
| 2831 | |
| 2832 | |
| 2833 | @app.route('/api/net/watchtower/clear', methods=['POST']) |
| 2834 | def watchtower_clear(): |
| 2835 | """Empty the Watchtower display ring and persist the cleared state. The |
| 2836 | tailer keeps its file offsets, so cleared alerts don't re-appear; only |
| 2837 | genuinely new watcher lines show up afterwards. The Pushover dedup memory |
| 2838 | is left intact so clearing the pane never re-pages old findings.""" |
| 2839 | try: |
| 2840 | summary = _watchtower_clear_core() |
| 2841 | except Exception as exc: |
| 2842 | logger.debug(f"[watchtower] clear failed: {exc}") |
| 2843 | return jsonify({'success': False, 'error': str(exc)}), 500 |
nothing calls this directly
no test coverage detected