<!-- Infrastructure Module — Trinity Console -->
<!-- Chronicler #78 | April 11, 2026 -->

<style>
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');

  #infra-module { font-family: 'JetBrains Mono', 'SF Mono', monospace; }
  #infra-module * { box-sizing: border-box; }

  .infra-card {
    background: #2d2d2d;
    border: 1px solid #404040;
    border-radius: 8px;
    padding: 16px;
  }

  .infra-metric-card {
    background: #2d2d2d;
    border: 1px solid #404040;
    border-radius: 8px;
    padding: 12px;
    text-align: center;
  }
  .infra-metric-card .value {
    font-size: 20px;
    font-weight: 700;
  }
  .infra-metric-card .label {
    font-size: 9px;
    color: #888;
    margin-top: 2px;
  }

  .topo-canvas-wrap {
    position: relative;
    width: 100%;
    height: 480px;
    background: radial-gradient(ellipse at 50% 0%, #2d2d2d 0%, #1a1a1a 70%);
    border-radius: 12px;
    border: 1px solid #404040;
    overflow: hidden;
  }
  .topo-grid {
    position: absolute;
    inset: 0;
    background-image:
      linear-gradient(#40404022 1px, transparent 1px),
      linear-gradient(90deg, #40404022 1px, transparent 1px);
    background-size: 40px 40px;
    pointer-events: none;
  }
  .topo-canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }

  .topo-node {
    position: absolute;
    cursor: pointer;
    transition: transform 0.15s ease;
    z-index: 1;
  }
  .topo-node:hover, .topo-node.selected {
    transform: scale(1.08);
    z-index: 10;
  }
  .topo-node-inner {
    background: #2d2d2dee;
    border: 1.5px solid #404040;
    border-radius: 8px;
    padding: 8px 6px;
    text-align: center;
    box-shadow: 0 2px 8px #00000044;
    transition: all 0.15s ease;
  }
  .topo-node:hover .topo-node-inner,
  .topo-node.selected .topo-node-inner {
    box-shadow: 0 0 20px var(--node-color-glow);
    border-color: var(--node-color);
  }
  .topo-node.selected .topo-node-inner {
    background: var(--node-color-bg);
  }
  .topo-node-name { font-size: 11px; font-weight: 700; color: #e5e5e5; }
  .topo-node-ip { font-size: 9px; color: #888; margin-top: 2px; }
  .topo-node-role { font-size: 8px; font-weight: 600; margin-top: 2px; }
  .topo-node-games { font-size: 8px; color: #aaa; margin-top: 3px; }

  .topo-ext {
    position: absolute;
    text-align: center;
    cursor: pointer;
    transition: transform 0.15s ease;
    width: 80px;
  }
  .topo-ext:hover { transform: scale(1.1); }
  .topo-ext-icon { font-size: 20px; }
  .topo-ext-label { font-size: 9px; font-weight: 600; margin-top: 2px; white-space: nowrap; }

  .topo-legend {
    position: absolute;
    bottom: 8px;
    right: 12px;
    display: flex;
    gap: 12px;
    font-size: 10px;
    color: #888;
    background: #1a1a1acc;
    padding: 4px 10px;
    border-radius: 6px;
  }

  .status-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    margin-right: 4px;
    vertical-align: middle;
  }
  .status-dot.healthy { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
  .status-dot.warning { background: #eab308; box-shadow: 0 0 6px #eab308; }
  .status-dot.critical { background: #ef4444; box-shadow: 0 0 6px #ef4444; }
  .status-dot.offline { background: #666; box-shadow: 0 0 6px #666; }

  .progress-bar {
    width: 100%;
    height: 6px;
    background: #1a1a1a;
    border-radius: 3px;
    overflow: hidden;
    margin-top: 6px;
  }
  .progress-bar-fill {
    height: 100%;
    border-radius: 3px;
    transition: width 0.5s ease;
  }

  .detail-view { animation: fadeSlideIn 0.2s ease; }
  @keyframes fadeSlideIn {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
  }

  .back-btn {
    background: none;
    border: none;
    color: #4ECDC4;
    cursor: pointer;
    font-size: 13px;
    padding: 0;
    margin-bottom: 12px;
    font-family: inherit;
  }
  .back-btn:hover { text-decoration: underline; }

  .svc-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    border-bottom: 1px solid #40404044;
  }
  .svc-row:last-child { border-bottom: none; }
  .svc-icon { font-size: 16px; }
  .svc-name { font-size: 12px; font-weight: 500; color: #ddd; }
  .svc-domain { font-size: 10px; color: #4ECDC4; }
  .svc-note { font-size: 10px; color: #888; }
  .svc-port {
    font-size: 9px;
    color: #666;
    background: #1a1a1a;
    padding: 2px 6px;
    border-radius: 4px;
    margin-left: auto;
  }

  .badge {
    font-size: 10px;
    padding: 2px 8px;
    border-radius: 10px;
    margin-left: 8px;
  }
  .badge-warning { background: #eab30822; color: #eab308; }
  .badge-critical { background: #ef444422; color: #ef4444; }

  .refresh-btn {
    background: #4ECDC415;
    border: 1px solid #4ECDC444;
    color: #4ECDC4;
    padding: 6px 14px;
    border-radius: 6px;
    cursor: pointer;
    font-size: 11px;
    font-family: inherit;
    transition: all 0.15s;
  }
  .refresh-btn:hover { background: #4ECDC425; border-color: #4ECDC4; }
  .refresh-btn:disabled { opacity: 0.5; cursor: not-allowed; }

  .conn-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    margin-right: 6px;
  }

  @media (max-width: 768px) {
    .topo-canvas-wrap { height: 380px; }
    .fleet-grid { grid-template-columns: repeat(3, 1fr) !important; }
    .detail-metrics { grid-template-columns: repeat(2, 1fr) !important; }
    .detail-panels { grid-template-columns: 1fr !important; }
  }

  /* Zoom controls */
  .zoom-controls {
    position: absolute;
    bottom: 12px;
    left: 12px;
    display: flex;
    gap: 4px;
    z-index: 20;
  }
  .zoom-btn {
    width: 32px;
    height: 32px;
    background: #1a1a1acc;
    border: 1px solid #404040;
    border-radius: 6px;
    color: #ccc;
    font-size: 16px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: inherit;
    transition: all 0.15s;
    user-select: none;
    -webkit-user-select: none;
  }
  .zoom-btn:hover { background: #2d2d2d; border-color: #4ECDC4; color: #4ECDC4; }
  .zoom-btn:active { transform: scale(0.92); }
  .zoom-level {
    height: 32px;
    padding: 0 8px;
    background: #1a1a1acc;
    border: 1px solid #404040;
    border-radius: 6px;
    color: #888;
    font-size: 10px;
    display: flex;
    align-items: center;
    font-family: inherit;
    min-width: 44px;
    justify-content: center;
  }
  .topo-zoomable {
    position: absolute;
    inset: 0;
    transform-origin: 0 0;
    transition: transform 0.15s ease;
    will-change: transform;
  }
  .topo-zoomable.dragging {
    transition: none;
    cursor: grabbing;
  }
  .topo-canvas-wrap { cursor: grab; }
  .topo-canvas-wrap.dragging { cursor: grabbing; }
</style>

<div id="infra-module">
  <!-- Title bar -->
  <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
    <div style="display:flex; align-items:center; gap:10px;">
      <span style="font-size:24px">🌐</span>
      <div>
        <h1 style="font-size:20px; font-weight:700; margin:0; background:linear-gradient(135deg, #FF6B35, #A855F7, #4ECDC4); -webkit-background-clip:text; -webkit-text-fill-color:transparent;">
          Infrastructure
        </h1>
        <div style="font-size:10px; color:#666;">
          <% if (fleet) { %>
            Last audit: <%= new Date(fleet.fetchedAt).toLocaleString('en-US', { month:'short', day:'numeric', hour:'numeric', minute:'2-digit' }) %>
          <% } %>
        </div>
      </div>
    </div>
    <div style="display:flex; gap:8px; align-items:center;">
      <button id="topo-btn" class="refresh-btn" onclick="showTopology()" style="display:none;">View Topology</button>
      <button id="refresh-btn" class="refresh-btn" onclick="refreshAudit()">⟳ Refresh</button>
    </div>
  </div>

  <% if (!fleet) { %>
    <div class="infra-card" style="text-align:center; padding:40px;">
      <div style="font-size:48px; margin-bottom:16px;">🔌</div>
      <div style="font-size:16px; font-weight:600; color:#e5e5e5;">Unable to reach Trinity Core</div>
      <div style="font-size:12px; color:#888; margin-top:8px;"><%= error || 'Connection failed' %></div>
      <button class="refresh-btn" onclick="refreshAudit()" style="margin-top:16px;">Try Again</button>
    </div>
  <% } else { %>

  <!-- Fleet Summary -->
  <div class="fleet-grid" style="display:grid; grid-template-columns:repeat(6, 1fr); gap:10px; margin-bottom:16px;">
    <div class="infra-metric-card">
      <div class="value" style="color:#4ECDC4;"><%= fleet.summary.totalServers %></div>
      <div class="label">Physical Servers</div>
    </div>
    <div class="infra-metric-card">
      <div class="value" style="color:#FF6B35;"><%= fleet.summary.totalGameServers %></div>
      <div class="label">Game Servers</div>
    </div>
    <div class="infra-metric-card">
      <div class="value" style="color:#A855F7;">~42</div>
      <div class="label">Docker Containers</div>
    </div>
    <div class="infra-metric-card">
      <div class="value" style="color:#3b82f6;">~526 GB</div>
      <div class="label">Fleet RAM</div>
    </div>
    <div class="infra-metric-card">
      <div class="value" style="color:#06b6d4;">~2 TB</div>
      <div class="label">Fleet Disk</div>
    </div>
    <div class="infra-metric-card">
      <div class="value" style="color:<%= fleet.summary.needRestart > 0 ? '#eab308' : '#22c55e' %>;"><%= fleet.summary.needRestart %>/<%= fleet.summary.totalServers %></div>
      <div class="label">Need Restart</div>
    </div>
  </div>

  <!-- Topology View -->
  <div id="topology-view">
    <div class="topo-canvas-wrap" id="topo-wrap">
      <div class="topo-zoomable" id="topo-zoomable">
        <div class="topo-grid"></div>
        <canvas id="topo-canvas" class="topo-canvas"></canvas>

        <!-- External service nodes -->
        <div class="topo-ext" data-node="cloudflare" style="left:calc(50% - 40px); top:14px;" onclick="showExternal('cloudflare')">
          <div class="topo-ext-icon" style="filter:drop-shadow(0 0 8px #f4812055);">☁️</div>
          <div class="topo-ext-label" style="color:#f48120;">Cloudflare</div>
        </div>
        <div class="topo-ext" data-node="claude" style="left:calc(12.5% - 40px); top:14px;" onclick="showExternal('claude')">
          <div class="topo-ext-icon" style="filter:drop-shadow(0 0 8px #d4a57455);">🧠</div>
          <div class="topo-ext-label" style="color:#d4a574;">Claude.ai</div>
        </div>
        <div class="topo-ext" data-node="website" style="left:calc(73% - 40px); top:14px;" onclick="showExternal('website')">
          <div class="topo-ext-icon" style="filter:drop-shadow(0 0 8px #4ECDC455);">🌐</div>
          <div class="topo-ext-label" style="color:#4ECDC4;">firefrostgaming.com</div>
        </div>
        <div class="topo-ext" data-node="stripe" style="left:calc(31.25% - 40px); top:14px;" onclick="showExternal('stripe')">
          <div class="topo-ext-icon" style="filter:drop-shadow(0 0 8px #635bff55);">💳</div>
          <div class="topo-ext-label" style="color:#635bff;">Stripe</div>
        </div>
        <div class="topo-ext" data-node="discord" style="left:calc(87.5% - 40px); top:14px;" onclick="showExternal('discord')">
          <div class="topo-ext-icon" style="filter:drop-shadow(0 0 8px #5865f255);">💬</div>
          <div class="topo-ext-label" style="color:#5865f2;">Discord</div>
        </div>

      <!-- Server nodes -->
      <% const positions = {
        'trinity-core':   { x: 12.5, y: 33 },
        'command-center':  { x: 43.75, y: 33 },
        'panel-vps':       { x: 73, y: 33 },
        'tx1-dallas':      { x: 60.4, y: 67 },
        'nc1-charlotte':   { x: 85.4, y: 67 },
        'wiki-vps':        { x: 27, y: 67 },
        'services-vps':    { x: 8.3, y: 67 },
        'dev-panel':       { x: 43.75, y: 88 }
      }; %>

      <% Object.entries(fleet.servers).forEach(([id, server]) => {
        const pos = positions[id];
        if (!pos) return;
        const statusClass = server.status || (server.online ? 'healthy' : 'offline');
        const nodeGames = id === 'tx1-dallas' ? fleet.summary.tx1Games : (id === 'nc1-charlotte' ? fleet.summary.nc1Games : 0);
      %>
        <div class="topo-node" data-node="<%= id %>"
             style="left:calc(<%= pos.x %>% - 56px); top:calc(<%= pos.y %>% - 28px); width:112px; --node-color:<%= server.color %>; --node-color-glow:<%= server.color %>33; --node-color-bg:<%= server.color %>22;"
             onclick="showServer('<%= id %>')"
             onmouseenter="highlightNode('<%= id %>')"
             onmouseleave="clearHighlight()">
          <div class="topo-node-inner">
            <div style="display:flex; align-items:center; justify-content:center; gap:4px; margin-bottom:4px;">
              <span class="status-dot <%= statusClass %>"></span>
              <span class="topo-node-name"><%= server.label %></span>
            </div>
            <div class="topo-node-ip"><%= id === 'trinity-core' ? 'Home (Tunnel)' : (server.online ? '' : 'OFFLINE') %></div>
            <div class="topo-node-role" style="color:<%= server.color %>;"><%= server.role %></div>
            <% if (nodeGames > 0) { %>
              <div class="topo-node-games">🎮 <%= nodeGames %> servers</div>
            <% } %>
          </div>
        </div>
      <% }); %>

      </div> <!-- end topo-zoomable -->

      <!-- Zoom Controls -->
      <div class="zoom-controls">
        <button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
        <button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
        <div class="zoom-level" id="zoom-level">100%</div>
        <button class="zoom-btn" onclick="zoomReset()" title="Reset View" style="font-size:12px;">⌂</button>
      </div>

      <div class="topo-legend">
        <span><span style="color:#f48120">━</span> External</span>
        <span><span style="color:#4ECDC4">━</span> Internal</span>
        <span style="color:#A855F7">╌ SSH</span>
        <span style="color:#d4a574">━ MCP</span>
      </div>
    </div>
  </div>

  <!-- Detail View (hidden initially) -->
  <div id="detail-view" style="display:none;"></div>

  <% } %>
</div>

<script>
// Fleet data from server
const fleet = <%- fleet ? JSON.stringify(fleet) : 'null' %>;

// Service definitions for detail views
const serviceMap = {
  'command-center': [
    { name:'Gitea', domain:'git.firefrostgaming.com', port:3000, icon:'📦' },
    { name:'Arbiter 3.5', domain:'discord-bot.firefrostgaming.com', port:3500, icon:'🤖' },
    { name:'Trinity Console', domain:'discord-bot.firefrostgaming.com/admin', icon:'🎛️' },
    { name:'Uptime Kuma', domain:'status.firefrostgaming.com', port:3001, icon:'📡' },
    { name:'Vaultwarden', domain:'vault.firefrostgaming.com', icon:'🔐' },
    { name:'Code Server', domain:'code.firefrostgaming.com', icon:'💻' },
    { name:'PostgreSQL', port:5432, icon:'🗃️' },
    { name:'Nginx', port:443, icon:'🌐' }
  ],
  'tx1-dallas': [
    { name:'Wings', icon:'🦅' },
    { name:'Nginx', icon:'🌐' },
    { name:'Fail2ban', icon:'🛡️' },
    { name:'Codex Stack', icon:'🧠', note:'Dify + Qdrant + n8n + Ollama' }
  ],
  'nc1-charlotte': [
    { name:'Wings', icon:'🦅' },
    { name:'MariaDB', icon:'🗃️' }
  ],
  'panel-vps': [
    { name:'Pterodactyl Panel', domain:'panel.firefrostgaming.com', icon:'🦕' },
    { name:'Nginx', icon:'🌐' },
    { name:'MariaDB', icon:'🗃️' },
    { name:'Redis', icon:'⚡' },
    { name:'PHP-FPM 8.3', icon:'🐘' }
  ],
  'dev-panel': [
    { name:'Pterodactyl Panel', icon:'🦕' },
    { name:'Wings', icon:'🦅' },
    { name:'Blueprint Beta', icon:'📐' }
  ],
  'wiki-vps': [
    { name:'Staff Wiki', domain:'staff.firefrostgaming.com', icon:'📚' },
    { name:'Subscriber Wiki', domain:'subscribers.firefrostgaming.com', icon:'📖' },
    { name:'Pokerole Wiki', domain:'pokerole.firefrostgaming.com', icon:'🎲' },
    { name:'MkDocs', domain:'docs.firefrostgaming.com', icon:'📄' },
    { name:'PostgreSQL 16', icon:'🗃️' }
  ],
  'services-vps': [
    { name:'Mailcow', domain:'mail.firefrostgaming.com', icon:'📧', note:'18 Docker containers' }
  ],
  'trinity-core': [
    { name:'MCP Server v2.1.0', domain:'mcp.firefrostgaming.com', icon:'🔮' },
    { name:'Cloudflare Tunnel', icon:'🚇' }
  ]
};

const externalInfo = {
  cloudflare: { label:'Cloudflare', icon:'☁️', color:'#f48120', desc:'DNS, CDN, Pages, Tunnel, Workers', connections:['command-center','panel-vps','wiki-vps','services-vps','trinity-core','website'] },
  stripe: { label:'Stripe', icon:'💳', color:'#635bff', desc:'Payment processing (LIVE)', connections:['command-center'] },
  discord: { label:'Discord', icon:'💬', color:'#5865f2', desc:'Community + Bot API', connections:['command-center'] },
  website: { label:'firefrostgaming.com', icon:'🌐', color:'#4ECDC4', desc:'11ty on Cloudflare Pages', connections:['cloudflare'] },
  claude: { label:'Claude.ai', icon:'🧠', color:'#d4a574', desc:'MCP Connector → Trinity Core', connections:['trinity-core'] }
};

// Connection definitions for canvas drawing
const connections = [
  { from:'cloudflare', to:'command-center', type:'external' },
  { from:'cloudflare', to:'panel-vps', type:'external' },
  { from:'cloudflare', to:'wiki-vps', type:'external' },
  { from:'cloudflare', to:'services-vps', type:'external' },
  { from:'cloudflare', to:'trinity-core', type:'tunnel' },
  { from:'cloudflare', to:'website', type:'external' },
  { from:'panel-vps', to:'tx1-dallas', type:'internal' },
  { from:'panel-vps', to:'nc1-charlotte', type:'internal' },
  { from:'command-center', to:'stripe', type:'external' },
  { from:'command-center', to:'discord', type:'external' },
  { from:'command-center', to:'panel-vps', type:'internal' },
  { from:'trinity-core', to:'command-center', type:'ssh' },
  { from:'trinity-core', to:'tx1-dallas', type:'ssh' },
  { from:'trinity-core', to:'nc1-charlotte', type:'ssh' },
  { from:'trinity-core', to:'panel-vps', type:'ssh' },
  { from:'trinity-core', to:'dev-panel', type:'ssh' },
  { from:'trinity-core', to:'wiki-vps', type:'ssh' },
  { from:'trinity-core', to:'services-vps', type:'ssh' },
  { from:'claude', to:'trinity-core', type:'mcp' }
];

let hoveredNode = null;

function getNodePositions() {
  const zoomable = document.getElementById('topo-zoomable');
  if (!zoomable) return {};
  const positions = {};

  // Temporarily remove transform to get unscaled positions
  const savedTransform = zoomable.style.transform;
  const savedTransition = zoomable.style.transition;
  zoomable.style.transition = 'none';
  zoomable.style.transform = 'none';

  const zoomRect = zoomable.getBoundingClientRect();

  document.querySelectorAll('.topo-node, .topo-ext').forEach(el => {
    const id = el.dataset.node;
    if (!id) return;
    const rect = el.getBoundingClientRect();
    positions[id] = {
      x: (rect.left - zoomRect.left) + rect.width / 2,
      y: (rect.top - zoomRect.top) + rect.height / 2
    };
  });

  // Restore transform
  zoomable.style.transform = savedTransform;
  zoomable.style.transition = savedTransition;

  return positions;
}

function drawConnections() {
  const canvas = document.getElementById('topo-canvas');
  const wrap = document.getElementById('topo-wrap');
  if (!canvas || !wrap) return;

  canvas.width = wrap.offsetWidth;
  canvas.height = wrap.offsetHeight;
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  const positions = getNodePositions();

  const colors = { external:'#f4812055', internal:'#4ECDC455', ssh:'#A855F744', tunnel:'#f4812077', mcp:'#d4a57455' };
  const hiColors = { external:'#f48120cc', internal:'#4ECDC4cc', ssh:'#A855F7aa', tunnel:'#f48120ee', mcp:'#d4a574cc' };

  connections.forEach(conn => {
    const from = positions[conn.from];
    const to = positions[conn.to];
    if (!from || !to) return;

    const isHi = hoveredNode && (conn.from === hoveredNode || conn.to === hoveredNode);

    ctx.beginPath();
    ctx.moveTo(from.x, from.y);
    const midY = (from.y + to.y) / 2;
    ctx.bezierCurveTo(from.x, midY, to.x, midY, to.x, to.y);

    ctx.strokeStyle = isHi ? (hiColors[conn.type] || '#fff8') : (colors[conn.type] || '#fff2');
    ctx.lineWidth = isHi ? 2.5 : 1.2;
    ctx.setLineDash(conn.type === 'ssh' ? [4, 4] : []);
    ctx.stroke();
    ctx.setLineDash([]);
  });
}

function highlightNode(id) {
  hoveredNode = id;
  drawConnections();
}

function clearHighlight() {
  hoveredNode = null;
  drawConnections();
}

function progressBarColor(pct) {
  if (pct > 80) return '#ef4444';
  if (pct > 60) return '#eab308';
  return '#4ECDC4';
}

function showTopology() {
  document.getElementById('topology-view').style.display = 'block';
  document.getElementById('detail-view').style.display = 'none';
  document.getElementById('topo-btn').style.display = 'none';
  document.querySelectorAll('.topo-node').forEach(n => n.classList.remove('selected'));
  setTimeout(drawConnections, 50);
}

function showServer(id) {
  if (dragMoved) return;
  if (!fleet) return;
  const server = fleet.servers[id];
  if (!server) return;

  document.querySelectorAll('.topo-node').forEach(n => n.classList.remove('selected'));
  document.querySelector(`.topo-node[data-node="${id}"]`)?.classList.add('selected');

  const services = serviceMap[id] || [];
  const games = fleet.gameServers.filter(g => {
    if (id === 'tx1-dallas') return g.node === 'TX1';
    if (id === 'nc1-charlotte') return g.node === 'NC1';
    return false;
  });
  const totalGameRam = games.reduce((a, g) => a + g.ram, 0);

  const statusClass = server.status || 'healthy';
  const badges = [];
  if (server.restart) badges.push('<span class="badge badge-warning">Restart Required</span>');
  if (server.diskWarning) badges.push('<span class="badge badge-critical">Disk Warning</span>');

  let html = `<div class="detail-view">
    <button class="back-btn" onclick="showTopology()">← Back to Topology</button>
    <div style="display:flex; align-items:center; gap:12px; margin-bottom:20px;">
      <div style="width:48px; height:48px; border-radius:10px; background:${server.color}22; border:2px solid ${server.color}; display:flex; align-items:center; justify-content:center; font-size:22px;">
        ${id === 'trinity-core' ? '🥧' : '🖥️'}
      </div>
      <div>
        <h2 style="font-size:22px; font-weight:700; color:#f5f5f5; margin:0; display:flex; align-items:center; gap:6px;">
          ${server.label}
          <span class="status-dot ${statusClass}"></span>
          ${badges.join('')}
        </h2>
        <div style="font-size:12px; color:#888;">${server.online ? (id === 'trinity-core' ? 'Home (Cloudflare Tunnel)' : '') : 'OFFLINE'} · ${server.role}</div>
      </div>
    </div>`;

  if (server.online) {
    html += `<div class="detail-metrics" style="display:grid; grid-template-columns:repeat(4, 1fr); gap:12px; margin-bottom:20px;">
      <div class="infra-card">
        <div style="font-size:10px; color:#888; margin-bottom:4px;">CPU</div>
        <div style="font-size:13px; font-weight:600; color:#e5e5e5;">${server.cpu || '?'}</div>
        <div style="font-size:10px; color:#666; margin-top:4px;">Load: ${(server.load || [0,0,0]).join(' / ')}</div>
      </div>
      <div class="infra-card">
        <div style="font-size:10px; color:#888; margin-bottom:4px;">RAM</div>
        <div style="font-size:13px; font-weight:600; color:#e5e5e5;">${server.ram?.used || '?'} / ${server.ram?.total || '?'}</div>
        <div class="progress-bar"><div class="progress-bar-fill" style="width:${server.ram?.pct || 0}%; background:${progressBarColor(server.ram?.pct || 0)};"></div></div>
      </div>
      <div class="infra-card">
        <div style="font-size:10px; color:#888; margin-bottom:4px;">Disk</div>
        <div style="font-size:13px; font-weight:600; color:#e5e5e5;">${server.disk?.used || '?'} / ${server.disk?.total || '?'}</div>
        <div class="progress-bar"><div class="progress-bar-fill" style="width:${server.disk?.pct || 0}%; background:${progressBarColor(server.disk?.pct || 0)};"></div></div>
      </div>
      <div class="infra-card">
        <div style="font-size:10px; color:#888; margin-bottom:4px;">Uptime</div>
        <div style="font-size:13px; font-weight:600; color:#e5e5e5;">${server.uptime || '?'}</div>
        <div style="font-size:10px; color:#666; margin-top:4px;">${server.os || ''}</div>
      </div>
    </div>`;
  }

  html += `<div class="detail-panels" style="display:grid; grid-template-columns:${games.length > 0 ? '1fr 1fr' : '1fr'}; gap:16px;">`;

  // Services panel
  html += `<div class="infra-card">
    <h3 style="font-size:14px; font-weight:600; color:#e5e5e5; margin:0 0 12px 0;">Services (${services.length})</h3>`;
  services.forEach(svc => {
    html += `<div class="svc-row">
      <span class="svc-icon">${svc.icon}</span>
      <div style="flex:1;">
        <div class="svc-name">${svc.name}</div>
        ${svc.domain ? `<div class="svc-domain">${svc.domain}</div>` : ''}
        ${svc.note ? `<div class="svc-note">${svc.note}</div>` : ''}
      </div>
      ${svc.port ? `<span class="svc-port">:${svc.port}</span>` : ''}
    </div>`;
  });
  html += `</div>`;

  // Game servers panel (TX1/NC1 only)
  if (games.length > 0) {
    html += `<div class="infra-card">
      <h3 style="font-size:14px; font-weight:600; color:#e5e5e5; margin:0 0 12px 0;">Game Servers (${games.length})</h3>
      <div style="max-height:280px; overflow-y:auto;">`;
    games.forEach(gs => {
      html += `<div style="display:flex; align-items:center; justify-content:space-between; padding:5px 0; border-bottom:1px solid #40404044;">
        <div style="display:flex; align-items:center; gap:6px;">
          <span style="font-size:12px;">🎮</span>
          <span style="font-size:11px; color:#ddd;">${gs.name}</span>
        </div>
        <span style="font-size:9px; color:#888;">${(gs.ram / 1024).toFixed(0)} GB</span>
      </div>`;
    });
    html += `</div>
      <div style="margin-top:8px; font-size:10px; color:#888; border-top:1px solid #404040; padding-top:8px;">
        Total RAM: ${(totalGameRam / 1024).toFixed(0)} GB allocated
      </div>
    </div>`;
  }

  html += `</div></div>`;

  document.getElementById('topology-view').style.display = 'none';
  document.getElementById('detail-view').innerHTML = html;
  document.getElementById('detail-view').style.display = 'block';
  document.getElementById('topo-btn').style.display = 'inline-block';
}

function showExternal(id) {
  if (dragMoved) return;
  const ext = externalInfo[id];
  if (!ext) return;

  const connTypeColors = { external:'#f48120', internal:'#4ECDC4', ssh:'#A855F7', tunnel:'#f48120', mcp:'#d4a574' };

  let html = `<div class="detail-view">
    <button class="back-btn" onclick="showTopology()">← Back to Topology</button>
    <div style="display:flex; align-items:center; gap:12px; margin-bottom:20px;">
      <div style="width:48px; height:48px; border-radius:10px; background:${ext.color}22; border:2px solid ${ext.color}; display:flex; align-items:center; justify-content:center; font-size:22px;">
        ${ext.icon}
      </div>
      <div>
        <h2 style="font-size:22px; font-weight:700; color:#f5f5f5; margin:0;">${ext.label}</h2>
        <div style="font-size:12px; color:#888;">${ext.desc}</div>
      </div>
    </div>
    <div class="infra-card">
      <h3 style="font-size:14px; font-weight:600; color:#e5e5e5; margin:0 0 12px 0;">Connected Infrastructure</h3>`;

  ext.connections.forEach(targetId => {
    const target = fleet?.servers[targetId] || externalInfo[targetId];
    if (!target) return;
    const conn = connections.find(c =>
      (c.from === id && c.to === targetId) || (c.from === targetId && c.to === id)
    );
    const connType = conn?.type || 'external';
    const color = connTypeColors[connType] || '#888';

    html += `<div style="display:flex; align-items:center; gap:8px; padding:8px 0; border-bottom:1px solid #40404044; cursor:pointer;" onclick="${fleet?.servers[targetId] ? `showServer('${targetId}')` : ''}">
      <span class="conn-dot" style="background:${target.color || color};"></span>
      <span style="font-size:12px; color:#ddd; flex:1;">${target.label}</span>
      <span style="font-size:10px; color:${color}; background:${color}15; padding:2px 8px; border-radius:10px;">${conn?.type || 'connected'}</span>
    </div>`;
  });

  html += `</div></div>`;

  document.getElementById('topology-view').style.display = 'none';
  document.getElementById('detail-view').innerHTML = html;
  document.getElementById('detail-view').style.display = 'block';
  document.getElementById('topo-btn').style.display = 'inline-block';
}

async function refreshAudit() {
  const btn = document.getElementById('refresh-btn');
  btn.disabled = true;
  btn.textContent = '⟳ Refreshing...';

  try {
    const res = await fetch('/admin/infrastructure/refresh', {
      headers: { 'CSRF-Token': '<%= typeof csrfToken !== "undefined" ? csrfToken : "" %>' }
    });
    if (res.ok) {
      window.location.reload();
    } else {
      btn.textContent = '⟳ Failed';
      setTimeout(() => { btn.textContent = '⟳ Refresh'; btn.disabled = false; }, 2000);
    }
  } catch (err) {
    btn.textContent = '⟳ Error';
    setTimeout(() => { btn.textContent = '⟳ Refresh'; btn.disabled = false; }, 2000);
  }
}

// ─── Zoom / Pan / Pinch System ───

let zoomScale = 1;
let panX = 0, panY = 0;
let isDragging = false;
let dragStartX = 0, dragStartY = 0;
let dragStartPanX = 0, dragStartPanY = 0;
let dragMoved = false;

const MIN_ZOOM = 0.5;
const MAX_ZOOM = 3.0;
const ZOOM_STEP = 0.15;

function applyTransform() {
  const el = document.getElementById('topo-zoomable');
  if (!el) return;
  el.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomScale})`;
  document.getElementById('zoom-level').textContent = Math.round(zoomScale * 100) + '%';
  // Redraw canvas connections at new scale
  setTimeout(drawConnections, 20);
}

function zoomIn() {
  zoomScale = Math.min(MAX_ZOOM, zoomScale + ZOOM_STEP);
  applyTransform();
}

function zoomOut() {
  zoomScale = Math.max(MIN_ZOOM, zoomScale - ZOOM_STEP);
  applyTransform();
}

function zoomReset() {
  zoomScale = 1;
  panX = 0;
  panY = 0;
  applyTransform();
}

function zoomAtPoint(delta, clientX, clientY) {
  const wrap = document.getElementById('topo-wrap');
  if (!wrap) return;
  const rect = wrap.getBoundingClientRect();

  // Mouse position relative to wrap
  const mx = clientX - rect.left;
  const my = clientY - rect.top;

  // Point in content space before zoom
  const contentX = (mx - panX) / zoomScale;
  const contentY = (my - panY) / zoomScale;

  // Apply zoom
  const oldScale = zoomScale;
  zoomScale = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoomScale + delta));

  // Adjust pan so the point under the mouse stays put
  panX = mx - contentX * zoomScale;
  panY = my - contentY * zoomScale;

  applyTransform();
}

// Mouse wheel zoom
document.addEventListener('DOMContentLoaded', () => {
  const wrap = document.getElementById('topo-wrap');
  if (!wrap) return;

  wrap.addEventListener('wheel', (e) => {
    e.preventDefault();
    const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
    zoomAtPoint(delta, e.clientX, e.clientY);
  }, { passive: false });

  // Mouse drag to pan
  wrap.addEventListener('mousedown', (e) => {
    if (e.target.closest('.topo-node, .topo-ext, .zoom-btn, .zoom-level')) return;
    isDragging = true;
    dragMoved = false;
    dragStartX = e.clientX;
    dragStartY = e.clientY;
    dragStartPanX = panX;
    dragStartPanY = panY;
    wrap.classList.add('dragging');
    document.getElementById('topo-zoomable')?.classList.add('dragging');
    e.preventDefault();
  });

  document.addEventListener('mousemove', (e) => {
    if (!isDragging) return;
    const dx = e.clientX - dragStartX;
    const dy = e.clientY - dragStartY;
    if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragMoved = true;
    panX = dragStartPanX + dx;
    panY = dragStartPanY + dy;
    applyTransform();
  });

  document.addEventListener('mouseup', () => {
    if (isDragging) {
      isDragging = false;
      wrap.classList.remove('dragging');
      document.getElementById('topo-zoomable')?.classList.remove('dragging');
      // Reset dragMoved after a tick so the click event fires first
      setTimeout(() => { dragMoved = false; }, 10);
    }
  });

  // Touch: pinch zoom + drag pan
  let lastTouchDist = 0;
  let lastTouchCenter = { x: 0, y: 0 };

  wrap.addEventListener('touchstart', (e) => {
    if (e.touches.length === 2) {
      e.preventDefault();
      const dx = e.touches[0].clientX - e.touches[1].clientX;
      const dy = e.touches[0].clientY - e.touches[1].clientY;
      lastTouchDist = Math.sqrt(dx * dx + dy * dy);
      lastTouchCenter = {
        x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
        y: (e.touches[0].clientY + e.touches[1].clientY) / 2
      };
    } else if (e.touches.length === 1) {
      if (e.target.closest('.topo-node, .topo-ext, .zoom-btn')) return;
      isDragging = true;
      dragMoved = false;
      dragStartX = e.touches[0].clientX;
      dragStartY = e.touches[0].clientY;
      dragStartPanX = panX;
      dragStartPanY = panY;
    }
  }, { passive: false });

  wrap.addEventListener('touchmove', (e) => {
    if (e.touches.length === 2) {
      e.preventDefault();
      const dx = e.touches[0].clientX - e.touches[1].clientX;
      const dy = e.touches[0].clientY - e.touches[1].clientY;
      const dist = Math.sqrt(dx * dx + dy * dy);
      const center = {
        x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
        y: (e.touches[0].clientY + e.touches[1].clientY) / 2
      };

      if (lastTouchDist > 0) {
        const scaleDelta = (dist - lastTouchDist) * 0.005;
        zoomAtPoint(scaleDelta, center.x, center.y);
      }

      lastTouchDist = dist;
      lastTouchCenter = center;
    } else if (e.touches.length === 1 && isDragging) {
      const dx = e.touches[0].clientX - dragStartX;
      const dy = e.touches[0].clientY - dragStartY;
      if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragMoved = true;
      panX = dragStartPanX + dx;
      panY = dragStartPanY + dy;
      applyTransform();
    }
  }, { passive: false });

  wrap.addEventListener('touchend', () => {
    isDragging = false;
    lastTouchDist = 0;
    setTimeout(() => { dragMoved = false; }, 10);
  });
});

// Draw connections on load and resize
window.addEventListener('load', () => setTimeout(drawConnections, 100));
window.addEventListener('resize', drawConnections);
</script>
