<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-client/src/main.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-09T13:48:53Z</updated>
<entry>
<title>fix: the PDF preview needs object-src and frame-src, in both policies</title>
<updated>2026-09-09T13:48:53Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T13:48:53Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=56776934c2ddfbad4884b252da6f5fb25864b8db'/>
<id>urn:sha1:56776934c2ddfbad4884b252da6f5fb25864b8db</id>
<content type='text'>
A PDF preview showed the "this browser will not display the PDF inline"
fallback everywhere — in the desktop client since its first launch, and in
the browser since the hub started sending a CSP on 2026-09-01. It read as a
missing native feature because before that commit the hub sent no policy at
all, so Chrome had once worked and the application never had.

Two directives govern one feature. `files-app.js` decrypts the file in the
page and hands it to `&lt;object type="application/pdf"&gt;` from a Blob; Chromium
loads that as plugin data (`object-src`, absent and therefore falling back to
`default-src 'none'`) and then renders it in an internal frame (`frame-src`).
Opening either alone changes nothing visible — the second refusal produces the
same fallback. `'self'` covers neither: a same-origin `blob:` URL is not
matched by it in either directive, measured in Chrome 152 against the deployed
page and in Electron 44 against the client's own policy.

`plugins` stays at its default `false`: the built-in viewer is not behind that
flag on Electron 44, verified by rendering one.

Widening `object-src` from `'none'` to `blob:` admits only what page script
minted itself, at a type this code sets — PDFium parsing bytes that came from
a node, which is what any browser does with the same file once downloaded.

Tests: each policy is pinned to carry `blob:` in both directives (each fails
if either token is removed), and the two policies are now held identical
directive by directive apart from the two deliberate differences — the comment
claiming they were the same had already drifted and nothing checked it. The
CSP source parser in test_desktop_shell.py read `//` comment lines as
directives, which is the "parse directives, not text" mistake this file
already records; it skips them now.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01XauykfBvRrpy6RYbF6F7Wu
</content>
</entry>
<entry>
<title>feat: MNP 3.0 — a transfer needs a lease</title>
<updated>2026-09-09T12:00:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T12:00:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=53ea44cb03ef6f8d941f6c8c9446551b0c5cd1ac'/>
<id>urn:sha1:53ea44cb03ef6f8d941f6c8c9446551b0c5cd1ac</id>
<content type='text'>
Stage 4 of ~/next/improve-downloads.md, the flag day. Leases become compulsory
and a 2.x peer is refused at the handshake.

**The bound on leaseless reads (§3.4.1) did not exist, and it is what makes the
rest mean anything.** Browsing a group is never subject to a transfer slot —
that is an operator decision and a requirement: a member must be able to browse
a group at capacity exactly as they browse an idle one. But "not leased" cannot
mean "unbounded", or a client that simply omits `tr` transfers outside every cap
and the caps are decoration. A session may now read two distinct files at once
without a lease: one because a viewer looks at one file, two so that prefetching
the next photo stays possible. A count of files and not a byte budget, because a
RAW photo is 60-80 MB and is browsing while a 40 MB archive is a download, and
no size threshold separates them. Thumbnails, posters and cover art never reach
this check at all — they resolve out of the node's own cache.

It is a fairness control among cooperating clients, in the company of
`max_concurrent_streams`, and is not a defence against a member determined to
saturate a node's disk. That member is a member, and the answer to them is
`member revoke`.

**MNP_VERSION and MNP_MIN_SUPPORTED both move to 3.0**, on both sides. The
messages are additive; the requirement is not. An opt-in switch would leave a
leaseless branch reachable on every node, which is finding C6's lesson — a
transport that accepted a bare JWT — one feature later.

**The desktop client now checks before it connects.** The SPA is served by the
hub and picks up a new client on reload; the application ships its own
interface, so an un-updated one would sign in, list groups, and fail every
connection with `version_too_old` — a refusal in a protocol vocabulary with
nothing anyone can act on. It asks `/v1/hub/version` for `client.minimum` and
says so plainly instead. An unreachable hub is deliberately *not* "too old": a
captive portal or a closed laptop must not make starting the application
impossible.

**Every package is aligned on 0.13.0.** `meshbay-client/package.json` had
drifted to 1.0.0 while the Python packages were on 0.12.0 — invisible until
something compared those numbers, and then load-bearing: an installed client
announcing 1.0.0 sorts above a 0.13.0 minimum and walks through the gate meant
to stop it. That is stated in the code rather than left to be rediscovered; it
is acceptable exactly once, because the operator is updating every client, node
and hub by hand for this flag day. A new test fails if two packages ever
disagree again, and another fails if the hub would refuse the client the tree
builds.

Node suite 1209 passed, hub suite 861 passed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(client): write a download to .part and rename it when it completes</title>
<updated>2026-09-08T11:21:02Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T11:21:02Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3c3ccf75edc007936d7c6e2d72b4ff289a5a97df'/>
<id>urn:sha1:3c3ccf75edc007936d7c6e2d72b4ff289a5a97df</id>
<content type='text'>
`save:abort` deleted a cancelled download, but nothing covered the application
being quit, killed or crashing mid-transfer: the write stream was abandoned and
a truncated file kept the final name — the exact thing save:abort's own comment
calls worse than no file at all, because it looks complete to whoever opens it
next.

Downloads go to `&lt;target&gt;.part` and are renamed after the stream has flushed,
which is the convention the node already uses for uploads (`_do_file_upload`).
A crash now leaves a self-evidently unfinished file. `before-quit` also clears
any `.part` still open, synchronously — it does not wait for promises — so a
deliberate quit leaves nothing at all.

Verified by hand: .part during the transfer, survives SIGKILL, renamed on
completion, and gone after a cancel or a clean quit.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<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>fix(client): break the Wayland ready-to-show deadlock</title>
<updated>2026-09-07T13:01:12Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-07T13:01:12Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=8883d60d0afa2ed9dd1ef68bc21fe1b9a65a59ff'/>
<id>urn:sha1:8883d60d0afa2ed9dd1ef68bc21fe1b9a65a59ff</id>
<content type='text'>
Some Wayland compositors never schedule a first paint for an unmapped
surface, but the window stays unmapped until show() runs, which was
gated entirely on that paint's ready-to-show event — a cycle with no
way out on its own. Observed under GNOME/Mutter on a VM whose
virtio-gpu device fails command-buffer creation. Add a bounded
fallback show(), guarded on isVisible() so it's a no-op once the
event has already fired normally and doesn't steal focus back from
whatever the person switched to.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01CHKaCz2gq3ya8t13CvQ7Hp
</content>
</entry>
<entry>
<title>fix(win): finish the desktop setup flow — node-key link + service task</title>
<updated>2026-09-05T17:48:10Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-05T17:48:10Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=fff1974edf19cf1186e0f49da5f8a4d237bcb13e'/>
<id>urn:sha1:fff1974edf19cf1186e0f49da5f8a4d237bcb13e</id>
<content type='text'>
Two independent breaks in the Windows first-run path:

- node:start's win32 branch never linked the node's Ed25519 key to the hub
  account, so the daemon sat at waiting_for_account and the Create Group
  wizard span on "Detecting local node…" for ever — the only way through
  was pasting the key by hand on the Profile page. The Linux branch has
  always done this inline; factor it into linkNodeKeyAndAwaitRunning() and
  call it from win32 too. PUT /v1/users/me/node_key overwrites, so this
  also recovers an account still carrying a previous machine's node key.

- service.ps1's install branch did `$action = New-ScheduledTaskAction`,
  shadowing its own [ValidateSet(...)][string]$Action parameter (PowerShell
  variable names are case-insensitive). The CimInstance was coerced to the
  string "MSFT_TaskExecAction", Register-ScheduledTask -Action rejected it,
  and "background service" mode never created the task — reproduced live.
  Rename the locals to $taskAction / $bootTrigger / $taskPrincipal.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): system tray on Windows, and a clearer tray icon</title>
<updated>2026-09-05T13:26:25Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-05T13:26:25Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=bd299df3468822e7b545a90bf55ffbc1bac87f58'/>
<id>urn:sha1:bd299df3468822e7b545a90bf55ffbc1bac87f58</id>
<content type='text'>
The tray's Start/Stop already drove nodeService.status/stop/restart, which
had full win32 branches for both startup modes from the Node page work --
so enabling it on Windows is widening two platform gates (the `tray`
capability in preload.js, the window:minimize-to-tray handler in main.js),
not new logic.

Replaced the tray icon: the previous white chevron-in-a-box read as an
envelope at tray size. New icon is a small "M" drawn as mesh nodes and
edges, echoing the app icon's own motif, in the brand blue instead of
plain white so it stays legible on both light and dark taskbars/panels.

Co-Authored-By: Claude Sonnet 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(client): tray menu starts and stops the node, and a clearer icon</title>
<updated>2026-09-05T11:32:26Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-04T22:15:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b3362e6b25bc77dfe94f4c088a865e6fd2be2601'/>
<id>urn:sha1:b3362e6b25bc77dfe94f4c088a865e6fd2be2601</id>
<content type='text'>
The first icon was an arrow dropping into a receptacle, which is the download
glyph -- a vertical stem above a container reads that way whatever the context.
Replaced with a window folding a chevron into itself: no stem, and the frame
says which object is being minimised. Applied to both the nav button and the
panel indicator, which carried the same wrong shape.

The menu now offers Start or Stop for the node daemon, chosen from its actual
state and shown only when there is a daemon to act on: `supported &amp;&amp; installed`,
so a machine with no node installed gets no entry rather than a control that
fails when used. `restart` is the start verb -- systemd's restart starts a
stopped unit, and there is no separate one to call.

The three service handlers become named functions so the tray drives exactly
what the Node page drives, instead of a second copy of the systemctl and Task
Scheduler branches. A read of the state that throws is treated as no control
at all.

The menu is rebuilt on a 5s timer while an indicator exists, and again straight
after an action. libappindicator has no "menu is about to open" event, so a menu
built once would show a stale Start/Stop for the life of the process; `systemctl
--user show` costs a few milliseconds.

Locales: tray.start_node / tray.stop_node in all ten.

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): 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>fix(win): graceful shutdown, one startup-mode control, and a stray-\r bug</title>
<updated>2026-09-05T11:06:55Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-05T11:06:55Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e89a57bb97b5a0d624e8d490b6b8aa38ba140817'/>
<id>urn:sha1:e89a57bb97b5a0d624e8d490b6b8aa38ba140817</id>
<content type='text'>
Windows-only changes, all found by actually running the previous session's
work rather than by review alone:

- CTRL_CLOSE_EVENT/LOGOFF/SHUTDOWN handler (platform.py, ctypes
  SetConsoleCtrlHandler) so closing a console window, signing off, or a
  system shutdown runs the daemon's real _shutdown() instead of Windows
  just ending the process — closing WebRTC sessions and any in-flight
  ffmpeg transcode instead of orphaning it. `taskkill /F` itself stays
  uncatchable (like SIGKILL), so autostart_run() now spawns with
  CREATE_NEW_PROCESS_GROUP instead of DETACHED_PROCESS and autostart_end()
  tries CTRL_BREAK_EVENT against the recorded pid first, falling back to
  the hard kill only if that doesn't stop it in time.

- Replaced the Node page's two independent autostart/service-mode toggles
  with one "start automatically" select (off / at sign-in / as a
  background service). The old pair let both be active at once — starting
  the daemon twice, at boot and at sign-in — and their layout broke
  wrapping inside .node-service's flex row. The new control always removes
  whichever mechanism is active before installing the target; platform.py's
  service_install() does the same on the CLI side. The "background
  service" option disables itself (with a hint pointing at the CLI) when
  running unpackaged, since service-mode.ps1/service.ps1/firewall.ps1 all
  assume an installed build's layout — verified live rather than assumed
  by actually running those scripts unelevated.

- findNodeBinary() no longer bakes a stray \r into resolved paths. Found by
  rebooting after enabling per-user autostart: where.exe listed two
  matches, and stdout.trim().split('\n')[0] only strips the whole string's
  ends, leaving line one's own trailing \r attached — which landed inside
  the Startup .vbs's quoted path and broke it with "Unterminated string
  constant" at boot. Fixed by splitting on \r?\n and trimming every line.

- Dependency audit for the Windows installer (docs/WINDOWS-PORT.md): no
  VC++ Redistributable needed, confirmed by inspecting the built
  node-runtime's actual import table rather than assuming. New
  docs/windows-build.md: a concise clone-to-installer build guide.

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