aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/transfers.js
Commit message (Collapse)AuthorAgeFilesLines
* feat(hub): drop files and folders onto Files to upload themChristophe Besson11 days1-0/+6
| | | | | | | | | Into the folder on screen, under the Upload button's rule. A name already there, or one the node would refuse, cancels the whole drop with a message. Folders are recreated level by level; files go out a few at a time. The in-flight upload guard is keyed by folder and name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat: resume an interrupted upload, and pause oneChristophe Besson2026-09-091-3/+6
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Stage 8 of ~/next/improve-downloads.md, second half, plus the gap it exposed in stage 7. **Asking where to resume.** The node identifies an upload by (member, directory, filename), so a client resuming one has to name the file — and `transfer_open`, the obvious place to ask, travels in clear. Naming it there would undo exactly what sealing this path bought in MNP 2.0: before it, the same file was ciphertext leaving a node and plaintext arriving at one. So the question is asked inside the seal that already exists, as an ordinary `file_upload` with no bytes and `chunk_index: -1`. The node writes nothing, creates no state, reserves no name, and answers with `resume_from` in the sealed ack. A node that predates it refuses the index, which the client reads as "start from the beginning" — the behaviour it had anyway — and the wait is bounded so one that answers neither does not strand an upload. The probe is answered after every check the write path makes, so it cannot ask questions about a directory the caller may not write to, and it answers only about the member who asks: otherwise one member could measure another's progress on a file they never sent, and worse, resume it. **Pausing an upload.** Reported: no pause button on an upload, even in the desktop app. Stage 7 built pause around the download path — a target declares whether it can be stopped — and an upload has no local target to ask. It was also refused by design, since a transfer handed a lease it cannot re-create must not be offered a button that would drop its slot for good. Uploads now ask for their slot rather than being handed one, and say they are pausable outright: a File is seekable and the node keeps the position. Resuming re-probes rather than trusting the client's own memory, so it works across a reconnect too. **And the slot they hold.** `_do_file_upload` never called `slots.touch(tr)`. Chunks are not gated by the lease, so the file arrived — but the node reclaimed a grant nobody appeared to be using after thirty seconds, twice, then abandoned it, and the widget follows the lease. Measured from the journal: a 3.5 GB upload read "waiting, 0 ahead" for a minute and a half while it was transferring. The download twin of this was fixed on 2026-09-08; the same omission was still here, invisible until uploads took a real lease. `test_the_upload_itself_is_sealed` now checks every message `uploadFile` sends rather than the first. Adding the probe put a second one in front of the one it was written for, and it would have kept passing while guarding nothing. Node suite 1202 passed, hub suite 850 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
* feat(spa): pause and resume a download, in sessionChristophe Besson2026-09-091-14/+104
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
* fix(hub): the transfers row exists from the clickChristophe Besson2026-09-081-18/+72
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
* feat(hub): client-side transfer leases and the transfers panelChristophe Besson2026-09-081-10/+95
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
* fix(downloads): automatic really is automatic, and a selection downloads all ↵Christophe Besson2026-08-151-2/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 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>
* feat(files): transfers that outlive the page, and selection instead of ↵Christophe Besson2026-08-151-0/+203
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>