feat: initialize managed portal
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
# Store Dwell Alert Design
|
||||
|
||||
**Date:** 2026-04-15
|
||||
**Target Host:** `192.168.5.108`
|
||||
**Video Source:** single `RTSP` stream
|
||||
**Alert Channel:** `HTTP webhook`
|
||||
|
||||
## Goal
|
||||
|
||||
Design a single-camera store monitoring service that detects long customer stays with `YOLO11n`, excludes staff from customer timing, alerts when at least 5 customers have each stayed more than 10 minutes, and emits a half-hour report with current and recently closed dwell sessions.
|
||||
|
||||
## Environment Summary
|
||||
|
||||
The target machine `192.168.5.108` is suitable for this service:
|
||||
|
||||
- `Ubuntu 24.04.4`
|
||||
- `Python 3.12.3`
|
||||
- `NVIDIA RTX 3080 20GB`
|
||||
- `Docker 29.3.0`
|
||||
- `ffmpeg` not installed yet
|
||||
- `ultralytics` not installed yet
|
||||
- ample free disk space
|
||||
|
||||
The comparison host `192.168.5.154` has similar compute capability but much less free disk space, so `.108` is the preferred deployment target.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
- Read one store camera `RTSP` stream continuously.
|
||||
- Detect only `person` objects using `YOLO11n`.
|
||||
- Track people across frames.
|
||||
- If a person briefly leaves and returns within 5 minutes, treat them as the same customer and resume timing.
|
||||
- If a person is absent for more than 5 minutes, close the current dwell session and preserve the dwell time accumulated before the pause.
|
||||
- Exclude staff from customer dwell timing and alert thresholds.
|
||||
- Trigger an alert when 5 or more customers each have `dwell_seconds >= 600`.
|
||||
- Emit a half-hour report every `:00` and `:30`.
|
||||
- Send both alerts and reports through `HTTP webhook`.
|
||||
|
||||
### Non-Functional
|
||||
|
||||
- Run on a single GPU host with long-lived service behavior.
|
||||
- Handle RTSP interruption and auto-reconnect.
|
||||
- Avoid alert spam through cooldown control.
|
||||
- Keep enough local logs and snapshots for troubleshooting.
|
||||
|
||||
## Constraints and Key Decisions
|
||||
|
||||
- There is no stable entrance/exit ROI available.
|
||||
- Re-identification must rely on full-frame tracking plus appearance features rather than doorway logic.
|
||||
- Staff are common, visually stable, and often wear similar clothing, so staff must be excluded via a manually curated whitelist rather than dwell duration heuristics.
|
||||
- The business identity must be `person_id`, not raw `track_id`.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
Use `YOLO11n + tracker + appearance ReID + business dwell state machine`.
|
||||
|
||||
Why this approach:
|
||||
|
||||
- `YOLO11n` is fast enough for a single RTSP stream on `.108`.
|
||||
- The main business risk is not raw person detection but stable identity across short disappear/reappear intervals.
|
||||
- A tracker alone cannot satisfy the 5-minute return rule because tracker IDs are short-lived.
|
||||
- A manual staff gallery is safer than attempting to auto-learn "frequent people" as staff.
|
||||
|
||||
Rejected alternatives:
|
||||
|
||||
- `YOLO11n + DeepSORT`: simpler but weaker for multi-minute re-association without a door ROI.
|
||||
- Heavier detector or heavier ReID from day one: more cost and tuning burden than needed for a single camera MVP.
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
The service runs as a single process on `192.168.5.108` with these modules:
|
||||
|
||||
1. `stream_reader`
|
||||
Pulls RTSP frames, handles reconnects, timestamps frames, and controls sampling FPS.
|
||||
|
||||
2. `detector_tracker`
|
||||
Runs `YOLO11n` person detection and short-term multi-object tracking to produce `bbox`, `track_id`, confidence, and crops.
|
||||
|
||||
3. `identity_resolver`
|
||||
Converts tracker outputs into stable `person_id` identities. Maintains active identities plus a recent paused cache for re-association within 5 minutes.
|
||||
|
||||
4. `staff_filter`
|
||||
Matches person appearance embeddings against a manually registered staff gallery. Staff are flagged and excluded from customer alert logic.
|
||||
|
||||
5. `dwell_engine`
|
||||
Owns the customer dwell session state machine, threshold evaluation, cooldown logic, and half-hour report windowing.
|
||||
|
||||
6. `notifier_reporter`
|
||||
Sends alert/report webhook payloads, persists local JSONL logs, and optionally stores debug snapshots.
|
||||
|
||||
## Data Flow
|
||||
|
||||
`RTSP -> frame sampling -> YOLO11n person detections -> tracker -> crop/embedding -> staff match -> person re-association -> dwell session update -> alert/report evaluation -> webhook/logging`
|
||||
|
||||
Important design rule:
|
||||
|
||||
- Detection and tracking generate transient runtime IDs.
|
||||
- Business counting, dwell timing, and webhook payloads use stable `person_id`.
|
||||
|
||||
## Identity and Re-Association Model
|
||||
|
||||
Each tracked person is represented by:
|
||||
|
||||
- `person_id`
|
||||
- latest tracker `track_id`
|
||||
- running appearance embedding
|
||||
- current role: `customer` or `staff`
|
||||
- session state: `active`, `paused`, `closed`
|
||||
- `dwell_seconds_accumulated`
|
||||
- `last_seen_ts`
|
||||
- `pause_start_ts`
|
||||
|
||||
Re-association logic:
|
||||
|
||||
- When a current tracker target disappears, its session moves from `active` to `paused`.
|
||||
- While paused, the service retains the appearance embedding and metadata for 5 minutes.
|
||||
- A new tracker target is compared against paused identities.
|
||||
- If similarity passes threshold inside the 5-minute window, the new target is merged back into the original `person_id`.
|
||||
- If no match occurs within 5 minutes, the paused session becomes `closed`.
|
||||
|
||||
This solves "leave briefly, come back, continue timing" without relying on a dedicated entrance area.
|
||||
|
||||
## Staff Exclusion Design
|
||||
|
||||
Staff are handled through a manually maintained gallery:
|
||||
|
||||
- Register each staff member under `staff_id`.
|
||||
- Store multiple body crops per staff member, ideally 3 to 10 images with the expected work uniform.
|
||||
- During runtime, customer candidates are embedded and compared against the gallery.
|
||||
- A single match is not enough; the identity must hit the staff threshold across multiple frames before promotion to `role=staff`.
|
||||
|
||||
Why manual whitelist:
|
||||
|
||||
- Long dwell duration does not imply staff.
|
||||
- Frequent appearance does not imply staff.
|
||||
- The user explicitly identified staff clothing consistency as a usable signal.
|
||||
|
||||
Behavior:
|
||||
|
||||
- `role=staff` identities are excluded from customer threshold counting.
|
||||
- Staff can still be logged separately for observability.
|
||||
|
||||
## Dwell Session State Machine
|
||||
|
||||
Each customer session has three states:
|
||||
|
||||
### `active`
|
||||
|
||||
- Customer currently visible in stream.
|
||||
- Dwell time continues accumulating.
|
||||
|
||||
### `paused`
|
||||
|
||||
- Customer no longer visible.
|
||||
- Dwell accumulation stops immediately.
|
||||
- `pause_start_ts` is recorded.
|
||||
|
||||
### `closed`
|
||||
|
||||
- Customer remained absent for more than 5 minutes.
|
||||
- Session ends and final dwell time is preserved as the accumulated value from before the pause.
|
||||
|
||||
Transitions:
|
||||
|
||||
- `active -> paused`: target disappears.
|
||||
- `paused -> active`: same `person_id` returns within 5 minutes through ReID match.
|
||||
- `paused -> closed`: absent for more than 300 seconds.
|
||||
- `closed`: immutable historical session.
|
||||
|
||||
## Alert Logic
|
||||
|
||||
Alert condition:
|
||||
|
||||
- consider only `role=customer`
|
||||
- consider only `state=active`
|
||||
- count customers with `dwell_seconds >= 600`
|
||||
- if count is `>= 5`, emit `long_stay_alert`
|
||||
|
||||
Cooldown:
|
||||
|
||||
- Apply a 10-minute cooldown after an alert to avoid duplicate webhook spam.
|
||||
- Reset eligibility once long-stay active customer count drops below 5.
|
||||
|
||||
## Half-Hour Report Logic
|
||||
|
||||
Reporting schedule:
|
||||
|
||||
- emit at every local half-hour boundary: `HH:00` and `HH:30`
|
||||
|
||||
Report content:
|
||||
|
||||
- currently active customers and their current dwell times
|
||||
- customers whose sessions closed during the preceding half-hour window
|
||||
- optional staff summary count
|
||||
|
||||
This split is important because otherwise recently departed long-stay customers disappear from operational reporting.
|
||||
|
||||
## Webhook Payloads
|
||||
|
||||
### Long-Stay Alert
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "long_stay_alert",
|
||||
"camera_id": "store_cam_01",
|
||||
"ts": "2026-04-15T11:30:00+08:00",
|
||||
"threshold": {
|
||||
"min_people": 5,
|
||||
"min_dwell_seconds": 600
|
||||
},
|
||||
"active_long_stay_count": 6,
|
||||
"people": [
|
||||
{
|
||||
"person_id": "cust_1024",
|
||||
"role": "customer",
|
||||
"status": "active",
|
||||
"dwell_seconds": 835
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Half-Hour Report
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "half_hour_report",
|
||||
"camera_id": "store_cam_01",
|
||||
"window_start": "2026-04-15T11:00:00+08:00",
|
||||
"window_end": "2026-04-15T11:30:00+08:00",
|
||||
"active_customer_count": 4,
|
||||
"active_customers": [
|
||||
{
|
||||
"person_id": "cust_1024",
|
||||
"dwell_seconds": 835
|
||||
}
|
||||
],
|
||||
"closed_customers": [
|
||||
{
|
||||
"person_id": "cust_0988",
|
||||
"final_dwell_seconds": 1240
|
||||
}
|
||||
],
|
||||
"staff_seen_count": 2
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment Plan on 192.168.5.108
|
||||
|
||||
Recommended initial deployment:
|
||||
|
||||
- install `ffmpeg`
|
||||
- install `python3-venv`
|
||||
- create a dedicated virtual environment
|
||||
- install runtime dependencies
|
||||
- run as a `systemd` service
|
||||
|
||||
Reasoning:
|
||||
|
||||
- The host is already a straightforward Python/GPU machine.
|
||||
- A venv-based service is easier to debug first than containerizing around RTSP, GPU, and model dependencies.
|
||||
- Containerization can be added later after validation.
|
||||
|
||||
## Suggested Project Structure
|
||||
|
||||
```text
|
||||
store_dwell_alert/
|
||||
app/
|
||||
main.py
|
||||
config.py
|
||||
schemas.py
|
||||
modules/
|
||||
stream_reader.py
|
||||
detector_tracker.py
|
||||
reid_encoder.py
|
||||
identity_resolver.py
|
||||
staff_filter.py
|
||||
dwell_engine.py
|
||||
notifier.py
|
||||
reporter.py
|
||||
config/
|
||||
config.example.yaml
|
||||
data/
|
||||
staff_gallery/
|
||||
runtime/
|
||||
logs/
|
||||
tests/
|
||||
test_config.py
|
||||
test_dwell_engine.py
|
||||
test_identity_resolver.py
|
||||
test_staff_filter.py
|
||||
test_reporter.py
|
||||
scripts/
|
||||
bootstrap_108.sh
|
||||
run_local.sh
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- RTSP disconnect: reconnect with backoff, preserve paused identities during outage.
|
||||
- Webhook failure: retry several times, then persist failed events locally.
|
||||
- Model inference exception: record structured error, restart processing loop.
|
||||
- Disk growth: rotate logs and clean old snapshots.
|
||||
- Low-confidence ReID: keep identity unresolved rather than aggressively merge people.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Minimum acceptance tests:
|
||||
|
||||
1. Single customer stays longer than 10 minutes: no alert because threshold count is below 5.
|
||||
2. Five customers each stay longer than 10 minutes: one alert webhook fires.
|
||||
3. Customer leaves for 3 minutes and returns: same `person_id`, dwell resumes.
|
||||
4. Customer leaves for 6 minutes and returns: old session closes, new session starts.
|
||||
5. Staff remains visible all day: not counted toward customer alert threshold.
|
||||
6. Half-hour report contains both active and closed customer sections.
|
||||
|
||||
## Open Risks
|
||||
|
||||
- Full-frame ReID without a doorway ROI can still produce mistaken merges in crowded scenes.
|
||||
- Staff and customer clothing similarity can cause false staff matches if gallery thresholds are too loose.
|
||||
- Camera angle quality heavily affects body-crop embedding quality.
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
Implement the service first on `192.168.5.108` using `YOLO11n`, a lightweight tracker, an appearance-based re-association cache, and a manual staff whitelist. Keep the first version single-stream, single-process, and webhook-driven. This is the narrowest design that satisfies the user’s required behavior without adding unnecessary operational complexity.
|
||||
@@ -0,0 +1,530 @@
|
||||
# Store Dwell Alert Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a single-camera RTSP service on `192.168.5.108` that detects long customer stays with `YOLO11n`, excludes staff using a whitelist gallery, resumes dwell timing across short disappear/reappear gaps, and sends HTTP webhook alerts plus half-hour reports.
|
||||
|
||||
**Architecture:** The service is a single Python process organized around RTSP ingestion, person detection/tracking, appearance-based identity resolution, staff filtering, dwell session state management, and webhook/report dispatch. Business timing is keyed on stable `person_id` identities rather than tracker IDs so short absences can pause and resume the same customer session.
|
||||
|
||||
**Tech Stack:** Python 3.12, `ultralytics`, `torch`, `opencv-python`, `ffmpeg`, `pytest`, `requests`, YAML config, `systemd`
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Bootstrap project skeleton and configuration loading
|
||||
|
||||
**Files:**
|
||||
- Create: `app/__init__.py`
|
||||
- Create: `app/config.py`
|
||||
- Create: `app/schemas.py`
|
||||
- Create: `config/config.example.yaml`
|
||||
- Create: `tests/test_config.py`
|
||||
- Create: `requirements.txt`
|
||||
- Create: `scripts/bootstrap_108.sh`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import load_config
|
||||
|
||||
|
||||
def test_load_config_reads_thresholds(tmp_path: Path):
|
||||
cfg = tmp_path / "config.yaml"
|
||||
cfg.write_text(
|
||||
"camera_id: store_cam_01\n"
|
||||
"thresholds:\n"
|
||||
" min_people: 5\n"
|
||||
" min_dwell_seconds: 600\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
data = load_config(cfg)
|
||||
|
||||
assert data.camera_id == "store_cam_01"
|
||||
assert data.thresholds.min_people == 5
|
||||
assert data.thresholds.min_dwell_seconds == 600
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_config.py::test_load_config_reads_thresholds -v`
|
||||
Expected: FAIL because `app.config` and `load_config` do not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass
|
||||
class Thresholds:
|
||||
min_people: int
|
||||
min_dwell_seconds: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
camera_id: str
|
||||
thresholds: Thresholds
|
||||
|
||||
|
||||
def load_config(path: Path) -> AppConfig:
|
||||
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
return AppConfig(
|
||||
camera_id=raw["camera_id"],
|
||||
thresholds=Thresholds(**raw["thresholds"]),
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_config.py::test_load_config_reads_thresholds -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/__init__.py app/config.py app/schemas.py config/config.example.yaml tests/test_config.py requirements.txt scripts/bootstrap_108.sh
|
||||
git commit -m "chore: bootstrap dwell alert project structure"
|
||||
```
|
||||
|
||||
### Task 2: Add RTSP reader with reconnect and frame sampling
|
||||
|
||||
**Files:**
|
||||
- Create: `app/modules/stream_reader.py`
|
||||
- Create: `tests/test_stream_reader.py`
|
||||
- Modify: `requirements.txt`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.stream_reader import StreamHealth
|
||||
|
||||
|
||||
def test_stream_health_marks_disconnect_after_failures():
|
||||
health = StreamHealth(max_failures=3)
|
||||
health.record_failure()
|
||||
health.record_failure()
|
||||
health.record_failure()
|
||||
assert health.is_disconnected is True
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_stream_reader.py::test_stream_health_marks_disconnect_after_failures -v`
|
||||
Expected: FAIL because `StreamHealth` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamHealth:
|
||||
max_failures: int
|
||||
failures: int = 0
|
||||
|
||||
@property
|
||||
def is_disconnected(self) -> bool:
|
||||
return self.failures >= self.max_failures
|
||||
|
||||
def record_failure(self) -> None:
|
||||
self.failures += 1
|
||||
|
||||
def reset(self) -> None:
|
||||
self.failures = 0
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_stream_reader.py::test_stream_health_marks_disconnect_after_failures -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/stream_reader.py tests/test_stream_reader.py requirements.txt
|
||||
git commit -m "feat: add RTSP reader health tracking"
|
||||
```
|
||||
|
||||
### Task 3: Implement dwell session state machine
|
||||
|
||||
**Files:**
|
||||
- Create: `app/modules/dwell_engine.py`
|
||||
- Create: `tests/test_dwell_engine.py`
|
||||
- Modify: `app/schemas.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.dwell_engine import DwellSession
|
||||
|
||||
|
||||
def test_session_pauses_without_adding_absence_time():
|
||||
session = DwellSession(person_id="cust_1", entered_ts=0)
|
||||
session.mark_seen(120)
|
||||
session.pause(130)
|
||||
session.close_if_expired(431, pause_timeout_seconds=300)
|
||||
assert session.state == "closed"
|
||||
assert session.dwell_seconds == 120
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_dwell_engine.py::test_session_pauses_without_adding_absence_time -v`
|
||||
Expected: FAIL because `DwellSession` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DwellSession:
|
||||
person_id: str
|
||||
entered_ts: int
|
||||
state: str = "active"
|
||||
dwell_seconds: int = 0
|
||||
last_seen_ts: int = 0
|
||||
pause_start_ts: int | None = None
|
||||
|
||||
def mark_seen(self, ts: int) -> None:
|
||||
self.dwell_seconds = ts - self.entered_ts
|
||||
self.last_seen_ts = ts
|
||||
self.state = "active"
|
||||
|
||||
def pause(self, ts: int) -> None:
|
||||
self.pause_start_ts = ts
|
||||
self.state = "paused"
|
||||
|
||||
def close_if_expired(self, ts: int, pause_timeout_seconds: int) -> None:
|
||||
if self.pause_start_ts is None:
|
||||
return
|
||||
if ts - self.pause_start_ts > pause_timeout_seconds:
|
||||
self.state = "closed"
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_dwell_engine.py::test_session_pauses_without_adding_absence_time -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/dwell_engine.py app/schemas.py tests/test_dwell_engine.py
|
||||
git commit -m "feat: add dwell session state machine"
|
||||
```
|
||||
|
||||
### Task 4: Implement paused-person re-association
|
||||
|
||||
**Files:**
|
||||
- Create: `app/modules/identity_resolver.py`
|
||||
- Create: `tests/test_identity_resolver.py`
|
||||
- Modify: `app/schemas.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.identity_resolver import choose_reentry_match
|
||||
|
||||
|
||||
def test_choose_reentry_match_prefers_recent_high_similarity():
|
||||
paused_people = [
|
||||
{"person_id": "cust_1", "paused_at": 100, "similarity": 0.91},
|
||||
{"person_id": "cust_2", "paused_at": 80, "similarity": 0.87},
|
||||
]
|
||||
|
||||
result = choose_reentry_match(
|
||||
paused_people=paused_people,
|
||||
now_ts=250,
|
||||
pause_timeout_seconds=300,
|
||||
min_similarity=0.90,
|
||||
)
|
||||
|
||||
assert result == "cust_1"
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_identity_resolver.py::test_choose_reentry_match_prefers_recent_high_similarity -v`
|
||||
Expected: FAIL because `choose_reentry_match` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def choose_reentry_match(
|
||||
paused_people: list[dict],
|
||||
now_ts: int,
|
||||
pause_timeout_seconds: int,
|
||||
min_similarity: float,
|
||||
) -> str | None:
|
||||
valid = [
|
||||
item
|
||||
for item in paused_people
|
||||
if now_ts - item["paused_at"] <= pause_timeout_seconds
|
||||
and item["similarity"] >= min_similarity
|
||||
]
|
||||
if not valid:
|
||||
return None
|
||||
valid.sort(key=lambda item: (item["similarity"], item["paused_at"]), reverse=True)
|
||||
return valid[0]["person_id"]
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_identity_resolver.py::test_choose_reentry_match_prefers_recent_high_similarity -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/identity_resolver.py app/schemas.py tests/test_identity_resolver.py
|
||||
git commit -m "feat: add paused-person re-association logic"
|
||||
```
|
||||
|
||||
### Task 5: Implement staff whitelist matching
|
||||
|
||||
**Files:**
|
||||
- Create: `app/modules/staff_filter.py`
|
||||
- Create: `tests/test_staff_filter.py`
|
||||
- Create: `data/staff_gallery/.gitkeep`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.staff_filter import staff_vote
|
||||
|
||||
|
||||
def test_staff_vote_requires_multiple_hits():
|
||||
assert staff_vote([True, False, True], min_hits=2) is True
|
||||
assert staff_vote([True, False, False], min_hits=2) is False
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_staff_filter.py::test_staff_vote_requires_multiple_hits -v`
|
||||
Expected: FAIL because `staff_vote` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def staff_vote(matches: list[bool], min_hits: int) -> bool:
|
||||
return sum(1 for item in matches if item) >= min_hits
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_staff_filter.py::test_staff_vote_requires_multiple_hits -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/staff_filter.py tests/test_staff_filter.py data/staff_gallery/.gitkeep
|
||||
git commit -m "feat: add staff whitelist voting logic"
|
||||
```
|
||||
|
||||
### Task 6: Implement alert thresholding and half-hour reporting
|
||||
|
||||
**Files:**
|
||||
- Create: `app/modules/notifier.py`
|
||||
- Create: `app/modules/reporter.py`
|
||||
- Create: `tests/test_reporter.py`
|
||||
- Modify: `app/modules/dwell_engine.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.reporter import should_emit_half_hour_report
|
||||
|
||||
|
||||
def test_half_hour_report_emits_on_half_hour_boundaries():
|
||||
assert should_emit_half_hour_report("2026-04-15T11:00:00+08:00") is True
|
||||
assert should_emit_half_hour_report("2026-04-15T11:30:00+08:00") is True
|
||||
assert should_emit_half_hour_report("2026-04-15T11:17:00+08:00") is False
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_reporter.py::test_half_hour_report_emits_on_half_hour_boundaries -v`
|
||||
Expected: FAIL because `should_emit_half_hour_report` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def should_emit_half_hour_report(ts: str) -> bool:
|
||||
dt = datetime.fromisoformat(ts)
|
||||
return dt.minute in {0, 30} and dt.second == 0
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_reporter.py::test_half_hour_report_emits_on_half_hour_boundaries -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/notifier.py app/modules/reporter.py app/modules/dwell_engine.py tests/test_reporter.py
|
||||
git commit -m "feat: add webhook alert and half-hour reporting logic"
|
||||
```
|
||||
|
||||
### Task 7: Integrate YOLO11n inference and tracker adapter
|
||||
|
||||
**Files:**
|
||||
- Create: `app/modules/detector_tracker.py`
|
||||
- Create: `tests/test_detector_tracker.py`
|
||||
- Modify: `requirements.txt`
|
||||
- Modify: `config/config.example.yaml`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.detector_tracker import filter_person_detections
|
||||
|
||||
|
||||
def test_filter_person_detections_keeps_only_person_class():
|
||||
detections = [
|
||||
{"class_name": "person", "confidence": 0.8},
|
||||
{"class_name": "chair", "confidence": 0.9},
|
||||
]
|
||||
|
||||
result = filter_person_detections(detections)
|
||||
|
||||
assert result == [{"class_name": "person", "confidence": 0.8}]
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_detector_tracker.py::test_filter_person_detections_keeps_only_person_class -v`
|
||||
Expected: FAIL because `filter_person_detections` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def filter_person_detections(detections: list[dict]) -> list[dict]:
|
||||
return [item for item in detections if item["class_name"] == "person"]
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_detector_tracker.py::test_filter_person_detections_keeps_only_person_class -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/detector_tracker.py tests/test_detector_tracker.py requirements.txt config/config.example.yaml
|
||||
git commit -m "feat: add YOLO11n detector and tracker adapter"
|
||||
```
|
||||
|
||||
### Task 8: Integrate application loop and deployment assets
|
||||
|
||||
**Files:**
|
||||
- Create: `app/main.py`
|
||||
- Create: `deploy/store-dwell-alert.service`
|
||||
- Create: `scripts/run_local.sh`
|
||||
- Create: `tests/test_main_smoke.py`
|
||||
- Modify: `README.md`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.main import build_app
|
||||
|
||||
|
||||
def test_build_app_returns_named_components():
|
||||
app = build_app()
|
||||
assert "stream_reader" in app
|
||||
assert "dwell_engine" in app
|
||||
assert "notifier" in app
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_main_smoke.py::test_build_app_returns_named_components -v`
|
||||
Expected: FAIL because `build_app` does not exist yet.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def build_app() -> dict:
|
||||
return {
|
||||
"stream_reader": object(),
|
||||
"dwell_engine": object(),
|
||||
"notifier": object(),
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_main_smoke.py::test_build_app_returns_named_components -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/main.py deploy/store-dwell-alert.service scripts/run_local.sh tests/test_main_smoke.py README.md
|
||||
git commit -m "feat: wire app entrypoint and deployment assets"
|
||||
```
|
||||
|
||||
### Task 9: Validate end-to-end scenarios on 192.168.5.108
|
||||
|
||||
**Files:**
|
||||
- Modify: `config/config.example.yaml`
|
||||
- Create: `docs/runbook.md`
|
||||
- Create: `tests/fixtures/events/`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
```python
|
||||
from app.modules.dwell_engine import long_stay_count
|
||||
|
||||
|
||||
def test_long_stay_count_excludes_staff():
|
||||
sessions = [
|
||||
{"role": "customer", "state": "active", "dwell_seconds": 700},
|
||||
{"role": "staff", "state": "active", "dwell_seconds": 40000},
|
||||
]
|
||||
assert long_stay_count(sessions, min_dwell_seconds=600) == 1
|
||||
```
|
||||
|
||||
**Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_dwell_engine.py::test_long_stay_count_excludes_staff -v`
|
||||
Expected: FAIL until final integration exposes the helper correctly.
|
||||
|
||||
**Step 3: Write minimal implementation**
|
||||
|
||||
```python
|
||||
def long_stay_count(sessions: list[dict], min_dwell_seconds: int) -> int:
|
||||
return sum(
|
||||
1
|
||||
for item in sessions
|
||||
if item["role"] == "customer"
|
||||
and item["state"] == "active"
|
||||
and item["dwell_seconds"] >= min_dwell_seconds
|
||||
)
|
||||
```
|
||||
|
||||
**Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `pytest tests/test_dwell_engine.py::test_long_stay_count_excludes_staff -v`
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add app/modules/dwell_engine.py config/config.example.yaml docs/runbook.md tests/test_dwell_engine.py tests/fixtures/events
|
||||
git commit -m "test: validate end-to-end alert semantics"
|
||||
```
|
||||
@@ -0,0 +1,121 @@
|
||||
# Store Dwell Alert Offline Bundle Design
|
||||
|
||||
## Goal
|
||||
|
||||
Add a portable offline delivery bundle for `store_dwell_alert_108` so the project can be copied to another machine matching `192.168.5.108` and installed without internet access.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Target machines are assumed to match `.108` closely:
|
||||
- Ubuntu 24.04
|
||||
- Python 3.12
|
||||
- NVIDIA GPU in the RTX 3080 class
|
||||
- Compatible CUDA driver already present
|
||||
- The bundle must not depend on live package downloads.
|
||||
- Production configuration must not be embedded in the bundle.
|
||||
- Operators should edit `scripts/run.sh` after install to fill in the RTSP URL and other local values.
|
||||
- Existing runtime behavior must stay unchanged.
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
Build a native offline bundle similar to `people_flow_project.gz`, but scoped to this lighter project. The bundle should contain source code, locked Python requirements, wheels, model weights, install/run scripts, and a portable service template. The generated archive should be self-contained for installation on the target machine.
|
||||
|
||||
This is preferred over bundling the current `.venv` because Python virtual environments are less portable across machines, paths, and local shared-library differences. It is also preferred over an online installer because the user requires offline installation.
|
||||
|
||||
## Bundle Layout
|
||||
|
||||
The generated archive should unpack into a single directory such as `store_dwell_alert_bundle/` with the following contents:
|
||||
|
||||
- `app/`
|
||||
- `config/config.example.yaml`
|
||||
- `data/staff_gallery/`
|
||||
- `data/runtime/`
|
||||
- `deploy/store-dwell-alert.service.tpl`
|
||||
- `logs/`
|
||||
- `requirements.txt`
|
||||
- `requirements.lock.txt`
|
||||
- `scripts/install.sh`
|
||||
- `scripts/run.sh`
|
||||
- `scripts/install_service.sh`
|
||||
- `wheelhouse/`
|
||||
- `weights/yolo11n.pt`
|
||||
- `README.md`
|
||||
|
||||
The bundle should intentionally omit host-specific files such as `config/108.local.yaml`, local runtime logs, and any active virtual environment.
|
||||
|
||||
## Dependency Strategy
|
||||
|
||||
Dependencies should be split into two groups:
|
||||
|
||||
1. Runtime Python packages needed on the target machine.
|
||||
2. Runtime assets needed to execute immediately after install.
|
||||
|
||||
The offline installer must use:
|
||||
|
||||
```bash
|
||||
pip install --no-index --find-links wheelhouse -r requirements.lock.txt
|
||||
```
|
||||
|
||||
`requirements.lock.txt` should pin exact versions suitable for the `.108` class of host. It should include the versions already proven on the running deployment where practical, especially for `torch`, `torchvision`, `ultralytics`, `opencv-python-headless`, `numpy`, `PyYAML`, and `requests`.
|
||||
|
||||
The `wheelhouse/` directory should contain all required wheels for the locked requirements set. `weights/` should contain `yolo11n.pt` so the target host does not need to download model weights at first run.
|
||||
|
||||
## Install Flow
|
||||
|
||||
`scripts/install.sh` should:
|
||||
|
||||
1. Verify the project root and required bundled files exist.
|
||||
2. Verify `python3.12` and `ffmpeg` are available on the target machine.
|
||||
3. Create `.venv` under the unpacked bundle directory.
|
||||
4. Install Python packages from `wheelhouse/` using `requirements.lock.txt`.
|
||||
5. Ensure runtime directories exist.
|
||||
6. Print the next-step instruction to edit and run `scripts/run.sh`.
|
||||
|
||||
The installer should fail fast with clear messages if any required file is missing.
|
||||
|
||||
## Run Flow
|
||||
|
||||
`scripts/run.sh` should be the operator-facing entry point. It should:
|
||||
|
||||
- Expose editable shell variables near the top of the file, including:
|
||||
- `RTSP_URL`
|
||||
- `CONFIG_TEMPLATE`
|
||||
- `CONFIG_PATH`
|
||||
- `LOG_DIR`
|
||||
- Generate or refresh a local config file from the template if needed.
|
||||
- Validate that `RTSP_URL` has been changed from its placeholder.
|
||||
- Launch `.venv/bin/python -m app.main --config "$CONFIG_PATH"`.
|
||||
|
||||
The script should avoid storing production configuration in git-tracked files.
|
||||
|
||||
## Service Installation
|
||||
|
||||
`deploy/store-dwell-alert.service.tpl` should be a portable template rather than a host-pinned unit. `scripts/install_service.sh` should render the template using the unpack path and config path, then install the final service file with `systemctl`.
|
||||
|
||||
This keeps the bundle portable even when unpacked into different directories on different machines.
|
||||
|
||||
## Packaging Workflow
|
||||
|
||||
Add a development-side script `scripts/package_bundle.sh` that:
|
||||
|
||||
1. Creates a clean staging directory under `dist/`.
|
||||
2. Copies the required source tree and assets into the staging directory.
|
||||
3. Generates `requirements.lock.txt`.
|
||||
4. Builds or refreshes `wheelhouse/`.
|
||||
5. Ensures `weights/yolo11n.pt` is present.
|
||||
6. Writes bundle-facing scripts and service template into the staging directory.
|
||||
7. Produces a tarball such as `dist/store_dwell_alert_bundle_2026-04-16.tar.gz`.
|
||||
|
||||
The script should be idempotent and safe to rerun.
|
||||
|
||||
## Validation
|
||||
|
||||
The bundle feature is complete when all of the following are true:
|
||||
|
||||
- A tarball can be generated locally from the project.
|
||||
- The tarball unpacks into the expected layout.
|
||||
- `scripts/install.sh` installs without using the network.
|
||||
- `scripts/run.sh` starts the app after the operator sets `RTSP_URL`.
|
||||
- `python -m app.main --once` works on a compatible target host using the bundle environment.
|
||||
- Events are written to `logs/events.jsonl`.
|
||||
- The optional service install flow can render and install a working unit file.
|
||||
@@ -0,0 +1,240 @@
|
||||
# Offline Bundle Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build a self-contained offline delivery bundle for `store_dwell_alert_108` that can be unpacked on a `.108`-class machine, installed without internet, and run after the operator edits `scripts/run.sh`.
|
||||
|
||||
**Architecture:** Add a delivery layer around the existing runtime by introducing a bundle staging pipeline, a locked requirements file, an offline wheelhouse, portable install/run scripts, and a relocatable service template. Keep the application logic unchanged and isolate all new behavior to packaging, deployment, and documentation.
|
||||
|
||||
**Tech Stack:** Bash, Python virtual environments, pip wheelhouse installs, tar.gz packaging, systemd, existing `ultralytics` runtime.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Audit runtime inputs and delivery assets
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Create: `docs/plans/2026-04-16-offline-bundle-design.md`
|
||||
- Create: `docs/plans/2026-04-16-offline-bundle.md`
|
||||
|
||||
**Step 1: Verify the current runtime entrypoints and required assets**
|
||||
|
||||
Check `app/main.py`, `requirements.txt`, `config/config.example.yaml`, `scripts/run_local.sh`, and `deploy/store-dwell-alert.service` to identify everything the bundle must include.
|
||||
|
||||
**Step 2: Update the top-level README scope if needed**
|
||||
|
||||
Add a short note that the project now supports building an offline delivery bundle for compatible hosts.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md docs/plans/2026-04-16-offline-bundle-design.md docs/plans/2026-04-16-offline-bundle.md
|
||||
git commit -m "docs: add offline bundle design and plan"
|
||||
```
|
||||
|
||||
### Task 2: Add bundle-facing deployment files
|
||||
|
||||
**Files:**
|
||||
- Create: `deploy/store-dwell-alert.service.tpl`
|
||||
- Create: `scripts/install.sh`
|
||||
- Create: `scripts/run.sh`
|
||||
- Create: `scripts/install_service.sh`
|
||||
- Test: `tests/test_bundle_layout.py`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add tests that assert:
|
||||
- the new scripts exist
|
||||
- `run.sh` contains a placeholder `RTSP_URL`
|
||||
- the service template contains placeholder tokens rather than a host-pinned path
|
||||
|
||||
**Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 3: Write the minimal implementation**
|
||||
|
||||
Create the installer, runner, and service template files with portable placeholders and safe shell behavior.
|
||||
|
||||
**Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add deploy/store-dwell-alert.service.tpl scripts/install.sh scripts/run.sh scripts/install_service.sh tests/test_bundle_layout.py
|
||||
git commit -m "feat: add offline bundle deployment scripts"
|
||||
```
|
||||
|
||||
### Task 3: Lock Python dependencies for offline delivery
|
||||
|
||||
**Files:**
|
||||
- Create: `requirements.lock.txt`
|
||||
- Modify: `requirements.txt`
|
||||
- Test: `tests/test_bundle_layout.py`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Extend bundle layout tests to assert:
|
||||
- `requirements.lock.txt` exists
|
||||
- it pins exact versions for the runtime packages needed by the bundle
|
||||
|
||||
**Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 3: Write the minimal implementation**
|
||||
|
||||
Add a checked-in `requirements.lock.txt` aligned with the known-good runtime stack and keep `requirements.txt` as the human-edited high-level list if needed.
|
||||
|
||||
**Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add requirements.txt requirements.lock.txt tests/test_bundle_layout.py
|
||||
git commit -m "build: lock offline bundle dependencies"
|
||||
```
|
||||
|
||||
### Task 4: Add bundle staging and archive generation
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/package_bundle.sh`
|
||||
- Modify: `README.md`
|
||||
- Test: `tests/test_bundle_layout.py`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add tests that assert:
|
||||
- `scripts/package_bundle.sh` exists
|
||||
- it references the expected bundle contents
|
||||
- it excludes host-specific config such as `config/108.local.yaml`
|
||||
|
||||
**Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 3: Write the minimal implementation**
|
||||
|
||||
Create `scripts/package_bundle.sh` to stage the bundle into `dist/store_dwell_alert_bundle/` and archive it as `dist/store_dwell_alert_bundle_<date>.tar.gz`.
|
||||
|
||||
**Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/package_bundle.sh README.md tests/test_bundle_layout.py
|
||||
git commit -m "build: add offline bundle packaging script"
|
||||
```
|
||||
|
||||
### Task 5: Vendor weights and prepare runtime directories
|
||||
|
||||
**Files:**
|
||||
- Create: `weights/.gitkeep`
|
||||
- Create: `data/staff_gallery/.gitkeep`
|
||||
- Create: `data/runtime/.gitkeep`
|
||||
- Modify: `README.md`
|
||||
- Test: `tests/test_bundle_layout.py`
|
||||
|
||||
**Step 1: Write the failing tests**
|
||||
|
||||
Add tests that assert the bundle pipeline expects:
|
||||
- `weights/`
|
||||
- `data/staff_gallery/`
|
||||
- `data/runtime/`
|
||||
|
||||
**Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 3: Write the minimal implementation**
|
||||
|
||||
Create the tracked directories and document that `weights/yolo11n.pt` must exist before packaging.
|
||||
|
||||
**Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `pytest tests/test_bundle_layout.py -v`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add weights/.gitkeep data/staff_gallery/.gitkeep data/runtime/.gitkeep README.md tests/test_bundle_layout.py
|
||||
git commit -m "chore: track offline bundle asset directories"
|
||||
```
|
||||
|
||||
### Task 6: Document operator workflow
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Test: `tests/test_main_smoke.py`
|
||||
|
||||
**Step 1: Write the failing test**
|
||||
|
||||
If practical, add a smoke-level documentation assertion or adjust an existing smoke test so the offline scripts are covered by a simple presence/usage check.
|
||||
|
||||
**Step 2: Run the test to verify it fails**
|
||||
|
||||
Run: `pytest tests/test_main_smoke.py -v`
|
||||
|
||||
**Step 3: Write the minimal implementation**
|
||||
|
||||
Document:
|
||||
- how to build the bundle
|
||||
- how to transfer it
|
||||
- how to run `scripts/install.sh`
|
||||
- how to edit `scripts/run.sh`
|
||||
- how to optionally install the service
|
||||
|
||||
**Step 4: Run the tests to verify they pass**
|
||||
|
||||
Run: `pytest tests/test_main_smoke.py -v`
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add README.md tests/test_main_smoke.py
|
||||
git commit -m "docs: add offline bundle operator workflow"
|
||||
```
|
||||
|
||||
### Task 7: Produce and verify a real bundle locally
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/package_bundle.sh`
|
||||
- Modify: `README.md`
|
||||
|
||||
**Step 1: Run the packaging script**
|
||||
|
||||
Run: `bash scripts/package_bundle.sh`
|
||||
|
||||
Expected:
|
||||
- `dist/store_dwell_alert_bundle/` is created
|
||||
- `dist/store_dwell_alert_bundle_<date>.tar.gz` is created
|
||||
|
||||
**Step 2: Inspect the archive contents**
|
||||
|
||||
Run: `tar -tzf dist/store_dwell_alert_bundle_<date>.tar.gz`
|
||||
|
||||
Expected:
|
||||
- bundled scripts, config template, service template, app code, `requirements.lock.txt`, and tracked runtime directories are present
|
||||
|
||||
**Step 3: Fix any path or omission bugs**
|
||||
|
||||
Adjust the packaging script until the archive layout matches the design.
|
||||
|
||||
**Step 4: Run the full test suite**
|
||||
|
||||
Run: `pytest -q`
|
||||
|
||||
Expected: PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add scripts/package_bundle.sh README.md
|
||||
git commit -m "build: verify offline bundle artifact generation"
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
# Service Autostart Implementation Plan
|
||||
|
||||
**Goal:** Make both `store_dwell_alert_108` and `people_flow_project` install into a `systemd`-managed service that starts immediately after install and automatically starts on boot.
|
||||
|
||||
**Scope:**
|
||||
- Add dependency checks and auto-install for lightweight system packages.
|
||||
- Generate local runtime config from editable script variables.
|
||||
- Install and start `systemd` services from project-local templates.
|
||||
- Add Chinese README files to both projects.
|
||||
- Rebuild distributable archives after the changes.
|
||||
|
||||
**Execution Steps:**
|
||||
1. Update `store_dwell_alert_108` scripts to auto-install missing system packages, prepare config, install service, and `enable --now`.
|
||||
2. Add a dedicated Chinese deployment README to `store_dwell_alert_108`.
|
||||
3. Refactor `people_flow_project` to use a generated local config, a foreground `run.sh`, and a `systemd` service template.
|
||||
4. Add a dedicated Chinese deployment README to `people_flow_project`.
|
||||
5. Validate script and Python entrypoint behavior.
|
||||
6. Rebuild both project archives.
|
||||
Reference in New Issue
Block a user