<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-node/src/meshbay_node/indexer/indexer.py, branch 0.11</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.11</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.11'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-25T16:18:48Z</updated>
<entry>
<title>fix(hub,node): Create Group wizard silently skipped apps, and lost track of scanning progress</title>
<updated>2026-08-25T16:18:48Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-25T16:18:48Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3c44f55f6b0aba77c7ad57d0a5ebe3e55b409473'/>
<id>urn:sha1:3c44f55f6b0aba77c7ad57d0a5ebe3e55b409473</id>
<content type='text'>
Two real-world bugs found together while testing multi-root group
creation:

- CreateGroupWizard only sent the enabled-apps PUT when the operator had
  *unchecked* something, assuming "every box left checked" already matched
  the node's own default (Roster.DEFAULT_APPS = chat, files). It doesn't —
  so leaving every app checked, the common case, silently left
  Videos/Music/Photos disabled on the node. Now sent unconditionally.

- The wizard's "add extra roots" step never polled index-status, so once
  step 3 (which only watches the first/upload root) finished, the
  progress bar froze while the node kept scanning the remaining roots for
  minutes, unwatched. Added waitForRootsIndexed (platform.js), mirroring
  waitForGroupHosted's own race handling.

That fix exposed a deeper one: indexer.py's _scan_root() only flipped
`progress.scanning` on *after* walking the directory and stat()-ing every
file — both off-loop, but slow enough on a large root that a poller's
grace period (waitForRootsIndexed's 5s) could expire before ever
observing `scanning: true` (confirmed against production logs: a GEK-init
step fired 5.058s after a root started scanning, matching the grace
period almost exactly). The stat() pass was also a synchronous loop
directly on the asyncio event loop — blocking the whole daemon (WebRTC,
chat, admin UI) for as long as it took on a root with many files. Both
fixed: `scanning` now flips on before the walk starts, and stat()-ing is
now off-loop too (_size_files).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_013XSohfUQQiaE77qyFLgSv3
</content>
</entry>
<entry>
<title>fix(node): skip indexing audio files under 50KB, likely-corrupt source</title>
<updated>2026-08-24T18:55:52Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T18:55:52Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=75d1f8b93dfa0bffda3a59a6d143b06dcc3ca67f'/>
<id>urn:sha1:75d1f8b93dfa0bffda3a59a6d143b06dcc3ca67f</id>
<content type='text'>
The "P.H. Theme" failure investigated earlier turned out to be a genuinely
corrupt 1256-byte source file with no audio stream at all, just an ID3
tag — a real, if rare, corruption pattern worth guarding against directly
rather than only handling gracefully at playback time. Scoped to audio
only, applied wherever a file actually gets hashed/typed (fresh scan and
the cache-miss rehash path alike) — a tiny file of any other type is still
indexed normally.
</content>
</entry>
<entry>
<title>feat(node): recognize WMA and Musepack as audio, read their real tag keys</title>
<updated>2026-08-24T18:31:04Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-24T18:31:04Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6c56877675958e40ae526e0965266ab68aae44f2'/>
<id>urn:sha1:6c56877675958e40ae526e0965266ab68aae44f2</id>
<content type='text'>
A real-library scan turned up 250 .wma and 23 .mpc files that the indexer
was silently classifying as "other" — genuinely lost from the Music app,
not a consolidation-rule artifact (checked separately: the grouping logic
itself drops nothing). Both are now indexed as audio and tagged properly:

- WMA has no mutagen "easy" wrapper, so the generic tag reader was reading
  nothing from it at all. Reads the real ASF keys directly instead
  (Title/Author/WM-AlbumTitle/WM-TrackNumber), confirmed against a real
  sample file before writing the mapping.
- Musepack's format auto-detection is unreliable enough (misidentified a
  real .mpc as MP3 in spot checks) that it now always opens by its own
  class instead of guessing from content.
- Filters out another placeholder value found along the way: a French
  ripping tool's auto-generated "Album inconnu (&lt;timestamp&gt;)".

Neither format decodes natively in a browser's &lt;audio&gt; element, so this
gets them correctly visible, tagged, and covered — not yet playable
in-browser. That would need server-side transcoding, deliberately left
out of this change.
</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>
<entry>
<title>feat(node): persistent index cache, visible scan progress, adaptive reconcile, and delta sync</title>
<updated>2026-08-23T19:55:20Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-23T19:55:20Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b3709ac4d362987a9d025616c95065ceed0d216b'/>
<id>urn:sha1:b3709ac4d362987a9d025616c95065ceed0d216b</id>
<content type='text'>
Indexer performance work, in four parts:

- Persistent (path, size, mtime) -&gt; hash cache (indexer/cache.py) so a node
  restart no longer re-hashes every file — measured at 23 minutes for a
  114 GB library on a slow disk before this, near-instant after. Hashing
  is deliberately kept sequential (max_workers=1): it was never actually
  concurrent despite the pool size, and two interleaved reads seek-thrash
  a spinning disk instead of going faster.

- Byte-based scan progress (IndexProgress), surfaced via the loopback
  index-status route, the handshake ack, and a periodic INDEX_PROGRESS
  push to connected peers — drives a progress bar in the Create Group
  wizard and "add a directory" in Settings, and an animated presence dot.
  Guaranteed to settle back to idle via try/finally and a final push on
  the scanning-&gt;false transition.

- The reconcile backstop's directory walks now run in the executor
  instead of blocking the daemon's event loop; its interval defaults to
  10 min (was 60s) with adaptive backoff to 2h when nothing changes,
  reset on a real change or a peer connecting, and is now a per-group
  operator setting (signed op + group Settings UI).

- INDEX_DELTA wired up (protocol support existed, nothing called it):
  _on_index_change now sends additions/deletions instead of rebuilding
  the full entries list, coalesced over a short window so a burst of
  file events produces one push, and the hub swarm registration for
  public groups only (re-)registers newly added hashes.

Also fixes several bugs found while testing the above against real
libraries (a 114 GB and a 100+ GB group on a USB HDD):

- /api/reload blocked until the reload — including a brand-new group's
  full initial scan — finished, which the Electron bridge's fixed 30s
  call timeout turned into a hard failure on any real library. The route
  now fires the reload without waiting (ops.start_reload), matching
  add_root/remove_root's existing pattern; the wizard's own step order
  was fixed to wait for the group to actually appear hosted before the
  steps that need it (extra roots, GEK), with retries for the residual
  race between that and the daemon's own bookkeeping.
- transport.js's hand-rolled msgpack codec had no case for uint64/int64
  (0xcf/0xd3) and crashed decoding any message containing one — hit by
  IndexProgress.scanned_bytes/total_bytes for any group over ~4.3 GB.
  Verified against real msgpack-encoded bytes from the Python side.
- chat_hist_resp, and this change's own index_progress and
  set_scan_settings_ack pushes, were not routed by message type and
  could be handed to an unrelated pending request by the transport's
  "oldest pending" fallback, stalling it until its own 30s timeout and
  corrupting whatever received the wrong reply in its place.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
</content>
</entry>
<entry>
<title>fix: first-run wizard reliability and node startup performance</title>
<updated>2026-08-22T14:40:54Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-22T14:40:54Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=9c136a0e37add42f5d0c8675a797969cfe690de0'/>
<id>urn:sha1:9c136a0e37add42f5d0c8675a797969cfe690de0</id>
<content type='text'>
Node daemon no longer blocks startup on slow directory scans — initial
indexing runs in the background so the node reaches "running" immediately
after transports are up. Fixes the wizard failing to detect the node when
large USB/NAS roots take minutes to scan.

Also: wizard key-linking deadlock resolved (main.js links during poll),
invite form stays in DOM during reconnects (disabled instead of destroyed),
pairing code bridges to renderer, and firewall docs for LAN casting added.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node): several named roots per group, and one implementation per operation</title>
<updated>2026-08-18T00:15:02Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-18T00:15:02Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e9d5e979fdab9a1cc3c729d602e6f27207b9480c'/>
<id>urn:sha1:e9d5e979fdab9a1cc3c729d602e6f27207b9480c</id>
<content type='text'>
Stage A — a group's content is a set of named roots
---------------------------------------------------
`shared_dir` becomes a list of {name, path, kind}. The name is the directory's
basename, derived once at add time and *stored*: recomputing it would
re-identify a whole library the day someone renames a folder on disk. Duplicate
names are refused case-insensitively and no root may contain another — both
compared with NFC folding, because most of these directories live on exFAT or
NTFS where `Films` and `films` are one directory.

Every index path carries its root name, in a one-root group as much as in a
five-root one. One path shape has to be got right once; two have to be kept
right for ever.

**A root that goes away freezes; it never empties.** Unmounting a volume makes
watchdog report every file under it as deleted, or presents an empty directory
to the next scan. Acting on either propagates deletions for a whole library to
every member, as though the owner had erased it. So a deletion is acted on only
once its root is confirmed readable, and availability is tracked per root — one
unplugged drive leaves the others serving. 12 tests, verified to fail against an
indexer without the check.

Events are not trusted to be complete either: ReadDirectoryChangesW drops them
under load and inotify on a FUSE mount misses changes made outside it. A
periodic reconciliation sweep is the only thing that recovers a missed event.

MNP 0.2 → 0.3 (additive). The hub needs no change: SwarmSource carries a content
hash, a node id and an endpoint — no paths, no filenames — and private groups
register nothing (H7).

Stage B — one implementation behind every front door
----------------------------------------------------
C1 and C6 were both "a second path into the node with its own weaker
handshake". Two implementations of `revoke` with two authorization checks is
that shape one size down. `meshbay_node/ops.py` holds each operation once,
takes the daemon state, and knows nothing about HTTP, argv or MNP. The loopback
API is one `_op(...)` line per endpoint; the MNP handlers call the same
functions. test_ops.py asserts the shape rather than trusting it.

Phase 14 is finished on top of it — `group list`, `gek init|rotate`, `reload`
(SIGHUP), `denylist show|clear`, `file list|rm`. **No operator action requires a
browser any more.** Plus `gek_rotate` and `member_unpin` as operator-signed MNP
operations: rotation is the half of revocation that revocation cannot do, since
the ex-member holds the current key, and the node generates the replacement
with its own CSPRNG — no key material crosses the wire, which is what the C5b
rule is actually about.

Two bugs found by running it rather than by testing it
------------------------------------------------------
GroupIndex is keyed by **content hash**, so the same bytes at two paths are one
entry — which is also why a scan reports ten files and indexes nine.
Reconciliation compared paths, so it decided the second path was a missed event
every 60 s, rewrote the entry and pushed an index update to every connected
peer. Seen in a live node's log.

`meshbay-node reload` crashed on first use with `subprocess` unimported: the
module compiles fine, which is the "syntax, not names" trap already recorded for
the SPA. test_cli_dispatch.py now walks every verb and refuses to let one be
added to the parser without an entry there.

Also corrected: protocol.py declared a second MNP_VERSION of "0.1" while the
wire carried "0.2" — harmless only because nothing imported it. And
_do_dir_create/_do_dir_delete referenced an undefined `filename` on their error
path.

740 tests pass; QE/deploy/e2e.py passes end to end against the live deployment.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: Phase 9 — Web client SPA with WebRTC P2P transport</title>
<updated>2026-08-11T02:13:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-11T02:13:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e23e33adeaf8ee7439187d4451c856b37816a51f'/>
<id>urn:sha1:e23e33adeaf8ee7439187d4451c856b37816a51f</id>
<content type='text'>
Complete browser-based client: Preact SPA with login, group file browser,
encrypted download, video playback, group chat, i18n, and dark/light theme.
Browser connects P2P to nodes behind residential NAT via WebRTC DataChannel
(aiortc). Hub handles signaling only — all data flows E2E.

Performance: pipelined downloads (8-chunk sliding window), binary msgpack
wire format (no base64), redundant I/O elimination. Large file downloads
stream to disk via File System Access API (showSaveFilePicker).

Validated on SFR + Orange residential NATs, Chrome + Firefox, IPv4/IPv6.
132 tests passing. Deployed to meshbay.org + Orange node.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix: 4 corrections — streaming hash, watchdog bug, cipher doc, deps</title>
<updated>2026-08-09T20:56:18Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-09T20:56:18Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=217b61ff34390fd24d0a2ad338f6094183debbe3'/>
<id>urn:sha1:217b61ff34390fd24d0a2ad338f6094183debbe3</id>
<content type='text'>
1. indexer.py: streaming blake3 (8MB chunks) instead of read_bytes().
   Large files (initrd.img, ISOs, VM images) no longer load into RAM.

2. QE/demo-v1/run_node.py: call indexer.start() not initial_scan().
   initial_scan() alone never starts the watchdog observer — files added
   after startup were silently ignored. Added indexer.stop() on shutdown.

3. USERGUIDE.md §8: clarify symmetric vs asymmetric.
   Ed25519/X25519 = asymmetric (key pairs). ChaCha20-Poly1305 and
   AES-256-GCM = symmetric AEAD 256-bit (content encryption).
   ChaCha20 is PRIMARY; AES-GCM is optional browser-compat variant only.

4. pyproject.toml: aioquic, websockets, aiosqlite, slowapi added to
   proper package deps (were installed manually, now declared).

Co-Authored-By: Claude Sonnet 4.6 (1M context) &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(node): add directory indexer and Mesh Group Index</title>
<updated>2026-08-09T02:08:43Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-09T02:08:43Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=46b6353ebfb57c7fea481a9aac919b7977e3d186'/>
<id>urn:sha1:46b6353ebfb57c7fea481a9aac919b7977e3d186</id>
<content type='text'>
GroupIndex: msgpack→zstd→GEK-encrypt→sign for private groups,
plaintext+sign for public groups. DirectoryIndexer: watchdog-based
watcher, async initial scan via thread pool, on_change callback.
Delta support (diff between versions). 10/10 tests passing.

Co-Authored-By: Claude Sonnet 4.6 (1M context) &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
