fix(activity-log): final-review fixes - crosslink keys + sanitize parity
- _ENTITY_NAV map keys corrected to match backend entity_type (device, color_strip_source, audio_source data-id) + scene_playlist crosslink added - sanitize_display applied uniformly to owner-authored names at remaining record sites (dependencies entity_name, device_health, automation_engine, output_targets_control, scene_presets, scene_playlists)
This commit is contained in:
@@ -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 | 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 | 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 |
|
| 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
|
## Final Review
|
||||||
|
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ def fire_entity_event(
|
|||||||
severity=ActivitySeverity.INFO,
|
severity=ActivitySeverity.INFO,
|
||||||
entity_type=entity_type,
|
entity_type=entity_type,
|
||||||
entity_id=entity_id,
|
entity_id=entity_id,
|
||||||
entity_name=resolved_name,
|
entity_name=sanitize_display(resolved_name) if resolved_name else None,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from ledgrab.storage.color_strip_source import (
|
|||||||
from ledgrab.storage.picture_source_store import PictureSourceStore
|
from ledgrab.storage.picture_source_store import PictureSourceStore
|
||||||
from ledgrab.storage.wled_output_target import WledOutputTarget
|
from ledgrab.storage.wled_output_target import WledOutputTarget
|
||||||
from ledgrab.storage.output_target_store import OutputTargetStore
|
from ledgrab.storage.output_target_store import OutputTargetStore
|
||||||
|
from ledgrab.core.activity_log.sanitize import sanitize_display
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -48,7 +49,7 @@ def _record_capture(action: str, target_id: str, target_name: str | None, messag
|
|||||||
severity=ActivitySeverity.INFO,
|
severity=ActivitySeverity.INFO,
|
||||||
entity_type="output_target",
|
entity_type="output_target",
|
||||||
entity_id=target_id,
|
entity_id=target_id,
|
||||||
entity_name=target_name,
|
entity_name=sanitize_display(target_name) if target_name else None,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -75,11 +76,13 @@ async def bulk_start_processing(
|
|||||||
await manager.start_processing(target_id)
|
await manager.start_processing(target_id)
|
||||||
started.append(target_id)
|
started.append(target_id)
|
||||||
logger.info(f"Bulk start: started processing for target {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(
|
_record_capture(
|
||||||
"capture.started",
|
"capture.started",
|
||||||
target_id,
|
target_id,
|
||||||
getattr(_tgt, "name", None),
|
_tgt_safe,
|
||||||
f"Capture started for target '{getattr(_tgt, 'name', target_id)}' (bulk)",
|
f"Capture started for target '{_tgt_safe or target_id}' (bulk)",
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
errors[target_id] = str(e)
|
errors[target_id] = str(e)
|
||||||
@@ -119,11 +122,12 @@ async def bulk_stop_processing(
|
|||||||
_tgt_name = target_store.get_target(target_id).name
|
_tgt_name = target_store.get_target(target_id).name
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
_tgt_name_safe = sanitize_display(_tgt_name) if _tgt_name else None
|
||||||
_record_capture(
|
_record_capture(
|
||||||
"capture.stopped",
|
"capture.stopped",
|
||||||
target_id,
|
target_id,
|
||||||
_tgt_name,
|
_tgt_name_safe,
|
||||||
f"Capture stopped for target '{_tgt_name or target_id}' (bulk)",
|
f"Capture stopped for target '{_tgt_name_safe or target_id}' (bulk)",
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
errors[target_id] = str(e)
|
errors[target_id] = str(e)
|
||||||
@@ -153,11 +157,13 @@ async def start_processing(
|
|||||||
await manager.start_processing(target_id)
|
await manager.start_processing(target_id)
|
||||||
|
|
||||||
logger.info(f"Started processing for target {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(
|
_record_capture(
|
||||||
"capture.started",
|
"capture.started",
|
||||||
target_id,
|
target_id,
|
||||||
getattr(target, "name", None),
|
_tgt_safe2,
|
||||||
f"Capture started for target '{getattr(target, 'name', target_id)}'",
|
f"Capture started for target '{_tgt_safe2 or target_id}'",
|
||||||
)
|
)
|
||||||
return {"status": "started", "target_id": 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
|
_target_name = target_store.get_target(target_id).name
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
_target_name_safe = sanitize_display(_target_name) if _target_name else None
|
||||||
_record_capture(
|
_record_capture(
|
||||||
"capture.stopped",
|
"capture.stopped",
|
||||||
target_id,
|
target_id,
|
||||||
_target_name,
|
_target_name_safe,
|
||||||
f"Capture stopped for target '{_target_name or target_id}'",
|
f"Capture stopped for target '{_target_name_safe or target_id}'",
|
||||||
)
|
)
|
||||||
return {"status": "stopped", "target_id": target_id}
|
return {"status": "stopped", "target_id": target_id}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from ledgrab.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
|
from ledgrab.core.activity_log.sanitize import sanitize_display
|
||||||
from ledgrab.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_playlist_engine,
|
get_playlist_engine,
|
||||||
@@ -272,14 +273,15 @@ async def start_scene_playlist(
|
|||||||
_pl_name = store.get_playlist(playlist_id).name
|
_pl_name = store.get_playlist(playlist_id).name
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
_safe_pl_name = sanitize_display(_pl_name) if _pl_name else None
|
||||||
rec.record(
|
rec.record(
|
||||||
category=ActivityCategory.CAPTURE,
|
category=ActivityCategory.CAPTURE,
|
||||||
action="playlist.started",
|
action="playlist.started",
|
||||||
severity=ActivitySeverity.INFO,
|
severity=ActivitySeverity.INFO,
|
||||||
entity_type="scene_playlist",
|
entity_type="scene_playlist",
|
||||||
entity_id=playlist_id,
|
entity_id=playlist_id,
|
||||||
entity_name=_pl_name,
|
entity_name=_safe_pl_name,
|
||||||
message=f"Playlist '{_pl_name or playlist_id}' started",
|
message=f"Playlist '{_safe_pl_name or playlist_id}' started",
|
||||||
)
|
)
|
||||||
|
|
||||||
return PlaylistRuntimeStateSchema(**engine.get_state())
|
return PlaylistRuntimeStateSchema(**engine.get_state())
|
||||||
@@ -312,14 +314,15 @@ async def stop_scene_playlist(
|
|||||||
|
|
||||||
rec = get_module_recorder()
|
rec = get_module_recorder()
|
||||||
if rec is not None:
|
if rec is not None:
|
||||||
|
_safe_stopped_name = sanitize_display(_stopped_name) if _stopped_name else None
|
||||||
rec.record(
|
rec.record(
|
||||||
category=ActivityCategory.CAPTURE,
|
category=ActivityCategory.CAPTURE,
|
||||||
action="playlist.stopped",
|
action="playlist.stopped",
|
||||||
severity=ActivitySeverity.INFO,
|
severity=ActivitySeverity.INFO,
|
||||||
entity_type="scene_playlist",
|
entity_type="scene_playlist",
|
||||||
entity_id=stopped_id,
|
entity_id=stopped_id,
|
||||||
entity_name=_stopped_name,
|
entity_name=_safe_stopped_name,
|
||||||
message=f"Playlist '{_stopped_name or stopped_id}' stopped",
|
message=f"Playlist '{_safe_stopped_name or stopped_id}' stopped",
|
||||||
)
|
)
|
||||||
|
|
||||||
return PlaylistRuntimeStateSchema(**engine.get_state())
|
return PlaylistRuntimeStateSchema(**engine.get_state())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from datetime import datetime, timezone
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
from ledgrab.api.auth import AuthRequired
|
from ledgrab.api.auth import AuthRequired
|
||||||
|
from ledgrab.core.activity_log.sanitize import sanitize_display
|
||||||
from ledgrab.api.dependencies import (
|
from ledgrab.api.dependencies import (
|
||||||
fire_entity_event,
|
fire_entity_event,
|
||||||
get_output_target_store,
|
get_output_target_store,
|
||||||
@@ -294,14 +295,15 @@ async def activate_scene_preset(
|
|||||||
|
|
||||||
rec = get_module_recorder()
|
rec = get_module_recorder()
|
||||||
if rec is not None:
|
if rec is not None:
|
||||||
|
_safe_preset_name = sanitize_display(preset.name) if preset.name else None
|
||||||
rec.record(
|
rec.record(
|
||||||
category=ActivityCategory.CAPTURE,
|
category=ActivityCategory.CAPTURE,
|
||||||
action="scene.activated",
|
action="scene.activated",
|
||||||
severity=ActivitySeverity.INFO,
|
severity=ActivitySeverity.INFO,
|
||||||
entity_type="scene_preset",
|
entity_type="scene_preset",
|
||||||
entity_id=preset_id,
|
entity_id=preset_id,
|
||||||
entity_name=preset.name,
|
entity_name=_safe_preset_name,
|
||||||
message=f"Scene preset '{preset.name}' activated",
|
message=f"Scene preset '{_safe_preset_name or preset_id}' activated",
|
||||||
)
|
)
|
||||||
|
|
||||||
return ActivateResponse(status=status, errors=errors)
|
return ActivateResponse(status=status, errors=errors)
|
||||||
|
|||||||
@@ -729,10 +729,12 @@ class AutomationEngine:
|
|||||||
# Audit record — best-effort.
|
# Audit record — best-effort.
|
||||||
try:
|
try:
|
||||||
from ledgrab.core.activity_log.recorder import get_module_recorder
|
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
|
from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity
|
||||||
|
|
||||||
rec = get_module_recorder()
|
rec = get_module_recorder()
|
||||||
if rec is not None:
|
if rec is not None:
|
||||||
|
_safe_name = sanitize_display(automation.name) if automation.name else None
|
||||||
rec.record(
|
rec.record(
|
||||||
category=ActivityCategory.CAPTURE,
|
category=ActivityCategory.CAPTURE,
|
||||||
action="automation.activated",
|
action="automation.activated",
|
||||||
@@ -740,8 +742,8 @@ class AutomationEngine:
|
|||||||
actor="system",
|
actor="system",
|
||||||
entity_type="automation",
|
entity_type="automation",
|
||||||
entity_id=automation.id,
|
entity_id=automation.id,
|
||||||
entity_name=automation.name,
|
entity_name=_safe_name,
|
||||||
message=f"Automation '{automation.name}' activated",
|
message=f"Automation '{_safe_name or automation.id}' activated",
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -774,6 +776,7 @@ class AutomationEngine:
|
|||||||
# Audit record — best-effort.
|
# Audit record — best-effort.
|
||||||
try:
|
try:
|
||||||
from ledgrab.core.activity_log.recorder import get_module_recorder
|
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
|
from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity
|
||||||
|
|
||||||
rec = get_module_recorder()
|
rec = get_module_recorder()
|
||||||
@@ -783,6 +786,7 @@ class AutomationEngine:
|
|||||||
_auto_name = self._store.get_automation(automation_id).name
|
_auto_name = self._store.get_automation(automation_id).name
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
_safe_deact_name = sanitize_display(_auto_name) if _auto_name else None
|
||||||
rec.record(
|
rec.record(
|
||||||
category=ActivityCategory.CAPTURE,
|
category=ActivityCategory.CAPTURE,
|
||||||
action="automation.deactivated",
|
action="automation.deactivated",
|
||||||
@@ -790,8 +794,8 @@ class AutomationEngine:
|
|||||||
actor="system",
|
actor="system",
|
||||||
entity_type="automation",
|
entity_type="automation",
|
||||||
entity_id=automation_id,
|
entity_id=automation_id,
|
||||||
entity_name=_auto_name,
|
entity_name=_safe_deact_name,
|
||||||
message=f"Automation '{_auto_name or automation_id}' deactivated",
|
message=f"Automation '{_safe_deact_name or automation_id}' deactivated",
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from ledgrab.core.devices.led_client import (
|
|||||||
check_device_health,
|
check_device_health,
|
||||||
get_device_capabilities,
|
get_device_capabilities,
|
||||||
)
|
)
|
||||||
|
from ledgrab.core.activity_log.sanitize import sanitize_display
|
||||||
from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity
|
from ledgrab.storage.activity_log import ActivityCategory, ActivitySeverity
|
||||||
from ledgrab.utils import get_logger
|
from ledgrab.utils import get_logger
|
||||||
|
|
||||||
@@ -142,7 +143,8 @@ class DeviceHealthMixin:
|
|||||||
device_name = self._device_store.get_device(device_id).name
|
device_name = self._device_store.get_device(device_id).name
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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"
|
action = "device.online" if is_online else "device.offline"
|
||||||
severity = ActivitySeverity.INFO if is_online else ActivitySeverity.WARNING
|
severity = ActivitySeverity.INFO if is_online else ActivitySeverity.WARNING
|
||||||
status_word = "came online" if is_online else "went offline"
|
status_word = "came online" if is_online else "went offline"
|
||||||
@@ -153,7 +155,7 @@ class DeviceHealthMixin:
|
|||||||
actor="system",
|
actor="system",
|
||||||
entity_type="device",
|
entity_type="device",
|
||||||
entity_id=device_id,
|
entity_id=device_id,
|
||||||
entity_name=device_name,
|
entity_name=safe_name,
|
||||||
message=f"Device '{display}' {status_word}",
|
message=f"Device '{display}' {status_word}",
|
||||||
metadata={"latency_ms": state.health.latency_ms},
|
metadata={"latency_ms": state.health.latency_ms},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,12 +89,13 @@ const _filters: ActiveFilters = {
|
|||||||
|
|
||||||
const _ENTITY_NAV: Record<string, { tab: string; subTab: string | null; attr: string } | null> = {
|
const _ENTITY_NAV: Record<string, { tab: string; subTab: string | null; attr: string } | null> = {
|
||||||
output_target: { tab: 'targets', subTab: 'led-targets', attr: 'data-target-id' },
|
output_target: { tab: 'targets', subTab: 'led-targets', attr: 'data-target-id' },
|
||||||
led_device: { tab: 'targets', subTab: 'led-devices', attr: 'data-device-id' },
|
device: { tab: 'targets', subTab: 'led-devices', attr: 'data-device-id' },
|
||||||
picture_source: { tab: 'streams', subTab: 'raw', attr: 'data-stream-id' },
|
picture_source: { tab: 'streams', subTab: 'raw', attr: 'data-stream-id' },
|
||||||
color_strip: { tab: 'streams', subTab: 'color_strip', attr: 'data-strip-id' },
|
color_strip_source: { tab: 'streams', subTab: 'color_strip', attr: 'data-css-id' },
|
||||||
audio_source: { tab: 'streams', subTab: 'audio_capture', attr: 'data-audio-source-id' },
|
audio_source: { tab: 'streams', subTab: 'audio_capture', attr: 'data-id' },
|
||||||
automation: { tab: 'automations', subTab: 'automations', attr: 'data-automation-id' },
|
automation: { tab: 'automations', subTab: 'automations', attr: 'data-automation-id' },
|
||||||
scene_preset: { tab: 'automations', subTab: 'scenes', attr: 'data-scene-id' },
|
scene_preset: { tab: 'automations', subTab: 'scenes', attr: 'data-scene-id' },
|
||||||
|
scene_playlist: { tab: 'automations', subTab: 'playlists', attr: 'data-playlist-id' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Severity icon helper ────────────────────────────────────
|
// ─── Severity icon helper ────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user