<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/video-app.js, branch 0.13</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.13</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.13'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-09-10T15:29:50Z</updated>
<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>
<entry>
<title>fix(hub): the show detail modal must not move when the season does</title>
<updated>2026-09-02T07:49:41Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T07:49:41Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9e3dfcb8229e0cb3d8e296acd09cf5e2acb9565e'/>
<id>urn:sha1:9e3dfcb8229e0cb3d8e296acd09cf5e2acb9565e</id>
<content type='text'>
The previous pass fixed the synopsis and the cast, and the dialog still
jumped: the episode count moves things a fixed-height synopsis cannot reach.

- The body scrolled as a whole, so a thirteen-episode season pushed the modal
  to its max-height where a six-episode one had not. `.video-overlay` centres
  its child, so the taller modal also *started higher up the screen* — title
  bar, close button and all. `.video-detail-steady` (a multi-season show only)
  gives the modal a height rather than a max-height, makes the body a flex
  column, and hands the leftover to the episode list as the one scrolling
  part. A constant-height box is centred in the same place every time, so both
  halves settle at once.
- A scrolling season draws a scrollbar where a non-scrolling one draws none,
  which is a scrollbar's width of content and re-wrapped the file path above
  it, shifting everything below by a line. `scrollbar-gutter: stable`.
- The season panel was clipped by the modal's own `overflow: hidden` whenever
  the seasons outran the room under the picker — at a 740px viewport it wanted
  320px and had 288, and the rest sat where no scroll could reach it. It is
  `position: fixed` now, placed by `placeSeasonPanel()`, which takes the
  trigger's rect and the window height, picks whichever side has more room,
  and caps the panel to it.

Scoped to multi-season shows throughout: a movie has no season to switch to
and a fixed height would buy it nothing but empty space.

test_video_detail_measured.py now builds each block inside a real
`.video-overlay`, since the centring is half the defect, and asserts the modal
top and height as well as the picker's offset — for a long and a short
synopsis and for a six- and a twenty-four-episode season.
test_season_panel_placement.py runs placeSeasonPanel() in node over a rect and
a window height. Two guards are declarations rather than rectangles and say so
in their docstrings: headless Chrome gives the probe zero-width overlay
scrollbars, so the gutter cannot be measured there.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
</content>
</entry>
<entry>
<title>fix(hub): steady the show detail modal, and give a series its director</title>
<updated>2026-09-02T01:23:46Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-02T01:13:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=12b6dc4dd3e009f2e844d87181800aa12d07a3a6'/>
<id>urn:sha1:12b6dc4dd3e009f2e844d87181800aa12d07a3a6</id>
<content type='text'>
Opening a different season of the same show moved everything under the
synopsis, which is where the season control and the episode list are, so the
thing just clicked was no longer under the pointer.

- The synopsis is exactly three lines for a multi-season show, with a "read
  more" link floated into the third line box (-webkit-line-clamp only ever
  puts its ellipsis at the end of the last line and leaves no room after it).
  Clamped from above and pinned from below to the same number: a constant,
  not a range — a season summary runs two lines and the next one twelve, and
  a band still reads as a jump. Whether three lines is all of it depends on
  the modal's width, so it is measured in the browser and re-measured on a
  resize.
- The cast is clamped to two lines.
- SeasonMenu replaces SeasonTabs: the tab row scrolled sideways once a show
  had more seasons than fit, which is close to unusable on a phone. One
  trigger reading "Season 5 · 1997" and a menu of every season with its
  episode count, one row high whatever the season count.
- media_meta_resp.director was filled from the credits crew's job ==
  "Director", a movie shape. TMDB's aggregate tv_credits crew is routinely
  empty and never carries that job, so every show answered null and the modal
  dropped the line. It now comes from created_by on the show details. Cached
  show metadata keeps its null until TMDB_META_TTL_SECS expires or an
  operator re-matches.

The facts line is joined rather than concatenated (a title with no rating
used to open with " · ") and carries the show's own year next to the
director; the selected season's air year moved onto the picker.

test_video_detail_measured.py asserts rectangles through layout_probe.py, not
declarations: the picker's offset inside its own modal body is the same pixel
either way, the synopsis and cast heights, where the read-more link lands,
and the open menu at 320 px. Each measured block sits in a whole-pixel-height
container, or two identical layouts an eighth of a pixel apart round to tops
one pixel apart. test_tmdb_show_director.py covers the credit.
docs/mediacenter.md §10.4.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
</content>
</entry>
<entry>
<title>fix(hub): stop Search-view video posters flickering to spinners</title>
<updated>2026-09-01T12:48:13Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-01T12:48:13Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b83803a91753b38435eb4d3be2e683daae9b4de9'/>
<id>urn:sha1:b83803a91753b38435eb4d3be2e683daae9b4de9</id>
<content type='text'>
The cross-group "Search files" view caps its WebRTC connection pool at
MAX_POOL_SIZE but pre-connected every indexed group, so any account with
more groups than the cap thrashed the pool. An evicted transport was
never removed from SearchPage's own groupConns map, so every tile of that
group kept a `_tRef` pointing at a closed transport; MediaThumb and
useMediaMeta bail on a disconnected transport with no retry, so posters
rendered for a moment then fell back to a loading spinner for good. Every
connect also fired the module-wide bumpMediaMetaGeneration(), clearing
every mounted tile's metadata across all groups and flickering the whole
grid through the warm-up walk. A one-group account (grenet) never hit it;
a many-group account (cbesson) always did.

- ConnectionPool takes an onEvict callback; SearchPage prunes groupConns
  and rebuilds the entry lists when a connection is evicted or closed.
- MAX_POOL_SIZE 3 -&gt; 12; the pre-connect walk is capped to it. Groups
  past the cap connect lazily when a tile scrolls into view (onNeedConn).
- connectGroup() no longer fires the module-wide meta/thumb generation
  bumps on a normal connect (kept for operator TMDB override/rematch).
  Per-group gen counter + concurrent-caller guard: a group's tiles
  refetch once per (re)connect, not once per mounting tile.
- MediaThumb/useMediaMeta take an optional reloadKey; each group's
  `_connGen` is threaded through so a tile refetches when its group
  reconnects instead of staying stuck on a spinner.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01W4Yj8EuXkjjYxbeS5kPd3U
</content>
</entry>
<entry>
<title>chore: scrub copyrighted names from tests, comments and docs</title>
<updated>2026-08-30T15:05:15Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-30T15:05:15Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=d5fc45ff907e618d884a8d2d91c1f2b45e6898d1'/>
<id>urn:sha1:d5fc45ff907e618d884a8d2d91c1f2b45e6898d1</id>
<content type='text'>
Real franchise / show / release-group names had crept back into test
fixtures, code comments, a docstring and docs/mediacenter.md while fixing
the saga-match and misclassification bugs. Replace them all with invented
placeholders ("Some Saga", "A Different Show") and shape descriptions
("a franchise-origin film", "a 3-season show"). Behaviour and assertions
unchanged; 738 node tests still pass.

Record the rule in CLAUDE.md so it stops recurring.

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