fix: close unconfirmed displays superseded by confirmed updates
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Successful in 6m29s

This commit is contained in:
渡口浪人
2026-10-18 19:27:35 +08:00
parent dec17aeb1d
commit cb8ac32b74
11 changed files with 129 additions and 26 deletions
+2 -2
View File
@@ -126,7 +126,7 @@ def create_app(settings=None, start_worker=True):
app = FastAPI(
title="PIGWay Frame Flow API",
docs_url=None,
version="0.1.21",
version="0.1.22",
lifespan=lifespan,
responses=error_responses,
)
@@ -1040,7 +1040,7 @@ def create_app(settings=None, start_worker=True):
supports_upload=True,
),
],
version="0.1.21",
version="0.1.22",
mock=settings.mock,
write_enabled=settings.write_enabled,
api_key_configured=bool(settings.api_key)
+46
View File
@@ -1,8 +1,10 @@
"""Durable, read-only recovery progress for writes with uncertain outcomes."""
import json
from datetime import UTC, datetime
from uuid import uuid4
from pigway_frame_flow.errors import ErrorCode
from pigway_frame_flow.storage.database import encode, now_iso
@@ -71,3 +73,47 @@ class RecoveryMonitor:
if row:
row["requires_manual_review"] = row["state"] == "manual_review"
return row
def close_superseded_displays(self):
for op in self.db.all(
"SELECT * FROM operations WHERE action='display' AND write_started=1 "
"AND status IN ('SYNCING','UNKNOWN')"
):
result = json.loads(op["result"] or "{}")
if not result.get("image_id") or json.loads(op["payload"]).get(
"options", {}
).get("restore_previous"):
continue
newer = self.db.one(
"SELECT id,result FROM operations WHERE device_id=? AND action='display' "
"AND status='COMPLETED' AND write_started=1 AND created_at>? "
"AND COALESCE(json_extract(payload,'$.options.restore_previous'),0)=0 "
"AND json_extract(result,'$.image_id') IS NOT NULL "
"ORDER BY created_at DESC LIMIT 1",
(op["device_id"], op["created_at"]),
)
if (
not newer
or json.loads(newer["result"])["image_id"] == result["image_id"]
):
continue
# A later confirmed permanent display ends this attempt, but cannot
# prove whether the earlier image was ever shown. Never replay it.
self.service.update(
op["id"],
"FAILED",
error={
"code": ErrorCode.DISPLAY_NOT_CONFIRMED,
"message": "Superseded by a later confirmed display; earlier display unconfirmed",
"request_id": op["request_id"],
"details": {
"superseded_by_operation_id": newer["id"],
"previous_error": json.loads(op["error"] or "null"),
},
},
)
self.db.history(op, "DISPLAY_NOT_CONFIRMED")
self.db.event(
"operation.failed",
{"operation_id": op["id"], "code": ErrorCode.DISPLAY_NOT_CONFIRMED},
)
+2
View File
@@ -884,6 +884,8 @@ class DisplayService:
except FlowError:
pass
self.recovery.close_superseded_displays()
def expire(self):
if not self.settings.mock and not self.settings.write_enabled:
return
+2 -2
View File
@@ -366,7 +366,7 @@ class SettingsView(BaseModel):
class Health(BaseModel):
status: str
version: str = "0.1.21"
version: str = "0.1.22"
class Readiness(BaseModel):
@@ -397,7 +397,7 @@ class RestoreRequest(StrictModel):
class Version(BaseModel):
product: str = "PIGWay Frame Flow"
version: str = "0.1.21"
version: str = "0.1.22"
api_version: str = "v1"
commit: str = "development"
build_time: str = "unknown"
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="color-scheme" content="light dark"><title>PIGWay Frame Flow</title><link rel="stylesheet" href="/ui/style.css"></head>
<body><aside class="sidebar"><a class="brand" href="/"><span>PIGWay Frame Flow<small>Smart Frame Automation</small></span></a><nav id="nav"></nav><div class="sidebar-bottom"><small>Version 0.1.21</small></div></aside><div class="workspace"><header><div><span class="eyebrow">PIGWay Frame Flow</span><h1 id="page-title"></h1></div><div class="toolbar"><select id="device" aria-label="Device"></select><button id="lang" class="icon-button" aria-label="Change language">EN</button><button id="theme" class="icon-button" aria-label="Change theme">◐</button></div></header><main id="main"></main><footer id="footer"></footer></div><div id="toast" role="status" aria-live="polite"></div><dialog id="modal"><div id="modal-content"></div></dialog><script src="/ui/i18n.js?v=0.1.21"></script><script src="/ui/features.js?v=0.1.21"></script><script src="/ui/extensions.js?v=0.1.21"></script><script src="/ui/app.js?v=0.1.21"></script></body></html>
<body><aside class="sidebar"><a class="brand" href="/"><span>PIGWay Frame Flow<small>Smart Frame Automation</small></span></a><nav id="nav"></nav><div class="sidebar-bottom"><small>Version 0.1.22</small></div></aside><div class="workspace"><header><div><span class="eyebrow">PIGWay Frame Flow</span><h1 id="page-title"></h1></div><div class="toolbar"><select id="device" aria-label="Device"></select><button id="lang" class="icon-button" aria-label="Change language">EN</button><button id="theme" class="icon-button" aria-label="Change theme">◐</button></div></header><main id="main"></main><footer id="footer"></footer></div><div id="toast" role="status" aria-live="polite"></div><dialog id="modal"><div id="modal-content"></div></dialog><script src="/ui/i18n.js?v=0.1.22"></script><script src="/ui/features.js?v=0.1.22"></script><script src="/ui/extensions.js?v=0.1.22"></script><script src="/ui/app.js?v=0.1.22"></script></body></html>