diff --git a/ccw/src/commands/install.js b/ccw/src/commands/install.js index 69fe10cd..eca00f67 100644 --- a/ccw/src/commands/install.js +++ b/ccw/src/commands/install.js @@ -1,12 +1,13 @@ -import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, rmSync } from 'fs'; import { join, dirname, basename, relative } from 'path'; -import { homedir } from 'os'; +import { homedir, tmpdir } from 'os'; import { fileURLToPath } from 'url'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { showHeader, showBanner, createSpinner, success, info, warning, error, summaryBox, step, divider } from '../utils/ui.js'; import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findManifest, getAllManifests } from '../core/manifest.js'; import { validatePath } from '../utils/path-resolver.js'; +import { fetchLatestRelease, fetchLatestCommit, downloadAndExtract, REPO_URL } from '../utils/version-fetcher.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/ccw/src/core/dashboard-generator-patch.js b/ccw/src/core/dashboard-generator-patch.js new file mode 100644 index 00000000..9b9ab0e8 --- /dev/null +++ b/ccw/src/core/dashboard-generator-patch.js @@ -0,0 +1,29 @@ +// Add after line 13 (after REVIEW_TEMPLATE constant) + +// Modular dashboard JS files (in dependency order) +const MODULE_FILES = [ + // Base (no dependencies) + 'dashboard-js/state.js', + 'dashboard-js/utils.js', + 'dashboard-js/api.js', + // Components (independent) + 'dashboard-js/components/theme.js', + 'dashboard-js/components/sidebar.js', + 'dashboard-js/components/modals.js', + 'dashboard-js/components/flowchart.js', + // Components (dependent) + 'dashboard-js/components/task-drawer-renderers.js', + 'dashboard-js/components/task-drawer-core.js', + 'dashboard-js/components/tabs-context.js', + 'dashboard-js/components/tabs-other.js', + // Views + 'dashboard-js/views/home.js', + 'dashboard-js/views/project-overview.js', + 'dashboard-js/views/review-session.js', + 'dashboard-js/views/fix-session.js', + 'dashboard-js/views/lite-tasks.js', + 'dashboard-js/views/session-detail.js', + // Navigation & Main + 'dashboard-js/components/navigation.js', + 'dashboard-js/main.js' +]; diff --git a/ccw/src/core/dashboard-generator.js b/ccw/src/core/dashboard-generator.js index 4665c01b..4b7e9e06 100644 --- a/ccw/src/core/dashboard-generator.js +++ b/ccw/src/core/dashboard-generator.js @@ -12,6 +12,28 @@ const CSS_FILE = join(__dirname, '../templates/dashboard.css'); const WORKFLOW_TEMPLATE = join(__dirname, '../templates/workflow-dashboard.html'); const REVIEW_TEMPLATE = join(__dirname, '../templates/review-cycle-dashboard.html'); +const MODULE_FILES = [ + 'utils.js', + 'state.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', + 'views/home.js', + 'views/project-overview.js', + 'views/session-detail.js', + 'views/review-session.js', + 'views/lite-tasks.js', + 'views/fix-session.js', + 'main.js' +]; + /** * Generate dashboard HTML from aggregated data * Uses bundled templates from ccw package @@ -41,10 +63,22 @@ export async function generateDashboard(data) { function generateFromUnifiedTemplate(data) { let html = readFileSync(UNIFIED_TEMPLATE, 'utf8'); - // Read JS and CSS files - let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : ''; + // Read CSS file let cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; + // Read JS content + let jsContent = ''; + const moduleBase = join(__dirname, '../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(), @@ -630,4 +664,4 @@ function renderReviewTab(reviewData) { `; -} +} \ No newline at end of file diff --git a/ccw/src/core/server.js b/ccw/src/core/server.js index eaa5e33a..2f29ef80 100644 --- a/ccw/src/core/server.js +++ b/ccw/src/core/server.js @@ -1,6 +1,6 @@ import http from 'http'; import { URL } from 'url'; -import { readFileSync, existsSync, readdirSync } from 'fs'; +import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs'; import { join } from 'path'; import { scanSessions } from './session-scanner.js'; import { aggregateData } from './data-aggregator.js'; @@ -9,7 +9,56 @@ import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html'); const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css'); const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js'); +const MODULE_JS_DIR = join(import.meta.dirname, '../templates/dashboard-js'); +/** + * Handle POST request with JSON body + */ +function handlePostRequest(req, res, handler) { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', async () => { + try { + const parsed = JSON.parse(body); + const result = await handler(parsed); + + if (result.error) { + const status = result.status || 500; + res.writeHead(status, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: result.error })); + } else { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } + } catch (error) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: error.message })); + } + }); +} + +// Modular JS files in dependency order +const MODULE_FILES = [ + 'utils.js', + 'state.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', + 'views/home.js', + 'views/project-overview.js', + 'views/session-detail.js', + 'views/review-session.js', + 'views/lite-tasks.js', + 'views/fix-session.js', + 'main.js' +]; /** * Create and start the dashboard server * @param {Object} options - Server options @@ -37,6 +86,11 @@ export async function startServer(options = {}) { } try { + // Debug log for API requests + if (pathname.startsWith('/api/')) { + console.log(`[API] ${req.method} ${pathname}`); + } + // API: Get workflow data for a path if (pathname === '/api/data') { const projectPath = url.searchParams.get('path') || initialPath; @@ -72,6 +126,43 @@ export async function startServer(options = {}) { return; } + // API: Update task status + if (pathname === '/api/update-task-status' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { sessionPath, taskId, newStatus } = body; + + if (!sessionPath || !taskId || !newStatus) { + return { error: 'sessionPath, taskId, and newStatus are required', status: 400 }; + } + + return await updateTaskStatus(sessionPath, taskId, newStatus); + }); + return; + } + + // API: Bulk update task status + if (pathname === '/api/bulk-update-task-status' && req.method === 'POST') { + handlePostRequest(req, res, async (body) => { + const { sessionPath, taskIds, newStatus } = body; + + if (!sessionPath || !taskIds || !newStatus) { + return { error: 'sessionPath, taskIds, and newStatus are required', status: 400 }; + } + + const results = []; + for (const taskId of taskIds) { + try { + const result = await updateTaskStatus(sessionPath, taskId, newStatus); + results.push(result); + } catch (err) { + results.push({ taskId, error: err.message }); + } + } + return { success: true, results }; + }); + return; + } + // Serve dashboard HTML if (pathname === '/' || pathname === '/index.html') { const html = generateServerDashboard(initialPath); @@ -311,6 +402,74 @@ async function getSessionDetailData(sessionPath, dataType) { return result; } +/** + * Update task status in a task JSON file + * @param {string} sessionPath - Path to session directory + * @param {string} taskId - Task ID (e.g., IMPL-001) + * @param {string} newStatus - New status (pending, in_progress, completed) + * @returns {Promise} + */ +async function updateTaskStatus(sessionPath, taskId, newStatus) { + // Normalize path (handle both forward and back slashes) + let normalizedPath = sessionPath.replace(/\\/g, '/'); + + // Handle Windows drive letter format + if (normalizedPath.match(/^[a-zA-Z]:\//)) { + // Already in correct format + } else if (normalizedPath.match(/^\/[a-zA-Z]\//)) { + // Convert /D/path to D:/path + normalizedPath = normalizedPath.charAt(1).toUpperCase() + ':' + normalizedPath.slice(2); + } + + const taskDir = join(normalizedPath, '.task'); + + // Check if task directory exists + if (!existsSync(taskDir)) { + throw new Error(`Task directory not found: ${taskDir}`); + } + + // Try to find the task file + let taskFile = join(taskDir, `${taskId}.json`); + + if (!existsSync(taskFile)) { + // Try without .json if taskId already has it + if (taskId.endsWith('.json')) { + taskFile = join(taskDir, taskId); + } + if (!existsSync(taskFile)) { + throw new Error(`Task file not found: ${taskId}.json in ${taskDir}`); + } + } + + try { + const content = JSON.parse(readFileSync(taskFile, 'utf8')); + const oldStatus = content.status || 'pending'; + content.status = newStatus; + + // Add status change timestamp + if (!content.status_history) { + content.status_history = []; + } + content.status_history.push({ + from: oldStatus, + to: newStatus, + changed_at: new Date().toISOString() + }); + + writeFileSync(taskFile, JSON.stringify(content, null, 2), 'utf8'); + + return { + success: true, + taskId, + oldStatus, + newStatus, + file: taskFile + }; + } catch (error) { + throw new Error(`Failed to update task ${taskId}: ${error.message}`); + } +} + /** * Generate dashboard HTML for server mode * @param {string} initialPath @@ -319,9 +478,14 @@ async function getSessionDetailData(sessionPath, dataType) { function generateServerDashboard(initialPath) { let html = readFileSync(TEMPLATE_PATH, 'utf8'); - // Read CSS and JS files + // Read CSS file const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; - let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : ''; + + // Read and concatenate modular JS files in dependency order + let jsContent = MODULE_FILES.map(file => { + const filePath = join(MODULE_JS_DIR, file); + return existsSync(filePath) ? readFileSync(filePath, 'utf8') : ''; + }).join('\n\n'); // Inject CSS content html = html.replace('{{CSS_CONTENT}}', cssContent); diff --git a/ccw/src/core/server.js.bak b/ccw/src/core/server.js.bak new file mode 100644 index 00000000..eaa5e33a --- /dev/null +++ b/ccw/src/core/server.js.bak @@ -0,0 +1,385 @@ +import http from 'http'; +import { URL } from 'url'; +import { readFileSync, existsSync, readdirSync } from 'fs'; +import { join } from 'path'; +import { scanSessions } from './session-scanner.js'; +import { aggregateData } from './data-aggregator.js'; +import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; + +const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html'); +const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css'); +const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js'); + +/** + * Create and start the dashboard server + * @param {Object} options - Server options + * @param {number} options.port - Port to listen on (default: 3456) + * @param {string} options.initialPath - Initial project path + * @returns {Promise} + */ +export async function startServer(options = {}) { + const port = options.port || 3456; + const initialPath = options.initialPath || process.cwd(); + + const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${port}`); + const pathname = url.pathname; + + // CORS headers for API requests + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + try { + // API: Get workflow data for a path + if (pathname === '/api/data') { + const projectPath = url.searchParams.get('path') || initialPath; + const data = await getWorkflowData(projectPath); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); + return; + } + + // API: Get recent paths + if (pathname === '/api/recent-paths') { + const paths = getRecentPaths(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ paths })); + return; + } + + // API: Get session detail data (context, summaries, impl-plan, review) + if (pathname === '/api/session-detail') { + const sessionPath = url.searchParams.get('path'); + const dataType = url.searchParams.get('type') || 'all'; + + if (!sessionPath) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Session path is required' })); + return; + } + + const detail = await getSessionDetailData(sessionPath, dataType); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(detail)); + return; + } + + // Serve dashboard HTML + if (pathname === '/' || pathname === '/index.html') { + const html = generateServerDashboard(initialPath); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(html); + return; + } + + // 404 + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + + } catch (error) { + console.error('Server error:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: error.message })); + } + }); + + return new Promise((resolve, reject) => { + server.listen(port, () => { + console.log(`Dashboard server running at http://localhost:${port}`); + resolve(server); + }); + server.on('error', reject); + }); +} + +/** + * Get workflow data for a project path + * @param {string} projectPath + * @returns {Promise} + */ +async function getWorkflowData(projectPath) { + const resolvedPath = resolvePath(projectPath); + const workflowDir = join(resolvedPath, '.workflow'); + + // Track this path + trackRecentPath(resolvedPath); + + // Check if .workflow exists + if (!existsSync(workflowDir)) { + return { + generatedAt: new Date().toISOString(), + activeSessions: [], + archivedSessions: [], + liteTasks: { litePlan: [], liteFix: [] }, + reviewData: { dimensions: {} }, + projectOverview: null, + statistics: { + totalSessions: 0, + activeSessions: 0, + totalTasks: 0, + completedTasks: 0, + reviewFindings: 0, + litePlanCount: 0, + liteFixCount: 0 + }, + projectPath: normalizePathForDisplay(resolvedPath), + recentPaths: getRecentPaths() + }; + } + + // Scan and aggregate data + const sessions = await scanSessions(workflowDir); + const data = await aggregateData(sessions, workflowDir); + + data.projectPath = normalizePathForDisplay(resolvedPath); + data.recentPaths = getRecentPaths(); + + return data; +} + +/** + * Get session detail data (context, summaries, impl-plan, review) + * @param {string} sessionPath - Path to session directory + * @param {string} dataType - Type of data to load: context, summary, impl-plan, review, or all + * @returns {Promise} + */ +async function getSessionDetailData(sessionPath, dataType) { + const result = {}; + + // Normalize path + const normalizedPath = sessionPath.replace(/\\/g, '/'); + + try { + // Load context-package.json (in .process/ subfolder) + if (dataType === 'context' || dataType === 'all') { + // Try .process/context-package.json first (common location) + let contextFile = join(normalizedPath, '.process', 'context-package.json'); + if (!existsSync(contextFile)) { + // Fallback to session root + contextFile = join(normalizedPath, 'context-package.json'); + } + if (existsSync(contextFile)) { + try { + result.context = JSON.parse(readFileSync(contextFile, 'utf8')); + } catch (e) { + result.context = null; + } + } + } + + // Load task JSONs from .task/ folder + if (dataType === 'tasks' || dataType === 'all') { + const taskDir = join(normalizedPath, '.task'); + result.tasks = []; + if (existsSync(taskDir)) { + const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-')); + for (const file of files) { + try { + const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8')); + result.tasks.push({ + filename: file, + task_id: file.replace('.json', ''), + ...content + }); + } catch (e) { + // Skip unreadable files + } + } + // Sort by task ID + result.tasks.sort((a, b) => a.task_id.localeCompare(b.task_id)); + } + } + + // Load summaries from .summaries/ + if (dataType === 'summary' || dataType === 'all') { + const summariesDir = join(normalizedPath, '.summaries'); + result.summaries = []; + if (existsSync(summariesDir)) { + const files = readdirSync(summariesDir).filter(f => f.endsWith('.md')); + for (const file of files) { + try { + const content = readFileSync(join(summariesDir, file), 'utf8'); + result.summaries.push({ name: file.replace('.md', ''), content }); + } catch (e) { + // Skip unreadable files + } + } + } + } + + // Load plan.json (for lite tasks) + if (dataType === 'plan' || dataType === 'all') { + const planFile = join(normalizedPath, 'plan.json'); + if (existsSync(planFile)) { + try { + result.plan = JSON.parse(readFileSync(planFile, 'utf8')); + } catch (e) { + result.plan = null; + } + } + } + + // Load IMPL_PLAN.md + if (dataType === 'impl-plan' || dataType === 'all') { + const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md'); + if (existsSync(implPlanFile)) { + try { + result.implPlan = readFileSync(implPlanFile, 'utf8'); + } catch (e) { + result.implPlan = null; + } + } + } + + // Load review data from .review/ + if (dataType === 'review' || dataType === 'all') { + const reviewDir = join(normalizedPath, '.review'); + result.review = { + state: null, + dimensions: [], + severityDistribution: null, + totalFindings: 0 + }; + + if (existsSync(reviewDir)) { + // Load review-state.json + const stateFile = join(reviewDir, 'review-state.json'); + if (existsSync(stateFile)) { + try { + const state = JSON.parse(readFileSync(stateFile, 'utf8')); + result.review.state = state; + result.review.severityDistribution = state.severity_distribution || {}; + result.review.totalFindings = state.total_findings || 0; + result.review.phase = state.phase || 'unknown'; + result.review.dimensionSummaries = state.dimension_summaries || {}; + result.review.crossCuttingConcerns = state.cross_cutting_concerns || []; + result.review.criticalFiles = state.critical_files || []; + } catch (e) { + // Skip unreadable state + } + } + + // Load dimension findings + const dimensionsDir = join(reviewDir, 'dimensions'); + if (existsSync(dimensionsDir)) { + const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json')); + for (const file of files) { + try { + const dimName = file.replace('.json', ''); + const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8')); + + // Handle array structure: [ { findings: [...] } ] + let findings = []; + let summary = null; + + if (Array.isArray(data) && data.length > 0) { + const dimData = data[0]; + findings = dimData.findings || []; + summary = dimData.summary || null; + } else if (data.findings) { + findings = data.findings; + summary = data.summary || null; + } + + result.review.dimensions.push({ + name: dimName, + findings: findings, + summary: summary, + count: findings.length + }); + } catch (e) { + // Skip unreadable files + } + } + } + } + } + + } catch (error) { + console.error('Error loading session detail:', error); + result.error = error.message; + } + + return result; +} + +/** + * Generate dashboard HTML for server mode + * @param {string} initialPath + * @returns {string} + */ +function generateServerDashboard(initialPath) { + let html = readFileSync(TEMPLATE_PATH, 'utf8'); + + // Read CSS and JS files + const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; + let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : ''; + + // Inject CSS content + html = html.replace('{{CSS_CONTENT}}', cssContent); + + // Prepare JS content with empty initial data (will be loaded dynamically) + const emptyData = { + generatedAt: new Date().toISOString(), + activeSessions: [], + archivedSessions: [], + liteTasks: { litePlan: [], liteFix: [] }, + reviewData: { dimensions: {} }, + projectOverview: null, + statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 } + }; + + // Replace JS placeholders + jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(emptyData, null, 2)); + jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); + jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(getRecentPaths())); + + // Add server mode flag and dynamic loading functions at the start of JS + const serverModeScript = ` +// Server mode - load data dynamically +window.SERVER_MODE = true; +window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/')}'; + +async function loadDashboardData(path) { + try { + const res = await fetch('/api/data?path=' + encodeURIComponent(path)); + if (!res.ok) throw new Error('Failed to load data'); + return await res.json(); + } catch (err) { + console.error('Error loading data:', err); + return null; + } +} + +async function loadRecentPaths() { + try { + const res = await fetch('/api/recent-paths'); + if (!res.ok) return []; + const data = await res.json(); + return data.paths || []; + } catch (err) { + return []; + } +} + +`; + + // Prepend server mode script to JS content + jsContent = serverModeScript + jsContent; + + // Inject JS content + html = html.replace('{{JS_CONTENT}}', jsContent); + + // Replace any remaining placeholders in HTML + html = html.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); + + return html; +} diff --git a/ccw/src/core/server_original.bak b/ccw/src/core/server_original.bak new file mode 100644 index 00000000..eaa5e33a --- /dev/null +++ b/ccw/src/core/server_original.bak @@ -0,0 +1,385 @@ +import http from 'http'; +import { URL } from 'url'; +import { readFileSync, existsSync, readdirSync } from 'fs'; +import { join } from 'path'; +import { scanSessions } from './session-scanner.js'; +import { aggregateData } from './data-aggregator.js'; +import { resolvePath, getRecentPaths, trackRecentPath, normalizePathForDisplay, getWorkflowDir } from '../utils/path-resolver.js'; + +const TEMPLATE_PATH = join(import.meta.dirname, '../templates/dashboard.html'); +const CSS_FILE = join(import.meta.dirname, '../templates/dashboard.css'); +const JS_FILE = join(import.meta.dirname, '../templates/dashboard.js'); + +/** + * Create and start the dashboard server + * @param {Object} options - Server options + * @param {number} options.port - Port to listen on (default: 3456) + * @param {string} options.initialPath - Initial project path + * @returns {Promise} + */ +export async function startServer(options = {}) { + const port = options.port || 3456; + const initialPath = options.initialPath || process.cwd(); + + const server = http.createServer(async (req, res) => { + const url = new URL(req.url, `http://localhost:${port}`); + const pathname = url.pathname; + + // CORS headers for API requests + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + try { + // API: Get workflow data for a path + if (pathname === '/api/data') { + const projectPath = url.searchParams.get('path') || initialPath; + const data = await getWorkflowData(projectPath); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); + return; + } + + // API: Get recent paths + if (pathname === '/api/recent-paths') { + const paths = getRecentPaths(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ paths })); + return; + } + + // API: Get session detail data (context, summaries, impl-plan, review) + if (pathname === '/api/session-detail') { + const sessionPath = url.searchParams.get('path'); + const dataType = url.searchParams.get('type') || 'all'; + + if (!sessionPath) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Session path is required' })); + return; + } + + const detail = await getSessionDetailData(sessionPath, dataType); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(detail)); + return; + } + + // Serve dashboard HTML + if (pathname === '/' || pathname === '/index.html') { + const html = generateServerDashboard(initialPath); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(html); + return; + } + + // 404 + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + + } catch (error) { + console.error('Server error:', error); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: error.message })); + } + }); + + return new Promise((resolve, reject) => { + server.listen(port, () => { + console.log(`Dashboard server running at http://localhost:${port}`); + resolve(server); + }); + server.on('error', reject); + }); +} + +/** + * Get workflow data for a project path + * @param {string} projectPath + * @returns {Promise} + */ +async function getWorkflowData(projectPath) { + const resolvedPath = resolvePath(projectPath); + const workflowDir = join(resolvedPath, '.workflow'); + + // Track this path + trackRecentPath(resolvedPath); + + // Check if .workflow exists + if (!existsSync(workflowDir)) { + return { + generatedAt: new Date().toISOString(), + activeSessions: [], + archivedSessions: [], + liteTasks: { litePlan: [], liteFix: [] }, + reviewData: { dimensions: {} }, + projectOverview: null, + statistics: { + totalSessions: 0, + activeSessions: 0, + totalTasks: 0, + completedTasks: 0, + reviewFindings: 0, + litePlanCount: 0, + liteFixCount: 0 + }, + projectPath: normalizePathForDisplay(resolvedPath), + recentPaths: getRecentPaths() + }; + } + + // Scan and aggregate data + const sessions = await scanSessions(workflowDir); + const data = await aggregateData(sessions, workflowDir); + + data.projectPath = normalizePathForDisplay(resolvedPath); + data.recentPaths = getRecentPaths(); + + return data; +} + +/** + * Get session detail data (context, summaries, impl-plan, review) + * @param {string} sessionPath - Path to session directory + * @param {string} dataType - Type of data to load: context, summary, impl-plan, review, or all + * @returns {Promise} + */ +async function getSessionDetailData(sessionPath, dataType) { + const result = {}; + + // Normalize path + const normalizedPath = sessionPath.replace(/\\/g, '/'); + + try { + // Load context-package.json (in .process/ subfolder) + if (dataType === 'context' || dataType === 'all') { + // Try .process/context-package.json first (common location) + let contextFile = join(normalizedPath, '.process', 'context-package.json'); + if (!existsSync(contextFile)) { + // Fallback to session root + contextFile = join(normalizedPath, 'context-package.json'); + } + if (existsSync(contextFile)) { + try { + result.context = JSON.parse(readFileSync(contextFile, 'utf8')); + } catch (e) { + result.context = null; + } + } + } + + // Load task JSONs from .task/ folder + if (dataType === 'tasks' || dataType === 'all') { + const taskDir = join(normalizedPath, '.task'); + result.tasks = []; + if (existsSync(taskDir)) { + const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f.startsWith('IMPL-')); + for (const file of files) { + try { + const content = JSON.parse(readFileSync(join(taskDir, file), 'utf8')); + result.tasks.push({ + filename: file, + task_id: file.replace('.json', ''), + ...content + }); + } catch (e) { + // Skip unreadable files + } + } + // Sort by task ID + result.tasks.sort((a, b) => a.task_id.localeCompare(b.task_id)); + } + } + + // Load summaries from .summaries/ + if (dataType === 'summary' || dataType === 'all') { + const summariesDir = join(normalizedPath, '.summaries'); + result.summaries = []; + if (existsSync(summariesDir)) { + const files = readdirSync(summariesDir).filter(f => f.endsWith('.md')); + for (const file of files) { + try { + const content = readFileSync(join(summariesDir, file), 'utf8'); + result.summaries.push({ name: file.replace('.md', ''), content }); + } catch (e) { + // Skip unreadable files + } + } + } + } + + // Load plan.json (for lite tasks) + if (dataType === 'plan' || dataType === 'all') { + const planFile = join(normalizedPath, 'plan.json'); + if (existsSync(planFile)) { + try { + result.plan = JSON.parse(readFileSync(planFile, 'utf8')); + } catch (e) { + result.plan = null; + } + } + } + + // Load IMPL_PLAN.md + if (dataType === 'impl-plan' || dataType === 'all') { + const implPlanFile = join(normalizedPath, 'IMPL_PLAN.md'); + if (existsSync(implPlanFile)) { + try { + result.implPlan = readFileSync(implPlanFile, 'utf8'); + } catch (e) { + result.implPlan = null; + } + } + } + + // Load review data from .review/ + if (dataType === 'review' || dataType === 'all') { + const reviewDir = join(normalizedPath, '.review'); + result.review = { + state: null, + dimensions: [], + severityDistribution: null, + totalFindings: 0 + }; + + if (existsSync(reviewDir)) { + // Load review-state.json + const stateFile = join(reviewDir, 'review-state.json'); + if (existsSync(stateFile)) { + try { + const state = JSON.parse(readFileSync(stateFile, 'utf8')); + result.review.state = state; + result.review.severityDistribution = state.severity_distribution || {}; + result.review.totalFindings = state.total_findings || 0; + result.review.phase = state.phase || 'unknown'; + result.review.dimensionSummaries = state.dimension_summaries || {}; + result.review.crossCuttingConcerns = state.cross_cutting_concerns || []; + result.review.criticalFiles = state.critical_files || []; + } catch (e) { + // Skip unreadable state + } + } + + // Load dimension findings + const dimensionsDir = join(reviewDir, 'dimensions'); + if (existsSync(dimensionsDir)) { + const files = readdirSync(dimensionsDir).filter(f => f.endsWith('.json')); + for (const file of files) { + try { + const dimName = file.replace('.json', ''); + const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8')); + + // Handle array structure: [ { findings: [...] } ] + let findings = []; + let summary = null; + + if (Array.isArray(data) && data.length > 0) { + const dimData = data[0]; + findings = dimData.findings || []; + summary = dimData.summary || null; + } else if (data.findings) { + findings = data.findings; + summary = data.summary || null; + } + + result.review.dimensions.push({ + name: dimName, + findings: findings, + summary: summary, + count: findings.length + }); + } catch (e) { + // Skip unreadable files + } + } + } + } + } + + } catch (error) { + console.error('Error loading session detail:', error); + result.error = error.message; + } + + return result; +} + +/** + * Generate dashboard HTML for server mode + * @param {string} initialPath + * @returns {string} + */ +function generateServerDashboard(initialPath) { + let html = readFileSync(TEMPLATE_PATH, 'utf8'); + + // Read CSS and JS files + const cssContent = existsSync(CSS_FILE) ? readFileSync(CSS_FILE, 'utf8') : ''; + let jsContent = existsSync(JS_FILE) ? readFileSync(JS_FILE, 'utf8') : ''; + + // Inject CSS content + html = html.replace('{{CSS_CONTENT}}', cssContent); + + // Prepare JS content with empty initial data (will be loaded dynamically) + const emptyData = { + generatedAt: new Date().toISOString(), + activeSessions: [], + archivedSessions: [], + liteTasks: { litePlan: [], liteFix: [] }, + reviewData: { dimensions: {} }, + projectOverview: null, + statistics: { totalSessions: 0, activeSessions: 0, totalTasks: 0, completedTasks: 0, reviewFindings: 0, litePlanCount: 0, liteFixCount: 0 } + }; + + // Replace JS placeholders + jsContent = jsContent.replace('{{WORKFLOW_DATA}}', JSON.stringify(emptyData, null, 2)); + jsContent = jsContent.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); + jsContent = jsContent.replace('{{RECENT_PATHS}}', JSON.stringify(getRecentPaths())); + + // Add server mode flag and dynamic loading functions at the start of JS + const serverModeScript = ` +// Server mode - load data dynamically +window.SERVER_MODE = true; +window.INITIAL_PATH = '${normalizePathForDisplay(initialPath).replace(/\\/g, '/')}'; + +async function loadDashboardData(path) { + try { + const res = await fetch('/api/data?path=' + encodeURIComponent(path)); + if (!res.ok) throw new Error('Failed to load data'); + return await res.json(); + } catch (err) { + console.error('Error loading data:', err); + return null; + } +} + +async function loadRecentPaths() { + try { + const res = await fetch('/api/recent-paths'); + if (!res.ok) return []; + const data = await res.json(); + return data.paths || []; + } catch (err) { + return []; + } +} + +`; + + // Prepend server mode script to JS content + jsContent = serverModeScript + jsContent; + + // Inject JS content + html = html.replace('{{JS_CONTENT}}', jsContent); + + // Replace any remaining placeholders in HTML + html = html.replace(/\{\{PROJECT_PATH\}\}/g, normalizePathForDisplay(initialPath).replace(/\\/g, '/')); + + return html; +} diff --git a/ccw/src/templates/dashboard-js/api.js b/ccw/src/templates/dashboard-js/api.js new file mode 100644 index 00000000..c1e81f03 --- /dev/null +++ b/ccw/src/templates/dashboard-js/api.js @@ -0,0 +1,156 @@ +// ======================================== +// API and Data Loading +// ======================================== +// Server communication and data loading functions +// Note: Some functions are only available in server mode + +// ========== Data Loading ========== + +/** + * Load dashboard data from API (server mode only) + * @param {string} path - Project path to load data for + * @returns {Promise} Dashboard data object or null if failed + */ +async function loadDashboardData(path) { + if (!window.SERVER_MODE) { + console.warn('loadDashboardData called in static mode'); + return null; + } + + try { + const response = await fetch(`/api/data?path=${encodeURIComponent(path)}`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + return await response.json(); + } catch (err) { + console.error('Failed to load dashboard data:', err); + return null; + } +} + +// ========== Path Management ========== + +/** + * Switch to a new project path (server mode only) + * Loads dashboard data and updates UI + * @param {string} path - Project path to switch to + */ +async function switchToPath(path) { + // Show loading state + const container = document.getElementById('mainContent'); + container.innerHTML = '
Loading...
'; + + try { + const data = await loadDashboardData(path); + if (data) { + // Update global data + workflowData = data; + projectPath = data.projectPath; + recentPaths = data.recentPaths || []; + + // Update UI + document.getElementById('currentPath').textContent = projectPath; + renderDashboard(); + refreshRecentPaths(); + } + } catch (err) { + console.error('Failed to switch path:', err); + container.innerHTML = '
Failed to load project data
'; + } +} + +/** + * Select a path from recent paths list + * @param {string} path - Path to select + */ +async function selectPath(path) { + localStorage.setItem('selectedPath', path); + + // Server mode: load data dynamically + if (window.SERVER_MODE) { + await switchToPath(path); + return; + } + + // Static mode: show command to run + const modal = document.createElement('div'); + modal.className = 'path-modal-overlay'; + modal.innerHTML = ` +
+
+ ${icons.terminal} +

Run Command

+
+
+

To view the dashboard for this project, run:

+
+ ccw view -p "${path}" + +
+

+ Or use ccw serve for live path switching. +

+
+ +
+ `; + document.body.appendChild(modal); + + // Add copy handler + document.getElementById('copyCommandBtn').addEventListener('click', function() { + navigator.clipboard.writeText('ccw view -p "' + path + '"').then(() => { + this.innerHTML = icons.check + ' Copied!'; + setTimeout(() => { this.innerHTML = icons.copy + ' Copy'; }, 2000); + }); + }); +} + +/** + * Refresh recent paths dropdown UI + */ +function refreshRecentPaths() { + const recentContainer = document.getElementById('recentPaths'); + recentContainer.innerHTML = ''; + + recentPaths.forEach(path => { + const item = document.createElement('div'); + item.className = 'path-item' + (path === projectPath ? ' active' : ''); + item.textContent = path; + item.dataset.path = path; + item.addEventListener('click', () => selectPath(path)); + recentContainer.appendChild(item); + }); +} + +// ========== File System Access ========== + +/** + * Browse for folder using File System Access API or fallback to input dialog + */ +async function browseForFolder() { + // Try modern File System Access API first + if ('showDirectoryPicker' in window) { + try { + const dirHandle = await window.showDirectoryPicker({ + mode: 'read', + startIn: 'documents' + }); + // Get the directory name (we can't get full path for security reasons) + const dirName = dirHandle.name; + showPathSelectedModal(dirName, dirHandle); + return; + } catch (err) { + if (err.name === 'AbortError') { + // User cancelled + return; + } + console.warn('Directory picker failed:', err); + } + } + + // Fallback: show input dialog + showPathInputModal(); +} diff --git a/ccw/src/templates/dashboard-js/components/flowchart.js b/ccw/src/templates/dashboard-js/components/flowchart.js new file mode 100644 index 00000000..342c5e16 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/flowchart.js @@ -0,0 +1,493 @@ +// ========================================== +// FLOWCHART RENDERING (D3.js) +// ========================================== + +function renderFlowchartForTask(sessionId, task) { + // Will render on section expand +} + +function renderFlowchart(containerId, steps) { + if (!steps || steps.length === 0) return; + if (typeof d3 === 'undefined') { + document.getElementById(containerId).innerHTML = '
D3.js not loaded
'; + return; + } + + const container = document.getElementById(containerId); + const width = container.clientWidth || 500; + const nodeHeight = 50; + const nodeWidth = Math.min(width - 40, 300); + const padding = 15; + const height = steps.length * (nodeHeight + padding) + padding * 2; + + // Clear existing content + container.innerHTML = ''; + + const svg = d3.select('#' + containerId) + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('class', 'flowchart-svg'); + + // Arrow marker + svg.append('defs').append('marker') + .attr('id', 'arrow-' + containerId) + .attr('viewBox', '0 -5 10 10') + .attr('refX', 8) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', 'hsl(var(--border))'); + + // Draw arrows + for (let i = 0; i < steps.length - 1; i++) { + const y1 = padding + i * (nodeHeight + padding) + nodeHeight; + const y2 = padding + (i + 1) * (nodeHeight + padding); + + svg.append('line') + .attr('x1', width / 2) + .attr('y1', y1) + .attr('x2', width / 2) + .attr('y2', y2) + .attr('stroke', 'hsl(var(--border))') + .attr('stroke-width', 2) + .attr('marker-end', 'url(#arrow-' + containerId + ')'); + } + + // Draw nodes + const nodes = svg.selectAll('.node') + .data(steps) + .enter() + .append('g') + .attr('class', 'flowchart-node') + .attr('transform', (d, i) => `translate(${(width - nodeWidth) / 2}, ${padding + i * (nodeHeight + padding)})`); + + // Node rectangles + nodes.append('rect') + .attr('width', nodeWidth) + .attr('height', nodeHeight) + .attr('rx', 6) + .attr('fill', (d, i) => i === 0 ? 'hsl(var(--primary))' : 'hsl(var(--card))') + .attr('stroke', 'hsl(var(--border))') + .attr('stroke-width', 1); + + // Step number circle + nodes.append('circle') + .attr('cx', 20) + .attr('cy', nodeHeight / 2) + .attr('r', 12) + .attr('fill', (d, i) => i === 0 ? 'rgba(255,255,255,0.2)' : 'hsl(var(--muted))'); + + nodes.append('text') + .attr('x', 20) + .attr('y', nodeHeight / 2) + .attr('text-anchor', 'middle') + .attr('dominant-baseline', 'central') + .attr('font-size', '11px') + .attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--muted-foreground))') + .text((d, i) => i + 1); + + // Node text (step name) + nodes.append('text') + .attr('x', 45) + .attr('y', nodeHeight / 2) + .attr('dominant-baseline', 'central') + .attr('fill', (d, i) => i === 0 ? 'white' : 'hsl(var(--foreground))') + .attr('font-size', '12px') + .text(d => { + const text = d.step || d.action || 'Step'; + return text.length > 35 ? text.substring(0, 32) + '...' : text; + }); +} + +function renderFullFlowchart(flowControl) { + if (!flowControl) return; + + const container = document.getElementById('flowchartContainer'); + if (!container) return; + + const preAnalysis = Array.isArray(flowControl.pre_analysis) ? flowControl.pre_analysis : []; + const implSteps = Array.isArray(flowControl.implementation_approach) ? flowControl.implementation_approach : []; + + if (preAnalysis.length === 0 && implSteps.length === 0) { + container.innerHTML = '
No flowchart data available
'; + return; + } + + const width = container.clientWidth || 500; + const nodeHeight = 90; + const nodeWidth = Math.min(width - 40, 420); + const nodeGap = 45; + const sectionGap = 30; + + // Calculate total nodes and height + const totalPreNodes = preAnalysis.length; + const totalImplNodes = implSteps.length; + const hasBothSections = totalPreNodes > 0 && totalImplNodes > 0; + const height = (totalPreNodes + totalImplNodes) * (nodeHeight + nodeGap) + + (hasBothSections ? sectionGap + 60 : 0) + 60; + + // Clear existing + d3.select('#flowchartContainer').selectAll('*').remove(); + + const svg = d3.select('#flowchartContainer') + .append('svg') + .attr('width', '100%') + .attr('height', height) + .attr('viewBox', `0 0 ${width} ${height}`); + + // Add arrow markers + const defs = svg.append('defs'); + + defs.append('marker') + .attr('id', 'arrowhead-pre') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 8) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', '#f59e0b'); + + defs.append('marker') + .attr('id', 'arrowhead-impl') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 8) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', 'hsl(var(--primary))'); + + let currentY = 20; + + // Render Pre-Analysis section + if (totalPreNodes > 0) { + // Section label + svg.append('text') + .attr('x', 20) + .attr('y', currentY) + .attr('fill', '#f59e0b') + .attr('font-weight', 'bold') + .attr('font-size', '13px') + .text('📋 Pre-Analysis Steps'); + + currentY += 25; + + preAnalysis.forEach((step, idx) => { + const x = (width - nodeWidth) / 2; + + // Connection line to next node + if (idx < preAnalysis.length - 1) { + svg.append('line') + .attr('x1', width / 2) + .attr('y1', currentY + nodeHeight) + .attr('x2', width / 2) + .attr('y2', currentY + nodeHeight + nodeGap - 10) + .attr('stroke', '#f59e0b') + .attr('stroke-width', 2) + .attr('marker-end', 'url(#arrowhead-pre)'); + } + + // Node group + const nodeG = svg.append('g') + .attr('class', 'flowchart-node') + .attr('transform', `translate(${x}, ${currentY})`); + + // Node rectangle (pre-analysis style - amber/orange) + nodeG.append('rect') + .attr('width', nodeWidth) + .attr('height', nodeHeight) + .attr('rx', 10) + .attr('fill', 'hsl(var(--card))') + .attr('stroke', '#f59e0b') + .attr('stroke-width', 2) + .attr('stroke-dasharray', '5,3'); + + // Step badge + nodeG.append('circle') + .attr('cx', 25) + .attr('cy', 25) + .attr('r', 15) + .attr('fill', '#f59e0b'); + + nodeG.append('text') + .attr('x', 25) + .attr('y', 30) + .attr('text-anchor', 'middle') + .attr('fill', 'white') + .attr('font-weight', 'bold') + .attr('font-size', '11px') + .text('P' + (idx + 1)); + + // Step name + const stepName = step.step || step.action || 'Pre-step ' + (idx + 1); + nodeG.append('text') + .attr('x', 50) + .attr('y', 28) + .attr('fill', 'hsl(var(--foreground))') + .attr('font-weight', '600') + .attr('font-size', '13px') + .text(truncateText(stepName, 40)); + + // Action description + if (step.action && step.action !== stepName) { + nodeG.append('text') + .attr('x', 15) + .attr('y', 52) + .attr('fill', 'hsl(var(--muted-foreground))') + .attr('font-size', '11px') + .text(truncateText(step.action, 50)); + } + + // Output indicator + if (step.output_to) { + nodeG.append('text') + .attr('x', 15) + .attr('y', 75) + .attr('fill', '#f59e0b') + .attr('font-size', '10px') + .text('→ ' + truncateText(step.output_to, 45)); + } + + currentY += nodeHeight + nodeGap; + }); + } + + // Section divider if both sections exist + if (hasBothSections) { + currentY += 10; + svg.append('line') + .attr('x1', 40) + .attr('y1', currentY) + .attr('x2', width - 40) + .attr('y2', currentY) + .attr('stroke', 'hsl(var(--border))') + .attr('stroke-width', 1) + .attr('stroke-dasharray', '4,4'); + + // Connecting arrow from pre-analysis to implementation + svg.append('line') + .attr('x1', width / 2) + .attr('y1', currentY - nodeGap + 5) + .attr('x2', width / 2) + .attr('y2', currentY + sectionGap - 5) + .attr('stroke', 'hsl(var(--primary))') + .attr('stroke-width', 2) + .attr('marker-end', 'url(#arrowhead-impl)'); + + currentY += sectionGap; + } + + // Render Implementation section + if (totalImplNodes > 0) { + // Section label + svg.append('text') + .attr('x', 20) + .attr('y', currentY) + .attr('fill', 'hsl(var(--primary))') + .attr('font-weight', 'bold') + .attr('font-size', '13px') + .text('🔧 Implementation Steps'); + + currentY += 25; + + implSteps.forEach((step, idx) => { + const x = (width - nodeWidth) / 2; + + // Connection line to next node + if (idx < implSteps.length - 1) { + svg.append('line') + .attr('x1', width / 2) + .attr('y1', currentY + nodeHeight) + .attr('x2', width / 2) + .attr('y2', currentY + nodeHeight + nodeGap - 10) + .attr('stroke', 'hsl(var(--primary))') + .attr('stroke-width', 2) + .attr('marker-end', 'url(#arrowhead-impl)'); + } + + // Node group + const nodeG = svg.append('g') + .attr('class', 'flowchart-node') + .attr('transform', `translate(${x}, ${currentY})`); + + // Node rectangle (implementation style - blue) + nodeG.append('rect') + .attr('width', nodeWidth) + .attr('height', nodeHeight) + .attr('rx', 10) + .attr('fill', 'hsl(var(--card))') + .attr('stroke', 'hsl(var(--primary))') + .attr('stroke-width', 2); + + // Step badge + nodeG.append('circle') + .attr('cx', 25) + .attr('cy', 25) + .attr('r', 15) + .attr('fill', 'hsl(var(--primary))'); + + nodeG.append('text') + .attr('x', 25) + .attr('y', 30) + .attr('text-anchor', 'middle') + .attr('fill', 'white') + .attr('font-weight', 'bold') + .attr('font-size', '12px') + .text(step.step || idx + 1); + + // Step title + nodeG.append('text') + .attr('x', 50) + .attr('y', 28) + .attr('fill', 'hsl(var(--foreground))') + .attr('font-weight', '600') + .attr('font-size', '13px') + .text(truncateText(step.title || 'Step ' + (idx + 1), 40)); + + // Description + if (step.description) { + nodeG.append('text') + .attr('x', 15) + .attr('y', 52) + .attr('fill', 'hsl(var(--muted-foreground))') + .attr('font-size', '11px') + .text(truncateText(step.description, 50)); + } + + // Output/depends indicator + if (step.depends_on?.length) { + nodeG.append('text') + .attr('x', 15) + .attr('y', 75) + .attr('fill', 'var(--warning-color)') + .attr('font-size', '10px') + .text('← Depends: ' + step.depends_on.join(', ')); + } + + currentY += nodeHeight + nodeGap; + }); + } +} + +// D3.js Vertical Flowchart for Implementation Approach (legacy) +function renderImplementationFlowchart(steps) { + if (!Array.isArray(steps) || steps.length === 0) return; + + const container = document.getElementById('flowchartContainer'); + if (!container) return; + + const width = container.clientWidth || 500; + const nodeHeight = 100; + const nodeWidth = Math.min(width - 40, 400); + const nodeGap = 50; + const height = steps.length * (nodeHeight + nodeGap) + 40; + + // Clear existing + d3.select('#flowchartContainer').selectAll('*').remove(); + + const svg = d3.select('#flowchartContainer') + .append('svg') + .attr('width', '100%') + .attr('height', height) + .attr('viewBox', `0 0 ${width} ${height}`); + + // Add arrow marker + svg.append('defs').append('marker') + .attr('id', 'arrowhead') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 8) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', 'hsl(var(--primary))'); + + // Draw nodes and connections + steps.forEach((step, idx) => { + const y = idx * (nodeHeight + nodeGap) + 20; + const x = (width - nodeWidth) / 2; + + // Connection line to next node + if (idx < steps.length - 1) { + svg.append('line') + .attr('x1', width / 2) + .attr('y1', y + nodeHeight) + .attr('x2', width / 2) + .attr('y2', y + nodeHeight + nodeGap - 10) + .attr('stroke', 'hsl(var(--primary))') + .attr('stroke-width', 2) + .attr('marker-end', 'url(#arrowhead)'); + } + + // Node group + const nodeG = svg.append('g') + .attr('class', 'flowchart-node') + .attr('transform', `translate(${x}, ${y})`); + + // Node rectangle with gradient + nodeG.append('rect') + .attr('width', nodeWidth) + .attr('height', nodeHeight) + .attr('rx', 10) + .attr('fill', 'hsl(var(--card))') + .attr('stroke', 'hsl(var(--primary))') + .attr('stroke-width', 2) + .attr('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))'); + + // Step number badge + nodeG.append('circle') + .attr('cx', 25) + .attr('cy', 25) + .attr('r', 15) + .attr('fill', 'hsl(var(--primary))'); + + nodeG.append('text') + .attr('x', 25) + .attr('y', 30) + .attr('text-anchor', 'middle') + .attr('fill', 'white') + .attr('font-weight', 'bold') + .attr('font-size', '12px') + .text(step.step || idx + 1); + + // Step title + nodeG.append('text') + .attr('x', 50) + .attr('y', 30) + .attr('fill', 'hsl(var(--foreground))') + .attr('font-weight', '600') + .attr('font-size', '14px') + .text(truncateText(step.title || 'Step ' + (idx + 1), 35)); + + // Step description (if available) + if (step.description) { + nodeG.append('text') + .attr('x', 15) + .attr('y', 55) + .attr('fill', 'hsl(var(--muted-foreground))') + .attr('font-size', '12px') + .text(truncateText(step.description, 45)); + } + + // Output indicator + if (step.output) { + nodeG.append('text') + .attr('x', 15) + .attr('y', 80) + .attr('fill', 'var(--success-color)') + .attr('font-size', '11px') + .text('→ ' + truncateText(step.output, 40)); + } + }); +} diff --git a/ccw/src/templates/dashboard-js/components/modals.js b/ccw/src/templates/dashboard-js/components/modals.js new file mode 100644 index 00000000..c67e63a7 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/modals.js @@ -0,0 +1,260 @@ +// ========================================== +// MODAL DIALOGS +// ========================================== + +// SVG Icons +const icons = { + folder: '', + check: '', + copy: '', + terminal: '' +}; + +function showPathSelectedModal(dirName, dirHandle) { + // Try to guess full path based on current project path + const currentPath = projectPath || ''; + const basePath = currentPath.substring(0, currentPath.lastIndexOf('/')) || 'D:/projects'; + const suggestedPath = basePath + '/' + dirName; + + const modal = document.createElement('div'); + modal.className = 'path-modal-overlay'; + modal.innerHTML = ` +
+
+ ${icons.folder} +

Folder Selected

+
+
+
+ ${dirName} +
+

+ Confirm or edit the full path: +

+
+ + + +
+
+ +
+ `; + document.body.appendChild(modal); + + // Add event listeners (use arrow functions to ensure proper scope) + document.getElementById('pathGoBtn').addEventListener('click', () => { + console.log('Open button clicked'); + goToPath(); + }); + document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal()); + + // Focus input, select all text, and add enter key listener + setTimeout(() => { + const input = document.getElementById('fullPathInput'); + input?.focus(); + input?.select(); + input?.addEventListener('keypress', (e) => { + if (e.key === 'Enter') goToPath(); + }); + }, 100); +} + +function showPathInputModal() { + const modal = document.createElement('div'); + modal.className = 'path-modal-overlay'; + modal.innerHTML = ` +
+
+ ${icons.folder} +

Open Project

+
+
+
+ + + +
+
+ +
+ `; + document.body.appendChild(modal); + + // Add event listeners (use arrow functions to ensure proper scope) + document.getElementById('pathGoBtn').addEventListener('click', () => { + console.log('Open button clicked'); + goToPath(); + }); + document.getElementById('pathCancelBtn').addEventListener('click', () => closePathModal()); + + // Focus input and add enter key listener + setTimeout(() => { + const input = document.getElementById('fullPathInput'); + input?.focus(); + input?.addEventListener('keypress', (e) => { + if (e.key === 'Enter') goToPath(); + }); + }, 100); +} + +function goToPath() { + const input = document.getElementById('fullPathInput'); + const path = input?.value?.trim(); + if (path) { + closePathModal(); + selectPath(path); + } else { + // Show error - input is empty + input.style.borderColor = 'var(--danger-color)'; + input.placeholder = 'Please enter a path'; + input.focus(); + } +} + +function closePathModal() { + const modal = document.querySelector('.path-modal-overlay'); + if (modal) { + modal.remove(); + } +} + +function copyCommand(btn, dirName) { + const input = document.getElementById('fullPathInput'); + const path = input?.value?.trim() || `[full-path-to-${dirName}]`; + const command = `ccw view -p "${path}"`; + navigator.clipboard.writeText(command).then(() => { + btn.innerHTML = icons.check + ' Copied!'; + setTimeout(() => { btn.innerHTML = icons.copy + ' Copy'; }, 2000); + }); +} + +function showJsonModal(jsonId, taskId) { + // Get JSON from memory store instead of DOM + const rawTask = taskJsonStore[jsonId]; + if (!rawTask) return; + + const jsonContent = JSON.stringify(rawTask, null, 2); + + // Create modal + const overlay = document.createElement('div'); + overlay.className = 'json-modal-overlay'; + overlay.innerHTML = ` +
+
+
+ ${escapeHtml(taskId)} + Task JSON +
+ +
+
+
${escapeHtml(jsonContent)}
+
+ +
+ `; + + document.body.appendChild(overlay); + + // Trigger animation + requestAnimationFrame(() => overlay.classList.add('active')); + + // Close on overlay click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) closeJsonModal(overlay.querySelector('.json-modal-close')); + }); + + // Close on Escape key + const escHandler = (e) => { + if (e.key === 'Escape') { + closeJsonModal(overlay.querySelector('.json-modal-close')); + document.removeEventListener('keydown', escHandler); + } + }; + document.addEventListener('keydown', escHandler); +} + +function closeJsonModal(btn) { + const overlay = btn.closest('.json-modal-overlay'); + overlay.classList.remove('active'); + setTimeout(() => overlay.remove(), 200); +} + +function copyJsonToClipboard(btn) { + const content = btn.closest('.json-modal').querySelector('.json-modal-content').textContent; + navigator.clipboard.writeText(content).then(() => { + const original = btn.textContent; + btn.textContent = 'Copied!'; + setTimeout(() => btn.textContent = original, 2000); + }); +} + +function openMarkdownModal(title, content, type = 'markdown') { + const modal = document.getElementById('markdownModal'); + const titleEl = document.getElementById('markdownModalTitle'); + const rawEl = document.getElementById('markdownRaw'); + const previewEl = document.getElementById('markdownPreview'); + + // Normalize line endings + const normalizedContent = normalizeLineEndings(content); + + titleEl.textContent = title; + rawEl.textContent = normalizedContent; + + // Render preview based on type + if (typeof marked !== 'undefined' && type === 'markdown') { + previewEl.innerHTML = marked.parse(normalizedContent); + } else if (type === 'json') { + // For JSON, try to parse and re-stringify with formatting + try { + const parsed = typeof normalizedContent === 'string' ? JSON.parse(normalizedContent) : normalizedContent; + const formatted = JSON.stringify(parsed, null, 2); + previewEl.innerHTML = '
' + escapeHtml(formatted) + '
'; + } catch (e) { + // If not valid JSON, show as-is + previewEl.innerHTML = '
' + escapeHtml(normalizedContent) + '
'; + } + } else { + // Fallback: simple text with line breaks + previewEl.innerHTML = '
' + escapeHtml(normalizedContent) + '
'; + } + + // Show modal and default to preview tab + modal.classList.remove('hidden'); + switchMarkdownTab('preview'); +} + +function closeMarkdownModal() { + const modal = document.getElementById('markdownModal'); + modal.classList.add('hidden'); +} + +function switchMarkdownTab(tab) { + const rawEl = document.getElementById('markdownRaw'); + const previewEl = document.getElementById('markdownPreview'); + const rawTabBtn = document.getElementById('mdTabRaw'); + const previewTabBtn = document.getElementById('mdTabPreview'); + + if (tab === 'raw') { + rawEl.classList.remove('hidden'); + previewEl.classList.add('hidden'); + rawTabBtn.classList.add('active', 'bg-background', 'text-foreground'); + rawTabBtn.classList.remove('text-muted-foreground'); + previewTabBtn.classList.remove('active', 'bg-background', 'text-foreground'); + previewTabBtn.classList.add('text-muted-foreground'); + } else { + rawEl.classList.add('hidden'); + previewEl.classList.remove('hidden'); + previewTabBtn.classList.add('active', 'bg-background', 'text-foreground'); + previewTabBtn.classList.remove('text-muted-foreground'); + rawTabBtn.classList.remove('active', 'bg-background', 'text-foreground'); + rawTabBtn.classList.add('text-muted-foreground'); + } +} diff --git a/ccw/src/templates/dashboard-js/components/navigation.js b/ccw/src/templates/dashboard-js/components/navigation.js new file mode 100644 index 00000000..77f3a026 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/navigation.js @@ -0,0 +1,210 @@ +// Navigation and Routing +// Manages navigation events, active state, content title updates, search, and path selector + +// Path Selector +function initPathSelector() { + const btn = document.getElementById('pathButton'); + const menu = document.getElementById('pathMenu'); + const recentContainer = document.getElementById('recentPaths'); + + // Render recent paths + if (recentPaths && recentPaths.length > 0) { + recentPaths.forEach(path => { + const item = document.createElement('div'); + item.className = 'path-item' + (path === projectPath ? ' active' : ''); + item.textContent = path; + item.dataset.path = path; + item.addEventListener('click', () => selectPath(path)); + recentContainer.appendChild(item); + }); + } + + btn.addEventListener('click', (e) => { + e.stopPropagation(); + menu.classList.toggle('hidden'); + }); + + document.addEventListener('click', () => { + menu.classList.add('hidden'); + }); + + document.getElementById('browsePath').addEventListener('click', async () => { + await browseForFolder(); + }); +} + +// Navigation +function initNavigation() { + document.querySelectorAll('.nav-item[data-filter]').forEach(item => { + item.addEventListener('click', () => { + setActiveNavItem(item); + currentFilter = item.dataset.filter; + currentLiteType = null; + currentView = 'sessions'; + currentSessionDetailKey = null; + updateContentTitle(); + renderSessions(); + }); + }); + + // Lite Tasks Navigation + document.querySelectorAll('.nav-item[data-lite]').forEach(item => { + item.addEventListener('click', () => { + setActiveNavItem(item); + currentLiteType = item.dataset.lite; + currentFilter = null; + currentView = 'liteTasks'; + currentSessionDetailKey = null; + updateContentTitle(); + renderLiteTasks(); + }); + }); + + // Project Overview Navigation + document.querySelectorAll('.nav-item[data-view]').forEach(item => { + item.addEventListener('click', () => { + setActiveNavItem(item); + currentView = item.dataset.view; + currentFilter = null; + currentLiteType = null; + currentSessionDetailKey = null; + updateContentTitle(); + renderProjectOverview(); + }); + }); +} + +function setActiveNavItem(item) { + document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); + item.classList.add('active'); +} + +function updateContentTitle() { + const titleEl = document.getElementById('contentTitle'); + if (currentView === 'project-overview') { + titleEl.textContent = 'Project Overview'; + } else if (currentView === 'liteTasks') { + const names = { 'lite-plan': 'Lite Plan Sessions', 'lite-fix': 'Lite Fix Sessions' }; + titleEl.textContent = names[currentLiteType] || 'Lite Tasks'; + } else if (currentView === 'sessionDetail') { + titleEl.textContent = 'Session Detail'; + } else if (currentView === 'liteTaskDetail') { + titleEl.textContent = 'Lite Task Detail'; + } else { + const names = { 'all': 'All Sessions', 'active': 'Active Sessions', 'archived': 'Archived Sessions' }; + titleEl.textContent = names[currentFilter] || 'Sessions'; + } +} + +// Search +function initSearch() { + const input = document.getElementById('searchInput'); + input.addEventListener('input', (e) => { + const query = e.target.value.toLowerCase(); + document.querySelectorAll('.session-card').forEach(card => { + const text = card.textContent.toLowerCase(); + card.style.display = text.includes(query) ? '' : 'none'; + }); + }); +} + +// Refresh Workspace +function initRefreshButton() { + const btn = document.getElementById('refreshWorkspace'); + if (btn) { + btn.addEventListener('click', refreshWorkspace); + } +} + +async function refreshWorkspace() { + const btn = document.getElementById('refreshWorkspace'); + + // Add spinning animation + btn.classList.add('refreshing'); + btn.disabled = true; + + try { + if (window.SERVER_MODE) { + // Reload data from server + const data = await loadDashboardData(projectPath); + if (data) { + // Update stores + sessionDataStore = {}; + liteTaskDataStore = {}; + + // Populate stores + [...(data.activeSessions || []), ...(data.archivedSessions || [])].forEach(s => { + sessionDataStore[s.session_id] = s; + }); + + [...(data.liteTasks?.litePlan || []), ...(data.liteTasks?.liteFix || [])].forEach(s => { + liteTaskDataStore[s.session_id] = s; + }); + + // Update global data + window.workflowData = data; + + // Update sidebar counts + updateSidebarCounts(data); + + // Re-render current view + if (currentView === 'sessions') { + renderSessions(); + } else if (currentView === 'liteTasks') { + renderLiteTasks(); + } else if (currentView === 'sessionDetail' && currentSessionDetailKey) { + showSessionDetailPage(currentSessionDetailKey); + } else if (currentView === 'liteTaskDetail' && currentSessionDetailKey) { + showLiteTaskDetailPage(currentSessionDetailKey); + } else if (currentView === 'project-overview') { + renderProjectOverview(); + } + + showRefreshToast('Workspace refreshed', 'success'); + } + } else { + // Non-server mode: just reload page + window.location.reload(); + } + } catch (error) { + console.error('Refresh failed:', error); + showRefreshToast('Refresh failed: ' + error.message, 'error'); + } finally { + btn.classList.remove('refreshing'); + btn.disabled = false; + } +} + +function updateSidebarCounts(data) { + // Update session counts + const activeCount = document.querySelector('.nav-item[data-filter="active"] .nav-count'); + const archivedCount = document.querySelector('.nav-item[data-filter="archived"] .nav-count'); + const allCount = document.querySelector('.nav-item[data-filter="all"] .nav-count'); + + if (activeCount) activeCount.textContent = data.activeSessions?.length || 0; + if (archivedCount) archivedCount.textContent = data.archivedSessions?.length || 0; + if (allCount) allCount.textContent = (data.activeSessions?.length || 0) + (data.archivedSessions?.length || 0); + + // Update lite task counts + const litePlanCount = document.querySelector('.nav-item[data-lite="lite-plan"] .nav-count'); + const liteFixCount = document.querySelector('.nav-item[data-lite="lite-fix"] .nav-count'); + + if (litePlanCount) litePlanCount.textContent = data.liteTasks?.litePlan?.length || 0; + if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0; +} + +function showRefreshToast(message, type) { + // Remove existing toast + const existing = document.querySelector('.status-toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.className = `status-toast ${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + setTimeout(() => { + toast.classList.add('fade-out'); + setTimeout(() => toast.remove(), 300); + }, 2000); +} diff --git a/ccw/src/templates/dashboard-js/components/sidebar.js b/ccw/src/templates/dashboard-js/components/sidebar.js new file mode 100644 index 00000000..1a7ba44d --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/sidebar.js @@ -0,0 +1,31 @@ +// ========================================== +// SIDEBAR MANAGEMENT +// ========================================== + +function initSidebar() { + const sidebar = document.getElementById('sidebar'); + const toggle = document.getElementById('sidebarToggle'); + const menuToggle = document.getElementById('menuToggle'); + const overlay = document.getElementById('sidebarOverlay'); + + // Restore collapsed state + if (localStorage.getItem('sidebarCollapsed') === 'true') { + sidebar.classList.add('collapsed'); + } + + toggle.addEventListener('click', () => { + sidebar.classList.toggle('collapsed'); + localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed')); + }); + + // Mobile menu + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('open'); + overlay.classList.toggle('open'); + }); + + overlay.addEventListener('click', () => { + sidebar.classList.remove('open'); + overlay.classList.remove('open'); + }); +} diff --git a/ccw/src/templates/dashboard-js/components/tabs-context.js b/ccw/src/templates/dashboard-js/components/tabs-context.js new file mode 100644 index 00000000..b74756c7 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/tabs-context.js @@ -0,0 +1,963 @@ +// ========================================== +// Tab Content Renderers - Context Tab +// ========================================== +// Functions for rendering Context tab content in the dashboard +// Note: getRelevanceColor and getRoleBadgeClass are defined in utils.js + +// ========================================== +// Context Tab Rendering +// ========================================== + +function renderContextContent(context) { + if (!context) { + return ` +
+
đŸ“Ļ
+
No Context Data
+
No context-package.json found for this session.
+
+ `; + } + + const contextJson = JSON.stringify(context, null, 2); + // Store in global variable for modal access + window._currentContextJson = contextJson; + + // Parse context structure + const metadata = context.metadata || {}; + const projectContext = context.project_context || {}; + const techStack = projectContext.tech_stack || metadata.tech_stack || {}; + const codingConventions = projectContext.coding_conventions || {}; + const architecturePatterns = projectContext.architecture_patterns || []; + const assets = context.assets || {}; + const dependencies = context.dependencies || {}; + const testContext = context.test_context || {}; + const conflictDetection = context.conflict_detection || {}; + + return ` +
+ +
+
+

đŸ“Ļ Context Package

+ +
+
+ + + ${metadata.task_description || metadata.session_id ? ` +
+
+ 📋 +

Task Metadata

+
+
+ ${metadata.task_description ? ` +

${escapeHtml(metadata.task_description)}

+ ` : ''} +
+ ${metadata.session_id ? ` +
+ SESSION + ${escapeHtml(metadata.session_id)} +
+ ` : ''} + ${metadata.complexity ? ` +
+ COMPLEXITY + ${escapeHtml(metadata.complexity.toUpperCase())} +
+ ` : ''} + ${metadata.timestamp ? ` +
+ CREATED + ${formatDate(metadata.timestamp)} +
+ ` : ''} +
+ ${metadata.keywords && metadata.keywords.length > 0 ? ` +
+ ${metadata.keywords.map(kw => `${escapeHtml(kw)}`).join('')} +
+ ` : ''} +
+
+ ` : ''} + + + ${architecturePatterns.length > 0 ? ` +
+
+ đŸ›ī¸ +

Architecture Patterns

+ ${architecturePatterns.length} +
+
+
+ ${architecturePatterns.map(p => `${escapeHtml(p)}`).join('')} +
+
+
+ ` : ''} + + + ${Object.keys(techStack).length > 0 ? ` +
+
+ đŸ’ģ +

Technology Stack

+
+
+ ${renderTechStackCards(techStack)} +
+
+ ` : ''} + + + ${Object.keys(codingConventions).length > 0 ? ` +
+
+ 📝 +

Coding Conventions

+
+
+ ${renderCodingConventionsCards(codingConventions)} +
+
+ ` : ''} + + + ${Object.keys(assets).length > 0 ? ` +
+
+ 📚 +

Assets & Resources

+
+
+ ${renderAssetsCards(assets)} +
+
+ ` : ''} + + + ${(dependencies.internal && dependencies.internal.length > 0) || (dependencies.external && dependencies.external.length > 0) ? ` +
+
+ 🔗 +

Dependencies

+
+
+ ${renderDependenciesCards(dependencies)} +
+
+ ` : ''} + + + ${Object.keys(testContext).length > 0 ? ` +
+
+ đŸ§Ē +

Test Context

+
+
+ ${renderTestContextCards(testContext)} +
+
+ ` : ''} + + + ${Object.keys(conflictDetection).length > 0 ? ` +
+
+ âš ī¸ +

Risk Analysis

+ ${conflictDetection.risk_level ? ` + ${escapeHtml(conflictDetection.risk_level.toUpperCase())} + ` : ''} +
+
+ ${renderConflictCards(conflictDetection)} +
+
+ ` : ''} +
+ `; +} + +// New card-based renderers +function renderTechStackCards(techStack) { + const sections = []; + + if (techStack.languages) { + const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages]; + sections.push(` +
+ Languages +
+ ${langs.map(l => `${escapeHtml(String(l))}`).join('')} +
+
+ `); + } + + if (techStack.frameworks) { + const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks]; + sections.push(` +
+ Frameworks +
+ ${frameworks.map(f => `${escapeHtml(String(f))}`).join('')} +
+
+ `); + } + + if (techStack.frontend_frameworks) { + const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks]; + sections.push(` +
+ Frontend +
+ ${ff.map(f => `${escapeHtml(String(f))}`).join('')} +
+
+ `); + } + + if (techStack.backend_frameworks) { + const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks]; + sections.push(` +
+ Backend +
+ ${bf.map(f => `${escapeHtml(String(f))}`).join('')} +
+
+ `); + } + + if (techStack.libraries && typeof techStack.libraries === 'object') { + Object.entries(techStack.libraries).forEach(([category, libList]) => { + if (Array.isArray(libList) && libList.length > 0) { + sections.push(` +
+ ${escapeHtml(category)} +
+ ${libList.map(lib => `${escapeHtml(String(lib))}`).join('')} +
+
+ `); + } + }); + } + + return sections.join(''); +} + +function renderCodingConventionsCards(conventions) { + const sections = []; + + // Helper to format leaf values + const formatLeafValue = (val) => { + if (val === null || val === undefined) return '-'; + if (Array.isArray(val)) return val.map(v => escapeHtml(String(v))).join(', '); + return escapeHtml(String(val)); + }; + + // Helper to render items (handles nested objects like {backend: ..., frontend: ...}) + const renderItems = (data) => { + return Object.entries(data).map(([key, val]) => { + // Check if val is a nested object (like {backend: {...}, frontend: {...}}) + if (val && typeof val === 'object' && !Array.isArray(val)) { + // Render sub-items for nested structure + return Object.entries(val).map(([subKey, subVal]) => ` +
+ ${escapeHtml(key)} + ${formatLeafValue(subVal)} +
+ `).join(''); + } + return ` +
+ ${escapeHtml(key)} + ${formatLeafValue(val)} +
+ `; + }).join(''); + }; + + // Render all convention sections + Object.entries(conventions).forEach(([key, val]) => { + if (val && typeof val === 'object' && !Array.isArray(val) && Object.keys(val).length > 0) { + const label = key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + sections.push(` +
+ ${escapeHtml(label)} +
+ ${renderItems(val)} +
+
+ `); + } + }); + + return sections.length > 0 ? sections.join('') : ''; +} + +function renderAssetsCards(assets) { + const sections = []; + + // Documentation section - card grid layout + if (assets.documentation && assets.documentation.length > 0) { + sections.push(` +
+
+ 📄 + Documentation + ${assets.documentation.length} +
+
+ ${assets.documentation.map(doc => ` +
+ ${escapeHtml(doc.path)} + ${(doc.relevance_score * 100).toFixed(0)}% +
+ `).join('')} +
+
+ `); + } + + // Source Code section - card grid layout + if (assets.source_code && assets.source_code.length > 0) { + sections.push(` +
+
+ đŸ’ģ + Source Code + ${assets.source_code.length} +
+
+ ${assets.source_code.map(src => ` +
+ ${escapeHtml(src.path)} + ${src.role ? `${escapeHtml(src.role.replace(/-/g, ' '))}` : ''} +
+ `).join('')} +
+
+ `); + } + + // Tests section - card grid layout + if (assets.tests && assets.tests.length > 0) { + sections.push(` +
+
+ đŸ§Ē + Tests + ${assets.tests.length} +
+
+ ${assets.tests.map(test => ` +
+ ${escapeHtml(test.path)} + ${test.test_count ? `${test.test_count} tests` : ''} +
+ `).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderDependenciesCards(dependencies) { + const sections = []; + + if (dependencies.internal && dependencies.internal.length > 0) { + sections.push(` +
+
+ Internal Dependencies + ${dependencies.internal.length} +
+
+
+ From + Type + To +
+
+ ${dependencies.internal.map(dep => ` +
+ ${escapeHtml(dep.from)} + + ${escapeHtml(dep.type)} + + ${escapeHtml(dep.to)} +
+ `).join('')} +
+
+
+ `); + } + + if (dependencies.external && dependencies.external.length > 0) { + sections.push(` +
+
+ External Packages + ${dependencies.external.length} +
+
+ ${dependencies.external.map(dep => ` + ${escapeHtml(dep.package)}${dep.version ? `@${escapeHtml(dep.version)}` : ''} + `).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderTestContextCards(testContext) { + const sections = []; + + // Stats row + const tests = testContext.existing_tests || {}; + let totalTests = 0; + if (tests.backend) { + if (tests.backend.integration) totalTests += tests.backend.integration.tests || 0; + if (tests.backend.api_endpoints) totalTests += tests.backend.api_endpoints.tests || 0; + } + + sections.push(` +
+
+ ${totalTests} + Total Tests +
+ ${testContext.coverage_config?.target ? ` +
+ ${escapeHtml(testContext.coverage_config.target)} + Coverage Target +
+ ` : ''} +
+ `); + + if (testContext.frameworks) { + const fw = testContext.frameworks; + sections.push(` +
+ ${fw.backend ? ` +
+ Backend + ${escapeHtml(fw.backend.name || 'N/A')} +
+ ` : ''} + ${fw.frontend ? ` +
+ Frontend + ${escapeHtml(fw.frontend.name || 'N/A')} +
+ ` : ''} +
+ `); + } + + return sections.join(''); +} + +function renderConflictCards(conflictDetection) { + const sections = []; + + if (conflictDetection.mitigation_strategy) { + // Parse numbered items like "(1) ... (2) ..." into list + const strategy = conflictDetection.mitigation_strategy; + const items = strategy.split(/\(\d+\)/).filter(s => s.trim()); + + if (items.length > 1) { + sections.push(` +
+ Mitigation Strategy +
    + ${items.map(item => `
  1. ${escapeHtml(item.trim())}
  2. `).join('')} +
+
+ `); + } else { + sections.push(` +
+ Mitigation Strategy +

${escapeHtml(strategy)}

+
+ `); + } + } + + if (conflictDetection.risk_factors) { + const factors = conflictDetection.risk_factors; + if (factors.test_gaps?.length > 0) { + sections.push(` +
+ Test Gaps +
    + ${factors.test_gaps.map(gap => `
  • ${escapeHtml(gap)}
  • `).join('')} +
+
+ `); + } + } + + if (conflictDetection.affected_modules?.length > 0) { + sections.push(` +
+ Affected Modules +
+ ${conflictDetection.affected_modules.map(mod => `${escapeHtml(mod)}`).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderTechStackSection(techStack) { + const sections = []; + + if (techStack.languages) { + const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages]; + sections.push(` +
+ Languages: +
+ ${langs.map(l => `${escapeHtml(String(l))}`).join('')} +
+
+ `); + } + + if (techStack.frameworks) { + const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks]; + sections.push(` +
+ Frameworks: +
+ ${frameworks.map(f => `${escapeHtml(String(f))}`).join('')} +
+
+ `); + } + + if (techStack.frontend_frameworks) { + const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks]; + sections.push(` +
+ Frontend: +
+ ${ff.map(f => `${escapeHtml(String(f))}`).join('')} +
+
+ `); + } + + if (techStack.backend_frameworks) { + const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks]; + sections.push(` +
+ Backend: +
+ ${bf.map(f => `${escapeHtml(String(f))}`).join('')} +
+
+ `); + } + + if (techStack.libraries) { + const libs = techStack.libraries; + if (typeof libs === 'object' && !Array.isArray(libs)) { + Object.entries(libs).forEach(([category, libList]) => { + if (Array.isArray(libList) && libList.length > 0) { + sections.push(` +
+ ${escapeHtml(category)}: +
    + ${libList.map(lib => `
  • ${escapeHtml(String(lib))}
  • `).join('')} +
+
+ `); + } + }); + } + } + + return sections.join(''); +} + +function renderCodingConventions(conventions) { + const sections = []; + + if (conventions.naming) { + sections.push(` +
+ Naming: +
    + ${Object.entries(conventions.naming).map(([key, val]) => + `
  • ${escapeHtml(key)}: ${escapeHtml(String(val))}
  • ` + ).join('')} +
+
+ `); + } + + if (conventions.error_handling) { + sections.push(` +
+ Error Handling: +
    + ${Object.entries(conventions.error_handling).map(([key, val]) => + `
  • ${escapeHtml(key)}: ${escapeHtml(String(val))}
  • ` + ).join('')} +
+
+ `); + } + + if (conventions.testing) { + sections.push(` +
+ Testing: +
    + ${Object.entries(conventions.testing).map(([key, val]) => { + if (Array.isArray(val)) { + return `
  • ${escapeHtml(key)}: ${val.map(v => escapeHtml(String(v))).join(', ')}
  • `; + } + return `
  • ${escapeHtml(key)}: ${escapeHtml(String(val))}
  • `; + }).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderAssetsSection(assets) { + const sections = []; + + // Documentation + if (assets.documentation && assets.documentation.length > 0) { + sections.push(` +
+
📄 Documentation
+
+ ${assets.documentation.map(doc => ` +
+
+ ${escapeHtml(doc.path)} + ${(doc.relevance_score * 100).toFixed(0)}% +
+
+
${escapeHtml(doc.scope || '')}
+ ${doc.contains && doc.contains.length > 0 ? ` +
+ ${doc.contains.map(tag => `${escapeHtml(tag)}`).join('')} +
+ ` : ''} +
+
+ `).join('')} +
+
+ `); + } + + // Source Code + if (assets.source_code && assets.source_code.length > 0) { + sections.push(` +
+
đŸ’ģ Source Code
+
+ ${assets.source_code.map(src => ` +
+
+ ${escapeHtml(src.path)} + ${(src.relevance_score * 100).toFixed(0)}% +
+
+
${escapeHtml(src.role || '')}
+ ${src.exports && src.exports.length > 0 ? ` +
Exports: ${src.exports.map(e => `${escapeHtml(e)}`).join(', ')}
+ ` : ''} + ${src.features && src.features.length > 0 ? ` +
${src.features.map(f => `${escapeHtml(f)}`).join('')}
+ ` : ''} +
+
+ `).join('')} +
+
+ `); + } + + // Tests + if (assets.tests && assets.tests.length > 0) { + sections.push(` +
+
đŸ§Ē Tests
+
+ ${assets.tests.map(test => ` +
+
+ ${escapeHtml(test.path)} + ${test.test_count ? `${test.test_count} tests` : ''} +
+
+
${escapeHtml(test.type || '')}
+ ${test.test_classes ? `
Classes: ${escapeHtml(test.test_classes.join(', '))}
` : ''} + ${test.coverage ? `
Coverage: ${escapeHtml(test.coverage)}
` : ''} +
+
+ `).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderDependenciesSection(dependencies) { + const sections = []; + + // Internal Dependencies + if (dependencies.internal && dependencies.internal.length > 0) { + sections.push(` +
+
🔄 Internal Dependencies
+
+ ${dependencies.internal.slice(0, 10).map(dep => ` +
+
${escapeHtml(dep.from)}
+
+ ${escapeHtml(dep.type)} + → +
+
${escapeHtml(dep.to)}
+
+ `).join('')} + ${dependencies.internal.length > 10 ? `
... and ${dependencies.internal.length - 10} more
` : ''} +
+
+ `); + } + + // External Dependencies + if (dependencies.external && dependencies.external.length > 0) { + sections.push(` +
+
đŸ“Ļ External Dependencies
+
+ ${dependencies.external.map(dep => ` +
+
${escapeHtml(dep.package)}
+
${escapeHtml(dep.version || '')}
+
${escapeHtml(dep.usage || '')}
+
+ `).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderTestContextSection(testContext) { + const sections = []; + + // Test Frameworks + if (testContext.frameworks) { + const frameworks = testContext.frameworks; + sections.push(` +
+
đŸ› ī¸ Test Frameworks
+
+ ${frameworks.backend ? ` +
+
+ Backend + ${escapeHtml(frameworks.backend.name || 'N/A')} +
+ ${frameworks.backend.plugins ? ` +
${frameworks.backend.plugins.map(p => `${escapeHtml(p)}`).join('')}
+ ` : ''} +
+ ` : ''} + ${frameworks.frontend ? ` +
+
+ Frontend + ${escapeHtml(frameworks.frontend.name || 'N/A')} +
+ ${frameworks.frontend.recommended ? `` : ''} + ${frameworks.frontend.gap ? `
âš ī¸ ${escapeHtml(frameworks.frontend.gap)}
` : ''} +
+ ` : ''} +
+
+ `); + } + + // Existing Tests Statistics + if (testContext.existing_tests) { + const tests = testContext.existing_tests; + let totalTests = 0; + let totalClasses = 0; + + if (tests.backend) { + if (tests.backend.integration) { + totalTests += tests.backend.integration.tests || 0; + totalClasses += tests.backend.integration.classes || 0; + } + if (tests.backend.api_endpoints) { + totalTests += tests.backend.api_endpoints.tests || 0; + totalClasses += tests.backend.api_endpoints.classes || 0; + } + } + + sections.push(` +
+
📊 Test Statistics
+
+
+
${totalTests}
+
Total Tests
+
+
+
${totalClasses}
+
Test Classes
+
+ ${testContext.coverage_config && testContext.coverage_config.target ? ` +
+
${escapeHtml(testContext.coverage_config.target)}
+
Coverage Target
+
+ ` : ''} +
+
+ `); + } + + // Test Markers + if (testContext.test_markers) { + sections.push(` +
+
đŸˇī¸ Test Markers
+
+ ${Object.entries(testContext.test_markers).map(([marker, desc]) => ` +
+ @${escapeHtml(marker)} + ${escapeHtml(desc)} +
+ `).join('')} +
+
+ `); + } + + return sections.join(''); +} + +function renderConflictDetectionSection(conflictDetection) { + const sections = []; + + // Risk Level Indicator + if (conflictDetection.risk_level) { + const riskLevel = conflictDetection.risk_level; + const riskColor = riskLevel === 'high' ? '#ef4444' : riskLevel === 'medium' ? '#f59e0b' : '#10b981'; + sections.push(` +
+
+ ${escapeHtml(riskLevel.toUpperCase())} RISK +
+ ${conflictDetection.mitigation_strategy ? ` +
+ Mitigation Strategy: ${escapeHtml(conflictDetection.mitigation_strategy)} +
+ ` : ''} +
+ `); + } + + // Risk Factors + if (conflictDetection.risk_factors) { + const factors = conflictDetection.risk_factors; + sections.push(` +
+
âš ī¸ Risk Factors
+
+ ${factors.test_gaps && factors.test_gaps.length > 0 ? ` +
+ Test Gaps: +
    + ${factors.test_gaps.map(gap => `
  • ${escapeHtml(gap)}
  • `).join('')} +
+
+ ` : ''} + ${factors.existing_implementations && factors.existing_implementations.length > 0 ? ` +
+ Existing Implementations: +
    + ${factors.existing_implementations.map(impl => `
  • ${escapeHtml(impl)}
  • `).join('')} +
+
+ ` : ''} +
+
+ `); + } + + // Affected Modules + if (conflictDetection.affected_modules && conflictDetection.affected_modules.length > 0) { + sections.push(` +
+
đŸ“Ļ Affected Modules
+
+ ${conflictDetection.affected_modules.map(mod => ` + ${escapeHtml(mod)} + `).join('')} +
+
+ `); + } + + // Historical Conflicts + if (conflictDetection.historical_conflicts && conflictDetection.historical_conflicts.length > 0) { + sections.push(` +
+
📜 Historical Lessons
+
+ ${conflictDetection.historical_conflicts.map(conflict => ` +
+
Source: ${escapeHtml(conflict.source || 'Unknown')}
+ ${conflict.lesson ? `
Lesson: ${escapeHtml(conflict.lesson)}
` : ''} + ${conflict.recommendation ? `
Recommendation: ${escapeHtml(conflict.recommendation)}
` : ''} + ${conflict.challenge ? `
Challenge: ${escapeHtml(conflict.challenge)}
` : ''} +
+ `).join('')} +
+
+ `); + } + + return sections.join(''); +} diff --git a/ccw/src/templates/dashboard-js/components/tabs-other.js b/ccw/src/templates/dashboard-js/components/tabs-other.js new file mode 100644 index 00000000..ef86946a --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/tabs-other.js @@ -0,0 +1,353 @@ +// ========================================== +// Tab Content Renderers - Other Tabs +// ========================================== +// Functions for rendering Summary, IMPL Plan, Review, and Lite Context tabs + +// ========================================== +// Summary Tab Rendering +// ========================================== + +function renderSummaryContent(summaries) { + if (!summaries || summaries.length === 0) { + return ` +
+
📝
+
No Summaries
+
No summaries found in .summaries/
+
+ `; + } + + // Store summaries in global variable for modal access + window._currentSummaries = summaries; + + return ` +
+ ${summaries.map((s, idx) => { + const normalizedContent = normalizeLineEndings(s.content || ''); + // Extract first 3 lines for preview + const previewLines = normalizedContent.split('\n').slice(0, 3).join('\n'); + const hasMore = normalizedContent.split('\n').length > 3; + return ` +
+
+

📄 ${escapeHtml(s.name || 'Summary')}

+ +
+
+
${escapeHtml(previewLines)}${hasMore ? '\n...' : ''}
+
+
+ `; + }).join('')} +
+ `; +} + +// ========================================== +// IMPL Plan Tab Rendering +// ========================================== + +function renderImplPlanContent(implPlan) { + if (!implPlan) { + return ` +
+
📐
+
No IMPL Plan
+
No IMPL_PLAN.md found for this session.
+
+ `; + } + + // Normalize and store in global variable for modal access + const normalizedContent = normalizeLineEndings(implPlan); + window._currentImplPlan = normalizedContent; + + // Extract first 5 lines for preview + const previewLines = normalizedContent.split('\n').slice(0, 5).join('\n'); + const hasMore = normalizedContent.split('\n').length > 5; + + return ` +
+
+
+

📐 Implementation Plan

+ +
+
+
${escapeHtml(previewLines)}${hasMore ? '\n...' : ''}
+
+
+
+ `; +} + +// ========================================== +// Review Tab Rendering +// ========================================== + +function renderReviewContent(review) { + if (!review || !review.dimensions) { + return ` +
+
🔍
+
No Review Data
+
No review findings in .review/
+
+ `; + } + + const dimensions = Object.entries(review.dimensions); + if (dimensions.length === 0) { + return ` +
+
🔍
+
No Findings
+
No review findings found.
+
+ `; + } + + return ` +
+ ${dimensions.map(([dim, rawFindings]) => { + // Normalize findings to always be an array + let findings = []; + if (Array.isArray(rawFindings)) { + findings = rawFindings; + } else if (rawFindings && typeof rawFindings === 'object') { + // If it's an object with a findings array, use that + if (Array.isArray(rawFindings.findings)) { + findings = rawFindings.findings; + } else { + // Wrap single object in array or show raw JSON + findings = [{ title: dim, description: JSON.stringify(rawFindings, null, 2), severity: 'info' }]; + } + } + + return ` +
+
+ ${escapeHtml(dim)} + ${findings.length} finding${findings.length !== 1 ? 's' : ''} +
+
+ ${findings.map(f => ` +
+
+ ${f.severity || 'medium'} + ${escapeHtml(f.title || 'Finding')} +
+

${escapeHtml(f.description || '')}

+ ${f.file ? `
📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''} +
+ `).join('')} +
+
+ `}).join('')} +
+ `; +} + +// ========================================== +// Lite Context Tab Rendering +// ========================================== + +function renderLiteContextContent(context, session) { + const plan = session.plan || {}; + + // If we have context from context-package.json + if (context) { + return ` +
+
${escapeHtml(JSON.stringify(context, null, 2))}
+
+ `; + } + + // Fallback: show context from plan + if (plan.focus_paths?.length || plan.summary) { + return ` +
+ ${plan.summary ? ` +
+

Summary

+

${escapeHtml(plan.summary)}

+
+ ` : ''} + ${plan.focus_paths?.length ? ` +
+

Focus Paths

+
+ ${plan.focus_paths.map(p => `${escapeHtml(p)}`).join('')} +
+
+ ` : ''} +
+ `; + } + + return ` +
+
đŸ“Ļ
+
No Context Data
+
No context-package.json found for this session.
+
+ `; +} + +// ========================================== +// Exploration Context Rendering +// ========================================== + +function renderExplorationContext(explorations) { + if (!explorations || !explorations.manifest) { + return ''; + } + + const manifest = explorations.manifest; + const data = explorations.data || {}; + + let sections = []; + + // Header with manifest info + sections.push(` +
+

${escapeHtml(manifest.task_description || 'Exploration Context')}

+
+ Complexity: ${escapeHtml(manifest.complexity || 'N/A')} + Explorations: ${manifest.exploration_count || 0} +
+
+ `); + + // Render each exploration angle as collapsible section + const explorationOrder = ['architecture', 'dependencies', 'patterns', 'integration-points']; + const explorationTitles = { + 'architecture': 'Architecture', + 'dependencies': 'Dependencies', + 'patterns': 'Patterns', + 'integration-points': 'Integration Points' + }; + + for (const angle of explorationOrder) { + const expData = data[angle]; + if (!expData) continue; + + sections.push(` +
+
+ â–ļ + +
+ +
+ `); + } + + return `
${sections.join('')}
`; +} + +function renderExplorationAngle(angle, data) { + let content = []; + + // Project structure (architecture) + if (data.project_structure) { + content.push(` +
+ +

${escapeHtml(data.project_structure)}

+
+ `); + } + + // Relevant files + if (data.relevant_files && data.relevant_files.length) { + content.push(` +
+ +
+ ${data.relevant_files.slice(0, 10).map(f => ` +
+
${escapeHtml(f.path || '')}
+
Relevance: ${(f.relevance * 100).toFixed(0)}%
+ ${f.rationale ? `
${escapeHtml(f.rationale.substring(0, 200))}...
` : ''} +
+ `).join('')} + ${data.relevant_files.length > 10 ? `
... and ${data.relevant_files.length - 10} more files
` : ''} +
+
+ `); + } + + // Patterns + if (data.patterns) { + content.push(` +
+ +

${escapeHtml(data.patterns)}

+
+ `); + } + + // Dependencies + if (data.dependencies) { + content.push(` +
+ +

${escapeHtml(data.dependencies)}

+
+ `); + } + + // Integration points + if (data.integration_points) { + content.push(` +
+ +

${escapeHtml(data.integration_points)}

+
+ `); + } + + // Constraints + if (data.constraints) { + content.push(` +
+ +

${escapeHtml(data.constraints)}

+
+ `); + } + + // Clarification needs + if (data.clarification_needs && data.clarification_needs.length) { + content.push(` +
+ +
+ ${data.clarification_needs.map(c => ` +
+
${escapeHtml(c.question)}
+ ${c.options && c.options.length ? ` +
+ ${c.options.map((opt, i) => ` + ${escapeHtml(opt)} + `).join('')} +
+ ` : ''} +
+ `).join('')} +
+
+ `); + } + + return content.join('') || '

No data available

'; +} diff --git a/ccw/src/templates/dashboard-js/components/task-drawer-core.js b/ccw/src/templates/dashboard-js/components/task-drawer-core.js new file mode 100644 index 00000000..656473bb --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/task-drawer-core.js @@ -0,0 +1,477 @@ +// ========================================== +// TASK DRAWER CORE +// ========================================== +// Core drawer functionality and main rendering functions + +let currentDrawerTasks = []; + +function openTaskDrawer(taskId) { + const task = currentDrawerTasks.find(t => (t.task_id || t.id) === taskId); + if (!task) { + console.error('Task not found:', taskId); + return; + } + + document.getElementById('drawerTaskTitle').textContent = task.title || taskId; + document.getElementById('drawerContent').innerHTML = renderTaskDrawerContent(task); + document.getElementById('taskDetailDrawer').classList.add('open'); + document.getElementById('drawerOverlay').classList.add('active'); + + // Initialize flowchart after DOM is updated + setTimeout(() => { + renderFullFlowchart(task.flow_control); + }, 100); +} + +function openTaskDrawerForLite(sessionId, taskId) { + const session = liteTaskDataStore[currentSessionDetailKey]; + if (!session) return; + + const task = session.tasks?.find(t => t.id === taskId); + if (!task) return; + + // Set current drawer tasks and session context + currentDrawerTasks = session.tasks || []; + window._currentDrawerSession = session; + + document.getElementById('drawerTaskTitle').textContent = task.title || taskId; + // Use dedicated lite task drawer renderer + document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session); + document.getElementById('taskDetailDrawer').classList.add('open'); + document.getElementById('drawerOverlay').classList.add('active'); +} + +function closeTaskDrawer() { + document.getElementById('taskDetailDrawer').classList.remove('open'); + document.getElementById('drawerOverlay').classList.remove('active'); +} + +function switchDrawerTab(tabName) { + // Update tab buttons + document.querySelectorAll('.drawer-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.tab === tabName); + }); + + // Update tab panels + document.querySelectorAll('.drawer-panel').forEach(panel => { + panel.classList.toggle('active', panel.dataset.tab === tabName); + }); + + // Render flowchart if switching to flowchart tab + if (tabName === 'flowchart') { + const taskId = document.getElementById('drawerTaskTitle').textContent; + const task = currentDrawerTasks.find(t => t.title === taskId || t.task_id === taskId); + if (task?.flow_control) { + setTimeout(() => renderFullFlowchart(task.flow_control), 50); + } + } +} + +function renderTaskDrawerContent(task) { + const fc = task.flow_control || {}; + + return ` + +
+ ${escapeHtml(task.task_id || task.id || 'N/A')} + ${task.status || 'pending'} +
+ + +
+ + + + +
+ + +
+ +
+ ${renderPreAnalysisSteps(fc.pre_analysis)} + ${renderImplementationStepsList(fc.implementation_approach)} +
+ + +
+
+
+ + +
+ ${renderTargetFiles(fc.target_files)} + ${fc.test_commands ? renderTestCommands(fc.test_commands) : ''} +
+ + +
+
${escapeHtml(JSON.stringify(task, null, 2))}
+
+
+ `; +} + +function renderLiteTaskDrawerContent(task, session) { + const rawTask = task._raw || task; + + return ` + +
+ ${escapeHtml(task.task_id || task.id || 'N/A')} + ${rawTask.action ? `${escapeHtml(rawTask.action)}` : ''} +
+ + +
+ + + + +
+ + +
+ +
+ ${renderLiteTaskOverview(rawTask)} +
+ + +
+ ${renderLiteTaskImplementation(rawTask)} +
+ + +
+ ${renderLiteTaskFiles(rawTask)} +
+ + +
+
${escapeHtml(JSON.stringify(rawTask, null, 2))}
+
+
+ `; +} + +// Render plan.json task details in drawer (for lite tasks) +function renderPlanTaskDetails(task, session) { + if (!task) return ''; + + // Get corresponding plan task if available + const planTask = session?.plan?.tasks?.find(pt => pt.id === task.id); + if (!planTask) { + // Fallback: task itself might have plan-like structure + return renderTaskImplementationDetails(task); + } + + return renderTaskImplementationDetails(planTask); +} + +function renderTaskImplementationDetails(task) { + const sections = []; + + // Description + if (task.description) { + sections.push(` +
+

Description

+

${escapeHtml(task.description)}

+
+ `); + } + + // Modification Points + if (task.modification_points?.length) { + sections.push(` +
+

Modification Points

+
+ ${task.modification_points.map(mp => ` +
+
+ 📄 + ${escapeHtml(mp.file || mp.path || '')} +
+ ${mp.target ? `
Target: ${escapeHtml(mp.target)}
` : ''} + ${mp.change ? `
${escapeHtml(mp.change)}
` : ''} +
+ `).join('')} +
+
+ `); + } + + // Implementation Steps + if (task.implementation?.length) { + sections.push(` +
+

Implementation Steps

+
    + ${task.implementation.map(step => ` +
  1. ${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}
  2. + `).join('')} +
+
+ `); + } + + // Reference + if (task.reference) { + sections.push(` +
+

Reference

+ ${task.reference.pattern ? `
Pattern: ${escapeHtml(task.reference.pattern)}
` : ''} + ${task.reference.files?.length ? ` +
+ Files: +
    + ${task.reference.files.map(f => `
  • ${escapeHtml(f)}
  • `).join('')} +
+
+ ` : ''} + ${task.reference.examples ? `
Examples: ${escapeHtml(task.reference.examples)}
` : ''} +
+ `); + } + + // Acceptance Criteria + if (task.acceptance?.length) { + sections.push(` +
+

Acceptance Criteria

+
    + ${task.acceptance.map(a => `
  • ${escapeHtml(a)}
  • `).join('')} +
+
+ `); + } + + // Dependencies + if (task.depends_on?.length) { + sections.push(` +
+

Dependencies

+
+ ${task.depends_on.map(dep => `${escapeHtml(dep)}`).join(' ')} +
+
+ `); + } + + return sections.join(''); +} + +// Render lite task overview +function renderLiteTaskOverview(task) { + let sections = []; + + // Description Card + if (task.description) { + sections.push(` +
+
+ 📝 +

Description

+
+
+

${escapeHtml(task.description)}

+
+
+ `); + } + + // Scope Card + if (task.scope) { + sections.push(` +
+
+ 📂 +

Scope

+
+
+
+ ${escapeHtml(task.scope)} +
+
+
+ `); + } + + // Acceptance Criteria Card + if (task.acceptance && task.acceptance.length > 0) { + sections.push(` +
+
+ ✅ +

Acceptance Criteria

+ ${task.acceptance.length} +
+
+
    + ${task.acceptance.map(a => ` +
  • + ○ + ${escapeHtml(a)} +
  • + `).join('')} +
+
+
+ `); + } + + // Reference Card + if (task.reference) { + sections.push(` +
+
+ 📚 +

Reference

+
+
+ ${task.reference.pattern ? ` +
+ Pattern: + ${escapeHtml(task.reference.pattern)} +
+ ` : ''} + ${task.reference.files && task.reference.files.length > 0 ? ` +
+ Files: +
+ ${task.reference.files.map(f => `${escapeHtml(f)}`).join('')} +
+
+ ` : ''} + ${task.reference.examples ? ` +
+ Examples: + ${escapeHtml(task.reference.examples)} +
+ ` : ''} +
+
+ `); + } + + // Dependencies Card + if (task.depends_on && task.depends_on.length > 0) { + sections.push(` +
+
+ 🔗 +

Dependencies

+
+
+
+ ${task.depends_on.map(dep => `${escapeHtml(dep)}`).join('')} +
+
+
+ `); + } + + return sections.length > 0 ? sections.join('') : '
No overview data
'; +} + +// Render lite task implementation steps +function renderLiteTaskImplementation(task) { + let sections = []; + + // Implementation Steps Card + if (task.implementation && task.implementation.length > 0) { + sections.push(` +
+
+ 📋 +

Implementation Steps

+ ${task.implementation.length} +
+
+
+ ${task.implementation.map((step, idx) => ` +
+
${idx + 1}
+
+

${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}

+
+
+ `).join('')} +
+
+
+ `); + } + + // Modification Points Card + if (task.modification_points && task.modification_points.length > 0) { + sections.push(` +
+
+ 🔧 +

Modification Points

+ ${task.modification_points.length} +
+
+
+ ${task.modification_points.map(mp => ` +
+
+ ${escapeHtml(mp.file || '')} +
+ ${mp.target ? ` +
+ Target: + ${escapeHtml(mp.target)} +
+ ` : ''} + ${mp.change ? ` +
${escapeHtml(mp.change)}
+ ` : ''} +
+ `).join('')} +
+
+
+ `); + } + + return sections.length > 0 ? sections.join('') : '
No implementation data
'; +} + +// Render lite task files +function renderLiteTaskFiles(task) { + const files = []; + + // Collect from modification_points + if (task.modification_points) { + task.modification_points.forEach(mp => { + if (mp.file && !files.includes(mp.file)) files.push(mp.file); + }); + } + + // Collect from scope + if (task.scope && !files.includes(task.scope)) { + files.push(task.scope); + } + + if (files.length === 0) { + return '
No files specified
'; + } + + return ` +
+

Target Files

+
    + ${files.map(f => ` +
  • + 📄 + ${escapeHtml(f)} +
  • + `).join('')} +
+
+ `; +} diff --git a/ccw/src/templates/dashboard-js/components/task-drawer-renderers.js b/ccw/src/templates/dashboard-js/components/task-drawer-renderers.js new file mode 100644 index 00000000..ff3fa751 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/task-drawer-renderers.js @@ -0,0 +1,447 @@ +// ========================================== +// TASK DRAWER RENDERERS +// ========================================== +// Detailed content renderers and helper functions for task drawer + +function renderPreAnalysisSteps(preAnalysis) { + if (!Array.isArray(preAnalysis) || preAnalysis.length === 0) { + return '
No pre-analysis steps
'; + } + + return ` +
+
+ 🔍 +

Pre-Analysis Steps

+ ${preAnalysis.length} +
+
+
+ ${preAnalysis.map((item, idx) => ` +
+
${idx + 1}
+
+

${escapeHtml(item.step || item.action || 'Step ' + (idx + 1))}

+ ${item.action && item.action !== item.step ? ` +
+ Action: + ${escapeHtml(item.action)} +
+ ` : ''} + ${item.commands?.length ? ` +
+ ${item.commands.map(c => `${escapeHtml(typeof c === 'string' ? c : JSON.stringify(c))}`).join('')} +
+ ` : ''} + ${item.output_to ? ` +
+ Output: + ${escapeHtml(item.output_to)} +
+ ` : ''} +
+
+ `).join('')} +
+
+
+ `; +} + +function renderImplementationStepsList(steps) { + if (!Array.isArray(steps) || steps.length === 0) { + return '
No implementation steps
'; + } + + return ` +
+
+ 📋 +

Implementation Approach

+ ${steps.length} +
+
+
+ ${steps.map((step, idx) => { + const hasMods = step.modification_points?.length; + const hasFlow = step.logic_flow?.length; + + return ` +
+
+
${step.step || idx + 1}
+
${escapeHtml(step.title || 'Untitled Step')}
+
+ ${step.description ? `
${escapeHtml(step.description)}
` : ''} + ${hasMods ? ` +
+ +
+ ${step.modification_points.map(mp => ` +
+ ${typeof mp === 'string' ? `${escapeHtml(mp)}` : ` + ${escapeHtml(mp.file || mp.path || '')} + ${mp.changes ? `${escapeHtml(mp.changes)}` : ''} + `} +
+ `).join('')} +
+
+ ` : ''} + ${hasFlow ? ` +
+ +
+ ${step.logic_flow.map((lf, lfIdx) => ` +
+ ${lfIdx + 1} + ${escapeHtml(typeof lf === 'string' ? lf : lf.action || JSON.stringify(lf))} +
+ `).join('')} +
+
+ ` : ''} + ${step.depends_on?.length ? ` +
+ Dependencies: +
+ ${step.depends_on.map(d => `${escapeHtml(d)}`).join('')} +
+
+ ` : ''} +
+ `}).join('')} +
+
+
+ `; +} + +function renderTargetFiles(files) { + if (!Array.isArray(files) || files.length === 0) { + return '
No target files
'; + } + + // Get current project path for building full paths + const projectPath = window.currentProjectPath || ''; + + return ` +
+
+ 📁 +

Target Files

+ ${files.length} +
+
+
+ ${files.map(f => { + const filePath = typeof f === 'string' ? f : (f.path || JSON.stringify(f)); + // Build full path for vscode link + const fullPath = filePath.startsWith('/') || filePath.includes(':') + ? filePath + : (projectPath ? `${projectPath}/${filePath}` : filePath); + const vscodeUri = `vscode://file/${fullPath.replace(/\\/g, '/')}`; + + return ` + + 📄 + ${escapeHtml(filePath)} + ↗ + + `; + }).join('')} +
+
+
+ `; +} + +function renderTestCommands(testCommands) { + if (!testCommands || typeof testCommands !== 'object') return ''; + + const entries = Object.entries(testCommands); + if (entries.length === 0) return ''; + + return ` +
+
+ đŸ§Ē +

Test Commands

+ ${entries.length} +
+
+
+ ${entries.map(([key, val]) => ` +
+ ${escapeHtml(key)} + ${escapeHtml(typeof val === 'string' ? val : JSON.stringify(val))} +
+ `).join('')} +
+
+
+ `; +} + +function renderTaskDetail(sessionId, task) { + // Get raw task data for JSON view + const rawTask = task._raw || task; + const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-'); + + // Store JSON in memory instead of inline script tag + taskJsonStore[taskJsonId] = rawTask; + + return ` +
+
+ ${escapeHtml(task.id)} + ${escapeHtml(task.title || 'Untitled')} + ${task.status} +
+ +
+
+ + +
+
+ â–ļ + + ${escapeHtml((task.meta?.type || task.meta?.action || '') + (task.meta?.scope ? ' | ' + task.meta.scope : ''))} +
+ +
+ + +
+
+ â–ļ + + ${escapeHtml(getContextPreview(task.context, rawTask))} +
+ +
+ + +
+
+ â–ļ + + ${escapeHtml(getFlowControlPreview(task.flow_control, rawTask))} +
+ +
+
+ `; +} + +function getContextPreview(context, rawTask) { + const items = []; + if (context?.requirements?.length) items.push(`${context.requirements.length} reqs`); + if (context?.acceptance?.length) items.push(`${context.acceptance.length} acceptance`); + if (context?.focus_paths?.length) items.push(`${context.focus_paths.length} paths`); + if (rawTask?.modification_points?.length) items.push(`${rawTask.modification_points.length} mods`); + return items.join(' | ') || 'No context'; +} + +function getFlowControlPreview(flowControl, rawTask) { + const steps = flowControl?.implementation_approach?.length || rawTask?.implementation?.length || 0; + return steps > 0 ? `${steps} steps` : 'No steps'; +} + +function renderDynamicFields(obj, priorityKeys = []) { + if (!obj || typeof obj !== 'object') return '
null
'; + + const entries = Object.entries(obj).filter(([k, v]) => v !== null && v !== undefined && k !== '_raw'); + if (entries.length === 0) return '
Empty
'; + + // Sort: priority keys first, then alphabetically + entries.sort(([a], [b]) => { + const aIdx = priorityKeys.indexOf(a); + const bIdx = priorityKeys.indexOf(b); + if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx; + if (aIdx !== -1) return -1; + if (bIdx !== -1) return 1; + return a.localeCompare(b); + }); + + return `
${entries.map(([key, value]) => renderFieldRow(key, value)).join('')}
`; +} + +function renderFieldRow(key, value) { + return ` +
+ ${escapeHtml(key)}: +
${renderFieldValue(key, value)}
+
+ `; +} + +function renderFieldValue(key, value) { + if (value === null || value === undefined) { + return 'null'; + } + + if (typeof value === 'boolean') { + return `${value}`; + } + + if (typeof value === 'number') { + return `${value}`; + } + + if (typeof value === 'string') { + // Check if it's a path + if (key.includes('path') || key.includes('file') || value.includes('/') || value.includes('\\')) { + return `${escapeHtml(value)}`; + } + return `${escapeHtml(value)}`; + } + + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + + // Check if array contains objects or strings + if (typeof value[0] === 'object') { + return `
${value.map((item, i) => ` +
+
[${i + 1}]
+ ${renderDynamicFields(item)} +
+ `).join('')}
`; + } + + // Array of strings/primitives + const isPathArray = key.includes('path') || key.includes('file'); + return `
${value.map(v => + `${escapeHtml(String(v))}` + ).join('')}
`; + } + + if (typeof value === 'object') { + return renderDynamicFields(value); + } + + return escapeHtml(String(value)); +} + +function renderContextFields(context, rawTask) { + const sections = []; + + // Requirements / Description + const requirements = context?.requirements || []; + const description = rawTask?.description; + if (requirements.length > 0 || description) { + sections.push(` +
+ + ${description ? `

${escapeHtml(description)}

` : ''} + ${requirements.length > 0 ? `
    ${requirements.map(r => `
  • ${escapeHtml(r)}
  • `).join('')}
` : ''} +
+ `); + } + + // Focus paths / Modification points + const focusPaths = context?.focus_paths || []; + const modPoints = rawTask?.modification_points || []; + if (focusPaths.length > 0 || modPoints.length > 0) { + sections.push(` +
+ + ${modPoints.length > 0 ? ` +
+ ${modPoints.map(m => ` +
+ ${escapeHtml(m.file || m)} + ${m.target ? `→ ${escapeHtml(m.target)}` : ''} + ${m.change ? `

${escapeHtml(m.change)}

` : ''} +
+ `).join('')} +
+ ` : ` +
${focusPaths.map(p => `${escapeHtml(p)}`).join('')}
+ `} +
+ `); + } + + // Acceptance criteria + const acceptance = context?.acceptance || rawTask?.acceptance || []; + if (acceptance.length > 0) { + sections.push(` +
+ +
    ${acceptance.map(a => `
  • ${escapeHtml(a)}
  • `).join('')}
+
+ `); + } + + // Dependencies + const depends = context?.depends_on || rawTask?.depends_on || []; + if (depends.length > 0) { + sections.push(` +
+ +
${depends.map(d => `${escapeHtml(d)}`).join('')}
+
+ `); + } + + // Reference + const reference = rawTask?.reference; + if (reference) { + sections.push(` +
+ + ${renderDynamicFields(reference)} +
+ `); + } + + return sections.length > 0 + ? `
${sections.join('')}
` + : '
No context data
'; +} + +function renderFlowControlDetails(flowControl, rawTask) { + const sections = []; + + // Pre-analysis + const preAnalysis = flowControl?.pre_analysis || rawTask?.pre_analysis || []; + if (preAnalysis.length > 0) { + sections.push(` +
+ +
    ${preAnalysis.map(p => `
  • ${escapeHtml(p)}
  • `).join('')}
+
+ `); + } + + // Target files + const targetFiles = flowControl?.target_files || rawTask?.target_files || []; + if (targetFiles.length > 0) { + sections.push(` +
+ +
${targetFiles.map(f => `${escapeHtml(f)}`).join('')}
+
+ `); + } + + return sections.join(''); +} diff --git a/ccw/src/templates/dashboard-js/components/theme.js b/ccw/src/templates/dashboard-js/components/theme.js new file mode 100644 index 00000000..b3da36e2 --- /dev/null +++ b/ccw/src/templates/dashboard-js/components/theme.js @@ -0,0 +1,21 @@ +// ========================================== +// THEME MANAGEMENT +// ========================================== + +function initTheme() { + const saved = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', saved); + updateThemeIcon(saved); + + document.getElementById('themeToggle').addEventListener('click', () => { + const current = document.documentElement.getAttribute('data-theme'); + const next = current === 'light' ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('theme', next); + updateThemeIcon(next); + }); +} + +function updateThemeIcon(theme) { + document.getElementById('themeToggle').textContent = theme === 'light' ? '🌙' : 'â˜€ī¸'; +} diff --git a/ccw/src/templates/dashboard-js/main.js b/ccw/src/templates/dashboard-js/main.js new file mode 100644 index 00000000..4d80b739 --- /dev/null +++ b/ccw/src/templates/dashboard-js/main.js @@ -0,0 +1,40 @@ +// Application Entry Point +// Initializes all components and sets up global event handlers + +document.addEventListener('DOMContentLoaded', async () => { + // Initialize components with error handling to prevent cascading failures + try { initTheme(); } catch (e) { console.error('Theme init failed:', e); } + try { initSidebar(); } catch (e) { console.error('Sidebar init failed:', e); } + try { initPathSelector(); } catch (e) { console.error('Path selector init failed:', e); } + try { initNavigation(); } catch (e) { console.error('Navigation init failed:', e); } + try { initSearch(); } catch (e) { console.error('Search init failed:', e); } + try { initRefreshButton(); } catch (e) { console.error('Refresh button init failed:', e); } + + // Server mode: load data from API + try { + if (window.SERVER_MODE) { + await switchToPath(window.INITIAL_PATH || projectPath); + } else { + renderDashboard(); + } + } catch (e) { + console.error('Dashboard render failed:', e); + } + + // Global Escape key handler for modals + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + closeMarkdownModal(); + + // Close JSON modal if exists + const jsonModal = document.querySelector('.json-modal-overlay'); + if (jsonModal) { + const closeBtn = jsonModal.querySelector('.json-modal-close'); + if (closeBtn) closeJsonModal(closeBtn); + } + + // Close path modal if exists + closePathModal(); + } + }); +}); diff --git a/ccw/src/templates/dashboard-js/state.js b/ccw/src/templates/dashboard-js/state.js new file mode 100644 index 00000000..d37a74f6 --- /dev/null +++ b/ccw/src/templates/dashboard-js/state.js @@ -0,0 +1,37 @@ +// ======================================== +// State Management +// ======================================== +// Global state variables and template placeholders +// This module must be loaded first as other modules depend on these variables + +// ========== Data Placeholders ========== +// These placeholders are replaced by the dashboard generator at build time +let workflowData = {{WORKFLOW_DATA}}; +let projectPath = '{{PROJECT_PATH}}'; +let recentPaths = {{RECENT_PATHS}}; + +// ========== Application State ========== +// Current filter for session list view ('all', 'active', 'archived') +let currentFilter = 'all'; + +// Current lite task type ('lite-plan', 'lite-fix', or null) +let currentLiteType = null; + +// Current view mode ('sessions', 'liteTasks', 'project-overview', 'sessionDetail', 'liteTaskDetail') +let currentView = 'sessions'; + +// Current session detail key (null when not in detail view) +let currentSessionDetailKey = null; + +// ========== Data Stores ========== +// Store session data for modal/detail access +// Key: session key, Value: session data object +const sessionDataStore = {}; + +// Store lite task session data for detail page access +// Key: session key, Value: lite session data object +const liteTaskDataStore = {}; + +// Store task JSON data in a global map instead of inline script tags +// Key: unique task ID, Value: raw task JSON data +const taskJsonStore = {}; diff --git a/ccw/src/templates/dashboard-js/utils.js b/ccw/src/templates/dashboard-js/utils.js new file mode 100644 index 00000000..4daf785d --- /dev/null +++ b/ccw/src/templates/dashboard-js/utils.js @@ -0,0 +1,134 @@ +// ======================================== +// Utility Functions +// ======================================== +// General-purpose helper functions used across the application + +// ========== HTML/Text Processing ========== + +/** + * Escape HTML special characters to prevent XSS attacks + * @param {string} str - String to escape + * @returns {string} Escaped string safe for HTML insertion + */ +function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Truncate text to specified maximum length + * @param {string} text - Text to truncate + * @param {number} maxLen - Maximum length (including ellipsis) + * @returns {string} Truncated text with '...' if needed + */ +function truncateText(text, maxLen) { + if (!text) return ''; + return text.length > maxLen ? text.substring(0, maxLen - 3) + '...' : text; +} + +/** + * Normalize line endings in content + * Handles both literal \r\n escape sequences and actual newlines + * @param {string} content - Content to normalize + * @returns {string} Content with normalized line endings (LF only) + */ +function normalizeLineEndings(content) { + if (!content) return ''; + let normalized = content; + // If content has literal \r\n or \n as text (escaped), convert to actual newlines + if (normalized.includes('\\r\\n')) { + normalized = normalized.replace(/\\r\\n/g, '\n'); + } else if (normalized.includes('\\n')) { + normalized = normalized.replace(/\\n/g, '\n'); + } + // Normalize CRLF to LF for consistent rendering + normalized = normalized.replace(/\r\n/g, '\n'); + return normalized; +} + +// ========== Date/Time Formatting ========== + +/** + * Format ISO date string to human-readable format + * @param {string} dateStr - ISO date string + * @returns {string} Formatted date string (YYYY/MM/DD HH:mm) or '-' if invalid + */ +function formatDate(dateStr) { + if (!dateStr) return '-'; + try { + const date = new Date(dateStr); + // Check if date is valid + if (isNaN(date.getTime())) return '-'; + // Format: YYYY/MM/DD HH:mm + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${year}/${month}/${day} ${hours}:${minutes}`; + } catch (e) { + return '-'; + } +} + +// ========== UI Helpers ========== + +/** + * Get color for relevance score visualization + * @param {number} score - Relevance score (0-1) + * @returns {string} CSS color value + */ +function getRelevanceColor(score) { + if (score >= 0.95) return '#10b981'; + if (score >= 0.90) return '#3b82f6'; + if (score >= 0.80) return '#f59e0b'; + return '#6b7280'; +} + +/** + * Get CSS class for role badge styling + * @param {string} role - Role identifier + * @returns {string} CSS class name + */ +function getRoleBadgeClass(role) { + const roleMap = { + 'core-hook': 'primary', + 'api-client': 'success', + 'api-router': 'info', + 'service-layer': 'warning', + 'pydantic-schemas': 'secondary', + 'orm-model': 'secondary', + 'typescript-types': 'info' + }; + return roleMap[role] || 'secondary'; +} + +/** + * Toggle collapsible section visibility + * @param {HTMLElement} header - Section header element + */ +function toggleSection(header) { + const content = header.nextElementSibling; + const icon = header.querySelector('.collapse-icon'); + const isCollapsed = content.classList.contains('collapsed'); + + content.classList.toggle('collapsed'); + header.classList.toggle('expanded'); + icon.textContent = isCollapsed ? 'â–ŧ' : 'â–ļ'; + + // Render flowchart if expanding flow_control section + if (isCollapsed && header.querySelector('.section-label')?.textContent === 'flow_control') { + const taskId = content.closest('[data-task-id]')?.dataset.taskId; + if (taskId) { + const task = taskJsonStore[taskId]; + if (task?.flow_control) { + setTimeout(() => renderFullFlowchart(task.flow_control), 100); + } + } + } +} diff --git a/ccw/src/templates/dashboard-js/views/fix-session.js b/ccw/src/templates/dashboard-js/views/fix-session.js new file mode 100644 index 00000000..0d849228 --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/fix-session.js @@ -0,0 +1,180 @@ +// ============================================ +// FIX SESSION VIEW +// ============================================ +// Fix session detail page rendering + +function renderFixSessionDetailPage(session) { + const isActive = session._isActive !== false; + const tasks = session.tasks || []; + + // Calculate fix statistics + const totalTasks = tasks.length; + const fixedCount = tasks.filter(t => t.status === 'completed' && t.result === 'fixed').length; + const failedCount = tasks.filter(t => t.status === 'completed' && t.result === 'failed').length; + const pendingCount = tasks.filter(t => t.status === 'pending').length; + const inProgressCount = tasks.filter(t => t.status === 'in_progress').length; + const percentComplete = totalTasks > 0 ? ((fixedCount + failedCount) / totalTasks * 100) : 0; + + return ` +
+ +
+ +
+

🔧 ${escapeHtml(session.session_id)}

+
+ Fix + + ${isActive ? 'ACTIVE' : 'ARCHIVED'} + +
+
+
+ + +
+
+

🔧 Fix Progress

+ ${session.phase || 'Execution'} +
+ + +
+
+
+
+ ${fixedCount + failedCount}/${totalTasks} completed (${percentComplete.toFixed(1)}%) +
+ + +
+
+
📊
+
${totalTasks}
+
Total Tasks
+
+
+
✅
+
${fixedCount}
+
Fixed
+
+
+
❌
+
${failedCount}
+
Failed
+
+
+
âŗ
+
${pendingCount}
+
Pending
+
+
+ + + ${session.stages && session.stages.length > 0 ? ` +
+ ${session.stages.map((stage, idx) => ` +
+
Stage ${idx + 1}
+
${stage.execution_mode === 'parallel' ? '⚡ Parallel' : 'âžĄī¸ Serial'}
+
${stage.groups?.length || 0} groups
+
+ `).join('')} +
+ ` : ''} +
+ + +
+
+

📋 Fix Tasks

+
+ + + + + +
+
+
+ ${renderFixTasksGrid(tasks)} +
+
+ + +
+
+ Created: + ${formatDate(session.created_at)} +
+ ${session.archived_at ? ` +
+ Archived: + ${formatDate(session.archived_at)} +
+ ` : ''} +
+ Project: + ${escapeHtml(session.project || '-')} +
+
+
+ `; +} + +function renderFixTasksGrid(tasks) { + if (!tasks || tasks.length === 0) { + return ` +
+
📋
+
No fix tasks found
+
+ `; + } + + return tasks.map(task => { + const statusClass = task.status === 'completed' ? (task.result || 'completed') : task.status; + const statusText = task.status === 'completed' ? (task.result || 'completed') : task.status; + + return ` +
+
+ ${escapeHtml(task.task_id || task.id || 'N/A')} + ${statusText} +
+
${escapeHtml(task.title || 'Untitled Task')}
+ ${task.finding_title ? `
${escapeHtml(task.finding_title)}
` : ''} + ${task.file ? `
📄 ${escapeHtml(task.file)}${task.line ? ':' + task.line : ''}
` : ''} +
+ ${task.dimension ? `${escapeHtml(task.dimension)}` : ''} + ${task.attempts && task.attempts > 1 ? `🔄 ${task.attempts} attempts` : ''} + ${task.commit_hash ? `💾 ${task.commit_hash.substring(0, 7)}` : ''} +
+
+ `; + }).join(''); +} + +function initFixSessionPage(session) { + // Initialize event handlers for fix session page + // Filter handlers are inline onclick +} + +function filterFixTasks(status) { + // Update filter buttons + document.querySelectorAll('.task-filters .filter-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.status === status); + }); + + // Filter task cards + document.querySelectorAll('.fix-task-card').forEach(card => { + if (status === 'all' || card.dataset.status === status) { + card.style.display = ''; + } else { + card.style.display = 'none'; + } + }); +} diff --git a/ccw/src/templates/dashboard-js/views/home.js b/ccw/src/templates/dashboard-js/views/home.js new file mode 100644 index 00000000..d666b579 --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/home.js @@ -0,0 +1,108 @@ +// ========================================== +// HOME VIEW - Dashboard Homepage +// ========================================== + +function renderDashboard() { + updateStats(); + updateBadges(); + renderSessions(); + document.getElementById('generatedAt').textContent = workflowData.generatedAt || new Date().toISOString(); +} + +function updateStats() { + const stats = workflowData.statistics || {}; + document.getElementById('statTotalSessions').textContent = stats.totalSessions || 0; + document.getElementById('statActiveSessions').textContent = stats.activeSessions || 0; + document.getElementById('statTotalTasks').textContent = stats.totalTasks || 0; + document.getElementById('statCompletedTasks').textContent = stats.completedTasks || 0; +} + +function updateBadges() { + const active = workflowData.activeSessions || []; + const archived = workflowData.archivedSessions || []; + + document.getElementById('badgeAll').textContent = active.length + archived.length; + document.getElementById('badgeActive').textContent = active.length; + document.getElementById('badgeArchived').textContent = archived.length; + + // Lite Tasks badges + const liteTasks = workflowData.liteTasks || {}; + document.getElementById('badgeLitePlan').textContent = liteTasks.litePlan?.length || 0; + document.getElementById('badgeLiteFix').textContent = liteTasks.liteFix?.length || 0; +} + +function renderSessions() { + const container = document.getElementById('mainContent'); + + let sessions = []; + + if (currentFilter === 'all' || currentFilter === 'active') { + sessions = sessions.concat((workflowData.activeSessions || []).map(s => ({ ...s, _isActive: true }))); + } + if (currentFilter === 'all' || currentFilter === 'archived') { + sessions = sessions.concat((workflowData.archivedSessions || []).map(s => ({ ...s, _isActive: false }))); + } + + if (sessions.length === 0) { + container.innerHTML = ` +
+
📭
+
No Sessions Found
+
No workflow sessions match your current filter.
+
+ `; + return; + } + + container.innerHTML = `
${sessions.map(session => renderSessionCard(session)).join('')}
`; +} + +function renderSessionCard(session) { + const tasks = session.tasks || []; + const taskCount = session.taskCount || tasks.length; + const completed = tasks.filter(t => t.status === 'completed').length; + const progress = taskCount > 0 ? Math.round((completed / taskCount) * 100) : 0; + + // Use _isActive flag set during rendering, default to true + const isActive = session._isActive !== false; + const date = session.created_at; + + // Get session type badge + const sessionType = session.type || 'workflow'; + const typeBadge = sessionType !== 'workflow' ? `${sessionType}` : ''; + + // Store session data for modal + const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-'); + sessionDataStore[sessionKey] = session; + + return ` +
+
+
${escapeHtml(session.session_id || 'Unknown')}
+
+ ${typeBadge} + + ${isActive ? 'ACTIVE' : 'ARCHIVED'} + +
+
+
+
+ 📅 ${formatDate(date)} + 📋 ${taskCount} tasks +
+ ${taskCount > 0 ? ` +
+ Progress +
+
+
+
+ ${completed}/${taskCount} (${progress}%) +
+
+ ` : ''} +
+
+ `; +} diff --git a/ccw/src/templates/dashboard-js/views/lite-tasks.js b/ccw/src/templates/dashboard-js/views/lite-tasks.js new file mode 100644 index 00000000..a71705ad --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/lite-tasks.js @@ -0,0 +1,382 @@ +// ============================================ +// LITE TASKS VIEW +// ============================================ +// Lite-plan and lite-fix task list and detail rendering + +function renderLiteTasks() { + const container = document.getElementById('mainContent'); + + const liteTasks = workflowData.liteTasks || {}; + const sessions = currentLiteType === 'lite-plan' + ? liteTasks.litePlan || [] + : liteTasks.liteFix || []; + + if (sessions.length === 0) { + container.innerHTML = ` +
+
⚡
+
No ${currentLiteType} Sessions
+
No sessions found in .workflow/.${currentLiteType}/
+
+ `; + return; + } + + container.innerHTML = `
${sessions.map(session => renderLiteTaskCard(session)).join('')}
`; + + // Initialize collapsible sections + document.querySelectorAll('.collapsible-header').forEach(header => { + header.addEventListener('click', () => toggleSection(header)); + }); + + // Render flowcharts for expanded tasks + sessions.forEach(session => { + session.tasks?.forEach(task => { + if (task.flow_control?.implementation_approach) { + renderFlowchartForTask(session.id, task); + } + }); + }); +} + +function renderLiteTaskCard(session) { + const tasks = session.tasks || []; + + // Store session data for detail page + const sessionKey = `lite-${session.type}-${session.id}`.replace(/[^a-zA-Z0-9-]/g, '-'); + liteTaskDataStore[sessionKey] = session; + + return ` +
+
+
${escapeHtml(session.id)}
+ + ${session.type === 'lite-plan' ? '📝 PLAN' : '🔧 FIX'} + +
+
+
+ 📅 ${formatDate(session.createdAt)} + 📋 ${tasks.length} tasks +
+
+
+ `; +} + +// Lite Task Detail Page +function showLiteTaskDetailPage(sessionKey) { + const session = liteTaskDataStore[sessionKey]; + if (!session) return; + + currentView = 'liteTaskDetail'; + currentSessionDetailKey = sessionKey; + + // Also store in sessionDataStore for tab switching compatibility + sessionDataStore[sessionKey] = { + ...session, + session_id: session.id, + created_at: session.createdAt, + path: session.path, + type: session.type + }; + + const container = document.getElementById('mainContent'); + const tasks = session.tasks || []; + const plan = session.plan || {}; + const progress = session.progress || { total: 0, completed: 0, percentage: 0 }; + + const completed = tasks.filter(t => t.status === 'completed').length; + const inProgress = tasks.filter(t => t.status === 'in_progress').length; + const pending = tasks.filter(t => t.status === 'pending').length; + + container.innerHTML = ` +
+ +
+ +
+

${session.type === 'lite-plan' ? '📝' : '🔧'} ${escapeHtml(session.id)}

+
+ ${session.type} +
+
+
+ + +
+
+ Created: + ${formatDate(session.createdAt)} +
+
+ Tasks: + ${tasks.length} tasks +
+
+ + +
+ + + + +
+ + +
+ ${renderLiteTasksTab(session, tasks, completed, inProgress, pending)} +
+
+ `; + + // Initialize collapsible sections + setTimeout(() => { + document.querySelectorAll('.collapsible-header').forEach(header => { + header.addEventListener('click', () => toggleSection(header)); + }); + }, 50); +} + +function goBackToLiteTasks() { + currentView = 'liteTasks'; + currentSessionDetailKey = null; + updateContentTitle(); + renderLiteTasks(); +} + +function switchLiteDetailTab(tabName) { + // Update active tab + document.querySelectorAll('.detail-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.tab === tabName); + }); + + const session = liteTaskDataStore[currentSessionDetailKey]; + if (!session) return; + + const contentArea = document.getElementById('liteDetailTabContent'); + const tasks = session.tasks || []; + const completed = tasks.filter(t => t.status === 'completed').length; + const inProgress = tasks.filter(t => t.status === 'in_progress').length; + const pending = tasks.filter(t => t.status === 'pending').length; + + switch (tabName) { + case 'tasks': + contentArea.innerHTML = renderLiteTasksTab(session, tasks, completed, inProgress, pending); + // Re-initialize collapsible sections + setTimeout(() => { + document.querySelectorAll('.collapsible-header').forEach(header => { + header.addEventListener('click', () => toggleSection(header)); + }); + }, 50); + break; + case 'plan': + contentArea.innerHTML = renderLitePlanTab(session); + break; + case 'context': + loadAndRenderLiteContextTab(session, contentArea); + break; + case 'summary': + loadAndRenderLiteSummaryTab(session, contentArea); + break; + } +} + +function renderLiteTasksTab(session, tasks, completed, inProgress, pending) { + // Populate drawer tasks for click-to-open functionality + currentDrawerTasks = tasks; + + if (tasks.length === 0) { + return ` +
+
📋
+
No Tasks
+
This session has no tasks defined.
+
+ `; + } + + return ` +
+
+ ${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')} +
+
+ `; +} + +function renderLiteTaskDetailItem(sessionId, task) { + const rawTask = task._raw || task; + const taskJsonId = `task-json-${sessionId}-${task.id}`.replace(/[^a-zA-Z0-9-]/g, '-'); + taskJsonStore[taskJsonId] = rawTask; + + // Get preview info for lite tasks + const action = rawTask.action || ''; + const scope = rawTask.scope || ''; + const modCount = rawTask.modification_points?.length || 0; + const implCount = rawTask.implementation?.length || 0; + const acceptCount = rawTask.acceptance?.length || 0; + + return ` +
+
+ ${escapeHtml(task.id)} + ${escapeHtml(task.title || 'Untitled')} + +
+
+ ${action ? `${escapeHtml(action)}` : ''} + ${scope ? `${escapeHtml(scope)}` : ''} + ${modCount > 0 ? `${modCount} mods` : ''} + ${implCount > 0 ? `${implCount} steps` : ''} + ${acceptCount > 0 ? `${acceptCount} acceptance` : ''} +
+
+ `; +} + +function getMetaPreviewForLite(task, rawTask) { + const meta = task.meta || {}; + const parts = []; + if (meta.type || rawTask.action) parts.push(meta.type || rawTask.action); + if (meta.scope || rawTask.scope) parts.push(meta.scope || rawTask.scope); + return parts.join(' | ') || 'No meta'; +} + +function openTaskDrawerForLite(sessionId, taskId) { + const session = liteTaskDataStore[currentSessionDetailKey]; + if (!session) return; + + const task = session.tasks?.find(t => t.id === taskId); + if (!task) return; + + // Set current drawer tasks and session context + currentDrawerTasks = session.tasks || []; + window._currentDrawerSession = session; + + document.getElementById('drawerTaskTitle').textContent = task.title || taskId; + // Use dedicated lite task drawer renderer + document.getElementById('drawerContent').innerHTML = renderLiteTaskDrawerContent(task, session); + document.getElementById('taskDetailDrawer').classList.add('open'); + document.getElementById('drawerOverlay').classList.add('active'); +} + +function renderLitePlanTab(session) { + const plan = session.plan; + + if (!plan) { + return ` +
+
📐
+
No Plan Data
+
No plan.json found for this session.
+
+ `; + } + + return ` +
+ + ${plan.summary ? ` +
+

📋 Summary

+

${escapeHtml(plan.summary)}

+
+ ` : ''} + + + ${plan.approach ? ` +
+

đŸŽ¯ Approach

+

${escapeHtml(plan.approach)}

+
+ ` : ''} + + + ${plan.focus_paths?.length ? ` +
+

📁 Focus Paths

+
+ ${plan.focus_paths.map(p => `${escapeHtml(p)}`).join('')} +
+
+ ` : ''} + + +
+

â„šī¸ Metadata

+
+ ${plan.estimated_time ? `
Estimated Time: ${escapeHtml(plan.estimated_time)}
` : ''} + ${plan.complexity ? `
Complexity: ${escapeHtml(plan.complexity)}
` : ''} + ${plan.recommended_execution ? `
Execution: ${escapeHtml(plan.recommended_execution)}
` : ''} +
+
+ + +
+

{ } Raw JSON

+
${escapeHtml(JSON.stringify(plan, null, 2))}
+
+
+ `; +} + +async function loadAndRenderLiteContextTab(session, contentArea) { + contentArea.innerHTML = '
Loading context data...
'; + + try { + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderLiteContextContent(data.context, session); + return; + } + } + // Fallback: show plan context if available + contentArea.innerHTML = renderLiteContextContent(null, session); + } catch (err) { + contentArea.innerHTML = `
Failed to load context: ${err.message}
`; + } +} + +async function loadAndRenderLiteSummaryTab(session, contentArea) { + contentArea.innerHTML = '
Loading summaries...
'; + + try { + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderSummaryContent(data.summaries); + return; + } + } + // Fallback + contentArea.innerHTML = ` +
+
📝
+
No Summaries
+
No summaries found in .summaries/
+
+ `; + } catch (err) { + contentArea.innerHTML = `
Failed to load summaries: ${err.message}
`; + } +} diff --git a/ccw/src/templates/dashboard-js/views/project-overview.js b/ccw/src/templates/dashboard-js/views/project-overview.js new file mode 100644 index 00000000..8ebb311d --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/project-overview.js @@ -0,0 +1,243 @@ +// ========================================== +// PROJECT OVERVIEW VIEW +// ========================================== + +function renderProjectOverview() { + const container = document.getElementById('mainContent'); + const project = workflowData.projectOverview; + + if (!project) { + container.innerHTML = ` +
+
📋
+

No Project Overview

+

+ Run /workflow:init to initialize project analysis +

+
+ `; + return; + } + + container.innerHTML = ` + +
+
+
+

${escapeHtml(project.projectName)}

+

${escapeHtml(project.description || 'No description available')}

+
+
+
Initialized: ${formatDate(project.initializedAt)}
+
Mode: ${escapeHtml(project.metadata?.analysis_mode || 'unknown')}
+
+
+
+ + +
+

+ đŸ’ģ Technology Stack +

+ + +
+

Languages

+
+ ${project.technologyStack.languages.map(lang => ` +
+ ${escapeHtml(lang.name)} + ${lang.file_count} files + ${lang.primary ? 'Primary' : ''} +
+ `).join('') || 'No languages detected'} +
+
+ + +
+

Frameworks

+
+ ${project.technologyStack.frameworks.map(fw => ` + ${escapeHtml(fw)} + `).join('') || 'No frameworks detected'} +
+
+ + +
+

Build Tools

+
+ ${project.technologyStack.build_tools.map(tool => ` + ${escapeHtml(tool)} + `).join('') || 'No build tools detected'} +
+
+ + +
+

Test Frameworks

+
+ ${project.technologyStack.test_frameworks.map(fw => ` + ${escapeHtml(fw)} + `).join('') || 'No test frameworks detected'} +
+
+
+ + +
+

+ đŸ—ī¸ Architecture +

+ +
+ +
+

Style

+
+ ${escapeHtml(project.architecture.style)} +
+
+ + +
+

Layers

+
+ ${project.architecture.layers.map(layer => ` + ${escapeHtml(layer)} + `).join('') || 'None'} +
+
+ + +
+

Patterns

+
+ ${project.architecture.patterns.map(pattern => ` + ${escapeHtml(pattern)} + `).join('') || 'None'} +
+
+
+
+ + +
+

+ âš™ī¸ Key Components +

+ + ${project.keyComponents.length > 0 ? ` +
+ ${project.keyComponents.map(comp => { + const importanceColors = { + high: 'border-l-4 border-l-destructive bg-destructive/5', + medium: 'border-l-4 border-l-warning bg-warning/5', + low: 'border-l-4 border-l-muted-foreground bg-muted' + }; + const importanceBadges = { + high: 'High', + medium: 'Medium', + low: 'Low' + }; + return ` +
+
+

${escapeHtml(comp.name)}

+ ${importanceBadges[comp.importance] || ''} +
+

${escapeHtml(comp.description)}

+ ${escapeHtml(comp.path)} +
+ `; + }).join('')} +
+ ` : '

No key components identified

'} +
+ + +
+

+ 📝 Development History +

+ + ${renderDevelopmentIndex(project.developmentIndex)} +
+ + +
+

+ 📊 Statistics +

+ +
+
+
${project.statistics.total_features || 0}
+
Total Features
+
+
+
${project.statistics.total_sessions || 0}
+
Total Sessions
+
+
+
Last Updated
+
${formatDate(project.statistics.last_updated)}
+
+
+
+ `; +} + +function renderDevelopmentIndex(devIndex) { + if (!devIndex) return '

No development history available

'; + + const categories = [ + { key: 'feature', label: 'Features', icon: '✨', badgeClass: 'bg-primary-light text-primary' }, + { key: 'enhancement', label: 'Enhancements', icon: '⚡', badgeClass: 'bg-success-light text-success' }, + { key: 'bugfix', label: 'Bug Fixes', icon: '🐛', badgeClass: 'bg-destructive/10 text-destructive' }, + { key: 'refactor', label: 'Refactorings', icon: '🔧', badgeClass: 'bg-warning-light text-warning' }, + { key: 'docs', label: 'Documentation', icon: '📚', badgeClass: 'bg-muted text-muted-foreground' } + ]; + + const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0); + + if (totalEntries === 0) { + return '

No development history entries

'; + } + + return ` +
+ ${categories.map(cat => { + const entries = devIndex[cat.key] || []; + if (entries.length === 0) return ''; + + return ` +
+

+ ${cat.icon} + ${cat.label} + ${entries.length} +

+
+ ${entries.slice(0, 5).map(entry => ` +
+
+
${escapeHtml(entry.title)}
+ ${formatDate(entry.date)} +
+ ${entry.description ? `

${escapeHtml(entry.description)}

` : ''} +
+ ${entry.sub_feature ? `${escapeHtml(entry.sub_feature)}` : ''} + ${entry.status ? `${escapeHtml(entry.status)}` : ''} +
+
+ `).join('')} + ${entries.length > 5 ? `
... and ${entries.length - 5} more
` : ''} +
+
+ `; + }).join('')} +
+ `; +} diff --git a/ccw/src/templates/dashboard-js/views/review-session.js b/ccw/src/templates/dashboard-js/views/review-session.js new file mode 100644 index 00000000..2219baeb --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/review-session.js @@ -0,0 +1,176 @@ +// ========================================== +// REVIEW SESSION DETAIL PAGE +// ========================================== + +function renderReviewSessionDetailPage(session) { + const isActive = session._isActive !== false; + const tasks = session.tasks || []; + const dimensions = session.reviewDimensions || []; + + // Calculate review statistics + const totalFindings = dimensions.reduce((sum, d) => sum + (d.findings?.length || 0), 0); + const criticalCount = dimensions.reduce((sum, d) => + sum + (d.findings?.filter(f => f.severity === 'critical').length || 0), 0); + const highCount = dimensions.reduce((sum, d) => + sum + (d.findings?.filter(f => f.severity === 'high').length || 0), 0); + + return ` +
+ +
+ +
+

🔍 ${escapeHtml(session.session_id)}

+
+ Review + + ${isActive ? 'ACTIVE' : 'ARCHIVED'} + +
+
+
+ + +
+
+

📊 Review Progress

+ ${session.phase || 'In Progress'} +
+ + +
+
+
📊
+
${totalFindings}
+
Total Findings
+
+
+
🔴
+
${criticalCount}
+
Critical
+
+
+
🟠
+
${highCount}
+
High
+
+
+
📋
+
${dimensions.length}
+
Dimensions
+
+
+ + +
+ ${dimensions.map((dim, idx) => ` +
+
D${idx + 1}
+
${escapeHtml(dim.name || 'Unknown')}
+
${dim.findings?.length || 0} findings
+
+ `).join('')} +
+
+ + +
+
+

🔍 Findings by Dimension

+
+ + + + +
+
+
+ ${renderReviewFindingsGrid(dimensions)} +
+
+ + +
+
+ Created: + ${formatDate(session.created_at)} +
+ ${session.archived_at ? ` +
+ Archived: + ${formatDate(session.archived_at)} +
+ ` : ''} +
+ Project: + ${escapeHtml(session.project || '-')} +
+
+
+ `; +} + +function renderReviewFindingsGrid(dimensions) { + if (!dimensions || dimensions.length === 0) { + return ` +
+
🔍
+
No review dimensions found
+
+ `; + } + + let html = ''; + dimensions.forEach(dim => { + const findings = dim.findings || []; + if (findings.length === 0) return; + + html += ` +
+
+ ${escapeHtml(dim.name)} + ${findings.length} findings +
+
+ ${findings.map(f => ` +
+
+ ${f.severity || 'medium'} + ${f.fix_status ? `${f.fix_status}` : ''} +
+
${escapeHtml(f.title || 'Finding')}
+
${escapeHtml((f.description || '').substring(0, 100))}${f.description?.length > 100 ? '...' : ''}
+ ${f.file ? `
📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}
` : ''} +
+ `).join('')} +
+
+ `; + }); + + return html || '
No findings
'; +} + +function initReviewSessionPage(session) { + // Initialize event handlers for review session page + // Filter handlers are inline onclick +} + +function filterReviewFindings(severity) { + // Update filter buttons + document.querySelectorAll('.findings-filters .filter-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.severity === severity); + }); + + // Filter finding cards + document.querySelectorAll('.finding-card').forEach(card => { + if (severity === 'all' || card.dataset.severity === severity) { + card.style.display = ''; + } else { + card.style.display = 'none'; + } + }); +} diff --git a/ccw/src/templates/dashboard-js/views/session-detail.js b/ccw/src/templates/dashboard-js/views/session-detail.js new file mode 100644 index 00000000..c0ad84e0 --- /dev/null +++ b/ccw/src/templates/dashboard-js/views/session-detail.js @@ -0,0 +1,761 @@ +// ============================================ +// SESSION DETAIL VIEW +// ============================================ +// Standard workflow session detail page rendering + +function showSessionDetailPage(sessionKey) { + const session = sessionDataStore[sessionKey]; + if (!session) return; + + currentView = 'sessionDetail'; + currentSessionDetailKey = sessionKey; + updateContentTitle(); + + const container = document.getElementById('mainContent'); + const sessionType = session.type || 'workflow'; + + // Render specialized pages for review and test-fix sessions + if (sessionType === 'review' || sessionType === 'review-cycle') { + container.innerHTML = renderReviewSessionDetailPage(session); + initReviewSessionPage(session); + return; + } + + if (sessionType === 'test-fix' || sessionType === 'fix') { + container.innerHTML = renderFixSessionDetailPage(session); + initFixSessionPage(session); + return; + } + + // Default workflow session detail page + const tasks = session.tasks || []; + const completed = tasks.filter(t => t.status === 'completed').length; + const inProgress = tasks.filter(t => t.status === 'in_progress').length; + const pending = tasks.filter(t => t.status === 'pending').length; + const isActive = session._isActive !== false; + + container.innerHTML = ` +
+ +
+ +
+

${escapeHtml(session.session_id)}

+
+ ${session.type || 'workflow'} + + ${isActive ? 'ACTIVE' : 'ARCHIVED'} + +
+
+
+ + +
+
+ Created: + ${formatDate(session.created_at)} +
+ ${session.archived_at ? ` +
+ Archived: + ${formatDate(session.archived_at)} +
+ ` : ''} +
+ Project: + ${escapeHtml(session.project || '-')} +
+
+ Tasks: + ${completed}/${tasks.length} completed +
+
+ + +
+ + + + + ${session.hasReview ? ` + + ` : ''} +
+ + +
+ ${renderTasksTab(session, tasks, completed, inProgress, pending)} +
+
+ `; +} + +function goBackToSessions() { + currentView = 'sessions'; + currentSessionDetailKey = null; + updateContentTitle(); + renderSessions(); +} + +function switchDetailTab(tabName) { + // Update active tab + document.querySelectorAll('.detail-tab').forEach(tab => { + tab.classList.toggle('active', tab.dataset.tab === tabName); + }); + + const session = sessionDataStore[currentSessionDetailKey]; + if (!session) return; + + const contentArea = document.getElementById('detailTabContent'); + const tasks = session.tasks || []; + const completed = tasks.filter(t => t.status === 'completed').length; + const inProgress = tasks.filter(t => t.status === 'in_progress').length; + const pending = tasks.filter(t => t.status === 'pending').length; + + switch (tabName) { + case 'tasks': + contentArea.innerHTML = renderTasksTab(session, tasks, completed, inProgress, pending); + break; + case 'context': + loadAndRenderContextTab(session, contentArea); + break; + case 'summary': + loadAndRenderSummaryTab(session, contentArea); + break; + case 'impl-plan': + loadAndRenderImplPlanTab(session, contentArea); + break; + case 'review': + loadAndRenderReviewTab(session, contentArea); + break; + } +} + +function renderTasksTab(session, tasks, completed, inProgress, pending) { + // Populate drawer tasks for click-to-open functionality + currentDrawerTasks = tasks; + + // Auto-load full task details in server mode + if (window.SERVER_MODE && session.path) { + // Schedule auto-load after DOM render + setTimeout(() => loadFullTaskDetails(), 50); + } + + // Show task list with loading state or basic list + const showLoading = window.SERVER_MODE && session.path; + + return ` +
+ +
+
+ ✓ ${completed} completed + âŸŗ ${inProgress} in progress + ○ ${pending} pending +
+
+
+ Quick Actions: + + + +
+
+ +
+ ${showLoading ? ` +
Loading task details...
+ ` : (tasks.length === 0 ? ` +
+
📋
+
No Tasks
+
This session has no tasks defined.
+
+ ` : tasks.map(task => renderDetailTaskItem(task)).join(''))} +
+
+ `; +} + +async function loadFullTaskDetails() { + const session = sessionDataStore[currentSessionDetailKey]; + if (!session || !window.SERVER_MODE || !session.path) return; + + const tasksContainer = document.getElementById('tasksListContent'); + tasksContainer.innerHTML = '
Loading full task details...
'; + + try { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=tasks`); + if (response.ok) { + const data = await response.json(); + if (data.tasks && data.tasks.length > 0) { + // Populate drawer tasks for click-to-open functionality + currentDrawerTasks = data.tasks; + tasksContainer.innerHTML = data.tasks.map(task => renderDetailTaskItem(task)).join(''); + } else { + tasksContainer.innerHTML = ` +
+
📋
+
No Task Files
+
No IMPL-*.json files found in .task/
+
+ `; + } + } + } catch (err) { + tasksContainer.innerHTML = `
Failed to load tasks: ${err.message}
`; + } +} + +function renderDetailTaskItem(task) { + const taskId = task.task_id || task.id || 'Unknown'; + const status = task.status || 'pending'; + + // Status options for dropdown + const statusOptions = ['pending', 'in_progress', 'completed']; + + return ` +
+
+ ${escapeHtml(taskId)} + + ${escapeHtml(task.title || task.meta?.title || 'Untitled')} + +
+ +
+
+
+ `; +} + +function formatStatusLabel(status) { + const labels = { + 'pending': '○ Pending', + 'in_progress': 'âŸŗ In Progress', + 'completed': '✓ Completed' + }; + return labels[status] || status; +} + +function getMetaPreview(task) { + const meta = task.meta || {}; + const parts = []; + if (meta.type) parts.push(meta.type); + if (meta.action) parts.push(meta.action); + if (meta.scope) parts.push(meta.scope); + return parts.join(' | ') || 'No meta'; +} + +function getTaskContextPreview(task) { + const items = []; + const ctx = task.context || {}; + if (ctx.requirements?.length) items.push(`${ctx.requirements.length} reqs`); + if (ctx.focus_paths?.length) items.push(`${ctx.focus_paths.length} paths`); + if (task.modification_points?.length) items.push(`${task.modification_points.length} mods`); + if (task.description) items.push('desc'); + return items.join(' | ') || 'No context'; +} + +function getFlowPreview(task) { + const steps = task.flow_control?.implementation_approach?.length || task.implementation?.length || 0; + return steps > 0 ? `${steps} steps` : 'No steps'; +} + +function renderTaskContext(task) { + const sections = []; + const ctx = task.context || {}; + + // Description + if (task.description) { + sections.push(` +
+ +

${escapeHtml(task.description)}

+
+ `); + } + + // Requirements + if (ctx.requirements?.length) { + sections.push(` +
+ +
    ${ctx.requirements.map(r => `
  • ${escapeHtml(r)}
  • `).join('')}
+
+ `); + } + + // Focus paths + if (ctx.focus_paths?.length) { + sections.push(` +
+ +
${ctx.focus_paths.map(p => `${escapeHtml(p)}`).join('')}
+
+ `); + } + + // Modification points + if (task.modification_points?.length) { + sections.push(` +
+ +
+ ${task.modification_points.map(m => ` +
+ ${escapeHtml(m.file || m)} + ${m.target ? `→ ${escapeHtml(m.target)}` : ''} + ${m.change ? `

${escapeHtml(m.change)}

` : ''} +
+ `).join('')} +
+
+ `); + } + + // Acceptance criteria + const acceptance = ctx.acceptance || task.acceptance || []; + if (acceptance.length) { + sections.push(` +
+ +
    ${acceptance.map(a => `
  • ${escapeHtml(a)}
  • `).join('')}
+
+ `); + } + + return sections.length > 0 + ? `
${sections.join('')}
` + : '
No context data
'; +} + +function renderFlowControl(task) { + const sections = []; + const fc = task.flow_control || {}; + + // Implementation approach + const steps = fc.implementation_approach || task.implementation || []; + if (steps.length) { + sections.push(` +
+ +
    + ${steps.map(s => `
  1. ${escapeHtml(typeof s === 'string' ? s : s.step || s.action || JSON.stringify(s))}
  2. `).join('')} +
+
+ `); + } + + // Pre-analysis + const preAnalysis = fc.pre_analysis || task.pre_analysis || []; + if (preAnalysis.length) { + sections.push(` +
+ +
    ${preAnalysis.map(p => `
  • ${escapeHtml(p)}
  • `).join('')}
+
+ `); + } + + // Target files + const targetFiles = fc.target_files || task.target_files || []; + if (targetFiles.length) { + sections.push(` +
+ +
${targetFiles.map(f => `${escapeHtml(f)}`).join('')}
+
+ `); + } + + return sections.length > 0 + ? `
${sections.join('')}
` + : '
No flow control data
'; +} + +async function loadAndRenderContextTab(session, contentArea) { + contentArea.innerHTML = '
Loading context data...
'; + + try { + // Try to load context-package.json from server + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=context`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderContextContent(data.context); + return; + } + } + // Fallback: show placeholder + contentArea.innerHTML = ` +
+
đŸ“Ļ
+
Context Data
+
Context data will be loaded from context-package.json
+
+ `; + } catch (err) { + contentArea.innerHTML = `
Failed to load context: ${err.message}
`; + } +} + +async function loadAndRenderSummaryTab(session, contentArea) { + contentArea.innerHTML = '
Loading summaries...
'; + + try { + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=summary`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderSummaryContent(data.summaries); + return; + } + } + contentArea.innerHTML = ` +
+
📝
+
Summaries
+
Session summaries will be loaded from .summaries/
+
+ `; + } catch (err) { + contentArea.innerHTML = `
Failed to load summaries: ${err.message}
`; + } +} + +async function loadAndRenderImplPlanTab(session, contentArea) { + contentArea.innerHTML = '
Loading IMPL plan...
'; + + try { + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=impl-plan`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderImplPlanContent(data.implPlan); + return; + } + } + contentArea.innerHTML = ` +
+
📐
+
IMPL Plan
+
IMPL plan will be loaded from IMPL_PLAN.md
+
+ `; + } catch (err) { + contentArea.innerHTML = `
Failed to load IMPL plan: ${err.message}
`; + } +} + +async function loadAndRenderReviewTab(session, contentArea) { + contentArea.innerHTML = '
Loading review data...
'; + + try { + if (window.SERVER_MODE && session.path) { + const response = await fetch(`/api/session-detail?path=${encodeURIComponent(session.path)}&type=review`); + if (response.ok) { + const data = await response.json(); + contentArea.innerHTML = renderReviewContent(data.review); + return; + } + } + contentArea.innerHTML = ` +
+
🔍
+
Review Data
+
Review data will be loaded from review files
+
+ `; + } catch (err) { + contentArea.innerHTML = `
Failed to load review: ${err.message}
`; + } +} + +function showRawSessionJson(sessionKey) { + const session = sessionDataStore[sessionKey]; + if (!session) return; + + // Close current modal + const currentModal = document.querySelector('.session-modal-overlay'); + if (currentModal) currentModal.remove(); + + // Show JSON modal + const overlay = document.createElement('div'); + overlay.className = 'json-modal-overlay active'; + overlay.innerHTML = ` +
+
+
+ ${escapeHtml(session.session_id)} + Session JSON +
+ +
+
+
${escapeHtml(JSON.stringify(session, null, 2))}
+
+ +
+ `; + document.body.appendChild(overlay); + + // Close on overlay click + overlay.addEventListener('click', (e) => { + if (e.target === overlay) closeJsonModal(); + }); +} + +// ========================================== +// TASK STATUS MANAGEMENT +// ========================================== + +async function updateSingleTaskStatus(taskId, newStatus) { + const session = sessionDataStore[currentSessionDetailKey]; + if (!session || !window.SERVER_MODE || !session.path) { + showToast('Status update requires server mode', 'error'); + return; + } + + try { + const response = await fetch('/api/update-task-status', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionPath: session.path, + taskId: taskId, + newStatus: newStatus + }) + }); + + const result = await response.json(); + if (result.success) { + // Update UI + updateTaskItemUI(taskId, newStatus); + updateTaskStatsBar(); + showToast(`Task ${taskId} → ${formatStatusLabel(newStatus)}`, 'success'); + } else { + showToast(result.error || 'Failed to update status', 'error'); + // Revert select + revertTaskSelect(taskId); + } + } catch (error) { + showToast('Error updating status: ' + error.message, 'error'); + revertTaskSelect(taskId); + } +} + +async function bulkSetAllStatus(newStatus) { + const session = sessionDataStore[currentSessionDetailKey]; + if (!session || !window.SERVER_MODE || !session.path) { + showToast('Bulk update requires server mode', 'error'); + return; + } + + const taskIds = currentDrawerTasks.map(t => t.task_id || t.id); + if (taskIds.length === 0) return; + + if (!confirm(`Set all ${taskIds.length} tasks to "${formatStatusLabel(newStatus)}"?`)) { + return; + } + + try { + const response = await fetch('/api/bulk-update-task-status', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionPath: session.path, + taskIds: taskIds, + newStatus: newStatus + }) + }); + + const result = await response.json(); + if (result.success) { + // Update all task UIs + taskIds.forEach(id => updateTaskItemUI(id, newStatus)); + updateTaskStatsBar(); + showToast(`All ${taskIds.length} tasks → ${formatStatusLabel(newStatus)}`, 'success'); + } else { + showToast(result.error || 'Failed to bulk update', 'error'); + } + } catch (error) { + showToast('Error in bulk update: ' + error.message, 'error'); + } +} + +async function bulkSetPendingToInProgress() { + const session = sessionDataStore[currentSessionDetailKey]; + if (!session || !window.SERVER_MODE || !session.path) { + showToast('Bulk update requires server mode', 'error'); + return; + } + + const pendingTaskIds = currentDrawerTasks + .filter(t => (t.status || 'pending') === 'pending') + .map(t => t.task_id || t.id); + + if (pendingTaskIds.length === 0) { + showToast('No pending tasks to start', 'info'); + return; + } + + try { + const response = await fetch('/api/bulk-update-task-status', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionPath: session.path, + taskIds: pendingTaskIds, + newStatus: 'in_progress' + }) + }); + + const result = await response.json(); + if (result.success) { + pendingTaskIds.forEach(id => updateTaskItemUI(id, 'in_progress')); + updateTaskStatsBar(); + showToast(`${pendingTaskIds.length} tasks: Pending → In Progress`, 'success'); + } else { + showToast(result.error || 'Failed to update', 'error'); + } + } catch (error) { + showToast('Error: ' + error.message, 'error'); + } +} + +async function bulkSetInProgressToCompleted() { + const session = sessionDataStore[currentSessionDetailKey]; + if (!session || !window.SERVER_MODE || !session.path) { + showToast('Bulk update requires server mode', 'error'); + return; + } + + const inProgressTaskIds = currentDrawerTasks + .filter(t => t.status === 'in_progress') + .map(t => t.task_id || t.id); + + if (inProgressTaskIds.length === 0) { + showToast('No in-progress tasks to complete', 'info'); + return; + } + + try { + const response = await fetch('/api/bulk-update-task-status', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sessionPath: session.path, + taskIds: inProgressTaskIds, + newStatus: 'completed' + }) + }); + + const result = await response.json(); + if (result.success) { + inProgressTaskIds.forEach(id => updateTaskItemUI(id, 'completed')); + updateTaskStatsBar(); + showToast(`${inProgressTaskIds.length} tasks: In Progress → Completed`, 'success'); + } else { + showToast(result.error || 'Failed to update', 'error'); + } + } catch (error) { + showToast('Error: ' + error.message, 'error'); + } +} + +function updateTaskItemUI(taskId, newStatus) { + const taskItem = document.querySelector(`.detail-task-item[data-task-id="${taskId}"]`); + if (!taskItem) return; + + // Update classes + taskItem.className = `detail-task-item ${newStatus} status-${newStatus}`; + + // Update select + const select = taskItem.querySelector('.task-status-select'); + if (select) { + select.value = newStatus; + select.className = `task-status-select ${newStatus}`; + select.dataset.current = newStatus; + } + + // Update drawer tasks data + const task = currentDrawerTasks.find(t => (t.task_id || t.id) === taskId); + if (task) { + task.status = newStatus; + } +} + +function updateTaskStatsBar() { + const completed = currentDrawerTasks.filter(t => t.status === 'completed').length; + const inProgress = currentDrawerTasks.filter(t => t.status === 'in_progress').length; + const pending = currentDrawerTasks.filter(t => (t.status || 'pending') === 'pending').length; + + const statsBar = document.querySelector('.task-stats-bar'); + if (statsBar) { + statsBar.innerHTML = ` + ✓ ${completed} completed + âŸŗ ${inProgress} in progress + ○ ${pending} pending + `; + } +} + +function revertTaskSelect(taskId) { + const taskItem = document.querySelector(`.detail-task-item[data-task-id="${taskId}"]`); + if (!taskItem) return; + + const select = taskItem.querySelector('.task-status-select'); + if (select) { + select.value = select.dataset.current; + } +} + +function showToast(message, type = 'info') { + // Remove existing toast + const existing = document.querySelector('.status-toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.className = `status-toast ${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + // Auto-remove after 3 seconds + setTimeout(() => { + toast.classList.add('fade-out'); + setTimeout(() => toast.remove(), 300); + }, 3000); +} diff --git a/ccw/src/templates/dashboard.css b/ccw/src/templates/dashboard.css index 2dcb5b24..8bf72ed4 100644 --- a/ccw/src/templates/dashboard.css +++ b/ccw/src/templates/dashboard.css @@ -8,6 +8,16 @@ CSS variables are defined inline in dashboard.html - - -
- -
-
- - -
- - -
- -
- -
- -
- -
-
- -
-
-
-
- - -
- -
-
- - - - - -
- - - - -
- -
-
-
📊
-
0
-
Total Sessions
-
-
-
đŸŸĸ
-
0
-
Active Sessions
-
-
-
📋
-
0
-
Total Tasks
-
-
-
✅
-
0
-
Completed Tasks
-
-
- - -
-

All Sessions

- -
- - -
- -
-
-
- - -
-
Generated: -
-
CCW Dashboard v1.0
-
- - -
-
-

Task Details

- -
-
- -
-
-
-
- - - - - - - diff --git a/ccw/src/templates/dashboard.js b/ccw/src/templates/dashboard.js.backup similarity index 100% rename from ccw/src/templates/dashboard.js rename to ccw/src/templates/dashboard.js.backup diff --git a/ccw/src/templates/review-cycle-dashboard.html.backup b/ccw/src/templates/review-cycle-dashboard.html.backup deleted file mode 100644 index b308a748..00000000 --- a/ccw/src/templates/review-cycle-dashboard.html.backup +++ /dev/null @@ -1,2816 +0,0 @@ - - - - - - Code Review Dashboard - {{SESSION_ID}} - - - -
-
-

🔍 Code Review Dashboard

-
- 📋 Session: Loading... - 🆔 Review ID: Loading... - 🕒 Last Updated: Loading... -
- -
- - -
- 0 findings selected - - -
- - - -
-
- - -
-
-

Fix Progress

- PLANNING -
- - -
-
- - - - - - - - - -
- - -
-
-

Review Progress

- LOADING -
-
-
-
-
- Initializing... -
-
- - -
-
-
🔴
-
0
-
Critical
-
-
-
🟠
-
0
-
High
-
-
-
🟡
-
0
-
Medium
-
-
-
đŸŸĸ
-
0
-
Low
-
-
- - -
-
Findings by Dimension
- - - - - - - - - - - - - - - -
DimensionCriticalHighMediumLowTotalStatus
-
- - -
- - - - - - - - -
- - -
-
-
đŸŽ¯ Advanced Filters & Sort
- -
-
- -
- Severity: -
- - - - -
-
- - -
- Sort: - - -
- - -
- Select: - - - -
-
-
- - -
-
-

Findings (0)

-
-
-
-
âŗ
-

Loading findings...

-
-
-
-
- - -
-
-
-
Finding Details
- -
-
- -
-
- - -
-
-
-
📜 Fix History
- -
-
- -
-
- - - - - - diff --git a/ccw/src/templates/workflow-dashboard.html.backup b/ccw/src/templates/workflow-dashboard.html.backup deleted file mode 100644 index 96744194..00000000 --- a/ccw/src/templates/workflow-dashboard.html.backup +++ /dev/null @@ -1,664 +0,0 @@ - - - - - - Workflow Dashboard - Task Board - - - -
-
-

🚀 Workflow Dashboard

-

Task Board - Active and Archived Sessions

- -
- - -
- - - -
-
-
- -
-
-
0
-
Total Sessions
-
-
-
0
-
Active Sessions
-
-
-
0
-
Total Tasks
-
-
-
0
-
Completed Tasks
-
-
- -
-
-

📋 Active Sessions

-
-
-
- -
-
-

đŸ“Ļ Archived Sessions

-
-
-
-
- - - - - - \ No newline at end of file diff --git a/ccw/test-dashboard.html b/ccw/test-dashboard.html new file mode 100644 index 00000000..915838f3 --- /dev/null +++ b/ccw/test-dashboard.html @@ -0,0 +1,9086 @@ + + + + + + CCW Dashboard + + + + + + + + + + +
+ +
+
+ +
+ ⚡ + +
+
+ + +
+ +
+ +
+ + +
+
+ + + +
+
+ + + + + +
+ + + + +
+ +
+
+
📊
+
0
+
Total Sessions
+
+
+
đŸŸĸ
+
0
+
Active Sessions
+
+
+
📋
+
0
+
Total Tasks
+
+
+
✅
+
0
+
Completed Tasks
+
+
+ + +
+

All Sessions

+
+ 🔍 + +
+
+ + +
+ +
+
+
+ + +
+
Generated: -
+
CCW Dashboard v1.0
+
+ + +
+
+

Task Details

+ +
+
+ +
+
+ +
+ + + + + + + + + + + +