Commit Graph
49 Commits
Author SHA1 Message Date
Jannis Braun fff39f8d76 polish(desktop): non-destructive Change Instance + recovery enter/exit logs
UX bug found during smoke testing: clicking Change Instance immediately
deleted the saved instance URL and showed an empty picker, with no way
back if the user changed their mind.

Fix:
- Don't clearInstanceUrl() in recovery action 'change-instance' — picker
  is now non-destructive
- Picker pre-fills the input with the current saved URL when present
- Cancel button (shown only when a saved URL exists) returns to current
  instance via idempotent setInstanceUrl re-save
- Header copy switches to 'Switch instance' / 'Cancel to stay' framing
  when a saved URL is present
- URL only overwrites on explicit Connect to a different instance

Also: add console.log enter/exit lines in enterRecoveryMode and the
clear-recovery-state action handlers, so smoke-test scripts can grep
stderr for recovery activity without UI introspection.

Spec + docs/systems/desktop.md updated.
2026-05-03 13:16:55 +02:00
Jannis Braun cff9a8e2cf polish(desktop): final cross-cutting fixes
Two real bugs from final review:
- Clear recovery state on window 'closed' so macOS dock-activate doesn't
  drop the recovery surface (window recreated with stale recoveryStore.mode)
- Hoist setOnQuitRequested before createWindow so synchronous boot failures
  reach a wired Quit handler

Three polish items:
- Tray's Change Instance now routes through handleRecoveryAction so both
  paths share one implementation; recovery action's change-instance also
  show()+focus() for hidden-window tray clicks
- install-update action guards against state.updateState !== 'downloaded'
  (defense in depth against malicious or buggy renderers)
- Object.freeze rationale documented in RecoveryStateStore.update
2026-05-03 12:19:51 +02:00
Jannis Braun ece81a2b71 feat(desktop): add recovery bridge methods to preload + types
- rendererReady (boot-completion ping)
- getRecoveryState / onRecoveryStateChanged (recovery.html subscribers)
- recoveryAction (button click dispatcher with enum action)

Type declarations kept ambient (no export) to preserve window.backspace
global augmentation — exporting from an ambient .d.ts converts it to a
module and breaks the Window interface extension.
2026-05-03 11:57:51 +02:00
Jannis Braun d8266e62da feat(desktop): wire auto-updater events into recovery store
- All updater events update recoveryStore (drives tray/menu UI)
- Native notification fires on update-downloaded when window unfocused
- Notification click calls quitAndInstall() directly (force-kill fix)
- Preserves existing renderer IPC channels (in-app banner unaffected)
- Graceful degradation when electron-updater module unavailable
2026-05-03 11:52:21 +02:00
Jannis Braun 56677da8ad refactor(desktop): pass trayActions to buildAppMenuTemplate, remove appMenuActions duplication
buildAppMenuTemplate's actions param is Partial<MenuActions>, so the
tray-only callbacks (onShow/onHide/onQuit) are simply ignored. Removes
the three-callback duplication between the two objects without changing
behavior. Future onChangeInstance/onCheckForUpdates/onRestartToInstall
changes only need to be made in one place.
2026-05-03 11:49:42 +02:00
Jannis Braun bb25a3ba5e feat(desktop): wire recovery store into main process
- setMainWindow on createWindow, setMainWindow(null) on closed
- attachRecoveryHandlers wires Electron unresponsive/crash/load-fail events
- Store subscriber drives tray context menu + macOS app menu + mode-gated
  recovery-state-changed push to renderer; single applyMenusForState
  function shared between subscriber and initial fire (no drift)
- Old hard-coded createTray Menu and macOS app-menu construction deleted;
  Win/Linux Edit-only menu retained as one-time setup for keyboard accelerators
- requestQuit exported, wired via setOnQuitRequested callback
- Recovery IPC handlers: renderer-ready, recovery-action, get-recovery-state
- setAppUserModelId('com.backspace.desktop') for Win32 notification attribution
- Extended showNotification with optional onClick (existing 2-arg callers unchanged)
2026-05-03 04:30:58 +02:00
Jannis Braun 1c4bde3029 feat(desktop): attach Electron webContents handlers for recovery detection
- did-navigate + did-finish-load arm/disarm boot timer (skips SPA routing)
- did-fail-load filtered by isMainFrame and ERR_ABORTED
- render-process-gone filtered to exclude clean-exit
- unresponsive uses 10s grace with responsive cancellation
2026-05-03 04:25:03 +02:00
Jannis Braun 0c489701fc feat(desktop): add enterRecoveryMode and recovery action funnel
- mainWindow/autoUpdater/quit handover via setter pattern (no main.ts imports)
- Optimistic recovery exit on Reload — re-enters via did-fail-load if reload fails
- install-update calls autoUpdater.quitAndInstall() directly (force-kill fix)
- Strict allowlist validation in handleRecoveryAction
2026-05-03 04:20:22 +02:00
Jannis Braun 323b9017d7 feat(desktop): add boot-completion timer with packaged-only and URL-protocol guards 2026-05-03 04:16:22 +02:00
Jannis Braun a9122d1fdf feat(desktop): add macOS app menu template builder
Adds buildAppMenuTemplate pure function to recovery.ts that produces the
three-submenu macOS app menu (App/Edit/Window), reusing MenuActions and
checkForUpdatesItem from T4. Includes Restart to Install Update item
conditionally on updateState=downloaded. Tests use destructuring to satisfy
noUncheckedIndexedAccess. 26 tests pass, tsc clean.
2026-05-03 04:13:19 +02:00
Jannis Braun 172f53e3a5 feat(desktop): add tray menu template builder with state-driven labels
Pure buildTrayMenuTemplate function returns MenuItemConstructorOptions[]
without constructing real Menu objects, enabling full test coverage.
All 7 new tests pass (22 total); MenuActions interface and
checkForUpdatesItem helper are intentionally unexported.
2026-05-03 04:10:11 +02:00
Jannis Braun b5273b596a feat(desktop): add extractErrorCode helper for updater errors 2026-05-03 04:07:46 +02:00
Jannis Braun d3b3abacff harden(desktop): RecoveryStateStore listener safety + frozen state
- Snapshot listener set before notifying so subscribers can subscribe/
  unsubscribe during notification without breaking the pass
- Per-callback try/catch so one throwing subscriber does not silence others
- Object.freeze on each state object so the live reference returned by
  get() cannot be accidentally mutated externally (compile-time
  Readonly<> is hint only)
- 3 new tests pinning these invariants
2026-05-03 04:06:43 +02:00
Jannis Braun 15dfa0a68c feat(desktop): add RecoveryStateStore with subscriber lifecycle 2026-05-03 04:03:07 +02:00
Jannis Braun dcbae46e0e refactor(desktop): extract instance-URL helpers to instanceUrl.ts
Pure mechanical extraction — no behavior change. Enables recovery.ts
to share these helpers without a circular import on main.ts.
2026-05-03 03:58:00 +02:00
Jannis Braun f538f1d92e fix(desktop): enable PulseAudio loopback flag so screen share starts on Linux
Chromium gates the PulseAudio loopback path behind the
PulseaudioLoopbackForScreenShare feature flag. Without it, returning
audio: 'loopback' from setDisplayMediaRequestHandler rejects the whole
getDisplayMedia request, so screen share never starts when the user has
"Share system audio" enabled. Also surface a clear warning toast on
loopback failure (PipeWire-only without pulse compat, macOS without
Catap) instead of failing silently — no auto-retry, since the picker
selection is already consumed.
2026-04-29 23:43:44 +02:00
Jannis Braun b00d21df0e feat(desktop): preload exposes setConnectedOrigins + onOpenInternalRoute 2026-04-29 22:07:26 +02:00
Jannis Braun eb57d2fd5c feat(desktop): intercept own-instance /join URLs in setWindowOpenHandler 2026-04-29 22:05:50 +02:00
Jannis Braun bd4a7b073a fix(desktop): rewrite tray loader with explicit per-platform branches
Three branches now: macOS template (.png + @2x, auto-recolour), Windows
multi-size .ico (DPI auto-pick), Linux 22x22 PNG. Drops the runtime
.resize({16,16}) — pre-rendered assets are at correct platform sizes;
runtime resize re-introduced bicubic blur. Defensive fallback to the
programmatic blurple circle is preserved but should not trigger in
practice now that templates ship populated.
2026-04-27 14:38:42 +02:00
Jannis Braun e7c65ca308 fix(desktop): preserve startMinimized preference across off/on cycle on Windows
When autostart is disabled, Windows deletes the Run registry entry, so
deriveStartMinimizedFromArgs(undefined) was returning false and silently
resetting the user's startMinimized preference the next time autostart
was re-enabled. Both the get- and set-auto-launch-settings handlers now
fall back to the disk-cached value when no OS entry exists, matching the
existing macOS/Linux fallback pattern.
2026-04-27 13:22:14 +02:00
Jannis Braun 962b669cac docs(desktop): clarify cross-platform hidden-launch detection 2026-04-27 12:54:42 +02:00
Jannis Braun b0d0ae7be6 fix(desktop): drop unconditional startup re-apply, narrow to AppImage path-refresh 2026-04-27 12:52:18 +02:00
Jannis Braun 0d652a72cd fix(desktop): base set-auto-launch-settings on OS truth, not stale disk 2026-04-27 12:49:00 +02:00
Jannis Braun cd00da07d8 fix(desktop): make get-auto-launch-settings OS-authoritative on Windows 2026-04-27 12:46:10 +02:00
Jannis Braun 622b9a6929 fix(desktop): pass enabled/path/args/name to setLoginItemSettings per platform
macOS: add args:[--hidden] alongside openAsHidden for defence-in-depth detection on macOS 13+.
Windows: add enabled/path/args/name so re-enabling the toggle clears the StartupApproved\Run disable marker.
Linux: add deterministic name:'backspace' so the .desktop filename is stable across Electron/AppImage updates.
2026-04-27 12:40:44 +02:00
Jannis Braun 495cad717d refactor(desktop): tighten autoLaunch helpers per code review 2026-04-27 12:38:33 +02:00
Jannis Braun 191ba5f248 feat(desktop): add pure auto-launch helpers with tests 2026-04-27 12:33:52 +02:00
Jannis Braun 1bdd9a387a revert: remove ineffective GPU encoding flags from Electron
Hardware H.264 encoding in Electron's WebRTC requires SDP profile
negotiation changes (NVENC rejects Constrained Baseline), not
command-line flags. VP9 via libvpx is the correct default — proven
by empirical benchmarks (CS2: VP9 60fps vs H.264 30fps).
2026-03-24 02:00:43 +01:00
Jannis Braun 6844f8d8fd fix: clean up GPU flags — hardware H.264 encoding not available on Win/Mac
AcceleratedVideoEncoder only enables VA-API encoding on Linux.
Windows/macOS hardware H.264 encoding in Chromium's WebRTC is not
reliably controllable — VP9 via libvpx remains the recommended codec.
2026-03-24 01:55:18 +01:00
Jannis Braun e4f6f9a4c4 fix: translate uiohook keycodes to match web UI keybind hashes
The web UI records keybinds as djb2 hashes of DOM event.code strings
(e.g. "KeyM" → 2090563626), but uiohook fires native scan codes
(e.g. M → 50). These never matched, so global keybinds only worked
in-focus via the web fallback, never out-of-focus via the native hook.

Add a uiohook-to-DOM-code mapping table and pre-compute the djb2
hashes so pressedKeys uses the same numbering as the keybind store.
2026-03-22 23:50:02 +01:00
Jannis Braun 3ce9ee0db0 feat(desktop): add KeybindManager with uiohook-napi for global shortcuts
Installs uiohook-napi for OS-level non-consuming input hooks, creates
KeybindManager class that receives keybind configs from the renderer,
matches the OS-wide key/mouse event stream, and sends matched actions
back via IPC. Exposes syncKeybinds, onKeybindAction, onAccessibilityStatus,
onKeybindHookError, and checkAccessibility APIs through the preload bridge.
2026-03-22 20:47:41 +01:00
Jannis Braun 948fa6de5c feat(desktop): add remote dictionary sync engine to activity detector
Refactor loadDictionary to accept a file path and support both legacy
bare-array (version 0) and versioned object formats. Add background
sync that fetches the latest games.json from GitHub, uses ETag-based
conditional requests (304 support), atomic cache writes, and hot-swaps
processMap/gameEntries without resetting current detection state.
On startup, the best local source (cache vs bundled seed) is loaded
immediately for instant detection, then sync runs fire-and-forget.
2026-03-21 04:12:21 +01:00
Jannis Braun 2c122d066e fix: add console warnings for invalid games.json entries 2026-03-21 03:00:14 +01:00
Jannis Braun 0f4f129337 feat(desktop): add automatic game/app activity detection
Add cross-platform process scanning that detects running games and apps,
then broadcasts them as Rich Presence activities via the existing Phase 1
pipeline (activityStore -> WS -> other users).

- games.json dictionary with 26 entries (games, Spotify, VLC, OBS)
- activityDetector module: polls OS process list every 15s via execFile,
  matches against dictionary, emits IPC on state change only
- IPC bridge in main.ts/preload.ts with cleanup on quit
- Frontend activityBridge subscribes to IPC and feeds activityStore
- Supports Windows (tasklist CSV), macOS (ps -c), Linux (ps)
2026-03-21 02:57:30 +01:00
Jannis Braun 6ebb124d25 feat(desktop): update preload bridge types for enhanced update-error and getVersion 2026-03-21 02:30:06 +01:00
Jannis Braun a7d5abfca9 feat(desktop): enhance update-error payload with release URL, add get-app-version IPC 2026-03-21 02:29:42 +01:00
Jannis Braun ac7d3cb71b feat: add start-at-boot setting and fix tray icon to use app logo
Add auto-launch settings (start at boot + start minimized) to the
Desktop section in Account settings, using Electron's built-in
setLoginItemSettings API across macOS, Windows, and Linux.

Replace the fallback colored-circle tray icon with proper B logo assets:
template images for macOS (adapts to light/dark menu bar) and colored
icons for Windows/Linux. Fix BGRA channel order bug in fallback generator
and move tray icons to resources/ so they're packaged into the app.
2026-03-17 02:28:38 +01:00
Jannis Braun 26f2e2a817 fix: remove experimental useSystemPicker causing double-picker and no audio on macOS
useSystemPicker triggers applyConstraints to re-open the system picker
(Electron #44684) and doesn't reliably pass audio capture. All platforms
now use the custom Aether Drift picker with explicit loopback control.
2026-03-16 19:13:14 +01:00
Jannis Braun bda7930e61 fix: restore setDisplayMediaRequestHandler with useSystemPicker
Electron requires setDisplayMediaRequestHandler for getDisplayMedia() to
work — removing it broke screen sharing entirely. Restored the handler
with useSystemPicker: true, which on macOS 15+ uses the native system
picker (honoring restrictOwnAudio), while Windows/Linux fall back to
the custom picker with the shareAudio toggle for echo control.
2026-03-16 18:34:50 +01:00
Jannis Braun 9ff3761640 fix: remove setDisplayMediaRequestHandler to let restrictOwnAudio work natively
The custom screen share picker intercepted getDisplayMedia() and created
a raw loopback stream, bypassing Chromium's constraint pipeline entirely.
restrictOwnAudio was silently discarded. Removing the handler lets
Chromium 144's native getDisplayMedia run end-to-end with restrictOwnAudio
applied, eliminating the audio feedback loop in the desktop app.
2026-03-16 18:18:42 +01:00
Jannis Braun 46f55643ae fix: eliminate screen share audio feedback loop + upgrade Electron 33→40
Screen sharing with audio captured the app's own voice playback, causing
users to hear themselves echoed back. Fixed via two layers:

- Add restrictOwnAudio constraint (Chrome 141+/Chromium 144) to exclude
  the app's own audio from system audio capture
- Add shareAudio toggle so users can disable system audio entirely
- Remove outdated macOS audio block (now supported via ScreenCaptureKit)
- Upgrade Electron 33→40 (Chromium 130→144) so restrictOwnAudio works
  natively in the desktop app
- Add NSAudioCaptureUsageDescription for macOS 14.2+ audio capture
- Add GTK 3 fallback for Linux GNOME compatibility (Electron 36+)
2026-03-16 18:01:40 +01:00
Jannis Braun 7a86cde67e feat: frameless title bar for Windows/Linux with native overlay controls
Use titleBarStyle: 'hidden' + titleBarOverlay on Win/Linux to remove the
ugly native title bar and menu bar while keeping OS-rendered min/max/close
buttons. Hidden Edit menu preserves keyboard shortcuts (Ctrl+C/V/X/Z/A).
Title bar drag region and separator line rendered via web frontend, with
colors matching the Aether Drift design system.

Also adds electron-builder metadata (description, author, homepage,
artifactName) and multi-size icons for cross-platform builds.
2026-03-16 06:16:43 +01:00
Jannis Braun cd2f1e52ef fix: proper macOS dock icon via .icns bundle replacement
Replace the Swift squircle hack with the correct Apple approach:
generate a proper .icns using sips + iconutil, then replace
electron.icns in the Electron bundle before launch. macOS applies
its native squircle mask + shadow from the .icns automatically.
2026-03-16 04:29:09 +01:00
Jannis Braun c176ea801a feat: squircle dock icon for macOS dev mode
macOS only applies the squircle mask to packaged .app bundles. For dev
mode, generate a pre-masked icon-dock.png via Swift/AppKit at launch
and set it with app.dock.setIcon(). Silently skipped on non-macOS.
2026-03-16 04:15:09 +01:00
Jannis Braun da05eb3262 fix: replace placeholder purple square icons with actual Backspace logo
Regenerate all web icons (favicon, PWA, apple-touch) from master 1024x1024
icon.png. Add BrowserWindow icon property for Windows/Linux taskbar icon
in Electron dev mode.
2026-03-16 04:01:52 +01:00
Jannis Braun 95ec3ff952 feat: Electron screen share picker, instance selector, and system audio loopback
- Custom screen share picker for Electron (ScreenSharePicker.tsx) with
  Aether Drift design: glass-bubble footer, adaptive grid, pill tabs,
  border-based selection (avoids overflow clipping), hover brightness
- Instance URL picker so Electron connects to any Backspace server
- System audio loopback on Windows/Linux via desktopCapturer callback
- macOS: video-only callback (OS blocks system audio capture)
- IPC bridge for screen source enumeration and selection
- Purge stale service worker caches on Electron launch
2026-03-16 03:50:07 +01:00
Jannis Braun 8ae3ffc912 feat: Electron desktop app — hardening, IPC bridge, and dev launch fixes
- Dev/prod URL auto-detection (Vite 5173 in dev, server 3000 in prod)
- Typed IPC bridge via preload (notifications, badge, window controls, updates, deep links)
- Native OS notifications via NotificationController with window focus suppression
- Auto-update via electron-updater with UpdateToast UI
- Deep linking (backspace:// protocol) for macOS and Windows/Linux
- Window state persistence (position, size, maximize across restarts)
- Tray icon with graceful fallback when icon asset missing
- Suppress PWA service worker polling/reloads inside Electron
- Platform detection layer (isElectron, getElectronAPI)
- Root workspace scripts (dev:desktop, build:desktop)
- Document BACKSPACE_URL and BACKSPACE_UPDATE_URL env vars
2026-03-16 00:10:47 +01:00
Jannis Braun e219229b63 feat: add Backspace design prototype and rebrand from Opencord
- Add Backspace-design-prototype.html: finalized "Aether Drift" design
  language with warm matte surfaces and subtle frosted glass accents
- Update CLAUDE.md with DESIGN SYSTEM section and remove Discord clone references
- Rename all Opencord references to Backspace across the full codebase
- Archive outdated design experiments and Discord-specific assets
- Add science-backed accessibility fallback (prefers-reduced-transparency)
2026-03-01 21:26:57 +01:00
Jannis Braun 4fd17084a5 chore: Initial commit of Opencord base state 2026-02-18 02:49:21 +01:00