<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/locales, 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-24T20:44:27Z</updated>
<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(music): transcode WMA/Musepack to AAC so they actually play</title>
<updated>2026-08-24T19:08:34Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T19:08:34Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f4ed927fa873133c5fed73fb7dd60e7747fc3dc2'/>
<id>urn:sha1:f4ed927fa873133c5fed73fb7dd60e7747fc3dc2</id>
<content type='text'>
Tagging and covers for these two formats landed already, but neither one
decodes in any mainstream browser's &lt;audio&gt; element at all — a real
library scan turned up 273 such files that would show up correctly in the
Music app and then simply fail on click. This closes that gap: the node
transcodes to AAC/M4A on request (a one-shot whole-file conversion, not
live-piped like video's fMP4 segments — an audio file is small enough that
streaming it buys nothing), caches the result under its own content hash
the same way a TMDB poster or a MusicBrainz cover is cached, and serves it
back through the ordinary file_req/chunk path. That path used to assume
anything in the media cache was thumbnail-sized (single chunk, always);
generalized it to slice a cached blob the same way a real file on disk
gets sliced, since a transcoded track can be several MB.

New MNP pair (`audio_transcode_req`/`_resp`, version bump to 0.9), shares
its concurrency cap with video's transcode pool rather than getting its
own — both are real ffmpeg processes on the same node. Every other audio
format is untouched: this only fires for .wma/.mpc, the two extensions
that need it.
</content>
</entry>
<entry>
<title>fix(hub): auto-skip a track that fails to load instead of stalling the queue</title>
<updated>2026-08-24T18:30:12Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T18:30:12Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ed97f138cfc0aee5ac04e4d12b9e392277cbcc40'/>
<id>urn:sha1:ed97f138cfc0aee5ac04e4d12b9e392277cbcc40</id>
<content type='text'>
A genuinely corrupt source file (found via a real "1 track, fails to play"
report) would stop a "play all" queue dead with no way forward except
manually picking the next track. The player now counts consecutive load
failures and advances past them on its own, capped so a pathological queue
(everything broken) doesn't spin forever; the existing Promise-rejection
catch and a new &lt;audio&gt; onerror handler both feed the same counter, since a
decode failure can surface either way depending on the browser.
</content>
</entry>
<entry>
<title>feat(hub): consolidate loose tracks, "&amp;"/"and" fold, player close/queue</title>
<updated>2026-08-24T17:46:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T17:46:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=62253b9592a83ce152d0c64471e20514218fb132'/>
<id>urn:sha1:62253b9592a83ce152d0c64471e20514218fb132</id>
<content type='text'>
Album-grid readability, part two:

- groupMusicEntries (music-app.js): an album bucket left with exactly
  one track - a real album tag, but only one song from it, not the
  whole release - clutters the grid the same way an untagged loose
  track does. Both kinds now fold into one "&lt;artist&gt; - Various" tile
  per artist, unless there is only one leftover track overall, where
  relabeling buys nothing and the track keeps its own name (or the
  generic placeholder, if it never had one).
- foldKey also normalizes "&amp;" vs "and" ("Artist &amp; The Band" / "Artist
  and The Band" is one act, tagged both ways across different rips of
  the same catalogue) alongside the existing case/whitespace fold.
- music-player.js: a close button pauses and tears the player down;
  an unmount cleanup effect (pause, revoke every cached blob URL)
  fires either way, whether that's the close button or the shell
  tearing the bar down on its own. A "current queue" button opens an
  overlay listing the whole playing queue with the current track
  highlighted, click any to jump to it - works identically regardless
  of how the queue was built (an album, the consolidated misc bucket,
  a single standalone track), since it only ever reads the player's
  own live tracks/order/pos.
- group-page.js: this component is not remounted when switching to a
  *different* group on the same /group/:id route (only the groupId
  prop changes) - so without an explicit reset, music from one group
  would carry into the next one opened. Resets musicQueue to null on
  groupId change; a tab switch inside one group still leaves it alone.
- Scrubbed real artist/band names that had leaked into code comments
  and test fixtures (enrich_audio.py's docstrings, several
  test_enrich_audio.py assertions, a music-app.js comment) - replaced
  with generic placeholders, no behavioural change.
- i18n: music.various, music.player_close, music.player_queue,
  music.queue_title added across all ten locales.

Client-side only except none of this touches the node at all. npm run
sync-ui re-run. 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(hub): Music app client — album grid, flat list, persistent player</title>
<updated>2026-08-24T15:39:41Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T15:39:41Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8c780b9928db8145b7fe18ecd137384ccfe25d8f'/>
<id>urn:sha1:8c780b9928db8145b7fe18ecd137384ccfe25d8f</id>
<content type='text'>
Implements the client half of docs/musicbay.md against MNP 0.8:

- music-app.js: album grid (grouped by artist -&gt; album, from index-time
  artist/album fields) or flat folder view, per-group localStorage
  toggle like Videos. MusicBrainz (music_meta_req) is only looked up
  when a track has no embedded cover at all — well-tagged files never
  trigger a network call, unlike Videos where TMDB is unconditional.
  Reuses video-app.js's MediaThumb/LazyTile (now exported) rather than
  duplicating the chunk-path thumbnail decode + virtualization.
- music-player.js: the persistent player bar — queue, shuffle (Fisher-
  Yates, keeps the current track in place when toggled), repeat (off/
  all/one), volume (localStorage), prev/next, a one-track prefetch
  cache. No MSE, no node-side streaming: a track is downloaded and
  decrypted once via file-utils.js's pipelinedDownload, same chunk
  pipeline Files already uses, then played from a blob URL.
- group-page.js: owns musicQueue/musicbrainzConfig state and renders
  MusicPlayerBar outside the tab-switched area — deliberately, so
  playback survives navigating to Chat/Files, the same reasoning the
  video/preview modals are shell-owned rather than app-owned.
- apps.js: registers "music". transport.js: fetchMusicMeta (keyed by
  path, same reordering-hazard fix as fetchMediaMeta),
  setMusicbrainzConfig/setMusicbrainzEnabled (signed ops, mirroring
  TMDB's), and the three new ack handlers. group-settings.js: a
  MusicBrainz settings section (contact string, per-group toggle) —
  the existing Applications checklist already picks up "music" for
  free, per apps.md's own claim.
- icon.js: music/pause/skip-next/skip-prev/shuffle/repeat/volume,
  drawn in the same stroked style as the existing set.
- i18n: group.tab_music, the music.* and settings_node.musicbrainz_*
  keys, translated (not just copied) across all ten locales, Polish
  carrying full one/few/many/other plural forms for music.n_tracks.
- webapp.py's _ASSETS, test_hook_ordering.py's STATIC_FILES and
  test_transport_contracts.py's SPLIT_FILES gain the two new files.

Full suite (common + hub + node): 1116 passed, no regressions.
`npm run sync-ui` in meshbay-client confirmed both files copied.

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,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>
