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.