← 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 — 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;">▼</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>