Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
229ea3e
Replace kiosk browser detection with an Electron shell
claude Sep 1, 2026
8982e9d
Document the Electron kiosk shell rationale and future auto-update paths
claude Sep 1, 2026
38d6006
Update Node.js requirements across documentation and scripts for comp…
Cormac131 Sep 2, 2026
bd20578
Merge remote-tracking branch 'upstream/main' into feat/electron-react…
Cormac131 Sep 2, 2026
b2578b7
refactor(kiosk): migrate Electron main file from .cjs to .js and upda…
Cormac131 Sep 2, 2026
5d60168
feat(kiosk): add Electron kiosk shell for a consistent UI experience
Cormac131 Sep 2, 2026
31ee7e1
Potential fix for pull request finding
Cormac131 Sep 2, 2026
8f9d4d7
Merge branch 'feat/electron-react-shell' of https://github.com/Cormac…
Cormac131 Sep 2, 2026
e94a68c
docs: update changelog and documentation for Electron kiosk shell cha…
Cormac131 Sep 4, 2026
ea98c41
fix(kiosk): ensure kiosk UI is initialized before launch and enforce …
Cormac131 Sep 4, 2026
a188095
fix(kiosk): stop cleanup from killing kiosks owned by other launcher …
claude Sep 4, 2026
72391ce
Merge remote-tracking branch 'upstream/main' into feat/electron-react…
Cormac131 Sep 9, 2026
fd31a5e
docs: add electron-kiosk-shell.md to navigation and update links in r…
Cormac131 Sep 9, 2026
ebb79c2
Merge remote-tracking branch 'upstream/main' into feat/electron-react…
Cormac131 Sep 16, 2026
d312e68
Merge remote-tracking branch 'upstream/main' into feat/electron-react…
Cormac131 Sep 23, 2026
cd99422
Merge branch 'main' into feat/electron-react-shell
Cormac131 Sep 23, 2026
8c6bba9
Merge branch 'main' into feat/electron-react-shell
Cormac131 Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sh text eol=lf
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Thank you for your interest in contributing to OpenFlight! This document provide
### Prerequisites

- Python 3.10 or higher
- Node.js 20+ (for UI development)
- Node.js 22.12 or newer (for UI development; CI uses the version in `.node-version`)
- Git
- [uv](https://github.com/astral-sh/uv) package manager (required)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ Build it locally with `make docs` (serves at `localhost:8000`).
- **[Parts List](docs/get-started/parts.md)** — What to buy
- **[Sound Trigger Wiring](docs/build/sound-trigger.md)** — How to wire the sound trigger
- **[Raspberry Pi Setup](docs/setup/raspberry-pi.md)** — Full setup guide
- **[Electron Kiosk Shell](docs/electron-kiosk-shell.md)** — Why the kiosk runs in Electron and how self-updating could work later
- **[Battery Monitoring](docs/using/battery.md)** — Provider architecture, UI states, and shared Pi support
- **[Geekworm X1202/X1206 Operator Guide](docs/build/battery.md)** — Batteries, Pi setup, native telemetry, and warnings
- **[IWR6843 Operator Guide](docs/iwr6843/index.md)** — Wire, flash, mount, aim, and calibrate the angle radar
Expand Down
39 changes: 39 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **Chromium fallback is reachable during Electron upgrades.** If `ui/dist`
already exists, a missing Electron install no longer requires Node 22.12 and
a successful `npm install` before the kiosk can start. Old Node or a failed
install warns and continues to system Chromium. A missing UI still requires
Node 22.12+ and a successful build.
- **First switch from Chromium to Electron resets browser-local UI state.**
Electron persists its own session under `~/.config/openflight-ui` (Linux),
not the system Chromium profile. Units, language, theme, pinned Live metric,
and validation annotations in `localStorage` do not carry over. Export the
Shots CSV on Chromium before switching. Profiles and shot logs are
server-owned and unaffected. See
[Electron Kiosk Shell](electron-kiosk-shell.md#browser-local-state-breaking-on-first-electron-launch).

### Fixed
- **A crash-looping boot service no longer kills the desktop kiosk.** Every
launcher exit ran a `pkill` that matched the Electron binary path, so an
`openflight.service` that failed at startup (for example because systemd's
PATH hides `~/.local/bin/uv`) restarted every 5 s and killed whichever
kiosk was on screen; Chromium then died with "GPU process isn't usable.
Goodbye." `start-kiosk.sh` now launches the browser in its own process
group and stops only that group (`scripts/kiosk-browser.sh`), refuses to
start while another instance holds `/tmp/openflight-kiosk-<port>.lock`
(exit 3, `OPENFLIGHT_KIOSK_LOCK_FILE` overrides the path), finds `uv` in
`~/.local/bin` / `~/.cargo/bin` when PATH omits them, and prints the
recovery hint to the terminal and journal. The unit file stops retrying
after five failures in five minutes and never retries exit 3. Re-copy
`scripts/setup/openflight.service` (or rerun `scripts/setup/setup.sh`) on
existing Pis to pick up the unit changes.
- **Kiosk startup no longer rebuilds the UI after Electron has already launched.**
`ensure_kiosk_ui` now runs before the splash browser. The helper is also
stored with Unix line endings so a Windows checkout cannot make `ui/dist`
look missing (a CR in the path) and run `npm install` over a live Electron
GPU process.
- **On-screen keyboard for profile names.** Adding or renaming a profile on the
Pi kiosk now shows a full-screen keyboard. Chromium in `--kiosk` mode does not
surface a system keyboard, so the native text field was unusable on the
Expand All @@ -20,6 +53,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
full horizontal speed, overstating attack angle on any shot with club path.

### Added
- **Electron kiosk shell.** `scripts/start-kiosk.sh` now opens the UI in a pinned
Electron window (`electron@44`) instead of whichever system browser happens to
be installed. Chromium remains a fallback if Electron is not installed (including
when Node is older than 22.12 or `npm install` fails and `ui/dist` already
exists). Installing Electron needs **Node.js 22.12 or newer**. See
[Electron Kiosk Shell](electron-kiosk-shell.md).
- **PAR-TEE connector.** `"type": "partee"` in `config/sim.json` streams shots
to the [PAR-TEE](https://playpartee.com) iPhone app over OpenConnect V1 on the
phone's Wi-Fi address (port 921 by default). Same shared codec as GSPro and
Expand Down
182 changes: 182 additions & 0 deletions docs/electron-kiosk-shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Electron Kiosk Shell

`scripts/start-kiosk.sh` launches the React UI inside Electron
(`ui/electron/main.js`) rather than shelling out to whatever browser
happens to be installed on the Pi. This document explains why that's an
improvement, and sketches how it could support self-updating later. It does
not describe anything implemented yet beyond the shell itself — see
[Auto-Updates (Future Work)](#auto-updates-future-work).

## Why Electron Instead Of A System Browser

The old `launch_kiosk_browser` tried `chromium-browser`, then `chromium`,
then `google-chrome`, then `firefox` — whichever the OS image happened to
have, with `--kiosk` flags tuned mostly for Chromium. That worked, but it
carried a few risks an Electron shell removes:

| Concern | System browser | Electron shell |
|---|---|---|
| Rendering engine version | Whatever `apt` installed/upgraded on that Pi — can silently drift between units or after an OS update | Pinned in `ui/package-lock.json` (`electron@44.1.0` today), identical across every Pi until deliberately bumped |
| Kiosk lockdown | `--kiosk` behaves differently across Chromium, Chrome, and Firefox; Firefox's kiosk mode in particular is looser (menu/shortcuts still reachable) | One `BrowserWindow` with `kiosk: true`, no application menu, and `setWindowOpenHandler` denying any popup — the same guarantees everywhere |
| Startup noise | Chromium's "restore previous session" / crash bubbles needed extra flags (`--disable-session-crashed-bubble`) to suppress | Electron has no Chromium session-restore prompt to suppress. Its **default session still persists** under the app `userData` directory (`~/.config/openflight-ui` on Linux) — [Session](https://www.electronjs.org/docs/latest/api/session), [app.getPath('userData')](https://www.electronjs.org/docs/latest/api/app#appgetpathname). That is a *different* profile from system Chromium (`~/.config/chromium` / `chromium-browser`) |
| Maintenance surface | A 4-branch `if/elif` detection ladder to keep working across Raspberry Pi OS Bookworm/Bullseye, Lite/Desktop images | One binary, one launch path; `npm ci` makes the exact runtime reproducible in CI the same way any other dependency is |
| Extensibility | A browser tab is sandboxed from the OS — no filesystem, process, or native API access | The Electron **main process** is a regular Node.js process with full OS access, which is what makes [self-updating](#auto-updates-future-work) possible at all |

The old detection ladder is kept as a fallback (`launch_kiosk_browser` still
tries `chromium-browser`/`chromium` if `ui/node_modules/.bin/electron` is
missing), so a Pi that hasn't installed Electron doesn't lose its kiosk
entirely — it just loses the guarantees above until Electron is installed.

`start-kiosk.sh` builds `ui/dist` only when that directory is missing. If the
UI is already built but Electron is not installed, it *tries* `npm install`
when Node.js is 22.12+. Old Node, an offline Pi, or a failed install logs a
warning and continues to the Chromium fallback instead of aborting startup.

## Browser-local state (breaking on first Electron launch)

Electron does **not** reuse the system Chromium profile. The first time a unit
switches from Chromium to Electron, browser-local `localStorage` looks empty:

| Data | Storage | Survives the switch? |
|---|---|---|
| Profiles and shot logs | Server (`~/.config/openflight/profiles.json`, session JSONL) | Yes |
| Units, theme, language, pinned Live metric | Chromium `localStorage` | No — re-set in the footer / Live grid |
| Validation annotations (comparator device, speed, notes) | `localStorage` key `openflight-validation-entries` | No |

**Before** switching a validation unit to Electron, export the Shots CSV
(**Export CSV** on the Shots tab) while still on Chromium. After the switch,
re-enter units, theme, language, and the pinned metric once.

This is an accepted one-time reset, not a silent migration. Chromium's LevelDB
profile is not copied into Electron `userData`.

## What Didn't Change

Electron here is a shell, not a rewrite: `ui/electron/main.js` opens a
`BrowserWindow` and points it at the same URL the browser used to load
(`http://localhost:8080`, served by Flask from `ui/dist`). The React app,
the WebSocket connection (`socketService.ts`), and the Flask server are
untouched — `getServerOrigin()` still resolves to `window.location.origin`,
which is the Electron window's origin now instead of a browser tab's.

## Process Ownership

`scripts/kiosk-browser.sh` launches Electron (or the Chromium fallback) with
`setsid`, so the whole browser tree, including the zygote, GPU, network and
renderer helpers Chromium forks, lives in one process group that nothing
else on the Pi belongs to. Shutdown signals that group and nothing else. The
earlier cleanup matched the Electron binary path with `pkill -f`, which also
killed kiosks started by *other* launcher instances; see the changelog for
the boot-service crash loop that exposed it. `start-kiosk.sh` additionally
holds `/tmp/openflight-kiosk-<port>.lock` for its lifetime and exits with
status 3 if another instance already holds it.

## Auto-Updates (Future Work)

Nothing below is implemented. It's worth writing down now because "Electron
shell" and "auto-update" are usually mentioned in the same breath, and
because OpenFlight's deployment shape (a small fleet of Pis you personally
maintain, not a public app store release) points toward a different design
than the default Electron answer.

There are two separate things that could be "updated," and they call for
different mechanisms.

### 1. UI content (the React build) — already effectively live

Electron loads a URL, not a bundled copy of `ui/dist`. Whatever Flask is
currently serving is what the window shows. So once a Pi has pulled a new
`ui/dist` (via the existing `git pull && npm run build` flow in
[splash-screen.md](setup/splash-screen.md#updating-an-existing-pi)) and the
service restarts, the Electron window shows the new UI on its next launch —
no Electron-specific update logic needed for this layer. This is already
true today.

### 2. The Electron shell itself

`electron` is a normal `devDependency` in `ui/package.json`. Bumping its
version is a normal dependency bump: change the version, `npm install`,
commit the updated lockfile, `git pull` on each Pi. No runtime auto-update
machinery is needed for this either, as long as updates continue to arrive
through `git pull` + reinstall rather than an out-of-band download.

Installing that package (not running the Electron binary) needs **Node.js
22.12+** on the Pi. Node 20 prints `npm WARN EBADENGINE` for `electron@44`
and its `@electron/get` helper. See the Node install step in
[Raspberry Pi setup](setup/raspberry-pi.md).

### 3. The interesting case: OpenFlight self-updating without an SSH session

The capability an Electron main process adds that a browser tab never had
is **the kiosk can update itself**, because `main.js` runs as a full
Node.js process on the Pi rather than inside a sandboxed tab. Two designs,
in increasing order of complexity:

**A. Main-process-driven `git pull` (recommended starting point)**

The main process periodically (or on a UI-triggered "Check for Updates"
action, via a `contextBridge` preload script) does the same thing an
operator does by hand today:

1. `git fetch` and compare `HEAD` against `origin/<branch>`.
2. If behind: `git pull`, `uv sync`, `npm run build` (in `ui/`).
3. Decide how to apply it:
- Content-only change (`ui/` touched, `ui/electron/` and
`ui/package.json`'s `electron` version untouched) → `win.loadURL()`
again, or just wait for the operator's next launch.
- Shell change (Electron itself bumped, or `main.js` changed) →
`app.relaunch(); app.exit(0)`, or restart the systemd unit
(`systemctl --user restart openflight` / `sudo systemctl restart
openflight`, per `scripts/setup/openflight.service`) so the new
`main.js` is picked up.

This reuses the exact update path already documented for manual updates —
it just runs it from inside the app instead of over SSH. It also keeps
using GitHub as the source of truth, so no new release infrastructure,
signing, or hosting is required.

Things to get right if this is built:
- **Trust boundary:** whatever triggers the pull (a timer or a UI button)
must not be reachable by anything the Flask server exposes over the
network — this must stay a main-process-only action, not a socket event
or HTTP endpoint, so a device on the same LAN can't trigger arbitrary
`git pull`/`uv sync` execution on the Pi.
- **Partial-failure safety:** a `git pull` that succeeds but an `npm run
build` that fails should not leave the Pi worse off than before — keep
the previous `ui/dist` until the new build succeeds (e.g. build to a
temp directory and swap), and skip the restart on build failure.
- **Mid-round updates:** don't apply an update (especially the
shell-restart kind) while a shot/session is in progress; gate it on
session/idle state the same way the splash screen gates on startup state.
- **Network dependence:** the Pi may be on a golf-sim LAN with no general
internet access even when it can reach GitHub, or vice versa — the check
should fail closed (skip silently) rather than block startup.

**B. `electron-updater` + a packaged build**

The conventional Electron answer — `electron-builder` packages the app,
`electron-updater`'s `autoUpdater.checkForUpdatesAndNotify()` polls a feed
(GitHub Releases, S3, or a self-hosted static server) and swaps the
installed build. This is the right model for shipping to users you don't
operate the hardware for.

It's a bigger lift than option A here, for two reasons specific to this
project:
- It requires the packaging step this shell deliberately skipped (see the
original Electron-shell decision: "just run from source, no installers").
`ui/dist` would need to be bundled into the package rather than loaded
live from Flask, which reintroduces the "which layer updates independently"
question this doc just resolved for the source-checkout model.
- `electron-updater`'s Linux auto-update support is limited to the AppImage
format. That's buildable for `arm64` (Raspberry Pi OS 64-bit, which this
fleet already requires), but it's a new build target, a new artifact to
test on real hardware, and a release/signing pipeline to stand up — none
of which exists for this project today.

**Recommendation:** start with (A) if/when self-updating is prioritized. It
matches the fleet's actual shape (Pis you `git pull` on, not an app store
audience), reuses infrastructure that already exists (`uv sync`, `npm run
build`, the systemd unit), and doesn't require adopting a packaging and
release pipeline before there's a concrete need for one. Revisit (B) only if
OpenFlight starts distributing prebuilt images to people who don't run `git
pull` themselves.
34 changes: 34 additions & 0 deletions docs/setup/raspberry-pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ Run the following command:
sudo apt update && sudo apt install -y swig liblgpio-dev python3-dev
```

The UI/Electron kiosk shell needs **Node.js 22.12 or newer** to *install*
Electron. Raspberry Pi OS `apt` Node is often 18 or 20 and will print
`EBADENGINE` (or fail) for `electron@44`. A Pi that already has `ui/dist` can
still start: `start-kiosk.sh` falls back to system Chromium until Node is
upgraded and `npm install` in `ui/` succeeds. Install Node 22 LTS before a
first UI build or to actually run the Electron shell:

```bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v # should report v22.12.0 or later
```

If `./scripts/setup/setup.sh` updates `~/.bashrc`, you may need to run `source ~/.bashrc` (or open a new terminal) so your current shell picks up the new environment variables immediately without needing to reboot or re-login.

### 2. Run the setup script
Expand Down Expand Up @@ -284,6 +297,27 @@ If the mapping is missing or points at the wrong radar, re-run the wizard:
/dev/ttyUSB...` in the server logs. See [K-LD7 Troubleshooting](../legacy/troubleshooting.md)
for "Wrong length reply" and other connection issues.

### Kiosk Window Closes Seconds After Loading

If the UI appears and then vanishes with `GPU process launch failed`,
`Failed to send GetTerminationStatus message to zygote`, and finally
`GPU process isn't usable. Goodbye.` in the terminal, something outside the
window killed Electron's helper processes. The usual culprit is a second
copy of `start-kiosk.sh`, typically a failing boot service restarting in a
loop while you launch by hand:

```bash
sudo systemctl status openflight --no-pager # "activating (auto-restart)" = looping
journalctl -u openflight -n 40 --no-pager # the recovery hint is printed here
```

Fix whatever the journal reports, or `sudo systemctl disable openflight` if
you launch from the desktop instead. Current launchers refuse to start while
another instance holds `/tmp/openflight-kiosk-<port>.lock` (exit code 3) and
only ever stop the browser they started, so an old unit file is the one thing
left to update: re-copy `scripts/setup/openflight.service` as shown in
[Auto-Start on Boot](#auto-start-on-boot).

### Service Won't Start

```bash
Expand Down
8 changes: 8 additions & 0 deletions docs/setup/splash-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ installer always preserves that checkout's local wrapper. No reboot is
required. Close an existing OpenFlight session first, then launch the refreshed
desktop icon.

If this pull is the first that launches **Electron** instead of system
Chromium, browser-local settings (units, theme, language, pinned Live metric)
and Shots validation notes do not migrate. Export **Export CSV** on the Shots
tab while Chromium is still the kiosk, then re-set preferences after Electron
starts. See [Electron Kiosk Shell](../electron-kiosk-shell.md#browser-local-state-breaking-on-first-electron-launch).
If Node is still 20 or the Pi is offline, startup keeps using Chromium until
Electron can be installed.

Raspberry Pi desktop settings determine whether icons require a single click or
a double click. The installer removes the separate “execute or execute in
terminal” choice; it does not change the user's global file-manager click
Expand Down
Loading
Loading