Skip to content
Closed
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: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
# -----------------------------------------------------------------------------
Expand Down
6 changes: 5 additions & 1 deletion src/app/admin/_components/AdminShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -204,6 +206,7 @@ export function AdminShell({
);

return (
<SessionActivityProvider>
<div className="fairway-ds min-h-screen bg-canvas-gradient">
<AppShell
sections={sections}
Expand Down Expand Up @@ -253,5 +256,6 @@ export function AdminShell({
placeholder="Jump to…"
/>
</div>
</SessionActivityProvider>
);
}
14 changes: 12 additions & 2 deletions src/app/admin/_components/__tests__/admin-nav.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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');
});
});
10 changes: 10 additions & 0 deletions src/app/admin/_components/admin-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type AdminHref =
| '/admin/golf'
| '/admin/baseball'
| '/admin/ben-leah'
| '/admin/work'
| '/admin/users'
| '/admin/jobs'
| '/admin/deploys'
Expand All @@ -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;
}
169 changes: 169 additions & 0 deletions src/app/admin/ben-leah/BenLeahIssueBoard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PanelNoData
label="GitHub issue tracker not configured"
description="Set GITHUB_ISSUES_TOKEN in production so Helm Bridge can list ben-leah issues."
/>
);
}

if (board.status === 'error' || !board.data) {
return <PanelStale label="Ben + Leah issue tracker" error={board.error} />;
}

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 (
<div className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap gap-2">
<StatusPill tone={githubIntegrationTone(board.status)} dot size="sm">
GitHub {board.status === 'ok' ? 'live' : 'stale'}
</StatusPill>
<StatusPill tone={productionReadyAt ? 'success' : 'neutral'} dot={Boolean(productionReadyAt)} size="sm">
{productionReadyAt
? `prod ${productionCommitSha?.slice(0, 7) ?? 'build'}`
: 'prod timing n/a'}
</StatusPill>
{fetchedAt ? (
<StatusPill tone="neutral" dot={false} size="sm">
checked {new Date(fetchedAt).toLocaleTimeString()}
</StatusPill>
) : null}
</div>
<Link
href={repoIssuesUrl}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-xs font-medium text-accent-700 hover:underline"
>
<GitPullRequest size={14} aria-hidden />
All issues on {repoLabel}
</Link>
</div>

<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{(
[
['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]) => (
<div key={label} className="rounded-xl border border-warm-200/70 bg-surface px-3 py-2">
<StatTile label={label} value={value} tone="neutral" mono />
<p className="mt-1 text-xs text-warm-500">{caption}</p>
</div>
))}
</div>

{productionReadyAt ? (
<p className="flex items-center gap-2 text-xs text-warm-500">
<Rocket size={14} className="text-accent-600" aria-hidden />
Production last ready {formatWhen(new Date(productionReadyAt).toISOString())}
{productionCommitSha ? ` · ${productionCommitSha.slice(0, 7)}` : null}
. Closed issues after that time show as <span className="font-medium text-warm-700">In production</span>.
</p>
) : (
<p className="text-xs text-warm-500">
Set VERCEL_API_TOKEN to correlate closed issues with production deploy timing. Without it, status uses GitHub labels only.
</p>
)}

{issues.length === 0 ? (
<PanelNoData
label="No Ben + Leah issues yet"
description="Submissions from this tab create GitHub issues tagged ben-leah. Submit the first one above."
/>
) : (
<BenLeahIssueTable issues={issues} />
)}

<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{(Object.keys(TRACK_META) as BenLeahTrackStatus[]).map((status) => (
<div key={status} className="rounded-fw-md bg-surface-sunken px-3 py-2">
<div className="flex items-center justify-between gap-2">
<StatusPill tone={TRACK_META[status].tone} dot size="sm">
{TRACK_META[status].label}
</StatusPill>
<span className="font-fw-mono text-sm tabular-nums text-warm-900">{counts[status]}</span>
</div>
<p className="mt-1 text-xs text-warm-500">{TRACK_META[status].description}</p>
</div>
))}
</div>
</div>
);
}
100 changes: 100 additions & 0 deletions src/app/admin/ben-leah/BenLeahIssueTable.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
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 (
<tr>
<td className="sticky left-0 z-10 bg-surface py-2 pl-3 pr-3">
<Link
href={issue.html_url}
target="_blank"
rel="noreferrer"
className="group inline-flex items-center gap-1 font-fw-mono text-xs text-accent-700 hover:underline"
>
#{issue.number}
<ExternalLink size={12} className="opacity-60 group-hover:opacity-100" aria-hidden />
</Link>
<p className="mt-1 max-w-[280px] truncate text-sm font-medium text-warm-900" title={issue.displayTitle}>
{issue.displayTitle}
</p>
</td>
<td className="px-3">
<StatusPill tone={meta.tone} dot size="sm">
{meta.label}
</StatusPill>
</td>
<td className="px-3">
<BenLeahIssueWorkflowSelect issue={issue} />
</td>
<td className="px-3 text-xs text-warm-600">
{issue.kind ? KIND_LABEL[issue.kind] ?? issue.kind : '—'}
</td>
<td className="px-3 text-xs capitalize text-warm-600">{issue.priority ?? '—'}</td>
<td className="px-3 text-xs text-warm-600">{issue.category ?? '—'}</td>
<td className="px-3 text-xs text-warm-500">{formatWhen(issue.updated_at)}</td>
<td className="px-3 text-xs text-warm-500">
{issue.closed_at ? formatWhen(issue.closed_at) : '—'}
</td>
</tr>
);
}

export function BenLeahIssueTable({ issues }: { issues: BenLeahTrackedIssue[] }) {
return (
<div className="overflow-x-auto rounded-xl border border-warm-200/70">
<table className="w-full min-w-[980px] text-sm">
<thead>
<tr className="text-left text-xs uppercase tracking-widest text-warm-500">
<th className="sticky left-0 z-10 bg-surface-sunken py-2 pl-3 pr-3">Issue</th>
<th className="px-3">Derived status</th>
<th className="px-3">Set workflow</th>
<th className="px-3">Type</th>
<th className="px-3">Priority</th>
<th className="px-3">Category</th>
<th className="px-3">Updated</th>
<th className="px-3">Closed</th>
</tr>
</thead>
<tbody className="divide-y divide-warm-200/60 bg-surface">
{issues.map((issue) => (
<IssueRow key={issue.number} issue={issue} />
))}
</tbody>
</table>
</div>
);
}
Loading
Loading