"""Detection-level taxonomy. The 4 default levels are seeded at boot. M7 exposes read-only access so the blue side of a mission test can pick a level; M8 will add CRUD. The seed is idempotent and additive: rows whose `key` already exists are left alone (operators may have renamed labels). Only missing keys are inserted. """ from __future__ import annotations import logging import uuid from dataclasses import dataclass from sqlalchemy import select from app.db.session import session_scope from app.models.setting import DetectionLevel log = logging.getLogger("metamorph.detection_levels") @dataclass(frozen=True) class DetectionLevelView: id: uuid.UUID key: str label_fr: str label_en: str color_token: str position: int is_default: bool is_system: bool @dataclass(frozen=True) class _DefaultLevel: key: str label_fr: str label_en: str color_token: str position: int is_default: bool # Seed catalogue. Colors map onto the design-system accents (cf. tasks/design.md). DEFAULT_LEVELS: tuple[_DefaultLevel, ...] = ( _DefaultLevel( key="detected_blocked", label_fr="Bloqué", label_en="Blocked", color_token="red", position=0, is_default=False, ), _DefaultLevel( key="detected_alert", label_fr="Alerte détectée", label_en="Alert detected", color_token="orange", position=1, is_default=False, ), _DefaultLevel( key="logged_only", label_fr="Loggé uniquement", label_en="Logged only", color_token="yellow", position=2, is_default=False, ), _DefaultLevel( key="not_detected", label_fr="Non détecté", label_en="Not detected", color_token="rose", position=3, is_default=True, ), ) def _to_view(r: DetectionLevel) -> DetectionLevelView: return DetectionLevelView( id=r.id, key=r.key, label_fr=r.label_fr, label_en=r.label_en, color_token=r.color_token, position=r.position, is_default=r.is_default, is_system=r.is_system, ) def seed_detection_levels() -> dict[str, int]: """Insert any default level whose `key` is missing. Idempotent. We never mutate existing rows here — operators are free to rename labels or change the default flag. Adding a new entry to `DEFAULT_LEVELS` in a future release will surface it on the next boot. """ created = 0 with session_scope() as s: existing_keys = set(s.scalars(select(DetectionLevel.key)).all()) for lvl in DEFAULT_LEVELS: if lvl.key in existing_keys: continue s.add( DetectionLevel( key=lvl.key, label_fr=lvl.label_fr, label_en=lvl.label_en, color_token=lvl.color_token, position=lvl.position, is_default=lvl.is_default, is_system=True, ) ) created += 1 # `created` is a reserved LogRecord attribute (timestamp) — use a prefixed key. log.info( "metamorph.detection_levels.seeded", extra={"rows_created": created, "total": len(DEFAULT_LEVELS)}, ) return {"created": created, "total": len(DEFAULT_LEVELS)} def list_detection_levels() -> list[DetectionLevelView]: with session_scope() as s: rows = s.scalars( select(DetectionLevel).order_by(DetectionLevel.position, DetectionLevel.key) ).all() return [_to_view(r) for r in rows] def get_detection_level(level_id: uuid.UUID) -> DetectionLevelView | None: with session_scope() as s: r = s.get(DetectionLevel, level_id) return _to_view(r) if r is not None else None