All checks were successful
Deploy / deploy (push) Successful in 33s
Allows promoting any replica to master with zero document re-downloads.
The sync_map rebuild uses existing DB data only — pure in-memory join.
Changes:
- app/sync/promote.py: preflight() checks (doc count, sync lock, ack
warnings) and promote() transaction (pause scheduler, rebuild all
sync_maps, create old-master replica, swap settings, resume scheduler)
- app/api/master.py: GET /api/master/promote/{id}/preflight (dry run)
and POST /api/master/promote/{id} (execute)
- app/models.py: add promoted_from_master bool field to Replica
- app/database.py: idempotent ALTER TABLE migration for new column
- app/main.py: register master router
- app/templates/replica_detail.html: "Promote to Master" button +
dialog with pre-flight summary, 3-card stats, ack checkboxes, spinner
- app/ui/routes.py: flash query param on dashboard route
- app/templates/dashboard.html: blue info banner for post-promotion flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
67 lines
2.2 KiB
Python
67 lines
2.2 KiB
Python
from sqlalchemy import event
|
|
from sqlmodel import Session, SQLModel, create_engine
|
|
|
|
from .config import get_config
|
|
|
|
_engine = None
|
|
|
|
|
|
def get_engine():
|
|
global _engine
|
|
if _engine is None:
|
|
config = get_config()
|
|
_engine = create_engine(
|
|
config.database_url,
|
|
connect_args={"check_same_thread": False},
|
|
)
|
|
|
|
# Set PRAGMAs on every new connection (foreign_keys must be per-connection)
|
|
@event.listens_for(_engine, "connect")
|
|
def _set_pragmas(dbapi_conn, _record):
|
|
cursor = dbapi_conn.cursor()
|
|
cursor.execute("PRAGMA journal_mode=WAL")
|
|
cursor.execute("PRAGMA foreign_keys=ON")
|
|
cursor.close()
|
|
|
|
return _engine
|
|
|
|
|
|
def get_session():
|
|
with Session(get_engine()) as session:
|
|
yield session
|
|
|
|
|
|
def create_db_and_tables() -> None:
|
|
from . import models # noqa: ensure model classes are registered
|
|
|
|
SQLModel.metadata.create_all(get_engine())
|
|
engine = get_engine()
|
|
with engine.connect() as conn:
|
|
conn.exec_driver_sql(
|
|
"CREATE INDEX IF NOT EXISTS idx_sync_map_replica ON sync_map(replica_id)"
|
|
)
|
|
conn.exec_driver_sql(
|
|
"CREATE INDEX IF NOT EXISTS idx_sync_map_status ON sync_map(replica_id, status)"
|
|
)
|
|
conn.exec_driver_sql(
|
|
"CREATE VIRTUAL TABLE IF NOT EXISTS logs_fts "
|
|
"USING fts5(message, content=logs, content_rowid=id)"
|
|
)
|
|
conn.exec_driver_sql(
|
|
"CREATE TRIGGER IF NOT EXISTS logs_ai AFTER INSERT ON logs BEGIN "
|
|
"INSERT INTO logs_fts(rowid, message) VALUES (new.id, new.message); END"
|
|
)
|
|
conn.exec_driver_sql(
|
|
"CREATE TRIGGER IF NOT EXISTS logs_ad AFTER DELETE ON logs BEGIN "
|
|
"INSERT INTO logs_fts(logs_fts, rowid, message) "
|
|
"VALUES('delete', old.id, old.message); END"
|
|
)
|
|
# Idempotent column additions for schema evolution (no Alembic)
|
|
try:
|
|
conn.exec_driver_sql(
|
|
"ALTER TABLE replicas ADD COLUMN promoted_from_master INTEGER DEFAULT 0"
|
|
)
|
|
except Exception:
|
|
pass # column already exists
|
|
conn.commit()
|