// control-panel.js — Header / Status / Primary controls / Error banner / Toast import { t } from './i18n.js'; // ---------- Server state 常數 ---------- // 必須與 Go 端 server_control.go §ServerState 常數對齊(全部 lowercase)。 // 千萬不要改成 PascalCase,會讓 state 比對全面失敗。 export const STATE_IDLE = 'idle'; export const STATE_STARTING = 'starting'; export const STATE_RUNNING = 'running'; export const STATE_STOPPING = 'stopping'; export const STATE_STOPPED = 'stopped'; export const STATE_ERROR = 'error'; // ---------- Server state ---------- export function setServerState(status) { const root = document.getElementById('app'); const dot = document.getElementById('status-dot'); const textEl = document.getElementById('status-text'); if (!status) return; const s = status.state || STATE_IDLE; root.setAttribute('data-state', s); dot.className = 'status-dot state-' + s; let text; switch (s) { case STATE_IDLE: text = t('control.status.idle'); break; case STATE_STARTING: text = t('control.status.starting'); break; case STATE_RUNNING: text = t('control.status.running'); if (status.port) text += ' · :' + status.port; break; case STATE_STOPPING: text = t('control.status.stopping'); break; case STATE_STOPPED: text = t('control.status.stopped'); break; case STATE_ERROR: text = t('control.status.error', { reason: (status.lastError || '').slice(0, 80) }); break; default: text = s; } textEl.textContent = text; } // ---------- Server meta ---------- let headerClockTimer = null; export function updateServerMeta(status) { const portEl = document.getElementById('meta-port'); const pidEl = document.getElementById('meta-pid'); portEl.textContent = status && status.port ? String(status.port) : '—'; pidEl.textContent = status && status.pid ? String(status.pid) : '—'; // uptime 由 initHeaderClock 定時刷新 } export function initHeaderClock(getServer) { if (headerClockTimer) clearInterval(headerClockTimer); const uptimeEl = document.getElementById('meta-uptime'); headerClockTimer = setInterval(() => { const s = getServer(); if (!s || !s.startedAt || s.state !== STATE_RUNNING) { uptimeEl.textContent = '—'; return; } const ms = Date.now() - s.startedAt; uptimeEl.textContent = formatUptime(ms); }, 1000); } function formatUptime(ms) { if (ms < 0) ms = 0; const s = Math.floor(ms / 1000); const hh = String(Math.floor(s / 3600)).padStart(2, '0'); const mm = String(Math.floor((s % 3600) / 60)).padStart(2, '0'); const ss = String(s % 60).padStart(2, '0'); return `${hh}:${mm}:${ss}`; } // ---------- Primary controls enable/disable ---------- export function updatePrimaryControls(status) { const s = status && status.state; const openBtn = document.getElementById('btn-open-browser'); const startBtn = document.getElementById('btn-start'); const manageBtn = document.getElementById('btn-manage'); const miStop = document.getElementById('mi-stop'); const miRestart = document.getElementById('mi-restart'); openBtn.disabled = s !== STATE_RUNNING; startBtn.disabled = !(s === STATE_STOPPED || s === STATE_IDLE || s === STATE_ERROR); manageBtn.disabled = !(s === STATE_RUNNING || s === STATE_ERROR); miStop.disabled = s !== STATE_RUNNING; miRestart.disabled = !(s === STATE_RUNNING || s === STATE_ERROR); } // ---------- Error banner ---------- export function showErrorBanner(errorMsg) { const banner = document.getElementById('error-banner'); const desc = document.getElementById('banner-desc'); desc.textContent = errorMsg || ''; banner.removeAttribute('hidden'); } export function hideErrorBanner() { const banner = document.getElementById('error-banner'); banner.setAttribute('hidden', ''); } // ---------- Primary CTA pulse(引導使用者點擊 Open in Browser) ---------- // 對齊 Design Spec v2.1 startup-progress.md §4.1(stage 6 manual hint mode) // 於 stage 5 skipped 時由 app.js 主動開啟,成功建立 WS 連線或 panel 關閉時取消 export function setPrimaryCTAPulse(enabled) { const openBtn = document.getElementById('btn-open-browser'); if (!openBtn) return; if (enabled) { openBtn.classList.add('pulse-cta'); } else { openBtn.classList.remove('pulse-cta'); } } // ---------- Toast ---------- let toastTimer = null; export function showToast(message, duration = 3000) { const el = document.getElementById('toast'); if (!el) return; el.textContent = message; el.removeAttribute('hidden'); if (toastTimer) clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.setAttribute('hidden', ''); }, duration); }