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
94 lines
3.2 KiB
Go
94 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"image/png"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
// The tray icon shipped as a PNG for a while. systray hands the bytes to
|
|
// LoadImageW, which decodes .ico only, so Windows logged one line and drew
|
|
// nothing — and nothing on a Mac could notice. These tests are the substitute
|
|
// for the Windows box we do not have.
|
|
func TestEncodeICOIsAValidIconFile(t *testing.T) {
|
|
b := encodeICO(circle(colorFor("ok")))
|
|
if len(b) < 22 {
|
|
t.Fatalf("far too short: %d bytes", len(b))
|
|
}
|
|
if got := b[:6]; !bytes.Equal(got, []byte{0, 0, 1, 0, 1, 0}) {
|
|
t.Errorf("ICONDIR = % x, want 00 00 01 00 01 00", got)
|
|
}
|
|
if b[6] != iconSize || b[7] != iconSize {
|
|
t.Errorf("entry is %dx%d, want %dx%d", b[6], b[7], iconSize, iconSize)
|
|
}
|
|
if bpp := binary.LittleEndian.Uint16(b[12:14]); bpp != 32 {
|
|
t.Errorf("bits per pixel = %d, want 32", bpp)
|
|
}
|
|
// A wrong length here loads as a truncated or garbage icon rather than
|
|
// failing outright, which is the harder version of this bug to spot.
|
|
size := binary.LittleEndian.Uint32(b[14:18])
|
|
off := binary.LittleEndian.Uint32(b[18:22])
|
|
if off != 22 {
|
|
t.Errorf("image offset = %d, want 22", off)
|
|
}
|
|
if int(off)+int(size) != len(b) {
|
|
t.Errorf("entry claims %d bytes at %d, file is %d", size, off, len(b))
|
|
}
|
|
if h := binary.LittleEndian.Uint32(b[22:26]); h != 40 {
|
|
t.Errorf("BITMAPINFOHEADER size = %d, want 40", h)
|
|
}
|
|
// Height must be doubled for the implied AND mask or Windows draws the
|
|
// bottom half of the icon stretched over the whole square.
|
|
if hh := int32(binary.LittleEndian.Uint32(b[30:34])); hh != int32(iconSize*2) {
|
|
t.Errorf("biHeight = %d, want %d", hh, iconSize*2)
|
|
}
|
|
}
|
|
|
|
func TestEncodeICOPixelsAreBGRABottomUp(t *testing.T) {
|
|
want := colorFor("error") // red: distinguishable from B and G if swapped
|
|
img := circle(want)
|
|
b := encodeICO(img)
|
|
// Centre of the circle, which is solid fill. Bottom-up means image row
|
|
// iconSize/2 lands at DIB row iconSize/2-1 counting from the start.
|
|
row := iconSize - 1 - iconSize/2
|
|
i := 22 + 40 + (row*iconSize+iconSize/2)*4
|
|
got := b[i : i+4]
|
|
if !bytes.Equal(got, []byte{want.B, want.G, want.R, 0xFF}) {
|
|
t.Errorf("centre pixel = % x, want % x (BGRA)",
|
|
got, []byte{want.B, want.G, want.R, 0xFF})
|
|
}
|
|
}
|
|
|
|
func TestIconForMatchesThePlatformDecoder(t *testing.T) {
|
|
b := iconFor("ok")
|
|
pngMagic := []byte{0x89, 'P', 'N', 'G'}
|
|
if runtime.GOOS == "windows" {
|
|
if bytes.HasPrefix(b, pngMagic) {
|
|
t.Fatal("Windows tray icon is a PNG; LoadImageW will refuse it")
|
|
}
|
|
if !bytes.HasPrefix(b, []byte{0, 0, 1, 0}) {
|
|
t.Fatalf("Windows tray icon is not an ICO: % x", b[:4])
|
|
}
|
|
return
|
|
}
|
|
if _, err := png.Decode(bytes.NewReader(b)); err != nil {
|
|
t.Fatalf("non-Windows tray icon is not a decodable PNG: %v", err)
|
|
}
|
|
}
|
|
|
|
// The four states exist to be told apart at a glance; identical bytes would
|
|
// mean the icon never changes and the amber "running but blind" state — the
|
|
// one that otherwise goes unnoticed for weeks — looks exactly like healthy.
|
|
func TestEveryStateLooksDifferent(t *testing.T) {
|
|
seen := map[string]string{}
|
|
for _, s := range []string{"ok", "warn", "error", "stopped"} {
|
|
k := string(iconFor(s))
|
|
if prev, dup := seen[k]; dup {
|
|
t.Errorf("%q and %q render identically", prev, s)
|
|
}
|
|
seen[k] = s
|
|
}
|
|
}
|