Desktop client architecture
High level#
- Main process (
src/main.js) owns windows, menus, tray, global shortcuts, SSE, polling, and IPC handlers registered viasrc/ipc.js. - Preload (
src/preload.js) exposes a whitelistedwindow.ahdClientAPI to the game web app (remote Next.js UI loaded inBrowserWindow). - Session — The main window and pop-out windows use Electron session partition
persist:ahd. Cookies set by the game origin are shared so login state matches across windows. - Game origin — Resolved at runtime through
src/active-game-url.js(production, sandbox, dev server, orAHD_GAME_URLoverride).
Key modules#
| Area | Files |
|---|---|
| App menu, Navigate, View | menu.js, nav.js, countries.js, urls.js |
| Client manifest | site-api.js (GET /api/client-nav, /api/countries), nav-manifest.js (normalization) |
| Corporation enrichment | corporation-enrich.js (pathId, ceoId, strip/merge) |
| Game menu quick links | game-panel-links.js, game-panel-config.html, game-panel-config-window.js |
| PiP / mini mode | pip.js, pip.html, pip-view-poller.js, dashboard.js |
| Tray & notifications | tray.js, notifications.js |
| Pop-out presets | windows.js |
| Global shortcuts | shortcuts.js (defaults + customShortcuts preference) |
| Theme / cache / queue | cache.js, action-queue.js, sse.js |
Client-nav pipeline#
- After each full load (
did-finish-load), on first SSE connect, and on a focus-aware interval (30s focused / 60s unfocused), the main process callsGET /api/client-navwith the same cookies as the game window. - The JSON response is normalized in
nav-manifest.js(e.g.character_countryId→characterCountryId, derivedhasCharacterwhen the API omits the flag but sends character-related fields). - Optionally enriched from
GET /api/character/mefor corporation / CEO fields (enrichClientNavManifestinmain.js). - Effects: IPC to renderer (
client-nav,nav-data-updated,auth-state, unread counts), MenuManager.setNavConfig, window preset updates, and game-state hydration for tray/PiP.
If the first fetch returns null (timing, parse error), pullClientNav({ retryOnNull: true }) retries with backoff so the Navigate menu is not stuck until the next poll. Both focus and show events on the main window also trigger a pullClientNav so the menu refreshes on window restore as well as focus gain. Navigating to /corporation or /stockmarket paths triggers an automatic re-sync.
Preload surface (window.ahdClient)#
The game page may invoke only channels listed in preload.js (INVOKE_CHANNELS) and listen on RECEIVE_CHANNELS. Common uses:
- Navigation:
go-home,go-back,go-forward,navigate-to,open-external - Shell state:
fetch-nav-data,get-game-state,set-theme,toggle-pip,open-window(pop-out presets) - Account:
switch-character,sign-out
Game panel configuration window#
A separate small BrowserWindow loads game-panel-config.html with game-panel-config-preload.js, which exposes window.gamePanelConfig (not ahdClient). It talks to main via IPC: game menu entries, PiP status bar stats, and keyboard shortcut overrides (get-custom-shortcuts / save-shortcuts).
Dynamic countries#
After each SSE connect (and on startup from cache), the main process calls GET /api/countries via site-api.fetchCountries(). The response is stored in cache.js (countries key) and set on countries.js via setCountriesCache(). All subsequent getCountryConfig() calls prefer the server list; hardcoded defaults in COUNTRIES serve only as offline fallback. If the list changes (different length or ids), the menu is rebuilt immediately.
Corporation enrichment (corporation-enrich.js)#
enrichClientNavManifest in main.js delegates to mergeCharacterMeIntoManifest() after fetching /api/character/me. The module:
- Extracts
pathId(preferred),sequentialId,sequential_id,_id, oridas the URL segment viacorporationPathIdForUrl(). - Detects CEO status by matching
character._idagainstcorp.ceoId, or checkingisCeo/rolefields. - Calls
stripCorporationEnrichment()on catch or whenhasCharacteris false, so stale corporation data is cleared on 401 / logout / leaving a corporation.
All corporation URL construction (menu.js, game-panel-links.js, pip.html) wraps the id segment in encodeURIComponent for slug safety.
SSE fallback polling#
When SSE disconnects (common with Vercel multi-instance), main.js starts a 30-second setInterval that calls fetchClientNav() and pipes the result through handleClientNav(). The timer is cleared on SSE reconnect and on app cleanup.
Version header#
All authenticated net.request() calls (site-api.js) send X-AHD-Client-Version: <package.json version> so the server can detect outdated clients.
Builds#
electron-builder targets: Windows NSIS (.exe), macOS DMG, Linux AppImage. CI builds are unsigned (CSC_IDENTITY_AUTO_DISCOVERY=false); see the main README for macOS Gatekeeper notes.
A House Divided
Grand Century
MetroForge
Verdigris
Electioneer