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

54
app/services/seed.py Normal file
View File

@@ -0,0 +1,54 @@
"""Seed-Export/-Import: Startdaten (importierte Tenants) als Datei im Repo.
So startet eine frische Instanz (z. B. Coolify) mit den aktuellen Tenants, ohne Zugriff auf das alte Git-Repo.
Secrets-WERTE sind nie enthalten – nur die Schlüsselnamen.
"""
from __future__ import annotations
import base64
import gzip
import json
from pathlib import Path
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models import Blob, Secret, Setting, Tenant, Version
COLS = ["id", "name", "status", "source", "subdomain", "custom_domain", "domain_status", "cpu", "memory",
"current_version", "deployed_version", "deployed_image", "preview_sample", "oidc", "notes"]
def export(db: Session, path: Path) -> None:
tenants = db.scalars(select(Tenant).order_by(Tenant.id)).all()
shas = set()
out = {"tenants": [], "versions": [], "secrets": [], "settings": {}}
for t in tenants:
out["tenants"].append({c: getattr(t, c) for c in COLS})
for v in db.scalars(select(Version).where(Version.tenant_id == t.id).order_by(Version.number)):
out["versions"].append({"tenant_id": t.id, "number": v.number, "message": v.message, "author": v.author,
"config": v.config, "files": v.files})
shas |= set(v.files.values())
for s in db.scalars(select(Secret)):
out["secrets"].append({"tenant_id": s.tenant_id, "key": s.key})
out["settings"] = {s.key: s.value for s in db.scalars(select(Setting))}
out["blobs"] = {sha: {"b64": base64.b64encode(db.get(Blob, sha).content).decode(), "text": db.get(Blob, sha).is_text} for sha in shas}
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(gzip.compress(json.dumps(out).encode(), 9))
def load(db: Session, path: Path) -> int:
data = json.loads(gzip.decompress(path.read_bytes()))
for sha, b in data["blobs"].items():
raw = base64.b64decode(b["b64"])
db.add(Blob(sha=sha, content=raw, size=len(raw), is_text=b["text"]))
for t in data["tenants"]:
db.add(Tenant(**t))
db.flush()
for v in data["versions"]:
db.add(Version(**v))
for s in data["secrets"]:
db.add(Secret(tenant_id=s["tenant_id"], key=s["key"], value_enc=None, source="sops-import"))
for k, v in data["settings"].items():
db.add(Setting(key=k, value=v))
return len(data["tenants"])