← 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>