Diagnose-Sichtbarkeit (Plan A):
- Job.step_started_at + last_heartbeat_at, Heartbeat-Task pingt alle 3 s
während laufender Mac-Calls
- Mac-Worker hält per X-Job-Id Header einen Log-Ringbuffer pro Job
(200 Zeilen, 1 h TTL); GET /api/jobs/{id}/log liefert ihn aus
- LXC-Endpoint GET /api/jobs/{id}/diag bündelt Job-Status, Stage-Sekunden,
Heartbeat-Alter und Mac-Worker-Log
- Frontend: Live-Timing im Status-Pill ("läuft seit Xs · letzter Ping vor Ys"),
klappbares Diagnose-Panel mit Mac-Log für FAILED + langlaufende Jobs
Robustheit (Plan B):
- MacClient: einmaliger Retry mit 3 s Backoff bei RemoteProtocolError /
ConnectError / ReadError für summarize/protocol/export
- Mac-Worker /api/preload heizt Ollama vor (keep_alive 30 m, num_predict 1);
Pipeline ruft Preload vor summarize, damit Modell-Reload nicht mitten
in der Generierung passiert (Hauptursache "Server disconnected")
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""Leichte In-Place-Migrationen für SQLite (kein Alembic).
|
|
|
|
Wird beim App-Start nach create_all() ausgeführt. Idempotent.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import text
|
|
from sqlmodel import Session
|
|
|
|
from app.core.db import engine
|
|
|
|
log = logging.getLogger("voice-agent.migrate")
|
|
|
|
|
|
def _existing_columns(conn, table: str) -> set[str]:
|
|
rows = conn.execute(text(f"PRAGMA table_info({table})")).all()
|
|
return {r[1] for r in rows} # row[1] = column name
|
|
|
|
|
|
def run_migrations() -> None:
|
|
with engine.begin() as conn:
|
|
# 1) Job.owner_id Spalte nachrüsten, falls die Tabelle aus alter Version kommt.
|
|
cols = _existing_columns(conn, "job")
|
|
if "owner_id" not in cols:
|
|
log.info("Migration: füge job.owner_id hinzu")
|
|
conn.execute(text("ALTER TABLE job ADD COLUMN owner_id INTEGER"))
|
|
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_job_owner_id ON job(owner_id)"))
|
|
|
|
# 2) Job.profile Spalte nachrüsten (Profil-System).
|
|
cols = _existing_columns(conn, "job")
|
|
if "profile" not in cols:
|
|
log.info("Migration: füge job.profile hinzu (default 'meeting')")
|
|
conn.execute(text("ALTER TABLE job ADD COLUMN profile VARCHAR DEFAULT 'meeting'"))
|
|
|
|
# 3) User.default_profile nachrüsten.
|
|
user_cols = _existing_columns(conn, "user")
|
|
if user_cols and "default_profile" not in user_cols:
|
|
log.info("Migration: füge user.default_profile hinzu (default 'meeting')")
|
|
conn.execute(text("ALTER TABLE user ADD COLUMN default_profile VARCHAR DEFAULT 'meeting'"))
|
|
|
|
# 4) Job.pdf_path nachrüsten (PDF-Export zusätzlich zu DOCX).
|
|
cols = _existing_columns(conn, "job")
|
|
if "pdf_path" not in cols:
|
|
log.info("Migration: füge job.pdf_path hinzu")
|
|
conn.execute(text("ALTER TABLE job ADD COLUMN pdf_path VARCHAR DEFAULT ''"))
|
|
|
|
# 5) Diagnose-Felder: step_started_at, last_heartbeat_at
|
|
cols = _existing_columns(conn, "job")
|
|
if "step_started_at" not in cols:
|
|
log.info("Migration: füge job.step_started_at hinzu")
|
|
conn.execute(text("ALTER TABLE job ADD COLUMN step_started_at DATETIME"))
|
|
if "last_heartbeat_at" not in cols:
|
|
log.info("Migration: füge job.last_heartbeat_at hinzu")
|
|
conn.execute(text("ALTER TABLE job ADD COLUMN last_heartbeat_at DATETIME"))
|
|
|
|
|
|
def assign_orphan_jobs_to(user_id: int) -> int:
|
|
"""Weist alle Jobs ohne owner_id dem angegebenen User zu. Gibt Anzahl zurück."""
|
|
with Session(engine) as session:
|
|
result = session.execute(
|
|
text("UPDATE job SET owner_id = :uid WHERE owner_id IS NULL"),
|
|
{"uid": user_id},
|
|
)
|
|
session.commit()
|
|
n = result.rowcount or 0
|
|
if n:
|
|
log.info("Migration: %d Bestand-Jobs an user_id=%d zugeordnet", n, user_id)
|
|
return n
|