Files

244 lines
16 KiB
Markdown
Raw Permalink Normal View History

2026-08-12 10:55:49 +00:00
---
phase: 01-federation-mesh-hardening
plan: 17
subsystem: ui
tags: [vue, responsive, tailwind, vitest, transport, security-signal]
status: complete
requires:
- phase: 01-federation-mesh-hardening
plan: 14
provides: "Paid Files row treatment and the Cloud.vue/PeerFiles.vue shapes this plan audited"
provides:
- "A recorded, browser-measured mobile verdict for all five transport-pill render sites in the cloud surfaces"
- "flex-wrap + shrink-0 on the Cloud.vue peer-card badge row, so the transport badge drops to a second line intact instead of having its own text broken mid-label at 320px"
- "neode-ui/src/views/__tests__/TransportPills.test.ts — a site-specific pin that fails the build if any transport pill is removed"
- "A stated, reasoned decision (with evidence) that the file-level rows do NOT carry a per-file transport pill"
affects: [cloud, peer-files]
tech-stack:
added: []
patterns:
- "Overflow-only responsive fix: flex-wrap on a badge row is inert whenever the row already fits, so it changes nothing at any width where the layout was already correct. Verified by diffing measured pill geometry pre/post at 390/320/1440 — identical everywhere except the case being fixed."
- "shrink-0 on the security-relevant badge: when a row must degrade, degrade by wrapping the whole badge rather than by compressing the badge until its label breaks. The transport WORD is the signal; the milliseconds are not."
- "Pin tests keyed to a render site, not to a string: each assertion targets a shape only that site produces (the peer card's `FIPS · 0.4s` form, the `hidden md:block` title block, the `md:hidden` copy), proven bidirectionally by deleting one pill at a time."
key-files:
created:
- neode-ui/src/views/__tests__/TransportPills.test.ts
modified:
- neode-ui/src/views/Cloud.vue
key-decisions:
- "Cloud.vue's aggregated Peer Files rows do NOT get a per-file transport pill. Transport is measured per peer per browse (`cloud.peer-browse:<onion>.transport`), never per file; the aggregated list interleaves files from many peers sorted by filename, so a per-row pill would repeat one peer-level reading dozens of times while implying a per-file measurement the app never took (T-01-78). Each row already carries the peer-name pill and taps straight through to PeerFiles.vue, where the peer-level pill is shown once, correctly."
- "Cloud.vue's Paid Files rows do NOT get a transport pill — the stronger case. These rows come from the local purchase cache (content.owned-list); the bytes are already on this node and the current session may never have browsed that seller at all. Any pill here would be a stale or fabricated claim, a direct violation of the transparency prohibition."
- "PeerFiles.vue's per-file card bodies do NOT get a transport pill. Every file on that page came from the same peer over the same transport, which the header pill already states once."
- "Only Cloud.vue was modified. PeerFiles.vue passed the audit at both narrow widths and was left untouched — the plan forbids changing a site the audit passed."
requirements-completed: [UIFIX-01]
metrics:
duration: ~1h20m
tasks: 2
files-changed: 2
tests-added: 13
---
# 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` 311331 (pre-fix) | **Yes** — 90×24 px pill, in viewport at both 390 and 320 | **Conditionally** — fine with real trust labels (3669 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` 199218 | n/a — **no transport pill exists at any width** | n/a | **No change** — decision recorded below |
| S3 | Paid Files rows | `Cloud.vue` 150178 | 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` 838 | **Yes** — the `md:hidden` copy renders at 390 and 320 (41×20 px, 183253 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` 152232 | 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 5456 character peer names and 6570
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 5456 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**, including `keepAliveTabs.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-xs` and
`inline-flex shrink-0 items-center gap-1.5 px-2 py-1 rounded-full` are both present in the built
`Cloud-*.js` chunk, the `md:hidden` pill class string is intact in `PeerFiles-*.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:
1. The plan's `<output>` says to `git push gitea-ai main`. The execution brief for this run explicitly
forbids pushing, tagging and deploying. **Not pushed** — commit `8255b69a` is local on `main`.
Per CLAUDE.md's commit-and-push rule this work is not "done" until someone pushes it.
2. 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 status` for 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` — FOUND
- `neode-ui/src/views/Cloud.vue` — FOUND, contains `flex flex-wrap items-center gap-2 text-xs` and `inline-flex shrink-0`
- commit `8255b69a` — FOUND in `git log`
- `neode-ui/src/views/PeerFiles.vue` — unmodified, `git status` clean