The bridge on the rack PC self-updates: it checks a release feed, tells you when a
new version is out, and installs it on your click (or automatically, if you turn on
auto-update). This doc covers how that works, the on-disk layout it relies on, and —
separately — how to update the tray itself.
Status: LIVE. Deployed on the rack PC (DEN-WS-EDIT) and verified end-to-end
(v0.1.0 → v0.1.1 installed cleanly, config preserved, bridge stayed healthy).
See .context/self-update-system.md for the full record.
On-disk layout (why it’s split)
Windows won’t let you rename a folder that contains a running program, so the
tray (which drives the update) must live outside the folder that gets swapped.
The install is per-user (under the operator account, av-admin) so neither the
deploy nor an update needs administrator elevation:
%LOCALAPPDATA%\Programs\BHAV Bridge\
mrx7d-bridge-tray.exe ← the tray: stable supervisor. NOT touched by updates.
app\ ← the bridge payload (dist + node_modules + package.json).
Swapped on update.
%LOCALAPPDATA%\BHAV Bridge\
data\ ← settings.json, mappings.json, aliases, dashboards.
NEVER touched by an update. BHAV_DATA_DIR points here.
The tray supervises the bridge and runs updates. It updates only the app\
payload — it stops the bridge, swaps app\, and relaunches it.
Runtime data lives in %LOCALAPPDATA%\BHAV Bridge\data, disjoint from app\.
Both the bridge and the tray resolve the same path via the BHAV_DATA_DIR
environment variable — the updater refuses to run if data sits inside app\
(it would be destroyed by the swap).
Environment the tray reads (set once, per-user)
The tray is configured entirely through user-level environment variables:
Variable
Value on the rack PC
Purpose
BRIDGE_DIR
…\Programs\BHAV Bridge\app
Where the bridge payload lives (the swap target)
BRIDGE_CMD
node dist/index.js
Run the built bridge (not the dev tsx default)
BHAV_DATA_DIR
…\BHAV Bridge\data
Where settings/mappings live
UPDATE_FEED_URL
https://git.kdsp.net/…/latest.json
The release feed the tray polls
FORGEJO_TOKEN
(read-only feed token)
Auth for the private release repo
Gotcha — session environment is a snapshot. Windows builds a process’s
environment block once, at logon. If you set/change any of these variables while
the operator is already logged in, a tray launched from the Startup shortcut (or
Start-Process) in that stale session won’t see them — it falls back to the
dev default (npx tsx src/index.ts) and the bridge fails to start with
ERR_MODULE_NOT_FOUND … src\index.ts. Fix: log off/on (or reboot). A fresh
logon rebuilds the environment from the registry and the Startup shortcut then
launches the tray correctly. A reboot is the canonical way to apply env changes and
doubles as the unattended-boot test.
Updating the bridge (the normal flow)
A release is cut (maintainer): tag vX.Y.Z → CI/build-win-app.ps1 builds the
payload → scripts/publish-release.ps1 zips the bridge payload + its SHA-256
and publishes a Forgejo Release with a latest.json manifest.
The tray notices on its next check and shows “Update available (vX.Y.Z)” in
the tray menu and the web UI’s Updates panel.
You install it — click Install in the tray menu, or hit the button in the
web UI (reachable over the mesh). Or turn on Auto-update to have it apply on
detection. Auto-update self-disables after 2 consecutive failed attempts and falls
back to manual + a notice, so a bad release can’t loop.
What happens under the hood: download → verify SHA-256 → stop the bridge →
back up app\ → extract the new payload → relaunch → health-check /api/status.
Any failure after the backup rolls back to the previous version automatically.
Your data in %ProgramData% is never touched.
Updating the tray (manual — rare)
The tray is deliberately outside the auto-update path (it’s the thing doing the
updating). It changes rarely; update it by hand:
Build the new tray on a Windows machine with Go + mingw-w64:
cd tray.\build-win-app.ps1 # produces mrx7d-bridge-tray.exe (+ embedded icon/version)
On the rack PC, quit the tray (right-click its icon → Quit — this also stops
the bridge) so its .exe is no longer locked.
Relaunch the tray. The clean way is to reboot (or log off/on): the Startup
shortcut fires in a fresh session with the full environment, brings the bridge back
up, and doubles as the unattended-boot test. (Double-clicking the Startup shortcut
in an already-running session also works, provided the env vars above were already
in place at logon — see the session gotcha above.)
Cross-compiling the tray from a Mac (what was used for the live deploy): with Go
This produces a working binary but with a blank icon and no embedded version
resource — fine for a hotfix; use build-win-app.ps1 on Windows for a release build.
Because the tray lives outside app\, this never disturbs the bridge payload or your
data. If you ever want the tray to self-update too, that needs a small detached
helper (Option A in the design) — not built, and not needed for normal operation.
Troubleshooting
“tray is installed inside the bridge dir” on install → the layout is wrong: the
tray.exe must be in the parent of app\, not inside it. Re-lay the install per the
layout above.
Update fails immediately, no disruption → checksum mismatch (bad/corrupt
download). The bridge was never stopped; just re-check and retry.
Update rolled back → a step after the swap failed (e.g. the new version didn’t
come healthy). The previous version was restored and relaunched. Check the tray log.
Auto-update turned itself off → two consecutive auto attempts failed; it fell
back to manual on purpose. Fix the release, then re-enable auto-update.
Tray is up but the bridge won’t start, bridge.log shows
exec: npx tsx src/index.ts and ERR_MODULE_NOT_FOUND … src\index.ts → the tray
didn’t see BRIDGE_DIR/BRIDGE_CMD (stale session env — see the gotcha above).
Reboot / log off-on and let the Startup shortcut relaunch it.
Update /check returns 503 briefly → historically a too-short proxy timeout;
fixed in v0.1.1 (bridge → tray proxy waits 30 s for the feed fetch). If you still see
it, the tray control server is down — restart the tray.