Commit Graph

4 Commits

Author SHA1 Message Date
02b2c5bc39 "Open dashboard" in the tray did nothing reliable, and there is a Mac build
Reported from the shipped Windows app. Three faults in one call, and the
first is why it failed rather than merely misbehaved.

runtime.Show is implemented by Wails as a bare mainWindow.Show(), while
runtime.WindowShow wraps the identical work in runtime.LockOSThread. Win32
window operations have to run on the thread owning the window's message
pump, and the tray's handler runs on the SYSTRAY's goroutine, which is
never that thread. An unlocked Win32 call from an arbitrary goroutine is
the bug.

Two more that would each have been enough on their own:

- Showing is not un-minimising. Hidden and minimised are different states
  and Show only fixes the first, so a window the user minimised stayed
  minimised.
- Windows refuses the foreground to a process that does not already hold
  it, so the window came back BEHIND whatever was being looked at.
  Clicking a tray icon is by definition a moment when this app is not in
  front, so that is not an edge case here - it is every time. The
  always-on-top flip is the ordinary way to ask, and it is why this now
  runs in a goroutine rather than on the menu loop, which must not sleep.

The same four calls fix OnSecondInstanceLaunch, which had the same shape
and is reached far more often: double-clicking the desktop icon while the
app is already running.

OnBeforeClose used runtime.Hide against a reopen that used WindowShow -
different calls on Windows, one thread-locked and one not. Paired now.

And a Mac build, because the question came up and the answer turned out
to be yes. Wails' darwin frontend references UTType without linking
UniformTypeIdentifiers, so the build failed at the LINK step after
compiling everything - which reads like a broken toolchain rather than
one missing flag. There was no Mac version because of that, not because
of a design limit. darwin_link.go declares the framework in source rather
than leaving it as a CGO_LDFLAGS incantation, for the same reason
deploy.sh now finds Go itself. Verified: plain `go build` produces a
16 MB arm64 binary on this Mac, and the Windows build is unchanged.

Worth knowing for whoever edits that file: the comment directly above
`import "C"` is cgo's C preamble, not documentation. The first attempt put
the explanation there and the prose was compiled as C.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-29 17:42:39 +05:30
50a843ce46 The shop app starts once, and a second launch just shows the window
The window hides to the tray on close, so the natural next step for a
shop assistant is to double-click the shortcut again. That started a
second full copy of the app: a second tray icon, a second engine
supervisor on the same SQLite WAL and the same port - the start-twice
failure the agent package was built to prevent, on the one binary that
never had the guard. Seen on a Windows install as a row of tray icons.
Wails' SingleInstanceLock now hands the second launch to the first
process, which brings its window to the front.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-18 11:01:19 +05:30
5e544eee3d The camera tiles put a password in the page, and loaded nothing
StreamURL built http://user:pass@127.0.0.1:8010/api/cameras/<id>/
stream.mjpeg and handed it to an <img>, with a comment saying the
credentials were inline "so an <img> tag can load it".

It cannot. Chromium strips credentials from subresource URLs and has
since M59, and WebView2 is Chromium - so on the one platform this
product ships to, every camera tile on a shop counter was a broken
image. Measured against a running engine: the app's Go-side calls
returned stats and people while an <img> on that very URL failed, and
curl proved the URL answered 200. The engine was never the problem.

The password now stays on this side of the process boundary. A loopback
relay attaches Basic auth and streams the engine's bytes back
unchanged - the same reasoning Shot.jsx already follows at head office,
where an <img> equally cannot carry a session.

What the relay is careful about, since it is a door onto the biometric
API with a credential attached:

  - loopback only, on a port the OS picks; a fixed one would collide
    with whatever else a shop PC runs and read as "the cameras broke"
  - a per-run random token in the path. The engine's own credential
    exists so the live face feed is never served open; an
    unauthenticated relay would hand that feed to any other process on
    the PC. Compared in constant time, and a wrong one is 404, not 403
  - an allow-list of stream.mjpeg and frame.jpg. Holding the token does
    not reach the identity list, the gallery, or erasure
  - camera ids validated, not interpolated
  - every chunk flushed; a buffered MJPEG stream is a tile that never
    paints, which looks identical to the bug being fixed

Two of those were written after a test failed, not before:

  - `..` MATCHES the id pattern, because real camera ids contain dots.
    `/api/cameras/../stream.mjpeg` is not the endpoint anyone intended.
    The id can never hold a slash, so `.` and `..` are the whole
    remaining traversal surface and are now refused by name.
  - the serve goroutine read p.srv off the struct while stop() was
    nilling it, so a quick start/stop dereferenced nil and took the
    process down. Captured before launching now.

FrameURL is deliberately not added. No screen asks for a still, and a
bound method nothing calls is the same defect as a capability the UI
cannot reach, only pointing the other way.

Verified: nine unit tests, plus a live test against the real engine and
the real office camera - two MJPEG frames, 90,793 bytes, no credential
in the URL. Windows and darwin both build; vet clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pcn9asw19WGBfCEaHvNug6
2026-09-10 19:53:28 +05:30
dad04e8cda Behavision: face recognition for retail, edge to head office
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
2026-09-04 11:14:18 +05:30