<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/platform.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-08T01:14:34Z</updated>
<entry>
<title>feat(client): create the system tray at launch, not on first minimise</title>
<updated>2026-09-08T01:14:34Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T01:14:34Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8b0f4ba3bc5fc146bad4730b1ccdcd5be55f192d'/>
<id>urn:sha1:8b0f4ba3bc5fc146bad4730b1ccdcd5be55f192d</id>
<content type='text'>
ensureTray() was reachable only from the window:minimize-to-tray handler, so
the indicator did not exist until you had already hidden the window into it.
That is backwards on both desktops — most of what a tray is for is finding an
application that is not in front of you — and on Windows it read as the app
having no tray presence at all.

Created during app.whenReady(), after registerBridge() and before
createWindow(). The order matters: buildTrayMenu reads the nodeService that
registerBridge assigns, so the other way round puts the Start/Stop entry on
the menu one five-second poll late.

The menu's labels were the one thing that came *from* the minimise call, since
the main process has no i18n. A new tray:labels IPC (platform.setTrayLabels)
carries them instead, sent from the renderer's boot once initLocale() has a
catalogue; a language change reloads the page, so the same call covers it. The
window between launch and that first message shows TRAY_FALLBACK, in English.

§5.10's two platform gates become one — trayOS() in main.js, which every tray
path calls. test_desktop_shell.py's existing test is rewritten against it and
two are added: the launch ordering, and that no tray path tests
process.platform inline instead of calling the gate.

Windows still files a new tray icon under hidden icons until the person drags
it onto the taskbar. No API promotes it; documented in WINDOWS-PORT.md §5.11
rather than worked around.

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): minimise to a system tray indicator (GNOME)</title>
<updated>2026-09-05T11:32:26Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T16:10:39Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=89ed51e9b7ea38bd4475ce3a234f8c783b3d9b09'/>
<id>urn:sha1:89ed51e9b7ea38bd4475ce3a234f8c783b3d9b09</id>
<content type='text'>
A dedicated monochrome button in the nav, immediately left of the notification
bell, hides the window to a tray indicator. Linux only for now; Windows is
being done on that OS, and the capability is declared per platform so the
button never appears where the desktop shows no indicator -- there it would
hide the window for good.

Hides, never closes: `window-all-closed` quits the app, so closing here would
make "minimise" mean "exit" and drop the session, the transfers and the node
connection. `second-instance` now calls the same restore path, since focusing
a hidden window does nothing visible.

The context menu is not decoration. Under libappindicator -- how GNOME shows a
tray at all, via the AppIndicator extension -- `tray.on('click')` never fires;
the indicator only opens its menu. A tray whose sole affordance was a click
would be inert on the one desktop this targets. The click handler is kept for
desktops that do send it.

Menu labels come from the renderer with the IPC call: the locale files are the
interface's, the main process has no i18n, and a second string table is how two
of them start disagreeing. English fallbacks if none arrive.

The icon lives in src/, not build/: package.json `files` packages only `src/**`
and `ui/**`, so an icon under build/ is present in a dev run and missing from
every installed one. Monochrome, stroked, matching the nav glyph.

Verified on this host: Ubuntu GNOME with ubuntu-appindicators@ubuntu.com and
libayatana-appindicator3 present, so the indicator has somewhere to appear.
Not yet run end to end.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01DtfG7z6wHWj8RKHCvxQtY1
</content>
</entry>
<entry>
<title>feat(client): a Node-page toggle to switch into/out of service mode</title>
<updated>2026-09-05T07:28:03Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-05T07:28:03Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=7601991ccb1d75637c055062c38b1852eeef9700'/>
<id>urn:sha1:7601991ccb1d75637c055062c38b1852eeef9700</id>
<content type='text'>
The installer's own mode question is effectively one-shot: customInstall
skips it entirely once the firewall rules already exist, for any reason --
and per-user mode sets those up on its own, with no Scheduled Task involved.
So declining once (or the rules existing from something unrelated, as
happened on a dev machine this session) was a dead end: no reinstall, repair,
or uninstall/reinstall cycle could ever bring the question back, since
uninstall defaults to leaving both alone.

Add the other door in (and out): a checkbox on the Node page, next to the
existing per-user autostart toggle, wired main.js -&gt; preload.js -&gt; platform.js
-&gt; node-page.js. It runs packaging/win/service-mode.ps1 -- the exact script
installer.nsh already runs -- via one Start-Process -Verb RunAs elevation, so
the two paths can never disagree about what service mode means. The elevation
helper writes a tiny param()-based .ps1 to %TEMP% so the target script path
and its arguments bind through real PowerShell parameters instead of nested
string-quoting.

Also fixes a real pre-existing gap found while checking this: 8 of the 10
locale catalogues (all but en/fr) were missing the autostart/service-mode
keys added in an earlier commit this session (b782886) -- test_locales.py's
key-set-parity check uses a for-loop with an inline assert, so it stopped at
the first mismatch (fr) and never actually reached the other eight. Backfilled
all five keys (three pre-existing, two new) in de/es/it/ja/nl/pl/pt-BR/zh-CN.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>fix(client): Stop/Restart buttons and an autostart toggle on the Node page</title>
<updated>2026-09-04T14:50:32Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T14:50:32Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=13d145253a871ef47ef4344f90566eea21b994ab'/>
<id>urn:sha1:13d145253a871ef47ef4344f90566eea21b994ab</id>
<content type='text'>
Root cause of both reports: node:service-status's `installed` field was
winAutostartInstalled() -- whether the Startup-folder launcher exists --
not whether the daemon can be managed at all. The Node page gates
Stop/Restart on `installed`, so with no autostart configured (the default
-- nothing installs it automatically) those buttons silently never
rendered, leaving only Start. A perfectly manageable daemon looked
unmanageable because a different, unrelated setting was off.

installed now reflects the actual daemon binary (findNodeBinary()), same
definition node:installed already used; autostart moves to its own field
carrying what installed used to mean.

That field also fixes the other half: there was no way to turn autostart
on except the CLI. NodeServicePanel now shows a toggle-switch next to
Start/Stop/Restart, wired through a new platform.node.autostart
(install/remove) that mirrors the existing service.* pattern -- the
preload/main.js bridge (W3) was already there, just never called from the
interface. English and French strings; other locales fall back to English
per the project's own stated policy (test_locales.py's own docstring).

Verified: node --check on every edited file; hub/node suites green
(863 pass, 0 fail — pre-existing test_locales.py encoding failures on
this Windows checkout are unrelated, reproduced identically on the
clean tree).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<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>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>feat(node): systemd service panel on the Node page, and a clean CLI restart</title>
<updated>2026-08-22T16:26:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-22T16:26:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da'/>
<id>urn:sha1:8dc11dc05a35a5d64ba4d2c892ccc01c7bfae3da</id>
<content type='text'>
Add a status panel at the top of the Node page — always visible, even
before an MNP connection exists — showing the meshbay-node systemd
unit's own state (via `systemctl --user show`, main process only) with
Start/Stop/Restart controls. This is the piece the rest of the page
cannot provide: it has to work while the daemon is stopped or crash-
looping, which the MNP-based sections require the daemon to already
answer.

While touching node lifecycle: `reload` and `restart-daemon` in the
CLI shelled out to pgrep + SIGTERM/SIGHUP and respawned the process by
hand, logging to a hardcoded /tmp path. That pattern already SIGHUPed
a developer's own running node by accident once (see the old
test_cli_dispatch.py comment). Both now delegate to
`systemctl --user reload|restart meshbay-node`, which the unit already
supports correctly (ExecReload=, Restart=on-failure).

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
</content>
</entry>
<entry>
<title>feat: node:start auto-provisions config and unlock key</title>
<updated>2026-08-22T10:29:32Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-22T10:29:32Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6d1ece3156c3ee7ce3fe1c19aabeb45b34d6436a'/>
<id>urn:sha1:6d1ece3156c3ee7ce3fe1c19aabeb45b34d6436a</id>
<content type='text'>
When the wizard calls node:start with {hubUrl, username}, the handler
writes a minimal node.toml and a random unlock.key if they don't exist.
The daemon then creates the keystore on first start using the unlock
file — fully non-interactive.

Existing configs are left untouched: if node.toml or unlock.key already
exist, provisioning is skipped and the node starts as before.

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: D.6 first-run wizard — detect, start, link, group, gek-init, pair</title>
<updated>2026-08-21T15:50:08Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-21T15:50:08Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=73490d331179cb640828b8625abb75208cd7ae5f'/>
<id>urn:sha1:73490d331179cb640828b8625abb75208cd7ae5f</id>
<content type='text'>
Desktop client guides new users through the full onboarding sequence
without a terminal: detect local node → start daemon (systemd with
direct-start fallback for dev) → link node key to hub → create group →
attach directories → gek-init → operator pair.

- node:detect returns configured field to distinguish missing vs stopped
- node:start tries systemctl first, checks is-active, falls back to
  spawning the binary directly when the unit crashes (dev mode)
- probeNode() extracts the shared config→token→fetch pattern
- SetupWelcome on empty HomePage links to /create-group
- CreateGroupWizard step 0 handles node detection and start
- platform.js exposes node.installed() and node.start()
- 12 setup.* i18n keys added to all 10 locales
- Integration test for node:start fallback logic

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat: LAN Wi-Fi casting to Chromecast via local HTTP relay</title>
<updated>2026-08-21T11:52:11Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-21T11:52:11Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8730281d739d9ed1d6f2d366772582eea8ba0294'/>
<id>urn:sha1:8730281d739d9ed1d6f2d366772582eea8ba0294</id>
<content type='text'>
Re-serve decrypted fMP4 video over HTTP on the LAN so a Chromecast can
play the stream. The relay runs in the Electron main process — same trust
boundary as downloads and MSE playback.

- cast-relay.js: HTTP server with BoxAccumulator (reassembles WebRTC
  chunks into moof+mdat pairs), ring buffer, backpressure, finish() for
  clean end-of-stream, fixed port range 19550-19553
- cast-chromecast.js: mDNS discovery (bonjour-service) + CASTV2 protocol
  (castv2-client), connect/reload/disconnect lifecycle
- Seek-aware: relay restarts on every seek, Chromecast reloads new URL;
  generation counter prevents stale async errors from killing active
  restarts; landingPlayheadRef suppresses programmatic seeking events
- Device picker in video top bar with scan, device selection, copy-URL
  fallback, and cast status indicator
- IPC bridge (main/preload/platform) for start/push/stop/finish/status/
  discover/chromecastConnect/chromecastReload/chromecastDisconnect
- Phase 3 design doc for DLNA/Smart TV in docs/cast-smart-tv.md

Co-Authored-By: Claude Opus 4.6 &lt;noreply@anthropic.com&gt;
</content>
</entry>
</feed>
