<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.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-28T00:51:00Z</updated>
<entry>
<title>feat(hub): let a hub admin disable public groups instance-wide</title>
<updated>2026-08-28T00:51:00Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T00:51:00Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b5b4f188a39fc96c4d32e67151e067b1add6dcfc'/>
<id>urn:sha1:b5b4f188a39fc96c4d32e67151e067b1add6dcfc</id>
<content type='text'>
A new General tab in Administration carries one switch, allow_public_groups,
stored in a hub_settings key/value table (runtime-editable, unlike hub.toml).
Default is on; an absent row means on, so an upgrade changes nothing.

Enforcement is server-side on every hub-mediated path, not just the SPA:

  - create_group        refuses visibility=public (403), staff included
  - list_public_groups  the directory returns nothing (local + federated)
  - join_group          open-joining a public group is refused
  - group_online_nodes  a non-member of a public group is handed no node
  - signaling.webrtc_offer  drops the "node hosts an open group" fallback
  - federation.export_directory  advertises nothing to peer hubs

The switch is read live, so flipping it back restores every path. Existing
members of a group that predates the switch keep their membership row and
their access — this is plan A, not a purge. GET /v1/hub/info exposes the
flag (unauthenticated) so the create-group form and the sidebar's "Public
groups" link render correctly.

Also in the admin Groups tab: a Revoke action beside Suspend. Suspend is the
reversible hub flag; Revoke calls POST /v1/admin/revoke, which sets
status=revoked and broadcasts a signed revocation every node enforces
(denylist + dropped live sessions). It is confirm-guarded and names the group.

And a message fix the revoke work surfaced: group_online_nodes, join_group and
webrtc_offer answered "Group is suspended" for any non-active status. They now
report the real state, so a member of a revoked group is told "Group is
revoked" rather than something reversible-sounding.

Tests: test_public_groups_toggle.py (10) covers the switch end to end and the
five enforcement paths; test_revocation.py gains the status-message assertion.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</content>
</entry>
<entry>
<title>feat(video): add an All/Movies/Series filter to the toolbar</title>
<updated>2026-08-26T14:45:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-26T14:45:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f2b90449841373d9b0fdd70449f3e94c9dd0116d'/>
<id>urn:sha1:f2b90449841373d9b0fdd70449f3e94c9dd0116d</id>
<content type='text'>
A segmented control to the left of the search box, defaulting to "All".
Applied before the text filter — a title match within a type nobody asked
to see still isn't shown. Resets to "All" on group change, matching the
text filter's own reset.
</content>
</entry>
<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>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(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>
</feed>
