<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/roster.py, branch 0.11</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.11</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.11'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-04T00:20:48Z</updated>
<entry>
<title>fix(node): pin utf-8 (and LF) on every text file the node reads or writes</title>
<updated>2026-09-04T00:20:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T00:20:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3'/>
<id>urn:sha1:7a4b905ddb64bdc92b7f9acf2ccde9bd84d7a6f3</id>
<content type='text'>
node.toml, the keystore envelope, the unlock key, the loopback UI token,
pairing/invite code files and the denylist were all read and written with
the platform default encoding and newline translation. On Windows that is
cp1252 + CRLF: a node.toml or keystore holding any non-ASCII byte failed to
load, and ops.py's line-based node.toml editor round-tripped CRLF in and
LF out.

Every read is now `encoding="utf-8"`; every write is `encoding="utf-8",
newline="\n"` so the files stay LF whatever the OS. No-op where the locale
was already UTF-8.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>refactor(node): platform abstraction for Windows portability (W1-W2-W5-W6-W7)</title>
<updated>2026-09-03T14:20:28Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-03T14:20:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=753653b4df62723b82d32799825135822886eac7'/>
<id>urn:sha1:753653b4df62723b82d32799825135822886eac7</id>
<content type='text'>
Platform directories, signal handling, chmod guards, ffmpeg discovery,
and platform-conditional CLI messages — all testable on Linux.

See docs/WINDOWS-PORT.md §5 for the plan these implement.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(ui): configurable ICE interfaces in the Node page</title>
<updated>2026-08-30T20:36:56Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T20:36:56Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2d6c276734fd19f80edf84cdb95f91a929df6365'/>
<id>urn:sha1:2d6c276734fd19f80edf84cdb95f91a929df6365</id>
<content type='text'>
The ice_interfaces setting (auto-exclude vs explicit whitelist) is now
editable from the Node page, persisted via the settings API and roster,
and hot-swapped at runtime by re-installing the aioice filter.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<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: 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>feat: add Photos group app</title>
<updated>2026-08-25T09:46:17Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-25T09:46:17Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2fcdd07d1e5d331ad02b723f1c45603a0989c264'/>
<id>urn:sha1:2fcdd07d1e5d331ad02b723f1c45603a0989c264</id>
<content type='text'>
A new group application (docs/apps.md's plug-in mechanism), following the
plan in docs/photos.md. Unlike Videos/Music: several photo roots per group
instead of one (photo_roots is a set, one signed op replaces it whole),
a single album-grid view with no third-party matching step, and per-photo
info read from the file's own EXIF at index time — no metadata service,
no credential, no outbound network call at all.

Protocol (meshbay-common, MNP 0.10 -&gt; 0.11, additive): `taken_at`/`camera`
on IndexEntry; `photo_roots`/`photo_roots_ack`; `OP_PHOTO_ROOTS`.

Node: roster.py stores photo_roots as a group_settings entry (JSON list,
same shape as enabled_apps); ops.py/webrtc_server.py validate and sign the
whole set in one op, same pattern as apps_enabled; a new PhotoEnricher
(indexer/enrich_photo.py) runs Pillow in its own small bounded pool,
separate from the video/audio pools, producing a resized thumbnail plus
the two EXIF fields — never GPS, checked by a grep-based regression test.

Client: photos-app.js — one album card per directory containing images,
a per-album photo grid, and a lightbox with next/previous (keyboard and
buttons), zoom in/out/fit/100% starting from the actual on-screen fit
percentage, and a "zip this album" button reusing files-app.js's own zip
mechanism (lifted into file-utils.js's downloadDirectory so both call the
same implementation). group-settings.js gets an add/remove multi-root
picker, distinct from Videos/Music's single-value one.

Bugs found and fixed before this ever shipped, worth keeping the story of:

- enrich_photo.py read width/height from the raw image *before* applying
  EXIF orientation correction, and read DateTimeOriginal off the plain
  0th-IFD Exif object — a real camera stores it in the Exif sub-IFD, which
  Pillow only exposes via get_ifd(Exif). A flat, hand-built EXIF dict
  round-trips through Pillow either way, which is exactly what would have
  hidden both bugs; the regression test builds EXIF with piexif instead,
  matching what real hardware produces.
- photos-app.js's album grouping stripped a trailing path segment from
  entry.path under the assumption it still carried a filename — it
  doesn't (files-app.js's own convention: e.path is already the
  containing directory), so every album collapsed one level into its
  parent. Found live against a real multi-folder library.
- transport.js's ADMIN_OP_TYPES allowlist (already the fix for an
  identical bug on video_root/apps_enabled, see 4783d81) was missing
  photo_roots: its admin_challenge matched no pending request and was
  silently dropped, so saving a photo root just timed out after 30s with
  no error.
- daemon.py pruned a thumbnail when its file left the index (root removed
  or reconfigured) but never forgot the content hash was "already
  attempted" — the same bytes reappearing under a renamed/relocated root
  (an operator's real workflow) were then permanently skipped, forever,
  with nothing to indicate why. Discarding the attempt alongside the
  cache entry on prune is what makes pruning actually reversible.
- packages/meshbay-client's app:// protocol handler served every file
  with no Cache-Control header, so Chromium was free to serve a stale
  cached copy indefinitely — none of several `npm run sync-ui` + reload
  cycles during development actually picked up the new code until the
  renderer's disk cache was cleared by hand. Now sends Cache-Control:
  no-store.
- the lightbox's zoomed image used flex centering (align-items/
  justify-content: center) combined with overflow: auto — a well-known
  trap where the browser centers overflowing content by shifting it, and
  the leading half of that overflow (here, the top of a zoomed photo)
  sits outside what the scrollport can actually reach. Reported live as
  "unusable". Fixed by switching to top/left alignment once zoomed.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01TiZG4AuSnxHohQMpwTHTyL
</content>
</entry>
<entry>
<title>feat(node): add audio_root, gate Music enrichment on it like video_root</title>
<updated>2026-08-24T20:24:52Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T20:24:52Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=665fb2004e55b72ea483aa50fea81f6a6fd9c322'/>
<id>urn:sha1:665fb2004e55b72ea483aa50fea81f6a6fd9c322</id>
<content type='text'>
musicbay.md's original call — Music needs no root, tag reads are cheap so
just cover the whole shared tree — didn't hold up against a real messy
library: everything under every shared folder got mixed together with no
way to scope Music down to an actual music collection. This adds an
audio_root setting, symmetric to video_root in every respect: signed
operator op (audio_root/audio_root_ack, MNP bumped to 0.10), validated
against a real directory in the group's own roots before a signature is
even asked for, gates tag/cover enrichment exactly like video_root gates
ffprobe/TMDB (nothing runs until it's set, only files under it once it
is), and a set/change fires a one-off sweep of whatever the folder already
contains.

The old trigger — sweep everything the instant "music" joins enabled_apps
— is gone along with the root-less design it belonged to; setting
audio_root is now the trigger, mirroring set_video_root's
enrich_video_root_fn exactly.

Test coverage mirrors the video_root suite: policy (refuse before a
signature round trip, accept/store correctly) and the enrichment gate
itself (nothing without a root, only files under it, sweep on set).
</content>
</entry>
<entry>
<title>feat(node): Music app node-side — indexing, MusicBrainz enrichment, protocol</title>
<updated>2026-08-24T15:12:36Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T15:12:36Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=941d1a135dd7b03834576855e8e9fdaa24c4e406'/>
<id>urn:sha1:941d1a135dd7b03834576855e8e9fdaa24c4e406</id>
<content type='text'>
Implements the node half of docs/musicbay.md against MNP 0.8:

- IndexEntry gains artist/album/track_no (reuses duration/thumb_hash/
  display_title, already generic). New musicbrainz_config/_enabled and
  music_meta_req/_resp message pairs, mirroring the TMDB shape.
- title_parse.parse_track_filename: track-number-prefix + title parsing,
  fallback-only (embedded tags are the primary source, unlike Videos).
- indexer.enrich_audio.AudioEnricher: mutagen-based tag/embedded-cover
  extraction through its own bounded pool (asyncio.to_thread, no
  subprocess — no ffmpeg-shaped deadlock risk). Gated on "music" in a
  group's enabled_apps rather than a video_root-style scoped folder.
- musicbrainz.py: MusicBrainzClient — no API key (unlike TMDB), just a
  self-imposed ~1 req/s pace and a configurable, non-default User-Agent
  contact string; inert (no calls at all) when no contact is configured,
  never sends an unidentified client.
- media_cache.py: file_mbid/mbid_meta tables alongside the existing TMDB
  ones, cover art reusing the thumbs table via a synthetic
  musicbrainz:{mbid} id, pruned on file deletion.
- roster.py/ops.py/webrtc_server.py: musicbrainz_contact (node-wide) and
  musicbrainz_enabled (per-group, from the start) as signed operator
  settings, ALLOWED_APPS gains "music", _do_music_meta_request resolves
  and caches a release-level MusicBrainz match per (artist, album).
- daemon.py: AudioEnricher/MusicBrainzClient wired alongside the video
  ones; a group's existing library is swept when "music" is newly
  enabled (no video_root equivalent — see musicbay.md §2.1).

41 new tests (musicbrainz.py against a mocked transport, admin-op policy
for both new settings, media_cache round-trip/pruning, enrich_audio
end-to-end against real ffmpeg-generated MP3s). Full suite (common +
node + hub): 1116 passed, no regressions.

Client-side (music-app.js, persistent player bar) not started yet.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
</content>
</entry>
<entry>
<title>fix(node,hub): HEVC transcode fallback, live-add progress, per-group TMDB toggle</title>
<updated>2026-08-24T13:57:41Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T13:57:41Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c5585beab3d6adefaa2ef9444946dd3816960a7c'/>
<id>urn:sha1:c5585beab3d6adefaa2ef9444946dd3816960a7c</id>
<content type='text'>
Three bugs found live testing the Videos app against a real HEVC/EAC3 show,
plus a design change requested afterward:

- Streaming always did "-c:v copy", which faithfully reports a source's real
  hev1 codec string but is unplayable in a browser with no HEVC decoder
  (most Chrome/Linux builds). The node now transcodes to H264 whenever the
  probed codec is browser-incompatible (media_probe.py's new
  BROWSER_INCOMPATIBLE_VIDEO_CODECS), with a `transcode_incompatible_video`
  node.toml opt-out for operators who know their viewers already decode it.

- Dropping a whole season into an already-watched folder gave no scanning
  indicator and no progress bar: IndexProgress was only ever updated by the
  two bulk scan paths, never by the real-time per-file watchdog path
  (_schedule_update/_debounce/_update_entry). That path now accounts a
  "burst" the same way, without double-counting a file rewritten mid-debounce.

- A stray literal "0" rendered in the video detail modal when there was no
  TMDB match (`meta.confidence` is 0, and `0 &amp;&amp; x` renders "0" in JSX/htm,
  not nothing) — `confident` is now a real boolean.

- Whether TMDB is used at all moves from a node-wide setting to per-group
  (OP_TMDB_ENABLED/tmdb_enabled/tmdb_enabled_ack, scoped like OP_VIDEO_ROOT):
  an operator running a real media-library group alongside test/demo groups
  on one node wants outbound TMDB traffic for the one that needs it, not all
  of them. The custom API token and query language stay node-wide, one
  shared credential/cache (tmdb_config/OP_TMDB_CONFIG, unchanged reasoning).
  MNP_VERSION 0.6 -&gt; 0.7, additive.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY
</content>
</entry>
</feed>
