"""Tiny JSON state store so dashboard/bot settings survive a restart. Persists things the user configures on the dashboard — per-guild listen lists, bot behaviour toggles, TTS controls, and the chosen STT/LLM models — to a single JSON file so a service or container restart keeps them. Path: ``WSAI_STATE_FILE`` env, else ``~/.config/wsai/state.json``. For a container deployment, mount that path (or point the env at a mounted volume) to keep the file across ``docker restart``. Pure stdlib, thread-safe, best-effort: a read/write failure never raises into the caller (the dashboard must keep working even if the disk is unwritable). """ from __future__ import annotations import json import logging import os import threading from typing import Any log = logging.getLogger("wsai.state") _LOCK = threading.Lock() def path() -> str: return os.environ.get("WSAI_STATE_FILE") or os.path.expanduser("~/.config/wsai/state.json") def load() -> dict[str, Any]: try: with open(path(), encoding="utf-8") as f: data = json.load(f) return data if isinstance(data, dict) else {} except FileNotFoundError: return {} except Exception as exc: # noqa: BLE001 — corrupt/unreadable state must not crash startup log.warning("state load failed (%s): %s", path(), exc) return {} def save(state: dict[str, Any]) -> None: p = path() try: with _LOCK: os.makedirs(os.path.dirname(p), exist_ok=True) tmp = p + ".tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False, indent=2) os.replace(tmp, p) # atomic except Exception as exc: # noqa: BLE001 log.warning("state save failed (%s): %s", p, exc) def patch(key: str, value: Any) -> None: """Read-modify-write one top-level key.""" s = load() s[key] = value save(s)