<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/transport.js, branch v0.9.0</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=v0.9.0</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=v0.9.0'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-30T20:17:30Z</updated>
<entry>
<title>feat: configurable STUN server fallbacks for WebRTC ICE</title>
<updated>2026-08-30T20:17:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T20:17:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=07e6e4271ea3a45e9cd364eb6ec05801a653d9e8'/>
<id>urn:sha1:07e6e4271ea3a45e9cd364eb6ec05801a653d9e8</id>
<content type='text'>
The WebRTC transport relied on a single Google STUN server — if it was
unreachable, ICE gathering waited the full 4s timeout. Now four public
servers are used by default (Google ×2, Cloudflare, Mozilla), configurable
via node.toml, the Node page UI, and the CLI (meshbay-node stun list|add|
remove|reset). Changes are hot-swapped on the live transport.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node): editable node settings in the Node page (D5)</title>
<updated>2026-08-29T16:52:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T16:42:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1f8a52484412b48205e5ff6aac506428e2fb77ed'/>
<id>urn:sha1:1f8a52484412b48205e5ff6aac506428e2fb77ed</id>
<content type='text'>
Expose invite_ttl_hours, pair_ttl_hours, device_request_ttl_minutes,
max_concurrent_streams and transcode_incompatible_video in the Node
management panel. Changes are applied immediately via roster.db and
written back to node.toml so they survive a DB wipe. On startup,
roster overrides take precedence over node.toml defaults.

Draft v6 §2.11 documents the design; MNP gains node_settings_set /
node_settings_set_ack for the browser path.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: V13 — per-card "re-match this one file" (MNP 0.13)</title>
<updated>2026-08-29T13:57:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T13:57:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=4d167e9f958d26c1db920274974ae446426928e3'/>
<id>urn:sha1:4d167e9f958d26c1db920274974ae446426928e3</id>
<content type='text'>
A one-click alternative to the full "Fix match" search-and-pick flow, and
reachable without SSH (`meshbay-node video rematch` clears a whole group).

- MNP 0.13: tmdb_rematch / tmdb_rematch_ack (additive — an older node
  logs "unknown type", the button just does nothing). OP_TMDB_REMATCH,
  signed like tmdb_override (media_cache is shared node-wide).
- media_cache.drop_tmdb_match(file_id): forgets the match AND the
  override marker — deliberately stronger than clear_file_tmdb, since the
  operator is explicitly asking for a fresh resolution.
- webrtc_server: _do_tmdb_rematch / _admin_exec_tmdb_rematch, dispatch +
  admin-response routing, broadcasts tmdb_rematch_ack.
- transport.js: rematchTmdbMatch(fileId, signFn); 'tmdb_rematch' in the
  admin-op allowlist; tmdb_rematch_ack handled like tmdb_override_ack.
- video-app.js: a "Re-match" button beside "Fix match" in the detail
  modal (isNodeAdmin), then bumpMediaMetaGeneration(). video.rematch_one
  key in all ten locales.

docs/mediacenter.md §10.1: V8–V13 marked done.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018BMLQjqFGCize2KtNBT79v
</content>
</entry>
<entry>
<title>feat: node workflow redesign — wizard auto-config, reset, MusicBrainz contact</title>
<updated>2026-08-29T09:15:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T09:15:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b7733e812fadd6007976d262bd1d793572a36ba7'/>
<id>urn:sha1:b7733e812fadd6007976d262bd1d793572a36ba7</id>
<content type='text'>
Wizard (Electron):
- Auto-provisions node config (hub URL + username) from logged-in user
- node:start handles both cold start and restart of misconfigured daemon
- Waits for daemon to reach 'running', auto-links node key on hub
- probeNode accepts intermediate states for wizard progress feedback

Reset (meshbay-node reset):
- Unlinks node key from hub (DELETE /me/node_key, best-effort)
- Stops and disables daemon (systemctl --user disable --now)
- Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay

MusicBrainz contact:
- Resolved from owner's hub email instead of per-node roster config
- Removed musicbrainz_contact UI and WebRTC handshake field
- Removed set_musicbrainz_contact/musicbrainz_contact from roster

Node pairing:
- Added operator pairing banner on NodePage
- Added operator_paired flag to list_groups

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(transport): chat_hist_resp dispatch skipped when older pending entries exist</title>
<updated>2026-08-28T15:54:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T15:54:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=09a007937f03fad04a390323f3817f1ae7d7c2a9'/>
<id>urn:sha1:09a007937f03fad04a390323f3817f1ae7d7c2a9</id>
<content type='text'>
chat_hist_resp checked only the single oldest pending entry across all
types. When switching tabs, keyed requests (media_meta_req, file_req)
from Videos/Music/Photos stayed in _pending and were older than the
chat_hist entry, so the response was dropped and the chat appeared empty
on return. Now scans for the first chat_hist entry instead. Same fix
applied to index_sync dispatch for consistency.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(chat): link previews for pasted URLs</title>
<updated>2026-08-28T01:43:19Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T01:43:19Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ce4e10c4b8bd9c66c375c3a5d5c18d8552655775'/>
<id>urn:sha1:ce4e10c4b8bd9c66c375c3a5d5c18d8552655775</id>
<content type='text'>
Paste an http(s) link in a group's chat and it unfurls into an OpenGraph
card — title, description, site name, and image — the way WhatsApp/Signal/
Slack do it.

The fetch is the node's, never the browser's or the hub's. The browser
cannot: a strict img-src/connect-src and CORS block it, and a direct fetch
would leak every reader's IP to the linked host on each render. The hub must
not touch group content (draft-v6 §2.5). The node already fetches third-party
metadata for the Videos and Music apps, over the same authorised path.

Flow mirrors media_meta_req: the client sends `link_preview_req {url}`, the
node replies `link_preview_resp` with the card fields (or `ok: false`), and
any OG image is stored under its blake3 in the existing media_cache thumb
store — the client then fetches it via the normal file_req path, exactly like
a poster. Nothing durable is added: the card text lives in a bounded in-memory
TTL cache on the node (draft-v6 §2.7 — enrichment on demand, the asking device
caches), and MNP goes 0.11 → 0.12 (additive: an older node logs "unknown type"
and the client shows the bare link).

Because the URL is chosen by a *member* and triggers an outbound request from
the operator's machine, `linkpreview.safe_url` is an SSRF gate: http(s) only,
no credentials, and every resolved address must be globally routable — no
loopback, private, link-local, multicast or reserved range, cloud-metadata
included. Redirects are followed by hand so each hop is re-checked. Residual,
documented in the module: DNS rebinding between the check and connect, closed
properly by pinning the checked IP — a follow-up.

Also fixes a long-standing chat annoyance the preview cards made worse:
opening the Chat tab landed a screen or two above the newest message because
the scroll-to-bottom ran before attachment thumbnails and (now) preview cards
had loaded and grown the content. A ResizeObserver keeps the view pinned to
the bottom through late content growth, and does nothing once the reader
scrolls up.

Tests: test_linkpreview.py (the SSRF gate and the OpenGraph parse, incl.
redirect re-validation and image downscaling) and test_link_preview_request.py
(reply shape, the media_cache image round-trip, the result cache).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
<entry>
<title>fix(transport): reconnect used a fresh handshake token but a stale signaling one</title>
<updated>2026-08-26T11:54:56Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T11:54:56Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=a31b26df45860af16061fb43ccc3443381ed3df2'/>
<id>urn:sha1:a31b26df45860af16061fb43ccc3443381ed3df2</id>
<content type='text'>
Found live: every reconnect attempt failed "Signaling failed: 401 Invalid
or expired token", looping for 4+ minutes with no chance of ever
succeeding. connect() takes one token but uses it in two places — the
handshake sent to the node, and the Authorization header on the signaling
POST to the hub — and only the constructor's original `this._accessToken`
was ever used for the latter. onNeedToken correctly fetches a fresh token
for each reconnect attempt, but it only ever reached the handshake; the
signaling call kept sending whatever token the transport was constructed
with, no matter how many minutes had passed or how many attempts fetched
a new one.

connect() now updates this._accessToken on every call, reconnects included,
so both places use the same current token.
</content>
</entry>
<entry>
<title>fix(transport): wake a backing-off reconnect on visibilitychange, fix listener leak</title>
<updated>2026-08-26T10:44:50Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T10:44:50Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=5dcf3066a2be83d5422ea176e39b413c4769bcf8'/>
<id>urn:sha1:5dcf3066a2be83d5422ea176e39b413c4769bcf8</id>
<content type='text'>
Confirmed live by trace: during a screen lock, every reconnect attempt
failed with "Failed to fetch" (the browser grants no network access to a
locked/backgrounded tab, no code can change that) — expected. But the
backoff timer itself was also throttled while locked: an attempt scheduled
30s out took ~3 minutes of wall clock to fire, because a backgrounded tab's
timers run only when the OS lets them. Recovery after unlocking was
correspondingly delayed rather than prompt.

Fix: an always-on visibilitychange listener (separate from the diagnostic
one, and unlike it not gated on trace mode) resolves the current backoff
wait immediately once the page is visible again, instead of waiting out
whatever of it is left. The actual reconnect this enables is fast (~1.2s in
the trace that showed the "Failed to fetch" run) — the wait was the
throttled part.

Also fixes a real bug the same trace exposed: connect() re-arms the
diagnostic visibility listener and health-ping interval on every attempt
without ever removing the previous instance's — 8 failed attempts during
one lock left 8 duplicate `visibility` trace lines per real event, and (more
than a cosmetic issue) 8 concurrent health-ping intervals once reconnected.
</content>
</entry>
<entry>
<title>fix(music): don't throw "Transport not connected" while a reconnect is landing</title>
<updated>2026-08-26T09:44:23Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T09:44:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6e0d2b9a477f1e9f1dec646c0de15440106ed7ed'/>
<id>urn:sha1:6e0d2b9a477f1e9f1dec646c0de15440106ed7ed</id>
<content type='text'>
Found live: &gt;5min screen lock while music played, then "Transport not
connected" at the 2nd track's end (~8min in) despite the auto-reconnect
from the previous commit. Root cause: fetchTrackBlob's own
`!transport.connected` pre-flight check ran and threw before the
already-in-progress reconnect got the few seconds it needed — it never
reached _sendAndWait, which is the only place the previous fix taught the
transport to wait.

Adds transport.waitForReconnect(), factored out of _sendAndWait's existing
gate, and calls it from fetchTrackBlob before giving up. No-op when nothing
is being reconnected, so the ordinary path is unchanged.
</content>
</entry>
<entry>
<title>fix(transport): auto-reconnect after WebRTC failure, without dropping in-flight streams/downloads</title>
<updated>2026-08-26T09:17:37Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T09:17:37Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=27d59cbb2d11d863273e2282257d81ac9911ab97'/>
<id>urn:sha1:27d59cbb2d11d863273e2282257d81ac9911ab97</id>
<content type='text'>
Confirmed live (client trace + node logs, mobile screen-lock ~5min): ICE
goes disconnected -&gt; failed within ~10s on both ends, but the DataChannel's
readyState stays "open" throughout, so nothing failed fast — every request
just sat out its own 8s/30s timeout, matching the reported symptom (poster
spinners, blocked chat, dead new streams).

transport.js: on connectionState "failed", reject pending requests
immediately (TransportLostError) and start a self-contained reconnect loop
(capped exponential backoff, redoes the full signaling handshake — the node
already discards the old session on its own "failed"/"closed", so there is
nothing lower-level to resume). New hooks: onNeedToken (fetch a fresh JWT,
since the captured one may have expired during the outage) and
onReconnected (let a consumer resume something that was mid-flight).

file-utils.js: pipelinedDownload retries a lost chunk instead of aborting
the whole transfer — covers Files downloads, poster/thumbnail fetches, and
music-player.js's blob-based track download, all of which go through it.

video-player.js: onReconnected reissues the existing seek-to-current-time
path, which already knows how to land a new stream_init on the live
SourceBuffer without resetting playback.

Playing audio is unaffected either way — musicbay.md's design downloads a
track to a blob before playing it, so a dead transport was never a network
dependency for what is already playing.

Stays on this branch until confirmed by real-device testing.
</content>
</entry>
</feed>
