import { readFileSync, existsSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; interface ReviewDimensionInfo { count: number; [key: string]: unknown; } interface ReviewData { totalFindings: number; severityDistribution: Record; dimensionSummary: Record; [key: string]: unknown; } interface SessionTaskData { status?: string; title?: string; task_id?: string; [key: string]: unknown; } interface SessionData { session_id?: string; project?: string; created_at?: string; tasks: SessionTaskData[]; taskCount: number; [key: string]: unknown; } interface DashboardStatistics { totalSessions: number; activeSessions: number; totalTasks: number; completedTasks: number; [key: string]: unknown; } interface DashboardData { generatedAt?: string; activeSessions: SessionData[]; archivedSessions: SessionData[]; statistics: DashboardStatistics; reviewData?: ReviewData; liteTasks?: { litePlan?: unknown[]; liteFix?: unknown[]; [key: string]: unknown; }; projectPath?: string; recentPaths?: string[]; [key: string]: unknown; } const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Bundled template paths (from dist/core/ -> src/templates/) const UNIFIED_TEMPLATE = join(__dirname, '../../src/templates/dashboard.html'); const JS_FILE = join(__dirname, '../../src/templates/dashboard.js'); const MODULE_CSS_DIR = join(__dirname, '../../src/templates/dashboard-css'); const WORKFLOW_TEMPLATE = join(__dirname, '../../src/templates/workflow-dashboard.html'); const REVIEW_TEMPLATE = join(__dirname, '../../src/templates/review-cycle-dashboard.html'); // Modular CSS files in load order const MODULE_CSS_FILES = [ '01-base.css', '02-session.css', '03-tasks.css', '04-lite-tasks.css', '05-context.css', '06-cards.css', '07-managers.css', '08-review.css', '09-explorer.css', // CLI modules (split from 10-cli.css) '10-cli-status.css', '11-cli-history.css', '12-cli-legacy.css', '13-cli-ccw.css', '14-cli-modals.css', '15-cli-endpoints.css', '16-cli-session.css', '17-cli-conversation.css', '18-cli-settings.css', '19-cli-native-session.css', '20-cli-taskqueue.css', '21-cli-toolmgmt.css', '22-cli-semantic.css', // Other modules '23-memory.css', '24-prompt-history.css', '25-skills-rules.css', '26-claude-manager.css', '27-graph-explorer.css', '28-mcp-manager.css', '29-help.css', '30-core-memory.css', '31-api-settings.css', '32-issue-manager.css', '33-cli-stream-viewer.css', '34-discovery.css', '36-loop-monitor.css' ]; const MODULE_FILES = [ 'i18n.js', // Must be loaded first for translations 'utils.js', 'state.js', 'services.js', // CacheManager, EventManager, PreloadService - must be before main.js 'api.js', 'components/theme.js', 'components/modals.js', 'components/navigation.js', 'components/sidebar.js', 'components/tabs-context.js', 'components/tabs-other.js', 'components/task-drawer-core.js', 'components/task-drawer-renderers.js', 'components/flowchart.js', 'components/carousel.js', 'components/notifications.js', 'components/global-notifications.js', 'components/task-queue-sidebar.js', 'components/cli-status.js', 'components/cli-history.js', 'components/mcp-manager.js', 'components/hook-manager.js', 'components/version-check.js', 'components/storage-manager.js', 'components/index-manager.js', 'views/home.js', 'views/project-overview.js', 'views/session-detail.js', 'views/review-session.js', 'views/lite-tasks.js', 'views/fix-session.js', 'views/cli-manager.js', 'views/codexlens-manager.js', 'views/explorer.js', 'views/mcp-manager.js', 'views/hook-manager.js', 'views/history.js', 'views/graph-explorer.js', 'views/memory.js', 'views/core-memory.js', 'views/core-memory-graph.js', 'views/core-memory-clusters.js', 'views/prompt-history.js', 'views/skills-manager.js', 'views/rules-manager.js', 'views/claude-manager.js', 'views/api-settings.js', 'views/issue-manager.js', 'views/issue-discovery.js', 'views/help.js', 'main.js' ]; /** * Generate dashboard HTML from aggregated data * Uses bundled templates from ccw package * @param {Object} data - Aggregated dashboard data * @returns {Promise} - Generated HTML */ export async function generateDashboard(data: unknown): Promise { const dashboardData = (data ?? {}) as DashboardData; // Use new unified template (with sidebar layout) if (existsSync(UNIFIED_TEMPLATE)) { return generateFromUnifiedTemplate(dashboardData); } // Fallback to legacy workflow template if (existsSync(WORKFLOW_TEMPLATE)) { return generateFromBundledTemplate(dashboardData, WORKFLOW_TEMPLATE); } // Fallback to inline dashboard if templates missing return generateInlineDashboard(dashboardData); } /** * Generate dashboard using unified template (new sidebar layout) * @param {Object} data - Dashboard data * @returns {string} - Generated HTML */ function generateFromUnifiedTemplate(data: DashboardData): string { let html = readFileSync(UNIFIED_TEMPLATE, 'utf8'); // Read and concatenate modular CSS files in load order let cssContent = MODULE_CSS_FILES.map(file => { const filePath = join(MODULE_CSS_DIR, file); return existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''; }).join('\n\n'); // Read JS content let jsContent = ''; const moduleBase = join(__dirname, '../../src/templates/dashboard-js'); if (existsSync(moduleBase)) { jsContent = MODULE_FILES.map(file => { const filePath = join(moduleBase, file); return existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''; }).join('\n\n'); } else if (existsSync(JS_FILE)) { jsContent = readFileSync(JS_FILE, 'utf8'); } // Prepare complete workflow data const workflowData = { generatedAt: data.generatedAt || new Date().toISOString(), activeSessions: data.activeSessions || [], archivedSessions: data.archivedSessions || [], liteTasks: data.liteTasks || { litePlan: [], liteFix: [] }, reviewData: data.reviewData || { dimensions: {} }, statistics: data.statistics || { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, litePlanCount: 0, liteFixCount: 0 } }; // Get project path and recent paths const projectPath = data.projectPath || process.cwd(); const recentPaths = data.recentPaths || [projectPath]; // Replace JS placeholders with actual data jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(workflowData, null, 2)); jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, projectPath.replace(/\\/g, '/')); jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(recentPaths)); // Inject platform information for cross-platform MCP command generation // 'win32' for Windows, 'darwin' for macOS, 'linux' for Linux jsContent = jsContent.replace(/\{\{SERVER_PLATFORM\}\}/g, process.platform); // Inject JS and CSS into HTML template html = html.replace('{{JS_CONTENT}}', jsContent); html = html.replace('{{CSS_CONTENT}}', cssContent); // Also replace any remaining placeholders in HTML html = html.replace(/\{\{PROJECT_PATH\}\}/g, projectPath.replace(/\\/g, '/')); return html; } /** * Generate dashboard using bundled template * @param {Object} data - Dashboard data * @param {string} templatePath - Path to workflow-dashboard.html * @returns {string} - Generated HTML */ function generateFromBundledTemplate(data: DashboardData, templatePath: string): string { let html = readFileSync(templatePath, 'utf8'); // Prepare workflow data for injection const workflowData = { activeSessions: data.activeSessions, archivedSessions: data.archivedSessions }; // Inject workflow data html = html.replace('{{WORKFLOW_DATA}}', JSON.stringify(workflowData, null, 2)); // If we have review data, add a review tab if (data.reviewData && data.reviewData.totalFindings > 0) { html = injectReviewTab(html, data.reviewData); } return html; } /** * Inject review tab into existing dashboard * @param {string} html - Base dashboard HTML * @param {Object} reviewData - Review data to display * @returns {string} - Modified HTML with review tab */ function injectReviewTab(html: string, reviewData: ReviewData): string { // Add review tab button in header controls const tabButtonHtml = ` `; // Insert after filter-group html = html.replace( '\n \n ', `
${tabButtonHtml}
` ); // Add review section before closing container const reviewSectionHtml = generateReviewSection(reviewData); html = html.replace( '\n\n ${hasReviews ? '' : ''}
${stats.totalSessions}
Total Sessions
${stats.activeSessions}
Active Sessions
${stats.totalTasks}
Total Tasks
${stats.completedTasks}
Completed Tasks

Active Sessions

${data.activeSessions.length === 0 ? '
No active sessions
' : data.activeSessions.map(s => renderSessionCard(s, true)).join('')}

Archived Sessions

${data.archivedSessions.length === 0 ? '
No archived sessions
' : data.archivedSessions.map(s => renderSessionCard(s, false)).join('')}
${hasReviews ? renderReviewTab(data.reviewData as ReviewData) : ''} `; } /** * Render a session card * @param {Object} session - Session data * @param {boolean} isActive - Whether session is active * @returns {string} - HTML string */ function renderSessionCard(session: SessionData, isActive: boolean): string { const completedTasks = isActive ? session.tasks.filter(t => t.status === 'completed').length : session.taskCount; const totalTasks = isActive ? session.tasks.length : session.taskCount; const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; const tasksHtml = isActive && session.tasks.length > 0 ? session.tasks.map(t => `
${t.title}
${t.task_id}
`).join('') : ''; return `
${session.session_id}
${session.project ? `
${session.project}
` : ''}
${session.created_at} | ${completedTasks}/${totalTasks} tasks
${totalTasks > 0 ? `
` : ''} ${tasksHtml}
`; } /** * Render review tab HTML * @param {Object} reviewData - Review data * @returns {string} - HTML string */ function renderReviewTab(reviewData: ReviewData): string { const { severityDistribution, dimensionSummary } = reviewData; return ` `; }