/* Dashboard — overview statistics, pipeline flow, task progress */ var _dashTimer = null; var _dashVisible = true; var _dashController = null; var _dashLoadEpoch = 0; // ── Formatting ──────────────────────────────────────── function fmtNum(n) { if (n == null) return '0'; return n.toLocaleString('en-US'); } function fmtToday(n) { if (!n || n <= 0) return ''; return '+' + fmtNum(n) + ' today'; } function timeAgo(isoStr) { if (!isoStr) return ''; var diff = Date.now() - new Date(isoStr).getTime(); var sec = Math.floor(diff / 1000); if (sec < 60) return 'just now'; if (sec < 3600) return Math.floor(sec / 60) + 'm ago'; if (sec < 86400) return Math.floor(sec / 3600) + 'h ago'; return Math.floor(sec / 86400) + 'd ago'; } function statusBadge(status) { var map = { 'completed': { cls: 'bg-yellow-900/50 text-yellow-400', label: t('stat_reviewing') }, 'to_review': { cls: 'bg-yellow-900/50 text-yellow-400', label: t('stat_reviewing') }, 'reviewed': { cls: 'bg-green-900/50 text-green-400', label: t('stat_approved') }, 'approved': { cls: 'bg-green-900/50 text-green-400', label: t('stat_approved') }, 'processing': { cls: 'bg-blue-900/50 text-blue-400', label: t('stat_processing') }, 'received': { cls: 'bg-indigo-900/50 text-indigo-400', label: t('stat_received') }, 'failed': { cls: 'bg-red-900/50 text-red-400', label: t('stat_failed') }, }; var m = map[status] || { cls: 'bg-gray-800 text-gray-400', label: status || 'unknown' }; return '' + m.label + ''; } // ── Load all data ───────────────────────────────────── async function refreshDashboard() { if (_dashController) _dashController.abort(); var controller = new AbortController(); _dashController = controller; var loadEpoch = ++_dashLoadEpoch; var btn = document.getElementById('btn-refresh'); if (btn) { btn.disabled = true; var icon = btn.querySelector('iconify-icon'); if (icon) icon.setAttribute('icon', 'ant-design:loading-outlined'); } // These four panels are independent. Request them together so a slow // remote-storage read cannot delay all of the other dashboard sections. const results = await Promise.allSettled([ fetch('/api/v1/dashboard/overview', { signal: controller.signal }), fetch('/api/v1/dashboard/recent-episodes?limit=6', { signal: controller.signal }), fetch('/api/v1/projects/summary?limit=5', { signal: controller.signal }), fetch('/api/v1/dashboard/trend?days=30', { signal: controller.signal }), ]); if (loadEpoch !== _dashLoadEpoch) return; const [overview, recent, projectSummary, trend] = results; try { if (overview.status !== 'fulfilled' || !overview.value.ok) { throw new Error('Overview request failed'); } renderStats(await overview.value.json()); hideError(); } catch (e) { showError(); } try { if (recent.status === 'fulfilled' && recent.value.ok) { renderRecent((await recent.value.json()).episodes || []); } } catch (e) { /* silent */ } try { if (projectSummary.status === 'fulfilled' && projectSummary.value.ok) { renderProjectSummary((await projectSummary.value.json()).tasks || []); } } catch (e) { /* silent */ } try { if (trend.status === 'fulfilled' && trend.value.ok) { const trendData = await trend.value.json(); renderTrend(trendData.labels || [], trendData.counts || []); } } catch (e) { /* silent */ } if (btn && _dashController === controller) { btn.disabled = false; var icon = btn.querySelector('iconify-icon'); if (icon) icon.setAttribute('icon', 'ant-design:reload-outlined'); } if (_dashController === controller) _dashController = null; } // ── 4 Stat cards ────────────────────────────────────── function setCardValue(id, value) { var el = document.getElementById(id); if (el) el.textContent = fmtNum(value); } function setCardToday(id, today) { var el = document.getElementById(id); if (!el) return; var text = fmtToday(today); if (text) { el.textContent = text; el.classList.remove('hidden'); } else { el.classList.add('hidden'); } } function renderStats(data) { document.querySelectorAll('.stat-card-skeleton').forEach(function(el) { el.remove(); }); var real = document.getElementById('stat-cards-real'); if (real) real.classList.remove('hidden'); setCardValue('stat-total-total', data.total ? data.total.total : 0); setCardToday('stat-total-today', data.total ? data.total.today : 0); var reviewing = data.reviewing ? data.reviewing.total : 0; var approved = data.approved ? data.approved.total : 0; var failed = data.failed ? data.failed.total : 0; setCardValue('stat-reviewing-total', reviewing); setCardToday('stat-reviewing-today', data.reviewing ? data.reviewing.today : 0); setCardValue('stat-approved-total', approved); setCardToday('stat-approved-today', data.approved ? data.approved.today : 0); setCardValue('stat-failed-total', failed); // Donut chart renderDonut(reviewing, approved, failed); } // ── Projects (原 Task progress,任务概念已移除) ─────── function renderProjectSummary(projects) { var el = document.getElementById('task-progress-list'); if (!el) return; if (!projects || projects.length === 0) { el.innerHTML = `