247 lines
15 KiB
Markdown
247 lines
15 KiB
Markdown
---
|
|
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 && test -f src/components/__tests__/ScreensaverRing.test.ts && test -f src/components/__tests__/PaidTick.test.ts && 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 && npx vitest run src/components/__tests__/PaidTick.test.ts && 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>
|