← back to Malden House
network.html
320 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Network · Live infrastructure viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0a0a;
--surface: #141414;
--surface-2: #1e1e1e;
--line: rgba(255,255,255,.08);
--line-2: rgba(255,255,255,.14);
--text: #f4f4f5;
--muted: #a1a1aa;
--dim: #71717a;
--ok: #22c55e;
--warn: #f59e0b;
--err: #ef4444;
--accent: #8b5cf6;
--accent-2: #06b6d4;
--mono: "Geist Mono", ui-monospace, Menlo, monospace;
--sans: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--text); font-family: var(--sans); min-height: 100vh; }
body { padding: 32px; }
header { display: flex; justify-content: space-between; align-items: flex-end; gap: 40px; margin-bottom: 32px; border-bottom: 1px solid var(--line); padding-bottom: 24px; }
.brand { display: flex; align-items: baseline; gap: 14px; }
.brand-dot { width: 8px; height: 8px; border-radius: 999px; background: var(--ok); box-shadow: 0 0 12px var(--ok); animation: pulse 2s infinite; align-self: center; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
.brand h1 { font-family: var(--sans); font-weight: 900; font-size: 28px; letter-spacing: -0.02em; margin: 0; }
.brand p { font-family: var(--mono); font-size: 12px; color: var(--muted); margin: 0; letter-spacing: 0.02em; }
.meta { display: flex; gap: 24px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
.meta b { color: var(--text); font-weight: 700; margin-right: 6px; }
.meta .dot { display: inline-block; width: 6px; height: 6px; border-radius: 999px; background: var(--ok); margin-right: 6px; }
/* Stage — the animated topology */
.stage {
position: relative;
height: 540px;
background:
radial-gradient(circle at 20% 20%, rgba(139,92,246,.06) 0%, transparent 40%),
radial-gradient(circle at 80% 80%, rgba(6,182,212,.06) 0%, transparent 40%),
var(--surface);
border: 1px solid var(--line);
border-radius: 16px;
overflow: hidden;
margin-bottom: 32px;
}
.stage-grid {
position: absolute; inset: 0;
background-image:
linear-gradient(to right, var(--line) 1px, transparent 1px),
linear-gradient(to bottom, var(--line) 1px, transparent 1px);
background-size: 40px 40px;
opacity: .3;
mask-image: radial-gradient(ellipse at center, black 30%, transparent 80%);
}
.stage svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Nodes */
.node {
position: absolute;
display: flex; flex-direction: column; align-items: center; gap: 8px;
width: 180px;
transform: translate(-50%, -50%);
z-index: 2;
}
.node-card {
width: 100%;
padding: 12px 14px;
background: var(--surface-2);
border: 1px solid var(--line-2);
border-radius: 10px;
display: flex; flex-direction: column; gap: 4px;
backdrop-filter: blur(8px);
transition: border-color .2s ease, box-shadow .2s ease;
}
.node-card.on { border-color: rgba(34,197,94,.4); box-shadow: 0 0 0 1px rgba(34,197,94,.2), 0 8px 24px rgba(0,0,0,.3); }
.node-card.off { border-color: rgba(239,68,68,.35); }
.node-card.warn { border-color: rgba(245,158,11,.35); }
.node-head { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.node-head .status-dot { width: 7px; height: 7px; border-radius: 999px; background: var(--dim); flex-shrink: 0; }
.node-card.on .status-dot { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
.node-card.off .status-dot { background: var(--err); }
.node-card.warn .status-dot { background: var(--warn); }
.node-name { font-family: var(--mono); font-weight: 700; font-size: 14px; color: var(--text); }
.node-sub { font-family: var(--mono); font-size: 11px; color: var(--dim); }
.node-icon {
width: 44px; height: 44px; border-radius: 10px;
background: linear-gradient(135deg, var(--surface-2), var(--surface));
border: 1px solid var(--line-2);
display: flex; align-items: center; justify-content: center;
font-size: 22px;
position: relative;
}
/* Live data-flow particles */
.particle {
offset-rotate: 0deg;
width: 4px; height: 4px; border-radius: 999px;
background: var(--accent-2);
box-shadow: 0 0 12px var(--accent-2);
position: absolute; left: 0; top: 0;
pointer-events: none;
animation: flow 3s linear infinite;
}
.particle.syncthing { background: var(--ok); box-shadow: 0 0 10px var(--ok); }
.particle.tailscale { background: var(--accent); box-shadow: 0 0 10px var(--accent); }
.particle.http { background: var(--accent-2); box-shadow: 0 0 10px var(--accent-2); }
@keyframes flow {
0% { offset-distance: 0%; opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { offset-distance: 100%; opacity: 0; }
}
/* Info grid below */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 14px; }
.panel {
padding: 18px 20px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface);
display: flex; flex-direction: column; gap: 10px;
}
.panel h3 { margin: 0; font-family: var(--sans); font-weight: 800; font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); display: flex; align-items: center; gap: 8px; }
.panel h3 .cnt { font-family: var(--mono); font-weight: 700; font-size: 11px; padding: 2px 6px; background: var(--surface-2); border-radius: 4px; color: var(--text); letter-spacing: 0; }
.kv { display: flex; justify-content: space-between; padding: 6px 0; border-top: 1px dashed var(--line); font-family: var(--mono); font-size: 12px; }
.kv:first-child { border-top: 0; padding-top: 0; }
.kv > span:first-child { color: var(--muted); }
.kv > span:last-child { color: var(--text); font-weight: 500; word-break: break-all; text-align: right; max-width: 60%; }
.kv .pill { padding: 1px 6px; border-radius: 4px; font-size: 10px; letter-spacing: 0.08em; font-weight: 700; }
.pill.on { background: rgba(34,197,94,.15); color: #4ade80; }
.pill.off { background: rgba(239,68,68,.15); color: #f87171; }
.pill.warn { background: rgba(245,158,11,.15); color: #fbbf24; }
.service-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; background: var(--surface-2); border-radius: 6px; font-family: var(--mono); font-size: 12px; }
.service-row + .service-row { margin-top: 4px; }
.service-row .s-name { display: flex; align-items: center; gap: 8px; }
.service-row .s-port { color: var(--dim); font-size: 11px; }
/* Topology label & legend */
.stage-label { position: absolute; top: 16px; left: 20px; font-family: var(--mono); font-size: 11px; color: var(--dim); letter-spacing: 0.12em; text-transform: uppercase; }
.legend { position: absolute; bottom: 16px; left: 20px; display: flex; gap: 16px; font-family: var(--mono); font-size: 11px; color: var(--muted); }
.legend > div { display: flex; align-items: center; gap: 6px; }
.legend .sw { width: 8px; height: 8px; border-radius: 999px; }
footer { margin-top: 32px; display: flex; justify-content: space-between; font-family: var(--mono); font-size: 11px; color: var(--dim); border-top: 1px solid var(--line); padding-top: 16px; }
a { color: var(--accent-2); }
</style>
</head>
<body>
<header>
<div class="brand">
<span class="brand-dot"></span>
<div>
<h1>Network · Live viewer</h1>
<p>infrastructure topology · stevestudio · real-time polling</p>
</div>
</div>
<div class="meta">
<div><b>LAN</b><span id="lan-ip">—</span></div>
<div><b>Polled</b><span id="last-poll">—</span></div>
<div><span class="dot"></span><span id="last-ok">ready</span></div>
</div>
</header>
<section class="stage" id="stage">
<div class="stage-grid"></div>
<span class="stage-label">Topology · flows animate when data moves</span>
<svg id="edges" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1400 540" preserveAspectRatio="xMidYMid meet">
<!-- Tailscale mesh edges -->
<defs>
<linearGradient id="edgeTs" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="rgba(139,92,246,.1)"/>
<stop offset="50%" stop-color="rgba(139,92,246,.5)"/>
<stop offset="100%" stop-color="rgba(139,92,246,.1)"/>
</linearGradient>
<linearGradient id="edgeSync" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="rgba(34,197,94,.1)"/>
<stop offset="50%" stop-color="rgba(34,197,94,.5)"/>
<stop offset="100%" stop-color="rgba(34,197,94,.1)"/>
</linearGradient>
<path id="path-mac2-mac1" d="M 280 270 Q 700 200 1120 270" stroke="url(#edgeTs)" stroke-width="2" fill="none" stroke-dasharray="6 4"/>
<path id="path-mac1-henry" d="M 1120 270 Q 1220 400 1280 480" stroke="url(#edgeSync)" stroke-width="2" fill="none"/>
<path id="path-mac2-yuri" d="M 280 270 Q 280 420 280 490" stroke="url(#edgeTs)" stroke-width="1.5" fill="none" stroke-dasharray="3 3"/>
<path id="path-mac2-mac1-sync" d="M 280 300 Q 700 440 1120 300" stroke="url(#edgeSync)" stroke-width="1.5" fill="none"/>
<path id="path-macbook-mac2" d="M 560 70 Q 380 120 280 250" stroke="url(#edgeTs)" stroke-width="1.5" fill="none" stroke-dasharray="3 3"/>
<path id="path-iphone-mac2" d="M 840 70 Q 560 130 280 250" stroke="url(#edgeTs)" stroke-width="1.5" fill="none" stroke-dasharray="3 3"/>
</defs>
<use href="#path-mac2-mac1"/>
<use href="#path-mac2-mac1-sync"/>
<use href="#path-mac1-henry"/>
<use href="#path-mac2-yuri"/>
<use href="#path-macbook-mac2"/>
<use href="#path-iphone-mac2"/>
</svg>
<!-- Nodes are absolutely positioned via CSS -->
<!-- MacStudio2 (this Mac) -->
<div class="node" style="left: 20%; top: 50%;">
<div class="node-icon">🖥️</div>
<div class="node-card on" data-node="mac2">
<div class="node-head"><span class="status-dot"></span>MACSTUDIO 2</div>
<div class="node-name" id="mac2-host">this mac</div>
<div class="node-sub" id="mac2-ts">100.111.38.111</div>
<div class="node-sub" id="mac2-role">primary · editing</div>
</div>
</div>
<!-- MacStudio1 (always-on hub) -->
<div class="node" style="left: 80%; top: 50%;">
<div class="node-icon">🖥️</div>
<div class="node-card on" data-node="mac1">
<div class="node-head"><span class="status-dot"></span>MACSTUDIO 1</div>
<div class="node-name" id="mac1-host">steves-mac-studio</div>
<div class="node-sub" id="mac1-ts">100.94.103.98</div>
<div class="node-sub" id="mac1-role">hub · backups · Henry</div>
</div>
</div>
<!-- Henry (2TB, attached to MacStudio1) -->
<div class="node" style="left: 91%; top: 91%;">
<div class="node-icon">💾</div>
<div class="node-card" data-node="henry">
<div class="node-head"><span class="status-dot"></span>HENRY · 2TB</div>
<div class="node-name">/Volumes/Henry</div>
<div class="node-sub" id="henry-free">—</div>
</div>
</div>
<!-- Yuri / Kamatera server -->
<div class="node" style="left: 20%; top: 91%;">
<div class="node-icon">☁️</div>
<div class="node-card" data-node="yuri">
<div class="node-head"><span class="status-dot"></span>KAMATERA</div>
<div class="node-name">45.61.58.125</div>
<div class="node-sub" id="yuri-state">yuri · hormuzy · resize-it</div>
</div>
</div>
<!-- MacBook Pro (tailnet peer) -->
<div class="node" style="left: 40%; top: 12%;">
<div class="node-icon">💻</div>
<div class="node-card on" data-node="macbook">
<div class="node-head"><span class="status-dot"></span>MACBOOK PRO</div>
<div class="node-name">stevens-macbook-pro-2021</div>
<div class="node-sub">100.90.99.32</div>
<div class="node-sub">tailnet · mobile</div>
</div>
</div>
<!-- iPhone (tailnet peer) -->
<div class="node" style="left: 60%; top: 12%;">
<div class="node-icon">📱</div>
<div class="node-card on" data-node="iphone">
<div class="node-head"><span class="status-dot"></span>IPHONE 14 PRO MAX</div>
<div class="node-name">iphone-14-pro-max</div>
<div class="node-sub">100.81.193.101</div>
<div class="node-sub">tailnet · iOS</div>
</div>
</div>
<div class="legend">
<div><span class="sw" style="background: #8b5cf6"></span>Tailscale mesh</div>
<div><span class="sw" style="background: #22c55e"></span>Syncthing sync</div>
<div><span class="sw" style="background: #06b6d4"></span>HTTP</div>
</div>
</section>
<section class="grid">
<!-- Tailscale peers -->
<div class="panel">
<h3>Tailscale Mesh <span class="cnt" id="ts-count">—</span></h3>
<div id="ts-peers"><div class="kv"><span>loading…</span><span>—</span></div></div>
</div>
<!-- Syncthing status -->
<div class="panel">
<h3>Syncthing · ~/Work <span class="cnt" id="sync-state">—</span></h3>
<div id="sync-peers"><div class="kv"><span>loading…</span><span>—</span></div></div>
</div>
<!-- Local services -->
<div class="panel">
<h3>Local services (MacStudio2) <span class="cnt" id="local-count">—</span></h3>
<div id="local-services">
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-vs-dot"></span>Victory Stays</div><div class="s-port">:8787</div></div>
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-st-dot"></span>Syncthing</div><div class="s-port">:8384</div></div>
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-ts-dot"></span>Tailscale daemon</div><div class="s-port">socket</div></div>
</div>
</div>
<!-- Remote services -->
<div class="panel">
<h3>Remote (Kamatera) <span class="cnt" id="remote-count">—</span></h3>
<div id="remote-services">
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-yuri-dot"></span>Yuri / YOLO Agent</div><div class="s-port">:9670</div></div>
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-sku-dot"></span>Update SKU agent</div><div class="s-port">:9750</div></div>
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-plumb-dot"></span>Plumbing agent</div><div class="s-port">:9751</div></div>
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-horm-dot"></span>Hormuzy</div><div class="s-port">:9801</div></div>
<div class="service-row"><div class="s-name"><span class="status-dot" id="s-rsz-dot"></span>resize-it</div><div class="s-port">:9876</div></div>
</div>
</div>
</section>
<footer>
<span>polling every 5s · network.html · served from ~/Work/malden-house/</span>
<span>· <a href="/">Victory Stays</a> · <a href="/dashboard.html">Dashboard</a> · <a href="/schedule.html">Schedule</a> ·</span>
</footer>
<script src="network.js?v=1"></script>
</body>
</html>