Files
archy/.planning/phases/01-federation-mesh-hardening/01-03-PLAN.md
T

247 lines
15 KiB
Markdown
Raw Normal View History

2026-08-12 10:55:49 +00:00
---
phase: 01-federation-mesh-hardening
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
- neode-ui/src/components/ScreensaverRing.vue
- neode-ui/src/components/SendBitcoinModal.vue
- neode-ui/src/components/WalletScanModal.vue
- neode-ui/src/components/__tests__/ScreensaverRing.test.ts
- neode-ui/src/components/__tests__/PaidTick.test.ts
autonomous: true
requirements: [FED-06]
must_haves:
truths:
- "The payment-success tick in SendBitcoinModal renders the screensaver EQ-segment ring, not a CSS ripple burst"
- "The payment-success tick in WalletScanModal renders the same EQ-segment ring, so the paid tick is identical on every surface it appears"
- "ScreensaverRing exposes a third badge size variant sized 160px on mobile and 192px from 768px up, with --viz-radius 80px/96px, alongside the untouched default and compact variants"
- "The badge ring fits inside the modal card without clipping — the success pane's ring container is no larger than the badge diameter at either breakpoint"
- "The success amount numerals and SENT / Done copy are unchanged — only the ring geometry behind the checkmark changes"
- "SystemDangerZone and Screensaver continue to render the compact and default variants unchanged"
- statement: "ScreensaverRing's segment animation is disabled under prefers-reduced-motion for every size variant including the new badge, matching the site-wide reduced-motion convention"
verification: backstop
prohibitions:
- statement: "The paid-tick change MUST NOT alter what the success pane asserts about the payment — the ring is decoration; it must never render a success state for a payment that has not actually settled, and no success-gating condition may be relaxed to make the animation easier to trigger"
category: safety
artifacts:
- path: neode-ui/src/components/ScreensaverRing.vue
provides: "badge size variant + reduced-motion guard"
contains: "viz-ring-badge"
- path: neode-ui/src/components/__tests__/PaidTick.test.ts
provides: "Assertions that both paid-tick surfaces render the badge ring"
min_lines: 25
key_links:
- from: neode-ui/src/components/SendBitcoinModal.vue
to: neode-ui/src/components/ScreensaverRing.vue
via: "success pane renders <ScreensaverRing size=\"badge\" /> layered under the checkmark core"
pattern: "ScreensaverRing"
- from: neode-ui/src/components/WalletScanModal.vue
to: neode-ui/src/components/ScreensaverRing.vue
via: "success pane renders <ScreensaverRing size=\"badge\" /> in place of the plain circle"
pattern: "ScreensaverRing"
---
<objective>
Make the invoice/payment "paid" tick on-brand: the circle around the checkmark becomes the
screensaver ring with its outer EQ-segment lines, everywhere the paid tick appears.
Purpose: FED-06, locked by the user in CONTEXT.md — the paid-tick circle is the ScreensaverRing
style, applied consistently to every paid/success tick surface. RESEARCH.md flagged that a naive
drop-in overflows the modal card (the existing compact variant is 240-320px against a 96-112px
badge); 01-UI-SPEC.md resolved that by deciding on a new `badge` size variant rather than a
transform hack, and also recorded that `ScreensaverRing` has no `prefers-reduced-motion` guard at
all today — a real gap this phase must close.
Output: a third size variant plus a reduced-motion guard in the shared component, both paid-tick
call sites swapped, and component tests pinning the result.
</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-PATTERNS.md
@neode-ui/src/components/ScreensaverRing.vue
@neode-ui/src/components/Screensaver.vue
</context>
## Artifacts this phase produces
Created or changed by **this plan**:
| Symbol | Kind | File |
|---|---|---|
| `size: 'default' \| 'compact' \| 'badge'` | widened prop union | `neode-ui/src/components/ScreensaverRing.vue` |
| `.viz-ring-badge` | CSS class (160px / 192px, `--viz-radius` 80px / 96px) | same |
| reduced-motion media guard on `.viz-segment` | CSS | same |
| `neode-ui/src/components/__tests__/ScreensaverRing.test.ts` | new vitest suite | new file |
| `neode-ui/src/components/__tests__/PaidTick.test.ts` | new vitest suite | new file |
<!-- planner-discipline-allow: burst-ring -->
<tasks>
<task type="tracer" tdd="true">
<name>Task 1: End-to-end — the badge ring variant renders as the payment-success tick</name>
<files>neode-ui/src/components/ScreensaverRing.vue, neode-ui/src/components/SendBitcoinModal.vue, neode-ui/src/components/__tests__/ScreensaverRing.test.ts, neode-ui/src/components/__tests__/PaidTick.test.ts</files>
<read_first>
- `neode-ui/src/components/ScreensaverRing.vue` — the whole file (about 115 lines): the
`withDefaults(defineProps<{ size?: ... }>())` union, the `sizeClass` computed, the two
existing size CSS classes with their `min-width: 768px` breakpoints and `--viz-radius`
custom properties, and the `segment-pulse` keyframes.
- `neode-ui/src/components/SendBitcoinModal.vue` lines 1-30 (the success pane markup: the
success-burst container, its three ripple span elements, and the core circle plus checkmark)
and lines 680-740 (the corresponding CSS block, including the existing
`@media (prefers-reduced-motion: reduce)` rule — copy that exact media-query syntax into
ScreensaverRing).
- `neode-ui/src/components/Screensaver.vue` — the existing `ScreensaverRing` + `ScreensaverLogo`
centred-absolute layering pattern (`position: relative` wrapper, `position: absolute; inset: 0`
inner content) to reuse for the checkmark core.
- `neode-ui/src/components/__tests__/BaseModal.test.ts` — the house vitest + `@vue/test-utils`
conventions for mounting a component in this repo.
- `.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md` — the "FED-06 Sizing Decision"
table (exact diameters and radii) and the "UI Considerations" rows for the paid-tick ring.
</read_first>
<behavior>
- `ScreensaverRing.test.ts`: mounting with `size="badge"` puts `viz-ring-badge` on the root
element; mounting with `size="compact"` still yields `viz-ring-compact`; the default mount
still yields `viz-ring-default`; the rendered segment count matches the `segmentCount` prop.
- `PaidTick.test.ts`: SendBitcoinModal driven into its payment-success state renders exactly one
`ScreensaverRing` with `size="badge"`, renders the checkmark core, and renders zero ripple
elements; the success amount text is unchanged.
</behavior>
<action>
Write both test files first and confirm they fail before implementing.
In `ScreensaverRing.vue`: widen the `size` prop union with a third member `'badge'`, extend
`sizeClass` to map it to `viz-ring-badge`, and add a `.viz-ring-badge` CSS rule following the
exact shape of the existing two — `width`/`height` 160px and `--viz-radius: 80px` at mobile,
then a `@media (min-width: 768px)` block with 192px and `--viz-radius: 96px`. Do not touch
`.viz-ring-default` or `.viz-ring-compact`; `Screensaver.vue` and `SystemDangerZone.vue` must
keep their current rendering.
Also inside `ScreensaverRing.vue`, add the missing motion guard so it applies to every variant:
a `@media (prefers-reduced-motion: reduce)` block that sets `animation: none` and a static
reduced opacity on `.viz-segment`. Use the same media-query syntax as the guard already present
in `SendBitcoinModal.vue` so the two read identically.
In `SendBitcoinModal.vue`'s payment-success pane: import `ScreensaverRing`, replace the three
ripple span elements with `<ScreensaverRing size="badge" />`, keep the existing core circle and
checkmark markup untouched, and wrap the pair in the Screensaver-style layering (a
`position: relative` container sized to the badge diameter, with the core absolutely centred over
the ring). Remove the ripple elements' now-dead CSS rules and their keyframes; keep the core and
checkmark rules, and keep the existing reduced-motion rule but drop the clause that referenced
the removed elements. Do not change the success amount numerals, the SENT copy, the Done button,
or any condition that decides when the success pane is shown.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; test -f src/components/__tests__/ScreensaverRing.test.ts &amp;&amp; test -f src/components/__tests__/PaidTick.test.ts &amp;&amp; npx vitest run src/components/__tests__/ScreensaverRing.test.ts src/components/__tests__/PaidTick.test.ts</automated>
</verify>
<acceptance_criteria>
- Both test files exist and `npx vitest run src/components/__tests__/ScreensaverRing.test.ts src/components/__tests__/PaidTick.test.ts` exits 0 (the explicit `test -f` guards are required — `vitest.config.ts` sets `passWithNoTests: true`, so a missing file would otherwise pass vacuously).
- `grep -c 'viz-ring-badge' neode-ui/src/components/ScreensaverRing.vue` is at least 2 (computed mapping + CSS rule).
- `grep -c 'prefers-reduced-motion' neode-ui/src/components/ScreensaverRing.vue` equals 1.
- `grep -Eq '160px' neode-ui/src/components/ScreensaverRing.vue` and `grep -Eq '192px' neode-ui/src/components/ScreensaverRing.vue` both succeed.
- `grep -c 'viz-ring-compact' neode-ui/src/components/ScreensaverRing.vue` is unchanged from before the edit (the compact variant is untouched).
- `grep -c 'ScreensaverRing' neode-ui/src/components/SendBitcoinModal.vue` is at least 2 (import + usage).
- `grep -c 'burst-ring' neode-ui/src/components/SendBitcoinModal.vue` equals 0.
- `cd neode-ui && npx vitest run` exits 0 — no existing suite regressed.
- `cd neode-ui && npm run build` exits 0 and `grep -rq 'viz-ring-badge' ../web/dist/neode-ui/assets/` succeeds (per CLAUDE.md: the build can silently no-op, so grep the built bundle for the new string).
</acceptance_criteria>
<done>The badge variant exists, the send-payment success tick renders it, and both are pinned by tests that failed before the change.</done>
</task>
<task type="auto">
<name>Task 2: Bring the scan-modal paid tick to the same ring</name>
<files>neode-ui/src/components/WalletScanModal.vue, neode-ui/src/components/__tests__/PaidTick.test.ts</files>
<read_first>
- `neode-ui/src/components/WalletScanModal.vue` around line 232 (the success circle markup — a
fixed 24-unit inline-flex circle with the success-ring class) and around line 861 (its CSS
rule). Note it has no ripple animation at all today, unlike the send modal.
- `neode-ui/src/components/SendBitcoinModal.vue` as left by Task 1 — the layering wrapper to
copy verbatim.
- `.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md` — the FED-06 sizing table row
confirming this call site also uses the badge variant.
</read_first>
<action>
Replace WalletScanModal's fixed success circle with the same composition Task 1 established:
a `position: relative` container sized to the badge diameter holding `<ScreensaverRing size="badge" />`
with the existing checkmark content absolutely centred over it. Import `ScreensaverRing`. Drop
the now-unused fixed-size utility classes and the plain-circle CSS rule; keep the checkmark
glyph, its colour, and the surrounding copy exactly as they are.
Extend `PaidTick.test.ts` with a WalletScanModal case asserting its success state renders one
`ScreensaverRing` with `size="badge"` and still renders the checkmark.
Verify on the dev preview before considering this done, per the user requirement recorded in
CONTEXT.md: run the dev preview and confirm neither ring is clipped by the modal card's
scrolling container at a narrow viewport and at desktop width. Record the observation in the
SUMMARY. The blocking human sign-off for this is consolidated into plan 01-07.
</action>
<verify>
<automated>cd neode-ui &amp;&amp; npx vitest run src/components/__tests__/PaidTick.test.ts &amp;&amp; npm run build</automated>
</verify>
<acceptance_criteria>
- `cd neode-ui && npx vitest run src/components/__tests__/PaidTick.test.ts` exits 0 and the suite contains both a SendBitcoinModal case and a WalletScanModal case.
- `grep -c 'ScreensaverRing' neode-ui/src/components/WalletScanModal.vue` is at least 2.
- `grep -c 'success-ring' neode-ui/src/components/WalletScanModal.vue` equals 0.
- `cd neode-ui && npx vitest run` exits 0.
- `cd neode-ui && npm run build` exits 0.
- The SUMMARY records the dev-preview observation for both surfaces at a narrow and a desktop viewport.
</acceptance_criteria>
<done>Both paid-tick surfaces render the identical branded ring, with no clipping at either breakpoint.</done>
</task>
</tasks>
## Planner Assumptions (flagged, unresolved)
- **FED-06 / spec-less probe, category `unclassified`:** the probe surfaced an unclassified edge for
FED-06 that no defensible acceptance criterion covers. Surfaced rather than dropped: the phase
requirement says the ring applies "everywhere the paid tick appears", and a repo-wide grep found
exactly two paid-tick surfaces (`SendBitcoinModal.vue`, `WalletScanModal.vue`). If a third
success-tick surface is added between planning and execution — or exists under markup this grep
did not match — it will not be covered by this plan. The FED-03 review in plan 01-07 re-runs the
grep as a check.
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| payment result → success pane render | The only security-relevant edge: what the UI asserts about a payment's settlement |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-01-08 | Spoofing | success pane rendered for a payment that has not settled | high | mitigate | This plan changes decoration only; the acceptance criteria forbid touching any condition that gates the success pane, and `npx vitest run` on the existing suites must stay green |
| T-01-09 | Denial of Service | 48 animated segments rendered inside a modal degrading low-power devices | low | mitigate | The badge variant is the smallest of the three; the new `prefers-reduced-motion` guard disables the animation entirely for users who ask for it |
| T-01-10 | Repudiation | the success amount or recipient text changing as a side effect of the swap | medium | mitigate | Tests assert the success amount text is unchanged; the action forbids touching the numerals and copy |
</threat_model>
<verification>
- `cd neode-ui && npx vitest run` — green.
- `cd neode-ui && npm run build` — green, and the built bundle contains the new class name.
- Dev-preview observation recorded for both modals at narrow and desktop widths.
</verification>
<success_criteria>
- A third `badge` size variant exists on the shared ring component; existing variants and their consumers are untouched.
- Both paid-tick surfaces render the branded ring with the checkmark layered centred.
- A reduced-motion guard covers every variant.
- Component tests pin all of the above and were proven to fail before the change.
</success_criteria>
<output>
Create `.planning/phases/01-federation-mesh-hardening/01-03-SUMMARY.md` when done.
Stage by explicit path, commit, and `git push gitea-ai main`.
</output>