- Add complete Project Hub & Workspace Interface implementation - New data models: Project, ModuleKit, Workspace - 3 fixture projects (cybermind.fr, cybermood.eu, secubox-c3) - 4 module kits (Security, Network, Automation, Media) - Workspace routes with project switching and kit installation - 4 workspace tabs: Overview, Module Kits, Devices, Composer - New navigation item: Workspace (7th section) - Remove all glowing effects from UI - Remove Command Center widget glow and backdrop blur - Remove device status indicator glow - Remove toggle button glow effects - Extend DataStore with 13 new methods for workspace management - Add 270+ lines of workspace-specific CSS with responsive layouts - Create workspace templates and result partials 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
"""Ingest package metadata into the WebUI module catalog."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, Iterable, List, Optional
|
|
|
|
from .config import settings
|
|
|
|
REPO_ROOT = settings.project_root.parent.parent
|
|
PACKAGE_ROOT = REPO_ROOT / "package" / "secubox"
|
|
TOP_LEVEL_PATTERNS = ("luci-app-*", "luci-theme-*")
|
|
|
|
ASSIGN_RE = re.compile(r"^([A-Za-z0-9_]+)\s*(?::|\+)?=\s*(.+)$")
|
|
AFFECTED_PREFIXES = ("luci-app-", "luci-theme-", "secubox-app-")
|
|
ALLOWED_EXACT = {"secubox-core", "secubox-app"}
|
|
|
|
CATEGORY_MAP = {
|
|
"luci-app": "LuCI Application",
|
|
"luci-theme": "LuCI Theme",
|
|
"secubox-app": "SecuBox Service",
|
|
}
|
|
|
|
SECURE_CONTEXTS = {
|
|
"luci-app": ["ui-sandbox"],
|
|
"luci-theme": ["theme-assets"],
|
|
"secubox-app": ["service-sandbox"],
|
|
}
|
|
|
|
ACTIONS = {
|
|
"luci-app": ["Preview UI", "Run Diagnostics"],
|
|
"luci-theme": ["Preview Theme", "Export Assets"],
|
|
"secubox-app": ["Simulate Service", "Push Config"],
|
|
}
|
|
|
|
|
|
class ModuleRecord(Dict[str, object]):
|
|
pass
|
|
|
|
|
|
def is_allowed_name(name: str) -> bool:
|
|
if name in ALLOWED_EXACT:
|
|
return True
|
|
return any(name.startswith(prefix) for prefix in AFFECTED_PREFIXES)
|
|
|
|
|
|
def read_makefile(path: Path) -> Dict[str, str]:
|
|
metadata: Dict[str, str] = {}
|
|
with path.open() as handle:
|
|
for raw in handle:
|
|
line = raw.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
match = ASSIGN_RE.match(line)
|
|
if match:
|
|
key, value = match.groups()
|
|
metadata[key] = value.strip()
|
|
return metadata
|
|
|
|
|
|
def read_readme_summary(module_dir: Path) -> Optional[str]:
|
|
for candidate in ("README.md", "README"):
|
|
doc = module_dir / candidate
|
|
if not doc.exists():
|
|
continue
|
|
for raw in doc.read_text().splitlines():
|
|
text = raw.strip()
|
|
if not text or text.startswith("#"):
|
|
continue
|
|
return text
|
|
return None
|
|
|
|
|
|
def derive_category(pkg_name: str) -> str:
|
|
if pkg_name == "secubox-core":
|
|
return "Framework"
|
|
for prefix, category in CATEGORY_MAP.items():
|
|
if pkg_name.startswith(prefix):
|
|
return category
|
|
return "SecuBox Package"
|
|
|
|
|
|
def derive_secure_contexts(pkg_name: str) -> List[str]:
|
|
if pkg_name == "secubox-core":
|
|
return ["privileged"]
|
|
for prefix, contexts in SECURE_CONTEXTS.items():
|
|
if pkg_name.startswith(prefix):
|
|
return contexts
|
|
return ["module-sandbox"]
|
|
|
|
|
|
def derive_actions(pkg_name: str) -> List[str]:
|
|
if pkg_name == "secubox-core":
|
|
return ["Device Snapshot", "Catalog Sync", "Profile Merge"]
|
|
for prefix, actions in ACTIONS.items():
|
|
if pkg_name.startswith(prefix):
|
|
return actions
|
|
return ["Inspect", "Simulate"]
|
|
|
|
|
|
def derive_tags(pkg_name: str) -> List[str]:
|
|
tokens = [token for token in pkg_name.split("-") if token not in {"luci", "app", "secubox", "theme", "core"}]
|
|
return tokens or [pkg_name]
|
|
|
|
|
|
def humanize_name(pkg_name: str) -> str:
|
|
cleaned = pkg_name
|
|
for prefix in AFFECTED_PREFIXES:
|
|
if cleaned.startswith(prefix):
|
|
cleaned = cleaned[len(prefix):]
|
|
break
|
|
cleaned = cleaned.replace("-", " ").strip()
|
|
return cleaned.title() if cleaned else pkg_name
|
|
|
|
|
|
def detect_health(info: Dict[str, str]) -> str:
|
|
release = info.get("PKG_RELEASE", "").lower()
|
|
desc = " ".join(info.get(key, "") for key in ("LUCI_DESCRIPTION", "TITLE", "PKG_DESCRIPTION")).lower()
|
|
if "beta" in release or "beta" in desc:
|
|
return "beta"
|
|
if "rc" in release:
|
|
return "testing"
|
|
return "stable"
|
|
|
|
|
|
def create_module_record(module_dir: Path) -> ModuleRecord:
|
|
makefile = module_dir / "Makefile"
|
|
if not makefile.exists():
|
|
raise FileNotFoundError(f"{module_dir} missing Makefile")
|
|
|
|
data = read_makefile(makefile)
|
|
raw_pkg_name = data.get("PKG_NAME", module_dir.name)
|
|
pkg_name = raw_pkg_name if is_allowed_name(raw_pkg_name) else module_dir.name
|
|
title = data.get("LUCI_TITLE") or data.get("TITLE")
|
|
display_name = title.split(" - ")[0].strip() if title else ""
|
|
if not display_name or display_name.startswith("("):
|
|
display_name = humanize_name(pkg_name)
|
|
|
|
summary = (
|
|
data.get("LUCI_DESCRIPTION")
|
|
or data.get("PKG_DESCRIPTION")
|
|
or (title.split(" - ", 1)[1].strip() if title and " - " in title else None)
|
|
or read_readme_summary(module_dir)
|
|
or "No summary provided."
|
|
)
|
|
|
|
record: ModuleRecord = ModuleRecord(
|
|
id=pkg_name,
|
|
name=display_name,
|
|
category=derive_category(pkg_name),
|
|
version=data.get("PKG_VERSION", "0.0.0"),
|
|
summary=summary,
|
|
health=detect_health(data),
|
|
tags=derive_tags(pkg_name),
|
|
secure_contexts=derive_secure_contexts(pkg_name),
|
|
actions=derive_actions(pkg_name),
|
|
)
|
|
return record
|
|
|
|
|
|
def gather_module_dirs() -> Iterable[Path]:
|
|
seen = set()
|
|
|
|
for pattern in TOP_LEVEL_PATTERNS:
|
|
for path in settings.project_root.parent.glob(pattern):
|
|
if path.is_dir() and is_allowed_name(path.name):
|
|
resolved = path.resolve()
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
yield path
|
|
|
|
if PACKAGE_ROOT.exists():
|
|
for path in PACKAGE_ROOT.iterdir():
|
|
if path.is_dir() and not path.name.startswith('.') and is_allowed_name(path.name):
|
|
resolved = path.resolve()
|
|
if resolved not in seen:
|
|
seen.add(resolved)
|
|
yield path
|
|
|
|
|
|
def write_records(records: List[ModuleRecord], output: Path, pretty: bool) -> None:
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
with output.open("w") as handle:
|
|
if pretty:
|
|
json.dump(records, handle, indent=2)
|
|
else:
|
|
json.dump(records, handle)
|
|
|
|
|
|
def ingest(pretty: bool = False, output: Optional[Path] = None) -> Path:
|
|
destination = output or (settings.data_dir / "modules.json")
|
|
records: List[ModuleRecord] = []
|
|
for module_dir in gather_module_dirs():
|
|
try:
|
|
records.append(create_module_record(module_dir))
|
|
except Exception as exc: # noqa: BLE001
|
|
print(f"⚠️ Skipping {module_dir}: {exc}")
|
|
|
|
records.sort(key=lambda item: str(item["name"]).lower())
|
|
write_records(records, destination, pretty)
|
|
print(f"✅ Wrote {len(records)} modules to {destination}")
|
|
return destination
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Generate WebUI module metadata from repository packages.")
|
|
parser.add_argument("--output", type=Path, default=settings.data_dir / "modules.json", help="Destination JSON file")
|
|
parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
|
|
return parser
|
|
|
|
|
|
def main(argv: Optional[List[str]] = None) -> None:
|
|
parser = build_arg_parser()
|
|
args = parser.parse_args(argv)
|
|
ingest(pretty=args.pretty, output=args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|