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
25 changes: 14 additions & 11 deletions apps/console/src/app/CommandBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Search } from "lucide-react";
import { AnimatePresence, motion } from "framer-motion";
import { DEMO_MODE } from "../demo/flag";

// Jump-to targets mirror the sidebar: in real mode only the backend-backed
// screens are reachable, so the palette hides the demo-only destinations too.
const DESTINATIONS = [
{ label: "Dashboard", to: "/" },
{ label: "Contractors", to: "/contractors" },
{ label: "Payroll", to: "/payroll" },
{ label: "Invoices to pay", to: "/invoices" },
{ label: "Send & vendor pay", to: "/pay" },
{ label: "Approvals", to: "/approvals" },
{ label: "Treasury", to: "/treasury" },
{ label: "Auditor grants", to: "/grants" },
{ label: "Audit log", to: "/audit" },
{ label: "Settings & team", to: "/settings" },
];
{ label: "Dashboard", to: "/", real: false },
{ label: "Contractors", to: "/contractors", real: false },
{ label: "Payroll", to: "/payroll", real: true },
{ label: "Invoices to pay", to: "/invoices", real: false },
{ label: "Send & vendor pay", to: "/pay", real: false },
{ label: "Approvals", to: "/approvals", real: false },
{ label: "Treasury", to: "/treasury", real: true },
{ label: "Auditor grants", to: "/grants", real: false },
{ label: "Audit log", to: "/audit", real: false },
{ label: "Settings & team", to: "/settings", real: false },
].filter((d) => DEMO_MODE || d.real);
Comment on lines +10 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Derive palette destinations from the shared navigation model.

This duplicates both route definitions and real-mode visibility metadata instead of using visibleNavItems(). It can drift from the sidebar and route-gating contract; keep any command-specific labels as metadata in NavItemDef if needed.

Suggested direction
-import { DEMO_MODE } from "../demo/flag";
+import { visibleNavItems } from "./nav";

-const DESTINATIONS = [
-  // duplicated destinations and `real` flags
-].filter((d) => DEMO_MODE || d.real);
+const DESTINATIONS = visibleNavItems().map((item) => ({
+  label: item.label,
+  to: item.to,
+}));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { DEMO_MODE } from "../demo/flag";
// Jump-to targets mirror the sidebar: in real mode only the backend-backed
// screens are reachable, so the palette hides the demo-only destinations too.
const DESTINATIONS = [
{ label: "Dashboard", to: "/" },
{ label: "Contractors", to: "/contractors" },
{ label: "Payroll", to: "/payroll" },
{ label: "Invoices to pay", to: "/invoices" },
{ label: "Send & vendor pay", to: "/pay" },
{ label: "Approvals", to: "/approvals" },
{ label: "Treasury", to: "/treasury" },
{ label: "Auditor grants", to: "/grants" },
{ label: "Audit log", to: "/audit" },
{ label: "Settings & team", to: "/settings" },
];
{ label: "Dashboard", to: "/", real: false },
{ label: "Contractors", to: "/contractors", real: false },
{ label: "Payroll", to: "/payroll", real: true },
{ label: "Invoices to pay", to: "/invoices", real: false },
{ label: "Send & vendor pay", to: "/pay", real: false },
{ label: "Approvals", to: "/approvals", real: false },
{ label: "Treasury", to: "/treasury", real: true },
{ label: "Auditor grants", to: "/grants", real: false },
{ label: "Audit log", to: "/audit", real: false },
{ label: "Settings & team", to: "/settings", real: false },
].filter((d) => DEMO_MODE || d.real);
import { visibleNavItems } from "./nav";
// Jump-to targets mirror the sidebar: in real mode only the backend-backed
// screens are reachable, so the palette hides the demo-only destinations too.
const DESTINATIONS = visibleNavItems().map((item) => ({
label: item.label,
to: item.to,
}));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/app/CommandBar.tsx` around lines 10 - 25, Replace the
duplicated DESTINATIONS definitions and DEMO_MODE filtering in CommandBar with
the shared visibleNavItems() navigation model. Map the returned NavItemDef
entries to the command palette shape, and add any command-specific labels or
metadata to NavItemDef so both sidebar and palette use the same routes and
real-mode visibility contract.


export function CommandBar() {
const nav = useNavigate();
Expand Down
93 changes: 50 additions & 43 deletions apps/console/src/app/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,17 @@
* (workspace switcher, ⌘K search, live network badge, mask-eye, bell, avatar),
* and the routed content area with the cursor-interactive canvas behind the cards.
*/
import {
ArrowUpRight,
Bell,
ChevronDown,
Eye,
EyeOff,
FileText,
LayoutDashboard,
ScrollText,
Settings,
ShieldCheck,
Users,
Wallet,
CheckCheck,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Bell, CheckCheck, ChevronDown, Eye, EyeOff, Settings, Users } from "lucide-react";
import { Fragment, useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { NavLink, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { NavLink, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
import { StageVideo } from "../ui/StageVideo";
import { CommandBar } from "./CommandBar";
import { AvalancheMark, Logo } from "../ui/Logo";
import { NetworkMenu } from "./NetworkMenu";
import { useConsole } from "../lib/store";
import { DEMO_MODE } from "../demo/flag";
import { REAL_HOME, visibleNavGroups, visibleNavItems } from "./nav";
import { formatAddress } from "../lib/format";
import { Dashboard } from "../screens/Dashboard";
import { Approvals } from "../screens/Approvals";
Expand Down Expand Up @@ -213,21 +201,28 @@ export function Shell() {
{/* body: sidebar nav + routed content */}
<div className="flex flex-1 overflow-hidden">
<aside className="flex w-[240px] flex-none flex-col gap-1 border-r border-border bg-surface px-3.5 py-4">
<Eyebrow>Overview</Eyebrow>
<NavItem to="/" icon={LayoutDashboard} label="Overview" />
<Eyebrow>Payments</Eyebrow>
<NavItem to="/contractors" icon={Users} label="Contractors" />
<NavItem to="/payroll" icon={Users} label="Payroll" />
<NavItem to="/invoices" icon={FileText} label="Invoices" />
<NavItem to="/pay" icon={ArrowUpRight} label="One-off payment" />
<Eyebrow>Operations</Eyebrow>
<NavItem to="/approvals" icon={CheckCheck} label="Approvals" badge={pending || undefined} />
<NavItem to="/treasury" icon={Wallet} label="Treasury" />
<Eyebrow>Compliance</Eyebrow>
<NavItem to="/grants" icon={ShieldCheck} label="Auditor access" />
<NavItem to="/audit" icon={ScrollText} label="Audit log" />
{visibleNavGroups().map((group) => (
<Fragment key={group}>
<Eyebrow>{group}</Eyebrow>
{visibleNavItems()
.filter((item) => !item.footer && item.group === group)
.map((item) => (
<NavItem
key={item.to}
to={item.to}
icon={item.icon}
label={item.label}
badge={item.to === "/approvals" ? pending || undefined : undefined}
/>
))}
</Fragment>
))}
<div className="flex-1" />
<NavItem to="/settings" icon={Settings} label="Settings & team" />
{visibleNavItems()
.filter((item) => item.footer)
.map((item) => (
<NavItem key={item.to} to={item.to} icon={item.icon} label={item.label} />
))}
<div className="mt-auto flex items-center justify-center gap-1.5 border-t border-border/60 pt-3 text-[11px] font-medium text-[#8a9099]" data-testid="built-on-avalanche">
<AvalancheMark size={13} /> Built on Avalanche
</div>
Expand All @@ -237,18 +232,30 @@ export function Shell() {
<main className="no-scrollbar relative z-10 h-full overflow-y-auto px-5 py-6">
{activeOrg ? (
<Routes location={loc} key={loc.pathname}>
<Route path="/" element={<Dashboard />} />
<Route path="/approvals" element={<Approvals />} />
<Route path="/contractors" element={<Contractors />} />
<Route path="/payroll" element={<Payroll />} />
<Route path="/invoices" element={<Invoices />} />
<Route path="/pay" element={<Pay />} />
<Route path="/treasury" element={<Treasury />} />
<Route path="/grants" element={<Grants />} />
<Route path="/audit" element={<AuditLog />} />
<Route path="/claim" element={<InviteClaim />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="*" element={<Dashboard />} />
{DEMO_MODE ? (
<>
<Route path="/" element={<Dashboard />} />
<Route path="/approvals" element={<Approvals />} />
<Route path="/contractors" element={<Contractors />} />
<Route path="/payroll" element={<Payroll />} />
<Route path="/invoices" element={<Invoices />} />
<Route path="/pay" element={<Pay />} />
<Route path="/treasury" element={<Treasury />} />
<Route path="/grants" element={<Grants />} />
<Route path="/audit" element={<AuditLog />} />
<Route path="/claim" element={<InviteClaim />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="*" element={<Dashboard />} />
</>
) : (
Comment on lines +235 to +250

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Demo Screens Still Imported

This branch makes the demo-only routes unreachable in real mode, but the demo screens are still statically imported at module load. A real build can still include and evaluate those screen modules and their transitive dependencies, so the intended real-mode bundle split does not happen from this route gate alone.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/console/src/app/Shell.tsx
Line: 235-250

Comment:
**Demo Screens Still Imported**

This branch makes the demo-only routes unreachable in real mode, but the demo screens are still statically imported at module load. A real build can still include and evaluate those screen modules and their transitive dependencies, so the intended real-mode bundle split does not happen from this route gate alone.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Cursor Fix in Codex

<>
{/* Real mode: only the backend-backed screens; everything else
is demo-only and redirects to the treasury landing. */}
<Route path="/payroll" element={<Payroll />} />
<Route path="/treasury" element={<Treasury />} />
<Route path="*" element={<Navigate to={REAL_HOME} replace />} />
</>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Invite Claim Route Redirected

When a real-mode user with an active org opens /claim, this wildcard catches the URL and sends them to /treasury. The previous route table registered InviteClaim, so invite links can no longer reach the claim screen in the real build.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/console/src/app/Shell.tsx
Line: 257

Comment:
**Invite Claim Route Redirected**

When a real-mode user with an active org opens `/claim`, this wildcard catches the URL and sends them to `/treasury`. The previous route table registered `InviteClaim`, so invite links can no longer reach the claim screen in the real build.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

)}
</Routes>
) : (
<NoOrgPlaceholder address={session?.user.address} />
Expand Down
40 changes: 40 additions & 0 deletions apps/console/src/app/nav.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { NAV_ITEMS, REAL_HOME, visibleNavGroups, visibleNavItems } from "./nav";

describe("real-mode nav gating", () => {
it("real mode exposes only the backend-backed screens (Payroll + Treasury)", () => {
const tos = visibleNavItems(false)
.map((i) => i.to)
.sort();
expect(tos).toEqual(["/payroll", "/treasury"]);
});

it("real mode hides every no-backend screen", () => {
const tos = new Set(visibleNavItems(false).map((i) => i.to));
for (const hidden of ["/", "/contractors", "/invoices", "/pay", "/approvals", "/grants", "/audit", "/settings"]) {
expect(tos.has(hidden)).toBe(false);
}
});

it("demo mode shows the full product", () => {
const items = visibleNavItems(true);
expect(items).toHaveLength(NAV_ITEMS.length);
expect(items.some((i) => i.to === "/contractors")).toBe(true);
expect(items.some((i) => i.to === "/settings")).toBe(true);
});

it("real-mode groups collapse to the two wired sections, in order", () => {
expect(visibleNavGroups(false)).toEqual(["Payments", "Operations"]);
});

it("demo-mode keeps all non-footer sections", () => {
expect(visibleNavGroups(true)).toEqual(["Overview", "Payments", "Operations", "Compliance"]);
});

it("real mode lands on the treasury (Overview has no backend)", () => {
expect(REAL_HOME).toBe("/treasury");
// the landing must itself be a real-backed screen
const landing = NAV_ITEMS.find((i) => i.to === REAL_HOME);
expect(landing?.realBacked).toBe(true);
});
});
55 changes: 55 additions & 0 deletions apps/console/src/app/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* 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,
* Settings) have no backend on this platform and stay **demo-only**. Demo mode
Comment on lines +5 to +6

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep backed organization and team settings reachable in real mode.

/settings is marked demo-only, so it disappears from navigation and the real-mode wildcard in Shell.tsx redirects it to Treasury. This conflicts with the requirement to retain backed organization/team settings. Mark the supported Settings entry real-backed, register it in the real-mode routes, and gate only unsupported Settings cards/tabs within SettingsScreen.

Also applies to: 36-36

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/console/src/app/nav.ts` around lines 5 - 6, Keep the supported
organization and team settings accessible in real mode: update the Settings
navigation metadata and real-mode route registration in Shell.tsx so /settings
is not treated as demo-only, then modify SettingsScreen to gate only unsupported
settings cards or tabs rather than the entire screen.

* (`VITE_DEMO_MODE=1`) shows the full product as the vision.
*/
import type { LucideIcon } from "lucide-react";
import { ArrowUpRight, CheckCheck, FileText, LayoutDashboard, ScrollText, Settings, ShieldCheck, Users, Wallet } from "lucide-react";
import { DEMO_MODE } from "../demo/flag";

export type NavGroup = "Overview" | "Payments" | "Operations" | "Compliance" | "Settings";

export interface NavItemDef {
to: string;
label: string;
icon: LucideIcon;
group: NavGroup;
/** True only when the screen is wired to a real backend endpoint. */
realBacked: boolean;
/** Pinned to the bottom of the sidebar (Settings). */
footer?: boolean;
}

export const NAV_ITEMS: NavItemDef[] = [
{ to: "/", label: "Overview", icon: LayoutDashboard, group: "Overview", realBacked: false },
{ to: "/contractors", label: "Contractors", icon: Users, group: "Payments", realBacked: false },
{ to: "/payroll", label: "Payroll", icon: Users, group: "Payments", realBacked: true },
{ to: "/invoices", label: "Invoices", icon: FileText, group: "Payments", realBacked: false },
{ to: "/pay", label: "One-off payment", icon: ArrowUpRight, group: "Payments", realBacked: false },
{ to: "/approvals", label: "Approvals", icon: CheckCheck, group: "Operations", realBacked: false },
{ to: "/treasury", label: "Treasury", icon: Wallet, group: "Operations", realBacked: true },
{ to: "/grants", label: "Auditor access", icon: ShieldCheck, group: "Compliance", realBacked: false },
{ to: "/audit", label: "Audit log", icon: ScrollText, group: "Compliance", realBacked: false },
{ to: "/settings", label: "Settings & team", icon: Settings, group: "Settings", realBacked: false, footer: true },
];

/** Where real mode lands (Overview has no backend). */
export const REAL_HOME = "/treasury";

/** Nav items to render for the given build mode. Real mode hides no-backend screens. */
export function visibleNavItems(demoMode: boolean = DEMO_MODE): NavItemDef[] {
return NAV_ITEMS.filter((item) => demoMode || item.realBacked);
}

/** Ordered, de-duplicated list of groups that have at least one visible non-footer item. */
export function visibleNavGroups(demoMode: boolean = DEMO_MODE): NavGroup[] {
const groups: NavGroup[] = [];
for (const item of visibleNavItems(demoMode)) {
if (item.footer) continue;
if (!groups.includes(item.group)) groups.push(item.group);
}
return groups;
}
Loading