Three intertwined bodies of work developed and tested together this session. Full test suite green: python (edge) + phpunit (cloud). ## Edge/Pi install path - setup_online.sh: apt-lock retry (apt-daily contention), Type=exec unit + StartLimitIntervalSec in [Unit], journalctl-on-failure, crash-loop detection. - onnxruntime is now best-effort (no sdist / no 32-bit-ARM wheel); [6/8] probes the real backend and hard-fails if none works. - Hailo HEF auto-fetch: edge/tools/fetch_hailo_hef.py (pinned sha256, refuses to guess 8 vs 8L, HAILO_ARCH override for serial-less boards); hailo_pose.py run() timeout fix. - install_offline.sh: mirrors the backend gate; onnxruntime optional. ## Cafe24 cloud package (cloud/deploy/) - build_cafe24_package.sh emits move_detect_app/ (private, one folder) + www/move_detect/ (docroot subdir); no-dev classmap autoloader; preflight.php; hardened .htaccess. - Subdirectory mount support: Request::baseFrom/pathFor strip the mount prefix; index.php walks up to find the app root; views/JS prefix every URL with APP_BASE; session cookie scoped to the mount. ## Delivery audit — silent-failure & correctness (docs/delivery-audit.md) All 4 blockers + all Phase-1 silent-failure + Phase-2 HIGH correctness: - Edge cloud clients send a custom User-Agent (cafe24 WAF blocks python-requests) and allow_redirects=False. - Daemon fails loud on blank CAFE24_URL/DEVICE_SECRET; run_loop logs swallowed exceptions; heartbeat carries real fps + per-camera health; UvcSource reopens a dead camera; offline queue is size-capped and drops poison rows; TARGET_FPS/QUEUE_DB validated. - Zone coordinates normalized 0..1 on both sides (were canvas-px vs camera-px — every zone misplaced); pinch zones emit PINCH/HIGH (were downgraded to WARNING). - Fall detection uses bbox cues when keypoints are low-confidence (occluded fallen person); ghost incident not cancelled by a bystander; displacement normalized per-second (FPS-invariant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
13 KiB
13 KiB
Delivery Audit — move-detect (납품 전 결함 목록 · 순차 처리)
Source: 62-agent adversarial audit (2026-07-16) + skill iot-airgap-delivery §3 + manual
verification of every blocker against real code. Ordered for sequential fixing. Each item is
checked off only after a test locks it and verification passes.
Legend: [B] blocks delivery · [H] high · [M] medium · [L] low ·
OFF-HW = cannot be verified without the Hailo board.
Phase 1 — Silent failure & misconfiguration (the #1 delivery risk)
A safety system that looks healthy while doing nothing is the worst failure. Make every misconfig/degradation fail LOUD.
- P1-1 [H]
edge/cloudclient.py,edge/alert.py—requestssends the defaultpython-requests/x.yUser-Agent; cafe24's shared-hosting WAF commonly blocks it with an HTML 403 while curl passes. Every event then silently fails. Alsoallow_redirects=Trueturns the .htaccess http→https 302 into a GET → 405/404. Fix: custom UA (move-detect-edge/1.0) on every call +allow_redirects=False, log once on 3xx. DONE —USER_AGENTin alert.py, both clients send it +allow_redirects=False; tests in test_alert.py / test_cloudclient.py. - P1-2 [B]
deploy/systemd/move-detect-edge.env.example,setup_online.shprompt, docs —CAFE24_URLmust behttps://<domain>/move_detect(subdir mount). Empty/typo/ missing-subdir is 100% silent. Fix: examples show the subdir; installer validates. DONE — env.example + installer prompt now show/require the/move_detectsubdir. - P1-3 [H]
edge/config.py:40+edge/daemon.py— emptyCAFE24_URL/DEVICE_SECRETdefaults let the daemon run green while every upload fails and the queue grows forever. Fix: fail loud at startup if either is blank. DONE —require_upload_config(cfg)raises at daemon startup; test_config.py locks it. - P1-4 [H]
edge/daemon.py:23—run_loopswallows every per-frame/per-camera exception with no log and no degraded-monitoring signal → a permanently broken pipeline looks green. Fix: rate-limitedlogging.exception+ consecutive-error counter. DONE — per-camera + periodic error streaks, logged 1st + every 300th, names the camera; test_daemon.py locks the rate-limit. - P1-5 [H]
edge/daemon.py:33— heartbeat hardcodesfps=0.0and counts ticks wheresource.read()returned nothing, so the cloud cannot tell a dead device from a live one. Fix: real fps + per-camera frame-age/health in the heartbeat body. DONE — heartbeat now sends measured fps + cameras_total/cameras_erroring/errors; test_daemon.py locks fps math + failing-camera flag. - P1-6 [H]
edge/adapters/uvc_source.py:12— a camera that stops delivering frames is never detected or reopened; monitoring dies silently. Fix: fail loud on open failure; emitdevice_fault/monitoring_degradedwhen a camera goes stale. DONE — open-failure raises (build_runtimes logs+skips); read() releases+reopens after a streak; heartbeat's cameras_erroring (P1-5) reports the fault. test_uvc_source.py locks it. - P1-7 [M]
edge/queue.py— offline sqlite queue has no size cap/TTL; an outage (or a permanent misconfig) fills the SD card, then new events drop silently. Fix: bounded queue (row/byte budget), log dropped-oldest count. DONE —max_rows(default 2000) evicts oldest on enqueue; test_queue.py locks it. - P1-8 [H]
edge/queue.py:52— a permanently-rejected event (e.g. 400) is retried forever at the head, blocking the whole queue. Fix: drop/quarantine after N failures. DONE —attemptscolumn; a row is dropped aftermax_send_attempts(default 20) so a poison head unwedges the queue; test locks the drain-past-poison behavior. - P1-9 [M]
edge/alert.py:43— a POST that succeeds server-side but times out client-side is re-queued and re-sent; no dedup key → duplicate alerts. Fix: stable client-generatedevent_idin the payload; cloud dedups on it. DEFERRED to the cloud-ingest batch (needs a schema column + IngestController dedup); sequenced with Phase 4 / P4-7. - P1-10 [M]
edge/daemon.py:134—TARGET_FPS=0/non-numeric → ZeroDivision/ValueError at every start → infinite systemd restart loop. Fix: validate, default sanely. DONE —frame_interval_from_envvalidates; test_config.py locks it. - P1-11 [L]
edge/daemon.py:120—QUEUE_DBdefaults to relativequeue.db, unwritable under the hardened unit (ProtectSystem=strict) → crash loop when env omitted. Fix: absolute default under the state dir. DONE —queue_db_pathdefaults to /var/lib/move-detect/queue.db; test_config.py locks it.
Phase 2 — Core safety correctness (fall / zone / tracking)
- P2-1 [B]
edge/zones.py:19↔cloud/public/assets/zone-draw.js:196— zones are saved in 960×540 canvas pixels but the edge compares against camera-native pixels; every zone is silently misplaced. Fix: normalized 0..1 polygon contract on both sides. DONE — dashboard stores/loads 0..1 (zone-draw.js), edge normalizes the foot by frame size (zone.py → engine.process → run_once threads frame_wh); Polygon::validate is range-agnostic so PHP stores it opaquely. test_zone.py locks the normalized contract. - P2-2 [B]
edge/engine.py:72— apinch(협착) zone falls through toZONE_WARNING; the PINCH event + HIGH severity are never emitted. Fix: mappinch→PINCH,restricted→ZONE_RESTRICTED, else WARNING. DONE — dict map pinch→PINCH/restricted→ZONE_RESTRICTED; test_engine.py locks PINCH+HIGH. - P2-3 [H]
edge/detectors/fall.py:65— a per-frame keypoint-confidence gate silently disables fall, no_motion AND ghost for an occluded/low-confidence fallen person — exactly when someone is on the floor. Fix: use bbox cues when keypoints are low-conf. DONE — bbox cues (aspect/height-collapse) always run; keypoint cues gated by kp_ok; test_fall.py locks both the low-conf detection and the no-false-positive-standing case. - P2-4 [H]
edge/detectors/ghost.py:38— a ghost candidate is cancelled when ANY person (a colleague) passes near the loss point. Fix: only the plausibly-same track cancels it. DONE — candidate snapshots the active track ids at loss time; only a NEW track (not a known bystander) near the loss point cancels. test_ghost.py locks the bystander case. - P2-5 [H]
edge/geometry.py:66— stillness compares raw per-frame displacement to a fixed threshold with no dt normalization → immobility/no_motion flips with FPS. Fix: normalize by elapsed time. DONE — track_displacement divides by dt (per-second speed); test_geometry.py locks FPS-invariance. NOTE: motion_thr is now per-second and needs on-device re-tuning. - P2-6 [M]
edge/tracking.py:50— greedy IoU matching swaps IDs when workers cross, transferring FALLEN state + frozen baseline to the wrong person. Fix: gate matches; reset per-track safety state on a low-confidence match. - P2-7 [M]
edge/detectors/fall.py:92— a single prone-looking frame sets FALLING and arms a CRITICAL ghost if the person then leaves. Fix: require sustained prone. - P2-8 [M]
edge/app.py:30— detection dropouts keep a track 'active' with a frozen frame; time-based timers accrue on stale data. Fix: treat a stale track as no-data. - P2-9 [M]
edge/detectors/zone.py:45— foot point averages both ankles with no confidence check; a hallucinated ankle at (0,0) drags the foot into/out of a zone. Fix: confidence-gate the ankles, fall back to bbox-bottom-center. - P2-10 [M]
edge/adapters/movenet_pose.py:34— the CPU MoveNet fallback is single-person; with 2+ workers all but one are invisible to every detector, no warning. Fix: warn loudly that MoveNet fallback is single-person (Hailo is multi-person). - P2-11 [L]
edge/geometry.py:39—head_below_hipsuses the NOSE keypoint regardless of confidence. Fix: confidence-gate.
Phase 3 — Privacy / PIPA (delivery compliance)
- P3-1 [H]
edge/app.py:18—FaceBlurrerblurs a fixed ~40×40px patch regardless of face size; close faces stay identifiable. Fix: size the blur box from the person bbox. - P3-2 [H]
cloud/src/Ingest/IngestController.php:68— worker snapshots are stored forever; no retention/purge anywhere. PIPA violation. Fix:snapshot_retention_daysconfig + a token-guarded purge route +SnapshotRepository::purgeOlderThan. - P3-2b [M]
cloud/src/Dashboard/EventListController.php:17—site_scopeis stored in the session but never enforced; any authenticated user sees every device's worker snapshots. Fix: scope event/snapshot queries by the session'ssite_scope. - P3-3 [H]
edge/tools/fetch_movenet.py:12— downloads the ONNX from a mutable ref with no checksum; spec mandates SHA256. Fix: pin URL to a commit + sha256+size, mirrorfetch_hailo_hef.py.
Phase 4 — Cloud hardening (shared hosting)
- P4-1 [H]
cloud/public/index.php:106— no top-level try/catch; a PDOException becomes a raw 500 that leaks DSN/db_user ifdisplay_errorsis on (cafe24 often ships On). Fix: global try/catch → generic 500 +error_log;ini_set('display_errors','0'); ship.user.ini. - P4-2 [H]
cloud/src/Api/ConfigController.php:22,HeartbeatController.php— no rate limiting (unlike /api/events). Fix: per-IP + per-device limiter, fail-open on storage error. - P4-3 [M]
cloud/src/Repository/Pdo/PdoNonceRepository.php:30— nonces + rate_limits grow without bound; no cron on cafe24. Fix: opportunistic in-request reap of expired rows. - P4-4 [M]
cloud/src/Http/Router.php:18— route params are neverrawurldecoded; acamera_idwith a space/+/Korean saves zones but never loads them. Fix: decode captured params. - P4-5 [M]
cloud/src/Dashboard/EventListController.php:17— event list hard-capped at newest 100, no pagination/date filter; older events unreachable. Fix: cursor pagination. - P4-6 [M]
cloud/src/Repository/Pdo/PdoZoneRepository.php:86— editing a zone with a changed device_id/camera_id throws an uncaught RuntimeException → 500. Fix: 400 with a clear error. - P4-7 [L]
cloud/src/Ingest/IngestController.php:64— snapshot write failure is swallowed (201 with evidence lost); file-write/DB-insert non-atomic → orphan JPEGs. Fix: order write→insert, log failures, reconcile orphans. - P4-8 [L]
cloud/src/Auth/AuthController.php:31— login form emits a CSRF token thatloginnever checks. Fix: verify it (or drop it). - P4-9 [L]
cloud/src/Auth/AuthController.php:71— logout callsregenerate()aftersession_destroy()→ E_WARNING, old cookie not replaced. Fix: reorder. - P4-10 [L]
cloud/src/Ingest/EventValidator.php:26— no string length caps → an overlong id becomes a MySQL 'Data too long' 500 retry loop instead of a 400. Fix: cap lengths in the validator. - P4-11 [L]
cloud/src/Http/Request.php:45—pathForroutes any//...URI to/(parse_url scheme-relative). Fix: collapse/normalize.
Phase 5 — Install / bundle / supply-chain
- P5-1 [B]
deploy/bundle/install_offline.sh:79— the OFFLINE installer has no backend-usability gate (setup_online's [6/8] does): it installsPOSE_BACKEND=movenetverbatim even when the bundle carries no working backend. Fix: mirror the [6/8] probe. DONE — [5/6] probes HEF+hailo_platform / onnx+onnxruntime, reconciles POSE_BACKEND in the installed env, hard-fails if neither. test_deploy_artifacts.py locks the gate. - P5-2 [H]
deploy/bundle/build_offline_bundle.sh:55— a native build on x86 produces an aarch64-named tarball full of x86_64 wheels. Fix: assertuname -m == TARGET_ARCH. - P5-3 [M]
deploy/bundle/build_offline_bundle.sh:65— the air-gapped bundle never carries the Hailo HEF, so offline Hailo install is impossible. Fix: stage the HEF (viafetch_hailo_hef --arch) into the bundle. - P5-4 [M]
deploy/bundle/build_offline_bundle.sh:28— pip deps unpinned, no digests, MANIFEST has no hashes, bundle unsigned; spec requires SHA256/GPG + SBOM. Fix: pin + hash + manifest digests. - P5-5 [H]
deploy/systemd/move-detect-edge.service:46OFF-HW—DeviceAllowomits/dev/media*and/dev/dma_heap; picamera2/libcamera on Pi 5 needs them, so the CSI camera is denied by the unit's own policy. Fix: add the device classes; verify on hardware. - P5-6 [L]
cloud/deploy/build_cafe24_package.sh— dev cache dirs (.impeccable/,.ruff_cache/) can ship inside the web-served package. Fix: exclude.
Progress log
(updated as each item lands — newest first)
- 2026-07-17 — 17 items landed, all with tests, full suite green. ALL 4 BLOCKERS cleared (P1-2, P2-1, P2-2, P5-1) + all Phase-1 silent-failure (P1-1,3,4,5,6,7,8,10,11) + Phase-2 HIGH correctness (P2-3,4,5). Remaining: P1-9 (deferred to cloud batch), P2-6..11 (mostly threshold tuning — needs on-device validation), Phase 3 (privacy — P3-2 retention needs a policy decision: how many days), Phase 4 (cloud hardening, 11 items — mostly clear), Phase 5 (P5-2..6). Nothing committed yet.