709 lines
58 KiB
HTML
709 lines
58 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ourpad — Infrastructure</title>
|
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
<link rel="apple-touch-icon" href="/favicon.svg">
|
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Geist', 'system-ui', '-apple-system', 'sans-serif'],
|
|
mono: ['Geist Mono', 'ui-monospace', 'SFMono-Regular', 'monospace'],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.3 } }
|
|
.animate-pulse-slow { animation: pulse 2s infinite; }
|
|
[x-cloak] { display: none !important; }
|
|
.topo-line { stroke: #d4d4d4; stroke-width: 1.5; fill: none; }
|
|
.dark .topo-line { stroke: #404040; }
|
|
.topo-box { fill: #fafafa; stroke: #d4d4d4; stroke-width: 1; rx: 6; }
|
|
.dark .topo-box { fill: #222; stroke: #404040; }
|
|
.topo-pill { fill: #ebf5ff; stroke: none; rx: 4; }
|
|
.dark .topo-pill { fill: #1e3a5f; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-white dark:bg-[#1a1a1a] text-[#171717] dark:text-[#e5e5e5] antialiased min-h-screen" x-data="dashboard()" :class="{ 'dark': dark }" x-init="init()">
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- NAV -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<nav class="sticky top-0 z-50 bg-white/90 dark:bg-[#1a1a1a]/90 backdrop-blur-md border-b border-black/5 dark:border-white/5 px-6 h-14 flex items-center justify-between">
|
|
<a href="/" class="text-lg font-semibold tracking-tight">🏠 <span class="hidden sm:inline">Ourpad</span></a>
|
|
<div class="flex items-center gap-4 sm:gap-6">
|
|
<a href="https://gitea.ourpad.casa" class="text-sm font-medium text-[#808080] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-white transition-colors">Gitea</a>
|
|
<a href="https://pass.ourpad.casa" class="text-sm font-medium text-[#808080] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-white transition-colors">Pass</a>
|
|
<a href="https://status.ourpad.casa" class="text-sm font-medium text-[#808080] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-white transition-colors">Status</a>
|
|
<span class="text-[#d4d4d4] dark:text-[#404040] hidden sm:inline">|</span>
|
|
<a href="https://canteen.ourpad.casa" class="text-sm font-medium text-[#808080] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-white transition-colors hidden sm:inline">Canteen</a>
|
|
<a href="https://hermes.ourpad.casa" class="text-sm font-medium text-[#808080] dark:text-[#a0a0a0] hover:text-[#171717] dark:hover:text-white transition-colors hidden sm:inline">Hermes</a>
|
|
<button @click="toggleDark()" class="border border-[#d4d4d4] dark:border-[#404040] rounded-md px-2 py-1 text-base cursor-pointer hover:bg-[#fafafa] dark:hover:bg-[#2a2a2a] transition-colors shrink-0" x-text="dark ? '☀️' : '🌙'" :title="dark ? 'Light mode' : 'Dark mode'"></button>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- HERO -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<section class="text-center py-12 sm:py-16 px-6 max-w-3xl mx-auto">
|
|
<h1 class="text-3xl sm:text-[42px] font-semibold tracking-[-2px] mb-3">Ourpad Infrastructure</h1>
|
|
<p class="text-[15px] sm:text-[17px] text-[#808080] dark:text-[#a0a0a0] leading-relaxed">
|
|
Live service dashboard <span class="text-[#d4d4d4] dark:text-[#404040]">·</span> Infrastructure source of truth
|
|
<span class="text-[#d4d4d4] dark:text-[#404040]">·</span> <span class="font-mono text-sm">project-directory + network-inventory</span>
|
|
</p>
|
|
</section>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- NETWORK TOPOLOGY DIAGRAM -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<div class="max-w-[900px] mx-auto px-6 pb-8">
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-xl p-5 overflow-x-auto">
|
|
<svg viewBox="0 0 820 340" class="w-full h-auto" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
|
<path d="M0,0 L10,5 L0,10 Z" fill="#d4d4d4"/>
|
|
</marker>
|
|
<marker id="arrow-dark" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto">
|
|
<path d="M0,0 L10,5 L0,10 Z" fill="#404040"/>
|
|
</marker>
|
|
</defs>
|
|
|
|
<!-- Cloud / Internet -->
|
|
<rect x="320" y="5" width="160" height="36" rx="18" class="topo-pill"/>
|
|
<text x="400" y="28" text-anchor="middle" fill="#0068d6" font-size="12" font-family="Geist, sans-serif" font-weight="500" class="dark:fill-blue-400">☁️ Internet 68.202.6.107</text>
|
|
|
|
<!-- Cloudflare Badge -->
|
|
<rect x="295" y="50" width="90" height="20" rx="10" fill="#fef3c7"/>
|
|
<text x="340" y="64" text-anchor="middle" fill="#92400e" font-size="9" font-family="Geist, sans-serif" font-weight="500">🔒 Cloudflare Proxy</text>
|
|
|
|
<!-- Direct Badge -->
|
|
<rect x="415" y="50" width="90" height="20" rx="10" fill="#e0f2fe"/>
|
|
<text x="460" y="64" text-anchor="middle" fill="#0369a1" font-size="9" font-family="Geist, sans-serif" font-weight="500">🔓 Direct</text>
|
|
|
|
<!-- Arrow: Internet → NPMplus -->
|
|
<line x1="400" y1="72" x2="400" y2="95" class="topo-line" marker-end="url(#arrow)"/>
|
|
|
|
<!-- NPMplus Box -->
|
|
<rect x="320" y="95" width="160" height="42" rx="8" class="topo-box"/>
|
|
<text x="400" y="115" text-anchor="middle" fill="#171717" font-size="13" font-family="Geist, sans-serif" font-weight="600" class="dark:fill-[#e5e5e5]">🌐 NPMplus Reverse Proxy</text>
|
|
<text x="400" y="130" text-anchor="middle" fill="#808080" font-size="10" font-family="Geist Mono, monospace">192.168.0.110 · ourpad N150</text>
|
|
|
|
<!-- Arrow: NPMplus → PVE -->
|
|
<line x1="400" y1="137" x2="400" y2="160" class="topo-line" marker-end="url(#arrow)"/>
|
|
|
|
<!-- PVE Box -->
|
|
<rect x="295" y="160" width="210" height="52" rx="8" class="topo-box"/>
|
|
<text x="400" y="180" text-anchor="middle" fill="#171717" font-size="13" font-family="Geist, sans-serif" font-weight="600" class="dark:fill-[#e5e5e5]">🖥️ PVE Host (i5-6500 · 31GB)</text>
|
|
<text x="400" y="195" text-anchor="middle" fill="#808080" font-size="10" font-family="Geist Mono, monospace">192.168.0.144 · 15 LXC + 1 VM · No Tailscale</text>
|
|
|
|
<!-- Arrow: PVE → VM 110 (left path) -->
|
|
<line x1="345" y1="212" x2="295" y2="240" class="topo-line" marker-end="url(#arrow)"/>
|
|
|
|
<!-- Arrow: PVE → Docker CT 100 (right path) -->
|
|
<line x1="455" y1="212" x2="465" y2="240" class="topo-line" marker-end="url(#arrow)"/>
|
|
|
|
<!-- VM 110 Box (this server) -->
|
|
<rect x="120" y="240" width="190" height="82" rx="8" class="topo-box"/>
|
|
<text x="215" y="258" text-anchor="middle" fill="#171717" font-size="12" font-family="Geist, sans-serif" font-weight="600" class="dark:fill-[#e5e5e5]">🖥️ VM 110 — This Server</text>
|
|
<text x="215" y="273" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">192.168.0.127 (LAN)</text>
|
|
<text x="215" y="286" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">100.89.190.113 (Tailscale)</text>
|
|
<text x="215" y="299" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">68.202.6.107 (Public)</text>
|
|
<text x="215" y="314" text-anchor="middle" fill="#808080" font-size="8" font-family="Geist, sans-serif">Hermes · Canteen · Dashboards · News</text>
|
|
|
|
<!-- Docker CT 100 Box -->
|
|
<rect x="395" y="240" width="190" height="82" rx="8" class="topo-box"/>
|
|
<text x="490" y="258" text-anchor="middle" fill="#171717" font-size="12" font-family="Geist, sans-serif" font-weight="600" class="dark:fill-[#e5e5e5]">🐳 Docker CT 100</text>
|
|
<text x="490" y="273" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">192.168.0.223 (LAN)</text>
|
|
<text x="490" y="286" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">No Tailscale (via PVE)</text>
|
|
<text x="490" y="299" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">—</text>
|
|
<text x="490" y="314" text-anchor="middle" fill="#808080" font-size="8" font-family="Geist, sans-serif">Gitea · Pass Vault</text>
|
|
|
|
<!-- Ourpad N150 Box (bottom right) -->
|
|
<rect x="610" y="240" width="190" height="82" rx="8" class="topo-box"/>
|
|
<text x="705" y="258" text-anchor="middle" fill="#171717" font-size="12" font-family="Geist, sans-serif" font-weight="600" class="dark:fill-[#e5e5e5]">🖧 Ourpad N150</text>
|
|
<text x="705" y="273" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">192.168.0.110 (LAN)</text>
|
|
<text x="705" y="286" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">No Tailscale</text>
|
|
<text x="705" y="299" text-anchor="middle" fill="#808080" font-size="9" font-family="Geist Mono, monospace">—</text>
|
|
<text x="705" y="314" text-anchor="middle" fill="#808080" font-size="8" font-family="Geist, sans-serif">NPMplus · Jellyfin · cf-ddns</text>
|
|
|
|
<!-- Arrow: PVE → Ourpad N150 (not directly, but via network) -->
|
|
<line x1="505" y1="212" x2="705" y2="240" class="topo-line" marker-end="url(#arrow)"/>
|
|
|
|
<!-- Tailscale overlay box -->
|
|
<rect x="550" y="5" width="140" height="36" rx="18" fill="#ede9fe"/>
|
|
<text x="620" y="28" text-anchor="middle" fill="#6d28d9" font-size="12" font-family="Geist, sans-serif" font-weight="500">🔗 Tailscale 100.x.x.x</text>
|
|
|
|
<!-- Dashed line from VM 110 to Tailscale -->
|
|
<line x1="215" y1="240" x2="550" y2="23" stroke="#6d28d9" stroke-width="1.5" stroke-dasharray="4,3" fill="none" opacity="0.5"/>
|
|
<line x1="620" y1="41" x2="620" y2="240" stroke="#6d28d9" stroke-width="1" stroke-dasharray="4,3" fill="none" opacity="0.4"/>
|
|
|
|
<!-- Legend: 3 address types -->
|
|
<rect x="30" y="5" width="180" height="72" rx="6" fill="none" stroke="#00000010" stroke-width="1" class="dark:stroke-white/5"/>
|
|
<text x="40" y="22" fill="#808080" font-size="9" font-family="Geist, sans-serif" font-weight="600">Access Legend</text>
|
|
<rect x="40" y="30" width="8" height="8" rx="2" fill="#0072f5"/>
|
|
<text x="53" y="38" fill="#808080" font-size="9" font-family="Geist Mono, monospace">External (domain / public IP)</text>
|
|
<rect x="40" y="46" width="8" height="8" rx="2" fill="#808080"/>
|
|
<text x="53" y="54" fill="#808080" font-size="9" font-family="Geist Mono, monospace">LAN (192.168.0.x)</text>
|
|
<rect x="40" y="62" width="8" height="8" rx="2" fill="#6d28d9"/>
|
|
<text x="53" y="70" fill="#808080" font-size="9" font-family="Geist Mono, monospace">Tailscale (100.x.x.x)</text>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- QUICK STATS BAR -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<div class="max-w-[1300px] mx-auto px-6 pb-8">
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 text-center">
|
|
<div class="bg-[#fafafa] dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg py-4 px-3">
|
|
<div class="text-2xl font-semibold" x-text="hosts.length">0</div>
|
|
<div class="text-xs text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider font-medium mt-0.5">Proxmox Hosts</div>
|
|
</div>
|
|
<div class="bg-[#fafafa] dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg py-4 px-3">
|
|
<div class="text-2xl font-semibold" x-text="hosts.reduce((a,h)=>a+(h.lxcs?.length||0),0)">0</div>
|
|
<div class="text-xs text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider font-medium mt-0.5">Containers</div>
|
|
</div>
|
|
<div class="bg-[#fafafa] dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg py-4 px-3">
|
|
<div class="text-2xl font-semibold" x-text="onlineCount">0</div>
|
|
<div class="text-xs text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider font-medium mt-0.5">Services Online</div>
|
|
</div>
|
|
<div class="bg-[#fafafa] dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg py-4 px-3">
|
|
<div class="text-2xl font-semibold">33</div>
|
|
<div class="text-xs text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider font-medium mt-0.5">Mapped Ports</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- LIVE DASHBOARD -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<div class="max-w-[1300px] mx-auto px-6 pb-6">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<div class="h-5 w-1 bg-[#0072f5] dark:bg-[#5ba4ff] rounded-full"></div>
|
|
<h2 class="text-lg font-semibold tracking-tight">📊 Live Dashboard</h2>
|
|
</div>
|
|
|
|
<!-- Proxmox Hosts -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<h3 class="text-sm font-semibold text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider">🖥️ Proxmox Hosts</h3>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<template x-for="h in hosts" :key="h.id">
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg p-5">
|
|
<template x-if="h.error">
|
|
<div><h4 class="font-semibold flex items-center gap-2"><span>🖥️</span><span x-text="h.name"></span></h4><span class="text-sm italic text-[#808080]" x-text="'Error: '+h.error"></span></div>
|
|
</template>
|
|
<template x-if="!h.error">
|
|
<div>
|
|
<h4 class="font-semibold flex items-center gap-2 mb-3"><span>🖥️</span><span x-text="h.name"></span><span class="ml-auto text-xs text-[#808080] dark:text-[#a0a0a0] font-mono" x-text="h.node.pveversion + ' · ' + h.node.uptime_hrs + 'h'"></span></h4>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center gap-3"><span class="text-xs font-medium text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider w-8 shrink-0">CPU</span><div class="flex-1 h-2 bg-[#ebebeb] dark:bg-[#333] rounded-full overflow-hidden"><div class="h-full rounded-full transition-all duration-1000" :style="'width:'+Math.min(h.node.cpu,100)+'%;background:'+barColor(h.node.cpu)"></div></div><span class="font-mono text-xs text-[#808080] dark:text-[#a0a0a0] w-12 text-right" x-text="h.node.cpu+'%'"></span></div>
|
|
<div class="flex items-center gap-3"><span class="text-xs font-medium text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider w-8 shrink-0">RAM</span><div class="flex-1 h-2 bg-[#ebebeb] dark:bg-[#333] rounded-full overflow-hidden"><div class="h-full rounded-full transition-all duration-1000" :style="'width:'+h.node.memory.pct+'%;background:'+barColor(h.node.memory.pct)"></div></div><span class="font-mono text-xs text-[#808080] dark:text-[#a0a0a0] w-20 text-right" x-text="h.node.memory.used_mb+'/'+h.node.memory.total_mb+' MB'"></span></div>
|
|
<div class="flex items-center gap-3"><span class="text-xs font-medium text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider w-8 shrink-0">DISK</span><div class="flex-1 h-2 bg-[#ebebeb] dark:bg-[#333] rounded-full overflow-hidden"><div class="h-full rounded-full transition-all duration-1000" :style="'width:'+h.node.disk.pct+'%;background:'+barColor(h.node.disk.pct)"></div></div><span class="font-mono text-xs text-[#808080] dark:text-[#a0a0a0] w-20 text-right" x-text="h.node.disk.used_gb+'/'+h.node.disk.total_gb+' GB'"></span></div>
|
|
</div>
|
|
<div class="mt-4 pt-3 border-t border-black/5 dark:border-white/5">
|
|
<div class="grid grid-cols-[1.4fr_0.5fr_0.5fr_0.5fr] gap-1.5 items-center text-[11px] text-[#808080] dark:text-[#a0a0a0] font-semibold uppercase tracking-wider pb-1">
|
|
<span>Container</span><span>CPU</span><span>RAM</span><span>Disk</span>
|
|
</div>
|
|
<template x-for="ct in (h.lxcs||[])" :key="ct.vmid">
|
|
<div class="grid grid-cols-[1.4fr_0.5fr_0.5fr_0.5fr] gap-1.5 items-center text-xs py-0.5" :class="ct.status!='running'?'opacity-40':''">
|
|
<span class="font-medium truncate text-xs" x-text="'CT'+ct.vmid+'.'+ct.name"></span>
|
|
<span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="ct.cpu_pct+'%'"></span>
|
|
<div class="flex items-center gap-1"><div class="flex-1 h-1.5 bg-[#ebebeb] dark:bg-[#333] rounded-full overflow-hidden"><div class="h-full rounded-full" :style="'width:'+ct.mem_pct+'%;background:'+barColor(ct.mem_pct)"></div></div><span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="ct.mem_mb+'M'"></span></div>
|
|
<div class="flex items-center gap-1"><div class="flex-1 h-1.5 bg-[#ebebeb] dark:bg-[#333] rounded-full overflow-hidden"><div class="h-full rounded-full" :style="'width:'+ct.disk_pct+'%;background:'+barColor(ct.disk_pct)"></div></div><span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="ct.disk_gb+'G'"></span></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Docker -->
|
|
<div class="mb-6" x-show="dockerContainers.length" x-cloak>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<h3 class="text-sm font-semibold text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider">🐳 Docker (CT 100)</h3>
|
|
<span class="inline-flex items-center bg-[#ebf5ff] dark:bg-blue-900/30 text-[#0068d6] dark:text-blue-400 text-xs font-medium px-2 h-5 rounded-full" x-text="dockerContainers.length"></span>
|
|
</div>
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg p-4 overflow-x-auto">
|
|
<div class="grid grid-cols-[1.6fr_0.5fr_0.6fr_1.2fr_1.2fr_0.3fr] gap-2 items-center text-[11px] text-[#808080] dark:text-[#a0a0a0] font-semibold uppercase tracking-wider pb-2 border-b border-black/5 dark:border-white/5 mb-2 min-w-[480px]">
|
|
<span>Container</span><span>CPU</span><span>RAM</span><span>Network ↓ / ↑</span><span>Block ↓ / ↑</span><span>PIDs</span>
|
|
</div>
|
|
<template x-for="c in dockerContainers" :key="c.name">
|
|
<div class="grid grid-cols-[1.6fr_0.5fr_0.6fr_1.2fr_1.2fr_0.3fr] gap-2 items-center text-xs py-1 border-b border-[#fafafa] dark:border-[#2a2a2a] last:border-0 min-w-[480px]">
|
|
<span class="font-medium truncate" x-text="c.name" :title="c.name"></span>
|
|
<span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="c.cpu+'%'"></span>
|
|
<div class="flex items-center gap-1"><div class="flex-1 h-1.5 bg-[#ebebeb] dark:bg-[#333] rounded-full overflow-hidden"><div class="h-full rounded-full" :style="'width:'+Math.min(parseFloat(c.mem_pct)||0,100)+'%;background:'+barColor(parseFloat(c.mem_pct)||0)"></div></div><span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="c.mem_used"></span></div>
|
|
<span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="c.net_in+' / '+c.net_out"></span>
|
|
<span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="c.blk_in+' / '+c.blk_out"></span>
|
|
<span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0]" x-text="c.pids"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services with live status + 3-address expand -->
|
|
<div class="mb-6">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<h3 class="text-sm font-semibold text-[#808080] dark:text-[#a0a0a0] uppercase tracking-wider">🌐 Service Health</h3>
|
|
<span class="inline-flex items-center bg-[#ebf5ff] dark:bg-blue-900/30 text-[#0068d6] dark:text-blue-400 text-xs font-medium px-2 h-5 rounded-full" x-text="publicServices.length + internalServices.length"></span>
|
|
</div>
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2" x-data="{ expanded: null }">
|
|
<template x-for="s in [...publicServices, ...internalServices]" :key="s.id">
|
|
<div>
|
|
<div class="flex items-center gap-2 px-3 py-2.5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg cursor-pointer hover:border-[#808080]/30 dark:hover:border-[#808080]/30 transition-colors"
|
|
@click="expanded = expanded === s.id ? null : s.id">
|
|
<span x-text="s.icon" class="text-base shrink-0"></span>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-1.5">
|
|
<span x-show="!s.nostatus" class="shrink-0">
|
|
<span class="inline-block w-2 h-2 rounded-full" :class="'bg-'+((serviceStatus[s.id]||'checking')=='online'?'[#22c55e]':(serviceStatus[s.id]=='offline'?'[#ef4444]':(serviceStatus[s.id]=='checking'?'[#f59e0b] animate-pulse-slow':'[#f59e0b]'))"></span>
|
|
</span>
|
|
<span x-show="s.nostatus" class="shrink-0"><span class="inline-block w-2 h-2 rounded-full bg-[#d4d4d4]"></span></span>
|
|
<span class="text-xs font-medium truncate" x-text="s.name"></span>
|
|
</div>
|
|
<div class="text-[11px] text-[#808080] dark:text-[#a0a0a0] truncate" x-text="s.desc"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Expanded: 3-address card -->
|
|
<div x-show="expanded === s.id" x-cloak x-transition:enter.duration.200ms class="px-3 py-2.5 bg-[#fafafa] dark:bg-[#2a2a2a] rounded-b-lg border-x border-b border-black/5 dark:border-white/5 text-xs text-[#808080] dark:text-[#a0a0a0] space-y-1">
|
|
<div class="flex gap-2 items-start">
|
|
<span class="font-semibold uppercase tracking-wider text-[9px] bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 px-1 py-0.5 rounded shrink-0 mt-0.5">EXT</span>
|
|
<template x-if="s.external">
|
|
<a :href="s.external" target="_blank" class="font-mono text-[11px] text-[#0072f5] dark:text-[#5ba4ff] no-underline hover:underline truncate" x-text="s.external"></a>
|
|
</template>
|
|
<template x-if="!s.external">
|
|
<span class="font-mono text-[11px] italic opacity-50">no external access</span>
|
|
</template>
|
|
</div>
|
|
<div class="flex gap-2 items-start">
|
|
<span class="font-semibold uppercase tracking-wider text-[9px] bg-[#ebebeb] dark:bg-[#333] text-[#808080] dark:text-[#a0a0a0] px-1 py-0.5 rounded shrink-0 mt-0.5">LAN</span>
|
|
<template x-if="s.lan">
|
|
<span class="font-mono text-[11px]" x-text="s.lan"></span>
|
|
</template>
|
|
<template x-if="!s.lan">
|
|
<span class="font-mono text-[11px] italic opacity-50">—</span>
|
|
</template>
|
|
</div>
|
|
<div class="flex gap-2 items-start">
|
|
<span class="font-semibold uppercase tracking-wider text-[9px] bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 px-1 py-0.5 rounded shrink-0 mt-0.5">TS</span>
|
|
<template x-if="s.ts">
|
|
<span class="font-mono text-[11px]" x-text="s.ts"></span>
|
|
</template>
|
|
<template x-if="!s.ts">
|
|
<span class="font-mono text-[11px] italic opacity-50">not on Tailscale</span>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- SECTION DIVIDER -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<div class="border-t border-black/5 dark:border-white/5 my-6 max-w-[1300px] mx-auto px-6"></div>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- SOURCE OF TRUTH -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<div class="max-w-[1300px] mx-auto px-6 pb-12">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<div class="h-5 w-1 bg-[#22c55e] rounded-full"></div>
|
|
<h2 class="text-lg font-semibold tracking-tight">📋 Source of Truth</h2>
|
|
<span class="inline-flex items-center bg-[#ecfdf5] dark:bg-green-900/20 text-[#065f46] dark:text-green-400 text-xs font-medium px-2 h-5 rounded-full">live from network-inventory.yaml</span>
|
|
</div>
|
|
|
|
<div x-show="!inv" class="text-sm italic text-[#808080] dark:text-[#a0a0a0] py-8 text-center">Loading inventory...</div>
|
|
|
|
<template x-if="inv && !inv.error">
|
|
<div class="space-y-6">
|
|
|
|
<!-- ──────── CREDENTIALS ──────── -->
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg overflow-hidden" x-data="{ credOpen: true }">
|
|
<div class="flex items-center justify-between px-5 py-3.5 bg-[#fafafa] dark:bg-[#2a2a2a] border-b border-black/5 dark:border-white/5 cursor-pointer select-none" @click="credOpen = !credOpen">
|
|
<div class="flex items-center gap-2.5">
|
|
<span class="text-lg">🔑</span>
|
|
<h3 class="font-semibold text-sm">Credentials</h3>
|
|
<span class="inline-flex items-center bg-[#ebf5ff] dark:bg-blue-900/30 text-[#0068d6] dark:text-blue-400 text-xs font-medium px-2 h-5 rounded-full" x-text="(inv.credentials?.entries?.length||0)+' entries'"></span>
|
|
</div>
|
|
<span class="text-xs text-[#808080] dark:text-[#a0a0a0] transition-transform duration-200" :class="credOpen ? 'rotate-180' : ''">▾</span>
|
|
</div>
|
|
<div x-show="credOpen">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-semibold uppercase tracking-wider border-b border-black/5 dark:border-white/5 bg-white dark:bg-[#222]">
|
|
<th class="text-left px-5 py-3 w-[35%]">Service</th>
|
|
<th class="text-left px-5 py-3 w-[25%]">Pass Path</th>
|
|
<th class="text-left px-5 py-3 w-[15%] hidden sm:table-cell">User</th>
|
|
<th class="text-left px-5 py-3 w-[25%] hidden md:table-cell">Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="(entry, i) in (inv.credentials?.entries||[])" :key="i">
|
|
<tr class="border-b border-[#fafafa] dark:border-[#2a2a2a] last:border-0 hover:bg-[#fafafa] dark:hover:bg-[#2a2a2a]">
|
|
<td class="px-5 py-3 font-medium text-sm" x-text="entry.service"></td>
|
|
<td class="px-5 py-3">
|
|
<a :href="'https://pass.ourpad.casa/'+entry.pass_path" target="_blank" class="font-mono text-xs text-[#0072f5] dark:text-[#5ba4ff] no-underline hover:underline bg-[#ebf5ff] dark:bg-blue-900/20 px-1.5 py-0.5 rounded" x-text="entry.pass_path"></a>
|
|
</td>
|
|
<td class="px-5 py-3 text-[#808080] dark:text-[#a0a0a0] text-xs hidden sm:table-cell font-mono" x-text="entry.username||'—'"></td>
|
|
<td class="px-5 py-3 text-[#808080] dark:text-[#a0a0a0] text-xs hidden md:table-cell" x-text="entry.note||'—'"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ──────── PORT MAP ──────── -->
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg overflow-hidden" x-data="{ portOpen: true }">
|
|
<div class="flex items-center justify-between px-5 py-3.5 bg-[#fafafa] dark:bg-[#2a2a2a] border-b border-black/5 dark:border-white/5 cursor-pointer select-none" @click="portOpen = !portOpen">
|
|
<div class="flex items-center gap-2.5">
|
|
<span class="text-lg">📡</span>
|
|
<h3 class="font-semibold text-sm">Port Map</h3>
|
|
<span class="inline-flex items-center bg-[#ebf5ff] dark:bg-blue-900/30 text-[#0068d6] dark:text-blue-400 text-xs font-medium px-2 h-5 rounded-full" x-text="Object.keys(inv.port_map||{}).length+' ports'"></span>
|
|
</div>
|
|
<span class="text-xs text-[#808080] dark:text-[#a0a0a0] transition-transform duration-200" :class="portOpen ? 'rotate-180' : ''">▾</span>
|
|
</div>
|
|
<div x-show="portOpen" x-cloak>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-semibold uppercase tracking-wider border-b border-black/5 dark:border-white/5">
|
|
<th class="text-left px-5 py-3 w-[10%]">Port</th>
|
|
<th class="text-left px-5 py-3 w-[65%]">Service</th>
|
|
<th class="text-left px-5 py-3 w-[25%] hidden sm:table-cell">Access</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="(desc, port) in sortPorts(inv.port_map||{})" :key="port">
|
|
<tr class="border-b border-[#fafafa] dark:border-[#2a2a2a] last:border-0 hover:bg-[#fafafa] dark:hover:bg-[#2a2a2a]">
|
|
<td class="px-5 py-2"><code class="font-mono text-sm font-bold bg-[#ebf5ff] dark:bg-blue-900/20 text-[#0068d6] dark:text-blue-400 px-2 py-0.5 rounded" x-text="port"></code></td>
|
|
<td class="px-5 py-2 text-sm" x-text="desc"></td>
|
|
<td class="px-5 py-2 text-xs text-[#808080] dark:text-[#a0a0a0] hidden sm:table-cell">
|
|
<span x-text="portAccessLabel(port, inv)"></span>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ──────── SERVICES (detailed YAML) ──────── -->
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg overflow-hidden" x-data="{ svcOpen: true }">
|
|
<div class="flex items-center justify-between px-5 py-3.5 bg-[#fafafa] dark:bg-[#2a2a2a] border-b border-black/5 dark:border-white/5 cursor-pointer select-none" @click="svcOpen = !svcOpen">
|
|
<div class="flex items-center gap-2.5">
|
|
<span class="text-lg">🔧</span>
|
|
<h3 class="font-semibold text-sm">Service Details</h3>
|
|
<span class="inline-flex items-center bg-[#ebf5ff] dark:bg-blue-900/30 text-[#0068d6] dark:text-blue-400 text-xs font-medium px-2 h-5 rounded-full" x-text="(inv.services?.length||0)+' services'"></span>
|
|
</div>
|
|
<span class="text-xs text-[#808080] dark:text-[#a0a0a0] transition-transform duration-200" :class="svcOpen ? 'rotate-180' : ''">▾</span>
|
|
</div>
|
|
<div x-show="svcOpen" x-cloak>
|
|
<div class="divide-y divide-black/5 dark:divide-white/5">
|
|
<template x-for="(svc, i) in (inv.services||[])" :key="i">
|
|
<div class="px-5 py-4 hover:bg-[#fafafa] dark:hover:bg-[#2a2a2a]">
|
|
<div class="flex items-start justify-between gap-3 mb-1">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium text-sm" x-text="svc.name"></span>
|
|
<span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded" x-text="svc.category||''"></span>
|
|
</div>
|
|
<div class="flex gap-1.5 shrink-0">
|
|
<template x-if="svc.port">
|
|
<code class="font-mono text-xs bg-[#ebf5ff] dark:bg-blue-900/20 text-[#0068d6] dark:text-blue-400 px-1.5 py-0.5 rounded" x-text="':'+svc.port"></code>
|
|
</template>
|
|
<template x-if="svc.auth_port">
|
|
<code class="font-mono text-xs bg-[#fef3c7] dark:bg-yellow-900/20 text-[#92400e] dark:text-yellow-400 px-1.5 py-0.5 rounded" x-text="'proxy:'+svc.auth_port"></code>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] mb-2 leading-relaxed" x-text="svc.description||''"></p>
|
|
<div class="flex flex-wrap gap-x-6 gap-y-1 text-xs">
|
|
<template x-if="svc.systemd">
|
|
<div><span class="text-[10px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider">systemd</span> <code class="font-mono text-xs" x-text="typeof svc.systemd==='string'?svc.systemd:'(multiple)'"></code></div>
|
|
</template>
|
|
<template x-if="svc.working_dir">
|
|
<div><span class="text-[10px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider">dir</span> <code class="font-mono text-xs" x-text="svc.working_dir"></code></div>
|
|
</template>
|
|
<template x-if="svc.db_path">
|
|
<div class="w-full"><span class="text-[10px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider">db</span> <code class="font-mono text-xs" x-text="svc.db_path"></code></div>
|
|
</template>
|
|
<template x-if="svc.tech_stack">
|
|
<div class="flex flex-wrap gap-1 mt-1">
|
|
<template x-for="tech in (Array.isArray(svc.tech_stack)?svc.tech_stack:[svc.tech_stack])" :key="tech">
|
|
<span class="font-mono text-[10px] text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded" x-text="tech"></span>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ──────── PROXMOX HARDWARE ──────── -->
|
|
<div class="bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg overflow-hidden" x-data="{ hwOpen: true }">
|
|
<div class="flex items-center justify-between px-5 py-3.5 bg-[#fafafa] dark:bg-[#2a2a2a] border-b border-black/5 dark:border-white/5 cursor-pointer select-none" @click="hwOpen = !hwOpen">
|
|
<div class="flex items-center gap-2.5">
|
|
<span class="text-lg">🖥️</span>
|
|
<h3 class="font-semibold text-sm">Proxmox Hardware Specs</h3>
|
|
<span class="inline-flex items-center bg-[#ebf5ff] dark:bg-blue-900/30 text-[#0068d6] dark:text-blue-400 text-xs font-medium px-2 h-5 rounded-full" x-text="(inv.proxmox_hosts?.length||0)+' hosts'"></span>
|
|
</div>
|
|
<span class="text-xs text-[#808080] dark:text-[#a0a0a0] transition-transform duration-200" :class="hwOpen ? 'rotate-180' : ''">▾</span>
|
|
</div>
|
|
<div x-show="hwOpen" x-cloak>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-0 divide-y md:divide-y-0 md:divide-x divide-black/5 dark:divide-white/5">
|
|
<template x-for="(host, i) in (inv.proxmox_hosts||[])" :key="i">
|
|
<div class="p-5">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h4 class="font-semibold" x-text="host.hostname"></h4>
|
|
<a :href="host.web_ui" target="_blank" class="text-[11px] text-[#0072f5] dark:text-[#5ba4ff] no-underline hover:underline">Web UI →</a>
|
|
</div>
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">Model</span><span class="font-mono text-xs" x-text="host.hardware?.model||'—'"></span></div>
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">CPU</span><span class="font-mono text-xs" x-text="host.hardware?.cpu||'—'"></span></div>
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">Cores</span><span class="font-mono text-xs" x-text="host.hardware?.cores||'—'"></span></div>
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">RAM</span><span class="font-mono text-xs" x-text="host.hardware?.ram||'—'"></span></div>
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">IP</span><span class="font-mono text-xs" x-text="host.local_ip||'—'"></span></div>
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">OS</span><span class="font-mono text-xs" x-text="host.os||'—'"></span></div>
|
|
<div class="flex gap-2"><span class="text-[11px] text-[#808080] dark:text-[#a0a0a0] font-medium uppercase tracking-wider min-w-[48px]">Boot</span><span class="font-mono text-xs" x-text="host.boot_disk_size+'GB'"></span></div>
|
|
</div>
|
|
<div class="mt-4 pt-3 border-t border-black/5 dark:border-white/5">
|
|
<div class="flex flex-wrap gap-1">
|
|
<template x-for="(storage, si) in (host.hardware?.storage||[])" :key="si">
|
|
<span class="font-mono text-[11px] text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded" x-text="storage"></span>
|
|
</template>
|
|
</div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] mt-3 leading-relaxed" x-text="host.notes||''"></p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="inv && inv.error">
|
|
<div class="bg-[#fef2f2] dark:bg-red-900/20 border border-[#fecaca] dark:border-red-800 rounded-lg p-4 text-sm text-[#991b1b] dark:text-red-400">
|
|
⚠️ Error loading inventory: <span x-text="inv.error"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- PROJECT CARDS -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<div class="max-w-[1300px] mx-auto px-6 pb-12">
|
|
<div class="border-t border-black/5 dark:border-white/5 pt-6 mb-6">
|
|
<div class="flex items-center gap-3 mb-5">
|
|
<div class="h-5 w-1 bg-[#f59e0b] rounded-full"></div>
|
|
<h2 class="text-lg font-semibold tracking-tight">🛠️ Projects</h2>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
|
|
<a href="https://gitea.ourpad.casa/shawn/pass-vault" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#fef3c7]">🔐</div><span class="font-semibold text-sm">Pass Vault</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">Browser-based password manager — GPG-encrypted pass store.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded">FastAPI</span><span class="text-[10px] font-mono uppercase tracking-wider text-[#065f46] dark:text-green-400 bg-[#ecfdf5] dark:bg-green-900/20 px-1.5 py-0.5 rounded">active</span></div>
|
|
</a>
|
|
|
|
<a href="https://gitea.ourpad.casa/shawn/canteen-asset-tracker" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#e0f2fe]">📍</div><span class="font-semibold text-sm">Canteen Asset Tracker</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">Machine-id scanning, OCR, GPS check-ins, geofences, CSV export.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded">FastAPI</span><span class="text-[10px] font-mono uppercase tracking-wider text-[#92400e] dark:text-yellow-400 bg-[#fef3c7] dark:bg-yellow-900/20 px-1.5 py-0.5 rounded">wip</span></div>
|
|
</a>
|
|
|
|
<a href="https://gitea.ourpad.casa/shawn/canteen-web-app" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#fce7f3]">🌐</div><span class="font-semibold text-sm">Canteen Web App</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">Full-stack frontend — login, navigation, asset CRUD, reporting.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded">Frontend</span><span class="text-[10px] font-mono uppercase tracking-wider text-[#92400e] dark:text-yellow-400 bg-[#fef3c7] dark:bg-yellow-900/20 px-1.5 py-0.5 rounded">wip</span></div>
|
|
</a>
|
|
|
|
<a href="https://gitea.ourpad.casa/shawn/canteen-seed-import" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#fef3c7]">🌱</div><span class="font-semibold text-sm">Canteen Seed Import</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">CSV parsing, GPS derivation, location extraction, sync to main app.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#808080] dark:text-[#a0a0a0] bg-[#fafafa] dark:bg-[#2a2a2a] px-1.5 py-0.5 rounded">Python</span><span class="text-[10px] font-mono uppercase tracking-wider text-[#92400e] dark:text-yellow-400 bg-[#fef3c7] dark:bg-yellow-900/20 px-1.5 py-0.5 rounded">wip</span></div>
|
|
</a>
|
|
|
|
<a href="https://gitea.ourpad.casa/shawn/raspberry-pi-mixer" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#fef2f2]">🎛️</div><span class="font-semibold text-sm">Raspberry Pi Mixer</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">Custom Linux distro for RPi4B — low-latency digital mixer.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#065f46] dark:text-green-400 bg-[#ecfdf5] dark:bg-green-900/20 px-1.5 py-0.5 rounded">active</span></div>
|
|
</a>
|
|
|
|
<a href="https://gitea.ourpad.casa/shawn/project-directory" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#ecfdf5]">📂</div><span class="font-semibold text-sm">Project Directory</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">This page — living dashboard of all Ourpad projects and infrastructure.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#065f46] dark:text-green-400 bg-[#ecfdf5] dark:bg-green-900/20 px-1.5 py-0.5 rounded">active</span></div>
|
|
</a>
|
|
|
|
<a href="https://github.com/NousResearch/hermes-agent" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#ede9fe]">🤖</div><span class="font-semibold text-sm">Hermes Agent</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">AI agent framework — 5 profiles, cron jobs, kanban, MCP tools.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#065f46] dark:text-green-400 bg-[#ecfdf5] dark:bg-green-900/20 px-1.5 py-0.5 rounded">active</span></div>
|
|
</a>
|
|
|
|
<a href="https://obsidian.md" class="flex flex-col gap-2.5 p-5 bg-white dark:bg-[#222] border border-black/5 dark:border-white/5 rounded-lg no-underline text-inherit transition-all hover:border-[#808080]/30 dark:hover:border-[#808080]/30 hover:-translate-y-0.5">
|
|
<div class="flex items-center gap-2.5"><div class="w-8 h-8 rounded flex items-center justify-center shrink-0 bg-[#f1f5f9]">📝</div><span class="font-semibold text-sm">Obsidian Vault</span></div>
|
|
<p class="text-xs text-[#808080] dark:text-[#a0a0a0] leading-relaxed">Three-layer vault, LLM Wiki, daily notes, automated reviews.</p>
|
|
<div class="flex gap-1 flex-wrap mt-auto"><span class="text-[10px] font-mono uppercase tracking-wider text-[#065f46] dark:text-green-400 bg-[#ecfdf5] dark:bg-green-900/20 px-1.5 py-0.5 rounded">active</span></div>
|
|
</a>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ════════════════════════════════════ -->
|
|
<!-- FOOTER -->
|
|
<!-- ════════════════════════════════════ -->
|
|
<footer class="border-t border-black/5 dark:border-white/5 py-8 px-6 text-center">
|
|
<p class="text-sm text-[#808080] dark:text-[#a0a0a0]">
|
|
Built with <a href="https://github.com/NousResearch/hermes-agent" class="text-[#0072f5] dark:text-[#5ba4ff] no-underline hover:underline">Hermes Agent</a>
|
|
<span class="mx-2 text-[#d4d4d4] dark:text-[#404040]">·</span>
|
|
<a href="https://gitea.ourpad.casa/shawn/project-directory" class="text-[#0072f5] dark:text-[#5ba4ff] no-underline hover:underline">Source</a>
|
|
<span class="mx-2 text-[#d4d4d4] dark:text-[#404040]">·</span>
|
|
<span class="font-mono text-xs" x-text="new Date().toLocaleDateString('en-US', {year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})"></span>
|
|
</p>
|
|
</footer>
|
|
|
|
<script>
|
|
function dashboard() {
|
|
return {
|
|
dark: localStorage.getItem('dark') === 'true',
|
|
hosts: [],
|
|
dockerContainers: [],
|
|
serviceStatus: {},
|
|
publicServices: [],
|
|
internalServices: [],
|
|
inv: null,
|
|
pollTimers: [],
|
|
|
|
get onlineCount() {
|
|
return Object.values(this.serviceStatus).filter(s => s === 'online').length;
|
|
},
|
|
|
|
barColor(pct) {
|
|
if (pct > 90) return '#ef4444';
|
|
if (pct > 70) return '#f59e0b';
|
|
return '#22c55e';
|
|
},
|
|
|
|
toggleDark() {
|
|
this.dark = !this.dark;
|
|
localStorage.setItem('dark', String(this.dark));
|
|
document.documentElement.classList.toggle('dark', this.dark);
|
|
},
|
|
|
|
portAccessLabel(port, inv) {
|
|
if (!inv || !inv.services) return '';
|
|
for (const svc of inv.services) {
|
|
if (String(svc.port) === String(port) && svc.auth_port) return '🔒 proxy:' + svc.auth_port;
|
|
if (String(svc.auth_port) === String(port)) return '🔒 auth proxy';
|
|
if (String(svc.port) === String(port)) return '🔓 direct';
|
|
}
|
|
const portNum = parseInt(port);
|
|
if (portNum < 1024) return '⚙️ system';
|
|
if (portNum > 10000 && portNum < 20000) return '🔒 internal';
|
|
return '';
|
|
},
|
|
|
|
sortPorts(map) {
|
|
return Object.fromEntries(Object.entries(map).sort(([a],[b]) => parseInt(a)-parseInt(b)));
|
|
},
|
|
|
|
async fetchHosts() {
|
|
try { const r = await fetch('/api/proxmox/stats'); const d = await r.json(); this.hosts = d.hosts || []; }
|
|
catch(e) { console.log('host fetch failed', e); }
|
|
},
|
|
|
|
async fetchDocker() {
|
|
try { const r = await fetch('/api/docker/stats'); const d = await r.json(); this.dockerContainers = d.containers || []; }
|
|
catch(e) { console.log('docker fetch failed', e); }
|
|
},
|
|
|
|
async fetchStatus() {
|
|
try {
|
|
const r = await fetch('/api/status'); const d = await r.json();
|
|
if (d.services) { const map = {}; for (const s of d.services) map[s.id] = s.status; this.serviceStatus = map; }
|
|
} catch(e) { console.log('status fetch failed', e); }
|
|
},
|
|
|
|
init() {
|
|
document.documentElement.classList.toggle('dark', this.dark);
|
|
// Each service has: external (public domain/ip), lan (192.168.x), ts (tailscale 100.x)
|
|
// Fields left undefined = not applicable
|
|
this.publicServices = [
|
|
{ id:'gitea', name:'Gitea', desc:'Self-hosted Git — repos, issues, CI', icon:'🦊', external:'https://gitea.ourpad.casa', lan:'192.168.0.223:3000' },
|
|
{ id:'pass-vault', name:'Pass Vault', desc:'Web-based secret management', icon:'🔐', external:'https://pass.ourpad.casa', lan:'192.168.0.223:8833' },
|
|
{ id:'projects', name:'Project Directory', desc:'This page — infrastructure dashboard', icon:'📂', external:'https://projects.ourpad.casa', lan:'192.168.0.127:8834', ts:'100.89.190.113:8834' },
|
|
{ id:'hermes', name:'Hermes Dashboard', desc:'Agent management UI (default profile)', icon:'🤖', external:'https://hermes.ourpad.casa', lan:'192.168.0.127:9119', ts:'100.89.190.113:9119' },
|
|
{ id:'coder', name:'Hermes Coder', desc:'Agent dashboard (coder profile)', icon:'💻', external:'https://coder.ourpad.casa', lan:'192.168.0.127:9120', ts:'100.89.190.113:9120' },
|
|
{ id:'system-db', name:'System Dashboard', desc:'Real-time CPU/mem/disk/network monitoring', icon:'📊', external:'https://system-dashboard.ourpad.casa', lan:'192.168.0.127:8899', ts:'100.89.190.113:8899' },
|
|
{ id:'canteen', name:'Canteen App', desc:'Asset tracker with OCR and GPS (WIP)', icon:'🏗️', external:'https://canteen.ourpad.casa', lan:'192.168.0.127:8901', ts:'100.89.190.113:8901' },
|
|
{ id:'canteen-admin',name:'Canteen Admin', desc:'Admin panel — manage assets, users, config', icon:'🛠️', external:'https://admin.canteen.ourpad.casa', lan:'192.168.0.127:8090', ts:'100.89.190.113:8090' },
|
|
{ id:'canteen-dev', name:'Canteen (Dev)', desc:'Dev instance — port 8904', icon:'🧪', external:'https://dev.canteen.ourpad.casa', lan:'192.168.0.127:8904', ts:'100.89.190.113:8904' },
|
|
{ id:'canteen-admin-dev',name:'Canteen Admin (Dev)',desc:'Dev admin panel — port 8092', icon:'🧪', external:'https://canteen-admin-dev.ourpad.casa',lan:'192.168.0.127:8092', ts:'100.89.190.113:8092' },
|
|
{ id:'canteen-seed', name:'Seed Import', desc:'Import seed data — CSV, GPS, OCR (prod :8091)', icon:'🌱', external:'https://canteen-seed.ourpad.casa', lan:'192.168.0.127:8091', ts:'100.89.190.113:8091' },
|
|
{ id:'canteen-seed-dev',name:'Seed Import (Dev)',desc:'Dev seed instance — port 8093', icon:'🌱', external:'https://canteen-seed-dev.ourpad.casa',lan:'192.168.0.127:8093', ts:'100.89.190.113:8093' },
|
|
{ id:'sonarr', name:'Sonarr', desc:'TV series automation (CT 104)', icon:'📺', external:'https://sonarr.ourpad.casa', lan:'192.168.0.178:8989' },
|
|
{ id:'radarr', name:'Radarr', desc:'Movie automation (CT 111)', icon:'🎬', external:'https://radarr.ourpad.casa', lan:'192.168.0.123:7878' },
|
|
{ id:'jellyseerr', name:'Jellyseerr', desc:'Media requests — Plex/Overseerr (CT 101)', icon:'🎞️', external:'https://jellyseerr.ourpad.casa', lan:'192.168.0.229:5055' },
|
|
{ id:'deluge', name:'Deluge', desc:'Torrent client (CT 110)', icon:'📥', external:'https://deluge.ourpad.casa', lan:'192.168.0.243:8112' },
|
|
{ id:'jellyfin', name:'Jellyfin', desc:'Media streaming (ourpad:8096)', icon:'▶️', external:'https://jellyfin.ourpad.casa', lan:'192.168.0.110:8096' },
|
|
{ id:'obsidian', name:'Obsidian LiveSync', desc:'CouchDB sync server (CT 100)', icon:'🔄', external:'https://obsidian.ourpad.casa', lan:'192.168.0.223:5984' },
|
|
{ id:'inventree', name:'Inventree', desc:'Inventory management (CT 106)', icon:'📦', external:'https://inventory.ourpad.casa', lan:'192.168.0.132:8080' },
|
|
{ id:'npmplus', name:'NPMplus', desc:'Reverse proxy admin (ourpad)', icon:'🌐', external:'https://npmplus.ourpad.casa', lan:'192.168.0.110:81' },
|
|
{ id:'proxmox', name:'Proxmox Admin', desc:'VE management UI (PVE host)', icon:'🖥️', external:'https://admin.ourpad.casa', lan:'192.168.0.144:8006' },
|
|
{ id:'rustdesk', name:'RustDesk', desc:'Remote desktop relay (PVE host)', icon:'🖧', external:'https://rustdesk.ourpad.casa', lan:'192.168.0.144:21116' },
|
|
{ id:'status', name:'Network Status', desc:'Live service health monitor', icon:'📡', external:'https://status.ourpad.casa', lan:'192.168.0.127:8898', ts:'100.89.190.113:8898' },
|
|
{ id:'dashboard', name:'Heimdall', desc:'Service landing page (CT 113)', icon:'🏠', external:'https://dashboard.ourpad.casa', lan:'192.168.0.213:80', nostatus:true },
|
|
{ id:'news', name:'News Brief', desc:'Daily news summary — automated', icon:'📰', external:'https://news.ourpad.casa', lan:'192.168.0.127:8920', ts:'100.89.190.113:8920' },
|
|
];
|
|
this.internalServices = [
|
|
{ id:'prowlarr', name:'Prowlarr', desc:'Indexer aggregator (CT 104)', icon:'🔍', lan:'192.168.0.225:9696' },
|
|
{ id:'readarr', name:'Readarr', desc:'Book management (CT 108)', icon:'📚', lan:'192.168.0.193:8787' },
|
|
{ id:'configarr', name:'Configarr', desc:'Config sync via API (CT 107)', icon:'⚙️', lan:'192.168.0.238:9091' },
|
|
{ id:'unpackerr', name:'Unpackerr', desc:'RAR extraction automation (CT 114)', icon:'📦', lan:'192.168.0.119:5656' },
|
|
{ id:'element', name:'Element Synapse', desc:'Matrix chat server (CT 112)', icon:'💬', lan:'192.168.0.220:6167' },
|
|
{ id:'portainer', name:'Portainer', desc:'Docker management UI (CT 100)', icon:'🐳', lan:'192.168.0.223:9443' },
|
|
{ id:'hermes-webui', name:'Hermes WebUI', desc:'Agent dashboard — alternative UI (CT 102)', icon:'🧠', lan:'192.168.0.127:8787', ts:'100.89.190.113:8787' },
|
|
{ id:'deskbrid', name:'Deskbrid', desc:'Desktop automation service', icon:'⌨️' },
|
|
{ id:'cf-ddns', name:'Cloudflare DDNS', desc:'Dynamic DNS updater (ourpad CT 105)', icon:'☁️', lan:'192.168.0.110:9090' },
|
|
{ id:'tailscale', name:'Tailscale', desc:'Mesh VPN — 3 active nodes', icon:'🔗', ts:'100.89.190.113' },
|
|
];
|
|
this.fetchHosts(); this.fetchDocker(); this.fetchStatus();
|
|
fetch('/api/inventory').then(r=>r.json()).then(d=>this.inv=d).catch(e=>console.log('inv fetch failed', e));
|
|
this.pollTimers = [
|
|
setInterval(() => this.fetchHosts(), 30000),
|
|
setInterval(() => this.fetchDocker(), 30000),
|
|
setInterval(() => this.fetchStatus(), 30000),
|
|
];
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|