Files
Onyscribe-Admin/app/models.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

150 lines
8.1 KiB
Python

"""Datenmodell. Inhalte sind content-addressed (Blob) und versioniert (Version)."""
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, Integer, LargeBinary, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db import Base
def now() -> datetime:
return datetime.now(timezone.utc)
class Tenant(Base):
__tablename__ = "tenants"
id: Mapped[str] = mapped_column(String(64), primary_key=True) # Slug, == TENANT-Env im Container
name: Mapped[str] = mapped_column(String(200))
status: Mapped[str] = mapped_column(String(20), default="active") # active | paused | archived
source: Mapped[str] = mapped_column(String(10), default="git") # TENANT_SOURCE im Container: git | admin
subdomain: Mapped[str] = mapped_column(String(100), default="")
custom_domain: Mapped[str] = mapped_column(String(200), default="")
domain_status: Mapped[str] = mapped_column(String(20), default="none") # none | pending | verified
cpu: Mapped[float] = mapped_column(Float, default=0.5)
memory: Mapped[str] = mapped_column(String(20), default="512M")
current_version: Mapped[int | None] = mapped_column(Integer, nullable=True)
deployed_version: Mapped[int | None] = mapped_column(Integer, nullable=True)
deployed_image: Mapped[str] = mapped_column(String(100), default="")
deployed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
draft: Mapped[dict | None] = mapped_column(JSON, nullable=True) # {"config": {...}, "files": {path: sha}}
preview_sample: Mapped[str | None] = mapped_column(Text, nullable=True)
oidc: Mapped[dict | None] = mapped_column(JSON, nullable=True) # Soll-Zustand des OIDC-Clients + letzter Abgleich
core_pin: Mapped[str | None] = mapped_column(String(100), nullable=True) # fester Core-Tag (nimmt an Rollouts nicht teil)
render_baseline: Mapped[dict | None] = mapped_column(JSON, nullable=True) # {version, core, fp:{…}} Referenz für die Kompatibilitätsprüfung
notes: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
versions: Mapped[list["Version"]] = relationship(back_populates="tenant", order_by="Version.number.desc()")
class Blob(Base):
"""Dateiinhalt, adressiert über SHA-256. Später: große Binärdateien nach S3 auslagern."""
__tablename__ = "blobs"
sha: Mapped[str] = mapped_column(String(64), primary_key=True)
content: Mapped[bytes] = mapped_column(LargeBinary)
size: Mapped[int] = mapped_column(Integer)
is_text: Mapped[bool] = mapped_column(Boolean, default=True)
class Version(Base):
"""Unveränderlicher Snapshot eines Tenants."""
__tablename__ = "versions"
__table_args__ = (UniqueConstraint("tenant_id", "number"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), index=True)
number: Mapped[int] = mapped_column(Integer)
message: Mapped[str] = mapped_column(String(500), default="")
author: Mapped[str] = mapped_column(String(100), default="") # "ruben" | "agent:<run>" | "import"
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
config: Mapped[dict] = mapped_column(JSON) # Inhalt der tenant.json (ohne deploy-Block)
files: Mapped[dict] = mapped_column(JSON) # {pfad: sha}
tenant: Mapped[Tenant] = relationship(back_populates="versions")
class Secret(Base):
"""Verschlüsselter Secret-Wert. tenant_id NULL = geteilt für alle Tenants."""
__tablename__ = "secrets"
__table_args__ = (UniqueConstraint("tenant_id", "key"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True)
key: Mapped[str] = mapped_column(String(100))
value_enc: Mapped[str | None] = mapped_column(Text, nullable=True)
source: Mapped[str] = mapped_column(String(30), default="manual") # manual | sops-import
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now, onupdate=now)
class Setting(Base):
__tablename__ = "settings"
key: Mapped[str] = mapped_column(String(100), primary_key=True)
value: Mapped[str] = mapped_column(Text, default="")
class Deployment(Base):
__tablename__ = "deployments"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
batch_id: Mapped[str] = mapped_column(String(40), default="", index=True)
tenant_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
kind: Mapped[str] = mapped_column(String(20), default="deploy") # deploy | dry-run | offboard
version: Mapped[int | None] = mapped_column(Integer, nullable=True)
image_tag: Mapped[str] = mapped_column(String(100), default="")
status: Mapped[str] = mapped_column(String(20), default="running") # running | success | failed
simulated: Mapped[bool] = mapped_column(Boolean, default=True)
log: Mapped[str] = mapped_column(Text, default="")
actor: Mapped[str] = mapped_column(String(100), default="")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class AuditLog(Base):
__tablename__ = "audit_log"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
ts: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now, index=True)
actor: Mapped[str] = mapped_column(String(100), default="")
action: Mapped[str] = mapped_column(String(60))
tenant_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
detail: Mapped[str] = mapped_column(Text, default="")
class ChatMessage(Base):
__tablename__ = "chat_messages"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), index=True)
role: Mapped[str] = mapped_column(String(20)) # user | assistant
content: Mapped[str] = mapped_column(Text, default="")
steps: Mapped[list | None] = mapped_column(JSON, nullable=True) # Tool-Aufrufe (Anzeige)
proposal: Mapped[dict | None] = mapped_column(JSON, nullable=True) # vorgeschlagene Änderung
proposal_status: Mapped[str] = mapped_column(String(20), default="") # "" | open | applied | rejected
usage: Mapped[dict | None] = mapped_column(JSON, nullable=True) # {"in":…, "out":…, "backend":…, "model":…}
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
class CoreRelease(Base):
"""Vom Core-CI gemeldetes Release (Image-Tag). Wird geprüft, freigegeben und ausgerollt."""
__tablename__ = "core_releases"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
sha: Mapped[str] = mapped_column(String(64), unique=True, index=True)
tag: Mapped[str] = mapped_column(String(100))
ref: Mapped[str] = mapped_column(String(100), default="")
image: Mapped[str] = mapped_column(String(300), default="")
message: Mapped[str] = mapped_column(String(500), default="")
announced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
# new → checked → approved → rolling → active (rejected / failed / superseded)
status: Mapped[str] = mapped_column(String(20), default="new")
compat: Mapped[dict | None] = mapped_column(JSON, nullable=True) # {checked_at, core_sha, tenants:{id:{status,…}}, summary:{…}}
approved_by: Mapped[str] = mapped_column(String(100), default="")
approved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
previous_tag: Mapped[str] = mapped_column(String(100), default="") # aktiver Tag vor dem Rollout (für Rollback)
rollout_batch: Mapped[str] = mapped_column(String(40), default="")