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
2 lines
9.3 KiB
CSS
2 lines
9.3 KiB
CSS
:root{--ground: #0E1317;--surface: #161D23;--surface-2: #1D262D;--line: #27333B;--line-soft: #1F2A31;--ink: #E7EEF3;--ink-2: #B4C2CC;--muted: #7C8B97;--accent: #45B0C7;--accent-dim:#123039;--ok: #4FB37B;--warn: #E0A33A;--bad: #E0655A;--radius: 8px;--mono: "SFMono-Regular", ui-monospace, Menlo, Consolas, monospace}*{box-sizing:border-box;margin:0}html,body,#root{height:100%}body{background:var(--ground);color:var(--ink);font:14px/1.55 system-ui,-apple-system,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased;overflow:hidden;-webkit-user-select:none;user-select:none}button,input,select,textarea{font:inherit;color:inherit}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.shell{display:grid;grid-template-columns:216px 1fr;height:100%}.side{background:var(--surface);border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0}.side .brand{padding:18px 18px 14px;border-bottom:1px solid var(--line-soft)}.side .brand h1{font-size:15px;font-weight:650;letter-spacing:-.01em}.side .brand p{font-size:11.5px;color:var(--muted);margin-top:3px}.nav{padding:10px;display:flex;flex-direction:column;gap:2px;flex:1}.nav button{display:flex;align-items:center;gap:10px;width:100%;background:none;border:0;border-radius:6px;padding:8px 10px;color:var(--ink-2);cursor:pointer;text-align:left;font-size:13.5px}.nav button:hover{background:var(--surface-2);color:var(--ink)}.nav button[aria-current=page]{background:var(--accent-dim);color:var(--accent);font-weight:550}.nav .glyph{width:16px;text-align:center;opacity:.85;font-size:13px}.enginebox{padding:12px;border-top:1px solid var(--line-soft)}.enginebox .row{display:flex;align-items:center;gap:8px;font-size:12px}.enginebox .label{color:var(--muted);font-size:11px;margin-top:2px;display:block;line-height:1.4}.enginebox .actions{display:flex;gap:6px;margin-top:10px}.main{min-width:0;min-height:0;overflow-y:auto}.page{padding:22px 26px 40px;max-width:1180px}.page>header{margin-bottom:18px}.page h2{font-size:19px;font-weight:620;letter-spacing:-.01em}.page header p{color:var(--muted);font-size:13px;margin-top:3px}.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:16px}.card h3{font-size:12px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);font-weight:600;margin-bottom:12px}.grid{display:grid;gap:14px}.cols-4{grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}.cols-2{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.stat .value{font-size:30px;font-weight:620;letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1.1}.stat .unit{font-size:15px;color:var(--muted);margin-left:3px}.stat .sub{color:var(--muted);font-size:12px;margin-top:5px}.dot{width:8px;height:8px;border-radius:50%;flex:none}.dot.ok{background:var(--ok)}.dot.warn{background:var(--warn)}.dot.bad{background:var(--bad)}.dot.idle{background:var(--muted)}.pill{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:3px 8px;border-radius:99px;border:1px solid var(--line);color:var(--muted);white-space:nowrap}.pill.ok{color:var(--ok);border-color:#2b5c42;background:#12251b}.pill.warn{color:var(--warn);border-color:#5c4a22;background:#241d0f}.pill.bad{color:var(--bad);border-color:#5c2e2a;background:#241312}.btn{background:var(--surface-2);border:1px solid var(--line);border-radius:6px;padding:7px 13px;cursor:pointer;font-size:13px;color:var(--ink);white-space:nowrap}.btn:hover:not(:disabled){background:#26323a}.btn:disabled{opacity:.45;cursor:default}.btn.primary{background:var(--accent);border-color:var(--accent);color:#06222a;font-weight:600}.btn.primary:hover:not(:disabled){background:#5ac0d6}.btn.danger{color:var(--bad);border-color:#4a2823}.btn.sm{padding:4px 9px;font-size:12px}.field{display:block;margin-bottom:12px}.field span{display:block;font-size:11.5px;color:var(--muted);margin-bottom:4px;letter-spacing:.01em}.field input,.field select,.field textarea{width:100%;background:var(--ground);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-size:13.5px;-webkit-user-select:text;user-select:text}.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--accent);outline:none}.field textarea{resize:vertical;min-height:66px}.fieldrow{display:grid;gap:0 12px;grid-template-columns:1fr 1fr}table{width:100%;border-collapse:collapse;font-size:13px}th{text-align:left;font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;padding:8px 10px;border-bottom:1px solid var(--line)}td{padding:9px 10px;border-bottom:1px solid var(--line-soft);vertical-align:middle}tr:last-child td{border-bottom:0}tbody tr.click{cursor:pointer}tbody tr.click:hover{background:var(--surface-2)}td.num{font-variant-numeric:tabular-nums;text-align:right}.tablewrap{overflow-x:auto}.empty{color:var(--muted);font-size:13px;padding:26px 4px;text-align:center}.err{border:1px solid #5c2e2a;background:#241312;color:#f0b3ad;border-radius:6px;padding:10px 12px;font-size:13px;margin-bottom:14px}.note{color:var(--muted);font-size:12.5px}.mono{font-family:var(--mono);font-size:12px}.login{height:100%;display:grid;place-items:center;padding:24px}.login .box{width:100%;max-width:380px}.login h1{font-size:21px;font-weight:650;letter-spacing:-.015em}.login .lead{color:var(--muted);font-size:13px;margin:6px 0 22px}.login form{background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:20px}.login .btn{width:100%;margin-top:6px}.login .foot{color:var(--muted);font-size:11.5px;margin-top:14px;text-align:center;line-height:1.5}.login .alt{margin-top:18px;padding-top:16px;text-align:center;border-top:1px solid var(--line-soft)}.login .alt .note{line-height:1.55;margin-bottom:12px;text-align:left}.login .alt .btn{margin-top:0}.linkbtn{background:none;border:0;padding:0;cursor:pointer;font:inherit;font-size:12.5px;color:var(--accent);text-decoration:underline;text-underline-offset:3px}.linkbtn:hover{color:var(--ink)}.feeds{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}.feed{background:#000;border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}.feed img{width:100%;display:block;aspect-ratio:16/9;object-fit:cover;background:#000}.feed .cap{display:flex;justify-content:space-between;align-items:center;padding:8px 11px;background:var(--surface);font-size:12.5px}.events{list-style:none;max-height:420px;overflow-y:auto}.events li{display:flex;gap:9px;align-items:baseline;padding:7px 2px;border-bottom:1px solid var(--line-soft);font-size:12.5px}.events li:last-child{border-bottom:0}.events .when{color:var(--muted);font-family:var(--mono);font-size:11px;flex:none}.tag{font-size:10px;padding:2px 6px;border-radius:4px;flex:none;background:var(--surface-2);color:var(--muted)}.tag.new{background:#17364f;color:#86c2ec}.tag.seen{background:#14301f;color:#7fcb9c}.tag.miss{background:#3a1c1a;color:#eb9a92}.bars{display:flex;align-items:flex-end;gap:3px;height:150px;margin-top:4px}.bars .col{flex:1;display:flex;flex-direction:column;justify-content:flex-end;gap:2px;min-width:0}.bars .seg{border-radius:2px 2px 0 0}.bars .seg.ret{background:var(--accent)}.bars .seg.new{background:#2f6f81}.axis{display:flex;justify-content:space-between;color:var(--muted);font-size:10.5px;margin-top:6px;font-family:var(--mono)}.key{display:flex;gap:14px;font-size:11.5px;color:var(--muted);margin-top:10px}.key i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;vertical-align:-1px}.drawer{position:fixed;top:0;right:0;bottom:0;left:0;background:#04080a99;display:flex;justify-content:flex-end;z-index:30}.drawer .panel{width:min(480px,100%);height:100%;background:var(--surface);border-left:1px solid var(--line);overflow-y:auto;padding:20px 22px 40px}.drawer h3{font-size:16px;font-weight:620;text-transform:none;letter-spacing:-.01em;color:var(--ink);margin-bottom:2px}.who{position:sticky;top:-20px;z-index:1;display:flex;gap:14px;align-items:flex-start;background:var(--surface);margin:-20px -22px 18px;padding:20px 22px 14px;border-bottom:1px solid var(--line-soft)}.who .grow{flex:1;min-width:0}.who h3{margin-bottom:2px}.avatar{width:64px;height:64px;border-radius:10px;flex:none;object-fit:cover;background:var(--ground);border:1px solid var(--line)}.avatar.none{display:grid;place-items:center;color:var(--muted);font-size:20px;font-weight:600;letter-spacing:.02em}.timeline{list-style:none;max-height:220px;overflow-y:auto}.timeline li{display:flex;gap:10px;align-items:baseline;padding:6px 0;border-bottom:1px solid var(--line-soft);font-size:12.5px}.timeline li:last-child{border-bottom:0}.timeline .when{font-family:var(--mono);font-size:11px;color:var(--muted);flex:none;min-width:108px}.timeline .where{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.danger-zone{margin-top:22px;border-color:#4a2823}.danger-zone>h3{color:var(--bad)}.danger-zone .note{margin-bottom:10px}.confirm h4{font-size:13.5px;font-weight:620;margin-bottom:10px}.confirm .cols{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:12px}@media (max-width: 560px){.confirm .cols{grid-template-columns:1fr}}.confirm .lbl{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);margin-bottom:5px}.confirm .lbl.bad{color:var(--bad)}.confirm ul{list-style:none;font-size:12.5px}.confirm li{padding:3px 0 3px 12px;position:relative;color:var(--ink)}.confirm li:before{content:"·";position:absolute;left:2px;color:var(--muted)}.confirm .row{display:flex;gap:8px}
|