"""Export the admin settings manifest as a demo-safe JSON snapshot. The Mini App demo (docs site) has no backend, so its admin Settings screen is fed by a mock. Previously the section/field structure was frozen inside the externally-generated ``demoDataset.js`` snapshot, which drifted from the real manifest (e.g. the Remnawave Panel section was missing). This script regenerates ``frontend/src/lib/webapp/settingsManifest.generated.json`` straight from :func:`manifest_payload` — the same source of truth the live ``/admin/settings`` endpoint uses — grouped into sections exactly like ``admin_settings_get_route``. The demo overlays its realistic values on top of this structure, so adding a field in Python is enough to keep the demo in sync. Usage:: python scripts/export_settings_manifest.py A pytest drift guard (``tests/test_settings_manifest_demo_sync.py``) compares the committed JSON against a fresh build, so the snapshot cannot silently drift. After regenerating, run Prettier so the file matches the frontend code style:: npx --prefix frontend prettier --write src/lib/webapp/settingsManifest.generated.json """ from __future__ import annotations import json import sys from pathlib import Path from typing import Any, Dict, List ROOT = Path(__file__).resolve().parents[1] BACKEND = ROOT / "backend" for _path in (str(BACKEND), str(ROOT)): if _path not in sys.path: sys.path.insert(0, _path) from bot.app.web.admin_settings_manifest import manifest_payload # noqa: E402 OUTPUT_PATH = ROOT / "frontend" / "src" / "lib" / "webapp" / "settingsManifest.generated.json" def build_demo_settings_sections() -> List[Dict[str, Any]]: """Group the manifest into ordered sections with demo-safe field values. Mirrors :func:`bot.app.web.admin_api_impl.settings.admin_settings_get_route` but without any database- or settings-derived values: every field starts empty/unoverridden and secrets expose no value. The demo fills in realistic values per field key at runtime. """ fields = manifest_payload() sections: Dict[str, Dict[str, Any]] = {} for field in fields: section_id = field["section"] if section_id not in sections: sections[section_id] = { "id": section_id, "order": field["section_order"], "fields": [], } is_secret = bool(field.get("secret")) response_field: Dict[str, Any] = { **field, "value": "", "overridden": False, "updated_at": None, } if is_secret: response_field["has_value"] = False webhook_path = str(response_field.get("webhook_path") or "").strip() if webhook_path: if not webhook_path.startswith("/"): webhook_path = f"/{webhook_path}" response_field["webhook_path"] = webhook_path response_field["webhook_base_url_configured"] = False sections[section_id]["fields"].append(response_field) return sorted(sections.values(), key=lambda section: section["order"]) def render_json(sections: List[Dict[str, Any]]) -> str: return json.dumps(sections, ensure_ascii=False, indent=2) + "\n" def main() -> int: sections = build_demo_settings_sections() OUTPUT_PATH.write_text(render_json(sections), encoding="utf-8") field_count = sum(len(section["fields"]) for section in sections) print( f"Wrote {len(sections)} sections / {field_count} fields to {OUTPUT_PATH.relative_to(ROOT)}" ) return 0 if __name__ == "__main__": raise SystemExit(main())