← back to Yolo Agent

dashboard/task-ledger.html

1301 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>YOLO Task Ledger</title>
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

  :root {
    --bg: #0a0a0a;
    --surface: #141414;
    --surface-2: #1a1a1a;
    --border: #222;
    --text: #e5e5e5;
    --text-muted: #666;
    --text-dim: #444;
    --green: #22c55e;
    --green-dim: #16a34a;
    --blue: #3b82f6;
    --amber: #f59e0b;
    --red: #ef4444;
    --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  }

  body { font-family: var(--font); background: var(--bg); color: var(--text); -webkit-font-smoothing: antialiased; min-height: 100vh; }

  .header {
    border-bottom: 1px solid var(--border); padding: 20px 32px;
    display: flex; align-items: center; justify-content: space-between;
    position: sticky; top: 0; background: var(--bg); z-index: 10;
  }
  .header h1 { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; }
  .header-stats { display: flex; gap: 24px; font-size: 13px; color: var(--text-muted); }
  .header-stats .val { color: var(--text); font-weight: 600; }
  .header-stats .green { color: var(--green); }
  .header-stats .red { color: var(--red); }

  .live-dot {
    width: 8px; height: 8px; background: var(--green); border-radius: 50%;
    display: inline-block; margin-right: 8px; animation: pulse-dot 2s ease infinite;
  }
  @keyframes pulse-dot {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(34,197,94,0.4); }
    50% { opacity: 0.7; box-shadow: 0 0 0 6px rgba(34,197,94,0); }
  }

  .container { max-width: 960px; margin: 0 auto; padding: 24px 32px 80px; }

  .section-label {
    font-size: 11px; font-weight: 600; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--text-dim); margin: 32px 0 12px; padding-left: 4px;
  }

  .task-list { display: flex; flex-direction: column; gap: 2px; }

  .task {
    display: grid; grid-template-columns: 40px 1fr auto;
    align-items: center; gap: 16px; padding: 14px 16px;
    background: var(--surface); border-radius: 8px; transition: background 0.15s;
  }
  .task:hover { background: var(--surface-2); }

  /* Running task — expanded card */
  .task--running-card {
    background: #0a1a0a; border: 1px solid #1a3a1a; border-radius: 12px;
    padding: 0; overflow: hidden; margin-bottom: 8px;
  }
  .task--running-header {
    display: grid; grid-template-columns: 40px 1fr auto;
    align-items: center; gap: 16px; padding: 16px 20px;
  }
  .task--running-detail {
    border-top: 1px solid #1a3a1a; padding: 16px 20px 20px;
    display: flex; flex-direction: column; gap: 12px;
  }
  .detail-row { display: flex; gap: 12px; align-items: flex-start; }
  .detail-label {
    font-size: 11px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-dim); min-width: 60px; padding-top: 1px;
  }
  .detail-value { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
  .detail-value strong { color: var(--text); font-weight: 500; }

  .timer {
    font-size: 24px; font-weight: 600; color: var(--green);
    font-variant-numeric: tabular-nums; letter-spacing: 0.02em;
  }
  .timer-label { font-size: 11px; color: var(--text-dim); margin-top: 2px; text-align: right; }

  .btn-restart {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 8px 16px; border-radius: 6px; border: 1px solid #3a1a1a;
    background: rgba(239,68,68,0.08); color: var(--red);
    font-family: var(--font); font-size: 12px; font-weight: 500;
    cursor: pointer; transition: all 0.15s; margin-top: 8px;
  }
  .btn-restart:hover { background: rgba(239,68,68,0.18); border-color: var(--red); }
  .btn-restart:active { transform: scale(0.97); }
  .btn-restart--confirming { background: var(--red); color: #fff; border-color: var(--red); }
  .btn-restart--done { background: rgba(34,197,94,0.15); color: var(--green); border-color: #1a3a1a; }
  .btn-restart svg { flex-shrink: 0; }

  .status-icon {
    width: 32px; height: 32px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 14px; flex-shrink: 0;
  }
  .status-icon--running { background: rgba(34,197,94,0.1); }
  .status-icon--queued { background: rgba(100,100,100,0.1); color: var(--text-dim); }
  .status-icon--done { background: rgba(34,197,94,0.1); color: var(--green); }
  .status-icon--failed { background: rgba(239,68,68,0.1); color: var(--red); }

  .spinner {
    width: 20px; height: 20px; border: 2.5px solid rgba(34,197,94,0.2);
    border-top-color: var(--green); border-radius: 50%; animation: spin 0.8s linear infinite;
  }
  .spinner--large {
    width: 28px; height: 28px; border-width: 3px;
  }
  @keyframes spin { to { transform: rotate(360deg); } }

  .task-info { min-width: 0; }
  .task-title { font-size: 14px; font-weight: 500; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .task-title--green { color: var(--green); }
  .task-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

  /* Expandable questionnaire for all tasks */
  .task-wrap { border-radius: 8px; overflow: hidden; margin-bottom: 2px; }
  .task-row { cursor: pointer; }
  .task-row .expand-icon {
    font-size: 10px; color: var(--text-dim); transition: transform 0.2s;
    margin-left: 8px; display: inline-block;
  }
  .task-wrap--open .task-row .expand-icon { transform: rotate(90deg); }
  .task-questionnaire {
    display: none; padding: 12px 20px 16px; padding-left: 56px;
    background: var(--surface-2); border-top: 1px solid var(--border);
  }
  .task-wrap--open .task-questionnaire { display: block; }
  .tq-label {
    font-size: 10px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.08em; color: var(--text-dim); min-width: 50px;
    display: inline-block; padding-top: 1px;
  }
  .tq-row { display: flex; gap: 10px; margin-bottom: 6px; align-items: flex-start; }
  .tq-val { font-size: 12px; color: var(--text-muted); line-height: 1.45; }
  .tq-section-title {
    font-size: 10px; font-weight: 700; text-transform: uppercase;
    letter-spacing: 0.1em; color: var(--text-dim); margin-bottom: 8px;
    border-bottom: 1px solid var(--border); padding-bottom: 4px;
  }

  /* Start YOLO button — shown when idle/stuck */
  .start-yolo-bar {
    background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
    padding: 20px 24px; display: flex; align-items: center; justify-content: space-between;
    margin-bottom: 16px;
  }
  .start-yolo-bar .idle-msg {
    display: flex; align-items: center; gap: 10px;
  }
  .idle-dot {
    width: 10px; height: 10px; border-radius: 50%; background: var(--amber);
    animation: pulse-amber 2s ease infinite;
  }
  @keyframes pulse-amber {
    0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(245,158,11,0.4); }
    50% { opacity: 0.6; box-shadow: 0 0 0 6px rgba(245,158,11,0); }
  }
  .idle-text { font-size: 14px; color: var(--amber); font-weight: 500; }
  .idle-sub { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
  .btn-start-yolo {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 10px 20px; border-radius: 8px; border: 1px solid #1a3a1a;
    background: rgba(34,197,94,0.1); color: var(--green);
    font-family: var(--font); font-size: 13px; font-weight: 600;
    cursor: pointer; transition: all 0.15s; white-space: nowrap;
  }
  .btn-start-yolo:hover { background: rgba(34,197,94,0.2); border-color: var(--green); }
  .btn-start-yolo:active { transform: scale(0.97); }
  .btn-start-yolo--working { background: rgba(34,197,94,0.15); color: var(--green); cursor: wait; }
  .btn-start-yolo svg { flex-shrink: 0; }

  .task-badge {
    font-size: 11px; font-weight: 500; padding: 4px 10px;
    border-radius: 12px; white-space: nowrap; flex-shrink: 0;
  }
  .task-badge--running { background: rgba(34,197,94,0.15); color: var(--green); animation: pulse-badge 2s ease infinite; }
  @keyframes pulse-badge { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
  .task-badge--queued { background: rgba(100,100,100,0.1); color: var(--text-dim); }
  .task-badge--done { background: rgba(34,197,94,0.1); color: var(--green-dim); }
  .task-badge--failed { background: rgba(239,68,68,0.1); color: var(--red); }

  .refresh-note { text-align: center; font-size: 11px; color: var(--text-dim); margin-top: 40px; }

  /* Live Feed Side Panel */
  .live-panel {
    position: fixed; top: 61px; right: 0; bottom: 0; width: 380px;
    background: #0d0d0d; border-left: 1px solid var(--border);
    z-index: 20; transform: translateX(100%); transition: transform 0.25s ease;
    display: flex; flex-direction: column;
  }
  .live-panel--open { transform: translateX(0); }
  .live-panel-toggle {
    position: fixed; top: 72px; right: 12px; z-index: 25;
    padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text-muted); font-family: var(--font);
    font-size: 11px; font-weight: 500; cursor: pointer; transition: all 0.15s;
    display: flex; align-items: center; gap: 6px;
  }
  .live-panel-toggle:hover { background: var(--surface-2); color: var(--text); }
  .live-panel-toggle--active { right: 392px; background: #0a1a0a; color: var(--green); border-color: #1a3a1a; }
  .live-panel-toggle .toggle-dot {
    width: 6px; height: 6px; border-radius: 50%; background: var(--text-dim);
  }
  .live-panel-toggle--active .toggle-dot {
    background: var(--green); animation: pulse-dot 2s ease infinite;
  }
  .live-panel-header {
    padding: 14px 16px; border-bottom: 1px solid var(--border);
    font-size: 12px; font-weight: 600; color: var(--text-muted);
    text-transform: uppercase; letter-spacing: 0.08em;
    display: flex; align-items: center; justify-content: space-between;
  }
  .live-panel-header .line-count { font-weight: 400; color: var(--text-dim); text-transform: none; letter-spacing: 0; }
  .live-feed {
    flex: 1; overflow-y: auto; padding: 8px 0; font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
    font-size: 11px; line-height: 1.6; scrollbar-width: thin;
  }
  .live-feed::-webkit-scrollbar { width: 4px; }
  .live-feed::-webkit-scrollbar-track { background: transparent; }
  .live-feed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
  .live-line {
    padding: 2px 16px; color: var(--text-muted); word-break: break-all;
    transition: background 0.1s;
  }
  .live-line:hover { background: rgba(255,255,255,0.03); }
  .live-line--sku { color: var(--green); }
  .live-line--error { color: var(--red); }
  .live-line--info { color: var(--blue); }
  .live-line .ts { color: var(--text-dim); margin-right: 8px; font-size: 10px; }
  .live-empty { padding: 40px 16px; color: var(--text-dim); text-align: center; font-family: var(--font); font-size: 12px; }

  /* Shift container when panel is open */
  body.panel-open .container { margin-right: 380px; }
</style>
</head>
<body>

<div class="header">
  <h1><span class="live-dot" id="live-dot"></span>YOLO Task Ledger</h1>
  <div style="display:flex;align-items:center;gap:24px;">
    <span id="live-status-text" style="font-size:12px;font-weight:500;color:var(--amber);">Idle &mdash; waiting for next task</span>
    <div class="header-stats" id="stats"></div>
  </div>
</div>
<div class="container" id="container"></div>
<div style="max-width:960px;margin:0 auto;padding:0 32px;">
  <div id="live-output" style="background:#0a0a0a;border:1px solid #1a1a22;border-radius:6px;padding:12px;max-height:400px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a0a0a0;line-height:1.6;white-space:pre-wrap;word-wrap:break-word;"></div>
</div>

<!-- Live Feed Toggle -->
<button class="live-panel-toggle" id="panel-toggle" type="button">
  <span class="toggle-dot"></span> Live Feed
</button>

<!-- Live Feed Side Panel -->
<div class="live-panel" id="live-panel">
  <div class="live-panel-header">
    <span>Live Output</span>
    <span class="line-count" id="line-count">0 lines</span>
  </div>
  <div class="live-feed" id="live-feed"></div>
</div>

<script>
var API_BASE = '/api';
var taskStartedAt = null;
var timerInterval = null;

var TASK_META = {
  '00_hex-color-gemini-blitz': {
    desc: 'Batch Gemini Vision on all products missing color_hex via AI analysis',
    what: 'Sends each product image to Gemini 2.0 Flash Vision API to extract dominant colors with hex codes and percentages. Results stored in PostgreSQL color_hex field and pushed to Shopify as tags.',
    why: '59,338 active products need color data for Shop by Color. Without hex codes, products are invisible in color browsing. AI vision is more accurate than text-based guessing.',
    how: 'Claude CLI reads product list from PostgreSQL, sends each image URL to Gemini 2.0 Flash generateContent API with structured output schema. Gemini returns top 5 colors + hex + percentages. Results written to catalog.color_hex and enrichment_tracking.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Gemini 2.0 Flash Vision API',
    where: 'PostgreSQL: catalog.color_hex, enrichment_tracking | Shopify: product tags (color names)',
    eta: '~8-10 min (rate limited by Gemini API, ~500 products/batch)'
  },
  '00_create-79-missing-collections': {
    desc: 'Create smart collections for 79 vendors with no browsable collection page',
    what: 'Creates Shopify smart collections with "vendor equals X" rules. Each becomes an indexable URL with products auto-populated.',
    why: '50% of vendors (79/232) have no dedicated collection page = no SEO landing page, no browsable URL. Each new collection is an instant SEO win.',
    how: 'Reads vendor list from collection-audit-report.json. For each, POST /admin/api/smart_collections.json with vendor rule. 600ms delay between calls for rate limiting.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Admin API',
    where: 'Shopify: smart_collections | Report: /yolo-agent/logs/collections-created-report.json',
    eta: '~2 min (79 API calls at 600ms each)'
  },
  '00a_brewster-line-manager': {
    desc: 'Build line manager for Brewster sub-brands (Advantage, A-Street, etc.)',
    what: 'Creates routing system mapping Brewster sub-brand names to correct scraper configs. Each sub-brand has different page layouts and CDN paths.',
    why: 'Brewster has 8+ sub-brands on yorkwall.com. Without a line manager, each needs a separate scraper. The manager routes by brand prefix.',
    how: 'Reads existing Brewster scraper config, creates a brand-routing module that inspects product URLs and delegates to the correct extraction logic per sub-brand.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 writes to /vendor-scrapers/',
    where: 'Code: /DW-Agents/vendor-scrapers/brewster-line-manager.js',
    eta: '~5-8 min'
  },
  '00b_designtex-scraper-fix': {
    desc: 'Fix Designtex scraper stuck at 96 products \u2014 pagination or auth issue',
    what: 'Diagnoses pagination bug or session timeout. Fixes cursor logic and re-runs for full ~400+ catalog.',
    why: 'Only 96 of ~400+ Designtex products captured. Major commercial vendor with missing inventory.',
    how: 'Reads scraper source, identifies pagination/auth issue, patches the offset or cookie renewal logic, test runs to verify full catalog.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Dex agent config (port 9633)',
    where: 'Code: /DW-Agents/vendor-agent-core/configs/designtex.json | DB: catalog table',
    eta: '~5-10 min'
  },
  '01_cleanup-37-empty-collections': {
    desc: 'Unpublish 37 collections with 0 active products to fix thin SEO pages',
    what: 'Unpublishes empty collections (reversible). Checks homepage/nav links first.',
    why: 'Empty pages = thin content penalized by Google. Unpublishing removes from sitemap while keeping data.',
    how: 'Reads audit report, checks each collection handle against homepage HTML, then PUT /smart_collections/{id}.json with published:false.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Admin API',
    where: 'Shopify: collection published status | Report: /yolo-agent/logs/empty-collections-cleanup-report.json',
    eta: '~3 min'
  },
  '08_push-specs-remaining': {
    desc: 'Push metafield specs for ~4,500 products across 6 vendors',
    what: 'Reads specs from PostgreSQL, pushes to Shopify metafields (width, repeat, material, fire rating).',
    why: 'Blank spec tables = customers can\'t calculate how much to order = lost sales.',
    how: 'SQL query joins catalog + specs tables, batches Shopify metafield mutations via GraphQL Admin API at 2 req/s.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 PostgreSQL \u2192 Shopify GraphQL',
    where: 'Shopify: product metafields (dw.width, dw.repeat_v, dw.material, etc.)',
    eta: '~8-10 min (4,500 products at 2/s)'
  },
  '09_run-vendor-standardization': {
    desc: 'Normalize vendor names \u2014 fix capitalization and aliases',
    what: 'Scans for inconsistent vendor names and normalizes to canonical form.',
    why: 'Inconsistent names break smart collections, search, and navigation.',
    how: 'Queries distinct vendor names, builds alias map, runs UPDATE on PostgreSQL then Shopify product.vendor via Admin API.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 PostgreSQL + Shopify Admin API',
    where: 'PostgreSQL: catalog.vendor | Shopify: product.vendor',
    eta: '~5-8 min'
  },
  '10_resume-weight-fix': {
    desc: 'Set shipping weight on 10,217 variants',
    what: 'Samples=0 weight, priced variants get calculated weight from width + material.',
    why: 'No weight = wrong shipping costs or checkout errors.',
    how: 'Queries variants missing weight, calculates from product specs, PUTs to /variants/{id}.json.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Admin API (variant updates)',
    where: 'Shopify: variant.weight, variant.weight_unit',
    eta: '~10 min (10K variants, timeout may hit)'
  },
  '11_thibaut-302-archive': {
    desc: 'Archive Thibaut products returning 302 redirects',
    what: 'HEAD requests to vendor URLs, archives products that 302 (discontinued).',
    why: 'Dead vendor URLs = unorderable products misleading customers.',
    how: 'Fetches Thibaut product list, HEAD each vendor_url, if 302 \u2192 PUT product status:archived via Shopify API.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Thibaut website + Shopify Admin API',
    where: 'Shopify: product.status \u2192 archived',
    eta: '~5-8 min'
  },
  '12_shop-by-color-phase3': {
    desc: 'CSS conic-gradient color wheel with lazy loading',
    what: 'Interactive color wheel UI. Click a color segment \u2192 filter products.',
    why: 'Designers search by color. Visual wheel unlocks color-based discovery.',
    how: 'Generates CSS conic-gradient from hex color distribution, builds Liquid section with lazy-loaded product grid.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Theme Asset API',
    where: 'Shopify theme: sections/shop-by-color.liquid + assets/shop-by-color.css',
    eta: '~8 min'
  },
  '13_hollywood-hex-extraction': {
    desc: 'Gemini Vision hex extraction for ~2,900 Hollywood products',
    what: 'AI extracts top 5 colors with hex + percentages from product images.',
    why: 'Hollywood is one of largest vendors. No color data = invisible in Shop by Color.',
    how: 'Queries Hollywood products missing color_hex, sends images to Gemini 2.0 Flash, writes results to PostgreSQL.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Gemini 2.0 Flash Vision API',
    where: 'PostgreSQL: catalog.color_hex, enrichment_tracking',
    eta: '~10 min (rate limited, may need multiple batches)'
  },
  '14_stout-timorous-width': {
    desc: 'Scrape width specs from Stout/Timorous vendor sites',
    what: 'Visits product pages to extract width. Updates DB + Shopify metafields.',
    why: 'Width is HARD REQUIREMENT for FULLPRODUCT import gate. Missing = rejected.',
    how: 'Puppeteer visits each product page, extracts width from specs table, writes to PostgreSQL + Shopify.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Vendor websites (Puppeteer) \u2192 PostgreSQL',
    where: 'PostgreSQL: catalog.width | Shopify: dw.width metafield',
    eta: '~8-10 min'
  },
  '15_legal-violations-audit': {
    desc: 'Scan product descriptions for legal compliance violations',
    what: 'Scans body_html and titles for trademark, origin, settlement term violations.',
    why: 'Legal compliance is non-negotiable. Previous incidents cost real money.',
    how: 'SQL query fetches all body_html, regex scans for banned terms, generates flagged product report.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 PostgreSQL (read-only scan)',
    where: 'Report: /yolo-agent/logs/legal-violations-report.json (no data modified)',
    eta: '~5 min (read-only audit)'
  },
  '16_philippe-romano-legacy': {
    desc: 'Migrate 1,721 legacy Philippe Romano DWKK products',
    what: 'Updates SKUs from DWKK- to DW- format, moves specs from body to metafields.',
    why: 'Legacy data predates current pipeline. Needs modernization for consistency.',
    how: 'Queries DWKK-prefixed products, generates new DW- SKUs, rewrites body_html, pushes metafields.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 PostgreSQL + Shopify Admin API',
    where: 'PostgreSQL: catalog SKU + body | Shopify: product title, body_html, variant.sku, metafields',
    eta: '~10 min (1,721 products, may timeout)'
  },
  '19_hex-color-gemini-blitz': {
    desc: 'Batch Gemini Vision on ALL vendors missing color_hex',
    what: 'Comprehensive pass across all vendors, not just Hollywood.',
    why: 'Shop by Color requires hex data on every product. This is the catch-all sweep.',
    how: 'Same as 00_hex-color-gemini-blitz \u2014 queries ALL vendors with NULL color_hex, batches through Gemini.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Gemini 2.0 Flash Vision API',
    where: 'PostgreSQL: catalog.color_hex | Shopify: color tags',
    eta: '~10 min per batch'
  }
};

// All remaining tasks with full detail
var TASK_SHORT_META = {
  '00_command54-private-label': {
    desc: 'Scrape Command54 and private-label as Phillipe Romano with beach city collections',
    what: 'Scrapes command54.com/designs for all products. Renames patterns with East Coast beach city prefixes (Montauk, Nantucket, Cape May, etc.). Saves to PostgreSQL, assigns DW SKUs, then runs Full Monte 8-phase pipeline.',
    why: 'Phillipe Romano is DW\'s private label brand. Command54 is the hidden source. Adding their catalog expands the exclusive collection without revealing the real manufacturer.',
    how: 'Phase 1: Puppeteer crawls command54.com/designs \u2192 extracts name, images, specs. Phase 2: Maps each product to beach city collection. Phase 3-8: AI enrichment, Silas validation, VCC approval, room settings, spin viewers, Shopify push.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Puppeteer \u2192 PostgreSQL \u2192 Full Monte pipeline \u2192 Shopify',
    where: 'PostgreSQL: command54_catalog | Shopify: vendor "Phillipe Romano" | Hidden: private_label.real_vendor_name = "Command54"',
    eta: '~10-15 min (scrape + DB, Full Monte runs after)'
  },
  '00_fix-thibaut-anna-french': {
    desc: 'Fix Thibaut + Anna French SKU prefixes, product types, and tags',
    what: 'Corrects SKU prefixes (DWTT=Thibaut, DWAF=Anna French). Fixes product_type. Re-attributes AF-prefix products from Thibaut to Anna French vendor.',
    why: 'Anna French is a Thibaut sub-brand needing separate vendor attribution. Mixed prefixes break collection rules and vendor filtering.',
    how: 'Queries Shopify products with Thibaut/AF prefixes, batch updates vendor, SKU, product_type via Admin API.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Admin API',
    where: 'Shopify: product.vendor, variant.sku, product.product_type',
    eta: '~5-8 min'
  },
  '00_fix-thibaut-titles-vendor': {
    desc: 'Remove MFR SKUs from Thibaut titles + fix Anna French vendor',
    what: 'Strips manufacturer codes like "(af9637)" from titles. Changes vendor to "Anna French" for AF-prefix products.',
    why: 'SKUs in titles look unprofessional. Anna French misattributed to Thibaut breaks vendor collections.',
    how: 'Regex removes parenthesized SKU patterns. Checks prefix to determine correct vendor. Batch Shopify API updates.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Admin API',
    where: 'Shopify: product.title, product.vendor',
    eta: '~5 min'
  },
  '12_social-posts': {
    desc: 'Post to X/Twitter and Bluesky via Agent Abrams',
    what: 'Crafts social post, sanitizes per MEMORY.md rules, posts via OAuth scripts.',
    why: 'Agent Abrams brand building \u2014 consistent social presence drives traffic to goodquestion.ai.',
    how: 'Writes text \u2192 sanitizer.js \u2192 tweet.cjs (OAuth 1.0a) + bsky-post.cjs (@atproto/api).',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 tweet.cjs + bsky-post.cjs',
    where: 'X.com @agentabrams | Bluesky @agentabrams.bsky.social',
    eta: '~1 min'
  },
  '13_git-commit-session109': {
    desc: 'Commit session #109 changes to git',
    what: 'Stages modified files, commits with descriptive message, pushes to GitHub.',
    why: 'Uncommitted work can be lost. Git history tracks every change for accountability.',
    how: 'git add \u2192 git commit \u2192 git push to Stevemdr/DW-Agents (private).',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 git + GitHub',
    where: 'Git: /root/DW-Agents/ \u2192 GitHub Stevemdr/DW-Agents',
    eta: '~30s'
  },
  '14_styles-page-tile-images': {
    desc: 'Replace broken tile images on Styles page',
    what: 'Finds broken images, replaces with best-selling product images per style category.',
    why: 'Broken images hurt credibility and reduce click-through to style collections.',
    how: 'Reads page HTML, identifies broken URLs, queries Shopify for top products, updates page content.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Admin API',
    where: 'Shopify: pages/styles content HTML',
    eta: '~5 min'
  },
  '15_momentum-sku-investigation': {
    desc: 'Investigate 1,059 unmatched Momentum SKUs',
    what: 'Analyzes SKUs that failed import matching. Identifies format differences, discontinued items, or mapping errors.',
    why: '1,000+ potential products stuck in limbo. Fixing mapping could add significant inventory.',
    how: 'Queries momentum_colorways + dw_sku_crossref, compares formats, generates match/nomatch report.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 PostgreSQL (read-only)',
    where: 'Report: /yolo-agent/logs/momentum-sku-report.json',
    eta: '~5 min'
  },
  '16_vcc-dashboard-status-fix': {
    desc: 'Fix VCC /api/status returning 404',
    what: 'Diagnoses and fixes the missing status endpoint in Vendor Command Center.',
    why: 'VCC dashboard is how Steve monitors vendor pipeline health. Broken = blind operations.',
    how: 'Reads Victor agent source, finds missing route, adds it, restarts PM2.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Victor agent (port 9660)',
    where: 'Code: /DW-Agents/vendor-command-center/server.js',
    eta: '~3 min'
  },
  '17_shop-by-color-integration': {
    desc: 'Integrate hex-based color filtering into Shopify theme',
    what: 'Adds color filter UI to collection pages using hex codes from AI enrichment.',
    why: 'Turns 38K+ enriched color hex values into a usable browsing experience for designers.',
    how: 'Creates Liquid section with color swatches, JS filter logic, CSS styling. Uploads via Asset API.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Shopify Theme Asset API',
    where: 'Shopify theme: sections/shop-by-color.liquid + assets/',
    eta: '~8 min'
  },
  '17_youtube-oauth-reauth': {
    desc: 'Re-authenticate YouTube OAuth tokens',
    what: 'Refreshes expired OAuth tokens for Agent Abrams YouTube channel.',
    why: 'Upload script fails without valid tokens. Channel content pipeline is blocked.',
    how: 'Runs OAuth refresh flow via youtube-upload.cjs. May need manual browser auth.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 YouTube OAuth API',
    where: '.youtube-token.json (BLOCKED \u2014 may need phone verification)',
    eta: 'May fail \u2014 requires manual step'
  },
  '18_kalshi-calendar-panel': {
    desc: 'Build calendar panel for Kalshi trading dashboard',
    what: 'Calendar view showing upcoming events, expiration dates, and P&L timeline.',
    why: 'Ken needs visual calendar to see event clustering and avoid overexposure.',
    how: 'Creates React calendar component, queries ken_trades + ken_market_mappings for data.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Ken agent (port 9816)',
    where: 'Code: /DW-Agents/ken/ | PostgreSQL: ken_trades, ken_market_mappings',
    eta: '~8 min'
  },
  '18_old-youtube-videos-unlist': {
    desc: 'Unlist old silent YouTube videos',
    what: 'Sets videos without narration to "unlisted". Keeps narrated videos public.',
    why: 'Silent videos look unfinished and hurt channel quality metrics.',
    how: 'YouTube Data API v3 \u2192 update video privacy from "public" to "unlisted".',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 YouTube Data API v3',
    where: 'YouTube @AgentAbrams (IDs: 6iCCuuk7dDM, mT2LZBrajas, OZY5D0NgQ_g, 5un2PCEhtmw)',
    eta: '~2 min'
  },
  '11_blog-post-session109': {
    desc: 'Publish blog post about session #109',
    what: 'Writes post, sanitizes, publishes to goodquestion.ai, tweets + Bluesky.',
    why: 'Regular blog output builds Agent Abrams brand and drives organic search traffic.',
    how: 'Write markdown \u2192 sanitizer \u2192 Astro build \u2192 social post scripts.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Astro + tweet.cjs + bsky-post.cjs',
    where: 'goodquestion.ai/posts/ | X.com | Bluesky',
    eta: '~3 min'
  },
  '20_phoebe-videos-dir-fix': {
    desc: 'Fix Phoebe agent missing /videos directory',
    what: 'Creates missing directory causing startup crash.',
    why: 'Phoebe (Figma QA) crashes on every PM2 restart without this directory.',
    how: 'mkdir -p + pm2 restart phoebe.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 bash + PM2',
    where: 'Filesystem: /DW-Agents/phoebe/ | PM2: phoebe process',
    eta: '~1 min'
  },
  '20_pm2-health-cleanup': {
    desc: 'Audit PM2 \u2014 restart crashed, delete zombies',
    what: 'Scans 143 PM2 processes. Restarts errored. Deletes zombies. Reports cleanup.',
    why: 'Process creep wastes RAM. Errored processes fail silently.',
    how: 'pm2 jlist \u2192 scan \u2192 pm2 restart errored \u2192 pm2 delete zombies.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 PM2 CLI',
    where: 'PM2 process list (server-wide, all 143 processes)',
    eta: '~3 min'
  },
  '21_knoll-maharam-scraper-investigate': {
    desc: 'Investigate Knoll/Maharam scraper \u2014 site structure changed?',
    what: 'Tests scraper against live site. Checks if selectors, auth, or pagination broke.',
    why: 'Major commercial vendor. Broken scraper = new products not captured.',
    how: 'Test scrape \u2192 compare selectors \u2192 check auth walls \u2192 report.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 Kurt agent config + vendor site',
    where: 'Report: /yolo-agent/logs/ | Config: /vendor-agent-core/configs/knoll.json',
    eta: '~5 min'
  },
  '22_cron-audit-cleanup': {
    desc: 'Review cron jobs \u2014 remove stale, fix broken',
    what: 'Lists all crontab + PM2 crons. Identifies stale, broken, undocumented jobs.',
    why: 'Stale jobs waste CPU. Undocumented schedules are invisible risks.',
    how: 'crontab -l + agent-cronjob scan \u2192 cross-reference with existing scripts \u2192 report.',
    who: 'YOLO Agent (Yuri) \u2192 Claude CLI \u2192 crontab + agent-cronjob (port 9602)',
    where: 'System crontab + /DW-Agents/agent-cronjob/ configs',
    eta: '~3 min'
  }
};

// Track which task detail panels are open — pause refresh while any are expanded
var openTaskSlugs = new Set();

function fetchJSON(url) {
  return new Promise(function(resolve, reject) {
    var x = new XMLHttpRequest();
    x.open('GET', url, true);
    x.withCredentials = true;
    x.onload = function() {
      try { resolve(JSON.parse(x.responseText)); }
      catch(e) { reject(e); }
    };
    x.onerror = function() { reject(new Error('XHR failed')); };
    x.send();
  });
}

function formatTitle(slug) {
  return slug.replace(/^\d+[a-z]?_/, '').replace(/-/g, ' ')
    .replace(/\b\w/g, function(c) { return c.toUpperCase(); })
    .replace(/Pm2/g, 'PM2').replace(/Vcc/g, 'VCC').replace(/Sku/g, 'SKU')
    .replace(/Seo/g, 'SEO').replace(/Css/g, 'CSS').replace(/Api/g, 'API')
    .replace(/Oauth/g, 'OAuth');
}

function getMeta(slug) {
  if (TASK_META[slug]) return TASK_META[slug];
  if (TASK_SHORT_META[slug]) return TASK_SHORT_META[slug];
  return null;
}

function getDesc(slug) {
  var m = getMeta(slug);
  return m ? m.desc : '';
}

function formatElapsed(ms) {
  var s = Math.floor(ms / 1000);
  var m = Math.floor(s / 60);
  var h = Math.floor(m / 60);
  s = s % 60;
  m = m % 60;
  var parts = [];
  if (h > 0) parts.push(h + 'h');
  parts.push(String(m).padStart(2, '0') + 'm');
  parts.push(String(s).padStart(2, '0') + 's');
  return parts.join(' ');
}

function createRunningCard(filename, startedAt) {
  var slug = filename.replace('.md', '');
  var title = formatTitle(slug);
  var meta = getMeta(slug) || {};
  var desc = meta.desc || '';
  var what = meta.what || '';
  var why = meta.why || '';

  var card = document.createElement('div');
  card.className = 'task--running-card';

  // Header row
  var header = document.createElement('div');
  header.className = 'task--running-header';

  var iconWrap = document.createElement('div');
  iconWrap.className = 'status-icon status-icon--running';
  var spinner = document.createElement('div');
  spinner.className = 'spinner spinner--large';
  iconWrap.appendChild(spinner);

  var info = document.createElement('div');
  info.className = 'task-info';
  var titleEl = document.createElement('div');
  titleEl.className = 'task-title task-title--green';
  titleEl.textContent = title;
  info.appendChild(titleEl);
  if (desc) {
    var descEl = document.createElement('div');
    descEl.className = 'task-desc';
    descEl.textContent = desc;
    info.appendChild(descEl);
  }

  // Timer
  var timerWrap = document.createElement('div');
  timerWrap.style.textAlign = 'right';
  var timerEl = document.createElement('div');
  timerEl.className = 'timer';
  timerEl.id = 'running-timer';
  timerEl.textContent = '00m 00s';
  var timerLabel = document.createElement('div');
  timerLabel.className = 'timer-label';
  timerLabel.textContent = 'elapsed';
  timerWrap.appendChild(timerEl);
  timerWrap.appendChild(timerLabel);

  header.appendChild(iconWrap);
  header.appendChild(info);
  header.appendChild(timerWrap);
  card.appendChild(header);

  // Detail panel — What, Why, How, Who, Where, ETA, Live Log
  var fields = [
    ['What', what],
    ['Why', why],
    ['How', meta.how || ''],
    ['Who', meta.who || ''],
    ['Where', meta.where || ''],
    ['ETA', meta.eta || '']
  ];
  var hasAny = fields.some(function(f) { return f[1]; });
  if (hasAny) {
    var detail = document.createElement('div');
    detail.className = 'task--running-detail';

    fields.forEach(function(f) {
      if (!f[1]) return;
      var row = document.createElement('div');
      row.className = 'detail-row';
      var label = document.createElement('div');
      label.className = 'detail-label';
      label.textContent = f[0];
      var val = document.createElement('div');
      val.className = 'detail-value';
      val.textContent = f[1];
      row.appendChild(label);
      row.appendChild(val);
      detail.appendChild(row);
    });

    // Live output is rendered in persistent #live-output div below the card — not inside it

    // Restart button (kill + re-queue)
    var btnRow = document.createElement('div');
    btnRow.className = 'detail-row';
    btnRow.style.marginTop = '4px';
    var btnLabel = document.createElement('div');
    btnLabel.className = 'detail-label';
    btnLabel.textContent = '';
    var btnWrap = document.createElement('div');

    var btn = document.createElement('button');
    btn.className = 'btn-restart';
    btn.type = 'button';
    btn.id = 'btn-restart-task';

    var btnIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    btnIcon.setAttribute('width', '14');
    btnIcon.setAttribute('height', '14');
    btnIcon.setAttribute('viewBox', '0 0 14 14');
    btnIcon.setAttribute('fill', 'none');
    var btnPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    btnPath.setAttribute('d', 'M1.5 7a5.5 5.5 0 1 1 1.02 3.2M1.5 12.5v-2.5H4');
    btnPath.setAttribute('stroke', 'currentColor');
    btnPath.setAttribute('stroke-width', '1.5');
    btnPath.setAttribute('stroke-linecap', 'round');
    btnPath.setAttribute('stroke-linejoin', 'round');
    btnIcon.appendChild(btnPath);
    btn.appendChild(btnIcon);

    var btnText = document.createElement('span');
    btnText.textContent = 'Kill & Restart Task';
    btn.appendChild(btnText);

    var confirmState = false;
    btn.addEventListener('click', function() {
      if (!confirmState) {
        confirmState = true;
        btn.className = 'btn-restart btn-restart--confirming';
        btnText.textContent = 'Confirm Kill?';
        setTimeout(function() {
          if (confirmState) {
            confirmState = false;
            btn.className = 'btn-restart';
            btnText.textContent = 'Kill & Restart Task';
          }
        }, 3000);
      } else {
        btnText.textContent = 'Killing...';
        btn.disabled = true;
        (new Promise(function(resolve, reject) {
          var x = new XMLHttpRequest();
          x.open('POST', API_BASE + '/kill', true);
          x.withCredentials = true;
          x.onload = function() { try { resolve(JSON.parse(x.responseText)); } catch(e) { resolve({}); } };
          x.onerror = function() { reject(new Error('XHR failed')); };
          x.send();
        }))
          .then(function(data) {
            btn.className = 'btn-restart btn-restart--done';
            btnText.textContent = 'Killed \u2014 restarting from queue';
            confirmState = false;
            setTimeout(refresh, 2000);
          })
          .catch(function() {
            btnText.textContent = 'Error \u2014 try again';
            btn.disabled = false;
            btn.className = 'btn-restart';
            confirmState = false;
          });
      }
    });

    btnWrap.appendChild(btn);
    btnRow.appendChild(btnLabel);
    btnRow.appendChild(btnWrap);
    detail.appendChild(btnRow);

    card.appendChild(detail);

    // Start live output polling
    startLiveOutputPolling();
  }

  // Start timer
  taskStartedAt = startedAt ? new Date(startedAt).getTime() : Date.now();
  if (timerInterval) clearInterval(timerInterval);
  timerInterval = setInterval(function() {
    var el = document.getElementById('running-timer');
    if (el) el.textContent = formatElapsed(Date.now() - taskStartedAt);
  }, 1000);

  return card;
}

function createTask(filename, status, duration, queueIndex) {
  var slug = filename.replace('.md', '');
  var title = formatTitle(slug);
  var meta = getMeta(slug) || {};
  var desc = meta.desc || '';

  var wrap = document.createElement('div');
  wrap.className = 'task-wrap';

  // Main row (clickable)
  var row = document.createElement('div');
  row.className = 'task task-row';

  var iconWrap = document.createElement('div');
  iconWrap.className = 'status-icon status-icon--' + status;
  if (status === 'done') { iconWrap.textContent = '\u2713'; }
  else if (status === 'failed') { iconWrap.textContent = '\u2717'; }
  else { iconWrap.textContent = '\u25CB'; }

  var info = document.createElement('div');
  info.className = 'task-info';
  var titleEl = document.createElement('div');
  titleEl.className = 'task-title';
  if (status === 'queued' && queueIndex != null) {
    var idSpan = document.createElement('span');
    idSpan.style.cssText = 'color:#8888A0;font-family:\'SF Mono\',\'Fira Code\',monospace;font-size:12px;display:inline-block;min-width:40px;margin-right:4px;';
    idSpan.textContent = '#' + queueIndex;
    titleEl.appendChild(idSpan);
    titleEl.appendChild(document.createTextNode(title));
  } else {
    titleEl.textContent = title;
  }
  var expandIcon = document.createElement('span');
  expandIcon.className = 'expand-icon';
  expandIcon.textContent = '\u25B6';
  titleEl.appendChild(expandIcon);
  info.appendChild(titleEl);
  if (desc) {
    var descEl = document.createElement('div');
    descEl.className = 'task-desc';
    descEl.textContent = desc;
    info.appendChild(descEl);
  }

  var badge = document.createElement('div');
  badge.className = 'task-badge task-badge--' + status;
  if (status === 'queued') badge.textContent = 'Queued';
  else if (status === 'done') badge.textContent = duration || 'Done';
  else badge.textContent = 'Failed';

  row.appendChild(iconWrap);
  row.appendChild(info);
  row.appendChild(badge);
  wrap.appendChild(row);

  // Task Questionnaire (expandable)
  var hasQ = meta.what || meta.why || meta.how || meta.who || meta.where || meta.eta;
  if (hasQ) {
    var tq = document.createElement('div');
    tq.className = 'task-questionnaire';

    var tqTitle = document.createElement('div');
    tqTitle.className = 'tq-section-title';
    tqTitle.textContent = 'Task Questionnaire';
    tq.appendChild(tqTitle);

    var fields = [
      ['What', meta.what],
      ['Why', meta.why],
      ['How', meta.how],
      ['Who', meta.who],
      ['Where', meta.where],
      ['ETA', meta.eta]
    ];
    fields.forEach(function(f) {
      if (!f[1]) return;
      var r = document.createElement('div');
      r.className = 'tq-row';
      var lbl = document.createElement('span');
      lbl.className = 'tq-label';
      lbl.textContent = f[0];
      var val = document.createElement('span');
      val.className = 'tq-val';
      val.textContent = f[1];
      r.appendChild(lbl);
      r.appendChild(val);
      tq.appendChild(r);
    });

    wrap.appendChild(tq);

    // Toggle on click — track open state to pause refresh
    row.addEventListener('click', function() {
      var wasOpen = wrap.classList.contains('task-wrap--open');
      wrap.classList.toggle('task-wrap--open');
      if (wasOpen) {
        openTaskSlugs.delete(slug);
      } else {
        openTaskSlugs.add(slug);
      }
    });
  }

  return wrap;
}

function createLabel(text) {
  var el = document.createElement('div');
  el.className = 'section-label';
  el.textContent = text;
  return el;
}

function createStartBar(queueCount) {
  var bar = document.createElement('div');
  bar.className = 'start-yolo-bar';

  var msgWrap = document.createElement('div');
  msgWrap.className = 'idle-msg';
  var dot = document.createElement('div');
  dot.className = 'idle-dot';
  var textWrap = document.createElement('div');
  var idleText = document.createElement('div');
  idleText.className = 'idle-text';
  idleText.textContent = 'YOLO is idle \u2014 ' + queueCount + ' tasks waiting';
  var idleSub = document.createElement('div');
  idleSub.className = 'idle-sub';
  idleSub.textContent = 'May be stuck in cooldown or stopped. Click to restart.';
  textWrap.appendChild(idleText);
  textWrap.appendChild(idleSub);
  msgWrap.appendChild(dot);
  msgWrap.appendChild(textWrap);

  var btn = document.createElement('button');
  btn.className = 'btn-start-yolo';
  btn.type = 'button';
  var playIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  playIcon.setAttribute('width', '16');
  playIcon.setAttribute('height', '16');
  playIcon.setAttribute('viewBox', '0 0 16 16');
  playIcon.setAttribute('fill', 'none');
  var playPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  playPath.setAttribute('d', 'M4 2.5v11l9-5.5L4 2.5z');
  playPath.setAttribute('fill', 'currentColor');
  playIcon.appendChild(playPath);
  btn.appendChild(playIcon);
  var btnLabel = document.createElement('span');
  btnLabel.textContent = 'Start YOLO';
  btn.appendChild(btnLabel);

  btn.addEventListener('click', function() {
    btnLabel.textContent = 'Starting...';
    btn.className = 'btn-start-yolo btn-start-yolo--working';
    btn.disabled = true;
    fetch(API_BASE + '/stop', { method: 'POST', credentials: 'same-origin' })
      .then(function() { return fetch(API_BASE + '/start', { method: 'POST', credentials: 'same-origin' }); })
      .then(function(r) { return r.json(); })
      .then(function(data) {
        btnLabel.textContent = data.ok ? 'Started!' : (data.msg || 'Error');
        setTimeout(refresh, 3000);
      })
      .catch(function() {
        btnLabel.textContent = 'Error \u2014 try again';
        btn.disabled = false;
        btn.className = 'btn-start-yolo';
      });
  });

  bar.appendChild(msgWrap);
  bar.appendChild(btn);
  return bar;
}

function refresh() {
  // If any task detail panel is open, only update stats — don't rebuild the DOM
  var skipDomRebuild = openTaskSlugs.size > 0;

  Promise.all([
    fetchJSON(API_BASE + '/status'),
    fetchJSON(API_BASE + '/queue'),
    fetchJSON(API_BASE + '/history')
  ]).then(function(results) {
    var status = results[0];
    var queue = results[1];
    var history = results[2];

    // Stats — always update these (lightweight, no modal disruption)
    var statsEl = document.getElementById('stats');
    statsEl.textContent = '';
    [['Queue', status.queueLength, ''], ['Success', status.totalSuccess, 'green'],
     ['Failed', status.totalFailed, 'red'], ['Runs', status.totalRuns, '']
    ].forEach(function(p) {
      var span = document.createElement('span');
      span.textContent = p[0] + ': ';
      var val = document.createElement('span');
      val.className = 'val' + (p[2] ? ' ' + p[2] : '');
      val.textContent = p[1];
      span.appendChild(val);
      statsEl.appendChild(span);
    });

    document.getElementById('live-dot').style.background =
      status.running ? 'var(--green)' : 'var(--amber)';

    // Update LIVE status text in header
    var liveStatusEl = document.getElementById('live-status-text');
    if (liveStatusEl) {
      if (status.running && status.startedAt) {
        var elapsed = formatElapsed(Date.now() - new Date(status.startedAt).getTime());
        liveStatusEl.textContent = 'Running \u2014 ' + elapsed;
        liveStatusEl.style.color = 'var(--green)';
      } else {
        liveStatusEl.textContent = 'Idle \u2014 waiting for next task';
        liveStatusEl.style.color = 'var(--amber)';
      }
    }

    // Stop live output polling when no task is running
    if (!status.running && liveOutputInterval) {
      clearInterval(liveOutputInterval);
      liveOutputInterval = null;
      lastLiveOutputLength = 0;
    }

    // Skip full DOM rebuild if a task detail panel is open
    if (skipDomRebuild) return;

    var container = document.getElementById('container');
    container.textContent = '';

    // Running — expanded card with timer and details
    if (status.currentTask) {
      container.appendChild(createLabel('Running'));
      var list = document.createElement('div');
      list.className = 'task-list';
      list.appendChild(createRunningCard(status.currentTask, status.startedAt));
      container.appendChild(list);
    } else {
      if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }

      // Show Start YOLO button when idle with tasks in queue
      if (queue.length > 0) {
        container.appendChild(createStartBar(queue.length));
      }
    }

    // Queued
    var queued = queue.filter(function(t) { return t.name !== status.currentTask; });
    if (queued.length > 0) {
      container.appendChild(createLabel('Queued (' + queued.length + ')'));
      var qList = document.createElement('div');
      qList.className = 'task-list';
      queued.forEach(function(t, i) { qList.appendChild(createTask(t.name, 'queued', null, i + 1)); });
      container.appendChild(qList);
    }

    // History
    var recent = history.slice(-20).reverse();
    if (recent.length > 0) {
      container.appendChild(createLabel('Completed'));
      var hList = document.createElement('div');
      hList.className = 'task-list';
      recent.forEach(function(h) {
        var st = h.status === 'success' ? 'done' : 'failed';
        hList.appendChild(createTask(h.task, st, h.duration));
      });
      container.appendChild(hList);
    }

    // Background Processes section
    fetchJSON(API_BASE + '/background').then(function(bg) {
      if (!bg || !bg.processes || bg.processes.length === 0) return;
      container.appendChild(createLabel('Background Processes (' + bg.count + ')'));
      var bgList = document.createElement('div');
      bgList.className = 'task-list';
      bg.processes.forEach(function(p) {
        var card = document.createElement('div');
        card.className = 'task-card';
        card.style.borderLeft = '3px solid #F59E0B';
        var inner = document.createElement('div');
        inner.style.cssText = 'display:flex;justify-content:space-between;align-items:center;';
        var info = document.createElement('div');
        info.style.flex = '1';
        var nameEl = document.createElement('div');
        nameEl.style.cssText = 'font-weight:600;font-size:13px;color:#F59E0B;';
        nameEl.textContent = p.cmd.substring(0, 80);
        info.appendChild(nameEl);
        if (p.lastLine) {
          var lineEl = document.createElement('div');
          lineEl.style.cssText = 'font-size:11px;color:#8888A0;margin-top:2px;font-family:monospace;';
          lineEl.textContent = p.lastLine.substring(0, 120);
          info.appendChild(lineEl);
        }
        var stats = document.createElement('div');
        stats.style.cssText = 'text-align:right;font-size:11px;color:#8888A0;white-space:nowrap;margin-left:12px;';
        stats.textContent = 'PID ' + p.pid + ' | CPU ' + p.cpu + '% | ' + p.mem;
        inner.appendChild(info);
        inner.appendChild(stats);
        card.appendChild(inner);
        bgList.appendChild(card);
      });
      container.appendChild(bgList);
    }).catch(function() {});

    var note = document.createElement('div');
    note.className = 'refresh-note';
    note.textContent = 'Auto-refreshes every 5s \u00b7 Timer updates every 1s';
    container.appendChild(note);
  }).catch(function(e) { console.error('Refresh error:', e); });
}

// Live Feed Panel
var panelOpen = false;
var feedInterval = null;
var lastFeedLength = 0;

document.getElementById('panel-toggle').addEventListener('click', function() {
  panelOpen = !panelOpen;
  document.getElementById('live-panel').classList.toggle('live-panel--open', panelOpen);
  document.getElementById('panel-toggle').classList.toggle('live-panel-toggle--active', panelOpen);
  document.body.classList.toggle('panel-open', panelOpen);
  if (panelOpen) {
    refreshFeed();
    if (!feedInterval) feedInterval = setInterval(function() { if (openTaskSlugs.size === 0) refreshFeed(); }, 5000);
  } else {
    if (feedInterval) { clearInterval(feedInterval); feedInterval = null; }
  }
});

function refreshFeed() {
  fetchJSON(API_BASE + '/live-log').then(function(lines) {
    var feed = document.getElementById('live-feed');
    var countEl = document.getElementById('line-count');

    // Only update if new lines
    if (lines.length === lastFeedLength) return;
    lastFeedLength = lines.length;

    feed.textContent = '';
    countEl.textContent = lines.length + ' lines';

    if (lines.length === 0) {
      var empty = document.createElement('div');
      empty.className = 'live-empty';
      empty.textContent = 'Waiting for task output...';
      feed.appendChild(empty);
      return;
    }

    lines.forEach(function(line) {
      var el = document.createElement('div');
      el.className = 'live-line';

      // Classify line
      var t = line.text || '';
      if (t.match(/DW[A-Z]{2}-\d+|SKU|sku|mfr_sku/i)) el.className += ' live-line--sku';
      else if (t.match(/error|fail|FAIL|Error|exception/i)) el.className += ' live-line--error';
      else if (t.match(/\u2713|\u2705|success|complete|done|created|updated|pushed/i)) el.className += ' live-line--info';

      // Timestamp
      var ts = document.createElement('span');
      ts.className = 'ts';
      var d = new Date(line.ts);
      ts.textContent = String(d.getHours()).padStart(2,'0') + ':' + String(d.getMinutes()).padStart(2,'0') + ':' + String(d.getSeconds()).padStart(2,'0');
      el.appendChild(ts);

      // Text
      var txt = document.createTextNode(t.length > 200 ? t.substring(0, 197) + '...' : t);
      el.appendChild(txt);

      feed.appendChild(el);
    });

    // Auto-scroll to bottom
    feed.scrollTop = feed.scrollHeight;
  }).catch(function() {});
}

var liveOutputInterval = null;
var lastLiveHash = '';
var sessionLog = [];

function startLiveOutputPolling() {
  if (liveOutputInterval) clearInterval(liveOutputInterval);
  function doFetch() {
    var el = document.getElementById('live-output');
    if (!el) { clearInterval(liveOutputInterval); liveOutputInterval = null; return; }
    fetchJSON(API_BASE + '/live-log').then(function(lines) {
      if (!Array.isArray(lines) || lines.length === 0) {
        if (!sessionLog.length) el.textContent = 'Waiting for Claude output...';
        return;
      }
      // APPEND only — never clear, never flash
      // Find new lines by comparing to what we already have
      lines.forEach(function(line) {
        var t = line.text || String(line);
        // Dedup: skip if last session log entry is identical
        if (sessionLog.length && sessionLog[sessionLog.length-1].text === t) return;
        sessionLog.push({ ts: line.ts || new Date().toISOString(), text: t, source: line.source || '' });
        // Append to live output div
        if (el.childElementCount === 0 || el.textContent === 'Waiting for Claude output...') el.textContent = '';
        var d = new Date(line.ts || Date.now());
        var ts = String(d.getHours()).padStart(2,'0') + ':' +
                 String(d.getMinutes()).padStart(2,'0') + ':' +
                 String(d.getSeconds()).padStart(2,'0');
        var lineEl = document.createElement('div');
        lineEl.style.padding = '1px 0';
        var tsSpan = document.createElement('span');
        tsSpan.style.cssText = 'color:#444;margin-right:8px;font-size:10px;';
        tsSpan.textContent = ts;
        lineEl.appendChild(tsSpan);
        var color = '#a0a0a0';
        if (t.match(/error|fail|FAIL|Error|exception/i)) color = '#ef4444';
        else if (t.match(/✅|success|complete|done|created|updated|pushed/i)) color = '#22c55e';
        else if (t.match(/DW[A-Z]{2}-\d+|SKU|sku|mfr_sku/i)) color = '#3b82f6';
        var textSpan = document.createElement('span');
        textSpan.style.color = color;
        textSpan.textContent = t;
        lineEl.appendChild(textSpan);
        el.appendChild(lineEl);
      });
      el.scrollTop = el.scrollHeight;
    }).catch(function() {});
  }
  doFetch();
  liveOutputInterval = setInterval(doFetch, 3000);
}

refresh();
setInterval(function() {
  // NEVER refresh if user has a panel open
  if (openTaskSlugs.size > 0) return;
  refresh();
}, 10000);

// ALWAYS poll live log every 3s — independent of task card rendering
setInterval(function() {
  fetchJSON(API_BASE + '/live-log').then(function(lines) {
    if (!Array.isArray(lines) || lines.length === 0) return;
    var hash = lines[lines.length-1].text || '';
    if (hash === lastLiveHash) return;
    lastLiveHash = hash;
    lines.forEach(function(line) {
      var t = line.text || String(line);
      if (!sessionLog.length || sessionLog[sessionLog.length-1].text !== t) {
        sessionLog.push({ ts: line.ts || new Date().toISOString(), text: t, source: line.source || '' });
      }
    });
    var el = document.getElementById('live-output');
    if (el) {
      el.textContent = '';
      lines.forEach(function(line) {
        var t = line.text || String(line);
        var d = document.createElement('div');
        d.style.padding = '1px 0';
        d.textContent = t;
        if (t.match(/error|fail/i)) d.style.color = '#ef4444';
        else if (t.match(/✅|success|done|complete/i)) d.style.color = '#22c55e';
        el.appendChild(d);
      });
      el.scrollTop = el.scrollHeight;
    }
    var sBody = document.getElementById('slog-body');
    var sTitle = document.getElementById('slog-title');
    if (sBody) {
      var rendered = sBody.childElementCount;
      for (var i = rendered; i < sessionLog.length; i++) {
        var sl = sessionLog[i];
        var sdiv = document.createElement('div');
        sdiv.style.cssText = 'padding:1px 0;border-bottom:1px solid #111;';
        var dd = new Date(sl.ts);
        var tss = String(dd.getHours()).padStart(2,'0')+':'+String(dd.getMinutes()).padStart(2,'0')+':'+String(dd.getSeconds()).padStart(2,'0');
        var tsSpan = document.createElement('span');
        tsSpan.style.cssText = 'color:#555;font-size:10px;margin-right:6px;';
        tsSpan.textContent = tss;
        var txtSpan = document.createElement('span');
        txtSpan.textContent = sl.text;
        if (sl.text.match(/error|fail/i)) txtSpan.style.color = '#ef4444';
        else if (sl.text.match(/✅|done|complete|updated/i)) txtSpan.style.color = '#22c55e';
        sdiv.appendChild(tsSpan);
        sdiv.appendChild(txtSpan);
        sBody.appendChild(sdiv);
      }
      sBody.scrollTop = sBody.scrollHeight;
    }
    if (sTitle) sTitle.textContent = 'Session Log (' + sessionLog.length + ' lines)';
  }).catch(function(){});
}, 3000);
</script>

<div style="max-width:900px;margin:32px auto;padding:0 16px;">
  <div onclick="var b=document.getElementById('slog-body');b.style.display=b.style.display==='none'?'block':'none'" style="cursor:pointer;padding:10px 16px;background:#1A1A22;border-radius:6px 6px 0 0;display:flex;justify-content:space-between;">
    <span style="color:#10B981;font-weight:600;font-size:13px;" id="slog-title">Session Log (0 lines)</span>
    <span style="color:#8888A0;">&#9660;</span>
  </div>
  <div id="slog-body" style="background:#0a0a0a;border:1px solid #1A1A22;border-radius:0 0 6px 6px;max-height:400px;overflow-y:auto;padding:8px 12px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a0a0a0;line-height:1.5;white-space:pre-wrap;"></div>
</div>
</body>
</html>