diff --git a/fablednetmon/settings/__init__.py b/fablednetmon/settings/__init__.py new file mode 100644 index 0000000..ac2cf97 --- /dev/null +++ b/fablednetmon/settings/__init__.py @@ -0,0 +1 @@ +# fablednetmon/settings/__init__.py diff --git a/fablednetmon/settings/routes.py b/fablednetmon/settings/routes.py new file mode 100644 index 0000000..40eaac8 --- /dev/null +++ b/fablednetmon/settings/routes.py @@ -0,0 +1,142 @@ +# fablednetmon/settings/routes.py +from __future__ import annotations +import json +import logging +from pathlib import Path +from quart import Blueprint, current_app, render_template, request, redirect, url_for + +from fablednetmon.auth.middleware import require_role +from fablednetmon.models.users import UserRole +from fablednetmon.core.settings import ( + DEFAULTS, get_all_settings, set_setting, + to_smtp_cfg, to_webhook_cfg, to_ansible_cfg, to_plugins_cfg, +) + +settings_bp = Blueprint("settings", __name__, url_prefix="/settings") +logger = logging.getLogger(__name__) + + +def _discover_plugins() -> list[dict]: + """Scan PLUGIN_DIR for plugin.yaml files. Returns list of plugin metadata dicts.""" + import yaml + plugin_dir = Path(current_app.config.get("PLUGIN_DIR", "plugins")).resolve() + plugins = [] + if not plugin_dir.exists(): + return plugins + for entry in sorted(plugin_dir.iterdir()): + yaml_path = entry / "plugin.yaml" + if entry.is_dir() and yaml_path.exists(): + try: + with yaml_path.open() as f: + meta = yaml.safe_load(f) or {} + meta["_dir"] = entry.name + plugins.append(meta) + except Exception: + logger.warning("Could not read %s", yaml_path) + return plugins + + +@settings_bp.get("/") +@require_role(UserRole.admin) +async def index(): + async with current_app.db_sessionmaker() as db: + settings = await get_all_settings(db) + + discovered_plugins = _discover_plugins() + + # Merge current plugin settings from DB into discovered plugin list + plugins_cfg = to_plugins_cfg(settings) + for plugin in discovered_plugins: + name = plugin["_dir"] + stored = plugins_cfg.get(name, {}) + plugin["_enabled"] = stored.get("enabled", False) + # Merge config defaults with stored overrides for display + defaults = plugin.get("config", {}) + plugin["_config"] = {**defaults, **{k: v for k, v in stored.items() if k != "enabled"}} + + return await render_template( + "settings/index.html", + settings=settings, + discovered_plugins=discovered_plugins, + ) + + +@settings_bp.post("/") +@require_role(UserRole.admin) +async def save(): + form = await request.form + + async with current_app.db_sessionmaker() as db: + async with db.begin(): + # General settings + await set_setting(db, "session.lifetime_hours", + int(form.get("session.lifetime_hours", 8))) + await set_setting(db, "data.retention_days", + int(form.get("data.retention_days", 90))) + await set_setting(db, "monitors.poll_interval_seconds", + int(form.get("monitors.poll_interval_seconds", 60))) + + # SMTP + recipients_raw = form.get("smtp.recipients", "") + recipients = [r.strip() for r in recipients_raw.split(",") if r.strip()] + await set_setting(db, "smtp.host", form.get("smtp.host", "")) + await set_setting(db, "smtp.port", int(form.get("smtp.port", 587))) + await set_setting(db, "smtp.tls", "smtp.tls" in form) + await set_setting(db, "smtp.username", form.get("smtp.username", "")) + if form.get("smtp.password"): + await set_setting(db, "smtp.password", form.get("smtp.password")) + await set_setting(db, "smtp.recipients", recipients) + + # Webhook + await set_setting(db, "webhook.url", form.get("webhook.url", "")) + await set_setting(db, "webhook.template", form.get("webhook.template", "")) + + # Ansible sources (JSON textarea) + sources_raw = form.get("ansible.sources", "[]") + try: + sources = json.loads(sources_raw) + if not isinstance(sources, list): + sources = [] + except json.JSONDecodeError: + sources = [] + await set_setting(db, "ansible.sources", sources) + + # Build per-plugin config dicts from form fields + discovered = _discover_plugins() + for plugin in discovered: + name = plugin["_dir"] + enabled = f"plugin.{name}.enabled" in form + plugin_cfg: dict = {"enabled": enabled} + for cfg_key in plugin.get("config", {}).keys(): + field_name = f"plugin.{name}.{cfg_key}" + if field_name in form: + raw_val = form[field_name] + # Coerce to the default type + default_val = plugin["config"][cfg_key] + if isinstance(default_val, bool): + plugin_cfg[cfg_key] = raw_val.lower() in ("1", "true", "yes", "on") + elif isinstance(default_val, int): + try: + plugin_cfg[cfg_key] = int(raw_val) + except ValueError: + plugin_cfg[cfg_key] = default_val + else: + plugin_cfg[cfg_key] = raw_val + await set_setting(db, f"plugin.{name}", plugin_cfg) + + # Live-update app.config so settings take effect without restart + from fablednetmon.core.settings import get_all_settings as _get_all + async with current_app.db_sessionmaker() as db: + fresh = await _get_all(db) + + current_app.config.update( + SESSION_LIFETIME_HOURS=fresh.get("session.lifetime_hours", 8), + DATA_RETENTION_DAYS=fresh.get("data.retention_days", 90), + MONITORS_POLL_INTERVAL=fresh.get("monitors.poll_interval_seconds", 60), + SMTP=to_smtp_cfg(fresh), + WEBHOOK=to_webhook_cfg(fresh), + ANSIBLE=to_ansible_cfg(fresh), + PLUGINS=to_plugins_cfg(fresh), + ) + + return redirect(url_for("settings.index")) diff --git a/fablednetmon/templates/base.html b/fablednetmon/templates/base.html index 9ca6090..093574d 100644 --- a/fablednetmon/templates/base.html +++ b/fablednetmon/templates/base.html @@ -37,6 +37,9 @@ Hosts Alerts Ansible + {% if session.user_role == 'admin' %} + Settings + {% endif %} Logout ({{ session.username }}) {% endif %} diff --git a/fablednetmon/templates/settings/index.html b/fablednetmon/templates/settings/index.html new file mode 100644 index 0000000..fd01108 --- /dev/null +++ b/fablednetmon/templates/settings/index.html @@ -0,0 +1,124 @@ +{# fablednetmon/templates/settings/index.html #} +{% extends "base.html" %} +{% block title %}Settings — FabledNetMon{% endblock %} +{% block content %} +