""" PluginLoader — registers action plugins at startup and syncs them with the DB. Startup flow: 1. For each builtin action, INSERT into `plugins` table (skip if already exists). This preserves the `enabled` flag set by admins. 2. Load the list of enabled plugin names from DB. 3. Register only enabled actions in the registry. Hot enable/disable (via Admin API): - Disable: set DB.enabled=False, call action_registry.unregister(name) - Enable: set DB.enabled=True, call action_registry.register(BUILTIN_ACTIONS[name]) """ from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.dialects.postgresql import insert as pg_insert from app.models.plugin import Plugin from app.plugins.base import BaseAction from app.plugins.builtin.camera.get_status import CameraGetStatusAction from app.plugins.builtin.camera.set_ntp import CameraSetNTPAction from app.plugins.builtin.common.ping import PingAction from app.plugins.builtin.mikrotik.get_config import MikrotikGetConfigAction from app.plugins.builtin.mikrotik.get_ntp import MikrotikGetNTPAction from app.plugins.builtin.mikrotik.set_ntp import MikrotikSetNTPAction from app.plugins.builtin.ssh.debian_system_info import DebianSystemInfoAction from app.plugins.builtin.ssh.get_file import GetFileAction from app.plugins.builtin.ssh.get_timedatectl import GetTimedatectlAction from app.plugins.builtin.ssh.ssh_command import SSHCommandAction from app.plugins.registry import action_registry from app.services.database import AsyncSessionLocal # Public dict — used by Admin router for hot enable/disable BUILTIN_ACTIONS: dict[str, BaseAction] = { a.name: a for a in [ PingAction(), SSHCommandAction(), DebianSystemInfoAction(), GetTimedatectlAction(), GetFileAction(), MikrotikGetConfigAction(), MikrotikGetNTPAction(), MikrotikSetNTPAction(), CameraGetStatusAction(), CameraSetNTPAction(), ] } async def load_plugins() -> None: now = datetime.now(timezone.utc) async with AsyncSessionLocal() as db: async with db.begin(): for action in BUILTIN_ACTIONS.values(): stmt = ( pg_insert(Plugin) .values( name=action.name, display_name=action.display_name, description=(action.__class__.__doc__ or "").strip() or None, enabled=True, registered_at=now, ) .on_conflict_do_nothing(index_elements=["name"]) ) await db.execute(stmt) # Load enabled plugins from DB async with AsyncSessionLocal() as db: result = await db.execute(select(Plugin.name).where(Plugin.enabled == True)) enabled_names = {row[0] for row in result.all()} # Register only enabled actions for name, action in BUILTIN_ACTIONS.items(): if name in enabled_names: action_registry.register(action) print(f"[plugins] Registered {len(enabled_names)}/{len(BUILTIN_ACTIONS)} plugin(s)")