diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..28ef78a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,14 @@
+.git
+.DS_Store
+.venv
+__pycache__/
+*.py[cod]
+.pytest_cache/
+dist/
+build/
+*.egg-info/
+logs/
+*.jsonl
+web/node_modules/
+web/dist/
+managed-portal.textClipping
diff --git a/.gitignore b/.gitignore
index e6ab47b..047f286 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
__pycache__/
*.py[cod]
.DS_Store
+*.textClipping
.pytest_cache/
.venv/
dist/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4b3969a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,29 @@
+FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.12-slim-bookworm
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1 \
+ PYTHONPATH=/app/src \
+ TZ=Asia/Shanghai
+
+WORKDIR /app
+
+RUN sed -i 's|http://deb.debian.org/debian|http://mirrors.aliyun.com/debian|g; s|http://deb.debian.org/debian-security|http://mirrors.aliyun.com/debian-security|g' /etc/apt/sources.list.d/debian.sources && \
+ apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates \
+ ffmpeg \
+ tzdata \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY pyproject.toml README_zh.md /app/
+COPY src /app/src
+COPY config /app/config
+COPY scripts /app/scripts
+
+RUN chmod +x /app/scripts/*.sh && mkdir -p /app/logs
+
+EXPOSE 19080
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+ CMD python3 -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:19080/api/manage/health', timeout=3).read()" || exit 1
+
+CMD ["python3", "-m", "cold_display_guard.manage_api", "--config", "/app/config/example.toml", "--host", "0.0.0.0", "--port", "19080"]
diff --git a/README_zh.md b/README_zh.md
index 5d7522a..983ff4c 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -1,19 +1,22 @@
# 冷藏展示柜食品批次计时报警
-这是一个独立项目,用于单摄像头监控冷藏展示柜和同画面垃圾桶,记录每个展示区域内食品批次的放置时长,并发现 3 小时到期后的违规行为。
+这是一个独立项目,用于单摄像头监控冷藏展示柜和同画面垃圾桶,记录每个展示区域内食品批次的放置时长,并发现超过自定义报警时间后的异常处理行为。
## 已确认业务规则
- 摄像头同时看到展示柜和垃圾桶。
-- 展示柜初始布局为横向 4 列、竖向 2 行。
-- 布局后期可以通过配置调整。
+- 展示柜食品区域支持 1 到 10 个自定义区域。
+- 食品区域使用阿拉伯数字标注:`1`、`2`、`3` ...
+- 垃圾桶 ROI 独立标定,不占用食品区域编号。
- 每个区域可以放多份食品,但这些食品按同一批次计时。
- 同一区域不允许混批,必须清空后才能放入新批次。
- 食品放入区域时记录开始时间。
- 区域清空时记录结束时间。
-- 未满 3 小时清空视为正常消耗。
-- 超过 3 小时清空后必须在确认窗口内看到垃圾桶投放动作。
-- 超过 3 小时的食品拿出后又放回展示柜,触发报警。
+- 未达到报警阈值前清空视为正常消耗。
+- 食品在区域内达到 `max_dwell_seconds` 时先产生 `time_alarm`。
+- 已报警食品从区域移出后,必须在确认窗口内看到垃圾桶投放动作。
+- 如果已报警食品移出后没有丢到垃圾桶里,报警事件升级为 `warning_escalated` 警告事件。
+- 已报警食品拿出后又放回展示柜,触发违规事件。
## 当前实现范围
@@ -23,8 +26,8 @@
{
"ts": "2026-04-27T10:00:00+08:00",
"zone_counts": {
- "r1c1": 3,
- "r1c2": 0
+ "1": 1,
+ "2": 0
},
"trash_deposit": false
}
@@ -33,12 +36,13 @@
程序会输出 JSONL 事件,例如:
- `batch_started`
+- `time_alarm`
- `batch_consumed`
- `batch_pending_disposal`
- `batch_discarded`
+- `warning_escalated`
- `mixed_batch_violation`
- `overdue_return_violation`
-- `missing_disposal_violation`
## 配置
@@ -46,9 +50,25 @@
默认阈值:
-- 最大放置时间:`10800` 秒,也就是 3 小时
+- 时间报警阈值:`10800` 秒,也就是 3 小时;管理页按分钟输入,例如 20 分钟会保存为 `1200` 秒
- 垃圾桶投放确认窗口:`120` 秒
+食品区域配置示例:
+
+```toml
+[layout]
+zone_count = 3
+zone_ids = ["1", "2", "3"]
+
+[[zones]]
+id = "1"
+label = "区域 1"
+polygon = [[0.1, 0.1], [0.3, 0.1], [0.3, 0.3]]
+
+[trash]
+roi = [[0.7, 0.7], [0.9, 0.7], [0.9, 0.9]]
+```
+
## 区域标定
项目现在有正式管理页,前端默认 `23000`,后端默认 `19080`。
@@ -73,10 +93,10 @@ http://127.0.0.1:23000
- 配置 RTSP 地址和阈值
- 从 RTSP 拉取一帧截图
-- 标定 `r1c1` 到 `r2c4` 的 8 个格口
-- 标定垃圾桶区域
+- 设置 1 到 10 个食品区域
+- 标定数字食品区域和垃圾桶 ROI
- 直接保存标定结果到项目配置文件
-- 查看事件汇总和最近 JSONL 事件
+- 查看事件汇总、区域序号、停留时间、报警和警告事件
项目仍保留 `tools/calibrator` 作为轻量单页标定工具,但正式使用建议走 `23000` 管理页。
@@ -131,11 +151,22 @@ frame_width = 640
frame_height = 360
capture_timeout_seconds = 12.0
baseline_frames = 3
-sample_stride_pixels = 8
-occupancy_mean_delta = 24.0
+sample_stride_pixels = 4
+occupancy_mean_delta = 55.0
occupancy_texture_delta = 18.0
+occupancy_dark_luma_threshold = 80.0
+occupancy_dark_fraction = 0.06
+occupancy_texture_dark_fraction = 0.04
+occupancy_bright_luma_threshold = 220.0
+occupancy_bright_reflection_fraction = 0.18
+occupancy_reflection_dark_fraction = 0.10
+occupancy_reflection_bright_dark_ratio = 2.0
+occupancy_confirm_frames = 2
+empty_confirm_frames = 2
trash_motion_delta = 18.0
-trash_motion_cooldown_seconds = 8
+trash_sustained_motion_delta = 8.0
+trash_sustained_motion_frames = 2
+trash_motion_cooldown_seconds = 3
diagnostics_path = "logs/runtime_diagnostics.jsonl"
```
@@ -144,3 +175,10 @@ diagnostics_path = "logs/runtime_diagnostics.jsonl"
```bash
PYTHONPATH=src python3 -m unittest discover -s tests -v
```
+
+前端测试和构建:
+
+```bash
+node --test web/test/zone-state.test.js
+cd web && pnpm build
+```
diff --git a/agent.md b/agent.md
new file mode 100644
index 0000000..26d215e
--- /dev/null
+++ b/agent.md
@@ -0,0 +1,99 @@
+# Cold Display Guard Agent Instructions
+
+## Repository Snapshot
+
+- Root purpose: `cold-display-guard` monitors refrigerated display food batches, tracks dwell time per configured display zone, and records disposal compliance events.
+- Backend: Python 3.11+ package in `src/cold_display_guard`, using only the standard library for application code.
+- Frontend: Vite + vanilla JavaScript management console in `web/`.
+- Data/storage: JSONL runtime outputs under `logs/` by default; configuration is TOML in `config/example.toml`.
+- Runtime services:
+ - Management API: `127.0.0.1:19080`, routes under `/api/manage/*`.
+ - Web console: `127.0.0.1:23000`, Vite proxies `/api` to the management API.
+ - Runtime monitor: RTSP frame sampling through `ffmpeg`, writing events and diagnostics JSONL.
+- Deployment: Docker/Compose files are present; containers mount `config/` and `logs/`, use `Asia/Shanghai`, and prefer China-accessible package/image mirrors.
+
+## Repository Map
+
+- `src/cold_display_guard/engine.py`: pure batch state machine and compliance event generation.
+- `src/cold_display_guard/models.py`: domain dataclasses and observation parsing.
+- `src/cold_display_guard/config.py`: TOML loading, saving, calibration merge, and path resolution.
+- `src/cold_display_guard/manage_api.py`: standard-library HTTP management API and RTSP snapshot capture.
+- `src/cold_display_guard/main.py`: long-running RTSP monitor that connects frame capture, vision detection, engine, and JSONL sinks.
+- `src/cold_display_guard/frame_source.py`: `ffmpeg` raw RGB frame capture.
+- `src/cold_display_guard/vision.py`: heuristic ROI occupancy and trash-motion detection.
+- `src/cold_display_guard/cli.py`: JSONL observation CLI for deterministic engine processing.
+- `web/src/main.js` and `web/src/styles.css`: management console UI.
+- `scripts/`: local launch scripts for API, web, and runtime services.
+- `deploy/`, `Dockerfile`, `web/Dockerfile`: container deployment artifacts.
+- `tests/`: unittest coverage for engine, CLI, config, management summary/config behavior, and vision heuristics.
+- `docs/plans/`, `task_plan.md`, `progress.md`, `findings.md`: existing planning and project-history artifacts.
+
+## Core Domain Rules
+
+- The reliable business unit is a display-zone batch, not an individual food item.
+- Default layout is 2 rows by 4 columns with zone IDs `r1c1` through `r2c4`; layout and polygons are configurable.
+- A batch starts when a zone changes from empty to occupied.
+- A batch ends when a zone changes from occupied to empty.
+- Count decreases keep the same batch active and emit `batch_count_changed`.
+- Count increases before the zone clears are mixed-batch violations and emit `mixed_batch_violation`.
+- Removal before `max_dwell_seconds` emits `batch_consumed`.
+- Removal at or after `max_dwell_seconds` emits `batch_pending_disposal` and waits for trash confirmation.
+- A trash deposit within `trash_confirmation_seconds` emits `batch_discarded`.
+- No trash deposit before the deadline emits `missing_disposal_violation`.
+- Any new occupied zone while an overdue batch is pending disposal emits `overdue_return_violation`.
+- The current vision layer reports binary `0/1` occupancy per zone; it does not count individual items.
+- The detector learns an empty baseline from the first configured frames. If food is already present at startup, it may become baseline until the image changes.
+
+## Change Rules
+
+- Keep `BatchEngine` deterministic and free of camera, file, HTTP, subprocess, or wall-clock dependencies.
+- Add or update focused tests when changing business rules, event names, event payloads, observation parsing, config formatting, or path resolution.
+- Keep the observation contract stable: `ts`, `zone_counts`, and `trash_deposit` or `trash_deposit_count`.
+- If event names or payload shapes change, update engine tests, CLI tests, runtime code, management summary behavior, frontend rendering, and README examples together.
+- Keep ROI and polygon coordinates normalized to `0.0..1.0`; clamp or validate inputs at config/API boundaries.
+- Keep `manage_api.py` as a small standard-library HTTP service unless the user explicitly asks to introduce a web framework.
+- Preserve explicit `ffmpeg` timeout and error reporting behavior in `frame_source.py` and snapshot capture.
+- Treat RTSP URLs, camera credentials, captured frames, and logs as sensitive operational data. Do not paste secrets into new docs, commits, or test fixtures.
+- Do not commit generated runtime data such as `logs/`, captured snapshots, Vite `dist/`, Python caches, or ad hoc diagnostics.
+- Frontend changes should preserve the current Vite single-page app, `/api/manage/*` backend contract, and 23000/19080 local development split.
+- Deployment changes must keep README commands, scripts, ports, env vars, compose volumes, Docker health checks, and config paths aligned.
+- Be careful with mirror settings in Dockerfiles; they are intentional for the expected deployment network.
+
+## Local Commands
+
+- Full Python test suite:
+ - `PYTHONPATH=src python3 -m unittest discover -s tests -v`
+- Targeted Python tests:
+ - `PYTHONPATH=src python3 -m unittest tests/test_engine.py -v`
+ - `PYTHONPATH=src python3 -m unittest tests/test_config.py -v`
+ - `PYTHONPATH=src python3 -m unittest tests/test_manage_api.py -v`
+ - `PYTHONPATH=src python3 -m unittest tests/test_vision.py -v`
+ - `PYTHONPATH=src python3 -m unittest tests/test_cli.py -v`
+- Management API:
+ - `scripts/run_manage_api.sh`
+ - Health check: `curl http://127.0.0.1:19080/api/manage/health`
+- Web console:
+ - `scripts/run_web.sh`
+ - Build check: `cd web && pnpm build`
+- Runtime monitor:
+ - `scripts/run_runtime.sh`
+ - One-frame smoke test when RTSP and `ffmpeg` are available: `PYTHONPATH=src python3 -m cold_display_guard.main --config config/example.toml --once`
+- Compose config check:
+ - `docker compose --env-file deploy/cold-display-guard.env -f deploy/docker-compose.yml config`
+
+## Validation Matrix
+
+- Engine or domain behavior: run the targeted engine/CLI tests first, then the full Python test suite.
+- Config or calibration behavior: run `tests/test_config.py`, `tests/test_manage_api.py`, then the full Python test suite.
+- Vision or RTSP capture behavior: run `tests/test_vision.py`; use the one-frame runtime smoke test only when camera access and `ffmpeg` are available.
+- Management API changes: run management API tests and, when practical, start `scripts/run_manage_api.sh` and hit `/api/manage/health`.
+- Frontend changes: run `cd web && pnpm build`; if API interactions changed, also run or inspect the management API route behavior.
+- Deployment changes: run the compose config check and verify Dockerfile/package mirror choices, ports, volumes, and health checks.
+- Documentation-only changes: verify the documented paths, commands, ports, and business rules against the current files before reporting completion.
+
+## Workflow
+
+- Read the relevant source and tests before editing; this project has tight coupling between business rules, event payloads, README examples, and UI summaries.
+- Prefer small, surgical changes that preserve the current architecture.
+- For non-trivial work, update or add planning notes in the existing project style (`docs/plans/`, `task_plan.md`, `progress.md`, or `findings.md`) only when useful for handoff or explicitly requested.
+- Keep the final response grounded in verification evidence: say exactly which commands were run, or say when a validation step was skipped because it requires RTSP, Docker, network, or another external dependency.
diff --git a/config/example.toml b/config/example.toml
index b6146d5..c38cfb5 100644
--- a/config/example.toml
+++ b/config/example.toml
@@ -1,52 +1,56 @@
-camera_id = "cold_display_cam_01"
+camera_id = "1"
timezone = "Asia/Shanghai"
[stream]
-rtsp_url = "rtsp://admin:Zxjp2026@192.168.8.9:554/h264/ch1/main/av_stream"
+rtsp_url = ""
[thresholds]
-max_dwell_seconds = 10800
+max_dwell_seconds = 1200
trash_confirmation_seconds = 120
[layout]
-rows = 2
-cols = 4
-zone_ids = ["r1c1", "r1c2", "r1c3", "r1c4", "r2c1", "r2c2", "r2c3", "r2c4"]
+zone_count = 4
+zone_ids = ["1", "2", "3", "4"]
[[zones]]
-id = "r1c1"
-polygon = [[0.441053, 0.344678], [0.475789, 0.372749], [0.453684, 0.455088], [0.404211, 0.428889]]
+id = "1"
+label = "区域 1"
+polygon = [[0.241988, 0.289459], [0.323741, 0.306900], [0.319817, 0.438286], [0.256377, 0.420845]]
[[zones]]
-id = "r1c2"
-polygon = [[0.486316, 0.367135], [0.520000, 0.397076], [0.503158, 0.468187], [0.467368, 0.451345]]
+id = "2"
+label = "区域 2"
+polygon = [[0.354480, 0.320852], [0.423152, 0.330154], [0.419228, 0.470842], [0.378025, 0.454564], [0.357096, 0.446425]]
[[zones]]
-id = "r1c3"
+id = "3"
+label = "区域 3"
polygon = [[0.545263, 0.400819], [0.587368, 0.417661], [0.554737, 0.500000], [0.509474, 0.483158]]
[[zones]]
-id = "r1c4"
+id = "4"
+label = "区域 4"
polygon = [[0.581255, 0.408928], [0.717971, 0.468544], [0.711092, 0.574018], [0.556320, 0.500645]]
-[[zones]]
-id = "r2c1"
-polygon = [[0.396842, 0.475673], [0.487368, 0.543041], [0.472632, 0.612281], [0.373684, 0.584211]]
-
-[[zones]]
-id = "r2c2"
-polygon = [[0.502105, 0.528070], [0.535789, 0.546784], [0.516842, 0.660936], [0.477895, 0.632865]]
-
-[[zones]]
-id = "r2c3"
-polygon = [[0.555789, 0.552398], [0.602105, 0.569240], [0.580000, 0.657193], [0.535789, 0.645965]]
-
-[[zones]]
-id = "r2c4"
-polygon = [[0.602105, 0.567368], [0.700000, 0.606667], [0.689474, 0.722690], [0.581053, 0.683392]]
-
[trash]
roi = [[0.776842, 0.486901], [0.896842, 0.522456], [0.841053, 0.857427], [0.716842, 0.853684]]
+[runtime]
+sample_stride_pixels = 4
+occupancy_mean_delta = 55.0
+occupancy_dark_luma_threshold = 80.0
+occupancy_dark_fraction = 0.06
+occupancy_texture_dark_fraction = 0.04
+occupancy_bright_luma_threshold = 220.0
+occupancy_bright_reflection_fraction = 0.18
+occupancy_reflection_dark_fraction = 0.10
+occupancy_reflection_bright_dark_ratio = 2.0
+occupancy_confirm_frames = 2
+empty_confirm_frames = 2
+trash_motion_delta = 18.0
+trash_sustained_motion_delta = 8.0
+trash_sustained_motion_frames = 2
+trash_motion_cooldown_seconds = 3
+
[event_sink]
path = "logs/events.jsonl"
diff --git a/deploy/cold-display-guard.env b/deploy/cold-display-guard.env
new file mode 100644
index 0000000..1f9a567
--- /dev/null
+++ b/deploy/cold-display-guard.env
@@ -0,0 +1,7 @@
+IMAGE_VERSION=dev
+TZ=Asia/Shanghai
+
+COLD_DISPLAY_GUARD_API_PORT=19080
+COLD_DISPLAY_GUARD_WEB_PORT=23000
+COLD_DISPLAY_GUARD_CONFIG_DIR=../config
+COLD_DISPLAY_GUARD_LOG_DIR=../logs
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
new file mode 100644
index 0000000..75cf6ef
--- /dev/null
+++ b/deploy/docker-compose.yml
@@ -0,0 +1,71 @@
+name: cold-display-guard
+
+services:
+ cold-display-guard-api:
+ build:
+ context: ..
+ dockerfile: Dockerfile
+ image: cold-display-guard:${IMAGE_VERSION:-dev}
+ container_name: cold-display-guard-api
+ restart: unless-stopped
+ environment:
+ TZ: ${TZ:-Asia/Shanghai}
+ command:
+ - python3
+ - -m
+ - cold_display_guard.manage_api
+ - --config
+ - /app/config/example.toml
+ - --host
+ - 0.0.0.0
+ - --port
+ - "19080"
+ ports:
+ - "${COLD_DISPLAY_GUARD_API_PORT:-19080}:19080"
+ volumes:
+ - ${COLD_DISPLAY_GUARD_CONFIG_DIR:-../config}:/app/config
+ - ${COLD_DISPLAY_GUARD_LOG_DIR:-../logs}:/app/logs
+ networks:
+ - cold-display-guard
+
+ cold-display-guard-runtime:
+ image: cold-display-guard:${IMAGE_VERSION:-dev}
+ container_name: cold-display-guard-runtime
+ restart: unless-stopped
+ healthcheck:
+ disable: true
+ depends_on:
+ cold-display-guard-api:
+ condition: service_started
+ environment:
+ TZ: ${TZ:-Asia/Shanghai}
+ command:
+ - python3
+ - -m
+ - cold_display_guard.main
+ - --config
+ - /app/config/example.toml
+ volumes:
+ - ${COLD_DISPLAY_GUARD_CONFIG_DIR:-../config}:/app/config
+ - ${COLD_DISPLAY_GUARD_LOG_DIR:-../logs}:/app/logs
+ networks:
+ - cold-display-guard
+
+ cold-display-guard-web:
+ build:
+ context: ../web
+ dockerfile: Dockerfile
+ image: cold-display-guard-web:${IMAGE_VERSION:-dev}
+ container_name: cold-display-guard-web
+ restart: unless-stopped
+ depends_on:
+ cold-display-guard-api:
+ condition: service_started
+ ports:
+ - "${COLD_DISPLAY_GUARD_WEB_PORT:-23000}:80"
+ networks:
+ - cold-display-guard
+
+networks:
+ cold-display-guard:
+ driver: bridge
diff --git a/docs/project.md b/docs/project.md
new file mode 100644
index 0000000..eec1f71
--- /dev/null
+++ b/docs/project.md
@@ -0,0 +1,84 @@
+# Cold Display Guard Project Documentation
+
+## Goal
+
+`cold-display-guard` monitors refrigerated display food batches by camera region. It tracks how long each configured food region remains occupied, raises a configurable time alarm, and escalates alarmed food to a warning if it is removed without a matching trash-bin deposit.
+
+The `v1.1 优化改造` batch upgrades the product from a fixed 8-zone layout to a configurable 1-10 zone workflow with numeric region labels and editable trash ROI calibration. All v1.1 items are part of one batch; backend, API, frontend, and documentation are implementation workstreams inside that same batch.
+
+## Architecture
+
+- Backend package: `src/cold_display_guard/`
+ - `models.py`: settings, observations, and batch dataclasses.
+ - `engine.py`: deterministic batch state machine.
+ - `config.py`: TOML config load/save, calibration merge, and project path resolution.
+ - `manage_api.py`: standard-library HTTP management API.
+ - `main.py`: RTSP runtime loop connecting frame capture, vision, state engine, and JSONL sinks.
+ - `vision.py`: heuristic ROI occupancy and trash-motion detection.
+- Frontend package: `web/`
+ - Vite + vanilla JavaScript management console.
+ - Default web port `23000`.
+ - API proxy target `http://127.0.0.1:19080`.
+ - Runtime home view falls back to clearly marked demo data when no real events exist, so the operational layout still shows summary cards, dwell timers, and event rows.
+- Runtime data:
+ - Events JSONL default path `logs/events.jsonl`.
+ - Diagnostics JSONL default path `logs/runtime_diagnostics.jsonl`.
+- Deployment:
+ - Root Python Docker image for API/runtime.
+ - `web/Dockerfile` for static web console.
+ - `deploy/docker-compose.yml` wires API, runtime, and web services.
+
+## Configuration
+
+- Main config path: `config/example.toml`.
+- Camera identity: `camera_id`.
+- Timezone default: `Asia/Shanghai`.
+- RTSP input: `[stream] rtsp_url`.
+- Thresholds:
+ - `max_dwell_seconds`: v1.1 time-alarm threshold. Default can remain 10800 seconds; users can set values such as 1200 seconds for 20 minutes.
+ - `trash_confirmation_seconds`: window after an alarmed batch is removed where a trash deposit must be observed before warning escalation.
+- Food zones:
+ - v1.1 food zone IDs are numeric strings from `"1"` through `"10"`.
+ - The configured zone count must be between 1 and 10.
+ - If both `zone_count` and numeric `zone_ids` are present, they must agree.
+ - Each `[[zones]]` polygon must have at least 3 normalized points.
+- Trash ROI:
+ - Stored under `[trash] roi`.
+ - Does not use a food zone number.
+
+## Event Model
+
+- `batch_started`: a food region changes from empty to occupied.
+- `time_alarm`: an active batch reaches `max_dwell_seconds` while still in the display region.
+- `batch_count_changed`: count decreases while the region remains occupied.
+- `mixed_batch_violation`: count increases before the region clears.
+- `batch_consumed`: a non-alarmed batch clears before the threshold.
+- `batch_pending_disposal`: an alarmed batch clears and waits for trash confirmation.
+- `batch_discarded`: a pending alarmed batch is matched to a trash deposit.
+- `warning_escalated`: a pending alarmed batch is not matched to a trash deposit before the confirmation deadline.
+
+Events should include `zone_id`, `zone_index`, `zone_label`, `started_at`, `dwell_seconds`, and relevant alarm/removal/deadline timestamps when available.
+
+## Runbook
+
+- Python tests:
+ - `PYTHONPATH=src python3 -m unittest discover -s tests -v`
+- Management API:
+ - `scripts/run_manage_api.sh`
+ - Health: `curl http://127.0.0.1:19080/api/manage/health`
+- Web console:
+ - `scripts/run_web.sh`
+ - Build: `cd web && pnpm build`
+ - Frontend logic tests: `node --test web/test/zone-state.test.js`
+ - Docker web URL: `http://127.0.0.1:23000`
+- Runtime monitor:
+ - `scripts/run_runtime.sh`
+ - One-frame smoke test when camera and `ffmpeg` are available:
+ - `PYTHONPATH=src python3 -m cold_display_guard.main --config config/example.toml --once`
+
+## Known Risks
+
+- The current vision detector is heuristic and reports binary occupancy, not item counts.
+- If food is already present during baseline collection, those regions may be treated as empty baseline until visual changes occur.
+- Changing calibration while the runtime process has active batches can create operational ambiguity; v1.1 should document or enforce a pause/restart expectation.
+- Historical events must keep the zone index at the time of emission so later region reordering does not reinterpret old logs.
diff --git a/findings.md b/findings.md
index 5cdacdd..6901ad9 100644
--- a/findings.md
+++ b/findings.md
@@ -14,3 +14,84 @@
- If a batch is removed after the maximum dwell threshold, the system expects a trash-bin deposit event within a configurable window.
- If a removed over-threshold batch reappears in any display zone before being discarded, that is a violation.
- If food is added while a zone is already occupied, that is a mixed-batch violation.
+
+## v1.1 优化改造 Findings
+
+## Architecture
+
+- The current backend already accepts configured `layout.zone_ids`, so the engine does not require a fixed 2x4 grid internally.
+- The current frontend is the main fixed-grid constraint: `web/src/main.js` hard-codes `r1c1` through `r2c4` and draws those region controls.
+- `merge_calibration()` already accepts arbitrary zone IDs and clamps polygon points, but it does not enforce the new 1-10 numeric region policy.
+- The runtime vision layer consumes `[[zones]]` from config, so it can follow numeric zones once config and frontend write them.
+- `BatchEngine` only emits events when a zone changes or pending disposal expires; a time alarm while the batch remains occupied requires a new periodic active-batch check.
+
+## Constraints
+
+- Food regions must be numbered `1` through `N`, with `N` between 1 and 10.
+- Trash ROI is a separate region under `[trash]` and must not consume a food zone number.
+- The management API should preserve standard-library HTTP behavior.
+- Existing JSONL consumers may still expect `event`, `zone_id`, `dwell_seconds`, and timestamps; v1.1 should add fields rather than remove core fields.
+- The frontend remains Vite + vanilla JavaScript; no framework migration in this batch.
+
+## Decisions
+
+- Keep `max_dwell_seconds` as the configurable time-alarm threshold to avoid introducing two competing dwell thresholds.
+- Add `time_alarm` when an active batch reaches the threshold, while keeping the batch active until the zone clears.
+- Once an alarmed batch clears, put it into pending trash confirmation and emit `batch_pending_disposal`.
+- If no trash deposit occurs before the confirmation deadline, emit `warning_escalated` with severity `warning`; retain compatibility by using the same pending-disposal mechanism.
+- Emit `zone_index` and `zone_label` on every zone event when the zone ID is numeric.
+- The backend planning agent suggested `batch_dwell_alert` and `missing_disposal_warning`, but the accepted prototype and user phrasing use `time_alarm` and `warning_escalated`; implementation should follow the accepted prototype names.
+- Every future subagent dispatch must begin with the standard context header requested by the user:
+
+```text
+[项目: /Users/yoilun/Code/cold_display_guard]
+[工作流批次: v1.1 优化改造]
+[阶段: 阶段 x]
+[角色: 对应智能体角色]
+```
+
+Use `阶段 x` only as a workflow stage inside the same `v1.1 优化改造` batch.
+
+## Backend Planning Notes
+
+- `EngineSettings.zone_ids` should remain config driven; numeric zones are preferred for new configs, but old `r1c1` style IDs should continue loading.
+- v1.1 can derive `zone_index` from numeric `zone_id` when possible.
+- Suggested batch additions:
+ - `alerted_at`
+ - an indicator that the time alarm has already been emitted
+- Suggested state flow:
+ - `active`: region occupied before threshold.
+ - `alerted`: region occupied after `time_alarm`.
+ - `pending_disposal`: alarmed food removed and waiting for trash deposit.
+ - `discarded`: pending batch matched to trash.
+ - `warning`: alarmed food removed and no trash deposit before deadline.
+ - `consumed`: non-alarmed food removed.
+- `build_summary()` currently counts only `*_violation`; v1.1 should include `severity: alert|warning` in alert summaries.
+- FIFO matching of pending batches to trash deposits remains acceptable but should stay covered by tests.
+
+## Frontend Planning Notes
+
+- Replace the fixed `zoneIds` array in `web/src/main.js` with dynamic food-zone state derived from config, constrained to 1-10 zones.
+- Keep `trashRoi` separate from `foodZones`; never include it in `zone_index` numbering.
+- Suggested frontend state groups:
+ - `foodZoneCount`
+ - `foodZones: [{id, label, points}]`
+ - `trashRoi`
+ - active edit target type/index
+ - config form values including alarm threshold
+ - normalized event rows for display
+- Current target must be visually obvious before canvas clicks add points.
+- Coordinates must remain normalized relative to the displayed frame/image.
+- Reducing zone count should truncate removed zones only after a clear confirmation or obvious UI affordance.
+- Re-capturing an RTSP frame must not discard unsaved point edits.
+- Event table must safely render old and new events, including `time_alarm` and `warning_escalated`.
+- Frontend validation should cover 1/10 zones, food/trash editing, save/reload recovery, old 8-zone config compatibility, and threshold validation.
+
+## Homepage Demo Runtime Notes
+
+- Docker runtime can write diagnostics while no events exist, especially when the configured RTSP stream is unreachable.
+- A diagnostics-only summary is not enough to represent the accepted prototype; the home runtime view should show a clearly marked demo state until real events exist.
+- Demo runtime content must never look like a real alarm: banner, metrics labels, event source tags, and event file text identify it as demo data.
+- Real events take precedence over demo data. Per-zone progress uses the latest event by timestamp when both candidates have timestamps; otherwise it falls back to event order.
+- Event-derived progress uses the configured dwell threshold when an event omits `max_dwell_seconds`.
+- Any runtime summary value rendered through `innerHTML`, including `latest_zone_counts`, must be HTML-escaped.
diff --git a/progress.md b/progress.md
index 3cfb2b0..e32793c 100644
--- a/progress.md
+++ b/progress.md
@@ -11,3 +11,175 @@
- Initialized git repository and created the initial project commit.
- Added RTSP single-frame calibration tool under `tools/calibrator`.
- Added formal management API on port `19080` and Vite frontend on port `23000`.
+
+## 2026-05-26 v1.1 优化改造
+
+### Session Log
+
+| Time | Batch Workstream | Actor | Action | Result |
+| --- | --- | --- | --- | --- |
+| 2026-05-26 | Batch setup and planning | Main Agent | Created active goal for `v1.1 优化改造` | Goal tracks dynamic zones, trash ROI editing, custom alarm threshold, warning escalation |
+| 2026-05-26 | Batch setup and planning | Main Agent | Started backend and frontend planning sub-agents | Waiting for role outputs while updating plan files |
+| 2026-05-26 | Batch setup and planning | Main Agent | Updated `task_plan.md` and `findings.md` with v1.1 scope | `v1.1 优化改造` planning in progress |
+| 2026-05-26 | Batch setup and planning | Frontend Agent | Returned frontend planning notes | Added dynamic zones, trash ROI, event display, and validation notes to `findings.md` |
+| 2026-05-26 | Batch setup and planning | Backend Agent | Returned backend event model and test risk notes | Added event flow, summary risk, and compatibility notes to `findings.md` |
+| 2026-05-26 | Batch setup and planning | User | Clarified that all requested work belongs to one v1.1 development batch with different workstreams | Updated `task_plan.md` and `docs/project.md` wording |
+| 2026-05-26 | Batch setup and planning | User | Asked to remove earlier split-work wording and make it part of one batch | Renamed v1.1 plan table to batch workstreams and replaced split-work wording in progress |
+| 2026-05-26 | Batch setup and planning | User | Set the batch name to `v1.1 优化改造` | Updated planning documents to use this name |
+| 2026-05-26 | Backend event model | Review Agent | Re-reviewed event model after severity fix | Pass; no blocking issues for Config/API workstream |
+| 2026-05-26 | Batch setup and planning | User | Required a standard context header before each subagent task dispatch | Added header convention to `task_plan.md` and `findings.md`; future subagent prompts will include project, batch, stage, and role |
+| 2026-05-26 | Batch setup and planning | User | Clarified that `项目` in subagent task headers should use the actual project path | Updated the required header path to `/Users/yoilun/Code/cold_display_guard` |
+| 2026-05-26 | Config and management API | Main Agent | Added 1-10 numeric zone validation, `zone_count`, `label`, trash ROI separation, and alarm/warning summary counts | Target config/API tests and full Python tests passed |
+| 2026-05-26 | Config and management API | Review Agent | Reviewed Config/API workstream | No blocking issues; raised two major contract issues, both fixed with regression tests |
+| 2026-05-26 | Frontend management console | Main Agent | Added dynamic 1-10 numeric zone editor, independent trash ROI editing, minute-based alarm threshold, and alarm/warning event rendering | Frontend unit test and Vite build passed |
+| 2026-05-26 | Frontend management console | Frontend Agent | Reviewed frontend workstream | No blocking issues; raised two major legacy-mapping/label issues, both fixed with regression tests and sent for re-review |
+| 2026-05-26 | Documentation and final review | Main Agent | Updated `README_zh.md`, `docs/project.md`, and `config/example.toml` for v1.1 numeric zones and event flow | Docs now describe one `v1.1 优化改造` batch and current configuration/event model |
+| 2026-05-26 | Frontend management console | Frontend Agent | Re-reviewed frontend legacy mapping and label normalization fixes | Pass; no blocking, no major, no new minor issues |
+| 2026-05-26 | Documentation and final review | Review Agent | Ran final v1.1 review | No blocking; found two major issues in removal-time alarm ordering and partial calibration zone-count preservation, plus one planning-doc minor |
+| 2026-05-26 | Documentation and final review | Main Agent | Fixed final review issues | Added tests and fixes for removal-frame `time_alarm`, partial numeric calibration preserving `zone_count`, and updated top-level plan wording |
+| 2026-05-26 | Documentation and final review | Review Agent | Re-reviewed final fixes | Pass; no blocking, no major, no minor issues |
+| 2026-05-26 | Documentation and final review | Main Agent | Completed `v1.1 优化改造` batch | Stop conditions satisfied and final verification recorded |
+| 2026-05-26 | Homepage demo runtime display | Main Agent | Cleared old event data and added complete demo runtime homepage | Homepage now defaults to runtime view with demo banner, metrics, dwell progress, and event table when real events are empty |
+| 2026-05-26 | Homepage demo runtime display | Frontend Agent | Implemented demo runtime display and tests | Added `buildRuntimeDisplayModel`, progress rows, demo/real labels, and responsive styles |
+| 2026-05-26 | Homepage demo runtime display | Review Agent | Reviewed homepage demo runtime display | Found progress ordering, threshold fallback, and XSS issues; all were fixed with regression tests |
+| 2026-05-26 | Homepage demo runtime display | Review Agent | Final re-review | Pass; no blocking or major issues |
+| 2026-05-26 | Homepage demo runtime display | Main Agent | Rebuilt Docker web service | `http://127.0.0.1:23000` serves nginx container with latest frontend asset |
+| 2026-05-26 | Homepage demo runtime display | User | Reported that the runtime demo homepage was still not visible | Reproduced in Chrome; found frontend initialization stopped before tab switching |
+| 2026-05-26 | Homepage demo runtime display | Main Agent | Fixed null config runtime display crash | `buildRuntimeDisplayModel()` now tolerates `config: null`; Chrome shows runtime page with demo data, metrics, progress, and event table |
+| 2026-05-26 | Remote Docker deployment | Main Agent | Probed `xiaozheng@192.168.5.206` for Docker availability | Blocked by SSH authentication failure: `Permission denied (publickey,password)` |
+| 2026-05-26 | Remote Docker deployment | Main Agent | Synced project to `xiaozheng@192.168.5.206:/home/xiaozheng/cold_display_guard` | `rsync -az --delete` completed with `.git`, local env, node modules, web dist, and logs excluded |
+| 2026-05-26 | Remote Docker deployment | Main Agent | Built and started Docker services on `192.168.5.206` | API and Web are running; runtime was stopped because `[stream].rtsp_url` is empty |
+| 2026-05-26 | Hide demo runtime data | Main Agent | Removed synthetic demo runtime summary/events/progress from the frontend model | Empty or diagnostics-only runtime data now renders empty states and real metrics only |
+| 2026-05-26 | Hide demo runtime data | Main Agent | Synced and rebuilt Web on `192.168.5.206` | Remote Web serves `index-D3qCb2DS.js` without demo batch/camera or visible demo-data strings |
+| 2026-05-27 | Runtime recognition startup | Main Agent | Checked whether recognition had started on `192.168.5.206` | Runtime was stopped, then started after fixing remote timezone from `shanghai` to `Asia/Shanghai`; diagnostics show frame capture and `baseline_ready: true` |
+| 2026-05-27 | Runtime recognition investigation | Main Agent | Investigated why zones 1 and 6 appeared to stop counting after 20 minutes | Evidence shows zones 1 and 6 remain occupied; frontend freezes at the one-time `time_alarm` event dwell value because no live tick is emitted/rendered |
+| 2026-05-28 | Runtime vision small-object detection | Main Agent | Investigated why zones 1/2/5 placements were missed, zone 4 started timing, and zone 2 produced repeated short events | Evidence showed small dark objects were diluted by whole-region mean/texture while zone 4 reflection drove texture false positives |
+| 2026-05-28 | Runtime vision small-object detection | Main Agent | Added dark-pixel fraction occupancy and bright-reflection filtering, then deployed to `192.168.5.206` | Current remote diagnostics show zones 1/2/5 occupied and zone 4 empty |
+| 2026-05-29 | Same-frame trash confirmation | Main Agent | Investigated why zone 1 was removed and trash motion was visible but still escalated | Diagnostics showed `deposit=true` in the same frame as removal, but the engine applied trash deposits before creating `pending_disposal` |
+| 2026-05-29 | Same-frame trash confirmation | Main Agent | Applied remaining trash deposits again after zone transitions and deployed to `192.168.5.206` | Same-frame removal plus visible trash ROI motion now emits `batch_pending_disposal` followed by `batch_discarded` |
+| 2026-05-29 | Zone 4 reflection and stale progress | Main Agent | Investigated why zone 4 kept timing and why zone 9/10 progress bars appeared | Zone 4 had high bright reflection plus a small dark edge crossing the dark threshold; zone 9/10 came from historical events outside the current 1-8 config |
+| 2026-05-29 | Zone 4 reflection and stale progress | Main Agent | Added high-bright/small-dark reflection suppression and filtered progress rows to current configured zones only | Remote summary now reports zones 1-8 all empty and frontend model returns no progress rows |
+| 2026-05-29 | Event table dwell display | Main Agent | Investigated why a zone 1 `batch_started` row kept counting after the batch was consumed | Frontend displayed live dwell per row and did not know the same batch had a later terminal event |
+| 2026-05-29 | Event table dwell display | Main Agent | Limited event-table live dwell to the latest non-terminal event per batch and deployed the Web container | Removed `batch_started` rows now keep their recorded dwell value while the terminal row shows final dwell |
+
+### Test Results
+
+| Time | Command | Result | Notes |
+| --- | --- | --- | --- |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest tests/test_engine.py -v` | pass | 11 engine tests passed after v1.1 event model changes |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 20 full Python tests passed |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest tests/test_config.py -v` | pass | 6 config tests passed after numeric zone validation fixes |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest tests/test_manage_api.py -v` | pass | 7 manage API tests passed after warning summary fixes |
+| 2026-05-26 | `node --test web/test/zone-state.test.js` | pass | 7 frontend zone-state tests passed |
+| 2026-05-26 | `cd web && pnpm build` | pass | Vite production build passed |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 28 full Python tests passed after v1.1 config/API changes |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 30 full Python tests passed after final review fixes |
+| 2026-05-26 | `node --test web/test/zone-state.test.js` | pass | 7 frontend zone-state tests passed after final review fixes |
+| 2026-05-26 | `cd web && pnpm build` | pass | Vite production build passed after final review fixes |
+| 2026-05-26 | `node --test web/test/zone-state.test.js` | pass | 14 frontend zone-state tests passed after homepage demo runtime and review fixes |
+| 2026-05-26 | `cd web && pnpm build` | pass | Vite production build passed with latest homepage runtime asset |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 30 full Python tests passed after homepage runtime changes |
+| 2026-05-26 | `docker compose --env-file cold-display-guard.env -f docker-compose.yml up -d --build cold-display-guard-web` | pass | Docker web image rebuilt and container restarted |
+| 2026-05-26 | `node --test web/test/zone-state.test.js` | pass | 15 frontend zone-state tests passed after null-config startup fix |
+| 2026-05-26 | `cd web && pnpm build` | pass | Vite production build passed after null-config startup fix |
+| 2026-05-26 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 30 full Python tests passed after null-config startup fix |
+| 2026-05-26 | `docker compose build cold-display-guard-api cold-display-guard-web` on `192.168.5.206` | pass | Built `cold-display-guard:dev` and `cold-display-guard-web:dev` |
+| 2026-05-26 | `docker compose up -d` on `192.168.5.206` | pass | API, runtime, and web containers created; runtime exited due missing RTSP |
+| 2026-05-26 | `curl http://192.168.5.206:19080/api/manage/health` | pass | API returned `{"status":"ok"}` |
+| 2026-05-26 | `curl -I http://192.168.5.206:23000/` | pass | Web returned `HTTP/1.1 200 OK` |
+| 2026-05-26 | `node --test web/test/zone-state.test.js` | pass | 16 frontend model tests passed after hiding demo runtime data |
+| 2026-05-26 | `cd web && pnpm build` | pass | Vite production build passed with `index-D3qCb2DS.js` |
+| 2026-05-26 | `docker compose up -d --build cold-display-guard-web` on `192.168.5.206` | pass | Remote Web and API containers restarted; API healthy, Web returned `HTTP/1.1 200 OK` |
+| 2026-05-26 | `rg "演示数据|DEMO DATA|demo_batch|demo_camera" /private/tmp/cold-display-guard-remote-web.js` | pass | No matches in the deployed remote JS asset |
+| 2026-05-27 | `docker ps -a --filter name=cold-display-guard` on `192.168.5.206` | pass | Runtime is `Up`; API is healthy; Web is up |
+| 2026-05-27 | `tail -5 logs/runtime_diagnostics.jsonl` on `192.168.5.206` | pass | Runtime is writing fresh diagnostics; baseline became ready and all 10 zones reported counts |
+| 2026-05-27 | `grep '"zone_id": "1"' logs/events.jsonl | tail -20` on `192.168.5.206` | pass | Zone 1 started `2026-05-27T09:23:43+08:00` and emitted `time_alarm` at `2026-05-27T09:43:48+08:00`; no removal event followed |
+| 2026-05-27 | `grep '"zone_id": "6"' logs/events.jsonl | tail -20` on `192.168.5.206` | pass | Zone 6 started `2026-05-27T09:23:49+08:00` and emitted `time_alarm` at `2026-05-27T09:43:54+08:00`; no removal event followed |
+| 2026-05-27 | `tail -5 logs/runtime_diagnostics.jsonl` on `192.168.5.206` | pass | Latest diagnostics still report zones 1 and 6 as `occupied: true` |
+| 2026-05-27 | `node --test web/test/zone-state.test.js` | pass | 18 frontend model tests passed after live dwell timer fix |
+| 2026-05-27 | `PYTHONPATH=src python3 -m unittest tests/test_vision.py -v` | pass | Vision regression tests passed for consecutive occupancy confirmation and raised reflection threshold |
+| 2026-05-27 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 32 full Python tests passed after runtime vision changes |
+| 2026-05-27 | `pnpm build` in `web/` | pass | Vite production build passed with `index-BkBYO5x5.js` |
+| 2026-05-27 | `rsync -az --delete ... --exclude config/example.toml` to `192.168.5.206` | pass | Code synced while preserving remote RTSP/calibration config |
+| 2026-05-27 | Remote config append `[runtime]` thresholds | pass | Added `occupancy_mean_delta = 45.0`, `occupancy_confirm_frames = 2`, and `empty_confirm_frames = 2` without changing RTSP |
+| 2026-05-27 | `docker compose build cold-display-guard-api cold-display-guard-web` on `192.168.5.206` | pass | Built new API/runtime shared image and web image with live timer code |
+| 2026-05-27 | `docker compose up -d --no-deps cold-display-guard-api cold-display-guard-web` on `192.168.5.206` | pass | API and Web restarted; runtime intentionally left running to avoid relearning baseline with items still present |
+| 2026-05-27 | `curl` remote health and web index | pass | API returned healthy and Web serves `index-BkBYO5x5.js` |
+| 2026-05-27 | `node --test web/test/zone-state.test.js` | pass | 20 frontend model tests passed after event-table live dwell and current-zone filtering |
+| 2026-05-27 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 36 full Python tests passed after runtime state restore and seeded vision baseline |
+| 2026-05-27 | `docker compose up -d --no-deps cold-display-guard-runtime cold-display-guard-api` on `192.168.5.206` | pass | Runtime restarted on new image while preserving active 1/6/7 timers from event history and prior baseline |
+| 2026-05-27 | `tail -n 3 logs/runtime_diagnostics.jsonl` on `192.168.5.206` | pass | New diagnostics include `raw_occupied`/streaks; 1/6/7 occupied, 3/4/5/8 empty |
+| 2026-05-27 | Remote summary after 12 seconds | pass | Event count stayed at 579; no new false events after runtime restart |
+| 2026-05-28 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 39 full Python tests passed after dark-fraction occupancy and reflection filtering |
+| 2026-05-28 | `node --test web/test/zone-state.test.js` | pass | 20 frontend model tests passed |
+| 2026-05-28 | `pnpm build` in `web/` | pass | Vite production build passed with `index-DFRi3R8X.js` |
+| 2026-05-28 | `rsync -az --delete ... --exclude config/example.toml` to `192.168.5.206` | pass | Code synced while preserving remote RTSP/calibration config |
+| 2026-05-28 | Remote config runtime patch | pass | Added dark-fraction and bright-reflection runtime thresholds without printing or changing RTSP |
+| 2026-05-28 | `docker compose build cold-display-guard-api` and `up -d --no-deps cold-display-guard-runtime cold-display-guard-api` on `192.168.5.206` | pass | API/runtime rebuilt and restarted on the new image |
+| 2026-05-28 | Remote diagnostics/API summary | pass | Current counts show zones 1/2/5 occupied and zones 3/4/6/7/8 empty; zone 4 has reflection texture but dark fraction remains `0.0` |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_engine.BatchEngineTests.test_same_observation_removal_and_trash_motion_discards_alerted_batch -v` | red then pass | Reproduced and fixed same-frame trash motion being ignored for a newly pending alerted batch |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_main -v` | red then pass | Runtime restart state restore now uses dark-fraction/bright-reflection occupancy rules |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 41 full Python tests passed after same-frame trash confirmation and restore-rule fixes |
+| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 20 frontend model tests still passed |
+| 2026-05-29 | `rsync -az --delete ... --exclude config/example.toml` to `192.168.5.206` | pass | Code synced while preserving remote RTSP/calibration config |
+| 2026-05-29 | `docker compose build cold-display-guard-api` and `up -d --no-deps cold-display-guard-runtime cold-display-guard-api` on `192.168.5.206` | pass | API/runtime rebuilt and restarted on the same-frame trash confirmation fix |
+| 2026-05-29 | Remote status and diagnostics check | pass | Runtime/API are up; API healthy; latest diagnostics are being written after restart |
+| 2026-05-29 | Targeted reflection/progress tests | red then pass | Added regressions for bright reflection with small dark edge, runtime restore/API recompute using that rule, and hiding historical zone 9/10 progress |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 42 full Python tests passed after zone 4 reflection suppression |
+| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 21 frontend model tests passed after current-zone progress filtering |
+| 2026-05-29 | `pnpm build` in `web/` | pass | Vite production build passed with `index-sJMxcaD6.js` |
+| 2026-05-29 | `docker compose build cold-display-guard-api cold-display-guard-web` and `up -d --no-deps cold-display-guard-runtime cold-display-guard-api cold-display-guard-web` on `192.168.5.206` | pass | Runtime/API/Web rebuilt and restarted with reflection and progress fixes |
+| 2026-05-29 | Remote API/diagnostics/frontend model verification | pass | API `latest_zone_counts` shows zones 1-8 all `0`; latest diagnostics show zone 4 `occupied: false`; model progress rows are empty |
+| 2026-05-29 | `node --test web/test/zone-state.test.js` | red then pass | Added regression so `batch_started` rows stop live ticking after the same batch has a terminal event |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 42 Python tests still passed after frontend-only dwell display fix |
+| 2026-05-29 | `pnpm build` in `web/` | pass | Vite production build passed with `index-BoXFyXbk.js` |
+| 2026-05-29 | `docker compose build cold-display-guard-web` and `up -d --no-deps cold-display-guard-web` on `192.168.5.206` | pass | Remote Web rebuilt and serves `index-BoXFyXbk.js` |
+| 2026-05-29 | Remote frontend model verification for `batch_000473` | pass | `batch_started` displays `0` seconds and `batch_consumed` displays final `64` seconds |
+| 2026-05-29 | API stable-occupancy regression tests | red then pass | `latest_zone_counts` now uses runtime's debounced `occupied` state before raw threshold fallback |
+| 2026-05-29 | Trash sustained-motion regression test | red then pass | Two consecutive moderate trash motions below the strong threshold now confirm disposal |
+| 2026-05-29 | Runtime restore stable-occupancy regression test | red then pass | Restart restore now preserves debounced occupancy for threshold-edge zones |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 45 full Python tests passed |
+| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 22 frontend model tests passed |
+| 2026-05-29 | `docker compose build cold-display-guard-runtime cold-display-guard-api` and `up -d --no-deps cold-display-guard-runtime cold-display-guard-api` on `192.168.5.206` | pass | Runtime/API rebuilt and restarted with stable count and sustained trash-motion fixes |
+| 2026-05-29 | Remote API/diagnostics verification | pass | API healthy; runtime writes `motion_streak`/`strong_motion`/`sustained_motion`; API summary matches stable zone counts |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_vision.VisionTests.test_runtime_vision_defaults_raise_brightness_reflection_threshold -v` | red then pass | Default runtime sampling is now dense enough for small ROIs: `sample_stride_pixels = 4` |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 45 full Python tests passed after dense sampling default |
+| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 22 frontend model tests passed |
+| 2026-05-29 | Remote runtime/API rebuild and config patch | pass | Remote config now has `sample_stride_pixels = 4`; runtime/API restarted |
+| 2026-05-29 | Remote zone 1 diagnostics verification | pass | Zone 1 stayed occupied for 16 consecutive post-deploy checks; API summary reports zone 1 as occupied |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_engine.BatchEngineTests.test_same_observation_trash_motion_discards_multiple_newly_pending_batches -v` | red then pass | Same-frame trash motion now discards multiple alerted batches that clear together |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest tests.test_vision.VisionTests.test_detector_allows_quick_sequential_strong_trash_motions -v` | red then pass | Quick sequential strong trash motions are no longer suppressed by the old 8-second cooldown |
+| 2026-05-29 | `PYTHONPATH=src python3 -m unittest discover -s tests -v` | pass | 47 full Python tests passed after multi-zone trash confirmation fix |
+| 2026-05-29 | `node --test web/test/zone-state.test.js` | pass | 22 frontend model tests passed |
+| 2026-05-29 | Remote runtime/API rebuild and config patch | pass | Remote config now has `trash_motion_cooldown_seconds = 3`; diagnostics include `in_cooldown` |
+
+### Bug Loop
+
+| Batch Workstream | Bug | Fix Attempt | Retest Result |
+| --- | --- | --- | --- |
+| Backend event model | Review agent found `severity` missing from some events such as `batch_started` and `mixed_batch_violation` | Added regression assertions and made `_event()` assign default severity by event type | Resolved; re-review passed |
+| Config and management API | Review agent found `zone_count` could conflict with numeric `zone_ids`, and pending disposal was counted as an upgraded warning | Added regression tests; validate `zone_count == len(zone_ids)` and count only `warning_escalated`/legacy `_violation` as warning events | Resolved; target tests and full Python tests passed |
+| Frontend management console | Frontend agent found old `rows/cols` configs without `zone_ids` lost legacy polygons, and old labels could be saved back to numeric zones | Added regression tests; derive legacy `rNcM` source IDs from `rows/cols` and normalize labels to `区域 N` | Resolved; re-review passed |
+| Documentation and final review | Final review found removal observations at the threshold skipped `time_alarm` before pending disposal | Added regression tests and moved time-alarm application before zone-clear transitions | Resolved; Python tests passed |
+| Documentation and final review | Final review found partial calibration saves could shrink `zone_count` to the number of completed polygons | Added frontend payload `layout`, backend merge support for target numeric zone IDs, and regression tests preserving existing polygons/count | Resolved; Python and frontend tests passed |
+| Homepage demo runtime display | Review found real progress kept maximum dwell time instead of the latest event | Added latest-event regression tests and selected per-zone progress by timestamp or event order | Resolved; frontend tests passed |
+| Homepage demo runtime display | Review found event rows missing threshold values fell back to 1200 instead of config threshold | Added regression test and inherited the configured threshold | Resolved; frontend tests passed |
+| Homepage demo runtime display | Review found `latest_zone_counts` was interpolated into `innerHTML` without escaping | Added shared `escapeHtml()` and escaped each zone-count fragment | Resolved; final review passed |
+| Homepage demo runtime display | Browser showed static calibration page because runtime display read `config.thresholds` while `config` was still `null` | Added regression test for `config: null` and normalized missing config to `{}` before deriving thresholds | Resolved; Chrome verification shows runtime demo homepage |
+| Remote Docker deployment | SSH to `xiaozheng@192.168.5.206` failed with `Permission denied (publickey,password)` | Tried normal SSH and escalated SSH using the same host/user; local key was not accepted and no interactive password was available | Blocked until credentials are provided or the local public key is authorized on the remote host |
+| Remote Docker deployment | Initial `docker compose up -d --build` attempted to pull `cold-display-guard:dev` before a local image existed | Stopped the hanging SSH command, confirmed no existing local image, then explicitly ran `docker compose build cold-display-guard-api cold-display-guard-web` before `up -d` | Resolved; images built and API/Web started |
+| Remote Docker deployment | `cold-display-guard-runtime` restarted repeatedly | Checked runtime logs; root cause was `ValueError: stream.rtsp_url is required` because `config/example.toml` has an empty RTSP URL | Stopped runtime container until an RTSP URL is configured |
+| Hide demo runtime data | Empty runtime data still generated synthetic demo summary/events/progress | Added failing frontend tests, then replaced demo fallback with empty summary, empty events, and empty progress rows | Resolved; frontend tests and build passed |
+| Hide demo runtime data | Legacy `event.demo` rows or `cold_display_guard_demo` summaries could still surface if old data existed | Added failing regression test, filtered demo-marked events/summaries, and removed visible demo labels from event rendering | Resolved; remote JS asset has no visible demo-data strings |
+| Runtime recognition startup | Runtime restarted after RTSP was configured but exited with `ZoneInfoNotFoundError: 'No time zone found with key shanghai'` | Updated remote config and local example config to `timezone = "Asia/Shanghai"`, then restarted runtime | Resolved; runtime remains up and writes diagnostics |
+| Runtime recognition investigation | Frontend dwell display stops near the 20-minute alarm even while the item remains present | Added live dwell computation from `started_at` for non-ended batches, one-second frontend re-render, five-second runtime-data polling, and broader event fetch limit | Resolved; Web/API deployed |
+| Runtime vision false positives | Reflections in empty zones crossed the old mean-luma threshold and triggered occupancy/alarm | Raised default `occupancy_mean_delta` to `55.0`, added 2-frame occupied/empty confirmation, recomputed current counts in API from diagnostics, restored runtime state from events/diagnostics, and restarted runtime | Resolved; latest diagnostics show only 1/6/7 occupied |
+| Runtime vision small dark objects | Zones 1/2/5 contained compact dark objects that did not always exceed whole-region mean/texture thresholds; zone 4 bright reflection exceeded texture threshold; zone 2 flickered around the old threshold and created repeated short batches | Added dark-fraction metrics, required dark evidence for texture occupancy, ignored bright reflection without dark evidence, and added manage API recomputation with the same rule | Resolved; latest remote diagnostics keep 1/2/5 occupied, 4 empty, and zone 2 no longer produces consume/start flicker after deployment |
+| Same-frame trash confirmation | Trash motion in the visible trash ROI could occur in the exact frame where an alerted zone was removed; the engine consumed trash deposits before it created the new pending-disposal batch, so the deposit was lost and the batch later escalated | Added a regression test and reapplied leftover trash deposits after zone transitions; also updated runtime restore to use the dark-fraction rules before restart | Resolved for future events; existing historical `warning_escalated` rows are not rewritten |
+| Zone 4 reflection and stale zone 9/10 progress | Zone 4 reflection produced both bright pixels and a small dark edge above the dark-object threshold; old zone 9/10 events were still eligible for progress rows because missing live counts were treated as occupied | Added a reflection classifier for high-bright/small-dark patterns and required progress rows to belong to current configured food zones with explicit live occupancy when diagnostics are present | Resolved; remote currently shows no occupied zones and no progress rows |
+| Event table live dwell after removal | Earlier `batch_started` rows had no `ended_at`, so the frontend kept applying live dwell even after a later event in the same batch ended it | Event rows now compute live dwell only for the latest non-terminal event in each batch | Resolved; removed batches no longer keep counting in their `batch_started` row |
+| Zone 2 progress flicker | API summary recomputed latest zone counts from raw metrics, bypassing runtime's occupied/empty confirmation; threshold-edge zone 2 could flip to `0` while runtime still held stable occupied | Summary now prefers per-zone stable `occupied` from diagnostics, then falls back to raw recompute only for older diagnostics | Resolved; API counts align with runtime stable state |
+| Zone 2 disposal escalated after trash drop | The zone 2 batch was removed and entered pending disposal, but trash `motion_delta` peaked around `10.1`, below the one-frame threshold `18`, so no trash deposit was counted before the deadline | Added sustained trash-motion confirmation: two consecutive moderate motions at `>= 8.0` count as a deposit, while the strong one-frame threshold remains | Resolved for future events; historical `batch_000474` warning row is not rewritten |
+| Runtime restart can drop threshold-edge timers | Runtime restore used raw threshold recompute instead of stable `occupied`, so a restart during a one-frame raw dip could lose an active timer | Restore now uses stable `occupied` when present and keeps raw recompute only as a fallback | Resolved; regression test covers zone 2-style flicker |
+| Zone 1 timer reset | Zone 1's ROI had too few samples with the default stride `8`; dark evidence jumped between `0.0714` and `0.0357/0`, causing two occupied frames followed by two empty frames and repeated short batches | Reduced default and remote `sample_stride_pixels` to `4` so small ROI/object evidence is less quantized | Resolved in current verification; zone 1 remains continuously occupied after deploy |
+| Zone 1/4 disposal missed after zone 2 discard | Zone 2 generated a trash deposit at `14:32:13`; zone 1/4 cleared at `14:32:19`, but their strong trash motion was inside the old 8-second cooldown, and one same-frame deposit could only discard one newly pending batch | Reduced trash cooldown to 3 seconds and let one same-frame trash motion discard all newly pending alerted batches from that observation | Resolved for future events; historical zone 1/4 rows are not rewritten |
diff --git a/prototype/custom-zones/index.html b/prototype/custom-zones/index.html
new file mode 100644
index 0000000..f43e604
--- /dev/null
+++ b/prototype/custom-zones/index.html
@@ -0,0 +1,1110 @@
+
+
+