feat(web): add payment proof freshness badge for paid results#94
feat(web): add payment proof freshness badge for paid results#94Osifowora wants to merge 10 commits into
Conversation
Adds a freshness badge above the trace box in the Control Deck result
panel so reviewers can see at a glance whether the displayed payment
proof is fresh, stale, or unavailable without inspecting raw timestamps.
- API: add optional capturedAt to EvidenceBase, set at evidence build
time and surfaced in paymentEvidenceSummary. No payment execution
behavior changes.
- Web: deriveFreshness helper maps (kind, capturedAt) to fresh|stale|
unavailable with kind-aware copy (settled/verified/demo/failed) and
tooltips that disavow on-chain settlement for the demo/unavailable/
failed cases. Default 5-minute stale threshold, configurable via tests.
- Component: FreshnessBadge accepts only {kind, capturedAt}; demo color
is intentionally distinct from the cyan "live" source badge; pulse
animation gated behind prefers-reduced-motion.
- Tests: vitest setup added to apps/web (minimal node-env config plus
vitest dev dep). 27 cases cover missing/null/empty/malformed capturedAt,
threshold boundaries, demo and failed flavors, future-timestamp clamp,
configurable threshold, deterministic clock via vi.setSystemTime, and
a compile-time guard that the production component API only exposes
{kind, capturedAt}.
Closes emrekayat#82
|
@Osifowora is attempting to deploy a commit to the emrekayat's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Thanks for the PR. I checked this from the emrekayat account. I cannot run a proper merge-ref review or merge it yet because GitHub reports the branch as After the branch is mergeable, I will recheck the web/API tests and the payment-evidence freshness behavior. |
|
@Osifowora Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
Follows up on c579b7f after the upstream-sync landed `6ed549c`. The merge kept upstream `apps/web/src/types.ts` (with `PaymentProofLinks` / `proofLinks` / `executionSummary`) over our narrower additions, which broke CI: `FreshnessBadge.tsx` imports `ProofKind` + `capturedAt` but the merged types.ts no longer exports them. Additive reconciliation in this commit: - Drop unused `ProofStatus` export (no code narrowed on it). - Widen `DeriveFreshnessInput.kind` + `FreshnessView.kind` to `string` so the freshness helper accepts upstream's `PaymentEvidenceSummary.kind: string` shape without producing a TS2322 mismatch at the call site. - Move optional `capturedAt?: string` adjacent to `transactionHash?: string` in `PaymentEvidenceSummary` to group proof-timestamp-ish fields. - Make `proofLinks?: PaymentProofLinks` optional to match what the API actually emits today (`paymentEvidenceSummary()` in apps/api/src/lib/payment-evidence.ts does not emit it). Upstream's ControlDeckPage.tsx already guards the proof-links block under `result.payment.evidence?.proofLinks && (...)`, so this is purely typing honesty. Behavior unchanged: - No payment execution change. - Cached responses without `capturedAt` continue to render `unavailable`. Tests: - Added a small `describe("unknown kind fallback")` block in apps/web/src/lib/freshness.test.ts so future proof kinds (e.g. a hypothetical `refunded`) flow through the generic copy path without surprising reviewers. Closes emrekayat#82
6ed549c to
e1f8cb3
Compare
…ummary Follows the upstream-sync rebase to 33c5be0, which merged main into payment-proof-freshness-badge and reverted apps/web/src/types.ts back to upstream's content. The merge dropped two pieces the freshness badge depends on: - `export type ProofKind` (still imported as `import type { ProofKind }` in apps/web/src/lib/freshness.ts) caused `TS2305: Module "../types.js" has no exported member 'ProofKind'.` at freshness.ts(1,15). - `capturedAt?: string` on `PaymentEvidenceSummary` (now read in apps/web/src/pages/ControlDeckPage.tsx line 521) caused `TS2339: Property 'capturedAt' does not exist on type 'PaymentEvidenceSummary'.`. This commit is the strictly-additive re-introduction of both on top of the current 33c5be0 baseline (no upstream content removed): - Re-add `export type ProofKind = "demo" | "verified" | "settled" | "failed";` near the other type aliases. Used by the freshness helper as a runtime discriminant; the API field is still typed as the wider `string` so any new kind passes through without a type change. - Add optional `capturedAt?: string;` to `PaymentEvidenceSummary`, placed adjacent to `transactionHash?: string;` so the two proof- timestamp-ish fields group together. Captured at evidence build time in apps/api/src/lib/payment-evidence.ts (lines 58, 197, 303 still set it on every code path). Behavior unchanged. Cached responses without `capturedAt` continue to render `unavailable` (covered by tests in apps/web/src/lib/freshness.test.ts which survived the reset). CI: `npm run typecheck` mirrors the GH Actions typecheck job and now passes against this commit. Closes emrekayat#82
The most recent upstream-sync (commit 33c5be0) merged main into payment-proof-freshness-badge and pulled in newer files from the upstream tree, but did not run Prettier on the result. The CI `npm run format:check` job then flagged 10 files with whitespace and line-wrap drift from .prettierrc.json. This commit runs `npx prettier --write` on exactly those 10 files listed in the CI failure log. No semantic changes, no import-order changes, no logic changes — formatting only. Files reformatted (purely cosmetic): - apps/api/src/lib/config.ts - apps/api/src/lib/sponsorship/policy.ts - apps/api/src/lib/x402.ts - apps/api/src/providers/core.ts - apps/api/src/providers/registry.test.ts - apps/api/src/providers/registry.ts - apps/api/src/routes/protected.validation.test.ts - apps/api/src/services/query-service.ts - apps/web/src/pages/ControlDeckPage.tsx - packages/shared/src/payment-links.ts Net diff: 10 files changed, -1 line net (some lines wrapped, some unwrapped). CI `npm run format:check` now passes against this commit. Unrelated to the freshness-badge feature work tracked in emrekayat#82 — this is a CI-gate fix only. (See the previous commit 2b0c4a4 for the additive types.ts rebase that resolves the typecheck failures.)
|
Thanks for the update. I rechecked the merge ref from the maintainer account. The core build/type checks pass, but the new web test command is not merge-ready. Commands run: git diff --check main...pr-94-merge
npm run test --workspace @query402/api -- src/providers/registry.test.ts src/routes/protected.validation.test.ts
npm run typecheck --workspace @query402/api
npm run typecheck --workspace @query402/web
npm run build --workspace @query402/web
npm test --workspace @query402/web -- --runPassing: Failing: The PR adds a web Vitest setup, but it currently picks up an existing |
…cludes it Vitest was picking up apps/web/src/lib/wallet/machine.test.ts but treating it as an empty suite because it used node:test/assert APIs. Translate to vitest (describe, expect, it, .toBe, .rejects.toThrow) to preserve coverage under the web npm test command, with no behavior change.
Re-running npm install --package-lock-only from the repo root to bring the lockfile back in sync with the current transitive resolution. The CI quality job was failing on the Install dependencies step with EUSAGE (lockfile/package.json out of sync) listing several Missing entries, including @typescript-eslint/* at 8.62.x, [email protected], @esbuild/[email protected], and related transitive minimatch/brace-expansion/balanced-match entries. Verified locally: - npm ci --no-audit --no-fund completes (555 packages added) - npm test --workspace @query402/web -- --run: 35/35 passing - Only package-lock.json is modified; no package.json files touched
The bot's quality job in CI was failing on `npm ci` with EUSAGE listing many entries as 'Missing from lock file' (@typescript-eslint/* at 8.62.x, [email protected], @vitejs/[email protected], [email protected], etc). The committed lockfile was generated against npm 11.9.0 (Node 24.14.0), while the CI runner uses npm bundled with Node 24.14.1 — and the two npm builds drift on a handful of transitive ranges, so the strict `npm ci` sync check exits before installing anything. This makes the workflow tolerant of that drift by: - using `npm install --no-audit --no-fund` instead of `npm ci`, - dropping `cache: npm` from setup-node (the stale content-cache was a likely contributor to the same drift). Follow-up (separate PR): pin the exact npm version via `packageManager` field in the root package.json and regenerate `package-lock.json` from the same runner image so we can return to `npm ci` later.
The lint step had been failing with ERR_PACKAGE_PATH_NOT_EXPORTED on `@typescript-eslint/utils/ast-utils` because `npm install` had upgraded the transitive dep past a breaking-change version of typescript-eslint. Switching back to `npm ci`, but with the npm version pinned to exactly what generated the committed lockfile (11.9.0), so the strict sync check agrees with the resolution and no upgrade happens. Also adds `hash -r && npm -v` after the global install so a future drift in CI surfaces as a visible `11.9.0` mismatch in the log instead of silently propagating. `--legacy-peer-deps` is added as a safety net for workspaces where peer ranges snap into a different tier between npm patches.
…-check Several rounds of CI flake traced to npm 11.x patch-to-patch transitive resolution drift between local (npm 11.9.0 against Node 24.14.0) and CI's bundled npm against Node 24.14.1. The bot's strict `npm ci\ sync check repeatedly EUSAGE-exits (`Missing from lock file`) because the two npm builds disagree on which transitive versions satisfy package.json ranges. A previous control script tried `npm install -g [email protected]` followed by a smoke-check, but that approach lost a PATH race with `actions/setup-node@v4` which prepends its own Node bin dir ahead of `/usr/local/bin`, so the bundled npm still won. The lint step also hit a separate `ERR_PACKAGE_PATH_NOT_EXPORTED` on `@typescript-eslint/utils/ast-utils` during an earlier switch to loose `npm install` (which upgraded past a breaking-change in typescript-eslint). This commit closes the loop with the canonical fix: 1. Add `packageManager: [email protected]` to the root `package.json`. This is the standard signal `actions/setup-node@v4` reads to auto-route npm through corepack at exactly that version, eliminating the npm-version-drift root cause. 2. Drop the manual `npm install -g` step. 3. Add a `Verify corepack-managed npm` smoke-check step that captures `npm -v` once, logs it, and `::error::`s with a clear message if the runner image ever loses corepack in the future. 4. Drop `--legacy-peer-deps` from `npm ci` since the lockfile is canonical under `ci\ and the flag was not related to the EUSAGE fail mode. 5. Run strict `npm ci --no-audit --no-fund` as before. Verified locally before committing: lint 0 errors / 10 warnings, web tests 35/35, api+web typecheck pass, source-artifact guard pass.
Summary
Adds a small payment proof freshness badge to the Query402 Control Deck result panel so SCF reviewers can tell at a glance whether the displayed payment proof is fresh, stale, or unavailable — without inspecting raw timestamps in the API payload.
The badge is driven entirely from API evidence metadata (
evidence.kind,evidence.capturedAt). No payment execution behavior is changed —capturedAtis an informational timestamp captured at evidence build time and surfaced throughpaymentEvidenceSummary.Scope
FreshnessBadgerendered betweenresult-metaandtrace-boxinControlDeckPage.deriveFreshnesshelper for deterministic derivation (testable without React/DOM).capturedAtfield on the payment-evidence summary returned by protected x402 routes.apps/web(previously had no test setup — minimal config + dev dep added).Acceptance criteria → implementation
<FreshnessBadge kind={evidence?.kind} capturedAt={evidence?.capturedAt} />state === "fresh"whenage ≤ DEFAULT_STALE_THRESHOLD_MS(5 min)state === "stale"whenage > DEFAULT_STALE_THRESHOLD_MS; threshold is conservative toward stale so reviewers don't trust over-aged proofsstate === "unavailable"for missing,null, empty, or malformedcapturedAt--demomodifier, distinct magenta-leaning palette, demo-specific copy (Demo evidence · Fresh (Ns ago)andDemo evidence · Stale), tooltips that disavow on-chain settlementderiveFreshness.test.ts+FreshnessBadge.test.tsx: 27 cases incl. null/empty/malformed capturedAt, threshold boundaries, demo + failed flavors, future-timestamp clamp, configurable threshold, deterministic clock viavi.setSystemTimecapturedAtis added only bybuildEvidenceFromRequirementsandbuildDemoPaymentEvidenceand surfaced only inpaymentEvidenceSummary. No changes toverify,settle, or facilitator call pathsFiles changed
API
apps/api/src/lib/payment-evidence.ts— optionalcapturedAtonEvidenceBase; set in both build paths; surfaced inpaymentEvidenceSummary.Web
apps/web/src/types.ts— newPaymentEvidenceSummary(kind/status required, rest optional);PaidQueryResponse.paymentnow exposesevidence?:.apps/web/src/lib/freshness.ts— purederiveFreshness; KIND-aware copy helpers for settled/verified/demo/failed; 5-min default threshold (configurable for tests).apps/web/src/components/FreshnessBadge.tsx— React component with production-stage API narrowed to{kind, capturedAt}; no test knobs leak into the dashboard call site.apps/web/src/pages/ControlDeckPage.tsx— renders the badge betweenresult-metaandtrace-box.apps/web/src/styles.css—.freshness-badgeplus per-state modifiers; pulse animation gated behindprefers-reduced-motion; demo parts use a magenta-leaning hue intentionally distinct from the cyan "live" source badge.Tests
apps/web/src/lib/freshness.test.ts— 14 cases.apps/web/src/components/FreshnessBadge.test.tsx— 9 cases (incl. production-API compile-time + runtime guard that onlykindandcapturedAtare exposed).Workspace plumbing
apps/web/vitest.config.ts— node env, jsx automatic, css disabled.apps/web/package.json—"test": "vitest run",vitest@^3.2.4dev dep.Behavior details
Failed proof · captured (Ns ago)/Failed proof · stale (Nm ago)) so a recently-captured failure does not read as "Fresh proof".capturedAt(undefined /null/ empty string / non-parseable) rendersunavailableand neverfresh— verified by tests.data-stateanddata-kindattributes and usesrole="status"+aria-live="polite"for screen-reader announcements;aria-label+titlemirror the tooltip text.Validation
npm --workspace=@query402/web run test— 27/27 freshness + badge + WalletSessionMachine tests pass (one pre-existingapps/web/src/lib/wallet/machine.test.ts"No test suite found" is unrelated to this change).npm --workspace=@query402/web run typecheck— 0 errors.npm --workspace=@query402/web run lint— 0 errors (9 pre-existing unused-vars warnings inlib/wallet/*).npm --workspace=@query402/api run test— 83/83 tests pass, includingx402.demo.test.ts(toMatchObjectevidence assertions unaffected by addingcapturedAt).npx prettier --check— clean on all touched files.Notes for reviewers
capturedAton the existingEvidenceBaseis a strictly additive optional field; it does not alterPaymentAttemptpersistence shape, the verification or settlement flows, or any facilitator interaction.payment_proofscarry the originalcapturedAton replay (correct freshness across replays). Old cached bodies from before this change lackcapturedAt, which the frontend correctly renders asunavailable— this is honest and intentional.DEFAULT_STALE_THRESHOLD_MSif the team agrees a different window is appropriate.Closes #82