server-up/app/core/modules.py
2026-05-09 13:55:29 +00:00

161 lines
5.1 KiB
Python

"""Module system — base class + discovery."""
from __future__ import annotations
import importlib.util, inspect, json, sys
from pathlib import Path
from flask import Blueprint
CORE = frozenset({
# Core feature IDs
"stacks", "app_store", "docker_images", "audit", "settings", "wizard",
"language", "i18n", "projects", "system_info", "updater", "git_browser",
"store", "dashboard", "docker", "core", "modules",
# Repo module IDs that are core features (not optional)
"audit_log", "audit-log",
"system-info", "system_info",
"git-browser", "git_browser",
})
class Module:
ID = ""
NAME = ""
ICON = "🧩"
DESC = ""
VER = "1.0.0"
# Old-style compat attributes
MODULE_ID = ""
MODULE_NAME = ""
MODULE_ICON = ""
MODULE_DESC = ""
def __init_subclass__(cls, **kw):
"""Sync old MODULE_* attrs to new ID/NAME/ICON/DESC."""
super().__init_subclass__(**kw)
# Old-style MODULE_ID → new ID (old takes priority if set)
if cls.__dict__.get("MODULE_ID"):
cls.ID = cls.MODULE_ID
if cls.__dict__.get("MODULE_NAME"):
cls.NAME = cls.MODULE_NAME
if cls.__dict__.get("MODULE_ICON"):
cls.ICON = cls.MODULE_ICON
if cls.__dict__.get("MODULE_DESC"):
cls.DESC = cls.MODULE_DESC
def blueprint(self) -> Blueprint | None:
return None
def pages(self) -> list[dict]:
return []
def on_load(self, app) -> None:
pass
def settings_html(self) -> str | None:
return None
def get_config(self, key=None, default=None):
"""Haal module-specifieke config op."""
import core as cfg
mc = cfg.load().get("MODULE_SETTINGS", {}).get(self.ID, {})
if key is None:
return mc
return mc.get(key, default)
def save_config(self, updates: dict):
"""Sla module-specifieke config op."""
import core as cfg
c = cfg.load()
ms = dict(c.get("MODULE_SETTINGS", {}))
mc = dict(ms.get(self.ID, {}))
mc.update(updates)
ms[self.ID] = mc
cfg.patch({"MODULE_SETTINGS": ms})
def info(self) -> dict:
return {"id": self.ID, "name": self.NAME, "icon": self.ICON,
"desc": self.DESC, "version": self.VER,
"pages": self.pages(), "core": self.ID in CORE}
def discover(dirs: list[Path]) -> list[tuple[str, type, Path]]:
found = []
seen = set()
for base in dirs:
if not base.exists():
continue
for d in sorted(base.iterdir()):
if not d.is_dir() or d.name.startswith("_"):
continue
if not (d / "__init__.py").exists():
continue
# Vereist module.json
if not (d / "module.json").exists():
continue
mid = d.name
if mid in seen:
continue
# Skip CORE modules (dir name check)
if mid in CORE:
continue
seen.add(mid)
meta = _meta(d)
# Skip CORE modules (meta ID check)
meta_id = meta.get("id", mid)
if meta_id in CORE:
continue
if meta.get("enabled") is False:
continue
cls = _load(mid, d)
if cls:
# Final check: skip if the class ID is CORE
try:
inst = cls()
if inst.ID in CORE:
continue
except Exception:
pass
found.append((meta.get("order", 50), mid, cls, d.resolve()))
found.sort(key=lambda x: x[0])
return [(m, c, p) for _, m, c, p in found]
def _meta(d: Path) -> dict:
f = d / "module.json"
if f.exists():
try:
return json.loads(f.read_text())
except Exception:
pass
return {}
def _load(mid: str, d: Path) -> type | None:
cls, _ = _load_detail(mid, d)
return cls
def _load_detail(mid: str, d: Path) -> tuple[type | None, str]:
"""Load module class, return (cls, error_msg)."""
try:
name = f"_mod_{mid}"
init_file = d / "__init__.py"
if not init_file.exists():
return None, f"__init__.py niet gevonden in {d}"
spec = importlib.util.spec_from_file_location(
name, str(init_file),
submodule_search_locations=[str(d)])
if not spec or not spec.loader:
return None, f"kon spec niet laden voor {init_file}"
pkg = importlib.util.module_from_spec(spec)
sys.modules[name] = pkg
spec.loader.exec_module(pkg)
for _, obj in inspect.getmembers(pkg, inspect.isclass):
if obj is not Module and issubclass(obj, Module) and obj.__module__ == name:
return obj, ""
# Toon welke classes er WEL zijn
classes = [n for n, o in inspect.getmembers(pkg, inspect.isclass) if o.__module__ == name]
return None, f"geen Module subclass gevonden. Classes: {classes or 'geen'}"
except Exception as e:
print(f"{mid}: {e}")
return None, f"{type(e).__name__}: {e}"