Commit Graph

6 Commits

Author SHA1 Message Date
f5eb2bb124 The green button was disabled by an options block that was not there
Reported from the Mac build: the window cannot be maximised. It is not a
Wails limitation or a WebView quirk, it is an omission with a very
specific consequence.

Wails computes zoomable INSIDE `if frontendOptions.Mac != nil`:

    var fullSizeContent, hideTitleBar, zoomable, ... C.int   // 0
    if frontendOptions.Mac != nil {
        zoomable = bool2Cint(!frontendOptions.Mac.DisableZoom)
    }

and the native side then acts on the zero:

    if (!zoomable && resizable) {
        NSButton *button = [self.mainWindow
                              standardWindowButton:NSWindowZoomButton];
        [button setEnabled: NO];
    }

So leaving Mac unset does not mean "take the defaults" - it means the
green button is created and then explicitly disabled. There was a Windows
options block and no Mac one, which is how this survived: the platform
that was configured behaved, and the platform that was not looked broken.

Fixed by the block existing. The fields are written out rather than left
as an empty struct so it reads as a decision rather than something half
typed.

Verified at runtime rather than by reasoning about the source alone: all
three title-bar buttons report enabled=true through the accessibility
API, and the window resizes to 1440x900, the full display.

One correction to my own first check, recorded because it nearly sent me
the wrong way: querying AXFullScreenButton as an ATTRIBUTE of the window
returns "missing value" whether or not the button exists. It has to be
found by subrole among the window's buttons. The button was fine; the
question was wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 13:22:04 +05:30
e33761d6d0 A Mac build, and the two ways it crashed first
Chosen deliberately as a DEVELOPER build, not a product. Indian retail
counters are Windows; shipping a Mac product means an Apple Developer
account, notarisation, a second installer format, a second frozen engine
and DPAPI having no macOS equivalent - a permanent second platform for
customers who do not have Macs. What a Mac build is worth is demoing the
desktop app on the machine it is written on, without needing the Windows
box.

It built after one missing framework (previous commit) and then crashed
within a second, twice, both times in the tray:

  systray.Run              SIGTRAP inside cgo. nativeLoop() takes the
                           macOS main run loop for itself and Wails
                           already has it. macOS has exactly one.
  RunWithExternalLoop      "NSWindow should only be instantiated on the
                           main thread!" - it registers in the existing
                           NSApplication rather than starting a second,
                           but still builds AppKit objects, and Wails'
                           OnStartup is not the main thread.

Making it work needs the status item created through a main-queue
dispatch inside Wails' lifecycle. That is real work for a build whose
purpose is a demo, so macOS has no tray and the file says so at length
rather than leaving the next person to rediscover both crashes.

The consequence is handled rather than left lying. With no tray there is
no way back from a hidden window and no way to quit, so hiding on close
would strand a running engine behind no window, no tray and no control -
force-quit or nothing. On macOS closing the window therefore quits, and
OnShutdown stops the engine. Same rule the tray's Quit already follows:
never leave it watching with no visible control. Windows is untouched,
where hiding is correct because the tray is how it comes back.

The runner is split by build tag rather than branched at runtime because
the two platforms need different systray ENTRY POINTS, not different
arguments.

Verified: 18 seconds up, zero crash markers, 88 MB resident, and an
honest "engine not installed yet" instead of a crash - against a
throwaway data dir so it claimed nothing and touched no camera. The
frozen Mac engine is deliberately not built; the app takes an engine
command from config, which is how the dev setup already points at the
venv.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 11:44:49 +05:30
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