diff --git a/src/display_controller.py b/src/display_controller.py
index ace55b58..60ab71b8 100644
--- a/src/display_controller.py
+++ b/src/display_controller.py
@@ -1133,6 +1133,29 @@ def _get_on_demand_remaining(self) -> Optional[float]:
remaining = self.on_demand_expires_at - time.time()
return max(0.0, remaining)
+ def _publish_current_mode_state(self) -> None:
+ """Publish the currently active display mode/plugin to cache for the web UI."""
+ try:
+ state = {
+ 'mode': self.current_display_mode,
+ 'plugin_id': self.mode_to_plugin_id.get(self.current_display_mode),
+ 'mode_index': self.current_mode_index,
+ 'total_modes': len(self.available_modes),
+ 'on_demand_active': self.on_demand_active,
+ 'is_display_active': self.is_display_active,
+ 'last_updated': time.time(),
+ }
+ self.cache_manager.set('display_current_state', state)
+ self._last_published_mode = self.current_display_mode
+ except (OSError, RuntimeError, ValueError, TypeError) as err:
+ logger.error("Failed to publish current display state: %s", err, exc_info=True)
+
+ def _publish_current_mode_state_if_changed(self) -> None:
+ """Publish current mode state only when it actually changed, to avoid
+ writing to the shared cache on every render tick."""
+ if self.current_display_mode != getattr(self, '_last_published_mode', None):
+ self._publish_current_mode_state()
+
def _publish_on_demand_state(self) -> None:
"""Publish current on-demand state to cache for external consumers."""
try:
@@ -1652,6 +1675,7 @@ def run(self):
logger.info("Starting display with cached data (fast startup mode)")
self.current_display_mode = self.available_modes[self.current_mode_index] if self.available_modes else 'none'
logger.info(f"Initial mode set to: {self.current_display_mode} (index: {self.current_mode_index}, total modes: {len(self.available_modes)})")
+ self._publish_current_mode_state()
while True:
# Apply plugin enable/disable edits saved via the web UI. The
@@ -1712,9 +1736,11 @@ def run(self):
logger.debug(f"Error clearing display when inactive: {e}")
logger.info(f"Display not active (is_display_active={self.is_display_active}), sleeping...")
+ self._publish_current_mode_state()
self._sleep_with_plugin_updates(60)
continue
+ self._publish_current_mode_state_if_changed()
logger.debug("Display active, processing mode: %s", self.current_display_mode)
# Plugins update on their own schedules - no forced sync updates needed
diff --git a/src/logging_config.py b/src/logging_config.py
index 4211e861..48eba67c 100644
--- a/src/logging_config.py
+++ b/src/logging_config.py
@@ -139,23 +139,41 @@ def setup_logging(
sys.stderr.write(f"Warning: Could not set up file logging to {log_file}: {e}\n")
-def get_logger(name: str, plugin_id: Optional[str] = None) -> logging.Logger:
+class PluginLoggerAdapter(logging.LoggerAdapter):
+ """LoggerAdapter that stamps every record with its plugin_id.
+
+ A plain `logging.Logger` attribute (the old approach) is never copied
+ onto individual `LogRecord`s, so `ContextualFormatter`/`StructuredFormatter`
+ only ever saw `plugin_id` on calls that explicitly passed
+ `extra={'plugin_id': ...}` (i.e. `log_with_context`). This adapter injects
+ it into `extra` on every call, so `self.logger.info(...)` in plugin code
+ is tagged automatically.
+ """
+
+ def process(self, msg, kwargs):
+ extra = dict(kwargs.get('extra') or {})
+ extra.setdefault('plugin_id', self.extra.get('plugin_id'))
+ kwargs['extra'] = extra
+ return msg, kwargs
+
+
+def get_logger(name: str, plugin_id: Optional[str] = None):
"""
Get a logger with consistent configuration.
-
+
Args:
name: Logger name (typically __name__)
plugin_id: Optional plugin ID for automatic context
-
+
Returns:
- Configured logger instance
+ Configured logger instance (or a PluginLoggerAdapter when plugin_id
+ is given, which supports the same .debug/.info/.warning/.error API)
"""
logger = logging.getLogger(name)
-
- # Add plugin_id as attribute for formatters
+
if plugin_id:
- logger.plugin_id = plugin_id
-
+ return PluginLoggerAdapter(logger, {'plugin_id': plugin_id})
+
return logger
diff --git a/src/plugin_system/base_plugin.py b/src/plugin_system/base_plugin.py
index a990ecd9..54759855 100644
--- a/src/plugin_system/base_plugin.py
+++ b/src/plugin_system/base_plugin.py
@@ -86,7 +86,9 @@ def __init__(
self.display_manager: Any = display_manager
self.cache_manager: Any = cache_manager
self.plugin_manager: Any = plugin_manager
- self.logger: logging.Logger = get_logger(f"plugin.{plugin_id}", plugin_id=plugin_id)
+ # get_logger returns a PluginLoggerAdapter here (plugin_id given), which
+ # stamps every record with plugin_id so it survives into formatted output.
+ self.logger = get_logger(f"plugin.{plugin_id}", plugin_id=plugin_id)
self.enabled: bool = config.get("enabled", True)
self.logger.info("Initialized plugin: %s", plugin_id)
diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py
index 60025c2b..67d9325f 100644
--- a/web_interface/blueprints/api_v3.py
+++ b/web_interface/blueprints/api_v3.py
@@ -6891,6 +6891,29 @@ def list_plugin_assets():
logger.error('Unhandled exception', exc_info=True)
return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
+@api_v3.route('/display/current-status', methods=['GET'])
+def get_current_display_status():
+ """Return the display mode/plugin currently intended to be shown.
+
+ Published by the display process (display_controller._publish_current_mode_state)
+ to the shared cache whenever the active mode changes, so the web UI (e.g. the
+ System Logs page) can show what's on screen without querying the display
+ process directly.
+ """
+ try:
+ cache = _ensure_cache_manager()
+ state = cache.get('display_current_state', max_age=120)
+ if state is None:
+ state = {
+ 'mode': None,
+ 'plugin_id': None,
+ 'last_updated': None,
+ }
+ return jsonify({'status': 'success', 'data': state})
+ except Exception:
+ logger.error('Error in get_current_display_status', exc_info=True)
+ return jsonify({'status': 'error', 'message': 'An error occurred; see logs for details'}), 500
+
@api_v3.route('/logs', methods=['GET'])
def get_logs():
"""Get system logs from journalctl"""
diff --git a/web_interface/templates/v3/partials/logs.html b/web_interface/templates/v3/partials/logs.html
index cb2cff73..9e7794de 100644
--- a/web_interface/templates/v3/partials/logs.html
+++ b/web_interface/templates/v3/partials/logs.html
@@ -4,6 +4,17 @@
System Logs
View real-time logs from the LED matrix service for troubleshooting.