| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
| |
A native confirm() or alert() leaves the desktop client unable to type
until the window is refocused. ask.js draws both in the page; the SPA
test now bans all three browser dialogs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The per-feature design notes were merged into docs/MESHBAY_DESIGN.md and
deleted from the tree on 2026-09-11, but ~230 comments across the three
packages still named them — usually written `docs/musicbay.md §3.2`, as
though the file were still in docs/. A reader had to know §16 existed to
resolve any of them. They now name the section directly.
Every mapping comes from §16, the concordance, which already records where
each old section landed: musicbay -> §9.8, mediacenter -> §9.7 for the
Videos app and §6.5 where the subject is derived data, photos -> §9.9,
auth-confirm -> §3.6, refactoring-search -> §9.11, invite-pairing-v1 ->
§3.4, per-node-identity-v1 -> §3.2, captcha -> §7.7, chat-sender-keys ->
§4.5, apps/refactor-groups -> §9.1–§9.4, desktop-client-v1 per section.
Bare citations of the same documents (`draft-v6 §2.11`, `§4.8`, `§3.4`)
are retargeted too: those collide with real section numbers in the design
document, so leaving them would have been worse than the named form.
Four cases the concordance does not cover, each decided rather than guessed:
Sub-item references into documents that no longer exist — mediacenter's
`§3.3 row 4`, `§3.4b/c`, `apps.md §3b` — name rows and sub-items §9.7 and
§9.2 do not reproduce. The module-level citation stays; the sub-item
pointer is dropped.
The V-findings keep their labels but lose the dead `§10.1/` prefix.
§13.8 lists V1–V13 as per-application open items, which is not what the
labels mean in these comments, so pointing them at §13.8 would have been
a false citation.
`apps.md §5`'s virtualization requirement has no counterpart anywhere in
the design document. The requirement is stated in the comment itself, so
the citation is dropped rather than aimed at a section that does not say
it.
Comments that attributed a *sentence* to an old note — musicbay's "several
thousand files" example, its "what got measured" note, its measured
~11%/~26% cover-art figures, the "original no root, whole shared tree"
call — state the fact without attribution now. §9.8 does not contain those
sentences and citing it for them would have been wrong.
CLAUDE.md's "a reference to a document that no longer exists" row now says
the concordance is for git history and out-of-tree material; the code cites
sections directly.
Verified: 2851 passed, 4 skipped. The 12 errors in the run are the Firefox
leg of test_sticky_header.py's browser harness, which is broken at the
browser level on this machine — headless Firefox (snap) dies with
`[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer`, renders
nothing, and the probe exits `{"error": "no measurement"}` after its full
90s wait. Chrome runs the same 12 assertions in 3.2s and passes. Nothing
here can affect it: every changed line in style.css is inside a comment.
Also checked: ast.parse on every changed .py, `node --check` on every
changed .js, the /* */ balance in style.css, and that no changed line
exceeds the width its file already used.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Seven comments pointed at sections of `~/next/improve-downloads.md`, which is
not in the tree and not anywhere a reader of this repository can follow. Each
now states the thing it was citing: why a paused transfer holds nothing, why the
lease is taken after the save target and not before, why a chunk request marks
a lease alive, where the leaseless bound's number comes from.
The leaseless comment also said "two files at a time" three paragraphs under
`MAX_LEASELESS_IN_FLIGHT = 12`, left behind when the bound was raised. A comment
that contradicts the constant beside it is worse than no comment: one of them is
wrong and the reader cannot tell which.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Four downloads on Firefox all sat at "preparing", with the node journal showing
`d=0/8(q0) u=0/8(q0)` — not one transfer opened, so nothing had got past the
client's target opening.
Serialising those openings was new in d6c4808, and on Firefox it regressed what
had always worked: four openings that ran at the same time began waiting on the
slowest. `_targetQueue` is module-level and never reset, so an opening that
never settles leaves the page unable to start any download again until it is
reloaded.
Two bounds, both narrowings of the queue rather than of any capability:
Only an opening that could actually show a dialog joins it. Firefox and Safari
have no `showSaveFilePicker` at all, so nothing there can race anything and the
queue bought nothing while costing everything; they now bypass it entirely,
which restores the previous behaviour by construction rather than by tuning.
And no opening waits behind another for longer than TARGET_QUEUE_BUDGET_MS
(90s) — generous enough never to cut in front of a real dialog, finite because
the alternative is a download panel that only a reload can fix. Releasing early
is safe: whatever is ahead is still the only unbatched opening, so the released
one takes the streamed path and opens no second dialog.
Measured on Firefox 154 against the deployed hub before writing any of this:
`register` and `ready` return instantly, the page is controlled, and four
serialised openings are served in 5-18 ms. The streamed path was never the
delay; the queue was.
Both new cases were checked against the unfixed source: without the bypass the
peak concurrency is 1 instead of 4, and without the budget the stuck-opening
case hangs. Hub suite 826 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Selecting four files on Chrome produced a Save As dialog for the first, then
one for the second only after that file had finished, while the last two timed
out; on a later attempt the three remaining transfers appeared frozen.
Two things were going on. Opening the target inside `prepare` had removed the
accidental serialisation that `for (…) await downloadFile(e)` used to provide,
so `_openTargetInTurn` now queues the openings — but a queue whose head is an
unanswered dialog is a head-of-line block, which is what the "freeze" was.
The code already recovered from a picker with no gesture behind it by streaming
instead, on the `SecurityError` Chrome throws. That branch was never reached:
Chrome does not throw, it shows the dialog anyway and waits for a human. So
anything that has to wait its turn is now marked `batched`, and a batched
opening prefers the streamed path whatever the download mode says. The first
file of a batch — the one actually holding the gesture — still gets its dialog,
so the preference is honoured where it can be. For the rest there is no gesture
left to spend and nothing is lost by streaming: the file still lands on disk,
in the browser's own download folder. Only the choice of folder goes, and it
was not on offer. If the worker does not answer, a batched download falls back
to the dialog rather than failing.
Also logs which path led to a dialog. A dialog is the one outcome nobody can
diagnose after the fact — it looks the same whether it was asked for or fallen
back to — and the report this fixes needed three test cycles to narrow. The two
harnesses that lift `_openDownloadTarget` as text now route console.info to
stderr, since they parse stdout as JSON.
Measured against the deployed hub in Chrome 152: the streamed path serves the
hidden iframe in 2-3 ms on a normal load, after a hard reload (via the
`mbdl-claim` recovery already in `_claimController`), and twice in the same
document. Hub suite 824 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`pipelinedDownload` with no writable allocates `new Array(totalChunks)` and
keeps every decrypted chunk, so whatever `_openDownloadTarget` returns null for
is held whole in RAM. That floor had no upper bound: the
`!window.showSaveFilePicker` branch returned null at any size, so on a browser
without the File System Access API a 20 GB film went to memory whenever the
streamed path did not answer. Nothing logged, nothing refused; the symptom was
the tab dying, with no error attributable to this code.
MEMORY_CEILING is 100 MB and every `return null` in that chain now goes through
a guard that throws above it. The refusal names the size, the limit and why the
streamed path declined, and lands in the transfers panel as a failed transfer
rather than in a console nobody opens.
This is a guard, not a limit on what can be downloaded: with the streamed path
primed and retried (previous commit), a file of any size still goes to disk
progressively on every browser. Two things had to change for that to be true:
- the streamed path is now tried in "ask" mode too, for a file over the ceiling
on a browser with no Save As of its own. The mode decides whether to show a
dialog; it was silently deciding whether a film could be downloaded at all;
- FilePreview had no size check whatsoever — a multi-gigabyte PDF or .csv was
fetched whole, and the text branch decoded all of it to keep 500 000
characters. It refuses above the same ceiling and offers the download.
ZIP_MAX_BYTES (512 MB) and the ceiling do not contradict: the archive limit
bounds the archive, the ceiling bounds what may be built in the page, so a
400 MB zip is allowed when there is somewhere to stream it and refused when the
only route left is memory. The build-in-memory confirmation only appears below
the ceiling now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
An arbitrary ceiling, not a technical one: the zip writer streams and holds
one chunk plus a record per file, so it would happily produce a hundred
gigabytes. Past half a gigabyte the honest answer is a subfolder at a time,
or the files individually.
Enforced in file-utils.js's downloadDirectory, which is the one
implementation behind every zip button — Files' single folder, Files'
multi-folder selection, and the Photos album button (docs/photos.md §3).
- Per directory, not per selection: Files zips a whole multi-directory
selection in one click, so an oversized folder is refused and its siblings
still download.
- Before _openDownloadTarget, so no save dialog opens for an archive that is
never going to be written.
- The bound is strict, so a folder of exactly 512 MB still goes through.
- Counted in the 1024-based units formatSize already prints, so the number in
the refusal is the number in the constant.
group.zip_too_large in all ten catalogues. test_zip_size_limit.py runs the
module under Node and pins the refusal, the inclusive bound, and that nothing
is asked or started when a folder is over.
The user guide's "a 40 GB folder costs 40 GB of disk" is no longer true and
now documents the cap instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8EDjk6pkYZrCbo63m2x87
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
With MNP 0.15 no node can emit a base64 `file_chunk`, so the browser's fallback
for that shape is unreachable. Three things go with it:
- `file-utils.js` kept a third branch below the fallback that base64-decoded
`chunkMsg.ct_b64 || chunkMsg.data_b64` when neither was present, i.e. decoded
`undefined` and wrote the result into the file the user was saving. A chunk we
cannot decrypt now stops the download with an error naming the file and
suggesting the node is older than the page. Deliberately not in
`_isRetryableTransportError`: this is a version mismatch, not a bad moment on
the link.
- `crypto.js` `decryptChunk` (base64) was the real path until Phase 9.15 and has
had no caller since.
- `crypto.js` `decryptFile` was never called in any commit. It fetched
`${nodeUrl}/file/${id}/${chunk}?token=` in a loop — the node's unauthenticated
HTTP file API, which is finding C1 and was deleted in Phase 11.5. A client for
an endpoint that no longer exists, kept alive by being exported.
`decryptChunkBin` — every file download and every video segment — is untouched.
`packages/meshbay-client/ui/` was resynchronised with `npm run sync-ui`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
in-flight streams/downloads
Confirmed live (client trace + node logs, mobile screen-lock ~5min): ICE
goes disconnected -> failed within ~10s on both ends, but the DataChannel's
readyState stays "open" throughout, so nothing failed fast — every request
just sat out its own 8s/30s timeout, matching the reported symptom (poster
spinners, blocked chat, dead new streams).
transport.js: on connectionState "failed", reject pending requests
immediately (TransportLostError) and start a self-contained reconnect loop
(capped exponential backoff, redoes the full signaling handshake — the node
already discards the old session on its own "failed"/"closed", so there is
nothing lower-level to resume). New hooks: onNeedToken (fetch a fresh JWT,
since the captured one may have expired during the outage) and
onReconnected (let a consumer resume something that was mid-flight).
file-utils.js: pipelinedDownload retries a lost chunk instead of aborting
the whole transfer — covers Files downloads, poster/thumbnail fetches, and
music-player.js's blob-based track download, all of which go through it.
video-player.js: onReconnected reissues the existing seek-to-current-time
path, which already knows how to land a new stream_init on the live
SourceBuffer without resetting playback.
Playing audio is unaffected either way — musicbay.md's design downloads a
track to a blob before playing it, so a dead transport was never a network
dependency for what is already playing.
Stays on this branch until confirmed by real-device testing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A new group application (docs/apps.md's plug-in mechanism), following the
plan in docs/photos.md. Unlike Videos/Music: several photo roots per group
instead of one (photo_roots is a set, one signed op replaces it whole),
a single album-grid view with no third-party matching step, and per-photo
info read from the file's own EXIF at index time — no metadata service,
no credential, no outbound network call at all.
Protocol (meshbay-common, MNP 0.10 -> 0.11, additive): `taken_at`/`camera`
on IndexEntry; `photo_roots`/`photo_roots_ack`; `OP_PHOTO_ROOTS`.
Node: roster.py stores photo_roots as a group_settings entry (JSON list,
same shape as enabled_apps); ops.py/webrtc_server.py validate and sign the
whole set in one op, same pattern as apps_enabled; a new PhotoEnricher
(indexer/enrich_photo.py) runs Pillow in its own small bounded pool,
separate from the video/audio pools, producing a resized thumbnail plus
the two EXIF fields — never GPS, checked by a grep-based regression test.
Client: photos-app.js — one album card per directory containing images,
a per-album photo grid, and a lightbox with next/previous (keyboard and
buttons), zoom in/out/fit/100% starting from the actual on-screen fit
percentage, and a "zip this album" button reusing files-app.js's own zip
mechanism (lifted into file-utils.js's downloadDirectory so both call the
same implementation). group-settings.js gets an add/remove multi-root
picker, distinct from Videos/Music's single-value one.
Bugs found and fixed before this ever shipped, worth keeping the story of:
- enrich_photo.py read width/height from the raw image *before* applying
EXIF orientation correction, and read DateTimeOriginal off the plain
0th-IFD Exif object — a real camera stores it in the Exif sub-IFD, which
Pillow only exposes via get_ifd(Exif). A flat, hand-built EXIF dict
round-trips through Pillow either way, which is exactly what would have
hidden both bugs; the regression test builds EXIF with piexif instead,
matching what real hardware produces.
- photos-app.js's album grouping stripped a trailing path segment from
entry.path under the assumption it still carried a filename — it
doesn't (files-app.js's own convention: e.path is already the
containing directory), so every album collapsed one level into its
parent. Found live against a real multi-folder library.
- transport.js's ADMIN_OP_TYPES allowlist (already the fix for an
identical bug on video_root/apps_enabled, see 4783d81) was missing
photo_roots: its admin_challenge matched no pending request and was
silently dropped, so saving a photo root just timed out after 30s with
no error.
- daemon.py pruned a thumbnail when its file left the index (root removed
or reconfigured) but never forgot the content hash was "already
attempted" — the same bytes reappearing under a renamed/relocated root
(an operator's real workflow) were then permanently skipped, forever,
with nothing to indicate why. Discarding the attempt alongside the
cache entry on prune is what makes pruning actually reversible.
- packages/meshbay-client's app:// protocol handler served every file
with no Cache-Control header, so Chromium was free to serve a stale
cached copy indefinitely — none of several `npm run sync-ui` + reload
cycles during development actually picked up the new code until the
renderer's disk cache was cleared by hand. Now sends Cache-Control:
no-store.
- the lightbox's zoomed image used flex centering (align-items/
justify-content: center) combined with overflow: auto — a well-known
trap where the browser centers overflowing content by shifting it, and
the leading half of that overflow (here, the top of a zoomed photo)
sits outside what the scrollport can actually reach. Reported live as
"unusable". Fixed by switching to top/left alignment once zoomed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TiZG4AuSnxHohQMpwTHTyL
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Audio files were invisible to file-utils.js's canPreview, so their
name never rendered as a clickable link and the toolbar's Play/View
buttons never enabled for them - the only way to do anything with an
audio file in Files was to download it.
- file-utils.js: canPreview now includes 'audio' alongside image/
video/document.
- files-app.js: the toolbar's canPlay/canView split treats audio like
video (Play, not View) rather than falling into the generic preview
path, which was never built for it anyway.
- group-page.js: onPreview routes an audio entry to onPlayQueue - the
same persistent player Music uses - instead of the preview modal.
Deliberately not Music's artist/album grouping: the queue is every
audio entry sharing the clicked file's literal containing directory
(entry.path), sorted by filename, so previous/next in Explorer stays
scoped to what's actually in that folder, tags or no tags. onPlayQueue
already replaces whatever queue is playing unconditionally, so
starting a track from Files while Music (or another Files folder) is
already playing needs no special handling - it's the same "just a new
queue" path either way.
Client-side only, no protocol/index change. npm run sync-ui re-run.
Full suite: 1129 passed, no regressions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KBi7ALLGfwcjBXt57yNMcy
|
|
|
GroupPage's 6620-line app.js carried Chat and Files wedged in directly, with
no way to add another group-level app without touching the shell itself. It
is now app.js (routing, non-group pages) plus nine focused files — apps.js
(the registry), chat-app.js, files-app.js, video-player.js, group-page.js
(the shell), group-settings.js, hub-client.js, icon.js and file-utils.js —
with docs/apps.md as the checklist for adding one (Videos/Music/Photos are
sketched there, not built).
Node side gained the matching enablement mechanism, mirroring
member_upload exactly: a roster setting, a signed apps_enabled op enforced
by _has_admin_authority, exposed in the handshake ack. Operators toggle
applications per group from Settings, which also gained a small reorder:
Invite, Pairing, Applications, Shared directories, Uploads, danger zone,
Your devices, Members.
Two bugs surfaced during the split, both missing an import across the new
file boundary and invisible to node --check or a module-load probe since
they only throw when the code path actually runs:
- group-page.js called onRefreshAuth on a stale-token handshake rejection,
but app.js never imported refreshAccessToken from hub-client.js — so a
brand new member (including a group's own creator) hit "Not a member of
this group" and the retry silently failed, throwing before it could
refresh the token.
- chat-app.js called getLocale() for message timestamps without importing
it from i18n.js. Opening Chat on a group with real messages threw mid-
render; uncaught, that appears to wedge Preact's render scheduler, so
every button on the page stopped responding until reload.
Caught the second class of bug with a proper no-undef audit across all
split files (a temporarily installed ESLint 9, since the system one is too
old to parse this codebase's syntax) rather than trusting grep. 827 tests
pass; 6 new ones cover the apps_enabled policy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016SF6RKNBKg9qejmoMJ9ybA
|