Changelog
All notable changes to the A House Divided desktop client are documented here.
[1.3.0] - 2026-08-27#
Added#
Bulk corporation wage controls — CEOs can set the wage level for every sector at once from Navigate > World > My Corporation > Wages, via presets (0.80x–1.50x) or a custom value. Levels are clamped to
[0.8, 1.5], mirroringWAGE_LEVEL_MIN/MAXin the game'slaborCost.ts, and the menu is only enabled for the CEO of a corporation (src/corporation-wages.js,src/menu.js).The apply step is paced against the server's real budget of 20 wage writes per minute per user. Sectors are enumerated first so the confirmation states the true count and, for large corporations, the expected duration; a rolling-window pacer then admits at most 20 writes per 60 s, and any 429 that still lands is honoured via
Retry-Afterand retried rather than counted as a failure. Progress is reported on the taskbar icon.This matters at the tail: production holds 661 corporations averaging 6.8 sectors, but 30 exceed 20 sectors and the largest holds 105. Measured against a mock of the real limiter, a 105-sector apply went from 40 succeeded / 65 rate-limited failures to 105/105 with no 429s at all. Corporations at or under 20 sectors are unaffected and still apply instantly.
scripts/mock-game-server.js(npm run mock) stubs the endpoints and mirrors the limiter, so the paced path can be exercised locally —MOCK_SECTORS,MOCK_RATE_LIMIT, andMOCK_WINDOW_MScontrol the shape.
Fixed#
- Update download failures were completely silent —
downloadUpdate()was fire-and-forget: on any network error the promise rejected with no UI, the "Downloading…" taskbar progress vanished, and users were left waiting forever after clicking Download (looked like the app hung). Failures now show an error dialog with the reason and a hint to retry after restarting the app; repeated Download clicks are guarded against re-entry while a download is in flight (src/updater.js). Regression tests added. - "Restart Now" left the app closed after installing —
quitAndInstall()defaults to not relaunching: the installer ran silently and the app simply exited, reading as if the update ate the app. It now passesisForceRunAfterso the freshly installed build starts immediately (src/updater.js). - Raw JSON / certificate-error JSON shown on manual refresh during turn processing — While the server is busy processing a turn, a refresh could render a bare 5xx body or a raw
application/jsonerror document as the main frame. Main-frame responses with HTTP ≥ 500 now show the friendly "server is processing" overlay, and any main frame that finishes loading as a JSON document is covered too — including 200-with-JSON-body responses that have no HTTP error code (src/main.js,src/error-handler.js). - Command palette buttons did nothing — The injected Cmd+K palette invoked the
navigateIPC channel, which is receive-only inpreload.js(the invokable channel isnavigate-to), so pressing Enter or clicking a result silently rejected and the palette appeared broken. Both call sites now usenavigate-to(src/main.js). - Reload threw you back to the home page — Game menu Reload (Ctrl+R) and context-menu Reload loaded the bare game URL instead of reloading the current page, abandoning whatever route the user was on. Both now reload in place via
webContents.reload()when a game page is open (src/menu.js,src/main.js). - Custom keyboard shortcut overrides never applied —
ShortcutManagerrequired theCacheManagerclass at module level and calledgetPreferenceon it (a non-static method), so saved overrides were always silently ignored. The live instance is now injected via the constructor frommain.js, andgetEffectiveShortcutsno longer mutates the shared defaults object (src/shortcuts.js). Regression tests added. - Intermittently missed turn events — A single SSE frame is routinely delivered over several TCP chunks, but the parser kept the event type and data accumulator as locals inside
processBuffer(). A frame whoseevent:line arrived in one chunk anddata:line in the next lost its type and was emitted as a genericmessage, so every named-type listener silently skipped it: turn caching, thetheme_changedhandler, the immediate post-turn dashboard re-poll, and turn/election desktop notifications. Balances would go quietly stale until the next scheduled poll. Parse state now lives on the instance and resets on connect, disconnect, and buffer overflow (src/sse.js). Regression tests added.
Changed#
Reasonable balance updates — Dashboard polling is now adaptive: every 30 s while the app window is focused (funds / AP / countdown feel live without hammering the API), every 60 s in the background. Returning focus to the window triggers an immediate poll plus an instant client-nav hydration so balances are never stale after playing elsewhere. Immediate re-polls after turn/action SSE events are unchanged (
src/dashboard.js,src/main.js). New unit suitetests/unit/dashboard.test.js.Electron 33 → 42 — Nine major versions of Chromium and Node, bringing the accumulated browser security fixes with it. Also electron-builder 25 → 26, electron-store 8 → 11, electron-updater 6.8.3 → 6.8.9, and the whole dev toolchain. The client now requires Node 22 to build (Electron 42 bundles Node 22.x).
Eight high-severity dependency advisories cleared —
fast-uri(host confusion and path traversal),js-yaml,flatted, andbrace-expansion.js-yamlis the one that ships:electron-updaterparses the update feed'slatest.ymlwith it.npm auditreports zero vulnerabilities.
[1.2.2] - 2026-08-25#
Fixed#
- Navigation can no longer crash the main process — All
webContents.loadURLcalls now go through asafeLoadURLhelper (src/safe-load-url.js) that guards a destroyed window and swallows the promise rejection Electron raises on aborted/failed navigations (e.g.ERR_ABORTEDwhen a newer load supersedes one). Menu items, the Sign Out / character-switch flows, and global shortcuts route through it, and menu handlers gainedcanNavigate()/isDestroyed()guards so clicking after the window closes is a no-op instead of a crash. - TLS/certificate failures get a dedicated overlay —
did-fail-loaddistinguishes certificate errors (CERT_ERROR_PATTERN) from generic connection failures and shows a specific recovery message (check your clock, or the site owner must renew the certificate) instead of the generic offline overlay. - Dashboard & PiP pollers no longer hang on stalled responses — The request timeout stays armed for the whole response (headers and body) instead of being cleared once headers arrive, and every resolve/reject path clears the timer through a single helper (
src/dashboard.js,src/pip-view-poller.js). - Corrupted non-ASCII API responses — Response bodies are buffered and decoded as UTF-8 once at the end, so multi-byte characters split across chunk boundaries are no longer mangled (
src/site-api.js,src/error-handler.js). - Cookie-store failures no longer crash background polls — Authenticated GET/POST helpers degrade to "no cookies" instead of rejecting into an unhandled-rejection crash (
src/site-api.js). - Silent shortcut registration failures —
globalShortcut.register()returningfalse(OS rejected the accelerator) is now logged instead of passing unnoticed (src/shortcuts.js). - Feedback capture — Guards the main window with
isDestroyed()after the async screenshot/save-dialog steps, and the floatingexecuteJavaScriptfeedback trigger now has explicit error handling (src/feedback.js).
[1.2.1] - 2026-08-25#
Fixed#
- SSE 404 no longer crashes the app — The web app removed the
/api/eventsSSE endpoint (replaced by polling), so the client's SSE connection received a permanent 404 on every page navigation.src/sse.jsemitted a Node'error'event with no listener attached, which throws as an uncaught exception in the main process and showed the "A JavaScript error occurred in the main process" crash dialog (error spam after Login with Discord redirects). Errors are now emitted through a safe helper that logs instead of throwing, 404/410 responses are treated as endpoint-gone (the existing 30-second fallback polling engages, no pointless reconnect loop), andsrc/main.jsforwards SSE errors to the renderer's connection status. Regression tests intests/unit/sse.test.jsreproduce the exact reported 404 scenario (ticket #1182).
[1.2.0] - 2026-06-01#
Added#
- Dynamic countries — Fetches
/api/countrieson SSE connect and caches the result insrc/cache.js(newcountriesschema entry) andsrc/countries.js. The client now falls back to hardcoded defaults only when the server is unreachable; country-scoped menu items, paths, and labels update live when the server list changes. - SSE fallback polling — When SSE disconnects (e.g., Vercel multi-instance), the main process starts a 30-second
fetchClientNavpoll loop (sseFallbackTimer) so the Navigate menu and tray stay in sync. The timer stops automatically when SSE reconnects. X-AHD-Client-Versionheader — Allnet.request()calls to the game origin (site-api.js) now sendX-AHD-Client-Version: <package.json version>so the server can detect outdated clients.corporation-enrich.js— Corporation-path logic extracted frommain.jsinto a dedicated module.corporationPathIdForUrlprefers APIpathId, falls back tosequentialId(including 0),sequential_id,_id, andid.mergeCharacterMeIntoManifestandstripCorporationEnrichmenthandle the full/api/character/me→ manifest merge lifecycle.- Corporation path encoding —
encodeURIComponentapplied to corporation IDs inmenu.js,game-panel-links.js, andpip.htmlso slug-basedpathIdvalues are safe in URLs. - Tests — New
tests/unit/corporation-enrich.test.jscoveringcorporationPathIdForUrl,stripCorporationEnrichment, andmergeCharacterMeIntoManifestedge cases.
Changed#
- Dashboard poll interval —
DashboardPollerdefault poll period increased from 10s to 60s to match the spec for dashboard bar updates. - Window focus/show — Both
focusandshowevents on the main window now triggerpullClientNav, so the Navigate menu refreshes when the window is restored from minimize as well as when it gains focus. - Corporation/stockmarket re-sync — Navigating to
/corporationor/stockmarketpaths triggers apullClientNavso corporation menu items and CEO state update after in-app actions. - Cache schema —
cache.jsschema extended with acountriesentry (typearray, default[]).
Fixed#
- Corporation enrichment on error —
enrichClientNavManifestnow callsstripCorporationEnrichmenton catch, so stalemyCorporationId/isCeovalues are cleared when/api/character/mefails (401, network error, etc.) instead of persisting old data. - Countries cache default — Cache schema
countriesdefault is[](array) to preventgetCountries()returningundefinedon first launch.
[1.1.0] - 2026-04-07#
Added#
- GitHub Actions — multi-platform releases — Pushing a
v*tag runs tests once on Ubuntu, then builds Windows (NSIS.exe), macOS (.dmg), and Linux (.AppImage) on native runners; all artifacts attach to a single GitHub Release (.github/workflows/release.yml). - Keyboard shortcuts UI — Game menu → Customize Game Panel… includes a Keyboard Shortcuts tab to override global accelerators; stored in
userPreferences.customShortcuts(save-shortcuts/get-custom-shortcutsIPC,shortcuts.js). - Documentation — Rewrote
README.md(player features table, npm scripts, project tree, contributing, release badge, MIT license aligned withpackage.json); addeddocs/README.md(index) anddocs/architecture.md(main modules, client-nav pipeline, preload and game-panel IPC).
Fixed#
- Navigate menu —
src/nav-manifest.jsderiveshasCharacterwhen/api/client-navomits the flag but still sendshomeState,adminCharacters, nestedcharacter, orhas_character, so Profile / State / Nation / World items are not hidden behind only Pop Out Window. - Navigate menu (timing) —
pullClientNav({ retryOnNull: true })insrc/main.jsretries/api/client-navafterdid-finish-loadand first SSE connect when the response isnull(session/cookies briefly behind the page load), instead of waiting for the 30–60s poll. - Cmd+K command palette —
injectCommandPaletteinsrc/main.jsreads stringroutevalues fromgetNavForCountry()objects (executive,legislature, etc.) instead of passing whole objects intonavigate.
Changed#
- Unsigned macOS builds —
CSC_IDENTITY_AUTO_DISCOVERY=falsefor CI andnpm run build:mac;package.jsonbuild.macsetshardenedRuntime: falseandgatekeeperAssess: falseso DMGs build without Apple signing keys (users may need to right-click → Open the first time).
[1.0.3] - 2026-04-06#
Added#
- Game menu — customizable quick links — The Game menu opens with shortcuts (Profile, Campaign HQ, Notifications, Portfolio, corporation). Customize Game Panel… opens a small window to enable or disable built-in links and add custom paths; the layout is stored in
userPreferences.gamePanelEntries. - CEO / Create a corporation — The corporation shortcut is included by default. Labels: CEO →
/corporation/{id}/ceo; My corporation →/corporation/{id}when the character has a corporation but is not CEO; Create a corporation →/corporation/newwhen none. Client-nav enrichment mergesisCeoandmyCorporationIdfrom/api/character/me(supports alternate field shapes and 2xx-only JSON parsing for that request). - IPC —
get-game-panel-config,set-game-panel-entries, andreset-game-panel-entriessupport the config window (game-panel-config.html+ preload). - Active game URL —
src/active-game-url.jsresolves the current game origin; works with environment-driven config and dev/sandbox toggles (src/game-server-dev.js). - PiP / turn dashboard — Richer floating dashboard (multi-view Standard / Corp / Elections / Global, customizable bar and custom panel layout, AP and stat strip).
- PiP view data —
pip-view-poller.jspolls/api/pip/standard,/api/pip/corp,/api/pip/elections, and/api/pip/globalon a 60s interval (with immediate refresh on view change) to hydrate each view and custom-panel bundles. - PiP labels —
pip-labels.jsmaps party slugs, election and corporation types, and related display strings for the PiP window. - Compact currency in PiP — Dollar amounts use suffix-style formatting (e.g.
$130.19k,$140m) viaformat-compact-number.jsinstead of locale thousands grouping.
Changed#
- Main process URL loading — Components that previously used a fixed
config.GAME_URLnow useactiveGameUrl.get()where appropriate so dev, sandbox, and production origins stay consistent (menus, tray, shortcuts, windows, SSE, dashboard, devtools, error handler, etc.).
Tests#
- Unit coverage for
game-panel-links,active-game-url,game-server-dev,format-compact-number, andpip-labels.
[1.0.2] - 2026-04-04#
Added#
- Game server selection (View menu) — Use sandbox / test server (Supporter+) points at
https://test.ahousedividedgame.comby default (AHD_SANDBOX_GAME_URLoverrides). Withnpm run dev(NODE_ENV=development), Use local dev server (the local dev server) loadsthe local dev server(AHD_DEV_GAME_URLoverrides); it is mutually exclusive with the test-server toggle. Preferences:useSandboxServer,useDevServer. - Focused view & website navbar parity (main process) — country config and URL helpers (
src/countries.js,src/urls.js) align executive, legislature, budget, metrics, and related paths with the web app (e.g./white-house,/congress,/national-metrics?country=). src/site-api.js— shared authenticated GET/POST against the game origin (fetchClientNav,fetchCharacterMe,postJsonAuthed) using thepersist:ahdsession.src/nav-manifest.js— normalizescharacter_countryIdvscharacterCountryIdfrom/api/client-navfor a single internal shape.- Client-nav enrichment — after each manifest, optionally merges
myCorporationIdfrom/api/character/mewhencorporation.sequentialIdis present (World → My Corporation in the site UI). - IPC for in-page / Electron navbar —
fetch-nav-data,navigate-to,open-external,switch-character,sign-out; preload whitelist extended withnav-data-updated,toggle-focused-view, andnavigate(receive). nav-data-updatedevent — same payload asclient-nav, for renderers that follow the newer channel name.- Tray —
setFocusedViewToggleHandleradds a Toggle Focused View item; mirrors View → Focused Mode. - Global shortcut —
CmdOrCtrl+Shift+Ftoggles focused vs classic display mode (cookieahd-display-mode+ reload); fundraise moved toCmdOrCtrl+Alt+F. - Tests —
nav-manifest, extended IPC (nav handlers,isGameUrlgate for absolute URLs), preload allowlist, tray toggle handler,urlshelpers.
Fixed#
navigate-to/profile — IPC navigation maps/profileto/politician, matching the live “My Politician” route (spec text used/profile).- Native notification spam — SSE frames with no configured desktop notification type (including the default SSE type
messageand server events such astheme_changed) no longer trigger a generic “A House Divided” notification. Only types listed in the client notification map and explicitnotificationevents alert the user.
Changed#
- Pop-out windows use session partition
persist:ahdso login state matches the main window. - PiP dashboard and DevTools panel windows enable
sandbox: trueto align with the main window’s renderer hardening. /api/client-nav— overlapping fetches share a single in-flight request; responses larger than 512 KiB are dropped to bound main-process memory use.- Client-nav polling — interval is 30 seconds while the main window is focused and 60 seconds when unfocused (SSE connect/disconnect still restarts the timer).
- Navigate menu & window presets — follow the new country paths; presidential election prefers
activePresidentElectionSeatIdwhen present; Navigate includes national budget, campaign HQ, central bank, stock market, and expanded ordering toward parity with the site’s Nation dropdown. - IPC
set-preference— onlynotificationsEnabled,miniModeEnabled, anddisplayModeare accepted. - IPC
set-zoom— zoom factor is clamped between 0.25 and 3 and non-finite values are ignored.
Security#
navigate-to— absolutehttp(s)URLs are loaded only when they pass the same host check as the main game window (isGameUrl); other origins are ignored.
[1.0.1] - 2026-03-12#
Fixed#
- Restore application menu bar (Game / Navigate / View / Help) on Windows — it was hidden by the
titleBarStyle: 'hidden'setting introduced in 1.0.0
Changed#
- Removed custom titlebar overlay colours (reverted
titleBarStyle: 'hidden'andtitleBarOverlay) to keep the native application menu visible - Theme background colours per theme still applied on window creation (eliminates load-flash)
[1.0.0] - 2026-03-12#
Added#
- Country-aware navigation — menus and window presets update dynamically based on the player's character country (US, UK, CA, DE)
/api/client-navintegration — replaces/api/auth/me; single endpoint delivers user, nav config, unread count, party, and active election state- 404 recovery overlay — detects HTTP 404 responses and injects a "Page not found" overlay with a Go Home button
- Network failure overlay — detects connection failures and injects a "Connection lost" overlay with a retry button
go-homeIPC handler — renderer can trigger a navigation back to the game home page- Dynamic Navigate menu — legislature, executive, and election items reflect the active country; My Party and Presidential Election items appear only when applicable
WindowManager.updatePresets(nav)— congress and country window presets update their routes/titles when country changes- Per-theme window background colours — eliminates white flash on load for dark themes
- Custom titlebar overlay colours per theme (Windows) — close/min/max buttons match the active theme
- Turn Dashboard Widget — replaces PiP with a full dashboard showing action points, funds, election countdown, and more
- Dashboard Poller — polls
/api/game/turn/dashboardand feeds data into the tray/cache pipeline - Focused mode —
ahd-display-modecookie hides the game's in-page navigation when using the desktop client - SSE integration — real-time event stream for turn completion, notifications, and state sync
- System tray — game state summary, unread notification badge
- Auto-updater — checks for new releases on launch via
electron-updater - Keyboard shortcuts — toggle status bar, mini mode, open feedback dialog
- Multi-window presets — elections, congress, campaign, state, country, notifications pop-outs
- Automated GitHub Actions release workflow — tag
v*triggers Windows build and uploads.exeto GitHub Releases
A House Divided
Grand Century
MetroForge
Verdigris
Electioneer