@fix-portal/ci-frontend 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -766,6 +766,9 @@ function CiBoardContent() {
|
|
|
766
766
|
const hideNoCi = useHideNoCi();
|
|
767
767
|
const isAdmin = useCiAdmin();
|
|
768
768
|
const [stepperOpen, setStepperOpen] = useState5(false);
|
|
769
|
+
const earlyRepos = snapshot.data?.repositories ?? [];
|
|
770
|
+
const earlyFiltered = isAdmin ? earlyRepos : earlyRepos.filter((r) => !r.private);
|
|
771
|
+
const openPrs = flattenOpenPrs(hideNoCi.hidden ? earlyFiltered.filter((r) => !isNoCi(r)) : earlyFiltered);
|
|
769
772
|
useEffect5(() => {
|
|
770
773
|
if (openPrs.length === 0) {
|
|
771
774
|
setStepperOpen(false);
|
|
@@ -782,10 +785,10 @@ function CiBoardContent() {
|
|
|
782
785
|
}
|
|
783
786
|
const { refreshedAt, repositories: allRepositories, lastMergedPr: rawLastMerged } = snapshot.data;
|
|
784
787
|
const repositories = isAdmin ? allRepositories : allRepositories.filter((r) => !r.private);
|
|
785
|
-
const summary = isAdmin && !hideNoCi.hidden ? snapshot.data.summary : computeSummary(visibleRepos);
|
|
786
|
-
const lastMergedPr = rawLastMerged && visibleRepos.some((r) => r.name === rawLastMerged.repo) ? rawLastMerged : null;
|
|
787
788
|
const noCiCount = repositories.filter(isNoCi).length;
|
|
788
789
|
const visibleRepos = hideNoCi.hidden ? repositories.filter((r) => !isNoCi(r)) : repositories;
|
|
790
|
+
const summary = isAdmin && !hideNoCi.hidden ? snapshot.data.summary : computeSummary(visibleRepos);
|
|
791
|
+
const lastMergedPr = rawLastMerged && visibleRepos.some((r) => r.name === rawLastMerged.repo) ? rawLastMerged : null;
|
|
789
792
|
const repoNames = visibleRepos.map((r) => r.name);
|
|
790
793
|
const hiddenCount = repositories.length - visibleRepos.length;
|
|
791
794
|
const publicRepos = visibleRepos.filter((r) => !r.private);
|
|
@@ -795,7 +798,6 @@ function CiBoardContent() {
|
|
|
795
798
|
const KEY_PRIVATE = "section:private";
|
|
796
799
|
const sectionKeys = showGroups ? [KEY_PUBLIC, KEY_PRIVATE] : [];
|
|
797
800
|
const allCollapsed = collapse.allCollapsed([...repoNames, ...sectionKeys]);
|
|
798
|
-
const openPrs = flattenOpenPrs(visibleRepos);
|
|
799
801
|
const nextPr = openPrs[0] ?? null;
|
|
800
802
|
let repoListContent;
|
|
801
803
|
if (visibleRepos.length === 0 && hideNoCi.hidden) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/CiBoard.tsx","../src/CiAdminContext.tsx","../src/CiConfigContext.tsx","../src/pages/CiBoardContent.tsx","../src/hooks/useDashboardSnapshot.ts","../src/api/getDashboardSnapshot.ts","../src/hooks/useCollapseState.ts","../src/hooks/useHideNoCi.ts","../src/lib/isNoCi.ts","../src/lib/computeSummary.ts","../src/components/SummaryStrip.tsx","../src/lib/formatCompactNumber.ts","../src/lib/relativeTime.ts","../src/components/CiWeatherBar.tsx","../src/lib/isAllowedHref.ts","../src/components/RepoBoard.tsx","../src/components/SignalChip.tsx","../src/lib/stateLabel.ts","../src/components/RepoMetricsLine.tsx","../src/components/PullRequestList.tsx","../src/components/JobLaneRow.tsx","../src/lib/dedupeJobLabel.ts","../src/lib/worstState.ts","../src/components/RepoActivityIndicator.tsx","../src/components/RepoSection.tsx","../src/components/MetricsLegend.tsx","../src/components/StatusLegend.tsx","../src/components/PullRequestStepper.tsx","../src/lib/prAgeTone.ts","../src/lib/flattenOpenPrs.ts","../src/DefaultFooter.tsx"],"sourcesContent":["import { useState, useContext } from 'react'\nimport type { ReactNode } from 'react'\nimport { QueryClient, QueryClientProvider, QueryClientContext } from '@tanstack/react-query'\nimport { CiAdminProvider } from './CiAdminContext'\nimport { CiConfigProvider, DEFAULT_CI_API_BASE } from './CiConfigContext'\nimport { CiBoardContent } from './pages/CiBoardContent'\nimport { DefaultFooter } from './DefaultFooter'\n\nexport interface CiBoardProps {\n /** Whether the viewer is an admin: sees private repos + actionable PR links. Host-computed. */\n adminSignal: boolean\n /** Origin of the CI backend snapshot API (no trailing slash). Defaults to '' (relative URLs — requires a same-origin /api/ proxy). Pass 'https://ci.fixportal.org' to reach the public FixPortal backend. */\n apiBase?: string\n /** Full URL the board fetches when the viewer is admin. The host's backend should proxy this to the CI backend's /api/dashboard/snapshot/admin endpoint, adding the X-Admin-Key header server-side so the shared secret never reaches the browser. When unset, admin viewers see the public (private-repo-stripped) snapshot. */\n adminSnapshotUrl?: string\n /** Brand mark for the header. Defaults to a plain text wordmark. */\n logo?: ReactNode\n /** Footer node. Defaults to a generic, brand-free footer. */\n footerSlot?: ReactNode\n}\n\nfunction QueryClientSafeProvider({ children }: { children: ReactNode }) {\n const existingClient = useContext(QueryClientContext)\n const [localClient] = useState(() => {\n if (!existingClient) {\n return new QueryClient({\n defaultOptions: {\n queries: {\n retry: 1,\n refetchOnWindowFocus: false,\n },\n },\n })\n }\n return null\n })\n\n if (existingClient) {\n return <>{children}</>\n }\n\n return (\n <QueryClientProvider client={localClient!}>\n {children}\n </QueryClientProvider>\n )\n}\n\n// The board is style-free at the component level: consumers import the\n// stylesheets explicitly -- `@fix-portal/ci-frontend/board.css` (always) and\n// optionally `@fix-portal/ci-frontend/tokens.css` if they have no design system\n// of their own. This keeps CSS out of the JS bundle and lets a host with its\n// own tokens (e.g. the simulator) skip the vendored set.\nexport function CiBoard({ adminSignal, apiBase = DEFAULT_CI_API_BASE, adminSnapshotUrl, logo, footerSlot }: CiBoardProps) {\n return (\n <CiConfigProvider value={{ apiBase, adminSnapshotUrl }}>\n <QueryClientSafeProvider>\n <div className=\"ci-page\">\n <div className=\"ci-embed\">\n <header className=\"ci-embed__header\">\n <span className=\"ci-embed__lockup\">\n {logo ?? <span className=\"ci-embed__wordmark-text\">CI Dashboard</span>}\n <span className=\"ci-embed__descriptor\">\n CI Dashboard {adminSignal ? '[Admin]' : '[Guest]'}\n </span>\n </span>\n </header>\n <CiAdminProvider value={adminSignal}>\n <CiBoardContent />\n </CiAdminProvider>\n </div>\n {footerSlot ?? <DefaultFooter />}\n </div>\n </QueryClientSafeProvider>\n </CiConfigProvider>\n )\n}\n","import { createContext, use } from 'react'\n\n// Whether the current viewer is the signed-in platform admin. Drives whether PR\n// rows render as actionable GitHub links or as plain, non-interactive text.\n// Defaults to false (anonymous → read-only).\nconst CiAdminContext = createContext(false)\nCiAdminContext.displayName = 'CiAdminContext'\n\nexport const CiAdminProvider = CiAdminContext.Provider\n\nexport function useCiAdmin(): boolean {\n return use(CiAdminContext)\n}\n","import { createContext, use } from 'react'\n\n// Runtime config for the CI board. apiBase is the origin of the CI backend\n// snapshot API (no trailing slash). Empty string means relative URLs — works\n// with any nginx proxy (Docker Compose or www.fixportal.org/ci).\n// adminSnapshotUrl: when set, the board fetches this URL directly (instead of\n// apiBase + /api/dashboard/snapshot) when the viewer is admin. Lets a host\n// proxy the admin endpoint server-side without exposing the shared key to the\n// browser — the host passes a same-origin relative URL here, and its backend\n// adds the X-Admin-Key before forwarding to the CI backend.\nexport interface CiConfig {\n apiBase: string\n adminSnapshotUrl?: string\n}\n\nexport const DEFAULT_CI_API_BASE = ''\n\nconst CiConfigContext = createContext<CiConfig>({ apiBase: DEFAULT_CI_API_BASE })\nCiConfigContext.displayName = 'CiConfigContext'\n\nexport const CiConfigProvider = CiConfigContext.Provider\n\nexport function useCiConfig(): CiConfig {\n return use(CiConfigContext)\n}\n","import { useState, useEffect } from 'react'\nimport { useDashboardSnapshot } from '../hooks/useDashboardSnapshot'\nimport { useCollapseState } from '../hooks/useCollapseState'\nimport { useHideNoCi } from '../hooks/useHideNoCi'\nimport { useCiAdmin } from '../CiAdminContext'\nimport { isNoCi } from '../lib/isNoCi'\nimport { computeSummary } from '../lib/computeSummary'\nimport { SummaryStrip } from '../components/SummaryStrip'\nimport { RepoBoard } from '../components/RepoBoard'\nimport { RepoSection } from '../components/RepoSection'\nimport { MetricsLegend } from '../components/MetricsLegend'\nimport { StatusLegend } from '../components/StatusLegend'\nimport { PullRequestStepper } from '../components/PullRequestStepper'\nimport { flattenOpenPrs } from '../lib/flattenOpenPrs'\nimport { formatRelativeTime } from '../lib/relativeTime'\n\nexport function CiBoardContent() {\n const snapshot = useDashboardSnapshot()\n const collapse = useCollapseState()\n const hideNoCi = useHideNoCi()\n const isAdmin = useCiAdmin()\n const [stepperOpen, setStepperOpen] = useState(false)\n\n useEffect(() => {\n if (openPrs.length === 0) {\n setStepperOpen(false)\n }\n }, [openPrs.length])\n\n if (snapshot.isPending) {\n return (\n <main className=\"dashboard-page\">\n <div className=\"state-msg\">Loading dashboard…</div>\n </main>\n )\n }\n\n if (snapshot.isError) {\n return (\n <main className=\"dashboard-page\">\n <div className=\"state-msg state-msg--error\">Dashboard unavailable.</div>\n </main>\n )\n }\n\n // 204 -> null: backend is up but has not produced a snapshot yet.\n if (!snapshot.data) {\n return (\n <main className=\"dashboard-page\">\n <div className=\"state-msg\">Waiting for the first refresh…</div>\n </main>\n )\n }\n\n const { refreshedAt, repositories: allRepositories, lastMergedPr: rawLastMerged } = snapshot.data\n // Non-admin viewers see only public repos; admin sees all. Filtering here drives\n // everything downstream — summary counts, stepper PRs, and next-in-queue all\n // reflect exactly the repos displayed on screen.\n const repositories = isAdmin ? allRepositories : allRepositories.filter(r => !r.private)\n const summary = (isAdmin && !hideNoCi.hidden) ? snapshot.data.summary : computeSummary(visibleRepos)\n // Only surface lastMergedPr when its repo is in the visible set; a private-repo\n // merge is invisible to the public viewer and the link would 404 for them.\n const lastMergedPr = rawLastMerged && visibleRepos.some(r => r.name === rawLastMerged.repo)\n ? rawLastMerged\n : null\n // Compute the names list and the all-collapsed flag once — they were rebuilt\n // and re-traversed twice per render (the onClick and the button label).\n const noCiCount = repositories.filter(isNoCi).length\n const visibleRepos = hideNoCi.hidden ? repositories.filter(r => !isNoCi(r)) : repositories\n const repoNames = visibleRepos.map(r => r.name)\n const hiddenCount = repositories.length - visibleRepos.length\n const publicRepos = visibleRepos.filter(r => !r.private)\n const privateRepos = visibleRepos.filter(r => r.private)\n const showGroups = publicRepos.length > 0 && privateRepos.length > 0\n const KEY_PUBLIC = 'section:public'\n const KEY_PRIVATE = 'section:private'\n const sectionKeys = showGroups ? [KEY_PUBLIC, KEY_PRIVATE] : []\n const allCollapsed = collapse.allCollapsed([...repoNames, ...sectionKeys])\n // The stepper opens at the head of this oldest-first list, so its first entry\n // is \"next in queue\" — derive it from visibleRepos (the Hide No-CI filter\n // applied) so the card and stepper never advertise a PR from a repo the board\n // is currently hiding.\n const openPrs = flattenOpenPrs(visibleRepos)\n const nextPr = openPrs[0] ?? null\n\n let repoListContent\n if (visibleRepos.length === 0 && hideNoCi.hidden) {\n repoListContent = <div className=\"state-msg\">All repositories are No-CI — hidden.</div>\n } else if (showGroups) {\n repoListContent = (\n <>\n <RepoSection\n label=\"Public\"\n count={publicRepos.length}\n collapsed={collapse.isCollapsed(KEY_PUBLIC)}\n onToggle={() => collapse.toggle(KEY_PUBLIC)}\n />\n {!collapse.isCollapsed(KEY_PUBLIC) &&\n publicRepos.map(repository => (\n <RepoBoard\n key={repository.name}\n repository={repository}\n collapsed={collapse.isCollapsed(repository.name)}\n onToggle={collapse.toggle}\n />\n ))\n }\n <RepoSection\n label=\"Private\"\n count={privateRepos.length}\n collapsed={collapse.isCollapsed(KEY_PRIVATE)}\n onToggle={() => collapse.toggle(KEY_PRIVATE)}\n />\n {!collapse.isCollapsed(KEY_PRIVATE) &&\n privateRepos.map(repository => (\n <RepoBoard\n key={repository.name}\n repository={repository}\n collapsed={collapse.isCollapsed(repository.name)}\n onToggle={collapse.toggle}\n />\n ))\n }\n </>\n )\n } else {\n repoListContent = visibleRepos.map(repository => (\n <RepoBoard\n key={repository.name}\n repository={repository}\n collapsed={collapse.isCollapsed(repository.name)}\n onToggle={collapse.toggle}\n />\n ))\n }\n\n return (\n <main className=\"dashboard-page\">\n <div className=\"dashboard__toolbar\">\n <span className=\"dashboard__scope\">{snapshot.data.org} · {isAdmin ? 'all repositories' : 'public repositories'}</span>\n <span className=\"dashboard__toolbar-right\">\n {noCiCount > 0 && (\n <button\n type=\"button\"\n className={`dashboard__hide-noci${hideNoCi.hidden ? ' dashboard__hide-noci--on' : ''}`}\n onClick={hideNoCi.toggle}\n aria-pressed={hideNoCi.hidden}\n >\n {hideNoCi.hidden ? `Show No-CI · ${hiddenCount} hidden` : 'Hide No-CI'}\n </button>\n )}\n <button\n type=\"button\"\n className=\"dashboard__collapse-all\"\n onClick={() => (allCollapsed ? collapse.expandAll() : collapse.collapseAll([...repoNames, ...sectionKeys]))}\n >\n {allCollapsed ? '⊞ Expand all' : '⊟ Collapse all'}\n </button>\n <span className=\"dashboard__refreshed\">\n <span className=\"live-dot\" aria-hidden=\"true\" />\n updated {formatRelativeTime(refreshedAt)}\n </span>\n </span>\n </div>\n <SummaryStrip summary={summary} onOpenPrs={isAdmin ? () => setStepperOpen(true) : undefined} lastMerged={lastMergedPr} nextPr={nextPr} ciTrend={snapshot.data.ciTrend ?? []} />\n <div className=\"repo-list\">\n {repoListContent}\n </div>\n <StatusLegend />\n <MetricsLegend />\n {stepperOpen && openPrs.length > 0 && (\n <PullRequestStepper prs={openPrs} onClose={() => setStepperOpen(false)} />\n )}\n </main>\n )\n}\n","import { useQuery } from '@tanstack/react-query'\nimport { getDashboardSnapshot } from '../api/getDashboardSnapshot'\nimport { useCiConfig } from '../CiConfigContext'\nimport { useCiAdmin } from '../CiAdminContext'\n\nexport function useDashboardSnapshot() {\n const { apiBase, adminSnapshotUrl } = useCiConfig()\n const isAdmin = useCiAdmin()\n // Admin viewers use the host-proxied admin URL so the shared key stays\n // server-side. Fall back to the public endpoint for non-admin or when no\n // admin URL was wired up by the host.\n const snapshotUrl = isAdmin && adminSnapshotUrl\n ? adminSnapshotUrl\n : `${apiBase.replace(/\\/$/, '')}/api/dashboard/snapshot`\n return useQuery({\n queryKey: ['dashboard-snapshot', snapshotUrl],\n queryFn: () => getDashboardSnapshot(snapshotUrl),\n refetchInterval: 60_000,\n // The 60s poll already drives freshness; without these, an incidental tab\n // focus refetches and re-renders the whole board between ticks. Set per-query\n // (not on the shared app QueryClient) so the host app is unaffected;\n // structural sharing then lets the memoised boards skip a no-change tick.\n staleTime: 30_000,\n refetchOnWindowFocus: false,\n })\n}\n","import type { DashboardSnapshot } from './types'\n\n// 204 No Content is the documented \"no snapshot yet\" state. Return null rather\n// than calling response.json() on an empty body. snapshotUrl is the resolved\n// URL to fetch — callers compute it from apiBase + /api/dashboard/snapshot or\n// from an adminSnapshotUrl override when the viewer is an admin.\nexport async function getDashboardSnapshot(snapshotUrl: string): Promise<DashboardSnapshot | null> {\n const response = await fetch(snapshotUrl)\n if (response.status === 204) return null\n if (!response.ok) throw new Error(`Dashboard snapshot failed: ${response.status}`)\n return response.json()\n}\n","import { useCallback, useEffect, useState } from 'react'\n\nconst KEY = 'ci-dashboard:collapsed'\n\nfunction load(): Set<string> {\n try {\n const raw = localStorage.getItem(KEY)\n return new Set(raw ? (JSON.parse(raw) as string[]) : [])\n } catch {\n return new Set()\n }\n}\n\nfunction save(set: Set<string>) {\n try {\n localStorage.setItem(KEY, JSON.stringify([...set]))\n } catch {\n // ignore (private mode / quota) — collapse state is best-effort\n }\n}\n\n// A repo absent from the set is expanded, so new repos default to expanded.\nexport function useCollapseState() {\n const [collapsed, setCollapsed] = useState<Set<string>>(load)\n\n useEffect(() => {\n save(collapsed)\n }, [collapsed])\n\n const mutate = useCallback((fn: (next: Set<string>) => void) => {\n setCollapsed(prev => {\n const next = new Set(prev)\n fn(next)\n return next\n })\n }, [])\n\n return {\n isCollapsed: useCallback((name: string) => collapsed.has(name), [collapsed]),\n allCollapsed: useCallback(\n (names: string[]) => names.length > 0 && names.every(n => collapsed.has(n)),\n [collapsed],\n ),\n toggle: useCallback((name: string) => mutate(s => (s.has(name) ? s.delete(name) : s.add(name))), [mutate]),\n collapseAll: useCallback((names: string[]) => mutate(s => names.forEach(n => s.add(n))), [mutate]),\n expandAll: useCallback(() => mutate(s => s.clear()), [mutate]),\n }\n}\n","import { useCallback, useEffect, useState } from 'react'\n\nconst KEY = 'ci-dashboard:hide-no-ci'\n\nfunction load(): boolean {\n try {\n return localStorage.getItem(KEY) === 'true'\n } catch {\n return false\n }\n}\n\n// A single persisted boolean: whether No-CI repos are hidden from the board.\n// Default false (shown). Mirrors useCollapseState's best-effort persistence.\nexport function useHideNoCi() {\n const [hidden, setHidden] = useState<boolean>(load)\n\n useEffect(() => {\n try {\n localStorage.setItem(KEY, String(hidden))\n } catch {\n // ignore (private mode / quota) — hide state is best-effort\n }\n }, [hidden])\n\n const toggle = useCallback(() => {\n setHidden(prev => !prev)\n }, [])\n\n return { hidden, toggle }\n}\n","import type { RepositorySnapshot } from '../api/types'\n\n// A repository is \"No CI\" when it has no workflows at all — the same definition\n// the backend uses for the `no-ci` summary count (repos with Workflows.Count == 0).\nexport function isNoCi(repository: RepositorySnapshot): boolean {\n return (repository.workflows?.length ?? 0) === 0\n}\n","import type { RepositorySnapshot, SummaryCount } from '../api/types'\nimport { isNoCi } from './isNoCi'\n\n// Keys that always appear in the summary, even at zero, so the strip structure\n// is stable regardless of the filtered repo set. Mirrors what the server sends\n// in snapshot.data.summary so admin and guest panels look the same.\nconst ALWAYS_VISIBLE_KEYS = new Set([\n 'repos', 'workflows', 'nloc', // inventory\n 'open-prs', // Review panel (carries next-in-queue / last-merged)\n 'running', 'failing', 'no-ci', // core CI status\n 'deploys-running', 'deploys-failing', // deploy lane (zero = nothing deploying / all clean)\n 'packages-failing', // package lane\n])\n\n// Recomputes summary counts from a filtered repo list. Mirrors the server-side\n// aggregation so the strip reflects exactly the repos being displayed.\nexport function computeSummary(repos: RepositorySnapshot[]): SummaryCount[] {\n const workflows = repos.flatMap(r => r.workflows)\n const deploys = repos.flatMap(r => r.deploys ?? [])\n const packages = repos.flatMap(r => r.packages ?? [])\n const openPrs = repos.flatMap(r => r.pullRequests ?? [])\n const nloc = repos.reduce((acc, r) => acc + (r.metrics?.nloc ?? 0), 0)\n\n const all: SummaryCount[] = [\n { key: 'repos', count: repos.length },\n { key: 'workflows', count: workflows.length },\n { key: 'failing', count: workflows.filter(w => w.state === 'failure').length },\n { key: 'running', count: workflows.filter(w => w.state === 'running').length },\n { key: 'no-ci', count: repos.filter(isNoCi).length },\n { key: 'open-prs', count: openPrs.length },\n { key: 'nloc', count: nloc },\n { key: 'deploys-failing', count: deploys.filter(d => d.state === 'failure').length },\n { key: 'deploys-running', count: deploys.filter(d => d.state === 'running').length },\n { key: 'packages-failing', count: packages.filter(p => p.state === 'failure').length },\n ]\n\n return all.filter(c => ALWAYS_VISIBLE_KEYS.has(c.key) || c.count > 0)\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type { CiTrendBucket, MergedPr, SummaryCount } from '../api/types'\nimport type { OpenPr } from '../lib/flattenOpenPrs'\nimport { formatCompactNumber } from '../lib/formatCompactNumber'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { CiWeatherBar } from './CiWeatherBar'\nimport { isAllowedHref } from '../lib/isAllowedHref'\nconst SUMMARY_LABELS: Record<string, string> = {\n repos: 'Repositories',\n workflows: 'Workflows',\n failing: 'Failing',\n running: 'Running',\n 'no-ci': 'No CI',\n 'open-prs': 'Open PRs',\n nloc: 'Lines of code',\n 'deploys-failing': 'Deploys failing',\n 'deploys-running': 'Deploys running',\n 'packages-failing': 'Packages failing',\n}\n\n// Three panels group the counts by what the operator is looking for: review work,\n// pipeline health, and inventory. Keys appear in this fixed order regardless of the\n// summary array's order; a panel with no present keys is hidden.\nconst PANELS: { title: string; keys: string[] }[] = [\n { title: 'Review', keys: ['open-prs'] },\n { title: 'CI status', keys: ['running', 'failing', 'packages-failing', 'deploys-running', 'deploys-failing', 'no-ci'] },\n { title: 'Inventory', keys: ['repos', 'workflows', 'nloc'] },\n]\n\nconst NEUTRAL_KEYS = new Set(['repos', 'workflows', 'nloc'])\nconst EMPTY_TREND: CiTrendBucket[] = []\n\nfunction labelFor(key: string, count: number) {\n // 'Open PRs' is the only count-driven noun on the strip; singularise it so a\n // single PR doesn't read as '1 Open PRs'.\n if (key === 'open-prs') return count === 1 ? 'Open PR' : 'Open PRs'\n return SUMMARY_LABELS[key] ?? key.replaceAll('-', ' ')\n}\n\nfunction formatCount(key: string, count: number) {\n return key === 'nloc' ? formatCompactNumber(count) : count\n}\n\n// A non-zero count is coloured to mirror its chip: failures red, running blue,\n// no-ci indigo, the rest amber. open-prs gets its own non-alarm \"review\" tone.\n// Zero / inventory quiet.\nfunction toneFor(key: string, count: number): string {\n if (count === 0 || NEUTRAL_KEYS.has(key)) return 'ok'\n if (key === 'open-prs') return 'review'\n if (key === 'failing' || key === 'deploys-failing' || key === 'packages-failing') return 'fail'\n if (key === 'running' || key === 'deploys-running') return 'run'\n if (key === 'no-ci') return 'no-ci'\n return 'alert'\n}\n\nexport function SummaryStrip({ summary, onOpenPrs, lastMerged, nextPr = null, ciTrend = EMPTY_TREND }: { summary: SummaryCount[]; onOpenPrs?: () => void; lastMerged: MergedPr | null; nextPr?: OpenPr | null; ciTrend?: CiTrendBucket[] }) {\n const byKey = new Map(summary.map(s => [s.key, s.count]))\n\n const [trendInfoOpen, setTrendInfoOpen] = useState(false)\n const trendLabelRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n if (!trendInfoOpen) return\n function handleOutside(e: MouseEvent) {\n if (!trendLabelRef.current?.contains(e.target as Node)) {\n setTrendInfoOpen(false)\n }\n }\n function handleEsc(e: KeyboardEvent) {\n if (e.key === 'Escape') setTrendInfoOpen(false)\n }\n document.addEventListener('mousedown', handleOutside)\n document.addEventListener('keydown', handleEsc)\n return () => {\n document.removeEventListener('mousedown', handleOutside)\n document.removeEventListener('keydown', handleEsc)\n }\n }, [trendInfoOpen])\n\n return (\n <section className=\"summary-panels\">\n {PANELS.map(panel => {\n const items: { key: string; count: number }[] = []\n for (const k of panel.keys) {\n const count = byKey.get(k)\n if (count !== undefined) items.push({ key: k, count })\n }\n if (items.length === 0) return null\n const isReview = panel.title === 'Review'\n const isCiStatus = panel.title === 'CI status'\n return (\n <div key={panel.title} className={`summary-panel${isReview ? ' summary-panel--review' : ''}${isCiStatus ? ' summary-panel--ci' : ''}`}>\n <span className=\"summary-panel__title\">{panel.title}</span>\n <div className=\"summary-panel__items\">\n {items.map(item => {\n const body = (\n <>\n <span className=\"summary__count\">{formatCount(item.key, item.count)}</span>\n <span className=\"summary__label\">{labelFor(item.key, item.count)}</span>\n </>\n )\n if (item.key === 'open-prs' && onOpenPrs) {\n return (\n <button\n key={item.key}\n type=\"button\"\n className=\"summary__item summary__item--btn\"\n data-key={item.key}\n data-tone={toneFor(item.key, item.count)}\n onClick={onOpenPrs}\n disabled={item.count === 0}\n >{body}</button>\n )\n }\n return (\n <div key={item.key} className=\"summary__item\" data-key={item.key} data-tone={toneFor(item.key, item.count)}>{body}</div>\n )\n })}\n </div>\n {isReview && nextPr && (\n <div className=\"summary-panel__next\">\n <span className=\"summary-panel__q-lab\">next in queue</span>\n <span className=\"summary-panel__q-body\">{nextPr.repo} #{nextPr.number}</span>\n <span className=\"summary-panel__q-title\">{nextPr.title}</span>\n </div>\n )}\n {isReview && lastMerged && (\n <a className=\"summary-panel__merged\" href={isAllowedHref(lastMerged.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\">\n <span className=\"summary-panel__q-lab\">\n last merged <span className=\"summary-panel__q-age\">({formatRelativeTime(lastMerged.mergedAt)})</span>\n </span>\n <span className=\"summary-panel__q-body\">{lastMerged.repo} #{lastMerged.number}</span>\n <span className=\"summary-panel__q-title\">{lastMerged.title}</span>\n </a>\n )}\n {panel.title === 'CI status' && ciTrend.length > 0 && (\n <div className=\"summary-panel__trend\">\n <CiWeatherBar trend={ciTrend} />\n <div ref={trendLabelRef} className=\"summary-panel__trend-label-row\">\n <span className=\"summary-panel__trend-lab\">CI health · 24h</span>\n <button\n type=\"button\"\n className=\"ci-trend-info-btn\"\n aria-label=\"CI health information\"\n aria-expanded={trendInfoOpen}\n aria-controls={trendInfoOpen ? 'ci-trend-popover' : undefined}\n onClick={() => setTrendInfoOpen(o => !o)}\n >\n i\n </button>\n {trendInfoOpen && (\n <section\n id=\"ci-trend-popover\"\n aria-label=\"CI health explanation\"\n className=\"ci-trend-popover\"\n >\n <div className=\"ci-trend-popover__title\">CI health · 24h</div>\n <p>Each bar is a 1-hour bucket of CI activity across the whole org.</p>\n <p><span aria-hidden=\"true\" className=\"ci-trend-popover__swatch ci-trend-popover__swatch--fail\">■</span> Red — any run failed that hour (on any branch).</p>\n <p><span aria-hidden=\"true\" className=\"ci-trend-popover__swatch ci-trend-popover__swatch--pass\">■</span> Green — runs present, none failed. Quiet hours inherit the previous state.</p>\n <p>Oldest bar on the left, newest on the right.</p>\n <div className=\"ci-trend-popover__caret\" aria-hidden=\"true\" />\n </section>\n )}\n </div>\n </div>\n )}\n </div>\n )\n })}\n </section>\n )\n}\n","/** Compact integer formatting for large counts: 12345 -> \"12.3k\", 980 -> \"980\". */\nexport function formatCompactNumber(value: number): string {\n if (value < 1000) return String(value)\n if (value < 1_000_000) return `${(value / 1000).toFixed(1)}k`\n return `${(value / 1_000_000).toFixed(1)}M`\n}\n","/** Compact \"x ago\" formatting for observed-at timestamps in the status board. */\nexport function formatRelativeTime(iso: string): string {\n const then = new Date(iso).getTime()\n if (Number.isNaN(then)) return ''\n\n const minutes = Math.round((Date.now() - then) / 60_000)\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes}m ago`\n\n const hours = Math.round(minutes / 60)\n if (hours < 24) return `${hours}h ago`\n\n const days = Math.round(hours / 24)\n return `${days}d ago`\n}\n","import type { CiTrendBucket, CiTrendState } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\n\n// A status-page \"weather\" strip: one block per hourly bucket, coloured by the\n// carry-forward worst-state from the backend. Pure presentational; colours come\n// from CSS via the data-state attribute.\nconst BLOCK_WORD: Record<CiTrendState, string> = {\n passing: 'healthy',\n failing: 'failing',\n noData: 'no data',\n}\n\nexport function CiWeatherBar({ trend }: { trend: CiTrendBucket[] }) {\n if (trend.length === 0) return null\n const failing = trend.filter(b => b.state === 'failing').length\n const healthy = trend.filter(b => b.state === 'passing').length\n const label = `CI health, last 24h: ${failing} failing, ${healthy} healthy`\n return (\n <>\n <div className=\"ci-weather\" role=\"img\" aria-label={label}>\n {trend.map((b, i) => (\n // Per-block hover reveals which hour a block is and its state — the\n // data was previously exposed only to screen readers via aria-label.\n <span\n key={i}\n className=\"ci-weather__block\"\n data-state={b.state}\n title={`${formatRelativeTime(b.bucketStart)} · ${BLOCK_WORD[b.state]}`}\n />\n ))}\n </div>\n {/* Visible count parity with the aria-label (which already announces it,\n so this is hidden from SR to avoid a double read). */}\n <span className=\"ci-weather__readout\" aria-hidden=\"true\">\n {failing} failing · {healthy} healthy\n </span>\n </>\n )\n}\n","export function isAllowedHref(url: string | undefined): string {\n if (!url) return '#'\n // Accept only http: and https: protocols to prevent javascript: or data: injection.\n // Using a try/catch with URL parser to be safe against malformed strings.\n try {\n const parsed = new URL(url)\n if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {\n return url\n }\n } catch {\n // If it's a relative URL or invalid, check if it starts with / (same-origin relative URL is safe)\n if (url.startsWith('/')) {\n return url\n }\n }\n return '#'\n}\n","import { memo } from 'react'\nimport type { RepositorySnapshot } from '../api/types'\nimport { isNoCi } from '../lib/isNoCi'\nimport { isAllowedHref } from '../lib/isAllowedHref'\nimport { SignalChip } from './SignalChip'\nimport { RepoMetricsLine } from './RepoMetricsLine'\nimport { PullRequestList } from './PullRequestList'\nimport { JobLaneRow } from './JobLaneRow'\nimport { RepoActivityIndicator } from './RepoActivityIndicator'\n\n// Memoised so a poll tick that returns the same data (React Query preserves the\n// repository reference via structural sharing) doesn't re-render every board.\n// onToggle takes the repo name so the parent can pass one stable callback rather\n// than a fresh per-repo closure that would defeat the memo.\nexport const RepoBoard = memo(function RepoBoard({\n repository, collapsed, onToggle,\n}: {\n repository: RepositorySnapshot\n collapsed: boolean\n onToggle: (name: string) => void\n}) {\n const pullRequests = repository.pullRequests ?? []\n const noCi = isNoCi(repository)\n return (\n <section className={`repo-board${collapsed ? ' repo-board--collapsed' : ''}${noCi ? ' repo-board--no-ci' : ''}`}>\n <header>\n <button type=\"button\" className=\"repo-board__toggle\" onClick={() => onToggle(repository.name)} aria-expanded={!collapsed}>\n <span className=\"repo-board__chev\" aria-hidden=\"true\">▸</span>\n {repository.name}\n </button>\n {noCi && <span className=\"repo-board__noci-tag\">No CI</span>}\n <RepoMetricsLine metrics={repository.metrics} />\n <RepoActivityIndicator repository={repository} />\n <a className=\"repo-board__gh-link\" href={isAllowedHref(repository.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\" aria-label={`Open ${repository.name} on GitHub`}>\n GitHub ↗\n </a>\n </header>\n {!collapsed && (\n <>\n {repository.workflows.length === 0 ? (\n <div className=\"repo-board__empty\">no workflows</div>\n ) : (\n <div className=\"repo-workflows\">\n <span className=\"repo-workflows__label\">Workflows · {repository.workflows.length}</span>\n <div className=\"repo-top-signals\">\n {repository.workflows.map(wf => (\n <SignalChip key={wf.file} workflow={wf} />\n ))}\n </div>\n </div>\n )}\n <JobLaneRow kind=\"deploys\" glyph=\"▲\" label=\"Deploys\" signals={repository.deploys ?? []} />\n <JobLaneRow kind=\"packages\" glyph=\"▣\" label=\"Packages\" signals={repository.packages ?? []} />\n <PullRequestList pullRequests={pullRequests} />\n </>\n )}\n </section>\n )\n})\n","import { memo } from 'react'\nimport type { WorkflowSnapshot } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { stateLabel } from '../lib/stateLabel'\nimport { isAllowedHref } from '../lib/isAllowedHref'\n\nfunction meta(wf: WorkflowSnapshot): string {\n // Unknown carries no trustworthy run time, so say why rather than show a\n // misleading age; known states show how long ago the run last updated.\n if (wf.state === 'unknown') return wf.lastRun ? 'no status' : 'no runs'\n return wf.lastRun ? formatRelativeTime(wf.lastRun.updatedAt) : 'no runs'\n}\n\n// Memoised: on a no-change poll tick React Query preserves the workflow object\n// reference (structural sharing), so the chip skips re-rendering.\nexport const SignalChip = memo(function SignalChip({ workflow }: { workflow: WorkflowSnapshot }) {\n const url = workflow.lastRun?.htmlUrl\n const linkable = Boolean(url)\n const className = `chip chip--${workflow.state}${linkable ? '' : ' chip--static'}`\n const body = (\n <>\n <span className=\"chip__dot\" aria-hidden=\"true\" />\n <span className=\"chip__label\">{workflow.name}</span>\n {/* State in words for SR / colour-blind users — the dot is colour+shape only. */}\n <span className=\"sr-only\">{stateLabel(workflow.state)}</span>\n <span className=\"chip__meta\">{meta(workflow)}</span>\n </>\n )\n // Open the run in a new tab so the always-on board never navigates away.\n return linkable ? (\n <a className={className} href={isAllowedHref(url)} title={stateLabel(workflow.state)} target=\"_blank\" rel=\"noopener noreferrer\">{body}</a>\n ) : (\n <span className={className} title={stateLabel(workflow.state)}>{body}</span>\n )\n})\n","import type { SignalState } from '../api/types'\n\n// The spoken form of a workflow/job state. Status on the board is carried\n// visually by dot colour + dot shape; this is the text equivalent rendered\n// into each chip's accessible name (an .sr-only span) so screen-reader and\n// colour-blind users get the state in words — not a colour-only signal.\n// (WCAG 2.2 SC 1.4.1 / 1.1.1.)\nconst STATE_LABELS: Record<SignalState, string> = {\n success: 'passing',\n failure: 'failing',\n running: 'running',\n unknown: 'status unknown',\n}\n\nexport function stateLabel(state: SignalState): string {\n return STATE_LABELS[state]\n}\n","import type { RepoMetrics } from '../api/types'\nimport { formatCompactNumber } from '../lib/formatCompactNumber'\n\nexport function RepoMetricsLine({ metrics }: { metrics: RepoMetrics | null }) {\n if (!metrics || metrics.nloc === 0) return null\n return (\n <dl className=\"repo-metrics\" aria-label=\"code metrics\">\n <div title=\"non-comment lines of code\"><dt>NLOC</dt><dd>{formatCompactNumber(metrics.nloc)}</dd></div>\n <div title=\"average cyclomatic complexity (branch paths per function)\"><dt>avg CCN</dt><dd>{metrics.avgComplexity.toFixed(1)}</dd></div>\n <div title=\"number of functions\"><dt>functions</dt><dd>{formatCompactNumber(metrics.functionCount)}</dd></div>\n {metrics.highComplexityCount > 0 && (\n <div className=\"repo-metrics__complex\" title=\"functions over CCN 15 — refactor candidates\">\n <dt>complex</dt><dd>{metrics.highComplexityCount}</dd>\n </div>\n )}\n </dl>\n )\n}\n","import type { PullRequest } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { isAllowedHref } from '../lib/isAllowedHref'\n\nexport function PullRequestList({ pullRequests }: { pullRequests: PullRequest[] }) {\n if (pullRequests.length === 0) return null\n return (\n <div className=\"repo-prs\">\n <span className=\"repo-prs__count\">\n {pullRequests.length} open PR{pullRequests.length === 1 ? '' : 's'}\n </span>\n <ul>\n {pullRequests.map(pr => (\n <li key={pr.number} className={pr.isDraft ? 'repo-prs__item repo-prs__item--draft' : 'repo-prs__item'}>\n <a href={isAllowedHref(pr.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\">\n <span className=\"repo-prs__num\">#{pr.number}</span>\n <span className=\"repo-prs__title\">{pr.title}</span>\n </a>\n <span className=\"repo-prs__meta\">\n {pr.author} · {formatRelativeTime(pr.createdAt)}{pr.isDraft ? ' · draft' : ''}\n </span>\n </li>\n ))}\n </ul>\n </div>\n )\n}\n","import { memo } from 'react'\nimport type { JobSignal } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { dedupeJobLabel } from '../lib/dedupeJobLabel'\nimport { stateLabel } from '../lib/stateLabel'\nimport { isAllowedHref } from '../lib/isAllowedHref'\n// Memoised: the signals array reference is preserved across no-change poll ticks\n// (React Query structural sharing), so the lane skips re-rendering.\nexport const JobLaneRow = memo(function JobLaneRow({\n kind, glyph, label, signals,\n}: {\n kind: 'deploys' | 'packages'\n glyph: string\n label: string\n signals: JobSignal[]\n}) {\n if (signals.length === 0) return null\n return (\n <div className={`repo-joblane repo-joblane--${kind}`}>\n <span className=\"repo-joblane__label\">{glyph} {label}</span>\n <div className=\"repo-joblane__chips\">\n {signals.map((s, i) => (\n <a\n key={`${s.workflow}/${s.name}/${i}`}\n className={`chip chip--${s.state} chip--joblane`}\n href={isAllowedHref(s.htmlUrl)}\n title={`${s.workflow} · ${s.state}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className=\"chip__dot\" aria-hidden=\"true\" />\n <span className=\"chip__label\">{dedupeJobLabel(s.name)}</span>\n {/* State in words for SR / colour-blind users — the dot is colour+shape only. */}\n <span className=\"sr-only\">{stateLabel(s.state)}</span>\n <span className=\"chip__meta\">{formatRelativeTime(s.updatedAt)}</span>\n </a>\n ))}\n </div>\n </div>\n )\n})\n","// GitHub renders reusable-workflow / matrix job names as \"caller / called\", which\n// for a deploy or publish job is often the same segment twice\n// (\"Deploy (x) / Deploy (x)\"). Collapse consecutive identical segments so the chip\n// reads once; genuinely different segments are preserved.\nexport function dedupeJobLabel(name: string): string {\n const parts = name.split(' / ')\n const deduped = parts.filter((part, i) => i === 0 || part !== parts[i - 1])\n return deduped.join(' / ')\n}\n","import type { SignalState } from '../api/types'\n\nexport type Activity = 'failure' | 'running' | 'success' | 'none'\n\n// Precedence for a repo's worst signal across a set of states: a failure dominates,\n// then anything running, then a clean success; an empty/all-unknown set reads as\n// \"none\" (hollow indicator).\nexport function worstState(states: SignalState[]): Activity {\n if (states.includes('failure')) return 'failure'\n if (states.includes('running')) return 'running'\n if (states.includes('success')) return 'success'\n return 'none'\n}\n","import type { RepositorySnapshot } from '../api/types'\nimport { worstState } from '../lib/worstState'\n\nexport function RepoActivityIndicator({ repository }: { repository: RepositorySnapshot }) {\n const prCount = (repository.pullRequests ?? []).length\n const ci = worstState((repository.workflows ?? []).map(w => w.state))\n const cd = worstState([...(repository.deploys ?? []), ...(repository.packages ?? [])].map(s => s.state))\n return (\n <span className=\"repo-activity\">\n {prCount > 0 && <span className=\"repo-activity__pr\">{prCount} PR</span>}\n <span className=\"repo-activity__sig\">CI<span className=\"repo-activity__dot\" data-activity={ci} aria-label={`CI ${ci}`} /></span>\n <span className=\"repo-activity__sig\">CD<span className=\"repo-activity__dot\" data-activity={cd} aria-label={`CD ${cd}`} /></span>\n </span>\n )\n}\n","export function RepoSection({ label, count, collapsed, onToggle }: {\n label: string\n count: number\n collapsed: boolean\n onToggle: () => void\n}) {\n return (\n <button\n type=\"button\"\n className=\"repo-section\"\n aria-expanded={!collapsed}\n onClick={onToggle}\n >\n <span className=\"repo-section__chevron\" aria-hidden=\"true\">\n {collapsed ? '▸' : '▾'}\n </span>\n <span className=\"repo-section__label\">{label}</span>\n <span className=\"repo-section__count\" aria-hidden=\"true\">· {count}</span>\n </button>\n )\n}\n","// Defines the abbreviations on each repo's Lizard metrics line. Rendered once at\n// the foot of the board; the same wording backs the per-metric hover tooltips.\nconst ITEMS: ReadonlyArray<readonly [string, string]> = [\n ['NLOC', 'non-comment lines of code'],\n ['avg CCN', 'average cyclomatic complexity (branch paths per function)'],\n ['functions', 'number of functions'],\n ['complex', 'functions over CCN 15 — refactor candidates'],\n]\n\nexport function MetricsLegend() {\n return (\n <footer className=\"metrics-legend\" aria-label=\"Lizard metrics legend\">\n <span className=\"metrics-legend__title\">Lizard metrics</span>\n {ITEMS.map(([term, meaning]) => (\n <span key={term} className=\"metrics-legend__item\">\n <b>{term}</b> {meaning}\n </span>\n ))}\n </footer>\n )\n}\n","import type { SignalState } from '../api/types'\n\n// Decodes the board's primary visual language — the status dot colours and\n// shapes — for a first-time visitor. Sits beside the Lizard MetricsLegend at\n// the foot of the board. Swatches mirror the live dot treatment: colour plus a\n// non-colour shape cue (square = failing) so the key reads in grayscale too.\nconst STATUS_ITEMS: ReadonlyArray<readonly [SignalState, string]> = [\n ['success', 'passing'],\n ['failure', 'failing'],\n ['running', 'running'],\n ['unknown', 'unknown / no runs'],\n]\n\nexport function StatusLegend() {\n return (\n <footer className=\"status-legend\" aria-label=\"Status colour key\">\n <span className=\"status-legend__title\">Status</span>\n {STATUS_ITEMS.map(([state, label]) => (\n <span key={state} className=\"status-legend__item\">\n <span className=\"status-legend__dot\" data-state={state} aria-hidden=\"true\" />\n {label}\n </span>\n ))}\n <span className=\"status-legend__item\">\n <span className=\"status-legend__noci\" aria-hidden=\"true\" />\n No-CI repo\n </span>\n <span className=\"status-legend__gloss\">\n <b>CI</b> workflow runs · <b>CD</b> deploys & packages\n </span>\n </footer>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type { OpenPr } from '../lib/flattenOpenPrs'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { prAgeTone } from '../lib/prAgeTone'\nimport { isAllowedHref } from '../lib/isAllowedHref'\nexport function PullRequestStepper({ prs, onClose }: { prs: OpenPr[]; onClose: () => void }) {\n const [i, setI] = useState(0)\n const dialogRef = useRef<HTMLDialogElement>(null)\n // Background CI polling can shrink `prs` while the stepper is open (e.g. the\n // PR being viewed just merged). Clamp against the live length so `pr` never\n // reads undefined — an undefined read would return null and unmount the\n // <dialog> WITHOUT firing onClose, wedging the parent's stepperOpen at true\n // and permanently breaking the Open-PRs button for the session.\n const safeIndex = Math.min(i, Math.max(0, prs.length - 1))\n const pr = prs[safeIndex]\n\n useEffect(() => {\n const previouslyFocused = document.activeElement as HTMLElement | null\n const dlg = dialogRef.current\n dlg?.showModal()\n dlg?.focus()\n return () => {\n previouslyFocused?.focus?.()\n dlg?.close()\n }\n }, [])\n\n // If the list drains entirely, there is nothing to step through — close via\n // the callback so the parent resets stepperOpen, rather than silently\n // rendering null and leaving a dangling open flag.\n useEffect(() => {\n if (prs.length === 0) onClose()\n }, [prs.length, onClose])\n\n if (!pr) return null\n\n return (\n <dialog\n ref={dialogRef}\n className=\"pr-modal\"\n aria-label=\"Open pull requests\"\n tabIndex={-1}\n onCancel={e => { e.preventDefault(); onClose() }}\n onClick={e => { if (e.target === e.currentTarget) onClose() }}\n // Left/Right arrow paging, scoped to the dialog. Escape closes via the\n // native cancel event above.\n onKeyDown={e => {\n if (e.key === 'ArrowRight') setI(Math.min(safeIndex + 1, prs.length - 1))\n if (e.key === 'ArrowLeft') setI(Math.max(safeIndex - 1, 0))\n }}\n >\n <div className=\"pr-modal__top\">\n <span className=\"pr-modal__title\">Open pull requests</span>\n <span className=\"pr-modal__counter\">{safeIndex + 1} / {prs.length}</span>\n <button type=\"button\" className=\"pr-modal__x\" onClick={onClose} aria-label=\"Close\">✕</button>\n </div>\n <div className={`pr-card pr-card--${prAgeTone(pr.createdAt)}${pr.isDraft ? ' pr-card--draft' : ''}`}>\n <div className=\"pr-card__head\">\n <span className=\"pr-card__repo\">{pr.repo}</span>\n <span className=\"pr-card__num\">#{pr.number}</span>\n <span className=\"pr-card__meta\">{formatRelativeTime(pr.createdAt)} · {pr.isDraft ? 'draft' : 'ready'}</span>\n </div>\n <div className=\"pr-card__title\">{pr.title}</div>\n <div className=\"pr-card__foot\">\n <span className=\"pr-card__author\">@{pr.author}</span>\n <a className=\"pr-card__gh\" href={isAllowedHref(pr.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\">Open on GitHub ↗</a>\n </div>\n </div>\n {/* One PR needs no nav — the '1 / 1' counter already says so; two disabled\n buttons would just be dead chrome. */}\n {prs.length > 1 && (\n <div className=\"pr-modal__nav\">\n <button type=\"button\" onClick={() => setI(Math.max(safeIndex - 1, 0))} disabled={safeIndex === 0}>‹ Prev</button>\n <button type=\"button\" onClick={() => setI(Math.min(safeIndex + 1, prs.length - 1))} disabled={safeIndex === prs.length - 1}>Next ›</button>\n </div>\n )}\n </dialog>\n )\n}\n","// Age-based emphasis for an open PR card edge: stale PRs warm up.\nexport type AgeTone = 'red' | 'amber' | 'quiet'\n\nexport function prAgeTone(createdAtIso: string, now: number = Date.now()): AgeTone {\n const days = (now - new Date(createdAtIso).getTime()) / 86_400_000\n if (days > 14) return 'red'\n if (days > 7) return 'amber'\n return 'quiet'\n}\n","import type { PullRequest, RepositorySnapshot } from '../api/types'\n\nexport type OpenPr = PullRequest & { repo: string }\n\n// Aggregate every repo's open PRs into one list tagged with its repo, oldest-first\n// (the most stale surface first for triage).\nexport function flattenOpenPrs(repositories: RepositorySnapshot[]): OpenPr[] {\n return repositories\n .flatMap(r => (r.pullRequests ?? []).map(pr => ({ ...pr, repo: r.name })))\n .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())\n}\n","// Neutral footer shipped as the CiBoard default. No FixPortal branding or\n// personal attribution -- reusers pass their own node via CiBoard's footerSlot.\nexport function DefaultFooter() {\n return (\n <footer className=\"site-footer\">\n <div className=\"site-footer__band\" aria-hidden=\"true\">\n <span className=\"site-footer__tagline\">Continuous-integration overview</span>\n </div>\n </footer>\n )\n}\n"],"mappings":"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","names":["useState","createContext","use","useState","useEffect","useCallback","useEffect","useState","KEY","load","useEffect","useState","Fragment","jsx","jsxs","useState","useEffect","memo","Fragment","jsx","jsxs","SignalChip","jsx","jsxs","jsx","jsxs","memo","jsx","jsxs","memo","JobLaneRow","jsx","jsxs","Fragment","jsx","jsxs","memo","RepoBoard","jsx","jsxs","jsx","jsxs","jsx","jsxs","useEffect","useRef","useState","jsx","jsxs","useState","useRef","useEffect","Fragment","jsx","jsxs","useState","useEffect","jsx","Fragment","jsx","jsxs","useState"]}
|
|
1
|
+
{"version":3,"sources":["../src/CiBoard.tsx","../src/CiAdminContext.tsx","../src/CiConfigContext.tsx","../src/pages/CiBoardContent.tsx","../src/hooks/useDashboardSnapshot.ts","../src/api/getDashboardSnapshot.ts","../src/hooks/useCollapseState.ts","../src/hooks/useHideNoCi.ts","../src/lib/isNoCi.ts","../src/lib/computeSummary.ts","../src/components/SummaryStrip.tsx","../src/lib/formatCompactNumber.ts","../src/lib/relativeTime.ts","../src/components/CiWeatherBar.tsx","../src/lib/isAllowedHref.ts","../src/components/RepoBoard.tsx","../src/components/SignalChip.tsx","../src/lib/stateLabel.ts","../src/components/RepoMetricsLine.tsx","../src/components/PullRequestList.tsx","../src/components/JobLaneRow.tsx","../src/lib/dedupeJobLabel.ts","../src/lib/worstState.ts","../src/components/RepoActivityIndicator.tsx","../src/components/RepoSection.tsx","../src/components/MetricsLegend.tsx","../src/components/StatusLegend.tsx","../src/components/PullRequestStepper.tsx","../src/lib/prAgeTone.ts","../src/lib/flattenOpenPrs.ts","../src/DefaultFooter.tsx"],"sourcesContent":["import { useState, useContext } from 'react'\nimport type { ReactNode } from 'react'\nimport { QueryClient, QueryClientProvider, QueryClientContext } from '@tanstack/react-query'\nimport { CiAdminProvider } from './CiAdminContext'\nimport { CiConfigProvider, DEFAULT_CI_API_BASE } from './CiConfigContext'\nimport { CiBoardContent } from './pages/CiBoardContent'\nimport { DefaultFooter } from './DefaultFooter'\n\nexport interface CiBoardProps {\n /** Whether the viewer is an admin: sees private repos + actionable PR links. Host-computed. */\n adminSignal: boolean\n /** Origin of the CI backend snapshot API (no trailing slash). Defaults to '' (relative URLs — requires a same-origin /api/ proxy). Pass 'https://ci.fixportal.org' to reach the public FixPortal backend. */\n apiBase?: string\n /** Full URL the board fetches when the viewer is admin. The host's backend should proxy this to the CI backend's /api/dashboard/snapshot/admin endpoint, adding the X-Admin-Key header server-side so the shared secret never reaches the browser. When unset, admin viewers see the public (private-repo-stripped) snapshot. */\n adminSnapshotUrl?: string\n /** Brand mark for the header. Defaults to a plain text wordmark. */\n logo?: ReactNode\n /** Footer node. Defaults to a generic, brand-free footer. */\n footerSlot?: ReactNode\n}\n\nfunction QueryClientSafeProvider({ children }: { children: ReactNode }) {\n const existingClient = useContext(QueryClientContext)\n const [localClient] = useState(() => {\n if (!existingClient) {\n return new QueryClient({\n defaultOptions: {\n queries: {\n retry: 1,\n refetchOnWindowFocus: false,\n },\n },\n })\n }\n return null\n })\n\n if (existingClient) {\n return <>{children}</>\n }\n\n return (\n <QueryClientProvider client={localClient!}>\n {children}\n </QueryClientProvider>\n )\n}\n\n// The board is style-free at the component level: consumers import the\n// stylesheets explicitly -- `@fix-portal/ci-frontend/board.css` (always) and\n// optionally `@fix-portal/ci-frontend/tokens.css` if they have no design system\n// of their own. This keeps CSS out of the JS bundle and lets a host with its\n// own tokens (e.g. the simulator) skip the vendored set.\nexport function CiBoard({ adminSignal, apiBase = DEFAULT_CI_API_BASE, adminSnapshotUrl, logo, footerSlot }: CiBoardProps) {\n return (\n <CiConfigProvider value={{ apiBase, adminSnapshotUrl }}>\n <QueryClientSafeProvider>\n <div className=\"ci-page\">\n <div className=\"ci-embed\">\n <header className=\"ci-embed__header\">\n <span className=\"ci-embed__lockup\">\n {logo ?? <span className=\"ci-embed__wordmark-text\">CI Dashboard</span>}\n <span className=\"ci-embed__descriptor\">\n CI Dashboard {adminSignal ? '[Admin]' : '[Guest]'}\n </span>\n </span>\n </header>\n <CiAdminProvider value={adminSignal}>\n <CiBoardContent />\n </CiAdminProvider>\n </div>\n {footerSlot ?? <DefaultFooter />}\n </div>\n </QueryClientSafeProvider>\n </CiConfigProvider>\n )\n}\n","import { createContext, use } from 'react'\n\n// Whether the current viewer is the signed-in platform admin. Drives whether PR\n// rows render as actionable GitHub links or as plain, non-interactive text.\n// Defaults to false (anonymous → read-only).\nconst CiAdminContext = createContext(false)\nCiAdminContext.displayName = 'CiAdminContext'\n\nexport const CiAdminProvider = CiAdminContext.Provider\n\nexport function useCiAdmin(): boolean {\n return use(CiAdminContext)\n}\n","import { createContext, use } from 'react'\n\n// Runtime config for the CI board. apiBase is the origin of the CI backend\n// snapshot API (no trailing slash). Empty string means relative URLs — works\n// with any nginx proxy (Docker Compose or www.fixportal.org/ci).\n// adminSnapshotUrl: when set, the board fetches this URL directly (instead of\n// apiBase + /api/dashboard/snapshot) when the viewer is admin. Lets a host\n// proxy the admin endpoint server-side without exposing the shared key to the\n// browser — the host passes a same-origin relative URL here, and its backend\n// adds the X-Admin-Key before forwarding to the CI backend.\nexport interface CiConfig {\n apiBase: string\n adminSnapshotUrl?: string\n}\n\nexport const DEFAULT_CI_API_BASE = ''\n\nconst CiConfigContext = createContext<CiConfig>({ apiBase: DEFAULT_CI_API_BASE })\nCiConfigContext.displayName = 'CiConfigContext'\n\nexport const CiConfigProvider = CiConfigContext.Provider\n\nexport function useCiConfig(): CiConfig {\n return use(CiConfigContext)\n}\n","import { useState, useEffect } from 'react'\nimport { useDashboardSnapshot } from '../hooks/useDashboardSnapshot'\nimport { useCollapseState } from '../hooks/useCollapseState'\nimport { useHideNoCi } from '../hooks/useHideNoCi'\nimport { useCiAdmin } from '../CiAdminContext'\nimport { isNoCi } from '../lib/isNoCi'\nimport { computeSummary } from '../lib/computeSummary'\nimport { SummaryStrip } from '../components/SummaryStrip'\nimport { RepoBoard } from '../components/RepoBoard'\nimport { RepoSection } from '../components/RepoSection'\nimport { MetricsLegend } from '../components/MetricsLegend'\nimport { StatusLegend } from '../components/StatusLegend'\nimport { PullRequestStepper } from '../components/PullRequestStepper'\nimport { flattenOpenPrs } from '../lib/flattenOpenPrs'\nimport { formatRelativeTime } from '../lib/relativeTime'\n\nexport function CiBoardContent() {\n const snapshot = useDashboardSnapshot()\n const collapse = useCollapseState()\n const hideNoCi = useHideNoCi()\n const isAdmin = useCiAdmin()\n const [stepperOpen, setStepperOpen] = useState(false)\n\n // Compute openPrs before the useEffect so the dependency array does not\n // access a const in its temporal dead zone (TDZ). Defaults to [] when\n // snapshot.data is not yet available (before the early returns below).\n const earlyRepos = snapshot.data?.repositories ?? []\n const earlyFiltered = isAdmin ? earlyRepos : earlyRepos.filter(r => !r.private)\n const openPrs = flattenOpenPrs(hideNoCi.hidden ? earlyFiltered.filter(r => !isNoCi(r)) : earlyFiltered)\n\n useEffect(() => {\n if (openPrs.length === 0) {\n setStepperOpen(false)\n }\n }, [openPrs.length])\n\n if (snapshot.isPending) {\n return (\n <main className=\"dashboard-page\">\n <div className=\"state-msg\">Loading dashboard…</div>\n </main>\n )\n }\n\n if (snapshot.isError) {\n return (\n <main className=\"dashboard-page\">\n <div className=\"state-msg state-msg--error\">Dashboard unavailable.</div>\n </main>\n )\n }\n\n // 204 -> null: backend is up but has not produced a snapshot yet.\n if (!snapshot.data) {\n return (\n <main className=\"dashboard-page\">\n <div className=\"state-msg\">Waiting for the first refresh…</div>\n </main>\n )\n }\n\n const { refreshedAt, repositories: allRepositories, lastMergedPr: rawLastMerged } = snapshot.data\n // Non-admin viewers see only public repos; admin sees all. Filtering here drives\n // everything downstream — summary counts, stepper PRs, and next-in-queue all\n // reflect exactly the repos displayed on screen.\n const repositories = isAdmin ? allRepositories : allRepositories.filter(r => !r.private)\n // Compute the names list and the all-collapsed flag once — they were rebuilt\n // and re-traversed twice per render (the onClick and the button label).\n const noCiCount = repositories.filter(isNoCi).length\n const visibleRepos = hideNoCi.hidden ? repositories.filter(r => !isNoCi(r)) : repositories\n const summary = (isAdmin && !hideNoCi.hidden) ? snapshot.data.summary : computeSummary(visibleRepos)\n // Only surface lastMergedPr when its repo is in the visible set; a private-repo\n // merge is invisible to the public viewer and the link would 404 for them.\n const lastMergedPr = rawLastMerged && visibleRepos.some(r => r.name === rawLastMerged.repo)\n ? rawLastMerged\n : null\n const repoNames = visibleRepos.map(r => r.name)\n const hiddenCount = repositories.length - visibleRepos.length\n const publicRepos = visibleRepos.filter(r => !r.private)\n const privateRepos = visibleRepos.filter(r => r.private)\n const showGroups = publicRepos.length > 0 && privateRepos.length > 0\n const KEY_PUBLIC = 'section:public'\n const KEY_PRIVATE = 'section:private'\n const sectionKeys = showGroups ? [KEY_PUBLIC, KEY_PRIVATE] : []\n const allCollapsed = collapse.allCollapsed([...repoNames, ...sectionKeys])\n // The stepper opens at the head of this oldest-first list, so its first entry\n // is \"next in queue\" — derive it from visibleRepos (the Hide No-CI filter\n // applied) so the card and stepper never advertise a PR from a repo the board\n // is currently hiding. openPrs is computed above before the early returns.\n const nextPr = openPrs[0] ?? null\n\n let repoListContent\n if (visibleRepos.length === 0 && hideNoCi.hidden) {\n repoListContent = <div className=\"state-msg\">All repositories are No-CI — hidden.</div>\n } else if (showGroups) {\n repoListContent = (\n <>\n <RepoSection\n label=\"Public\"\n count={publicRepos.length}\n collapsed={collapse.isCollapsed(KEY_PUBLIC)}\n onToggle={() => collapse.toggle(KEY_PUBLIC)}\n />\n {!collapse.isCollapsed(KEY_PUBLIC) &&\n publicRepos.map(repository => (\n <RepoBoard\n key={repository.name}\n repository={repository}\n collapsed={collapse.isCollapsed(repository.name)}\n onToggle={collapse.toggle}\n />\n ))\n }\n <RepoSection\n label=\"Private\"\n count={privateRepos.length}\n collapsed={collapse.isCollapsed(KEY_PRIVATE)}\n onToggle={() => collapse.toggle(KEY_PRIVATE)}\n />\n {!collapse.isCollapsed(KEY_PRIVATE) &&\n privateRepos.map(repository => (\n <RepoBoard\n key={repository.name}\n repository={repository}\n collapsed={collapse.isCollapsed(repository.name)}\n onToggle={collapse.toggle}\n />\n ))\n }\n </>\n )\n } else {\n repoListContent = visibleRepos.map(repository => (\n <RepoBoard\n key={repository.name}\n repository={repository}\n collapsed={collapse.isCollapsed(repository.name)}\n onToggle={collapse.toggle}\n />\n ))\n }\n\n return (\n <main className=\"dashboard-page\">\n <div className=\"dashboard__toolbar\">\n <span className=\"dashboard__scope\">{snapshot.data.org} · {isAdmin ? 'all repositories' : 'public repositories'}</span>\n <span className=\"dashboard__toolbar-right\">\n {noCiCount > 0 && (\n <button\n type=\"button\"\n className={`dashboard__hide-noci${hideNoCi.hidden ? ' dashboard__hide-noci--on' : ''}`}\n onClick={hideNoCi.toggle}\n aria-pressed={hideNoCi.hidden}\n >\n {hideNoCi.hidden ? `Show No-CI · ${hiddenCount} hidden` : 'Hide No-CI'}\n </button>\n )}\n <button\n type=\"button\"\n className=\"dashboard__collapse-all\"\n onClick={() => (allCollapsed ? collapse.expandAll() : collapse.collapseAll([...repoNames, ...sectionKeys]))}\n >\n {allCollapsed ? '⊞ Expand all' : '⊟ Collapse all'}\n </button>\n <span className=\"dashboard__refreshed\">\n <span className=\"live-dot\" aria-hidden=\"true\" />\n updated {formatRelativeTime(refreshedAt)}\n </span>\n </span>\n </div>\n <SummaryStrip summary={summary} onOpenPrs={isAdmin ? () => setStepperOpen(true) : undefined} lastMerged={lastMergedPr} nextPr={nextPr} ciTrend={snapshot.data.ciTrend ?? []} />\n <div className=\"repo-list\">\n {repoListContent}\n </div>\n <StatusLegend />\n <MetricsLegend />\n {stepperOpen && openPrs.length > 0 && (\n <PullRequestStepper prs={openPrs} onClose={() => setStepperOpen(false)} />\n )}\n </main>\n )\n}\n","import { useQuery } from '@tanstack/react-query'\nimport { getDashboardSnapshot } from '../api/getDashboardSnapshot'\nimport { useCiConfig } from '../CiConfigContext'\nimport { useCiAdmin } from '../CiAdminContext'\n\nexport function useDashboardSnapshot() {\n const { apiBase, adminSnapshotUrl } = useCiConfig()\n const isAdmin = useCiAdmin()\n // Admin viewers use the host-proxied admin URL so the shared key stays\n // server-side. Fall back to the public endpoint for non-admin or when no\n // admin URL was wired up by the host.\n const snapshotUrl = isAdmin && adminSnapshotUrl\n ? adminSnapshotUrl\n : `${apiBase.replace(/\\/$/, '')}/api/dashboard/snapshot`\n return useQuery({\n queryKey: ['dashboard-snapshot', snapshotUrl],\n queryFn: () => getDashboardSnapshot(snapshotUrl),\n refetchInterval: 60_000,\n // The 60s poll already drives freshness; without these, an incidental tab\n // focus refetches and re-renders the whole board between ticks. Set per-query\n // (not on the shared app QueryClient) so the host app is unaffected;\n // structural sharing then lets the memoised boards skip a no-change tick.\n staleTime: 30_000,\n refetchOnWindowFocus: false,\n })\n}\n","import type { DashboardSnapshot } from './types'\n\n// 204 No Content is the documented \"no snapshot yet\" state. Return null rather\n// than calling response.json() on an empty body. snapshotUrl is the resolved\n// URL to fetch — callers compute it from apiBase + /api/dashboard/snapshot or\n// from an adminSnapshotUrl override when the viewer is an admin.\nexport async function getDashboardSnapshot(snapshotUrl: string): Promise<DashboardSnapshot | null> {\n const response = await fetch(snapshotUrl)\n if (response.status === 204) return null\n if (!response.ok) throw new Error(`Dashboard snapshot failed: ${response.status}`)\n return response.json()\n}\n","import { useCallback, useEffect, useState } from 'react'\n\nconst KEY = 'ci-dashboard:collapsed'\n\nfunction load(): Set<string> {\n try {\n const raw = localStorage.getItem(KEY)\n return new Set(raw ? (JSON.parse(raw) as string[]) : [])\n } catch {\n return new Set()\n }\n}\n\nfunction save(set: Set<string>) {\n try {\n localStorage.setItem(KEY, JSON.stringify([...set]))\n } catch {\n // ignore (private mode / quota) — collapse state is best-effort\n }\n}\n\n// A repo absent from the set is expanded, so new repos default to expanded.\nexport function useCollapseState() {\n const [collapsed, setCollapsed] = useState<Set<string>>(load)\n\n useEffect(() => {\n save(collapsed)\n }, [collapsed])\n\n const mutate = useCallback((fn: (next: Set<string>) => void) => {\n setCollapsed(prev => {\n const next = new Set(prev)\n fn(next)\n return next\n })\n }, [])\n\n return {\n isCollapsed: useCallback((name: string) => collapsed.has(name), [collapsed]),\n allCollapsed: useCallback(\n (names: string[]) => names.length > 0 && names.every(n => collapsed.has(n)),\n [collapsed],\n ),\n toggle: useCallback((name: string) => mutate(s => (s.has(name) ? s.delete(name) : s.add(name))), [mutate]),\n collapseAll: useCallback((names: string[]) => mutate(s => names.forEach(n => s.add(n))), [mutate]),\n expandAll: useCallback(() => mutate(s => s.clear()), [mutate]),\n }\n}\n","import { useCallback, useEffect, useState } from 'react'\n\nconst KEY = 'ci-dashboard:hide-no-ci'\n\nfunction load(): boolean {\n try {\n return localStorage.getItem(KEY) === 'true'\n } catch {\n return false\n }\n}\n\n// A single persisted boolean: whether No-CI repos are hidden from the board.\n// Default false (shown). Mirrors useCollapseState's best-effort persistence.\nexport function useHideNoCi() {\n const [hidden, setHidden] = useState<boolean>(load)\n\n useEffect(() => {\n try {\n localStorage.setItem(KEY, String(hidden))\n } catch {\n // ignore (private mode / quota) — hide state is best-effort\n }\n }, [hidden])\n\n const toggle = useCallback(() => {\n setHidden(prev => !prev)\n }, [])\n\n return { hidden, toggle }\n}\n","import type { RepositorySnapshot } from '../api/types'\n\n// A repository is \"No CI\" when it has no workflows at all — the same definition\n// the backend uses for the `no-ci` summary count (repos with Workflows.Count == 0).\nexport function isNoCi(repository: RepositorySnapshot): boolean {\n return (repository.workflows?.length ?? 0) === 0\n}\n","import type { RepositorySnapshot, SummaryCount } from '../api/types'\nimport { isNoCi } from './isNoCi'\n\n// Keys that always appear in the summary, even at zero, so the strip structure\n// is stable regardless of the filtered repo set. Mirrors what the server sends\n// in snapshot.data.summary so admin and guest panels look the same.\nconst ALWAYS_VISIBLE_KEYS = new Set([\n 'repos', 'workflows', 'nloc', // inventory\n 'open-prs', // Review panel (carries next-in-queue / last-merged)\n 'running', 'failing', 'no-ci', // core CI status\n 'deploys-running', 'deploys-failing', // deploy lane (zero = nothing deploying / all clean)\n 'packages-failing', // package lane\n])\n\n// Recomputes summary counts from a filtered repo list. Mirrors the server-side\n// aggregation so the strip reflects exactly the repos being displayed.\nexport function computeSummary(repos: RepositorySnapshot[]): SummaryCount[] {\n const workflows = repos.flatMap(r => r.workflows)\n const deploys = repos.flatMap(r => r.deploys ?? [])\n const packages = repos.flatMap(r => r.packages ?? [])\n const openPrs = repos.flatMap(r => r.pullRequests ?? [])\n const nloc = repos.reduce((acc, r) => acc + (r.metrics?.nloc ?? 0), 0)\n\n const all: SummaryCount[] = [\n { key: 'repos', count: repos.length },\n { key: 'workflows', count: workflows.length },\n { key: 'failing', count: workflows.filter(w => w.state === 'failure').length },\n { key: 'running', count: workflows.filter(w => w.state === 'running').length },\n { key: 'no-ci', count: repos.filter(isNoCi).length },\n { key: 'open-prs', count: openPrs.length },\n { key: 'nloc', count: nloc },\n { key: 'deploys-failing', count: deploys.filter(d => d.state === 'failure').length },\n { key: 'deploys-running', count: deploys.filter(d => d.state === 'running').length },\n { key: 'packages-failing', count: packages.filter(p => p.state === 'failure').length },\n ]\n\n return all.filter(c => ALWAYS_VISIBLE_KEYS.has(c.key) || c.count > 0)\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type { CiTrendBucket, MergedPr, SummaryCount } from '../api/types'\nimport type { OpenPr } from '../lib/flattenOpenPrs'\nimport { formatCompactNumber } from '../lib/formatCompactNumber'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { CiWeatherBar } from './CiWeatherBar'\nimport { isAllowedHref } from '../lib/isAllowedHref'\nconst SUMMARY_LABELS: Record<string, string> = {\n repos: 'Repositories',\n workflows: 'Workflows',\n failing: 'Failing',\n running: 'Running',\n 'no-ci': 'No CI',\n 'open-prs': 'Open PRs',\n nloc: 'Lines of code',\n 'deploys-failing': 'Deploys failing',\n 'deploys-running': 'Deploys running',\n 'packages-failing': 'Packages failing',\n}\n\n// Three panels group the counts by what the operator is looking for: review work,\n// pipeline health, and inventory. Keys appear in this fixed order regardless of the\n// summary array's order; a panel with no present keys is hidden.\nconst PANELS: { title: string; keys: string[] }[] = [\n { title: 'Review', keys: ['open-prs'] },\n { title: 'CI status', keys: ['running', 'failing', 'packages-failing', 'deploys-running', 'deploys-failing', 'no-ci'] },\n { title: 'Inventory', keys: ['repos', 'workflows', 'nloc'] },\n]\n\nconst NEUTRAL_KEYS = new Set(['repos', 'workflows', 'nloc'])\nconst EMPTY_TREND: CiTrendBucket[] = []\n\nfunction labelFor(key: string, count: number) {\n // 'Open PRs' is the only count-driven noun on the strip; singularise it so a\n // single PR doesn't read as '1 Open PRs'.\n if (key === 'open-prs') return count === 1 ? 'Open PR' : 'Open PRs'\n return SUMMARY_LABELS[key] ?? key.replaceAll('-', ' ')\n}\n\nfunction formatCount(key: string, count: number) {\n return key === 'nloc' ? formatCompactNumber(count) : count\n}\n\n// A non-zero count is coloured to mirror its chip: failures red, running blue,\n// no-ci indigo, the rest amber. open-prs gets its own non-alarm \"review\" tone.\n// Zero / inventory quiet.\nfunction toneFor(key: string, count: number): string {\n if (count === 0 || NEUTRAL_KEYS.has(key)) return 'ok'\n if (key === 'open-prs') return 'review'\n if (key === 'failing' || key === 'deploys-failing' || key === 'packages-failing') return 'fail'\n if (key === 'running' || key === 'deploys-running') return 'run'\n if (key === 'no-ci') return 'no-ci'\n return 'alert'\n}\n\nexport function SummaryStrip({ summary, onOpenPrs, lastMerged, nextPr = null, ciTrend = EMPTY_TREND }: { summary: SummaryCount[]; onOpenPrs?: () => void; lastMerged: MergedPr | null; nextPr?: OpenPr | null; ciTrend?: CiTrendBucket[] }) {\n const byKey = new Map(summary.map(s => [s.key, s.count]))\n\n const [trendInfoOpen, setTrendInfoOpen] = useState(false)\n const trendLabelRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n if (!trendInfoOpen) return\n function handleOutside(e: MouseEvent) {\n if (!trendLabelRef.current?.contains(e.target as Node)) {\n setTrendInfoOpen(false)\n }\n }\n function handleEsc(e: KeyboardEvent) {\n if (e.key === 'Escape') setTrendInfoOpen(false)\n }\n document.addEventListener('mousedown', handleOutside)\n document.addEventListener('keydown', handleEsc)\n return () => {\n document.removeEventListener('mousedown', handleOutside)\n document.removeEventListener('keydown', handleEsc)\n }\n }, [trendInfoOpen])\n\n return (\n <section className=\"summary-panels\">\n {PANELS.map(panel => {\n const items: { key: string; count: number }[] = []\n for (const k of panel.keys) {\n const count = byKey.get(k)\n if (count !== undefined) items.push({ key: k, count })\n }\n if (items.length === 0) return null\n const isReview = panel.title === 'Review'\n const isCiStatus = panel.title === 'CI status'\n return (\n <div key={panel.title} className={`summary-panel${isReview ? ' summary-panel--review' : ''}${isCiStatus ? ' summary-panel--ci' : ''}`}>\n <span className=\"summary-panel__title\">{panel.title}</span>\n <div className=\"summary-panel__items\">\n {items.map(item => {\n const body = (\n <>\n <span className=\"summary__count\">{formatCount(item.key, item.count)}</span>\n <span className=\"summary__label\">{labelFor(item.key, item.count)}</span>\n </>\n )\n if (item.key === 'open-prs' && onOpenPrs) {\n return (\n <button\n key={item.key}\n type=\"button\"\n className=\"summary__item summary__item--btn\"\n data-key={item.key}\n data-tone={toneFor(item.key, item.count)}\n onClick={onOpenPrs}\n disabled={item.count === 0}\n >{body}</button>\n )\n }\n return (\n <div key={item.key} className=\"summary__item\" data-key={item.key} data-tone={toneFor(item.key, item.count)}>{body}</div>\n )\n })}\n </div>\n {isReview && nextPr && (\n <div className=\"summary-panel__next\">\n <span className=\"summary-panel__q-lab\">next in queue</span>\n <span className=\"summary-panel__q-body\">{nextPr.repo} #{nextPr.number}</span>\n <span className=\"summary-panel__q-title\">{nextPr.title}</span>\n </div>\n )}\n {isReview && lastMerged && (\n <a className=\"summary-panel__merged\" href={isAllowedHref(lastMerged.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\">\n <span className=\"summary-panel__q-lab\">\n last merged <span className=\"summary-panel__q-age\">({formatRelativeTime(lastMerged.mergedAt)})</span>\n </span>\n <span className=\"summary-panel__q-body\">{lastMerged.repo} #{lastMerged.number}</span>\n <span className=\"summary-panel__q-title\">{lastMerged.title}</span>\n </a>\n )}\n {panel.title === 'CI status' && ciTrend.length > 0 && (\n <div className=\"summary-panel__trend\">\n <CiWeatherBar trend={ciTrend} />\n <div ref={trendLabelRef} className=\"summary-panel__trend-label-row\">\n <span className=\"summary-panel__trend-lab\">CI health · 24h</span>\n <button\n type=\"button\"\n className=\"ci-trend-info-btn\"\n aria-label=\"CI health information\"\n aria-expanded={trendInfoOpen}\n aria-controls={trendInfoOpen ? 'ci-trend-popover' : undefined}\n onClick={() => setTrendInfoOpen(o => !o)}\n >\n i\n </button>\n {trendInfoOpen && (\n <section\n id=\"ci-trend-popover\"\n aria-label=\"CI health explanation\"\n className=\"ci-trend-popover\"\n >\n <div className=\"ci-trend-popover__title\">CI health · 24h</div>\n <p>Each bar is a 1-hour bucket of CI activity across the whole org.</p>\n <p><span aria-hidden=\"true\" className=\"ci-trend-popover__swatch ci-trend-popover__swatch--fail\">■</span> Red — any run failed that hour (on any branch).</p>\n <p><span aria-hidden=\"true\" className=\"ci-trend-popover__swatch ci-trend-popover__swatch--pass\">■</span> Green — runs present, none failed. Quiet hours inherit the previous state.</p>\n <p>Oldest bar on the left, newest on the right.</p>\n <div className=\"ci-trend-popover__caret\" aria-hidden=\"true\" />\n </section>\n )}\n </div>\n </div>\n )}\n </div>\n )\n })}\n </section>\n )\n}\n","/** Compact integer formatting for large counts: 12345 -> \"12.3k\", 980 -> \"980\". */\nexport function formatCompactNumber(value: number): string {\n if (value < 1000) return String(value)\n if (value < 1_000_000) return `${(value / 1000).toFixed(1)}k`\n return `${(value / 1_000_000).toFixed(1)}M`\n}\n","/** Compact \"x ago\" formatting for observed-at timestamps in the status board. */\nexport function formatRelativeTime(iso: string): string {\n const then = new Date(iso).getTime()\n if (Number.isNaN(then)) return ''\n\n const minutes = Math.round((Date.now() - then) / 60_000)\n if (minutes < 1) return 'just now'\n if (minutes < 60) return `${minutes}m ago`\n\n const hours = Math.round(minutes / 60)\n if (hours < 24) return `${hours}h ago`\n\n const days = Math.round(hours / 24)\n return `${days}d ago`\n}\n","import type { CiTrendBucket, CiTrendState } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\n\n// A status-page \"weather\" strip: one block per hourly bucket, coloured by the\n// carry-forward worst-state from the backend. Pure presentational; colours come\n// from CSS via the data-state attribute.\nconst BLOCK_WORD: Record<CiTrendState, string> = {\n passing: 'healthy',\n failing: 'failing',\n noData: 'no data',\n}\n\nexport function CiWeatherBar({ trend }: { trend: CiTrendBucket[] }) {\n if (trend.length === 0) return null\n const failing = trend.filter(b => b.state === 'failing').length\n const healthy = trend.filter(b => b.state === 'passing').length\n const label = `CI health, last 24h: ${failing} failing, ${healthy} healthy`\n return (\n <>\n <div className=\"ci-weather\" role=\"img\" aria-label={label}>\n {trend.map((b, i) => (\n // Per-block hover reveals which hour a block is and its state — the\n // data was previously exposed only to screen readers via aria-label.\n <span\n key={i}\n className=\"ci-weather__block\"\n data-state={b.state}\n title={`${formatRelativeTime(b.bucketStart)} · ${BLOCK_WORD[b.state]}`}\n />\n ))}\n </div>\n {/* Visible count parity with the aria-label (which already announces it,\n so this is hidden from SR to avoid a double read). */}\n <span className=\"ci-weather__readout\" aria-hidden=\"true\">\n {failing} failing · {healthy} healthy\n </span>\n </>\n )\n}\n","export function isAllowedHref(url: string | undefined): string {\n if (!url) return '#'\n // Accept only http: and https: protocols to prevent javascript: or data: injection.\n // Using a try/catch with URL parser to be safe against malformed strings.\n try {\n const parsed = new URL(url)\n if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {\n return url\n }\n } catch {\n // If it's a relative URL or invalid, check if it starts with / (same-origin relative URL is safe)\n if (url.startsWith('/')) {\n return url\n }\n }\n return '#'\n}\n","import { memo } from 'react'\nimport type { RepositorySnapshot } from '../api/types'\nimport { isNoCi } from '../lib/isNoCi'\nimport { isAllowedHref } from '../lib/isAllowedHref'\nimport { SignalChip } from './SignalChip'\nimport { RepoMetricsLine } from './RepoMetricsLine'\nimport { PullRequestList } from './PullRequestList'\nimport { JobLaneRow } from './JobLaneRow'\nimport { RepoActivityIndicator } from './RepoActivityIndicator'\n\n// Memoised so a poll tick that returns the same data (React Query preserves the\n// repository reference via structural sharing) doesn't re-render every board.\n// onToggle takes the repo name so the parent can pass one stable callback rather\n// than a fresh per-repo closure that would defeat the memo.\nexport const RepoBoard = memo(function RepoBoard({\n repository, collapsed, onToggle,\n}: {\n repository: RepositorySnapshot\n collapsed: boolean\n onToggle: (name: string) => void\n}) {\n const pullRequests = repository.pullRequests ?? []\n const noCi = isNoCi(repository)\n return (\n <section className={`repo-board${collapsed ? ' repo-board--collapsed' : ''}${noCi ? ' repo-board--no-ci' : ''}`}>\n <header>\n <button type=\"button\" className=\"repo-board__toggle\" onClick={() => onToggle(repository.name)} aria-expanded={!collapsed}>\n <span className=\"repo-board__chev\" aria-hidden=\"true\">▸</span>\n {repository.name}\n </button>\n {noCi && <span className=\"repo-board__noci-tag\">No CI</span>}\n <RepoMetricsLine metrics={repository.metrics} />\n <RepoActivityIndicator repository={repository} />\n <a className=\"repo-board__gh-link\" href={isAllowedHref(repository.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\" aria-label={`Open ${repository.name} on GitHub`}>\n GitHub ↗\n </a>\n </header>\n {!collapsed && (\n <>\n {repository.workflows.length === 0 ? (\n <div className=\"repo-board__empty\">no workflows</div>\n ) : (\n <div className=\"repo-workflows\">\n <span className=\"repo-workflows__label\">Workflows · {repository.workflows.length}</span>\n <div className=\"repo-top-signals\">\n {repository.workflows.map(wf => (\n <SignalChip key={wf.file} workflow={wf} />\n ))}\n </div>\n </div>\n )}\n <JobLaneRow kind=\"deploys\" glyph=\"▲\" label=\"Deploys\" signals={repository.deploys ?? []} />\n <JobLaneRow kind=\"packages\" glyph=\"▣\" label=\"Packages\" signals={repository.packages ?? []} />\n <PullRequestList pullRequests={pullRequests} />\n </>\n )}\n </section>\n )\n})\n","import { memo } from 'react'\nimport type { WorkflowSnapshot } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { stateLabel } from '../lib/stateLabel'\nimport { isAllowedHref } from '../lib/isAllowedHref'\n\nfunction meta(wf: WorkflowSnapshot): string {\n // Unknown carries no trustworthy run time, so say why rather than show a\n // misleading age; known states show how long ago the run last updated.\n if (wf.state === 'unknown') return wf.lastRun ? 'no status' : 'no runs'\n return wf.lastRun ? formatRelativeTime(wf.lastRun.updatedAt) : 'no runs'\n}\n\n// Memoised: on a no-change poll tick React Query preserves the workflow object\n// reference (structural sharing), so the chip skips re-rendering.\nexport const SignalChip = memo(function SignalChip({ workflow }: { workflow: WorkflowSnapshot }) {\n const url = workflow.lastRun?.htmlUrl\n const linkable = Boolean(url)\n const className = `chip chip--${workflow.state}${linkable ? '' : ' chip--static'}`\n const body = (\n <>\n <span className=\"chip__dot\" aria-hidden=\"true\" />\n <span className=\"chip__label\">{workflow.name}</span>\n {/* State in words for SR / colour-blind users — the dot is colour+shape only. */}\n <span className=\"sr-only\">{stateLabel(workflow.state)}</span>\n <span className=\"chip__meta\">{meta(workflow)}</span>\n </>\n )\n // Open the run in a new tab so the always-on board never navigates away.\n return linkable ? (\n <a className={className} href={isAllowedHref(url)} title={stateLabel(workflow.state)} target=\"_blank\" rel=\"noopener noreferrer\">{body}</a>\n ) : (\n <span className={className} title={stateLabel(workflow.state)}>{body}</span>\n )\n})\n","import type { SignalState } from '../api/types'\n\n// The spoken form of a workflow/job state. Status on the board is carried\n// visually by dot colour + dot shape; this is the text equivalent rendered\n// into each chip's accessible name (an .sr-only span) so screen-reader and\n// colour-blind users get the state in words — not a colour-only signal.\n// (WCAG 2.2 SC 1.4.1 / 1.1.1.)\nconst STATE_LABELS: Record<SignalState, string> = {\n success: 'passing',\n failure: 'failing',\n running: 'running',\n unknown: 'status unknown',\n}\n\nexport function stateLabel(state: SignalState): string {\n return STATE_LABELS[state]\n}\n","import type { RepoMetrics } from '../api/types'\nimport { formatCompactNumber } from '../lib/formatCompactNumber'\n\nexport function RepoMetricsLine({ metrics }: { metrics: RepoMetrics | null }) {\n if (!metrics || metrics.nloc === 0) return null\n return (\n <dl className=\"repo-metrics\" aria-label=\"code metrics\">\n <div title=\"non-comment lines of code\"><dt>NLOC</dt><dd>{formatCompactNumber(metrics.nloc)}</dd></div>\n <div title=\"average cyclomatic complexity (branch paths per function)\"><dt>avg CCN</dt><dd>{metrics.avgComplexity.toFixed(1)}</dd></div>\n <div title=\"number of functions\"><dt>functions</dt><dd>{formatCompactNumber(metrics.functionCount)}</dd></div>\n {metrics.highComplexityCount > 0 && (\n <div className=\"repo-metrics__complex\" title=\"functions over CCN 15 — refactor candidates\">\n <dt>complex</dt><dd>{metrics.highComplexityCount}</dd>\n </div>\n )}\n </dl>\n )\n}\n","import type { PullRequest } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { isAllowedHref } from '../lib/isAllowedHref'\n\nexport function PullRequestList({ pullRequests }: { pullRequests: PullRequest[] }) {\n if (pullRequests.length === 0) return null\n return (\n <div className=\"repo-prs\">\n <span className=\"repo-prs__count\">\n {pullRequests.length} open PR{pullRequests.length === 1 ? '' : 's'}\n </span>\n <ul>\n {pullRequests.map(pr => (\n <li key={pr.number} className={pr.isDraft ? 'repo-prs__item repo-prs__item--draft' : 'repo-prs__item'}>\n <a href={isAllowedHref(pr.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\">\n <span className=\"repo-prs__num\">#{pr.number}</span>\n <span className=\"repo-prs__title\">{pr.title}</span>\n </a>\n <span className=\"repo-prs__meta\">\n {pr.author} · {formatRelativeTime(pr.createdAt)}{pr.isDraft ? ' · draft' : ''}\n </span>\n </li>\n ))}\n </ul>\n </div>\n )\n}\n","import { memo } from 'react'\nimport type { JobSignal } from '../api/types'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { dedupeJobLabel } from '../lib/dedupeJobLabel'\nimport { stateLabel } from '../lib/stateLabel'\nimport { isAllowedHref } from '../lib/isAllowedHref'\n// Memoised: the signals array reference is preserved across no-change poll ticks\n// (React Query structural sharing), so the lane skips re-rendering.\nexport const JobLaneRow = memo(function JobLaneRow({\n kind, glyph, label, signals,\n}: {\n kind: 'deploys' | 'packages'\n glyph: string\n label: string\n signals: JobSignal[]\n}) {\n if (signals.length === 0) return null\n return (\n <div className={`repo-joblane repo-joblane--${kind}`}>\n <span className=\"repo-joblane__label\">{glyph} {label}</span>\n <div className=\"repo-joblane__chips\">\n {signals.map((s, i) => (\n <a\n key={`${s.workflow}/${s.name}/${i}`}\n className={`chip chip--${s.state} chip--joblane`}\n href={isAllowedHref(s.htmlUrl)}\n title={`${s.workflow} · ${s.state}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n <span className=\"chip__dot\" aria-hidden=\"true\" />\n <span className=\"chip__label\">{dedupeJobLabel(s.name)}</span>\n {/* State in words for SR / colour-blind users — the dot is colour+shape only. */}\n <span className=\"sr-only\">{stateLabel(s.state)}</span>\n <span className=\"chip__meta\">{formatRelativeTime(s.updatedAt)}</span>\n </a>\n ))}\n </div>\n </div>\n )\n})\n","// GitHub renders reusable-workflow / matrix job names as \"caller / called\", which\n// for a deploy or publish job is often the same segment twice\n// (\"Deploy (x) / Deploy (x)\"). Collapse consecutive identical segments so the chip\n// reads once; genuinely different segments are preserved.\nexport function dedupeJobLabel(name: string): string {\n const parts = name.split(' / ')\n const deduped = parts.filter((part, i) => i === 0 || part !== parts[i - 1])\n return deduped.join(' / ')\n}\n","import type { SignalState } from '../api/types'\n\nexport type Activity = 'failure' | 'running' | 'success' | 'none'\n\n// Precedence for a repo's worst signal across a set of states: a failure dominates,\n// then anything running, then a clean success; an empty/all-unknown set reads as\n// \"none\" (hollow indicator).\nexport function worstState(states: SignalState[]): Activity {\n if (states.includes('failure')) return 'failure'\n if (states.includes('running')) return 'running'\n if (states.includes('success')) return 'success'\n return 'none'\n}\n","import type { RepositorySnapshot } from '../api/types'\nimport { worstState } from '../lib/worstState'\n\nexport function RepoActivityIndicator({ repository }: { repository: RepositorySnapshot }) {\n const prCount = (repository.pullRequests ?? []).length\n const ci = worstState((repository.workflows ?? []).map(w => w.state))\n const cd = worstState([...(repository.deploys ?? []), ...(repository.packages ?? [])].map(s => s.state))\n return (\n <span className=\"repo-activity\">\n {prCount > 0 && <span className=\"repo-activity__pr\">{prCount} PR</span>}\n <span className=\"repo-activity__sig\">CI<span className=\"repo-activity__dot\" data-activity={ci} aria-label={`CI ${ci}`} /></span>\n <span className=\"repo-activity__sig\">CD<span className=\"repo-activity__dot\" data-activity={cd} aria-label={`CD ${cd}`} /></span>\n </span>\n )\n}\n","export function RepoSection({ label, count, collapsed, onToggle }: {\n label: string\n count: number\n collapsed: boolean\n onToggle: () => void\n}) {\n return (\n <button\n type=\"button\"\n className=\"repo-section\"\n aria-expanded={!collapsed}\n onClick={onToggle}\n >\n <span className=\"repo-section__chevron\" aria-hidden=\"true\">\n {collapsed ? '▸' : '▾'}\n </span>\n <span className=\"repo-section__label\">{label}</span>\n <span className=\"repo-section__count\" aria-hidden=\"true\">· {count}</span>\n </button>\n )\n}\n","// Defines the abbreviations on each repo's Lizard metrics line. Rendered once at\n// the foot of the board; the same wording backs the per-metric hover tooltips.\nconst ITEMS: ReadonlyArray<readonly [string, string]> = [\n ['NLOC', 'non-comment lines of code'],\n ['avg CCN', 'average cyclomatic complexity (branch paths per function)'],\n ['functions', 'number of functions'],\n ['complex', 'functions over CCN 15 — refactor candidates'],\n]\n\nexport function MetricsLegend() {\n return (\n <footer className=\"metrics-legend\" aria-label=\"Lizard metrics legend\">\n <span className=\"metrics-legend__title\">Lizard metrics</span>\n {ITEMS.map(([term, meaning]) => (\n <span key={term} className=\"metrics-legend__item\">\n <b>{term}</b> {meaning}\n </span>\n ))}\n </footer>\n )\n}\n","import type { SignalState } from '../api/types'\n\n// Decodes the board's primary visual language — the status dot colours and\n// shapes — for a first-time visitor. Sits beside the Lizard MetricsLegend at\n// the foot of the board. Swatches mirror the live dot treatment: colour plus a\n// non-colour shape cue (square = failing) so the key reads in grayscale too.\nconst STATUS_ITEMS: ReadonlyArray<readonly [SignalState, string]> = [\n ['success', 'passing'],\n ['failure', 'failing'],\n ['running', 'running'],\n ['unknown', 'unknown / no runs'],\n]\n\nexport function StatusLegend() {\n return (\n <footer className=\"status-legend\" aria-label=\"Status colour key\">\n <span className=\"status-legend__title\">Status</span>\n {STATUS_ITEMS.map(([state, label]) => (\n <span key={state} className=\"status-legend__item\">\n <span className=\"status-legend__dot\" data-state={state} aria-hidden=\"true\" />\n {label}\n </span>\n ))}\n <span className=\"status-legend__item\">\n <span className=\"status-legend__noci\" aria-hidden=\"true\" />\n No-CI repo\n </span>\n <span className=\"status-legend__gloss\">\n <b>CI</b> workflow runs · <b>CD</b> deploys & packages\n </span>\n </footer>\n )\n}\n","import { useEffect, useRef, useState } from 'react'\nimport type { OpenPr } from '../lib/flattenOpenPrs'\nimport { formatRelativeTime } from '../lib/relativeTime'\nimport { prAgeTone } from '../lib/prAgeTone'\nimport { isAllowedHref } from '../lib/isAllowedHref'\nexport function PullRequestStepper({ prs, onClose }: { prs: OpenPr[]; onClose: () => void }) {\n const [i, setI] = useState(0)\n const dialogRef = useRef<HTMLDialogElement>(null)\n // Background CI polling can shrink `prs` while the stepper is open (e.g. the\n // PR being viewed just merged). Clamp against the live length so `pr` never\n // reads undefined — an undefined read would return null and unmount the\n // <dialog> WITHOUT firing onClose, wedging the parent's stepperOpen at true\n // and permanently breaking the Open-PRs button for the session.\n const safeIndex = Math.min(i, Math.max(0, prs.length - 1))\n const pr = prs[safeIndex]\n\n useEffect(() => {\n const previouslyFocused = document.activeElement as HTMLElement | null\n const dlg = dialogRef.current\n dlg?.showModal()\n dlg?.focus()\n return () => {\n previouslyFocused?.focus?.()\n dlg?.close()\n }\n }, [])\n\n // If the list drains entirely, there is nothing to step through — close via\n // the callback so the parent resets stepperOpen, rather than silently\n // rendering null and leaving a dangling open flag.\n useEffect(() => {\n if (prs.length === 0) onClose()\n }, [prs.length, onClose])\n\n if (!pr) return null\n\n return (\n <dialog\n ref={dialogRef}\n className=\"pr-modal\"\n aria-label=\"Open pull requests\"\n tabIndex={-1}\n onCancel={e => { e.preventDefault(); onClose() }}\n onClick={e => { if (e.target === e.currentTarget) onClose() }}\n // Left/Right arrow paging, scoped to the dialog. Escape closes via the\n // native cancel event above.\n onKeyDown={e => {\n if (e.key === 'ArrowRight') setI(Math.min(safeIndex + 1, prs.length - 1))\n if (e.key === 'ArrowLeft') setI(Math.max(safeIndex - 1, 0))\n }}\n >\n <div className=\"pr-modal__top\">\n <span className=\"pr-modal__title\">Open pull requests</span>\n <span className=\"pr-modal__counter\">{safeIndex + 1} / {prs.length}</span>\n <button type=\"button\" className=\"pr-modal__x\" onClick={onClose} aria-label=\"Close\">✕</button>\n </div>\n <div className={`pr-card pr-card--${prAgeTone(pr.createdAt)}${pr.isDraft ? ' pr-card--draft' : ''}`}>\n <div className=\"pr-card__head\">\n <span className=\"pr-card__repo\">{pr.repo}</span>\n <span className=\"pr-card__num\">#{pr.number}</span>\n <span className=\"pr-card__meta\">{formatRelativeTime(pr.createdAt)} · {pr.isDraft ? 'draft' : 'ready'}</span>\n </div>\n <div className=\"pr-card__title\">{pr.title}</div>\n <div className=\"pr-card__foot\">\n <span className=\"pr-card__author\">@{pr.author}</span>\n <a className=\"pr-card__gh\" href={isAllowedHref(pr.htmlUrl)} target=\"_blank\" rel=\"noopener noreferrer\">Open on GitHub ↗</a>\n </div>\n </div>\n {/* One PR needs no nav — the '1 / 1' counter already says so; two disabled\n buttons would just be dead chrome. */}\n {prs.length > 1 && (\n <div className=\"pr-modal__nav\">\n <button type=\"button\" onClick={() => setI(Math.max(safeIndex - 1, 0))} disabled={safeIndex === 0}>‹ Prev</button>\n <button type=\"button\" onClick={() => setI(Math.min(safeIndex + 1, prs.length - 1))} disabled={safeIndex === prs.length - 1}>Next ›</button>\n </div>\n )}\n </dialog>\n )\n}\n","// Age-based emphasis for an open PR card edge: stale PRs warm up.\nexport type AgeTone = 'red' | 'amber' | 'quiet'\n\nexport function prAgeTone(createdAtIso: string, now: number = Date.now()): AgeTone {\n const days = (now - new Date(createdAtIso).getTime()) / 86_400_000\n if (days > 14) return 'red'\n if (days > 7) return 'amber'\n return 'quiet'\n}\n","import type { PullRequest, RepositorySnapshot } from '../api/types'\n\nexport type OpenPr = PullRequest & { repo: string }\n\n// Aggregate every repo's open PRs into one list tagged with its repo, oldest-first\n// (the most stale surface first for triage).\nexport function flattenOpenPrs(repositories: RepositorySnapshot[]): OpenPr[] {\n return repositories\n .flatMap(r => (r.pullRequests ?? []).map(pr => ({ ...pr, repo: r.name })))\n .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())\n}\n","// Neutral footer shipped as the CiBoard default. No FixPortal branding or\n// personal attribution -- reusers pass their own node via CiBoard's footerSlot.\nexport function DefaultFooter() {\n return (\n <footer className=\"site-footer\">\n <div className=\"site-footer__band\" aria-hidden=\"true\">\n <span className=\"site-footer__tagline\">Continuous-integration overview</span>\n </div>\n </footer>\n )\n}\n"],"mappings":";AAAA,SAAS,YAAAA,WAAU,kBAAkB;AAErC,SAAS,aAAa,qBAAqB,0BAA0B;;;ACFrE,SAAS,eAAe,WAAW;AAKnC,IAAM,iBAAiB,cAAc,KAAK;AAC1C,eAAe,cAAc;AAEtB,IAAM,kBAAkB,eAAe;AAEvC,SAAS,aAAsB;AACpC,SAAO,IAAI,cAAc;AAC3B;;;ACZA,SAAS,iBAAAC,gBAAe,OAAAC,YAAW;AAe5B,IAAM,sBAAsB;AAEnC,IAAM,kBAAkBD,eAAwB,EAAE,SAAS,oBAAoB,CAAC;AAChF,gBAAgB,cAAc;AAEvB,IAAM,mBAAmB,gBAAgB;AAEzC,SAAS,cAAwB;AACtC,SAAOC,KAAI,eAAe;AAC5B;;;ACxBA,SAAS,YAAAC,WAAU,aAAAC,kBAAiB;;;ACApC,SAAS,gBAAgB;;;ACMzB,eAAsB,qBAAqB,aAAwD;AACjG,QAAM,WAAW,MAAM,MAAM,WAAW;AACxC,MAAI,SAAS,WAAW,IAAK,QAAO;AACpC,MAAI,CAAC,SAAS,GAAI,OAAM,IAAI,MAAM,8BAA8B,SAAS,MAAM,EAAE;AACjF,SAAO,SAAS,KAAK;AACvB;;;ADNO,SAAS,uBAAuB;AACrC,QAAM,EAAE,SAAS,iBAAiB,IAAI,YAAY;AAClD,QAAM,UAAU,WAAW;AAI3B,QAAM,cAAc,WAAW,mBAC3B,mBACA,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC;AACjC,SAAO,SAAS;AAAA,IACd,UAAU,CAAC,sBAAsB,WAAW;AAAA,IAC5C,SAAS,MAAM,qBAAqB,WAAW;AAAA,IAC/C,iBAAiB;AAAA;AAAA;AAAA;AAAA;AAAA,IAKjB,WAAW;AAAA,IACX,sBAAsB;AAAA,EACxB,CAAC;AACH;;;AEzBA,SAAS,aAAa,WAAW,gBAAgB;AAEjD,IAAM,MAAM;AAEZ,SAAS,OAAoB;AAC3B,MAAI;AACF,UAAM,MAAM,aAAa,QAAQ,GAAG;AACpC,WAAO,IAAI,IAAI,MAAO,KAAK,MAAM,GAAG,IAAiB,CAAC,CAAC;AAAA,EACzD,QAAQ;AACN,WAAO,oBAAI,IAAI;AAAA,EACjB;AACF;AAEA,SAAS,KAAK,KAAkB;AAC9B,MAAI;AACF,iBAAa,QAAQ,KAAK,KAAK,UAAU,CAAC,GAAG,GAAG,CAAC,CAAC;AAAA,EACpD,QAAQ;AAAA,EAER;AACF;AAGO,SAAS,mBAAmB;AACjC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAsB,IAAI;AAE5D,YAAU,MAAM;AACd,SAAK,SAAS;AAAA,EAChB,GAAG,CAAC,SAAS,CAAC;AAEd,QAAM,SAAS,YAAY,CAAC,OAAoC;AAC9D,iBAAa,UAAQ;AACnB,YAAM,OAAO,IAAI,IAAI,IAAI;AACzB,SAAG,IAAI;AACP,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL,aAAa,YAAY,CAAC,SAAiB,UAAU,IAAI,IAAI,GAAG,CAAC,SAAS,CAAC;AAAA,IAC3E,cAAc;AAAA,MACZ,CAAC,UAAoB,MAAM,SAAS,KAAK,MAAM,MAAM,OAAK,UAAU,IAAI,CAAC,CAAC;AAAA,MAC1E,CAAC,SAAS;AAAA,IACZ;AAAA,IACA,QAAQ,YAAY,CAAC,SAAiB,OAAO,OAAM,EAAE,IAAI,IAAI,IAAI,EAAE,OAAO,IAAI,IAAI,EAAE,IAAI,IAAI,CAAE,GAAG,CAAC,MAAM,CAAC;AAAA,IACzG,aAAa,YAAY,CAAC,UAAoB,OAAO,OAAK,MAAM,QAAQ,OAAK,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;AAAA,IACjG,WAAW,YAAY,MAAM,OAAO,OAAK,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;AAAA,EAC/D;AACF;;;AC/CA,SAAS,eAAAC,cAAa,aAAAC,YAAW,YAAAC,iBAAgB;AAEjD,IAAMC,OAAM;AAEZ,SAASC,QAAgB;AACvB,MAAI;AACF,WAAO,aAAa,QAAQD,IAAG,MAAM;AAAA,EACvC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIO,SAAS,cAAc;AAC5B,QAAM,CAAC,QAAQ,SAAS,IAAID,UAAkBE,KAAI;AAElD,EAAAH,WAAU,MAAM;AACd,QAAI;AACF,mBAAa,QAAQE,MAAK,OAAO,MAAM,CAAC;AAAA,IAC1C,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,SAASH,aAAY,MAAM;AAC/B,cAAU,UAAQ,CAAC,IAAI;AAAA,EACzB,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,QAAQ,OAAO;AAC1B;;;AC1BO,SAAS,OAAO,YAAyC;AAC9D,UAAQ,WAAW,WAAW,UAAU,OAAO;AACjD;;;ACAA,IAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EAAS;AAAA,EAAa;AAAA;AAAA,EACtB;AAAA;AAAA,EACA;AAAA,EAAW;AAAA,EAAW;AAAA;AAAA,EACtB;AAAA,EAAmB;AAAA;AAAA,EACnB;AAAA;AACF,CAAC;AAIM,SAAS,eAAe,OAA6C;AAC1E,QAAM,YAAY,MAAM,QAAQ,OAAK,EAAE,SAAS;AAChD,QAAM,UAAU,MAAM,QAAQ,OAAK,EAAE,WAAW,CAAC,CAAC;AAClD,QAAM,WAAW,MAAM,QAAQ,OAAK,EAAE,YAAY,CAAC,CAAC;AACpD,QAAM,UAAU,MAAM,QAAQ,OAAK,EAAE,gBAAgB,CAAC,CAAC;AACvD,QAAM,OAAO,MAAM,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,SAAS,QAAQ,IAAI,CAAC;AAErE,QAAM,MAAsB;AAAA,IAC1B,EAAE,KAAK,SAAS,OAAO,MAAM,OAAO;AAAA,IACpC,EAAE,KAAK,aAAa,OAAO,UAAU,OAAO;AAAA,IAC5C,EAAE,KAAK,WAAW,OAAO,UAAU,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE,OAAO;AAAA,IAC7E,EAAE,KAAK,WAAW,OAAO,UAAU,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE,OAAO;AAAA,IAC7E,EAAE,KAAK,SAAS,OAAO,MAAM,OAAO,MAAM,EAAE,OAAO;AAAA,IACnD,EAAE,KAAK,YAAY,OAAO,QAAQ,OAAO;AAAA,IACzC,EAAE,KAAK,QAAQ,OAAO,KAAK;AAAA,IAC3B,EAAE,KAAK,mBAAmB,OAAO,QAAQ,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE,OAAO;AAAA,IACnF,EAAE,KAAK,mBAAmB,OAAO,QAAQ,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE,OAAO;AAAA,IACnF,EAAE,KAAK,oBAAoB,OAAO,SAAS,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE,OAAO;AAAA,EACvF;AAEA,SAAO,IAAI,OAAO,OAAK,oBAAoB,IAAI,EAAE,GAAG,KAAK,EAAE,QAAQ,CAAC;AACtE;;;ACrCA,SAAS,aAAAK,YAAW,QAAQ,YAAAC,iBAAgB;;;ACCrC,SAAS,oBAAoB,OAAuB;AACzD,MAAI,QAAQ,IAAM,QAAO,OAAO,KAAK;AACrC,MAAI,QAAQ,IAAW,QAAO,IAAI,QAAQ,KAAM,QAAQ,CAAC,CAAC;AAC1D,SAAO,IAAI,QAAQ,KAAW,QAAQ,CAAC,CAAC;AAC1C;;;ACJO,SAAS,mBAAmB,KAAqB;AACtD,QAAM,OAAO,IAAI,KAAK,GAAG,EAAE,QAAQ;AACnC,MAAI,OAAO,MAAM,IAAI,EAAG,QAAO;AAE/B,QAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,QAAQ,GAAM;AACvD,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AAEnC,QAAM,QAAQ,KAAK,MAAM,UAAU,EAAE;AACrC,MAAI,QAAQ,GAAI,QAAO,GAAG,KAAK;AAE/B,QAAM,OAAO,KAAK,MAAM,QAAQ,EAAE;AAClC,SAAO,GAAG,IAAI;AAChB;;;ACII,mBAKM,KAUJ,YAfF;AAZJ,IAAM,aAA2C;AAAA,EAC/C,SAAS;AAAA,EACT,SAAS;AAAA,EACT,QAAQ;AACV;AAEO,SAAS,aAAa,EAAE,MAAM,GAA+B;AAClE,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,UAAU,MAAM,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE;AACzD,QAAM,UAAU,MAAM,OAAO,OAAK,EAAE,UAAU,SAAS,EAAE;AACzD,QAAM,QAAQ,wBAAwB,OAAO,aAAa,OAAO;AACjE,SACE,iCACE;AAAA,wBAAC,SAAI,WAAU,cAAa,MAAK,OAAM,cAAY,OAChD,gBAAM,IAAI,CAAC,GAAG;AAAA;AAAA;AAAA,MAGb;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,cAAY,EAAE;AAAA,UACd,OAAO,GAAG,mBAAmB,EAAE,WAAW,CAAC,SAAM,WAAW,EAAE,KAAK,CAAC;AAAA;AAAA,QAH/D;AAAA,MAIP;AAAA,KACD,GACH;AAAA,IAGA,qBAAC,UAAK,WAAU,uBAAsB,eAAY,QAC/C;AAAA;AAAA,MAAQ;AAAA,MAAY;AAAA,MAAQ;AAAA,OAC/B;AAAA,KACF;AAEJ;;;ACtCO,SAAS,cAAc,KAAiC;AAC7D,MAAI,CAAC,IAAK,QAAO;AAGjB,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,QAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAEN,QAAI,IAAI,WAAW,GAAG,GAAG;AACvB,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;;;AJ4EY,SAIM,YAAAC,WAJN,OAAAC,MAIM,QAAAC,aAJN;AArFZ,IAAM,iBAAyC;AAAA,EAC7C,OAAO;AAAA,EACP,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,MAAM;AAAA,EACN,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,oBAAoB;AACtB;AAKA,IAAM,SAA8C;AAAA,EAClD,EAAE,OAAO,UAAU,MAAM,CAAC,UAAU,EAAE;AAAA,EACtC,EAAE,OAAO,aAAa,MAAM,CAAC,WAAW,WAAW,oBAAoB,mBAAmB,mBAAmB,OAAO,EAAE;AAAA,EACtH,EAAE,OAAO,aAAa,MAAM,CAAC,SAAS,aAAa,MAAM,EAAE;AAC7D;AAEA,IAAM,eAAe,oBAAI,IAAI,CAAC,SAAS,aAAa,MAAM,CAAC;AAC3D,IAAM,cAA+B,CAAC;AAEtC,SAAS,SAAS,KAAa,OAAe;AAG5C,MAAI,QAAQ,WAAY,QAAO,UAAU,IAAI,YAAY;AACzD,SAAO,eAAe,GAAG,KAAK,IAAI,WAAW,KAAK,GAAG;AACvD;AAEA,SAAS,YAAY,KAAa,OAAe;AAC/C,SAAO,QAAQ,SAAS,oBAAoB,KAAK,IAAI;AACvD;AAKA,SAAS,QAAQ,KAAa,OAAuB;AACnD,MAAI,UAAU,KAAK,aAAa,IAAI,GAAG,EAAG,QAAO;AACjD,MAAI,QAAQ,WAAY,QAAO;AAC/B,MAAI,QAAQ,aAAa,QAAQ,qBAAqB,QAAQ,mBAAoB,QAAO;AACzF,MAAI,QAAQ,aAAa,QAAQ,kBAAmB,QAAO;AAC3D,MAAI,QAAQ,QAAS,QAAO;AAC5B,SAAO;AACT;AAEO,SAAS,aAAa,EAAE,SAAS,WAAW,YAAY,SAAS,MAAM,UAAU,YAAY,GAAwI;AAC1O,QAAM,QAAQ,IAAI,IAAI,QAAQ,IAAI,OAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AAExD,QAAM,CAAC,eAAe,gBAAgB,IAAIC,UAAS,KAAK;AACxD,QAAM,gBAAgB,OAAuB,IAAI;AAEjD,EAAAC,WAAU,MAAM;AACd,QAAI,CAAC,cAAe;AACpB,aAAS,cAAc,GAAe;AACpC,UAAI,CAAC,cAAc,SAAS,SAAS,EAAE,MAAc,GAAG;AACtD,yBAAiB,KAAK;AAAA,MACxB;AAAA,IACF;AACA,aAAS,UAAU,GAAkB;AACnC,UAAI,EAAE,QAAQ,SAAU,kBAAiB,KAAK;AAAA,IAChD;AACA,aAAS,iBAAiB,aAAa,aAAa;AACpD,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM;AACX,eAAS,oBAAoB,aAAa,aAAa;AACvD,eAAS,oBAAoB,WAAW,SAAS;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,aAAa,CAAC;AAElB,SACE,gBAAAH,KAAC,aAAQ,WAAU,kBAChB,iBAAO,IAAI,WAAS;AACnB,UAAM,QAA0C,CAAC;AACjD,eAAW,KAAK,MAAM,MAAM;AAC1B,YAAM,QAAQ,MAAM,IAAI,CAAC;AACzB,UAAI,UAAU,OAAW,OAAM,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC;AAAA,IACvD;AACA,QAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,UAAM,WAAW,MAAM,UAAU;AACjC,UAAM,aAAa,MAAM,UAAU;AACnC,WACE,gBAAAC,MAAC,SAAsB,WAAW,gBAAgB,WAAW,2BAA2B,EAAE,GAAG,aAAa,uBAAuB,EAAE,IACjI;AAAA,sBAAAD,KAAC,UAAK,WAAU,wBAAwB,gBAAM,OAAM;AAAA,MACpD,gBAAAA,KAAC,SAAI,WAAU,wBACZ,gBAAM,IAAI,UAAQ;AACjB,cAAM,OACJ,gBAAAC,MAAAF,WAAA,EACE;AAAA,0BAAAC,KAAC,UAAK,WAAU,kBAAkB,sBAAY,KAAK,KAAK,KAAK,KAAK,GAAE;AAAA,UACpE,gBAAAA,KAAC,UAAK,WAAU,kBAAkB,mBAAS,KAAK,KAAK,KAAK,KAAK,GAAE;AAAA,WACnE;AAEF,YAAI,KAAK,QAAQ,cAAc,WAAW;AACxC,iBACE,gBAAAA;AAAA,YAAC;AAAA;AAAA,cAEC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,YAAU,KAAK;AAAA,cACf,aAAW,QAAQ,KAAK,KAAK,KAAK,KAAK;AAAA,cACvC,SAAS;AAAA,cACT,UAAU,KAAK,UAAU;AAAA,cACzB;AAAA;AAAA,YAPK,KAAK;AAAA,UAOL;AAAA,QAEX;AACA,eACE,gBAAAA,KAAC,SAAmB,WAAU,iBAAgB,YAAU,KAAK,KAAK,aAAW,QAAQ,KAAK,KAAK,KAAK,KAAK,GAAI,kBAAnG,KAAK,GAAmG;AAAA,MAEtH,CAAC,GACH;AAAA,MACC,YAAY,UACX,gBAAAC,MAAC,SAAI,WAAU,uBACb;AAAA,wBAAAD,KAAC,UAAK,WAAU,wBAAuB,2BAAa;AAAA,QACpD,gBAAAC,MAAC,UAAK,WAAU,yBAAyB;AAAA,iBAAO;AAAA,UAAK;AAAA,UAAG,OAAO;AAAA,WAAO;AAAA,QACtE,gBAAAD,KAAC,UAAK,WAAU,0BAA0B,iBAAO,OAAM;AAAA,SACzD;AAAA,MAED,YAAY,cACX,gBAAAC,MAAC,OAAE,WAAU,yBAAwB,MAAM,cAAc,WAAW,OAAO,GAAG,QAAO,UAAS,KAAI,uBAChG;AAAA,wBAAAA,MAAC,UAAK,WAAU,wBAAuB;AAAA;AAAA,UACzB,gBAAAA,MAAC,UAAK,WAAU,wBAAuB;AAAA;AAAA,YAAE,mBAAmB,WAAW,QAAQ;AAAA,YAAE;AAAA,aAAC;AAAA,WAChG;AAAA,QACA,gBAAAA,MAAC,UAAK,WAAU,yBAAyB;AAAA,qBAAW;AAAA,UAAK;AAAA,UAAG,WAAW;AAAA,WAAO;AAAA,QAC9E,gBAAAD,KAAC,UAAK,WAAU,0BAA0B,qBAAW,OAAM;AAAA,SAC7D;AAAA,MAED,MAAM,UAAU,eAAe,QAAQ,SAAS,KAC/C,gBAAAC,MAAC,SAAI,WAAU,wBACb;AAAA,wBAAAD,KAAC,gBAAa,OAAO,SAAS;AAAA,QAC9B,gBAAAC,MAAC,SAAI,KAAK,eAAe,WAAU,kCACjC;AAAA,0BAAAD,KAAC,UAAK,WAAU,4BAA2B,gCAAe;AAAA,UAC1D,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,WAAU;AAAA,cACV,cAAW;AAAA,cACX,iBAAe;AAAA,cACf,iBAAe,gBAAgB,qBAAqB;AAAA,cACpD,SAAS,MAAM,iBAAiB,OAAK,CAAC,CAAC;AAAA,cACxC;AAAA;AAAA,UAED;AAAA,UACC,iBACC,gBAAAC;AAAA,YAAC;AAAA;AAAA,cACC,IAAG;AAAA,cACH,cAAW;AAAA,cACX,WAAU;AAAA,cAEV;AAAA,gCAAAD,KAAC,SAAI,WAAU,2BAA0B,gCAAe;AAAA,gBACxD,gBAAAA,KAAC,OAAE,8EAAgE;AAAA,gBACnE,gBAAAC,MAAC,OAAE;AAAA,kCAAAD,KAAC,UAAK,eAAY,QAAO,WAAU,2DAA0D,oBAAC;AAAA,kBAAO;AAAA,mBAAgD;AAAA,gBACxJ,gBAAAC,MAAC,OAAE;AAAA,kCAAAD,KAAC,UAAK,eAAY,QAAO,WAAU,2DAA0D,oBAAC;AAAA,kBAAO;AAAA,mBAA2E;AAAA,gBACnL,gBAAAA,KAAC,OAAE,0DAA4C;AAAA,gBAC/C,gBAAAA,KAAC,SAAI,WAAU,2BAA0B,eAAY,QAAO;AAAA;AAAA;AAAA,UAC9D;AAAA,WAEJ;AAAA,SACF;AAAA,SA1EM,MAAM,KA4EhB;AAAA,EAEJ,CAAC,GACH;AAEJ;;;AK5KA,SAAS,QAAAI,aAAY;;;ACArB,SAAS,YAAY;;;ACOrB,IAAM,eAA4C;AAAA,EAChD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AAAA,EACT,SAAS;AACX;AAEO,SAAS,WAAW,OAA4B;AACrD,SAAO,aAAa,KAAK;AAC3B;;;ADII,qBAAAC,WACE,OAAAC,MADF,QAAAC,aAAA;AAdJ,SAAS,KAAK,IAA8B;AAG1C,MAAI,GAAG,UAAU,UAAW,QAAO,GAAG,UAAU,cAAc;AAC9D,SAAO,GAAG,UAAU,mBAAmB,GAAG,QAAQ,SAAS,IAAI;AACjE;AAIO,IAAM,aAAa,KAAK,SAASC,YAAW,EAAE,SAAS,GAAmC;AAC/F,QAAM,MAAM,SAAS,SAAS;AAC9B,QAAM,WAAW,QAAQ,GAAG;AAC5B,QAAM,YAAY,cAAc,SAAS,KAAK,GAAG,WAAW,KAAK,eAAe;AAChF,QAAM,OACJ,gBAAAD,MAAAF,WAAA,EACE;AAAA,oBAAAC,KAAC,UAAK,WAAU,aAAY,eAAY,QAAO;AAAA,IAC/C,gBAAAA,KAAC,UAAK,WAAU,eAAe,mBAAS,MAAK;AAAA,IAE7C,gBAAAA,KAAC,UAAK,WAAU,WAAW,qBAAW,SAAS,KAAK,GAAE;AAAA,IACtD,gBAAAA,KAAC,UAAK,WAAU,cAAc,eAAK,QAAQ,GAAE;AAAA,KAC/C;AAGF,SAAO,WACL,gBAAAA,KAAC,OAAE,WAAsB,MAAM,cAAc,GAAG,GAAG,OAAO,WAAW,SAAS,KAAK,GAAG,QAAO,UAAS,KAAI,uBAAuB,gBAAK,IAEtI,gBAAAA,KAAC,UAAK,WAAsB,OAAO,WAAW,SAAS,KAAK,GAAI,gBAAK;AAEzE,CAAC;;;AE3BK,SAAuC,OAAAG,MAAvC,QAAAC,aAAA;AAJC,SAAS,gBAAgB,EAAE,QAAQ,GAAoC;AAC5E,MAAI,CAAC,WAAW,QAAQ,SAAS,EAAG,QAAO;AAC3C,SACE,gBAAAA,MAAC,QAAG,WAAU,gBAAe,cAAW,gBACtC;AAAA,oBAAAA,MAAC,SAAI,OAAM,6BAA4B;AAAA,sBAAAD,KAAC,QAAG,kBAAI;AAAA,MAAK,gBAAAA,KAAC,QAAI,8BAAoB,QAAQ,IAAI,GAAE;AAAA,OAAK;AAAA,IAChG,gBAAAC,MAAC,SAAI,OAAM,6DAA4D;AAAA,sBAAAD,KAAC,QAAG,qBAAO;AAAA,MAAK,gBAAAA,KAAC,QAAI,kBAAQ,cAAc,QAAQ,CAAC,GAAE;AAAA,OAAK;AAAA,IAClI,gBAAAC,MAAC,SAAI,OAAM,uBAAsB;AAAA,sBAAAD,KAAC,QAAG,uBAAS;AAAA,MAAK,gBAAAA,KAAC,QAAI,8BAAoB,QAAQ,aAAa,GAAE;AAAA,OAAK;AAAA,IACvG,QAAQ,sBAAsB,KAC7B,gBAAAC,MAAC,SAAI,WAAU,yBAAwB,OAAM,oDAC3C;AAAA,sBAAAD,KAAC,QAAG,qBAAO;AAAA,MAAK,gBAAAA,KAAC,QAAI,kBAAQ,qBAAoB;AAAA,OACnD;AAAA,KAEJ;AAEJ;;;ACTM,SAQQ,OAAAE,MARR,QAAAC,aAAA;AAJC,SAAS,gBAAgB,EAAE,aAAa,GAAoC;AACjF,MAAI,aAAa,WAAW,EAAG,QAAO;AACtC,SACE,gBAAAA,MAAC,SAAI,WAAU,YACb;AAAA,oBAAAA,MAAC,UAAK,WAAU,mBACb;AAAA,mBAAa;AAAA,MAAO;AAAA,MAAS,aAAa,WAAW,IAAI,KAAK;AAAA,OACjE;AAAA,IACA,gBAAAD,KAAC,QACE,uBAAa,IAAI,QAChB,gBAAAC,MAAC,QAAmB,WAAW,GAAG,UAAU,yCAAyC,kBACnF;AAAA,sBAAAA,MAAC,OAAE,MAAM,cAAc,GAAG,OAAO,GAAG,QAAO,UAAS,KAAI,uBACtD;AAAA,wBAAAA,MAAC,UAAK,WAAU,iBAAgB;AAAA;AAAA,UAAE,GAAG;AAAA,WAAO;AAAA,QAC5C,gBAAAD,KAAC,UAAK,WAAU,mBAAmB,aAAG,OAAM;AAAA,SAC9C;AAAA,MACA,gBAAAC,MAAC,UAAK,WAAU,kBACb;AAAA,WAAG;AAAA,QAAO;AAAA,QAAI,mBAAmB,GAAG,SAAS;AAAA,QAAG,GAAG,UAAU,gBAAa;AAAA,SAC7E;AAAA,SAPO,GAAG,MAQZ,CACD,GACH;AAAA,KACF;AAEJ;;;AC1BA,SAAS,QAAAC,aAAY;;;ACId,SAAS,eAAe,MAAsB;AACnD,QAAM,QAAQ,KAAK,MAAM,KAAK;AAC9B,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,MAAM,MAAM,KAAK,SAAS,MAAM,IAAI,CAAC,CAAC;AAC1E,SAAO,QAAQ,KAAK,KAAK;AAC3B;;;ADWM,SAWM,OAAAC,MAXN,QAAAC,aAAA;AAXC,IAAM,aAAaC,MAAK,SAASC,YAAW;AAAA,EACjD;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AACtB,GAKG;AACD,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SACE,gBAAAF,MAAC,SAAI,WAAW,8BAA8B,IAAI,IAChD;AAAA,oBAAAA,MAAC,UAAK,WAAU,uBAAuB;AAAA;AAAA,MAAM;AAAA,MAAE;AAAA,OAAM;AAAA,IACrD,gBAAAD,KAAC,SAAI,WAAU,uBACZ,kBAAQ,IAAI,CAAC,GAAG,MACf,gBAAAC;AAAA,MAAC;AAAA;AAAA,QAEC,WAAW,cAAc,EAAE,KAAK;AAAA,QAChC,MAAM,cAAc,EAAE,OAAO;AAAA,QAC7B,OAAO,GAAG,EAAE,QAAQ,SAAM,EAAE,KAAK;AAAA,QACjC,QAAO;AAAA,QACP,KAAI;AAAA,QAEJ;AAAA,0BAAAD,KAAC,UAAK,WAAU,aAAY,eAAY,QAAO;AAAA,UAC/C,gBAAAA,KAAC,UAAK,WAAU,eAAe,yBAAe,EAAE,IAAI,GAAE;AAAA,UAEtD,gBAAAA,KAAC,UAAK,WAAU,WAAW,qBAAW,EAAE,KAAK,GAAE;AAAA,UAC/C,gBAAAA,KAAC,UAAK,WAAU,cAAc,6BAAmB,EAAE,SAAS,GAAE;AAAA;AAAA;AAAA,MAXzD,GAAG,EAAE,QAAQ,IAAI,EAAE,IAAI,IAAI,CAAC;AAAA,IAYnC,CACD,GACH;AAAA,KACF;AAEJ,CAAC;;;AEjCM,SAAS,WAAW,QAAiC;AAC1D,MAAI,OAAO,SAAS,SAAS,EAAG,QAAO;AACvC,MAAI,OAAO,SAAS,SAAS,EAAG,QAAO;AACvC,MAAI,OAAO,SAAS,SAAS,EAAG,QAAO;AACvC,SAAO;AACT;;;ACHsB,SACuB,OAAAI,MADvB,QAAAC,aAAA;AANf,SAAS,sBAAsB,EAAE,WAAW,GAAuC;AACxF,QAAM,WAAW,WAAW,gBAAgB,CAAC,GAAG;AAChD,QAAM,KAAK,YAAY,WAAW,aAAa,CAAC,GAAG,IAAI,OAAK,EAAE,KAAK,CAAC;AACpE,QAAM,KAAK,WAAW,CAAC,GAAI,WAAW,WAAW,CAAC,GAAI,GAAI,WAAW,YAAY,CAAC,CAAE,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACvG,SACE,gBAAAA,MAAC,UAAK,WAAU,iBACb;AAAA,cAAU,KAAK,gBAAAA,MAAC,UAAK,WAAU,qBAAqB;AAAA;AAAA,MAAQ;AAAA,OAAG;AAAA,IAChE,gBAAAA,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,MAAE,gBAAAD,KAAC,UAAK,WAAU,sBAAqB,iBAAe,IAAI,cAAY,MAAM,EAAE,IAAI;AAAA,OAAE;AAAA,IACzH,gBAAAC,MAAC,UAAK,WAAU,sBAAqB;AAAA;AAAA,MAAE,gBAAAD,KAAC,UAAK,WAAU,sBAAqB,iBAAe,IAAI,cAAY,MAAM,EAAE,IAAI;AAAA,OAAE;AAAA,KAC3H;AAEJ;;;ARYQ,SAYA,YAAAE,WAXE,OAAAC,MADF,QAAAC,aAAA;AAZD,IAAM,YAAYC,MAAK,SAASC,WAAU;AAAA,EAC/C;AAAA,EAAY;AAAA,EAAW;AACzB,GAIG;AACD,QAAM,eAAe,WAAW,gBAAgB,CAAC;AACjD,QAAM,OAAO,OAAO,UAAU;AAC9B,SACE,gBAAAF,MAAC,aAAQ,WAAW,aAAa,YAAY,2BAA2B,EAAE,GAAG,OAAO,uBAAuB,EAAE,IAC3G;AAAA,oBAAAA,MAAC,YACC;AAAA,sBAAAA,MAAC,YAAO,MAAK,UAAS,WAAU,sBAAqB,SAAS,MAAM,SAAS,WAAW,IAAI,GAAG,iBAAe,CAAC,WAC7G;AAAA,wBAAAD,KAAC,UAAK,WAAU,oBAAmB,eAAY,QAAO,oBAAC;AAAA,QACtD,WAAW;AAAA,SACd;AAAA,MACC,QAAQ,gBAAAA,KAAC,UAAK,WAAU,wBAAuB,mBAAK;AAAA,MACrD,gBAAAA,KAAC,mBAAgB,SAAS,WAAW,SAAS;AAAA,MAC9C,gBAAAA,KAAC,yBAAsB,YAAwB;AAAA,MAC/C,gBAAAA,KAAC,OAAE,WAAU,uBAAsB,MAAM,cAAc,WAAW,OAAO,GAAG,QAAO,UAAS,KAAI,uBAAsB,cAAY,QAAQ,WAAW,IAAI,cAAc,2BAEvK;AAAA,OACF;AAAA,IACC,CAAC,aACA,gBAAAC,MAAAF,WAAA,EACG;AAAA,iBAAW,UAAU,WAAW,IAC/B,gBAAAC,KAAC,SAAI,WAAU,qBAAoB,0BAAY,IAE/C,gBAAAC,MAAC,SAAI,WAAU,kBACb;AAAA,wBAAAA,MAAC,UAAK,WAAU,yBAAwB;AAAA;AAAA,UAAa,WAAW,UAAU;AAAA,WAAO;AAAA,QACjF,gBAAAD,KAAC,SAAI,WAAU,oBACZ,qBAAW,UAAU,IAAI,QACxB,gBAAAA,KAAC,cAAyB,UAAU,MAAnB,GAAG,IAAoB,CACzC,GACH;AAAA,SACF;AAAA,MAEF,gBAAAA,KAAC,cAAW,MAAK,WAAU,OAAM,UAAI,OAAM,WAAU,SAAS,WAAW,WAAW,CAAC,GAAG;AAAA,MACxF,gBAAAA,KAAC,cAAW,MAAK,YAAW,OAAM,UAAI,OAAM,YAAW,SAAS,WAAW,YAAY,CAAC,GAAG;AAAA,MAC3F,gBAAAA,KAAC,mBAAgB,cAA4B;AAAA,OAC/C;AAAA,KAEJ;AAEJ,CAAC;;;AS7CK,gBAAAI,MAIA,QAAAC,aAJA;AAbC,SAAS,YAAY,EAAE,OAAO,OAAO,WAAW,SAAS,GAK7D;AACD,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAU;AAAA,MACV,iBAAe,CAAC;AAAA,MAChB,SAAS;AAAA,MAET;AAAA,wBAAAD,KAAC,UAAK,WAAU,yBAAwB,eAAY,QACjD,sBAAY,WAAM,UACrB;AAAA,QACA,gBAAAA,KAAC,UAAK,WAAU,uBAAuB,iBAAM;AAAA,QAC7C,gBAAAC,MAAC,UAAK,WAAU,uBAAsB,eAAY,QAAO;AAAA;AAAA,UAAG;AAAA,WAAM;AAAA;AAAA;AAAA,EACpE;AAEJ;;;ACRM,gBAAAC,OAEE,QAAAC,cAFF;AAVN,IAAM,QAAkD;AAAA,EACtD,CAAC,QAAQ,2BAA2B;AAAA,EACpC,CAAC,WAAW,2DAA2D;AAAA,EACvE,CAAC,aAAa,qBAAqB;AAAA,EACnC,CAAC,WAAW,kDAA6C;AAC3D;AAEO,SAAS,gBAAgB;AAC9B,SACE,gBAAAA,OAAC,YAAO,WAAU,kBAAiB,cAAW,yBAC5C;AAAA,oBAAAD,MAAC,UAAK,WAAU,yBAAwB,4BAAc;AAAA,IACrD,MAAM,IAAI,CAAC,CAAC,MAAM,OAAO,MACxB,gBAAAC,OAAC,UAAgB,WAAU,wBACzB;AAAA,sBAAAD,MAAC,OAAG,gBAAK;AAAA,MAAI;AAAA,MAAE;AAAA,SADN,IAEX,CACD;AAAA,KACH;AAEJ;;;ACJM,gBAAAE,OAEE,QAAAC,cAFF;AAVN,IAAM,eAA8D;AAAA,EAClE,CAAC,WAAW,SAAS;AAAA,EACrB,CAAC,WAAW,SAAS;AAAA,EACrB,CAAC,WAAW,SAAS;AAAA,EACrB,CAAC,WAAW,mBAAmB;AACjC;AAEO,SAAS,eAAe;AAC7B,SACE,gBAAAA,OAAC,YAAO,WAAU,iBAAgB,cAAW,qBAC3C;AAAA,oBAAAD,MAAC,UAAK,WAAU,wBAAuB,oBAAM;AAAA,IAC5C,aAAa,IAAI,CAAC,CAAC,OAAO,KAAK,MAC9B,gBAAAC,OAAC,UAAiB,WAAU,uBAC1B;AAAA,sBAAAD,MAAC,UAAK,WAAU,sBAAqB,cAAY,OAAO,eAAY,QAAO;AAAA,MAC1E;AAAA,SAFQ,KAGX,CACD;AAAA,IACD,gBAAAC,OAAC,UAAK,WAAU,uBACd;AAAA,sBAAAD,MAAC,UAAK,WAAU,uBAAsB,eAAY,QAAO;AAAA,MAAE;AAAA,OAE7D;AAAA,IACA,gBAAAC,OAAC,UAAK,WAAU,wBACd;AAAA,sBAAAD,MAAC,OAAE,gBAAE;AAAA,MAAI;AAAA,MAAiB,gBAAAA,MAAC,OAAE,gBAAE;AAAA,MAAI;AAAA,OACrC;AAAA,KACF;AAEJ;;;AChCA,SAAS,aAAAE,YAAW,UAAAC,SAAQ,YAAAC,iBAAgB;;;ACGrC,SAAS,UAAU,cAAsB,MAAc,KAAK,IAAI,GAAY;AACjF,QAAM,QAAQ,MAAM,IAAI,KAAK,YAAY,EAAE,QAAQ,KAAK;AACxD,MAAI,OAAO,GAAI,QAAO;AACtB,MAAI,OAAO,EAAG,QAAO;AACrB,SAAO;AACT;;;AD4CQ,gBAAAC,OACA,QAAAC,cADA;AA/CD,SAAS,mBAAmB,EAAE,KAAK,QAAQ,GAA2C;AAC3F,QAAM,CAAC,GAAG,IAAI,IAAIC,UAAS,CAAC;AAC5B,QAAM,YAAYC,QAA0B,IAAI;AAMhD,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,SAAS,CAAC,CAAC;AACzD,QAAM,KAAK,IAAI,SAAS;AAExB,EAAAC,WAAU,MAAM;AACd,UAAM,oBAAoB,SAAS;AACnC,UAAM,MAAM,UAAU;AACtB,SAAK,UAAU;AACf,SAAK,MAAM;AACX,WAAO,MAAM;AACX,yBAAmB,QAAQ;AAC3B,WAAK,MAAM;AAAA,IACb;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,EAAAA,WAAU,MAAM;AACd,QAAI,IAAI,WAAW,EAAG,SAAQ;AAAA,EAChC,GAAG,CAAC,IAAI,QAAQ,OAAO,CAAC;AAExB,MAAI,CAAC,GAAI,QAAO;AAEhB,SACE,gBAAAH;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAU;AAAA,MACV,cAAW;AAAA,MACX,UAAU;AAAA,MACV,UAAU,OAAK;AAAE,UAAE,eAAe;AAAG,gBAAQ;AAAA,MAAE;AAAA,MAC/C,SAAS,OAAK;AAAE,YAAI,EAAE,WAAW,EAAE,cAAe,SAAQ;AAAA,MAAE;AAAA,MAG5D,WAAW,OAAK;AACd,YAAI,EAAE,QAAQ,aAAc,MAAK,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,CAAC,CAAC;AACxE,YAAI,EAAE,QAAQ,YAAa,MAAK,KAAK,IAAI,YAAY,GAAG,CAAC,CAAC;AAAA,MAC5D;AAAA,MAEA;AAAA,wBAAAA,OAAC,SAAI,WAAU,iBACb;AAAA,0BAAAD,MAAC,UAAK,WAAU,mBAAkB,gCAAkB;AAAA,UACpD,gBAAAC,OAAC,UAAK,WAAU,qBAAqB;AAAA,wBAAY;AAAA,YAAE;AAAA,YAAI,IAAI;AAAA,aAAO;AAAA,UAClE,gBAAAD,MAAC,YAAO,MAAK,UAAS,WAAU,eAAc,SAAS,SAAS,cAAW,SAAQ,oBAAC;AAAA,WACtF;AAAA,QACA,gBAAAC,OAAC,SAAI,WAAW,oBAAoB,UAAU,GAAG,SAAS,CAAC,GAAG,GAAG,UAAU,oBAAoB,EAAE,IAC/F;AAAA,0BAAAA,OAAC,SAAI,WAAU,iBACb;AAAA,4BAAAD,MAAC,UAAK,WAAU,iBAAiB,aAAG,MAAK;AAAA,YACzC,gBAAAC,OAAC,UAAK,WAAU,gBAAe;AAAA;AAAA,cAAE,GAAG;AAAA,eAAO;AAAA,YAC3C,gBAAAA,OAAC,UAAK,WAAU,iBAAiB;AAAA,iCAAmB,GAAG,SAAS;AAAA,cAAE;AAAA,cAAI,GAAG,UAAU,UAAU;AAAA,eAAQ;AAAA,aACvG;AAAA,UACA,gBAAAD,MAAC,SAAI,WAAU,kBAAkB,aAAG,OAAM;AAAA,UAC1C,gBAAAC,OAAC,SAAI,WAAU,iBACb;AAAA,4BAAAA,OAAC,UAAK,WAAU,mBAAkB;AAAA;AAAA,cAAE,GAAG;AAAA,eAAO;AAAA,YAC9C,gBAAAD,MAAC,OAAE,WAAU,eAAc,MAAM,cAAc,GAAG,OAAO,GAAG,QAAO,UAAS,KAAI,uBAAsB,mCAAgB;AAAA,aACxH;AAAA,WACF;AAAA,QAGC,IAAI,SAAS,KACZ,gBAAAC,OAAC,SAAI,WAAU,iBACb;AAAA,0BAAAD,MAAC,YAAO,MAAK,UAAS,SAAS,MAAM,KAAK,KAAK,IAAI,YAAY,GAAG,CAAC,CAAC,GAAG,UAAU,cAAc,GAAG,yBAAM;AAAA,UACxG,gBAAAA,MAAC,YAAO,MAAK,UAAS,SAAS,MAAM,KAAK,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,CAAC,CAAC,GAAG,UAAU,cAAc,IAAI,SAAS,GAAG,yBAAM;AAAA,WACpI;AAAA;AAAA;AAAA,EAEJ;AAEJ;;;AExEO,SAAS,eAAe,cAA8C;AAC3E,SAAO,aACJ,QAAQ,QAAM,EAAE,gBAAgB,CAAC,GAAG,IAAI,SAAO,EAAE,GAAG,IAAI,MAAM,EAAE,KAAK,EAAE,CAAC,EACxE,KAAK,CAAC,GAAG,MAAM,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,CAAC;AACrF;;;A1B6BQ,SAyDF,YAAAK,WAzDE,OAAAC,OAyDF,QAAAC,cAzDE;AAvBD,SAAS,iBAAiB;AAC/B,QAAM,WAAW,qBAAqB;AACtC,QAAM,WAAW,iBAAiB;AAClC,QAAM,WAAW,YAAY;AAC7B,QAAM,UAAU,WAAW;AAC3B,QAAM,CAAC,aAAa,cAAc,IAAIC,UAAS,KAAK;AAKpD,QAAM,aAAa,SAAS,MAAM,gBAAgB,CAAC;AACnD,QAAM,gBAAgB,UAAU,aAAa,WAAW,OAAO,OAAK,CAAC,EAAE,OAAO;AAC9E,QAAM,UAAU,eAAe,SAAS,SAAS,cAAc,OAAO,OAAK,CAAC,OAAO,CAAC,CAAC,IAAI,aAAa;AAEtG,EAAAC,WAAU,MAAM;AACd,QAAI,QAAQ,WAAW,GAAG;AACxB,qBAAe,KAAK;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,QAAQ,MAAM,CAAC;AAEnB,MAAI,SAAS,WAAW;AACtB,WACE,gBAAAH,MAAC,UAAK,WAAU,kBACd,0BAAAA,MAAC,SAAI,WAAU,aAAY,qCAAkB,GAC/C;AAAA,EAEJ;AAEA,MAAI,SAAS,SAAS;AACpB,WACE,gBAAAA,MAAC,UAAK,WAAU,kBACd,0BAAAA,MAAC,SAAI,WAAU,8BAA6B,oCAAsB,GACpE;AAAA,EAEJ;AAGA,MAAI,CAAC,SAAS,MAAM;AAClB,WACE,gBAAAA,MAAC,UAAK,WAAU,kBACd,0BAAAA,MAAC,SAAI,WAAU,aAAY,iDAA8B,GAC3D;AAAA,EAEJ;AAEA,QAAM,EAAE,aAAa,cAAc,iBAAiB,cAAc,cAAc,IAAI,SAAS;AAI7F,QAAM,eAAe,UAAU,kBAAkB,gBAAgB,OAAO,OAAK,CAAC,EAAE,OAAO;AAGvF,QAAM,YAAY,aAAa,OAAO,MAAM,EAAE;AAC9C,QAAM,eAAe,SAAS,SAAS,aAAa,OAAO,OAAK,CAAC,OAAO,CAAC,CAAC,IAAI;AAC9E,QAAM,UAAW,WAAW,CAAC,SAAS,SAAU,SAAS,KAAK,UAAU,eAAe,YAAY;AAGnG,QAAM,eAAe,iBAAiB,aAAa,KAAK,OAAK,EAAE,SAAS,cAAc,IAAI,IACtF,gBACA;AACJ,QAAM,YAAY,aAAa,IAAI,OAAK,EAAE,IAAI;AAC9C,QAAM,cAAc,aAAa,SAAS,aAAa;AACvD,QAAM,cAAc,aAAa,OAAO,OAAK,CAAC,EAAE,OAAO;AACvD,QAAM,eAAe,aAAa,OAAO,OAAK,EAAE,OAAO;AACvD,QAAM,aAAa,YAAY,SAAS,KAAK,aAAa,SAAS;AACnE,QAAM,aAAa;AACnB,QAAM,cAAc;AACpB,QAAM,cAAc,aAAa,CAAC,YAAY,WAAW,IAAI,CAAC;AAC9D,QAAM,eAAe,SAAS,aAAa,CAAC,GAAG,WAAW,GAAG,WAAW,CAAC;AAKzE,QAAM,SAAS,QAAQ,CAAC,KAAK;AAE7B,MAAI;AACJ,MAAI,aAAa,WAAW,KAAK,SAAS,QAAQ;AAChD,sBAAkB,gBAAAA,MAAC,SAAI,WAAU,aAAY,uDAAoC;AAAA,EACnF,WAAW,YAAY;AACrB,sBACE,gBAAAC,OAAAF,WAAA,EACE;AAAA,sBAAAC;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,YAAY;AAAA,UACnB,WAAW,SAAS,YAAY,UAAU;AAAA,UAC1C,UAAU,MAAM,SAAS,OAAO,UAAU;AAAA;AAAA,MAC5C;AAAA,MACC,CAAC,SAAS,YAAY,UAAU,KAC/B,YAAY,IAAI,gBACd,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA,WAAW,SAAS,YAAY,WAAW,IAAI;AAAA,UAC/C,UAAU,SAAS;AAAA;AAAA,QAHd,WAAW;AAAA,MAIlB,CACD;AAAA,MAEH,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAM;AAAA,UACN,OAAO,aAAa;AAAA,UACpB,WAAW,SAAS,YAAY,WAAW;AAAA,UAC3C,UAAU,MAAM,SAAS,OAAO,WAAW;AAAA;AAAA,MAC7C;AAAA,MACC,CAAC,SAAS,YAAY,WAAW,KAChC,aAAa,IAAI,gBACf,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC;AAAA,UACA,WAAW,SAAS,YAAY,WAAW,IAAI;AAAA,UAC/C,UAAU,SAAS;AAAA;AAAA,QAHd,WAAW;AAAA,MAIlB,CACD;AAAA,OAEL;AAAA,EAEJ,OAAO;AACL,sBAAkB,aAAa,IAAI,gBACjC,gBAAAA;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA,WAAW,SAAS,YAAY,WAAW,IAAI;AAAA,QAC/C,UAAU,SAAS;AAAA;AAAA,MAHd,WAAW;AAAA,IAIlB,CACD;AAAA,EACH;AAEA,SACE,gBAAAC,OAAC,UAAK,WAAU,kBACd;AAAA,oBAAAA,OAAC,SAAI,WAAU,sBACb;AAAA,sBAAAA,OAAC,UAAK,WAAU,oBAAoB;AAAA,iBAAS,KAAK;AAAA,QAAI;AAAA,QAAI,UAAU,qBAAqB;AAAA,SAAsB;AAAA,MAC/G,gBAAAA,OAAC,UAAK,WAAU,4BACb;AAAA,oBAAY,KACX,gBAAAD;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAW,uBAAuB,SAAS,SAAS,8BAA8B,EAAE;AAAA,YACpF,SAAS,SAAS;AAAA,YAClB,gBAAc,SAAS;AAAA,YAEtB,mBAAS,SAAS,mBAAgB,WAAW,YAAY;AAAA;AAAA,QAC5D;AAAA,QAEF,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,SAAS,MAAO,eAAe,SAAS,UAAU,IAAI,SAAS,YAAY,CAAC,GAAG,WAAW,GAAG,WAAW,CAAC;AAAA,YAExG,yBAAe,sBAAiB;AAAA;AAAA,QACnC;AAAA,QACA,gBAAAC,OAAC,UAAK,WAAU,wBACd;AAAA,0BAAAD,MAAC,UAAK,WAAU,YAAW,eAAY,QAAO;AAAA,UAAE;AAAA,UACvC,mBAAmB,WAAW;AAAA,WACzC;AAAA,SACF;AAAA,OACF;AAAA,IACA,gBAAAA,MAAC,gBAAa,SAAkB,WAAW,UAAU,MAAM,eAAe,IAAI,IAAI,QAAW,YAAY,cAAc,QAAgB,SAAS,SAAS,KAAK,WAAW,CAAC,GAAG;AAAA,IAC7K,gBAAAA,MAAC,SAAI,WAAU,aACZ,2BACH;AAAA,IACA,gBAAAA,MAAC,gBAAa;AAAA,IACd,gBAAAA,MAAC,iBAAc;AAAA,IACd,eAAe,QAAQ,SAAS,KAC/B,gBAAAA,MAAC,sBAAmB,KAAK,SAAS,SAAS,MAAM,eAAe,KAAK,GAAG;AAAA,KAE5E;AAEJ;;;A2B/KQ,gBAAAI,aAAA;AAJD,SAAS,gBAAgB;AAC9B,SACE,gBAAAA,MAAC,YAAO,WAAU,eAChB,0BAAAA,MAAC,SAAI,WAAU,qBAAoB,eAAY,QAC7C,0BAAAA,MAAC,UAAK,WAAU,wBAAuB,6CAA+B,GACxE,GACF;AAEJ;;;A9B4BW,qBAAAC,WAAA,OAAAC,OAwBK,QAAAC,cAxBL;AAjBX,SAAS,wBAAwB,EAAE,SAAS,GAA4B;AACtE,QAAM,iBAAiB,WAAW,kBAAkB;AACpD,QAAM,CAAC,WAAW,IAAIC,UAAS,MAAM;AACnC,QAAI,CAAC,gBAAgB;AACnB,aAAO,IAAI,YAAY;AAAA,QACrB,gBAAgB;AAAA,UACd,SAAS;AAAA,YACP,OAAO;AAAA,YACP,sBAAsB;AAAA,UACxB;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AACA,WAAO;AAAA,EACT,CAAC;AAED,MAAI,gBAAgB;AAClB,WAAO,gBAAAF,MAAAD,WAAA,EAAG,UAAS;AAAA,EACrB;AAEA,SACE,gBAAAC,MAAC,uBAAoB,QAAQ,aAC1B,UACH;AAEJ;AAOO,SAAS,QAAQ,EAAE,aAAa,UAAU,qBAAqB,kBAAkB,MAAM,WAAW,GAAiB;AACxH,SACE,gBAAAA,MAAC,oBAAiB,OAAO,EAAE,SAAS,iBAAiB,GACnD,0BAAAA,MAAC,2BACC,0BAAAC,OAAC,SAAI,WAAU,WACb;AAAA,oBAAAA,OAAC,SAAI,WAAU,YACb;AAAA,sBAAAD,MAAC,YAAO,WAAU,oBAChB,0BAAAC,OAAC,UAAK,WAAU,oBACb;AAAA,gBAAQ,gBAAAD,MAAC,UAAK,WAAU,2BAA0B,0BAAY;AAAA,QAC/D,gBAAAC,OAAC,UAAK,WAAU,wBAAuB;AAAA;AAAA,UACvB,cAAc,YAAY;AAAA,WAC1C;AAAA,SACF,GACF;AAAA,MACA,gBAAAD,MAAC,mBAAgB,OAAO,aACtB,0BAAAA,MAAC,kBAAe,GAClB;AAAA,OACF;AAAA,IACC,cAAc,gBAAAA,MAAC,iBAAc;AAAA,KAChC,GACF,GACF;AAEJ;","names":["useState","createContext","use","useState","useEffect","useCallback","useEffect","useState","KEY","load","useEffect","useState","Fragment","jsx","jsxs","useState","useEffect","memo","Fragment","jsx","jsxs","SignalChip","jsx","jsxs","jsx","jsxs","memo","jsx","jsxs","memo","JobLaneRow","jsx","jsxs","Fragment","jsx","jsxs","memo","RepoBoard","jsx","jsxs","jsx","jsxs","jsx","jsxs","useEffect","useRef","useState","jsx","jsxs","useState","useRef","useEffect","Fragment","jsx","jsxs","useState","useEffect","jsx","Fragment","jsx","jsxs","useState"]}
|
package/package.json
CHANGED