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

4
vendor/platform/core/pdf/__init__.py vendored Normal file
View File

@@ -0,0 +1,4 @@
from core.pdf.context import build_pdf_context
from core.pdf.render import render_pdf
__all__ = ["build_pdf_context", "render_pdf"]

92
vendor/platform/core/pdf/context.py vendored Normal file
View File

@@ -0,0 +1,92 @@
"""Builds the structured data the PDF template renders.
This is the *content* half of the PDF — purely data, identical logic for every
tenant. The *layout* half lives in the tenant's (or core's) protocol.html +
protocol.css. Core guarantees the shape of this dict; templates may ignore any
field they don't use, so adding fields here never breaks an old template.
"""
from __future__ import annotations
import base64
from datetime import datetime
from pathlib import Path
from core.protocol import (
group_section_blocks,
normalize_protocol_text,
parse_address_lines,
parse_protocol_sections,
project_label,
)
def _logo_data_uri(logo_path: Path | None) -> str | None:
if not logo_path or not logo_path.exists():
return None
mime = "image/svg+xml" if logo_path.suffix.lower() == ".svg" else "image/png"
data = base64.b64encode(logo_path.read_bytes()).decode("ascii")
return f"data:{mime};base64,{data}"
def build_pdf_context(
cfg,
title: str,
summary: str,
insurance_type: str = "",
customer_2: str = "",
customer_name: str = "",
customer_address: str = "",
) -> dict:
clean = normalize_protocol_text(summary)
parsed = parse_protocol_sections(clean)
terminart = parsed["terminart"] or insurance_type.strip() or "Kundengespräch"
projekt: list[str] = []
if parsed["projekt"]:
projekt = parsed["projekt"]
else:
if customer_name.strip():
projekt.append(customer_name.strip())
if customer_2.strip():
projekt.append(customer_2.strip())
if not projekt:
projekt = ["Projekt"]
if parsed["kontakt"]:
kontakt = parsed["kontakt"]
elif customer_address.strip():
kontakt = parse_address_lines(customer_address)
else:
kontakt = []
sections = [
{"heading": s["heading"], "blocks": group_section_blocks(s)}
for s in parsed["sections"]
]
project_display = project_label(projekt)
date_de = datetime.now().strftime("%d.%m.%Y")
office = cfg.office
return {
"tenant": {"name": cfg.name, "id": cfg.id},
"colors": cfg.colors,
"logo_data_uri": _logo_data_uri(cfg.logo_path),
"title": title,
"terminart": terminart,
"projekt": projekt,
"projekt_label": cfg.raw.get("pdf_projekt_label", "Projekt"),
"project_display": project_display,
"kontakt": kontakt,
"office": office,
"sections": sections,
"date_de": date_de,
# {year} keeps a copyright line in the footer note current without a
# yearly config edit.
"footer_note": cfg.raw.get(
"pdf_footer_note",
"Internes Arbeitsdokument. Inhalte sind vertraulich und ausschließlich "
"für die genannten Empfänger bestimmt.",
).replace("{year}", str(datetime.now().year)),
}

33
vendor/platform/core/pdf/render.py vendored Normal file
View File

@@ -0,0 +1,33 @@
"""Renders the PDF from an HTML+CSS template via WeasyPrint.
The template + stylesheet are resolved per tenant (tenant override → core
default) in core/config.py. A designer can fully restyle the PDF by editing
those two text files — no Python involved.
"""
from __future__ import annotations
from jinja2 import Environment, FileSystemLoader, select_autoescape
from core.config import CORE_TEMPLATES
def render_pdf(context: dict, cfg) -> bytes:
from weasyprint import CSS, HTML # imported lazily: only needed for PDF
template_path = cfg.pdf_template_path
css_path = cfg.pdf_css_path
env = Environment(
loader=FileSystemLoader([str(template_path.parent), str(CORE_TEMPLATES)]),
autoescape=select_autoescape(["html", "xml"]),
)
template = env.get_template(template_path.name)
html_str = template.render(**context)
# CSS is rendered through Jinja too, so colour tokens can be injected.
css_env = Environment(loader=FileSystemLoader([str(css_path.parent)]))
css_str = css_env.get_template(css_path.name).render(**context)
base_url = str(template_path.parent)
doc = HTML(string=html_str, base_url=base_url)
return doc.write_pdf(stylesheets=[CSS(string=css_str)])