From 829295f326c8d4f01597dade4fbc97be8aad2133 Mon Sep 17 00:00:00 2001 From: Lyons800 <94101691+Lyons800@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:53:20 +0100 Subject: [PATCH] feat(web): land visitors on a welcome page, not inside someone's ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening the site dropped you straight into the demo organisation's dashboard with no explanation of what the product is or whose data you were looking at. Route groups split the two concerns without changing a single existing URL: (marketing) has no sidebar and owns /, (app) carries the workspace chrome and everything else. The dashboard moves to /dashboard. The welcome page leads with the argument rather than the feature list — that 'two people must approve' only ever verified two accounts clicking a button — and names the three refusals with the codes the domain layer actually returns. It says plainly that there is no account system yet and that the demo organisation is shared and synthetic, so nobody mistakes it for their own workspace. Also fixes the mobile nav, which had seven columns for eight items and was dropping one off the bar. --- .../approvals/[id]/approval-scenarios.ts | 0 .../approvals/[id]/approval-simulation.tsx | 8 +- .../app/{ => (app)}/approvals/[id]/page.tsx | 0 apps/web/src/app/{ => (app)}/audit/page.tsx | 10 +- .../src/app/{ => (app)/dashboard}/page.tsx | 14 +- .../web/src/app/{ => (app)}/invoices/page.tsx | 10 +- apps/web/src/app/(app)/layout.tsx | 35 +++++ .../web/src/app/{ => (app)}/payments/page.tsx | 8 +- apps/web/src/app/{ => (app)}/people/page.tsx | 10 +- .../web/src/app/{ => (app)}/policies/page.tsx | 6 +- .../src/app/{ => (app)}/suppliers/page.tsx | 8 +- apps/web/src/app/(marketing)/page.tsx | 129 ++++++++++++++++++ apps/web/src/app/layout.tsx | 18 +-- apps/web/src/components/sidebar.tsx | 4 +- 14 files changed, 204 insertions(+), 56 deletions(-) rename apps/web/src/app/{ => (app)}/approvals/[id]/approval-scenarios.ts (100%) rename apps/web/src/app/{ => (app)}/approvals/[id]/approval-simulation.tsx (96%) rename apps/web/src/app/{ => (app)}/approvals/[id]/page.tsx (100%) rename apps/web/src/app/{ => (app)}/audit/page.tsx (87%) rename apps/web/src/app/{ => (app)/dashboard}/page.tsx (94%) rename apps/web/src/app/{ => (app)}/invoices/page.tsx (95%) create mode 100644 apps/web/src/app/(app)/layout.tsx rename apps/web/src/app/{ => (app)}/payments/page.tsx (86%) rename apps/web/src/app/{ => (app)}/people/page.tsx (98%) rename apps/web/src/app/{ => (app)}/policies/page.tsx (93%) rename apps/web/src/app/{ => (app)}/suppliers/page.tsx (94%) create mode 100644 apps/web/src/app/(marketing)/page.tsx diff --git a/apps/web/src/app/approvals/[id]/approval-scenarios.ts b/apps/web/src/app/(app)/approvals/[id]/approval-scenarios.ts similarity index 100% rename from apps/web/src/app/approvals/[id]/approval-scenarios.ts rename to apps/web/src/app/(app)/approvals/[id]/approval-scenarios.ts diff --git a/apps/web/src/app/approvals/[id]/approval-simulation.tsx b/apps/web/src/app/(app)/approvals/[id]/approval-simulation.tsx similarity index 96% rename from apps/web/src/app/approvals/[id]/approval-simulation.tsx rename to apps/web/src/app/(app)/approvals/[id]/approval-simulation.tsx index 21b7648..114a301 100644 --- a/apps/web/src/app/approvals/[id]/approval-simulation.tsx +++ b/apps/web/src/app/(app)/approvals/[id]/approval-simulation.tsx @@ -2,19 +2,19 @@ import { useReducer } from 'react'; -import { Badge } from '../../../components/ui/badge'; -import { Button } from '../../../components/ui/button'; +import { Badge } from '../../../../components/ui/badge'; +import { Button } from '../../../../components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../../components/ui/card'; +} from '../../../../components/ui/card'; import { approvers, createInitialTheatre, theatreReducer, -} from '../../../lib/demo'; +} from '../../../../lib/demo'; import type { ApprovalScenario } from './approval-scenarios'; diff --git a/apps/web/src/app/approvals/[id]/page.tsx b/apps/web/src/app/(app)/approvals/[id]/page.tsx similarity index 100% rename from apps/web/src/app/approvals/[id]/page.tsx rename to apps/web/src/app/(app)/approvals/[id]/page.tsx diff --git a/apps/web/src/app/audit/page.tsx b/apps/web/src/app/(app)/audit/page.tsx similarity index 87% rename from apps/web/src/app/audit/page.tsx rename to apps/web/src/app/(app)/audit/page.tsx index 194fa57..1583dba 100644 --- a/apps/web/src/app/audit/page.tsx +++ b/apps/web/src/app/(app)/audit/page.tsx @@ -1,13 +1,13 @@ -import { HederaEvidenceDetail } from '../../components/hedera-evidence'; -import { Badge } from '../../components/ui/badge'; +import { HederaEvidenceDetail } from '../../../components/hedera-evidence'; +import { Badge } from '../../../components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../components/ui/card'; -import { auditEvents } from '../../lib/demo'; -import { loadHederaEvidence } from '../../lib/mirror-evidence.server'; +} from '../../../components/ui/card'; +import { auditEvents } from '../../../lib/demo'; +import { loadHederaEvidence } from '../../../lib/mirror-evidence.server'; const kindBadge = { allow: ['Allowed', 'default'], diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/(app)/dashboard/page.tsx similarity index 94% rename from apps/web/src/app/page.tsx rename to apps/web/src/app/(app)/dashboard/page.tsx index fddd74d..093f14b 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/(app)/dashboard/page.tsx @@ -1,14 +1,14 @@ import Link from 'next/link'; -import { HederaEvidenceSummary } from '../components/hedera-evidence'; -import { Badge } from '../components/ui/badge'; -import { Button } from '../components/ui/button'; +import { HederaEvidenceSummary } from '../../../components/hedera-evidence'; +import { Badge } from '../../../components/ui/badge'; +import { Button } from '../../../components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, -} from '../components/ui/card'; +} from '../../../components/ui/card'; import { Table, TableBody, @@ -16,9 +16,9 @@ import { TableHead, TableHeader, TableRow, -} from '../components/ui/table'; -import { auditEvents, dayInvoices, initialQueue } from '../lib/demo'; -import { loadHederaEvidence } from '../lib/mirror-evidence.server'; +} from '../../../components/ui/table'; +import { auditEvents, dayInvoices, initialQueue } from '../../../lib/demo'; +import { loadHederaEvidence } from '../../../lib/mirror-evidence.server'; export const dynamic = 'force-dynamic'; diff --git a/apps/web/src/app/invoices/page.tsx b/apps/web/src/app/(app)/invoices/page.tsx similarity index 95% rename from apps/web/src/app/invoices/page.tsx rename to apps/web/src/app/(app)/invoices/page.tsx index fb0dd31..01cadb9 100644 --- a/apps/web/src/app/invoices/page.tsx +++ b/apps/web/src/app/(app)/invoices/page.tsx @@ -3,14 +3,14 @@ import Link from 'next/link'; import { useState } from 'react'; -import { Badge } from '../../components/ui/badge'; -import { Button } from '../../components/ui/button'; +import { Badge } from '../../../components/ui/badge'; +import { Button } from '../../../components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../components/ui/card'; +} from '../../../components/ui/card'; import { Table, TableBody, @@ -18,13 +18,13 @@ import { TableHead, TableHeader, TableRow, -} from '../../components/ui/table'; +} from '../../../components/ui/table'; import { dayInvoices, HELD_INVOICE_ID, initialQueue, type QueueInvoice, -} from '../../lib/demo'; +} from '../../../lib/demo'; export default function QueuePage() { const [rows, setRows] = useState(initialQueue); diff --git a/apps/web/src/app/(app)/layout.tsx b/apps/web/src/app/(app)/layout.tsx new file mode 100644 index 0000000..ea37129 --- /dev/null +++ b/apps/web/src/app/(app)/layout.tsx @@ -0,0 +1,35 @@ +import type { ReactNode } from 'react'; + +import { Sidebar } from '../../components/sidebar'; + +/** + * The workspace chrome. + * + * Everything under this group is the product operating inside one + * organisation. The marketing group deliberately does not get the sidebar, so + * a first-time visitor lands on an explanation rather than inside someone + * else's ledger. + */ +export default function WorkspaceLayout({ + children, +}: Readonly<{ children: ReactNode }>): ReactNode { + return ( + <> + +
+
+
+

+ Demo organisation · synthetic AP records +

+ +

+ Live evidence · public Hedera Testnet facts only · no real funds +

+
+ {children} +
+
+ + ); +} diff --git a/apps/web/src/app/payments/page.tsx b/apps/web/src/app/(app)/payments/page.tsx similarity index 86% rename from apps/web/src/app/payments/page.tsx rename to apps/web/src/app/(app)/payments/page.tsx index d5479cb..618c606 100644 --- a/apps/web/src/app/payments/page.tsx +++ b/apps/web/src/app/(app)/payments/page.tsx @@ -1,12 +1,12 @@ -import { HederaEvidenceDetail } from '../../components/hedera-evidence'; -import { Badge } from '../../components/ui/badge'; +import { HederaEvidenceDetail } from '../../../components/hedera-evidence'; +import { Badge } from '../../../components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../components/ui/card'; -import { loadHederaEvidence } from '../../lib/mirror-evidence.server'; +} from '../../../components/ui/card'; +import { loadHederaEvidence } from '../../../lib/mirror-evidence.server'; export const dynamic = 'force-dynamic'; diff --git a/apps/web/src/app/people/page.tsx b/apps/web/src/app/(app)/people/page.tsx similarity index 98% rename from apps/web/src/app/people/page.tsx rename to apps/web/src/app/(app)/people/page.tsx index 55df5c0..eba4e26 100644 --- a/apps/web/src/app/people/page.tsx +++ b/apps/web/src/app/(app)/people/page.tsx @@ -2,14 +2,14 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; -import { Badge } from '../../components/ui/badge'; -import { Button } from '../../components/ui/button'; +import { Badge } from '../../../components/ui/badge'; +import { Button } from '../../../components/ui/button'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../components/ui/card'; +} from '../../../components/ui/card'; import { Table, TableBody, @@ -17,14 +17,14 @@ import { TableHead, TableHeader, TableRow, -} from '../../components/ui/table'; +} from '../../../components/ui/table'; import { APPROVER_ROLES, distinctApprovingHumans, findHumanCollisions, type Person, type PersonRole, -} from '../../lib/people'; +} from '../../../lib/people'; const REQUIRED_DISTINCT_HUMANS = 2; const ADDRESS = /^0x[0-9a-fA-F]{40}$/; diff --git a/apps/web/src/app/policies/page.tsx b/apps/web/src/app/(app)/policies/page.tsx similarity index 93% rename from apps/web/src/app/policies/page.tsx rename to apps/web/src/app/(app)/policies/page.tsx index 34f8864..751edd3 100644 --- a/apps/web/src/app/policies/page.tsx +++ b/apps/web/src/app/(app)/policies/page.tsx @@ -1,11 +1,11 @@ -import { Badge } from '../../components/ui/badge'; +import { Badge } from '../../../components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../components/ui/card'; -import { POLICY_VERSION, policyRules } from '../../lib/demo'; +} from '../../../components/ui/card'; +import { POLICY_VERSION, policyRules } from '../../../lib/demo'; export default function PoliciesPage() { return ( diff --git a/apps/web/src/app/suppliers/page.tsx b/apps/web/src/app/(app)/suppliers/page.tsx similarity index 94% rename from apps/web/src/app/suppliers/page.tsx rename to apps/web/src/app/(app)/suppliers/page.tsx index f9bb1ef..61b3cbd 100644 --- a/apps/web/src/app/suppliers/page.tsx +++ b/apps/web/src/app/(app)/suppliers/page.tsx @@ -1,10 +1,10 @@ -import { Badge } from '../../components/ui/badge'; +import { Badge } from '../../../components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle, -} from '../../components/ui/card'; +} from '../../../components/ui/card'; import { Table, TableBody, @@ -12,8 +12,8 @@ import { TableHead, TableHeader, TableRow, -} from '../../components/ui/table'; -import { suppliers } from '../../lib/demo'; +} from '../../../components/ui/table'; +import { suppliers } from '../../../lib/demo'; const standingBadge = { flagged: ['Account change pending', 'destructive'], diff --git a/apps/web/src/app/(marketing)/page.tsx b/apps/web/src/app/(marketing)/page.tsx new file mode 100644 index 0000000..a8f5707 --- /dev/null +++ b/apps/web/src/app/(marketing)/page.tsx @@ -0,0 +1,129 @@ +import Link from 'next/link'; + +export const metadata = { + title: 'Remit · Let agents pay the invoices', + description: + 'Let agents pay the invoices, and prove they paid the right thing.', +}; + +const REFUSALS = [ + { + code: 'ACTION_DIGEST_MISMATCH', + title: 'Change the account after approval', + body: 'The digest is the payment. Change one character and every approval collected against it is void.', + }, + { + code: 'ACTION_HUMAN_NOT_DISTINCT', + title: 'Approve twice from one person', + body: 'Two company logins, two agent wallets, one human. World AgentKit resolves both. Quorum stays at one.', + }, + { + code: 'REPLAY_DETECTED', + title: 'Reuse an approval to reach quorum', + body: 'An approval is single-use and bound to one action. Reusing it is refused, not counted.', + }, +]; + +export default function WelcomePage() { + return ( +
+
+ + R + +

+ Let agents pay the invoices. +
+ Prove they paid the right thing. +

+

+ Every company says it has a control: two people must approve. That + only ever verified that two accounts clicked a button. Point + an agent at it and a second independent approver costs one{' '} + new Wallet(). +

+
+ +
+
+

The 990

+

+ Known supplier, unchanged bank account, within tolerance. The agent + settles it on Hedera unattended. Nobody is asked anything. +

+
+
+

The 10

+

+ Anything that changes where money can go escalates and demands two{' '} + provably distinct humans — established through + World AgentKit, not through two logins. +

+
+
+ +
+

+ The demo is not the happy path. It is watching it refuse. +

+
+ {REFUSALS.map((refusal) => ( +
+

+ {refusal.code} +

+

{refusal.title}

+

{refusal.body}

+
+ ))} +
+

+ Each refusal is returned by the shipped domain package, not asserted + by a script. +

+
+ +
+
+

Have a look around

+

+ There is no account system yet. The demo organisation is a shared, + read-mostly workspace holding synthetic AP records — the Hedera + evidence in it is real and public, the invoices are not. +

+
+
+ + Open the demo organisation + + + See who can approve + + + Read the code + +
+
+ +
+

+ Remit proves the integrity of the authorisation path. It does not + prove that a supplier owns a bank account, that an invoice is genuine, + or that a verification service is truthful. +

+

ETHGlobal Lisbon 2026 · Hedera Testnet · World Chain · ENS

+
+
+ ); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index a98bf0e..5d598ad 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -2,8 +2,6 @@ import type { Metadata } from 'next'; import { Geist_Mono, Hedvig_Letters_Sans } from 'next/font/google'; import type { ReactNode } from 'react'; -import { Sidebar } from '../components/sidebar'; - import './styles.css'; /* midday maps its type to hedvig-sans; the open release is Hedvig Letters. */ @@ -28,21 +26,7 @@ export default function RootLayout({ return ( - -
-
-
-

- Product scenario · synthetic AP records -

- -

- Live evidence · public Hedera Testnet facts only · no real funds -

-
- {children} -
-
+ {children} ); diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index 18be9f6..a2bc749 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -20,7 +20,7 @@ import { cn } from '../lib/utils'; cubic-bezier(0.4,0,0.2,1); icons stay fixed-left, labels reveal. */ const items = [ - { href: '/', icon: LayoutGrid, label: 'Overview' }, + { href: '/dashboard', icon: LayoutGrid, label: 'Overview' }, { href: '/invoices', icon: ReceiptText, label: 'Invoices' }, { href: '/approvals/INV-2026-0912', @@ -126,7 +126,7 @@ export function Sidebar() {