<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/daemon.py, branch 0.7</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.7</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.7'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-24T22:29:24Z</updated>
<entry>
<title>fix(node): scope _enriched_attempted by group, not just content hash</title>
<updated>2026-08-24T22:29:24Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T22:29:24Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d427118bd91d67f1a041e5daf267aebcd34ca9d7'/>
<id>urn:sha1:d427118bd91d67f1a041e5daf267aebcd34ca9d7</id>
<content type='text'>
Major finding: entry.id is a content hash, so the exact same physical
file — the same MP3, byte-for-byte — indexed into two different groups
(a shared library reused across several demo/test groups, or genuinely
the same folder shared into two groups) produces the *same id* in both.
_enriched_attempted was a single flat set of bare ids shared across every
group this node hosts. The moment one group's copy got enriched, every
other group's otherwise-identical copy read as "already attempted" and
was skipped forever — nothing else ever revisits an id once it's in this
set. That group's Music tab (or Videos tab, same bug, same set) showed
every affected file at duration 0 with no artist/album/thumbnail,
permanently, no matter how long you waited or how many times you
reloaded — group A having been enriched first was enough to silently
starve every later group of the same content.

Now keyed by (group_id, entry.id) throughout — the enrichment gate, the
sweep, and the rename re-enrichment path, for both video and audio (they
already shared the one set, and the collision risk is identical for
both). New regression test constructs two groups with byte-identical
audio content and confirms both enrich independently.
</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>fix(node): stop inventing a fake artist from the shared root's name</title>
<updated>2026-08-24T16:30:00Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T16:30:00Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=584730f9486e153c6c477293f03a95e78f5ab5f3'/>
<id>urn:sha1:584730f9486e153c6c477293f03a95e78f5ab5f3</id>
<content type='text'>
Music grouping was measured against a real ~5700-file library and came
back worse than a plain file listing. Root cause: the artist/album
ancestor walk always climbed exactly two levels (parent = album,
grandparent = artist) with no idea where the group's own shared root
was. Any file in a flat top-level folder — common here: bare
`Artist/track.mp3`, no album subfolder at all — had its "grandparent"
resolve to the root directory's own name, so the artist got replaced
by the share's name. Measured: 289 of 5664 tracks across 41 real,
unrelated artists (Ben Harper, Dire Straits, Jimi Hendrix, Janis
Joplin, ...) collapsed into one fake artist this way — the single
biggest bucket in the whole library, ahead of every real one.

- `_artist_album_from_ancestors` now takes the entry's own root
  boundary (daemon.py resolves it via `RootSet.split`) and refuses to
  read it as a name. A file sitting in a top-level folder — genuinely
  ambiguous, artist or a standalone album/compilation — is handled by
  `_split_top_level_folder`: split on "Artist - Album" when the
  (cleaned) folder name has that shape, otherwise the whole name
  becomes the artist alone, the more common real case here.
- `_clean_tag` treats known tagger placeholders ("No Artist", a French
  tool's "Nouvel artiste (334)") as absent rather than a real value —
  they were just as truthy as a real name and were locking out the
  fallback that would have done better. "Various Artists" is kept, a
  real compilation credit rather than a placeholder.
- A `title` tag that's the bare filename copied verbatim (track number
  included — found live on a whole CD-single) is stripped through the
  same prefix rule the filename parser already used
  (`title_parse.strip_track_prefix`), since a tag normally wins over
  the parsed title.
- Cover art: only 11% of a 400-file sample had embedded art (expected
  for this era of rip), but 267 loose cover images sit beside the
  tracks across the library (Windows Media Player's `Folder.jpg`/
  `AlbumArt_{guid}_*.jpg`, manual `cover.jpg`) and were never looked
  at. `_find_sibling_cover` checks the track's own folder before
  giving up — measured coverage 11% -&gt; 26% on the same library, zero
  network calls.

`_enriched_attempted` is in-memory and resets on restart, so a node
restart is enough to re-run enrichment over an already-scanned library
with the fixed logic — no rescan flag, no cache to clear by hand.

22 tests in test_enrich_audio.py (11 new), including the exact
regression case end to end through the real pool. Full suite: 1129
passed, no regressions.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
</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>
<entry>
<title>feat(node,hub): season-specific overviews, manual TMDB match correction, and wizard polish</title>
<updated>2026-08-24T12:33:20Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T12:33:20Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a'/>
<id>urn:sha1:0b0da86f1f9d6f0b1a27b5e1e1658c42de9f356a</id>
<content type='text'>
Two operator-facing fixes for a real 3-season show whose automatic TMDB
match was wrong at the show level: per-season overview/air_date tabs in the
detail modal (falling back to the show-level text when a season's own is
empty), and a "Fix match…" search-and-correct affordance that re-resolves
every file sharing the corrected show's display_title. New signed op
OP_TMDB_OVERRIDE and two read-only pairs (season_meta_req/resp,
tmdb_search_req/resp), MNP_VERSION 0.5 -&gt; 0.6.

Also: the create-group wizard gets a spinning indexing indicator and an
app-selection step, group settings default the TMDB language to the
operator's own locale (never as a global default), and a file renamed
mid-session now re-triggers title parsing instead of being silently
skipped by the enrichment dedup guard.

Fixes two bugs found during this work: the search overlay's z-index lost
to the base video-overlay class and rendered invisibly, and season_meta's
own empty overview didn't fall back to the show-level one.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01LAmyXtc6dAADsH23ydXQpY
</content>
</entry>
<entry>
<title>feat(node,hub): add Videos group app (poster grid, flat list, TMDB metadata)</title>
<updated>2026-08-24T08:04:46Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T08:04:46Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6af05abf410bbd038ce7fa6915a659defc509071'/>
<id>urn:sha1:6af05abf410bbd038ce7fa6915a659defc509071</id>
<content type='text'>
Implements docs/mediacenter.md: a "Videos" group application built on the
existing files index rather than a separate catalogue. On the node side,
new indexer enrichment (technical probe, filename/season parsing, thumbnail
generation) runs per-file once an operator has chosen a video_root for the
group, plus a TMDB client for on-demand poster/metadata lookups (never
client-side, thumbnails delivered over the existing chunk path). On the hub
side, a new video-app.js renders a lazily-mounted poster grid or a
thumbnail-only flat list, with TMDB entirely optional per group.

Along the way: the global apps registry now drives Settings' default-tab
picker instead of a hardcoded list, and the video_root is configured from
group Settings (like uploads) rather than from Files, with the node
refusing to run any TMDB/thumbnail work until one is set.

Fixes several bugs found via live testing against a real library, notably
a race between two effects writing the same "image ready" state that could
leave a poster grid spinning forever on a same-tab revisit — see
mediacenter.md §5.4 for the full account of each one.
</content>
</entry>
<entry>
<title>feat(node): persistent index cache, visible scan progress, adaptive reconcile, and delta sync</title>
<updated>2026-08-23T19:55:20Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-23T19:55:20Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b3709ac4d362987a9d025616c95065ceed0d216b'/>
<id>urn:sha1:b3709ac4d362987a9d025616c95065ceed0d216b</id>
<content type='text'>
Indexer performance work, in four parts:

- Persistent (path, size, mtime) -&gt; hash cache (indexer/cache.py) so a node
  restart no longer re-hashes every file — measured at 23 minutes for a
  114 GB library on a slow disk before this, near-instant after. Hashing
  is deliberately kept sequential (max_workers=1): it was never actually
  concurrent despite the pool size, and two interleaved reads seek-thrash
  a spinning disk instead of going faster.

- Byte-based scan progress (IndexProgress), surfaced via the loopback
  index-status route, the handshake ack, and a periodic INDEX_PROGRESS
  push to connected peers — drives a progress bar in the Create Group
  wizard and "add a directory" in Settings, and an animated presence dot.
  Guaranteed to settle back to idle via try/finally and a final push on
  the scanning-&gt;false transition.

- The reconcile backstop's directory walks now run in the executor
  instead of blocking the daemon's event loop; its interval defaults to
  10 min (was 60s) with adaptive backoff to 2h when nothing changes,
  reset on a real change or a peer connecting, and is now a per-group
  operator setting (signed op + group Settings UI).

- INDEX_DELTA wired up (protocol support existed, nothing called it):
  _on_index_change now sends additions/deletions instead of rebuilding
  the full entries list, coalesced over a short window so a burst of
  file events produces one push, and the hub swarm registration for
  public groups only (re-)registers newly added hashes.

Also fixes several bugs found while testing the above against real
libraries (a 114 GB and a 100+ GB group on a USB HDD):

- /api/reload blocked until the reload — including a brand-new group's
  full initial scan — finished, which the Electron bridge's fixed 30s
  call timeout turned into a hard failure on any real library. The route
  now fires the reload without waiting (ops.start_reload), matching
  add_root/remove_root's existing pattern; the wizard's own step order
  was fixed to wait for the group to actually appear hosted before the
  steps that need it (extra roots, GEK), with retries for the residual
  race between that and the daemon's own bookkeeping.
- transport.js's hand-rolled msgpack codec had no case for uint64/int64
  (0xcf/0xd3) and crashed decoding any message containing one — hit by
  IndexProgress.scanned_bytes/total_bytes for any group over ~4.3 GB.
  Verified against real msgpack-encoded bytes from the Python side.
- chat_hist_resp, and this change's own index_progress and
  set_scan_settings_ack pushes, were not routed by message type and
  could be handed to an unrelated pending request by the transport's
  "oldest pending" fallback, stalling it until its own 30s timeout and
  corrupting whatever received the wrong reply in its place.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
</content>
</entry>
<entry>
<title>feat(hub): split the group UI into a pluggable "applications" architecture</title>
<updated>2026-08-23T13:15:35Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-23T13:15:35Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9f02ee2c09652abf1308bdfa4a3eec4e9ca9ac83'/>
<id>urn:sha1:9f02ee2c09652abf1308bdfa4a3eec4e9ca9ac83</id>
<content type='text'>
GroupPage's 6620-line app.js carried Chat and Files wedged in directly, with
no way to add another group-level app without touching the shell itself. It
is now app.js (routing, non-group pages) plus nine focused files — apps.js
(the registry), chat-app.js, files-app.js, video-player.js, group-page.js
(the shell), group-settings.js, hub-client.js, icon.js and file-utils.js —
with docs/apps.md as the checklist for adding one (Videos/Music/Photos are
sketched there, not built).

Node side gained the matching enablement mechanism, mirroring
member_upload exactly: a roster setting, a signed apps_enabled op enforced
by _has_admin_authority, exposed in the handshake ack. Operators toggle
applications per group from Settings, which also gained a small reorder:
Invite, Pairing, Applications, Shared directories, Uploads, danger zone,
Your devices, Members.

Two bugs surfaced during the split, both missing an import across the new
file boundary and invisible to node --check or a module-load probe since
they only throw when the code path actually runs:

- group-page.js called onRefreshAuth on a stale-token handshake rejection,
  but app.js never imported refreshAccessToken from hub-client.js — so a
  brand new member (including a group's own creator) hit "Not a member of
  this group" and the retry silently failed, throwing before it could
  refresh the token.
- chat-app.js called getLocale() for message timestamps without importing
  it from i18n.js. Opening Chat on a group with real messages threw mid-
  render; uncaught, that appears to wedge Preact's render scheduler, so
  every button on the page stopped responding until reload.

Caught the second class of bug with a proper no-undef audit across all
split files (a temporarily installed ESLint 9, since the system one is too
old to parse this codebase's syntax) rather than trusting grep. 827 tests
pass; 6 new ones cover the apps_enabled policy.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
</content>
</entry>
<entry>
<title>feat(node): systemd service panel on the Node page, and a clean CLI restart</title>
<updated>2026-08-22T16:26:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-22T16:26:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da'/>
<id>urn:sha1:8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da</id>
<content type='text'>
Add a status panel at the top of the Node page — always visible, even
before an MNP connection exists — showing the meshbay-node systemd
unit's own state (via `systemctl --user show`, main process only) with
Start/Stop/Restart controls. This is the piece the rest of the page
cannot provide: it has to work while the daemon is stopped or crash-
looping, which the MNP-based sections require the daemon to already
answer.

While touching node lifecycle: `reload` and `restart-daemon` in the
CLI shelled out to pgrep + SIGTERM/SIGHUP and respawned the process by
hand, logging to a hardcoded /tmp path. That pattern already SIGHUPed
a developer's own running node by accident once (see the old
test_cli_dispatch.py comment). Both now delegate to
`systemctl --user reload|restart meshbay-node`, which the unit already
supports correctly (ExecReload=, Restart=on-failure).

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