6 Commits

Author SHA1 Message Date
julian 7d67934a10 Merge branch 'dev' into stage
Build & push images / images (push) Successful in 2m48s
2026-09-03 17:29:31 +02:00
julian 56904422af feat(desktop): show the installed app's own version in the UI
Build desktop / desktop (push) Successful in 4m47s
Build & push images / images (push) Successful in 2m54s
CI / check (push) Successful in 42s
Nothing displayed which desktop build was actually installed — debugging
a stuck update meant inferring the current version backwards from the
update prompt's target version. Added DesktopVersionBadge (next to the
existing server-side VersionBadge) using @tauri-apps/api's getVersion(),
the real running app version baked in from tauri.conf.json. No-ops in a
browser. Exported inTauri() from origin.ts instead of redefining it again.
2026-09-03 16:31:48 +02:00
julian 8bcdea9e4a Merge remote-tracking branch 'origin/dev' into stage
Build & push images / images (push) Successful in 2m48s
2026-09-03 16:24:44 +02:00
julian 7804285dec fix(desktop): route update-failure logging through logClient, not console
Build desktop / desktop (push) Successful in 4m44s
Build & push images / images (push) Successful in 2m50s
CI / check (push) Successful in 43s
console.error/console.warn only forward to the server when the client log
level is debug/trace (default: info) — the earlier error-logging fix never
actually surfaced anything, and a real update failure produced zero logs
anywhere. desktop-updater.ts now calls logClient() directly, unconditionally,
plus download-progress events. Also documents the resource-sync-park-systems
branch misconfig (pointed at dev, Stacks are stage-tier) found while chasing
this — full writeup on fleet-deployment-komodo.md.
2026-09-03 16:23:02 +02:00
julian 4a7029cea6 chore(resources): bump stage TAG to 7317042
Build & push images / images (push) Successful in 2m49s
Promotes park-buzi + park-2 to the just-merged desktop-app fixes (login,
mixed-content routing, WS origin) and the WS_ALLOWED_ORIGINS fix — none of
this was on stage before. Wait for build-images.yml to confirm the image
actually exists before syncing/deploying in Komodo.
2026-09-03 16:04:57 +02:00
julian 7317042e8d fix(desktop): WS live feed offline — native plugin sends no Origin header
Build desktop / desktop (push) Successful in 4m42s
CI / check (push) Successful in 43s
Release desktop / bundle (push) Successful in 4m43s
Build & push images / images (push) Successful in 2m46s
Login worked after the mixed-content fix, but the live feed 403'd silently:
tauri-plugin-websocket's connect() runs on Tauri's Rust side, not inside the
webview page, so it never auto-attaches Origin the way a browser WebSocket
would — routes/ws.ts's anti-CSWSH check rejects a missing Origin before
auth. platform-ws.ts now sets Origin: tauri://localhost explicitly.

Also fixes a second, independent gap the above alone wouldn't have caught:
komodo/resources.toml's booth Stacks had WS_ALLOWED_ORIGINS= empty in
production despite .env.example documenting it as required for desktop.
Needs a Komodo sync + redeploy to reach a live booth.
2026-09-03 15:35:04 +02:00
10 changed files with 174 additions and 13 deletions
+1
View File
@@ -18,6 +18,7 @@
"@radix-ui/react-tabs": "^1.1.15",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-router": "^1.170.16",
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-http": "^2.5.2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
+32 -4
View File
@@ -9,6 +9,17 @@
// and never tries to resolve the Tauri APIs. Offline-first: a failed check (no
// network — the appliance is usually offline) is swallowed; updates only happen
// when someone has brought the box online (e.g. a phone hotspot) on purpose.
//
// A release build's console.error is invisible with no way to attach devtools
// in the field (kiosk mode blocks the context menu; this WebKitGTK build's
// remote inspector doesn't answer standard discovery endpoints either — both
// confirmed dead ends 2026-09-03). logClient() ships straight to the
// server-side app_logs store regardless of the client's console-forward log
// level (that gate is meant for noisy console chatter, not this), so a real
// post-accept install failure is visible via wiki/concepts/app-logs.md /
// LogsViewer.tsx without needing a terminal or devtools at all.
import { logClient } from "./logger.js";
/** True when running inside the Tauri webview (not a normal browser). */
function inTauri(): boolean {
@@ -42,13 +53,24 @@ export async function checkForDesktopUpdate(
// Download + install the signed update (signature verified against the
// pubkey in tauri.conf.json), then relaunch into the new version.
try {
await update.downloadAndInstall();
await update.downloadAndInstall((progress) => {
logClient({
level: "info",
message: `desktop update download progress: ${progress.event}`,
context: { kind: "desktop_update_progress", version: update.version, event: progress.event },
});
});
} catch (err) {
// A real update WAS found and accepted — this is a genuine install
// failure (bad signature, corrupted download, disk/permission issue),
// not "offline". Surface it instead of silently reverting to the old
// version with no explanation.
console.error("desktop update download/install failed:", err);
logClient({
level: "error",
message: `desktop update download/install failed: ${err instanceof Error ? err.message : String(err)}`,
stack: err instanceof Error ? err.stack : undefined,
context: { kind: "desktop_update_install_failed", version: update.version },
});
throw err;
}
const { relaunch } = await import("@tauri-apps/plugin-process");
@@ -56,7 +78,13 @@ export async function checkForDesktopUpdate(
} catch (err) {
// Offline / endpoint unreachable / no update server yet → ignore. The app
// keeps running on the current version; checking again next launch. Still
// log it so a real install failure (rethrown above) isn't invisible.
console.warn("desktop update check/apply skipped:", err);
// log it (info, not error — this path is expected/normal far more often
// than it's a real problem) so a real install failure (rethrown above,
// logged as error) isn't lost among routine offline checks.
logClient({
level: "info",
message: `desktop update check/apply skipped: ${err instanceof Error ? err.message : String(err)}`,
context: { kind: "desktop_update_skipped" },
});
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ export function wsUrl(path: string): string {
}
/** True when running inside the Tauri webview (not a normal browser). */
function inTauri(): boolean {
export function inTauri(): boolean {
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
+9 -2
View File
@@ -62,7 +62,13 @@ class TauriSocketAdapter implements PlatformSocket {
try {
const { default: TauriWebSocket } = await import("@tauri-apps/plugin-websocket");
if (this.#closed) return; // close() called before connect resolved
const conn = await TauriWebSocket.connect(url);
// Runs on Tauri's native (Rust) side, NOT inside the webview page — there
// is no page context to auto-attach an Origin header the way a real
// browser WebSocket would. The server's anti-CSWSH check (routes/ws.ts)
// rejects any handshake with a missing/mismatched Origin, so it must be
// set explicitly here to match what WS_ALLOWED_ORIGINS expects
// (tauri://localhost — see apps/server/.env.example).
const conn = await TauriWebSocket.connect(url, { headers: { Origin: "tauri://localhost" } });
if (this.#closed) {
void conn.disconnect();
return;
@@ -78,7 +84,8 @@ class TauriSocketAdapter implements PlatformSocket {
// routes/ws.ts) — nothing else is expected.
});
this.onopen?.();
} catch {
} catch (err) {
console.error("Tauri WebSocket connect failed:", url, err);
this.onerror?.();
this.onclose?.();
}
+26 -1
View File
@@ -6,7 +6,7 @@ import {
Outlet,
redirect,
} from "@tanstack/react-router";
import { lazy, Suspense, useState } from "react";
import { lazy, Suspense, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import type { Lang, Permission, SessionUser, Theme } from "./api.js";
@@ -30,6 +30,7 @@ import { Spinner } from "./ui/Spinner.js";
import { setLanguage } from "./lib/i18n/index.js";
import { applyTheme, applyFontScale } from "./lib/theme.js";
import { useLiveFeed } from "./lib/use-live-feed.js";
import { inTauri } from "./lib/origin.js";
import { useShift } from "./lib/use-shift.js";
import { DeviceFooter } from "./ui/DeviceFooter.js";
import { StatusDot } from "./ui/StatusDot.js";
@@ -106,6 +107,29 @@ function VersionBadge() {
return <span className="ml-auto shrink-0 pl-3 text-[0.7rem] text-term-muted">{version}</span>;
}
/** The installed Tauri app's own "vX.Y.Z" (from tauri.conf.json, synced to the git tag by
* release.yml — see wiki/decisions/desktop-shell-tauri.md) — the client's version, distinct
* from VersionBadge's SERVER build. No-op / renders nothing in a browser (there's no Tauri
* API to call). Was invisible before this: an operator had no way to tell which desktop
* build was actually installed short of reading the update-available prompt. */
function DesktopVersionBadge() {
const [version, setVersion] = useState<string | null>(null);
useEffect(() => {
if (!inTauri()) return;
let cancelled = false;
void import("@tauri-apps/api/app").then(({ getVersion }) =>
getVersion().then((v) => {
if (!cancelled) setVersion(v);
}),
);
return () => {
cancelled = true;
};
}, []);
if (!version) return null;
return <span className="ml-auto shrink-0 pl-3 text-[0.7rem] text-term-muted">app v{version}</span>;
}
/** Setup layout — the config hub. Renders a permission-gated tab bar and the active
* tab's screen via <Outlet>. Each tab is a child route (its own URL + guard), so
* deep links and the back button work and a denied tab redirects to the booth. */
@@ -125,6 +149,7 @@ function SetupLayout() {
{show("log:read") && <SetupTab to="/setup/logs" label={t("nav.logs")} />}
{show("backup:read") && <SetupTab to="/setup/backup" label={t("nav.backup")} />}
{show("site:read") && <VersionBadge />}
<DesktopVersionBadge />
</nav>
<Outlet />
</div>
+10 -4
View File
@@ -49,10 +49,13 @@ REGISTRY=git.infra.msai.al/mca/parking_solution
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
# exists as the pointer; we deploy the sha, not the mover.
TAG=stage-28bd838
TAG=stage-7317042
COOKIE_SECURE=0
VISION_ENABLED=1
WS_ALLOWED_ORIGINS=
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
# platform-ws.ts, since the native WS plugin has no page context to auto-attach
# one). Linux may also send http://tauri.localhost. See routes/ws.ts anti-CSWSH check.
WS_ALLOWED_ORIGINS=tauri://localhost,http://tauri.localhost
JWT_SECRET=[[park_buzi_jwt_secret]]
EVENT_SIGNING_KEY=[[park_buzi_event_signing_key]]
BACKUP_KEY=[[park_buzi_backup_key]]
@@ -79,10 +82,13 @@ REGISTRY=git.infra.msai.al/mca/parking_solution
# Staging booth: pinned immutable stage-<sha>. After each promotion (merge dev → stage, CI builds
# :stage-<sha>), bump this to the new sha and re-sync/deploy from Core. The moving `:stage` tag
# exists as the pointer; we deploy the sha, not the mover.
TAG=stage-28bd838
TAG=stage-7317042
COOKIE_SECURE=0
VISION_ENABLED=1
WS_ALLOWED_ORIGINS=
# Desktop app WS handshake: Origin is tauri://localhost (set explicitly by
# platform-ws.ts, since the native WS plugin has no page context to auto-attach
# one). Linux may also send http://tauri.localhost. See routes/ws.ts anti-CSWSH check.
WS_ALLOWED_ORIGINS=tauri://localhost,http://tauri.localhost
JWT_SECRET=[[park_2_jwt_secret]]
EVENT_SIGNING_KEY=[[park_2_event_signing_key]]
BACKUP_KEY=[[park_2_backup_key]]
+3
View File
@@ -108,6 +108,9 @@ importers:
'@tanstack/react-router':
specifier: ^1.170.16
version: 1.170.16(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
'@tauri-apps/api':
specifier: ^2.11.1
version: 2.11.1
'@tauri-apps/plugin-http':
specifier: ^2.5.2
version: 2.6.0
+24
View File
@@ -164,6 +164,30 @@ Per the user's choices — the operator **keeps OS access** (no fullscreen lockd
- `logger.ts`'s `flushBeacon()` (page-hide `navigator.sendBeacon`) is a native browser API with no
Tauri equivalent — it still drops silently in the desktop shell on unload. Accepted: the regular
4s-interval flush (now fixed, routes through `platformFetch`) covers the common case.
- **Gotcha (found immediately after shipping the above): the native WS plugin sends no `Origin`
header.** `tauri-plugin-websocket`'s `connect()` runs on Tauri's Rust side, not inside the
webview page — there's no page context to auto-attach `Origin: tauri://localhost` the way a real
browser `WebSocket` would. The server's anti-CSWSH check (`routes/ws.ts`, `isAllowedOrigin`)
treats a missing Origin as untrusted and 403s the handshake before touching auth — the live feed
showed **"JASHTË LINJË"** (offline) in the desktop app while the browser showed **"LIVE"**, same
server, same moment. **Fix (two parts, both needed):** `platform-ws.ts`'s `connect()` call now
passes `{ headers: { Origin: "tauri://localhost" } }` explicitly; separately, `komodo/
resources.toml`'s booth Stacks had `WS_ALLOWED_ORIGINS=` **empty** in production (despite
`.env.example` documenting `tauri://localhost,http://tauri.localhost` as required) — even a
correct Origin header is useless if the server's allowlist doesn't include it. Both fixed
together; a `resources.toml` change still needs a Komodo sync + Stack redeploy to take effect on
a live booth, it isn't automatic from a git push alone — and see [[fleet-deployment-komodo]] for
a real ResourceSync-branch gotcha this exact fix ran into.
- **No way to see the installed app's own version (found + fixed 2026-09-03).** `VersionBadge` in
`router.tsx` shows the *server's* `<branch>-<sha>` (from `/api/version`, gated `site:read`) — but
nothing showed the *desktop client's* own version. An operator debugging a stuck update had no way
to confirm which build was actually installed short of reading the update-available prompt's
target version and inferring backwards. Fixed with `DesktopVersionBadge`, next to `VersionBadge`:
calls `@tauri-apps/api/app`'s `getVersion()` (the real running app's version, baked in from
`tauri.conf.json` — synced to the git tag by `release.yml`, see the version-drift gotcha above),
no-ops/renders nothing in a browser (`inTauri()` guard, now exported from `origin.ts` instead of
redefined a 4th time). `@tauri-apps/api` added as an explicit dependency (was only ever transitive
via the plugins).
- **`VITE_API_BASE` — desktop vs. browser (regression found + fixed 2026-09-03):**
`apps/web/.env.production` (committed, shared by both builds) sets `VITE_API_BASE=` (empty) — this
is correct for the **browser/booth** build (Fastify same-origin, stays relative) since commit
+30 -1
View File
@@ -2,7 +2,7 @@
type: decision
tags: [parking, deployment, fleet, komodo, netbird, offline-first, threat-model]
sources: []
updated: 2026-07-07
updated: 2026-09-03
status: settled
---
@@ -192,3 +192,32 @@ FILE is read from, each stack's `branch` picks its compose files, `TAG` picks th
secrets even in the lab (blast radius). The lab box earned its keep immediately: it caught the
USB close-cancel truncation, the printer/controller wizard gate, and the Periphery v2.2.0
root_directory default before any of them reached a real booth ([[appliance-provisioning]] §7a).
## ResourceSync branch drift — the exact gotcha this page already warned about (2026-09-03)
This page's own §"park-lab" note (2026-07-07) already spelled it out: *"the ResourceSync's own
branch only governs where the FILE is read from"* — independent of any `[[stack]]`'s own `branch`
field. It bit anyway. `resource-sync-park-systems` in Komodo Core was pointed at **`dev`**, while
`park-buzi` and `park-2` are `stage`-tier Stacks (`branch = "stage"`, pinned `TAG=stage-<sha>`, per
the promotion-tiers model above). `resources.toml` had been byte-identical on `dev` and `stage`
since park-buzi's Stack was first written, so this had **zero observable effect for months** — until
a desktop-app debugging session (see [[desktop-shell-tauri]]) landed 9 real commits on `dev`
(including a `WS_ALLOWED_ORIGINS` fix) that were never merged to `stage`, creating the first genuine
divergence between the two branches.
**Symptom:** merged `dev` → `stage`, pushed, bumped `TAG` in `resources.toml` on `stage`, committed,
pushed — then destroyed + recreated the `park-2` Stack in Komodo Core and it STILL came back running
the old image. Every sync was silently re-reading `resources.toml` from `dev` (which still had the
stale `TAG`), overwriting the correct value just committed on `stage`. No error, no warning — the
sync just quietly did what it was configured to do, from the wrong branch.
**Fix:** pointed `resource-sync-park-systems` at `stage` in Komodo Core's UI (Sync config → branch
field), then re-synced + redeployed `park-2` — confirmed via `/api/version` (previously 404,
proving a stale image; correctly 401-auth-gated after the fix, proving the new image + route exist).
**Standing lesson, now written twice:** a `[[stack]]`'s promotion tier (which branch its own
`branch`/`TAG` fields track) and the ResourceSync resource's own git branch are **two independently
configured settings in Komodo Core — nothing enforces they agree**, and a mismatch is invisible
until the two branches' `resources.toml` actually diverge. **Check this FIRST** whenever a
redeploy doesn't pick up an expected `resources.toml` change, before assuming the change itself,
the CI build, or the deploy step is broken.
+38
View File
@@ -2779,3 +2779,41 @@ HTTP/WS client instead of the webview's own: tauri-plugin-http (a genuine fetch(
into api.ts/logger.ts via a new platformFetch() in origin.ts) and tauri-plugin-websocket (NOT a
drop-in — async/listener API — adapted behind a native-WebSocket-shaped interface in the new
platform-ws.ts so use-live-feed.ts needed no changes). Full detail on [[desktop-shell-tauri]].
## [2026-09-03] fix | Desktop live feed offline: native WS plugin sends no Origin, prod allowlist was empty
Login worked after the mixed-content fix, but the live feed showed offline in the desktop app while
the browser showed LIVE, same server. tauri-plugin-websocket's connect() runs on Tauri's Rust side,
not inside the webview page, so it never auto-attaches an Origin header — routes/ws.ts's anti-CSWSH
check treats a missing Origin as untrusted and 403s before auth. Compounded by a second, independent
gap: komodo/resources.toml's booth Stacks had WS_ALLOWED_ORIGINS= empty in production, despite
.env.example documenting tauri://localhost as required for the desktop app. Fixed both: platform-ws.ts
now passes Origin: tauri://localhost explicitly in connect()'s headers; resources.toml's two Stacks
get the real allowlist. Needs a Komodo sync + redeploy to reach a live booth, not just a git push.
Also confirmed the "update downloads then nothing happens" report was an older pre-fix build (v0.1.2)
self-updating — expected, not a new bug; v0.1.3 carries the error-logging fix from the mixed-content
commit and should surface a real error going forward. Full detail on [[desktop-shell-tauri]].
## [2026-09-03] fix | Update failures were invisible: console-forward gate blocked the error logging
The desktop-updater.ts error logging added earlier this session used console.error/console.warn,
but logger.ts only forwards console output to the server when the client log level is debug/trace
(default: info) — so the "fix" never actually surfaced anything, and a real v0.1.3→v0.1.4 update
failure showed zero logs anywhere, sending debugging in circles (a WebKit remote-inspector attempt
via WEBKIT_INSPECTOR_SERVER also dead-ended — this build doesn't answer standard discovery
endpoints). Fixed by calling logClient() directly in desktop-updater.ts, unconditionally, bypassing
the console-forward gate entirely — a genuine post-accept install failure now always reaches
app_logs regardless of client log level. Also added download-progress logging. Separately: found
and fixed a real, pre-existing Komodo ResourceSync misconfig (resource-sync-park-systems pointed at
`dev`, not `stage`, silently reading resources.toml from the wrong branch for months with zero
effect until dev/stage first diverged today) — full writeup on [[fleet-deployment-komodo]], which
had already warned about exactly this gotcha back in 2026-07-07 and it happened anyway.
## [2026-09-03] feat | Desktop app version now visible in the UI (was invisible)
There was no way to see which desktop build was actually installed anywhere in the app — an
operator debugging a stuck update had to infer it backwards from the update prompt's target
version ("it's offering v0.1.4, so I must be on v0.1.3"). Added DesktopVersionBadge next to the
existing server-side VersionBadge in router.tsx, using @tauri-apps/api's getVersion() (the real
running app version, synced to the git tag at build time by release.yml). No-ops in a browser.
Full detail on [[desktop-shell-tauri]].