Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/console/src/app/NetworkMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Top-bar network indicator + chain info **read-only, not a switcher**. Benzo
* Top-bar network indicator + chain info, **read-only, not a switcher**. Benzo
* Console runs on a single chain (the permissioned BenzoNet L1 by default; pinned at
* build time), so this shows the active network's identity, environment, and details
* (chain id / RPC / explorer). There is no network selection: the console isn't
Expand All @@ -19,7 +19,7 @@ function envToneCls(env: NetworkEnv): string {
: "border-warning/30 bg-warning/12 text-[#9a6b12]";
}

/** Explicit environment badge Testnet / Mainnet / Permissioned L1. */
/** Explicit environment badge, Testnet / Mainnet / Permissioned L1. */
function EnvBadge({ env }: { env: NetworkEnv }) {
return (
<span className={`flex-none rounded-full border px-1.5 py-px text-[10px] font-semibold uppercase tracking-wide ${envToneCls(env)}`}>
Expand Down Expand Up @@ -69,11 +69,11 @@ export function NetworkMenu({ live }: { live: boolean }) {
};
}, [open]);

// Everything is keyed to the single build-time NETWORK there is no selection.
// Everything is keyed to the single build-time NETWORK, there is no selection.
const info = chainInfo(NETWORK);
const env = NETWORK_ENV_BY_NETWORK[NETWORK];
// Chrome by ENVIRONMENT (never by liveness): amber for testnet / permissioned L1,
// green only for real-money mainnet. Liveness is a separate axis a subtle heartbeat
// green only for real-money mainnet. Liveness is a separate axis, a subtle heartbeat
// when connected, a red "Offline · …" when the chain is unreachable.
const chipTone = live ? envToneCls(env) : "border-danger/30 bg-danger/10 text-[#b4232a]";
const chipLabel = live ? env.chip : `Offline · ${env.badge}`;
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/app/RootGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function RootGate() {
const { session, loading, refresh } = useConsole();
const isDesktop = useIsDesktop();
// Demo mode skips SIWE onboarding entirely and boots into the Shell (the
// desktop-only gate below still applies this stays a desktop product).
// desktop-only gate below still applies, this stays a desktop product).
const [onboarded, setOnboarded] = useState(() => DEMO_MODE || localStorage.getItem("benzo.console.onboarded") === "1");
const [orgOnboarding, setOrgOnboarding] = useState(false);
function finish() {
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/app/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function Shell() {
<div className="relative flex-none">
<button
onClick={() => setMenu((m) => (m === "bell" ? null : "bell"))}
aria-label={pending ? `Notifications ${pending} awaiting approval` : "Notifications"}
aria-label={pending ? `Notifications, ${pending} awaiting approval` : "Notifications"}
title={pending ? `${pending} awaiting approval` : "Notifications"}
aria-haspopup="menu"
aria-expanded={menu === "bell"}
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/app/nav.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Nav model the single source of truth for the sidebar + command bar, and which
* Nav model, the single source of truth for the sidebar + command bar, and which
* screens are exposed in each build mode. In **real mode** the console only shows
* screens that actually talk to the eERC backend (auth/onboarding/treasury/payroll);
* the rest (Overview, Contractors, Invoices, one-off Pay, Approvals, Grants, Audit,
Expand Down
6 changes: 3 additions & 3 deletions apps/console/src/demo/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Demo api a drop-in for the real typed `api` client that resolves every read
* Demo api, a drop-in for the real typed `api` client that resolves every read
* against a seeded in-memory org and every write against a mutable copy of it, on
* realistic timers, with NO network. Swapped in at build time by `VITE_DEMO_MODE=1`
* (see ../lib/api.ts). Writes mutate the shared `db`, and prove/settle flows return
Expand Down Expand Up @@ -50,7 +50,7 @@ let demoMockKyc: { name?: string; country?: string } | undefined;
// ---- fake-chain helpers ---------------------------------------------------
function randHex(len: number): string {
const bytes = new Uint8Array(Math.ceil(len / 2));
// Fallback must fill `bytes` IN PLACE `.map()` returns a fresh array that
// Fallback must fill `bytes` IN PLACE, `.map()` returns a fresh array that
// would be discarded, leaving `bytes` all-zeros (identical output every call).
const rng = globalThis.crypto ?? {
getRandomValues: (a: Uint8Array) => {
Expand Down Expand Up @@ -556,7 +556,7 @@ export const demoApi = {
return {
packet: built,
integrity: { ok: true, headHash: built.anchor.headHash },
disclosure: "Anchored on-chain only the Merkle root leaves the ciphertext.",
disclosure: "Anchored on-chain, only the Merkle root leaves the ciphertext.",
packetHash: fakeTx(),
orgHash: fakeTx(),
anchor: { onChain: true, contractId: fakeVerifier(), txHash, sequence: String(Math.floor(Math.random() * 90000) + 10000), explorer: explorerTxUrl(txHash) },
Expand Down
8 changes: 4 additions & 4 deletions apps/console/src/demo/data.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Seed data for demo mode a rich, believable org so every console screen and
* Seed data for demo mode, a rich, believable org so every console screen and
* cinematic has something real to render with zero backend. Pure factory: it
* returns a fresh, mutable in-memory "db" the demo api reads/writes, so a session
* feels alive (approvals clear, payroll completes, grants revoke) without a network.
Expand Down Expand Up @@ -56,7 +56,7 @@ export interface DemoDb {
onboardingStatus: OnboardingStatus;
treasuryProvision: ProvisionTreasuryResponse;
treasuryDeposits: TreasuryDeposit[];
/** Private (shielded) pool total, minor units grows when "Make private" runs. */
/** Private (shielded) pool total, minor units, grows when "Make private" runs. */
privateTotal: string;
/** Public liquid USDC balance, minor units. */
publicUnits: string;
Expand Down Expand Up @@ -238,7 +238,7 @@ export function createDemoDb(): DemoDb {
orgId: ORG_ID,
number: "INV-2042",
counterpartyId: "cp_diego",
lineItems: [{ description: "Brand system July milestone", quantity: 1, unitAmount: usd(6200) }],
lineItems: [{ description: "Brand system, July milestone", quantity: 1, unitAmount: usd(6200) }],
total: { amount: usd(6200), assetCode: "USDC" },
status: "open",
dueDate: ISO("2026-07-25"),
Expand Down Expand Up @@ -321,7 +321,7 @@ export function createDemoDb(): DemoDb {
{ id: "pr_jun", kind: "payroll", title: "June payroll · 5 people", status: "completed", amountLabel: "Private", at: ISO("2026-06-01T12:00:00Z") },
{ id: "po_done_2", kind: "invoice", title: "INV-2039 · Northwind", status: "paid", amountLabel: "$9,800.00", at: ISO("2026-06-28T16:22:00Z") },
{ id: "dep_1", kind: "deposit", title: "Shielded USDC deposit", status: "confirmed", amountLabel: "Private", at: ISO("2026-07-02T09:30:00Z") },
{ id: "vg_1", kind: "grant", title: "Grant Thornton LLP · Q2", status: "active", amountLabel: "", at: ISO("2026-07-01T00:00:00Z") },
{ id: "vg_1", kind: "grant", title: "Grant Thornton LLP · Q2", status: "active", amountLabel: "-", at: ISO("2026-07-01T00:00:00Z") },
];

const onboardingStatus: OnboardingStatus = {
Expand Down
12 changes: 6 additions & 6 deletions apps/console/src/lib/copy.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/**
* Central copy constants reusable privacy / disclosure wording so every screen
* Central copy constants, reusable privacy / disclosure wording so every screen
* says the same thing the same way. Business language first; cryptographic detail
* only where it earns its place. Screens adopt these in later waves; exported now so
* the vocabulary is fixed before the rewrites start.
*
* Rule of thumb:
* - PRIVACY.privateOnChain the amount AND recipient are not publicly visible.
* - PRIVACY.visibleToWorkspace authorized teammates can see it in this console.
* - PRIVACY.hiddenDueToRole you personally can't see it (your role, not a failure).
* - PRIVACY.disclosedThroughProof revealed deliberately, backed by an on-chain proof.
* - PRIVACY.hideBalances the screen-level mask toggle label.
* - PRIVACY.privateOnChain, the amount AND recipient are not publicly visible.
* - PRIVACY.visibleToWorkspace, authorized teammates can see it in this console.
* - PRIVACY.hiddenDueToRole, you personally can't see it (your role, not a failure).
* - PRIVACY.disclosedThroughProof, revealed deliberately, backed by an on-chain proof.
* - PRIVACY.hideBalances, the screen-level mask toggle label.
*/
export const PRIVACY = {
/** Amount + recipient are not publicly visible on-chain. */
Expand Down
8 changes: 4 additions & 4 deletions apps/console/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ function toDate(ts: number | string | Date): Date | null {
}

/**
* Date-only display: "22 Jul 2026" NO time component. Formatted from the value's
* Date-only display: "22 Jul 2026", NO time component. Formatted from the value's
* UTC calendar day so a date-only value (a due date / expiry stored as midnight UTC)
* can never leak a spurious local time (the "05:30" IST artifact) or shift a day
* across timezones. Use this for due dates, expiries, period labels anything that
* across timezones. Use this for due dates, expiries, period labels, anything that
* is conceptually a day, not an instant.
*/
export function fmtDate(ts: number | string | Date): string {
Expand All @@ -79,7 +79,7 @@ export function fmtDate(ts: number | string | Date): string {

/**
* Full timestamp display (local): date + time, for genuine instants (posted-at,
* settled-at). Use this ONLY when the time actually matters otherwise use fmtDate.
* settled-at). Use this ONLY when the time actually matters, otherwise use fmtDate.
*/
export function fmtDateTime(ts: number | string | Date): string {
const d = toDate(ts);
Expand All @@ -93,7 +93,7 @@ export function formatDate(ts: number | string | Date): string {
}

/**
* Person initials for avatars the ONE shared source so the top-bar avatar and the
* Person initials for avatars, the ONE shared source so the top-bar avatar and the
* Settings/team rows never disagree (the "JE vs JO" bug). Two words → first letter of
* first + last ("John Everett" → "JE"); one word / an email → first two chars
* ("jordan@acme.co" → "JO"). Always uppercase.
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/lib/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const NETWORK_LABEL_BY_NETWORK = {
export const NETWORK_LABEL = NETWORK_LABEL_BY_NETWORK[NETWORK];

/**
* Environment framing for the network chip a financial-safety signal, not
* Environment framing for the network chip, a financial-safety signal, not
* wording. A testnet must NEVER read as a green "Live"; only real-money mainnet
* gets the green (`success`) treatment. Testnet and the permissioned L1 are amber
* (`warning`) so nobody mistakes play money for production. Consumed by the top-bar
Expand Down
2 changes: 1 addition & 1 deletion apps/console/src/lib/store.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ describe("ConsoleProvider treasury read model", () => {
});

it("does not sign out on a transient session failure (stays on the loading screen)", async () => {
// A network blip / 5xx / timeoutNOT a 401 must not strand the session on
// A network blip / 5xx / timeout, NOT a 401, must not strand the session on
// sign-in: no logout, and RootGate keeps showing "Loading workspace…" (loading
// stays true while session is still null), so the boot retry can recover.
apiMock.session.mockRejectedValue(new Error("network blip"));
Expand Down
8 changes: 4 additions & 4 deletions apps/console/src/lib/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function ConsoleProvider({ children }: { children: ReactNode }) {
// Mirror the live session in a ref so refresh()'s catch can tell "we already
// have a workspace to keep" (a transient failure should stay on the shell) from
// "the first load hasn't landed yet" (a transient failure should stay on the
// loading screen never the sign-in screen).
// loading screen, never the sign-in screen).
const sessionRef = useRef<AuthSession | null>(null);

const clearReadModels = useCallback(() => {
Expand Down Expand Up @@ -137,7 +137,7 @@ export function ConsoleProvider({ children }: { children: ReactNode }) {
if (sessionRef.current) {
// Mid-session: keep the workspace and its last-loaded data on screen,
// just surface a reconnecting note; the boot retry / 30s poll recover.
setError("Couldn't refresh reconnecting…");
setError("Couldn't refresh, reconnecting…");
setLoading(false);
} else {
// First load hasn't succeeded yet: stay on the "Loading workspace…"
Expand Down Expand Up @@ -195,7 +195,7 @@ export function ConsoleProvider({ children }: { children: ReactNode }) {
}, [clearReadModels]);

// Switching workspace must reload the org-scoped read models, not just the
// label otherwise the UI shows one workspace's name over another's data.
// label, otherwise the UI shows one workspace's name over another's data.
const setActiveOrg = useCallback((id: string) => {
setSession((current) => (current ? sessionWithActiveOrg(current, id) : current));
void refresh();
Expand Down Expand Up @@ -242,7 +242,7 @@ export function ConsoleProvider({ children }: { children: ReactNode }) {
}, []);

// #61: a confirmed 401 (session expired) is signalled by AUTH_REQUIRED_EVENT via
// notifyAuthRequired(). Thisand an explicit logout (AUTH_CHANGED) are the
// notifyAuthRequired(). This, and an explicit logout (AUTH_CHANGED), are the
// ONLY things that clear auth in real mode; a transient read failure never fires
// it, so it can no longer strand a live session on the sign-in screen.
useEffect(() => {
Expand Down
22 changes: 11 additions & 11 deletions apps/console/src/screens/Approvals.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Approvals the dual-control release gate. Each payment awaiting approval reads as
* Approvals, the dual-control release gate. Each payment awaiting approval reads as
* a structured three-area row: what it is (memo, counterparty, reference, proposer),
* where it stands (M-of-N progress, policy, privacy, risk), and the decision (amount,
* Deny, Approve). Denying requires a reason; approving that satisfies the policy
Expand Down Expand Up @@ -39,7 +39,7 @@ const HIGH_VALUE = 10_000n * 1_000_000n;
/** "…on-chain" network prose without doubling "Mainnet" on a mainnet build. */
const NETWORK_PROSE = `${NETWORK_LABEL}${NETWORK_ENV.kind === "mainnet" ? "" : ` ${NETWORK_ENV.badge}`}`;

/** "2 hours ago" a calm relative age for a proposed-at timestamp. */
/** "2 hours ago", a calm relative age for a proposed-at timestamp. */
function timeAgo(ts: string | number | Date): string {
const then = new Date(ts).getTime();
if (Number.isNaN(then)) return "";
Expand Down Expand Up @@ -79,7 +79,7 @@ function matchPolicy(p: PaymentOrder, policies: ApprovalPolicy[]): ApprovalPolic
return matches.sort((a, b) => amountConds(b) - amountConds(a))[0];
}

/** Segmented M-of-N progress filled segments for recorded approvals. */
/** Segmented M-of-N progress, filled segments for recorded approvals. */
function Progress({ have, need }: { have: number; need: number }) {
return (
<div data-testid="approval-progress">
Expand Down Expand Up @@ -176,7 +176,7 @@ export function Approvals() {
const have = approvedTrail.length;
const knownNeed = policy ? totalApprovers(policy) : null;
// Only claim this approval RELEASES the payment when the threshold is
// actually known an unresolved policy must not over-promise a settle.
// actually known, an unresolved policy must not over-promise a settle.
const willRelease = knownNeed != null && have + 1 >= knownNeed;
const need = knownNeed ?? Math.max(have + 1, 2); // display fallback for the progress bar
const risky = BigInt(p.amount.amount) > HIGH_VALUE;
Expand All @@ -191,7 +191,7 @@ export function Approvals() {
>
<Card data-testid="approval-card">
<div className="grid gap-5 md:grid-cols-[1fr_auto_auto] md:items-start">
{/* Left what it is */}
{/* Left, what it is */}
<div className="min-w-0">
<div className="t-card-title truncate text-fg">{p.memo ?? "Payment"}</div>
<div className="t-secondary mt-0.5">
Expand All @@ -204,7 +204,7 @@ export function Approvals() {
</div>
</div>

{/* Middle where it stands */}
{/* Middle, where it stands */}
<div className="space-y-2.5 md:w-60 md:border-l md:border-border md:pl-5">
<Progress have={have} need={need} />
{have > 0 ? (
Expand All @@ -225,7 +225,7 @@ export function Approvals() {
</div>
</div>

{/* Right the decision */}
{/* Right, the decision */}
<div className="flex flex-col items-end gap-3 md:w-44 md:pl-5">
<div className="font-display tnum text-2xl font-semibold text-fg" data-testid="approval-amount">
{masked || p.privacy.amountHidden ? "••••••" : fmtUsd(p.amount.amount)}
Expand Down Expand Up @@ -296,7 +296,7 @@ export function Approvals() {
<StatusPill status={p.status} />
)}
</Td>
<Td>{last ? `${memberName(last.approverMemberId)}${memberRole(last.approverMemberId) ? ` · ${memberRole(last.approverMemberId)}` : ""}` : ""}</Td>
<Td>{last ? `${memberName(last.approverMemberId)}${memberRole(last.approverMemberId) ? ` · ${memberRole(last.approverMemberId)}` : ""}` : "-"}</Td>
<Td align="right" className="tnum">
{masked || p.privacy.amountHidden ? "••••••" : fmtUsd(p.amount.amount)}
</Td>
Expand All @@ -311,7 +311,7 @@ export function Approvals() {
Receipt <ExternalLink size={12} />
</a>
) : (
<span className="text-muted"></span>
<span className="text-muted">-</span>
)}
</Td>
</Tr>
Expand All @@ -322,7 +322,7 @@ export function Approvals() {
</div>
) : null}

{/* Approve confirm releases explicitly say what happens on-chain. */}
{/* Approve confirm, releases explicitly say what happens on-chain. */}
<Modal
open={!!confirm}
onClose={() => busy === null && setConfirm(null)}
Expand Down Expand Up @@ -382,7 +382,7 @@ export function Approvals() {
<p className="t-helper">A reason is recorded in the audit trail and shared with the proposer.</p>
<Textarea
label="Reason for denial"
placeholder="e.g. Duplicate of PO-4480 already paid."
placeholder="e.g. Duplicate of PO-4480, already paid."
value={denyReason}
onChange={(e) => setDenyReason(e.target.value)}
data-testid="deny-reason"
Expand Down
Loading
Loading