feat: add marketing site and expand Proxmox management features
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
---
|
||||
// Static data for the mockup — compiled away at build time, zero runtime JS.
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', icon: 'grid', active: true },
|
||||
{ label: 'Nodes', icon: 'server', active: false },
|
||||
{ label: 'VMs', icon: 'box', active: false },
|
||||
{ label: 'Containers', icon: 'box', active: false },
|
||||
{ label: 'Storage', icon: 'hdd', active: false },
|
||||
{ label: 'Tasks', icon: 'todo', active: false },
|
||||
{ label: 'Backups', icon: 'shield', active: false },
|
||||
]
|
||||
|
||||
const clusterNodes = [
|
||||
{ name: 'pve1', dot: 'bg-success shadow-[0_0_6px] shadow-success/60', chip: 'primary' },
|
||||
{ name: 'pve2', dot: 'bg-warning', chip: null },
|
||||
{ name: 'pve3', dot: 'bg-success', chip: null },
|
||||
]
|
||||
|
||||
const nodeCards = [
|
||||
{
|
||||
name: 'pve1',
|
||||
dot: 'bg-success shadow-[0_0_6px] shadow-success/60',
|
||||
chip: { label: 'primary', cls: 'border-primary/30 bg-primary/10 text-primary' },
|
||||
statusLabel: null,
|
||||
fill: 'bg-primary',
|
||||
rows: [
|
||||
{ label: 'CPU', value: '42% · 3.4/8 c', width: '42%' },
|
||||
{ label: 'RAM', value: '10.2/32 GiB', width: '32%' },
|
||||
{ label: 'Disk', value: '187/500 GB', width: '37%' },
|
||||
],
|
||||
footer: ['up 42d 11h', '4 VMs'],
|
||||
},
|
||||
{
|
||||
name: 'pve2',
|
||||
dot: 'bg-warning',
|
||||
chip: null,
|
||||
statusLabel: 'failover',
|
||||
statusCls: 'text-warning',
|
||||
fill: 'bg-warning',
|
||||
rows: [
|
||||
{ label: 'CPU', value: '31% · 2.5/8 c', width: '31%' },
|
||||
{ label: 'RAM', value: '7.1/32 GiB', width: '22%' },
|
||||
{ label: 'Disk', value: '163/500 GB', width: '33%' },
|
||||
],
|
||||
footer: ['up 9d 4h', '2 VMs'],
|
||||
},
|
||||
{
|
||||
name: 'pve3',
|
||||
dot: 'bg-success shadow-[0_0_6px] shadow-success/60',
|
||||
chip: null,
|
||||
statusLabel: 'online',
|
||||
statusCls: 'text-success',
|
||||
fill: 'bg-primary',
|
||||
rows: [
|
||||
{ label: 'CPU', value: '18% · 1.4/8 c', width: '18%' },
|
||||
{ label: 'RAM', value: '5.8/16 GiB', width: '36%' },
|
||||
{ label: 'Disk', value: '121/240 GB', width: '50%' },
|
||||
],
|
||||
footer: ['up 117d 2h', '3 VMs'],
|
||||
},
|
||||
]
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
chip: { label: 'RUNNING', cls: 'border-warning/25 bg-warning/10 text-warning' },
|
||||
text: 'start VM 100',
|
||||
node: 'pve1',
|
||||
time: '12s ago',
|
||||
},
|
||||
{
|
||||
chip: { label: 'OK', cls: 'border-success/25 bg-success/10 text-success' },
|
||||
text: 'backup VM 101 → local-lvm',
|
||||
node: 'pve2',
|
||||
time: '3m ago',
|
||||
},
|
||||
{
|
||||
chip: { label: 'OK', cls: 'border-success/25 bg-success/10 text-success' },
|
||||
text: 'migrate VM 102',
|
||||
node: 'pve1 → pve3',
|
||||
time: '18m ago',
|
||||
},
|
||||
{
|
||||
chip: { label: 'DONE', cls: 'border-border bg-muted text-muted-foreground' },
|
||||
text: 'update apt',
|
||||
node: 'pve3',
|
||||
time: '1h ago',
|
||||
},
|
||||
]
|
||||
|
||||
const gauges = [
|
||||
{ label: 'CPU', icon: 'cpu', percent: '30%', width: '30%', detail: '7.3 / 24 cores' },
|
||||
{ label: 'RAM', icon: 'mem', percent: '24%', width: '24%', detail: '23.1 / 96 GiB' },
|
||||
]
|
||||
|
||||
const quickActions = [
|
||||
{ label: 'Start', icon: 'play' },
|
||||
{ label: 'Stop', icon: 'stop' },
|
||||
{ label: 'Migrate', icon: 'migrate' },
|
||||
{ label: 'Console', icon: 'term' },
|
||||
]
|
||||
---
|
||||
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Clustri desktop app showing the cluster dashboard"
|
||||
class="w-full select-none"
|
||||
>
|
||||
<!-- Inline icon library (rendered once, referenced via <use>) -->
|
||||
<svg class="absolute h-0 w-0 overflow-hidden" aria-hidden="true">
|
||||
<defs>
|
||||
<symbol id="i-hex" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
</symbol>
|
||||
<symbol id="i-grid" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="7" height="9" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="5" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="9" x="14" y="12" rx="1" />
|
||||
<rect width="7" height="5" x="3" y="16" rx="1" />
|
||||
</symbol>
|
||||
<symbol id="i-server" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="20" height="8" x="2" y="2" rx="2" />
|
||||
<rect width="20" height="8" x="2" y="14" rx="2" />
|
||||
<line x1="6" x2="6.01" y1="6" y2="6" />
|
||||
<line x1="6" x2="6.01" y1="18" y2="18" />
|
||||
</symbol>
|
||||
<symbol id="i-box" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z" />
|
||||
<path d="m3.3 7 8.7 5 8.7-5" />
|
||||
<path d="M12 22V12" />
|
||||
</symbol>
|
||||
<symbol id="i-hdd" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" x2="2" y1="12" y2="12" />
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
|
||||
<line x1="6" x2="6.01" y1="16" y2="16" />
|
||||
<line x1="10" x2="10.01" y1="16" y2="16" />
|
||||
</symbol>
|
||||
<symbol id="i-todo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="6" height="6" x="3" y="5" rx="1" />
|
||||
<path d="m3 17 2 2 4-4" />
|
||||
<path d="M13 6h8" />
|
||||
<path d="M13 12h8" />
|
||||
<path d="M13 18h8" />
|
||||
</symbol>
|
||||
<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1 1 0 0 1 1.52 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z" />
|
||||
</symbol>
|
||||
<symbol id="i-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</symbol>
|
||||
<symbol id="i-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</symbol>
|
||||
<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</symbol>
|
||||
<symbol id="i-cpu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="16" height="16" x="4" y="4" rx="2" />
|
||||
<rect width="6" height="6" x="9" y="9" rx="1" />
|
||||
<path d="M15 2v2" />
|
||||
<path d="M15 20v2" />
|
||||
<path d="M2 15h2" />
|
||||
<path d="M2 9h2" />
|
||||
<path d="M20 15h2" />
|
||||
<path d="M20 9h2" />
|
||||
<path d="M9 2v2" />
|
||||
<path d="M9 20v2" />
|
||||
</symbol>
|
||||
<symbol id="i-mem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6 19v-3" />
|
||||
<path d="M10 19v-3" />
|
||||
<path d="M14 19v-3" />
|
||||
<path d="M18 19v-3" />
|
||||
<path d="M8 11V9" />
|
||||
<path d="M16 11V9" />
|
||||
<path d="M12 11V9" />
|
||||
<path d="M2 15h20" />
|
||||
<path d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v1.1a2 2 0 0 0 0 3.837V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5.1a2 2 0 0 0 0-3.837Z" />
|
||||
</symbol>
|
||||
<symbol id="i-activity" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</symbol>
|
||||
<symbol id="i-play" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="6 3 20 12 6 21 6 3" />
|
||||
</symbol>
|
||||
<symbol id="i-stop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="10" height="10" x="7" y="7" rx="1" />
|
||||
</symbol>
|
||||
<symbol id="i-migrate" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m16 3 4 4-4 4" />
|
||||
<path d="M20 7H4" />
|
||||
<path d="m8 21-4-4 4-4" />
|
||||
<path d="M4 17h16" />
|
||||
</symbol>
|
||||
<symbol id="i-term" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5" />
|
||||
<line x1="12" x2="20" y1="19" y2="19" />
|
||||
</symbol>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<!-- Window frame -->
|
||||
<div class="overflow-hidden rounded-lg border border-border bg-card shadow-card">
|
||||
<!-- Title bar -->
|
||||
<div class="relative flex h-9 items-center border-b border-border bg-background">
|
||||
<div class="absolute left-4 flex items-center gap-1.5" aria-hidden="true">
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
</div>
|
||||
<p class="w-full text-center font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
Clustri — Ops Console
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Failover banner -->
|
||||
<div class="flex items-center gap-2 border-b border-warning/25 bg-warning/10 px-4 py-1.5">
|
||||
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-warning" aria-hidden="true"></span>
|
||||
<p class="shrink-0 font-mono text-[10px] text-warning">Operating on a fallback node</p>
|
||||
<p class="ml-auto min-w-0 truncate font-mono text-[10px] tabular-nums text-warning/80">
|
||||
https://192.168.1.12:8006
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex">
|
||||
<!-- Sidebar -->
|
||||
<aside class="hidden w-52 shrink-0 flex-col border-r border-border bg-card sm:flex">
|
||||
<!-- Brand header -->
|
||||
<div class="flex h-14 shrink-0 items-center gap-2.5 border-b border-border px-4">
|
||||
<div class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-primary/15 text-primary">
|
||||
<svg class="h-4 w-4" aria-hidden="true"><use href="#i-hex" /></svg>
|
||||
</div>
|
||||
<div class="min-w-0 leading-tight">
|
||||
<p class="truncate text-sm font-semibold tracking-tight text-foreground">Clustri</p>
|
||||
<p class="font-mono text-[9px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Ops Console
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="min-h-0 flex-1 p-2">
|
||||
<div class="space-y-1">
|
||||
<!-- Active connection -->
|
||||
<div class="flex items-center gap-2.5 rounded-md bg-accent px-3 py-2">
|
||||
<span
|
||||
class="h-1.5 w-1.5 shrink-0 rounded-full bg-success shadow-[0_0_6px] shadow-success/60"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="truncate font-mono text-[13px] font-medium text-accent-foreground">home-lab</span>
|
||||
</div>
|
||||
|
||||
<!-- Sub-nav -->
|
||||
<div class="ml-4 mt-1 space-y-0.5">
|
||||
{
|
||||
navItems.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
class:list={[
|
||||
'relative flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[12px] font-medium transition-colors',
|
||||
item.active
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
|
||||
]}
|
||||
>
|
||||
{item.active && (
|
||||
<span
|
||||
class="absolute left-0 top-1/2 h-4 w-0.5 -translate-y-1/2 rounded-full bg-primary"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
)}
|
||||
<svg class="h-4 w-4 shrink-0" aria-hidden="true"><use href={`#i-${item.icon}`} /></svg>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Second connection (offline) -->
|
||||
<div class="mt-2">
|
||||
<div class="flex items-center gap-2.5 rounded-md px-3 py-2">
|
||||
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/40" aria-hidden="true"></span>
|
||||
<span class="truncate font-mono text-[13px] font-medium text-muted-foreground">office-prod</span>
|
||||
</div>
|
||||
<p class="ml-7 font-mono text-[10px] text-muted-foreground">Offline</p>
|
||||
</div>
|
||||
|
||||
<!-- Cluster nodes -->
|
||||
<p class="px-3 pb-1 pt-3 font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Cluster nodes
|
||||
</p>
|
||||
<div class="space-y-0.5">
|
||||
{
|
||||
clusterNodes.map((n) => (
|
||||
<div class="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5">
|
||||
<span class={`h-1.5 w-1.5 shrink-0 rounded-full ${n.dot}`} aria-hidden="true"></span>
|
||||
<span class="min-w-0 flex-1 truncate font-mono text-[13px] font-medium text-muted-foreground">
|
||||
{n.name}
|
||||
</span>
|
||||
{n.chip && (
|
||||
<span class="shrink-0 rounded border border-primary/30 bg-primary/10 px-1 py-px font-mono text-[9px] uppercase text-primary">
|
||||
{n.chip}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar footer -->
|
||||
<div class="space-y-1 border-t border-border p-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2.5 rounded-md px-3 py-1.5 text-[12px] font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||
>
|
||||
<svg class="h-4 w-4 shrink-0" aria-hidden="true"><use href="#i-gear" /></svg>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-md border border-border bg-background px-3 py-1.5 text-[12px] font-medium text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5 shrink-0" aria-hidden="true"><use href="#i-plus" /></svg>
|
||||
<span>Add Connection</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main area -->
|
||||
<main class="min-w-0 flex-1 bg-background p-4">
|
||||
<div class="space-y-4">
|
||||
<!-- Header row -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="text-base font-semibold tracking-tight">Overview</h2>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
home-lab · 192.168.1.10:8006
|
||||
</span>
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
3 nodes · 2 online
|
||||
</span>
|
||||
<span class="rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] text-success">
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node health grid -->
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{
|
||||
nodeCards.map((node) => (
|
||||
<article class="rounded-md border border-border bg-card p-3">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class={`h-1.5 w-1.5 shrink-0 rounded-full ${node.dot}`} aria-hidden="true"></span>
|
||||
<h3 class="truncate font-mono text-[13px] font-semibold tracking-tight text-foreground">
|
||||
{node.name}
|
||||
</h3>
|
||||
</div>
|
||||
{node.chip && (
|
||||
<span class={`shrink-0 rounded border px-1 py-px font-mono text-[9px] uppercase ${node.chip.cls}`}>
|
||||
{node.chip.label}
|
||||
</span>
|
||||
)}
|
||||
{node.statusLabel && (
|
||||
<span class={`shrink-0 font-mono text-[9px] font-medium uppercase ${node.statusCls}`}>
|
||||
{node.statusLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div class="mt-3 space-y-2.5">
|
||||
{
|
||||
node.rows.map((row) => (
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-[9px] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
{row.label}
|
||||
</span>
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
{row.value}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-1 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div class={`h-full rounded-full ${node.fill}`} style={`width: ${row.width}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="mt-3 flex items-center justify-between border-t pt-2 font-mono text-[9px] tabular-nums text-muted-foreground">
|
||||
<span>{node.footer[0]}</span>
|
||||
<span>{node.footer[1]}</span>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Lower row: activity feed + resource gauges -->
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-5">
|
||||
<section class="rounded-md border border-border bg-card sm:col-span-3">
|
||||
<header class="flex items-center gap-2 border-b border-border px-3 py-2">
|
||||
<svg class="h-3.5 w-3.5 shrink-0 text-muted-foreground" aria-hidden="true"><use href="#i-activity" /></svg>
|
||||
<h3 class="font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Task activity
|
||||
</h3>
|
||||
</header>
|
||||
<div class="divide-y divide-border">
|
||||
{
|
||||
tasks.map((task) => (
|
||||
<div class="flex items-center justify-between gap-3 px-3 py-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class={`shrink-0 rounded border px-1.5 py-0.5 font-mono text-[9px] font-medium ${task.chip.cls}`}>
|
||||
{task.chip.label}
|
||||
</span>
|
||||
<p class="truncate font-mono text-[11px] tabular-nums text-foreground">
|
||||
{task.text}
|
||||
<span class="text-muted-foreground"> · {task.node}</span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="shrink-0 font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
{task.time}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="space-y-3 sm:col-span-2">
|
||||
{
|
||||
gauges.map((gauge) => (
|
||||
<section class="rounded-md border border-border bg-card p-3">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg class="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true"><use href={`#i-${gauge.icon}`} /></svg>
|
||||
<span class="font-mono text-[9px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
||||
{gauge.label}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-end justify-between gap-2">
|
||||
<span class="font-mono text-2xl font-semibold leading-none tracking-tight tabular-nums">
|
||||
{gauge.percent}
|
||||
</span>
|
||||
<span class="font-mono text-[10px] tabular-nums text-muted-foreground">
|
||||
{gauge.detail}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full rounded-full bg-primary" style={`width: ${gauge.width}`}></div>
|
||||
</div>
|
||||
</section>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{
|
||||
quickActions.map((action) => (
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-1.5 rounded border border-border px-3 py-1.5 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
||||
>
|
||||
<svg class="h-3 w-3" aria-hidden="true"><use href={`#i-${action.icon}`} /></svg>
|
||||
<span>{action.label}</span>
|
||||
</button>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
ArchitectureStrip — the request path from React to the Proxmox API.
|
||||
|
||||
Three sequential stages: React frontend → Rust backend → Proxmox API.
|
||||
The middle stage carries the brand accent (border-primary/30,
|
||||
bg-primary/10) so the pipeline reads left-to-right, not as three equal
|
||||
cards. Below lg the stages stack with ↓ connectors between them.
|
||||
|
||||
Design constraints (see src/styles/global.css):
|
||||
- Dark-only surfaces: bg-background / bg-card; muted-orange text-primary is
|
||||
the single brand accent.
|
||||
- Green / amber / red are reserved for status semantics (success /
|
||||
warning / destructive) only.
|
||||
- No gradients, no glows, no glassmorphism, no emoji, no fake metrics.
|
||||
-->
|
||||
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<section
|
||||
id="architecture"
|
||||
aria-labelledby="architecture-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
ARCHITECTURE
|
||||
</p>
|
||||
<h2
|
||||
id="architecture-title"
|
||||
class="mt-4 text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
One proxy between you and the API.
|
||||
</h2>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
The frontend calls Rust commands over Tauri IPC. The backend proxies them
|
||||
to the Proxmox API over HTTPS.
|
||||
</p>
|
||||
|
||||
<!--
|
||||
3-stage pipeline: React frontend → Rust backend → Proxmox API.
|
||||
On lg the grid is 1fr / auto / 1fr / auto / 1fr with centered arrows;
|
||||
below lg the stages stack with ↓ connectors between them.
|
||||
-->
|
||||
<div class="mt-10 grid gap-3 lg:grid-cols-[1fr_auto_1fr_auto_1fr] lg:items-center">
|
||||
<!-- Stage 01 · React frontend -->
|
||||
<div class="rounded-lg border border-border bg-card p-5">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<p class="text-sm font-semibold text-foreground">React frontend</p>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">01</span>
|
||||
</div>
|
||||
<div class="mt-1.5 space-y-1 font-mono text-[11px] text-muted-foreground">
|
||||
<p>TanStack Query · Zustand</p>
|
||||
<p>Geist · JetBrains Mono</p>
|
||||
<p>React 19</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow · tauri invoke -->
|
||||
<div class="hidden flex-col items-center justify-center gap-1 lg:flex" aria-hidden="true">
|
||||
<span class="text-xl text-primary">→</span>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">tauri invoke</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-1.5 py-2 lg:hidden" aria-hidden="true">
|
||||
<span class="text-xl text-primary">↓</span>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">tauri invoke</span>
|
||||
</div>
|
||||
|
||||
<!-- Stage 02 · Rust backend (emphasized core) -->
|
||||
<div class="rounded-lg border border-primary/30 bg-primary/10 p-5">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<p class="text-sm font-semibold text-primary">Rust backend</p>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">02</span>
|
||||
</div>
|
||||
<div class="mt-1.5 space-y-1 font-mono text-[11px] text-primary/80">
|
||||
<p>ConnectionManager</p>
|
||||
<p>endpoint rotation · retry</p>
|
||||
<p>TOFU cert pinning</p>
|
||||
<p>credentials in OS keyring</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow · HTTPS -->
|
||||
<div class="hidden flex-col items-center justify-center gap-1 lg:flex" aria-hidden="true">
|
||||
<span class="text-xl text-primary">→</span>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">HTTPS</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-1.5 py-2 lg:hidden" aria-hidden="true">
|
||||
<span class="text-xl text-primary">↓</span>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">HTTPS</span>
|
||||
</div>
|
||||
|
||||
<!-- Stage 03 · Proxmox API -->
|
||||
<div class="rounded-lg border border-border bg-card p-5">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<p class="text-sm font-semibold text-foreground">Proxmox API</p>
|
||||
<span class="font-mono text-[9px] text-muted-foreground">03</span>
|
||||
</div>
|
||||
<div class="mt-1.5 space-y-1 font-mono text-[11px] text-muted-foreground">
|
||||
<p>/api2/json</p>
|
||||
<p>nodes · cluster · vms</p>
|
||||
<p>self-signed TLS</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request example + auth note, and resilience note -->
|
||||
<div class="mt-8 grid gap-6 lg:grid-cols-2 lg:items-stretch">
|
||||
<!-- Request example -->
|
||||
<div class="overflow-x-auto rounded-lg border border-border bg-card p-5">
|
||||
<p class="font-mono text-[10px] text-muted-foreground">
|
||||
POST /nodes/pve/qemu/100/status/start
|
||||
</p>
|
||||
<p class="mt-3 whitespace-nowrap font-mono text-[13px] tabular-nums text-foreground">
|
||||
https://192.168.1.10:8006/api2/json/nodes/pve/qemu/100/status/start
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<span class="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
PVEAPIToken
|
||||
</span>
|
||||
<span class="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground">
|
||||
PVEAuthCookie + CSRFPreventionToken
|
||||
</span>
|
||||
<span class="font-mono text-[10px] text-muted-foreground">
|
||||
Headers injected by the backend
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resilience note -->
|
||||
<div class="lg:pt-5">
|
||||
<p class="font-mono text-[10px] text-muted-foreground">failover model</p>
|
||||
<p class="mt-3 text-sm leading-relaxed text-muted-foreground">
|
||||
Requests ride an ordered, deduplicated endpoint list: primary,
|
||||
fallbacks, then discovered cluster nodes. A connect, timeout, DNS, or
|
||||
transport failure retries on the next endpoint.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-4 font-mono text-[10px]">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-success" aria-hidden="true"></span>
|
||||
<span class="text-muted-foreground">connected</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-warning" aria-hidden="true"></span>
|
||||
<span class="text-muted-foreground">failover</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-destructive" aria-hidden="true"></span>
|
||||
<span class="text-muted-foreground">failed</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
<!--
|
||||
DownloadCta — closing section, one centered column (the page's closing
|
||||
statement). Id anchors the Download link in the nav and footer.
|
||||
-->
|
||||
|
||||
<section id="download" class="scroll-mt-24 py-24 sm:py-32">
|
||||
<div class="mx-auto max-w-3xl px-6 text-center">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
RELEASES
|
||||
</p>
|
||||
<h2 class="mt-4 text-3xl font-semibold tracking-tighter text-foreground sm:text-4xl">
|
||||
Releases coming soon.
|
||||
</h2>
|
||||
<p class="mx-auto mt-4 max-w-[46ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Clustri is in active development. Packages for Linux, macOS, and Windows are on the way.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
Get Clustri
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition hover:bg-accent/60"
|
||||
>
|
||||
Build from source
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center justify-center gap-2">
|
||||
<span class="rounded border border-border bg-muted px-2.5 py-1 font-mono text-[11px] text-muted-foreground">
|
||||
Linux
|
||||
</span>
|
||||
<span class="rounded border border-border bg-muted px-2.5 py-1 font-mono text-[11px] text-muted-foreground">
|
||||
macOS
|
||||
</span>
|
||||
<span class="rounded border border-border bg-muted px-2.5 py-1 font-mono text-[11px] text-muted-foreground">
|
||||
Windows
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close the section, consistent with the hero's closing hairline -->
|
||||
<div class="mt-16 border-b border-border" aria-hidden="true"></div>
|
||||
</section>
|
||||
@@ -0,0 +1,336 @@
|
||||
<!--
|
||||
FeatureBlocks — the five feature sections of the Clustri marketing site.
|
||||
|
||||
Design constraints (see src/styles/global.css):
|
||||
- Dark-only surfaces: bg-background / bg-card; muted-orange text-primary is
|
||||
the single brand accent.
|
||||
- Green / amber are reserved for status semantics (success / warning) only.
|
||||
- No gradients, no glows, no glassmorphism, no emoji, no fake metrics.
|
||||
- No uppercase eyebrows: section identity is the mono index (01–05) beside
|
||||
each heading.
|
||||
All visuals are hand-built schematic markup, never screenshots.
|
||||
-->
|
||||
|
||||
<div id="features" class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<!--
|
||||
01 · FAILOVER — split text/diagram.
|
||||
Below lg the grid collapses to a single column (copy first, stack after).
|
||||
-->
|
||||
<section
|
||||
id="failover"
|
||||
aria-labelledby="features-failover-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="grid gap-10 lg:grid-cols-2 lg:items-center">
|
||||
<div>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">01</span>
|
||||
<h2
|
||||
id="features-failover-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Failover that happens on its own
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Requests ride an ordered endpoint list: primary, fallbacks, then
|
||||
discovered cluster nodes. When one dies, the next takes over, and the
|
||||
status flips to failover so you always know which node answered.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Ordered endpoint stack: primary, fallback, then discovered cluster node, with a failover status bar"
|
||||
class="select-none rounded-lg border border-border bg-card p-5"
|
||||
>
|
||||
<p class="font-mono text-[10px] text-muted-foreground">
|
||||
request → endpoints[0] → retry on failure
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-3">
|
||||
<span class="min-w-0 truncate font-mono text-[13px] tabular-nums text-foreground">
|
||||
192.168.1.10:8006
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded border border-primary/30 bg-primary/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-primary"
|
||||
>
|
||||
Primary
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-3">
|
||||
<span class="min-w-0 truncate font-mono text-[13px] tabular-nums text-foreground">
|
||||
192.168.1.11:8006
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] uppercase text-muted-foreground"
|
||||
>
|
||||
Fallback
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-3 last:border-0">
|
||||
<span class="min-w-0 truncate font-mono text-[13px] tabular-nums text-foreground">
|
||||
pve3.discovery
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] uppercase text-muted-foreground"
|
||||
>
|
||||
Cluster
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="font-mono text-[10px] text-muted-foreground">
|
||||
status: failover
|
||||
</span>
|
||||
<div class="mt-1.5 h-1 overflow-hidden rounded-full bg-muted">
|
||||
<div class="h-full w-1/2 rounded-full bg-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
02 · SECURITY — full-width editorial band.
|
||||
Single column on every breakpoint; the fingerprint block is the visual
|
||||
and spans full width up to max-w-2xl.
|
||||
-->
|
||||
<section
|
||||
id="security"
|
||||
aria-labelledby="features-security-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">02</span>
|
||||
<h2
|
||||
id="features-security-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Credentials never touch disk
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
API tokens live in the OS keyring. Self-signed certificates are pinned by
|
||||
SHA-256 fingerprint on first connect and verified on every connect after.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 max-w-2xl rounded-lg border border-border bg-card p-5">
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<span class="font-mono text-[10px] text-muted-foreground">
|
||||
SHA-256 fingerprint
|
||||
</span>
|
||||
<span
|
||||
class="rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success"
|
||||
>
|
||||
Pinned
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-4 overflow-x-auto">
|
||||
<p class="whitespace-nowrap font-mono text-sm tracking-[0.18em] text-foreground">
|
||||
A3:0F:71:C9:52:B8:4D:E0:11:2A:7F:9C:63:D4:18:0B
|
||||
</p>
|
||||
</div>
|
||||
<p class="mt-3 font-mono text-[10px] text-muted-foreground">
|
||||
Example fingerprint · captured on first connect, verified on every connect after.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
03 · VMS — asymmetric split (5/7).
|
||||
Below lg the grid collapses to a single column (copy first, list after).
|
||||
-->
|
||||
<section
|
||||
id="vms"
|
||||
aria-labelledby="features-vms-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="grid gap-10 lg:grid-cols-12 lg:items-start">
|
||||
<div class="lg:col-span-5">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">03</span>
|
||||
<h2
|
||||
id="features-vms-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
The full VM lifecycle in one place
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Start, stop, shutdown, reboot, suspend, resume, migrate. Plus disks,
|
||||
NICs, snapshots, backup jobs, and restore.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="lg:col-span-7">
|
||||
<p class="mb-3 font-mono text-[10px] text-muted-foreground">
|
||||
migrate 102 <span class="text-muted-foreground/70">→</span> pve3
|
||||
</p>
|
||||
<div
|
||||
role="img"
|
||||
aria-label="VM list: VM 100 web-01 running, VM 101 db-01 running, VM 102 worker-01 stopped"
|
||||
class="select-none rounded-lg border border-border bg-card"
|
||||
>
|
||||
<div class="flex items-center justify-between border-b border-border px-4 py-2.5">
|
||||
<span class="font-mono text-[10px] uppercase text-muted-foreground">
|
||||
VM ID
|
||||
</span>
|
||||
<span class="font-mono text-[10px] uppercase text-muted-foreground">
|
||||
Status
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-2.5">
|
||||
<span class="min-w-0 font-mono text-[13px] tabular-nums text-foreground">
|
||||
100<span class="text-muted-foreground"> · web-01</span>
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-2.5">
|
||||
<span class="min-w-0 font-mono text-[13px] tabular-nums text-foreground">
|
||||
101<span class="text-muted-foreground"> · db-01</span>
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded border border-success/25 bg-success/10 px-1.5 py-0.5 font-mono text-[10px] uppercase text-success"
|
||||
>
|
||||
Running
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border px-4 py-2.5 last:border-0">
|
||||
<span class="min-w-0 font-mono text-[13px] tabular-nums text-foreground">
|
||||
102<span class="text-muted-foreground"> · worker-01</span>
|
||||
</span>
|
||||
<span
|
||||
class="shrink-0 rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] uppercase text-muted-foreground"
|
||||
>
|
||||
Stopped
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
04 · CONSOLE — full-width terminal.
|
||||
Single column on every breakpoint; the terminal window spans full width
|
||||
up to max-w-3xl.
|
||||
-->
|
||||
<section
|
||||
id="console"
|
||||
aria-labelledby="features-console-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">04</span>
|
||||
<h2
|
||||
id="features-console-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Consoles that behave like the real thing
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
noVNC for VMs, xterm.js for containers. Fullscreen, keyboard passthrough,
|
||||
and Ctrl+Alt+Del where you expect it.
|
||||
</p>
|
||||
|
||||
<div
|
||||
role="img"
|
||||
aria-label="Terminal session inside a container: pct enter 101, uptime, and its output"
|
||||
class="mt-8 max-w-3xl select-none overflow-hidden rounded-lg border border-border bg-card shadow-card"
|
||||
>
|
||||
<div class="relative flex items-center border-b border-border bg-background px-4 py-2">
|
||||
<div class="absolute left-4 flex items-center gap-1.5" aria-hidden="true">
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
<span class="h-2.5 w-2.5 rounded-full border border-border bg-muted-foreground/40"></span>
|
||||
</div>
|
||||
<p class="w-full text-center font-mono text-[10px] text-muted-foreground">
|
||||
root@pve1: ~ (xterm)
|
||||
</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto bg-background px-4 py-4 font-mono text-[12px] leading-relaxed">
|
||||
<p>
|
||||
<span class="text-primary">root@pve1:~#</span>
|
||||
<span class="text-foreground">pct enter 101</span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="text-primary">root@db-01:~#</span>
|
||||
<span class="text-foreground">uptime</span>
|
||||
</p>
|
||||
<!-- indentation survives Astro's whitespace trimming -->
|
||||
<p class="whitespace-pre text-muted-foreground"> 23:41:05 up 9 days, 4:21, 1 user, load average: 0.42, 0.38, 0.35</p>
|
||||
<p>
|
||||
<span class="text-primary">root@db-01:~#</span>
|
||||
<span class="animate-pulse text-foreground">█</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<kbd
|
||||
class="rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
Ctrl+Alt+Del
|
||||
</kbd>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
05 · POWER USER — shortcut sheet.
|
||||
Two columns on sm and up, single column below sm.
|
||||
-->
|
||||
<section
|
||||
id="power-user"
|
||||
aria-labelledby="features-power-user-title"
|
||||
class="scroll-mt-24 py-16 sm:py-20"
|
||||
>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-mono text-sm text-primary">05</span>
|
||||
<h2
|
||||
id="features-power-user-title"
|
||||
class="text-2xl font-semibold tracking-tighter text-foreground sm:text-3xl"
|
||||
>
|
||||
Built for the daily grind
|
||||
</h2>
|
||||
</div>
|
||||
<p class="mt-3 max-w-[58ch] text-[15px] leading-relaxed text-muted-foreground">
|
||||
Tasks and events stream over WebSocket, with polling as the fallback.
|
||||
Cmd/Ctrl+K opens the command palette. The tray menu keeps one-click
|
||||
actions one click away.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 grid gap-x-12 gap-y-0 border-t border-border sm:grid-cols-2">
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<kbd
|
||||
class="shrink-0 rounded border border-border bg-muted px-2 py-0.5 font-mono text-[10px] text-muted-foreground"
|
||||
>
|
||||
Cmd/Ctrl + K
|
||||
</kbd>
|
||||
<p class="text-right text-sm text-muted-foreground">Command palette</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<span class="shrink-0 font-mono text-[11px] text-primary">WebSocket</span>
|
||||
<p class="text-right text-sm text-muted-foreground">
|
||||
Live task & event stream
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<span class="shrink-0 font-mono text-[11px] text-primary">Tray menu</span>
|
||||
<p class="text-right text-sm text-muted-foreground">
|
||||
Show / hide · connections · quit
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 border-b border-border py-4">
|
||||
<span class="shrink-0 font-mono text-[11px] text-primary">10s</span>
|
||||
<p class="text-right text-sm text-muted-foreground">
|
||||
Connection status polling
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
const year = new Date().getFullYear()
|
||||
---
|
||||
|
||||
<!--
|
||||
Footer — minimal, one row on desktop, stacked on mobile.
|
||||
-->
|
||||
|
||||
<footer class="border-t border-border">
|
||||
<div class="mx-auto max-w-7xl px-6 py-10 lg:px-8">
|
||||
<div class="flex flex-col gap-8 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-primary/15 text-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-sm font-semibold tracking-tight text-foreground">Clustri</span>
|
||||
<span class="hidden font-mono text-[9px] uppercase tracking-[0.14em] text-muted-foreground sm:inline">
|
||||
Ops Console
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-3 font-mono text-[11px] text-muted-foreground">v0.1.0</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8 sm:flex-row sm:gap-16">
|
||||
<nav aria-label="Navigate" class="space-y-2.5">
|
||||
<p class="text-[11px] font-mono uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Navigate
|
||||
</p>
|
||||
<a href="#failover" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Features
|
||||
</a>
|
||||
<a href="#security" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Security
|
||||
</a>
|
||||
<a href="#console" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Console
|
||||
</a>
|
||||
<a href="#architecture" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Architecture
|
||||
</a>
|
||||
<a href="#download" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Download
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<nav aria-label="Source" class="space-y-2.5">
|
||||
<p class="text-[11px] font-mono uppercase tracking-[0.14em] text-muted-foreground">
|
||||
Source
|
||||
</p>
|
||||
<a href="#" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
GitHub
|
||||
</a>
|
||||
<a href="#" class="block text-[13px] text-muted-foreground transition-colors hover:text-foreground">
|
||||
Build from source
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-2 border-t border-border pt-6 md:flex-row md:items-center md:justify-between">
|
||||
<p class="text-[12px] text-muted-foreground">© {year} Clustri. MIT license.</p>
|
||||
<p class="text-[12px] text-muted-foreground">Not affiliated with Proxmox.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
import AppWindowMockup from './AppWindowMockup.astro'
|
||||
---
|
||||
|
||||
<section class="relative overflow-hidden">
|
||||
<!-- Copy block -->
|
||||
<div class="hero-copy mx-auto max-w-7xl px-6 pt-20 lg:px-8 lg:pt-24">
|
||||
<p class="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">
|
||||
OPS CONSOLE
|
||||
</p>
|
||||
<h1 class="mt-4 max-w-3xl text-4xl font-semibold leading-[1.05] tracking-tighter text-foreground sm:text-5xl lg:text-6xl">
|
||||
One <span class="text-primary">console</span> for your Proxmox cluster.
|
||||
</h1>
|
||||
<p class="mt-5 max-w-2xl text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||
Manage Proxmox VE nodes, VMs, storage, and backups from a desktop app that keeps working when a node goes down.
|
||||
</p>
|
||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
Get Clustri
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md border border-border bg-background px-5 py-2.5 text-sm font-semibold text-foreground transition hover:bg-accent/60 active:scale-[0.98]"
|
||||
>
|
||||
Read the docs
|
||||
</a>
|
||||
</div>
|
||||
<p class="mt-8 font-mono text-[11px] tabular-nums text-muted-foreground">
|
||||
<span>v0.1.0</span><span aria-hidden="true"> · </span><span>Linux</span><span aria-hidden="true"> · </span><span>macOS</span><span aria-hidden="true"> · </span><span>Windows</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Product mockup, full-bleed below the copy -->
|
||||
<div class="hero-mockup mt-16 sm:mt-20">
|
||||
<div class="mx-auto max-w-6xl px-6 lg:px-8">
|
||||
<div class="pointer-events-none select-none [mask-image:linear-gradient(to_bottom,black_0%,black_78%,transparent_100%)]">
|
||||
<AppWindowMockup />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Close the hero -->
|
||||
<div class="mt-16 border-b border-border" aria-hidden="true"></div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes hero-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hero-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
animation: hero-rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
}
|
||||
|
||||
.hero-mockup {
|
||||
animation: hero-fade 1s ease 0.15s both;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
Nav — sticky site header. Renders on a single line at lg, height capped at
|
||||
64px (h-16). Links point at in-page feature anchors.
|
||||
-->
|
||||
|
||||
<header class="sticky top-0 z-50 border-b border-border bg-background/85 backdrop-blur-sm">
|
||||
<nav class="mx-auto flex h-16 max-w-7xl items-center justify-between gap-6 px-6 lg:px-8" aria-label="Primary">
|
||||
<a href="#" class="flex items-center gap-2.5">
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-primary/15 text-primary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="text-sm font-semibold tracking-tight text-foreground">Clustri</span>
|
||||
<span class="hidden font-mono text-[9px] uppercase tracking-[0.14em] text-muted-foreground sm:inline">
|
||||
Ops Console
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="hidden items-center gap-6 md:flex">
|
||||
<a href="#failover" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Features
|
||||
</a>
|
||||
<a href="#security" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Security
|
||||
</a>
|
||||
<a href="#console" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Console
|
||||
</a>
|
||||
<a href="#architecture" class="text-[13px] font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
Architecture
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="rounded-md bg-primary px-4 py-2 text-[13px] font-semibold text-primary-foreground transition hover:brightness-110 active:scale-[0.98]"
|
||||
>
|
||||
Get Clustri
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
Reference in New Issue
Block a user