244 lines
16 KiB
Markdown
244 lines
16 KiB
Markdown
---
|
||
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` 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**, 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
|