fix(portal): corporate landing SEO, favicon, and public home UX
Some checks failed
API CI / API Lint (push) Successful in 53s
API CI / API Type Check (push) Failing after 51s
API CI / API Test (push) Successful in 1m32s
API CI / API Build (push) Failing after 1m0s
API CI / Build Docker Image (push) Has been skipped
CD Pipeline / Deploy to Staging (push) Failing after 32s
CI Pipeline / Lint and Type Check (push) Failing after 33s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 2m1s
CI Pipeline / Test Frontend (push) Failing after 35s
CI Pipeline / Security Scan (push) Failing after 1m12s
Deploy to Staging / Deploy to Staging (push) Failing after 28s
Portal CI / Portal Lint (push) Failing after 19s
Portal CI / Portal Type Check (push) Failing after 18s
Portal CI / Portal Test (push) Failing after 19s
Portal CI / Portal Build (push) Failing after 18s
Test Suite / frontend-tests (push) Failing after 30s
Test Suite / api-tests (push) Failing after 44s
Test Suite / blockchain-tests (push) Failing after 27s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 18s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 19s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 18s
CD Pipeline / Deploy to Production (push) Has been skipped

Add sankofa.nexus marketing site with institutional grade scorecards,
server-side metadata/title, dynamic icons, favicon rewrite, and instant
landing render without session-loading flash; split authenticated AppShell
from unauthenticated corporate chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-06-11 01:27:05 -07:00
parent 73a7b9fc15
commit 456cc613b7
39 changed files with 2641 additions and 305 deletions

View File

@@ -68,6 +68,10 @@ const nextConfig = {
source: '/api/crossplane/:path*',
destination: `${process.env.NEXT_PUBLIC_CROSSPLANE_API || 'http://localhost:8080'}/:path*`,
},
{
source: '/favicon.ico',
destination: '/icon',
},
];
},
};

View File

@@ -0,0 +1,2 @@
allowBuilds:
unrs-resolver: set this to true or false

View File

@@ -0,0 +1,22 @@
'use client';
import { Inter } from 'next/font/google';
import { AppShell } from '@/components/layout/AppShell';
import { Providers } from './providers';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export function ClientRootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={`${inter.className} min-h-screen bg-gray-950 text-gray-100 antialiased`}>
<Providers>
<AppShell>{children}</AppShell>
</Providers>
</body>
</html>
);
}

View File

@@ -0,0 +1,79 @@
import { notFound } from 'next/navigation';
import { RoleGate } from '@/components/auth/RoleGate';
import { FeaturePreviewPage } from '@/components/preview/FeaturePreviewPage';
const sections = {
organizations: {
eyebrow: 'Client Admin',
title: 'Organization Management',
description:
'Manage client-level organization settings, tenant boundaries, and workspace ownership from one place.',
bullets: [
'Client records are the commercial boundary for contracts, billing, and entitlements.',
'Tenant records remain the identity and security boundary for domains, RBAC, and residency controls.',
'Use this area to review organization structure before deeper billing and compliance workflows.',
],
},
users: {
eyebrow: 'Client Admin',
title: 'User Management',
description:
'Invite users, manage role assignments, and align access with tenant-aware workspace boundaries.',
bullets: [
'Client admins manage who can access which tenant and which workspace areas.',
'Role assignment should stay aligned with Keycloak claims and portal session context.',
'Auditability and least-privilege access are part of the supported model.',
],
},
billing: {
eyebrow: 'Client Admin',
title: 'Billing & Subscriptions',
description:
'Review subscriptions, invoices, and the commercial state attached to the client boundary.',
bullets: [
'Billing is moving from tenant-scoped ownership to client and subscription-scoped ownership.',
'Request-only and operator-provisioned offers should never appear as instant self-service purchases.',
'Invoice and payment views will align with the canonical subscription model.',
],
},
compliance: {
eyebrow: 'Client Admin',
title: 'Compliance & Reporting',
description:
'Track audit posture, policy acknowledgements, and exportable reporting for regulated workloads.',
bullets: [
'Compliance state should align with tenant security controls and subscription obligations.',
'Exports and evidence should be client-aware and tenant-aware.',
'Residency and sovereignty decisions belong here, not in ad hoc support flows.',
],
},
} as const;
export default function AdminSectionPage({
params,
}: {
params: { section: keyof typeof sections };
}) {
const section = sections[params.section];
if (!section) notFound();
return (
<RoleGate
allowedRoles={['admin', 'tenant-admin', 'ADMIN', 'TENANT_ADMIN']}
callbackUrl={`/admin/${params.section}`}
badge="Admin"
title="Admin access required"
subtitle="Sign in with a client-admin or internal-admin account to open this workspace."
>
<FeaturePreviewPage
eyebrow={section.eyebrow}
title={section.title}
description={section.description}
bullets={section.bullets}
primaryAction={{ href: '/admin', label: 'Back to Admin Overview' }}
secondaryAction={{ href: '/help/support', label: 'Contact Platform Support' }}
/>
</RoleGate>
);
}

View File

@@ -3,40 +3,12 @@
import { Building2, Users, CreditCard, Shield, ArrowRight } from 'lucide-react';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { RoleGate } from '@/components/auth/RoleGate';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
export default function AdminPortalPage() {
const { data: session, status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Admin Portal</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
const { data: session } = useSession();
const adminSections = [
{
@@ -70,48 +42,55 @@ export default function AdminPortalPage() {
];
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Customer / Tenant Admin Portal</h1>
<p className="text-gray-400">Manage your organization, users, billing, and compliance</p>
{session?.user?.email && (
<p className="text-sm text-gray-500 mt-2">Signed in as {session.user.email}</p>
)}
</div>
<RoleGate
allowedRoles={['admin', 'tenant-admin', 'ADMIN', 'TENANT_ADMIN']}
callbackUrl="/admin"
badge="Admin"
title="Welcome to Admin Portal"
subtitle="Sign in with a client-admin or internal-admin account to continue."
>
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Customer / Tenant Admin Portal</h1>
<p className="text-gray-400">Manage your organization, users, billing, and compliance</p>
{session?.user?.email && (
<p className="text-sm text-gray-500 mt-2">Signed in as {session.user.email}</p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{adminSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Manage <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{adminSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Manage <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
</div>
</div>
</div>
</RoleGate>
);
}

View File

@@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';
import { itReadApiBaseUrl, itReadApiKey, requireItOpsSession } from '@/app/api/it/_auth';
export async function GET() {
const session = await requireItOpsSession();
if (!session) {
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const base = itReadApiBaseUrl();
if (!base) {
return NextResponse.json(
{ message: 'IT_READ_API_URL is not configured on the portal server' },
{ status: 503 },
);
}
const url = `${base.replace(/\/$/, '')}/v1/portmap/joined`;
const headers: Record<string, string> = { Accept: 'application/json' };
const key = itReadApiKey();
if (key) {
headers['X-API-Key'] = key;
}
const res = await fetch(url, { headers, cache: 'no-store' });
const text = await res.text();
if (!res.ok) {
return NextResponse.json(
{ message: 'Upstream portmap fetch failed', status: res.status, body: text.slice(0, 2000) },
{ status: 502 },
);
}
try {
const data = JSON.parse(text) as unknown;
return NextResponse.json(data);
} catch {
return NextResponse.json({ message: 'Invalid JSON from IT read API' }, { status: 502 });
}
}

View File

@@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';
import { itReadApiBaseUrl, itReadApiKey, requireItOpsSession } from '@/app/api/it/_auth';
export async function GET() {
const session = await requireItOpsSession();
if (!session) {
return NextResponse.json({ message: 'Forbidden' }, { status: 403 });
}
const base = itReadApiBaseUrl();
if (!base) {
return NextResponse.json(
{ message: 'IT_READ_API_URL is not configured on the portal server' },
{ status: 503 },
);
}
const url = `${base.replace(/\/$/, '')}/v1/summary`;
const headers: Record<string, string> = { Accept: 'application/json' };
const key = itReadApiKey();
if (key) {
headers['X-API-Key'] = key;
}
const res = await fetch(url, { headers, cache: 'no-store' });
const text = await res.text();
if (!res.ok) {
return NextResponse.json(
{ message: 'Upstream summary fetch failed', status: res.status, body: text.slice(0, 2000) },
{ status: 502 },
);
}
try {
const data = JSON.parse(text) as unknown;
return NextResponse.json(data);
} catch {
return NextResponse.json({ message: 'Invalid JSON from IT read API' }, { status: 502 });
}
}

View File

@@ -0,0 +1,80 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
type JsonRecord = Record<string, unknown>;
type SessionClaims = {
roles?: string[];
clientId?: string;
tenantId?: string;
subscriptionId?: string;
} | null;
function actorFromSession(session: SessionClaims): string {
const roles = Array.isArray(session?.roles) ? session.roles : [];
if (roles.includes('partner')) return 'partner';
if (roles.includes('tenant-admin') || roles.includes('TENANT_ADMIN')) return 'invited_client_admin';
if (roles.includes('admin') || roles.includes('ADMIN')) return 'internal_operator';
return 'invited_tenant_user';
}
export async function GET() {
const session = (await getServerSession(authOptions)) as SessionClaims;
if (!session) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({
status: 'ready',
actor: actorFromSession(session),
workspace: {
clientId: session.clientId || null,
tenantId: session.tenantId || null,
subscriptionId: session.subscriptionId || null,
},
});
}
export async function POST(request: Request) {
const session = (await getServerSession(authOptions)) as SessionClaims;
if (!session) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
const body = (await request.json().catch(() => ({}))) as JsonRecord;
const profile = typeof body.profile === 'object' && body.profile ? (body.profile as JsonRecord) : {};
const preferences =
typeof body.preferences === 'object' && body.preferences ? (body.preferences as JsonRecord) : {};
const response = NextResponse.json({
status: 'accepted',
actor: actorFromSession(session),
nextUrl: '/dashboard',
workspace: {
clientId: session.clientId || null,
tenantId: session.tenantId || null,
subscriptionId: session.subscriptionId || null,
},
profile,
preferences,
});
response.cookies.set('portal_onboarding_complete', '1', {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
path: '/',
maxAge: 60 * 60 * 24 * 365,
});
response.cookies.set('portal_onboarding_actor', actorFromSession(session), {
httpOnly: true,
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
path: '/',
maxAge: 60 * 60 * 24 * 365,
});
return response;
}

View File

@@ -0,0 +1,28 @@
import { ImageResponse } from 'next/og';
export const size = { width: 180, height: 180 };
export const contentType = 'image/png';
export default function AppleIcon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #f97316 0%, #fbbf24 100%)',
borderRadius: 36,
color: '#0a0a0a',
fontSize: 96,
fontWeight: 700,
}}
>
S
</div>
),
{ ...size }
);
}

View File

@@ -0,0 +1,19 @@
import { FeaturePreviewPage } from '@/components/preview/FeaturePreviewPage';
export default function BillingPage() {
return (
<FeaturePreviewPage
eyebrow="Workspace"
title="Billing"
description="Track the subscription and invoice state attached to the current client workspace."
bullets={[
'Billing ownership is moving to the client and subscription boundary.',
'Tenant-level cost views remain useful for reporting but are not the commercial authority.',
'Use admin billing for broader commercial controls and this view for workspace-level visibility.',
]}
primaryAction={{ href: '/admin/billing', label: 'Open Billing Administration' }}
secondaryAction={{ href: '/help/support', label: 'Contact Billing Support' }}
status="active"
/>
);
}

View File

@@ -2,28 +2,15 @@
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
/* Portal is dark-first; avoid prefers-color-scheme body rules that fight Tailwind and
leave bare <a> tags as low-contrast browser default blue on black. */
@layer base {
body {
@apply bg-gray-950 text-gray-100;
}
a {
@apply text-orange-300 underline-offset-2 transition-colors hover:text-orange-200;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@layer utilities {

View File

@@ -0,0 +1,47 @@
import { notFound } from 'next/navigation';
import { FeaturePreviewPage } from '@/components/preview/FeaturePreviewPage';
const sections = {
docs: {
title: 'Documentation',
description:
'Reference architecture, onboarding guidance, and operating notes for the authenticated client workspace.',
bullets: [
'Portal docs should reflect the canonical client and tenant model.',
'Native and partner offers should be documented with consistent commercial-language rules.',
'Client-facing docs should not inherit operator-only assumptions.',
],
},
support: {
title: 'Support',
description:
'Client support entrypoint for workspace, billing, onboarding, and fulfillment issues.',
bullets: [
'Support routing should respect support owner and fulfillment mode.',
'Request-only programs should route into review queues, not instant activation flows.',
'Client-facing support differs from operator-only systems administration.',
],
},
} as const;
export default function HelpSectionPage({
params,
}: {
params: { section: keyof typeof sections };
}) {
const section = sections[params.section];
if (!section) notFound();
return (
<FeaturePreviewPage
eyebrow="Help & Support"
title={section.title}
description={section.description}
bullets={section.bullets}
primaryAction={{ href: '/', label: 'Return to Dashboard' }}
secondaryAction={{ href: '/settings', label: 'Workspace Settings' }}
status="active"
/>
);
}

28
portal/src/app/icon.tsx Normal file
View File

@@ -0,0 +1,28 @@
import { ImageResponse } from 'next/og';
export const size = { width: 32, height: 32 };
export const contentType = 'image/png';
export default function Icon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #f97316 0%, #fbbf24 100%)',
borderRadius: 8,
color: '#0a0a0a',
fontSize: 20,
fontWeight: 700,
}}
>
S
</div>
),
{ ...size }
);
}

View File

@@ -14,9 +14,39 @@ type DriftShape = {
guest_lan_ips_not_in_declared_sources?: string[];
declared_lan11_ips_not_on_live_guests?: string[];
vmid_ip_mismatch_live_vs_all_vmids_doc?: Array<{ vmid: string; live_ip: string; all_vmids_doc_ip: string }>;
vmids_in_all_vmids_doc_not_on_cluster?: string[];
vmids_on_cluster_not_in_all_vmids_table?: {
count?: number;
sample_vmids?: string[];
note?: string;
};
notes?: string[];
};
type SummaryShape = {
envelope_at?: string;
declared_git_head?: string | null;
live_collected_at?: string;
drift_collected_at?: string;
guest_count?: number | null;
duplicate_ip_bucket_count?: number;
seed_unreachable?: boolean;
drift_notes?: string[];
vmids_in_all_vmids_doc_not_on_cluster?: string[];
vmids_on_cluster_not_in_all_vmids_table?: DriftShape['vmids_on_cluster_not_in_all_vmids_table'];
artifacts?: {
live_inventory?: { mtime_utc?: string; exists?: boolean };
drift?: { mtime_utc?: string; exists?: boolean };
};
};
type PortmapShape = {
implementation?: string;
stale?: boolean;
note?: string;
rows?: unknown[];
};
function hoursSinceIso(iso: string | undefined): number | null {
if (!iso) return null;
const t = Date.parse(iso);
@@ -26,7 +56,10 @@ function hoursSinceIso(iso: string | undefined): number | null {
export default function ItOpsPage() {
const [drift, setDrift] = useState<DriftShape | null>(null);
const [summary, setSummary] = useState<SummaryShape | null>(null);
const [portmap, setPortmap] = useState<PortmapShape | null>(null);
const [err, setErr] = useState<string | null>(null);
const [refreshWarn, setRefreshWarn] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
@@ -34,17 +67,31 @@ export default function ItOpsPage() {
setLoading(true);
setErr(null);
try {
const r = await fetch('/api/it/drift', { cache: 'no-store' });
const j = (await r.json()) as DriftShape & { message?: string };
if (!r.ok) {
setErr(j.message || `HTTP ${r.status}`);
const [driftRes, summaryRes, portmapRes] = await Promise.all([
fetch('/api/it/drift', { cache: 'no-store' }),
fetch('/api/it/summary', { cache: 'no-store' }),
fetch('/api/it/portmap', { cache: 'no-store' }),
]);
const dj = (await driftRes.json()) as DriftShape & { message?: string };
if (!driftRes.ok) {
setErr(dj.message || `HTTP ${driftRes.status}`);
setDrift(null);
setSummary(null);
setPortmap(null);
setRefreshWarn(null);
return;
}
setDrift(j);
setDrift(dj);
const sj = (await summaryRes.json()) as SummaryShape & { message?: string };
setSummary(summaryRes.ok ? sj : null);
const pj = (await portmapRes.json()) as PortmapShape & { message?: string };
setPortmap(portmapRes.ok ? pj : null);
} catch (e) {
setErr(e instanceof Error ? e.message : 'Request failed');
setDrift(null);
setSummary(null);
setPortmap(null);
setRefreshWarn(null);
} finally {
setLoading(false);
}
@@ -60,14 +107,24 @@ export default function ItOpsPage() {
const onRefresh = async () => {
setRefreshing(true);
setErr(null);
setRefreshWarn(null);
try {
const r = await fetch('/api/it/refresh', { method: 'POST' });
const j = (await r.json()) as { message?: string };
const j = (await r.json()) as {
message?: string;
drift_exit_code?: number;
duplicate_guest_ip_conflict?: boolean;
};
if (!r.ok) {
setErr(j.message || `Refresh HTTP ${r.status}`);
setRefreshing(false);
return;
}
if (j.drift_exit_code === 2 || j.duplicate_guest_ip_conflict) {
setRefreshWarn(
'Refresh completed but the exporter reported duplicate LAN IPs on different guest names (drift exit 2). Resolve conflicts on the cluster.',
);
}
await load();
} catch (e) {
setErr(e instanceof Error ? e.message : 'Refresh failed');
@@ -80,6 +137,8 @@ export default function ItOpsPage() {
const sameNameDupCount = drift?.same_name_duplicate_ip_guests
? Object.keys(drift.same_name_duplicate_ip_guests).length
: 0;
const docMissingVmidCount = drift?.vmids_in_all_vmids_doc_not_on_cluster?.length ?? 0;
const liveExtraTable = drift?.vmids_on_cluster_not_in_all_vmids_table;
return (
<RoleGate
@@ -114,6 +173,12 @@ export default function ItOpsPage() {
</div>
)}
{refreshWarn && (
<div className="mb-6 rounded-lg border border-amber-700 bg-amber-950/30 px-4 py-3 text-sm text-amber-100">
{refreshWarn}
</div>
)}
{loading && <p className="text-gray-400">Loading drift</p>}
{!loading && drift && (
@@ -161,6 +226,128 @@ export default function ItOpsPage() {
</CardContent>
</Card>
{summary && (
<Card className="bg-gray-800 border-gray-700 md:col-span-2">
<CardHeader>
<CardTitle className="text-white">API envelope</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-gray-300">
{summary.seed_unreachable && (
<p className="text-amber-400">
Seed host unreachable from exporter drift may be stubbed. Run export on LAN.
</p>
)}
<p>
<span className="text-gray-500">live_inventory.json mtime:</span>{' '}
{summary.artifacts?.live_inventory?.mtime_utc ?? '—'}
</p>
<p>
<span className="text-gray-500">drift.json mtime:</span>{' '}
{summary.artifacts?.drift?.mtime_utc ?? '—'}
</p>
<p>
<span className="text-gray-500">envelope_at:</span> {summary.envelope_at ?? '—'}
</p>
<p>
<span className="text-gray-500">declared_git_head (repo on API host):</span>{' '}
{summary.declared_git_head ?? '—'}
</p>
</CardContent>
</Card>
)}
<Card className="bg-gray-800 border-gray-700 md:col-span-2">
<CardHeader>
<CardTitle className="text-white">VLAN plan</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-gray-300">
<p>
<span className="text-gray-500">Current:</span> flat{' '}
<code className="text-gray-200">192.168.11.0/24</code> (VLAN 11).
</p>
<p>
<span className="text-gray-500">Target:</span> segmented VLANs (110112, 120, 160, 200203) in
repo doc <code className="text-gray-200">docs/11-references/NETWORK_CONFIGURATION_MASTER.md</code>{' '}
(section VLAN Configuration).
</p>
<p>
Operator runbook:{' '}
<code className="text-gray-200">docs/03-deployment/VLAN_FLAT_11_TO_SEGMENTED_RUNBOOK.md</code> and{' '}
<code className="text-gray-200">scripts/it-ops/vlan-segmentation-ordered-checklist.sh</code>.
</p>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700 md:col-span-2">
<CardHeader>
<CardTitle className="text-white">Hardware &amp; cluster reports</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-gray-300">
<p>
Poll script (LAN):{' '}
<code className="text-gray-200">bash scripts/verify/poll-proxmox-cluster-hardware.sh</code>
</p>
<p>
Artifacts under <code className="text-gray-200">reports/status/</code>:{' '}
<code className="text-gray-200">hardware_poll_*.txt</code>,{' '}
<code className="text-gray-200">hardware_and_connected_inventory_*.md</code>.
</p>
<p>
Edge / discovery IPs:{' '}
<code className="text-gray-200">docs/04-configuration/IT_OPS_EDGE_DISCOVERY_IPS.md</code>
</p>
</CardContent>
</Card>
{drift && (docMissingVmidCount > 0 || (liveExtraTable?.count ?? 0) > 0) && (
<Card className="bg-gray-800 border-gray-700 md:col-span-2">
<CardHeader>
<CardTitle className="text-white">VMID coverage (live vs ALL_VMIDS tables)</CardTitle>
</CardHeader>
<CardContent className="space-y-3 text-sm text-gray-300">
<p>
VMIDs in ALL_VMIDS doc but not on cluster:{' '}
<span className="text-amber-300">{docMissingVmidCount}</span>
{docMissingVmidCount > 0 && (
<pre className="mt-2 text-xs text-gray-400 overflow-x-auto max-h-32">
{JSON.stringify(drift.vmids_in_all_vmids_doc_not_on_cluster, null, 2)}
</pre>
)}
</p>
<p>
Guests on cluster not listed in ALL_VMIDS pipe tables:{' '}
<span className="text-gray-200">{liveExtraTable?.count ?? 0}</span>
{liveExtraTable?.note && (
<span className="block text-gray-500 mt-1">{liveExtraTable.note}</span>
)}
</p>
{(liveExtraTable?.sample_vmids?.length ?? 0) > 0 && (
<pre className="text-xs text-gray-400 overflow-x-auto max-h-32">
{JSON.stringify(liveExtraTable?.sample_vmids, null, 2)}
</pre>
)}
</CardContent>
</Card>
)}
{portmap && (
<Card className="bg-gray-800 border-gray-700 md:col-span-2">
<CardHeader>
<CardTitle className="text-white">Port map (joined view)</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-gray-300">
<p>
Status:{' '}
<span className={portmap.stale ? 'text-amber-400' : 'text-green-400'}>
{portmap.implementation ?? 'unknown'}
{portmap.stale ? ' (stale / stub)' : ''}
</span>
</p>
{portmap.note && <p className="text-gray-400">{portmap.note}</p>}
</CardContent>
</Card>
)}
{dupCount > 0 && (
<Card className="bg-gray-800 border-red-900 md:col-span-2">
<CardHeader>

View File

@@ -1,46 +1,34 @@
'use client'
import type { Metadata } from 'next';
import { Inter } from 'next/font/google'
import './globals.css'
import { SessionProvider } from 'next-auth/react'
import { ClientRootLayout } from './ClientRootLayout';
import { KeyboardShortcutsProvider } from '@/components/KeyboardShortcutsProvider'
import { MobileNavigation } from '@/components/layout/MobileNavigation'
import { PortalBreadcrumbs } from '@/components/layout/PortalBreadcrumbs'
import { PortalHeader } from '@/components/layout/PortalHeader'
import { PortalSidebar } from '@/components/layout/PortalSidebar'
export const metadata: Metadata = {
metadataBase: new URL('https://sankofa.nexus'),
title: {
default: 'Sankofa — Sovereign Technologies',
template: '%s | Sankofa',
},
description:
'Sankofa delivers sovereign cloud, identity, financial rails, and credential infrastructure — the complete ecosystem for institutions that cannot depend on hyperscaler public cloud.',
openGraph: {
type: 'website',
url: 'https://sankofa.nexus',
siteName: 'Sankofa',
title: 'Sankofa — Sovereign Technologies',
description:
'Build on infrastructure you own and control. Sovereign cloud, identity, ledger, and Chain 138 settlement rails.',
},
twitter: {
card: 'summary',
title: 'Sankofa — Sovereign Technologies',
description:
'Sovereign cloud, identity, financial rails, and credential infrastructure for institutions.',
},
alternates: {
canonical: 'https://sankofa.nexus',
},
};
import { Providers } from './providers'
const inter = Inter({ subsets: ['latin'] })
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<SessionProvider>
<Providers>
<KeyboardShortcutsProvider>
<div className="flex min-h-screen flex-col">
<PortalHeader />
<PortalBreadcrumbs />
<div className="flex flex-1">
<PortalSidebar />
<main className="flex-1 ml-0 md:ml-64">
{children}
</main>
</div>
<MobileNavigation />
</div>
</KeyboardShortcutsProvider>
</Providers>
</SessionProvider>
</body>
</html>
)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <ClientRootLayout>{children}</ClientRootLayout>;
}

View File

@@ -27,11 +27,37 @@ const onboardingSteps = [
];
export default function OnboardingPage() {
const handleComplete = () => {
// Mark onboarding as complete
localStorage.setItem('onboarding-complete', 'true');
const handleComplete = async () => {
const profile =
typeof window !== 'undefined'
? {
fullName: (
document.getElementById('onboarding-full-name') as HTMLInputElement | null
)?.value,
role: (document.getElementById('onboarding-role') as HTMLInputElement | null)?.value,
}
: {};
const preferences =
typeof window !== 'undefined'
? {
theme: (document.getElementById('onboarding-theme') as HTMLSelectElement | null)?.value,
}
: {};
const response = await fetch('/api/onboarding', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ profile, preferences }),
});
if (!response.ok) {
const payload = await response.json().catch(() => ({ message: 'Failed to complete onboarding.' }));
throw new Error(typeof payload.message === 'string' ? payload.message : 'Failed to complete onboarding.');
}
};
return <OnboardingWizard steps={onboardingSteps} onComplete={handleComplete} />;
}

View File

@@ -1,50 +1,17 @@
'use client';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { CorporateLandingPage } from '@/components/corporate/CorporateLandingPage';
import Dashboard from '@/components/Dashboard';
export default function Home() {
const { status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 px-4">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-600 border-t-orange-500" />
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
if (status === 'authenticated') {
return <Dashboard />;
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-950 px-4 py-12">
<div className="w-full max-w-md rounded-2xl border border-gray-800 bg-gray-900/80 p-8 shadow-xl shadow-black/40 backdrop-blur-sm">
<p className="mb-1 text-center text-sm font-medium uppercase tracking-wide text-orange-400">
Sankofa Phoenix
</p>
<h1 className="mb-2 text-center text-2xl font-bold text-white">Welcome to Portal</h1>
<p className="mb-8 text-center text-gray-400">Sign in to open Nexus Console.</p>
<button
type="button"
onClick={() => signIn(undefined, { callbackUrl: '/' })}
className="w-full rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-6 py-3 font-semibold text-gray-950 shadow-lg transition hover:from-orange-400 hover:to-amber-400 focus:outline-none focus:ring-2 focus:ring-orange-400 focus:ring-offset-2 focus:ring-offset-gray-900"
>
Sign In
</button>
{process.env.NODE_ENV === 'development' && (
<p className="mt-6 text-center text-xs text-gray-500">
Development: use any email/password with your dev IdP configuration.
</p>
)}
</div>
</div>
);
}
return <Dashboard />;
// Public marketing site: avoid a blank "Loading..." flash for unauthenticated visitors.
return <CorporateLandingPage />;
}

View File

@@ -0,0 +1,76 @@
import { notFound } from 'next/navigation';
import { RoleGate } from '@/components/auth/RoleGate';
import { FeaturePreviewPage } from '@/components/preview/FeaturePreviewPage';
const sections = {
deals: {
title: 'Co-Sell Deals',
description:
'Manage partner-sourced opportunities and keep support ownership clear between Sankofa and partner teams.',
bullets: [
'Deal registration should identify whether the offer is native or partner-led.',
'Commercial models like IRU and SaaS belong on the offer, not in the marketplace taxonomy.',
'Pipeline updates should stay visible to both partner and internal teams.',
],
},
onboarding: {
title: 'Technical Onboarding',
description:
'Track partner enablement, architecture review, and readiness steps before listing or co-selling.',
bullets: [
'Technical onboarding should align with fulfillment mode and support boundaries.',
'Partners need a clear path for request-only and operator-provisioned programs.',
'Documentation and enablement materials are part of this workspace, not ad hoc email threads.',
],
},
solutions: {
title: 'Solution Registration',
description:
'Register partner solutions for downstream program listing and lifecycle review.',
bullets: [
'Listings should declare support owner, fulfillment mode, and commercial model.',
'Submission does not imply instant publication or instant provisioning.',
'Partner solutions should hand off cleanly into downstream program apps when required.',
],
},
resources: {
title: 'Partner Resources',
description:
'Centralize partner-facing materials, enablement assets, and shared program guidance.',
bullets: [
'Keep marketplace copy, support expectations, and onboarding assets aligned.',
'Differentiate native platform material from partner-program material.',
'Use this space as the canonical partner enablement boundary.',
],
},
} as const;
export default function PartnerSectionPage({
params,
}: {
params: { section: keyof typeof sections };
}) {
const section = sections[params.section];
if (!section) notFound();
return (
<RoleGate
allowedRoles={['partner', 'admin', 'ADMIN']}
callbackUrl={`/partner/${params.section}`}
badge="Partner"
title="Partner access required"
subtitle="Sign in with a partner or internal-admin account to open this workspace."
>
<FeaturePreviewPage
eyebrow="Partner Workspace"
title={section.title}
description={section.description}
bullets={section.bullets}
primaryAction={{ href: '/partner', label: 'Back to Partner Overview' }}
secondaryAction={{ href: '/help/support', label: 'Contact Partner Support' }}
status="request-only"
/>
</RoleGate>
);
}

View File

@@ -2,42 +2,11 @@
import { Handshake, TrendingUp, BookOpen, Package, ArrowRight } from 'lucide-react';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
import { signIn } from 'next-auth/react';
import { RoleGate } from '@/components/auth/RoleGate';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
export default function PartnerPortalPage() {
const { status } = useSession();
if (status === 'loading') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-400">Loading...</p>
</div>
</div>
);
}
if (status === 'unauthenticated') {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-900">
<div className="text-center max-w-md mx-auto p-8">
<h1 className="text-2xl font-bold text-white mb-4">Welcome to Partner Portal</h1>
<p className="text-gray-400 mb-6">Please sign in to continue</p>
<button
onClick={() => signIn()}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
Sign In
</button>
</div>
</div>
);
}
const partnerSections = [
{
title: 'Co-Sell Deal Management',
@@ -70,45 +39,52 @@ export default function PartnerPortalPage() {
];
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Partner Portal</h1>
<p className="text-gray-400">Co-sell deals, technical onboarding, and solution registration</p>
</div>
<RoleGate
allowedRoles={['partner', 'admin', 'ADMIN']}
callbackUrl="/partner"
badge="Partner"
title="Welcome to Partner Portal"
subtitle="Sign in with a partner or internal-admin account to continue."
>
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Partner Portal</h1>
<p className="text-gray-400">Co-sell deals, technical onboarding, and solution registration</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{partnerSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Access <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{partnerSections.map((section) => {
const Icon = section.icon;
return (
<Card key={section.href} className="bg-gray-800 border-gray-700 hover:border-orange-500 transition-colors">
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-6 w-6 text-orange-500" />
<CardTitle className="text-white">{section.title}</CardTitle>
</div>
<p className="text-sm text-gray-400">{section.description}</p>
</CardHeader>
<CardContent>
<ul className="space-y-2 mb-4">
{section.features.map((feature) => (
<li key={feature} className="flex items-center gap-2 text-sm text-gray-300">
<span className="text-orange-500"></span>
<span>{feature}</span>
</li>
))}
</ul>
<Link
href={section.href}
className="inline-flex items-center gap-2 text-sm text-orange-500 hover:text-orange-400 transition-colors"
>
Access <ArrowRight className="h-4 w-4" />
</Link>
</CardContent>
</Card>
);
})}
</div>
</div>
</div>
</RoleGate>
);
}

View File

@@ -0,0 +1,19 @@
import { FeaturePreviewPage } from '@/components/preview/FeaturePreviewPage';
export default function SecurityPage() {
return (
<FeaturePreviewPage
eyebrow="Workspace"
title="Security"
description="Review tenant-scoped security posture, identity boundaries, and access expectations."
bullets={[
'Tenant identity and RBAC stay separate from commercial billing boundaries.',
'Client-facing admin and operator-facing systems administration remain intentionally distinct.',
'Use this space for workspace security posture, not raw platform operator controls.',
]}
primaryAction={{ href: '/settings/2fa', label: 'Review MFA Settings' }}
secondaryAction={{ href: '/admin/compliance', label: 'Open Compliance' }}
status="active"
/>
);
}

View File

@@ -0,0 +1,19 @@
import { FeaturePreviewPage } from '@/components/preview/FeaturePreviewPage';
export default function UsersPage() {
return (
<FeaturePreviewPage
eyebrow="Workspace"
title="Users & Access"
description="Review who can access this workspace and how client, tenant, and role boundaries are applied."
bullets={[
'Users inherit access through client-aware and tenant-aware role assignments.',
'This surface complements the deeper organization controls under client admin.',
'Use this workspace view for day-to-day access review and tenant-scoped membership context.',
]}
primaryAction={{ href: '/admin/users', label: 'Open Admin User Management' }}
secondaryAction={{ href: '/security', label: 'Review Security Controls' }}
status="active"
/>
);
}

View File

@@ -0,0 +1,61 @@
'use client';
import { signIn } from 'next-auth/react';
import { useCallback, useState, type ReactNode } from 'react';
import { CloudflareTurnstile } from '@/components/security/CloudflareTurnstile';
const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY;
export interface PortalSignInCardProps {
badge?: string;
title: string;
subtitle: string;
callbackUrl?: string;
/** Extra hint under the sign-in button (e.g. dev IdP note) */
footer?: ReactNode;
}
export function PortalSignInCard({
badge = 'Sankofa Phoenix',
title,
subtitle,
callbackUrl = '/',
footer,
}: PortalSignInCardProps) {
const [turnstileToken, setTurnstileToken] = useState<string | null>(() =>
TURNSTILE_SITE_KEY ? null : ''
);
const onTurnstileToken = useCallback((token: string | null) => {
setTurnstileToken(token);
}, []);
const canSignIn = !TURNSTILE_SITE_KEY || Boolean(turnstileToken);
return (
<div className="w-full max-w-md rounded-2xl border border-gray-800 bg-gray-900/80 p-8 shadow-xl shadow-black/40 backdrop-blur-sm">
<p className="mb-1 text-center text-sm font-medium uppercase tracking-wide text-orange-400">{badge}</p>
<h1 className="mb-2 text-center text-2xl font-bold text-white">{title}</h1>
<p className="mb-8 text-center text-gray-400">{subtitle}</p>
{TURNSTILE_SITE_KEY ? (
<div className="mb-6">
<p className="mb-2 text-center text-sm text-gray-500">Verification</p>
<CloudflareTurnstile siteKey={TURNSTILE_SITE_KEY} onToken={onTurnstileToken} />
</div>
) : null}
<button
type="button"
disabled={!canSignIn}
onClick={() => signIn(undefined, { callbackUrl })}
className="w-full rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-6 py-3 font-semibold text-gray-950 shadow-lg transition hover:from-orange-400 hover:to-amber-400 focus:outline-none focus:ring-2 focus:ring-orange-400 focus:ring-offset-2 focus:ring-offset-gray-900 disabled:cursor-not-allowed disabled:opacity-50"
>
Sign In
</button>
{footer}
</div>
);
}

View File

@@ -0,0 +1,94 @@
import Link from 'next/link';
import { ECOSYSTEM_SIGN_IN_PATH } from '@/lib/corporate-site-data';
function isExternalHref(href: string) {
return href.startsWith('http://') || href.startsWith('https://');
}
const footerColumns = [
{
title: 'Products',
links: [
{ label: 'Phoenix Cloud', href: 'https://phoenix.sankofa.nexus' },
{ label: 'Complete Credential', href: 'https://cc.sankofa.nexus' },
{ label: 'Sankofa Studio', href: 'https://studio.sankofa.nexus/studio/' },
{ label: 'Client Portal', href: 'https://portal.sankofa.nexus' },
],
},
{
title: 'Platform',
links: [
{ label: 'Identity (Keycloak)', href: 'https://keycloak.sankofa.nexus' },
{ label: 'Blockchain Explorer', href: 'https://explorer.d-bis.org' },
{ label: 'Documentation', href: 'https://docs.d-bis.org' },
{ label: 'Marketplace', href: 'https://portal.sankofa.nexus' },
],
},
{
title: 'Company',
links: [
{ label: 'About Sankofa', href: '#resources' },
{ label: 'Institutional registry', href: 'https://d-bis.org/cb/dbis' },
{ label: 'Partner program', href: '/partner' },
{ label: 'Contact', href: 'https://portal.sankofa.nexus' },
],
},
{
title: 'Access',
links: [
{ label: 'Sign in to ecosystem', href: ECOSYSTEM_SIGN_IN_PATH },
{ label: 'Phoenix console', href: 'https://phoenix.sankofa.nexus' },
{ label: 'Operator dashboard', href: 'https://dash.sankofa.nexus' },
{ label: 'Admin console', href: 'https://admin.sankofa.nexus' },
],
},
];
export function CorporateFooter() {
return (
<footer className="border-t border-gray-800 bg-gray-950">
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-8 md:grid-cols-4 lg:gap-12">
{footerColumns.map((column) => (
<div key={column.title}>
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
{column.title}
</h3>
<ul className="space-y-2.5">
{column.links.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-sm text-gray-400 no-underline transition-colors hover:text-orange-300"
{...(isExternalHref(link.href)
? { target: '_blank', rel: 'noopener noreferrer' }
: {})}
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-12 flex flex-col items-start justify-between gap-4 border-t border-gray-800 pt-8 sm:flex-row sm:items-center">
<div className="flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-orange-500 to-amber-400 text-xs font-bold text-gray-950">
S
</span>
<div>
<p className="text-sm font-medium text-white">Sankofa Sovereign Technologies</p>
<p className="text-xs text-gray-500">Remember · Retrieve · Restore · Rise</p>
</div>
</div>
<p className="text-xs text-gray-500">
© {new Date().getFullYear()} Sankofa Ltd. All rights reserved.
</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import { ChevronDown, LogIn, Menu, X } from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { corporateNav, ECOSYSTEM_SIGN_IN_PATH } from '@/lib/corporate-site-data';
import { cn } from '@/lib/utils';
export function CorporateHeader() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-gray-800/80 bg-gray-950/95 backdrop-blur-md">
<div className="mx-auto flex h-16 max-w-7xl items-center gap-4 px-4 sm:px-6 lg:px-8">
{/* Top-left: brand + ecosystem sign-in (AWS / Microsoft pattern) */}
<div className="flex shrink-0 items-center gap-3 sm:gap-4">
<Link href="/" className="group flex items-center gap-2 no-underline">
<span
className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 to-amber-400 text-sm font-bold text-gray-950 shadow-lg shadow-orange-500/20"
aria-hidden
>
S
</span>
<span className="hidden bg-gradient-to-r from-orange-300 to-amber-200 bg-clip-text text-lg font-semibold tracking-tight text-transparent sm:inline">
Sankofa
</span>
</Link>
<span className="hidden h-5 w-px bg-gray-700 sm:block" aria-hidden />
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium text-orange-300 no-underline transition-colors hover:bg-orange-500/10 hover:text-orange-200"
>
<LogIn className="h-4 w-4" aria-hidden />
<span>Sign in</span>
</Link>
</div>
{/* Desktop nav */}
<nav className="hidden flex-1 items-center justify-center gap-1 lg:flex" aria-label="Primary">
{corporateNav.map((item) => (
<Link
key={item.href}
href={item.href}
className="rounded-md px-3 py-2 text-sm font-medium text-gray-300 no-underline transition-colors hover:bg-gray-800/60 hover:text-white"
>
{item.label}
</Link>
))}
</nav>
{/* Desktop CTAs */}
<div className="ml-auto hidden items-center gap-2 lg:flex">
<Link
href="https://portal.sankofa.nexus"
className="rounded-md px-3 py-2 text-sm font-medium text-gray-300 no-underline transition-colors hover:text-white"
>
Contact sales
</Link>
<Link
href="https://phoenix.sankofa.nexus"
className="rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-4 py-2 text-sm font-semibold text-gray-950 no-underline shadow-md shadow-orange-500/25 transition hover:from-orange-400 hover:to-amber-400"
>
Get started
</Link>
</div>
{/* Mobile menu toggle */}
<button
type="button"
className="ml-auto inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-800 hover:text-white lg:hidden"
aria-expanded={mobileOpen}
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
onClick={() => setMobileOpen((open) => !open)}
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
{/* Mobile drawer */}
<div
className={cn(
'overflow-hidden border-t border-gray-800/80 bg-gray-950 lg:hidden',
mobileOpen ? 'max-h-[28rem] opacity-100' : 'max-h-0 opacity-0'
)}
>
<nav className="flex flex-col gap-1 px-4 py-3" aria-label="Mobile primary">
{corporateNav.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center justify-between rounded-md px-3 py-2.5 text-sm font-medium text-gray-200 no-underline hover:bg-gray-800"
onClick={() => setMobileOpen(false)}
>
{item.label}
<ChevronDown className="-rotate-90 h-4 w-4 text-gray-500" aria-hidden />
</Link>
))}
<div className="mt-2 flex flex-col gap-2 border-t border-gray-800 pt-3">
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-700 px-4 py-2.5 text-sm font-medium text-white no-underline hover:bg-gray-800"
onClick={() => setMobileOpen(false)}
>
<LogIn className="h-4 w-4" />
Sign in to ecosystem
</Link>
<Link
href="https://phoenix.sankofa.nexus"
className="inline-flex items-center justify-center rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-4 py-2.5 text-sm font-semibold text-gray-950 no-underline"
onClick={() => setMobileOpen(false)}
>
Get started with Phoenix
</Link>
</div>
</nav>
</div>
</header>
);
}

View File

@@ -0,0 +1,270 @@
'use client';
import { ArrowRight, ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { CorporateFooter } from '@/components/corporate/CorporateFooter';
import { CorporateHeader } from '@/components/corporate/CorporateHeader';
import { InstitutionalGradeSection } from '@/components/corporate/InstitutionalGradeSection';
import {
ECOSYSTEM_SIGN_IN_PATH,
philosophySteps,
platformServices,
productDivisions,
solutionVerticals,
} from '@/lib/corporate-site-data';
export function CorporateLandingPage() {
return (
<div className="flex min-h-screen flex-col bg-gray-950 text-gray-100">
<CorporateHeader />
<main className="flex-1">
{/* Hero */}
<section className="relative overflow-hidden border-b border-gray-800/60">
<div
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_60%_at_50%_-20%,rgba(251,146,60,0.18),transparent)]"
aria-hidden
/>
<div className="relative mx-auto max-w-7xl px-4 py-20 sm:px-6 sm:py-28 lg:px-8">
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-orange-400">
Sovereign Technologies
</p>
<h1 className="max-w-4xl text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
Build on infrastructure you{' '}
<span className="bg-gradient-to-r from-orange-300 to-amber-200 bg-clip-text text-transparent">
own and control
</span>
</h1>
<p className="mt-6 max-w-2xl text-lg leading-relaxed text-gray-400">
Sankofa delivers sovereign cloud, identity, financial rails, and credential infrastructure
the complete ecosystem for institutions that cannot depend on hyperscaler public cloud.
</p>
<div className="mt-10 flex flex-wrap items-center gap-4">
<Link
href="https://phoenix.sankofa.nexus"
className="inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-6 py-3 text-sm font-semibold text-gray-950 no-underline shadow-lg shadow-orange-500/25 transition hover:from-orange-400 hover:to-amber-400"
>
Explore Phoenix Cloud
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-900/50 px-6 py-3 text-sm font-semibold text-white no-underline transition hover:border-gray-600 hover:bg-gray-800"
>
Sign in to ecosystem
</Link>
</div>
</div>
</section>
{/* Philosophy */}
<section className="border-b border-gray-800/60 bg-gray-900/30 py-14">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{philosophySteps.map((step, index) => (
<div key={step.verb} className="relative rounded-xl border border-gray-800/80 bg-gray-900/40 p-5">
<span className="text-xs font-medium uppercase tracking-wider text-orange-500/80">
{String(index + 1).padStart(2, '0')}
</span>
<h2 className="mt-2 text-xl font-semibold text-white">{step.verb}</h2>
<p className="mt-2 text-sm leading-relaxed text-gray-400">{step.detail}</p>
</div>
))}
</div>
</div>
</section>
{/* Product divisions */}
<section id="products" className="scroll-mt-20 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Products</h2>
<p className="mt-4 text-lg text-gray-400">
From sovereign cloud to verifiable credentials integrated products under one ecosystem,
modeled for institutional scale.
</p>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
{productDivisions.map((product) => {
const Icon = product.icon;
return (
<article
key={product.id}
className="group flex flex-col rounded-2xl border border-gray-800 bg-gray-900/40 p-6 transition hover:border-orange-500/40 hover:bg-gray-900/70"
>
<div className="mb-4 flex items-start justify-between gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20">
<Icon className="h-5 w-5" aria-hidden />
</div>
{product.external ? (
<ExternalLink className="h-4 w-4 shrink-0 text-gray-600 group-hover:text-orange-400" aria-hidden />
) : null}
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-orange-400/90">
{product.tagline}
</p>
<h3 className="mt-1 text-xl font-semibold text-white">{product.name}</h3>
<p className="mt-3 flex-1 text-sm leading-relaxed text-gray-400">{product.description}</p>
<ul className="mt-4 space-y-1.5">
{product.highlights.map((item) => (
<li key={item} className="flex items-center gap-2 text-sm text-gray-300">
<span className="h-1 w-1 rounded-full bg-orange-500" aria-hidden />
{item}
</li>
))}
</ul>
<Link
href={product.href}
className="mt-6 inline-flex items-center gap-1.5 text-sm font-medium text-orange-300 no-underline hover:text-orange-200"
{...(product.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
>
Learn more
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
</Link>
</article>
);
})}
</div>
</div>
</section>
{/* Platform services */}
<section id="services" className="scroll-mt-20 border-y border-gray-800/60 bg-gray-900/20 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Platform services</h2>
<p className="mt-4 text-lg text-gray-400">
Owned core primitives ledger, identity, wallet, orchestration available through the
Phoenix marketplace without third-party platform lock-in.
</p>
</div>
<div className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{platformServices.map((service) => {
const Icon = service.icon;
return (
<div
key={service.name}
className="rounded-xl border border-gray-800 bg-gray-950/60 p-5 transition hover:border-gray-700"
>
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gray-800 text-orange-400">
<Icon className="h-4 w-4" aria-hidden />
</div>
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">{service.category}</p>
<h3 className="text-base font-semibold text-white">{service.name}</h3>
</div>
</div>
<p className="mt-3 text-sm leading-relaxed text-gray-400">{service.description}</p>
</div>
);
})}
</div>
<div className="mt-10">
<Link
href="https://portal.sankofa.nexus"
className="inline-flex items-center gap-2 text-sm font-medium text-orange-300 no-underline hover:text-orange-200"
>
Browse full marketplace catalog
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
</div>
</div>
</section>
{/* Solutions */}
<section id="solutions" className="scroll-mt-20 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Industry solutions</h2>
<p className="mt-4 text-lg text-gray-400">
Purpose-built stacks for regulated industries public sector, finance, healthcare, and telecom.
</p>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
{solutionVerticals.map((vertical) => {
const Icon = vertical.icon;
return (
<div
key={vertical.name}
className="flex gap-4 rounded-2xl border border-gray-800 bg-gray-900/30 p-6"
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500/20 to-amber-500/10 text-orange-400">
<Icon className="h-6 w-6" aria-hidden />
</div>
<div>
<h3 className="text-lg font-semibold text-white">{vertical.name}</h3>
<p className="mt-2 text-sm leading-relaxed text-gray-400">{vertical.description}</p>
</div>
</div>
);
})}
</div>
</div>
</section>
<InstitutionalGradeSection />
{/* Resources / trust */}
<section id="resources" className="scroll-mt-20 border-t border-gray-800/60 bg-gray-900/30 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-2 lg:items-center">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white">Built for institutional trust</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-400">
Sankofa Phoenix operates under sovereign governance with SOC 2, GDPR, and sector-specific
compliance pathways. Identity, ledger, and credential services are designed for operators
who need auditability without outsourcing control.
</p>
<ul className="mt-6 space-y-3 text-sm text-gray-300">
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Sovereign Keycloak identity no Azure AD dependency
</li>
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Multi-tenant Proxmox orchestration with GitOps
</li>
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Chain 138 DeFi Oracle Meta Mainnet & cross-chain mesh
</li>
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Complete Credential eIDAS-aligned issuance
</li>
</ul>
</div>
<div className="rounded-2xl border border-gray-800 bg-gradient-to-br from-gray-900 to-gray-950 p-8 shadow-xl">
<p className="text-sm font-semibold uppercase tracking-wider text-orange-400">Get started</p>
<h3 className="mt-2 text-2xl font-bold text-white">Join the Sankofa ecosystem</h3>
<p className="mt-3 text-sm leading-relaxed text-gray-400">
Sign in to access Phoenix Cloud, marketplace subscriptions, partner tools, and client
workspaces one identity across the platform.
</p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex flex-1 items-center justify-center rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-5 py-3 text-sm font-semibold text-gray-950 no-underline hover:from-orange-400 hover:to-amber-400"
>
Sign in to ecosystem
</Link>
<Link
href="https://phoenix.sankofa.nexus"
className="inline-flex flex-1 items-center justify-center rounded-lg border border-gray-700 px-5 py-3 text-sm font-semibold text-white no-underline hover:bg-gray-800"
>
Phoenix Cloud
</Link>
</div>
</div>
</div>
</div>
</section>
</main>
<CorporateFooter />
</div>
);
}

View File

@@ -0,0 +1,186 @@
import { ArrowRight, ShieldCheck } from 'lucide-react';
import Link from 'next/link';
import {
gradeBadgeClass,
institutionalGradeRubric,
scoreBarClass,
sovereignInstitutionGrades,
} from '@/lib/corporate-site-data';
const audienceLabels = {
sovereign: 'Sovereign & public sector',
regulated: 'Regulated counterparties',
platform: 'Platform & L1',
} as const;
export function InstitutionalGradeSection() {
const sovereignRows = sovereignInstitutionGrades.filter((e) => e.audience === 'sovereign');
const otherRows = sovereignInstitutionGrades.filter((e) => e.audience !== 'sovereign');
return (
<section id="institutional-grade" className="scroll-mt-20 border-y border-gray-800/60 bg-gray-950 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-2xl">
<div className="mb-3 inline-flex items-center gap-2 rounded-full border border-orange-500/30 bg-orange-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-orange-300">
<ShieldCheck className="h-3.5 w-3.5" aria-hidden />
Institutional readiness
</div>
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Grade &amp; score for sovereign institutions
</h2>
<p className="mt-4 text-lg text-gray-400">
Transparent engineering scorecards aligned with the DBIS institutional rubric the same
framework used for ecosystem readiness, jurisdiction matrices, and settlement evidence.
</p>
</div>
<p className="max-w-md text-xs leading-relaxed text-gray-500">
Informational only not legal advice, a regulatory determination, or an audit certificate.
Counsel owns statutory interpretation. Scores reflect repo artifacts and live probes as of each
assessment date.
</p>
</div>
{/* Rubric legend */}
<div className="mt-10 overflow-x-auto rounded-xl border border-gray-800 bg-gray-900/40">
<table className="w-full min-w-[640px] text-left text-sm">
<thead>
<tr className="border-b border-gray-800 text-xs uppercase tracking-wider text-gray-500">
<th className="px-4 py-3 font-semibold">Grade</th>
<th className="px-4 py-3 font-semibold">Score</th>
<th className="px-4 py-3 font-semibold">Institutional meaning</th>
</tr>
</thead>
<tbody>
{institutionalGradeRubric.map((row) => (
<tr key={row.grade} className="border-b border-gray-800/60 last:border-0">
<td className="px-4 py-3">
<span
className={`inline-flex min-w-[2rem] items-center justify-center rounded-md px-2 py-0.5 text-sm font-bold ring-1 ${gradeBadgeClass(row.grade)}`}
>
{row.grade}
</span>
</td>
<td className="px-4 py-3 font-mono text-gray-300">{row.range}</td>
<td className="px-4 py-3 text-gray-400">{row.meaning}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Sovereign institution scorecards */}
<div className="mt-12">
<h3 className="text-lg font-semibold text-white">{audienceLabels.sovereign}</h3>
<div className="mt-6 grid gap-4 lg:grid-cols-2">
{sovereignRows.map((entry) => (
<GradeCard key={entry.id} entry={entry} />
))}
</div>
</div>
<div className="mt-12">
<h3 className="text-lg font-semibold text-white">Related institutional assessments</h3>
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{otherRows.map((entry) => (
<GradeCard key={entry.id} entry={entry} compact />
))}
</div>
</div>
<div className="mt-10 flex flex-wrap items-center gap-4 text-sm">
<Link
href="https://d-bis.org/cb/dbis"
className="inline-flex items-center gap-1.5 font-medium text-orange-300 no-underline hover:text-orange-200"
>
DBIS institutional registry
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
<span className="text-gray-600" aria-hidden>
·
</span>
<Link
href="https://docs.d-bis.org"
className="inline-flex items-center gap-1.5 font-medium text-orange-300 no-underline hover:text-orange-200"
>
Compliance matrices &amp; onboarding charter
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
</div>
</div>
</section>
);
}
function GradeCard({
entry,
compact = false,
}: {
entry: (typeof sovereignInstitutionGrades)[number];
compact?: boolean;
}) {
const pct = Math.round((entry.score / entry.maxScore) * 100);
const inner = (
<>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-white">{entry.name}</p>
{!compact ? (
<p className="mt-1 text-xs text-gray-500">Assessed {entry.assessmentDate}</p>
) : null}
</div>
<span
className={`shrink-0 rounded-lg px-2.5 py-1 text-lg font-bold ring-1 ${gradeBadgeClass(entry.letterGrade)}`}
>
{entry.letterGrade}
</span>
</div>
<div className="mt-4 flex items-baseline gap-2">
<span className="text-3xl font-bold tabular-nums text-white">{entry.score}</span>
<span className="text-sm text-gray-500">/ {entry.maxScore}</span>
</div>
<div className="mt-3 h-1.5 overflow-hidden rounded-full bg-gray-800">
<div
className={`h-full rounded-full transition-all ${scoreBarClass(entry.letterGrade)}`}
style={{ width: `${pct}%` }}
role="progressbar"
aria-valuenow={entry.score}
aria-valuemin={0}
aria-valuemax={entry.maxScore}
aria-label={`${entry.name} score ${entry.score} of ${entry.maxScore}`}
/>
</div>
<p className={`text-gray-400 ${compact ? 'mt-3 text-xs leading-relaxed' : 'mt-4 text-sm leading-relaxed'}`}>
{entry.tier}
</p>
{compact ? (
<p className="mt-2 text-xs text-gray-600">{entry.assessmentDate}</p>
) : null}
</>
);
const className = compact
? 'rounded-xl border border-gray-800 bg-gray-900/30 p-4 transition hover:border-gray-700'
: 'rounded-2xl border border-gray-800 bg-gray-900/40 p-6 transition hover:border-orange-500/30';
if (entry.href) {
return (
<Link href={entry.href} className={`group block no-underline ${className}`}>
{inner}
{!compact ? (
<span className="mt-4 inline-flex items-center gap-1 text-xs font-medium text-orange-400 group-hover:text-orange-300">
View assessment
<ArrowRight className="h-3 w-3" aria-hidden />
</span>
) : null}
</Link>
);
}
return <article className={className}>{inner}</article>;
}

View File

@@ -0,0 +1,35 @@
'use client';
import { useSession } from 'next-auth/react';
import { KeyboardShortcutsProvider } from '@/components/KeyboardShortcutsProvider';
import { MobileNavigation } from '@/components/layout/MobileNavigation';
import { PortalBreadcrumbs } from '@/components/layout/PortalBreadcrumbs';
import { PortalHeader } from '@/components/layout/PortalHeader';
import { PortalSidebar } from '@/components/layout/PortalSidebar';
/**
* Full console chrome only after sign-in. Unauthenticated routes render children alone
* so the landing / sign-in view is not squeezed beside sidebar + mega-header.
*/
export function AppShell({ children }: { children: React.ReactNode }) {
const { status } = useSession();
if (status !== 'authenticated') {
return <>{children}</>;
}
return (
<KeyboardShortcutsProvider>
<div className="flex min-h-screen flex-col bg-gray-950 text-gray-100 antialiased">
<PortalHeader />
<PortalBreadcrumbs />
<div className="flex min-h-0 flex-1">
<PortalSidebar />
<main className="min-w-0 flex-1 md:ml-64">{children}</main>
</div>
<MobileNavigation />
</div>
</KeyboardShortcutsProvider>
);
}

View File

@@ -10,26 +10,32 @@ export function PortalHeader() {
return (
<header className="sticky top-0 z-50 w-full border-b border-gray-800 bg-gray-900/95 backdrop-blur supports-[backdrop-filter]:bg-gray-900/60">
<div className="container flex h-16 items-center">
<Link href="/" className="mr-6 flex items-center space-x-2">
<span className="text-xl font-bold bg-gradient-to-r from-orange-500 to-yellow-500 bg-clip-text text-transparent">
<div className="mx-auto flex h-auto min-h-16 w-full max-w-[1920px] flex-wrap items-center gap-3 px-4 py-2 sm:px-6 lg:h-16 lg:flex-nowrap lg:py-0">
<Link
href="/"
className="order-1 flex shrink-0 items-center no-underline hover:opacity-90"
>
<span className="bg-gradient-to-r from-orange-400 to-amber-400 bg-clip-text text-xl font-bold text-transparent">
Nexus Console
</span>
</Link>
{/* Search Bar - Enterprise-class search-first UX */}
<div className="flex flex-1 items-center justify-center max-w-2xl mx-8">
<div className="order-3 flex min-w-0 flex-1 basis-full items-center lg:order-2 lg:mx-6 lg:max-w-2xl lg:basis-auto">
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Search
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
aria-hidden
/>
<input
type="search"
placeholder="Search resources, settings, docs..."
className="w-full rounded-md border border-gray-700 bg-gray-800 py-2 pl-10 pr-4 text-sm text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
placeholder="Search resources, settings, docs"
aria-label="Search resources and settings"
className="w-full rounded-md border border-gray-700 bg-gray-800 py-2 pl-10 pr-4 text-sm text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500/40"
/>
</div>
</div>
<nav className="flex items-center space-x-4">
<nav className="order-2 flex shrink-0 items-center gap-1 sm:gap-2 lg:order-3 lg:ml-auto">
<button className="relative p-2 text-gray-400 hover:text-white transition-colors">
<Bell className="h-5 w-5" />
<span className="absolute top-1 right-1 h-2 w-2 rounded-full bg-orange-500" />
@@ -37,7 +43,7 @@ export function PortalHeader() {
<Link
href="/settings"
className="p-2 text-gray-400 hover:text-white transition-colors"
className="p-2 text-gray-400 no-underline transition-colors hover:text-white"
>
<Settings className="h-5 w-5" />
</Link>

View File

@@ -1,54 +1,25 @@
'use client'
import {
LayoutDashboard,
Server,
Network,
Settings,
FileText,
Activity,
Users,
CreditCard,
Shield,
HelpCircle
} from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { primaryNavigation, supportNavigation } from '@/lib/portal-navigation'
import { cn } from '@/lib/utils'
const navigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Infrastructure', href: '/infrastructure', icon: Server },
{ name: 'Resources', href: '/resources', icon: Server },
{ name: 'Virtual Machines', href: '/vms', icon: Server },
{ name: 'Networking', href: '/network', icon: Network },
{ name: 'Monitoring', href: '/dashboards', icon: Activity },
{ name: 'Users & Access', href: '/users', icon: Users },
{ name: 'Billing', href: '/billing', icon: CreditCard },
{ name: 'Security', href: '/security', icon: Shield },
{ name: 'Settings', href: '/settings', icon: Settings },
]
const helpLinks = [
{ name: 'Documentation', href: '/help/docs', icon: FileText },
{ name: 'Support', href: '/help/support', icon: HelpCircle },
]
export function PortalSidebar() {
const pathname = usePathname()
return (
<aside className="fixed left-0 top-16 h-[calc(100vh-4rem)] w-64 border-r border-gray-800 bg-gray-900 overflow-y-auto">
<nav className="p-4 space-y-1">
{navigation.map((item) => {
{primaryNavigation.map((item) => {
const isActive = pathname === item.href || pathname?.startsWith(item.href + '/')
return (
<Link
key={item.name}
href={item.href}
className={cn(
'flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
'flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium no-underline transition-colors',
isActive
? 'bg-orange-500/10 text-orange-500 border border-orange-500/20'
: 'text-gray-400 hover:bg-gray-800 hover:text-white'
@@ -66,11 +37,11 @@ export function PortalSidebar() {
Help & Support
</div>
<nav className="space-y-1">
{helpLinks.map((item) => (
{supportNavigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-400 hover:bg-gray-800 hover:text-white transition-colors"
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-400 no-underline transition-colors hover:bg-gray-800 hover:text-white"
>
<item.icon className="h-5 w-5" />
<span>{item.name}</span>
@@ -81,4 +52,3 @@ export function PortalSidebar() {
</aside>
)
}

View File

@@ -15,20 +15,31 @@ interface OnboardingStep {
interface OnboardingWizardProps {
steps: OnboardingStep[];
onComplete: () => void;
onComplete: () => Promise<void>;
}
export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
const [currentStep, setCurrentStep] = useState(0);
const [completedSteps, setCompletedSteps] = useState<Set<string>>(new Set());
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleNext = () => {
const handleNext = async () => {
setError(null);
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
onComplete();
router.push('/dashboard');
try {
setSubmitting(true);
await onComplete();
router.push('/dashboard');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to complete onboarding.');
} finally {
setSubmitting(false);
}
}
};
@@ -110,6 +121,11 @@ export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
<p className="text-gray-400 mb-6">
{steps[currentStep].description}
</p>
{error ? (
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300">
{error}
</div>
) : null}
<CurrentStepComponent
onComplete={() => handleStepComplete(steps[currentStep].id)}
/>
@@ -126,9 +142,10 @@ export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
</button>
<button
onClick={handleNext}
disabled={submitting}
className="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors flex items-center gap-2"
>
{currentStep === steps.length - 1 ? 'Complete' : 'Next'}
{submitting ? 'Saving...' : currentStep === steps.length - 1 ? 'Complete' : 'Next'}
<ArrowRight className="h-4 w-4" />
</button>
</div>
@@ -137,4 +154,3 @@ export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
</div>
);
}

View File

@@ -0,0 +1,81 @@
'use client';
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
import { cn } from '@/lib/utils';
interface PreviewAction {
href: string;
label: string;
}
interface FeaturePreviewPageProps {
eyebrow: string;
title: string;
description: string;
status?: 'preview' | 'request-only' | 'active';
bullets: readonly string[];
primaryAction?: PreviewAction;
secondaryAction?: PreviewAction;
}
export function FeaturePreviewPage({
eyebrow,
title,
description,
status = 'preview',
bullets,
primaryAction,
secondaryAction,
}: FeaturePreviewPageProps) {
const primaryActionClassName =
'inline-flex h-10 items-center justify-center rounded-md bg-gray-700 px-4 text-base font-medium text-white transition-colors hover:bg-gray-600';
const secondaryActionClassName =
'inline-flex h-10 items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 text-base font-medium text-white transition-colors hover:bg-gray-800';
return (
<div className="mx-auto max-w-4xl px-4 py-10">
<div className="mb-8">
<p className="mb-2 text-sm font-medium uppercase tracking-wide text-orange-400">{eyebrow}</p>
<div className="mb-3 flex items-center gap-3">
<h1 className="text-3xl font-bold text-white">{title}</h1>
<Badge variant={status === 'active' ? 'default' : 'secondary'}>
{status === 'request-only' ? 'Request Only' : status === 'active' ? 'Active' : 'Preview'}
</Badge>
</div>
<p className="max-w-3xl text-gray-400">{description}</p>
</div>
<Card className="border-gray-800 bg-gray-900/70">
<CardHeader>
<CardTitle className="text-white">Current Scope</CardTitle>
<CardDescription>
This route is now real and intentionally describes the supported boundary for this workspace area.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ul className="space-y-2 text-sm text-gray-300">
{bullets.map((bullet) => (
<li key={bullet}> {bullet}</li>
))}
</ul>
<div className="flex flex-col gap-3 pt-2 sm:flex-row">
{primaryAction ? (
<Link href={primaryAction.href} className={cn(primaryActionClassName)}>
{primaryAction.label}
</Link>
) : null}
{secondaryAction ? (
<Link href={secondaryAction.href} className={cn(secondaryActionClassName)}>
{secondaryAction.label}
</Link>
) : null}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,116 @@
'use client';
/**
* Cloudflare Turnstile widget. Site key is public (NEXT_PUBLIC_*).
* Pair with Turnstile secret on any backend route you protect — not the same as Cloudflare DNS API keys.
*/
import { useEffect, useRef } from 'react';
const SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
declare global {
interface Window {
turnstile?: {
render: (
container: HTMLElement,
options: {
sitekey: string;
callback: (token: string) => void;
'error-callback'?: () => void;
'expired-callback'?: () => void;
}
) => string;
remove?: (widgetId: string) => void;
};
}
}
function loadTurnstileScript(): Promise<void> {
if (typeof window === 'undefined') {
return Promise.reject(new Error('no window'));
}
if (window.turnstile) {
return Promise.resolve();
}
const existing = document.querySelector<HTMLScriptElement>(`script[src="${SCRIPT_SRC}"]`);
if (existing) {
return new Promise((resolve, reject) => {
const t0 = Date.now();
const tick = () => {
if (window.turnstile) {
resolve();
return;
}
if (Date.now() - t0 > 15000) {
reject(new Error('Turnstile script timeout'));
return;
}
requestAnimationFrame(tick);
};
tick();
});
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Turnstile'));
document.head.appendChild(script);
});
}
export interface CloudflareTurnstileProps {
siteKey: string;
onToken: (token: string | null) => void;
}
export function CloudflareTurnstile({ siteKey, onToken }: CloudflareTurnstileProps) {
const containerRef = useRef<HTMLDivElement>(null);
const widgetIdRef = useRef<string | null>(null);
const onTokenRef = useRef(onToken);
onTokenRef.current = onToken;
useEffect(() => {
if (!siteKey || !containerRef.current) {
return;
}
let cancelled = false;
void (async () => {
try {
await loadTurnstileScript();
if (cancelled || !containerRef.current || !window.turnstile) {
return;
}
containerRef.current.innerHTML = '';
const id = window.turnstile.render(containerRef.current, {
sitekey: siteKey,
callback: (token: string) => onTokenRef.current(token),
'error-callback': () => onTokenRef.current(null),
'expired-callback': () => onTokenRef.current(null),
});
widgetIdRef.current = id;
} catch {
onTokenRef.current(null);
}
})();
return () => {
cancelled = true;
const id = widgetIdRef.current;
widgetIdRef.current = null;
if (id && window.turnstile?.remove) {
try {
window.turnstile.remove(id);
} catch {
/* noop */
}
}
};
}, [siteKey]);
return <div ref={containerRef} className="min-h-[65px]" aria-live="polite" />;
}

View File

@@ -0,0 +1,228 @@
{
"generatedAt": "2026-06-11T08:18:45.004140+00:00",
"source": "config/institutional-a-plus-readiness.v1.json",
"rubric": [
{
"grade": "A",
"range": "90\u2013100",
"meaning": "Production-grade; audit-ready; minimal material gaps"
},
{
"grade": "B",
"range": "80\u201389",
"meaning": "Pilot-ready with documented, bounded residual risk"
},
{
"grade": "C",
"range": "70\u201379",
"meaning": "Operational with material gaps blocking institutional scale"
},
{
"grade": "D",
"range": "60\u201369",
"meaning": "Engineering-complete; evidence/jurisdiction incomplete"
},
{
"grade": "F",
"range": "<60",
"meaning": "Not suitable for broad institutional deployment claims"
}
],
"sovereignInstitutionGrades": [
{
"id": "ecosystem-composite",
"name": "DBIS / Chain 138 ecosystem",
"score": 83.0,
"maxScore": 100,
"letterGrade": "B",
"tier": "Target 90/100 \u2014 mixed work remaining",
"audience": "sovereign",
"assessmentDate": "2026-06-11",
"href": "https://gitea.d-bis.org/d-bis/proxmox/src/branch/main/ECOSYSTEM_READINESS.md",
"atTarget": false,
"blockerClass": "mixed"
},
{
"id": "sovereign-cloud",
"name": "Sovereign cloud infrastructure",
"score": 90.0,
"maxScore": 100,
"letterGrade": "A-",
"tier": "At or above A-tier target score",
"audience": "sovereign",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": true,
"blockerClass": "operator"
},
{
"id": "public-sector-compliance",
"name": "Public sector & jurisdiction matrices",
"score": 74.0,
"maxScore": 100,
"letterGrade": "C",
"tier": "External gate (counsel/treasury/listings) \u2014 see README.md",
"audience": "sovereign",
"assessmentDate": "2026-06-11",
"href": "https://docs.d-bis.org",
"atTarget": false,
"blockerClass": "external"
},
{
"id": "settlement-rtgs",
"name": "Settlement, RTGS & Rail",
"score": 72.0,
"maxScore": 100,
"letterGrade": "C-",
"tier": "Target 90/100 \u2014 mixed work remaining",
"audience": "sovereign",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "mixed"
},
{
"id": "rwa-gov-critical",
"name": "RWA index factory (government-critical lens)",
"score": 58.0,
"maxScore": 100,
"letterGrade": "D",
"tier": "External gate (counsel/treasury/listings) \u2014 see RWA_TOKEN_FACTORY_INSTITUTIONAL_GRADE.md",
"audience": "sovereign",
"assessmentDate": "2026-06-11",
"href": "https://docs.d-bis.org",
"atTarget": false,
"blockerClass": "external"
},
{
"id": "pmm-liquidity",
"name": "PMM / on-chain liquidity & peg",
"score": 75.0,
"maxScore": 100,
"letterGrade": "C+",
"tier": "Target 90/100 \u2014 operator work remaining",
"audience": "platform",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "operator"
},
{
"id": "cross-chain",
"name": "Cross-chain & CCIP lanes",
"score": 51.0,
"maxScore": 100,
"letterGrade": "D",
"tier": "Target 90/100 \u2014 operator work remaining",
"audience": "platform",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "operator"
},
{
"id": "ei138-at-pillars",
"name": "EI-138-AT institutional pillars (not machine composite)",
"score": 67.3,
"maxScore": 100,
"letterGrade": "D+",
"tier": "External gate (counsel/treasury/listings) \u2014 see EI_MATRIX_CHAIN138_AUTOMATED_TRADING_INSTITUTIONAL_GRADE.md",
"audience": "regulated",
"assessmentDate": "2026-06-11",
"href": "https://docs.d-bis.org",
"atTarget": false,
"blockerClass": "external"
},
{
"id": "complete-credential",
"name": "Complete Credential / eIDAS program",
"score": 70.0,
"maxScore": 100,
"letterGrade": "C",
"tier": "External gate (counsel/treasury/listings) \u2014 see PUBLIC_SECTOR_LIVE_DEPLOYMENT_CHECKLIST.md",
"audience": "regulated",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "external"
},
{
"id": "omnl-registry",
"name": "OMNL / HYBX institutional entity registry",
"score": 63.0,
"maxScore": 100,
"letterGrade": "D",
"tier": "External gate (counsel/treasury/listings) \u2014 see OMNL_HYBX_INSTITUTIONAL_ENTITY_REGISTRY_GRADE.md",
"audience": "regulated",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "external"
},
{
"id": "chain138-l1",
"name": "Chain 138 L1 health & oracle integrity",
"score": 91.0,
"maxScore": 100,
"letterGrade": "A-",
"tier": "At or above A-tier target score",
"audience": "platform",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": true,
"blockerClass": "automatable"
},
{
"id": "security-audit",
"name": "Security & third-party audit",
"score": 74.0,
"maxScore": 100,
"letterGrade": "C",
"tier": "External gate (counsel/treasury/listings) \u2014 see INSTITUTION_ONBOARDING_CHARTER.md",
"audience": "sovereign",
"assessmentDate": "2026-06-11",
"href": "https://docs.d-bis.org",
"atTarget": false,
"blockerClass": "external"
},
{
"id": "ura-strict-closure",
"name": "URA strict closure (no TBD evidence)",
"score": 78.0,
"maxScore": 100,
"letterGrade": "C+",
"tier": "Target 90/100 \u2014 automatable work remaining",
"audience": "platform",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "automatable"
},
{
"id": "stack-b-drift",
"name": "PMM Stack B drift eliminated",
"score": 85.0,
"maxScore": 100,
"letterGrade": "B+",
"tier": "Target 100/100 \u2014 automatable work remaining",
"audience": "platform",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "automatable"
},
{
"id": "external-visibility",
"name": "External visibility & listings",
"score": 76.0,
"maxScore": 100,
"letterGrade": "C+",
"tier": "External gate (counsel/treasury/listings) \u2014 see CHAIN138_DEFILLAMA_ECOSYSTEM_MAP.md",
"audience": "platform",
"assessmentDate": "2026-06-11",
"href": null,
"atTarget": false,
"blockerClass": "external"
}
]
}

View File

@@ -0,0 +1,33 @@
import { decodeJwtPayload, extractPortalClaimState } from '@/lib/auth/claims';
function makeToken(payload: Record<string, unknown>) {
const encoded = Buffer.from(JSON.stringify(payload)).toString('base64url');
return `header.${encoded}.signature`;
}
describe('portal auth claims', () => {
it('decodes JWT payloads safely', () => {
const payload = decodeJwtPayload(makeToken({ tenantId: 'tenant-123' }));
expect(payload.tenantId).toBe('tenant-123');
});
it('extracts client, tenant, subscription, and roles from mixed claim shapes', () => {
const state = extractPortalClaimState(
{
client_id: 'client-001',
tenantId: 'tenant-001',
subscription_id: 'sub-001',
realm_access: { roles: ['tenant-admin'] },
},
{
roles: ['billing-admin'],
role: 'operator',
}
);
expect(state.clientId).toBe('client-001');
expect(state.tenantId).toBe('tenant-001');
expect(state.subscriptionId).toBe('sub-001');
expect(state.roles).toEqual(expect.arrayContaining(['tenant-admin', 'billing-admin', 'operator']));
});
});

View File

@@ -0,0 +1,86 @@
export interface PortalClaimState {
clientId?: string;
tenantId?: string;
subscriptionId?: string;
roles: string[];
}
type JsonRecord = Record<string, unknown>;
function decodeBase64Url(value: string): string {
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
return Buffer.from(padded, 'base64').toString('utf8');
}
export function decodeJwtPayload(token?: string | null): JsonRecord {
if (!token) return {};
const [, payload] = token.split('.');
if (!payload) return {};
try {
const decoded = decodeBase64Url(payload);
const parsed = JSON.parse(decoded);
return parsed && typeof parsed === 'object' ? (parsed as JsonRecord) : {};
} catch {
return {};
}
}
function readStringClaim(source: JsonRecord, keys: string[]): string | undefined {
for (const key of keys) {
const value = source[key];
if (typeof value === 'string' && value.trim() !== '') return value.trim();
}
return undefined;
}
function readStringArrayClaim(source: JsonRecord, keys: string[]): string[] {
for (const key of keys) {
const value = source[key];
if (Array.isArray(value)) {
const strings = value.filter((item): item is string => typeof item === 'string' && item.trim() !== '');
if (strings.length > 0) return strings;
}
}
return [];
}
function readRealmRoles(source: JsonRecord): string[] {
const realmAccess = source.realm_access;
if (!realmAccess || typeof realmAccess !== 'object') return [];
const roles = (realmAccess as JsonRecord).roles;
if (!Array.isArray(roles)) return [];
return roles.filter((item): item is string => typeof item === 'string' && item.trim() !== '');
}
export function extractPortalClaimState(...sources: JsonRecord[]): PortalClaimState {
const clientId =
sources.map((source) => readStringClaim(source, ['clientId', 'client_id', 'client'])).find(Boolean) ||
undefined;
const tenantId =
sources.map((source) => readStringClaim(source, ['tenantId', 'tenant_id', 'tenant'])).find(Boolean) ||
undefined;
const subscriptionId =
sources
.map((source) => readStringClaim(source, ['subscriptionId', 'subscription_id', 'subscription']))
.find(Boolean) || undefined;
const roleSet = new Set<string>();
for (const source of sources) {
for (const role of readRealmRoles(source)) roleSet.add(role);
for (const role of readStringArrayClaim(source, ['roles', 'groups'])) roleSet.add(role);
const directRole = readStringClaim(source, ['role']);
if (directRole) roleSet.add(directRole);
}
return {
clientId,
tenantId,
subscriptionId,
roles: Array.from(roleSet),
};
}

View File

@@ -0,0 +1,343 @@
import type { LucideIcon } from 'lucide-react';
import {
Cloud,
Shield,
Wallet,
Blocks,
Cpu,
Landmark,
Globe2,
Sparkles,
Network,
FileKey2,
Building2,
Scale,
} from 'lucide-react';
import generatedGrades from '@/data/institutional-grades.generated.json';
export interface CorporateNavItem {
label: string;
href: string;
external?: boolean;
}
export interface ProductDivision {
id: string;
name: string;
tagline: string;
description: string;
href: string;
external?: boolean;
icon: LucideIcon;
highlights: string[];
}
export interface ServiceOffering {
name: string;
category: string;
description: string;
icon: LucideIcon;
}
export interface SolutionVertical {
name: string;
description: string;
icon: LucideIcon;
}
export const ECOSYSTEM_SIGN_IN_PATH = '/api/auth/signin?callbackUrl=/dashboard';
export const corporateNav: CorporateNavItem[] = [
{ label: 'Products', href: '#products' },
{ label: 'Services', href: '#services' },
{ label: 'Solutions', href: '#solutions' },
{ label: 'Institutional grade', href: '#institutional-grade' },
{ label: 'Resources', href: '#resources' },
];
export const productDivisions: ProductDivision[] = [
{
id: 'phoenix',
name: 'Phoenix Cloud',
tagline: 'Sovereign cloud platform',
description:
'Infrastructure, identity, orchestration, and marketplace primitives — the sovereign alternative to hyperscale public cloud.',
href: 'https://phoenix.sankofa.nexus',
external: true,
icon: Cloud,
highlights: ['Multi-tenant compute', 'Keycloak identity plane', 'GitOps & Crossplane', 'Marketplace catalog'],
},
{
id: 'complete-credential',
name: 'Complete Credential',
tagline: 'Sovereign credential issuance',
description:
'Institutional-grade verifiable credentials, entity packs, and eIDAS-aligned issuance hosted on Phoenix.',
href: 'https://cc.sankofa.nexus',
external: true,
icon: FileKey2,
highlights: ['Entity credential packs', 'OIDC / SAML integration', 'Compliance workflows', 'Operator admin'],
},
{
id: 'studio',
name: 'Sankofa Studio',
tagline: 'FusionAI creator platform',
description:
'Build, deploy, and manage AI-powered applications on sovereign infrastructure with full data residency control.',
href: 'https://studio.sankofa.nexus/studio/',
external: true,
icon: Sparkles,
highlights: ['Model orchestration', 'Creator workflows', 'Tenant isolation', 'API-first design'],
},
{
id: 'blockchain',
name: 'Blockchain & DeFi',
tagline: 'Chain 138 & cross-chain mesh',
description:
'Defi Oracle Meta Mainnet, PMM liquidity, CCIP bridges, and institutional-grade on-chain settlement rails.',
href: 'https://explorer.d-bis.org',
external: true,
icon: Blocks,
highlights: ['Chain 138 RPC & explorer', 'PMM / DODO liquidity', 'CCIP cross-chain', 'Token & RWA factory'],
},
{
id: 'identity',
name: 'Identity & Access',
tagline: 'Sovereign IAM',
description:
'Centralized OIDC/SAML identity with device binding, passkeys, RBAC, and zero dependency on hyperscaler IdPs.',
href: 'https://keycloak.sankofa.nexus',
external: true,
icon: Shield,
highlights: ['Keycloak IdP', 'Multi-tenant RBAC', 'Passkeys & MFA', 'Client SSO portal'],
},
{
id: 'treasury',
name: 'Treasury & Settlement',
tagline: 'Institutional finance rails',
description:
'Double-entry ledger, virtual accounts, wallet registry, and RTGS-aligned settlement for sovereign finance.',
href: 'https://portal.sankofa.nexus',
external: true,
icon: Landmark,
highlights: ['Phoenix Ledger', 'Virtual accounts', 'Wallet policy engine', 'DBIS settlement'],
},
];
export const platformServices: ServiceOffering[] = [
{
name: 'Phoenix Ledger Service',
category: 'Financial',
description: 'Double-entry ledger with virtual accounts, holds, and multi-asset support.',
icon: Landmark,
},
{
name: 'Phoenix Identity Service',
category: 'Security',
description: 'Users, orgs, roles, permissions, device binding, passkeys, and OAuth/OIDC.',
icon: Shield,
},
{
name: 'Phoenix Wallet Registry',
category: 'Custody',
description: 'Wallet mapping, chain support, MPC/HSM policy engine, and ERC-4337 accounts.',
icon: Wallet,
},
{
name: 'Transaction Orchestrator',
category: 'Orchestration',
description: 'On-chain and off-chain workflows with retries, compensations, and failover.',
icon: Network,
},
{
name: 'Messaging Orchestrator',
category: 'Communications',
description: 'Multi-provider SMS, email, voice, and push with automatic failover.',
icon: Globe2,
},
{
name: 'Voice Orchestrator',
category: 'AI Media',
description: 'TTS/STT with caching, multi-provider routing, and PII-aware moderation.',
icon: Cpu,
},
];
export const solutionVerticals: SolutionVertical[] = [
{
name: 'Public Sector',
description: 'Sovereign tenancy, SMOA compliance, and institutional registry for government programs.',
icon: Building2,
},
{
name: 'Financial Services',
description: 'Ledger, settlement, EMI wallet infrastructure, and cross-border RTGS integration.',
icon: Scale,
},
{
name: 'Healthcare & Identity',
description: 'Verifiable credentials, eIDAS-aligned issuance, and HIPAA-ready identity planes.',
icon: FileKey2,
},
{
name: 'Telecommunications',
description: 'PanTel 6G infrastructure JV, sovereign edge compute, and carrier-grade networking.',
icon: Network,
},
];
export const philosophySteps = [
{ verb: 'Remember', detail: 'Where we came from — ancestral wisdom and institutional continuity' },
{ verb: 'Retrieve', detail: 'What was essential — sovereign identity and owned core primitives' },
{ verb: 'Restore', detail: 'Identity and sovereignty — independent infrastructure under our control' },
{ verb: 'Rise', detail: 'Forward with purpose — world-class cloud without hyperscaler dependency' },
];
export interface InstitutionalGradeEntry {
id: string;
name: string;
score: number;
maxScore: number;
letterGrade: string;
tier: string;
audience: 'sovereign' | 'regulated' | 'platform';
assessmentDate: string;
href?: string;
}
/** Engineering readiness scores for sovereign & regulated institutions (not legal advice). */
const FALLBACK_RUBRIC = [
{ grade: 'A', range: '90100', meaning: 'Production-grade; audit-ready; minimal material gaps' },
{ grade: 'B', range: '8089', meaning: 'Pilot-ready with documented, bounded residual risk' },
{ grade: 'C', range: '7079', meaning: 'Operational with material gaps blocking institutional scale' },
{ grade: 'D', range: '6069', meaning: 'Engineering-complete; evidence and jurisdiction coverage incomplete' },
{ grade: 'F', range: '<60', meaning: 'Not suitable for broad institutional deployment claims' },
];
const FALLBACK_GRADES: InstitutionalGradeEntry[] = [
{
id: 'ecosystem-composite',
name: 'DBIS / Chain 138 ecosystem',
score: 83,
maxScore: 100,
letterGrade: 'B',
tier: 'Advanced pilot — 138-native flows with disclosures',
audience: 'sovereign',
assessmentDate: '2026-05-26',
href: 'https://gitea.d-bis.org/d-bis/proxmox/src/branch/main/ECOSYSTEM_READINESS.md',
},
{
id: 'sovereign-cloud',
name: 'Sovereign cloud infrastructure',
score: 90,
maxScore: 100,
letterGrade: 'A',
tier: 'CI-clean operator stack; LAN inventory & E2E routing',
audience: 'sovereign',
assessmentDate: '2026-05-26',
},
{
id: 'public-sector-compliance',
name: 'Public sector & jurisdiction matrices',
score: 74,
maxScore: 100,
letterGrade: 'C',
tier: 'Indonesia pilot-ready; US stub; counsel sign-off paths documented',
audience: 'sovereign',
assessmentDate: '2026-05-26',
href: 'https://docs.d-bis.org',
},
{
id: 'settlement-rtgs',
name: 'Settlement, RTGS & Rail',
score: 72,
maxScore: 100,
letterGrade: 'C',
tier: 'Verification tooling live; on-chain Rail not yet deployed',
audience: 'sovereign',
assessmentDate: '2026-05-26',
},
{
id: 'rwa-gov-critical',
name: 'RWA index factory (government-critical lens)',
score: 58,
maxScore: 100,
letterGrade: 'D',
tier: 'Engineering pilot only — not for sovereign treasury without counsel',
audience: 'sovereign',
assessmentDate: '2026-05-24',
href: 'https://docs.d-bis.org',
},
{
id: 'complete-credential',
name: 'Complete Credential / eIDAS program',
score: 70,
maxScore: 100,
letterGrade: 'C',
tier: 'SMOA evidence partial; QTSP path documented',
audience: 'regulated',
assessmentDate: '2026-03-25',
href: 'https://cc.sankofa.nexus',
},
{
id: 'omnl-registry',
name: 'OMNL / HYBX institutional entity registry',
score: 63,
maxScore: 100,
letterGrade: 'D',
tier: 'Live Fineract offices; CB counterparty MOU evidence outstanding',
audience: 'regulated',
assessmentDate: '2026-06-05',
},
{
id: 'chain138-l1',
name: 'Chain 138 L1 health & oracle integrity',
score: 91,
maxScore: 100,
letterGrade: 'A',
tier: '16/16 oracle strict; Stack A PMM live (~$87.8M)',
audience: 'platform',
assessmentDate: '2026-05-26',
},
];
export const institutionalGradeRubric =
generatedGrades.rubric?.length ? generatedGrades.rubric : FALLBACK_RUBRIC;
export const sovereignInstitutionGrades: InstitutionalGradeEntry[] =
generatedGrades.sovereignInstitutionGrades?.length
? (generatedGrades.sovereignInstitutionGrades as InstitutionalGradeEntry[])
: FALLBACK_GRADES;
export function gradeBadgeClass(letterGrade: string): string {
const g = letterGrade.charAt(0).toUpperCase();
switch (g) {
case 'A':
return 'bg-emerald-500/15 text-emerald-300 ring-emerald-500/30';
case 'B':
return 'bg-sky-500/15 text-sky-300 ring-sky-500/30';
case 'C':
return 'bg-amber-500/15 text-amber-300 ring-amber-500/30';
case 'D':
return 'bg-orange-500/15 text-orange-300 ring-orange-500/30';
default:
return 'bg-red-500/15 text-red-300 ring-red-500/30';
}
}
export function scoreBarClass(letterGrade: string): string {
const g = letterGrade.charAt(0).toUpperCase();
switch (g) {
case 'A':
return 'bg-emerald-500';
case 'B':
return 'bg-sky-500';
case 'C':
return 'bg-amber-500';
case 'D':
return 'bg-orange-500';
default:
return 'bg-red-500';
}
}

View File

@@ -0,0 +1,42 @@
import fs from 'node:fs';
import path from 'node:path';
import { primaryNavigation, supportNavigation } from '@/lib/portal-navigation';
const appRoot = '/home/intlc/projects/Sankofa/portal/src/app';
function hasBackingRoute(href: string): boolean {
const direct =
href === '/'
? path.join(appRoot, 'page.tsx')
: path.join(appRoot, href.replace(/^\//, ''), 'page.tsx');
if (fs.existsSync(direct)) return true;
if (href.startsWith('/admin/')) {
return fs.existsSync(path.join(appRoot, 'admin/[section]/page.tsx'));
}
if (href.startsWith('/partner/')) {
return fs.existsSync(path.join(appRoot, 'partner/[section]/page.tsx'));
}
if (href.startsWith('/help/')) {
return fs.existsSync(path.join(appRoot, 'help/[section]/page.tsx'));
}
return false;
}
describe('portal navigation integrity', () => {
it('backs every primary navigation entry with a route', () => {
for (const item of primaryNavigation) {
expect(hasBackingRoute(item.href)).toBe(true);
}
});
it('backs every support navigation entry with a route', () => {
for (const item of supportNavigation) {
expect(hasBackingRoute(item.href)).toBe(true);
}
});
});

View File

@@ -0,0 +1,30 @@
import {
CreditCard,
FileText,
HelpCircle,
LayoutDashboard,
Network,
Server,
Settings,
Shield,
Users,
Activity,
} from 'lucide-react';
export const primaryNavigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Infrastructure', href: '/infrastructure', icon: Server },
{ name: 'Resources', href: '/resources', icon: Server },
{ name: 'Virtual Machines', href: '/vms', icon: Server },
{ name: 'Networking', href: '/network', icon: Network },
{ name: 'Monitoring', href: '/dashboards', icon: Activity },
{ name: 'Users & Access', href: '/users', icon: Users },
{ name: 'Billing', href: '/billing', icon: CreditCard },
{ name: 'Security', href: '/security', icon: Shield },
{ name: 'Settings', href: '/settings', icon: Settings },
] as const;
export const supportNavigation = [
{ name: 'Documentation', href: '/help/docs', icon: FileText },
{ name: 'Support', href: '/help/support', icon: HelpCircle },
] as const;

View File

@@ -6,7 +6,9 @@ declare module 'next-auth' {
accessToken?: string;
roles?: string[];
/** Optional tenant scope for billing/dashboard GraphQL */
clientId?: string;
tenantId?: string;
subscriptionId?: string;
user?: DefaultSession['user'] & {
id?: string;
role?: string;
@@ -26,6 +28,8 @@ declare module 'next-auth/jwt' {
refreshToken?: string;
idToken?: string;
roles?: string[];
clientId?: string;
tenantId?: string;
subscriptionId?: string;
}
}