diff --git a/.env.example b/.env.example index d27eaa80b..c0b20b4b7 100644 --- a/.env.example +++ b/.env.example @@ -202,6 +202,14 @@ GITHUB_ISSUES_REPO=njrini99-code/helmv3 # Private Supabase Storage bucket used for screenshot signed links. HELM_BRIDGE_FEEDBACK_BUCKET=helm-bridge-feedback +# ----------------------------------------------------------------------------- +# Helm Bridge Work log (PR timeline tab) +# ----------------------------------------------------------------------------- +# Comma-separated GitHub logins to filter PRs (e.g. your username). Omit to show all repo PRs. +GITHUB_PR_AUTHOR_LOGINS=your-github-username +# Max PRs to fetch (default 60, max 100) +# GITHUB_PR_FETCH_LIMIT=60 + # ----------------------------------------------------------------------------- # Inngest (Durable workflows — replaces scattered cron + retry loops) # ----------------------------------------------------------------------------- diff --git a/src/app/admin/_components/AdminShell.tsx b/src/app/admin/_components/AdminShell.tsx index ee7c60d8e..c74c058ac 100644 --- a/src/app/admin/_components/AdminShell.tsx +++ b/src/app/admin/_components/AdminShell.tsx @@ -5,7 +5,7 @@ import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { LayoutDashboard, Activity, AlertTriangle, KeyRound, Flag, CircleDot, - Users, Timer, Rocket, HeartPulse, ExternalLink, MessageSquarePlus, Gauge, SearchCheck, + Users, Timer, Rocket, HeartPulse, ExternalLink, MessageSquarePlus, Gauge, SearchCheck, ScrollText, } from 'lucide-react'; import { AppShell, @@ -16,6 +16,7 @@ import { type CommandGroup, type CommandItem, } from '@/components/fairway'; +import { SessionActivityProvider } from '@/components/providers/SessionActivityProvider'; import { ADMIN_NAV, hrefForShortcut } from './admin-nav'; /** Sub-route leaf labels the Breadcrumb trail can't derive from ADMIN_NAV @@ -58,6 +59,7 @@ const NAV_ICON_BY_HREF = { '/admin/golf': Flag, '/admin/baseball': CircleDot, '/admin/ben-leah': MessageSquarePlus, + '/admin/work': ScrollText, '/admin/users': Users, '/admin/jobs': Timer, '/admin/deploys': Rocket, @@ -204,6 +206,7 @@ export function AdminShell({ ); return ( +
+
); } diff --git a/src/app/admin/_components/__tests__/admin-nav.test.ts b/src/app/admin/_components/__tests__/admin-nav.test.ts index 9894779f8..1145aeeec 100644 --- a/src/app/admin/_components/__tests__/admin-nav.test.ts +++ b/src/app/admin/_components/__tests__/admin-nav.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { ADMIN_NAV, hrefForShortcut } from '@/app/admin/_components/admin-nav'; +import { ADMIN_NAV, ADMIN_COMMAND_SHORTCUTS, hrefForShortcut } from '@/app/admin/_components/admin-nav'; describe('ADMIN_NAV', () => { it('declares the canonical tabs in order', () => { @@ -11,19 +11,29 @@ describe('ADMIN_NAV', () => { '/admin/golf', '/admin/baseball', '/admin/ben-leah', + '/admin/work', '/admin/users', '/admin/jobs', '/admin/deploys', '/admin/health', ]); - expect(ADMIN_NAV.map((e) => e.key)).toEqual(['1', '2', '3', '4', '5', '6', 'B', '7', '8', '9', '0']); + expect(ADMIN_NAV.map((e) => e.key)).toEqual(['1', '2', '3', '4', '5', '6', 'B', 'W', '7', '8', '9', '0']); }); it('maps shortcut keys to hrefs', () => { expect(hrefForShortcut('2')).toBe('/admin/activity'); expect(hrefForShortcut('3')).toBe('/admin/errors'); expect(hrefForShortcut('B')).toBe('/admin/ben-leah'); + expect(hrefForShortcut('W')).toBe('/admin/work'); expect(hrefForShortcut('9')).toBe('/admin/deploys'); expect(hrefForShortcut('0')).toBe('/admin/health'); expect(hrefForShortcut('x')).toBeNull(); }); + + it('keeps command-center shortcuts on real Bridge tabs', () => { + const navHrefs = new Set(ADMIN_NAV.map((entry) => entry.href)); + for (const shortcut of ADMIN_COMMAND_SHORTCUTS) { + expect(navHrefs.has(shortcut.href), `${shortcut.href} is not a registered admin tab`).toBe(true); + } + expect(ADMIN_COMMAND_SHORTCUTS.map((s) => s.href)).not.toContain('/admin/audit'); + }); }); diff --git a/src/app/admin/_components/admin-nav.ts b/src/app/admin/_components/admin-nav.ts index e2ecbc43e..031dc2feb 100644 --- a/src/app/admin/_components/admin-nav.ts +++ b/src/app/admin/_components/admin-nav.ts @@ -6,6 +6,7 @@ type AdminHref = | '/admin/golf' | '/admin/baseball' | '/admin/ben-leah' + | '/admin/work' | '/admin/users' | '/admin/jobs' | '/admin/deploys' @@ -30,12 +31,21 @@ export const ADMIN_NAV: readonly AdminNavEntry[] = [ { label: 'Golf', href: '/admin/golf', key: '5', section: 'Apps', description: 'GolfHelm production signals' }, { label: 'Baseball', href: '/admin/baseball', key: '6', section: 'Apps', description: 'BaseballHelm production signals' }, { label: 'Ben + Leah', href: '/admin/ben-leah', key: 'B', section: 'Operations', description: 'Submit bugs, changes, additions', meta: 'issues' }, + { label: 'Work log', href: '/admin/work', key: 'W', section: 'Operations', description: 'PR timeline — problems, fixes, areas', meta: 'prs' }, { label: 'Users & Teams', href: '/admin/users', key: '7', section: 'Platform', description: 'Accounts, teams, engagement' }, { label: 'Jobs & Integrity', href: '/admin/jobs', key: '8', section: 'Platform', description: 'Crons, guards, integrity checks' }, { label: 'Deploys & Infra', href: '/admin/deploys', key: '9', section: 'Platform', description: 'Vercel releases and web insight' }, { label: 'Health', href: '/admin/health', key: '0', section: 'Platform', description: 'Feature health across every app', meta: 'map' }, ] as const; +/** Quick links in the Overview command header — must be real ADMIN_NAV routes. */ +export const ADMIN_COMMAND_SHORTCUTS = [ + { href: '/admin/errors', label: 'Errors' }, + { href: '/admin/health', label: 'Feature Map' }, + { href: '/admin/deploys', label: 'Deploys' }, + { href: '/admin/auth', label: 'Auth' }, +] as const satisfies ReadonlyArray<{ href: AdminHref; label: string }>; + export function hrefForShortcut(key: string): string | null { return ADMIN_NAV.find((e) => e.key === key)?.href ?? null; } diff --git a/src/app/admin/ben-leah/BenLeahIssueBoard.tsx b/src/app/admin/ben-leah/BenLeahIssueBoard.tsx new file mode 100644 index 000000000..b58cd380f --- /dev/null +++ b/src/app/admin/ben-leah/BenLeahIssueBoard.tsx @@ -0,0 +1,169 @@ +import Link from 'next/link'; +import { GitPullRequest, Rocket } from 'lucide-react'; +import { + fetchBenLeahIssueBoard, + type BenLeahTrackStatus, +} from '@/lib/admin/ben-leah-issues'; +import { PanelNoData, PanelStale } from '../_components/PanelStates'; +import { StatusPill, StatTile, type FwStatusTone } from '@/components/fairway'; +import { BenLeahIssueTable } from './BenLeahIssueTable'; + +const TRACK_META: Record< + BenLeahTrackStatus, + { label: string; tone: FwStatusTone; description: string } +> = { + open: { + label: 'Open', + tone: 'warning', + description: 'Still open on GitHub — waiting for work.', + }, + in_progress: { + label: 'In progress', + tone: 'info', + description: 'Label status:in-progress or status:triaged on the issue.', + }, + in_production: { + label: 'In production', + tone: 'success', + description: 'Closed and shipped, or label status:in-production.', + }, + fixed_pending_deploy: { + label: 'Fixed · pending deploy', + tone: 'warning', + description: 'Closed on GitHub; production has not deployed since the fix.', + }, + fixed: { + label: 'Fixed', + tone: 'success', + description: 'Closed on GitHub (deploy timing unknown).', + }, + wont_fix: { + label: "Won't fix", + tone: 'neutral', + description: 'Label status:wontfix on the issue.', + }, +}; + +function formatWhen(iso: string): string { + return new Date(iso).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); +} + +function githubIntegrationTone(status: string): FwStatusTone { + if (status === 'ok') return 'success'; + if (status === 'unconfigured') return 'warning'; + return 'danger'; +} + +export async function BenLeahIssueBoard() { + const board = await fetchBenLeahIssueBoard(); + + if (board.status === 'unconfigured') { + return ( + + ); + } + + if (board.status === 'error' || !board.data) { + return ; + } + + const { issues, counts, repoLabel, repoIssuesUrl, productionCommitSha, productionReadyAt, fetchedAt } = { + ...board.data, + fetchedAt: board.fetchedAt, + }; + + const openCount = counts.open + counts.in_progress; + const shippedCount = counts.in_production; + const pendingDeployCount = counts.fixed_pending_deploy; + + return ( +
+
+
+ + GitHub {board.status === 'ok' ? 'live' : 'stale'} + + + {productionReadyAt + ? `prod ${productionCommitSha?.slice(0, 7) ?? 'build'}` + : 'prod timing n/a'} + + {fetchedAt ? ( + + checked {new Date(fetchedAt).toLocaleTimeString()} + + ) : null} +
+ + + All issues on {repoLabel} + +
+ +
+ {( + [ + ['Open / in progress', openCount, 'Needs attention'], + ['In production', shippedCount, 'Shipped or labeled'], + ['Pending deploy', pendingDeployCount, 'Closed, not on prod yet'], + ['Total tracked', issues.length, 'Last 50 updates'], + ] as const + ).map(([label, value, caption]) => ( +
+ +

{caption}

+
+ ))} +
+ + {productionReadyAt ? ( +

+ + Production last ready {formatWhen(new Date(productionReadyAt).toISOString())} + {productionCommitSha ? ` · ${productionCommitSha.slice(0, 7)}` : null} + . Closed issues after that time show as In production. +

+ ) : ( +

+ Set VERCEL_API_TOKEN to correlate closed issues with production deploy timing. Without it, status uses GitHub labels only. +

+ )} + + {issues.length === 0 ? ( + + ) : ( + + )} + +
+ {(Object.keys(TRACK_META) as BenLeahTrackStatus[]).map((status) => ( +
+
+ + {TRACK_META[status].label} + + {counts[status]} +
+

{TRACK_META[status].description}

+
+ ))} +
+
+ ); +} diff --git a/src/app/admin/ben-leah/BenLeahIssueTable.tsx b/src/app/admin/ben-leah/BenLeahIssueTable.tsx new file mode 100644 index 000000000..3662dce12 --- /dev/null +++ b/src/app/admin/ben-leah/BenLeahIssueTable.tsx @@ -0,0 +1,100 @@ +'use client'; + +import Link from 'next/link'; +import { ExternalLink } from 'lucide-react'; +import type { BenLeahTrackedIssue } from '@/lib/admin/ben-leah-issue-tracker'; +import { StatusPill, type FwStatusTone } from '@/components/fairway'; +import { BenLeahIssueWorkflowSelect } from './BenLeahIssueWorkflowSelect'; + +const TRACK_META: Record< + BenLeahTrackedIssue['trackStatus'], + { label: string; tone: FwStatusTone } +> = { + open: { label: 'Open', tone: 'warning' }, + in_progress: { label: 'In progress', tone: 'info' }, + in_production: { label: 'In production', tone: 'success' }, + fixed_pending_deploy: { label: 'Fixed · pending deploy', tone: 'warning' }, + fixed: { label: 'Fixed', tone: 'success' }, + wont_fix: { label: "Won't fix", tone: 'neutral' }, +}; + +const KIND_LABEL: Record = { + bug: 'Bug', + change: 'Change', + addition: 'Addition', +}; + +function formatWhen(iso: string): string { + return new Date(iso).toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); +} + +function IssueRow({ issue }: { issue: BenLeahTrackedIssue }) { + const meta = TRACK_META[issue.trackStatus]; + + return ( + + + + #{issue.number} + + +

+ {issue.displayTitle} +

+ + + + {meta.label} + + + + + + + {issue.kind ? KIND_LABEL[issue.kind] ?? issue.kind : '—'} + + {issue.priority ?? '—'} + {issue.category ?? '—'} + {formatWhen(issue.updated_at)} + + {issue.closed_at ? formatWhen(issue.closed_at) : '—'} + + + ); +} + +export function BenLeahIssueTable({ issues }: { issues: BenLeahTrackedIssue[] }) { + return ( +
+ + + + + + + + + + + + + + + {issues.map((issue) => ( + + ))} + +
IssueDerived statusSet workflowTypePriorityCategoryUpdatedClosed
+
+ ); +} diff --git a/src/app/admin/ben-leah/BenLeahIssueWorkflowSelect.tsx b/src/app/admin/ben-leah/BenLeahIssueWorkflowSelect.tsx new file mode 100644 index 000000000..c9f2f81c2 --- /dev/null +++ b/src/app/admin/ben-leah/BenLeahIssueWorkflowSelect.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useState, useTransition } from 'react'; +import { + currentWorkflowSelection, + type BenLeahTrackedIssue, + type BenLeahWorkflowSelection, +} from '@/lib/admin/ben-leah-issue-tracker'; +import { NativeSelect } from '@/components/ui/native-select'; +import { cn } from '@/lib/utils'; +import { updateBenLeahIssueWorkflow } from './actions'; + +const WORKFLOW_OPTIONS: Array<{ value: BenLeahWorkflowSelection; label: string }> = [ + { value: 'triaged', label: 'Triaged' }, + { value: 'in_progress', label: 'In progress' }, + { value: 'in_production', label: 'In production' }, + { value: 'wont_fix', label: "Won't fix" }, +]; + +const selectClass = cn( + 'min-w-[9.5rem] rounded-fw-md border border-border-subtle bg-surface px-2 py-1 text-xs text-warm-900 shadow-flat outline-none', + 'focus:border-accent-500 focus:ring-2 focus:ring-accent-500/20 disabled:opacity-60', +); + +export function BenLeahIssueWorkflowSelect({ issue }: { issue: BenLeahTrackedIssue }) { + const router = useRouter(); + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const current = currentWorkflowSelection(issue.labels); + + const onChange = (raw: string) => { + if (!raw) return; + const next = raw as BenLeahWorkflowSelection; + if (next === current) return; + setError(null); + startTransition(async () => { + const result = await updateBenLeahIssueWorkflow(issue.number, issue.labels, next); + if (!result.ok) { + setError(result.message); + return; + } + router.refresh(); + }); + }; + + return ( +
+ onChange(event.target.value)} + > + {!current ? ( + + ) : null} + {WORKFLOW_OPTIONS.map((option) => ( + + ))} + + {error ?

{error}

: null} +
+ ); +} diff --git a/src/app/admin/ben-leah/actions.ts b/src/app/admin/ben-leah/actions.ts index f8f4398e6..dc98bb423 100644 --- a/src/app/admin/ben-leah/actions.ts +++ b/src/app/admin/ben-leah/actions.ts @@ -7,6 +7,8 @@ import { validateFeedback, type GitHubIssueResult, } from '@/lib/admin/github-feedback'; +import { setBenLeahIssueWorkflow } from '@/lib/admin/github-issues-workflow'; +import type { BenLeahWorkflowSelection } from '@/lib/admin/ben-leah-issue-tracker'; import { requireSuperAdmin } from '@/lib/admin/require-super-admin'; import { describeError } from '@/lib/utils/describe-error'; @@ -67,3 +69,44 @@ export async function submitBenLeahFeedback( }; } } + +const WORKFLOW_SELECTIONS = new Set([ + 'triaged', + 'in_progress', + 'in_production', + 'wont_fix', +]); + +export async function updateBenLeahIssueWorkflow( + issueNumber: number, + currentLabels: string[], + selection: BenLeahWorkflowSelection, +): Promise<{ ok: boolean; message: string }> { + try { + await requireSuperAdmin(); + } catch (err) { + return { + ok: false, + message: describeError(err) === 'Unauthorized' + ? 'Your session expired. Sign in again and retry.' + : 'You do not have permission to update Ben + Leah issues.', + }; + } + + if (!Number.isInteger(issueNumber) || issueNumber < 1) { + return { ok: false, message: 'Invalid issue number.' }; + } + if (!WORKFLOW_SELECTIONS.has(selection)) { + return { ok: false, message: 'Invalid workflow status.' }; + } + + try { + await setBenLeahIssueWorkflow(issueNumber, currentLabels, selection); + return { ok: true, message: 'GitHub workflow label updated.' }; + } catch (err) { + return { + ok: false, + message: `Could not update GitHub labels: ${describeError(err)}`, + }; + } +} diff --git a/src/app/admin/ben-leah/page.tsx b/src/app/admin/ben-leah/page.tsx index 6d268b0c3..d4f45ba6d 100644 --- a/src/app/admin/ben-leah/page.tsx +++ b/src/app/admin/ben-leah/page.tsx @@ -1,8 +1,10 @@ -import { GitPullRequest, ImageUp, Link2, MessageSquarePlus } from 'lucide-react'; +import { GitPullRequest, ImageUp, Link2, MessageSquarePlus, Tags } from 'lucide-react'; import { requireSuperAdmin } from '@/lib/admin/require-super-admin'; import { Eyebrow, Surface, StatusPill } from '@/components/fairway'; import { PanelBoundary } from '../_components/PanelBoundary'; +import { AutoRefresh } from '../_components/AutoRefresh'; import { BenLeahForm } from './BenLeahForm'; +import { BenLeahIssueBoard } from './BenLeahIssueBoard'; export const dynamic = 'force-dynamic'; @@ -64,6 +66,27 @@ export default async function BenLeahPage() { + +

Status labels (GitHub)

+
    +
  • + + status:in-progress or status:triaged — work started +
  • +
  • + + status:in-production — confirmed live (manual) +
  • +
  • + + status:wontfix — closed without shipping +
  • +
+

+ New submissions start as status:triaged. Use the tracker dropdown to move issues through the workflow — labels are created automatically in GitHub. +

+
+

Best submission shape

@@ -72,6 +95,12 @@ export default async function BenLeahPage() { + + + + + + ); } diff --git a/src/app/admin/errors/page.tsx b/src/app/admin/errors/page.tsx index 8958dc107..985c321da 100644 --- a/src/app/admin/errors/page.tsx +++ b/src/app/admin/errors/page.tsx @@ -126,9 +126,11 @@ export default async function ErrorsPage({ async function Body() { const tab = await fetchErrorsTab(filters); - const highSeverity = tab.incidents.filter((i) => i.severity === 'critical' || i.severity === 'error').length; - const affectedUsers = tab.incidents.reduce((sum, i) => sum + i.affectedUsers, 0); - const appGroups = tab.incidents.filter((i) => i.origin === 'app').length; + const { counts } = tab; + const showWiderWindowHint = + tab.incidents.length === 0 && + filters.windowHours < 168 && + ((tab.widerWindowUnresolved ?? 0) > 0 || (tab.widerWindowUntagged ?? 0) > 0); const sourceBreakdown = Array.from( tab.incidents.reduce((map, incident) => { const key = incident.source ?? incident.origin; @@ -141,22 +143,63 @@ export default async function ErrorsPage({ return (

+ {showWiderWindowHint ? ( + +

+ Nothing in the last {filters.windowHours}h + {filters.sport ? ` for sport=${filters.sport}` : ''}, but there are unresolved incidents in the last 7 days + {(tab.widerWindowUnresolved ?? 0) > 0 ? ( + <> + {' '} + ({tab.widerWindowUnresolved} + {filters.sport ? ` tagged ${filters.sport}` : ''}) + + ) : null} + {(tab.widerWindowUntagged ?? 0) > 0 ? ( + <> + {' '} + plus{' '} + {tab.widerWindowUntagged} legacy rows with no sport tag + + ) : null} + .{' '} + + Open 7-day view + + {filters.sport ? ( + <> + {' '} + or{' '} + + drop the sport filter + {' '} + to include untagged baseball errors. + + ) : null} +

+
+ ) : null}

active groups

-

{tab.incidents.length}

-

{appGroups} app · {tab.sentry.data?.length ?? 0} sentry

+

{counts.totalGroups}

+

+ {counts.appGroups} app · {counts.sentryGroups} sentry · {filters.windowHours}h window +

high severity

-

{highSeverity}

+

{counts.highSeverityGroups}

critical and error groups

affected users

-

{affectedUsers}

+

{counts.affectedUsers}

deduped per incident group

@@ -223,7 +266,12 @@ export default async function ErrorsPage({ -

Sentry unresolved

+

+ Sentry unresolved (org-wide) +

+

+ All open Sentry issues — not windowed. Active groups above only count Sentry issues with activity in the selected window. +

{tab.sentry.status === 'ok' && tab.sentry.data ? ( tab.sentry.data.length === 0 ? ( diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 3e753cd11..d55635855 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -21,6 +21,7 @@ import { PanelBoundary } from './_components/PanelBoundary'; import { PanelAllClear, PanelNoData, PanelStale } from './_components/PanelStates'; import { FeatureHealthRollup } from './_components/FeatureHealthRollup'; import { SkeletonStat, SkeletonList, Surface, Eyebrow, StatusPill } from '@/components/fairway'; +import { ADMIN_COMMAND_SHORTCUTS } from './_components/admin-nav'; export const dynamic = 'force-dynamic'; @@ -98,11 +99,11 @@ async function BannerAndKpis() {
w.label === 'Error pipeline'), href: '/admin/errors', }, @@ -281,7 +282,7 @@ function SavedCommandViews({ kpis }: { kpis: OverviewKpis }) { href: '/admin/errors?window=24&severity=error', label: 'Production Health', icon: Gauge, - metric: `${kpis.eventErrors24h} groups`, + metric: `${kpis.incidentGroups24h} groups`, detail: 'Real incidents, source mapped, grouped like Errors tab', }, { @@ -339,11 +340,8 @@ function SavedCommandViews({ kpis }: { kpis: OverviewKpis }) { } async function TriagePanel() { - const { items, sentry } = await fetchTriageQueue(); + const { items, sentry, counts } = await fetchTriageQueue(); const regressed = items.filter((i) => i.substatus === 'regressed'); - const highSeverity = items.filter((i) => i.severity === 'critical' || i.severity === 'error'); - const appItems = items.filter((i) => i.origin === 'app'); - const sentryItems = items.filter((i) => i.origin === 'sentry'); const sportCounts = [ ['Golf', items.filter((i) => i.sport === 'golf').length], ['Baseball', items.filter((i) => i.sport === 'baseball').length], @@ -363,12 +361,15 @@ async function TriagePanel() {

Action lanes

+

+ Last 24h · same incident feed as the Errors tab (unresolved app events + Sentry with activity in window) +

{[ - ['Total groups', items.length, 'coalesced incidents'], - ['High severity', highSeverity.length, 'critical and error'], - ['App groups', appItems.length, 'Supabase admin_events'], - ['Sentry groups', sentryItems.length, sentry.status === 'ok' ? 'live pull' : sentry.status], + ['Total groups', counts.totalGroups, 'coalesced incidents'], + ['High severity', counts.highSeverityGroups, 'critical and error'], + ['App groups', counts.appGroups, 'Supabase admin_events'], + ['Sentry groups', counts.sentryGroups, sentry.status === 'ok' ? 'active in 24h' : sentry.status], ].map(([label, value, caption]) => (

{label}

@@ -457,12 +458,12 @@ async function DeployRail() { } function CommandHeader() { - const nav = [ - { href: '/admin/errors', label: 'Errors', icon: Activity }, - { href: '/admin/health', label: 'Feature Map', icon: RadioTower }, - { href: '/admin/deploys', label: 'Deploys', icon: GitBranch }, - { href: '/admin/audit', label: 'Audit', icon: ShieldCheck }, - ]; + const iconByHref = { + '/admin/errors': Activity, + '/admin/health': RadioTower, + '/admin/deploys': GitBranch, + '/admin/auth': ShieldCheck, + } as const; return (
@@ -477,8 +478,8 @@ function CommandHeader() {