feat: improve webhook filtering, worker status startup handling, and timestamp parsing

- Skip half_hour_report events from webhook posts in people_flow
- Handle pre-existing stale worker status files during startup gracefully
- Make store_dwell_alert timestamp parsing robust against invalid/empty values
- Update lessons learned and todo documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 17:05:31 +08:00
parent 9cde462cd1
commit 4e2ca3cff7
8 changed files with 148 additions and 63 deletions

View File

@@ -212,23 +212,25 @@ def _build_summary(ctx: ManageContext) -> dict:
continue
if payload.get("event") == "half_hour_report":
last_report_time = _string_value(payload.get("window_end"))
active_count = _int_value(payload.get("active_customer_count"))
stat = _build_window_stat(payload)
window_stats.append(stat)
longest_dwell_seconds = max(
longest_dwell_seconds,
stat["max_wait_seconds"],
)
queue_level = stat["queue_level"]
over_threshold_count = stat["over_threshold_count"]
under_threshold_count = stat["under_threshold_count"]
status_change = stat["status_change"]
window_stats.sort(
key=lambda item: _parse_timestamp(item["window_end"]),
reverse=True,
)
window_stats.sort(key=lambda item: _sort_timestamp(item["window_end"]), reverse=True)
for stat in window_stats:
if _parse_timestamp(stat["window_end"]) is None:
continue
last_report_time = stat["window_end"]
active_count = stat["active_customer_count"]
queue_level = stat["queue_level"]
over_threshold_count = stat["over_threshold_count"]
under_threshold_count = stat["under_threshold_count"]
status_change = stat["status_change"]
break
headline = "No reports yet"
if last_report_time:
@@ -411,8 +413,20 @@ def _latest_timestamp(*values: str) -> str:
return latest_raw
def _parse_timestamp(value: str) -> datetime:
parsed = datetime.fromisoformat(value)
def _sort_timestamp(value: str) -> tuple[int, datetime]:
parsed = _parse_timestamp(value)
if parsed is None:
return (0, datetime.min.replace(tzinfo=datetime.now().astimezone().tzinfo))
return (1, parsed)
def _parse_timestamp(value: str) -> datetime | None:
if not value.strip():
return None
try:
parsed = datetime.fromisoformat(value)
except ValueError:
return None
if parsed.tzinfo is None:
return parsed.replace(tzinfo=datetime.now().astimezone().tzinfo)
return parsed

View File

@@ -52,7 +52,7 @@ def build_client(project_root: Path):
"over_threshold_count": 2,
"under_threshold_count": 1,
"queue_level": "normal",
"previous_queue_level": null,
"previous_queue_level": None,
"status_change": "initial",
},
}
@@ -149,6 +149,39 @@ def test_get_manage_summary(tmp_path: Path):
)
def test_get_manage_summary_ignores_invalid_report_timestamp(tmp_path: Path):
client, _ = build_client(tmp_path)
events_path = tmp_path / "logs" / "events.jsonl"
with events_path.open("a", encoding="utf-8") as handle:
handle.write(
json.dumps(
{
"event": "half_hour_report",
"camera_id": "store_cam_01",
"window_start": "2026-04-16T10:00:00+08:00",
"window_end": "",
"active_customer_count": 1,
"queue_metrics": {
"queue_level": "normal",
"over_threshold_count": 1,
"under_threshold_count": 0,
"status_change": "unchanged",
},
}
)
+ "\n"
)
response = client.get("/api/manage/summary")
assert response.status_code == 200
assert response.json["last_result_time"] == "2026-04-16T10:00:00+08:00"
assert (
response.json["metrics"]["recent_window_stats"][0]["window_end"]
== "2026-04-16T10:00:00+08:00"
)
def test_get_manage_windows(tmp_path: Path):
client, _ = build_client(tmp_path)