Track repair collection cycles and reconcile request availability
Magent CI/CD / verify (push) Successful in 10m47s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 1m49s

This commit is contained in:
2026-09-06 22:06:30 +12:00
parent 8d720de500
commit 009bb35032
11 changed files with 762 additions and 151 deletions
+71 -2
View File
@@ -187,6 +187,16 @@ def _has_secure_bootstrap_admin_credentials() -> bool:
def init_db() -> None:
with _connect() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS request_repairs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id TEXT NOT NULL,
started_at TEXT NOT NULL,
tracking_json TEXT NOT NULL,
completed_at TEXT,
UNIQUE(request_id, started_at)
)
""")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS snapshots (
@@ -706,6 +716,59 @@ def init_db() -> None:
pass
_backfill_auth_providers()
ensure_admin_user()
_backfill_request_repairs()
def start_request_repair(tracking: Dict[str, Any]) -> None:
"""Persist the new collection cycle before a managed file is removed."""
with _connect() as conn:
conn.execute(
"INSERT OR IGNORE INTO request_repairs (request_id, started_at, tracking_json) VALUES (?, ?, ?)",
(str(tracking["requestId"]), tracking["startedAt"], json.dumps(tracking)),
)
def _backfill_request_repairs() -> None:
# Carry existing issue repairs forward once, without depending on the ticket's
# lifetime. Deleting/closing an issue must not restore stale availability.
with _connect() as conn:
rows = conn.execute("""
SELECT a.metadata_json FROM portal_item_activity a
JOIN portal_items p ON p.id = a.item_id
WHERE p.kind = 'issue' AND p.status IN ('in_progress', 'blocked')
AND a.event_type IN ('replacement_started', 'missing_search_started')
AND a.id = (SELECT MAX(b.id) FROM portal_item_activity b
WHERE b.item_id = a.item_id
AND b.event_type IN ('replacement_started', 'missing_search_started'))
""").fetchall()
for (raw,) in rows:
try:
tracking = json.loads(raw or "{}").get("repairTracking")
if isinstance(tracking, dict) and tracking.get("requestId") and tracking.get("startedAt"):
start_request_repair(tracking)
except (TypeError, ValueError):
continue
def get_request_repairs(request_id: str, *, active_only: bool = True) -> list[Dict[str, Any]]:
with _connect() as conn:
rows = conn.execute(
"SELECT id, tracking_json, completed_at FROM request_repairs WHERE request_id = ?"
+ (" AND completed_at IS NULL" if active_only else "") + " ORDER BY id",
(str(request_id),),
).fetchall()
return [{"id": row[0], **json.loads(row[1]), "completedAt": row[2]} for row in rows]
def complete_request_repair(repair_id: int) -> None:
with _connect() as conn:
conn.execute("UPDATE request_repairs SET completed_at = ? WHERE id = ? AND completed_at IS NULL",
(datetime.now(timezone.utc).isoformat(), repair_id))
def active_repair_request_ids() -> set[str]:
with _connect() as conn:
return {row[0] for row in conn.execute("SELECT DISTINCT request_id FROM request_repairs WHERE completed_at IS NULL")}
def save_snapshot(snapshot: Snapshot) -> None:
@@ -826,15 +889,17 @@ def get_request_download_evidence(request_id: str, limit: int = 100) -> Dict[str
described honestly in the UI.
"""
with _connect() as conn:
cycle = conn.execute("SELECT MAX(started_at) FROM request_repairs WHERE request_id = ?",
(str(request_id),)).fetchone()[0]
rows = conn.execute(
"""
SELECT created_at, payload_json
FROM snapshots
WHERE request_id = ?
WHERE request_id = ? AND (? IS NULL OR created_at >= ?)
ORDER BY id DESC
LIMIT ?
""",
(request_id, max(1, min(int(limit or 100), 500))),
(request_id, cycle, cycle, max(1, min(int(limit or 100), 500))),
).fetchall()
for created_at, payload_json in rows:
@@ -842,6 +907,10 @@ def get_request_download_evidence(request_id: str, limit: int = 100) -> Dict[str
payload = json.loads(payload_json)
except (TypeError, ValueError):
continue
# A poll begun before deletion may finish afterwards. Its wall-clock save
# time alone is not evidence that it belongs to the replacement cycle.
if cycle and (payload.get("raw", {}).get("repairCycle") or "") < cycle:
continue
timeline = payload.get("timeline") if isinstance(payload, dict) else None
if not isinstance(timeline, list):
continue