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:
RubenRWU
2026-10-15 15:30:26 +02:00
commit 7d8c226da2
89 changed files with 13975 additions and 0 deletions

View File

@@ -0,0 +1,928 @@
{
"contract": 1,
"description": "Tenant-Schema des Onyscribe-Cores. Quelle der Wahrheit für Admin-Plattform (Formulare, Validierung, Deploy-Env).",
"ignored_keys": [
"deploy"
],
"fields": [
{
"path": "name",
"type": "string",
"group": "Marke",
"label": "Name",
"help": "Anzeigename des Kunden (Web-UI, E-Mail, PDF-Fallback).",
"default": null,
"since": 1,
"required": true
},
{
"path": "title",
"type": "string",
"group": "Marke",
"label": "Seitentitel (Browser-Tab)",
"help": "Standard: „<Name> – Gesprächsprotokoll“.",
"default": null,
"since": 1
},
{
"path": "description",
"type": "string",
"group": "Marke",
"label": "Beschreibung",
"help": "Meta-Beschreibung der Web-UI. Standard: „Gesprächsprotokoll-Tool für <Name>“.",
"default": null,
"since": 1
},
{
"path": "colors.accent",
"type": "color",
"group": "Farben",
"label": "Akzent",
"help": "",
"default": "#327F24",
"since": 1
},
{
"path": "colors.accent_dark",
"type": "color",
"group": "Farben",
"label": "Akzent dunkel",
"help": "",
"default": "#235A19",
"since": 1
},
{
"path": "colors.accent_mid",
"type": "color",
"group": "Farben",
"label": "Akzent mittel",
"help": "",
"default": "#3E9130",
"since": 1
},
{
"path": "colors.accent_light",
"type": "color",
"group": "Farben",
"label": "Akzent hell",
"help": "",
"default": "#EAF4E7",
"since": 1
},
{
"path": "colors.accent_xlight",
"type": "color",
"group": "Farben",
"label": "Akzent extra hell",
"help": "",
"default": "#F3FAF1",
"since": 1
},
{
"path": "colors.bg",
"type": "color",
"group": "Farben",
"label": "Seitenhintergrund",
"help": "",
"default": "#F1F5F0",
"since": 1
},
{
"path": "colors.surface",
"type": "color",
"group": "Farben",
"label": "Karten",
"help": "",
"default": "#FFFFFF",
"since": 1
},
{
"path": "colors.surface2",
"type": "color",
"group": "Farben",
"label": "Karten 2",
"help": "",
"default": "#F6FAF5",
"since": 1
},
{
"path": "colors.border",
"type": "color",
"group": "Farben",
"label": "Rahmen",
"help": "",
"default": "#DCE8D9",
"since": 1
},
{
"path": "colors.border2",
"type": "color",
"group": "Farben",
"label": "Rahmen 2",
"help": "",
"default": "#BDD1B8",
"since": 1
},
{
"path": "colors.text",
"type": "color",
"group": "Farben",
"label": "Text",
"help": "",
"default": "#444444",
"since": 1
},
{
"path": "colors.text_2",
"type": "color",
"group": "Farben",
"label": "Text sekundär",
"help": "",
"default": "#666666",
"since": 1
},
{
"path": "colors.text_3",
"type": "color",
"group": "Farben",
"label": "Text tertiär",
"help": "",
"default": "#9AA89A",
"since": 1
},
{
"path": "colors.red",
"type": "color",
"group": "Farben",
"label": "Rot (Hinweis)",
"help": "",
"default": "#DC2626",
"since": 1
},
{
"path": "colors.progress_to",
"type": "color",
"group": "Farben",
"label": "Fortschritt (Verlauf-Ende)",
"help": "",
"default": "#8bbf43",
"since": 1
},
{
"path": "office.name",
"type": "string",
"group": "Absender",
"label": "Name",
"help": "Kopf-/Fußzeile von PDF, Word und E-Mail.",
"default": "",
"since": 1
},
{
"path": "office.subtitle",
"type": "string",
"group": "Absender",
"label": "Zusatz (Rechtsform)",
"help": "",
"default": "",
"since": 1
},
{
"path": "office.address",
"type": "text",
"group": "Absender",
"label": "Adresse",
"help": "Mehrzeilig möglich.",
"default": "",
"since": 1
},
{
"path": "office.tel",
"type": "string",
"group": "Absender",
"label": "Telefon",
"help": "",
"default": "",
"since": 1
},
{
"path": "office.email",
"type": "string",
"group": "Absender",
"label": "E-Mail",
"help": "",
"default": "",
"since": 1
},
{
"path": "office.website",
"type": "string",
"group": "Absender",
"label": "Website",
"help": "",
"default": "",
"since": 1
},
{
"path": "pdf_projekt_label",
"type": "string",
"group": "PDF",
"label": "Bezeichnung „Projekt“",
"help": "Beschriftung des Projekt-Feldes im PDF (z. B. „Thema“).",
"default": "Projekt",
"since": 1
},
{
"path": "pdf_footer_note",
"type": "text",
"group": "PDF",
"label": "Fußzeilen-Hinweis",
"help": "Platzhalter {year} = aktuelles Jahr.",
"default": "Internes Arbeitsdokument. Inhalte sind vertraulich und ausschließlich für die genannten Empfänger bestimmt.",
"since": 1
},
{
"path": "ai_detect_org",
"type": "string",
"group": "Prompt-Identität",
"label": "Organisation (voll)",
"help": "Ersetzt @@ORG@@ im Standard-Prompt. Standard: Name.",
"default": null,
"since": 1
},
{
"path": "org_short",
"type": "string",
"group": "Prompt-Identität",
"label": "Kurzname im Protokolltext",
"help": "Ersetzt @@ORG_SHORT@@. Standard: Name.",
"default": null,
"since": 1
},
{
"path": "org_profile",
"type": "text",
"group": "Prompt-Identität",
"label": "Profil (ein Satz)",
"help": "Ersetzt @@ORG_PROFILE@@ – was die Organisation macht.",
"default": "",
"since": 1
},
{
"path": "flags.ui_kontakt_field",
"type": "bool",
"group": "Funktion",
"label": "Feld „Ansprechpartner Kunde“",
"help": "Aus: Feld in der Web-UI ausgeblendet (z. B. bei Vereinen).",
"default": true,
"since": 1
},
{
"path": "flags.multi_audio",
"type": "bool",
"group": "Funktion",
"label": "Mehrere Audiodateien (bis 5)",
"help": "Aus: nur eine Datei pro Protokoll.",
"default": true,
"since": 1
},
{
"path": "stt.provider",
"type": "enum",
"group": "Spracherkennung",
"label": "Provider",
"help": "Env STT_PROVIDER überschreibt. Bei „cortecs“ genügt der geteilte CORTECS_API_KEY.",
"default": "mistral",
"since": 1,
"options": [
"mistral",
"cortecs"
]
},
{
"path": "stt.model",
"type": "string",
"group": "Spracherkennung",
"label": "Modell",
"help": "Env STT_MODEL überschreibt. Standard je Provider.",
"default": null,
"since": 1
},
{
"path": "stt.cost_per_minute",
"type": "number",
"group": "Spracherkennung",
"label": "Kosten pro Minute (EUR)",
"help": "Nur für die Kostenerfassung; Standard je Provider.",
"default": null,
"since": 1,
"advanced": true
}
],
"files": [
{
"path": "prompt.txt",
"kind": "text",
"label": "Prompt",
"default": "defaults/prompt.txt",
"help": "Platzhalter @@ORG@@, @@ORG_SHORT@@, @@ORG_PROFILE@@. Ohne Datei gilt der Standard."
},
{
"path": "protocol.html",
"kind": "text",
"label": "PDF-Layout (HTML/Jinja)",
"default": "templates/protocol.html"
},
{
"path": "protocol.css",
"kind": "text",
"label": "PDF-Styling (CSS/Jinja)",
"default": "templates/protocol.css"
},
{
"path": "overrides.py",
"kind": "code",
"label": "Hook-Code",
"help": "Klasse Hooks(TenantHooks); Hooks siehe „hooks“. Optional HOOK_API = 1."
},
{
"path": "logo.svg",
"kind": "image",
"label": "Logo (SVG)",
"help": "Web-UI und PDF."
},
{
"path": "logo.png",
"kind": "image",
"label": "Logo (PNG)",
"help": "Word-Export braucht ein Rasterbild."
},
{
"path": "favicon.svg",
"kind": "image",
"label": "Favicon (SVG)"
}
],
"extra_files": true,
"hooks": [
{
"name": "build_prompt",
"help": "Prompt vor dem LLM-Aufruf anpassen (base_prompt, ctx)."
},
{
"name": "transform_transcript",
"help": "Transkript nachbearbeiten."
},
{
"name": "pdf_context",
"help": "Daten fürs PDF-Template ändern."
},
{
"name": "extra_routes",
"help": "Zusätzliche FastAPI-Routen registrieren."
}
],
"prompt_placeholders": [
"@@ORG@@",
"@@ORG_SHORT@@",
"@@ORG_PROFILE@@"
],
"env": [
{
"name": "OIDC_ISSUER",
"kind": "setting",
"scope": "shared",
"label": "OIDC-Issuer",
"help": "Login-System der Tenant-Container.",
"default": "https://login.onyva.dev",
"group": "Login",
"setting": "oidc_issuer"
},
{
"name": "AUTH_ENABLED",
"kind": "setting",
"scope": "shared",
"label": "Login aktiv",
"help": "Aus nur für lokale Entwicklung.",
"default": "true",
"group": "Login",
"setting": "auth_enabled",
"type": "bool"
},
{
"name": "APP_COOKIE_SECURE",
"kind": "setting",
"scope": "shared",
"label": "Cookies „secure“",
"help": "Hinter HTTPS immer true.",
"default": "false",
"group": "Login",
"setting": "app_cookie_secure",
"type": "bool"
},
{
"name": "MAX_FILE_SIZE_MB",
"kind": "setting",
"scope": "shared",
"label": "Max. Dateigröße (MB)",
"help": "",
"default": "300",
"group": "Limits",
"setting": "max_file_size_mb",
"type": "int"
},
{
"name": "STT_MAX_AUDIO_MINUTES",
"kind": "setting",
"scope": "shared",
"label": "Max. Audiolänge (Minuten)",
"help": "Grenze von Voxtral Mini Transcribe V2: 180.",
"default": "180",
"group": "Limits",
"setting": "stt_max_audio_minutes",
"type": "int"
},
{
"name": "LLM_MODEL",
"kind": "setting",
"scope": "shared",
"label": "Protokoll-Modell",
"help": "Modell-ID bei Cortecs.",
"default": "claude-4-6-sonnet",
"group": "LLM",
"setting": "llm_model"
},
{
"name": "LLM_MAX_TOKENS",
"kind": "setting",
"scope": "shared",
"label": "Max. Ausgabe-Tokens",
"help": "",
"default": "16000",
"group": "LLM",
"setting": "llm_max_tokens",
"type": "int"
},
{
"name": "LLM_TEMPERATURE",
"kind": "setting",
"scope": "shared",
"label": "Temperatur",
"help": "",
"default": "0.2",
"group": "LLM",
"setting": "llm_temperature",
"type": "float"
},
{
"name": "LLM_MAX_RETRIES",
"kind": "setting",
"scope": "shared",
"label": "Wiederholungen (SDK)",
"help": "",
"default": "4",
"group": "LLM",
"setting": "llm_max_retries",
"type": "int",
"advanced": true
},
{
"name": "LLM_TIMEOUT",
"kind": "setting",
"scope": "shared",
"label": "Timeout (s)",
"help": "",
"default": "600",
"group": "LLM",
"setting": "llm_timeout",
"type": "int",
"advanced": true
},
{
"name": "CORTECS_BASE_URL",
"kind": "setting",
"scope": "shared",
"label": "Cortecs-Basis-URL",
"help": "",
"default": "https://api.cortecs.ai/v1",
"group": "LLM",
"setting": "cortecs_base_url"
},
{
"name": "STT_PROVIDER",
"kind": "setting",
"scope": "shared",
"label": "STT-Provider (global)",
"help": "Überschreibt tenant.json; leer = je Tenant.",
"default": null,
"group": "Spracherkennung",
"setting": "stt_provider",
"advanced": true
},
{
"name": "STT_MODEL",
"kind": "setting",
"scope": "shared",
"label": "STT-Modell (global)",
"help": "Überschreibt tenant.json; leer = je Tenant.",
"default": null,
"group": "Spracherkennung",
"setting": "stt_model",
"advanced": true
},
{
"name": "STT_DIARIZE",
"kind": "setting",
"scope": "shared",
"label": "Sprechererkennung",
"help": "",
"default": "true",
"group": "Spracherkennung",
"setting": "stt_diarize",
"type": "bool"
},
{
"name": "STT_TIMEOUT",
"kind": "setting",
"scope": "shared",
"label": "STT-Timeout (s)",
"help": "Wächst zusätzlich mit der Audiolänge.",
"default": "600",
"group": "Spracherkennung",
"setting": "stt_timeout",
"type": "int"
},
{
"name": "MISTRAL_TRANSCRIBE_MODEL",
"kind": "setting",
"scope": "shared",
"label": "Mistral-Modell (Legacy)",
"help": "Wirkt nur für Tenants mit Provider mistral.",
"default": "voxtral-mini-latest",
"group": "Spracherkennung",
"setting": "mistral_transcribe_model",
"advanced": true
},
{
"name": "MISTRAL_DIARIZE",
"kind": "setting",
"scope": "shared",
"label": "Mistral Diarize (Legacy)",
"help": "",
"default": "true",
"group": "Spracherkennung",
"setting": "mistral_diarize",
"type": "bool",
"advanced": true
},
{
"name": "MISTRAL_TIMEOUT",
"kind": "setting",
"scope": "shared",
"label": "Mistral-Timeout (Legacy)",
"help": "",
"default": "600",
"group": "Spracherkennung",
"setting": "mistral_timeout",
"type": "int",
"advanced": true
},
{
"name": "SMTP_HOST",
"kind": "setting",
"scope": "shared",
"label": "SMTP-Host",
"help": "",
"default": "",
"group": "E-Mail",
"setting": "smtp_host"
},
{
"name": "SMTP_PORT",
"kind": "setting",
"scope": "shared",
"label": "SMTP-Port",
"help": "",
"default": "587",
"group": "E-Mail",
"setting": "smtp_port",
"type": "int"
},
{
"name": "SMTP_USER",
"kind": "setting",
"scope": "shared",
"label": "SMTP-Benutzer",
"help": "",
"default": "",
"group": "E-Mail",
"setting": "smtp_user"
},
{
"name": "SMTP_FROM",
"kind": "setting",
"scope": "shared",
"label": "Absendername",
"help": "",
"default": "",
"group": "E-Mail",
"setting": "smtp_from"
},
{
"name": "COST_API_BASE",
"kind": "setting",
"scope": "shared",
"label": "Cost-API-Basis",
"help": "Standard: <OIDC_ISSUER>/api/cost.",
"default": "",
"group": "Kosten",
"setting": "cost_api_base"
},
{
"name": "COST_TRACKING_ENABLED",
"kind": "setting",
"scope": "shared",
"label": "Kostenerfassung aktiv",
"help": "",
"default": "true",
"group": "Kosten",
"setting": "cost_tracking_enabled",
"type": "bool"
},
{
"name": "COST_LLM_INPUT_PER_MTOK",
"kind": "setting",
"scope": "shared",
"label": "LLM-Preis Eingabe (EUR / Mio. Tokens)",
"help": "",
"default": "2.87",
"group": "Kosten",
"setting": "cost_llm_input_per_mtok",
"type": "float",
"advanced": true
},
{
"name": "COST_LLM_OUTPUT_PER_MTOK",
"kind": "setting",
"scope": "shared",
"label": "LLM-Preis Ausgabe (EUR / Mio. Tokens)",
"help": "",
"default": "14.31",
"group": "Kosten",
"setting": "cost_llm_output_per_mtok",
"type": "float",
"advanced": true
},
{
"name": "COST_STT_PER_MINUTE",
"kind": "setting",
"scope": "shared",
"label": "STT-Preis (EUR / Minute, global)",
"help": "Nicht global setzen, wenn Tenants verschiedene Provider nutzen.",
"default": null,
"group": "Kosten",
"setting": "cost_stt_per_minute",
"type": "float",
"advanced": true
},
{
"name": "COST_FLUSH_INTERVAL",
"kind": "setting",
"scope": "shared",
"label": "Cost-Flush-Intervall (s)",
"help": "",
"default": "5",
"group": "Kosten",
"setting": "cost_flush_interval",
"type": "float",
"advanced": true
},
{
"name": "COST_HTTP_TIMEOUT",
"kind": "setting",
"scope": "shared",
"label": "Cost-HTTP-Timeout (s)",
"help": "",
"default": "5",
"group": "Kosten",
"setting": "cost_http_timeout",
"type": "float",
"advanced": true
},
{
"name": "RETRY_MAX_ATTEMPTS",
"kind": "setting",
"scope": "shared",
"label": "Retry: Versuche",
"help": "",
"default": "4",
"group": "Robustheit",
"setting": "retry_max_attempts",
"type": "int",
"advanced": true
},
{
"name": "RETRY_BASE_DELAY",
"kind": "setting",
"scope": "shared",
"label": "Retry: Basis-Wartezeit (s)",
"help": "",
"default": "1.0",
"group": "Robustheit",
"setting": "retry_base_delay",
"type": "float",
"advanced": true
},
{
"name": "RETRY_MAX_DELAY",
"kind": "setting",
"scope": "shared",
"label": "Retry: maximale Wartezeit (s)",
"help": "",
"default": "30.0",
"group": "Robustheit",
"setting": "retry_max_delay",
"type": "float",
"advanced": true
},
{
"name": "CORTECS_API_KEY",
"kind": "secret",
"scope": "shared",
"label": "Cortecs-API-Key",
"help": "LLM und (bei Provider cortecs) STT.",
"default": null,
"group": "Secrets",
"setting": "cortecs_api_key",
"required": true
},
{
"name": "SMTP_PASSWORD",
"kind": "secret",
"scope": "shared",
"label": "SMTP-Passwort",
"help": "",
"default": null,
"group": "Secrets",
"setting": "smtp_password"
},
{
"name": "MISTRAL_API_KEY",
"kind": "secret",
"scope": "tenant",
"label": "Mistral-API-Key",
"help": "Nur bei STT-Provider mistral.",
"default": null,
"group": "Secrets",
"setting": "mistral_api_key",
"required_if": "stt.provider!=cortecs"
},
{
"name": "OIDC_CLIENT_ID",
"kind": "secret",
"scope": "tenant",
"label": "OIDC-Client-ID",
"help": "Wird vom Admin über die Login-API verwaltet.",
"default": null,
"group": "Secrets",
"setting": "oidc_client_id",
"required": true
},
{
"name": "OIDC_CLIENT_SECRET",
"kind": "secret",
"scope": "tenant",
"label": "OIDC-Client-Secret",
"help": "",
"default": null,
"group": "Secrets",
"setting": "oidc_client_secret",
"required": true
},
{
"name": "SESSION_SECRET",
"kind": "secret",
"scope": "tenant",
"label": "Session-Secret",
"help": "Signiert Session-Cookies.",
"default": null,
"group": "Secrets",
"setting": "session_secret",
"required": true
},
{
"name": "COST_INGEST_KEY",
"kind": "secret",
"scope": "tenant",
"label": "Cost-Ingest-Key",
"help": "Schlüssel für die Kostenerfassung.",
"default": null,
"group": "Secrets",
"setting": "cost_ingest_key",
"required": true
},
{
"name": "TENANT",
"kind": "runtime",
"scope": "tenant",
"label": "Tenant-ID",
"help": "= tenant.id",
"default": null,
"group": "Laufzeit",
"setting": "tenant"
},
{
"name": "DATA_DIR",
"kind": "runtime",
"scope": "tenant",
"label": "Datenverzeichnis",
"help": "Volume des Containers.",
"default": "/data",
"group": "Laufzeit",
"setting": "data_dir"
},
{
"name": "PORT",
"kind": "runtime",
"scope": "tenant",
"label": "Port",
"help": "",
"default": "8276",
"group": "Laufzeit",
"setting": "port"
},
{
"name": "OIDC_REDIRECT_URI",
"kind": "runtime",
"scope": "tenant",
"label": "OIDC-Redirect-URI",
"help": "= https://<host>/auth/callback",
"default": null,
"group": "Laufzeit",
"setting": "oidc_redirect_uri"
},
{
"name": "OIDC_LOGOUT_REDIRECT_URI",
"kind": "runtime",
"scope": "tenant",
"label": "OIDC-Logout-Ziel",
"help": "= https://<host>/",
"default": null,
"group": "Laufzeit",
"setting": "oidc_logout_redirect_uri"
},
{
"name": "TENANT_SOURCE",
"kind": "runtime",
"scope": "tenant",
"label": "Quelle der Tenant-Daten",
"help": "git = ins Image gebacken (Legacy) · admin = Bundle der Admin-Plattform.",
"default": "git",
"group": "Laufzeit",
"setting": "tenant_source"
},
{
"name": "ADMIN_URL",
"kind": "runtime",
"scope": "tenant",
"label": "URL der Admin-Plattform",
"help": "Nur bei TENANT_SOURCE=admin.",
"default": null,
"group": "Laufzeit",
"setting": "admin_url"
},
{
"name": "ADMIN_BUNDLE_TOKEN",
"kind": "runtime",
"scope": "tenant",
"label": "Bundle-Token",
"help": "Pro Tenant; nur bei TENANT_SOURCE=admin.",
"default": null,
"group": "Laufzeit",
"setting": "admin_bundle_token",
"secret": true
},
{
"name": "BUNDLE_CACHE_DIR",
"kind": "runtime",
"scope": "tenant",
"label": "Bundle-Cache",
"help": "Standard: <DATA_DIR>/bundle.",
"default": null,
"group": "Laufzeit",
"setting": "bundle_cache_dir",
"advanced": true
},
{
"name": "BUNDLE_FALLBACK_BAKED",
"kind": "runtime",
"scope": "tenant",
"label": "Rückfall auf eingebackenen Tenant",
"help": "Nur Migrationszeit; danach false.",
"default": "true",
"group": "Laufzeit",
"setting": "bundle_fallback_baked",
"type": "bool",
"advanced": true
}
]
}