From c79e07739e46b20c316e01ce35e90eef9dedbd18 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Tue, 28 Jul 2026 11:48:17 -0700 Subject: [PATCH] Add public Ecosystem Atlas route --- portal/src/app/atlas/page.tsx | 31 ++ .../src/components/atlas/AtlasDashboard.tsx | 435 ++++++++++++++++++ .../components/corporate/CorporateFooter.tsx | 1 + portal/src/lib/atlas.ts | 398 ++++++++++++++++ portal/src/lib/corporate-site-data.ts | 1 + 5 files changed, 866 insertions(+) create mode 100644 portal/src/app/atlas/page.tsx create mode 100644 portal/src/components/atlas/AtlasDashboard.tsx create mode 100644 portal/src/lib/atlas.ts diff --git a/portal/src/app/atlas/page.tsx b/portal/src/app/atlas/page.tsx new file mode 100644 index 0000000..18baac2 --- /dev/null +++ b/portal/src/app/atlas/page.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from 'next'; + +import AtlasDashboard from '@/components/atlas/AtlasDashboard'; +import { loadAtlasSnapshot } from '@/lib/atlas'; + +export const dynamic = 'force-dynamic'; +export const revalidate = 0; + +export const metadata: Metadata = { + title: 'Ecosystem Atlas', + description: + 'Live Sankofa route matrix, hidden inventory, and revision notes with server-side IT inventory awareness.', + alternates: { + canonical: 'https://sankofa.nexus/atlas', + }, +}; + +export default async function AtlasPage() { + const snapshot = await loadAtlasSnapshot(); + + return ( + + ); +} diff --git a/portal/src/components/atlas/AtlasDashboard.tsx b/portal/src/components/atlas/AtlasDashboard.tsx new file mode 100644 index 0000000..fac51d8 --- /dev/null +++ b/portal/src/components/atlas/AtlasDashboard.tsx @@ -0,0 +1,435 @@ +'use client'; + +import { ArrowUpDown, Filter, Globe2, HardDrive, History, Search, ShieldCheck, Sparkles } from 'lucide-react'; +import Link from 'next/link'; +import { useMemo, useState } from 'react'; + +import { Badge } from '@/components/ui/badge'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card'; +import { Input } from '@/components/ui/Input'; +import type { AtlasGuestRow, AtlasHiddenRow, AtlasRecommendation, AtlasRevision, AtlasRouteRow } from '@/lib/atlas'; + +type Props = { + routeRows: AtlasRouteRow[]; + guestRows: AtlasGuestRow[]; + hiddenRows: AtlasHiddenRow[]; + revisions: AtlasRevision[]; + recommendations: AtlasRecommendation[]; + liveSummary: { + collectedAt?: string; + guestCount?: number | null; + runningCount?: number | null; + stoppedCount?: number | null; + portMapState?: string; + portMapNote?: string; + inventoryState?: string; + summaryState?: string; + readApiConfigured: boolean; + }; +}; + +type SortKey = 'name' | 'status' | 'host' | 'serviceFamily'; + +function includesQuery(value: string, query: string): boolean { + return value.toLowerCase().includes(query.toLowerCase()); +} + +function sortRouteRows(rows: AtlasRouteRow[], sortKey: SortKey): AtlasRouteRow[] { + return [...rows].sort((a, b) => { + if (sortKey === 'status') return a.status.localeCompare(b.status) || a.hostnames[0].localeCompare(b.hostnames[0]); + if (sortKey === 'host') return a.hostnames[0].localeCompare(b.hostnames[0]); + return a.hostnames.join(', ').localeCompare(b.hostnames.join(', ')); + }); +} + +function sortGuestRows(rows: AtlasGuestRow[], sortKey: SortKey): AtlasGuestRow[] { + return [...rows].sort((a, b) => { + if (sortKey === 'status') return a.status.localeCompare(b.status) || a.vmid.localeCompare(b.vmid); + if (sortKey === 'host') return a.node.localeCompare(b.node) || a.vmid.localeCompare(b.vmid); + if (sortKey === 'serviceFamily') return a.serviceFamily.localeCompare(b.serviceFamily) || a.vmid.localeCompare(b.vmid); + return a.name.localeCompare(b.name) || a.vmid.localeCompare(b.vmid); + }); +} + +export default function AtlasDashboard({ + routeRows, + guestRows, + hiddenRows, + revisions, + recommendations, + liveSummary, +}: Props) { + const [query, setQuery] = useState(''); + const [scope, setScope] = useState<'all' | 'routes' | 'guests' | 'hidden'>('all'); + const [sortKey, setSortKey] = useState('name'); + + const filteredRouteRows = useMemo(() => { + const rows = routeRows.filter((row) => { + if (!query) return true; + return ( + row.hostnames.some((host) => includesQuery(host, query)) || + includesQuery(row.edge, query) || + includesQuery(row.backend, query) || + includesQuery(row.source, query) || + includesQuery(row.notes ?? '', query) + ); + }); + return sortRouteRows(rows, sortKey); + }, [query, routeRows, sortKey]); + + const filteredGuestRows = useMemo(() => { + const rows = guestRows.filter((row) => { + if (!query) return true; + return ( + includesQuery(row.vmid, query) || + includesQuery(row.name, query) || + includesQuery(row.node, query) || + includesQuery(row.ip, query) || + includesQuery(row.status, query) || + includesQuery(row.serviceFamily, query) + ); + }); + return sortGuestRows(rows, sortKey); + }, [guestRows, query, sortKey]); + + const filteredHiddenRows = useMemo(() => { + const rows = hiddenRows.filter((row) => { + if (!query) return true; + return includesQuery(row.label, query) || includesQuery(row.detail, query) || includesQuery(row.source, query); + }); + return [...rows].sort((a, b) => a.label.localeCompare(b.label)); + }, [hiddenRows, query]); + + return ( +
+
+
+
+
+ + Live atlas + +

Ecosystem Atlas

+

+ Public Sankofa routing, live inventory, hidden operator surfaces, and revision notes in one + place. The live counters come from the portal server's IT read API, while the route matrix + is normalized from the repo's canonical hostname and tunnel docs. +

+
+ + Review routes + + + Inspect inventory + + + Back to Sankofa + +
+
+ +
+ + + Live guests + + +
+ {liveSummary.guestCount ?? guestRows.length} +
+

+ {liveSummary.readApiConfigured ? 'Server-side inventory feed configured' : 'Live inventory feed unavailable'} +

+
+
+ + + Running / stopped + + +
+ {liveSummary.runningCount ?? '—'} / {liveSummary.stoppedCount ?? '—'} +
+

+ {liveSummary.collectedAt ? `Collected ${liveSummary.collectedAt}` : 'No collection timestamp yet'} +

+
+
+ + + Port-map state + + +
+ + {liveSummary.portMapState ?? 'unknown'} +
+ {liveSummary.portMapNote ?

{liveSummary.portMapNote}

: null} +
+
+
+
+
+ +
+ + + + + Public routes + + + +
{routeRows.length}
+
+
+ + + + + Hidden inventory + + + +
{hiddenRows.length}
+
+
+ + + + + Revision notes + + + +
{revisions.length}
+
+
+
+ +
+
+ + setQuery(event.target.value)} + placeholder="Search hostnames, VMIDs, IPs, or notes" + className="border-sovereign-bronze/25 bg-sovereign-obsidian pl-10 text-sovereign-ivory placeholder:text-sovereign-ivory/35" + /> +
+ + + + +
+ + {(scope === 'all' || scope === 'routes') && ( +
+ + + + + Route matrix + + + + + + + + + + + + + + + {filteredRouteRows.map((row) => ( + + + + + + + + ))} + +
HostnameEdgeBackendVisibilityStatus
+
{row.hostnames.join(', ')}
+
{row.source}
+
{row.edge}{row.backend} + + {row.visibility} + + + + {row.status} + + {row.notes ?

{row.notes}

: null} +
+
+
+
+ )} + + {(scope === 'all' || scope === 'guests') && ( +
+ + + + + Live guest inventory + + + + + + + + + + + + + + + + {filteredGuestRows.map((row) => ( + + + + + + + + + ))} + +
VMIDNameIPNodeStatusFamily
{row.vmid} +
{row.name}
+
{row.type}
+
{row.ip}{row.node} + + {row.status} + + {row.serviceFamily}
+
+
+
+ )} + + {(scope === 'all' || scope === 'hidden') && ( +
+ + + + + Capture required + + + + {filteredHiddenRows.map((row) => ( +
+

{row.label}

+

{row.detail}

+

{row.source}

+
+ ))} +
+
+
+ )} + +
+ + + + + Revision history + + + + {revisions.map((revision) => ( +
+

{revision.date}

+

{revision.title}

+

{revision.detail}

+
+ ))} +
+
+ + + + + + Recommendations + + + + {recommendations.map((recommendation) => ( +
+
+

{recommendation.title}

+ + {recommendation.priority} + +
+

{recommendation.detail}

+
+ ))} +
+
+
+
+
+ ); +} diff --git a/portal/src/components/corporate/CorporateFooter.tsx b/portal/src/components/corporate/CorporateFooter.tsx index 792f339..19413e4 100644 --- a/portal/src/components/corporate/CorporateFooter.tsx +++ b/portal/src/components/corporate/CorporateFooter.tsx @@ -30,6 +30,7 @@ const footerColumns = [ title: 'Company', links: [ { label: 'About Sankofa', href: '#resources' }, + { label: 'Ecosystem Atlas', href: '/atlas' }, { label: 'Institutional registry', href: 'https://d-bis.org/cb/dbis' }, { label: 'Partner program', href: '/partner' }, { label: 'Contact', href: 'https://portal.sankofa.nexus' }, diff --git a/portal/src/lib/atlas.ts b/portal/src/lib/atlas.ts new file mode 100644 index 0000000..5c1836c --- /dev/null +++ b/portal/src/lib/atlas.ts @@ -0,0 +1,398 @@ +import { itReadApiBaseUrl, itReadApiKey } from '@/app/api/it/_auth'; + +export type AtlasRouteRow = { + hostnames: string[]; + edge: string; + backend: string; + visibility: 'public' | 'internal' | 'hidden'; + status: 'live' | 'documented' | 'pending'; + source: string; + notes?: string; +}; + +export type AtlasGuestRow = { + vmid: string; + type: string; + name: string; + node: string; + ip: string; + status: string; + visibility: 'live' | 'hidden'; + serviceFamily: string; +}; + +export type AtlasHiddenRow = { + label: string; + detail: string; + source: string; +}; + +export type AtlasRevision = { + date: string; + title: string; + detail: string; +}; + +export type AtlasRecommendation = { + priority: 'P1' | 'P2' | 'P3'; + title: string; + detail: string; +}; + +export type AtlasSnapshot = { + routeRows: AtlasRouteRow[]; + guestRows: AtlasGuestRow[]; + hiddenRows: AtlasHiddenRow[]; + revisions: AtlasRevision[]; + recommendations: AtlasRecommendation[]; + liveSummary: { + collectedAt?: string; + guestCount?: number | null; + runningCount?: number | null; + stoppedCount?: number | null; + portMapState?: string; + portMapNote?: string; + inventoryState?: string; + summaryState?: string; + readApiConfigured: boolean; + }; +}; + +type RecordLike = Record; + +async function fetchJson(url: string, apiKey?: string): Promise { + try { + const res = await fetch(url, { + cache: 'no-store', + headers: { + Accept: 'application/json', + ...(apiKey ? { 'X-API-Key': apiKey } : {}), + }, + }); + if (!res.ok) return null; + return (await res.json()) as unknown; + } catch { + return null; + } +} + +function asRecord(value: unknown): RecordLike | null { + return value && typeof value === 'object' && !Array.isArray(value) ? (value as RecordLike) : null; +} + +function firstRecordArray(value: unknown): RecordLike[] { + if (!value || typeof value !== 'object') return []; + if (Array.isArray(value)) { + return value.filter((entry): entry is RecordLike => Boolean(entry && typeof entry === 'object')); + } + const record = value as RecordLike; + for (const key of ['guests', 'rows', 'items', 'nodes', 'vms', 'inventory', 'data']) { + const candidate = record[key]; + if (Array.isArray(candidate)) { + return candidate.filter((entry): entry is RecordLike => Boolean(entry && typeof entry === 'object')); + } + } + for (const candidate of Object.values(record)) { + const nested = firstRecordArray(candidate); + if (nested.length > 0) return nested; + } + return []; +} + +function toText(value: unknown, fallback = '—'): string { + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + return fallback; +} + +function buildGuestRows(liveInventory: unknown): AtlasGuestRow[] { + const rows = firstRecordArray(liveInventory); + const vmRows = rows.filter((row) => + ['vmid', 'name', 'ip', 'status', 'node', 'type'].some((key) => key in row) + ); + + return vmRows.map((row) => ({ + vmid: toText(row.vmid ?? row.id ?? row.vmid_number ?? row.vmidId), + type: toText(row.type ?? row.kind ?? row.resourceType ?? 'guest'), + name: toText(row.name ?? row.hostname ?? row.label), + node: toText(row.node ?? row.host ?? row.site ?? row.cluster ?? '—'), + ip: toText(row.ip ?? row.ipAddress ?? row.address ?? row.primary_ip ?? '—'), + status: toText(row.status ?? row.state ?? row.phase ?? 'unknown'), + visibility: 'live', + serviceFamily: toText(row.serviceFamily ?? row.family ?? row.role ?? row.group ?? 'general'), + })); +} + +function buildRoutes(): AtlasRouteRow[] { + return [ + { + hostnames: ['sankofa.nexus', 'www.sankofa.nexus'], + edge: 'Cloudflare DNS -> 76.53.10.36 -> NPMplus', + backend: '192.168.11.51:3000 Sankofa public site', + visibility: 'public', + status: 'live', + source: 'docs/INSTITUTIONAL_REGISTRY.md', + notes: 'Public corporate apex. www canonicalizes to apex.', + }, + { + hostnames: ['phoenix.sankofa.nexus', 'www.phoenix.sankofa.nexus'], + edge: 'Cloudflare DNS -> 76.53.10.36 -> NPMplus', + backend: '192.168.11.50:4000 Phoenix surface', + visibility: 'public', + status: 'live', + source: 'docs/INSTITUTIONAL_REGISTRY.md', + }, + { + hostnames: ['the-order.sankofa.nexus', 'www.the-order.sankofa.nexus'], + edge: 'Cloudflare DNS -> 76.53.10.36 -> 192.168.11.39:80 HAProxy -> portal', + backend: '192.168.11.51:3000 Sankofa portal stack', + visibility: 'public', + status: 'live', + source: 'docs/04-configuration/ALL_VMIDS_ENDPOINTS.md', + notes: 'www redirects to apex.', + }, + { + hostnames: ['studio.sankofa.nexus'], + edge: 'Cloudflare DNS -> 76.53.10.36 -> NPMplus', + backend: '192.168.11.72:8000 Sankofa Studio', + visibility: 'public', + status: 'live', + source: 'docs/04-configuration/ALL_VMIDS_ENDPOINTS.md', + notes: 'Public tooling surface under /studio.', + }, + { + hostnames: ['portal.sankofa.nexus'], + edge: 'Cloudflare Tunnel -> control-plane ingress', + backend: 'portal.portal.svc.cluster.local:80', + visibility: 'internal', + status: 'live', + source: 'cloudflare/tunnel-configs/control-plane.yaml', + notes: 'Client workspace and SSO shell.', + }, + { + hostnames: ['keycloak.sankofa.nexus'], + edge: 'Cloudflare Tunnel -> control-plane ingress', + backend: 'keycloak.keycloak.svc.cluster.local:8080', + visibility: 'internal', + status: 'live', + source: 'cloudflare/tunnel-configs/control-plane.yaml', + notes: 'Shared identity provider.', + }, + { + hostnames: ['admin.sankofa.nexus'], + edge: 'Cloudflare DNS -> NPMplus / protected app', + backend: 'SSO administration boundary', + visibility: 'internal', + status: 'documented', + source: 'docs/02-architecture/EXPECTED_WEB_CONTENT.md', + }, + { + hostnames: ['dash.sankofa.nexus'], + edge: 'Cloudflare DNS -> NPMplus / operator access', + backend: 'IP allowlist + MFA operator dashboard', + visibility: 'hidden', + status: 'documented', + source: 'docs/02-architecture/EXPECTED_WEB_CONTENT.md', + }, + { + hostnames: ['pve.sankofa.nexus', 'pve1.sankofa.nexus'], + edge: 'Cloudflare DNS -> NPMplus / tunnel', + backend: 'Proxmox management surfaces', + visibility: 'hidden', + status: 'documented', + source: 'docs/INSTITUTIONAL_REGISTRY.md', + }, + { + hostnames: ['cc.sankofa.nexus', 'auth.cc.sankofa.nexus', 'admin.cc.sankofa.nexus', 'entity.cc.sankofa.nexus'], + edge: 'Cloudflare DNS -> Complete Credential stack', + backend: 'Credential issuance and admin portals', + visibility: 'hidden', + status: 'documented', + source: 'docs/INSTITUTIONAL_REGISTRY.md', + }, + { + hostnames: ['ml110-01.sankofa.nexus', 'ml110-01-api.sankofa.nexus', 'ml110-01-metrics.sankofa.nexus'], + edge: 'Cloudflare DNS -> internal instance routing', + backend: '192.168.11.10 / instance services', + visibility: 'hidden', + status: 'documented', + source: 'docs/proxmox/DNS_CONFIGURATION.md', + }, + { + hostnames: ['r630-01.sankofa.nexus', 'r630-01-api.sankofa.nexus', 'r630-01-metrics.sankofa.nexus'], + edge: 'Cloudflare DNS -> internal instance routing', + backend: '192.168.11.11 / instance services', + visibility: 'hidden', + status: 'documented', + source: 'docs/proxmox/DNS_CONFIGURATION.md', + }, + { + hostnames: ['rancher.sankofa.nexus', 'argocd.sankofa.nexus', 'grafana.sankofa.nexus', 'vault.sankofa.nexus', 'k8s-api.sankofa.nexus'], + edge: 'Cloudflare Tunnel -> control-plane ingress', + backend: 'Kubernetes control-plane services', + visibility: 'hidden', + status: 'documented', + source: 'cloudflare/tunnel-configs/control-plane.yaml', + }, + ]; +} + +function buildHiddenRows(): AtlasHiddenRow[] { + return [ + { + label: 'Atlas hidden inventory', + detail: 'Public page can surface routes and hosts that are documented in repo but not exposed in the main nav.', + source: 'portal/src/lib/atlas.ts', + }, + { + label: 'IT read API', + detail: 'Server-side inventory and drift fetches are backed by IT_READ_API_URL and IT_READ_API_KEY.', + source: 'portal/src/app/api/it/_auth.ts', + }, + { + label: 'Control-plane tunnel', + detail: 'portal, keycloak, rancher, argocd, grafana, vault, and k8s-api share the Cloudflare tunnel ingress.', + source: 'cloudflare/tunnel-configs/control-plane.yaml', + }, + { + label: 'Proxmox instance DNS', + detail: 'ml110-01 and r630-01 hostnames are documented internal inventory rather than public marketing surfaces.', + source: 'docs/proxmox/DNS_CONFIGURATION.md', + }, + ]; +} + +function buildRevisions(): AtlasRevision[] { + return [ + { + date: '2026-07-28', + title: 'Atlas route published', + detail: 'Added a public /atlas page on the Sankofa apex so the route can be linked from the corporate shell.', + }, + { + date: '2026-06-04', + title: 'Institutional registry published', + detail: 'Documented the primary Sankofa and Phoenix hostnames, including public portal and identity boundaries.', + }, + { + date: '2026-03-27', + title: 'NPM routing authority updated', + detail: 'Route ownership for sankofa.nexus, phoenix.sankofa.nexus, and the-order.sankofa.nexus was consolidated in the NPM routing docs.', + }, + { + date: '2025-12-15', + title: 'Cluster status baseline', + detail: 'The sfvalley-01 Proxmox / Ceph status report captured the original cluster baseline and quorum state.', + }, + ]; +} + +function buildRecommendations(): AtlasRecommendation[] { + return [ + { + priority: 'P1', + title: 'Publish an atlas snapshot feed', + detail: 'Generate a JSON snapshot of routes, hosts, and revision history so public rendering and incident review use the same payload.', + }, + { + priority: 'P1', + title: 'Keep live inventory server-side', + detail: 'Continue fetching the IT inventory from the server-side read API so the public page stays current without exposing credentials.', + }, + { + priority: 'P2', + title: 'Export hidden inventory separately', + detail: 'Add a distinct capture-required section for hidden or operator-only hosts so the public page stays readable while still exhaustive.', + }, + { + priority: 'P2', + title: 'Add CSV / JSON export buttons', + detail: 'Allow the atlas to feed handoff, audit, and escalation workflows without copy/paste.', + }, + { + priority: 'P3', + title: 'Add route health badges', + detail: 'Annotate route rows with health and 404/5xx checks once the checkers are wired to the public edge.', + }, + ]; +} + +async function loadLiveSummary() { + const base = itReadApiBaseUrl(); + const apiKey = itReadApiKey(); + const readApiConfigured = Boolean(base); + + if (!base) { + return { + collectedAt: undefined, + guestCount: null, + runningCount: null, + stoppedCount: null, + portMapState: 'unconfigured', + portMapNote: 'IT_READ_API_URL is not configured on the portal server.', + inventoryState: 'unconfigured', + summaryState: 'unconfigured', + readApiConfigured, + }; + } + + const root = base.replace(/\/$/, ''); + const [summary, inventory, portMap] = await Promise.all([ + fetchJson(`${root}/v1/summary`, apiKey), + fetchJson(`${root}/v1/inventory/live`, apiKey), + fetchJson(`${root}/v1/portmap/joined`, apiKey), + ]); + + const summaryRecord = asRecord(summary); + const inventoryRecord = asRecord(inventory); + const portMapRecord = asRecord(portMap); + const guestRows = buildGuestRows(inventoryRecord ?? inventory); + + const liveSummary = summaryRecord && 'artifacts' in summaryRecord ? (summaryRecord.artifacts as RecordLike) : null; + + return { + collectedAt: + toText(summaryRecord?.envelope_at ?? summaryRecord?.live_collected_at ?? summaryRecord?.drift_collected_at, undefined), + guestCount: typeof summaryRecord?.guest_count === 'number' ? summaryRecord.guest_count : guestRows.length, + runningCount: + typeof inventoryRecord?.running === 'number' + ? inventoryRecord.running + : typeof summaryRecord?.guest_count === 'number' + ? summaryRecord.guest_count + : null, + stoppedCount: + typeof inventoryRecord?.stopped === 'number' ? inventoryRecord.stopped : null, + portMapState: portMapRecord?.stale ? 'stale' : 'live', + portMapNote: toText(portMapRecord?.note, undefined), + inventoryState: inventory ? 'live' : 'unavailable', + summaryState: summary ? 'live' : 'unavailable', + readApiConfigured, + // keep the snapshot summary available if the UI wants to expose it later + // without another fetch path. + ...(liveSummary ? { liveArtifacts: liveSummary } : {}), + } as AtlasSnapshot['liveSummary'] & { liveArtifacts?: RecordLike }; +} + +export async function loadAtlasSnapshot(): Promise { + const liveSummary = await loadLiveSummary(); + const routes = buildRoutes(); + const hiddenRows = buildHiddenRows(); + const revisions = buildRevisions(); + const recommendations = buildRecommendations(); + + const liveInventory = await (async () => { + const base = itReadApiBaseUrl(); + const apiKey = itReadApiKey(); + if (!base) return null; + return fetchJson(`${base.replace(/\/$/, '')}/v1/inventory/live`, apiKey); + })(); + + return { + routeRows: routes, + guestRows: buildGuestRows(liveInventory), + hiddenRows, + revisions, + recommendations, + liveSummary, + }; +} diff --git a/portal/src/lib/corporate-site-data.ts b/portal/src/lib/corporate-site-data.ts index 7c43665..d140d51 100644 --- a/portal/src/lib/corporate-site-data.ts +++ b/portal/src/lib/corporate-site-data.ts @@ -68,6 +68,7 @@ export const corporateNav: CorporateNavItem[] = [ { label: 'Institutional grade', href: '#institutional-grade' }, { label: 'Compliance', href: '#institutional-compliance' }, { label: 'Resources', href: '#resources' }, + { label: 'Atlas', href: '/atlas' }, ]; export const productDivisions: ProductDivision[] = [