247 lines
16 KiB
Markdown
247 lines
16 KiB
Markdown
---
|
||||
|
|
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 && test -f src/views/__tests__/TransportPills.test.ts && 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 && npx vitest run && 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>
|