Five components that ship as one product:
- behavision/ the recognition engine. RTSP ingest, YuNet detection, IoU
tracking, ArcFace embeddings, a FAISS/SQLite gallery, and a
FastAPI dashboard. Identity is decided once per TRACK from an
average of at least three embeddings, never per frame.
- agent/ the Go edge agent: supervises the engine, holds a durable
spool, and drains it to MQTT. Nothing is acked before the
broker confirms.
- desktop/ the shop PC application (Wails + React + tray).
- server/ the cloud API, MQTT consumer, reports and assistant.
- web/ platform.loyaly.ai, the head-office app, embedded in the
server binary.
The gallery stores 512-float embeddings and timestamps - no images unless
`app.store_faces` is switched on. Those embeddings are biometric personal
data under GDPR and India's DPDP: template inversion reconstructs a
recognisable face from an ArcFace vector, so data/behavision.db is treated
as a biometric database and DELETE /api/visitors/{id} is a real erasure.
CLAUDE.md carries the reasoning behind every non-obvious decision here,
including the ones that were measured and the ones that were wrong first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
139 lines
5.9 KiB
Python
139 lines
5.9 KiB
Python
"""Static checks on the dashboard page.
|
|
|
|
The page is plain HTML+JS with no build step, which is a deliberate choice —
|
|
but it means nothing catches a typo'd element id or an unescaped
|
|
interpolation until a user opens the page. These tests are that safety net,
|
|
and they need no browser and no node.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PAGE = Path(__file__).parent.parent / "behavision" / "static" / "dashboard.html"
|
|
HTML = PAGE.read_text(encoding="utf-8")
|
|
SCRIPT = re.search(r"<script>(.*?)</script>", HTML, re.S).group(1)
|
|
IDS = set(re.findall(r'id="([^"]+)"', HTML))
|
|
|
|
|
|
def test_every_getelementbyid_target_exists():
|
|
"""A mistyped id fails silently as `null.innerHTML` at runtime, on a page
|
|
nobody runs in CI."""
|
|
referenced = set(re.findall(r"getElementById\('([^']+)'\)", SCRIPT))
|
|
assert referenced <= IDS, f"no such element: {sorted(referenced - IDS)}"
|
|
|
|
|
|
def test_every_form_field_in_F_has_an_input():
|
|
"""Fields are addressed as `f-<name>` built from the F list, so the static
|
|
check above cannot see them."""
|
|
fields = re.search(r"const F = \[(.*?)\];", SCRIPT, re.S).group(1)
|
|
names = re.findall(r"'([^']+)'", fields)
|
|
assert names, "F list not found"
|
|
missing = [n for n in names if f"f-{n}" not in IDS]
|
|
assert not missing, f"form fields with no input: {missing}"
|
|
|
|
|
|
def test_placement_wizard_is_present():
|
|
for needed in ("wizard", "wz-body", "wz-close", "wz-again", "wz-loosen"):
|
|
assert needed in IDS, f"missing {needed}"
|
|
|
|
|
|
def test_loosening_the_gate_is_only_offered_for_a_marginal_camera():
|
|
"""For a poor camera the answer is to move it. Dropping the gate there
|
|
turns a visible miss into an invisible wrong match, which is worse."""
|
|
handler = re.search(r"wz-loosen'\)\.hidden =(.*?);", SCRIPT, re.S).group(1)
|
|
assert "marginal" in handler
|
|
|
|
|
|
def test_camera_settings_ui_is_present():
|
|
for needed in ("cam-new", "cam-list", "cam-form", "cam-save",
|
|
"cam-test-btn", "cam-cancel"):
|
|
assert needed in IDS, f"missing {needed}"
|
|
|
|
|
|
def test_user_supplied_values_are_escaped():
|
|
"""Identity labels and camera ids are user input and this page has already
|
|
had one stored-XSS bug. A bare `${x.label}` is that bug coming back.
|
|
|
|
Scoped to template literals that build markup — a literal containing a tag
|
|
is destined for innerHTML. Interpolating into `textContent` needs no
|
|
escaping and must not be flagged, or the test trains people to ignore it.
|
|
"""
|
|
literals = re.findall(r"`([^`]*)`", SCRIPT, re.S)
|
|
offenders = []
|
|
for lit in literals:
|
|
if "<" not in lit:
|
|
continue
|
|
offenders += re.findall(
|
|
r"\$\{\s*[A-Za-z_][\w]*(?:\.[\w]+)+\s*\}", lit)
|
|
assert not offenders, f"unescaped interpolation into markup: {offenders}"
|
|
|
|
|
|
def test_live_feeds_are_not_rebuilt_on_every_refresh():
|
|
"""Re-assigning an MJPEG <img> src restarts the stream. Rebuilding the
|
|
feeds on the 3s refresh would leave every camera flickering forever, so
|
|
renderFeeds must bail out when the camera set is unchanged."""
|
|
body = re.search(r"function renderFeeds\(cams\) \{(.*?)\n\}", SCRIPT, re.S)
|
|
assert body, "renderFeeds not found"
|
|
assert "return" in body.group(1).split("feedKey = key")[0], \
|
|
"renderFeeds must return early when the camera set has not changed"
|
|
|
|
|
|
def test_feeds_use_id_not_camera_id():
|
|
"""`camera_id` comes from worker.stats() and only exists while the worker
|
|
runs; a stored camera that failed to start has only `id`. Using the wrong
|
|
one put the literal string 'undefined' in the stream URL."""
|
|
feeds = re.search(r"function renderFeeds\(cams\) \{(.*?)\n\}", SCRIPT, re.S)
|
|
assert "camera_id" not in feeds.group(1)
|
|
|
|
|
|
def test_script_is_syntactically_valid():
|
|
"""Caught a real `const cams` redeclaration the first time it ran.
|
|
Skipped when node is absent - the suite stays dependency-light."""
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
node = shutil.which("node")
|
|
if not node:
|
|
pytest.skip("node not installed")
|
|
tmp = Path(tempfile.mkdtemp()) / "dashboard.js"
|
|
tmp.write_text(SCRIPT)
|
|
r = subprocess.run([node, "--check", str(tmp)],
|
|
capture_output=True, text=True)
|
|
assert r.returncode == 0, r.stderr
|
|
|
|
|
|
def test_hidden_elements_are_actually_hidden():
|
|
"""An author `display:` rule beats the UA stylesheet's
|
|
`[hidden] { display: none }` — same specificity, author sheet wins. The
|
|
placement wizard is a `position:fixed` full-screen modal styled
|
|
`display:flex`, so it sat open over the dashboard on every page load until
|
|
a matching `[hidden]` rule was added. Nothing in the suite noticed, because
|
|
every other check reads the markup and the JS rather than the CSS.
|
|
|
|
Any element that carries the `hidden` attribute or is toggled through the
|
|
`.hidden` property must not be given an unconditional `display` by id.
|
|
"""
|
|
style = re.search(r"<style>(.*?)</style>", HTML, re.S).group(1)
|
|
toggled = set(re.findall(r'id="([^"]+)"[^>]*\shidden[\s>]', HTML))
|
|
toggled |= set(re.findall(r"getElementById\('([^']+)'\)\.hidden\s*=", SCRIPT))
|
|
assert toggled, "no hidden-toggled elements found - has the page changed?"
|
|
|
|
for name in sorted(toggled):
|
|
# Rules that set display for this id, ignoring any that are themselves
|
|
# qualified by [hidden] (those are the fix, not the bug).
|
|
rules = re.findall(r"#%s(\[[^\]]*\])?\s*\{([^}]*)\}" % re.escape(name),
|
|
style)
|
|
sets_display = [q for q, body in rules
|
|
if "display" in body and "[hidden]" not in (q or "")]
|
|
if not sets_display:
|
|
continue
|
|
guard = re.search(r"#%s\[hidden\]\s*\{[^}]*display\s*:\s*none"
|
|
% re.escape(name), style)
|
|
assert guard, (
|
|
f"#{name} is toggled with the `hidden` attribute but its CSS sets "
|
|
f"`display` unconditionally, which overrides it. Add "
|
|
f"`#{name}[hidden] {{ display: none; }}`."
|
|
)
|