Files
Onyscribe-Admin/vendor/platform/core/config.py
RubenRWU 7d8c226da2 Onyscribe Admin: Initialer Stand
Admin-Plattform für die Tenants von Onyscribe: Editor mit Live-Vorschau (echter Core in Sandbox), Versionen, KI-Agent (Cortecs),
OIDC-Abgleich über die Login-Developer-API, Deploy-Engine (Mittwald, gesperrt ohne DEPLOY_LIVE, Freigabe pro Tenant),
Core-Vertrag (Schema/Manifest), Core-Releases mit Kompatibilitätsprüfung, Canary-Rollout und Rollback, Zustandsabgleich.
Runbook: docs/RUNBOOK_PARALLELTEST.md

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 15:30:26 +02:00

155 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tenant configuration loading.
One process serves exactly one tenant, selected via the TENANT env var
(default "default"). The tenant folder lives in tenants/<id>/ and contains:
tenant.json branding, office info, feature flags, STT provider (data, not code)
incl. org_short/org_profile for the shared default prompt
logo.svg|png brand logo (web topbar + PDF header)
prompt.txt summary prompt (optional — falls back to core default)
protocol.html PDF layout template (optional — falls back to core default)
protocol.css PDF styling (optional — falls back to core default)
overrides.py code hooks (optional — see core/hooks.py)
Everything that differs per customer is *data* here. The core code never
changes per customer.
"""
from __future__ import annotations
import json
import os
from dataclasses import dataclass, field
from pathlib import Path
from core import bundle
ROOT = Path(__file__).resolve().parent.parent
# TENANTS_DIR (Env) nur für Tests/Entwicklung; Standard: eingebackene Tenants im Image
TENANTS_DIR = Path(os.getenv("TENANTS_DIR") or ROOT / "tenants")
CORE_DEFAULTS = ROOT / "core" / "defaults"
CORE_TEMPLATES = ROOT / "core" / "templates"
# Default theme — used for any colour a tenant does not override.
DEFAULT_COLORS = {
"accent": "#327F24",
"accent_dark": "#235A19",
"accent_mid": "#3E9130",
"accent_light": "#EAF4E7",
"accent_xlight": "#F3FAF1",
"bg": "#F1F5F0",
"surface": "#FFFFFF",
"surface2": "#F6FAF5",
"border": "#DCE8D9",
"border2": "#BDD1B8",
"text": "#444444",
"text_2": "#666666",
"text_3": "#9AA89A",
"red": "#DC2626",
"progress_to": "#8bbf43",
}
@dataclass
class TenantConfig:
id: str
dir: Path
name: str
title: str
description: str
colors: dict
office: dict
flags: dict
ai_detect_org: str
org_short: str = ""
org_profile: str = ""
stt: dict = field(default_factory=dict)
raw: dict = field(default_factory=dict)
# Herkunft der Tenant-Daten (siehe core/bundle.py): {source: git|admin|cache|baked, version, stale, error}
bundle: dict = field(default_factory=dict)
# ── resolved file paths (tenant override → core default) ──
def _pick(self, tenant_name: str, default_path: Path) -> Path:
candidate = self.dir / tenant_name
return candidate if candidate.exists() else default_path
@property
def prompt_path(self) -> Path:
return self._pick("prompt.txt", CORE_DEFAULTS / "prompt.txt")
@property
def pdf_template_path(self) -> Path:
return self._pick("protocol.html", CORE_TEMPLATES / "protocol.html")
@property
def pdf_css_path(self) -> Path:
return self._pick("protocol.css", CORE_TEMPLATES / "protocol.css")
@property
def logo_path(self) -> Path | None:
for name in ("logo.svg", "logo.png"):
p = self.dir / name
if p.exists():
return p
return None
@property
def docx_logo_path(self) -> Path | None:
"""Logo for the Word header — Word needs a raster image, no SVG.
A tenant may ship both (logo.svg for web/PDF, logo.png next to it);
this prefers the raster file where logo_path prefers the vector one.
"""
for name in ("logo.png", "logo.jpg", "logo.jpeg", "logo.svg"):
p = self.dir / name
if p.exists():
return p
return None
@property
def favicon_path(self) -> Path | None:
p = self.dir / "favicon.svg"
return p if p.exists() else None
def flag(self, key: str, default=None):
return self.flags.get(key, default)
def load_tenant(tenant_id: str | None = None) -> TenantConfig:
tenant_id = (tenant_id or os.getenv("TENANT", "default")).strip()
tenant_dir, bundle_info = bundle.resolve(tenant_id, TENANTS_DIR)
config_file = tenant_dir / "tenant.json"
if not config_file.exists():
raise RuntimeError(
f"Tenant '{tenant_id}' nicht gefunden: {config_file} fehlt. "
f"Verfügbar: {[p.name for p in TENANTS_DIR.iterdir() if p.is_dir()] if TENANTS_DIR.exists() else []}"
)
raw = json.loads(config_file.read_text(encoding="utf-8"))
colors = {**DEFAULT_COLORS, **(raw.get("colors") or {})}
name = raw.get("name", tenant_id)
return TenantConfig(
id=tenant_id,
dir=tenant_dir,
name=name,
title=raw.get("title", f"{name} – Gesprächsprotokoll"),
description=raw.get("description", f"Gesprächsprotokoll-Tool für {name}"),
colors=colors,
office=raw.get("office") or {},
flags=raw.get("flags") or {},
ai_detect_org=raw.get("ai_detect_org", name),
# How the tenant refers to itself inside the protocol text ("revier",
# "Onyva") and one optional sentence describing what it does — both are
# substituted into the shared default prompt.
org_short=raw.get("org_short", name),
org_profile=raw.get("org_profile", ""),
stt=raw.get("stt") or {},
raw=raw,
bundle=bundle_info,
)
# Singleton for the running process (TENANT is fixed per instance).
CFG: TenantConfig = load_tenant()