← back to Ai Workforce

public/app.js

141 lines

'use strict';
const $ = (s) => document.querySelector(s);
const LS = {
  get: (k, d) => { try { const v = localStorage.getItem('aiwf.' + k); return v === null ? d : v; } catch { return d; } },
  set: (k, v) => { try { localStorage.setItem('aiwf.' + k, v); } catch {} },
};
let SNAP = null;

// house rule: admin cards show created date AND time, local tz
function fmtWhen(ms) {
  if (!ms) return null;
  const d = new Date(ms);
  return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
}
function dur(ms) { // format an elapsed duration (ms) as compact d/h/m
  if (!ms || ms < 0) return '';
  const s = Math.floor(ms / 1000);
  if (s < 60) return s + 's';
  if (s < 3600) return Math.floor(s / 60) + 'm';
  if (s < 86400) return Math.floor(s / 3600) + 'h';
  return Math.floor(s / 86400) + 'd';
}
const esc = (s) => String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));

function ribbon(c, cost) {
  $('#ribbon').innerHTML = [
    ['total', c.total, 'Total staff'],
    ['working', c.working, 'Working now'],
    ['idle', (c.idle || 0) + (c.roster || 0), 'Idle / on-call'],
    ['err', (c.error || 0) + (c.stopped || 0), 'Needs attention'],
    ['cost', '$' + (cost.totalUSD || 0).toFixed(2), 'Cost today'],
  ].map(([cls, n, l]) => `<div class="stat ${cls}"><div class="n">${esc(n)}</div><div class="l">${esc(l)}</div></div>`).join('');
}

function offices(list) {
  $('#offices').innerHTML = list.map((m) => {
    if (!m.probed) {
      return `<div class="office dim"><h3>${esc(m.name)}</h3><div class="sub">${esc(m.label)}</div>
        <span class="badge">${esc(m.note || 'remote')}</span></div>`;
    }
    const pct = m.staff ? Math.round((m.working / m.staff) * 100) : 0;
    return `<div class="office"><h3>${esc(m.name)}</h3><div class="sub">${esc(m.label)}</div>
      <div class="row"><span>Staff on this box</span><b>${m.working}/${m.staff} working</b></div>
      <div class="bar"><i style="width:${pct}%"></i></div>
      <div class="row" style="margin-top:10px"><span>Cores</span><b>${esc(m.cores ?? '—')}</b></div>
      <div class="row"><span>Memory</span><b>${m.memGB ? m.memGB + ' GB' : '—'}</b></div>
      <div class="row"><span>Load (1m)</span><b>${m.load ? m.load.one.toFixed(2) + (m.loadPct != null ? ' · ' + m.loadPct + '%' : '') : '—'}</b></div>
    </div>`;
  }).join('');
}

function wins(w) {
  $('#wins-src').textContent = w.unavailable ? '(CNCP offline)' : '(' + w.today.length + ' today · ' + w.total + ' all-time)';
  if (!w.today.length) { $('#wins').innerHTML = `<div class="empty">${w.unavailable ? 'CNCP not reachable on :3333.' : 'No wins logged yet today.'}</div>`; return; }
  $('#wins').innerHTML = w.today.map((x) => `<div class="win">
    <div class="p">${esc(x.project || '')}</div>
    <div class="t">${esc(x.title || '')}</div>
    ${x.summary ? `<div class="m">${esc(x.summary).slice(0, 180)}</div>` : ''}
  </div>`).join('');
}

function payroll(cost) {
  const rows = (cost.byApp || []).map((r) => `<div class="prow"><span class="app">${esc(r.app)}</span><span class="usd">$${r.usd.toFixed(4)}</span></div>`).join('');
  $('#payroll').innerHTML = (rows || `<div class="empty">No paid-API spend today.</div>`) +
    `<div class="tot">$${(cost.totalUSD || 0).toFixed(4)} <span class="muted">· ${cost.calls || 0} calls</span></div>`;
}

function statusRank(s) { return { working: 0, error: 1, stopped: 2, idle: 3, roster: 4 }[s] ?? 9; }

function renderGrid() {
  if (!SNAP) return;
  const q = $('#search').value.trim().toLowerCase();
  const dept = $('#dept').value, st = $('#status').value, sort = $('#sort').value;
  let list = SNAP.employees.filter((e) =>
    (!dept || e.dept === dept) && (!st || e.status === st) &&
    (!q || (e.name + ' ' + e.role + ' ' + e.dept).toLowerCase().includes(q)));
  list.sort((a, b) => {
    if (sort === 'name') return a.name.localeCompare(b.name);
    if (sort === 'dept') return a.dept.localeCompare(b.dept) || a.name.localeCompare(b.name);
    if (sort === 'hired-new') return (b.hired || 0) - (a.hired || 0);
    if (sort === 'hired-old') return (a.hired || Infinity) - (b.hired || Infinity);
    return statusRank(a.status) - statusRank(b.status) || a.name.localeCompare(b.name);
  });
  $('#roster-count').textContent = list.length + ' shown';
  $('#grid').innerHTML = list.map((e) => {
    const m = e.metrics || {};
    const chips = [];
    if (m.cpu != null) chips.push('CPU ' + m.cpu + '%');
    if (m.memMB != null) chips.push(m.memMB + ' MB');
    if (m.restarts != null) chips.push('↻ ' + m.restarts);
    if (m.uptimeMs) chips.push('up ' + dur(m.uptimeMs));
    if (m.pid) chips.push('pid ' + m.pid);
    if (m.lastExit != null && m.lastExit !== 0) chips.push('exit ' + m.lastExit);
    if (e.machine && e.machine !== '—') chips.push('🖥 ' + e.machine);
    const when = fmtWhen(e.hired);
    const iso = e.hired ? new Date(e.hired).toISOString() : '';
    return `<div class="card">
      <div class="top">
        <div><div class="dept">${esc(e.dept)}</div><div class="nm">${esc(e.name)}</div></div>
        <span class="pill ${esc(e.status)}">${esc(e.status)}</span>
      </div>
      <div class="role">${esc(e.role)}</div>
      ${chips.length ? `<div class="chips">${chips.map((c) => `<span class="chip">${esc(c)}</span>`).join('')}</div>` : ''}
      <div class="when" title="${esc(iso)}">${when ? '🕓 hired <b>' + esc(when) + '</b>' : '🕓 hired —'}</div>
    </div>`;
  }).join('');
}

function fillDepts() {
  const depts = [...new Set(SNAP.employees.map((e) => e.dept))].sort();
  const cur = $('#dept').value;
  $('#dept').innerHTML = '<option value="">All departments</option>' +
    depts.map((d) => `<option ${d === cur ? 'selected' : ''} value="${esc(d)}">${esc(d)} (${SNAP.counts.byDept[d] || 0})</option>`).join('');
}

async function load() {
  $('#stamp').textContent = 'refreshing…';
  try {
    const r = await fetch('/api/snapshot', { cache: 'no-store' });
    SNAP = await r.json();
  } catch (e) { $('#stamp').textContent = 'snapshot failed'; return; }
  ribbon(SNAP.counts, SNAP.cost);
  offices(SNAP.machines);
  wins(SNAP.wins);
  payroll(SNAP.cost);
  fillDepts();
  renderGrid();
  $('#stamp').textContent = 'updated ' + new Date(SNAP.generatedAt).toLocaleTimeString();
}

// wire controls + restore persisted prefs
function applyDensity(v) { document.documentElement.style.setProperty('--cols', v); }
$('#density').value = LS.get('density', '4'); applyDensity($('#density').value);
$('#sort').value = LS.get('sort', 'status');
$('#density').addEventListener('input', (e) => { applyDensity(e.target.value); LS.set('density', e.target.value); });
$('#sort').addEventListener('change', (e) => { LS.set('sort', e.target.value); renderGrid(); });
['#search', '#dept', '#status'].forEach((s) => $(s).addEventListener('input', renderGrid));
$('#refresh').addEventListener('click', load);
load();
setInterval(load, 20000);