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>
This commit is contained in:
154
vendor/platform/core/config.py
vendored
Normal file
154
vendor/platform/core/config.py
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user