import { modalManager } from './ModalManager.js'; import { showToast } from '../utils/uiHelpers.js'; import { translate } from '../utils/i18nHelpers.js'; import { escapeHtml } from '../components/shared/utils.js'; const MAX_CONSOLE_ENTRIES = 200; function stringifyConsoleArg(value) { if (typeof value === 'string') { return value; } try { return JSON.stringify(value); } catch (_error) { return String(value); } } export class DoctorManager { constructor() { this.initialized = false; this.lastDiagnostics = null; this.consoleEntries = []; } initialize() { if (this.initialized) { return; } this.triggerButton = document.getElementById('doctorTriggerBtn'); this.badge = document.getElementById('doctorStatusBadge'); this.modal = document.getElementById('doctorModal'); this.issuesList = document.getElementById('doctorIssuesList'); this.summaryText = document.getElementById('doctorSummaryText'); this.summaryBadge = document.getElementById('doctorSummaryBadge'); this.loadingState = document.getElementById('doctorLoadingState'); this.refreshButton = document.getElementById('doctorRefreshBtn'); this.exportButton = document.getElementById('doctorExportBtn'); this.installConsoleCapture(); this.bindEvents(); this.initialized = true; } bindEvents() { if (this.triggerButton) { this.triggerButton.addEventListener('click', async () => { modalManager.showModal('doctorModal'); await this.refreshDiagnostics(); }); } if (this.refreshButton) { this.refreshButton.addEventListener('click', async () => { await this.refreshDiagnostics(); }); } if (this.exportButton) { this.exportButton.addEventListener('click', async () => { await this.exportBundle(); }); } } installConsoleCapture() { if (window.__lmDoctorConsolePatched) { this.consoleEntries = window.__lmDoctorConsoleEntries || []; return; } const originalConsole = {}; const levels = ['log', 'info', 'warn', 'error', 'debug']; window.__lmDoctorConsoleEntries = this.consoleEntries; levels.forEach((level) => { const original = console[level]?.bind(console); originalConsole[level] = original; console[level] = (...args) => { this.consoleEntries.push({ level, timestamp: new Date().toISOString(), message: args.map(stringifyConsoleArg).join(' '), }); if (this.consoleEntries.length > MAX_CONSOLE_ENTRIES) { this.consoleEntries.splice(0, this.consoleEntries.length - MAX_CONSOLE_ENTRIES); } if (original) { original(...args); } }; }); window.__lmDoctorConsolePatched = true; } getClientVersion() { return document.body?.dataset?.appVersion || ''; } setLoading(isLoading) { if (this.loadingState) { this.loadingState.classList.toggle('visible', isLoading); } if (this.refreshButton) { this.refreshButton.disabled = isLoading; } if (this.exportButton) { this.exportButton.disabled = isLoading; } } async refreshDiagnostics({ silent = false } = {}) { this.setLoading(true); try { const clientVersion = encodeURIComponent(this.getClientVersion()); const response = await fetch(`/api/lm/doctor/diagnostics?clientVersion=${clientVersion}`); const payload = await response.json(); if (!response.ok || payload.success === false) { throw new Error(payload.error || 'Failed to load doctor diagnostics'); } this.lastDiagnostics = payload; this.updateTriggerState(payload.summary); this.renderDiagnostics(payload); } catch (error) { console.error('Doctor diagnostics failed:', error); if (!silent) { showToast('doctor.toast.loadFailed', { message: error.message }, 'error'); } } finally { this.setLoading(false); } } updateTriggerState(summary = {}) { if (!this.badge || !this.triggerButton) { return; } const issueCount = Number(summary.issue_count || 0); this.badge.textContent = issueCount > 9 ? '9+' : String(issueCount); this.badge.classList.toggle('hidden', issueCount === 0); this.triggerButton.classList.remove('doctor-status-warning', 'doctor-status-error'); if (summary.status === 'error') { this.triggerButton.classList.add('doctor-status-error'); } else if (summary.status === 'warning') { this.triggerButton.classList.add('doctor-status-warning'); } } renderDiagnostics(payload) { if (!this.modal || !this.issuesList || !this.summaryText || !this.summaryBadge) { return; } const { summary = {}, diagnostics = [] } = payload; this.summaryText.textContent = this.getSummaryText(summary); this.summaryBadge.className = `doctor-summary-badge ${this.getStatusClass(summary.status)}`; this.summaryBadge.innerHTML = ` ${escapeHtml(this.getStatusLabel(summary.status))} `; this.issuesList.innerHTML = diagnostics.map((item) => this.renderIssueCard(item)).join(''); this.attachIssueActions(); } getSummaryText(summary) { if (summary.status === 'error') { return translate( 'doctor.summary.error', { count: summary.issue_count || 0 }, `${summary.issue_count || 0} issue(s) need attention before the app is fully healthy.` ); } if (summary.status === 'warning') { return translate( 'doctor.summary.warning', { count: summary.issue_count || 0 }, `${summary.issue_count || 0} issue(s) were found. Most can be fixed directly from this panel.` ); } return translate( 'doctor.summary.ok', {}, 'No active issues were found in the current environment.' ); } getStatusClass(status) { if (status === 'error') { return 'doctor-status-error'; } if (status === 'warning') { return 'doctor-status-warning'; } return 'doctor-status-ok'; } getStatusLabel(status) { return translate(`doctor.status.${status || 'ok'}`, {}, status || 'ok'); } renderIssueCard(item) { const status = item.status || 'ok'; const tagLabel = this.getStatusLabel(status); const details = Array.isArray(item.details) ? item.details : []; const listItems = details .filter((detail) => typeof detail === 'string') .map((detail) => `
${escapeHtml(item.summary || '')}