some fixes

This commit is contained in:
Ayzen
2026-06-05 14:40:10 +03:00
parent 22942d9dc9
commit bbea744459
35 changed files with 1797 additions and 297 deletions
+55
View File
@@ -7,9 +7,12 @@ state shared across them (runtime services, readers, history buffers, timer).
from __future__ import annotations
from collections import deque
from contextlib import suppress
from datetime import datetime
import html
import json
import logging
from logging.handlers import RotatingFileHandler
import os
from pathlib import Path
import sys
@@ -59,6 +62,7 @@ class AppWindow(
super().__init__()
self._init_paths(project_root)
self._init_headless_logger()
self._init_runtime_services()
self._init_config_profile_state()
self._init_reader_handles()
@@ -76,6 +80,42 @@ class AppWindow(
self._root_profile_path = project_root / "run_config.json"
self._active_profile_path = self._root_profile_path
self._pending_startup_log_entries: list[tuple[str, str, str | None]] = []
# Guards closeEvent against re-entrant teardown (e.g. a second signal).
self._closing = False
def _init_headless_logger(self) -> None:
"""Create a Python logger so headless WARN/ERROR reach journald and disk.
In headless mode the in-app log only reaches an offscreen widget, so an
operator (or `journalctl`) would never see failures. We attach a stderr
StreamHandler (captured by journald) plus a small rotating file under
`runtime/logs`; in GUI mode no handler is attached and the logger stays
inert, preserving the visible log widget as the sole sink.
"""
self._headless_logger: logging.Logger | None = None
if not self._is_truthy_env("RADAR_SYSTEM_HEADLESS"):
return
logger = logging.getLogger("radar_system.gui")
logger.setLevel(logging.WARNING)
logger.propagate = False
logger.handlers.clear()
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)-5s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
stream_handler = logging.StreamHandler(stream=sys.stderr)
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)
# A rotating file keeps recent failures around after a journald restart.
with suppress(Exception):
log_dir = self._project_root / "python_app/runtime/logs"
log_dir.mkdir(parents=True, exist_ok=True)
file_handler = RotatingFileHandler(
log_dir / "gui.log", maxBytes=1_000_000, backupCount=3, encoding="utf-8"
)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
self._headless_logger = logger
def _init_runtime_services(self) -> None:
"""Initialize long-lived service objects used by mixins."""
@@ -461,6 +501,15 @@ class AppWindow(
if level_upper == "ERROR" and hasattr(self, "_status_label"):
self._status_label.setText("Status: error")
# In headless mode the offscreen widget above is invisible, so also mirror
# WARN/ERROR to the Python logger (stderr -> journald, plus rotating file)
# where an operator can actually observe failures.
headless_logger = getattr(self, "_headless_logger", None)
if headless_logger is not None and level_upper in {"WARN", "ERROR"}:
log_message = text if not details else f"{text}\n{details}"
log_level = logging.ERROR if level_upper == "ERROR" else logging.WARNING
headless_logger.log(log_level, log_message)
def _log(self, text: str, *, once_key: str | None = None) -> None:
"""Append informational message to runtime log panel."""
self._append_log_entry("INFO", text, once_key=once_key)
@@ -564,6 +613,12 @@ class AppWindow(
def closeEvent(self, event) -> None: # noqa: N802
"""Ensure workers and dialogs are closed before window destruction."""
if self._closing:
# Re-entrant close (second signal, or window.close() after the event loop
# already returned): teardown is in progress or done — do nothing more.
super().closeEvent(event)
return
self._closing = True
try:
# 0) Stop the GPIO button watcher so a late press cannot start work.
self._stop_control_button_watcher()