From 7adac3f71817532c047615c4ebc8a877656bda43 Mon Sep 17 00:00:00 2001
From: Matvii Nesterenko <51422901+kapustazh@users.noreply.github.com>
Date: Wed, 9 Sep 2026 13:41:59 +0200
Subject: [PATCH] feat(web): implement Gate P5
---
...909T111228Z-gate-p5-frontend-acceptance.md | 61 ++++
.github/workflows/stack-lint.yml | 8 +
.gitignore | 2 +
README.md | 8 +-
apps/web/README.md | 14 +-
apps/web/index.html | 2 +-
apps/web/package.json | 6 +-
apps/web/playwright.config.ts | 18 ++
apps/web/src/App.tsx | 124 ++++++--
apps/web/src/api/recovery-client.ts | 273 ++++++++++++++++
apps/web/src/styles.css | 24 ++
apps/web/test/app-composition.test.tsx | 52 +++
apps/web/test/gate-p5.spec.ts | 238 ++++++++++++++
apps/web/test/recovery-client.test.ts | 143 +++++++++
apps/web/tsconfig.json | 6 +-
apps/web/vite.config.ts | 17 +
apps/web/vitest.config.ts | 17 +
docs/GATE_P5_CHECKLIST.md | 72 +++++
package.json | 2 +-
.../contracts/generated/contracts.schema.json | 297 ++++++++++++++++++
packages/contracts/openapi/openapi.v1.json | 297 ++++++++++++++++++
.../contracts/scripts/generate-contracts.mjs | 165 ++++++++++
packages/contracts/src/generated/api-types.ts | 55 ++++
packages/reconciliation/src/service.ts | 5 +-
packages/recovery-ui/src/DemoShell.tsx | 4 +-
packages/recovery-ui/src/RecoveryRoute.tsx | 52 +--
packages/recovery-ui/src/RecoveryTimeline.tsx | 31 +-
packages/recovery-ui/src/mock-server.ts | 1 +
packages/recovery-ui/src/styles.css | 6 +-
packages/settlement-ui/src/DemoShell.tsx | 12 +-
.../src/SettlementDetailsRoute.tsx | 13 +-
packages/settlement-ui/src/styles.css | 2 +
packages/storage-postgres/src/ledger.ts | 170 +++++++++-
.../test/ledger.integration.test.ts | 52 +++
plan.md | 2 +-
plan_missing_parts.md | 25 +-
pnpm-lock.yaml | 34 ++
vitest.config.ts | 7 +-
38 files changed, 2205 insertions(+), 112 deletions(-)
create mode 100644 .agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md
create mode 100644 apps/web/playwright.config.ts
create mode 100644 apps/web/src/api/recovery-client.ts
create mode 100644 apps/web/test/app-composition.test.tsx
create mode 100644 apps/web/test/gate-p5.spec.ts
create mode 100644 apps/web/test/recovery-client.test.ts
create mode 100644 docs/GATE_P5_CHECKLIST.md
diff --git a/.agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md b/.agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md
new file mode 100644
index 0000000..f332d4a
--- /dev/null
+++ b/.agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md
@@ -0,0 +1,61 @@
+# Session Context: Gate P5 Frontend Acceptance
+
+## Date/time
+
+- UTC: 2026-09-09T11:12:28Z
+
+## User goal
+
+Implement project Gate P5 according to `plan.md`, after ensuring the Recovery
+Agent decision exists before frontend Graph MCP composition.
+
+## Key decisions
+
+- Compose A05/B05/C05 in the A-owned shell using the configured frozen API.
+- Return the latest persisted Recovery Agent and deterministic-core result from
+ the recovery view; never hard-code an Agent decision.
+- Keep Subgraph MCP and model output advisory with settlement permission
+ `NEVER`.
+- Hide unsupported operator escalation in production instead of simulating a
+ successful external effect.
+- Scope B05/C05 CSS at package boundaries to prevent cross-slice overrides.
+
+## Files/components touched
+
+- `apps/web`: composed shell, recovery API projection, Playwright acceptance,
+ responsive/token safety checks.
+- `packages/contracts`: additive recovery decision and Graph observation view.
+- `packages/reconciliation`: persist Agent boundary acceptance and explanation.
+- `packages/storage-postgres`: project latest durable recovery command pack.
+- `packages/recovery-ui`, `packages/settlement-ui`: scoped composition styles;
+ recovery escalation capability flag.
+- CI, plan/status documentation, and Gate P5 evidence.
+
+## Commands/checks
+
+- Full workspace build/test: 58 files and 904 tests PASS.
+- Lint, typecheck, generated contracts, fixtures: PASS.
+- Web unit/component: 31 PASS.
+- Reconciliation: 84 PASS.
+- Storage unit: 8 PASS.
+- Playwright Chromium: 4 PASS.
+- Cloud Run `/health/ready`: HTTP 200, `{"status":"ok"}`.
+- Docker is unavailable locally; PostgreSQL integration test is added for CI.
+
+## Git and PR state
+
+- Branch: `feat/gate-p5-frontend-acceptance`
+- Base: `origin/develop` at `779c6cf`
+- Commit: uncommitted
+- PR: not created
+- CI: not run
+
+## Review gates
+
+- Gate A: NOT RUN
+- Gate B: NOT RUN
+
+## Handoff/next steps
+
+1. Run full validation and FreePi Gate A.
+2. Push a draft PR, await exact-head CI, run Gate B, and hand to a human.
diff --git a/.github/workflows/stack-lint.yml b/.github/workflows/stack-lint.yml
index 7f22bac..da04536 100644
--- a/.github/workflows/stack-lint.yml
+++ b/.github/workflows/stack-lint.yml
@@ -107,6 +107,14 @@ jobs:
if: steps.workspace.outputs.enabled == 'true'
run: pnpm test
+ - name: Install Chromium
+ if: steps.workspace.outputs.enabled == 'true'
+ run: pnpm --filter @oneshot/web exec playwright install --with-deps chromium
+
+ - name: Run Gate P5 browser acceptance
+ if: steps.workspace.outputs.enabled == 'true'
+ run: pnpm --filter @oneshot/web test:browser
+
- name: Run PostgreSQL integration tests
if: steps.workspace.outputs.enabled == 'true'
env:
diff --git a/.gitignore b/.gitignore
index bfb07c1..70f3c70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,8 @@ dist/
site-dist/
build/
coverage/
+playwright-report/
+test-results/
node_modules/
.venv/
venv/
diff --git a/README.md b/README.md
index 126bd61..b3776fa 100644
--- a/README.md
+++ b/README.md
@@ -202,9 +202,9 @@ Under active development. **Testnet only.**
| --------------------------------------------- | ----------------------------------------------------------------------------------- |
| Durable intent ledger, API, worker | Implemented |
| Settlement adapters and error taxonomy | Implemented; simulator-tested and live-verified on Arc Testnet through Privy |
-| Recovery evidence and safety core | Implemented against simulators |
-| Subgraph MCP discovery and LLM recovery agent | Implemented boundary; live path not verified |
-| Operator frontend | Intent/status UI and synthetic recovery viewer implemented; live API wiring pending |
+| Recovery evidence and safety core | Live Graph/Vertex path implemented; deterministic core remains authoritative |
+| Subgraph MCP discovery and LLM recovery agent | Live Subgraph MCP and Vertex AI path verified; deterministic core remains final |
+| Operator frontend | Gate P5 candidate composes A05/B05/C05 against the frozen API with browser coverage |
**One live testnet settlement has been executed.** A Privy-controlled execution
wallet and scoped policy authorized one 1.00 USDC Arc Testnet transfer; live
@@ -213,7 +213,7 @@ drill entered `UNKNOWN` and reconciled to that original settlement without a
replacement payment. Privy and Arc are `QUALIFIED` for the documented testnet
claim; see `docs/settlement/LIVE_EVIDENCE.md` and
`packages/reconciliation/docs/c06/QUALIFICATION_REPORT.md`. The Graph live
-Subgraph MCP and recovery-agent path remains `NOT VERIFIED`.
+Subgraph MCP and recovery-agent path is also `QUALIFIED` by the latter report.
Arc Mainnet is not configured. Its profile carries no chain ID, RPC, explorer,
or token value by design, and enabling it requires published official values
diff --git a/apps/web/README.md b/apps/web/README.md
index 68a3ceb..c9ea0fc 100644
--- a/apps/web/README.md
+++ b/apps/web/README.md
@@ -1,6 +1,7 @@
# OneShot web
-Minimal operator UI for creating or replaying a Business Intent and reading its authoritative status.
+Gate P5 operator UI composing intent creation/status, Privy and Arc settlement
+details, and Recovery Agent/Subgraph MCP evidence.
The Cloudflare asset deployment serves this app at the domain root and the
recovery fixture viewer from `@oneshot/recovery-ui` at `/recovery/`. Deploy it
@@ -22,4 +23,13 @@ pnpm build:frontend
This emits the main app to `apps/web/dist` and the recovery viewer to
`apps/web/dist/recovery`, matching the Wrangler asset directory.
-The client consumes generated `@oneshot/contracts` types from frozen OpenAPI v1. Tests use deterministic fetch responses matching that contract.
+The clients consume generated `@oneshot/contracts` types from frozen OpenAPI v1.
+The service token remains in React memory and is never written to browser
+storage. Run the Chromium acceptance suite with:
+
+```powershell
+pnpm --filter @oneshot/web test:browser
+```
+
+See [`../../docs/GATE_P5_CHECKLIST.md`](../../docs/GATE_P5_CHECKLIST.md) for the
+covered states and safety boundary.
diff --git a/apps/web/index.html b/apps/web/index.html
index d855379..5fa209f 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -3,7 +3,7 @@
- OneShot — Intent & Authoritative Status
+ OneShot — Operator Control
diff --git a/apps/web/package.json b/apps/web/package.json
index f8d4ab1..68b919e 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -7,17 +7,21 @@
"build": "tsc -b && vite build",
"clean": "tsc -b --clean",
"dev": "vite",
- "lint": "eslint src test",
+ "lint": "eslint src test playwright.config.ts",
"preview": "vite preview",
"test": "vitest run --config vitest.config.ts",
+ "test:browser": "playwright test",
"typecheck": "tsc -b --pretty false"
},
"dependencies": {
"@oneshot/contracts": "workspace:*",
+ "@oneshot/recovery-ui": "workspace:*",
+ "@oneshot/settlement-ui": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
+ "@playwright/test": "1.63.0",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7",
"@types/node": "24.13.3",
diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts
new file mode 100644
index 0000000..f00cd22
--- /dev/null
+++ b/apps/web/playwright.config.ts
@@ -0,0 +1,18 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './test',
+ testMatch: '**/*.spec.ts',
+ fullyParallel: true,
+ reporter: 'line',
+ use: {
+ baseURL: 'http://127.0.0.1:4173',
+ trace: 'retain-on-failure',
+ },
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
+ webServer: {
+ command: 'pnpm dev --host 127.0.0.1 --port 4173',
+ url: 'http://127.0.0.1:4173',
+ reuseExistingServer: false,
+ },
+});
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index ce425ab..643bde9 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -1,30 +1,68 @@
import { useMemo, useState } from 'react';
+import {
+ createSettlementClient,
+ SettlementDetailsRoute,
+ type SettlementClient,
+} from '@oneshot/settlement-ui';
+import { RecoveryRoute, type RecoveryClient } from '@oneshot/recovery-ui';
+import '@oneshot/recovery-ui/styles.css';
+import '@oneshot/settlement-ui/styles.css';
import { OneShotApiClient } from './api/client.js';
+import { createApiRecoveryClient } from './api/recovery-client.js';
import { ErrorBoundary } from './components/ErrorBoundary.js';
import { IntentForm } from './components/IntentForm.js';
import { IntentStatusView } from './components/IntentStatusView.js';
import { ReadinessBanner } from './components/ReadinessBanner.js';
import './styles.css';
-type Tab = 'create' | 'status';
+type Tab = 'create' | 'status' | 'settlement' | 'recovery';
-export function App() {
+export interface AppProps {
+ readonly apiClient?: OneShotApiClient;
+ readonly settlementClient?: SettlementClient;
+ readonly recoveryClient?: RecoveryClient;
+}
+
+const TABS: readonly { readonly id: Tab; readonly label: string }[] = [
+ { id: 'create', label: 'Create or replay' },
+ { id: 'status', label: 'Authoritative status' },
+ { id: 'settlement', label: 'Settlement details' },
+ { id: 'recovery', label: 'Recovery evidence' },
+];
+
+export function App(props: AppProps = {}) {
const [activeTab, setActiveTab] = useState('create');
const [selectedIntentId, setSelectedIntentId] = useState('');
const [authToken, setAuthToken] = useState('');
- const client = useMemo(
+ const apiBaseUrl = import.meta.env.VITE_ONESHOT_API_BASE_URL ?? '';
+ const apiClient = useMemo(
+ () =>
+ props.apiClient ??
+ new OneShotApiClient({ baseUrl: apiBaseUrl, getAuthToken: () => authToken.trim() || null }),
+ [apiBaseUrl, authToken, props.apiClient],
+ );
+ const settlementClient = useMemo(
+ () =>
+ props.settlementClient ??
+ createSettlementClient({
+ baseUrl: apiBaseUrl,
+ getAuthToken: () => authToken.trim() || null,
+ }),
+ [apiBaseUrl, authToken, props.settlementClient],
+ );
+ const recoveryClient = useMemo(
() =>
- new OneShotApiClient({
- baseUrl: import.meta.env.VITE_ONESHOT_API_BASE_URL ?? '',
+ props.recoveryClient ??
+ createApiRecoveryClient({
+ baseUrl: apiBaseUrl,
getAuthToken: () => authToken.trim() || null,
}),
- [authToken],
+ [apiBaseUrl, authToken, props.recoveryClient],
);
- function showStatus(intentId: string): void {
+ function selectIntent(intentId: string): void {
setSelectedIntentId(intentId);
- setActiveTab('status');
}
return (
@@ -34,7 +72,7 @@ export function App() {
ONESHOT / ARC TESTNET
One job. Many retries. One settlement.
Create a stable payment intent and follow its authoritative state.
-
+
@@ -49,34 +87,52 @@ export function App() {
Memory only. Sent as Bearer authorization.
-