chore: initial import

This commit is contained in:
2026-07-24 21:37:03 +02:00
commit 9b5425851a
28 changed files with 4020 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
"""
Zentrale Einstellungen der Suite.
Alle Tools brauchen dieselben Zugangsdaten (Tandoor, OpenAI). Statt sie in
jedem Plugin einzeln zu pflegen, liegen sie einmal in data/settings.json und
werden beim Laden in os.environ gespiegelt. Damit funktionieren die
Original-Tools unverändert weiter: sie lesen wie bisher ihre Umgebungsvariablen.
Vorrang:
1. data/settings.json (über die Oberfläche gepflegt)
2. Umgebung / .env (Fallback, wenn in settings.json leer)
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
MASK = "••••••••"
@dataclass(frozen=True)
class Field:
key: str
label: str
group: str
kind: str = "text" # text | secret | bool | number | select
default: str = ""
help: str = ""
options: tuple[str, ...] = ()
placeholder: str = ""
@property
def secret(self) -> bool:
return self.kind == "secret"
FIELDS: tuple[Field, ...] = (
Field("TANDOOR_URL", "Tandoor-URL", "Tandoor", "text",
placeholder="https://kitchen.example.de",
help="Basis-URL deiner Tandoor-Instanz, ohne /api."),
Field("TANDOOR_TOKEN", "Tandoor-Token", "Tandoor", "secret",
help="Token mit Schreibrechten. Verlässt das lokale Backend nicht."),
Field("TANDOOR_AUTH_SCHEME", "Auth-Schema", "Tandoor", "select",
default="Bearer", options=("Bearer", "Token"),
help="Bei älteren DRF-Tokens „Token“ wählen."),
Field("TANDOOR_VERIFY_TLS", "TLS prüfen", "Tandoor", "bool", default="true",
help="Nur für interne Instanzen mit selbstsigniertem Zertifikat abschalten."),
Field("TANDOOR_TIMEOUT", "Timeout (s)", "Tandoor", "number", default="45"),
Field("OPENAI_API_KEY", "OpenAI API-Key", "OpenAI", "secret",
help="Wird nur für den AI-Webimport verwendet."),
Field("OPENAI_MODEL", "Modell", "OpenAI", "text", default="gpt-5.5",
placeholder="gpt-5.5"),
Field("SOURCE_VERIFY_TLS", "TLS der Quelle prüfen", "Rezeptquellen", "bool", default="true"),
Field("SOURCE_TIMEOUT", "Timeout (s)", "Rezeptquellen", "number", default="20"),
Field("FETCH_MAX_BYTES", "Max. Downloadgröße (Byte)", "Rezeptquellen", "number",
default="3000000"),
Field("ALLOW_PRIVATE_SOURCE_URLS", "Private Quell-IPs erlauben", "Rezeptquellen", "bool",
default="false",
help="Standardmäßig aus: schützt vor SSRF über localhost und private Netze."),
)
BY_KEY = {f.key: f for f in FIELDS}
GROUPS = tuple(dict.fromkeys(f.group for f in FIELDS))
@dataclass
class Settings:
path: Path
values: dict[str, str] = field(default_factory=dict)
# ------------------------------------------------------------- Laden
@classmethod
def load(cls, path: Path) -> "Settings":
values: dict[str, str] = {}
if path.exists():
try:
raw = json.loads(path.read_text(encoding="utf-8"))
if isinstance(raw, dict):
values = {k: str(v) for k, v in raw.items() if k in BY_KEY}
except Exception:
values = {}
instance = cls(path=path, values=values)
instance.apply_env()
return instance
def save(self) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp = self.path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(self.values, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8")
tmp.replace(self.path)
try:
self.path.chmod(0o600)
except Exception:
pass
self.apply_env()
# -------------------------------------------------------------- Werte
def get(self, key: str) -> str:
value = self.values.get(key, "").strip()
if value:
return value
return os.environ.get(key, "").strip() or BY_KEY[key].default
def apply_env(self) -> None:
"""Spiegelt die Einstellungen in die Prozess-Umgebung."""
for f in FIELDS:
value = self.values.get(f.key, "").strip()
if value:
os.environ[f.key] = value
elif f.key not in os.environ and f.default:
os.environ[f.key] = f.default
def tool_env(self) -> dict[str, str]:
"""Umgebung für Subprozess-Tools (CLI-Plugins)."""
env = dict(os.environ)
for f in FIELDS:
value = self.get(f.key)
if value:
env[f.key] = value
return env
# ----------------------------------------------------------- Web-API
def public(self) -> dict[str, Any]:
"""Für die Oberfläche Geheimnisse werden maskiert."""
out: dict[str, Any] = {}
for f in FIELDS:
value = self.get(f.key)
out[f.key] = MASK if (f.secret and value) else ("" if f.secret else value)
return out
def status(self) -> dict[str, bool]:
return {
"tandoor": bool(self.get("TANDOOR_URL") and self.get("TANDOOR_TOKEN")),
"openai": bool(self.get("OPENAI_API_KEY") and self.get("OPENAI_MODEL")),
}
def update(self, payload: dict[str, Any]) -> None:
for key, value in payload.items():
f = BY_KEY.get(key)
if not f:
continue
text = "" if value is None else str(value).strip()
if f.secret and text == MASK:
continue # unverändert lassen
if f.kind == "bool":
text = "true" if str(value).lower() in {"1", "true", "on", "yes"} else "false"
self.values[key] = text
self.save()
def schema() -> list[dict[str, Any]]:
"""Beschreibung der Felder für die Oberfläche."""
return [
{
"group": group,
"fields": [
{
"key": f.key, "label": f.label, "kind": f.kind,
"help": f.help, "options": list(f.options),
"placeholder": f.placeholder, "default": f.default,
}
for f in FIELDS if f.group == group
],
}
for group in GROUPS
]