<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/group-page.js, branch 0.8</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.8</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.8'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-26T12:43:16Z</updated>
<entry>
<title>feat(music): network-adaptive prefetch depth, opt-in keep-screen-on toggle</title>
<updated>2026-08-26T12:43:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T12:43:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=59d9f50bf41b2b38b7c95f8da52b698dff923c2d'/>
<id>urn:sha1:59d9f50bf41b2b38b7c95f8da52b698dff923c2d</id>
<content type='text'>
Prefetch depth (music-player.js): 5 tracks ahead on Wi-Fi, 3 on cellular —
more runway through a screen-lock network gap when the connection is cheap
and fast, less when it's metered. navigator.connection is Chromium-only;
Firefox/Safari (where it's undefined) get the same conservative tier as an
unrecognized connection type, never assumed fast. MAX_CACHED_BLOBS raised
to 6 to hold the largest case (current + 5).

Keep-screen-on-during-audio (new user preference, off by default): a
Settings toggle, backed by a new whitelisted key on
/v1/users/me/preferences (music_keep_screen_on). music-player.js holds a
Screen Wake Lock only while a track is playing and only when the user has
opted in — unlike the video player's unconditional lock, this must not
fight the ordinary expectation (matching Spotify/Deezer) that the phone
locks on its own while listening.
</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>
<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>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>fix(hub): starting one player stops the other</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=5836214ef67cce5dd3c39168a4a00fee3e76b1c5'/>
<id>urn:sha1:5836214ef67cce5dd3c39168a4a00fee3e76b1c5</id>
<content type='text'>
Opening a film while a track was playing left both audio tracks running
together — nothing closed the music queue when a video opened, and nothing
closed the video modal when a track started. Both directions now stop
whichever player wasn't just asked for.
</content>
</entry>
<entry>
<title>feat(hub): play audio files from Explorer, scoped to the current folder</title>
<updated>2026-08-24T18:04:36Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T18:04:36Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=28be8eead316f5026e1c0c8ffd46b873d479091d'/>
<id>urn:sha1:28be8eead316f5026e1c0c8ffd46b873d479091d</id>
<content type='text'>
Audio files were invisible to file-utils.js's canPreview, so their
name never rendered as a clickable link and the toolbar's Play/View
buttons never enabled for them - the only way to do anything with an
audio file in Files was to download it.

- file-utils.js: canPreview now includes 'audio' alongside image/
  video/document.
- files-app.js: the toolbar's canPlay/canView split treats audio like
  video (Play, not View) rather than falling into the generic preview
  path, which was never built for it anyway.
- group-page.js: onPreview routes an audio entry to onPlayQueue - the
  same persistent player Music uses - instead of the preview modal.
  Deliberately not Music's artist/album grouping: the queue is every
  audio entry sharing the clicked file's literal containing directory
  (entry.path), sorted by filename, so previous/next in Explorer stays
  scoped to what's actually in that folder, tags or no tags. onPlayQueue
  already replaces whatever queue is playing unconditionally, so
  starting a track from Files while Music (or another Files folder) is
  already playing needs no special handling - it's the same "just a new
  queue" path either way.

Client-side only, no protocol/index change. 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): 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>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>
