<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/packages/meshbay-hub/src/meshbay_hub/static/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-09T12:24:59Z</updated>
<entry>
<title>fix(spa): say why a download cannot be paused</title>
<updated>2026-09-09T12:24:59Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T12:24:59Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=e6f895c473a0b19e7b186889c1836d3945bc880b'/>
<id>urn:sha1:e6f895c473a0b19e7b186889c1836d3945bc880b</id>
<content type='text'>
Reported from Chrome, with a screenshot: four downloads with no pause button
and an upload beside them with one, and nothing anywhere saying why.

The reason is real. Without a granted download folder the browser writes
through the service worker — a download it already owns, which cannot be paused
without stalling it somewhere we can neither see nor resume. An upload writes to
the node, which keeps the position, so it is always pausable. But that was
stated only in a Settings line nobody reads on the way to a download, and a gap
where the row above has a button is not an explanation.

So a download that cannot be paused now shows a dimmed pause icon where the
button would be, carrying the reason and the remedy in its tooltip. Not a
button: there is nothing to click, and a disabled one invites the click anyway.

And only where the advice can be taken. Firefox and Safari have no folder to
choose — the streamed path is the only target they have, which is what §6.5 of
~/next/improve-downloads.md costs out — so telling someone there to choose one
would be advice they cannot follow. Nothing is drawn.

Hub suite 866 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(spa): a paused transfer is not a finished one</title>
<updated>2026-09-09T12:14:11Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T12:14:11Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=b472b4d43149ed12a7f64c94f34c34f436bef492'/>
<id>urn:sha1:b472b4d43149ed12a7f64c94f34c34f436bef492</id>
<content type='text'>
Reported while testing the flag day: pausing an upload put it under "Finished".

"Finished" was defined by exclusion — everything that is not running, queued or
preparing — so it swallowed `paused` the day pausing shipped. A transfer
somebody stopped on purpose then sat beside the ones that are actually over,
offering a resume button in the section of things that cannot be resumed, and
dropped out of the badge, which announced less activity than there was.

Paused is now its own group, in all ten catalogues, and counts as active: it is
not over, the person means to come back to it.

The three filters are lifted out of `app.js` and executed rather than described
in the test, and one case asserts that every status lands in exactly one group
— a state added later that falls into none is a transfer the panel simply does
not show, which is how this one got in.

The same report also said the three running downloads lost their pause buttons
when the upload was paused. That part is **not** explained and **not** fixed:
the store returns `pausable` true and status `running` for all three (new test),
closing an upload lease pumps only the upload queue, and the button's condition
is a pure function of those two. All three say the buttons should have stayed,
so an observation is missing rather than a cause.

Hub suite 864 passed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>feat(spa): pause and resume a download, in session</title>
<updated>2026-09-09T08:48:49Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-09T08:48:49Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=29e93e5e553c94818cd2b4e587b0e54cfe7d8424'/>
<id>urn:sha1:29e93e5e553c94818cd2b4e587b0e54cfe7d8424</id>
<content type='text'>
Stage 7a of ~/next/improve-downloads.md: pausing within a session, on the
targets that can actually do it. Resuming across a reload is 7b.

A paused transfer holds **nothing**. Its slot goes back to the node the moment
it stops and resuming rejoins the queue at the tail, because anything else lets
one member close a node by pausing four downloads and going to lunch. So the
lease is taken inside the run loop rather than before it, and pause is refused
outright for a transfer that could not ask for another one.

Resuming is exact rather than approximate: the pipeline stops between two
chunks and never inside one, so what is on disk is always a whole number of
chunks and `fromChunk` is a verified position. The failure mode being avoided
is a file that looks complete and is quietly corrupt.

The target has to survive it, so a pause no longer reaches the `abort()` that a
failure does -- that would delete Electron's `.part` or the file just created in
the granted folder, leaving nothing to continue. And the in-memory fallback
keeps its accumulated chunks rather than starting a second array.

The button is drawn only where the target says it can. A service-worker stream
says no, in its own code and for its own reasons: the browser is already writing
an HTTP response into its own download folder, not feeding it stalls that
download where we cannot see or resume it, and an idle worker is terminated
within seconds. Firefox and Safari therefore keep cancel and get no pause, which
is the decision recorded in §6.5.

Cancelling a paused transfer ends it. A paused run is parked on a promise;
without waking it the row said "cancelled" over work that had not stopped and a
target that was still open.

Six cases, each checked against the unfixed source. Hub suite 842 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(hub): the transfers row exists from the click</title>
<updated>2026-09-08T21:43:46Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T21:43:46Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=77615ddb5fead3e74751a94847d3bcc99fc0a96d'/>
<id>urn:sha1:77615ddb5fead3e74751a94847d3bcc99fc0a96d</id>
<content type='text'>
Clicking Download produced nothing — no row, no icon, no panel — for as long as
it took to open somewhere to write, and then several rows at once. The streamed
path waits for the worker twice; a Save As dialog waits for a person. The row
was created after that, so the slowest part of a download happened with nothing
on screen to say it had begun.

The store gains a `prepare` step, distinct from `run`, and the order is now:
row, then target, then slot.

That last part is why the obvious fix was wrong. Taking the slot first would let
the row appear immediately, and it was tried this morning: a granted slot has to
be taken up within the node's deadline, opening a target can outlast it, and
three downloads became one. (The diagnosis at the time blamed that ordering for
revocations which were in fact a missing `touch()` call — the revert was right
for the wrong reason.) `makeLease` is called after `prepare` succeeds, never
before.

Three behaviours fall out, each with a test:

  - a dismissed dialog leaves nothing behind. `prepare` returning false drops
    the row: nothing started, so nothing should remain on screen to explain it;
  - the row takes the name the file was actually saved under, once known;
  - a refusal above the memory ceiling fails the row that is already there,
    rather than creating one to kill it.

`preparing` counts as live everywhere — badge, cancel, clearFinished, and
`_busy`, since closing a transport under a preparing transfer strands it exactly
as under a queued one. Six places asked "is this finished?" and were drifting
apart; there is one definition now.

Two mistakes in the tests, worth the note: one counted positions in an output
array by hand and was one out, which reads exactly like a failing assertion
about the code — the values are tagged now, not indexed. And
test_zip_size_limit.py's stub did not run `prepare`, so it no longer reached the
size check the file is about; it now behaves like the real store.

819 hub, 1169 node, 0 failed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>feat(hub): client-side transfer leases and the transfers panel</title>
<updated>2026-09-08T20:54:16Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T20:54:16Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=1a495f5ed3f8a55222d406152c833882264dc377'/>
<id>urn:sha1:1a495f5ed3f8a55222d406152c833882264dc377</id>
<content type='text'>
Steps 5 and 6 of ~/next/improve-downloads.md. The node has handed out slots
since step 2 and nothing asked for one; now the client does, and the panel shows
what is happening.

`transport.openTransfer()` returns a Lease: `acquire()` resolves when the node
grants, `release()` gives it back exactly once, and nothing else in the client
speaks to the node about slots. Whether a node hands out slots is read from the
handshake ack rather than guessed from a timeout — "no answer yet" and "this
node will never answer" are indistinguishable in time, and guessing wrong either
stalls every download or defeats the cap.

Two things exist only because a queue can lie: a watchdog re-asks when a pushed
grant does not arrive (the node is idempotent on `tr`, so asking again is free),
and a grant for a transfer the page has forgotten is handed straight back rather
than held until the node's deadline.

The slot is asked for **after** there is somewhere to write, and that ordering
is load-bearing: opening a target takes thirty seconds of streamed-download
timeouts, or as long as somebody leaves a Save As dialog open, and a grant not
taken up in time is revoked. Moving it earlier looked better and broke three
downloads into one. Pinned by a test.

The panel groups by state — running, waiting, finished — rather than re-sorting a
flat list, so a row moves only when its own state does. The ETA is withheld until
the speed window holds real measurement: a figure from the first two chunks
swings between four seconds and an hour, and people plan around the first number
they see. One live region announces state changes and not progress.

Three silent paths closed on the way: a download refused for want of a user
gesture (a browser grants one file picker per gesture, and downloading three
files is one gesture) now falls back to the streamed path, which needs none; a
click with no connection says so instead of doing nothing at all; and a queued
transfer counts as busy, so a transport is never closed under one that is
waiting for a grant that could then never arrive.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
</content>
</entry>
<entry>
<title>fix(hub): make the streamed download path reliable, and clean up after an abort</title>
<updated>2026-09-08T11:20:29Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T11:20:29Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3f2bb22586d3e1aef765149b555ccc8e174ce7eb'/>
<id>urn:sha1:3f2bb22586d3e1aef765149b555ccc8e174ce7eb</id>
<content type='text'>
On Firefox and Safari the service worker is the only unbounded way to write a
download to disk: neither has the File System Access API, and OPFS is not a
substitute — measured on Firefox 154, its quota is exactly 10% of the volume's
size (389,233,459 bytes of a 3,892,334,592-byte volume, refused to the byte),
which a film exceeds. So when this path declines, a large download has nowhere
left to go, which makes its reliability a correctness property.

Four ways it declined, all of them avoidable:

- it was registered inside the first click on Download, so that click paid
  install, activate and claim while somebody watched a button do nothing;
- `_swReady` cached a null for the life of the page. One slow first click left
  the tab unable to stream anything again, curable only by a reload nobody knew
  to do. Only a successful controller is remembered now;
- control was waited for with a 3 s cap. It is 15 s, and a page that is active
  but not controlled asks the worker to claim again (`mbdl-claim`) instead of
  declaring the path unavailable;
- a missed navigation gave up at once. It gets a second attempt with a fresh id
  and iframe, the failed one torn down completely first.

Also closes a MessagePort leaked per download, and gives the reason a name
(`lastStreamFailure`) so a refusal can say what happened. The timeouts became
parameters: the defaults are the production values, no caller passes any, and
the tests do not spend a minute waiting.

`openTarget` gets an unrelated but adjacent fix, in the same file: it creates
the destination with `getFileHandle({create: true})`, so an empty file exists
before the first byte, and `abort()` leaves the target untouched — every
cancelled download left a 0-byte file behind, and since `freeName` avoids
collisions, three cancels left film.mkv, film (2).mkv and film (3).mkv, all
empty. Its `abort()` now removes the entry. Safe here and only here, because
`freeName` guarantees the name was not taken: the `showSaveFilePicker` path
must not do the same, where the person may have picked an existing file whose
contents `abort()` correctly preserves. Verified by hand in Chrome.

test_streamed_download_reliability.py runs the real module under Node against a
stubbed browser — it fails if the null is cached again.

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(hub): drop the flag emoji from the language menu</title>
<updated>2026-09-08T01:11:22Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T01:11:22Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=6a5997655d9f2fa583bf707a5611bbb0c0f109fc'/>
<id>urn:sha1:6a5997655d9f2fa583bf707a5611bbb0c0f109fc</id>
<content type='text'>
The flags were regional-indicator pairs, which Windows has never shipped a
glyph for: Segoe UI Emoji renders the pair as its two letters, so the menu
read "GB", "FR", "NL" down the left-hand side instead of a flag. It only ever
looked right on a system with an emoji font that draws them, such as GNOME's.

Names only, rather than vendoring ten flag images. A flag is a country and
not a language anyway — none of them is the right answer for "Português
(Brasil)" — and each name is already written in its own language, which is
the self-identifying signal the flag was standing in for.

The `flag` field is removed from LOCALES rather than left unread; app.js's
menu was its only consumer. The submenu keeps its indent from
.user-menu-sub's padding, not from the icon box that is now gone.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01V8EDjk6pkYZrCbo63m2x87
</content>
</entry>
<entry>
<title>feat(hub): name a group's host on its card, not its visibility</title>
<updated>2026-09-08T01:09:59Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-09-08T01:09:59Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=436aa6d01ea3a9d2a6cd5d04b284c48db6d6f90c'/>
<id>urn:sha1:436aa6d01ea3a9d2a6cd5d04b284c48db6d6f90c</id>
<content type='text'>
"private" and "invite" were on every card because they are the default for
every group on the hub — two badges that never varied and so never told
anyone anything. Who hosts a group does vary, and it is the thing that tells
two groups of the same name apart: the hub enforces name uniqueness per owner
account, not globally.

- The "My groups" cards (app.js) drop both badges and render the name through
  GroupName, which puts the owner beside it as a smaller "@handle" — the same
  way the group header and Explore already write it. "admin" stays; it varies.
  /v1/groups/mine already returns owner_username, so no API change.
- Explore drops its join_policy badge for the same reason. An open group still
  announces itself through the Join button, which is the useful half, and its
  @host was already there.
- .group-card h3 .gn-owner is set to 0.8em: a card's h3 is reset to 1em, so
  the 0.55em meant for a page heading rendered the handle at about half the
  body size — smaller than the name, as it should be, but past readable.

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): 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>
</feed>
