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:
149
app/models.py
Normal file
149
app/models.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""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="")
|
||||
Reference in New Issue
Block a user