← back to AbramsEgo
public/index.html
1062 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AbramsEgo · Command Center</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230a0e14'/%3E%3Ctext x='16' y='23' font-size='20' text-anchor='middle' fill='%234d9fff' font-family='sans-serif' font-weight='bold'%3EA%3C/text%3E%3C/svg%3E"/>
<style>
:root{
--bg:#0a0e14; --panel:#111823; --panel2:#0d141d; --line:#1e2a3a; --ink:#dbe6f3;
--dim:#7d8fa6; --accent:#4d9fff; --good:#3fd18b; --warn:#ffb454; --bad:#ff5d6c;
--gold:#ffd166; --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);
color:var(--ink);font:14px/1.45 var(--mono);-webkit-font-smoothing:antialiased}
/* ============ LIQUID / FLUID BACKGROUND (site-wide, GPU, pausable) ========
Slow-morphing mesh-gradient "blobs" drift + breathe behind everything. Pure
CSS keyframes (compositor-only transform/opacity) so it costs ~0 CPU; paused
via body.paused when the tab is hidden. Colors are theme accents at very low
alpha so the dark command-center palette stays dominant. */
#liquid{position:fixed;inset:0;z-index:-2;overflow:hidden;pointer-events:none;
background:radial-gradient(1200px 600px at 70% -10%,#12203322,#0a0e14 60%),var(--bg)}
#liquid .blob{position:absolute;border-radius:50%;filter:blur(60px);opacity:.5;
mix-blend-mode:screen;will-change:transform}
.blob.b1{width:46vw;height:46vw;left:-8vw;top:-10vw;background:radial-gradient(circle at 30% 30%,#4d9fff,#4d9fff00 70%);animation:drift1 26s ease-in-out infinite}
.blob.b2{width:40vw;height:40vw;right:-6vw;top:8vh;background:radial-gradient(circle at 60% 40%,#3fd18b,#3fd18b00 70%);animation:drift2 32s ease-in-out infinite}
.blob.b3{width:38vw;height:38vw;left:22vw;bottom:-14vw;background:radial-gradient(circle at 50% 50%,#a78bfa,#a78bfa00 70%);animation:drift3 38s ease-in-out infinite}
.blob.b4{width:30vw;height:30vw;right:14vw;bottom:2vh;background:radial-gradient(circle at 50% 50%,#ffd166,#ffd16600 70%);animation:drift1 30s ease-in-out infinite reverse}
@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(9vw,7vh) scale(1.15)}66%{transform:translate(-5vw,4vh) scale(.92)}}
@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-8vw,6vh) scale(1.12)}66%{transform:translate(4vw,-5vh) scale(.9)}}
@keyframes drift3{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(6vw,-6vh) scale(.9)}66%{transform:translate(-7vw,3vh) scale(1.18)}}
body.paused .blob{animation-play-state:paused}
@media(prefers-reduced-motion:reduce){.blob{animation:none}}
/* entrance animation: panels rise + fade in on first load (staggered in JS) */
@keyframes riseIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
main.enter section.card{opacity:0;animation:riseIn .5s cubic-bezier(.2,.7,.3,1) forwards}
header{display:flex;align-items:center;gap:14px;padding:12px 18px;border-bottom:1px solid var(--line);
position:sticky;top:0;background:rgba(10,14,20,.9);backdrop-filter:blur(8px);z-index:5}
.logo{font-weight:700;letter-spacing:.5px;font-size:16px}
.logo b{color:var(--accent)}
.metrics{display:flex;gap:16px;margin-left:auto;flex-wrap:wrap;align-items:center}
.metric{display:flex;flex-direction:column;align-items:flex-end;line-height:1.1}
.metric .v{font-size:15px;font-weight:600}
.metric .k{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.6px}
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:5px}
.refresh{font-size:11px;color:var(--dim)}
main{padding:16px;display:grid;grid-template-columns:repeat(12,1fr);gap:14px;max-width:1500px;margin:0 auto}
.card{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--line);
border-radius:12px;padding:14px 16px;min-height:60px}
.card h2{margin:0 0 10px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--dim);
display:flex;align-items:center;gap:8px}
/* androoagi "[ OUTPUT TERMINAL ]" boxed-label panel headers */
.card h2::before{content:'[';color:#3b4f68;font-weight:400}
.card h2::after{content:']';color:#3b4f68;font-weight:400;margin-left:auto}
/* androoagi segmented 10-cell mini-gauges (top metrics bar) */
.seg{display:inline-flex;gap:1.5px;margin-top:2px}
.seg i{width:5px;height:7px;border-radius:1px;background:#ffffff14}
.seg i.on{background:currentColor}
/* stable per-agent hue chip (golden-angle) — same entity, same color, every panel */
.hue{display:inline-block;width:7px;height:7px;border-radius:2px;margin-right:5px;flex-shrink:0;vertical-align:baseline}
/* androoagi focus cue: clicked panel glows, siblings dim */
main.focused section.card{opacity:.45;transition:opacity .15s ease}
main.focused section.card.focus{opacity:1;border-color:#4d9fff88;
box-shadow:0 0 0 1px #4d9fff33,0 0 26px -8px #4d9fff99}
.span12{grid-column:span 12}.span8{grid-column:span 8}.span7{grid-column:span 7}.span6{grid-column:span 6}
.span5{grid-column:span 5}.span4{grid-column:span 4}.span3{grid-column:span 3}
@media(max-width:1000px){.span8,.span7,.span6,.span5,.span4,.span3{grid-column:span 12}}
/* ---- SCOREBOARD (the at-a-glance game-state hero) ---- */
.scoreboard{background:linear-gradient(135deg,#0e1826,#0a1018 60%,#0d141d);
border:1px solid #263a52;box-shadow:0 0 0 1px #4d9fff10, inset 0 0 60px #4d9fff08}
.sb-head{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:14px}
.sb-status{font-size:13px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
padding:5px 14px;border-radius:8px;border:1px solid var(--line)}
.sb-status.win{color:var(--good);border-color:#3fd18b55;background:#3fd18b12;text-shadow:0 0 14px #3fd18b55}
.sb-status.hold{color:var(--warn);border-color:#ffb45455;background:#ffb45412;text-shadow:0 0 14px #ffb45455}
.sb-status.down{color:var(--bad);border-color:#ff5d6c55;background:#ff5d6c12;text-shadow:0 0 14px #ff5d6c55}
.sb-clock{margin-left:auto;font-size:11px;color:var(--dim)}
.sb-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
.sb-tile{background:#0b1421;border:1px solid var(--line);border-radius:12px;padding:14px 16px;position:relative;overflow:hidden}
.sb-tile::after{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--dim);opacity:.6}
.sb-tile.good::after{background:var(--good)} .sb-tile.warn::after{background:var(--warn)}
.sb-tile.bad::after{background:var(--bad)} .sb-tile.gold::after{background:var(--gold)} .sb-tile.accent::after{background:var(--accent)}
.sb-tile .sk{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:1px}
.sb-tile .sv{font-size:38px;font-weight:800;line-height:1.05;margin-top:4px;font-variant-numeric:tabular-nums}
.sb-tile .su{font-size:15px;font-weight:600;color:var(--dim);margin-left:4px}
.sb-tile .ss{font-size:10.5px;color:var(--dim);margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sb-tile.hero .sv{font-size:44px}
/* motion: entrance shimmer on hero tile + glow sweep across scoreboard */
.sb-tile{animation:tileIn .5s ease both}
@keyframes tileIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
/* pulsing STATUS pill — the "heartbeat" of the operation */
.sb-status{position:relative}
.sb-status.win{animation:pulseGood 2.4s ease-in-out infinite}
.sb-status.hold{animation:pulseWarn 1.8s ease-in-out infinite}
.sb-status.down{animation:pulseBad 1.1s ease-in-out infinite}
@keyframes pulseGood{0%,100%{box-shadow:0 0 0 0 #3fd18b00}50%{box-shadow:0 0 16px 1px #3fd18b55}}
@keyframes pulseWarn{0%,100%{box-shadow:0 0 0 0 #ffb45400}50%{box-shadow:0 0 16px 1px #ffb45466}}
@keyframes pulseBad{0%,100%{box-shadow:0 0 0 0 #ff5d6c00}50%{box-shadow:0 0 20px 2px #ff5d6c88}}
body.paused .sb-status,body.paused .sb-tile{animation-play-state:paused}
/* ---- LIQUID-FILL self-funding dial ---- */
.dial-wrap{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.dial{position:relative;width:118px;height:118px;flex-shrink:0}
.dial svg{width:118px;height:118px;transform:rotate(-90deg)}
.dial .lqlabel{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}
.dial .lqlabel b{font-size:26px;font-weight:800;font-variant-numeric:tabular-nums}
.dial .lqlabel span{font-size:9px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
/* animated liquid wave that fills the dial from the bottom */
.wave{transform-box:fill-box;transform-origin:center;animation:waveMove 5s linear infinite}
@keyframes waveMove{from{transform:translateX(0)}to{transform:translateX(-50%)}}
body.paused .wave{animation-play-state:paused}
/* ---- radial fleet gauge + generic mini bits ---- */
.gauge{position:relative;width:110px;height:110px;flex-shrink:0}
.gauge svg{width:110px;height:110px;transform:rotate(-90deg)}
.gauge .glabel{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center}
.gauge .glabel b{font-size:22px;font-weight:800}
.gauge .glabel span{font-size:9px;color:var(--dim)}
/* canary W-L stacked bar */
.wl{display:flex;height:16px;border-radius:8px;overflow:hidden;border:1px solid var(--line);margin:8px 0}
.wl i{display:block;height:100%}
.wl .w{background:var(--good)}.wl .warn{background:var(--warn)}.wl .l{background:var(--bad)}
.wl-legend{display:flex;gap:14px;font-size:11px;flex-wrap:wrap}
.wl-legend b{font-variant-numeric:tabular-nums}
/* freshness "as of" chip — visible staleness so a tile isn't silently wrong */
.fresh{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;padding:1px 7px;border-radius:20px;
border:1px solid var(--line);color:var(--dim);white-space:nowrap;vertical-align:middle}
.fresh::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--good)}
.fresh.aging{color:var(--warn);border-color:#ffb45444;background:#ffb4540c}
.fresh.aging::before{background:var(--warn)}
.fresh.stale{color:var(--bad);border-color:#ff5d6c55;background:#ff5d6c12;animation:pulseBad 1.6s ease-in-out infinite}
.fresh.stale::before{background:var(--bad)}
body.paused .fresh.stale{animation-play-state:paused}
/* sparkline */
.spark{display:block;width:100%;height:auto}
/* count-up number sizing helper (shares tabular nums) */
.cu{font-variant-numeric:tabular-nums}
.big{font-size:30px;font-weight:700}
.good{color:var(--good)}.warn{color:var(--warn)}.bad{color:var(--bad)}.gold{color:var(--gold)}.dim{color:var(--dim)}
.row{display:flex;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px dashed #ffffff08}
.row:last-child{border-bottom:0}
/* list row with a visible created/started date+time line (Steve's admin-card rule) */
.lrow{padding:5px 0;border-bottom:1px dashed #ffffff08}
.lrow:last-child{border-bottom:0}
.lrow-top{display:flex;justify-content:space-between;gap:10px;align-items:baseline}
.lrow-when{margin-top:2px}
.when{font-size:9.5px;color:var(--dim);white-space:nowrap}
/* historical transcript snippet (session opener) — must NEVER read as live status */
.sess-open{margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sess-open-label{font-size:8.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);
border:1px solid var(--line);border-radius:20px;padding:1px 6px;margin-right:6px;vertical-align:1px}
.sess-open-quote{font-size:10px;font-style:italic;color:var(--dim)}
/* sort + density control bar shared by every grid/list */
.gridctl{display:flex;gap:12px;align-items:center;margin:0 0 10px;flex-wrap:wrap}
.gridctl select,.gridctl input[type=range]{background:#0c1420;border:1px solid var(--line);color:var(--ink);
border-radius:6px;font-family:var(--mono);font-size:11px;padding:3px 6px}
.gridctl label{display:flex;align-items:center;gap:5px;color:var(--dim);font-size:11px}
.gsentinel{grid-column:1/-1;text-align:center;color:var(--dim);font-size:10px;padding:6px}
.pill{font-size:10px;padding:2px 7px;border-radius:20px;border:1px solid var(--line);color:var(--dim);white-space:nowrap}
.pill.gated{color:var(--warn);border-color:#ffb45444;background:#ffb4540f}
.pill.live{color:var(--good);border-color:#3fd18b44;background:#3fd18b0f}
.bar{height:6px;border-radius:6px;background:#ffffff10;overflow:hidden;margin-top:6px}
.bar>span{display:block;height:100%;background:var(--accent)}
.prov-wrap{display:flex;gap:14px;align-items:center;margin-top:12px;flex-wrap:wrap}
.donut{position:relative;width:96px;height:96px;border-radius:50%;flex-shrink:0}
.donut::after{content:'';position:absolute;inset:22%;border-radius:50%;
background:linear-gradient(180deg,var(--panel),var(--panel2))}
.donut-c{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
justify-content:center;text-align:center;z-index:1}
.donut-c b{font-size:12px;color:var(--ink)}
.donut-c span{font-size:8.5px;color:var(--dim);text-transform:uppercase;letter-spacing:.5px}
.prov-legend{flex:1;min-width:150px;font-size:11px}
.prov-legend .row{align-items:center}
.prov-legend .dot{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:6px;flex-shrink:0}
.prov-legend .lbl{display:flex;align-items:center;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.prov-legend .amt{color:var(--dim);white-space:nowrap}
.pnlgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
@media(max-width:800px){.pnlgrid{grid-template-columns:repeat(2,1fr)}}
.stat{background:#0c1420;border:1px solid var(--line);border-radius:10px;padding:12px}
.stat .k{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.6px}
.stat .v{font-size:22px;font-weight:700;margin-top:3px}
.eng{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed #ffffff08}
.list{max-height:240px;overflow:auto}
.list::-webkit-scrollbar{width:6px}.list::-webkit-scrollbar-thumb{background:#ffffff18;border-radius:6px}
.mono{font-family:var(--mono)}
.chat input{width:100%;background:#0c1420;border:1px solid var(--line);color:var(--ink);
border-radius:8px;padding:10px;font-family:var(--mono)}
.chat .ans{margin-top:10px;color:var(--ink);white-space:pre-wrap;background:#0c1420;border:1px solid var(--line);
border-radius:8px;padding:10px;min-height:20px}
.muted{color:var(--dim);font-size:11px}
.subcard{background:#0c1420;border:1px solid var(--line);border-radius:9px;padding:9px 11px;margin-bottom:8px}
.subcard-top{display:flex;align-items:center;justify-content:space-between;gap:8px}
.subcard-proj{font-weight:600;color:var(--accent);font-size:12px}
.subcard-task{color:var(--ink);font-size:11px;margin:5px 0;opacity:.85;
display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.subcard-when{color:var(--dim);font-size:10px}
a{color:var(--accent);text-decoration:none}
.alert{grid-column:span 12;background:#ff5d6c14;border:1px solid #ff5d6c44;color:#ffd7db;
border-radius:10px;padding:8px 14px;font-size:12px;display:none}
.alert.show{display:block}
</style>
</head>
<body>
<!-- LIQUID / FLUID mesh-gradient blob background (GPU, pauses on tab-hide) -->
<div id="liquid" aria-hidden="true">
<div class="blob b1"></div><div class="blob b2"></div>
<div class="blob b3"></div><div class="blob b4"></div>
</div>
<header>
<div class="logo"><b>Abrams</b>Ego <span class="dim" style="font-size:11px">· command center</span></div>
<div class="metrics" id="metrics"></div>
<div class="refresh" id="refresh">…</div>
</header>
<!-- DATA FRESHNESS strip — per-source "as of" ages so stale tiles are visibly stale -->
<div id="freshbar" style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;padding:7px 18px;border-bottom:1px solid var(--line);
background:rgba(10,14,20,.7);position:sticky;top:49px;z-index:4;backdrop-filter:blur(6px)">
<span class="muted" style="font-size:10px;letter-spacing:.6px">DATA FRESHNESS</span>
</div>
<main id="main">
<div class="alert" id="alert"></div>
<!-- SCOREBOARD — at-a-glance game-state of the whole operation (all fields from /api/data) -->
<section class="card span12 scoreboard">
<div class="sb-head">
<h2 style="margin:0">🏟 Scoreboard</h2>
<span class="sb-status" id="sb-status">—</span>
<span class="sb-clock" id="sb-clock"></span>
</div>
<div class="sb-tiles" id="sb-tiles"></div>
</section>
<!-- VITALS & GRAPHS — liquid dial, radial gauge, canary W-L, wins sparkline -->
<section class="card span12">
<h2>📊 Vitals <span class="muted">· liquid self-funding dial · fleet gauge · canary W–L · wins trend</span></h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:18px;align-items:center">
<!-- liquid-fill self-funding dial -->
<div>
<div class="muted" style="margin-bottom:8px">Self-funding <span class="fresh" id="fresh-fund" title=""></span></div>
<div class="dial-wrap">
<div class="dial" id="fund-dial"></div>
<div style="font-size:11px" id="fund-note" class="muted"></div>
</div>
</div>
<!-- radial fleet up/total gauge -->
<div>
<div class="muted" style="margin-bottom:8px">Fleet online <span class="fresh" id="fresh-fleet" title=""></span></div>
<div class="dial-wrap"><div class="gauge" id="fleet-gauge"></div>
<div style="font-size:11px" id="fleet-note" class="muted"></div></div>
</div>
<!-- canary win-loss record -->
<div>
<div class="muted" style="margin-bottom:8px">Canary W–L</div>
<div class="wl" id="wl-bar"></div>
<div class="wl-legend" id="wl-legend"></div>
</div>
<!-- wins-over-time sparkline (real 7-day series) -->
<div>
<div class="muted" style="margin-bottom:8px">Wins · 7-day trend</div>
<div id="wins-spark"></div>
<div class="muted" id="wins-spark-note" style="font-size:11px;margin-top:4px"></div>
</div>
</div>
</section>
<!-- SELF-FUNDING P&L (the money panel) -->
<section class="card span8">
<h2>💰 Self-Funding P&L <span class="muted" id="pnlwin">· today</span></h2>
<div class="pnlgrid">
<div class="stat"><div class="k">Cost today</div><div class="v" id="pnl-cost">—</div><div class="muted" id="pnl-cost-sub"></div></div>
<div class="stat"><div class="k">Revenue today</div><div class="v gold" id="pnl-rev">—</div><div class="muted">5 engines · gated</div></div>
<div class="stat"><div class="k">Net</div><div class="v" id="pnl-net">—</div><div class="muted">revenue − cost</div></div>
<div class="stat"><div class="k">Self-funding</div><div class="v" id="pnl-pct">—</div><div class="muted">of run cost covered</div></div>
</div>
<div style="margin-top:14px">
<div class="muted" id="pnl-breakeven">Break-even: —</div>
<div class="bar" title="progress toward self-funding (100%)"><span id="pnl-progress" style="width:0%"></span></div>
<div class="muted" id="pnl-runway" style="margin-top:6px"></div>
</div>
<div style="margin-top:12px" id="engines"></div>
</section>
<!-- WPB MARKETPLACE SALES — proxied from wpb-sales-dashboard (:9812) via /api/wpb -->
<section class="card span12" id="wpb-card">
<h2>🛍️ WPB Marketplace Sales <span class="muted" id="wpb-sub">· Wallpaper's Back design shops · <a href="http://127.0.0.1:9812/" target="_blank" rel="noopener" style="color:inherit">open :9812 ↗</a></span></h2>
<div class="stats" id="wpb-stats"><div class="muted">loading…</div></div>
<div style="margin-top:12px" id="wpb-markets"></div>
</section>
<!-- COST BREAKDOWN -->
<section class="card span4">
<h2>🔌 Cost to run · 30d</h2>
<div class="big" id="cost30">—</div>
<div class="muted" id="cost30sub"></div>
<div class="prov-wrap" id="costproviders"></div>
<div style="margin-top:10px" id="costapps"></div>
</section>
<!-- COST TREND · 7d + PER-MODEL -->
<section class="card span12">
<h2>📈 Cost trend · 7d <span class="muted">· daily total (tokens + energy est) · per-model 30d</span></h2>
<div style="display:flex;gap:18px;flex-wrap:wrap;align-items:flex-start">
<div style="flex:2;min-width:280px" id="trend"><span class="muted">loading…</span></div>
<div style="flex:1;min-width:200px" id="bymodel"></div>
</div>
</section>
<!-- FLEET -->
<section class="card span4">
<h2>🛰 Fleet · Kamatera</h2>
<div class="big" id="kam-up">—</div>
<div id="kam-ctl"></div>
<div class="list" id="kam-list" style="margin-top:8px"></div>
</section>
<section class="card span4">
<h2>🖥 Fleet · Local</h2>
<div class="big" id="loc-up">—</div>
<div class="list" id="loc-list" style="margin-top:8px"></div>
</section>
<!-- CANARIES -->
<section class="card span4">
<h2>🐤 Canary board</h2>
<div id="canaries-ctl"></div>
<div class="list" id="canaries"></div>
</section>
<!-- SCHEDULE + OFFICERS -->
<section class="card span3">
<h2>⏱ launchd jobs</h2>
<div class="big" id="cron-loaded">—</div>
<div class="muted">loaded / total com.steve.*</div>
</section>
<section class="card span3">
<h2>🏛 Officers</h2>
<div class="big" id="off-count">—</div>
<div class="lrow-when" id="off-when"></div>
<div class="list" id="officers" style="margin-top:8px"></div>
</section>
<!-- WINS -->
<section class="card span6">
<h2>🏆 Wins <span class="muted" id="wins-today"></span></h2>
<div class="list" id="wins"></div>
</section>
<!-- SESSIONS -->
<section class="card span6">
<h2>💬 Live Claude sessions</h2>
<div class="list" id="sessions"></div>
</section>
<!-- MODEL USAGE + SUB-AGENT RUNS (panel 5) -->
<section class="card span5">
<h2>🧠 Model usage <span class="muted" id="usage-sub"></span></h2>
<div id="modelbars"></div>
</section>
<section class="card span7">
<h2>🧵 Recent sub-agent runs <span class="muted" id="subruns-sub"></span></h2>
<div id="subruns-ctl"></div>
<div class="list" id="subruns" style="max-height:340px"></div>
</section>
<!-- GIT ACTIVITY FEED (panel 6) -->
<section class="card span12">
<h2>📜 Activity <span class="muted" id="activity-sub">· recent commits across ~/Projects</span></h2>
<div id="activity-ctl"></div>
<div id="activity" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:8px;max-height:440px;overflow:auto"></div>
</section>
<!-- CRON / LAUNCHD (panel 4) — every scheduled com.steve.* job as an admin card -->
<section class="card span12">
<h2>⏱ Cron / launchd <span class="muted" id="crons-sub"></span></h2>
<div id="crongrid-ctl"></div>
<div id="crongrid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;max-height:560px;overflow:auto"></div>
</section>
<!-- AI CHAT -->
<section class="card span12 chat">
<h2>🤖 Ask AbramsEgo <span class="muted">· grounded in the live snapshot · cost shown per query</span></h2>
<input id="q" placeholder="e.g. what did the fleet do overnight? is it paying for itself?" />
<div class="ans" id="ans">Ask a question about your fleet, cost, or P&L.</div>
</section>
</main>
<script>
const $ = (id)=>document.getElementById(id);
const usd = (n)=> n==null? '—' : '$'+Number(n).toFixed(n<1?4:2);
const cls = (pct,warn,bad)=> pct>=bad?'bad':pct>=warn?'warn':'good';
// ---- MOTION: animated count-up. Tweens an element's number from its last shown
// value to the target over ~700ms (ease-out). Remembers per-element last value so
// re-renders every 15s only animate the delta, not a jarring 0→N each time.
// Respects prefers-reduced-motion (snaps to final). fmt() renders the number.
const _cuLast=new WeakMap();
const _reduceMotion=window.matchMedia&&window.matchMedia('(prefers-reduced-motion:reduce)').matches;
function countUp(el, target, fmt){
if(!el) return;
fmt = fmt || ((v)=>String(Math.round(v)));
target = Number(target)||0;
const from = _cuLast.has(el)? _cuLast.get(el) : 0;
_cuLast.set(el, target);
if(_reduceMotion || from===target){ el.textContent=fmt(target); return; }
const t0=performance.now(), dur=700;
function step(now){
const p=Math.min(1,(now-t0)/dur), e=1-Math.pow(1-p,3); // easeOutCubic
el.textContent=fmt(from+(target-from)*e);
if(p<1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
// ---- created/started date+time chip (Steve's hard admin-card rule) ----------
// Accepts an ISO string OR an epoch-ms number/string; returns {iso,label} or null.
function fmtWhen(v){
if(v==null||v==='') return null;
let iso = (typeof v==='number' || /^\d{10,}$/.test(String(v))) ? new Date(Number(v)).toISOString() : String(v);
const dt=new Date(iso); if(isNaN(dt)) return null;
return {iso, label: dt.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})};
}
// visible 🕓 date+time line with full ISO in title=; picks the first present field
function whenChip(...vals){
for(const v of vals){ const w=fmtWhen(v); if(w) return `<span class="when" title="${esc(w.iso)}">🕓 ${esc(w.label)}</span>`; }
return '';
}
function tsOf(...vals){ for(const v of vals){ const w=fmtWhen(v); if(w) return Date.parse(w.iso); } return 0; }
// two-line list row: main + right-meta, then a visible created/started date+time
function lrow(main, meta, whenHtml){
return `<div class="lrow"><div class="lrow-top"><span>${main}</span><span class="dim">${meta||''}</span></div>`+
(whenHtml?`<div class="lrow-when">${whenHtml}</div>`:'')+`</div>`;
}
// ---- reusable grid: sort <select> + density slider + infinite scroll --------
// Persists sort+density to localStorage; renders in batches with an
// IntersectionObserver sentinel so a list can exceed ~40 rows without lag.
const GRIDS={};
function initGrid(name, cfg){
if(GRIDS[name]) return GRIDS[name];
const g=GRIDS[name]={items:[], cfg, shown:cfg.batch};
const kSort='ae.'+name+'.sort', kDens='ae.'+name+'.dens';
const savedSort=localStorage.getItem(kSort)||cfg.sortModes[0].k;
const savedDens=parseInt(localStorage.getItem(kDens)||cfg.defaultCols,10);
const bar=document.createElement('div'); bar.className='gridctl';
bar.innerHTML=`<label>sort <select class="gsort">${cfg.sortModes.map(m=>`<option value="${m.k}"${m.k===savedSort?' selected':''}>${m.label}</option>`).join('')}</select></label>`+
`<label>▦ <input type="range" class="gdens" min="${cfg.minCols}" max="${cfg.maxCols}" value="${savedDens}"></label>`;
cfg.controlsHost.appendChild(bar);
g.sortEl=bar.querySelector('.gsort'); g.densEl=bar.querySelector('.gdens');
g.sortEl.addEventListener('change',()=>{localStorage.setItem(kSort,g.sortEl.value);g.shown=cfg.batch;draw(g);});
g.densEl.addEventListener('input',()=>{localStorage.setItem(kDens,g.densEl.value);applyDensity(g);});
applyDensity(g);
g.io=new IntersectionObserver((ents)=>{
if(ents.some(e=>e.isIntersecting) && g.shown<g.items.length){ g.shown+=cfg.batch; draw(g); }
},{root:cfg.host, rootMargin:'140px'});
return g;
}
function applyDensity(g){
const cols=parseInt(g.densEl.value,10);
g.cfg.host.style.display='grid';
g.cfg.host.style.gap='8px';
g.cfg.host.style.gridTemplateColumns=`repeat(${cols},minmax(0,1fr))`;
}
function draw(g){
const mode=g.cfg.sortModes.find(m=>m.k===g.sortEl.value)||g.cfg.sortModes[0];
const items=g.items.slice().sort(mode.cmp);
const slice=items.slice(0,g.shown);
g.cfg.host.innerHTML = slice.map(g.cfg.render).join('') || `<span class="muted">${g.cfg.empty||'nothing yet'}</span>`;
if(g.shown<items.length){
const s=document.createElement('div'); s.className='gsentinel';
s.textContent=`… ${items.length-g.shown} more · scroll to load`;
g.cfg.host.appendChild(s); g.io.observe(s);
}
}
function feedGrid(name, items){ const g=GRIDS[name]; if(!g) return; g.items=Array.isArray(items)?items:[]; if(g.shown>g.items.length) g.shown=g.cfg.batch; draw(g); }
async function load(){
let d; try{ d = await (await fetch('/api/data')).json(); }catch(e){ return; }
if(!d || !d.builtAt) return;
// metrics bar — segmented 10-cell gauges + bracketed labels (androoagi);
// cell fill keeps the threshold colors (green must still mean healthy)
const s=d.system||{};
const gauge=(pct)=>{ if(pct==null) return '';
const on=Math.max(0,Math.min(10,Math.round(pct/10)));
return `<span class="seg">${Array.from({length:10},(_,i)=>`<i${i<on?' class="on"':''}></i>`).join('')}</span>`; };
const swapMB=(s.swap && s.swap.usedMB!=null)?s.swap.usedMB:null;
$('metrics').innerHTML = [
['CPU', (s.loadPct??'—')+'%', cls(s.loadPct,60,85), s.loadPct],
['RAM', (s.memUsedPct??'—')+'%', cls(s.memUsedPct,70,88), s.memUsedPct],
['SWAP', swapMB!=null ? (swapMB>=1024 ? (swapMB/1024).toFixed(1)+'G' : Math.round(swapMB)+'M') : '—', swapMB!=null ? cls(swapMB, 2048, 8192) : '', swapMB!=null?Math.min(100,swapMB/8192*100):null],
['DISK', (s.diskUsedPct??'—')+'%', cls(s.diskUsedPct,80,90), s.diskUsedPct],
['NODE', s.node||'—','',null],
['UP', fmtDur(s.dashboardUptimeSec)||'—','',null],
].map(([k,v,c,g])=>`<div class="metric ${c}"><div class="v">${v}</div>${gauge(g)}<div class="k">[${k}]</div></div>`).join('');
$('refresh').textContent = 'snapshot '+new Date(d.builtAt).toLocaleTimeString()+' · auto 15s · $0 (local)'+(d.version?' · v'+d.version:'');
// ---- P&L ----
const p=(d.pnl&&d.pnl.today)||{}, c=(d.cost&&d.cost.today)||{};
$('pnl-cost').textContent=usd(p.cost);
$('pnl-cost-sub').textContent=`tokens ${usd(c.tokens)} + energy ${usd(c.energy)} est`;
$('pnl-rev').textContent=usd(p.revenue);
$('pnl-net').textContent=usd(p.net);
$('pnl-net').className='v '+((p.net||0)>=0?'good':'bad');
$('pnl-pct').textContent=(p.selfFundingPct==null?'0':p.selfFundingPct)+'%';
$('pnl-pct').className='v '+((p.selfFundingPct||0)>=100?'good':(p.selfFundingPct||0)>0?'warn':'bad');
// break-even target + progress toward self-funding (100%)
const pn=d.pnl||{}, pct=p.selfFundingPct||0;
const col=pct>=100?'var(--good)':pct<50?'var(--bad)':'var(--warn)';
$('pnl-breakeven').innerHTML=`Break-even: <b style="color:var(--gold)">${usd(pn.breakEvenUsdPerDay)}/day</b> · <b style="color:var(--gold)">${usd(pn.breakEvenUsdPerMonth)}/mo</b>`;
const bar=$('pnl-progress'); bar.style.width=Math.min(100,pct)+'%'; bar.style.background=col;
$('pnl-runway').textContent=pn.runwayNote||'';
const engs=(d.revenue&&d.revenue.engines)||[];
$('engines').innerHTML = engs.map(e=>`<div class="eng"><span>${e.label} <span class="muted">${e.note}</span></span>
<span class="pill ${e.status==='live'?'live':'gated'}">${e.status==='live'?usd(e.amount):'GATED'}</span></div>`).join('');
// ---- cost 30d ----
const c30=(d.cost&&d.cost.month)||{};
$('cost30').textContent=usd(c30.total);
$('cost30sub').textContent=`tokens ${usd(c30.tokens)} (${c30.tokensCount||0} calls) + energy ${usd(c30.energy)} est`;
renderProviderDonut(d);
const apps=Object.entries(c30.byApp||{}).sort((a,b)=>b[1]-a[1]).slice(0,6);
const max=Math.max(...apps.map(a=>a[1]),0.0001);
$('costapps').innerHTML=apps.map(([a,v])=>`<div class="row"><span class="dim">${a}</span><span>${usd(v)}</span></div><div class="bar"><span style="width:${Math.round(v/max*100)}%"></span></div>`).join('')||'<span class="muted">no cost data</span>';
// ---- cost trend · 7d (pure-SVG area chart) + per-model 30d ----
renderCostTrend(d);
// ---- model usage + sub-agent runs (panel 5) ----
renderUsage(d);
// ---- git activity feed (panel 6) ----
renderActivity(d);
// ---- cron / launchd (panel 4) ----
renderCrons(d);
// ---- fleet ----
const k=d.kamatera||{}, l=d.localFleet||{};
$('kam-up').innerHTML=`<span class="${cls(100-(k.up/(k.total||1)*100),20,50)}">${k.up??'—'}</span><span class="dim" style="font-size:16px">/${k.total??'—'} up</span> `+(whenChip(k.fetchedAt)||'')+(k.note?` <span class="warn" style="font-size:11px" title="${esc(k.error||'')}">⚠ ${esc(k.note)}</span>`:'');
renderKamFleet(k);
$('loc-up').innerHTML=`<span class="good">${l.up??'—'}</span><span class="dim" style="font-size:16px">/${l.total??'—'} up</span>`+(l.note?` <span class="warn" style="font-size:11px" title="${esc(l.error||'')}">⚠ ${esc(l.note)}</span>`:'');
$('loc-list').innerHTML=(l.procs||[]).map(pr=>lrow(`${statusDot(pr.status)}${hueChip(pr.name)}${esc(pr.name)}`,`${pr.memMB!=null?pr.memMB+'M':''}`,whenChip(pr.startedAt))).join('')||`<span class="muted">${esc(l.note||'no local pm2 procs')}</span>`;
// ---- canaries ---- (sort + density + infinite scroll like every DW grid)
const cans=(d.canaries&&d.canaries.canaries)||[];
renderCanaries(cans);
// ---- crons / officers ----
const cr=d.crons||{};
$('cron-loaded').innerHTML=`<span class="${cls(100-(cr.loadedCount/(cr.count||1)*100),40,70)}">${cr.loadedCount??'—'}</span><span class="dim" style="font-size:16px">/${cr.count??'—'}</span>`;
const off=d.officers||{};
$('off-count').textContent=off.count??'—';
$('off-when').innerHTML=whenChip(off.fetchedAt)||'';
$('officers').innerHTML=(off.officers||[]).map(o=>`<div class="row"><span>${o.vp||'—'}</span><span class="dim">${o.domain||''}</span></div>`).join('');
// ---- wins ----
const w=d.wins||{};
$('wins-today').textContent=`· ${w.today||0} today · ${w.count||0} total`;
$('wins').innerHTML=(w.recent||[]).map(x=>lrow(esc(x.title),`<span class="dim">${esc(x.project||'')}</span>`,whenChip(x.createdAt,x.date))).join('')||'<span class="muted">no wins logged</span>';
// ---- sessions ----
// firstUser is a HISTORICAL transcript snippet (the session's opening paste) —
// label it "session opener" + quote/italic/mute + truncate so it can never be
// mistaken for a live status card. Full text stays on hover via title=.
const sessOpener=(t)=>{ if(!t) return '';
const one=String(t).replace(/\s+/g,' ').trim();
const cut=one.length>140?one.slice(0,140).replace(/\s+\S*$/,'')+'…':one;
return `<div class="sess-open" title="${esc(one)}"><span class="sess-open-label">session opener</span><span class="sess-open-quote">“${esc(cut)}”</span></div>`; };
const ss=(d.sessions&&d.sessions.recent)||[];
$('sessions').innerHTML=ss.map(x=>lrow(esc(x.cwd||'session'),`${x.msgCount!=null?x.msgCount+' msg':''}`,whenChip(x.lastTs,x.mtime,x.firstTs)+sessOpener(x.firstUser))).join('')||'<span class="muted">no sessions</span>';
// ---- scoreboard (the game-state hero; reuses everything already in d) ----
renderScoreboard(d);
// ---- data freshness strip + tile chips (honest staleness) ----
renderFreshness(d);
// ---- vitals graphs: liquid dial, fleet gauge, canary W-L, wins sparkline ----
renderVitals(d);
// ---- alert banner ----
const alerts=[];
if((s.diskUsedPct||0)>=90) alerts.push('disk '+s.diskUsedPct+'%');
if((s.memUsedPct||0)>=88) alerts.push('RAM '+s.memUsedPct+'%');
const failCan=cans.filter(x=>/FAIL|DOWN|CRIT/.test((x.verdict||'').toUpperCase()));
if(failCan.length) alerts.push(failCan.length+' canary FAIL');
if((p.selfFundingPct||0)<100) alerts.push('not yet self-funding ('+ (p.selfFundingPct||0) +'% of cost covered)');
const a=$('alert'); if(alerts.length){a.classList.add('show');a.textContent='⚠ '+alerts.join(' · ');}else{a.classList.remove('show');}
}
// Per-provider share of 30d spend as a pure CSS conic-gradient donut + legend.
// Palette cycles the theme accent colors; each provider always shows its $ figure
// (Steve's "always show costs" rule) and free/local buckets read "$0 (local)".
const PROV_COLORS=['#4d9fff','#3fd18b','#ffb454','#ff5d6c','#ffd166','#a78bfa','#22d3ee','#f472b6','#94a3b8'];
function renderProviderDonut(d){
const el=$('costproviders'); if(!el) return;
const bp=((d.cost&&d.cost.byProvider)||[]).slice();
if(!bp.length){ el.innerHTML='<span class="muted">no provider data</span>'; return; }
const total=bp.reduce((s,p)=>s+(p.total||0),0);
// With no paid spend at all the conic-gradient would have nothing to draw —
// fall back to an even split so the local/$0 providers still render.
const denom=total>0?total:bp.reduce((s,p)=>s+1,0);
let acc=0; const stops=[];
bp.forEach((p,i)=>{
const share=total>0?(p.total||0):1;
const from=acc/denom*360, to=(acc+share)/denom*360; acc+=share;
const col=PROV_COLORS[i%PROV_COLORS.length];
stops.push(`${col} ${from.toFixed(2)}deg ${to.toFixed(2)}deg`);
});
const legend=bp.map((p,i)=>{
const col=PROV_COLORS[i%PROV_COLORS.length];
const pctv=total>0?Math.round((p.total||0)/total*100):Math.round(100/bp.length);
const amt=p.local?'$0 (local)':usd(p.total);
return `<div class="row"><span class="lbl"><span class="dot" style="background:${col}"></span>${p.provider}</span>`+
`<span class="amt">${amt} · ${pctv}%</span></div>`;
}).join('');
el.innerHTML=`<div class="donut" style="background:conic-gradient(${stops.join(',')})">`+
`<div class="donut-c"><b>${usd(total)}</b><span>${bp.length} prov</span></div></div>`+
`<div class="prov-legend">${legend}</div>`;
}
function renderCostTrend(d){
const series=(d.cost&&d.cost.series7d)||[];
const el=$('trend');
if(el){
if(!series.length){ el.innerHTML='<span class="muted">no cost data</span>'; }
else{
const W=560,H=150,padL=10,padR=10,padT=14,padB=24;
const iw=W-padL-padR, ih=H-padT-padB, n=series.length;
const vals=series.map(s=>s.total), max=Math.max(...vals,0.0001);
const x=i=> padL + (n<=1?iw/2:(i/(n-1))*iw);
const y=v=> padT + ih - (v/max)*ih;
const pts=series.map((s,i)=>[x(i),y(s.total)]);
const line=pts.map((p,i)=>(i?'L':'M')+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ');
const base=(padT+ih).toFixed(1);
const area='M '+pts[0][0].toFixed(1)+' '+base+' '+pts.map(p=>'L'+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ')+' L '+pts[n-1][0].toFixed(1)+' '+base+' Z';
const dots=pts.map((p,i)=>`<circle cx="${p[0].toFixed(1)}" cy="${p[1].toFixed(1)}" r="2.6" fill="var(--accent)"><title>${series[i].day}: ${usd(series[i].total)}</title></circle>`).join('');
const labels=series.map((s,i)=>`<text x="${x(i).toFixed(1)}" y="${H-7}" fill="var(--dim)" font-size="9" text-anchor="middle">${s.day.slice(5)}</text>`).join('');
const peak=`<text x="${padL}" y="${padT-2}" fill="var(--dim)" font-size="9">max ${usd(max)}</text>`;
el.innerHTML=`<svg viewBox="0 0 ${W} ${H}" width="100%" style="display:block;height:auto">
<defs><linearGradient id="cg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="var(--accent)" stop-opacity="0.35"/>
<stop offset="1" stop-color="var(--accent)" stop-opacity="0"/></linearGradient></defs>
<path d="${area}" fill="url(#cg)"/>
<path d="${line}" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
${dots}${labels}${peak}
</svg>`;
}
}
const bm=(d.cost&&d.cost.byModel)||[];
const bel=$('bymodel');
if(bel){
const mmax=Math.max(...bm.map(m=>m.usd),0.0001);
bel.innerHTML='<div class="muted" style="margin-bottom:6px">per-model · 30d</div>'+
(bm.slice(0,8).map(m=>`<div class="row"><span class="dim">${esc(m.api)}</span><span>${usd(m.usd)}</span></div><div class="bar"><span style="width:${Math.round(m.usd/mmax*100)}%"></span></div>`).join('')||'<span class="muted">no model data</span>');
}
}
function renderUsage(d){
const u=d.usage||{};
const sess=u.sessions||{}, models=(u.models&&u.models.allTime)||[], m24=(u.models&&u.models.last24h)||[];
const runs=(u.subagentRuns&&u.subagentRuns.recent)||[];
const fmtTok=(n)=> n==null?'—':(n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(0)+'k':String(n));
// header sub-lines
$('usage-sub').textContent=`· ${sess.active||0} active · ${sess.recent||0} recent sessions (24h)`;
$('subruns-sub').textContent=`· ${u.subagentRuns&&u.subagentRuns.count||0} total · showing ${runs.length}`;
// model mini-bar-chart — per-model share of all-time spend
const mel=$('modelbars');
if(mel){
const mmax=Math.max(...models.map(m=>m.cost),0.0001);
const c24=new Map(m24.map(m=>[m.model,m]));
mel.innerHTML=(models.slice(0,10).map(m=>{
const d24=c24.get(m.model);
const t=d24?` <span class="dim">· ${d24.calls} calls/24h</span>`:'';
return `<div class="row"><span class="dim">${esc(m.model)}${t}</span>`+
`<span>${usd(m.cost)} <span class="dim">· ${m.calls} calls · ${fmtTok(m.tokens)} tok</span></span></div>`+
`<div class="bar"><span style="width:${Math.round(m.cost/mmax*100)}%"></span></div>`;
}).join('')) || '<span class="muted">no model usage</span>';
}
// recent sub-agent runs — each is an ADMIN CARD → shows created date+time;
// grid gets sort + density + infinite scroll.
initGrid('subruns',{
controlsHost:$('subruns-ctl'), host:$('subruns'), batch:10, minCols:1, maxCols:3, defaultCols:1,
empty:'no sub-agent runs',
sortModes:[
{k:'new', label:'Newest', cmp:(a,b)=>tsOf(b.startedAt,b.updatedAt)-tsOf(a.startedAt,a.updatedAt)},
{k:'old', label:'Oldest', cmp:(a,b)=>tsOf(a.startedAt,a.updatedAt)-tsOf(b.startedAt,b.updatedAt)},
{k:'proj', label:'Project A→Z', cmp:(a,b)=>(a.project||'').localeCompare(b.project||'')},
{k:'model',label:'Model A→Z', cmp:(a,b)=>(a.model||'').localeCompare(b.model||'')},
],
render:(r)=>`<div class="subcard" title="${esc(r.startedAt||r.updatedAt||'')}">
<div class="subcard-top"><span class="subcard-proj">${hueChip(r.project)}${esc(r.project||'?')}</span>`+
`<span class="pill">${esc(r.model||'unknown')}</span></div>`+
`<div class="subcard-task">${esc(r.task||'')}</div>`+
`<div class="subcard-when">${whenChip(r.startedAt,r.updatedAt)||'🕓 —'}</div></div>`,
});
feedGrid('subruns', runs);
}
// git activity — each commit is an ADMIN CARD → shows created date+time;
// grid gets sort + density + infinite scroll (can exceed ~40 rows).
function renderActivity(d){
const a=d.activity||{};
const commits=a.commits||[];
const sub=$('activity-sub');
if(sub) sub.textContent=`· ${commits.length} commits · ${a.repoCount||0} repos`;
initGrid('activity',{
controlsHost:$('activity-ctl'), host:$('activity'), batch:12, minCols:1, maxCols:5, defaultCols:3,
empty:'no recent commits',
sortModes:[
{k:'new', label:'Newest', cmp:(x,y)=>tsOf(y.date)-tsOf(x.date)},
{k:'old', label:'Oldest', cmp:(x,y)=>tsOf(x.date)-tsOf(y.date)},
{k:'repo',label:'Repo A→Z',cmp:(x,y)=>(x.repo||'').localeCompare(y.repo||'')||tsOf(y.date)-tsOf(x.date)},
],
render:(cm)=>`<div class="subcard" title="${esc(cm.date||'')}">
<div class="subcard-top"><span class="subcard-proj">${hueChip(cm.repo)}${esc(cm.repo||'?')}</span>`+
`<span class="pill">${esc(cm.hash||'')}</span></div>`+
`<div class="subcard-task">${esc(cm.subject||'')}</div>`+
`<div class="subcard-when">${whenChip(cm.date)||'🕓 —'}${cm.author?' · '+esc(cm.author):''}</div></div>`,
});
feedGrid('activity', commits);
}
// panel 4 — every com.steve.* launchd job as an ADMIN CARD → MUST show created
// date+time (🕓 toLocaleString, full ISO in title=). Status pill: running=green
// PID · idle=grey · nonzero-exit=red.
function renderCrons(d){
const cr=d.crons||{};
const jobs=cr.jobs||[];
$('crons-sub').textContent=`· ${cr.loadedCount??0} loaded / ${cr.count??0} total · com.steve.*`;
const cronPill=(j)=>{
if(j.state==='running') return `<span class="pill live">▶ running · ${esc(j.pid)}</span>`;
if(j.state==='error') return `<span class="pill" style="color:var(--bad);border-color:#ff5d6c44;background:#ff5d6c0f">✕ exit ${esc(j.lastExit)}</span>`;
return `<span class="pill">idle</span>`;
};
const cronRank=(j)=> j.state==='error'?0 : j.state==='running'?1 : 2;
initGrid('crons',{
controlsHost:$('crongrid-ctl'), host:$('crongrid'), batch:18, minCols:1, maxCols:6, defaultCols:4,
empty:'no com.steve.* jobs',
sortModes:[
{k:'new', label:'Newest', cmp:(a,b)=>tsOf(b.created,b.modified)-tsOf(a.created,a.modified)},
{k:'old', label:'Oldest', cmp:(a,b)=>tsOf(a.created,a.modified)-tsOf(b.created,b.modified)},
{k:'name', label:'Name A→Z', cmp:(a,b)=>String(a.label||'').localeCompare(String(b.label||''))},
{k:'stat', label:'Status', cmp:(a,b)=>cronRank(a)-cronRank(b)||String(a.label||'').localeCompare(String(b.label||''))},
],
render:(j)=>{
const iso=j.created||j.modified||'';
const name=String(j.label||'').replace(/^com\.steve\./,'');
const sched=j.schedule?`⏲ ${esc(j.schedule)}`:'<span class="dim">no schedule</span>';
const prog=j.program?` · <span class="dim">${esc(j.program)}</span>`:'';
// SPEC panel 4: last/next run per job. Interval jobs have no knowable
// phase, so nextRun falls back to the schedule text.
const runs=`last ${whenChip(j.lastRun)||'—'} · next ${whenChip(j.nextRun)||esc(j.schedule||'—')}`;
const model=j.model?` <span class="pill">${esc(j.model)}</span>`:'';
return `<div class="subcard" title="created ${esc(iso)}">
<div class="subcard-top"><span class="subcard-proj">${esc(name)}</span>${cronPill(j)}</div>
<div class="subcard-when" style="margin-top:6px">${sched}${prog}</div>
<div class="subcard-when">${runs}${model}</div>
<div class="subcard-when">${whenChip(j.created,j.modified)||'🕓 —'}</div>
</div>`;
},
});
feedGrid('crons', jobs);
}
// ---- SCOREBOARD ------------------------------------------------------------
// The at-a-glance "game state" hero. Every number is pulled straight from the
// live /api/data snapshot (P&L, wins, canaries, fleet, sessions) — nothing is
// invented. Headline STATUS is a rolled-up verdict; tiles are the big scores.
function renderScoreboard(d){
const p=(d.pnl&&d.pnl.today)||{}, w=d.wins||{};
const cans=(d.canaries&&d.canaries.canaries)||[];
const k=d.kamatera||{}, l=d.localFleet||{}, us=d.usageSummary||{};
// canary W-L record: wins = PASS/UP/HEALTHY/OK, losses = FAIL/DOWN/CRIT, warns split out
const rank=(v)=>{v=(v||'').toUpperCase(); return /FAIL|DOWN|CRIT/.test(v)?'L':/WARN|DEGRAD/.test(v)?'W?':/PASS|UP|HEALTH|OK/.test(v)?'W':'?';};
const rec={W:0,'W?':0,L:0,'?':0}; cans.forEach(c=>rec[rank(c.verdict||c.status)]++);
const canFail=rec.L, canWarn=rec['W?'], canTotal=cans.length;
const fleetUp=(k.up||0)+(l.up||0), fleetTotal=(k.total||0)+(l.total||0);
const fleetDown=fleetTotal-fleetUp;
const selfPct=p.selfFundingPct||0;
const net=p.net||0;
// headline STATUS — the "who's winning" call. DOWN if a canary is failing or a
// proc is down; HOLD if warns or not self-funding; WIN only when all-green.
let status='WIN', scls='win';
if(canFail>0 || fleetDown>0){ status='DEFENSE'; scls='down'; }
else if(canWarn>0 || selfPct<100){ status='HOLDING'; scls='hold'; }
$('sb-status').textContent=status; $('sb-status').className='sb-status '+scls;
$('sb-clock').innerHTML=whenChip(d.builtAt)+' · live';
// tiles — big legible scores. self-funding is the headline "score".
const fundCls=selfPct>=100?'good':selfPct>0?'warn':'bad';
const canCls=canFail>0?'bad':canWarn>0?'warn':'good';
const fleetCls=fleetDown>0?'bad':'good';
const tiles=[
{cls:'hero '+fundCls, k:'Self-funding', num:selfPct, fmt:v=>Math.round(v)+'', u:'%',
s:`${usd(p.revenue)} rev − ${usd(p.cost)} cost = <b class="${net>=0?'good':'bad'}">${usd(net)}</b>`},
{cls:'gold', k:'Wins today', num:(w.today||0), fmt:v=>Math.round(v)+'', u:'',
s:`${(w.count||0).toLocaleString()} all-time`},
{cls:canCls, k:'Canary record', v:`${rec.W}–${canFail}`, u:'',
s:`${canTotal} checks${canWarn?` · ${canWarn} warn`:''}`},
{cls:fleetCls, k:'Fleet online', num:fleetUp, fmt:v=>Math.round(v)+'', u:'/'+fleetTotal,
s:`Mac2 ${l.up||0}/${l.total||0} · Kam ${k.up||0}/${k.total||0}${k.stale?' (stale)':''}`},
{cls:'accent', k:'Live sessions', num:(us.activeSessions||0), fmt:v=>Math.round(v)+'', u:'',
s:`${(us.subagentRuns||0).toLocaleString()} sub-agent runs`},
];
$('sb-tiles').innerHTML=tiles.map((t,i)=>
`<div class="sb-tile ${t.cls}"><div class="sk">${esc(t.k)}</div>`+
`<div class="sv"><span class="cu" data-tile="${i}">${t.num!=null?'0':t.v}</span>${t.u?`<span class="su">${esc(t.u)}</span>`:''}</div>`+
`<div class="ss">${t.s}</div></div>`).join('');
// MOTION: count-up the numeric tiles from their last value to the new one
tiles.forEach((t,i)=>{ if(t.num!=null){ countUp($('sb-tiles').querySelector(`[data-tile="${i}"]`), t.num, t.fmt); } });
}
// ---- DATA FRESHNESS: honest per-source "as of" ages (fixes the silent 6-day
// stale revenue.json powering the self-funding tile). Renders the header strip
// + returns a lookup so individual tiles can show their own fresh/aging/stale chip.
let FRESH={};
function freshChip(f){
if(!f) return '';
const cl = f.state==='stale'?'stale':f.state==='aging'?'aging':'';
return `<span class="fresh ${cl}" title="${esc(f.label)} — updated ${esc(f.updatedAt||'unknown')}">as of ${esc(f.ageLabel||'—')}</span>`;
}
function renderFreshness(d){
const fr=(d.freshness&&d.freshness.sources)||{};
FRESH=fr;
const bar=$('freshbar'); if(!bar) return;
const chips=Object.values(fr).map(f=>{
const cl=f.state==='stale'?'stale':f.state==='aging'?'aging':'';
return `<span class="fresh ${cl}" title="updated ${esc(f.updatedAt||'unknown')}">${esc(f.label)} · ${esc(f.ageLabel||'—')}</span>`;
}).join('');
const worst=d.freshness&&d.freshness.worst;
const warn=worst==='stale'?`<span class="fresh stale" title="one or more sources are stale">⚠ ${d.freshness.staleCount} stale</span>`:'';
bar.innerHTML=`<span class="muted" style="font-size:10px;letter-spacing:.6px">DATA FRESHNESS</span>${warn}${chips}`;
}
// ---- VITALS GRAPHS (zero-dependency SVG) ----------------------------------
function renderVitals(d){
const p=(d.pnl&&d.pnl.today)||{};
const k=d.kamatera||{}, l=d.localFleet||{};
const cans=(d.canaries&&d.canaries.canaries)||[];
renderFundDial(p);
renderFleetGauge(k,l);
renderCanaryWL(cans);
renderWinsSpark(d.wins||{});
// per-tile freshness chips (self-funding tile's true honesty fix)
if($('fresh-fund')){ const f=FRESH.revenue; $('fresh-fund').outerHTML=freshChip(f).replace('class="fresh','id="fresh-fund" class="fresh'); }
if($('fresh-fleet')){ const f=FRESH.kamatera; $('fresh-fleet').outerHTML=freshChip(f).replace('class="fresh','id="fresh-fleet" class="fresh'); }
}
// Liquid-fill self-funding dial: a circle whose fill LEVEL = % of run-cost
// covered by revenue, with an animated liquid wave surface. 0% honest = empty.
function renderFundDial(p){
const el=$('fund-dial'); if(!el) return;
const pct=Math.max(0,Math.min(200,p.selfFundingPct||0));
const fillPct=Math.min(100,pct); // clamp fill height to the cup
const col=pct>=100?'#3fd18b':pct>0?'#ffb454':'#ff5d6c';
const R=54,CX=59,CY=59;
// liquid surface y within the circle (100% = top, 0% = bottom)
const top=CY-R, h=2*R, level=CY+R-(fillPct/100)*h;
// a repeating wave path 2× the width so translateX(-50%) loops seamlessly
const wave=(y)=>{let dd=`M ${CX-R} ${y}`;const amp=fillPct>0&&fillPct<100?4:0;const w=R/2;
for(let x=CX-R;x<=CX+2*R;x+=w){dd+=` q ${w/2} ${-amp} ${w} 0 q ${w/2} ${amp} ${w} 0`;}
dd+=` L ${CX+2*R} ${CY+R} L ${CX-R} ${CY+R} Z`;return dd;};
el.innerHTML=`<svg viewBox="0 0 118 118" style="transform:none">
<defs><clipPath id="cup"><circle cx="${CX}" cy="${CY}" r="${R}"/></clipPath>
<linearGradient id="lqg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="${col}" stop-opacity=".95"/><stop offset="1" stop-color="${col}" stop-opacity=".45"/></linearGradient></defs>
<circle cx="${CX}" cy="${CY}" r="${R}" fill="#0b1421" stroke="var(--line)" stroke-width="2"/>
<g clip-path="url(#cup)">
${fillPct>0?`<path class="wave" d="${wave(level)}" fill="url(#lqg)"/>
<path class="wave" style="animation-duration:7s;opacity:.5" d="${wave(level+3)}" fill="${col}"/>`:''}
</g>
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${col}" stroke-width="2" opacity=".55"/>
</svg>
<div class="lqlabel"><b class="cu" data-fund style="color:${col}">0</b><span>of cost covered</span></div>`;
countUp(el.querySelector('[data-fund]'), pct, v=>Math.round(v)+'%');
const note=$('fund-note');
if(note) note.innerHTML=`${usd(p.revenue)} earned vs ${usd(p.cost)} spent today · net <b class="${(p.net||0)>=0?'good':'bad'}">${usd(p.net)}</b>`;
}
// Radial fleet gauge: an arc that fills to up/total, colored by whether any proc is down.
function renderFleetGauge(k,l){
const el=$('fleet-gauge'); if(!el) return;
const up=(k.up||0)+(l.up||0), total=(k.total||0)+(l.total||0);
const frac=total>0?up/total:0;
const col=up<total?'#ff5d6c':'#3fd18b';
const R=48,CX=55,CY=55,C=2*Math.PI*R;
const dash=(frac*C).toFixed(1);
el.innerHTML=`<svg viewBox="0 0 110 110">
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="#ffffff12" stroke-width="9"/>
<circle cx="${CX}" cy="${CY}" r="${R}" fill="none" stroke="${col}" stroke-width="9" stroke-linecap="round"
stroke-dasharray="${dash} ${(C-dash).toFixed(1)}" style="transition:stroke-dasharray .7s cubic-bezier(.2,.7,.3,1)"/>
</svg>
<div class="glabel"><b class="cu" data-fleet style="color:${col}">0</b><span>${total} total</span></div>`;
countUp(el.querySelector('[data-fleet]'), up, v=>Math.round(v)+'');
const note=$('fleet-note');
if(note) note.innerHTML=`Mac2 <b>${l.up||0}/${l.total||0}</b> · Kamatera <b>${k.up||0}/${k.total||0}</b>${k.stale?' <span class="warn">(cache stale)</span>':''}`;
}
// Canary W–L: PASS/UP/HEALTHY = W, WARN/DEGRADED = warn, FAIL/DOWN/CRIT = L.
function renderCanaryWL(cans){
const bar=$('wl-bar'), leg=$('wl-legend'); if(!bar) return;
const rank=(v)=>{v=(v||'').toUpperCase();return /FAIL|DOWN|CRIT/.test(v)?'l':/WARN|DEGRAD/.test(v)?'warn':/PASS|UP|HEALTH|OK/.test(v)?'w':'x';};
const rec={w:0,warn:0,l:0,x:0}; cans.forEach(c=>rec[rank(c.verdict||c.status)]++);
const tot=Math.max(1,cans.length);
bar.innerHTML=`<i class="w" style="width:${rec.w/tot*100}%"></i><i class="warn" style="width:${rec.warn/tot*100}%"></i>`+
`<i class="l" style="width:${rec.l/tot*100}%"></i><i style="width:${rec.x/tot*100}%;background:#2a3340"></i>`;
leg.innerHTML=`<span class="good">● <b>${rec.w}</b> pass</span><span class="warn">● <b>${rec.warn}</b> warn</span>`+
`<span class="bad">● <b>${rec.l}</b> fail</span><span class="dim">${cans.length} checks</span>`;
}
// Wins-over-time sparkline from the REAL server-computed 7-day series.
function renderWinsSpark(w){
const el=$('wins-spark'); if(!el) return;
const series=w.series7d||[];
if(!series.length){ el.innerHTML='<span class="muted">no wins series</span>'; return; }
const W=280,H=70,padL=6,padR=6,padT=8,padB=16;
const iw=W-padL-padR, ih=H-padT-padB, n=series.length;
const vals=series.map(s=>s.count), max=Math.max(...vals,1);
const x=i=> padL+(n<=1?iw/2:(i/(n-1))*iw);
const y=v=> padT+ih-(v/max)*ih;
const pts=series.map((s,i)=>[x(i),y(s.count)]);
const line=pts.map((p,i)=>(i?'L':'M')+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ');
const base=(padT+ih).toFixed(1);
const area='M '+pts[0][0].toFixed(1)+' '+base+' '+pts.map(p=>'L'+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ')+' L '+pts[n-1][0].toFixed(1)+' '+base+' Z';
const bars=series.map((s,i)=>{const bw=iw/n*.5,bx=x(i)-bw/2,bh=(s.count/max)*ih;
return `<rect x="${bx.toFixed(1)}" y="${(padT+ih-bh).toFixed(1)}" width="${bw.toFixed(1)}" height="${Math.max(1,bh).toFixed(1)}" fill="var(--gold)" opacity=".22" rx="1"/>`;}).join('');
const dots=pts.map((p,i)=>`<circle cx="${p[0].toFixed(1)}" cy="${p[1].toFixed(1)}" r="2.4" fill="var(--gold)"><title>${series[i].day}: ${series[i].count} wins</title></circle>`).join('');
const labels=series.map((s,i)=>`<text x="${x(i).toFixed(1)}" y="${H-4}" fill="var(--dim)" font-size="8" text-anchor="middle">${s.day.slice(5)}</text>`).join('');
el.innerHTML=`<svg class="spark" viewBox="0 0 ${W} ${H}">
<defs><linearGradient id="wg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="var(--gold)" stop-opacity=".3"/><stop offset="1" stop-color="var(--gold)" stop-opacity="0"/></linearGradient></defs>
${bars}<path d="${area}" fill="url(#wg)"/>
<path d="${line}" fill="none" stroke="var(--gold)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
${dots}${labels}</svg>`;
const total7=vals.reduce((a,b)=>a+b,0);
const note=$('wins-spark-note');
if(note) note.innerHTML=`<b class="gold">${total7}</b> wins in 7 days · <b class="gold">${w.today||0}</b> today · ${(w.count||0).toLocaleString()} all-time`;
}
// canary board — each canary is a card with a created/scanned date+time; grid
// gets sort + density + infinite scroll (verdict/staleness/name modes).
function renderCanaries(cans){
const vrank=(cn)=>{ const v=(cn.verdict||'').toUpperCase();
return /FAIL|DOWN|CRIT/.test(v)?0:/WARN|DEGRAD/.test(v)?1:/PASS|UP|HEALTH|OK/.test(v)?2:3; };
initGrid('canaries',{
controlsHost:$('canaries-ctl'), host:$('canaries'), batch:20, minCols:1, maxCols:3, defaultCols:1,
empty:'no canaries',
sortModes:[
{k:'new', label:'Newest', cmp:(a,b)=>tsOf(b.mtime,b.scanned)-tsOf(a.mtime,a.scanned)},
{k:'verdict',label:'Verdict', cmp:(a,b)=>vrank(a)-vrank(b)||(a.name||'').localeCompare(b.name||'')},
{k:'stale', label:'Staleness', cmp:(a,b)=>(b.ageMin||0)-(a.ageMin||0)},
{k:'name', label:'Name A→Z', cmp:(a,b)=>(a.name||'').localeCompare(b.name||'')},
],
render:(cn)=>{
const v=(cn.verdict||'').toUpperCase();
const col=/FAIL|DOWN|CRIT/.test(v)?'bad':/WARN|DEGRAD/.test(v)?'warn':/PASS|UP|HEALTH|OK/.test(v)?'good':'dim';
return lrow(esc(cn.name),`<span class="${col}">${esc(cn.verdict||'—')}</span> <span class="dim">${cn.ageMin}m${cn.stale?' ⚠':''}</span>`,whenChip(cn.mtime,cn.scanned));
},
});
feedGrid('canaries', cans);
}
// Kamatera fleet — 226 live procs; grid (sort + density + infinite scroll)
// replaces the old silent slice(0,40) cap. Same lrow render, 🕓 chip kept.
function renderKamFleet(k){
const srank=(pr)=> pr.status==='online'?1:0; // offline first
initGrid('kamfleet',{
controlsHost:$('kam-ctl'), host:$('kam-list'), batch:25, minCols:1, maxCols:3, defaultCols:1,
empty:(k.note||'kamatera unreachable'),
sortModes:[
{k:'new', label:'Newest', cmp:(a,b)=>tsOf(b.startedAt)-tsOf(a.startedAt)},
{k:'old', label:'Oldest', cmp:(a,b)=>tsOf(a.startedAt)-tsOf(b.startedAt)},
{k:'name', label:'Name A→Z', cmp:(a,b)=>(a.name||'').localeCompare(b.name||'')},
{k:'restarts',label:'Restarts ↓', cmp:(a,b)=>(b.restarts||0)-(a.restarts||0)},
{k:'status', label:'Status', cmp:(a,b)=>srank(a)-srank(b)||(a.name||'').localeCompare(b.name||'')},
],
render:(pr)=>lrow(`${statusDot(pr.status)}${hueChip(pr.name)}${esc(pr.name)}`,`${pr.restarts??''}↺`,whenChip(pr.startedAt)),
});
// empty text can change per snapshot (unreachable note vs default)
GRIDS['kamfleet'].cfg.empty=(k.note||'kamatera unreachable');
feedGrid('kamfleet', k.procs||[]);
}
function statusDot(st){const c=st==='online'?'var(--good)':st?'var(--bad)':'var(--dim)';return `<span class="dot" style="background:${c}"></span>`;}
// stable per-agent identity color: name hash spun around the golden angle
// (137.508°, same trick as /color) so every process/repo keeps one hue everywhere
function agentHue(name){let h=0;const s=String(name||'');for(let i=0;i<s.length;i++)h=(h*31+s.charCodeAt(i))>>>0;return Math.round((h%997)*137.508)%360;}
function hueChip(name){return `<span class="hue" style="background:hsl(${agentHue(name)},62%,56%)"></span>`;}
// click a panel → it glows, siblings dim (click again or Esc to clear);
// clicks on links/controls inside a panel are left alone
document.getElementById('main').addEventListener('click',(e)=>{
const card=e.target.closest('section.card'); if(!card) return;
if(e.target.closest('a,select,input,button,textarea,label')) return;
const main=document.getElementById('main');
const was=card.classList.contains('focus');
main.querySelectorAll('section.card.focus').forEach(c=>c.classList.remove('focus'));
main.classList.toggle('focused',!was);
if(!was) card.classList.add('focus');
});
document.addEventListener('keydown',(e)=>{
if(e.key!=='Escape') return;
const main=document.getElementById('main');
main.classList.remove('focused');
main.querySelectorAll('section.card.focus').forEach(c=>c.classList.remove('focus'));
});
function fmtDur(s){if(s==null)return null;if(s<60)return s+'s';if(s<3600)return Math.floor(s/60)+'m';return Math.floor(s/3600)+'h';}
function esc(x){return (x==null?'':String(x)).replace(/[<>&]/g,m=>({'<':'<','>':'>','&':'&'}[m]));}
// chat
$('q').addEventListener('keydown',async e=>{
if(e.key!=='Enter')return;
const q=$('q').value.trim(); if(!q)return;
$('ans').textContent='…thinking';
try{
const r=await (await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({q})})).json();
$('ans').textContent=(r.answer||r.error||'no answer')+`\n\n— ${r.engine||'?'} · cost ${r.cost||'$0 (local ollama)'}`;
}catch(e){ $('ans').textContent='chat error'; }
});
// MOTION: pause the liquid blobs + pulses + wave animations while the tab is
// hidden (perf + battery); resume on return. Also skip the 15s poll when hidden.
function applyPaused(){ document.body.classList.toggle('paused', document.hidden); }
document.addEventListener('visibilitychange', applyPaused); applyPaused();
// MOTION: staggered entrance — panels rise+fade in sequence on first paint only.
function playEntrance(){
const main=$('main'); if(!main) return;
main.classList.add('enter');
[...main.querySelectorAll('section.card')].forEach((c,i)=>{ c.style.animationDelay=(i*60)+'ms'; });
// strip the class after the last panel finishes so re-renders don't re-trigger it
setTimeout(()=>main.classList.remove('enter'), 60*24+700);
}
let _booted=false;
// WPB marketplace sales — fetched from this server's /api/wpb proxy (which pulls :9812).
async function loadWpb(){
let w; try{ w = await (await fetch('/api/wpb')).json(); }catch(e){ w = null; }
const stats = document.getElementById('wpb-stats'), mk = document.getElementById('wpb-markets');
if(!stats) return;
if(!w || !w.ok){ stats.innerHTML = '<div class="muted">WPB dashboard (:9812) unreachable</div>'; if(mk) mk.innerHTML=''; return; }
const k = w.kpis || {};
const money = v => '$' + (v||0).toLocaleString(undefined,{maximumFractionDigits:2});
stats.innerHTML = [
['Gross sales', money(k.salesGross), (k.ordersCount||0)+' orders · '+(k.unitsSold||0)+' units','gold'],
['Net', money(k.salesNet), 'after fees',''],
['Listings', (k.listingsTotal||0), (k.listingsLive||0)+' live · '+(k.listingsPrivate||0)+' private · '+(k.listingsPlanned||0)+' planned',''],
['Designs', (k.designsTotal||0), 'inventory',''],
['Marketplaces', (k.marketplacesLive||0)+'/'+(k.marketplacesTotal||0), 'live',''],
].map(([kk,v,sub,cls])=>`<div class="stat"><div class="k">${kk}</div><div class="v ${cls}">${v}</div><div class="muted">${sub}</div></div>`).join('');
if(mk){
mk.innerHTML = (w.markets||[]).map(m=>{
const pill = m.status==='live' ? '#2ec4b6' : '#8a8f9c';
return `<span style="display:inline-flex;align-items:center;gap:6px;margin:3px 10px 3px 0;font-size:12px">
<span style="width:9px;height:9px;border-radius:2px;background:${m.color||'#555'}"></span>
<b>${m.name}</b>
<span style="color:${pill}">${m.status}</span>
<span class="muted">${m.listings} listings${m.orders?' · '+money(m.salesGross):''}</span></span>`;
}).join('');
}
if(!w.launched){ const sub=document.getElementById('wpb-sub'); if(sub && !sub.dataset.pre){ sub.dataset.pre='1'; sub.innerHTML += ' · <span style="color:var(--muted,#9aa0ad)">🌱 pre-launch ($0)</span>'; } }
}
async function tick(){ if(document.hidden) return; await load(); loadWpb(); if(!_booted){ _booted=true; playEntrance(); } }
tick(); setInterval(tick, 15000);
</script>
<script>window.UNBLOCK_CONFIG={surface:'abramsego',endpoint:'/api/dispatch',statusEndpoint:'/api/yolo/status'};</script>
<script src="/unblock.js" defer></script>
</body>
</html>