16 KiB
phase, plan, subsystem, tags, status, requires, provides, affects, tech-stack, key-files, key-decisions, requirements-completed, metrics
| phase | plan | subsystem | tags | status | requires | provides | affects | tech-stack | key-files | key-decisions | requirements-completed | metrics | |||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-federation-mesh-hardening | 17 | ui |
|
complete |
|
|
|
|
|
|
|
|
Phase 1 Plan 17: Keep the FIPS/Tor pills forever, and make sure a phone shows them — Summary
Audited all five transport-pill render sites in a real Chromium at 390×740 and 320×640 with RPC interception supplying transport data; fixed the one site that degrades badly at the narrowest supported width; and pinned every pill with a site-specific vitest suite so removing one fails the build.
Method
The mock backend's content.browse-peer returns no transport field, so no pill renders against it.
Rather than change the mock (out of this plan's file boundary), the audit drove the already-running
:8100 dev preview through Playwright with page.route() interception on /rpc/v1, injecting
transport: 'fips' | 'tor' | <absent> per peer. That renders the real components with the real CSS
and gives measurable geometry (getBoundingClientRect, scrollWidth vs clientWidth, computed
display) instead of a subjective look. Harness lived in the scratchpad; nothing was written to the repo.
Per-site audit table
| # | Site | File and line | Renders on mobile | Legible | Action |
|---|---|---|---|---|---|
| S1 | Peer card badge row, Folders tab | Cloud.vue 311–331 (pre-fix) |
Yes — 90×24 px pill, in viewport at both 390 and 320 | Conditionally — fine with real trust labels (36–69 px slack at 320), but the row cannot wrap, so a longer trust label squeezes the badge until its own text breaks mid-label | FIXED — flex-wrap on the row, shrink-0 on the transport badge |
| S2 | Peer Files aggregated rows | Cloud.vue 199–218 |
n/a — no transport pill exists at any width | n/a | No change — decision recorded below |
| S3 | Paid Files rows | Cloud.vue 150–178 |
n/a — no transport pill exists at any width | n/a | No change — decision recorded below |
| S4 | Header pill (desktop copy + md:hidden mobile copy) |
PeerFiles.vue 8–38 |
Yes — the md:hidden copy renders at 390 and 320 (41×20 px, 183–253 px of slack); the hidden md:block desktop copy correctly does not |
Yes — labels are ≤ 4 chars (FIPS/Mesh/LAN/Tor), no truncation, no overlap |
No change — site passed |
| S5 | Per-file card body | PeerFiles.vue 152–232 |
n/a — no transport pill exists at any width | n/a | No change — decision recorded below |
Long-name robustness was exercised for S1/S2/S3/S4 with 54–56 character peer names and 65–70 character filenames: the peer name truncates in its own row above the badge row and never touches the pills, and the measured pill geometry was identical to the short-name run.
grep -rn "FIPS" src --include=*.vue confirms S1 and S4 are the only cloud-surface transport-pill
render sites — CloudFolder.vue, named speculatively in the original todo, has none.
The S1 defect, measured
At 320×640 the badge row's container is 238 px. With real trust values the row fits:
| Case | trust label | badge | row height | badge wrapped? | verdict |
|---|---|---|---|---|---|
| baseline | trusted |
TOR · 0.1s (90 px) |
24 px | no | fits, 69 px slack |
| slow peer | trusted |
TOR · 120.0s (105 px) |
24 px | no | fits, 54 px slack |
| observer | observer |
TOR · 120.0s |
24 px | no | fits, 44 px slack |
| unverified | unverified |
TOR · 120.0s |
24 px | no | fits, 36 px slack |
| longer trust label | pending verification |
TOR · 120.0s |
40 px | yes | FAILS — badge compressed to 96 px, its text broken across two lines as TOR · / 120.0s |
The row had flex items-center gap-2 with no wrapping and no shrink-0, so flexbox's only degradation
path was to shrink both pills until their labels wrapped internally. That is the "truncates into
meaninglessness" failure the plan forbids, and it lands on the transport badge — the security signal —
not on something decorative.
The fix, measured
flex-wrap on the row + shrink-0 on the transport badge. Same case, after:
| Case | badge width | badge wrapped? | on same line as trust? | row height |
|---|---|---|---|---|
pending verification + TOR · 120.0s |
105 px (full natural width) | no | no — dropped to line 2 intact | 56 px |
Every other case is byte-identical before and after (same row width, row height, trust width, badge
width, same line). flex-wrap only takes effect when the row would otherwise overflow, which is
exactly why it is safe.
Site decisions (the two open questions, settled)
Peer Files aggregated rows (S2): NO per-file pill.
Transport in this codebase is a peer-level, per-browse reading — cloud.peer-browse:<onion> stores
one transport and one latencyMs for the whole browse, and peerTransport(onion) reads exactly
that. The aggregated list merges files from every peer and sorts by filename, so a per-row pill would
render the same peer-level fact once per file (40 files from one FIPS peer ⇒ 40 identical pills) while
implying a per-file measurement that was never taken — the precise claim threat T-01-78 forbids. At
320 px it would also have to compete with a filename that already truncates and the existing peer-name
pill. The row already names its peer and taps through to PeerFiles.vue, where the peer-level pill is
shown once and correctly. The existing peer-level pill is sufficient for these rows.
Paid Files rows (S3): NO pill. Stronger still. These rows come from the local purchase cache
(content.owned-list) — the bytes are already on this node and were filed into Photos/Music/Documents
at purchase time. There is no live transport for them, and the session may never have browsed that
seller at all. Any pill here would be stale or fabricated. The honest treatment is the one already
shipping: none.
PeerFiles per-file cards (S5): NO pill, for the same reason at smaller scale — every file on that page came from one peer over one transport, already stated once in the header.
All three decisions are pinned as absence assertions in the test file, with a comment stating that they encode a recorded decision and that deliberately adding a pill means updating this summary and the test together.
The pin (TransportPills.test.ts, 13 tests)
Nothing in the repo pinned these pills before. The suite opens with a plain-English header stating that the pills are a permanent, user-requested feature and that a failure here most likely means someone removed one, not that the test is stale.
Assertions are keyed to shapes only one site produces: the peer card's FIPS · 0.4s form (S1), the
hidden md:block title block (S4 desktop), the md:hidden class (S4 mobile). Labels and colours are
asserted against PeerFiles.vue's canonical transportPill mapping across all four transports
(fips/mesh/lan/tor) rather than a duplicated table — grep -c 'transportPill' neode-ui/src/views/PeerFiles.vue is 9, unchanged (that file was not edited at all).
Unknown-transport cases assert no pill is fabricated at either site and that S1 keeps its existing
Peer Node not-known treatment.
One test-isolation subtlety worth knowing: cloud.peer-browse:<onion> is a persist: true key that
snapshots into sessionStorage, which outlives a per-test createPinia(). Without sessionStorage.clear()
in beforeEach the transport from an earlier test leaks forward and the unknown-transport case passes
against a stale FIPS reading. That is now explicit in the file.
Task 2 re-check
390×740 and 320×640, fixed site (S1): re-measured after the fix. In the fitting cases the pill
geometry is identical to pre-fix; in the overflow case the badge now renders at its full 105 px on a
second line with TOR · 120.0s intact and legible. Long-name case forced by injecting 54–56 character
peer names through the RPC interceptor, and the worst-case trust label (pending verification) forced
by editing the rendered text node in the page — neither the mock backend nor the source was changed
to produce them, exactly as the plan directs.
1440×900 desktop, per changed site: measured pill geometry was diffed pre-change vs post-change
for every captured surface. Cloud.vue S1 at 1440: pill identical at 90×24 px, x=771 / x=1159, y=331 —
the only textual difference is the live latency figure (0.1s vs 0.2s), which is a measurement, not
layout. S2, S3 and PeerFiles.vue at 390/320 were byte-identical. Desktop rendering at the one
changed site is unchanged. PeerFiles.vue was never modified.
Deliberate-removal check — the pin proven, bidirectionally:
| Pill removed | Result | Site-specificity |
|---|---|---|
Cloud.vue peer-card transport badge (706 chars, plus de-branching the orphaned v-else so the file still compiles) |
3 tests failed, message: peer card transport pill is missing — see the header of this file |
S4 PeerFiles tests still passed |
PeerFiles.vue md:hidden mobile pill copy (236 chars) |
5 tests failed, message: the md:hidden mobile transport pill is missing — a phone would show no transport here |
S1 Cloud tests still passed |
Both files were then restored and the suite passed again, 13/13.
git status --short -- neode-ui/src/views/Cloud.vue neode-ui/src/views/PeerFiles.vue is empty —
no leftover deliberate-removal edit.
A third variant is worth recording: deleting the pill without de-branching the v-else fails the
suite as a Vue compile error (v-else/v-else-if has no adjacent v-if) rather than an assertion, so
the careless version of the removal is caught too.
Verification
cd neode-ui && npx vitest run src/views/__tests__/TransportPills.test.ts— 13 passed.cd neode-ui && npx vitest run— 104 files, 845 tests, all passed, includingkeepAliveTabs.test.ts.cd neode-ui && npm run build— green (vue-tsc -b && vite build, built in 35.87 s).- Build is not a silent no-op:
flex flex-wrap items-center gap-2 text-xsandinline-flex shrink-0 items-center gap-1.5 px-2 py-1 rounded-fullare both present in the builtCloud-*.jschunk, themd:hiddenpill class string is intact inPeerFiles-*.js, and.flex-wrap{and.shrink-0{are both emitted in the main CSS bundle. - RED/GREEN was demonstrated explicitly: with the fix reverted, the responsive assertion fails
(
transport pill must not be compressible) while the 12 pill-existence assertions still pass; with the fix applied, all 13 pass.
Deviations from Plan
None affecting behaviour. Two process notes:
- The plan's
<output>says togit push gitea-ai main. The execution brief for this run explicitly forbids pushing, tagging and deploying. Not pushed — commit8255b69ais local onmain. Per CLAUDE.md's commit-and-push rule this work is not "done" until someone pushes it. - Task 2 made no source changes (it is a verification task), so it has no commit of its own. Its
acceptance criterion — a clean
git statusfor both views — is satisfied.
Known Stubs
None. No placeholder, mock or empty-value path was introduced.
Threat Flags
None. No new network endpoint, auth path, file-access pattern or schema change. The one source change is two Tailwind utility classes and a comment.
Threat register status:
- T-01-78 (pill claiming an unobserved transport) — mitigated: unknown-transport assertions at both render sites, canonical mapping reused, no per-file pill fabricated for the rows that have no live reading.
- T-01-79 (mobile user cannot see a Tor arrival) — mitigated: every render site has a recorded verdict at 390 and 320.
- T-01-80 (later cleanup silently deleting the pills) — mitigated and proven: the pin fails on removal at both sites, with site-specific failure messages.
- T-01-81 (long peer name pushing the pill off screen) — mitigated: exercised deliberately at both narrow widths; the name truncates in its own row and never reaches the badge row.
- T-01-SC (package installs) — no dependency added or installed.
Field note (not in scope, recorded for whoever picks up UIFIX-01 follow-up)
The user's report is dated 2026-07-30. Both pills predate it: the PeerFiles.vue mobile copy landed
9e3ac9ba (2026-07-20) and the Cloud.vue peer-card badge landed c83bade0 (2026-07-27). Since both
render correctly at phone widths, the most likely reason a pill was absent on the day of the report
is not layout but data: peerTransport() returns null and the pill does not render whenever the
content.browse-peer fan-out never resolves. That first-visit stall was root-caused and fixed on the
same day by e1a3f31a and 8fe6217b ("cap content.browse-peer fan-out — root cause of Cloud
first-visit hang"), and it would bite harder on a phone than on a desktop. If the user still reports a
missing pill after this ships, look at whether the browse resolved, not at the CSS.
Self-Check: PASSED
neode-ui/src/views/__tests__/TransportPills.test.ts— FOUNDneode-ui/src/views/Cloud.vue— FOUND, containsflex flex-wrap items-center gap-2 text-xsandinline-flex shrink-0- commit
8255b69a— FOUND ingit log neode-ui/src/views/PeerFiles.vue— unmodified,git statusclean