1710 Commits
Author SHA1 Message Date
Jannis Braun 3b1a0b64a3 feat(federation): relay envelope populates peer epoch baseline 2026-07-01 21:58:03 +02:00
Jannis Braun 8f60e92f94 feat(federation): deterministic baseline epoch-refresh worker 2026-07-01 21:49:18 +02:00
Jannis Braun bf74aa8bb2 feat(federation): signed /api/federation/epoch endpoint + caller 2026-07-01 21:43:02 +02:00
Jannis Braun 538519fcd2 feat(federation): exchange + store peer epoch on handshake 2026-07-01 21:35:20 +02:00
Jannis Braun 7acf48d0a4 feat(federation): shared epoch types + getInstanceId() 2026-07-01 21:19:25 +02:00
Jannis Braun d8f2b1a9c7 feat(federation): instance epoch schema + minting 2026-07-01 21:11:34 +02:00
Jannis Braun 807b2f6234 refactor(web): fold final-review polish into join modal feature 2026-07-01 19:06:10 +02:00
Jannis Braun cf9bcfbf1a docs(spaces): document discovery-first Join a Space modal 2026-07-01 18:59:48 +02:00
Jannis Braun 309abd86e2 feat(web): discovery-first Join a Space modal 2026-07-01 18:56:56 +02:00
Jannis Braun fc8df54cb0 feat(web): compact ExploreSpacePreviewCard for join modal 2026-07-01 18:51:31 +02:00
Jannis Braun e840dcbe40 refactor(web): SpaceCard uses shared useSpaceJoin hook 2026-07-01 18:47:16 +02:00
Jannis Braun 6c66af7a88 feat(web): add shared useSpaceJoin hook over exploreStore 2026-07-01 18:42:45 +02:00
Jannis Braun 215750cb7a fix(install): harden prereq/DNS paths against set -euo pipefail aborts
Found by running the previously-untested Docker-not-installed path in a
clean, no-dig container:

- resolve the current user with $(id -un), not $USER: under `set -u`
  an unset $USER aborted the script with 'USER: unbound variable' right
  after Docker was installed ($USER is not guaranteed set under sudo,
  `su` without -l, cron, or docker exec)
- make the Docker-install prompt EOF-safe (read ... || yn="") so a
  non-interactive / piped stdin falls through to the [Y/n] default
  instead of tripping `set -e`
- swallow getent's exit-2 on an unresolved domain (... || true): with
  `set -o pipefail` it aborted the DNS check before the graceful
  'Could not resolve' warning, breaking installs on minimal hosts (no
  dig) that run before DNS is pointed — an explicitly supported flow
- guard the LAN-IP and disk-space pipelines the same way so a no-match
  grep / missing `ip` can't abort the summary or prereq checks
2026-07-01 18:35:20 +02:00
Jannis Braun 737402b5d2 docs(deployment): note install.sh commit injection + HTTPS reachability check 2026-07-01 18:14:38 +02:00
Jannis Braun 20ba7b42f9 feat(install): production-hardening for public self-hosting
- inject the git commit at build time so /api/instance/info advertises
  the exact source version on the public clone->install.sh path (AGPL
  \xc2\xa713), passed as --build-arg so it survives the sudo/non-sudo split
- pass INSTANCE_NAME through the container env instead of interpolating
  it into node -e source; names with quotes/spaces/$ are stored verbatim
  and can't break or inject into the program
- add a post-install, NAT-hairpin-safe HTTPS reachability check (curl
  --resolve to the local Caddy with full cert verification) and report
  an honest 'Live' vs 'Not live yet' status instead of always claiming
  success
- list ALL required ports in the summary (80/443 + voice) with explicit
  host-firewall vs router port-forwarding guidance and the auto-detected
  LAN IP as the forward target; note 7880 must NOT be forwarded
- document the non-interactive env vars (DOMAIN/ENABLE_VOICE/INSTANCE_NAME)
2026-07-01 18:14:38 +02:00
Jannis Braun f481e1fe9e license: relicense to AGPL-3.0-only with commercial dual-license
- LICENSE -> verbatim GNU AGPL-3.0; add LICENSE-COMMERCIAL.md + SECURITY.md
- CLA -> exclusive-license grant (contributors keep copyright); add README
  anti-rugpull covenant + relicense record
- NOTICE / README / CONTRIBUTING / CLAUDE.md / package.json x5 updated;
  contact routed through GitHub (no email placeholders)
- AGPL section 13 source offer: operator-configurable BACKSPACE_SOURCE_URL +
  build-injected commit; sourceCodeUrl+commit on /api/instance/info;
  SourceCodeLink on login/register/settings/desktop; docs + .env.example updated
2026-07-01 16:38:22 +02:00
Jannis Braun 16d75f2806 docs(readme): drop private-test-instance mention from status 2026-07-01 13:25:52 +02:00
Jannis Braun 3be8b36064 docs(readme): finalize for 1.0.0 public launch
- Confident 1.0.0 framing: version badge + stable status callout; drop
  "early-stage / active development" and inaccurate "full-text" wording
- Add "What makes Backspace different" section leading with the verified
  wedge (per-stream media controls + federation + complete/polished)
- Fix two overclaims to match source: no unread divider (-> mark-as-unread),
  search is LIKE-based not full-text (-> "message search")
- Surface previously-omitted, source-verified voice/media features:
  per-user & per-stream volume, connection inspector, viewer detection,
  auto-ducking, measured tile badge, device/EC/AGC controls, selective
  subscription, browser push-to-talk
- Add Idle/DND + custom status, drag-drop/paste upload, avatar/banner crop
2026-07-01 13:22:37 +02:00
Jannis Braun ac20f22c72 fix(voice): consistent state on DM-call ↔ space-channel transitions
Two mirror-image bugs from voice/DM-call transitions leaving stale state.

DM call → space channel (stuck "Connecting…"):
The last participant to leave a DM call for a space channel receives a
`dm_call_ended` echo (server empties the DM room on their `voice_join`).
The handlers called `disconnectFn()` unconditionally, tearing down the
space room they had just connected to. Route `dm_call_ended` /
`dm_call_rejected` / terminal `dm_call_undeliverable` through a new
`teardownDmCall()` that only disconnects LiveKit when not in a space
channel (`currentVoiceChannelId` null).

Space channel → DM call (still shown as "in" the voice channel):
1. Entering a DM call never cleared `currentVoiceChannelId`, so
   `VoiceChannel` mapped the DM call's live LiveKit participants onto the
   old space channel. Add `clearSpaceVoiceForDmCall()`, called in
   `connect()` when `isDm`, restoring the invariant that a DM call has no
   `currentVoiceChannelId`.
2. `dm_call_accepted` gated the caller's connect on `!isLiveKitConnected`,
   so a caller already in a space channel was never connected to the DM
   room. Gate on `wasOutgoingCall` only (connect() de-dupes same-room).

Tests: teardownDmCall.test.ts, clearSpaceVoiceForDmCall.test.ts.
Docs: docs/systems/voice.md.
2026-07-01 02:08:53 +02:00
Jannis Braun 7a3d892c6e deploy: never rsync over the host-owned Caddyfile
The Pi's Caddyfile carries extra vhost blocks (e.g. other-site.example.com) that
aren't in this repo; add it to the rsync excludes so deploys don't overwrite it.
2026-07-01 01:34:16 +02:00
Jannis Braun d2e9b92e80 docs(readme): correct project-status + bans wording
- Project status: drop the test-instances mention and the self-contradictory
  'no tagged public release yet' framing → 'early-stage and under active development'.
- Moderation: 'Bans with reason and audit trail' overstated it — there's no
  append-only audit log and unban hard-deletes the record. Reworded to
  'Bans with reason and moderator attribution (who, why, and when)' to match the code.
2026-07-01 01:28:28 +02:00
Jannis Braun bdf957514c docs: accurate read-state wording + mobile/PWA and app-store-roadmap mention
- Features list: 'read states' -> 'unread tracking (badges + unread divider)',
  which is what the code actually does (no read receipts).
- Clarify the PWA bullet (add-to-home-screen standalone app) and the
  mobile-responsive touch layout.
- Add a Mobile section: working mobile-browser/PWA today, native iOS/Android
  app-store apps planned once traction and license funding are secured.
2026-07-01 01:15:33 +02:00
Jannis Braun 3f5b2920a4 docs: fix screenshot caption overclaiming read states
The chat screenshot shows messages + typing; "read states" reads as read
receipts (which don't exist). Backspace tracks unread state only. Reworded
captions to replies/reactions/typing.
2026-07-01 01:11:47 +02:00
Jannis Braun cb16c8feac docs: add screenshot showcase to README + gallery
Replace the placeholder Screenshots section with a hero voice/video shot
and a curated 6-shot grid (chat, screen-share controls, spaces, federated
DMs, social discovery, federation admin), linking to a new
docs/screenshots.md gallery of all 14 shots grouped by feature.

Images are optimized to 1600px WebP (~1.2MB total vs ~45MB raw); raw
originals in demo-pictures/ are gitignored.
2026-07-01 01:06:08 +02:00
Jannis Braun cfe4fd80c4 fix(icons): render small favicons from 3D raster to kill white tab-border
The flat app-icon.svg's gradient B mark has a bright (#fff) sheen that runs
to the badge perimeter with no dark separation. At favicon sizes (16/32px)
that edge anti-aliases into a white halo that reads as a border around the
icon — visible in Safari browser tabs, and the same defect in the small
Windows .ico / Linux launcher reps that also rendered from the flat SVG.

The committed 3D raster masters (used by every >=128px output already) frame
the mark in a dark surround and stay clean down to 16px. Set RASTER_THRESHOLD
0 so all app-icon sizes route through the raster path; the flat SVG is kept
as a gated source, re-enablable only with a corrected flat mark. Regenerated
favicons + small desktop reps; output remains byte-deterministic. Updated the
generator header/comments, README source matrix, and the dated icon spec.
2026-07-01 00:58:51 +02:00
Jannis Braun 0eb65b6608 fix(uploads): keep HEVC inline playback for Safari/WebKit
The server's `playable` flag is computed Chromium-first, but HEVC
web-playability is browser-dependent: WebKit (Safari on macOS/iOS) decodes
HEVC via the OS while Chromium/Firefox/Electron can't. Treating the flag as
global wrongly showed Safari users the download fallback for files they can
play inline.

The client now treats `playable === false` as "needs a capability check": it
pre-renders the fallback only when the current browser also can't decode the
format, gated on a one-time canPlayType probe (BROWSER_SUPPORTS_HEVC). Capable
browsers attempt inline playback; the <video> onError handler remains the
safety net for genuine failures.
2026-06-30 17:41:42 +02:00
Jannis Braun 209aef7e9d fix(uploads): graceful fallback for browser-unplayable video (HEVC .mov)
macOS screen recordings are HEVC inside a .mov container, which Chromium,
Firefox and stock Electron can't decode. The file uploaded fine and a
server-side ffmpeg poster was generated, but inline <video> playback failed
silently — stuck at 0:00 with no error, since AttachmentRenderer had no error
handling. Root cause: the system had no concept of web-playability.

Server detects, client degrades:
- mediaPlayable.ts: classifyVideoPlayable(mimetype, codec) — tri-state
  (false = known-undecodable e.g. HEVC/ProRes, true = web codec in web
  container, null = unknown/optimistic). Never widens `false` beyond codecs
  that fail everywhere, so ffmpeg-less instances keep prior behaviour.
- probeMediaMeta now captures the video codec_name; the upload finish hook
  stores the verdict in the new attachments.playable column (migration 0007).
- Flag propagated through every serializer: space messages, DMs, WS, and
  federation relay (outbound + inbound) — federation-compatible.
- VideoAttachment component: playable===false renders a download card (poster
  + "Can't play here — download" + name/duration/size) with no dead-player
  flash; otherwise plays inline with an onError fallback to the same card.

Specs updated: uploads.md, database.md, federation.md.
2026-06-30 17:38:11 +02:00
Jannis Braun e84daf57aa fix(voice): push voice presence to user on mid-session space join
Voice presence (voiceStates/voiceUserStates/spaceVoiceStates) was only ever
delivered in the WS `ready` payload — i.e. at connect/reload. A user joining a
space mid-session got `member_joined` (no voice data) and a bare space object;
`GET /api/spaces/:id` (the channel-sidebar hydrator) carries no voice state
either. So members already sitting in a voice channel stayed invisible in the
new member's sidebar until a full page reload.

Fix at the systemic root: ConnectionManager.addUserSpace — the single chokepoint
every join path funnels through (invite, public join, join-request approval),
and which is NOT used on reconnect (that path uses setUserSpaces) — now pushes a
scoped `space_voice_state` snapshot to the joining user. The snapshot is built by
a new buildSpaceVoiceState(spaceId, userId) helper that is also the single source
of truth feeding buildReadyPayload (refactored to use it), so the connect-time
and join-time paths can never drift.

Robustness:
- Delivered over the same ordered WebSocket as voice_state_update deltas — no
  REST snapshot-vs-event-stream race.
- VIEW_CHANNEL-filtered via computePermissions exactly like `ready`: a joiner is
  never told who occupies a voice channel they cannot see.
- Client applies it scoped to the space (utils/voiceStateSync.applySpaceVoiceState):
  merges occupants/statuses and rebuilds only that space's restriction keys,
  never disturbing voice state in other spaces.
- Skipped when the space has no active voice and no restrictions (e.g. space
  creation).

Tests: server helper behavior, the join push, and private-channel exclusion;
client scoped-apply. Specs updated (websocket.md, voice.md, spaces.md).
2026-06-30 17:00:37 +02:00
Jannis Braun f807524103 docs: public-release prep polish (README, CLA, CONTRIBUTING, prep script/spec) 2026-06-26 14:25:11 +02:00
Jannis Braun 77ceda148a docs(federation): document demand-driven peer recovery + recheck endpoint 2026-06-26 14:03:27 +02:00
Jannis Braun e372a7a571 feat(web): Check now button to manually recover unreachable federation peers 2026-06-26 13:58:38 +02:00
Jannis Braun 4ad83a7cc1 feat(federation): POST /peers/:id/recheck — manual reachability probe 2026-06-26 13:53:57 +02:00
Jannis Braun 603973a02e feat(federation): demand-driven recovery tick; health-check tick keeps rotation only 2026-06-26 13:49:40 +02:00
Jannis Braun a6c7584e0c feat(federation): lean federationRecovery module (probe + markPeerRecovered) 2026-06-26 13:44:37 +02:00
Jannis Braun bafc7fbb47 feat(federation): add last_probe_at/probe_attempts columns for peer recovery pacing 2026-06-26 13:41:29 +02:00
Jannis Braun 00a2876e96 fix(channels): newly created channel sometimes hidden until space reopened
The sidebar's visibleChannels filter is keyed on the channelPermissions
Map. Creating a channel raced two state updates: the optimistic create
(added to channels with no permission entry) and the channel_created WS
event (the only thing that set the permission). When the optimistic add
won the race, the WS handler hit its dedup guard, skipped setChannels,
and set the permission by mutating the Map in place — no new reference,
so visibleChannels never recomputed and the channel stayed hidden until
loadSpace rebuilt the maps (i.e. leaving and returning to the space).

Centralize the logic in a new upsertChannel store action that replaces
channels and channelPermissions with fresh references, used by both the
create path and the channel_created handler. Also return the creator's
computed myPermissions (and isPrivate) from POST so the channel renders
immediately from the response, independent of WS timing.

Adds spaceStore.upsertChannel.test.ts covering the reference-identity
regression and the optimistic-reconcile path.
2026-06-25 12:34:11 +02:00
Jannis Braun a4af708a41 fix(web): uniform category spacing in channel sidebar
The scroll container's space-y-[2px] utility set margin-bottom:0 (via
Tailwind's space-y reverse mechanism) on every category wrapper except
the first, with higher specificity (.space-y > :not ~ :not = 0,3,0) than
each category's mb-[19px] (0,1,0). This zeroed the 19px separator for all
but the first category, so the inter-category gap rendered only below the
first category and shifted when categories were reordered.

Remove the redundant container-level space-y; the per-category mb-[19px]
is the intended separator and now applies uniformly.
2026-06-25 12:21:37 +02:00
Jannis Braun 8dd76f3435 Public-release prep: ELv2 license, README/CLA/NOTICE, SSRF safeFetch, identifier genericization, export tooling 2026-06-22 16:04:03 +02:00
Jannis Braun c0a6477059 fix(install): post-install message reflects first-user-becomes-admin (no seeded admin/admin123) 2026-06-20 03:14:16 +02:00
Jannis Braun a91702a255 fix(backup): manual snapshot CLI must open its own DB handle (getRawDb undefined in standalone process) 2026-06-20 02:58:46 +02:00
Jannis Braun baf3a7d047 merge: deploy pipeline launch hardening (admin bootstrap, DB backups, image pinning, reproducible builds) 2026-06-20 02:52:22 +02:00
Jannis Braun 5107f63e60 harden(security): chmod seed-admin-rotated.txt to 0600 (guarantee perms on overwrite) 2026-06-20 02:51:13 +02:00
Jannis Braun 1c23c1fb60 docs(deploy): deployment.md (backup/restore, admin bootstrap, pinning) + env + CLAUDE.md 2026-06-20 02:45:33 +02:00
Jannis Braun 239578edc9 build(deploy): drop non-reproducible lockfile fallback; require frozen install 2026-06-20 02:41:26 +02:00
Jannis Braun 63732b9efb build: pin Docker image tags for reproducible builds
Pin floating tags to versions already running on both live boxes:
- livekit/livekit-server:latest -> v1.9.11
- caddy:2-alpine -> 2.11.1-alpine

No operational change; tags proven-good in production.
2026-06-20 02:40:09 +02:00
Jannis Braun a4333c4c23 fix(restore): robust empty-dir listing + injection-proof container swap
Finding 1: no-arg branch used 'ls ... | while' which, under set -euo
pipefail, exits 1 on an empty backups dir (glob matches nothing). Replace
with shopt nullglob array check that prints a clear message and exit 0.

Finding 2: pass $TS and $SNAP_NAME as positional args to the inner alpine
shell instead of interpolating them into the sh -c string, making the swap
injection-proof. Behavior unchanged: pre-restore copy -> clear WAL/SHM -> install.
2026-06-20 02:38:07 +02:00
Jannis Braun 70da1ede64 feat(backup): manual-snapshot CLI + host-side backup.sh and restore.sh (safe restore) 2026-06-20 02:34:30 +02:00
Jannis Braun 9407e48293 feat(backup): scheduled snapshot worker wired into server lifecycle 2026-06-20 02:32:17 +02:00
Jannis Braun 154feb2dd6 feat(backup): pre-migration snapshot gated on pending migrations; checkpoint WAL on shutdown 2026-06-20 02:26:51 +02:00
Jannis Braun 48bcd69031 feat(backup): VACUUM INTO snapshot core (create/list/prune) + config + off-box hook 2026-06-20 02:21:22 +02:00