host.controller_web_editor.routes.prefs
Server-side user preferences for the web editor.
Keeps a tiny JSON file alongside the server module so settings (theme, later UI toggles) survive server restarts without leaning on the browser's localStorage. This is per server install, not per user -- fine for a single-user dev tool.
1"""Server-side user preferences for the web editor. 2 3Keeps a tiny JSON file alongside the server module so settings (theme, 4later UI toggles) survive server restarts without leaning on the 5browser's localStorage. This is *per server install*, not per user -- 6fine for a single-user dev tool. 7""" 8 9from __future__ import annotations 10 11import json 12from pathlib import Path 13from typing import Any 14 15 16PREFS_PATH = Path(__file__).resolve().parent.parent / "prefs.json" 17 18# Only these keys are accepted from clients. Unknown keys are dropped 19# silently so a stale frontend can't smuggle arbitrary data into the 20# prefs file. 21KNOWN_KEYS = frozenset({"theme"}) 22 23# Allowed values per key, where applicable. Theme names must match the 24# `data-theme` attribute values understood by app.css. 25ALLOWED_THEMES = frozenset({ 26 "dark", "light", "raptacon", "solarized-dark", "high-contrast", 27}) 28 29 30def _validate(payload: dict[str, Any]) -> dict[str, Any]: 31 out: dict[str, Any] = {} 32 for key, value in payload.items(): 33 if key not in KNOWN_KEYS: 34 continue 35 if key == "theme": 36 if not isinstance(value, str) or value not in ALLOWED_THEMES: 37 raise ValueError( 38 f"invalid theme {value!r}; " 39 f"allowed: {sorted(ALLOWED_THEMES)}") 40 out["theme"] = value 41 return out 42 43 44def load_prefs() -> dict[str, Any]: 45 """Return the persisted preferences, or {} if the file is missing. 46 47 Corrupt files are treated as empty -- preferences are not load- 48 bearing, and a single malformed write shouldn't break the UI. 49 """ 50 if not PREFS_PATH.is_file(): 51 return {} 52 try: 53 data = json.loads(PREFS_PATH.read_text(encoding="utf-8")) 54 except (OSError, json.JSONDecodeError): 55 return {} 56 if not isinstance(data, dict): 57 return {} 58 # Filter to known keys on read so a previously-valid file with 59 # newer keys doesn't surprise us. 60 return {k: v for k, v in data.items() if k in KNOWN_KEYS} 61 62 63def save_prefs(payload: dict[str, Any]) -> dict[str, Any]: 64 """Merge ``payload`` into the prefs file and return the new state. 65 66 Raises ``ValueError`` if ``payload`` contains invalid values. 67 Unknown keys are silently dropped. 68 """ 69 incoming = _validate(payload) 70 current = load_prefs() 71 current.update(incoming) 72 PREFS_PATH.write_text( 73 json.dumps(current, indent=2) + "\n", encoding="utf-8") 74 return current 75 76 77__all__ = ["ALLOWED_THEMES", "KNOWN_KEYS", "load_prefs", "save_prefs"]
ALLOWED_THEMES =
frozenset({'high-contrast', 'raptacon', 'solarized-dark', 'light', 'dark'})
KNOWN_KEYS =
frozenset({'theme'})
def
load_prefs() -> dict[str, typing.Any]:
45def load_prefs() -> dict[str, Any]: 46 """Return the persisted preferences, or {} if the file is missing. 47 48 Corrupt files are treated as empty -- preferences are not load- 49 bearing, and a single malformed write shouldn't break the UI. 50 """ 51 if not PREFS_PATH.is_file(): 52 return {} 53 try: 54 data = json.loads(PREFS_PATH.read_text(encoding="utf-8")) 55 except (OSError, json.JSONDecodeError): 56 return {} 57 if not isinstance(data, dict): 58 return {} 59 # Filter to known keys on read so a previously-valid file with 60 # newer keys doesn't surprise us. 61 return {k: v for k, v in data.items() if k in KNOWN_KEYS}
Return the persisted preferences, or {} if the file is missing.
Corrupt files are treated as empty -- preferences are not load- bearing, and a single malformed write shouldn't break the UI.
def
save_prefs(payload: dict[str, typing.Any]) -> dict[str, typing.Any]:
64def save_prefs(payload: dict[str, Any]) -> dict[str, Any]: 65 """Merge ``payload`` into the prefs file and return the new state. 66 67 Raises ``ValueError`` if ``payload`` contains invalid values. 68 Unknown keys are silently dropped. 69 """ 70 incoming = _validate(payload) 71 current = load_prefs() 72 current.update(incoming) 73 PREFS_PATH.write_text( 74 json.dumps(current, indent=2) + "\n", encoding="utf-8") 75 return current
Merge payload into the prefs file and return the new state.
Raises ValueError if payload contains invalid values.
Unknown keys are silently dropped.