<feed xmlns='http://www.w3.org/2005/Atom'>
<title>meshbay.git/CLAUDE.md, branch 0.5</title>
<subtitle>MeshBay — read-only public mirror</subtitle>
<id>https://git.meshbay.org/meshbay.git/atom?h=0.5</id>
<link rel='self' href='https://git.meshbay.org/meshbay.git/atom?h=0.5'/>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/'/>
<updated>2026-08-17T15:38:14Z</updated>
<entry>
<title>docs: sessions renew themselves, and two faults of the same shape</title>
<updated>2026-08-17T15:38:14Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-17T15:38:14Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f23e8e51e4b8bc03d6109081fe048af1782e1f4e'/>
<id>urn:sha1:f23e8e51e4b8bc03d6109081fe048af1782e1f4e</id>
<content type='text'>
USERGUIDE said an hour in five places and presented renewal as something the
reader does with curl. Both are now wrong: it is four hours, the web app renews
for itself, and the endpoint rotates — so anyone driving it by hand has to store
the refresh token that comes back, or their next call revokes the family.

Also corrects what the token's life actually bounds. It is not how long a
revocation takes: the hub reloads the account on every request and refuses a
suspended one at once, and it pushes signed revocations to nodes. What remains
is a leaked token on an account still in good standing, which is the reason to
keep the number small.

Two lessons in CLAUDE.md. A rotated refresh token has to be stored or it is
spent once. And an effect keyed on a value that used to be constant: the WebRTC
dial listed `token` among its dependencies, harmless while a token only ever
expired, fatal once the session renewed itself — it tore the connection down
mid-handshake and the node waited for ever. That and the hook declared after its
own dependency are the same shape, and worth naming as one: code that reads
correctly on its own and is wrong against the component lifecycle.
</content>
</entry>
<entry>
<title>fix(hub): the transfers panel hung off the side of a phone</title>
<updated>2026-08-17T07:28:49Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-17T07:28:49Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=af30a10b83366416c25eaacec0b4df77526d0924'/>
<id>urn:sha1:af30a10b83366416c25eaacec0b4df77526d0924</id>
<content type='text'>
Reported: on mobile you see only the right-hand edge of the panel, without the
content. Measured, before anything was changed:

    320 px viewport -&gt; panel at -138..192, 138 px off the left
    360 px          -&gt;  -98..232
    412 px          -&gt;  -46..284

The panel is 330 px wide and anchored to the right edge of its button — but
that button is not at the right edge of the screen, since the bell and the user
menu come after it. What falls off is the left-hand side, which is where the
file names are, so what stayed on screen was a strip of progress bars belonging
to nothing.

Narrowing it would not have helped: the overflow comes from where the right
edge is pinned, not from the width. Below the existing 768 px breakpoint the
panel is anchored to the viewport instead, full width on a phone and capped at
420 px on a tablet, where stretching two filenames across 750 px would be
silly. Desktop keeps its 330 px against the button.

The interesting part is how it was found. The responsive tests read numbers out
of the stylesheet and said, in their own docstring, that a layout could not be
measured because the suite had no browser. It has one now — Chrome, from the
video work — so layout_probe.py renders the real stylesheet at a given width and
returns rectangles. `width: 330px` was never the thing worth asserting on.

An iframe carries the viewport, because a headless window will not go below
about 500 px, and one browser measures every width: launching one per test put
three minutes on the suite against twenty-six seconds for all of them. Checked
that the new tests fail with the rule removed — three of them do — and that
they pass with it back.
</content>
</entry>
<entry>
<title>test(hub): a hook that depends on one declared below it never runs</title>
<updated>2026-08-17T00:16:28Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-17T00:16:28Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=42047dac4041e72e09499e3adf145f1c0f83b284'/>
<id>urn:sha1:42047dac4041e72e09499e3adf145f1c0f83b284</id>
<content type='text'>
`const a = useCallback(fn, [b])` evaluates `[b]` where it is written, so a `b`
further down the component is still in its temporal dead zone. ReferenceError on
every render, before anything the component does can run — and the symptom is
the component simply not appearing. Clicking a video did nothing at all: no
picture, no error on screen, nothing in the node's log because nothing was ever
requested. It reached production.

Nothing caught it. `node --check` passes, the code is well-formed. Worse, the
MSE harness extracts the player functions into an order of its own and therefore
*reordered* them before running — quietly repairing the one class of defect it
was best placed to catch. It sorts by position in the file now, and
test_hook_ordering.py checks the property directly across the whole SPA. Both
the rule and the harness are checked against the layout that actually shipped.

test_video_seek.py covers the rest of seeking, and window_leak.mjs forces the
race that made the third seek hang: the whole in-flight window arriving while
`reinitAt` is still awaiting. Before, the player is left believing eight
segments are in flight and grants nothing; after, the window comes back. A run
that happens to work proves nothing about a race, which is the point of forcing
the worst case rather than trusting a longer session.
</content>
</entry>
<entry>
<title>docs: what this session cost to find</title>
<updated>2026-08-16T18:58:17Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-16T18:58:17Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=f57b2f6c0c2770fe0f456128d985e29427d2432f'/>
<id>urn:sha1:f57b2f6c0c2770fe0f456128d985e29427d2432f</id>
<content type='text'>
Five lessons, and the first is the expensive one: a test that models a fix
agrees with it by construction. The buffer-ceiling test passed against a player
that still hung, because the model and the fix had the same author and the same
misunderstanding.

Also: `no-cache` only binds a browser that asks; redeploying during someone
else's test kills their session and truncates the log holding the reproduction;
`updateend` fires for `remove()`; and flow control on a media stream is a
window, not a debt.

USERGUIDE section 7 rewritten — it still described 24 segments in flight and
two transcode slots, and said "transcode" where ffmpeg does a `-c copy` remux,
which is exactly why a slot costs little and why 500 MB really does go on the
wire.
</content>
</entry>
<entry>
<title>docs: record what this session cost to find</title>
<updated>2026-08-16T13:29:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-16T13:29:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3a37063aac59b8faaae72cd71c1990231a898144'/>
<id>urn:sha1:3a37063aac59b8faaae72cd71c1990231a898144</id>
<content type='text'>
Six lessons, each of which took a deployment and a user's patience to locate.
Five are the same shape: something that fails silently and looks like something
else. The asyncio task nobody holds, `proc.wait()` deadlocking on a pipe it will
not read, a peer whose loss is noted but not acted on, a service worker that is
active without controlling the page, and a React setter that outlives its state.

The sixth is about method: `node --check` validates syntax and not names, and
`e2e.py` is a second implementation of the client rather than a test of this
one. Neither caught any of the above. Source-reading tests are weak evidence and
are the only evidence available for the SPA — so prefer ones that re-derive a
value from the source over ones that restate it.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(hub): translate the web client into nine more languages</title>
<updated>2026-08-15T22:21:07Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-15T22:21:07Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=3b2d03a919e56d14f8097844b97b844ddf526cd6'/>
<id>urn:sha1:3b2d03a919e56d14f8097844b97b844ddf526cd6</id>
<content type='text'>
French, Spanish, Brazilian Portuguese, Simplified Chinese, Japanese,
German, Italian, Dutch and Polish, all in the formal register. `hub`,
`node` and `GEK` stay in English: they name the CLI, node.toml and the
docs, and translating them would cut the interface off from everything an
operator reads and types.

Catalogues move out of i18n.js into locales/, one file per language,
fetched with a dynamic import. A visitor downloads their language plus
English as a fallback — about 36 KB rather than the ~180 KB that ten
inlined catalogues would have cost everyone. i18n.js keeps only the
loader, so the first render now waits for initLocale().

Three things the old code got wrong, none of them visible until there was
a second language:

- Resolution trimmed a tag to its base before matching, so a browser
  reporting pt-BR looked for a `pt` catalogue that does not exist and fell
  back to English. Matching is now exact first, then by base language.
- Counted strings were single strings, so Polish could not express
  1 plik / 2 pliki / 5 plików at all. t() selects through
  Intl.PluralRules; en.js gains the same treatment, which incidentally
  fixes "1 files".
- Interpolation used String.replace, which reads `$&amp;` in the replacement.
  A file named rap$&amp;sody.mp3 rendered corrupted in its own delete dialog.

Five strings were still hardcoded in app.js — the group name placeholder
and the four visibility/join-policy descriptions — and are now keyed.

test_locales.py holds the nine translations to the shape of en.js: same
keys, a counted string stays counted everywhere, every plural entry covers
each category Intl actually produces for that language, and the
{placeholders} survive translation. Verified failing first, against a
catalogue with a key removed, a placeholder dropped and the Polish `few`
form deleted.

The language menu also grew from one entry to ten, which overran a short
viewport inside a dropdown that clipped instead of scrolling.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: record which download paths have actually been run</title>
<updated>2026-08-15T17:13:30Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-15T17:13:30Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=ef30201a99c6aeb9bf2406904005f0977f1c4ecc'/>
<id>urn:sha1:ef30201a99c6aeb9bf2406904005f0977f1c4ecc</id>
<content type='text'>
The download-to-disk story is three mechanisms — File System Access in
Chrome, a service worker streaming a response in Firefox and Safari, a
blob as the floor — and no test in this repository exercises any of them.
test_downloads.py pins their contracts by reading the source; whether a
browser really writes to disk needs a person with a large file.

One now has: Firefox, 180 MB, written to disk rather than assembled in
the tab. That is the path worth confirming, since it is the only one
Firefox has and it was written blind. It is also not the scale it exists
for, and the guide says which rows of that table are measured and which
are still only designed.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>feat(groups): editable description, and one source of operator authority</title>
<updated>2026-08-15T00:34:19Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-15T00:34:19Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=84b032c65e17267d41e04605e79eea82a6f5a59f'/>
<id>urn:sha1:84b032c65e17267d41e04605e79eea82a6f5a59f</id>
<content type='text'>
A description could only be set the moment a group was created, so every
group made before anyone thought of one stayed blank for good. The owner
can now edit it from the group's page, and PATCH /v1/groups/{id} takes it.

That endpoint takes the description and nothing else, deliberately. The
name, the visibility and the join policy are the terms members joined on;
a private group that can quietly become public is not the group they
agreed to be in. Changing those needs a decision about who gets told, not
a field on a form — there is a test saying so.

Separately, the legacy operator key is gone. `admin_pk_ed25519` in
node.toml named the operator before the roster existed and was kept so
that an existing deployment would keep working; nothing uses it, and a
second source of node authority is not something to carry around out of
politeness. Authority is the roster, read fresh on every check.

It is removed rather than ignored: a config that still names the key gets
a warning at startup pointing at the file. Dropping it in silence would
refuse invites and file deletion with a signature error that looks like a
bug somewhere else — which is exactly how finding M3 presented.

Two tests were verifying admin operations by naming a key in the context,
which was the legacy path. They now pair an operator into a roster, the
way an operator does. The authority test anchored on the deleted function
and passed vacuously once it disappeared; it states the invariant against
the verifier and the daemon instead.

Also defined .btn-secondary, used in four places and styled in none.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: account deletion, notifications, and the APIs that no longer exist</title>
<updated>2026-08-14T22:30:29Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T22:30:29Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=4156b5b8c3986278a887d1dce4db265746e9155f'/>
<id>urn:sha1:4156b5b8c3986278a887d1dce4db265746e9155f</id>
<content type='text'>
Account deletion is the headline, in the user guide and in draft-v5 §6.1,
and the important half is what deletion does *not* do. It releases the
username, clears the email and password hash, drops memberships,
notifications, refresh tokens and node registrations, and refuses any
access token still inside its hour. It does not touch a node: files, the
pinned identity and the keypair bundle stay on machines the hub does not
command, which is the same sovereignty §5.5 relies on — so deleting a hub
account is not an erasure request to the operators hosting you. The IP log
survives too, attributable, for its legal retention period. The claims
table in §2 gets a row saying exactly this, adversary by adversary.

Notifications get a section: one entry per conversation rather than per
message, never one for your own message, invitations that clear when you
join, muting that lives on the hub so it works from any browser.

Then the corrections, which is most of the diff. The guide still described
a node HTTP API — `GET /index`, `GET /file/{id}`, an HLS playlist, and a
`player.js` that does not exist — with curl examples inviting the reader
to expose port 19001. That surface was removed in 0.2.0 as findings C1 and
C6, precisely because it served files outside the handshake that decides
what a peer may see. Sections 6, 7 and the API reference now describe MNP
message pairs, and the quickstart says the same in French. Also corrected:
the JWT table advertised a `pk_user` claim that no longer exists (it was
what let the token issuer decide who could delete a file), `/pubkeys` no
longer returns identity keys, and the GEK-distribution endpoints are gone
entirely rather than merely unused.

draft-v5 §5.2 had uploads landing in `.uploads/{user_id}/`; they land in
`uploads/`, chat attachments included. §6.1 now says the hub learns the
author's user_id from chat_notify — a stable identifier, and a metadata
leak worth naming rather than leaving as "by whom".

CLAUDE.md records why the deployed hub broke this week: create_all()
creates missing tables, never missing columns, so a schema change passes
every test (fresh DB per run) and never reaches production.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
</content>
</entry>
<entry>
<title>docs: user guide and conventions catch up with per-node identity</title>
<updated>2026-08-14T17:15:49Z</updated>
<author>
<name>Christophe Besson</name>
<email>cbesson@gmail.com</email>
</author>
<published>2026-08-14T17:15:49Z</published>
<link rel='alternate' type='text/html' href='https://git.meshbay.org/meshbay.git/commit/?id=cfe49407abb3eb48b996ab6d08d66985f29846f0'/>
<id>urn:sha1:cfe49407abb3eb48b996ab6d08d66985f29846f0</id>
<content type='text'>
USERGUIDE said registration submits your public keys "so other members can wrap
GEK bundles for you". Both halves are wrong now: registration creates an account
and nothing else, and nobody wraps anything for a key fetched from the hub. The
API reference and the register body followed the same correction.

CLAUDE.md gains the block a future session needs before touching registration or
anything shaped like a user's public key: keys are born at first contact with a
node and stay there, the hub publishes none, tokens carry no pk_user, and a
scripted signup is now a real account.

Left alone deliberately: first-review.md, docs/poc-v1*.md and poc/spike-results.md
still describe the old JWT and registration. They are records of what was true on
their date, like second-review's verdict table, and draft-v5 is what states the
present.

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