Skip to content

Commit 81bba9f

Browse files
authored
Merge pull request #321 from Gfermoto/dev
fix(web): safe component status for readiness/status
2 parents adedb71 + c9765f3 commit 81bba9f

4 files changed

Lines changed: 60 additions & 4 deletions

File tree

app/web/routes/ui_status_push_routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from models import ActivityLog, db
1212
from services.cache import cache_get, cache_set
1313
from services.api_json_validation import parse_request_json_dict
14-
from services.component_status_service import build_component_status_payload
14+
from services.component_status_service import build_component_status_payload_safe
1515
from services.readiness_service import build_readiness_payload
1616
from services.feed_service import dispense_feed, get_last_dispense
1717
from services.web_push_service import (
@@ -86,7 +86,7 @@ def component_status():
8686
hit, cached = cache_get("component_status:v1")
8787
if hit:
8888
return cached
89-
payload = build_component_status_payload(db.session)
89+
payload = build_component_status_payload_safe(db.session)
9090
cache_set("component_status:v1", payload, CACHE_STATUS_SEC)
9191
return payload
9292

app/web/services/component_status_service.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import json
6+
import logging
67
import os
78
from datetime import datetime, timedelta, timezone
89

@@ -12,6 +13,8 @@
1213
from services.status_service import check_video_reachable, parse_yolo_status_from_heartbeat
1314
from util import ensure_utc
1415

16+
logger = logging.getLogger(__name__)
17+
1518
_TRIGGER_LABELS = {
1619
"opencv": "OpenCV",
1720
"frigate": "Frigate (MQTT)",
@@ -32,6 +35,30 @@ def _trigger_display(motion_source: str, frigate_parallel: bool) -> str:
3235
return trigger_display
3336

3437

38+
def _fallback_component_status_payload() -> dict[str, str | None]:
39+
"""Минимальный payload, если основная сборка упала (деплой/verify не должны валить воркер)."""
40+
return {
41+
"web": "ok",
42+
"processor": "unknown",
43+
"video": "unknown",
44+
"mqtt": "unknown",
45+
"esphome": "unknown",
46+
"yolo": "unknown",
47+
"motion_source": "unknown",
48+
"trigger_display": "unknown",
49+
"birdnet_url": None,
50+
}
51+
52+
53+
def build_component_status_payload_safe(session) -> dict:
54+
"""Как build_component_status_payload, но без необработанных исключений (readiness / status)."""
55+
try:
56+
return build_component_status_payload(session)
57+
except Exception:
58+
logger.exception("build_component_status_payload failed; using fallback")
59+
return _fallback_component_status_payload()
60+
61+
3562
def build_component_status_payload(session) -> dict:
3663
"""Статусы Video / MQTT / ESPHome / YOLO / процессор для UI."""
3764
last_heartbeat = (

app/web/services/readiness_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy import text
1010

1111
from data_paths import data_dir
12-
from services.component_status_service import build_component_status_payload
12+
from services.component_status_service import build_component_status_payload_safe
1313

1414

1515
def _path_status(path: Path, label: str) -> dict[str, object]:
@@ -44,6 +44,6 @@ def build_readiness_payload(session) -> tuple[dict[str, object], int]:
4444
"ready": ready,
4545
"checked_at": datetime.now(timezone.utc).isoformat(),
4646
"checks": checks,
47-
"components": build_component_status_payload(session),
47+
"components": build_component_status_payload_safe(session),
4848
}
4949
return payload, (200 if ready else 503)

app/web/tests/test_api.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,20 @@ def _boom(*_args, **_kwargs):
672672
assert r.json["checks"]["database"]["status"] == "error"
673673
assert r.json["checks"]["database"]["error"] == "database_unavailable"
674674

675+
def test_readiness_survives_component_status_exception(self, client, monkeypatch):
676+
import services.component_status_service as cs
677+
678+
def _boom(_session):
679+
raise RuntimeError("boom components")
680+
681+
monkeypatch.setattr(cs, "build_component_status_payload", _boom)
682+
r = client.get("/api/ui/readiness")
683+
assert r.status_code == 200
684+
data = r.json
685+
assert data["ready"] is True
686+
assert data["components"]["web"] == "ok"
687+
assert data["components"]["processor"] == "unknown"
688+
675689

676690
class TestStatus:
677691
def test_status_returns_component_status(self, client):
@@ -698,6 +712,21 @@ def test_status_esphome_reflects_feed_source(self, client):
698712
assert r.status_code == 200
699713
assert r.json["esphome"] in ("ok", "error", "not_configured", "not_used")
700714

715+
def test_status_survives_component_status_exception(self, client, monkeypatch):
716+
from services.cache import cache_delete
717+
718+
import services.component_status_service as cs
719+
720+
def _boom(_session):
721+
raise RuntimeError("boom components")
722+
723+
cache_delete("component_status:v1")
724+
monkeypatch.setattr(cs, "build_component_status_payload", _boom)
725+
r = client.get("/api/ui/status")
726+
assert r.status_code == 200
727+
assert r.json["web"] == "ok"
728+
assert r.json["processor"] == "unknown"
729+
701730

702731
class TestSettings:
703732
def test_settings_get_returns_config(self, client):

0 commit comments

Comments
 (0)