feat: add admin panel at /admin for full dashboard configuration
- Backend: ConfigManager write methods (atomic YAML save), admin CRUD router for settings (display/odoo/refresh) and nodes, WS broadcast on config changes, fix nmap scan blocking event loop with to_thread - Frontend: admin UI with tab navigation, overview dashboard, node CRUD table with modal form, Odoo/display/refresh settings pages, typed API wrappers, active views filtering, config_changed WS handler - Infra: nginx no-cache headers for HTML, cache-forever for hashed assets - Fixes: WebSocket reconnect loop (ref pattern), rotation index OOB when views shrink, mutable node list cache Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -18,6 +19,12 @@ class ConfigManager:
|
||||
with open(p) as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
def _save_yaml(self, path: str, data: dict[str, Any]) -> None:
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
||||
os.replace(tmp, path)
|
||||
|
||||
def get_settings(self) -> dict[str, Any]:
|
||||
if not self._settings:
|
||||
self._settings = self._load_yaml(self._settings_path)
|
||||
@@ -26,7 +33,7 @@ class ConfigManager:
|
||||
def get_nodes(self) -> list[dict[str, Any]]:
|
||||
if not self._services:
|
||||
self._services = self._load_yaml(self._services_path)
|
||||
return self._services.get("nodes", [])
|
||||
return list(self._services.get("nodes", []))
|
||||
|
||||
def get_node_by_ip(self, ip: str) -> dict[str, Any] | None:
|
||||
for node in self.get_nodes():
|
||||
@@ -39,6 +46,21 @@ class ConfigManager:
|
||||
self._services = self._load_yaml(self._services_path)
|
||||
return self._services.get("network_scan", {})
|
||||
|
||||
def update_settings(self, section: str, values: dict[str, Any]) -> dict[str, Any]:
|
||||
settings = self._load_yaml(self._settings_path)
|
||||
if section not in settings:
|
||||
settings[section] = {}
|
||||
settings[section].update(values)
|
||||
self._save_yaml(self._settings_path, settings)
|
||||
self._settings = {}
|
||||
return settings[section]
|
||||
|
||||
def save_nodes(self, nodes: list[dict[str, Any]]) -> None:
|
||||
services = self._load_yaml(self._services_path)
|
||||
services["nodes"] = nodes
|
||||
self._save_yaml(self._services_path, services)
|
||||
self._services = {}
|
||||
|
||||
def reload(self) -> None:
|
||||
self._settings = {}
|
||||
self._services = {}
|
||||
|
||||
Reference in New Issue
Block a user