← 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>