| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
of it
Two bugs in what shipped last, and both were mine.
Automatic mode still opened Save As, because with no folder granted the
code fell through to the file picker — while the documentation said it
would use the browser's own download folder. It does that now. Over
512 MB it still asks, since getting there means holding the file in
memory and a tab will not survive a 40 GB blob; Settings is where to stop
it asking again.
Selecting two files downloaded one. They were started without awaiting,
so each asked the browser for a save dialog at once, and a browser allows
exactly one — the rest were rejected and the errors went nowhere. They
are awaited one at a time now, which serializes the dialogs and not the
transfers: each call returns as soon as its transfer is registered.
Then the adjustments. The transfers widget offers Open on a finished
download that went into a granted folder — the bytes go to a new tab, and
that is the whole of what a page can do: no browser lets one start a
desktop application or show a file manager, so the folder half of that
request cannot be built and the guide says so.
The Files toolbar was four controls of three different heights in a row.
It is three groups now — what you can add, where you are, what you can do
with what is here — on one baseline, with icons from the set and a gap
between the dots and the word Actions. Chat comes first among the tabs
and is the one you land on. The three Discover entries in the sidebar
have icons. And a link in a chat message becomes a link: built as an
element and never as markup, http and https only, so `javascript:` is not
one message away from running here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
per-row menus
Downloads and uploads were state inside GroupPage. Leaving a group
unmounted the component, its cleanup closed the DataChannel, and a
half-written file was all you had — which is also why only one thing
could be in flight at a time.
They live in a module-level store now. A group page hands its transport
over on the way out rather than closing it, and the last transfer using
it closes it; signing out is the one thing that cancels everything,
because those transfers are moving data on a token about to stop being
ours. The store is plain JavaScript with no browser globals, so
test_transfers.py runs it under Node and pins the parts that are timing
and lifetime rather than markup: that a cancel stops the work instead of
greying out a row, that a stalled transfer reads as stalled rather than
reporting its own historical average, and that a released transport is
closed by the last transfer and not before.
The widget by the bell shows each transfer with its rate and a cancel
button, so the Files panel no longer carries progress bars — you can
watch a 40 GB archive from the chat, or from another group.
Selection replaces the per-row menu: a Select toggle puts checkboxes on
files and folders, and ⋮ Actions acts on what is ticked. Ticks survive
walking into another folder, so a selection can span directories.
Downloads start together and run together. Videos offer Play only — View
did the same thing, which is the sort of duplication that makes people
wonder what the difference is.
Uploads had to become parallel-safe for any of this to mean anything:
their acks were matched by arrival order, so two at once credited each
other's progress. The node names the file in every ack, so they are keyed
by name now — with the same file twice refused, since the node keys its
own upload state that way too.
Two mistakes worth recording. The selection column went into the body
rows and not the header, because that edit matched nothing and I had not
made it assert; the columns were misaligned until a screenshot showed it.
And the Actions menu opened leftwards from a button at the right edge of
the toolbar, half of it off-screen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The uploader read a 48 KB slice, sent it, and waited for the node to
acknowledge it before reading the next one. That caps throughput at one
chunk per round trip regardless of available bandwidth, and it is worse
than the arithmetic suggests: the sender is idle for almost the whole
time, so SCTP's congestion window never opens either, and the transport
stays slow even when the link is not.
Measured against the real node over a 100 ms path (netem on loopback):
48 KB chunks, one at a time 0.16 MB/s
48 KB chunks, 32 in flight 3.47 MB/s
On loopback with no latency both are ~32 MB/s, which is why nothing here
ever caught it: the local end-to-end run cannot see a round-trip problem.
transport.uploadFile() now keeps a window of chunks in flight and matches
acks by arrival, with the node's own ordering rule as the guard — a
DataChannel is ordered and reliable, and the node refuses any chunk that
is not the one it expects next. It pauses when the channel's buffered
amount gets high, so the progress bar keeps reporting what the node has
taken rather than what the browser has queued. Both callers, the Files
panel and chat attachments, go through it.
The end-to-end harness grew an opt-in benchmark behind MESHBAY_BENCH=1
that removes its own files afterwards, and it taught me something about
the harness rather than the code: it took an unsolicited index_sync push
for an upload ack, because unlike app.js it had no place to put one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
pairing form
Moving the invite form above the member list cut both out of MembersPanel and
pasted them into AdminPage, where `doInvite`, `members`, `adminId` and
`inviteCode` do not exist. A standard member saw an empty Members tab, the
group owner saw only a pairing form, and the hub's own Users tab referenced
four undefined names.
The pairing form outstaying its welcome is a second bug and an older one.
`is_node_admin` compares the connecting account with the account that owns the
node — it says nothing about whether *this browser's key* was ever paired, which
is the thing pairing changes and the thing that lets you sign an invite. So the
form showed for an operator who paired months ago, accepted a fresh code,
reported success, and stayed exactly where it was. The node already reports the
roster role in `join_result`; the transport keeps it, and the form appears only
when this identity is not an operator key yet.
Also dropped a clause from the pairing hint: the code never passing through the
hub is worth saying, the theory behind it is not.
test_spa_ordering.py gets three checks for this class of bug — a cut-and-paste
between components is invisible to every other test we have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The mechanism was already there — the encrypted keypair bundle goes to the node
after a first successful connection, and any client holding the password can
recover it — but nothing exercised it. e2e.py never pushed a bundle, so the case
that matters to an ordinary user was the one case never tested.
It now does what app.js does: backs the member's keys up to the node, then opens
a second client carrying nothing but a username and a password. Against the live
deployment that client recovers its identity keys, is recognised as the same
person with no second code, gets the same group key, and browses the group.
Also guards the ordering this depends on: the keypair bundle must be fetched
before joinGroup() runs, or a browser that did not register has no key to sign
the join with — invisible on the browser that did register, broken on every
other one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
|
join_request signs a transcript over the node key and the node nonce, and runs
before the GEK proof — a first-time member has no key to prove with. Both values
were read further down, beside the proof that also uses them, so by the time
joinGroup() ran neither was set and every invited member got "Handshake
incomplete — reconnect and retry".
They are now recorded the moment the challenge arrives.
Third bug of the same shape found in a browser, and the reason is worth writing
down: QE/deploy/e2e.py cannot catch any of them. It is a second implementation of
the client, written in the right order by construction, so it passes while the
SPA fails. It proves the protocol; it proves nothing about app.js.
So this adds ordering guards over transport.js — source-level, which is not how
one would normally test behaviour, but it is what sees this class of mistake:
- node_pk and nonce_node are captured before joinGroup() runs
- the join happens before the GEK proof
- the ack still verifies the key the challenge announced
Verified the way the suite requires: each fails against the source as it was, on
the ordering assertion rather than on a missing marker.
e2e.py also waits for the node to re-register rather than reporting "no nodes" at
whoever just restarted the hub.
Tests: 337 across the three packages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|