Track repair collection cycles and reconcile request availability
This commit is contained in:
+71
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user