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:
14
scripts/export_seed.py
Normal file
14
scripts/export_seed.py
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Schreibt den aktuellen DB-Stand (Tenants, Versionen, Dateien; ohne Secret-Werte) nach seed/tenants.seed.json.gz."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.db import session # noqa: E402
|
||||
from app.services import seed # noqa: E402
|
||||
|
||||
out = Path(__file__).resolve().parent.parent / "seed" / "tenants.seed.json.gz"
|
||||
with session() as db:
|
||||
seed.export(db, out)
|
||||
print(f"geschrieben: {out} ({out.stat().st_size/1024:.0f} KB)")
|
||||
42
scripts/fake_login_server.py
Normal file
42
scripts/fake_login_server.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lokaler Fake von login.onyva.dev /api/v1 – zum Ausprobieren des OIDC-Abgleichs ohne echtes Login-System.
|
||||
|
||||
python scripts/fake_login_server.py # http://127.0.0.1:8399/api/v1, Key: onyva_testkey, Nutzer: anna@x.de, bob@x.de
|
||||
ONYVA_LOGIN_API_KEY=onyva_testkey OIDC_SYNC_LIVE=true uvicorn app.main:app --port 8300
|
||||
→ Einstellungen → „API-Basis“ = http://127.0.0.1:8399/api/v1
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from tests.fakelogin import FakeLogin # noqa: E402
|
||||
|
||||
fake = FakeLogin()
|
||||
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def _do(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
body = json.loads(self.rfile.read(n)) if n else None
|
||||
status, data, hdrs = fake(self.command, f"http://x{self.path}", {"Authorization": self.headers.get("Authorization", "")}, body)
|
||||
self.send_response(status)
|
||||
for k, v in hdrs.items():
|
||||
self.send_header(k, v)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
out = json.dumps(data).encode() if data is not None else b""
|
||||
self.send_header("Content-Length", str(len(out)))
|
||||
self.end_headers()
|
||||
self.wfile.write(out)
|
||||
print(f"{self.command} {self.path} → {status}", flush=True)
|
||||
|
||||
do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = _do
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Fake-Login auf http://127.0.0.1:8399/api/v1 (Key onyva_testkey)", flush=True)
|
||||
HTTPServer(("127.0.0.1", 8399), H).serve_forever()
|
||||
27
scripts/import_tenants.py
Normal file
27
scripts/import_tenants.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Importiert die Tenants aus ../transkript-platform in die Admin-DB.
|
||||
|
||||
python scripts/import_tenants.py # nur Schlüsselnamen der Secrets
|
||||
python scripts/import_tenants.py --with-secrets # Secrets via `sops -d` entschlüsseln und verschlüsselt speichern
|
||||
python scripts/import_tenants.py --force # neue Version auch bei identischem Inhalt
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.db import init_db, session # noqa: E402
|
||||
from app.services.importer import import_all # noqa: E402
|
||||
from app.settings import LEGACY_PLATFORM # noqa: E402
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--root", default=str(LEGACY_PLATFORM))
|
||||
ap.add_argument("--with-secrets", action="store_true")
|
||||
ap.add_argument("--force", action="store_true")
|
||||
a = ap.parse_args()
|
||||
|
||||
init_db()
|
||||
with session() as db:
|
||||
for line in import_all(db, Path(a.root), a.with_secrets, a.force):
|
||||
print(line)
|
||||
13
scripts/sync_core.sh
Executable file
13
scripts/sync_core.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Aktualisiert den vendored Core-Snapshot (nur für lokale Entwicklung ohne Core-Image) aus der transkript-platform.
|
||||
# Standard: aktueller Stand des Quell-Repos. Im Container/Coolify wird stattdessen FROM <core-image> gebaut (Dockerfile.fromcore).
|
||||
set -euo pipefail
|
||||
SRC="${1:-$(cd "$(dirname "$0")/../.." && pwd)/transkript-platform}"
|
||||
DST="$(cd "$(dirname "$0")/.." && pwd)/vendor/platform"
|
||||
rm -rf "$DST/core" && mkdir -p "$DST"
|
||||
cp -r "$SRC/core" "$DST/core" && cp "$SRC/main.py" "$SRC/requirements.txt" "$DST/"
|
||||
find "$DST" -name __pycache__ -prune -exec rm -rf {} +
|
||||
SHA="$(git -C "$SRC" rev-parse HEAD)"; BRANCH="$(git -C "$SRC" rev-parse --abbrev-ref HEAD)"
|
||||
echo "$SHA $BRANCH" > "$DST/CORE_VERSION"
|
||||
printf '{"sha": "%s", "built_at": "%s"}\n' "$SHA" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$DST/core/BUILD_INFO.json"
|
||||
echo "Core synchronisiert: ${SHA:0:10} ($BRANCH)"
|
||||
39
scripts/verify_import.py
Normal file
39
scripts/verify_import.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Beweis für den Cut-over: Bundle aus der DB == Git-Ordner (Hash-Vergleich, ohne deploy-Block)."""
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from app.db import session # noqa: E402
|
||||
from app.models import Tenant # noqa: E402
|
||||
from app.services import store # noqa: E402
|
||||
from app.settings import LEGACY_PLATFORM # noqa: E402
|
||||
from sqlalchemy import select # noqa: E402
|
||||
|
||||
root = Path(sys.argv[1]) if len(sys.argv) > 1 else LEGACY_PLATFORM
|
||||
bad = 0
|
||||
with session() as db:
|
||||
for t in db.scalars(select(Tenant).order_by(Tenant.id)):
|
||||
tdir = root / "tenants" / t.id
|
||||
cfg, files = store.content(db, t, "current")
|
||||
git_cfg = json.loads((tdir / "tenant.json").read_text(encoding="utf-8"))
|
||||
git_cfg.pop("deploy", None)
|
||||
problems = []
|
||||
if json.dumps(git_cfg, sort_keys=True) != json.dumps(cfg, sort_keys=True):
|
||||
problems.append("tenant.json weicht ab")
|
||||
for path, sha in files.items():
|
||||
p = tdir / path
|
||||
if not p.exists():
|
||||
problems.append(f"{path}: fehlt in Git")
|
||||
elif hashlib.sha256(p.read_bytes()).hexdigest() != sha:
|
||||
problems.append(f"{path}: Hash weicht ab")
|
||||
for p in tdir.rglob("*"):
|
||||
rel = p.relative_to(tdir).as_posix()
|
||||
if p.is_file() and rel not in files and rel not in ("tenant.json", "secrets.enc.yaml") and "__pycache__" not in rel:
|
||||
problems.append(f"{rel}: fehlt in DB")
|
||||
print(("✔" if not problems else "✘"), t.id, "; ".join(problems))
|
||||
bad += bool(problems)
|
||||
sys.exit(1 if bad else 0)
|
||||
Reference in New Issue
Block a user