diff --git a/plans/activity-log/PLAN.md b/plans/activity-log/PLAN.md index 00928c9..841ee87 100644 --- a/plans/activity-log/PLAN.md +++ b/plans/activity-log/PLAN.md @@ -106,6 +106,8 @@ is an on-demand CSV/JSON **export** (no separate backup subsystem). | 6 | clearActivityLog() 401 path unreachable β†’ silent failure | 🟑 Warning | resolved β€” `handle401:false` surfaces auth-required toast | | 6 | Recent Activity widget dropped first live event when empty | πŸ”΅ Note | resolved β€” emptyβ†’list transition on first live event | | 6 | Widget outside dashboard layout-toggle/ordering system | πŸ”΅ Note | accepted β€” deliberate (always-visible), collapse still works | +| Final | Entity-crosslink map keys mismatched backend entity_type (device/color_strip_source/audio_source) | 🟑 Warning | resolved β€” `_ENTITY_NAV` keys corrected + scene_playlist added | +| Final | Owner-authored names interpolated raw at some record sites | πŸ”΅ Note (defense-in-depth) | resolved β€” `sanitize_display` applied uniformly | ## Final Review diff --git a/server/src/ledgrab/api/dependencies.py b/server/src/ledgrab/api/dependencies.py index 189eba0..470b152 100644 --- a/server/src/ledgrab/api/dependencies.py +++ b/server/src/ledgrab/api/dependencies.py @@ -318,7 +318,7 @@ def fire_entity_event( severity=ActivitySeverity.INFO, entity_type=entity_type, entity_id=entity_id, - entity_name=resolved_name, + entity_name=sanitize_display(resolved_name) if resolved_name else None, message=message, ) diff --git a/server/src/ledgrab/api/routes/output_targets_control.py b/server/src/ledgrab/api/routes/output_targets_control.py index 2e4bfca..5607a03 100644 --- a/server/src/ledgrab/api/routes/output_targets_control.py +++ b/server/src/ledgrab/api/routes/output_targets_control.py @@ -29,6 +29,7 @@ from ledgrab.storage.color_strip_source import ( from ledgrab.storage.picture_source_store import PictureSourceStore from ledgrab.storage.wled_output_target import WledOutputTarget from ledgrab.storage.output_target_store import OutputTargetStore +from ledgrab.core.activity_log.sanitize import sanitize_display from ledgrab.utils import get_logger logger = get_logger(__name__) @@ -48,7 +49,7 @@ def _record_capture(action: str, target_id: str, target_name: str | None, messag severity=ActivitySeverity.INFO, entity_type="output_target", entity_id=target_id, - entity_name=target_name, + entity_name=sanitize_display(target_name) if target_name else None, message=message, ) @@ -75,11 +76,13 @@ async def bulk_start_processing( await manager.start_processing(target_id) started.append(target_id) logger.info(f"Bulk start: started processing for target {target_id}") + _tgt_name_raw = getattr(_tgt, "name", None) + _tgt_safe = sanitize_display(_tgt_name_raw) if _tgt_name_raw else None _record_capture( "capture.started", target_id, - getattr(_tgt, "name", None), - f"Capture started for target '{getattr(_tgt, 'name', target_id)}' (bulk)", + _tgt_safe, + f"Capture started for target '{_tgt_safe or target_id}' (bulk)", ) except ValueError as e: errors[target_id] = str(e) @@ -119,11 +122,12 @@ async def bulk_stop_processing( _tgt_name = target_store.get_target(target_id).name except Exception: pass + _tgt_name_safe = sanitize_display(_tgt_name) if _tgt_name else None _record_capture( "capture.stopped", target_id, - _tgt_name, - f"Capture stopped for target '{_tgt_name or target_id}' (bulk)", + _tgt_name_safe, + f"Capture stopped for target '{_tgt_name_safe or target_id}' (bulk)", ) except ValueError as e: errors[target_id] = str(e) @@ -153,11 +157,13 @@ async def start_processing( await manager.start_processing(target_id) logger.info(f"Started processing for target {target_id}") + _tgt_name_raw2 = getattr(target, "name", None) + _tgt_safe2 = sanitize_display(_tgt_name_raw2) if _tgt_name_raw2 else None _record_capture( "capture.started", target_id, - getattr(target, "name", None), - f"Capture started for target '{getattr(target, 'name', target_id)}'", + _tgt_safe2, + f"Capture started for target '{_tgt_safe2 or target_id}'", ) return {"status": "started", "target_id": target_id} @@ -192,11 +198,12 @@ async def stop_processing( _target_name = target_store.get_target(target_id).name except Exception: pass + _target_name_safe = sanitize_display(_target_name) if _target_name else None _record_capture( "capture.stopped", target_id, - _target_name, - f"Capture stopped for target '{_target_name or target_id}'", + _target_name_safe, + f"Capture stopped for target '{_target_name_safe or target_id}'", ) return {"status": "stopped", "target_id": target_id} diff --git a/server/src/ledgrab/api/routes/scene_playlists.py b/server/src/ledgrab/api/routes/scene_playlists.py index 03f3db8..bf22eb5 100644 --- a/server/src/ledgrab/api/routes/scene_playlists.py +++ b/server/src/ledgrab/api/routes/scene_playlists.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException from ledgrab.api.auth import AuthRequired +from ledgrab.core.activity_log.sanitize import sanitize_display from ledgrab.api.dependencies import ( fire_entity_event, get_playlist_engine, @@ -272,14 +273,15 @@ async def start_scene_playlist( _pl_name = store.get_playlist(playlist_id).name except Exception: pass + _safe_pl_name = sanitize_display(_pl_name) if _pl_name else None rec.record( category=ActivityCategory.CAPTURE, action="playlist.started", severity=ActivitySeverity.INFO, entity_type="scene_playlist", entity_id=playlist_id, - entity_name=_pl_name, - message=f"Playlist '{_pl_name or playlist_id}' started", + entity_name=_safe_pl_name, + message=f"Playlist '{_safe_pl_name or playlist_id}' started", ) return PlaylistRuntimeStateSchema(**engine.get_state()) @@ -312,14 +314,15 @@ async def stop_scene_playlist( rec = get_module_recorder() if rec is not None: + _safe_stopped_name = sanitize_display(_stopped_name) if _stopped_name else None rec.record( category=ActivityCategory.CAPTURE, action="playlist.stopped", severity=ActivitySeverity.INFO, entity_type="scene_playlist", entity_id=stopped_id, - entity_name=_stopped_name, - message=f"Playlist '{_stopped_name or stopped_id}' stopped", + entity_name=_safe_stopped_name, + message=f"Playlist '{_safe_stopped_name or stopped_id}' stopped", ) return PlaylistRuntimeStateSchema(**engine.get_state()) diff --git a/server/src/ledgrab/api/routes/scene_presets.py b/server/src/ledgrab/api/routes/scene_presets.py index c2e763b..098803d 100644 --- a/server/src/ledgrab/api/routes/scene_presets.py +++ b/server/src/ledgrab/api/routes/scene_presets.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from fastapi import APIRouter, Depends, HTTPException from ledgrab.api.auth import AuthRequired +from ledgrab.core.activity_log.sanitize import sanitize_display from ledgrab.api.dependencies import ( fire_entity_event, get_output_target_store, @@ -294,14 +295,15 @@ async def activate_scene_preset( rec = get_module_recorder() if rec is not None: + _safe_preset_name = sanitize_display(preset.name) if preset.name else None rec.record( category=ActivityCategory.CAPTURE, action="scene.activated", severity=ActivitySeverity.INFO, entity_type="scene_preset", entity_id=preset_id, - entity_name=preset.name, - message=f"Scene preset '{preset.name}' activated", + entity_name=_safe_preset_name, + message=f"Scene preset '{_safe_preset_name or preset_id}' activated", ) return ActivateResponse(status=status, errors=errors) diff --git a/server/src/ledgrab/core/automations/automation_engine.py b/server/src/ledgrab/core/automations/automation_engine.py index 5a0dcd5..d4189b5 100644 --- a/server/src/ledgrab/core/automations/automation_engine.py +++ b/server/src/ledgrab/core/automations/automation_engine.py @@ -729,10 +729,12 @@ class AutomationEngine: # Audit record β€” best-effort. try: from ledgrab.core.activity_log.recorder import get_module_recorder + from ledgrab.core.activity_log.sanitize import sanitize_display from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity rec = get_module_recorder() if rec is not None: + _safe_name = sanitize_display(automation.name) if automation.name else None rec.record( category=ActivityCategory.CAPTURE, action="automation.activated", @@ -740,8 +742,8 @@ class AutomationEngine: actor="system", entity_type="automation", entity_id=automation.id, - entity_name=automation.name, - message=f"Automation '{automation.name}' activated", + entity_name=_safe_name, + message=f"Automation '{_safe_name or automation.id}' activated", ) except Exception: pass @@ -774,6 +776,7 @@ class AutomationEngine: # Audit record β€” best-effort. try: from ledgrab.core.activity_log.recorder import get_module_recorder + from ledgrab.core.activity_log.sanitize import sanitize_display from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity rec = get_module_recorder() @@ -783,6 +786,7 @@ class AutomationEngine: _auto_name = self._store.get_automation(automation_id).name except Exception: pass + _safe_deact_name = sanitize_display(_auto_name) if _auto_name else None rec.record( category=ActivityCategory.CAPTURE, action="automation.deactivated", @@ -790,8 +794,8 @@ class AutomationEngine: actor="system", entity_type="automation", entity_id=automation_id, - entity_name=_auto_name, - message=f"Automation '{_auto_name or automation_id}' deactivated", + entity_name=_safe_deact_name, + message=f"Automation '{_safe_deact_name or automation_id}' deactivated", ) except Exception: pass diff --git a/server/src/ledgrab/core/processing/device_health.py b/server/src/ledgrab/core/processing/device_health.py index bf931d0..2eaef07 100644 --- a/server/src/ledgrab/core/processing/device_health.py +++ b/server/src/ledgrab/core/processing/device_health.py @@ -11,6 +11,7 @@ from ledgrab.core.devices.led_client import ( check_device_health, get_device_capabilities, ) +from ledgrab.core.activity_log.sanitize import sanitize_display from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity from ledgrab.utils import get_logger @@ -142,7 +143,8 @@ class DeviceHealthMixin: device_name = self._device_store.get_device(device_id).name except Exception: pass - display = device_name or device_id + safe_name = sanitize_display(device_name) if device_name else None + display = safe_name or device_id action = "device.online" if is_online else "device.offline" severity = ActivitySeverity.INFO if is_online else ActivitySeverity.WARNING status_word = "came online" if is_online else "went offline" @@ -153,7 +155,7 @@ class DeviceHealthMixin: actor="system", entity_type="device", entity_id=device_id, - entity_name=device_name, + entity_name=safe_name, message=f"Device '{display}' {status_word}", metadata={"latency_ms": state.health.latency_ms}, ) diff --git a/server/src/ledgrab/static/js/features/activity-log.ts b/server/src/ledgrab/static/js/features/activity-log.ts index 0c4f453..6f2bb76 100644 --- a/server/src/ledgrab/static/js/features/activity-log.ts +++ b/server/src/ledgrab/static/js/features/activity-log.ts @@ -88,13 +88,14 @@ const _filters: ActiveFilters = { // ─── Category β†’ navigation target map (entity crosslinks) ── const _ENTITY_NAV: Record = { - output_target: { tab: 'targets', subTab: 'led-targets', attr: 'data-target-id' }, - led_device: { tab: 'targets', subTab: 'led-devices', attr: 'data-device-id' }, - picture_source: { tab: 'streams', subTab: 'raw', attr: 'data-stream-id' }, - color_strip: { tab: 'streams', subTab: 'color_strip', attr: 'data-strip-id' }, - audio_source: { tab: 'streams', subTab: 'audio_capture', attr: 'data-audio-source-id' }, + output_target: { tab: 'targets', subTab: 'led-targets', attr: 'data-target-id' }, + device: { tab: 'targets', subTab: 'led-devices', attr: 'data-device-id' }, + picture_source: { tab: 'streams', subTab: 'raw', attr: 'data-stream-id' }, + color_strip_source: { tab: 'streams', subTab: 'color_strip', attr: 'data-css-id' }, + audio_source: { tab: 'streams', subTab: 'audio_capture', attr: 'data-id' }, automation: { tab: 'automations', subTab: 'automations', attr: 'data-automation-id' }, scene_preset: { tab: 'automations', subTab: 'scenes', attr: 'data-scene-id' }, + scene_playlist: { tab: 'automations', subTab: 'playlists', attr: 'data-playlist-id' }, }; // ─── Severity icon helper ────────────────────────────────────