"""Resolve missing upload responses using read-only, exact-content evidence.""" import hashlib import json from datetime import UTC, datetime, timedelta from pigway_frame_flow.errors import ErrorCode from pigway_frame_flow.storage.database import encode, now_iso class UploadReconciliation: def __init__(self, service): self.service = service def prepare(self, operation_id, driver, asset, previous): path = self.service.assets.path(asset["asset_id"]) self.service.update( operation_id, "RENDERING", { "rendered_asset_id": asset["asset_id"], "upload_intent": { "name": asset["name"], "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), "size": path.stat().st_size, "before_ids": [image.image_id for image in driver.images()], "previous_image_id": previous, "started_at": now_iso(), }, }, ) def check(self, op, driver, device): service = self.service result = json.loads(op["result"] or "{}") intent = result.get("upload_intent") if not intent: return None expected = result.get("image_id") if not expected: start = datetime.fromisoformat(intent["started_at"]) candidates = [] for image in driver.images(): if ( image.image_id in intent["before_ids"] or image.name != intent["name"] ): continue if image.size_bytes is not None and image.size_bytes != intent["size"]: continue if not image.created_at: continue try: created = datetime.fromisoformat(image.created_at) age = (created - start).total_seconds() except (ValueError, TypeError): continue if 0 <= age <= service.settings.sync_timeout + 300: candidates.append(image) # Ambiguous matches never release a write lock. if len(candidates) != 1: service.recovery.observe( op, "gallery_match_missing" if not candidates else "gallery_match_ambiguous", ) return None expected = candidates[0].image_id data = driver.download(expected) if ( len(data) != intent["size"] or hashlib.sha256(data).hexdigest() != intent["sha256"] ): service.recovery.observe(op, "content_mismatch") return None result["image_id"] = expected result["upload_confirmed"] = True service.update(op["id"], "UNKNOWN", result, json.loads(op["error"] or "{}")) service.assets.promote(result["rendered_asset_id"]) source = service.db.one( "SELECT payload FROM asset_sources WHERE asset_id=?", (result["rendered_asset_id"],), ) service.db.execute( "INSERT OR REPLACE INTO image_sources VALUES(?,?,?,?,?)", ( op["device_id"], expected, result["rendered_asset_id"], op["id"], source["payload"] if source else encode({"type": "image"}), ), ) if not service.db.one( "SELECT image_id FROM managed_cloud_uploads WHERE operation_id=?", (op["id"],), ): service.cloud_library.register( op["device_id"], expected, result["rendered_asset_id"], op["id"] ) if device.current_content.id == expected and device.current_content.synced: options = json.loads(op["payload"]).get("options", {}) service.db.execute( "UPDATE device_state SET managed_image_id=? WHERE device_id=?", (expected, op["device_id"]), ) if options.get("restore_previous"): expires = datetime.fromisoformat(intent["started_at"]) + timedelta( seconds=options["duration"] ) service.db.execute( "INSERT OR IGNORE INTO overrides VALUES(?,?,?,?,?,?)", ( op["id"], op["device_id"], expected, options["priority"], expires.isoformat(), intent["started_at"], ), ) return expected elapsed = ( datetime.now(UTC) - datetime.fromisoformat(intent["started_at"]) ).total_seconds() if ( result.get("upload_confirmed") and not json.loads(op["payload"]).get("options", {}).get("restore_previous") and elapsed > service.settings.sync_timeout + 300 and device.current_content.id == intent["previous_image_id"] and device.current_content.synced ): # Upload is proven; display is not. Two separated stable observations # close the failed display, without replaying either vendor write. observed = result.get("unchanged_observed_at") if not observed: result["unchanged_observed_at"] = now_iso() service.update( op["id"], "UNKNOWN", result, json.loads(op["error"] or "{}") ) elif ( datetime.now(UTC) - datetime.fromisoformat(observed) ).total_seconds() >= 60: service.update( op["id"], "FAILED", result, { "code": ErrorCode.DISPLAY_NOT_CONFIRMED, "message": "Upload confirmed; frame remained on previous image", "request_id": op["request_id"], "details": {}, }, ) service.db.history(op, "DISPLAY_NOT_CONFIRMED") service.db.event( "operation.failed", {"operation_id": op["id"], "code": ErrorCode.DISPLAY_NOT_CONFIRMED}, ) elif result.get("unchanged_observed_at"): result.pop("unchanged_observed_at") service.update(op["id"], "UNKNOWN", result, json.loads(op["error"] or "{}")) return expected