The demo Settings screen was fed by a frozen snapshot baked into the externally generated demoDataset.js, so it drifted from the real manifest: the Remnawave Panel (plus System and Migrations) sections were missing and trial/checkout/common still showed as top-level sections instead of subsections. Generate frontend/src/lib/webapp/settingsManifest.generated.json from manifest_payload() (the same source the live /admin/settings endpoint uses) via scripts/export_settings_manifest.py, and build the demo section structure from it, overlaying realistic demo values per field key. A pytest drift guard fails if the Python manifest changes without regenerating the snapshot, so the demo stays in sync going forward.
97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""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())
|