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
+1 -1
View File
@@ -43,7 +43,7 @@ WORKDIR /app
ARG BUILD_COMMIT=development ARG BUILD_COMMIT=development
ARG BUILD_TIME=unknown ARG BUILD_TIME=unknown
ENV BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME ENV BUILD_COMMIT=$BUILD_COMMIT BUILD_TIME=$BUILD_TIME
LABEL org.opencontainers.image.title="PIGWay Frame Flow" org.opencontainers.image.version="0.1.21" org.opencontainers.image.revision=$BUILD_COMMIT LABEL org.opencontainers.image.title="PIGWay Frame Flow" org.opencontainers.image.version="0.1.22" org.opencontainers.image.revision=$BUILD_COMMIT
USER 10333:10333 USER 10333:10333
EXPOSE 18787 EXPOSE 18787
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
+4 -6
View File
@@ -1,9 +1,7 @@
# pigway-frame-flow v0.1.21 # pigway-frame-flow v0.1.22
基础映像改为固定摘要的官方 Python 3.14 Alpine 3.24,并应用系统安全更新,移除 Debian 基础系统遗留告警。保留原字体路径、中文字体、时区和第三方许可。 修复旧显示操作仍在等待同步时被后续任务替代,之后长期停留在结果未知的问题。后续常规显示已确认成功时,将较早的未确认操作收敛为 DISPLAY_NOT_CONFIRMED,保留原始错误和替代操作关联;不重发设备写入,不将旧操作误判为成功。临时覆盖、缺少上传目标、其他设备和删除操作不参与自动收敛。
发布脚本统一从完整 Dockerfile 构建,避免复用旧运行映像继续携带漏洞;版本标签读取项目元数据。移除旧 Debian 离线字体缓存和运行映像覆盖构建方式,同步构建参数与文档。 沿用 v0.1.21 的 Alpine 安全基础镜像和完整构建流程;本版本不沿用此前漏洞库快照的零检出结论。
585 项测试、Ruff、格式和 mypy 通过。AMD64 容器启动/重启、只读接口及中文实际字形和 PNG 渲染验证通过。Trivy 0.76.0 扫描系统包和全部 25 项 Python 运行依赖,各严重级别均未检出已知漏洞;结论限于 2026-10-18 漏洞库快照。 验证:589 项测试、Ruff、格式和 mypy 通过;真实设备结果以部署后的只读回执为准。
生产升级须通过现有挂载身份校验和部署流程完成,发布源码不等同于生产升级。
+10 -10
View File
@@ -5,12 +5,12 @@ b45d35d737f119a70bf40cb1f9626ff80772ed7913cb35f6e9b870d6ada01a15 .github/workfl
d3ad09bdc5dc60c41a774e1cad43f3e3f8ae5c851303ae6ec2aac030392a2939 .github/workflows/verify.yml d3ad09bdc5dc60c41a774e1cad43f3e3f8ae5c851303ae6ec2aac030392a2939 .github/workflows/verify.yml
3931cc314019760a582e0914e2d9c0f3b862a004d0a30d374900edcad5f599e5 .gitignore 3931cc314019760a582e0914e2d9c0f3b862a004d0a30d374900edcad5f599e5 .gitignore
4b4ed4926a350cbeb71436541b4566e4ba7d2494ac95236b88827a9f5dd171c2 CONTRIBUTING.md 4b4ed4926a350cbeb71436541b4566e4ba7d2494ac95236b88827a9f5dd171c2 CONTRIBUTING.md
660c76fccccae8643633d052d0d6733ecf278f2a3016de29e6850b312a365413 Dockerfile e210623dda97c2bcbd57f37d264c912ac11a20ab3bbd6fbf616439edd3a3491a Dockerfile
6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc LICENSE 6eb7728e6e73f9568ca6fd5ad6383d13ffc638f2870fefa2d86f72f39685c0fc LICENSE
250a2bcec248b074786d90e0329874628d70d5a0fe3b4fba47c8c57d4f84dcce NOTICE.md 250a2bcec248b074786d90e0329874628d70d5a0fe3b4fba47c8c57d4f84dcce NOTICE.md
26ad0d9063c0ee843f0f532604b8fd66e8b39fd72d129595ed078677c0d6d13c README.md 26ad0d9063c0ee843f0f532604b8fd66e8b39fd72d129595ed078677c0d6d13c README.md
ee00cb267b38485f1e574d610f0a57b51d4ae575731a18e57d06a7c43dc0a8b0 README.zh-CN.md ee00cb267b38485f1e574d610f0a57b51d4ae575731a18e57d06a7c43dc0a8b0 README.zh-CN.md
304bec983ac8242107b1ec4154522f451a5d33b6a77ed26669926cb3e161f863 RELEASE_NOTES.md 0e6aad6e9c52cfccecef96a9aa55ba8ee64d22e1ac6eace5ea63bba9afa1e90f RELEASE_NOTES.md
6a0c85c8d52a2d4a3b779dce306a93c4efc4dfe3ab3b23e4a6a3bb7c55079910 SECURITY.md 6a0c85c8d52a2d4a3b779dce306a93c4efc4dfe3ab3b23e4a6a3bb7c55079910 SECURITY.md
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 build-cache/wheels/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 build-cache/wheels/.gitkeep
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 config/.gitkeep e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 config/.gitkeep
@@ -33,23 +33,23 @@ edf44436fd044561539c48480c481e66c1a38164f51ba5226cde04c756f7bacb docs/PHOTO_SOU
bfc38b889fa966031e195c3131db1d4a713e0dc35c644e3047c7ed94f2354700 docs/RESEARCH.md bfc38b889fa966031e195c3131db1d4a713e0dc35c644e3047c7ed94f2354700 docs/RESEARCH.md
fac36f986e1a7a331d3669aa8e2013e6c0ea7a99c16a61ba3ff573e836aa87e1 docs/SYNOLOGY_PHOTOS_API.md fac36f986e1a7a331d3669aa8e2013e6c0ea7a99c16a61ba3ff573e836aa87e1 docs/SYNOLOGY_PHOTOS_API.md
ac0481196e593ff9cca2e5133ff41fb3614268e6a10728a0deca3ad3bfb80b34 docs/integrations/TICKTAR.md ac0481196e593ff9cca2e5133ff41fb3614268e6a10728a0deca3ad3bfb80b34 docs/integrations/TICKTAR.md
b191be41aec1dab92ff0d2956d32ec6a325b49786ce9051e5299fe20e2b6b62d docs/openapi.json 99ea92e28bb57d90a754ba894a86603d7b0526f8b4785fcc52ac03747147e41e docs/openapi.json
54bf7061e0626900ce35e19e81efc5c32c8fd6a0438bdad68f494e1ce81616d4 docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md 54bf7061e0626900ce35e19e81efc5c32c8fd6a0438bdad68f494e1ce81616d4 docs/research/KOKONNA_ACCOUNT_CAPABILITIES.md
a07cbe71571c6e2d5a1b94897bd63321c74675c05d15438c4964eef39db0fd76 docs/research/KOKONNA_CONTENT_SUBSCRIPTIONS.md a07cbe71571c6e2d5a1b94897bd63321c74675c05d15438c4964eef39db0fd76 docs/research/KOKONNA_CONTENT_SUBSCRIPTIONS.md
ec53d587b855be84283c50f8fb45be62a4c1caba78b28d436a8260fd171943a2 docs/research/KOKONNA_LOCAL_CONTROL.md ec53d587b855be84283c50f8fb45be62a4c1caba78b28d436a8260fd171943a2 docs/research/KOKONNA_LOCAL_CONTROL.md
2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 docs/research/KOKONNA_WIDGET_CATALOG.json 2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 docs/research/KOKONNA_WIDGET_CATALOG.json
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/__init__.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/api/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/api/__init__.py
06e2e5156383120271ec989b89236bd1baa2d7ba3d6e51b3018de2b23bd39d91 pigway_frame_flow/api/app.py e2e2e8b10b74d51752b26516f74b9ab029585beedef50d2ac24e62a2672b3aed pigway_frame_flow/api/app.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/core/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/core/__init__.py
fbadf6beaf25b9670dcbf125ec9d192188cf9e076ee4d4a0b5decdaf3834417c pigway_frame_flow/core/accounts.py fbadf6beaf25b9670dcbf125ec9d192188cf9e076ee4d4a0b5decdaf3834417c pigway_frame_flow/core/accounts.py
818e8fb29d22c13159a6cb22bcbc6b9e211a8a1301588126eb192ac4ed71441b pigway_frame_flow/core/auth.py 818e8fb29d22c13159a6cb22bcbc6b9e211a8a1301588126eb192ac4ed71441b pigway_frame_flow/core/auth.py
8f2220fa90b0dc2e45df2023dc5ac695efe55361644ef266829553c99151949b pigway_frame_flow/core/registry.py 8f2220fa90b0dc2e45df2023dc5ac695efe55361644ef266829553c99151949b pigway_frame_flow/core/registry.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/display/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/display/__init__.py
81effa34d6c452579cfa65781909f7c5a9e8ddad136e4c47ee0704939851879a pigway_frame_flow/display/cloud_library.py 81effa34d6c452579cfa65781909f7c5a9e8ddad136e4c47ee0704939851879a pigway_frame_flow/display/cloud_library.py
558d2e2111e3dea1917eaa4f81d7c2dac3b47af3eda1cdc9426af4bf9bc570dd pigway_frame_flow/display/recovery.py 4f336b86bea7747ad8d0706f2954cf9a33674139b46c9a9d8f898ee1f036462b pigway_frame_flow/display/recovery.py
e1e4ae3ba321ae7282c5a4e061c52f24d2a6a6e3d4637e69e4b1fbd05004f5f6 pigway_frame_flow/display/retention.py e1e4ae3ba321ae7282c5a4e061c52f24d2a6a6e3d4637e69e4b1fbd05004f5f6 pigway_frame_flow/display/retention.py
1ee9ee8745dc497dadcc20e5e238c78f6c228f7f8a4ed310ced32f9be55b512e pigway_frame_flow/display/service.py 7e511f06442b333af777aa1ed034afaaa2deb0c1254da2cde86d28716b9162a9 pigway_frame_flow/display/service.py
0975b24488be19f24e768c4c76d9f3c0348417c1351c4e410897da00f7be6656 pigway_frame_flow/display/upload_reconciliation.py 0975b24488be19f24e768c4c76d9f3c0348417c1351c4e410897da00f7be6656 pigway_frame_flow/display/upload_reconciliation.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/drivers/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/drivers/__init__.py
9a8386ef368a6df7fd8cb23bcc442c28e784729a5ce4a78911ad86245d5c5794 pigway_frame_flow/drivers/base.py 9a8386ef368a6df7fd8cb23bcc442c28e784729a5ce4a78911ad86245d5c5794 pigway_frame_flow/drivers/base.py
@@ -82,7 +82,7 @@ a518965aa4db9abeb60865c315229c3053c8e9edd26d80f24dc0af0528bad9e6 pigway_frame_f
2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 pigway_frame_flow/integrations/kokonna_official/widget_catalog.json 2a68fe95ba4afc24aec6abd3e6ab24a360eb6bc3005ab0fefd696190915d76c1 pigway_frame_flow/integrations/kokonna_official/widget_catalog.json
aea594fb4e4c9a6e2094d4d7973eb627ac02fb632811b2d1a558a0ee1cd12e77 pigway_frame_flow/integrations/kokonna_official/widgets.py aea594fb4e4c9a6e2094d4d7973eb627ac02fb632811b2d1a558a0ee1cd12e77 pigway_frame_flow/integrations/kokonna_official/widgets.py
1fe53f4bfb407b8db774fe730131ecf1213e16d752a3506c4c56b46365ea14e8 pigway_frame_flow/main.py 1fe53f4bfb407b8db774fe730131ecf1213e16d752a3506c4c56b46365ea14e8 pigway_frame_flow/main.py
0a7a96f84c3c1248eadf80ae412df36c71d308771f9f53512f3fc3c9bc6d5976 pigway_frame_flow/models.py 4befa445620c3f6f2e9ad535946a84d34b063d146ee170500cd0532cf9b0779e pigway_frame_flow/models.py
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/providers/__init__.py e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pigway_frame_flow/providers/__init__.py
b7a6cb8eb40d8dedfbce18777f8fbd433a174f14bcdadf498bd834132aa568fd pigway_frame_flow/providers/cloud_drives.py b7a6cb8eb40d8dedfbce18777f8fbd433a174f14bcdadf498bd834132aa568fd pigway_frame_flow/providers/cloud_drives.py
58b070f0c814d639a4a912d079479d751d459816d59f2199d0be03e4d573ec3b pigway_frame_flow/providers/external.py 58b070f0c814d639a4a912d079479d751d459816d59f2199d0be03e4d573ec3b pigway_frame_flow/providers/external.py
@@ -120,12 +120,12 @@ c43faa235848bec6370d7a3d8a7b3475cbd8eb4a6e494518dfaf160ca8f77bfa pigway_frame_f
d7d55b06ead173b4c23a277fa39800cadfc18f1163c500e8617cfc1f4ee0dea9 pigway_frame_flow/web/extensions.js d7d55b06ead173b4c23a277fa39800cadfc18f1163c500e8617cfc1f4ee0dea9 pigway_frame_flow/web/extensions.js
fe19e508ad60fab0ab4ca61f5d966f8340143e89f1572de00e78b3f1fed8384b pigway_frame_flow/web/features.js fe19e508ad60fab0ab4ca61f5d966f8340143e89f1572de00e78b3f1fed8384b pigway_frame_flow/web/features.js
3c4e3be6321d40547d45724fc3c54af090ebbbc2a41dec717966596577c72c5d pigway_frame_flow/web/i18n.js 3c4e3be6321d40547d45724fc3c54af090ebbbc2a41dec717966596577c72c5d pigway_frame_flow/web/i18n.js
d4518595bc39adfd88d908f98c8c14c02ffbc67a4289003e792b2ceade2838dd pigway_frame_flow/web/index.html 6310ad63f0c245e2fc640380164172c6513578300eaba744e13132210c467dd6 pigway_frame_flow/web/index.html
0609e796b5ca8346405b1bb6e65ac9f88ee163f7a93024d96d54d4e6ab96ba41 pigway_frame_flow/web/style.css 0609e796b5ca8346405b1bb6e65ac9f88ee163f7a93024d96d54d4e6ab96ba41 pigway_frame_flow/web/style.css
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 pigway_frame_flow/web/swagger-LICENSE cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 pigway_frame_flow/web/swagger-LICENSE
050bc415ee7048dcd881682678f720264e7da5e373f7461d7c58c755305255f7 pigway_frame_flow/web/swagger-ui-bundle.js 050bc415ee7048dcd881682678f720264e7da5e373f7461d7c58c755305255f7 pigway_frame_flow/web/swagger-ui-bundle.js
1ac324f7dcd27e4b9386b4bd6421271ec147e922a22c05ba24b11515e9aa6321 pigway_frame_flow/web/swagger-ui.css 1ac324f7dcd27e4b9386b4bd6421271ec147e922a22c05ba24b11515e9aa6321 pigway_frame_flow/web/swagger-ui.css
da96be4eeb3b57f6805ca766919e3899968179f1e7e6a5cb25dbabfd13bc1569 pyproject.toml 92bc0abc4d0a8747bb33d620330d83d4981e20ef084f210f98f084e905d553e7 pyproject.toml
d34ac82e4c3feacd38546aa2f0a4491ab17ddbf3ad8f489375783166d78923f0 requirements.lock d34ac82e4c3feacd38546aa2f0a4491ab17ddbf3ad8f489375783166d78923f0 requirements.lock
ae264ad19391bb2515def85963f39458cfa75a8611ff54e5329106c35bd8cc6e scripts/build-verify.sh ae264ad19391bb2515def85963f39458cfa75a8611ff54e5329106c35bd8cc6e scripts/build-verify.sh
43e8345ac5f29c2a5d1fa751c0e7c963b00b5391817393541e72c52919f0f2b4 scripts/check-compose-remote.py 43e8345ac5f29c2a5d1fa751c0e7c963b00b5391817393541e72c52919f0f2b4 scripts/check-compose-remote.py
@@ -207,7 +207,7 @@ f14bad45534041ff0b7dead21d2952ec9d6c60e9f718ab5393bdf5a47b2a6d3e tests/test_pho
ab773ca6868ec007786e7142d5425663a5c8da35b9aa90837ac8e46b51e51507 tests/test_production_bootstrap.py ab773ca6868ec007786e7142d5425663a5c8da35b9aa90837ac8e46b51e51507 tests/test_production_bootstrap.py
fee152c5f6eaeda712d38cf88a79213f5e885ba323eb15dfa5a12cf4c88028c3 tests/test_production_deploy.py fee152c5f6eaeda712d38cf88a79213f5e885ba323eb15dfa5a12cf4c88028c3 tests/test_production_deploy.py
f0601f5ed89e8a14a4584a9683611aa7510a964f50a642348b8ab6c62df72278 tests/test_publication_security.py f0601f5ed89e8a14a4584a9683611aa7510a964f50a642348b8ab6c62df72278 tests/test_publication_security.py
0ed189d3469097aa7538325d418425e0c7a3ccc8a285bcf4c2ac10658af4b0df tests/test_recovery.py 484e005d5211509aa9209ad487b4623292daa878ddec773fdbc3ed8b5b744bef tests/test_recovery.py
072e8ca2fa68ca73fb078b107ced96ea683652410b286017db883c97608d78f6 tests/test_retention.py 072e8ca2fa68ca73fb078b107ced96ea683652410b286017db883c97608d78f6 tests/test_retention.py
ab2cd30163453fe571b55c30cb26dc047a104a536ec01d2c64aa2bc91996ba11 tests/test_scheduler.py ab2cd30163453fe571b55c30cb26dc047a104a536ec01d2c64aa2bc91996ba11 tests/test_scheduler.py
5f25f608f13f96b245d6963c02a98add679c6e7d70d398ebb5c3c49b38fbeda8 tests/test_server_release.py 5f25f608f13f96b245d6963c02a98add679c6e7d70d398ebb5c3c49b38fbeda8 tests/test_server_release.py
+3 -3
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0", "openapi": "3.1.0",
"info": { "info": {
"title": "PIGWay Frame Flow API", "title": "PIGWay Frame Flow API",
"version": "0.1.21" "version": "0.1.22"
}, },
"paths": { "paths": {
"/api/v1/devices/{device_id}/widgets/selection": { "/api/v1/devices/{device_id}/widgets/selection": {
@@ -15227,7 +15227,7 @@
"version": { "version": {
"type": "string", "type": "string",
"title": "Version", "title": "Version",
"default": "0.1.21" "default": "0.1.22"
} }
}, },
"type": "object", "type": "object",
@@ -17490,7 +17490,7 @@
"version": { "version": {
"type": "string", "type": "string",
"title": "Version", "title": "Version",
"default": "0.1.21" "default": "0.1.22"
}, },
"api_version": { "api_version": {
"type": "string", "type": "string",
+2 -2
View File
@@ -126,7 +126,7 @@ def create_app(settings=None, start_worker=True):
app = FastAPI( app = FastAPI(
title="PIGWay Frame Flow API", title="PIGWay Frame Flow API",
docs_url=None, docs_url=None,
version="0.1.21", version="0.1.22",
lifespan=lifespan, lifespan=lifespan,
responses=error_responses, responses=error_responses,
) )
@@ -1040,7 +1040,7 @@ def create_app(settings=None, start_worker=True):
supports_upload=True, supports_upload=True,
), ),
], ],
version="0.1.21", version="0.1.22",
mock=settings.mock, mock=settings.mock,
write_enabled=settings.write_enabled, write_enabled=settings.write_enabled,
api_key_configured=bool(settings.api_key) api_key_configured=bool(settings.api_key)
+46
View File
@@ -1,8 +1,10 @@
"""Durable, read-only recovery progress for writes with uncertain outcomes.""" """Durable, read-only recovery progress for writes with uncertain outcomes."""
import json
from datetime import UTC, datetime from datetime import UTC, datetime
from uuid import uuid4 from uuid import uuid4
from pigway_frame_flow.errors import ErrorCode
from pigway_frame_flow.storage.database import encode, now_iso from pigway_frame_flow.storage.database import encode, now_iso
@@ -71,3 +73,47 @@ class RecoveryMonitor:
if row: if row:
row["requires_manual_review"] = row["state"] == "manual_review" row["requires_manual_review"] = row["state"] == "manual_review"
return row 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: except FlowError:
pass pass
self.recovery.close_superseded_displays()
def expire(self): def expire(self):
if not self.settings.mock and not self.settings.write_enabled: if not self.settings.mock and not self.settings.write_enabled:
return return
+2 -2
View File
@@ -366,7 +366,7 @@ class SettingsView(BaseModel):
class Health(BaseModel): class Health(BaseModel):
status: str status: str
version: str = "0.1.21" version: str = "0.1.22"
class Readiness(BaseModel): class Readiness(BaseModel):
@@ -397,7 +397,7 @@ class RestoreRequest(StrictModel):
class Version(BaseModel): class Version(BaseModel):
product: str = "PIGWay Frame Flow" product: str = "PIGWay Frame Flow"
version: str = "0.1.21" version: str = "0.1.22"
api_version: str = "v1" api_version: str = "v1"
commit: str = "development" commit: str = "development"
build_time: str = "unknown" build_time: str = "unknown"
+1 -1
View File
@@ -1,3 +1,3 @@
<!doctype html> <!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> <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>
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "pigway-frame-flow" name = "pigway-frame-flow"
version = "0.1.21" version = "0.1.22"
readme = "README.md" readme = "README.md"
license = {file = "LICENSE"} license = {file = "LICENSE"}
requires-python = ">=3.14,<3.15" requires-python = ">=3.14,<3.15"
+57
View File
@@ -1,5 +1,6 @@
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import pytest
from test_display import submit from test_display import submit
from pigway_frame_flow.display.service import DisplayService from pigway_frame_flow.display.service import DisplayService
@@ -107,3 +108,59 @@ def test_malformed_reconciliation_does_not_abort_other_background_work(
assert op["recovery"]["reason"] == "reconciliation_error" assert op["recovery"]["reason"] == "reconciliation_error"
assert "private" not in str(op["recovery"]) assert "private" not in str(op["recovery"])
assert service.blocked("living-room") assert service.blocked("living-room")
@pytest.mark.parametrize("status", ["SYNCING", "UNKNOWN"])
def test_later_confirmed_display_closes_earlier_syncing_attempt_without_replay(
service, status
):
first = submit(service)
service.process_one()
assert service.operation(first)["status"] == "SYNCING"
second = submit(service)
service.process_one()
driver = service.registry.get("living-room")
uploads = driver.client.uploads
previous_error = {"code": "WRITE_RESULT_UNKNOWN", "message": "Refresh unconfirmed"}
if status == "UNKNOWN":
service.update(first, status, error=previous_error)
service.sync()
assert service.operation(second)["status"] == "COMPLETED"
old = service.operation(first)
assert old["status"] == "FAILED"
assert old["error"]["code"] == "DISPLAY_NOT_CONFIRMED"
assert old["error"]["details"]["superseded_by_operation_id"] == second
if status == "UNKNOWN":
assert old["error"]["details"]["previous_error"] == previous_error
assert driver.client.uploads == uploads
assert not service.blocked("living-room")
service.sync()
assert (
len(
service.db.all(
"SELECT * FROM history WHERE operation_id=? AND result='DISPLAY_NOT_CONFIRMED'",
(first,),
)
)
== 1
)
def test_unconfirmed_later_display_does_not_close_old_attempt(service):
first = submit(service)
service.process_one()
second = submit(service)
service.process_one()
service.recovery.close_superseded_displays()
assert service.operation(first)["status"] == "SYNCING"
assert service.operation(second)["status"] == "SYNCING"
def test_confirmed_temporary_override_does_not_close_earlier_display(service):
first = submit(service)
service.process_one()
second = submit(service, 60, True)
service.process_one()
service.sync()
assert service.operation(second)["status"] == "COMPLETED"
assert service.operation(first)["status"] == "SYNCING"