Files

247 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 01-federation-mesh-hardening
plan: 17
type: execute
wave: 8
depends_on: ["01-14"]
files_modified:
- neode-ui/src/views/Cloud.vue
- neode-ui/src/views/PeerFiles.vue
- neode-ui/src/views/__tests__/TransportPills.test.ts
autonomous: true
requirements: [UIFIX-01]
gap_closure: true
must_haves:
truths:
- "Every place the cloud surfaces show a file's transport state shows it at mobile widths too — a phone user can see whether a file came over FIPS or over Tor (UIFIX-01)"
- "The pills are pinned by a test, so a future cleanup or refactor that removes one fails the suite instead of shipping (UIFIX-01 — 'kept, never removed')"
- "A peer whose transport is not yet known renders the existing not-known treatment rather than a fabricated pill (UIFIX-01 empty edge)"
- "A pill never truncates into meaninglessness or overlaps its neighbour at the narrowest supported width — it wraps or compacts instead"
- "Desktop rendering of every pill is unchanged: same text, same colours, same position, same spacing"
- "Every render site of the transport pill in the cloud surfaces has a recorded mobile verdict — no site is left unchecked"
prohibitions:
- statement: "The transport pill MUST NOT claim a transport the app has not actually observed — it renders from the recorded result of the last real browse, and a missing or stale reading shows the not-known treatment rather than defaulting to the more reassuring value"
category: transparency
- statement: "Nothing on these views may change except the transport pills' responsive rendering — file rows, peer cards, buttons, badges, counts, tabs and every animation stay exactly as they are, and desktop is untouched"
category: safety
artifacts:
- path: neode-ui/src/views/__tests__/TransportPills.test.ts
provides: "A render-site pin for every FIPS/Tor pill, so removal breaks the build"
min_lines: 40
key_links:
- from: neode-ui/src/views/PeerFiles.vue
to: neode-ui/src/views/Cloud.vue
via: "both read the same recorded browse transport for a peer, so the pill means the same thing wherever it renders"
pattern: "transport"
---
<objective>
Keep the FIPS/Tor pills forever, and make sure a phone shows them.
Purpose: UIFIX-01 is a BLOCKER with two halves. The user explicitly values these pills ("really
helpful") and asked that no future cleanup remove them — that half is solved by pinning them with a
test, which nothing in the repo does today. The other half is that at mobile widths they are hidden or
cramped, so exactly the users least able to judge their connection cannot see whether a file arrived
over the fast encrypted mesh or over Tor. The planner could not determine which specific render site
fails on a phone, so this plan audits every site rather than guessing at one.
Output: a complete, recorded per-site mobile verdict; a fix at every failing site; and a test that
makes their removal a build failure.
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md
@.planning/phases/01-federation-mesh-hardening/01-14-SUMMARY.md
</context>
## Artifacts this phase produces
Created or changed by **this plan**:
| Symbol | Kind | File |
|---|---|---|
| responsive transport-pill rendering | changed template classes at the failing sites | `neode-ui/src/views/Cloud.vue`, `neode-ui/src/views/PeerFiles.vue` |
| `neode-ui/src/views/__tests__/TransportPills.test.ts` | new vitest suite — the "never remove these" pin | new file |
<tasks>
<task type="tracer" tdd="true">
<name>Task 1: Audit every transport-pill site and fix the ones a phone cannot read</name>
<files>neode-ui/src/views/Cloud.vue, neode-ui/src/views/PeerFiles.vue, neode-ui/src/views/__tests__/TransportPills.test.ts</files>
<precondition>The local dev preview can be started (`cd neode-ui && npm run dev:mock` serves the UI on :8100 against the mock backend) with peer data present — jsdom cannot tell you whether a pill is cramped, only whether it exists</precondition>
<read_first>
- `neode-ui/src/views/Cloud.vue` lines 280-324 — the peer cards in the Folders tab. The badge row
is `flex items-center gap-2 text-xs` holding the trust pill and, when
`peerTransport(peer.onion)` is known, the transport pill rendering
`FIPS`/`TOR` plus a latency figure, with a `Peer Node` text fallback when it is not known. Note
the row has no wrapping and no responsive treatment at all.
- `neode-ui/src/views/Cloud.vue` lines 199-218 — the Peer Files aggregated list rows. Each row
shows a category icon, filename, size and price, and a peer-name pill — and no transport pill,
even though these rows are files from peers. Decide, and record, whether this is a site that
should carry one: the requirement is about a user seeing a file's transport state.
- `neode-ui/src/views/Cloud.vue` lines 150-178 — the Paid Files rows, for the same decision.
- `neode-ui/src/views/PeerFiles.vue` lines 8-38 — the header. There is a desktop title block
(`hidden md:block`) carrying the pill, and a separate `md:hidden` copy of the pill added
specifically so mobile still sees it. Read the comment above it: someone already fixed one half
of this. Confirm whether that copy actually renders and is legible on a phone today.
- `neode-ui/src/views/PeerFiles.vue` lines 640-676 — `transportPill`, the single source of the
label, colour classes and tooltip for `fips` / `mesh` / `lan` / `tor` / unknown. This is the
canonical mapping; anything this plan adds must use it rather than re-deriving colours.
- `neode-ui/src/views/PeerFiles.vue` lines 152-232 — the per-file card body, which shows an access
badge and action buttons, for the same site decision.
- `neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts` — the house convention for a
structural pin test, and the file the standing rule requires stay green.
</read_first>
<behavior>
- Mounting each surface with a known transport renders the transport pill with its label text.
- Mounting with an unknown transport renders the existing not-known treatment and no pill.
- Removing the pill from any audited site makes the suite fail — that is the whole point of the
test, so write each assertion so it is specific to a site, not satisfied by any pill anywhere.
- The pill's label and colour come from the canonical mapping, not from a duplicated table.
</behavior>
<action>
Start with the audit, because the fix depends on it. Start the dev preview and open each of the
sites listed above at a phone viewport (390×740 is the reference; also check 320×640, the narrowest
the app supports). For each site record: does the pill render at all, is its text fully readable,
does it overlap or push anything, and does it survive a long peer name or a long filename. Put the
result in the SUMMARY as a table with columns: site, file and line, renders on mobile, legible,
action.
Then fix every site the audit marked as failing, and only those. The likely shapes, depending on
what you find: let the badge row wrap (`flex-wrap`) so a pill drops to a second line instead of
overflowing; drop the latency figure from the pill at small widths while keeping the transport word,
since the word is the security-relevant part and the milliseconds are not; or render a compact pill
variant on mobile the way `PeerFiles.vue`'s header already renders a mobile-specific copy. Choose
per site based on what you actually saw, and record why. Do not apply a responsive change to a site
the audit passed — an unnecessary change to a working desktop layout is exactly what the standing
rule forbids.
Settle the two open site questions rather than leaving them: whether the Peer Files aggregated
rows and the Paid Files rows should carry a transport pill. Both list files that came from peers,
and the requirement is about a user seeing a file's transport state — but the aggregated rows show
files from many peers at once, and a per-row pill may be the honest answer or may be noise. Make a
decision, state the reasoning, and if the answer is yes, implement it using the canonical mapping
and add it to the pin test. If the answer is no, record why the existing peer-level pill is
sufficient for those rows.
Write the pin test as you go: one assertion per confirmed render site, each keyed to something that
identifies that site specifically, plus a comment at the top of the file saying in plain words that
these pills are a user-requested permanent feature and that a failure here means someone removed
one, not that the test is stale.
Change nothing else on either view. Desktop rendering must be untouched at every site, including
the ones you fix.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; test -f src/views/__tests__/TransportPills.test.ts &amp;&amp; npx vitest run src/views/__tests__/TransportPills.test.ts</automated>
</verify>
<acceptance_criteria>
- The test file exists and `cd neode-ui && npx vitest run src/views/__tests__/TransportPills.test.ts` exits 0 (the `test -f` guard is required — `vitest.config.ts` sets `passWithNoTests: true`).
- The suite has at least one site-specific assertion per render site the audit confirmed, and at least one unknown-transport case asserting no pill is fabricated.
- The SUMMARY contains the per-site audit table, with a row for each of the five sites named in `read_first` and an explicit action for each.
- The SUMMARY records the decision and reasoning for the Peer Files aggregated rows and the Paid Files rows.
- `grep -c 'transportPill' neode-ui/src/views/PeerFiles.vue` is unchanged or higher — the canonical mapping was reused, never replaced.
- `cd neode-ui && npx vitest run` exits 0 — every existing suite, including `keepAliveTabs.test.ts`, stays green.
</acceptance_criteria>
<done>Every transport-pill site has a recorded mobile verdict, the failing ones are fixed, and a test makes their removal a build failure.</done>
</task>
<task type="auto">
<name>Task 2: Re-check the fixed sites on a phone viewport and confirm desktop is untouched</name>
<files>neode-ui/src/views/__tests__/TransportPills.test.ts</files>
<precondition>Task 1's changes are in the working tree and the dev preview can be restarted against them</precondition>
<read_first>
- The audit table Task 1 wrote into the SUMMARY — it is the checklist for this task.
- `neode-ui/DEV-SCRIPTS.md` lines 1-40 — starting and stopping the preview.
</read_first>
<action>
Re-open every site the audit marked as fixed at both 390×740 and 320×640 and confirm the pill now
renders fully and legibly, with a long peer name and a long filename present so the overflow case
is actually exercised — if the mock data has no long names, edit the rendered text in the element
inspector to force it rather than changing the mock backend, and say so.
Then confirm desktop is untouched. Open each changed site at 1440×900 and compare against the
pre-change build. State in the SUMMARY that each changed site renders identically on desktop, or
name what moved and fix it — the standing rule is that the only visual change this phase ships is
the one the user asked for.
Finally, confirm the pin does its job: temporarily delete one pill from one site, run the suite,
and confirm it fails. Restore the pill and confirm the suite passes again. Record both results.
A pin that does not fail when the thing it pins is removed is not a pin.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; npx vitest run &amp;&amp; npm run build</automated>
</verify>
<acceptance_criteria>
- `cd neode-ui && npx vitest run` exits 0 and `cd neode-ui && npm run build` exits 0.
- The SUMMARY records the 390×740 and 320×640 re-check for every fixed site, including the long-name case and how it was forced.
- The SUMMARY states explicitly, per changed site, that desktop rendering at 1440×900 is unchanged.
- The SUMMARY records the deliberate-removal check: which pill was removed, that the suite failed, and that it passed again after restoring.
- `git status --short -- neode-ui/src/views/Cloud.vue neode-ui/src/views/PeerFiles.vue` shows no leftover deliberate-removal edit.
</acceptance_criteria>
<done>The pills are readable on the narrowest supported phone, desktop is unchanged, and the pin is proven to actually fail on removal.</done>
</task>
</tasks>
## Planner Assumptions (flagged, unresolved)
- **The planner could not identify which specific site fails on mobile.** Reading the source showed
`PeerFiles.vue`'s header already carries a mobile-specific pill copy added for exactly this reason,
and `Cloud.vue`'s peer-card badge row has no responsive treatment at all — the latter is the most
likely culprit, but "most likely" is not evidence. Task 1 is therefore an audit that fixes what it
finds, rather than a fix aimed at a guessed target. If the audit finds every site already renders
correctly, that is a legitimate outcome for the mobile half — record it with the evidence, and the
"kept, never removed" half of the requirement is still fully delivered by the pin test.
- **Whether the aggregated Peer Files rows and the Paid Files rows should carry their own pill is a
genuine product question**, not something the planner should decide from a file read. Task 1
requires a stated decision with reasoning either way, so the answer is recorded rather than assumed.
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| observed browse transport → security claim shown to the user | The pill is a security signal: it tells the user whether their file moved over the encrypted mesh or over Tor |
| peer-supplied names → rendered alongside the pill | Long or hostile peer names share the row the pill lives in |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-01-78 | Spoofing | a pill claiming a transport that was not actually used, so a user trusts a channel they should not | high | mitigate | The prohibition requires the pill to render only from the recorded browse result; an unknown-transport test case asserts no pill is fabricated, and the canonical mapping is reused rather than duplicated |
| T-01-79 | Information Disclosure | a mobile user unable to see that a file arrived over Tor and acting as though it were the trusted mesh path | high | mitigate | This is the requirement itself; Task 1's audit covers every render site and Task 2 re-checks each fix at the two narrowest supported widths |
| T-01-80 | Tampering | a later cleanup silently deleting the pills again | high | mitigate | The pin test asserts per site, and Task 2 proves the pin actually fails when a pill is removed |
| T-01-81 | Spoofing | a long peer-supplied name pushing the pill off screen so it is effectively absent on mobile | medium | mitigate | Task 2 requires the long-name case to be exercised deliberately at both narrow widths, not just whatever the mock data happens to contain |
| T-01-SC | Tampering | npm/pip/cargo installs | high | mitigate | This plan installs nothing — template class changes and one vitest file. If an implementation choice would add a dependency, stop: RESEARCH.md's Package Legitimacy Audit must cover it first, with a blocking human checkpoint for any `[ASSUMED]`/`[SUS]` entry |
</threat_model>
<verification>
- `cd neode-ui && npx vitest run` — green, including `keepAliveTabs.test.ts`.
- `cd neode-ui && npm run build` — green.
- Per-site audit table plus the 390×740 / 320×640 re-check and the 1440×900 desktop comparison, all recorded.
- The deliberate-removal check confirming the pin fails on removal.
</verification>
<success_criteria>
- Every transport-pill render site has an evidence-backed mobile verdict.
- Every failing site is fixed, and no passing site was touched.
- Desktop rendering is unchanged everywhere.
- A test pins the pills so removing one breaks the build, and that pin is proven to work.
</success_criteria>
<output>
Create `.planning/phases/01-federation-mesh-hardening/01-17-SUMMARY.md` when done, recording the audit
table, the two site decisions with reasoning, the re-check observations, and the deliberate-removal
result.
Stage by explicit path, commit, and `git push gitea-ai main`.
</output>