Server Deploy / deploy (push) Successful in 1m6s
web/ was the last app on its own palette — a neutral #0f1117 ground with an indigo accent, unrelated to the logo navy that site/ and adminsite/ are built on. It now draws from the same tokens, so all three apps are one system. It stays locked to dark, taking site/'s dark values. That is what keeps adminsite/'s light default meaningful: an operator with both open tells them apart by the ground before clicking anything destructive, and now that both are the same palette, the ground is the only thing left doing that work. The colour names stay this app's own — text-primary, border, surface rather than ink, rule, panel — because every screen already reads that way, and adminsite/ already establishes that each app names the shared tokens after its own subject. Tokens are stored as RGB channels with the hex in a trailing comment. The console leans on Tailwind opacity modifiers far more than the other two (bg-danger/10, border-accent/50, ring-accent/30), and <alpha-value> only compiles against channels; the comments keep the three token blocks diffable by eye. Beyond colour: - radii collapse to site/'s 4px in tailwind.config.ts rather than rewriting ~140 rounded-lg classes; rounded-full is untouched for dots and pills - badges become site/'s chip — mono, uppercase, tracked, currentColor rule, no fill — keeping their dot so state is never colour alone - table column heads take the keyed-label idiom, at text-secondary rather than tertiary, which lands under 4.5:1 at that size - sidebar marks the active item with an accent bar, the device site/ uses for the chosen plan, instead of a filled pill that reads as pressable - filled accent and danger buttons take accent-ink; the dark accent is a light blue and danger a coral, and white on either was unreadable - login's packet pulses shift from green to the accent, so the sign-in screen is the same two blues as the marketing hero The last hex literals and stock-palette classes are gone; the only ones left are NetworkBackground's canvas fills, which cannot read a CSS variable and are commented with the token each came from. Only the web image rebuilds from this.
228 lines
12 KiB
TypeScript
228 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { clsx } from "clsx";
|
|
import { useAuth } from "@/components/AuthProvider";
|
|
import { auth } from "@/lib/api";
|
|
import { Logo } from "./Logo";
|
|
|
|
interface NavItem {
|
|
href: string;
|
|
label: string;
|
|
icon: React.ReactNode;
|
|
/** Restricted to owner/admin the roles the backing API requires. */
|
|
adminOnly?: boolean;
|
|
}
|
|
|
|
function ServerIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function KeyIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function SecretIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function WorkflowIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9 3.75H6.912a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.012 1.244h3.22a2.25 2.25 0 002.012-1.244l.256-.512a2.25 2.25 0 012.012-1.244h3.86M12 3v8.25m0 0l-3-3m3 3l3-3"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function AuditIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function LicenceIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function SettingsIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
|
/>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function MonitorIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3 12h4l2 6 4-14 2 8h6" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function StepsIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M6 6.75A.75.75 0 016.75 6h10.5a.75.75 0 010 1.5H6.75A.75.75 0 016 6.75zm0 5.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H6.75A.75.75 0 016 12zm0 5.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H6.75A.75.75 0 016 17.25zM3 6.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM3 12a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 5.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function InstanceIcon() {
|
|
return (
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
const navItems: NavItem[] = [
|
|
{ href: "/servers", label: "Servers", icon: <ServerIcon /> },
|
|
{ href: "/monitors", label: "Monitors", icon: <MonitorIcon /> },
|
|
{ href: "/keys", label: "SSH Keys", icon: <KeyIcon /> },
|
|
{ href: "/secrets", label: "Secrets", icon: <SecretIcon /> },
|
|
{ href: "/workflows", label: "Workflows", icon: <WorkflowIcon /> },
|
|
{ href: "/steps", label: "Steps", icon: <StepsIcon /> },
|
|
{ href: "/audit", label: "Audit Log", icon: <AuditIcon /> },
|
|
{ href: "/settings/instance", label: "Instance", icon: <InstanceIcon />, adminOnly: true },
|
|
{ href: "/settings/license", label: "Licence", icon: <LicenceIcon />, adminOnly: true },
|
|
{ href: "/settings", label: "Settings", icon: <SettingsIcon />, adminOnly: true },
|
|
];
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const { user, instance, isAdmin } = useAuth();
|
|
|
|
const visibleItems = navItems.filter((item) => !item.adminOnly || isAdmin);
|
|
|
|
const activeHref = visibleItems.reduce<string | null>((best, item) => {
|
|
const matches = pathname === item.href || pathname.startsWith(item.href + "/");
|
|
if (!matches) return best;
|
|
return best === null || item.href.length > best.length ? item.href : best;
|
|
}, null);
|
|
|
|
async function handleLogout() {
|
|
try {
|
|
await auth.logout();
|
|
} catch {}
|
|
window.location.href = "/login";
|
|
}
|
|
|
|
return (
|
|
<aside className="flex h-screen w-60 flex-col border-r border-border bg-surface">
|
|
<div className="flex h-16 items-center gap-3 border-b border-border px-5">
|
|
<Logo className="h-8 w-8 text-logo" />
|
|
<div className="min-w-0">
|
|
<span className="block text-base font-extrabold leading-tight tracking-[-0.035em] text-text-primary">Vantage</span>
|
|
{instance && (
|
|
<span className="block truncate font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">{instance.name}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="flex-1 overflow-y-auto px-3 py-4">
|
|
<ul className="space-y-1">
|
|
{visibleItems.map((item) => {
|
|
const isActive = activeHref === item.href;
|
|
return (
|
|
<li key={item.href}>
|
|
<Link
|
|
href={item.href}
|
|
// The active marker is an accent bar, the same device
|
|
// site/ uses to mark the chosen plan. A filled pill
|
|
// reads as a button you can press again.
|
|
className={clsx(
|
|
"relative flex items-center gap-3 rounded px-3 py-2.5 text-sm transition-colors",
|
|
isActive
|
|
? "bg-surface-2 font-semibold text-text-primary before:absolute before:inset-y-1 before:left-0 before:w-[2px] before:rounded-full before:bg-accent before:content-['']"
|
|
: "font-medium text-text-secondary hover:bg-surface-2 hover:text-text-primary",
|
|
)}
|
|
>
|
|
{item.icon}
|
|
{item.label}
|
|
</Link>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</nav>
|
|
|
|
<div className="border-t border-border px-4 py-3">
|
|
{user && (
|
|
<div className="mb-3">
|
|
<p className="truncate text-sm font-medium text-text-primary">{user.name || user.email}</p>
|
|
<p className="truncate text-xs text-text-secondary">
|
|
{user.email}
|
|
{user.role && <span className="ml-1 text-text-tertiary">· {user.role}</span>}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center justify-between">
|
|
<p className="font-mono text-[0.68rem] uppercase tracking-[0.1em] text-text-secondary">Vantage v1.0</p>
|
|
{user && (
|
|
<button type="button" onClick={handleLogout} className="text-xs text-text-secondary transition-colors hover:text-danger">
|
|
Logout
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|