from __future__ import annotations from fastapi import APIRouter, Depends from sqlmodel import Session, select from ..db.models import Connector, Policy from ..db.session import get_session router = APIRouter(prefix='/api') def _connector_names(connectors: list[Connector], ids: set[str]) -> list[str]: return [connector.name for connector in connectors if connector.id in ids] def _policy_risk(session: Session, action: str, target: str, fallback: str = 'medium') -> str: policy = session.exec(select(Policy).where(Policy.action == action, Policy.target == target)).first() if policy and policy.risk in {'low', 'medium', 'high', 'critical'}: return policy.risk return fallback def _connector_labels(connectors: list[Connector], ids: set[str]) -> list[str]: labels = _connector_names(connectors, ids) known = {connector.id for connector in connectors} for connector_id in sorted(ids - known): labels.append(connector_id.replace('_', ' ').title()) return labels def _success_rate(connectors: dict[str, Connector], ids: set[str], fallback: int = 80) -> int: if not ids: return fallback present = [connectors.get(connector_id) for connector_id in ids] connected = sum(1 for connector in present if connector and connector.status == 'connected') return round((connected / len(ids)) * 100) @router.get('/playbooks') def list_playbooks(session: Session = Depends(get_session)): connectors = session.exec(select(Connector).order_by(Connector.id)).all() degraded = [ connector for connector in connectors if connector.status in {'missing_config', 'disconnected', 'error', 'auth_failed', 'unreachable'} ] connector_by_id = {connector.id: connector for connector in connectors} playbooks: list[dict] = [] playbooks.append({ 'id': 'storage-pressure-review', 'name': 'Storage Pressure Review', 'description': 'Brengt array-, cache-, disk-, share- en container-mountdruk samen zonder storage-mutaties te starten.', 'risk': 'medium', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': _connector_labels(connectors, {'unraid', 'portainer'}), 'steps': [ 'Open Storage & Volumes and review array/cache pressure', 'Inspect disks and critical shares above threshold', 'Compare Portainer mount hints with Unraid share usage', 'Open Safety or Remediation Planner for non-executing follow-up notes', 'Keep mover, prune and storage mutations outside this playbook', ], 'successRate': _success_rate(connector_by_id, {'unraid', 'portainer'}), }) playbooks.append({ 'id': 'exposure-auth-coverage-review', 'name': 'Exposure & Auth Coverage Review', 'description': 'Controleert NPM public hosts, SSL-force, certificaten en Authentik coverage als één securityroute.', 'risk': 'high', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': _connector_labels(connectors, {'nginx_proxy_manager', 'authentik'}), 'steps': [ 'Open Exposure Center and sort high-risk public hosts first', 'Check SSL-force, certificate status and access-list evidence', 'Open matching Authentik coverage details for unprotected hosts', 'Create or review a remediation draft before touching external systems', 'Re-run Safety refresh after operator-side NPM/Auth changes', ], 'successRate': _success_rate(connector_by_id, {'nginx_proxy_manager', 'authentik'}), }) playbooks.append({ 'id': 'media-stalled-download-review', 'name': 'Media Pipeline Stalled Download Review', 'description': 'Volgt Sonarr/Radarr wanted items, Prowlarr health en Deluge stalled downloads zonder data te verwijderen.', 'risk': 'medium', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': _connector_labels(connectors, {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}), 'steps': [ 'Open Media Pipeline summary and review stalled/wanted counters', 'Check Prowlarr indexers and Sonarr/Radarr queue status', 'Inspect Deluge stalled downloads and pause/resume policy guidance', 'Review Plex/Tautulli activity before starting scans', 'Use approval-gated actions only for non-destructive connector operations', ], 'successRate': _success_rate(connector_by_id, {'sonarr', 'radarr', 'prowlarr', 'deluge', 'plex', 'tautulli', 'widefrog'}), }) playbooks.append({ 'id': 'unhealthy-container-triage', 'name': 'Unhealthy Container Triage', 'description': 'Richt de operator op ongezonde of ongemapte containers met logs, Portainer deep-link en policy-aware restartpad.', 'risk': 'medium', 'allowedSources': ['manual'], 'requiredConnectors': _connector_labels(connectors, {'portainer'}), 'steps': [ 'Open Containers with attention filter', 'Inspect health, import status, app mapping and redacted logs', 'Open Portainer deep-link only when live context is available', 'Request approval before restarting protected apps', 'Verify the container and app detail after any approved action', ], 'successRate': _success_rate(connector_by_id, {'portainer'}), }) playbooks.append({ 'id': 'app-inventory-normalization', 'name': 'App Inventory Normalization', 'description': 'Werkt Unknown, needs-review en no-connector apps systematisch weg zodat Apps minder ruis tonen.', 'risk': 'low', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': _connector_labels(connectors, {'portainer'}), 'steps': [ 'Open Apps with attention filter', 'Review Unknown and needs-review app cards first', 'Map discovered containers to canonical apps where the match is clear', 'Bind connector candidates only after connector config has tested connected', 'Re-run inventory discovery and compare drift after review', ], 'successRate': _success_rate(connector_by_id, {'portainer'}), }) playbooks.append({ 'id': 'backup-readiness-check', 'name': 'Backup Readiness Check', 'description': 'Controleert SQLite backup posture, restore-notes en release hygiene zonder restore uit te voeren.', 'risk': 'low', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': ['AppOps DB'], 'steps': [ 'Open Settings backups section', 'Review latest backup metadata and integrity status', 'Confirm restore remains manual stop-container flow', 'Open Audit for recent backup events', 'Update release notes after verified production changes', ], 'successRate': 100, }) playbooks.append({ 'id': 'notification-delivery-review', 'name': 'Notification Delivery Review', 'description': 'Controleert dashboard- en Home Assistant notificatiebereik zonder automatisch testberichten te verzenden.', 'risk': 'low', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': _connector_labels(connectors, {'home_assistant'}), 'steps': [ 'Open Notifications and review active rules, channels and unread events', 'Validate Home Assistant readiness only when the operator enabled the channel', 'Run explicit evaluation for safety/media events when stale notifications appear', 'Keep external delivery opt-in and audited', ], 'successRate': _success_rate(connector_by_id, {'home_assistant'}, fallback=70), }) if degraded: degraded_names = [connector.name for connector in degraded] playbooks.append({ 'id': 'connector-readiness-review', 'name': 'Connector Readiness Review', 'description': 'Controleert welke connectors configuratie, binding of health-herstel nodig hebben zonder wijzigingen uit te voeren.', 'risk': 'medium', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': degraded_names, 'steps': [ f"Review {len(degraded)} degraded connector(s): {', '.join(degraded_names[:8])}", 'Open connector setup for missing_config entries', 'Run read-only connector tests', 'Review policy and audit status before any follow-up action', ], 'successRate': max(0, 100 - (len(degraded) * 10)), }) if {'portainer', 'widefrog'} <= set(connector_by_id): widefrog_ready = connector_by_id['widefrog'].status == 'connected' portainer_ready = connector_by_id['portainer'].status == 'connected' playbooks.append({ 'id': 'fix-widefrog', 'name': 'Fix Widefrog', 'description': 'Bereidt een veilige Widefrog herstelprocedure voor met read-only checks en policy-gated vervolgstappen.', 'risk': _policy_risk(session, 'widefrog.retry_failed', 'widefrog', 'medium'), 'allowedSources': ['manual', 'voice'], 'requiredConnectors': _connector_names(connectors, {'portainer', 'widefrog'}), 'steps': [ 'Read Portainer container status', 'Read Widefrog health, queue and failed jobs', 'Inspect recent redacted logs when available', 'Draft follow-up action only when policy allows it', 'Require approval for retry or restart when policy demands it', 'Write audit result after an approved execution path', ], 'successRate': 90 if widefrog_ready and portainer_ready else 50, }) if 'unraid' in connector_by_id: unraid_ready = connector_by_id['unraid'].status == 'connected' playbooks.append({ 'id': 'unraid-health-scan', 'name': 'Unraid Health Scan', 'description': 'Leest array, disk, cache en docker-status uit zonder muterende acties te starten.', 'risk': 'low', 'allowedSources': ['manual', 'automation'], 'requiredConnectors': _connector_names(connectors, {'unraid'}), 'steps': [ 'Read array status', 'Read parity status', 'Check disk warning count', 'Check cache usage', 'Check docker service status', 'Write read-only audit summary', ], 'successRate': 100 if unraid_ready else 60, }) return playbooks