Commit Graph
28 Commits
Author SHA1 Message Date
mats aab7931142 icons
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 27s
2026-09-22 14:24:14 +02:00
mats ec02a6ddaf sw
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 24s
2026-09-22 14:04:06 +02:00
mats 0b2c5ca4c9 pwa
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 37s
2026-09-22 13:55:06 +02:00
matsandClaude Sonnet 5 78d97ee733 Add a second, LAN-only TURN relay to avoid hairpin NAT entirely
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 25s
The existing coturn instance advertises the router's public IP as its
relay address (external-ip=94.255.139.35) — correct for real external
users, but when both peers are on the same home network as the router,
relay traffic has to hairpin through the router's own WAN IP. That only
worked with hwnat (hardware NAT offload) disabled, which pegged the
router's CPU at 93% for well under half its rated throughput — not an
acceptable trade for the whole household's network.

Instead of touching hwnat, added a second coturn deployment
(coturn-lan, own secret, own relay port range 49210-49220) exposed via
its own MetalLB LoadBalancer IP (10.10.3.46) on the same LAN segment.
That address is answered directly via L2 ARP — no NAT, no hairpin,
hwnat never touches it — and it's deliberately not port-forwarded from
WAN, so it's LAN-only by construction.

iceConfig() now appends this as an extra iceServers entry whenever
TURN_LAN_HOST/TURN_LAN_SECRET are set. It's harmless for anyone not on
that network — the address is simply unreachable and ICE ignores it —
and WebRTC tries every configured server, using whichever candidates
actually connect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:13:25 +02:00
matsandClaude Sonnet 5 5836424baf Force TURN relay on WebRTC retry — works around same-LAN NAT/isolation issues
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 26s
A connection failing specifically when both peers are on the same LAN
(as opposed to over the open internet) usually means direct/STUN
candidates are being silently dropped by the router — either AP/router
client isolation (common on home/guest Wi-Fi) or no NAT hairpin/
loopback support for two LAN devices reaching each other via their own
public IP.

On the first retry after a connection failure, set
iceTransportPolicy: 'relay' so the rebuilt RTCPeerConnection only offers
TURN-relayed candidates, bypassing the LAN path entirely. Confirmed the
coturn secret and pod are up in-cluster so relay auth will actually
succeed. Only one side needs to do this — since it only offers relay
candidates, whatever pairs with them is forced through the relay
regardless of what the other side additionally tried.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:10:12 +02:00
matsandClaude Sonnet 5 3517bdf52b fix(webrtc): rebuild the RTCPeerConnection on failure instead of restartIce()
"restarting ICE" on a connection that has reached 'failed' only restarts
ICE gathering — it reuses the same DTLS transport and certificate. If
that transport is what's actually broken, the next renegotiation throws
"Failed to set SSL role for the transport" (DTLS role can't be flipped
on an established transport), which is exactly what showed up in the
console on a LAN test, ruling out NAT/TURN as the cause.

Fix: on 'failed', fully close and recreate the RTCPeerConnection (fresh
certificate, nothing to conflict with) via recreatePeerConnection().
Since ICE/DTLS failure isn't guaranteed to fire on both ends at once,
the side that notices sends a new 'reset' signal (relayed by the
server like offer/answer/candidate) telling the other side to tear
down and wait for a fresh offer too — otherwise its old, now-mismatched
transport would just fail the same way against our new one. Capped at
3 rebuild attempts per peer before giving up with a toast, reset on any
successful 'connected' state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 12:08:07 +02:00
matsandClaude Sonnet 5 8474a85ed2 fix(spatial): decorrelate spawn radius from angle to stop avatar overlap
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 51s
The previous spawn fix (radius 20 -> 6) traded one problem for another:
hashing only the angle onto a small fixed-radius ring means two names
whose hashes land near the same angle bucket (only 360 of them) now
separate by just a few pixels of arc instead of dozens, since arc
length scales with radius. Confirmed against the actual guest names
from a live session (guest-pxj6/guest-rd7o): old algorithm placed them
~11 room units apart, easily close enough for their ~34px graph nodes
to visually merge into what looked like a single dot.

Now the radius is hashed independently from the angle (8-19 units), so
an angle collision no longer implies a full positional overlap. Same
test names now land ~27.6 units apart. Still well within the range
that keeps audio at full gain (refDistance=4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:58:11 +02:00
matsandClaude Sonnet 5 f68e77472e Clamp spatial-map dragging to the room and shrink initial spawn spread
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 51s
Dragging (yourself or a peer, for local-only listening position) never
clamped the node's actual on-screen position — only the *derived* room
coordinate got clamped to [0,100]. Cytoscape let the dot follow the
pointer arbitrarily far past the edge while the stored position stayed
pinned at the wall, so it visually flew off, then snapped back the
instant you released. Now every drag frame writes the clamped position
back onto the node itself, so it can't separate from its real, bounded
position. Given refDistance=4/rolloffFactor=1.4, staying inside the room
already keeps everyone's audio gain above ~18%, so this is the actual
"never drag out of hearing range" bound.

Also replaced the ':grabbed' re-query in the 100ms sync loop (which
decided whether to skip re-snapping the actively-dragged node) with an
explicitly tracked node id — strictly more correct regardless of
whether it contributed to the glitch above.

Dragging a peer is local-only (repositions how loud/where only you
hear them, never sent to anyone else) but had no feedback saying so;
added a one-time toast when you grab a peer's dot.

Also shrink the initial spawn-scatter radius from 20 to 6 room units —
at radius 20, two people could spawn diametrically opposite (40 units
apart), which was already down to ~7% audio gain before anyone said a
word.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:51:05 +02:00
matsandClaude Sonnet 5 7795cf84a3 Add a mobile-responsive layout and mirrored call controls
- Sidebar (channel list + nickname) becomes an off-canvas drawer below
  860px, opened via a new fixed top bar (hamburger + live status) and
  closed by tapping the backdrop or picking a channel.
- Mic/deafen/share get an always-visible bottom control bar so they're
  reachable mid-call without opening the drawer; updateMicButton/
  updateDeafenButton and their click wiring now target both the
  sidebar and mirror buttons via shared NodeLists instead of single
  elements.
- Per-user volume popup (previously hover-only) now also toggles on
  tap, and repositions below the row instead of to the side, which
  would overflow a narrow drawer.
- Screen-share tiles stack full-width, touch targets are enlarged,
  and the spatial map panel shrinks to leave room for the stage.

Also includes the working tree's prior swap of the spatial view from
Konva to Cytoscape.js (drag, scroll-to-zoom, fit-to-view, distance
edges) — not authored in this change, carried along since it was
already uncommitted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:39:05 +02:00
matsandClaude Sonnet 5 664417c6da fix(server): raise keepAliveTimeout above ingress-nginx's upstream idle timeout
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 1m2s
Node's default keepAliveTimeout (5s) is shorter than ingress-nginx's
default upstream keepalive idle timeout (60s). Under that mismatch,
nginx can forward a request onto a pooled connection Node has *just*
closed, which the browser sees as a 502 — and it never shows up in
this process's own traces because Node never received the request.
/api/rooms is polled every 5s from idle clients, landing almost
exactly on that 5s boundary, which is why it was the one showing up
for external users.

Also add abort/slow-request logging (tagged with the active trace ID
so it can be pivoted to from RUM) and a clientError handler, so a
genuine backend-side failure is now visible instead of indistinguishable
from the proxy-side race.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:21:52 +02:00
matsandClaude Sonnet 5 5bde1757d6 Require a nickname before joining and add a quick-start help modal
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 50s
New visitors (no nickname in localStorage) get a blocking welcome
modal instead of a silently-generated guest-N name, so everyone in a
room has a name they actually chose. It doubles as quick-start help
(join a channel, mic/deafen/share shortcuts, drag-to-reposition on the
spatial map) reachable any time via the new ? button. The sidebar nick
field no longer falls back to a random name when cleared — it now
just rejects the blank edit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:17:05 +02:00
matsandClaude Sonnet 5 099d637815 Replace hand-rolled spatial canvas drag with Konva
The custom pointer-capture + frozen-camera drag math was fragile and
felt worse than before. Konva handles the drag gesture itself; the
"me" node's screen position stays pinned to the stage center via
dragBoundFunc + a worldLayer offset that recenters on every dragmove,
so the world pans under the avatar instead of the avatar drifting off
center. Grid, peers, and the audible-range circle all live in the same
world-space layer so they pan together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:14:32 +02:00
matsandClaude Sonnet 5 5ecf415f64 Clarify mic mute state, center spatial map on you, show talking/muted on shares
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 23s
- Mic button now swaps glyph (mic vs muted) and syncs immediately on
  join/leave/load instead of relying on a subtle CSS underline.
- Spatial minimap camera now follows the local user, matching how the
  audio panning is already centered on them, instead of plotting
  absolute room coordinates stretched across whatever width the
  screen happens to be.
- Screen-share tiles now show a speaking glow and muted badge, kept in
  sync on the existing audio-meter tick without tearing down <video>
  elements.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 11:07:34 +02:00
mats 45d2a4911f Attach active screen share to new peers and prompt mic on join gesture
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 25s
2026-09-22 10:46:28 +02:00
mats dee8ef4f7c Show users in voice channels before joining
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 24s
2026-09-22 10:45:08 +02:00
mats a29bd58b0c fix(rum): restrict traceHeaderOrigins to same origin to prevent matrix CORS preflight failure
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 25s
2026-09-22 10:31:38 +02:00
mats 4d8a4a0dec fix(otel): point backend otel endpoint to in-cluster otel-debug-service in monitoring namespace
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 49s
2026-09-22 10:29:44 +02:00
mats b6cc9f3465 feat(build): bundle frontend with vite and matrix-js-sdk, fix HOST in server.js
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 22s
2026-09-22 10:29:03 +02:00
mats 5ce3b46bab matrix
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 22s
2026-09-22 10:21:28 +02:00
mats e687d41574 fix(ui): compact user panel with status icon and room indicator
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 22s
2026-09-22 10:06:01 +02:00
mats a2be7cf6a5 feat(ui): move users to channel list with hover volume popup, dedicate main stage to screenshares, add ws ping
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 17s
2026-09-22 10:02:03 +02:00
mats d3fd800ea7 fix(channel): assign S.room when joining voice room
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 33s
2026-09-22 09:54:28 +02:00
mats 7b29cf7d10 fix(screenshare): enable button feedback, audio capture fallback and bump version
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 17s
2026-09-22 09:52:05 +02:00
mats 5757156843 feat: migrate voice app, add otel/rum, dockerfile, k8s deployment with ingress
Build and Publish / Metadata (push) Successful in 1s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 18s
2026-09-22 09:43:47 +02:00
mats a0d5a9a2c7 voice-poc: multi-share grid + pop-out window per shared screen 2026-09-22 07:15:34 +00:00
mats ebbf1b07d8 voice-poc: wire up screen share button (S shortcut, active/disabled states) 2026-09-22 07:12:58 +00:00
mats 404dae3a28 voice-poc: WebRTC voice + screen share + spatial audio with configurable STUN/TURN 2026-09-22 07:10:44 +00:00
mats 89053153cc Initial commit 2026-09-22 08:41:20 +02:00