Files
clawmates/Infrastructure design package/Clawmates Infrastructure.dc.html
T
Omar SobhandClaude Opus 4.8 b840de7c3b
ci / gates (push) Failing after 5s
ci / rust (push) Has been skipped
ci / sandbox-k8s (push) Has been skipped
ci / frontend (push) Has been skipped
ci / e2e (push) Has been skipped
Infra: operator-console redesign (per the Infrastructure design package)
Refactor the Infrastructure tier into the dark operator console from the design
package, wired to the real fleet backend (no new plumbing):
- InfraNav: page-scoped left nav grouped RUN HERE FIRST (Local & Tailscale, with
  online count + sub-items) / NEXT (Cloud providers, SOON) + coral "Connect a
  host" button; coral spine on the active view.
- FleetConsole: center reads top-to-bottom — kicker → "Your fleet" → 5 stat tiles
  (HOSTS/ONLINE/MEMORY/STORAGE/CONTAINERS) → Tailscale device card → LOCAL HOSTS
  host cards + add-host dashed tile. Cloud view = how-it-works + SOON.
- Host cards (design system): status dot (blink when online) + hostname, IP/version
  line, CPU/RAM meter bars (cyan→amber→coral by load), disk/load/ctrs mini-row, and
  a footer that's a copyable `ssh <node>` target (online) OR a "waiting for daemon"
  spinner (pairing) — PLUS a Terminal button that opens the in-app shell.
- FleetPill (top bar, N/M hosts online) + FleetStatusBar (ambient: daemon · tailnet
  · WSS · sandbox).

Per the chosen reconciliation: kept the cloud-apps computer pull-out as the right
column, the connect-host wizard as a modal, and the in-app terminal. Dashboard
infra branch rewired (InfraNav + FleetConsole + status bar; dropped the old
InfraStage/InfraConsole split). cm-blink/spin/cm-fade keyframes already existed.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-24 22:49:17 -07:00

709 lines
69 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<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=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: #08080a; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
@keyframes cm-blink { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
@keyframes cm-spin { to { transform: rotate(360deg); } }
@keyframes cm-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
@keyframes cm-pip { 0%,100% { opacity: .35; } 50% { opacity: 1; } }
.cm-scroll::-webkit-scrollbar { width: 8px; }
.cm-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 4px; }
</style>
</helmet>
<div style="width:100%; height:100vh; min-height:660px; background:#08080a; color:#f3f3f5; display:flex; flex-direction:column; overflow:hidden;">
<!-- TOP BAR -->
<div style="height:50px; flex:none; display:flex; align-items:center; gap:13px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0d0d10,#0a0a0c);">
<div style="display:flex; align-items:center; gap:9px;">
<svg width="20" height="20" viewBox="0 0 22 22" fill="none"><path d="M11 3 L18.5 17 L3.5 17 Z" stroke="#ff6f61" stroke-width="1.3" stroke-linejoin="round" opacity="0.55"></path><circle cx="11" cy="3.5" r="2.2" fill="#ff6f61"></circle><circle cx="18" cy="17" r="2.2" fill="#ff6f61"></circle><circle cx="4" cy="17" r="2.2" fill="#ff6f61"></circle></svg>
<span style="font-size:14px; font-weight:700;">Clawmates</span>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">Large World</span>
<span style="color:#3a3a40;">/</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">Agents</span>
<span style="color:#3a3a40;">/</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#f3f3f5; background:rgba(255,111,97,.12); border:1px solid rgba(255,111,97,.28); padding:3px 9px; border-radius:6px;">Infrastructure</span>
<div style="flex:1;"></div>
<div style="display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#5fd08a; padding:5px 10px; border:1px solid rgba(95,208,138,.25); border-radius:7px; background:rgba(95,208,138,.06);">
<span style="width:6px; height:6px; border-radius:50%; background:#5fd08a; animation:cm-blink 1.6s infinite;"></span>{{ fleetOnline }}/{{ fleetTotal }} hosts online
</div>
<div style="width:30px; height:30px; border-radius:8px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#2a0d0a;">O</div>
</div>
<!-- BODY -->
<div style="flex:1; display:flex; min-height:0;">
<!-- ICON RAIL -->
<div style="width:54px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0a0a0c; display:flex; flex-direction:column; align-items:center; padding:12px 0; gap:6px;">
<div style="width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62;">
<svg width="18" height="18" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.4" fill="none"></circle><circle cx="10" cy="10" r="2.4" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">WORLD</span>
</div>
<div style="width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62;">
<svg width="18" height="18" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="10" cy="10" r="2.2" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">AGENT</span>
</div>
<div style="position:relative; width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#ff6f61; background:rgba(255,111,97,.1);">
<span style="position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
<svg width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="3" y="11" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="6" cy="6.5" r="1" fill="currentColor"></circle><circle cx="6" cy="13.5" r="1" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">INFRA</span>
</div>
<div style="flex:1;"></div>
<div style="width:28px; height:28px; border-radius:8px; border:1px dashed rgba(255,255,255,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:16px; font-weight:300;">+</div>
</div>
<!-- LEFT NAV -->
<div style="width:216px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
<div style="padding:14px 14px 12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">INFRASTRUCTURE</div>
<div style="font-size:17px; font-weight:700; margin-top:3px;">Your fleet</div>
</div>
<div style="flex:1; overflow-y:auto; padding:4px 8px;" class="cm-scroll">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.14em; color:#5a5a62; padding:8px 8px 6px;">RUN HERE FIRST</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; position:relative; {{ navLocalStyle }}" onClick="{{ goLocal }}">
<sc-if value="{{ isLocal }}" hint-placeholder-val="{{ true }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<svg width="16" height="16" viewBox="0 0 20 20" style="color:{{ navLocalIcon }};"><rect x="3" y="4" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="3" y="11" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="6" cy="6.5" r="1" fill="currentColor"></circle><circle cx="6" cy="13.5" r="1" fill="currentColor"></circle></svg>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:{{ navLocalText }};">Local & Tailscale</div></div>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">{{ fleetOnline }}</span>
</div>
<div style="display:flex; flex-direction:column; gap:1px; margin:2px 0 10px 14px; padding-left:10px; border-left:1px solid rgba(255,255,255,.07);">
<div style="display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer;"><span style="width:5px; height:5px; border-radius:50%; background:#5fd08a;"></span><span style="font-size:12px; color:#b5b5bd;">Local hosts</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">{{ fleetTotal }}</span></div>
<div style="display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer;"><span style="width:5px; height:5px; border-radius:50%; background:#5ec8d8;"></span><span style="font-size:12px; color:#b5b5bd;">Tailscale network</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">{{ tsDeviceCount }}</span></div>
</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.14em; color:#5a5a62; padding:8px 8px 6px;">NEXT</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; position:relative; {{ navCloudStyle }}" onClick="{{ goCloud }}">
<sc-if value="{{ isCloud }}" hint-placeholder-val="{{ false }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<svg width="16" height="16" viewBox="0 0 20 20" style="color:{{ navCloudIcon }};"><path d="M6 14h9a3 3 0 000-6 4.5 4.5 0 00-8.7-1.2A3.4 3.4 0 006 14z" stroke="currentColor" stroke-width="1.4" fill="none"></path></svg>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:{{ navCloudText }};">Cloud providers</div></div>
<span style="font-family:'JetBrains Mono',monospace; font-size:8px; color:#e8b465; padding:2px 6px; border-radius:5px; background:rgba(232,196,106,.1);">SOON</span>
</div>
</div>
<div style="flex:none; padding:10px 12px; border-top:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; justify-content:center; gap:7px; height:36px; border-radius:9px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ openHostWizard }}"><span style="font-size:15px; line-height:1;">+</span> Connect a host</div>
</div>
</div>
<!-- CENTER -->
<div style="flex:1; min-width:0; background:radial-gradient(130% 100% at 50% 0%, #0d0d12 0%, #08080a 60%); display:flex; flex-direction:column; min-height:0;">
<sc-if value="{{ isLocal }}" hint-placeholder-val="{{ true }}">
<div style="flex:1; min-height:0; overflow-y:auto; padding:22px 26px 30px;" class="cm-scroll">
<div style="margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">LOCAL &amp; TAILSCALE</div>
<div style="display:flex; align-items:flex-end; gap:14px; margin-bottom:20px;">
<div><div style="font-size:26px; font-weight:700; letter-spacing:-.02em;">Your fleet</div><div style="font-size:13px; color:#8a8a92; margin-top:3px;">Run agents on hardware you own. Put a node on your tailnet, then pair it — no inbound port, no keys.</div></div>
</div>
<!-- fleet stats -->
<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin-bottom:22px;">
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">HOSTS</div><div style="font-size:22px; font-weight:700; margin-top:4px;">{{ fleetTotal }}</div></div>
<div style="border-radius:12px; border:1px solid rgba(95,208,138,.18); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">ONLINE</div><div style="font-size:22px; font-weight:700; margin-top:4px; color:#5fd08a;">{{ fleetOnline }}</div></div>
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">vCPU</div><div style="font-size:22px; font-weight:700; margin-top:4px;">{{ vcpu }}</div></div>
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">MEMORY</div><div style="font-size:22px; font-weight:700; margin-top:4px;">{{ ram }}</div></div>
<div style="border-radius:12px; border:1px solid rgba(94,200,216,.18); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">CONTAINERS</div><div style="font-size:22px; font-weight:700; margin-top:4px; color:#5ec8d8;">{{ containersRunning }}</div></div>
</div>
<!-- Tailscale network -->
<div style="border-radius:14px; border:1px solid rgba(255,255,255,.08); background:#0c0c0f; margin-bottom:22px; overflow:hidden;">
<div style="display:flex; align-items:center; gap:11px; padding:13px 16px; border-bottom:1px solid rgba(255,255,255,.05);">
<div style="width:30px; height:30px; border-radius:8px; background:#fff; display:flex; align-items:center; justify-content:center;">
<svg width="17" height="17" viewBox="0 0 24 24"><g fill="#141414"><circle cx="5" cy="5" r="2.1" opacity=".3"></circle><circle cx="12" cy="5" r="2.1" opacity=".3"></circle><circle cx="19" cy="5" r="2.1" opacity=".3"></circle><circle cx="5" cy="12" r="2.1"></circle><circle cx="12" cy="12" r="2.1"></circle><circle cx="19" cy="12" r="2.1"></circle><circle cx="5" cy="19" r="2.1" opacity=".3"></circle><circle cx="12" cy="19" r="2.1"></circle><circle cx="19" cy="19" r="2.1" opacity=".3"></circle></g></svg>
</div>
<div style="flex:1;"><div style="font-size:14px; font-weight:700;">Tailscale network</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">{{ tailnet }} · live device status</div></div>
<span style="display:flex; align-items:center; gap:6px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a; padding:4px 10px; border-radius:7px; background:rgba(95,208,138,.08); border:1px solid rgba(95,208,138,.25);"><span style="width:6px; height:6px; border-radius:50%; background:#5fd08a; animation:cm-blink 1.6s infinite;"></span>connected</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ openTailscale }}">manage →</span>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1px; background:rgba(255,255,255,.04);">
<sc-for list="{{ tsDevices }}" as="d" hint-placeholder-count="4">
<div style="display:flex; align-items:center; gap:10px; padding:11px 16px; background:#0c0c0f;">
<span style="width:7px; height:7px; border-radius:50%; background:{{ d.dot }};"></span>
<span style="font-size:13px; font-weight:600; color:#eaeaee;">{{ d.name }}</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92;">{{ d.os }}</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; width:96px; text-align:right;">{{ d.ip }}</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; width:40px; text-align:right;">{{ d.seen }}</span>
</div>
</sc-for>
</div>
</div>
<!-- host health cards -->
<div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">LOCAL HOSTS</span>
<span style="flex:1; height:1px; background:rgba(255,255,255,.06);"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ openHostWizard }}">+ connect a host</span>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(290px, 1fr)); gap:14px;">
<sc-for list="{{ hostCards }}" as="h" hint-placeholder-count="3">
<div style="border-radius:13px; border:1px solid {{ h.accentBorder }}; background:#0d0d10; padding:15px; display:flex; flex-direction:column;">
<div style="display:flex; align-items:center; gap:9px; margin-bottom:13px;">
<span style="position:relative; width:9px; height:9px;"><span style="position:absolute; inset:0; border-radius:50%; background:{{ h.statusColor }};"></span></span>
<span style="font-size:14px; font-weight:700;">{{ h.name }}</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:{{ h.statusColor }};">{{ h.statusLabel }}</span>
</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; margin-bottom:14px;">{{ h.os }}</div>
<div style="display:flex; flex-direction:column; gap:9px; margin-bottom:13px;">
<div>
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>CPU</span><span style="color:{{ h.cpuCol }};">{{ h.cpu }}%</span></div>
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ h.cpuW }}; height:100%; background:{{ h.cpuCol }};"></div></div>
</div>
<div>
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>RAM</span><span style="color:{{ h.ramCol }};">{{ h.ram }}%</span></div>
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ h.ramW }}; height:100%; background:{{ h.ramCol }};"></div></div>
</div>
</div>
<div style="display:flex; gap:6px; margin-bottom:13px;">
<div style="flex:1; text-align:center; padding:7px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">disk</div><div style="font-size:12px; font-weight:600; margin-top:2px;">{{ h.disk }}%</div></div>
<div style="flex:1; text-align:center; padding:7px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">load</div><div style="font-size:12px; font-weight:600; margin-top:2px;">{{ h.load }}</div></div>
<div style="flex:1; text-align:center; padding:7px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">ctrs</div><div style="font-size:12px; font-weight:600; margin-top:2px;">{{ h.containers }}</div></div>
</div>
<sc-if value="{{ h.hasSsh }}" hint-placeholder-val="{{ true }}">
<div style="display:flex; align-items:center; gap:8px; padding:8px 11px; border-radius:8px; background:#070708; border:1px solid rgba(255,255,255,.07);">
<span style="width:14px; height:14px; flex:none;"><svg width="14" height="14" viewBox="0 0 24 24"><g fill="#5ec8d8"><circle cx="6" cy="6" r="1.8"></circle><circle cx="12" cy="6" r="1.8" opacity=".4"></circle><circle cx="6" cy="12" r="1.8"></circle><circle cx="12" cy="12" r="1.8"></circle><circle cx="18" cy="12" r="1.8" opacity=".4"></circle><circle cx="12" cy="18" r="1.8" opacity=".4"></circle></g></svg></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#cfcfd5; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ h.sshTarget }}</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8; cursor:pointer;">copy</span>
</div>
</sc-if>
<sc-if value="{{ h.pairing }}" hint-placeholder-val="{{ false }}">
<div style="display:flex; align-items:center; gap:8px; padding:8px 11px; border-radius:8px; background:rgba(232,196,106,.06); border:1px solid rgba(232,196,106,.2);">
<span style="width:11px; height:11px; border:2px solid #e8b465; border-top-color:transparent; border-radius:50%; animation:cm-spin .8s linear infinite;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#e8b465;">waiting for daemon to dial home…</span>
</div>
</sc-if>
</div>
</sc-for>
<!-- add host tile -->
<div style="border-radius:13px; border:1.5px dashed rgba(255,255,255,.14); background:transparent; padding:15px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; min-height:200px; cursor:pointer;" onClick="{{ openHostWizard }}">
<div style="width:38px; height:38px; border-radius:11px; background:rgba(255,111,97,.12); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:22px; font-weight:300;">+</div>
<div style="font-size:13px; font-weight:600; color:#cfcfd5;">Connect a host</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; text-align:center;">Mac · Linux · edge device</div>
</div>
</div>
</div>
</sc-if>
<sc-if value="{{ isCloud }}" hint-placeholder-val="{{ false }}">
<div style="flex:1; min-height:0; overflow-y:auto; padding:22px 26px 30px;" class="cm-scroll">
<div style="margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">CLOUD PROVIDERS</div>
<div style="font-size:26px; font-weight:700; letter-spacing:-.02em; margin-bottom:4px;">Power the platform from the cloud</div>
<div style="font-size:13px; color:#8a8a92; max-width:600px; margin-bottom:22px; line-height:1.5;">Connect a provider and Clawmates provisions agent runners on demand. Each booted VM runs the same daemon, dials home over outbound WSS, and joins your fleet — credentials stay in the secret broker.</div>
<!-- how cloud access works -->
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:26px;">
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; padding:15px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;"><span style="width:22px; height:22px; border-radius:6px; background:rgba(255,111,97,.14); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; color:#ff8a7a;">1</span><span style="font-size:13px; font-weight:700;">Connect credentials</span></div>
<div style="font-size:11.5px; color:#8a8a92; line-height:1.5;">Scoped IAM role or API token — held by the <span style="color:#cfcfd5;">secret broker</span>, never reaches agent code.</div>
</div>
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; padding:15px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;"><span style="width:22px; height:22px; border-radius:6px; background:rgba(94,200,216,.14); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; color:#5ec8d8;">2</span><span style="font-size:13px; font-weight:700;">Provision a runner</span></div>
<div style="font-size:11.5px; color:#8a8a92; line-height:1.5;">We bake <span style="font-family:'JetBrains Mono',monospace; color:#cfcfd5;">clawmates-node</span> into the image; it boots and dials home — no inbound port.</div>
</div>
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; padding:15px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;"><span style="width:22px; height:22px; border-radius:6px; background:rgba(95,208,138,.14); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; color:#5fd08a;">3</span><span style="font-size:13px; font-weight:700;">Agents run sandboxed</span></div>
<div style="font-size:11.5px; color:#8a8a92; line-height:1.5;">§15 network-isolated sandboxes on the cloud host; every egress is a gated door.</div>
</div>
</div>
<!-- provider grid -->
<div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">PROVIDERS</span>
<span style="flex:1; height:1px; background:rgba(255,255,255,.06);"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">1 connected</span>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(232px, 1fr)); gap:14px; margin-bottom:28px;">
<sc-for list="{{ cloudProviders }}" as="p" hint-placeholder-count="5">
<div style="border-radius:14px; border:1px solid {{ p.cardBorder }}; background:#0d0d10; padding:18px; display:flex; flex-direction:column;">
<div style="display:flex; align-items:flex-start; gap:11px; margin-bottom:14px;">
<div style="width:42px; height:42px; border-radius:11px; background:{{ p.tile }}; display:flex; align-items:center; justify-content:center; font-weight:700; color:{{ p.ink }}; font-size:13px;">
<sc-if value="{{ p.isText }}" hint-placeholder-val="{{ true }}">{{ p.short }}</sc-if>
<sc-if value="{{ p.isApple }}" hint-placeholder-val="{{ false }}"><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2.5" stroke="#1a1a1d" stroke-width="1.6"></rect><rect x="9.5" y="9.5" width="5" height="5" rx="1" fill="#1a1a1d"></rect><path d="M9 6V3M15 6V3M9 21v-3M15 21v-3M6 9H3M6 15H3M21 9h-3M21 15h-3" stroke="#1a1a1d" stroke-width="1.5" stroke-linecap="round"></path></svg></sc-if>
</div>
<div style="flex:1; min-width:0;">
<div style="font-size:14px; font-weight:700;">{{ p.name }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-top:2px;">{{ p.regions }}</div>
</div>
</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; margin-bottom:16px; line-height:1.5;">{{ p.offers }}</div>
<div style="margin-top:auto; display:flex; align-items:center; justify-content:center; gap:7px; height:34px; border-radius:8px; cursor:pointer; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; {{ p.statusStyle }}" onClick="{{ p.onConnect }}">{{ p.statusLabel }}</div>
</div>
</sc-for>
</div>
<!-- live cloud runners (connected provider) -->
<div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
<div style="width:24px; height:24px; border-radius:7px; background:linear-gradient(135deg,#ff9a3c,#ff7314); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:9px; font-weight:700; color:#241200;">aws</div>
<span style="font-size:14px; font-weight:700;">Cloud runners</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a;">● 2 online · 5 agents</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#e8b465;">~$0.44/hr</span>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:14px;">
<sc-for list="{{ cloudRunners }}" as="r" hint-placeholder-count="2">
<div style="border-radius:13px; border:1px solid rgba(95,208,138,.18); background:#0d0d10; padding:15px;">
<div style="display:flex; align-items:center; gap:9px; margin-bottom:12px;">
<span style="width:8px; height:8px; border-radius:50%; background:#5fd08a;"></span>
<span style="font-size:13px; font-weight:700;">{{ r.name }}</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8;">{{ r.region }}</span>
</div>
<div style="display:flex; gap:14px; margin-bottom:12px;">
<div style="flex:1;">
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>CPU</span><span style="color:#5ec8d8;">{{ r.cpu }}%</span></div>
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ r.cpuW }}; height:100%; background:#5ec8d8;"></div></div>
</div>
<div style="flex:1;">
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>RAM</span><span style="color:#5fd08a;">{{ r.ram }}%</span></div>
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ r.ramW }}; height:100%; background:#5fd08a;"></div></div>
</div>
</div>
<div style="display:flex; align-items:center; gap:8px; font-family:'JetBrains Mono',monospace; font-size:10px;">
<span style="color:#cfcfd5; padding:3px 8px; border-radius:6px; background:rgba(255,255,255,.04);">{{ r.agents }} agents</span>
<span style="flex:1;"></span>
<span style="color:#e8b465;">{{ r.cost }}</span>
</div>
</div>
</sc-for>
</div>
</div>
</sc-if>
</div>
<!-- RIGHT PANEL -->
<div style="width:344px; flex:none; border-left:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
<div style="flex:none; display:flex; align-items:center; gap:8px; padding:13px 16px; border-bottom:1px solid rgba(255,255,255,.06);">
<sc-if value="{{ showMenu }}" hint-placeholder-val="{{ true }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">ADD TO FLEET</span></sc-if>
<sc-if value="{{ showWizard }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ backToMenu }}">← add to fleet</span></sc-if>
<sc-if value="{{ showTailscale }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ backToMenu }}">← add to fleet</span></sc-if>
<sc-if value="{{ showCloudMenu }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">CONNECT A PROVIDER</span></sc-if>
<sc-if value="{{ showCloudConnect }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ backToMenu }}">← providers</span></sc-if>
</div>
<div style="flex:1; min-height:0; overflow-y:auto; padding:16px;" class="cm-scroll">
<!-- MENU -->
<sc-if value="{{ showMenu }}" hint-placeholder-val="{{ true }}">
<div style="display:flex; flex-direction:column; gap:11px;">
<div style="border-radius:13px; border:1px solid rgba(255,111,97,.3); background:linear-gradient(180deg,#140f12,#0d0d10); padding:16px; cursor:pointer;" onClick="{{ openHostWizard }}">
<div style="display:flex; align-items:center; gap:11px; margin-bottom:9px;">
<div style="width:34px; height:34px; border-radius:9px; background:rgba(255,111,97,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61;"><svg width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="3" y="11" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
<div style="flex:1;"><div style="font-size:14px; font-weight:700;">Connect a host</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">recommended first step</div></div>
<span style="color:#ff8a7a;"></span>
</div>
<div style="font-size:12px; color:#9a9aa2; line-height:1.5;">Pair a Mac, Linux box, or edge device. The daemon dials home over outbound WSS — no inbound port, no keys.</div>
</div>
<div style="border-radius:13px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; padding:16px; cursor:pointer;" onClick="{{ openTailscale }}">
<div style="display:flex; align-items:center; gap:11px; margin-bottom:9px;">
<div style="width:34px; height:34px; border-radius:9px; background:#fff; display:flex; align-items:center; justify-content:center;"><svg width="17" height="17" viewBox="0 0 24 24"><g fill="#141414"><circle cx="5" cy="5" r="2.1" opacity=".3"></circle><circle cx="12" cy="5" r="2.1" opacity=".3"></circle><circle cx="19" cy="5" r="2.1" opacity=".3"></circle><circle cx="5" cy="12" r="2.1"></circle><circle cx="12" cy="12" r="2.1"></circle><circle cx="19" cy="12" r="2.1"></circle><circle cx="5" cy="19" r="2.1" opacity=".3"></circle><circle cx="12" cy="19" r="2.1"></circle><circle cx="19" cy="19" r="2.1" opacity=".3"></circle></g></svg></div>
<div style="flex:1;"><div style="font-size:14px; font-weight:700;">Tailscale network</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">connected · {{ tsDeviceCount }} devices</div></div>
<span style="color:#8a8a92;"></span>
</div>
<div style="font-size:12px; color:#9a9aa2; line-height:1.5;">Paste your tailnet + API key for live device status across the whole network.</div>
</div>
<div style="border-radius:13px; border:1px solid rgba(255,255,255,.06); background:#0a0a0c; padding:16px; opacity:.7;">
<div style="display:flex; align-items:center; gap:11px; margin-bottom:9px;">
<div style="width:34px; height:34px; border-radius:9px; background:rgba(255,255,255,.05); display:flex; align-items:center; justify-content:center; color:#6a6a72;"><svg width="18" height="18" viewBox="0 0 20 20"><path d="M6 14h9a3 3 0 000-6 4.5 4.5 0 00-8.7-1.2A3.4 3.4 0 006 14z" stroke="currentColor" stroke-width="1.4" fill="none"></path></svg></div>
<div style="flex:1;"><div style="font-size:14px; font-weight:700; color:#9a9aa2;">Cloud providers</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">AWS · GCP · Azure</div></div>
<span style="font-family:'JetBrains Mono',monospace; font-size:8px; color:#e8b465; padding:2px 6px; border-radius:5px; background:rgba(232,196,106,.1);">NEXT</span>
</div>
<div style="font-size:12px; color:#6a6a72; line-height:1.5;">Local & Tailscale first. Cloud provisioning arrives next.</div>
</div>
</div>
</sc-if>
<!-- WIZARD -->
<sc-if value="{{ showWizard }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="font-size:16px; font-weight:700; margin-bottom:3px;">Connect a host</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; margin-bottom:16px;">3 steps · ~2 min</div>
<!-- stepper -->
<div style="display:flex; align-items:center; gap:6px; margin-bottom:18px;">
<span style="width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; {{ stepIs1Style }}">1</span>
<span style="flex:1; height:1px; background:rgba(255,255,255,.1);"></span>
<span style="width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; {{ stepIs2Style }}">2</span>
<span style="flex:1; height:1px; background:rgba(255,255,255,.1);"></span>
<span style="width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; {{ stepIs3Style }}">3</span>
</div>
<!-- STEP 1 INSTALL -->
<sc-if value="{{ step1 }}" hint-placeholder-val="{{ true }}">
<div style="animation:cm-fade .2s ease;">
<div style="font-size:14px; font-weight:700; margin-bottom:4px;">Install the daemon</div>
<div style="font-size:12px; color:#9a9aa2; line-height:1.55; margin-bottom:13px;">We minted a one-time token. Run this on the node you're adding:</div>
<div style="border-radius:10px; background:#070708; border:1px solid rgba(255,255,255,.08); overflow:hidden; margin-bottom:6px;">
<div style="display:flex; align-items:center; gap:7px; padding:7px 11px; border-bottom:1px solid rgba(255,255,255,.05);"><span style="width:6px; height:6px; border-radius:50%; background:#ff5f57;"></span><span style="width:6px; height:6px; border-radius:50%; background:#febc2e;"></span><span style="width:6px; height:6px; border-radius:50%; background:#28c840;"></span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8; cursor:pointer;" onClick="{{ copyToken }}">{{ copyLabel }}</span></div>
<div style="padding:12px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.7; color:#cfcfd5;"><span style="color:#5fd08a;">clawmates-node</span> \<br>&nbsp;&nbsp;--server <span style="color:#5ec8d8;">https://clawmates.work</span> \<br>&nbsp;&nbsp;--token <span style="color:#e8b465;">cmn_8f3a…d21</span></div>
</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62; margin-bottom:14px;">one-time token · expires in 15 min</div>
<div style="display:flex; flex-direction:column; gap:9px; margin-bottom:16px;">
<div style="display:flex; gap:9px; padding:10px 12px; border-radius:9px; background:rgba(94,200,216,.05); border:1px solid rgba(94,200,216,.18);">
<span style="color:#5ec8d8; flex:none;"></span>
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;"><span style="color:#cfcfd5; font-weight:600;">Docker</span> should be installed on the node so it can run agents later.</div>
</div>
<div style="display:flex; gap:9px; padding:10px 12px; border-radius:9px; background:rgba(255,255,255,.03); border:1px solid rgba(255,255,255,.07);">
<span style="width:14px; flex:none;"><svg width="14" height="14" viewBox="0 0 24 24"><g fill="#9a9aa2"><circle cx="6" cy="6" r="1.8"></circle><circle cx="12" cy="6" r="1.8" opacity=".4"></circle><circle cx="6" cy="12" r="1.8"></circle><circle cx="12" cy="12" r="1.8"></circle><circle cx="18" cy="12" r="1.8" opacity=".4"></circle></g></svg></span>
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;">Tip: run <span style="font-family:'JetBrains Mono',monospace; color:#cfcfd5;">sudo tailscale up --ssh</span> first for keyless, ACL-gated SSH to this node.</div>
</div>
</div>
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ toConnect }}">I've run it — waiting for connection →</div>
</div>
</sc-if>
<!-- STEP 2 CONNECT -->
<sc-if value="{{ step2 }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<sc-if value="{{ connecting }}" hint-placeholder-val="{{ true }}">
<div style="display:flex; flex-direction:column; align-items:center; text-align:center; padding:24px 0;">
<div style="width:42px; height:42px; border:3px solid rgba(94,200,216,.25); border-top-color:#5ec8d8; border-radius:50%; animation:cm-spin .8s linear infinite; margin-bottom:16px;"></div>
<div style="font-size:14px; font-weight:700; margin-bottom:5px;">Waiting for the daemon to dial home…</div>
<div style="font-size:12px; color:#8a8a92; line-height:1.5; max-width:240px;">Outbound WSS connection · NAT-friendly · no inbound port opened.</div>
</div>
</sc-if>
<sc-if value="{{ connected }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="display:flex; align-items:center; gap:9px; margin-bottom:14px;"><span style="width:26px; height:26px; border-radius:50%; background:rgba(95,208,138,.16); display:flex; align-items:center; justify-content:center; color:#5fd08a; font-size:14px;"></span><div><div style="font-size:14px; font-weight:700;">Connected</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">daemon online · forge-02</div></div></div>
<div style="border-radius:11px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:13px 15px; margin-bottom:16px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#5a5a62; margin-bottom:10px;">HOST SPECS</div>
<div style="display:flex; flex-direction:column; gap:8px; font-family:'JetBrains Mono',monospace; font-size:11px;">
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">hostname</span><span style="color:#cfcfd5;">forge-02</span></div>
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">os</span><span style="color:#cfcfd5;">Ubuntu 24.04 · x86_64</span></div>
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">cpu</span><span style="color:#cfcfd5;">16 vCPU · AMD EPYC</span></div>
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">memory</span><span style="color:#cfcfd5;">64 GB</span></div>
</div>
</div>
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ toVerify }}">Verify the host →</div>
</div>
</sc-if>
</div>
</sc-if>
<!-- STEP 3 VERIFY -->
<sc-if value="{{ step3 }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="font-size:14px; font-weight:700; margin-bottom:4px;">Verify we can operate</div>
<div style="font-size:12px; color:#9a9aa2; line-height:1.55; margin-bottom:13px;">Built-in check confirms the daemon can run agents on this host.</div>
<div style="border-radius:10px; background:#070708; border:1px solid rgba(255,255,255,.08); padding:13px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.8; margin-bottom:16px;">
<div style="color:#5fd08a;">✓ uname -a</div>
<div style="color:#6a6a72; padding-left:14px;">Linux forge-02 6.8.0 x86_64 GNU/Linux</div>
<div style="color:#5fd08a; margin-top:4px;">✓ docker version</div>
<div style="color:#6a6a72; padding-left:14px;">Client/Server 27.1.1 · runtime ok</div>
<div style="color:#5fd08a; margin-top:4px;">✓ §15 sandbox profile applied</div>
</div>
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#7fe0a0,#5fd08a); color:#06281a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ finishWizard }}">✓ Add forge-02 to fleet</div>
</div>
</sc-if>
</div>
</sc-if>
<!-- TAILSCALE CONNECT -->
<sc-if value="{{ showTailscale }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="display:flex; align-items:center; gap:10px; margin-bottom:14px;">
<div style="width:32px; height:32px; border-radius:8px; background:#fff; display:flex; align-items:center; justify-content:center;"><svg width="17" height="17" viewBox="0 0 24 24"><g fill="#141414"><circle cx="5" cy="5" r="2.1" opacity=".3"></circle><circle cx="12" cy="5" r="2.1" opacity=".3"></circle><circle cx="19" cy="5" r="2.1" opacity=".3"></circle><circle cx="5" cy="12" r="2.1"></circle><circle cx="12" cy="12" r="2.1"></circle><circle cx="19" cy="12" r="2.1"></circle><circle cx="5" cy="19" r="2.1" opacity=".3"></circle><circle cx="12" cy="19" r="2.1"></circle><circle cx="19" cy="19" r="2.1" opacity=".3"></circle></g></svg></div>
<div><div style="font-size:15px; font-weight:700;">Tailscale network</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">connected</div></div>
</div>
<div style="font-size:12px; color:#9a9aa2; line-height:1.55; margin-bottom:14px;">Connect your tailnet for live device status across the whole network — online, last-seen, IP, OS.</div>
<div style="margin-bottom:12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">TAILNET</div>
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5;">{{ tailnet }}</div>
</div>
<div style="margin-bottom:16px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">API KEY</div>
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">tskey-api-••••••••••••3f9</div>
</div>
<div style="border-radius:11px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; overflow:hidden;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#5a5a62; padding:10px 13px; border-bottom:1px solid rgba(255,255,255,.05);">{{ tsDeviceCount }} DEVICES</div>
<sc-for list="{{ tsDevices }}" as="d" hint-placeholder-count="4">
<div style="display:flex; align-items:center; gap:9px; padding:9px 13px; border-bottom:1px solid rgba(255,255,255,.03);">
<span style="width:6px; height:6px; border-radius:50%; background:{{ d.dot }};"></span>
<span style="font-size:12px; font-weight:600; color:#eaeaee;">{{ d.name }}</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;">{{ d.ip }}</span>
</div>
</sc-for>
</div>
</div>
</sc-if>
<!-- CLOUD MENU (provider quick-connect) -->
<sc-if value="{{ showCloudMenu }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="font-size:12px; color:#9a9aa2; line-height:1.5; margin-bottom:14px;">Pick a provider to connect. Credentials are held by the secret broker and never reach agent code.</div>
<div style="display:flex; flex-direction:column; gap:8px;">
<sc-for list="{{ cloudProviders }}" as="p" hint-placeholder-count="5">
<div style="display:flex; align-items:center; gap:11px; padding:11px 12px; border-radius:11px; border:1px solid {{ p.cardBorder }}; background:#0d0d10; cursor:pointer;" onClick="{{ p.onConnect }}">
<div style="width:32px; height:32px; flex:none; border-radius:8px; background:{{ p.tile }}; display:flex; align-items:center; justify-content:center; font-weight:700; color:{{ p.ink }}; font-size:11px;">
<sc-if value="{{ p.isText }}" hint-placeholder-val="{{ true }}">{{ p.short }}</sc-if>
<sc-if value="{{ p.isApple }}" hint-placeholder-val="{{ false }}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2.5" stroke="#1a1a1d" stroke-width="1.8"></rect><rect x="9.5" y="9.5" width="5" height="5" rx="1" fill="#1a1a1d"></rect></svg></sc-if>
</div>
<div style="flex:1; min-width:0;">
<div style="font-size:13px; font-weight:600; color:#eaeaee;">{{ p.name }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">{{ p.offers }}</div>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; padding:3px 8px; border-radius:6px; {{ p.statusStyle }}">{{ p.statusLabel }}</span>
</div>
</sc-for>
</div>
<div style="margin-top:16px; display:flex; gap:9px; padding:11px 12px; border-radius:10px; background:rgba(94,200,216,.05); border:1px solid rgba(94,200,216,.18);">
<span style="color:#5ec8d8; flex:none;"></span>
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;">Prefer your own hardware? <span style="color:#5ec8d8; cursor:pointer;" onClick="{{ goLocal }}">Local & Tailscale</span> has zero inbound exposure.</div>
</div>
</div>
</sc-if>
<!-- CLOUD CONNECT WIZARD -->
<sc-if value="{{ showCloudConnect }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="display:flex; align-items:center; gap:10px; margin-bottom:16px;">
<div style="width:36px; height:36px; border-radius:9px; background:{{ selProvTile }}; display:flex; align-items:center; justify-content:center; font-weight:700; color:{{ selProvInk }}; font-size:12px;">
<sc-if value="{{ selProvIsText }}" hint-placeholder-val="{{ true }}">{{ selProvShort }}</sc-if>
<sc-if value="{{ selProvIsApple }}" hint-placeholder-val="{{ false }}"><svg width="18" height="18" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2.5" stroke="#1a1a1d" stroke-width="1.8"></rect><rect x="9.5" y="9.5" width="5" height="5" rx="1" fill="#1a1a1d"></rect></svg></sc-if>
</div>
<div><div style="font-size:15px; font-weight:700;">{{ selProvName }}</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">connect provider</div></div>
</div>
<!-- creds -->
<sc-if value="{{ phaseCreds }}" hint-placeholder-val="{{ true }}">
<div style="animation:cm-fade .2s ease;">
<div style="display:flex; gap:9px; padding:10px 12px; border-radius:9px; background:rgba(95,208,138,.05); border:1px solid rgba(95,208,138,.2); margin-bottom:16px;">
<span style="width:14px; flex:none;"><svg width="14" height="14" viewBox="0 0 20 20"><path d="M10 2l5 2v4c0 4-2.5 6-5 7-2.5-1-5-3-5-7V4z" fill="none" stroke="#5fd08a" stroke-width="1.4" stroke-linejoin="round"></path></svg></span>
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;">Stored in the <span style="color:#cfcfd5; font-weight:600;">secret broker</span> over a private socket. Never enters agent code or any sandbox.</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">ACCESS KEY ID</div>
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5;">AKIA••••••••••7Q</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">SECRET ACCESS KEY</div>
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">••••••••••••••••••••</div>
</div>
<div style="margin-bottom:18px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">SCOPE</div>
<div style="display:flex; flex-wrap:wrap; gap:6px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.04);">compute:provision</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.04);">compute:terminate</span>
</div>
</div>
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ toConfig }}">Store &amp; continue →</div>
</div>
</sc-if>
<!-- config -->
<sc-if value="{{ phaseConfig }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="font-size:13px; font-weight:700; margin-bottom:11px;">Region</div>
<div style="display:flex; flex-wrap:wrap; gap:7px; margin-bottom:18px;">
<sc-for list="{{ regions }}" as="rg" hint-placeholder-count="3">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; padding:7px 12px; border-radius:8px; cursor:pointer; {{ rg.style }}" onClick="{{ rg.onPick }}">{{ rg.label }}</span>
</sc-for>
</div>
<div style="font-size:13px; font-weight:700; margin-bottom:11px;">Instance size</div>
<div style="display:flex; flex-wrap:wrap; gap:7px; margin-bottom:18px;">
<sc-for list="{{ sizes }}" as="sz" hint-placeholder-count="3">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; padding:7px 12px; border-radius:8px; cursor:pointer; {{ sz.style }}" onClick="{{ sz.onPick }}">{{ sz.label }}</span>
</sc-for>
</div>
<div style="border-radius:10px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:12px 14px; margin-bottom:16px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; line-height:1.7;">
image: <span style="color:#5fd08a;">clawmates-node baked</span><br>boot: <span style="color:#5ec8d8;">dials home · outbound WSS</span><br>sandbox: <span style="color:#6fd0c0;">§15 applied on boot</span>
</div>
<div style="display:flex; gap:8px;">
<div style="flex:none; width:44px; display:flex; align-items:center; justify-content:center; height:42px; border-radius:10px; border:1px solid rgba(255,255,255,.12); color:#9a9aa2; cursor:pointer;" onClick="{{ backToCreds }}"></div>
<div style="flex:1; display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ provisionRunner }}">Provision runner →</div>
</div>
</div>
</sc-if>
<!-- provisioning -->
<sc-if value="{{ provisioning }}" hint-placeholder-val="{{ false }}">
<div style="display:flex; flex-direction:column; align-items:center; text-align:center; padding:24px 0; animation:cm-fade .2s ease;">
<div style="width:42px; height:42px; border:3px solid rgba(232,196,106,.25); border-top-color:#e8b465; border-radius:50%; animation:cm-spin .8s linear infinite; margin-bottom:16px;"></div>
<div style="font-size:14px; font-weight:700; margin-bottom:5px;">Provisioning runner…</div>
<div style="font-size:12px; color:#8a8a92; line-height:1.6; max-width:250px;">launching instance · baking daemon · dialing home over WSS</div>
</div>
</sc-if>
<!-- provisioned -->
<sc-if value="{{ provisioned }}" hint-placeholder-val="{{ false }}">
<div style="animation:cm-fade .2s ease;">
<div style="display:flex; align-items:center; gap:9px; margin-bottom:14px;"><span style="width:26px; height:26px; border-radius:50%; background:rgba(95,208,138,.16); display:flex; align-items:center; justify-content:center; color:#5fd08a; font-size:14px;"></span><div><div style="font-size:14px; font-weight:700;">Runner online</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">joined fleet · dialing home ok</div></div></div>
<div style="border-radius:11px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:13px 15px; margin-bottom:16px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.8;">
<div style="display:flex;"><span style="color:#6a6a72; width:70px;">instance</span><span style="color:#cfcfd5;">{{ size }}</span></div>
<div style="display:flex;"><span style="color:#6a6a72; width:70px;">region</span><span style="color:#cfcfd5;">{{ region }}</span></div>
<div style="display:flex;"><span style="color:#6a6a72; width:70px;">daemon</span><span style="color:#5fd08a;">online · §15</span></div>
</div>
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#7fe0a0,#5fd08a); color:#06281a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ finishCloud }}">✓ Done</div>
</div>
</sc-if>
</div>
</sc-if>
</div>
</div>
</div>
<!-- STATUS BAR -->
<div style="height:26px; flex:none; display:flex; align-items:center; gap:16px; padding:0 16px; border-top:1px solid rgba(255,255,255,.06); background:#0a0a0c; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">
<span style="color:#5fd08a;">● daemon control plane ok</span>
<span>tailnet {{ tailnet }}</span>
<span style="flex:1;"></span>
<span>outbound WSS · no inbound port</span>
<span style="color:#5ec8d8;">§15 sandbox-ready</span>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script>
class Component extends DCLogic {
state = { nav: 'local', addMode: 'menu', step: 1, connecting: false, connected: false, copied: false, tsConnected: true,
cloudProvider: 'gcp', cloudPhase: 'creds', provisioning: false, provisioned: false, region: 'us-east-1', size: 'c7g.2xlarge' };
componentWillUnmount() { if (this._t) clearTimeout(this._t); if (this._t2) clearTimeout(this._t2); }
_enterConnecting() {
this.setState({ step: 2, connecting: true, connected: false });
if (this._t) clearTimeout(this._t);
this._t = setTimeout(() => this.setState({ connecting: false, connected: true }), 1800);
}
_enterProvisioning() {
this.setState({ cloudPhase: 'provisioning', provisioning: true, provisioned: false });
if (this._t2) clearTimeout(this._t2);
this._t2 = setTimeout(() => this.setState({ provisioning: false, provisioned: true }), 2200);
}
renderVals() {
const s = this.state;
const hosts = [
{ id:'forge', name:'forge-01', os:'Ubuntu 24.04 · x86_64', kind:'Linux', status:'online', cpu:34, ram:61, mem:'normal', memColor:'#5fd08a', disk:48, load:'1.24', containers:6, tsip:'100.92.14.3', spark:'0,13 8,9 16,11 24,5 32,8 40,4 48,7' },
{ id:'studio', name:'mac-studio', os:'macOS 15.3 · arm64', kind:'macOS', status:'online', cpu:12, ram:38, mem:'normal', memColor:'#5fd08a', disk:22, load:'0.86', containers:2, tsip:'100.92.14.7', spark:'0,12 8,13 16,10 24,11 32,7 40,9 48,8' },
{ id:'edge', name:'edge-rpi5', os:'Debian 12 · arm64', kind:'Linux', status:'pairing', cpu:0, ram:0, mem:'—', memColor:'#5a5a62', disk:0, load:'—', containers:0, tsip:'pending', spark:'0,8 8,8 16,8 24,8 32,8 40,8 48,8' },
];
const meter = (v, col) => ({ w: Math.max(3, v) + '%', col });
const cpuCol = v => v > 75 ? '#ff6f61' : v > 45 ? '#e8b465' : '#5ec8d8';
const ramCol = v => v > 80 ? '#ff6f61' : v > 55 ? '#e8b465' : '#5fd08a';
const hostCards = hosts.map(h => ({
...h,
online: h.status === 'online',
pairing: h.status === 'pairing',
statusColor: h.status === 'online' ? '#5fd08a' : h.status === 'pairing' ? '#e8b465' : '#5a5a62',
statusLabel: h.status === 'online' ? 'online' : h.status === 'pairing' ? 'pairing…' : 'offline',
accentBorder: h.status === 'online' ? 'rgba(95,208,138,.18)' : h.status === 'pairing' ? 'rgba(232,196,106,.3)' : 'rgba(255,255,255,.07)',
cpuW: Math.max(3, h.cpu) + '%', cpuCol: cpuCol(h.cpu),
ramW: Math.max(3, h.ram) + '%', ramCol: ramCol(h.ram),
diskW: Math.max(3, h.disk) + '%',
sshTarget: 'ssh ' + h.tsip,
hasSsh: h.status === 'online',
}));
const tsDevices = [
{ name:'forge-01', ip:'100.92.14.3', os:'linux', status:'online', seen:'now', dot:'#5fd08a' },
{ name:'mac-studio', ip:'100.92.14.7', os:'macOS', status:'online', seen:'now', dot:'#5fd08a' },
{ name:'edge-rpi5', ip:'100.92.14.9', os:'linux', status:'online', seen:'2m', dot:'#5fd08a' },
{ name:'ops-laptop', ip:'100.92.14.21', os:'macOS', status:'offline', seen:'3h', dot:'#5a5a62' },
];
// ---- cloud ----
const inCloud = s.nav === 'cloud';
const provData = [
{ id:'aws', name:'Amazon Web Services', short:'aws', tile:'linear-gradient(135deg,#ff9a3c,#ff7314)', ink:'#241200', offers:'EC2 · Fargate · Graviton', regions:'31 regions', connected:true, runners:2, icon:'text' },
{ id:'gcp', name:'Google Cloud', short:'GCP', tile:'linear-gradient(135deg,#5b9bf0,#3b6fd4)', ink:'#031027', offers:'Compute Engine · Cloud Run', regions:'40 regions', connected:false, runners:0, icon:'text' },
{ id:'apple', name:'Apple silicon', short:'', tile:'linear-gradient(135deg,#ececed,#b6b6be)', ink:'#1a1a1d', offers:'Mac instances · M-series', regions:'macOS / iOS builds', connected:false, runners:0, icon:'chip' },
{ id:'hetzner', name:'Hetzner', short:'HZ', tile:'linear-gradient(135deg,#e44a4a,#c5102d)', ink:'#2a0207', offers:'Cloud VMs · dedicated', regions:'EU · US · low cost', connected:false, runners:0, icon:'text' },
{ id:'do', name:'DigitalOcean', short:'DO', tile:'linear-gradient(135deg,#3aa0ff,#0069ff)', ink:'#021428', offers:'Droplets · App Platform · K8s', regions:'15 datacenters', connected:false, runners:0, icon:'text' },
];
const cloudProviders = provData.map(p => ({
...p,
isApple: p.icon === 'chip',
isText: p.icon === 'text',
statusLabel: p.connected ? '● connected' : 'Connect →',
statusStyle: p.connected
? 'color:#5fd08a; background:rgba(95,208,138,.08); border:1px solid rgba(95,208,138,.25);'
: 'color:#ff8a7a; background:rgba(255,111,97,.08); border:1px solid rgba(255,111,97,.28);',
cardBorder: p.connected ? 'rgba(95,208,138,.2)' : 'rgba(255,255,255,.08)',
onConnect: () => this.setState({ nav:'cloud', addMode:'cloud-connect', cloudProvider:p.id, cloudPhase:'creds', provisioning:false, provisioned:false }),
}));
const selProv = cloudProviders.find(p => p.id === s.cloudProvider) || cloudProviders[0];
const cloudRunners = [
{ name:'aws-ec2 · c7g.2xlarge', region:'us-east-1', status:'online', cpu:52, ram:44, agents:3, cost:'$0.29/hr', cpuW:'52%', ramW:'44%' },
{ name:'aws-ec2 · c7g.xlarge', region:'eu-west-1', status:'online', cpu:28, ram:36, agents:2, cost:'$0.15/hr', cpuW:'28%', ramW:'36%' },
];
const selChip = 'background:rgba(255,111,97,.14); border:1px solid rgba(255,111,97,.4); color:#ff8a7a;';
const idleChip = 'background:#0d0d10; border:1px solid rgba(255,255,255,.1); color:#9a9aa2;';
const regions = ['us-east-1','eu-west-1','ap-south-1'].map(r => ({ label:r, style: s.region===r?selChip:idleChip, onPick:()=>this.setState({region:r}) }));
const sizes = ['c7g.xlarge','c7g.2xlarge','c7g.4xlarge'].map(z => ({ label:z, style: s.size===z?selChip:idleChip, onPick:()=>this.setState({size:z}) }));
const navActive = 'background:rgba(255,111,97,.1);';
const navIdle = '';
return {
// cloud
cloudProviders, cloudRunners, regions, sizes,
selProvName: selProv?selProv.name:'', selProvTile: selProv?selProv.tile:'', selProvShort: selProv?selProv.short:'', selProvInk: selProv?selProv.ink:'', selProvIsApple: selProv?selProv.isApple:false, selProvIsText: selProv?selProv.isText:true,
phaseCreds: s.cloudPhase==='creds', phaseConfig: s.cloudPhase==='config', provisioning: s.provisioning, provisioned: s.provisioned,
toConfig: () => this.setState({ cloudPhase:'config' }),
backToCreds: () => this.setState({ cloudPhase:'creds' }),
provisionRunner: () => this._enterProvisioning(),
finishCloud: () => this.setState({ addMode:'menu', provisioning:false, provisioned:false }),
region: s.region, size: s.size,
showCloudMenu: inCloud && s.addMode!=='cloud-connect',
showCloudConnect: inCloud && s.addMode==='cloud-connect',
// nav
isLocal: s.nav==='local', isCloud: s.nav==='cloud',
navLocalStyle: s.nav==='local'?navActive:navIdle, navLocalIcon: s.nav==='local'?'#ff8a7a':'#8a8a92', navLocalText: s.nav==='local'?'#fff':'#cfcfd5',
navCloudStyle: s.nav==='cloud'?navActive:navIdle, navCloudIcon: s.nav==='cloud'?'#ff8a7a':'#6a6a72', navCloudText: s.nav==='cloud'?'#fff':'#9a9aa2',
goLocal: () => this.setState({ nav:'local', addMode:'menu' }),
goCloud: () => this.setState({ nav:'cloud', addMode:'menu' }),
// fleet
fleetTotal: 3, fleetOnline: 2, vcpu: 28, ram: '96 GB', containersRunning: 8,
hostCards, tsDevices, tsDeviceCount: tsDevices.length, tailnet: 'acme-org.ts.net',
tsConnected: s.tsConnected,
// right panel modes
showMenu: !inCloud && s.addMode==='menu', showWizard: !inCloud && s.addMode==='host-wizard', showTailscale: !inCloud && s.addMode==='tailscale',
openHostWizard: () => this.setState({ addMode:'host-wizard', step:1, connecting:false, connected:false }),
openTailscale: () => this.setState({ addMode:'tailscale' }),
backToMenu: () => this.setState({ addMode:'menu' }),
// wizard
step1: s.step===1, step2: s.step===2, step3: s.step===3,
stepIs1Style: this._stepDot(s.step,1), stepIs2Style: this._stepDot(s.step,2), stepIs3Style: this._stepDot(s.step,3),
connecting: s.connecting, connected: s.connected,
copied: s.copied,
copyToken: () => { this.setState({ copied:true }); setTimeout(()=>this.setState({copied:false}), 1400); },
copyLabel: s.copied ? 'copied ✓' : 'copy',
toInstall: () => this.setState({ step:1 }),
toConnect: () => this._enterConnecting(),
toVerify: () => this.setState({ step:3 }),
finishWizard: () => this.setState({ addMode:'menu', step:1 }),
};
}
_stepDot(cur, n) {
if (cur === n) return 'background:#ff6f61; color:#2a0d0a;';
if (cur > n) return 'background:rgba(95,208,138,.2); color:#5fd08a; border:1px solid rgba(95,208,138,.4);';
return 'background:#141417; color:#6a6a72; border:1px solid rgba(255,255,255,.1);';
}
}
</script>
</body>
</html>