← back to Abrams

public/index.html

294 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Abrams — The Panel</title>
<meta name="description" content="The Abrams empire — one panel for every Abrams / AgentAbrams project. Live build log, lead capture, trademark hub, SWOT ledger. Bloomberg-terminal-style umbrella over Steve Abrams's stack." />
<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=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/panel.css" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">
  </head>
<body>

<!-- chrome -->
<div class="chrome">
  <a class="logo" href="/">ABRAMS<span class="dot">.</span></a>
  <div class="nav-burger" role="button" aria-label="open menu">
    <span class="bars"><span></span><span></span><span></span></span>
    <span class="label">MENU</span>
  </div>
</div>

<!-- hero — single white-pill ENTER bottom-center (Gucci standing rule). Secondary nav lives in the burger drawer. -->
<section class="hero">
  <h1>ABRAMS</h1>
  <p class="tagline">The intelligence terminal for independent operators.</p>
  <a class="cta" href="#thesis">ENTER</a>
</section>

<!-- thesis — consensus positioning from debate-team-fast (Claude · Qwen3 panel) -->
<section id="thesis" style="background:linear-gradient(180deg,#0a0a0a,#141414);">
  <h2>The Thesis</h2>
  <p style="font-family:var(--serif);font-size:clamp(20px,2.4vw,28px);line-height:1.55;color:var(--fg);max-width:840px;margin:0 0 28px;font-style:italic;">
    Abrams is the intelligence terminal for independent operators — real-time news, AI agents, legal tools, and household-rights OS in one layer, for the solo founder, professional, and owner who runs their world without enterprise IT.
  </p>
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;max-width:980px;">
    <div style="background:var(--bg-2);border-left:3px solid var(--accent);padding:18px 22px;">
      <h4 style="font-family:var(--mono);font-size:11px;letter-spacing:.25em;color:var(--accent);margin:0 0 10px;">LIVES UNDER ABRAMS</h4>
      <ol style="color:var(--fg);font-size:14px;line-height:1.7;margin:0;padding-left:20px;">
        <li>Abrams Report — intelligence feed</li>
        <li>AbramsOS — ownership leg (warranties, recalls, receipts)</li>
        <li>AgentAbrams — build leg (publishing OS)</li>
        <li>Butlr — daily habit, friction-zero AI dialer</li>
        <li>Lawyers · SDCC · vertical intelligence beachheads</li>
      </ol>
    </div>
    <div style="background:var(--bg-2);border-left:3px solid var(--err);padding:18px 22px;">
      <h4 style="font-family:var(--mono);font-size:11px;letter-spacing:.25em;color:var(--err);margin:0 0 10px;">DOES NOT LIVE HERE</h4>
      <ol style="color:var(--fg);font-size:14px;line-height:1.7;margin:0;padding-left:20px;">
        <li>Entertainment / lifestyle content</li>
        <li>Enterprise SaaS plays</li>
        <li>DW commerce / wallpaper stores (keep namespaces clean)</li>
        <li>Generic social network features</li>
        <li>Fiduciary / advice products where Abrams holds liability</li>
      </ol>
    </div>
  </div>
  <p style="color:var(--fg-3);font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;margin-top:24px;">debate-team-fast · 2026-05-14 · audience: solo founders + independent professionals + prosumer operators · $29-99/mo self-serve, $499+/mo pro</p>
</section>

<!-- live counters strip (cross-project metrics from real services) -->
<div id="counters-strip" style="background:#000;border-top:1px solid var(--hairline);border-bottom:1px solid var(--hairline);padding:14px 24px;display:flex;gap:32px;overflow-x:auto;flex-wrap:nowrap;align-items:center;"></div>

<!-- terminal preview strip — live markets + top abrams index -->
<a href="/terminal" style="text-decoration:none;display:block;background:#000;border-bottom:1px solid var(--hairline);padding:10px 24px;color:var(--fg);font-family:var(--mono);font-size:11px;overflow-x:auto;white-space:nowrap;">
  <span style="color:var(--accent);letter-spacing:.25em;text-transform:uppercase;margin-right:18px;">▌ TERMINAL</span>
  <span id="term-preview" style="color:var(--fg-2);">loading market feed…</span>
  <span style="color:var(--fg-3);letter-spacing:.2em;float:right;">CLICK FOR FULL TERMINAL →</span>
</a>

<!-- live ticker -->
<div class="ticker" id="ticker"><span class="dot"></span> <span id="ticker-text">connecting to build feed…</span></div>

<!-- projects -->
<section id="projects">
  <h2>The Empire</h2>
  <p class="sub">Every <em>Abrams</em> and <em>AgentAbrams</em> project. Live health, sortable, density-adjustable.</p>
  <div class="controls">
    <span class="lbl">Sort</span>
    <select id="sort">
      <option value="status">Status (live first)</option>
      <option value="name">Name A→Z</option>
      <option value="newest">Newest</option>
      <option value="category">Category</option>
    </select>
    <span class="lbl">Density</span>
    <input type="range" id="density" min="2" max="6" value="4" />
    <span class="lbl">Refresh</span>
    <button onclick="loadProjects()" style="background:var(--bg-3);color:var(--fg);border:1px solid var(--hairline);padding:8px 14px;font-family:var(--mono);font-size:11px;letter-spacing:.2em;cursor:pointer;">RELOAD</button>
    <span class="lbl">Tick</span>
    <button onclick="manualTick()" style="background:var(--accent);color:#111;border:none;padding:8px 14px;font-family:var(--mono);font-size:11px;letter-spacing:.2em;cursor:pointer;font-weight:600;">YOLO TICK NOW</button>
  </div>
  <div class="grid" id="grid"></div>
</section>

<!-- ideas + SWOT preview -->
<section id="ideas">
  <h2>Business Ideas — Full SWOT</h2>
  <p class="sub">One SWOT analysis per site, refreshed on every YOLO tick. <a href="/ideas">Full ledger →</a></p>
  <div class="grid" id="ideas-grid" style="--cols:3"></div>
</section>

<!-- build log -->
<section id="feed">
  <h2>Live Build Log</h2>
  <p class="sub">SSE feed. Every tick, every commit, every email, every lead.</p>
  <div class="feed" id="feed-list"><div class="row"><span class="ts">connecting…</span><span class="type">SYS</span><span class="body">waiting for first event</span></div></div>
</section>

<!-- leaderboard -->
<section id="leaderboard">
  <h2>Leaderboard</h2>
  <p class="sub">Live XP race · panel visitors earn XP for actions, unlock badges, build streaks. Click your HUD bottom-right to see your stats.</p>
  <div id="leaderboard-list" style="font-family:var(--mono);font-size:12px;"></div>
</section>

<!-- lead capture -->
<section id="lead">
  <h2>Get on the Panel</h2>
  <p class="sub">Interested in partnership, licensing, or a vertical inside the Abrams empire? Leave a line.</p>
  <form class="lead-form" onsubmit="submitLead(event)">
    <input name="name" placeholder="Name" required />
    <input name="email" type="email" placeholder="Email" required />
    <input name="company" placeholder="Company (optional)" />
    <input name="source" placeholder="How did you hear about us?" />
    <textarea name="message" placeholder="What's on your mind?"></textarea>
    <button type="submit">SUBMIT</button>
  </form>
  <div id="lead-result" style="margin-top:16px;font-family:var(--mono);font-size:12px;color:var(--accent);"></div>
</section>

<footer>
  © 2026 Steven Abrams · <a href="/trademark">trademark</a> · <a href="/ideas">SWOT</a> · <a href="/wall">wall</a> · <a href="/chat">chat</a> · <a href="https://agentabrams.com">agentabrams.com</a>
</footer>

<script src="/static/js/corner-nav.js"></script>
<script src="/static/js/gamify.js"></script>
<script>
// ── projects grid ──
async function loadTermPreview() {
  try {
    const [m, a] = await Promise.all([
      fetch('/api/market').then(r => r.json()),
      fetch('/api/abrams-indexes').then(r => r.json()),
    ]);
    const top = (m.tickers || []).slice(0, 6).map(t => {
      const sign = (t.changePct||0) >= 0 ? '+' : '';
      const cls = (t.changePct||0) >= 0 ? 'color:#4ade80' : 'color:#ef4444';
      return `<span style="margin-right:18px;"><strong style="color:#fff">${escapeHtml(t.sym)}</strong> ${(t.price||0).toFixed(2)} <span style="${cls}">${sign}${(t.changePct||0).toFixed(2)}%</span></span>`;
    }).join('');
    const ix = (a.indexes || []).slice(0, 3).map(x => `<span style="margin-right:18px;"><strong style="color:#c9a96e">${escapeHtml(x.id)}</strong> ${escapeHtml(String(x.value))}${escapeHtml(x.unit||'')}</span>`).join('');
    document.getElementById('term-preview').innerHTML = top + ' &nbsp;·&nbsp; ' + ix;
  } catch {}
}

async function loadCounters() {
  const r = await fetch('/api/counters').then(x=>x.json()).catch(()=>({counters:[]}));
  document.getElementById('counters-strip').innerHTML = (r.counters||[]).map(c => `
    <a href="${c.url||'#'}" style="text-decoration:none;display:flex;flex-direction:column;min-width:140px;border-right:1px solid var(--hairline);padding-right:32px;">
      <span style="font-family:var(--mono);font-size:9px;color:var(--fg-3);letter-spacing:.2em;text-transform:uppercase;">${escapeHtml(c.label)}</span>
      <span style="font-family:var(--serif);font-size:26px;color:var(--accent);margin-top:4px;">${(c.value||0).toLocaleString()}</span>
    </a>
  `).join('') || '<div style="color:var(--fg-3);font-family:var(--mono);font-size:11px;">counters loading…</div>';
}

async function loadProjects() {
  const r = await fetch('/api/projects').then(x => x.json());
  const sortBy = document.getElementById('sort').value;
  const projects = (r.projects || []).slice();
  const rank = s => ({live:0, dev:1, idea:2, down:3}[s] ?? 9);
  if (sortBy === 'status') projects.sort((a,b) => rank(a.status) - rank(b.status));
  if (sortBy === 'name') projects.sort((a,b) => a.name.localeCompare(b.name));
  if (sortBy === 'newest') projects.sort((a,b) => (b.created_at||'').localeCompare(a.created_at||''));
  if (sortBy === 'category') projects.sort((a,b) => (a.category||'').localeCompare(b.category||''));
  document.getElementById('grid').innerHTML = projects.map(p => `
    <div class="tile" onclick="openProject('${encodeURIComponent(p.slug||p.name)}')">
      <div class="name">${escapeHtml(p.name)}</div>
      <div class="domain">${escapeHtml(p.domain || p.port ? ('localhost:'+p.port) : '—')}</div>
      <div class="desc">${escapeHtml(p.description || '')}</div>
      <div class="meta">
        <span class="pill ${p.status||'idea'}">${(p.status||'idea').toUpperCase()}</span>
        ${p.category ? `<span class="pill">${escapeHtml(p.category)}</span>` : ''}
      </div>
    </div>
  `).join('') || '<div style="color:var(--fg-3);padding:24px;font-family:var(--mono);font-size:12px;">no projects yet — first inventory tick pending</div>';
}

// ── ideas/SWOT ──
async function loadIdeas() {
  const r = await fetch('/api/ideas').then(x => x.json());
  const ideas = (r.ideas || []).slice(0, 6);
  document.getElementById('ideas-grid').innerHTML = ideas.map(i => `
    <div class="tile" onclick="openIdea('${encodeURIComponent(i.slug)}')">
      <div class="name">${escapeHtml(i.name)}</div>
      <div class="domain">${escapeHtml(i.tagline || '')}</div>
      <div class="desc">${escapeHtml((i.summary||'').slice(0,140))}…</div>
      <div class="meta"><span class="pill idea">${(i.stage||'idea').toUpperCase()}</span><span class="pill">SWOT</span></div>
    </div>
  `).join('') || '<div style="color:var(--fg-3);padding:24px;font-family:var(--mono);font-size:12px;grid-column:1/-1;">no ideas yet — next YOLO tick will spawn one</div>';
}

// ── ticker / feed ──
function fmtTs(ts){ return new Date(ts).toLocaleTimeString('en-US',{hour12:false}); }
function escapeHtml(s){ return String(s||'').replace(/[&<>"']/g, m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
const feedList = document.getElementById('feed-list');
const tickerText = document.getElementById('ticker-text');
const seenEvents = [];

function addEvent(ev){
  seenEvents.unshift(ev);
  if (seenEvents.length > 80) seenEvents.pop();
  feedList.innerHTML = seenEvents.map(e =>
    `<div class="row"><span class="ts">${fmtTs(e.ts)}</span><span class="type">${escapeHtml(e.type||'evt')}</span><span class="body">${escapeHtml(JSON.stringify(Object.fromEntries(Object.entries(e).filter(([k])=>!['ts','type'].includes(k)))))}</span></div>`
  ).join('');
  tickerText.textContent = `${(ev.type||'evt').toUpperCase()} · ${escapeHtml(JSON.stringify(Object.fromEntries(Object.entries(ev).filter(([k])=>!['ts','type'].includes(k))))).slice(0,140)}`;
}

async function loadInitialFeed(){
  const r = await fetch('/api/build-log?n=40').then(x=>x.json());
  (r.events||[]).reverse().forEach(addEvent);
}

function startStream(){
  const es = new EventSource('/api/stream');
  es.addEventListener('build', e => {
    try {
      const ev = JSON.parse(e.data);
      addEvent(ev);
      document.dispatchEvent(new CustomEvent('abrams:build-event', { detail: ev }));
    } catch {}
  });
  es.onerror = () => { setTimeout(startStream, 3000); es.close(); };
}

async function manualTick(){
  await fetch('/api/tick',{method:'POST'});
  tickerText.textContent = 'YOLO tick requested…';
}

async function submitLead(e){
  e.preventDefault();
  const fd = new FormData(e.target);
  const body = Object.fromEntries(fd.entries());
  const r = await fetch('/api/lead',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}).then(x=>x.json());
  document.getElementById('lead-result').textContent = r.ok ? '✓ Lead recorded. You will hear from Abrams soon.' : ('✗ '+(r.error||'failed'));
  if (r.ok) e.target.reset();
}

function openProject(slug){ window.location.href = '/#project-' + slug; /* future: project detail modal */ }
function openIdea(slug){ window.location.href = '/ideas#' + slug; }

// hydrate sort + density from localStorage BEFORE first grid load (Steve standing rule)
const savedSort = localStorage.getItem('abrams.sort');
if (savedSort) {
  const sortEl = document.getElementById('sort');
  if ([...sortEl.options].some(o => o.value === savedSort)) sortEl.value = savedSort;
}
const savedDensity = localStorage.getItem('abrams.density');
if (savedDensity) { document.getElementById('density').value = savedDensity; document.documentElement.style.setProperty('--cols', savedDensity); }

document.getElementById('sort').addEventListener('change', e => { localStorage.setItem('abrams.sort', e.target.value); loadProjects(); });
document.getElementById('density').addEventListener('input', e => { document.documentElement.style.setProperty('--cols', e.target.value); localStorage.setItem('abrams.density', e.target.value); });

async function loadLeaderboard() {
  const r = await fetch('/api/gamify/leaderboard').then(x=>x.json()).catch(()=>({leaderboard:[]}));
  const board = r.leaderboard || [];
  const me = (window.Gamify && Gamify.state()) || null;
  document.getElementById('leaderboard-list').innerHTML = `
    <div style="display:grid;grid-template-columns:50px 1fr 90px 90px 90px 200px;gap:12px;padding:10px 0;border-bottom:1px solid var(--hairline);color:var(--fg-3);font-size:10px;letter-spacing:.2em;text-transform:uppercase;">
      <span>#</span><span>Player</span><span>XP</span><span>LVL</span><span>Badges</span><span>Last seen</span>
    </div>
    ${board.slice(0,15).map((p,i) => {
      const isMe = me && p.player === me.player;
      return `<div style="display:grid;grid-template-columns:50px 1fr 90px 90px 90px 200px;gap:12px;padding:10px 0;border-bottom:1px solid var(--hairline);color:${isMe?'var(--accent)':'var(--fg)'};">
        <span style="color:var(--fg-3);">${(i+1).toString().padStart(2,'0')}</span>
        <span>${escapeHtml(p.player)}${isMe?' (you)':''}</span>
        <span>${p.total_xp}</span>
        <span>${p.level}</span>
        <span>${p.badge_count}</span>
        <span style="color:var(--fg-3);font-size:11px;">${new Date(p.last_seen).toLocaleTimeString()}</span>
      </div>`;
    }).join('') || '<div style="color:var(--fg-3);padding:24px;">no players yet — be the first</div>'}
    <div style="margin-top:14px;color:var(--fg-3);font-size:11px;">Total events: ${r.total_events||0} · refreshes every 30s</div>
  `;
}

loadProjects(); loadIdeas(); loadInitialFeed(); startStream(); loadLeaderboard(); loadCounters(); loadTermPreview();
setInterval(()=>{loadProjects();loadIdeas();loadLeaderboard();loadCounters();loadTermPreview();}, 30_000);
</script>
</body>
</html>