Commit Graph
1628 Commits
Author SHA1 Message Date
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 3dc3043faf Merge branch 'feature/audio-device-ux': polish audio device UX
15 commits implementing:
- useAudioDevices hook (single source of truth, privacy-respecting enumeration)
- AudioManager extensions (test tone, upstream-track-end event, hot-plug helpers)
- Seamless hot-plug in AppLayout (debounced re-acquire + new-device toast)
- Mic-track-loss recovery via AudioManager.onInputTrackEnded (corrected from original published-track design)
- Full Audio Input/Output sections in Voice & Video settings
- Privacy fix in UserAreaPanel (eliminates unconditional getUserMedia probe)
- Spec parallel to Camera Device Selection in voice.md
2026-05-03 02:31:27 +02:00
Jannis Braun 19d42445b0 polish(web): clear device lists on permission revoke; document probe-constraint omission 2026-05-03 02:09:13 +02:00
Jannis Braun 4044e910c3 docs(voice): add Audio Device Selection section parallel to Camera Device Selection 2026-05-03 02:01:09 +02:00
Jannis Braun 6905071459 refactor(web): UserAreaPanel uses useAudioDevices — kills unconditional getUserMedia probe; reuses shared DropdownItem 2026-05-03 01:56:24 +02:00
Jannis Braun ff8dcc28ea feat(web): wire AudioInput/Output sections into Voice & Video settings; fix EC tooltip 2026-05-03 01:52:20 +02:00
Jannis Braun 5b8af14e12 feat(web): AudioOutputSection — output picker, volume, test tone, lifecycle-aware sinkId check
- Uses shared SectionShell/DropdownItem from ./_shared/SettingsPickerPrimitives
  rather than redefining local copies (parity with AudioInputSection).
- supportsSinkId is reactive to AudioContext lifecycle via the audioCtxGen
  pattern (bumped by AudioManager.onResumed). Defaults to true and only flips
  to false when a real context exists AND lacks setSinkId (Safari < 17), so
  the picker is never preemptively hidden when the user opens Settings before
  joining voice. AudioManager.setOutputDevice + initContext re-apply path
  handles the deferred sinkId binding once the context appears.
2026-05-03 01:47:49 +02:00
Jannis Braun 8e67f1d553 fix(web): mic meter reacts to AudioContext lifecycle; extract picker primitives
- AudioInputSection now subscribes to AudioManager.onResumed and bumps an
  audioCtxGen state on each 'running' transition. Mic-level meter and
  resolved-default hint effects depend on it, so opening Settings before
  joining voice and then joining voice activates the meter without needing
  to remount the panel. Footer copy updated to match the new behavior.
- SectionShell and DropdownItem extracted to
  settingsPanels/_shared/SettingsPickerPrimitives.tsx so Task 6
  (AudioOutputSection) can import them instead of triplicating the markup.
  The _shared/ folder keeps these settings-internal primitives out of the
  broader ui/ namespace.
- Deliberate scope choice: VideoSection.tsx still has its own DropdownItem
  copy. Unifying all three is left to a follow-up; touching VideoSection
  here would expand scope beyond the audio-device-ux branch.
2026-05-03 01:43:01 +02:00
Jannis Braun 3aea8210bd feat(web): AudioInputSection — full input picker with level meter and resolved-default hint 2026-05-03 01:37:37 +02:00
Jannis Braun 8907bb305d fix(web): mic-loss recovery uses 'could not be restored' for unclassified errors 2026-05-03 01:35:07 +02:00
Jannis Braun 63bf9e684c fix(web): mic-loss detection observes upstream stream, not published clone
The previous Task 4 handler installed onended on the *published* mic track,
but that track is a clone of AudioManager's MediaStreamAudioDestinationNode
output — destination-node tracks never end on upstream loss, they just go
silent. The handler also called setInputDevice for silent-recovery and assumed
syncMic would re-publish, but syncMic's dep array does not depend on
streamGeneration, so the recovery never republished.

This commit moves loss detection into AudioManager (where the upstream
getUserMedia track lives) via a new onInputTrackEnded subscription, extracts
republishMicrophone from syncMic into a module-level helper that both the
normal device-change path and the recovery path call directly, and removes
the published-track Microphone branch from RoomEvent.LocalTrackPublished.
The plan and Task 9 spec text are updated to match.
2026-05-03 01:20:13 +02:00
Jannis Braun 9a26513009 feat(web): mic-track-loss recovery — probe, attempt re-acquire, toast 2026-05-03 01:09:04 +02:00
Jannis Braun 674d011d3c fix(web): debounce devicechange handler and gate listener on baseline seeding 2026-05-03 01:05:37 +02:00
Jannis Braun d992b2bb41 feat(web): seamless audio hot-plug — re-acquire live stream + toast new devices 2026-05-03 01:01:03 +02:00
Jannis Braun 2726cc5c5a feat(web): expose AudioManager helpers for device introspection + test tone 2026-05-03 00:57:26 +02:00
Jannis Braun a1ebb23264 feat(web): add useAudioDevices hook for permission-aware audio enumeration 2026-05-03 00:51:20 +02:00
Jannis Braun c0e71b1ded fix(federation): hydrate downloads replicated avatars locally + backfill stale URL rows
hydrateReplicatedUserProfile now calls downloadProfileAsset and stores bare local filenames, falling back to absolute URLs only on download failure. It also fills empty fields only — no longer clobbering local files written by processProfileUpdateEvent. Adds an idempotent startup backfill that converts existing http-prefixed avatar/banner rows on replicated users into local files, so federated profile pictures keep rendering when the home instance is offline.
2026-05-02 22:45:27 +02:00
Jannis Braun c0fd7e3a75 feat(web): home tile uses primary purple when active/hovered, grey when idle
Replaces the full-bleed black-bg logo badge in the top-left @me tile with a
white logo mark on the primary purple (matching toggle switches) when active
or hovered, and `--interactive-muted` grey when idle. Logo bumped to 25px.
2026-05-02 22:12:18 +02:00
Jannis Braun 12a71a12d4 fix(web): close writable on download pause/abort so partial bytes flush (resume continues from offset) 2026-05-02 21:13:09 +02:00
Jannis Braun f33ebe2aa7 chore(web): drop unused React import; static-import idbHandles consistently 2026-05-02 20:54:50 +02:00
Jannis Braun bc32eccc2d fix(server): CORS allows tus headers (federated uploads no longer blocked at preflight) 2026-05-02 20:42:02 +02:00
Jannis Braun 3a050f475b fix(web): tus uploads use per-origin token (federation auth) 2026-05-02 20:37:17 +02:00
Jannis Braun 33cfc66ac4 fix(web): boot rehydrate normalizes transfers; paused has its own visual 2026-05-02 19:10:29 +02:00
Jannis Braun 61be9d013f fix(web): hide Retry when blob unavailable; failed-state surfaces actionable error
Pending bubbles that survive a reload (or post-redeploy refresh) without a
FileSystemFileHandle had no way to recover the original File bytes, so the
Retry button silently no-op'd: resumeUpload set state back to 'paused' with
no surfaced error, leaving the user stuck.

- transferStore: add reactive hasInMemoryFile Set mirroring liveUploadFiles.
  setInMemoryRef helper keeps both in sync at every set/delete site
  (startUpload, resumeUpload, onSuccess, remove). Persisted shape is unchanged.
- resumeUpload: when no blob is reachable (no in-memory File and no FS handle),
  call setError with an actionable "File no longer available — discard and
  re-upload" message instead of silently flipping back to 'paused'.
- Message.tsx: compute canRetry reactively from transfersForRow + hasInMemoryFile;
  hide the Retry button when retry is infeasible. Discard remains.
- AttachmentProgress: optional error prop surfaces transfer.error.message via
  title= on the failed-state ring for hover context.
- Tests: existing 3 resumeUpload tests now assert state==='failed' with the
  actionable message; +2 new tests for hasInMemoryFile lifecycle (start/remove,
  abort retains).
2026-05-02 18:58:05 +02:00
Jannis Braun 2f0940c30b feat(admin): manual cleanup of stale tus upload sessions + visibility
Adds an admin-driven sweep on top of the existing 24h auto-expire so
operators can see and reap abandoned `.tus/` sessions without waiting.

- storageJanitor: extract `walkTusDir(predicate)` helper, add
  `getStaleTusInfo` + `cleanupStaleTusSessions(thresholdMs, dryRun)`;
  refactor `cleanupTusStragglers` to delegate while preserving its
  janitor-tick `{ removed }` contract.
- StorageStats gains `staleTusSessions` + `staleTusSize` (fixed 1h
  display threshold).
- New `POST /api/admin/storage/cleanup-tus` route with
  `maxAgeHours` validation (positive finite number, default 1) and
  `dryRun` support; admin-gated.
- StoragePanel: 6th overview card "Stale Uploads" + new cleanup
  subsection mirroring the media-cleanup pattern (preview-then-clean
  with shared result panel styling).
- Tests: 8 new janitor tests covering empty dir, threshold filtering,
  dry-run vs live, oldest-mtime tracking, subdir skipping, and the
  override path on the existing straggler sweep. New
  `routes/admin.test.ts` covers auth/admin gates, validation (zero,
  negative, NaN), default `maxAgeHours`, dry-run vs live unlink.
- Docs: `uploads.md` §Janitor expanded to the full lifecycle (cancel
  DELETE, discard DELETE, auto-expire, straggler sweep, admin route);
  `admin.md` Storage Management updated with the new endpoint and
  StorageStats fields.
2026-05-02 18:44:19 +02:00
Jannis Braun 4e5a440176 fix(web): server-side DELETE for failed/discarded uploads (no janitor wait) 2026-05-02 18:22:45 +02:00
Jannis Braun dd91f5f349 fix(web): retry-after-abort starts fresh tus session; preserve File past abort 2026-05-02 18:18:07 +02:00
Jannis Braun 61dea725a3 fix(web): polish failed-upload row — pill-shaped, content-width, proper button affordance 2026-05-02 18:11:31 +02:00
Jannis Braun a58b7459fb fix(web): in-session pause/resume preserves File ref; aborted transfers flip bubble to failed 2026-05-02 18:03:12 +02:00
Jannis Braun 403c6e9075 fix(web): orchestrator dispatches eager-upload bubbles via pendingMessageStore subscribe 2026-05-02 17:52:05 +02:00
Jannis Braun b8da1c1778 docs(design): tray as .glass popover; radial-progress ring primitive 2026-05-02 17:16:26 +02:00
Jannis Braun 38a3d4fba8 docs(api,federation): tus upload endpoints; federation worker boundary note 2026-05-02 17:16:00 +02:00
Jannis Braun fb7add4f83 docs(uploads): tus protocol, three-store architecture, download pipeline, capability matrix 2026-05-02 17:15:38 +02:00
Jannis Braun 348800bfca feat(web): channel-header transfer indicator + global tray panel 2026-05-02 17:11:41 +02:00
Jannis Braun 0be69808e4 feat(web): right-click Save Video / Save Audio via transferStore 2026-05-02 17:07:39 +02:00
Jannis Braun aaa15cef88 feat(web): file-card download routes through transferStore 2026-05-02 17:06:26 +02:00
Jannis Braun d3f45dbda4 feat(web): saveImage routes through transferStore 2026-05-02 17:05:51 +02:00
Jannis Braun 2be243336b feat: all profile uploads through transferStore; delete legacy POST /api/uploads
Migrates the remaining 5 profile/space upload sites (CreateSpace, AccountPanel
avatar+banner, OverviewPanel icon+banner) to transferStore.startUpload with
tray:false. Space sites pass _instanceOrigin so uploads route to the space's
home instance.

Removes upload/uploadWithProgress from api.uploads (and their private
uploadFile/uploadFileWithProgress helpers); api.uploads.url is preserved for
GET-path URL building. Deletes the server-side POST /api/uploads handler and
the now-unused @fastify/multipart plugin registration. GET /api/uploads/:filename
remains intact.
2026-05-02 16:46:29 +02:00
Jannis Braun 727c51f659 feat(web): RegisterPage avatar uses transferStore + waitForTransfer helper
Replaces the legacy /api/uploads call in RegisterPage with the tus-based
transferStore path. Extends Transfer to persist the server-assigned
filename (not just attachmentId) since downstream consumers store
attachment.filename on the user/space record.

- transferStore: rename setAttachmentId -> setAttachmentRef(id, attachmentId, filename)
  and add attachmentFilename field to Transfer
- both startUpload + resumeUpload onSuccess paths now record filename
- new utils/waitForTransfer.ts: waitForTransferAttachment(transferId)
  returns {attachmentId, filename}, with immediate-terminal handling
- RegisterPage: silent (tray:false) upload via transferStore, awaits
  the helper, passes the server filename to api.users.update
2026-05-02 16:40:50 +02:00