← back to Agent Abrams Kanban

index.html

428 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Abrams — Task Pipeline</title>
<style>
:root{
  --bg:#07070c;
  --panel:rgba(18,18,28,0.55);
  --card:rgba(32,33,46,0.92);
  --ink:#e7e9f3;
  --muted:#7d82a0;
  --queued:#ff2e88;
  --running:#00e5ff;
  --done:#39ff8b;
  --shipped:#ffb020;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  background:
    radial-gradient(1200px 700px at 15% -10%, rgba(255,46,136,0.10), transparent 60%),
    radial-gradient(1000px 600px at 85% 110%, rgba(0,229,255,0.10), transparent 60%),
    var(--bg);
  color:var(--ink);
  font-family:'Segoe UI',system-ui,-apple-system,Tahoma,sans-serif;
  overflow:hidden;
  height:100vh;
}
/* subtle animated grid */
body::before{
  content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
  background-image:linear-gradient(rgba(255,255,255,0.028) 1px,transparent 1px),
                   linear-gradient(90deg,rgba(255,255,255,0.028) 1px,transparent 1px);
  background-size:44px 44px;
  mask-image:radial-gradient(circle at 50% 40%, #000 40%, transparent 85%);
  animation:drift 24s linear infinite;
}
@keyframes drift{from{background-position:0 0,0 0}to{background-position:44px 44px,44px 44px}}

.app{position:relative;z-index:1;height:100vh;display:flex;flex-direction:column;padding:16px 20px 20px}

.topbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}
.brand{display:flex;align-items:center;gap:12px}
.brand .pulse{
  width:12px;height:12px;border-radius:50%;background:var(--running);
  box-shadow:0 0 14px var(--running),0 0 4px #fff inset;animation:beat 1.6s ease-in-out infinite;
}
@keyframes beat{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.35);opacity:.65}}
.brand h1{font-size:1.02rem;font-weight:700;letter-spacing:2px;text-transform:uppercase}
.brand span{color:var(--muted);font-weight:400}
.stats{display:flex;gap:18px;font-size:.72rem;color:var(--muted);letter-spacing:1px;text-transform:uppercase}
.stats b{color:var(--ink);font-variant-numeric:tabular-nums}

.controls{display:flex;gap:8px;align-items:center}
.controls button{
  font:inherit;font-size:.68rem;letter-spacing:1px;text-transform:uppercase;font-weight:700;
  color:var(--muted);background:rgba(255,255,255,0.05);
  border:1px solid rgba(255,255,255,0.12);border-radius:8px;
  padding:6px 12px;cursor:pointer;transition:all .18s;
}
.controls button:hover{color:var(--ink);border-color:rgba(255,255,255,0.28);background:rgba(255,255,255,0.09)}
.controls button.active{color:#06060b;background:var(--running);border-color:var(--running);box-shadow:0 0 12px rgba(0,229,255,.4)}
.controls button.paused{color:#06060b;background:var(--shipped);border-color:var(--shipped);box-shadow:0 0 12px rgba(255,176,32,.4)}

.board{
  flex:1;min-height:0;
  display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
}
.column{
  display:flex;flex-direction:column;min-height:0;
  background:var(--panel);
  border:1px solid rgba(255,255,255,0.08);
  border-radius:14px;
  backdrop-filter:blur(10px);
  box-shadow:0 10px 40px rgba(0,0,0,0.45);
  overflow:hidden;
}
.column-header{
  display:flex;justify-content:space-between;align-items:center;
  padding:16px 16px 13px;
  border-bottom:2px solid;
  font-weight:700;text-transform:uppercase;letter-spacing:2px;font-size:.78rem;
}
.count{
  min-width:26px;text-align:center;
  background:rgba(255,255,255,0.08);
  padding:3px 10px;border-radius:20px;font-size:.78rem;font-weight:700;
  font-variant-numeric:tabular-nums;transition:transform .25s;
}
.count.bump{transform:scale(1.35)}
.column-body{
  flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;
  padding:12px;display:flex;flex-direction:column;gap:10px;
}
.column-body::-webkit-scrollbar{width:7px}
.column-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.12);border-radius:4px}

/* per-column neon accents */
.col-queued{border-color:rgba(255,46,136,0.28)}
.col-queued .column-header{color:var(--queued);border-color:var(--queued);text-shadow:0 0 12px rgba(255,46,136,.55)}
.col-running{border-color:rgba(0,229,255,0.28)}
.col-running .column-header{color:var(--running);border-color:var(--running);text-shadow:0 0 12px rgba(0,229,255,.55)}
.col-done{border-color:rgba(57,255,139,0.28)}
.col-done .column-header{color:var(--done);border-color:var(--done);text-shadow:0 0 12px rgba(57,255,139,.55)}
.col-shipped{border-color:rgba(255,176,32,0.28)}
.col-shipped .column-header{color:var(--shipped);border-color:var(--shipped);text-shadow:0 0 12px rgba(255,176,32,.55)}

.card{
  background:var(--card);
  border:1px solid rgba(255,255,255,0.08);
  border-left:3px solid var(--muted);
  border-radius:10px;padding:11px 12px;
  box-shadow:0 4px 10px rgba(0,0,0,0.35);
  will-change:transform;cursor:pointer;
}
.card.enter{animation:pop .45s cubic-bezier(.2,.9,.25,1.2)}
@keyframes pop{from{opacity:0;transform:translateY(-8px) scale(.96)}to{opacity:1;transform:none}}
.card .row{display:flex;align-items:center;gap:9px}
.avatar{
  position:relative;width:26px;height:26px;border-radius:50%;flex-shrink:0;
  display:grid;place-items:center;font-size:.62rem;font-weight:800;color:#06060b;
  box-shadow:0 0 10px currentColor;
}
.title{
  font-size:.82rem;font-weight:600;color:#fff;flex:1;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.meta{display:flex;align-items:center;justify-content:space-between;margin-top:9px;gap:8px}
.agent{font-size:.66rem;color:var(--muted);letter-spacing:.4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.timer{
  font-size:.72rem;color:var(--muted);
  font-family:'SF Mono','Courier New',monospace;letter-spacing:.5px;font-variant-numeric:tabular-nums;
}
/* progress bar (running only) */
.bar{height:3px;margin-top:9px;border-radius:3px;background:rgba(255,255,255,0.08);overflow:hidden;display:none}
.bar>i{display:block;height:100%;width:0;border-radius:3px;background:var(--running);box-shadow:0 0 8px var(--running);transition:width .5s linear}

.card.s-queued{border-left-color:var(--queued)}
.card.s-running{border-left-color:var(--running);box-shadow:0 4px 16px rgba(0,229,255,0.14)}
.card.s-running .bar{display:block}
.card.s-running .avatar{animation:beat 1.4s ease-in-out infinite}
.card.s-done{border-left-color:var(--done)}
.card.s-shipped{border-left-color:var(--shipped);opacity:.82}
.card.s-shipped .title::after{content:" ✓";color:var(--shipped)}

@media (prefers-reduced-motion:reduce){
  *{animation:none!important;transition:none!important}
}
</style>
</head>
<body>
<div class="app">
  <div class="topbar">
    <div class="brand">
      <div class="pulse"></div>
      <h1>Agent Abrams <span>· Task Pipeline</span></h1>
    </div>
    <div class="stats">
      <div>Uptime <b id="uptime">00:00</b></div>
      <div>Shipped <b id="shipCount">0</b></div>
      <div>Throughput <b id="tput">0.0</b>/min</div>
    </div>
    <div class="controls">
      <button id="btnSpawn" title="Queue a new task">+ Task</button>
      <button id="btnSpeed" title="Cycle simulation speed">1×</button>
      <button id="btnPause" title="Pause / resume the pipeline">⏸ Pause</button>
    </div>
  </div>

  <div class="board" id="board">
    <div class="column col-queued"  data-status="queued">
      <div class="column-header"><span>Queued</span><span class="count" id="count-queued">0</span></div>
      <div class="column-body" id="body-queued"></div>
    </div>
    <div class="column col-running" data-status="running">
      <div class="column-header"><span>Running</span><span class="count" id="count-running">0</span></div>
      <div class="column-body" id="body-running"></div>
    </div>
    <div class="column col-done"    data-status="done">
      <div class="column-header"><span>Done</span><span class="count" id="count-done">0</span></div>
      <div class="column-body" id="body-done"></div>
    </div>
    <div class="column col-shipped" data-status="shipped">
      <div class="column-header"><span>Shipped</span><span class="count" id="count-shipped">0</span></div>
      <div class="column-body" id="body-shipped"></div>
    </div>
  </div>
</div>

<script>
(function(){
  "use strict";
  const STAGES = ['queued','running','done','shipped'];
  const bodies = {
    queued:  document.getElementById('body-queued'),
    running: document.getElementById('body-running'),
    done:    document.getElementById('body-done'),
    shipped: document.getElementById('body-shipped'),
  };
  const counts = {
    queued:  document.getElementById('count-queued'),
    running: document.getElementById('count-running'),
    done:    document.getElementById('count-done'),
    shipped: document.getElementById('count-shipped'),
  };

  const TASK_NAMES = [
    "Neural Training","Data Pipeline","Model Compile","API Gateway","Vector Index",
    "Auth Service","Cache Layer","Log Analysis","Image Process","Text Embed",
    "Schema Migrate","Feature Flags","Rate Limiter","Webhook Sync","Token Rotate",
    "Batch Reindex","Fraud Scan","Report Gen"
  ];
  const AGENTS = [
    {n:"Orion",  c:"#ff2e88"},{n:"Vega",   c:"#00e5ff"},{n:"Nova",  c:"#39ff8b"},
    {n:"Atlas",  c:"#ffb020"},{n:"Lyra",   c:"#bd6bff"},{n:"Rigel", c:"#22d3ee"},
    {n:"Cassia", c:"#ff5c5c"},{n:"Draco",  c:"#7cff5c"},{n:"Mira",  c:"#ff8ad4"},
    {n:"Phoenix",c:"#ffd23f"}
  ];
  // stage dwell windows in ms [min,max]
  const DWELL = { queued:[2500,6500], running:[4000,9000], done:[2000,5000], shipped:[5000,9000] };

  const rint = (a,b)=>a+Math.floor(Math.random()*(b-a+1));
  const pick = a=>a[Math.floor(Math.random()*a.length)];

  // --- virtual clock: freezes on pause so timers + throughput stay honest ---
  let paused = false, speed = 1, pausedAccum = 0, pauseStart = 0;
  const now = ()=>performance.now() - pausedAccum - (paused ? performance.now()-pauseStart : 0);
  // dwell scaled by sim speed
  const dwell = stage => rint(...DWELL[stage]) / speed;

  let uid = 0, shippedTotal = 0;
  const tasks = [];
  const startV = now();

  function makeTask(seedStage){
    const agent = pick(AGENTS);
    const stage = seedStage || 'queued';
    const t0 = now();
    const t = {
      id: ++uid,
      title: pick(TASK_NAMES)+"-"+rint(100,999),
      agent,
      status: stage,
      createdAt: t0,
      nextAt: t0 + dwell(stage),
      el: null,
    };
    t.el = buildEl(t);
    tasks.push(t);
    bodies[t.status].appendChild(t.el);
    t.el.classList.add('enter');
    t.el.addEventListener('animationend',()=>t.el.classList.remove('enter'),{once:true});
    t.el.addEventListener('click',()=>{ advance(t); refreshCounts(); });
    return t;
  }

  function buildEl(t){
    const el = document.createElement('div');
    el.className = 'card s-'+t.status;
    const initials = t.agent.n.slice(0,2).toUpperCase();
    el.innerHTML =
      '<div class="row">'+
        '<div class="avatar" style="background:'+t.agent.c+';color:'+t.agent.c+'">'+
          '<span style="color:#06060b">'+initials+'</span>'+
        '</div>'+
        '<div class="title">'+t.title+'</div>'+
      '</div>'+
      '<div class="bar"><i></i></div>'+
      '<div class="meta">'+
        '<span class="agent">◈ '+t.agent.n+'</span>'+
        '<span class="timer">00:00</span>'+
      '</div>';
    return el;
  }

  function fmt(ms){
    const s=Math.floor(ms/1000);
    const m=Math.floor(s/60), ss=s%60;
    if(m>=60){const h=Math.floor(m/60);return String(h).padStart(2,'0')+':'+String(m%60).padStart(2,'0')+':'+String(ss).padStart(2,'0');}
    return String(m).padStart(2,'0')+':'+String(ss).padStart(2,'0');
  }

  // ---- FLIP: smooth slide when a card changes column ----
  function advance(t){
    const i = STAGES.indexOf(t.status);
    if(i < STAGES.length-1){
      moveWithFlip(t, STAGES[i+1]);
    } else {
      // shipped -> recycle back to queued (keeps ~constant board population)
      shippedTotal++;
      t.title = pick(TASK_NAMES)+"-"+rint(100,999);
      t.agent = pick(AGENTS);
      t.createdAt = now();
      const initials = t.agent.n.slice(0,2).toUpperCase();
      const av = t.el.querySelector('.avatar');
      av.style.background = t.agent.c;
      av.style.color = t.agent.c;
      av.querySelector('span').textContent = initials;
      t.el.querySelector('.title').textContent = t.title;
      t.el.querySelector('.agent').textContent = '◈ '+t.agent.n;
      moveWithFlip(t, 'queued');
    }
    // status was set inside moveWithFlip; schedule its next hop
    t.nextAt = now() + dwell(t.status);
  }

  function refreshCounts(){
    STAGES.forEach(s=>{
      const n = tasks.filter(t=>t.status===s).length;
      if(counts[s].textContent !== String(n)) counts[s].textContent = n;
    });
  }

  function moveWithFlip(t, toStage){
    // FIRST: record all card rects
    const first = new Map();
    tasks.forEach(o=>first.set(o.id,o.el.getBoundingClientRect()));

    // MOVE in the DOM
    t.status = toStage;
    t.el.className = 'card s-'+toStage;
    bodies[toStage].appendChild(t.el);
    const bar = t.el.querySelector('.bar>i'); if(bar) bar.style.width='0';

    // LAST + INVERT + PLAY for every affected card
    tasks.forEach(o=>{
      const f = first.get(o.id); if(!f) return;
      const l = o.el.getBoundingClientRect();
      const dx = f.left - l.left, dy = f.top - l.top;
      if(dx || dy){
        o.el.style.transition='none';
        o.el.style.transform='translate('+dx+'px,'+dy+'px)';
        requestAnimationFrame(()=>{
          o.el.style.transition='transform .6s cubic-bezier(.4,0,.2,1)';
          o.el.style.transform='';
        });
      }
    });

    bumpCount(toStage);
  }

  function bumpCount(stage){
    const c = counts[stage];
    c.classList.add('bump');
    setTimeout(()=>c.classList.remove('bump'),260);
  }

  // ---- controls / cached stat elements ----
  const btnPause  = document.getElementById('btnPause');
  const btnSpeed  = document.getElementById('btnSpeed');
  const btnSpawn  = document.getElementById('btnSpawn');
  const elUptime  = document.getElementById('uptime');
  const elShipped = document.getElementById('shipCount');
  const elTput    = document.getElementById('tput');
  const SPEEDS = [1,2,4];

  // ---- ticks ----
  function tickAdvance(){
    if(paused) return;
    const t0 = now();
    // advance at most a couple per tick so slides don't collide visually
    let moved = 0;
    for(const t of tasks){
      if(t0 >= t.nextAt && moved < 2){
        advance(t); moved++;
      }
    }
    refreshCounts();
  }

  function tickSecond(){
    const t0 = now();
    for(const t of tasks){
      const tm = t.el.querySelector('.timer');
      if(tm) tm.textContent = fmt(t0 - t.createdAt);
      if(t.status==='running'){
        const bar = t.el.querySelector('.bar>i');
        if(bar){
          // progress = elapsed since entering running / its dwell window (approx via nextAt)
          const total = DWELL.running[1]/speed;
          const remain = Math.max(0, t.nextAt - t0);
          const pct = Math.min(100, Math.max(4, 100*(1 - remain/total)));
          bar.style.width = pct+'%';
        }
      }
    }
    // header stats (virtual clock => frozen while paused)
    const up = now() - startV;
    elUptime.textContent  = fmt(up);
    elShipped.textContent = shippedTotal;
    const mins = up/60000;
    elTput.textContent = mins>0 ? (shippedTotal/mins).toFixed(1) : '0.0';
  }

  btnPause.addEventListener('click',()=>{
    paused = !paused;
    if(paused){ pauseStart = performance.now(); }
    else { pausedAccum += performance.now()-pauseStart; }
    btnPause.textContent = paused ? '▶ Resume' : '⏸ Pause';
    btnPause.classList.toggle('paused', paused);
  });

  btnSpeed.addEventListener('click',()=>{
    const i = (SPEEDS.indexOf(speed)+1) % SPEEDS.length;
    // rescale every pending hop so the change takes effect immediately
    const t0 = now(), prev = speed;
    speed = SPEEDS[i];
    tasks.forEach(t=>{ t.nextAt = t0 + (t.nextAt - t0) * (prev/speed); });
    btnSpeed.textContent = speed+'×';
    btnSpeed.classList.toggle('active', speed!==1);
  });

  btnSpawn.addEventListener('click',()=>{ makeTask('queued'); refreshCounts(); });

  // ---- seed ~10 cards spread across the pipeline ----
  const seed = ['queued','queued','queued','running','running','running','done','done','shipped','shipped'];
  seed.forEach((s,i)=>{ setTimeout(()=>{ makeTask(s); refreshCounts(); }, i*90); });

  setInterval(tickAdvance, 700);
  setInterval(tickSecond, 500);
})();
</script>
</body>
</html>