<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/video-app.js, branch 0.16</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.16</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.16'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-25T08:25:44Z</updated>
<entry>
<title>refactor(client): move the connection pool and the media tiles to modules</title>
<updated>2026-09-25T08:25:44Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-25T08:25:44Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=950f4aa6d107e127fb6dc0579beedbff0e6f1af5'/>
<id>urn:sha1:950f4aa6d107e127fb6dc0579beedbff0e6f1af5</id>
<content type='text'>
ConnectionPool (with connectToGroup and its limits) leaves search-page.js
for connection-pool.js, and LazyTile/MediaThumb leave video-app.js for
media-tiles.js, cut as text. The shell and the Music and Photos apps now
reach them without importing the search page or the Videos app.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>refactor(node): move per-app enrichment out of NodeDaemon</title>
<updated>2026-09-24T16:30:10Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-24T16:30:10Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=387138410532daad174ed602fd03f4f979cd3d81'/>
<id>urn:sha1:387138410532daad174ed602fd03f4f979cd3d81</id>
<content type='text'>
The _enrich_*/_reenrich_* methods and _on_enriched become
EnrichmentMixin in meshbay_node/enrichment.py, with the two directory
helpers only they use. APP_DIR_KEYS stays on NodeDaemon.

Co-Authored-By: Claude Opus 5.5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: cite MESHBAY_DESIGN.md and a section instead of the merged notes</title>
<updated>2026-09-19T00:12:47Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-19T00:12:47Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=20a824118c09af15d6c338db4c9480ffe5cbcdb6'/>
<id>urn:sha1:20a824118c09af15d6c338db4c9480ffe5cbcdb6</id>
<content type='text'>
The per-feature design notes were merged into docs/MESHBAY_DESIGN.md and
deleted from the tree on 2026-09-11, but ~230 comments across the three
packages still named them — usually written `docs/musicbay.md §3.2`, as
though the file were still in docs/. A reader had to know §16 existed to
resolve any of them. They now name the section directly.

Every mapping comes from §16, the concordance, which already records where
each old section landed: musicbay -&gt; §9.8, mediacenter -&gt; §9.7 for the
Videos app and §6.5 where the subject is derived data, photos -&gt; §9.9,
auth-confirm -&gt; §3.6, refactoring-search -&gt; §9.11, invite-pairing-v1 -&gt;
§3.4, per-node-identity-v1 -&gt; §3.2, captcha -&gt; §7.7, chat-sender-keys -&gt;
§4.5, apps/refactor-groups -&gt; §9.1–§9.4, desktop-client-v1 per section.
Bare citations of the same documents (`draft-v6 §2.11`, `§4.8`, `§3.4`)
are retargeted too: those collide with real section numbers in the design
document, so leaving them would have been worse than the named form.

Four cases the concordance does not cover, each decided rather than guessed:

  Sub-item references into documents that no longer exist — mediacenter's
  `§3.3 row 4`, `§3.4b/c`, `apps.md §3b` — name rows and sub-items §9.7 and
  §9.2 do not reproduce. The module-level citation stays; the sub-item
  pointer is dropped.

  The V-findings keep their labels but lose the dead `§10.1/` prefix.
  §13.8 lists V1–V13 as per-application open items, which is not what the
  labels mean in these comments, so pointing them at §13.8 would have been
  a false citation.

  `apps.md §5`'s virtualization requirement has no counterpart anywhere in
  the design document. The requirement is stated in the comment itself, so
  the citation is dropped rather than aimed at a section that does not say
  it.

  Comments that attributed a *sentence* to an old note — musicbay's "several
  thousand files" example, its "what got measured" note, its measured
  ~11%/~26% cover-art figures, the "original no root, whole shared tree"
  call — state the fact without attribution now. §9.8 does not contain those
  sentences and citing it for them would have been wrong.

CLAUDE.md's "a reference to a document that no longer exists" row now says
the concordance is for git history and out-of-tree material; the code cites
sections directly.

Verified: 2851 passed, 4 skipped. The 12 errors in the run are the Firefox
leg of test_sticky_header.py's browser harness, which is broken at the
browser level on this machine — headless Firefox (snap) dies with
`[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer`, renders
nothing, and the probe exits `{"error": "no measurement"}` after its full
90s wait. Chrome runs the same 12 assertions in 3.2s and passes. Nothing
here can affect it: every changed line in style.css is inside a comment.
Also checked: ast.parse on every changed .py, `node --check` on every
changed .js, the /* */ balance in style.css, and that no changed line
exceeds the width its file already used.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): Videos and Music list their cards a page at a time</title>
<updated>2026-09-15T15:01:06Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-15T15:01:06Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=459fc93e98f23e326c2fa77fe86ba74c2bae77f0'/>
<id>urn:sha1:459fc93e98f23e326c2fa77fe86ba74c2bae77f0</id>
<content type='text'>
Previous/next arrows in the pinned toolbar, on group pages and in Search.
Page size is an account preference (Settings → Defaults), 50 by default,
10 to 200 in steps of 10.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>refactor(spa): stop asking a node what version it is</title>
<updated>2026-09-10T15:29:50Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-10T15:29:50Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=4753c67816c774323e3ab4efc76d3259e8ded40d'/>
<id>urn:sha1:4753c67816c774323e3ab4efc76d3259e8ded40d</id>
<content type='text'>
`MNP_MIN_SUPPORTED` is the version this build speaks, so `check_version`
refuses everything below it at the handshake. Every capability the client was
gating on the node's version is therefore true of every peer it can reach:

  * `supportsSealedUpload` — an upload is sealed or it is not sent;
  * `supportsAppOps` — one `app_directories` op, and no `setVideoRoot` /
    `setAudioRoot` / `setPhotoRoots` wrappers behind it;
  * `supportsTransferSlots` and `Lease._skip()` — a lease is always real, so
    there is no branch where a transfer runs without one;
  * `legacyNode`, the read-only shared-directories table, and the two hints
    telling an operator their node is too old to configure an app.

The version the node declares is still recorded, for diagnostics. Nothing
branches on it, and the comment says so, because a field kept "just in case" is
how the branches came back last time.

`test_mnp_1_0_node_compat.py` goes with them: it existed to hold the fallbacks
in place, and holding a fallback that cannot execute is how a suite starts
lying. The two locale strings for those hints are removed from all ten
catalogues.

Hub suite 872 passed (test_sticky_header deselected — failing before this).

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
</content>
</entry>
<entry>
<title>fix(hub): stop badging every poster "unmatched" when TMDB is off</title>
<updated>2026-09-08T00:17:00Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T00:17:00Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=2ac1f1f704d44e2b20f9598044f7e266ffae4d36'/>
<id>urn:sha1:2ac1f1f704d44e2b20f9598044f7e266ffae4d36</id>
<content type='text'>
The dashed warning outline and the "?" flag say a TMDB lookup ran and came
back with nothing — that is how an operator finds the titles worth a "Fix
match". With TMDB switched off for the group no lookup is made at all:
media_meta_req answers confidence 0 for the whole library by design, so the
poster grid drew every card as a failure of the very thing the operator
chose, with no poster ever coming to clear it.

Poster mode now shows the file's own thumbnail plainly in that case, the
same as Flat does, and keeps the badge for what it was built for.

- PosterCard takes tmdbEnabled and gates both the outline and the "?" on
  `tmdbEnabled &amp;&amp; !confident`. The title still falls back to the parsed
  filename exactly as before, and the ready/imageReady anti-flash gating is
  untouched.
- VideoDetailModal gates its "no confident match" paragraph the same way. A
  show still opens that modal with TMDB off — it is where the season list
  lives — so the card alone would have left the claim one click deeper.
- Its operator-only "Fix match" / "Rematch" buttons are gated too: both act
  on a match that does not exist, one opening a search the node answers
  empty, the other dropping a cached match never made. Those buttons are the
  only entry points to TmdbSearchOverlay and doRematch.
- Both props default to true, so a call site that forgets one keeps the badge
  rather than silently losing it.

search-page.js is deliberately left passing `enabled: true`: a cross-group
view has no single group's switch to read, and it never showed the operator
buttons anyway (isNodeAdmin={false}).

Recorded as §10.7 in docs/mediacenter.md.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V8EDjk6pkYZrCbo63m2x87
</content>
</entry>
<entry>
<title>feat(client): Phase 2 — per-app settings panes, folder tree, multi-directory</title>
<updated>2026-09-06T17:03:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-06T17:03:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ab44526a291fa673aa2850d105f6412a70a5341f'/>
<id>urn:sha1:ab44526a291fa673aa2850d105f6412a70a5341f</id>
<content type='text'>
Each app's settings were inlined in `group-settings.js` — TMDB, MusicBrainz,
and one folder picker per app, each with its own draft state and save handler
saying the same thing about a different key. They are one file per app now,
reached through the `apps.js` registry, and the page that renders them names no
application at all: adding one is a registry entry and a settings file.

The line between the two is what makes that true. What every app has — folders
— the page does generically, through one `saveDirectories` bound to the app.
What one app alone has, its pane does itself with the transport it is handed.
An app that only needs directories touches neither `group-settings.js` nor
`group-page.js`, which is `test_app_settings_plugin.py`'s subject.

`settings-ui.js` exists because a pane importing the page that renders it is a
cycle, and ES modules answer that with a temporal-dead-zone ReferenceError at
first render — a component that silently does not appear, the fault already
recorded in CLAUDE.md about hook ordering.

The flat depth-indented `&lt;select&gt;` of every folder in the library becomes a
modal tree. It asks the node for nothing: the tree is derived from paths the
client already holds, so it shows exactly what the group's index contains and
adds no folder-browsing protocol. For Chat's attachment folder — the one
directory that is written to rather than read — read-only roots are greyed
out, so the node's refusal arrives before the operator picks rather than when
somebody sends a file.

Videos and Music take a list of folders. A library on two drives could not be
described before; the only recourse was pointing the app at a parent containing
both, which pulls in everything else under it. The scalar shapes survive on the
wire alone, for a node speaking MNP 1.0, and the client reads them as a
one-element list.

Two things the tests caught that I would not have:

`test_asset_versioning` — six new modules were missing from `_ASSETS`. Reached
through the registry rather than imported by name, they are exactly the files
nothing else would notice changing, and a stale one is served from cache with
no version bump.

And `node --check foo.js` does **not** reliably report a module syntax error:
it accepted `${/* ... */''}` — htm template syntax pasted into a plain object
literal — and reported success. A `.mjs` copy forces the module parser and
reports it. The suite had no syntax check at all, which is how that reached a
file; `test_spa_syntax.py` does it for every module now, and pins that the
loose path is not what it uses.

Suite: 12 failures, all pre-existing.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
</content>
</entry>
<entry>
<title>feat: groups refactor Phase 1 — root RO/RW model + shared directories UI</title>
<updated>2026-09-06T14:05:39Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-06T14:05:39Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7'/>
<id>urn:sha1:e76e27868b30a2b00b1ba42dd8e7ee6071e0c0d7</id>
<content type='text'>
Replace the upload boolean with per-root writable/removable/ejected flags.
Backend: new ops (update_root, eject_root, plug_root), MNP 1.1 protocol
messages, live RootSet updates so API always reflects current state, CLI
root subcommand (add/remove/set/list/eject/plug).
Frontend: SharedDirectoriesTable with optimistic toggle switches, eject/plug
in Files and Settings, upload gated on root.writable, ejected-root filtering
in all media apps, updated Create Group wizard, 10-locale i18n.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(hub): merge duplicate sources in Search's Music and Photos too</title>
<updated>2026-09-02T14:21:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T14:21:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=10f8266e7152d7dc38dbfe2449327829bf020ad1'/>
<id>urn:sha1:10f8266e7152d7dc38dbfe2449327829bf020ad1</id>
<content type='text'>
Phases 5-8 of docs/refactoring-search.md, extending the Videos merge
outward. A library shared by two groups now lists each track once inside
an album and each photo once inside a photo album, and a card served by
several groups says "N sources" instead of naming one of them.

Units come from each application's own grouping, never a copy of its
keys. For Music that meant exporting foldKey: groupMusicEntries folds
case to group but keeps the first-seen spelling to display, and which
group is seen first is whichever index arrived first — so keying a unit
on the display strings would let the chosen source change between page
loads.

A group whose connection fails is marked down and stops being chosen, so
a unit fails over to another group that has the file. Eviction is not a
failure. Every source being down still yields an entry: a tile that
fails to load beats a film that vanished from the grid.

sourceLabel now takes the whole unit rather than one entry. A show's
poster entry is picked for its thumbnail, so a show in two groups whose
cover episode sits in only one of them would have claimed a single
source. SourceTag lives in group-name.js — source-merge.js must keep
importing nothing (its test executes it standalone), and a copy in each
of the three apps is three chances to disagree.

test_search_files_unmerged.py holds the one thing that must not change:
the Files explorer is not merged, because there each group is a folder
and merging would remove a file from one of them. It also asserts the
other three lists are merged, or deleting the merge outright would leave
it passing and saying nothing.

One plan item was dropped as wrong rather than built: the Music queue in
onPreview needed no change. It filters by groupId and is reachable only
from FilesPanel, which is not merged.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01AbwJDbNTkiRUh7HTWEoyss
</content>
</entry>
<entry>
<title>fix(hub): a show opens on its first season, not its first thumbnail</title>
<updated>2026-09-02T08:00:23Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T08:00:23Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9f6797bfc4d8b55373cc6a957969f03443b5b29d'/>
<id>urn:sha1:9f6797bfc4d8b55373cc6a957969f03443b5b29d</id>
<content type='text'>
Reported live: a show with a dozen seasons opened on season 6. Every season
was in the picker and none was missing — the default was wrong.

VideoDetailModal took it from `repEntry.season`. `repEntry` is the show's
"representative entry", which the poster grid picks as
`episodes.find((e) =&gt; e.thumb_hash) || episodes[0]`: the first episode that
has a thumbnail, so the card has a fallback frame when TMDB has no poster.
That is from the original Videos commit; the season tabs came later and read
the same entry as "the episode the reader is looking at", which it never was
on that path. Episodes are sorted by (season, episode), so a show whose first
five seasons had no thumbnail yet — a partial enrichment pass, or ffmpeg
failing on those files — hands back a season-6 episode.

`defaultSeason(show)` reads the season list and nothing else: the lowest
season present, specials only when there is nothing else, and the lowest
*number* rather than the first entry so it does not quietly depend on
buildSeasons keeping its sort. The effect's dependency on repEntry goes with
it — nothing in it reads that any more.

test_video_default_season.py runs the function in node. No input it takes can
carry a thumbnail, which is the point. docs/mediacenter.md §10.5.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
</content>
</entry>
</feed>
