Offline App — Electron Wrapper
Overview
The April Robots Offline application is a FastAPI backend paired with a compiled SvelteKit frontend, wrapped in Electron to give center specialists a native desktop experience on Windows. Electron manages the full lifecycle: spawning the backend process, serving the frontend over a local HTTP server, managing robot microservice containers via Podman, and handling automatic updates from GitHub Releases.
The Electron wrapper lives in the electron/ subdirectory of the april-robots-offline-general-api repository.
Directory structure
april-robots-offline-general-api/
├── app/ # FastAPI backend (Python)
├── frontend/ # Pre-built SvelteKit static files
├── database/ # File-based database (DictDataBase)
├── logs/ # Runtime logs (created on first run)
├── electron/
│ ├── src/
│ │ ├── main.js # Main process — app lifecycle, window management, IPC
│ │ ├── preload.js # IPC bridge exposed to renderer
│ │ ├── server.js # Local HTTP server for the frontend
│ │ ├── auto-updater.js # Update manager (GitHub Releases)
│ │ ├── podman-manager.js # Robot microservice container orchestration
│ │ ├── logger.js # Centralized logging utility
│ │ ├── loading.html # Splash screen shown during startup
│ │ ├── titlebar.html # Custom frameless window title bar
│ │ └── update-notification.html # Update download progress UI
│ ├── package.json
│ └── electron-builder.yml
├── build.ps1 # Windows build script (PyInstaller + electron-builder)
└── .env # Runtime configuration (not committed)Startup sequence
When the app is launched, the main process follows a fixed four-step startup before showing the main window. A loading window (220×140 frameless splash screen) is shown throughout and updates its status text at each step.
flowchart TD
Start([App launched]) --> LW["Show loading window\n(splash screen)"]
LW --> P{ENABLE_PODMAN?}
P -->|true| PM["PodmanManager.startup()\n1. Start Podman machine\n2. Login to GHCR\n3. podman compose down\n4. podman compose pull\n5. podman compose up -d"]
P -->|false| BE
PM -->|error → log + continue| BE
BE["Start backend process\n(see Backend execution)"]
BE --> W["Wait 3 seconds\n(initialization buffer)"]
W --> FS["Start frontend HTTP server\nlocalhost:3000"]
FS --> MW["Create main BrowserWindow\nload localhost:3000"]
MW --> TB["Inject custom title bar\nvia executeJavaScript"]
TB --> AU["Check for updates\n(only if APP_ENV=prod)"]
AU --> Show["Close loading window\nmaximize and show main window"]
Show --> Ready([App ready])
Backend execution
The backend is a standard FastAPI + Uvicorn app started as a child process. How it is started depends on whether the app is packaged or running in development:
| Mode | Executable | Working directory |
|---|---|---|
| Development | ./venv/bin/python -m app.main --api-only | Project root |
| Production (packaged) | april-robots-backend.exe --api-only | process.resourcesPath |
The process is spawned with stdio captured and piped to the logger. On shutdown:
- Windows —
taskkill /pid {pid} /T /F(kills the entire process tree) - Unix —
SIGTERMsignal
The --api-only flag tells the FastAPI app to run without the bundled frontend, since Electron serves the frontend separately.
Frontend serving
server.js creates a plain Node.js HTTP server that serves the pre-built SvelteKit static files from the frontend/ directory on localhost:3000. It handles:
- MIME type detection per file extension
- SPA fallback — any unmatched path returns
index.html(required for client-side routing) Cache-Control: no-cacheon all responses- Path traversal prevention (requests cannot escape the
frontend/directory)
The main BrowserWindow loads http://localhost:3000 once the server is up.
Window setup
The main window is frameless (no native OS title bar) with a custom 34px title bar injected into the page via executeJavaScript after load. This gives the app a branded look while keeping native window behaviour (drag, minimize, maximize, close).
| Window | Size | Notes |
|---|---|---|
| Main window | 1280×720 (min 1024×600) | Frameless, custom title bar, context isolation on |
| Loading window | 220×140 | Frameless, transparent, always on top |
| Update notification | 550×400 | Frameless, transparent, centered |
The custom title bar (titlebar.html) is a green (#72cd78) fixed-height bar positioned at the top of the page. It reflects the current page title dynamically and forwards minimize / maximize / close button clicks to the main process via IPC.
IPC communication
preload.js exposes a safe window.electronAPI object to the renderer (Svelte frontend) using Electron’s contextBridge. The renderer cannot access Node.js APIs directly.
window.electronAPI = {
// Window controls
windowMinimize(), windowMaximize(), windowClose(), windowIsMaximized()
// App info
getBackendUrl() // returns http://127.0.0.1:8000
getAppVersion() // returns APP_VERSION from .env
isDevMode() // true if not packaged
// Updates
checkForUpdates()
onUpdateDownloadProgress(callback)
sendUpdateAction(action)
onShowUpdateNotification(callback)
// Session and language
setSessionActive(payload) // prevents accidental quit mid-session
setAppLanguage(language) // en | ru | kk
platform // process.platform
}Podman manager
podman-manager.js orchestrates the robot microservice containers (NAO MS, Furhat MS) using Podman Compose. It is only active when APP_ENV=prod and ENABLE_PODMAN=true.
Startup sequence:
- Derive image tag from
APP_VERSION— the manager readsAPP_VERSIONfrom.env(e.g.,1.4.2or1.4.2-beta) and constructs the MS image tag as{major.minor}{suffix}-latest:1.4.2→1.4-latest1.4.2-beta→1.4-beta-latestThis tag is set as bothNAO_AGENT_VERSIONandFURHAT_AGENT_VERSIONin the process environment, overriding any values from.env.
- Start the Podman machine (WSL-backed VM), if not already running.
- Login to GHCR using
GHCR_ACTORandGHCR_TOKENfrom.env. podman compose down— ensure a clean slate.podman compose pull— pulls the image tagged1.4-latest(or equivalent) from GHCR.podman compose up -d— start containers in the background.- Begin streaming container logs to the logger.
On app shutdown, podman stop is called for each container. Containers are stopped but not removed, so their state is preserved.
This means the offline app always runs the latest patch of the microservice that is compatible with its current major.minor version — matching the major.minor-latest tag pushed to GHCR during the MS release. See Microservices and Release docs — Robot Microservices for how those tags are published.
Auto-updater
auto-updater.js manages over-the-air updates from the april-robots-offline-deployment-kit GitHub repository. Updates only run when APP_ENV=prod.
Update check:
- Fetches all releases from the GitHub API (authenticated with
GHCR_TOKEN). - Filters out drafts, sorts by semantic version.
- Compares with
APP_VERSIONfrom.env. - If a newer release exists, shows the update notification window.
Install flow:
- Downloads the release ZIP from the GitHub API (with redirect following).
- Tracks download progress, reporting to the notification window in 5% increments.
- Extracts the ZIP using PowerShell
Expand-Archive. - Environment merge — the new
.envfrom the release is merged with the existing one:CENTER_JWT_KEYis always preserved from the existing.env(it is center-specific and must not be overwritten).APP_VERSIONis updated to the new version.
- Generates a SHA256 hash manifest for all files to be copied.
- Generates and runs a PowerShell post-update script that:
- Waits for the Electron app to close (up to 30 seconds).
- Copies all new files into place.
- Preserves
database/storage/children.jsonanddatabase/storage/sessions.json(local data must not be overwritten). - Verifies copied files against the hash manifest.
- Relaunches the updated app.
Files and data preserved across updates:
CENTER_JWT_KEYin.env— the center’s authentication key entered at installation time.database/storage/children.json— local children records.database/storage/sessions.json— local session records.
All other files (including the rest of .env) are overwritten from the release package.
APP_ENV to a non-prod value to disable auto-updates until the updater is fixed.Logging
logger.js provides category-based file logging for each major subsystem. Log files are written to logs/{category}/YYYYMMDD_HHMMSS_{category}.log. A maximum of 10 log files per category are kept; older files are deleted automatically.
Categories: app, api, podman, nao, furhat, updater.
Subprocess stdout and stderr are piped directly into the appropriate logger via logger.pipeStream().
Build and packaging
The release build is fully automated via GitHub Actions in the april-robots-offline-deployment-kit repository. It triggers on push to any release/** branch (or manually via workflow_dispatch).
Local build (development only)
build.ps1 in the backend repo builds the app locally in two steps:
- PyInstaller — bundles the FastAPI app into
dist/april-robots-backend.exe. - electron-builder — packages the Electron app as an unpacked directory under
electron/dist/win-unpacked/.
This is only used for local testing. The production release always goes through the CI/CD pipeline below.
CI/CD release pipeline
The pipeline runs on Windows (windows-latest) and produces two release assets: a setup.exe installer and a release.zip of the raw unpacked app.
Step 1 — Extract version and derive image tag
The version is extracted from the branch name (e.g., release/1.4.2 → 1.4.2). The MS image tag is derived from it:
1.4.2→NAO_AGENT_VERSION=1.41.4.2-beta→NAO_AGENT_VERSION=1.4-beta
Step 2 — Tag the release
A Git tag v{VERSION} is created and pushed. If the tag already exists it is deleted and recreated.
Step 3 — Check out source repos
Both april-robots-offline-fe and april-robots-offline-general-api are checked out at their matching release/{VERSION} branch. All three repos must have a branch with this exact name.
Step 4 — Build frontend
Installs npm dependencies and runs npm run build in the frontend repo. The compiled output lands in frontend/build/.
Step 5 — Build backend
Sets up a Python 3.12.3 venv, installs requirements, and runs build.ps1 from the backend repo. This produces the Electron unpacked directory at backend/electron/dist/win-unpacked/.
Step 6 — Assemble release directory
The compiled frontend is copied into resources/frontend/ inside the unpacked directory. The contents of win-unpacked/ are then copied to a top-level release/ folder.
Step 7 — Assemble .env
A .env file is written into release/resources/ with all environment variables injected from GitHub Secrets and Variables:
APP_VERSION— the release versionNAO_AGENT_VERSION/FURHAT_AGENT_VERSION— derived fromAPP_VERSION- Robot SSH credentials, GHCR token, and all other variables
CENTER_JWT_KEY is not written into the .env at this stage. It is entered by the client during installation via the InnoSetup prompt and written to .env at that point.Step 8 — Compile installer
InnoSetup 6 is installed on the runner and compiles installer.iss into Output/setup.exe. The installer prompts the client for CENTER_JWT_KEY and writes it into the installed app’s .env.
Step 9 — Package and publish
release/ is zipped into release.zip. Both setup.exe and release.zip are uploaded to a GitHub Release tagged v{VERSION}:
- Versions containing
-(e.g.,1.4.2-beta) are published as pre-releases. - Clean versions (e.g.,
1.4.2) are published as latest.
Development vs production
| Aspect | Development | Production |
|---|---|---|
| Backend | ./venv/bin/python -m app.main | april-robots-backend.exe |
| Working directory | Project root | process.resourcesPath |
| Podman | Disabled (APP_ENV != prod) | Enabled |
| Auto-updates | Disabled | Enabled (incomplete — see Auto-updater) |
| DevTools | Auto-opened in Electron window | Open via browser at localhost:3000 |
Environment variables
The .env file is generated and bundled into each release by the CI/CD pipeline in the april-robots-offline-deployment-kit repository. Values are sourced from that repository’s GitHub Secrets and Variables — nothing is committed to the repository. To add or change an env value in a release, update the corresponding secret or variable in the deployment-kit repo settings.
At runtime, .env lives in the app root (project root in dev, resources/ in packaged).
| Variable | Source | Purpose |
|---|---|---|
APP_VERSION | GitHub Variable | Current app version (also updated by auto-updater on install) |
APP_ENV | GitHub Variable | prod enables Podman and updates; anything else disables both |
CENTER_JWT_KEY | Entered during installation | Center authentication key for cloud sync — inputted by the client in the InnoSetup installer window; preserved across updates and never overwritten by CI/CD |
DEBUG | GitHub Variable | Enables verbose FastAPI logging |
BACKEND_URL | GitHub Variable | FastAPI URL (default: http://127.0.0.1:8000) |
FRONTEND_DIR | GitHub Variable | Path to compiled frontend directory (default: frontend) |
ONLINE_BACKEND_URL | GitHub Variable | URL of the AR Online backend for sync |
ENABLE_PODMAN | GitHub Variable | Set to false to skip Podman startup |
GHCR_TOKEN | GitHub Secret | Token for pulling container images and checking GitHub releases |
GHCR_ACTOR | GitHub Variable | GitHub username for GHCR login |
NAO_AGENT_VERSION | GitHub Variable | Fallback MS image tag (overridden at runtime in prod — derived from APP_VERSION) |
FURHAT_AGENT_VERSION | GitHub Variable | Fallback MS image tag (overridden at runtime in prod — derived from APP_VERSION) |
NAO_MICROSERVICE_URL | GitHub Variable | NAO MS URL (default: http://localhost:5050) |
FURHAT_MICROSERVICE_URL | GitHub Variable | Furhat MS URL (default: http://localhost:5051) |
NAO_USERNAME / NAO_PASSWORD | GitHub Secret | SSH credentials for NAO robot |
FURHAT_USERNAME / FURHAT_PASSWORD | GitHub Secret | SSH credentials for Furhat robot |
Installation
The offline app is distributed as a setup.exe built with InnoSetup. The installer walks the client through setup, including a prompt to enter the CENTER_JWT_KEY for that center. This key is written into .env at install time and is preserved by the auto-updater across future updates.
TODO: detailed installation walkthrough (Malika).
Debugging
Browser access
The app and all its internal services can be accessed directly in a browser without opening the Electron window. This is the primary way to debug in production:
| Service | URL |
|---|---|
| Frontend (SvelteKit app) | http://localhost:3000 |
| Backend (FastAPI) | http://localhost:8000 |
| Backend API docs (Swagger) | http://localhost:8000/docs |
| NAO microservice | http://localhost:5050 |
| Furhat microservice | http://localhost:5051 |
Logs
To inspect logs, navigate to the app installation directory, open resources/logs/. Each subsystem writes to its own subfolder:
resources/logs/
├── app/ # main Electron process
├── api/ # FastAPI backend
├── podman/ # Podman startup and compose operations
├── nao/ # NAO container output
├── furhat/ # Furhat container output
└── updater/ # auto-updater activityEach folder keeps the 10 most recent log files, named YYYYMMDD_HHMMSS_{category}.log.
Robot microservice issues
If a robot microservice is not working correctly, use the Podman CLI to inspect the containers directly:
# List running containers
podman ps
# View logs for a specific container
podman logs podman_nao-agent_1
podman logs podman_furhat-agent_1
# Restart a container manually
podman restart podman_nao-agent_1The microservice health endpoints can also be checked in the browser at localhost:5050 and localhost:5051.