<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages, 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>fix(hub): poll for a root count change instead of trusting one fetch</title>
<updated>2026-08-24T22:29:03Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T22:29:03Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=06c101154d544290d27f18d6fc08fb5f58a4e5d5'/>
<id>urn:sha1:06c101154d544290d27f18d6fc08fb5f58a4e5d5</id>
<content type='text'>
Point 1 (directory list not updating without a full page reload) turned
out to still reproduce after the earlier onRefreshIndex fix — that one
addressed the Videos/Music root pickers (nodeDirs), but this section's own
list reads a different field entirely (ops.list_groups returns the
*runtime* root set, groups_ctx[gid]["roots"]) that only gets replaced once
_reload_config_inner's retarget actually finishes. /api/reload itself is
fire-and-forget on the node by design (ops.start_reload's own docstring —
a brand-new group's initial scan can take minutes, the caller must not
block on it), so a single loadNodeInfo() call right after can land in the
gap before that replacement happens and show the pre-change count.

Both the add and remove handlers now poll /api/groups (up to ~4s, every
400ms) until the root count actually matches what the action should have
produced, rather than fetching once and hoping the timing worked out.
</content>
</entry>
<entry>
<title>fix(hub): keep an unmatched admin_challenge visible, add trace logging</title>
<updated>2026-08-24T21:47:01Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T21:47:01Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=a41be5c5d6212f4eadc54bc17354311b0655a5a4'/>
<id>urn:sha1:a41be5c5d6212f4eadc54bc17354311b0655a5a4</id>
<content type='text'>
A real report showed audio_root timing out with *nothing* logged in
between the connection handshake and the timeout — no admin_challenge, no
error, nothing. The previous fix made an unmatched admin_challenge return
silently (correctly, to stop it stealing an unrelated pending request —
see the earlier commit), but that silence is indistinguishable from "the
request never reached the node at all", which is exactly the ambiguity
blocking this investigation. An unmatched admin_challenge is now logged
(op, op_id, and the full set of currently-pending keys) instead of
dropped quietly, and setAudioRoot/_authorizeAdminOp trace both hops of
the round trip explicitly. Node-side, _do_audio_root gets a debug log at
entry — cheap, and the only way to know from server logs alone whether
the request was ever received if the client-side trail comes up empty.

Diagnostic only: no routing behavior changed from the previous fix,
verified against the same reproduction script.
</content>
</entry>
<entry>
<title>fix(hub): make root-folder save success/failure actually visible</title>
<updated>2026-08-24T21:35:09Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T21:35:09Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=926ebce735afd01800a669a266b90fc98f673a6b'/>
<id>urn:sha1:926ebce735afd01800a669a266b90fc98f673a6b</id>
<content type='text'>
Flagged directly: "Saving" ran for a few seconds then just stopped, with
nothing telling the operator whether it had worked. Both outcomes used
the same dim .settings-hint styling, so a real failure and a real success
looked identical at a glance. Success and failure are now tracked
separately (previously one plain string held either) and rendered with
the same success-msg/error-msg styling already used elsewhere on this
page, so which one happened is unambiguous.
</content>
</entry>
<entry>
<title>test(node): a real signed audio_root save through the full challenge path</title>
<updated>2026-08-24T21:34:57Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T21:34:57Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=aeeaf0d537a1942010caf222b72333841b80b7f8'/>
<id>urn:sha1:aeeaf0d537a1942010caf222b72333841b80b7f8</id>
<content type='text'>
Every existing audio_root test either called ops.set_audio_root directly
or mocked out _issue_admin_challenge — none of them exercised real
signature verification, _do_admin_response, or the shared groups_ctx/
roster wiring _run_op depends on. Worth ruling out a break somewhere in
that real path specifically: a report described a save that looked like
it worked (the Music tab showed content right after) not surviving a
reload.

Drives the real _do_audio_root -&gt; admin_challenge -&gt; sign -&gt; _do_admin_response
-&gt; _admin_exec_audio_root path with a genuine Ed25519 operator key, then
opens a *separate* Roster instance against the same db file — the direct
question a "worked, then reverted" report raises: does the value actually
land durably, in a form any later connection reads back correctly. It
does; this passes. The one thing missing from the session fixture to get
this far was peer-registry self-registration (a real session adds itself
on handshake completion — without it, the final ack has nowhere to go,
including back to the requester).
</content>
</entry>
<entry>
<title>fix(hub): key admin_challenge/admin_response by op, not arrival order</title>
<updated>2026-08-24T21:09:28Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T21:09:28Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1e6abbb8db76cdabd53b85d938ee0e76486f5ae4'/>
<id>urn:sha1:1e6abbb8db76cdabd53b85d938ee0e76486f5ae4</id>
<content type='text'>
Reproduced from a real report: enabling the Music app and saving its root
folder in the same Settings visit (the new merged Directories section
makes this a fast, natural back-to-back sequence) fired two signed admin
ops within milliseconds. Neither the admin_challenge reply nor the
admin_response ack two steps later was keyed by anything — both were
matched purely by "whichever request happens to be oldest pending"
(transport.js's own documented last-resort guess). apps_enabled's
challenge stole audio_root's pending slot; audio_root's own request never
received a challenge at all and just sat there until its 30s timeout.

Both hops are now keyed by op name: admin_challenge already carries `op`
from the node, and admin_response is given one client-side purely for
this (the node's _do_admin_response never reads it — only op_id and
signature). A stray admin_challenge with no matching request is dropped
outright rather than guessed at — it is never a broadcast (one
`self._send`, no peer loop, docs/webrtc_server.py), so a session with no
matching key genuinely has nothing to do with it. A domain ack (an actual
broadcast — every connected client gets audio_root_ack, not just the
requester) still falls through to the existing per-type handling when
nobody here is waiting on it, unchanged.

Verified against a standalone reproduction of the exact race (two admin
ops racing, reordered replies) — this codebase has no browser-JS test
runner to add as a real regression test, so the repro lived in a scratch
script rather than the suite.
</content>
</entry>
<entry>
<title>fix(hub): refresh directory list after adding/removing a shared root</title>
<updated>2026-08-24T21:09:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T21:09:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=25bf672719be2e47c15fc3659635b50a1540571d'/>
<id>urn:sha1:25bf672719be2e47c15fc3659635b50a1540571d</id>
<content type='text'>
Folders (unlike files) only ever arrive over MNP as part of a full
index_sync — the ongoing index_delta push has no `dirs` field at all
(daemon.py never puts one there for incremental updates) — so a directory
added or removed via the Electron-local add/remove flow never showed up
in the Videos/Music root pickers until the whole page was reloaded. The
merged Directories section made this easy to hit: add a shared folder,
then immediately try to pick it as a root, in the same visit. Both
actions now call the same onRefreshIndex a chat upload already uses to
pick up its own effect on the index.
</content>
</entry>
<entry>
<title>feat(hub): audio_root wiring, mutually-exclusive players, Settings rework</title>
<updated>2026-08-24T20:44:27Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T20:44:27Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=df7eb105dda550989dccd3ba2bf22a50e919b19b'/>
<id>urn:sha1:df7eb105dda550989dccd3ba2bf22a50e919b19b</id>
<content type='text'>
Five related pieces of polish against the Music app and Settings, all
from the same conversation:

- Music app now requires audio_root, same as Videos requires video_root:
  an empty-state message until one is set, and grouping filtered to only
  what's under it (underAudioRoot, mirroring video-app.js's
  underVideoRoot). Wires the new audio_root/audio_root_ack pair through
  transport.js and group-page.js state the same way video_root already
  flows.
- Starting one player now stops the other — opening a film closes the
  music queue, starting a track closes the video modal. Both used to run
  at once, found live.
- Group Settings reworked: every section but a bare form (invite,
  pair-operator, approve-device) is now collapsible (CollapsibleSection);
  the uploads on/off button is a real toggle switch (ToggleSwitch,
  reused for TMDB/MusicBrainz's enabled switches too, each now with an
  icon + status badge in its header instead of a plain checkbox row);
  and shared directories, the Videos root picker, and the new Music root
  picker are merged into one "Directories" section (RootFolderRow) instead
  of three separate ones scattered down the page — the root pickers only
  show once their app is actually enabled.
</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(hub): give the Music flat list its own look instead of Videos' reskin</title>
<updated>2026-08-24T19:51:18Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T19:51:18Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=626365668508790dee70ab192a7d6c6f14725bf4'/>
<id>urn:sha1:626365668508790dee70ab192a7d6c6f14725bf4</id>
<content type='text'>
Two complaints against real use: the artist -&gt; album -&gt; track hierarchy
was invisible (every depth sat flush left, distinguishable only by which
chevron happened to be open — Videos' own flat list never needed more
than one level, so there was nothing to reuse for this), and a filled-in
album unfolded into a wall of identical little icon-box squares, one per
track, carrying no information a track row can actually use (unlike
Videos' per-episode thumbnail).

Track rows now reuse Mode A's own numbered tracklist style
(.music-track-row: number, title, duration, no icon box) instead of
Videos' boxy thumb-slot row. A folder's expanded contents get wrapped in a
new .music-flat-children indent + rule line, so nesting reads as visible
steps into the tree rather than same-level siblings. Folder rows
(artist/album headers) still reuse Videos' flat-row style, which fits them
fine — this is not a wholesale rewrite, only what didn't actually work.
</content>
</entry>
</feed>
