{% extends "base.html" %}
{% block title %}Tasks — CoderAI{% endblock %}

{% block content %}
<div class="page-header">
  <div>
    <h1>Tasks</h1>
    <p>Live view of generations and LoRA training. Cancel, interrupt, or restart a job.</p>
  </div>
  <div class="header-actions">
    <span id="queue-summary" class="dim small"></span>
  </div>
</div>

<div id="thermal-banner" style="display:none;margin:0 0 1rem;padding:.6rem .85rem;border-radius:8px;
     background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.4);color:#f59e0b;font-size:13px">
  <span style="font-weight:600">❄ Thermal cooldown</span>
  <span id="thermal-banner-msg" class="mono"></span>
  — running work is paused until the hardware cools.
</div>

<div id="throttle-banner" style="display:none;margin:0 0 1rem;padding:.6rem .85rem;border-radius:8px;
     background:rgba(56,189,248,.10);border:1px solid rgba(56,189,248,.4);color:#38bdf8;font-size:13px">
  <span style="font-weight:600">🐢 CPU soft-throttle</span>
  <span id="throttle-banner-msg" class="mono"></span>
  — generation is being gently slowed to keep the CPU below its pause limit.
</div>

<!-- Live hardware telemetry -->
<div id="sys-stats" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
     gap:.75rem;margin:0 0 1.25rem">
  <div class="sys-tile" id="tile-cpu"></div>
  <div class="sys-tile" id="tile-ram"></div>
  <!-- Per-card GPU tiles (util + VRAM) injected here when cards are detected. -->
  <div id="tile-cards" style="display:contents"></div>
  <!-- Fallback single tiles when per-card stats are unavailable. -->
  <div class="sys-tile" id="tile-gpu"></div>
  <div class="sys-tile" id="tile-vram"></div>
</div>

<!-- Engines (only shown in front/multi-engine mode) -->
<div id="engines-card" style="display:none;margin:0 0 1.25rem">
  <div style="display:flex;align-items:baseline;gap:.5rem;margin-bottom:.5rem">
    <h2 style="font-size:14px;margin:0">Engines</h2>
    <span class="dim small">restart a stuck engine — the supervisor respawns it</span>
  </div>
  <div id="engines-body" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:.6rem"></div>
</div>

<style>
.sys-tile{border:1px solid var(--border,#2a2a2a);border-radius:10px;padding:.7rem .85rem;
  background:var(--card-bg,rgba(255,255,255,.02))}
.sys-tile .sys-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:.45rem}
.sys-tile .sys-name{font-size:12px;font-weight:600;letter-spacing:.03em;text-transform:uppercase;color:var(--text-muted,#9aa0a6)}
.sys-tile .sys-val{font-size:13px;font-weight:600}
.sys-tile .sys-sub{font-size:11px;color:var(--text-muted,#9aa0a6);margin-top:.35rem;display:flex;justify-content:space-between}
.sys-bar{height:8px;border-radius:5px;background:rgba(127,127,127,.18);overflow:hidden}
.sys-bar > span{display:block;height:100%;border-radius:5px;transition:width .4s ease,background .4s ease}
.sys-ok > span{background:#22c55e}.sys-warn > span{background:#f59e0b}.sys-hot > span{background:#ef4444}
.sys-temp-ok{color:#22c55e}.sys-temp-warn{color:#f59e0b}.sys-temp-hot{color:#ef4444}
</style>

<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th>Type</th><th>Name / Model</th><th>Status</th>
        <th style="width:220px">Progress</th><th>Started</th><th style="text-align:right">Actions</th>
      </tr>
    </thead>
    <tbody id="tasks-body">
      <tr class="empty-row"><td colspan="6">No tasks yet</td></tr>
    </tbody>
  </table>
</div>

<div class="table-wrap" id="history-wrap" style="display:none;margin-top:1.25rem">
  <h2 style="font-size:14px;margin:0 0 .5rem">Recent tasks <span class="dim small">(last 10)</span></h2>
  <table>
    <thead>
      <tr>
        <th>Type</th><th>Name / Model</th><th>Result</th>
        <th style="width:220px">Throughput</th><th>Finished</th><th style="text-align:right"></th>
      </tr>
    </thead>
    <tbody id="history-body"></tbody>
  </table>
</div>
{% endblock %}

{% block scripts %}
<script>
function esc(s) { return String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }

function fmtTime(s) {
  if (!s) return '';
  try {
    // started_at is unix seconds (float) from the server.
    const d = new Date(s * 1000);
    return d.toLocaleTimeString(undefined, {hour:'2-digit', minute:'2-digit', second:'2-digit'});
  } catch { return ''; }
}

const KIND_LABEL = {training:'Training', image:'Image', video:'Video', upscale:'Upscale', interpolate:'Interpolate', audio:'Audio', text:'Text', tts:'Speech (TTS)', transcription:'Transcription', embedding:'Embedding', spatial:'3D / Spatial', pipeline:'Pipeline', request:'Request', loading:'Loading', download:'Download'};
const STATUS_BADGE = {
  running:'badge-admin', queued:'badge-user', done:'badge-ok', error:'badge-err',
  cancelled:'badge-user', interrupted:'badge-warn'
};

// Format a task's throughput with the right unit. Text → tok/s; generation →
// it/s, or s/it when each iteration takes longer than a second (more readable).
function fmtRate(t) {
  const r = Number(t.rate) || 0;
  if (r <= 0) return '';
  if (t.kind === 'text') return `${r} tok/s`;
  return r >= 1 ? `${r} it/s` : `${(1 / r).toFixed(1)} s/it`;
}

function progressBar(t) {
  // Downloads report a 0-100 percent; render a percentage bar (the filename /
  // rate / ETA detail is shown in the status cell, so it isn't repeated here).
  if (t.kind === 'download') {
    const pct = Math.max(0, Math.min(100, Math.round(t.percent || t.step || 0)));
    return `<div class="progress"><div class="progress-fill" style="width:${pct}%"></div></div>
            <span class="dim small">${pct}%</span>`;
  }
  const total = t.total || 0, step = t.step || 0;
  // Live throughput, shown while running: tokens/s for text, iterations/s (or
  // seconds/iteration when slow) for generation tasks (image/video/audio/…).
  const rate = (t.rate && t.status === 'running')
    ? ` <span class="dim small">· ${fmtRate(t)}</span>` : '';
  if (!total) {
    if (t.status === 'running') {
      const tok = step ? `${step} tok` : 'working…';
      return `<span class="dim small">${tok}</span>${rate}`;
    }
    return '<span class="dim small">—</span>';
  }
  const pct = Math.max(0, Math.min(100, Math.round(step / total * 100)));
  return `<div class="progress"><div class="progress-fill" style="width:${pct}%"></div></div>
          <span class="dim small">${step}/${total} (${pct}%)</span>${rate}`;
}

function actions(t) {
  const btns = [];
  if (t.paused) {
    btns.push(`<button class="btn btn-primary btn-sm" onclick="taskAction('${esc(t.id)}','resume')">Resume</button>`);
  } else if (t.pausable) {
    btns.push(`<button class="btn btn-ghost btn-sm" onclick="taskAction('${esc(t.id)}','pause')">Pause</button>`);
  }
  if (t.cancellable) {
    const label = t.status === 'running' ? 'Interrupt' : 'Cancel';
    const act = t.status === 'running' ? 'interrupt' : 'cancel';
    btns.push(`<button class="btn btn-danger btn-sm" onclick="taskAction('${esc(t.id)}','${act}')">${label}</button>`);
  }
  if (t.restartable) {
    btns.push(`<button class="btn btn-ghost btn-sm" onclick="taskAction('${esc(t.id)}','restart')">Restart</button>`);
  }
  if (!t.active) {
    btns.push(`<button class="btn btn-ghost btn-sm" onclick="removeTask('${esc(t.id)}')">Remove</button>`);
  }
  return btns.join(' ') || '<span class="dim small">—</span>';
}

function fmtDur(start, end) {
  if (!start || !end || end < start) return '';
  const s = Math.round(end - start);
  if (s < 60) return `${s}s`;
  const m = Math.floor(s / 60), ss = s % 60;
  if (m < 60) return `${m}m ${ss}s`;
  return `${Math.floor(m / 60)}h ${m % 60}m`;
}

// One active-task row (main table).
function taskRow(t) {
  const badge = STATUS_BADGE[t.status] || 'badge-dim';
  const title = t.title || '(untitled)';
  let statusCell;
  if (t.cooling) {
    statusCell = `<span class="badge badge-warn">❄ Cooling down</span>`
      + `<div class="dim small">${esc(t.cooling_message || 'paused for thermal cooldown')}</div>`;
  } else if (t.throttling) {
    statusCell = `<span class="badge ${badge}">${esc(t.status)}</span>`
      + ` <span class="badge badge-user">🐢 throttling</span>`
      + `<div class="dim small">${esc(t.throttle_message || 'CPU soft-throttle')}</div>`;
  } else if (t.paused) {
    statusCell = `<span class="badge badge-warn">⏸ Paused</span>`
      + `<div class="dim small">suspended — click Resume to continue</div>`;
  } else {
    statusCell = `<span class="badge ${badge}">${esc(t.status)}</span>`
      + (t.message ? `<div class="dim small">${esc(t.message)}</div>` : '');
  }
  return `<tr>
    <td><span class="badge badge-user">${esc(KIND_LABEL[t.kind] || t.kind)}</span></td>
    <td><div class="td-name">${esc(title)}${t.engine?` <span class="badge badge-user" style="font-size:9px;padding:.05rem .3rem;vertical-align:middle" title="Running on engine">${esc(t.engine)}</span>`:''}${t.device?` <span class="badge" style="font-size:9px;padding:.05rem .3rem;vertical-align:middle;background:${/offload/i.test(t.device)?'#7a5a1e':(/^GPU/i.test(t.device)?'#1e5a3a':'#444')};color:#fff" title="Compute device${/offload/i.test(t.device)?' — model partially offloaded to CPU RAM':''}">${esc(t.device)}</span>`:''}</div><div class="dim small mono">${esc(t.model || '')}</div></td>
    <td>${statusCell}</td>
    <td>${progressBar(t)}</td>
    <td class="dim small">${fmtTime(t.started_at)}</td>
    <td style="text-align:right">${actions(t)}</td>
  </tr>`;
}

// One finished-task row (history table): result + average throughput + duration.
function historyRow(t) {
  const badge = STATUS_BADGE[t.status] || 'badge-dim';
  const title = t.title || '(untitled)';
  const dur = fmtDur(t.started_at, t.ended_at);
  const rate = fmtRate(t);
  const result = `<span class="badge ${badge}">${esc(t.status)}</span>`
    + (dur ? ` <span class="dim small">in ${dur}</span>` : '')
    + (t.message && t.status !== 'done' ? `<div class="dim small">${esc(t.message)}</div>` : '');
  let thru = '<span class="dim small">—</span>';
  if (rate) {
    const cnt = t.total ? ` <span class="dim small">· ${t.total} ${t.kind === 'text' ? 'tok' : 'it'}</span>` : '';
    thru = `<span class="dim small">${rate}</span>${cnt}`;
  } else if (t.step) {
    thru = `<span class="dim small">${t.step} ${t.kind === 'text' ? 'tok' : 'it'}</span>`;
  }
  return `<tr>
    <td><span class="badge badge-user">${esc(KIND_LABEL[t.kind] || t.kind)}</span></td>
    <td><div class="td-name">${esc(title)}${t.engine?` <span class="badge badge-user" style="font-size:9px;padding:.05rem .3rem;vertical-align:middle" title="Ran on engine">${esc(t.engine)}</span>`:''}</div><div class="dim small mono">${esc(t.model || '')}</div></td>
    <td>${result}</td>
    <td>${thru}</td>
    <td class="dim small">${fmtTime(t.ended_at)}</td>
    <td style="text-align:right"><button class="btn btn-ghost btn-sm" onclick="removeTask('${esc(t.id)}')">Remove</button></td>
  </tr>`;
}

// ---- Live hardware telemetry ----
// `frac` is a 0-100 fraction OF CAPACITY (the bar fill + colour are driven by it).
function _utilClass(frac){ return frac == null ? 'sys-ok' : (frac >= 90 ? 'sys-hot' : frac >= 70 ? 'sys-warn' : 'sys-ok'); }
function _tempClass(t){ return t == null ? '' : (t >= 90 ? 'sys-temp-hot' : t >= 80 ? 'sys-temp-warn' : 'sys-temp-ok'); }
function _bar(frac){
  const p = frac == null ? 0 : Math.max(0, Math.min(100, frac));
  return `<div class="sys-bar ${_utilClass(frac)}"><span style="width:${p}%"></span></div>`;
}
// `max` = full-scale value for the bar (default 100). The CPU tile passes
// cores*100 so the displayed % can run 0..cores*100 (100% per core) while the bar
// still fills 0..100% of total capacity.
function _utilTile(name, pct, temp, max){
  const full = max || 100;
  const frac = pct == null ? null : (pct / full * 100);
  const valTxt = pct == null ? 'n/a' : `${Math.round(pct)}%`;
  const subTxt = (max && max > 100) ? `utilization (max ${max}%)` : 'utilization';
  const tempTxt = temp == null ? '<span class="dim">temp n/a</span>'
    : `<span class="${_tempClass(temp)}">${Math.round(temp)}°C</span>`;
  return `<div class="sys-head"><span class="sys-name">${name}</span><span class="sys-val">${valTxt}</span></div>`
    + _bar(frac) + `<div class="sys-sub"><span>${subTxt}</span>${tempTxt}</div>`;
}
function _memTile(name, used, total, pct){
  const valTxt = (used == null || total == null) ? 'n/a' : `${used.toFixed(1)} / ${total.toFixed(1)} GB`;
  const p = pct != null ? pct : (used != null && total ? used/total*100 : null);
  return `<div class="sys-head"><span class="sys-name">${name}</span><span class="sys-val">${valTxt}</span></div>`
    + _bar(p) + `<div class="sys-sub"><span>${p == null ? '' : Math.round(p)+'% used'}</span><span></span></div>`;
}
// One tile per physical card showing both GPU utilization and VRAM (+ temp).
function _cardTile(c){
  const vColor = c.vendor==='nvidia' ? '#76b900'
               : c.vendor==='amd' ? '#ed1c24' : 'var(--text-3)';
  const memP = (c.mem_total ? (c.mem_used / c.mem_total * 100) : null);
  const temp = (c.temp!=null) ? ' · '+Math.round(c.temp)+'°C' : '';
  const util = (c.util!=null) ? Math.round(c.util)+'%' : '—';
  return `<div class="sys-tile">
    <div class="sys-head"><span class="sys-name" style="color:${vColor}">${esc(c.name)}</span>
      <span class="sys-val">${util}${temp}</span></div>
    ${_bar(c.util)}
    <div class="sys-sub"><span>VRAM ${c.mem_used!=null?c.mem_used.toFixed(1):'—'}/${c.mem_total!=null?c.mem_total.toFixed(0):'—'} GB</span>
      <span>${memP!=null?Math.round(memP)+'% used':''}</span></div>
    ${_bar(memP)}
  </div>`;
}

async function loadSystemStats(){
  try {
    const s = await fetch(ROOT_PATH + '/admin/api/system-stats').then(r => r.json());
    const cpu = s.cpu || {}, gpu = s.gpu || {}, ram = s.ram || {}, vram = s.vram || {};
    document.getElementById('tile-cpu').innerHTML = _utilTile('CPU', cpu.util, cpu.temp, (cpu.cores || 1) * 100);
    document.getElementById('tile-ram').innerHTML = _memTile('RAM', ram.used, ram.total, ram.percent);

    // Per-card GPU+VRAM tiles for every physical card; fall back to single tiles.
    let cards = [];
    try { cards = ((await fetch(ROOT_PATH + '/admin/api/gpu-stats').then(r => r.json())).cards) || []; } catch(e){}
    const cardsEl = document.getElementById('tile-cards');
    const gpuEl = document.getElementById('tile-gpu');
    const vramEl = document.getElementById('tile-vram');
    if (cards.length) {
      cardsEl.innerHTML = cards.map(_cardTile).join('');
      gpuEl.style.display = 'none'; vramEl.style.display = 'none';
    } else {
      cardsEl.innerHTML = '';
      gpuEl.style.display = ''; vramEl.style.display = '';
      gpuEl.innerHTML = _utilTile('GPU', gpu.util, gpu.temp);
      vramEl.innerHTML = _memTile('VRAM', vram.used, vram.total, vram.percent);
    }
  } catch(e){ /* keep last render on transient errors */ }
}

// Engines panel — only present in front/multi-engine mode (404 in single-process).
async function loadEngines(){
  let engines = null;
  try {
    const r = await fetch(ROOT_PATH + '/admin/api/engines');
    if (!r.ok) { document.getElementById('engines-card').style.display = 'none'; return; }
    engines = (await r.json()).engines || [];
  } catch(e) { document.getElementById('engines-card').style.display = 'none'; return; }
  const card = document.getElementById('engines-card');
  if (!engines.length) { card.style.display = 'none'; return; }
  card.style.display = '';
  document.getElementById('engines-body').innerHTML = engines.map(e => {
    const dot = e.healthy ? '#3fb950' : '#e5534b';
    const state = e.healthy ? 'healthy' : 'down / starting';
    const vram = e.vram ? `${(e.vram.used ?? 0).toFixed ? e.vram.used.toFixed(1) : e.vram.used}/${e.vram.total} GB` : '';
    const cool = e.cooling ? ` <span class="badge badge-warn" style="font-size:9px">❄ cooling</span>` : '';
    const prim = e.primary ? ` <span class="badge badge-user" style="font-size:9px">primary</span>` : '';
    const proc = e.processing ? ` <span class="badge badge-admin" style="font-size:9px" title="actively processing a request">● processing</span>` : '';
    const loaded = e.loaded_models||[];
    const models = loaded.length;
    // Tooltip: one line per model with its memory footprint — VRAM, plus the
    // host-RAM slice separately when the model is CPU-offloading.
    const info = e.loaded_info||{};   // canonical model id -> {vram_gb, ram_gb}
    const mtip = models ? esc(loaded.map(name => {
      const m = info[name];
      if (!m || (!m.vram_gb && !m.ram_gb)) return name;
      let s = `${name} — VRAM ${m.vram_gb} GB`;
      if (m.ram_gb) s += ` · RAM ${m.ram_gb} GB (offload)`;
      return s;
    }).join('\n')) : 'no models loaded';
    return `<div class="sys-tile">
      <div class="sys-head">
        <span class="sys-name">${esc(e.name)} <span class="dim" style="text-transform:none">(${esc(e.backend)})</span>${prim}${proc}${cool}</span>
        <span style="width:9px;height:9px;border-radius:50%;background:${dot};display:inline-block" title="${state}"></span>
      </div>
      <div class="sys-sub"><span>${esc(state)}${vram?' · '+esc(vram):''}</span><span title="${mtip}" style="cursor:${models?'help':'default'};${models?'text-decoration:underline dotted':''}">${models} model${models!==1?'s':''}</span></div>
      <div style="margin-top:.5rem;text-align:right">
        <button class="btn btn-ghost" style="font-size:11px;padding:.15rem .5rem;color:var(--error,#e55)"
                onclick="restartEngine(${e.id}, '${esc(e.name)}')" title="Kill and respawn this engine">↻ Restart</button>
      </div>
    </div>`;
  }).join('');
}

async function restartEngine(id, name){
  if (!confirm(`Restart engine "${name}"? In-flight requests on it will fail; the supervisor respawns it immediately.`)) return;
  try {
    const r = await fetch(ROOT_PATH + '/admin/api/engines/' + id + '/restart', {method:'POST'});
    if (!r.ok) { const e = await r.json().catch(()=>({})); showAlert(e.detail || 'Restart failed'); }
    setTimeout(loadEngines, 800);
  } catch(e) { showAlert(e.message); }
}

let _refreshing = false;
async function loadTasks() {
  if (_refreshing) return;
  _refreshing = true;
  try {
    const data = await fetch(ROOT_PATH + '/admin/api/tasks').then(r => r.json());
    const tasks = data.tasks || [];
    const q = data.queue || {};
    document.getElementById('queue-summary').textContent =
      `${q.active || 0} active · ${q.waiting || 0} waiting · max ${q.max_parallel_requests || 0} parallel`;

    const therm = data.thermal || {};
    const banner = document.getElementById('thermal-banner');
    // Multi-engine: name which engine(s) are cooling and on what (GPU vs CPU).
    const cooling = data.cooling_engines || [];
    if (cooling.length) {
      const parts = cooling.map(c => {
        const what = (c.gpu != null && c.cpu == null) ? `GPU ${Math.round(c.gpu)}°C`
                   : (c.cpu != null && c.gpu == null) ? `CPU ${Math.round(c.cpu)}°C`
                   : (c.message || 'cooling');
        return `${esc(c.engine)} (${esc(what)})`;
      });
      document.getElementById('thermal-banner-msg').textContent =
        ' Cooling down: ' + parts.join(', ');
      banner.style.display = '';
    } else if (therm.active) {
      document.getElementById('thermal-banner-msg').textContent = ' ' + (therm.message || '');
      banner.style.display = '';
    } else {
      banner.style.display = 'none';
    }

    const soft = data.soft_throttle || {};
    const sbanner = document.getElementById('throttle-banner');
    // Hide the soft-throttle banner during a hard cooldown (the pause supersedes it).
    if (soft.active && !therm.active) {
      document.getElementById('throttle-banner-msg').textContent = ' ' + (soft.message || '');
      sbanner.style.display = '';
    } else {
      sbanner.style.display = 'none';
    }

    // Split: active tasks in the main table, the most recent finished ones in a
    // history section (last 10) so completed generations stay visible briefly.
    const active = tasks.filter(t => t.active);
    const finished = tasks.filter(t => !t.active)
      .sort((a, b) => (b.ended_at || b.started_at || 0) - (a.ended_at || a.started_at || 0))
      .slice(0, 10);

    const tbody = document.getElementById('tasks-body');
    tbody.innerHTML = active.length
      ? active.map(taskRow).join('')
      : '<tr class="empty-row"><td colspan="6">No active tasks</td></tr>';

    const hwrap = document.getElementById('history-wrap');
    const hbody = document.getElementById('history-body');
    if (finished.length) {
      hwrap.style.display = '';
      hbody.innerHTML = finished.map(historyRow).join('');
    } else {
      hwrap.style.display = 'none';
    }
  } catch (e) {
    // transient fetch errors during a model swap are fine; keep last render.
  } finally {
    _refreshing = false;
  }
}

async function taskAction(id, action) {
  const verb = {cancel:'Cancel', interrupt:'Interrupt', restart:'Restart', pause:'Pause', resume:'Resume'}[action] || action;
  // Only confirm destructive actions; pause/resume/restart act immediately.
  if ((action === 'cancel' || action === 'interrupt') && !confirm(`${verb} this task?`)) return;
  try {
    const r = await fetch(ROOT_PATH + '/admin/api/tasks/' + encodeURIComponent(id) + '/' + action, {method:'POST'});
    if (!r.ok) {
      const e = await r.json().catch(() => ({}));
      showAlert(e.detail || (verb + ' failed'));
    }
  } catch (e) { showAlert(e.message); }
  loadTasks();
}

async function removeTask(id) {
  try {
    const r = await fetch(ROOT_PATH + '/admin/api/tasks/' + encodeURIComponent(id), {method:'DELETE'});
    if (!r.ok) {
      const e = await r.json().catch(() => ({}));
      showAlert(e.detail || 'Remove failed');
    }
  } catch (e) { showAlert(e.message); }
  loadTasks();
}

loadTasks();
loadSystemStats();
loadEngines();
setInterval(loadTasks, 1000);
setInterval(loadSystemStats, 1000);
setInterval(loadEngines, 5000);
</script>
{% endblock %}
