aethex-forge/client/components/Layout.tsx
AeThex 7fec93e05c
Some checks are pending
Build / build (push) Waiting to run
Deploy / deploy (push) Waiting to run
Lint & Type Check / lint (push) Waiting to run
Security Scan / dependency-check (push) Waiting to run
Security Scan / semgrep (push) Waiting to run
Test / test (18.x) (push) Waiting to run
Test / test (20.x) (push) Waiting to run
feat: Authentik SSO, nav systems, project pages, and schema fixes
Auth & SSO
- Wire Authentik (auth.aethex.tech) as OIDC PKCE SSO provider
- Server-side only flow with HMAC-signed stateless state token
- Account linking via authentik_sub in user metadata
- AeThex ID connection card in Dashboard connections tab
- Unlink endpoint POST /api/auth/authentik/unlink
- Fix node:https helper to bypass undici DNS bug on Node 18
- Fix resolv.conf to use 1.1.1.1/8.8.8.8 in container

Schema & types
- Regenerate database.types.ts from live Supabase schema (23k lines)
- Fix 511 TypeScript errors caused by stale 582-line types file
- Fix UserProfile import in aethex-database-adapter.ts
- Add notifications migration (title, message, read columns)

Server fixes
- Remove badge_color from achievements seed/upsert (column doesn't exist)
- Rename name→title, add slug field in achievements seed
- Remove email from all user_profiles select queries (column doesn't exist)
- Fix email-based achievement target lookup via auth.admin.listUsers
- Add GET /api/projects/:projectId endpoint
- Fix import.meta.dirname → fileURLToPath for Node 18 compatibility
- Expose VITE_APP_VERSION from package.json at build time

Navigation systems
- DevPlatformNav: reorganize into Learn/Build grouped dropdowns with descriptions
- Migrate all 11 dev-platform pages from main Layout to DevPlatformLayout
- Remove dead isDevMode context nav swap from main Layout
- EthosLayout: purple-accented tab bar (Library, Artists, Licensing, Settings)
  with member-only gating and guest CTA — migrate 4 Ethos pages
- GameForgeLayout: orange-branded sidebar with Studio section and lock icons
  for unauthenticated users — migrate GameForge + GameForgeDashboard
- SysBar: live latency ping, status dot (green/yellow/red), real version

Layout dropdown
- Role-gate Admin (owner/admin/founder only) and Internal Docs (+ staff)
- Add Internal section label with separator
- Fix settings link from /dashboard?tab=profile#settings to /dashboard?tab=settings

Project pages
- Add ProjectDetail page at /projects/:projectId
- Fix ProfilePassport "View mission" link from /projects/new to /projects/:id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 05:01:10 +00:00

593 lines
25 KiB
TypeScript

import { useState, useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext";
import { useArmTheme } from "@/contexts/ArmThemeContext";
import NotificationBell from "@/components/notifications/NotificationBell";
import { AIChatButton } from "@/components/ai";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
User,
Settings,
LogOut,
Sparkles,
UserCircle,
Menu,
BookOpen,
Shield,
X,
} from "lucide-react";
// ─── Brand styles (injected once) ────────────────────────────────────────────
const AX_STYLES = `
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=Share+Tech+Mono&display=swap');
@keyframes ax-flicker {
0%,100%{opacity:1} 92%{opacity:1} 93%{opacity:0.82} 94%{opacity:1} 96%{opacity:0.9} 97%{opacity:1}
}
@keyframes ax-glitch {
0%,89%,100%{transform:translateX(0) skewX(0)}
90%{transform:translateX(-3px) skewX(-1deg)}
91%{transform:translateX(3px) skewX(1deg)}
92%{transform:translateX(0) skewX(0)}
}
@keyframes ax-blink { 0%,50%{opacity:1} 51%,100%{opacity:0} }
@keyframes ax-sweep { 0%{left:-100%} 100%{left:200%} }
@keyframes ax-pulse-glow {
0%,100%{box-shadow:0 0 8px rgba(0,255,255,0.08),inset 0 0 8px rgba(0,255,255,0.02)}
50%{box-shadow:0 0 24px rgba(0,255,255,0.2),inset 0 0 16px rgba(0,255,255,0.05)}
}
@keyframes ax-fade-up {
from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)}
}
.ax-flicker { animation: ax-flicker 12s infinite; }
.ax-orbitron { font-family: 'Orbitron', monospace !important; }
.ax-mono { font-family: 'Share Tech Mono', monospace !important; }
.ax-glitch { animation: ax-glitch 9s infinite; font-family: 'Orbitron', monospace !important; }
.ax-card-sweep { position: relative; overflow: hidden; }
.ax-card-sweep::after {
content: ''; position: absolute; top: 0; left: -100%;
width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.05), transparent);
animation: ax-sweep 5s infinite; pointer-events: none;
}
.ax-pulse { animation: ax-pulse-glow 4s infinite; }
.ax-fade-up { animation: ax-fade-up 0.4s ease both; }
.ax-corner-bracket { position: relative; }
.ax-corner-bracket::before, .ax-corner-bracket::after {
content: ''; position: absolute; width: 16px; height: 16px;
}
.ax-corner-bracket::before {
top: -1px; left: -1px;
border-top: 2px solid rgba(0,255,255,0.6);
border-left: 2px solid rgba(0,255,255,0.6);
}
.ax-corner-bracket::after {
bottom: -1px; right: -1px;
border-bottom: 2px solid rgba(0,255,255,0.6);
border-right: 2px solid rgba(0,255,255,0.6);
}
.ax-clip {
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px));
}
body::before {
content: '';
position: fixed; inset: 0;
background: repeating-linear-gradient(
0deg, transparent, transparent 2px,
rgba(0,255,255,0.015) 2px, rgba(0,255,255,0.015) 4px
);
pointer-events: none; z-index: 9990;
}
body::after {
content: '';
position: fixed; inset: 0;
background-image:
linear-gradient(rgba(0,255,255,0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,255,255,0.025) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none; z-index: 9989;
}
`;
// ─── SysBar ───────────────────────────────────────────────────────────────────
const APP_VERSION = import.meta.env.VITE_APP_VERSION || "0.1.0";
type SysStatus = "ok" | "degraded" | "outage" | "unknown";
function SysBar() {
const [time, setTime] = useState("");
const [latency, setLatency] = useState<number | null>(null);
const [status, setStatus] = useState<SysStatus>("unknown");
const [node, setNode] = useState("vps.aethex.tech");
// Live clock
useEffect(() => {
const tick = () => setTime(new Date().toLocaleTimeString("en-US", { hour12: false }));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []);
// Ping /api/health every 30s — measures real round-trip latency
useEffect(() => {
const ping = async () => {
const t0 = performance.now();
try {
const res = await fetch("/api/health", { cache: "no-store" });
const ms = Math.round(performance.now() - t0);
setLatency(ms);
if (!res.ok) {
setStatus("outage");
} else {
const body = await res.json().catch(() => ({}));
const host: string = body?.host ?? "";
if (host) setNode(new URL(host).hostname.replace("www.", ""));
setStatus(ms > 800 ? "degraded" : "ok");
}
} catch {
setLatency(null);
setStatus("outage");
}
};
ping();
const id = setInterval(ping, 30_000);
return () => clearInterval(id);
}, []);
const dotColor =
status === "ok" ? "#00ff41" :
status === "degraded" ? "#facc15" :
status === "outage" ? "#f87171" :
"rgba(0,255,255,0.3)";
return (
<div
className="ax-mono"
style={{
position: "fixed", top: 0, left: 0, right: 0, zIndex: 9995,
display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "4px 20px",
background: "rgba(3,3,3,0.98)",
borderBottom: "1px solid rgba(0,255,255,0.07)",
fontSize: 9, letterSpacing: 2, textTransform: "uppercase",
}}
>
<span style={{ color: "rgba(0,255,255,0.35)" }}>
AeThex.OS v{APP_VERSION} // Forge Terminal
</span>
<span style={{ color: "rgba(0,255,255,0.45)" }}>node: {node}</span>
<span style={{ display: "flex", alignItems: "center", gap: 6, color: "rgba(0,255,255,0.35)" }}>
<span style={{ color: dotColor, animation: "ax-blink 1s infinite", fontSize: 7 }}></span>
{latency !== null ? `${latency}ms` : "…"}
<span style={{ marginLeft: 6 }}>{time}</span>
</span>
</div>
);
}
// ─── HexLogo ──────────────────────────────────────────────────────────────────
function HexLogo({ size = 30 }: { size?: number }) {
return (
<svg viewBox="0 0 100 100" width={size} height={size}
style={{ filter: "drop-shadow(0 0 6px rgba(0,255,255,0.5))", flexShrink: 0 }}>
<polygon points="50,5 95,27.5 95,72.5 50,95 5,72.5 5,27.5"
fill="none" stroke="#00ffff" strokeWidth="1.5" opacity="0.7" />
<polygon points="50,16 84,33 84,67 50,84 16,67 16,33"
fill="none" stroke="#00ffff" strokeWidth="1" opacity="0.25" />
<text x="50" y="58" textAnchor="middle"
fontFamily="Orbitron" fontSize="23" fontWeight="700" fill="#00ffff">Æ</text>
</svg>
);
}
// ─── NavLink helper ───────────────────────────────────────────────────────────
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
return (
<Link
to={to}
className="ax-mono"
style={{
fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
color: "rgba(0,255,255,0.45)", textDecoration: "none", transition: "color 0.2s",
}}
onMouseEnter={e => (e.currentTarget.style.color = "#00ffff")}
onMouseLeave={e => (e.currentTarget.style.color = "rgba(0,255,255,0.45)")}
>
{children}
</Link>
);
}
// ─── Layout ───────────────────────────────────────────────────────────────────
interface LayoutProps {
children: React.ReactNode;
hideFooter?: boolean;
}
const FOOTER_LINKS = {
Platform: [
{ name: "Home", href: "/" },
{ name: "Realms", href: "/realms" },
{ name: "Developer Platform", href: "/dev-platform" },
{ name: "Dashboard", href: "/dashboard" },
],
Community: [
{ name: "Explore", href: "/engage" },
{ name: "Teams", href: "/teams" },
{ name: "Squads", href: "/squads" },
{ name: "Opportunities", href: "/opportunities" },
],
Resources: [
{ name: "Docs", href: "/docs" },
{ name: "API Reference", href: "/dev-platform/api-reference" },
{ name: "Quick Start", href: "/dev-platform/quick-start" },
{ name: "Roadmap", href: "/roadmap" },
],
Legal: [
{ name: "About", href: "/about" },
{ name: "Contact", href: "/contact" },
{ name: "Blog", href: "/blog" },
],
};
export default function CodeLayout({ children, hideFooter }: LayoutProps) {
const location = useLocation();
const { user, profile, roles, signOut, loading } = useAuth();
const isPrivileged = Array.isArray(roles) && roles.some(r => ["owner", "admin", "founder"].includes(r.toLowerCase()));
const isInternal = Array.isArray(roles) && roles.some(r => ["owner", "admin", "founder", "staff"].includes(r.toLowerCase()));
useArmTheme(); // keep context alive for downstream consumers
const [mobileOpen, setMobileOpen] = useState(false);
const publicNavLinks = [
{ name: "Realms", href: "/realms" },
{ name: "Dev Platform", href: "/dev-platform" },
{ name: "Docs", href: "/docs" },
{ name: "About", href: "/about" },
];
const userNavLinks = [
{ name: "Dashboard", href: "/dashboard" },
{ name: "Realms", href: "/realms" },
{ name: "Teams", href: "/teams" },
{ name: "Dev Platform", href: "/dev-platform" },
{ name: "Engage", href: "/engage" },
];
const navLinks = user ? userNavLinks : publicNavLinks;
// Inject global brand styles once
useEffect(() => {
const existing = document.getElementById("ax-forge-styles");
if (!existing) {
const el = document.createElement("style");
el.id = "ax-forge-styles";
el.textContent = AX_STYLES;
document.head.appendChild(el);
}
}, []);
const passportHref = profile?.username ? `/passport/${profile.username}` : "/passport/me";
const userInitials = (profile?.full_name || profile?.username || "U")
.split(" ").map((n: string) => n[0]).join("").toUpperCase();
return (
<div className="ax-flicker" style={{ minHeight: "100vh", background: "#050505", color: "#e0e0e0" }}>
<SysBar />
{/* ── Navigation ──────────────────────────────────────────────────────── */}
<header style={{
position: "fixed", top: 22, left: 0, right: 0, zIndex: 50,
borderBottom: "1px solid rgba(0,255,255,0.1)",
background: "rgba(5,5,5,0.95)",
backdropFilter: "blur(12px)",
}}>
{/* TrinityBar */}
<div style={{ display: "flex", height: 2 }}>
<div style={{ flex: 1, background: "#ff003c", opacity: 0.45 }} />
<div style={{ flex: 1, background: "#00d4ff", opacity: 0.45 }} />
<div style={{ flex: 1, background: "#ffb800", opacity: 0.45 }} />
</div>
<div style={{
maxWidth: 1200, margin: "0 auto",
padding: "10px 24px",
display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 16,
}}>
{/* Logo */}
<Link to="/" style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}>
<HexLogo size={30} />
<span
className="ax-orbitron ax-glitch"
style={{ fontSize: 13, fontWeight: 700, letterSpacing: 4, color: "#00ffff",
textShadow: "0 0 10px rgba(0,255,255,0.4)", textDecoration: "none" }}
>
AETHEX
</span>
</Link>
{/* Desktop nav links */}
<nav className="hidden md:flex" style={{ alignItems: "center", gap: 24, flex: 1, justifyContent: "center" }}>
{navLinks.map((item) => (
<NavLink key={item.href} to={item.href}>{item.name}</NavLink>
))}
</nav>
{/* Auth */}
<div style={{ display: "flex", alignItems: "center", gap: 12, flexShrink: 0 }}>
{!loading && (
<>
{user ? (
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<NotificationBell />
{profile ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button style={{
background: "none", border: "1px solid rgba(0,255,255,0.25)",
cursor: "pointer", borderRadius: 0, padding: 0,
}}>
<Avatar className="h-8 w-8" style={{ borderRadius: 0 }}>
<AvatarImage src={profile.avatar_url || undefined} alt={profile.full_name || profile.username} />
<AvatarFallback
className="ax-mono"
style={{ background: "rgba(0,255,255,0.08)", color: "#00ffff", fontSize: 11, borderRadius: 0 }}
>
{userInitials}
</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-52"
align="end"
style={{ background: "#080808", border: "1px solid rgba(0,255,255,0.2)", borderRadius: 0, zIndex: 99999 }}
>
<div className="ax-mono" style={{ padding: "10px 12px", borderBottom: "1px solid rgba(0,255,255,0.1)", fontSize: 10, color: "rgba(0,255,255,0.5)", letterSpacing: 1 }}>
{profile.full_name || profile.username}
</div>
{/* User items */}
<DropdownMenuItem asChild>
<Link to="/dashboard" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
<User className="mr-2 h-3.5 w-3.5" />Dashboard
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/profile" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
<UserCircle className="mr-2 h-3.5 w-3.5" />My Profile
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to={passportHref} className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
<Sparkles className="mr-2 h-3.5 w-3.5" />AeThex Passport
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link to="/dashboard?tab=settings" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
<Settings className="mr-2 h-3.5 w-3.5" />Settings
</Link>
</DropdownMenuItem>
{/* Internal section — staff/admin/owner only */}
{isInternal && (
<>
<DropdownMenuSeparator style={{ background: "rgba(0,255,255,0.08)" }} />
<div className="ax-mono" style={{ padding: "4px 12px 2px", fontSize: 9, color: "rgba(0,255,255,0.3)", letterSpacing: 2, textTransform: "uppercase" }}>
Internal
</div>
{isPrivileged && (
<DropdownMenuItem asChild>
<Link to="/admin" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
<Shield className="mr-2 h-3.5 w-3.5" />Admin
</Link>
</DropdownMenuItem>
)}
<DropdownMenuItem asChild>
<Link to="/internal-docs" className="cursor-pointer ax-mono" style={{ fontSize: 11 }}>
<BookOpen className="mr-2 h-3.5 w-3.5" />Internal Docs
</Link>
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator style={{ background: "rgba(0,255,255,0.1)" }} />
<DropdownMenuItem
className="cursor-pointer ax-mono"
style={{ fontSize: 11, color: "rgba(255,80,80,0.7)" }}
onClick={() => signOut()}
>
<LogOut className="mr-2 h-3.5 w-3.5" />Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<button
className="ax-mono"
onClick={() => signOut()}
style={{
background: "none", border: "none", cursor: "pointer",
fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
color: "rgba(255,80,80,0.6)", padding: "4px 8px",
}}
>
Sign out
</button>
)}
</div>
) : (
<div className="hidden sm:flex" style={{ alignItems: "center", gap: 12 }}>
<NavLink to="/login">Sign In</NavLink>
<Link
to="/onboarding"
className="ax-mono ax-clip"
style={{
fontSize: 10, letterSpacing: 2, textTransform: "uppercase",
border: "1px solid rgba(0,255,255,0.5)", color: "#00ffff",
padding: "8px 18px", textDecoration: "none",
background: "rgba(0,255,255,0.04)", transition: "all 0.2s",
display: "inline-block",
}}
onMouseEnter={e => {
e.currentTarget.style.background = "rgba(0,255,255,0.12)";
e.currentTarget.style.boxShadow = "0 0 16px rgba(0,255,255,0.2)";
}}
onMouseLeave={e => {
e.currentTarget.style.background = "rgba(0,255,255,0.04)";
e.currentTarget.style.boxShadow = "none";
}}
>
Join AeThex
</Link>
</div>
)}
</>
)}
{/* Mobile hamburger */}
<button
className="md:hidden"
onClick={() => setMobileOpen(o => !o)}
style={{ background: "none", border: "none", color: "rgba(0,255,255,0.6)", cursor: "pointer", padding: 4 }}
>
{mobileOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
</div>
</div>
{/* Mobile menu */}
{mobileOpen && (
<div style={{
borderTop: "1px solid rgba(0,255,255,0.1)",
background: "rgba(5,5,5,0.98)",
padding: "16px 24px",
display: "flex", flexDirection: "column", gap: 12,
}}>
{navLinks.map((item) => (
<Link
key={item.href}
to={item.href}
onClick={() => setMobileOpen(false)}
className="ax-mono"
style={{
fontSize: 11, letterSpacing: 2, textTransform: "uppercase",
color: "rgba(0,255,255,0.5)", textDecoration: "none", padding: "8px 0",
borderBottom: "1px solid rgba(0,255,255,0.06)",
}}
>
{item.name}
</Link>
))}
{!user && (
<Link
to="/onboarding"
onClick={() => setMobileOpen(false)}
className="ax-mono"
style={{
marginTop: 4, fontSize: 11, letterSpacing: 2, textTransform: "uppercase",
border: "1px solid rgba(0,255,255,0.4)", color: "#00ffff",
padding: "12px 16px", textDecoration: "none", textAlign: "center",
}}
>
Join AeThex
</Link>
)}
</div>
)}
</header>
{/* ── Content ─────────────────────────────────────────────────────────── */}
<main style={{ paddingTop: 80 }}>
{children}
</main>
{/* ── Footer ──────────────────────────────────────────────────────────── */}
{!hideFooter && (
<footer style={{
borderTop: "1px solid rgba(0,255,255,0.1)",
background: "rgba(3,3,3,0.95)",
marginTop: 80,
}}>
{/* TrinityBar */}
<div style={{ display: "flex", height: 2 }}>
<div style={{ flex: 1, background: "#ff003c", opacity: 0.3 }} />
<div style={{ flex: 1, background: "#00d4ff", opacity: 0.3 }} />
<div style={{ flex: 1, background: "#ffb800", opacity: 0.3 }} />
</div>
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "48px 24px 32px" }}>
<div style={{ display: "grid", gridTemplateColumns: "2fr 1fr 1fr 1fr 1fr", gap: 40, marginBottom: 48 }}>
{/* Brand column */}
<div>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
<HexLogo size={28} />
<span className="ax-orbitron" style={{ fontSize: 13, fontWeight: 700, letterSpacing: 4, color: "#00ffff" }}>
AETHEX
</span>
</div>
<p className="ax-mono" style={{ fontSize: 11, lineHeight: 1.8, color: "rgba(0,255,255,0.35)", letterSpacing: 1, maxWidth: 240 }}>
The integration layer connecting all metaverse platforms. Six specialized realms. One ecosystem.
</p>
<p className="ax-mono" style={{ fontSize: 10, color: "rgba(0,255,255,0.25)", marginTop: 16, letterSpacing: 1 }}>
Queen Creek, Arizona<br />
<a href="mailto:info@aethex.biz" style={{ color: "rgba(0,255,255,0.35)", textDecoration: "none" }}>info@aethex.biz</a>
</p>
</div>
{/* Link columns */}
{Object.entries(FOOTER_LINKS).map(([section, links]) => (
<div key={section}>
<p className="ax-orbitron" style={{ fontSize: 9, fontWeight: 700, letterSpacing: 3, color: "rgba(0,255,255,0.5)", textTransform: "uppercase", marginBottom: 16 }}>
{section}
</p>
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
{links.map((link) => (
<Link
key={link.href}
to={link.href}
className="ax-mono"
style={{ fontSize: 10, letterSpacing: 1, color: "rgba(0,255,255,0.3)", textDecoration: "none", transition: "color 0.2s" }}
onMouseEnter={e => (e.currentTarget.style.color = "rgba(0,255,255,0.7)")}
onMouseLeave={e => (e.currentTarget.style.color = "rgba(0,255,255,0.3)")}
>
{link.name}
</Link>
))}
</div>
</div>
))}
</div>
{/* Bottom bar */}
<div style={{
borderTop: "1px solid rgba(0,255,255,0.07)",
paddingTop: 20,
display: "flex", justifyContent: "space-between", alignItems: "center",
flexWrap: "wrap", gap: 12,
}}>
<span className="ax-mono" style={{ fontSize: 9, letterSpacing: 2, color: "rgba(0,255,255,0.25)", textTransform: "uppercase" }}>
© 2026 AeThex Corporation. All rights reserved.
</span>
<span className="ax-mono" style={{ fontSize: 9, letterSpacing: 2, color: "rgba(0,255,255,0.2)", textTransform: "uppercase" }}>
AeThex.OS v3.7.1 // aethex.dev
</span>
</div>
</div>
</footer>
)}
<AIChatButton />
</div>
);
}