Files
pngx-sync/app/api/master.py
domverse cdc9407ff3
All checks were successful
Deploy / deploy (push) Successful in 33s
Implement master promotion feature
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>
2026-03-25 21:17:01 +01:00

61 lines
2.1 KiB
Python

"""API endpoints for master promotion."""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
router = APIRouter(prefix="/api/master", tags=["master"])
class PromoteRequest(BaseModel):
old_master_name: str
acknowledge_pending: bool = False
acknowledge_errors: bool = False
@router.get("/promote/{replica_id}/preflight")
async def preflight_endpoint(replica_id: int):
"""Dry-run pre-flight check. No state is modified."""
from ..sync.promote import preflight
result = await preflight(replica_id)
return {
"can_promote": result.can_promote,
"error": result.error,
"detail": result.detail,
"pending_entries": result.pending_entries,
"error_entries": result.error_entries,
"ok_entries": result.ok_entries,
"missing_doc_count": result.missing_doc_count,
"replica_doc_count": result.replica_doc_count,
"master_doc_count": result.master_doc_count,
}
@router.post("/promote/{replica_id}")
async def promote_endpoint(replica_id: int, body: PromoteRequest):
"""Execute promotion. Pauses scheduler, runs transaction, resumes scheduler."""
from ..sync.promote import promote
try:
result = await promote(
replica_id=replica_id,
old_master_name=body.old_master_name,
acknowledge_pending=body.acknowledge_pending,
acknowledge_errors=body.acknowledge_errors,
)
except ValueError as exc:
error_code = exc.args[0] if exc.args else "UNKNOWN"
detail_msg = exc.args[1] if len(exc.args) > 1 else str(exc)
status = 409 if error_code == "SYNC_RUNNING" else 422
raise HTTPException(status_code=status, detail={"error": error_code, "detail": detail_msg})
return {
"ok": result.ok,
"new_master": {"url": result.new_master_url, "name": result.new_master_name},
"old_master_replica_id": result.old_master_replica_id,
"sync_map_rebuilt": {
"replicas_affected": result.replicas_affected,
"entries_mapped": result.entries_mapped,
"entries_skipped": result.entries_skipped,
},
}