<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js, branch 0.10</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.10</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.10'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-31T15:19:17Z</updated>
<entry>
<title>feat: email verification for registration, email change, and invitations</title>
<updated>2026-08-31T15:19:17Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-31T15:19:17Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=c6fd7ea89b6e0a96eb1d81989de891b4768b1044'/>
<id>urn:sha1:c6fd7ea89b6e0a96eb1d81989de891b4768b1044</id>
<content type='text'>
Registration now creates a pending account and sends a 6-digit code via
email; the account activates only after verification. Email changes on
the profile page follow the same flow. Group invitations send a
notification email to the invitee (without revealing their address to
the inviter) containing the invite code and hub link.

Backend: blind HMAC-SHA256 email index for uniqueness without decryption,
mail.py for localhost Postfix delivery, verification endpoints, cleanup
of expired codes and stale pending accounts, startup backfill of
email_hash for existing users.

Frontend: 3-phase register page, inline email change verification on
profile, invite-notify call with status display. All 10 locales updated.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: node workflow redesign — wizard auto-config, reset, MusicBrainz contact</title>
<updated>2026-08-29T09:15:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-29T09:15:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b7733e812fadd6007976d262bd1d793572a36ba7'/>
<id>urn:sha1:b7733e812fadd6007976d262bd1d793572a36ba7</id>
<content type='text'>
Wizard (Electron):
- Auto-provisions node config (hub URL + username) from logged-in user
- node:start handles both cold start and restart of misconfigured daemon
- Waits for daemon to reach 'running', auto-links node key on hub
- probeNode accepts intermediate states for wizard progress feedback

Reset (meshbay-node reset):
- Unlinks node key from hub (DELETE /me/node_key, best-effort)
- Stops and disables daemon (systemctl --user disable --now)
- Erases ~/.config/meshbay, ~/.local/share/meshbay, ~/.local/state/meshbay

MusicBrainz contact:
- Resolved from owner's hub email instead of per-node roster config
- Removed musicbrainz_contact UI and WebRTC handshake field
- Removed set_musicbrainz_contact/musicbrainz_contact from roster

Node pairing:
- Added operator pairing banner on NodePage
- Added operator_paired flag to list_groups

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): group names unique per owner, shown as name@owner</title>
<updated>2026-08-28T08:09:38Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-28T08:09:38Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=58ca879b017da62e77f40752d8d94fef3f315a1e'/>
<id>urn:sha1:58ca879b017da62e77f40752d8d94fef3f315a1e</id>
<content type='text'>
A group's identity stays its UUID. What changes is that "the name is
unique" — until now an unenforced expectation — becomes real, scoped to the
owner account, and the owner's username is surfaced so two groups called
"photos" on different nodes can be told apart.

Hub:
- `groups` gains a functional unique index `uq_groups_owner_name` on
  `(admin_id, lower(name))` (model + migration c3d4e5f6a7b8). The migration
  pre-flights: if the data already clashes it aborts and lists the
  offending (admin_id, name) pairs rather than renaming anyone's group.
  meshbay.org checked clean.
- `create_group` trims the name, rejects blank (422) and an owner-scoped
  case-insensitive clash (409), with an IntegrityError backstop for the
  race, and returns `owner_username`.
- `owner_username` added to `/v1/groups/mine`, `GET /v1/groups` (local
  rows), `POST /v1/groups/{id}/join`, `GET /v1/admin/groups`.

SPA:
- new `static/group-name.js` — `&lt;GroupName name owner [inline]&gt;` renders the
  name with the `@owner` handle on a smaller grey line under it.
- used in the sidebar, the group-page header, Explore cards, the Admin
  groups table, and cross-group Search (via a widened `cacheGroupIndex`
  carrying the owner). Delete/leave confirmations show `name@owner` inline.
- federated Explore rows show `@&lt;source_hub&gt;` instead of an account.

Design record and the locked decisions: ~/next/groupnames.md (out of repo).
MNP unchanged. Tests: test_group_name_unique.py, test_group_name_migration.py.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018gKJ85aZyvEwarXMFzFEwi
</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): 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): 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>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(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>
