Remove backup HTML template for workflow dashboard

This commit is contained in:
catlog22
2025-12-07 12:59:59 +08:00
parent a9a2004d4a
commit 724545ebd6
33 changed files with 17288 additions and 7682 deletions

View File

@@ -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);

View File

@@ -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'
];

View File

@@ -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) {
</div>
</div>
`;
}
}

View File

@@ -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<Object>}
*/
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);

385
ccw/src/core/server.js.bak Normal file
View File

@@ -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<http.Server>}
*/
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<Object>}
*/
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<Object>}
*/
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;
}

View File

@@ -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<http.Server>}
*/
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<Object>}
*/
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<Object>}
*/
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;
}

View File

@@ -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<Object|null>} 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 = '<div class="loading">Loading...</div>';
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 = '<div class="error">Failed to load project data</div>';
}
}
/**
* 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 = `
<div class="path-modal">
<div class="path-modal-header">
<span class="path-modal-icon">${icons.terminal}</span>
<h3>Run Command</h3>
</div>
<div class="path-modal-body">
<p>To view the dashboard for this project, run:</p>
<div class="path-modal-command">
<code>ccw view -p "${path}"</code>
<button class="copy-btn" id="copyCommandBtn">${icons.copy} <span>Copy</span></button>
</div>
<p class="path-modal-note" style="margin-top: 12px;">
Or use <code>ccw serve</code> for live path switching.
</p>
</div>
<div class="path-modal-footer">
<button class="path-modal-close" onclick="this.closest('.path-modal-overlay').remove()">OK</button>
</div>
</div>
`;
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 + ' <span>Copied!</span>';
setTimeout(() => { this.innerHTML = icons.copy + ' <span>Copy</span>'; }, 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();
}

View File

@@ -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 = '<div class="flowchart-fallback">D3.js not loaded</div>';
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 = '<div class="empty-section">No flowchart data available</div>';
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));
}
});
}

View File

@@ -0,0 +1,260 @@
// ==========================================
// MODAL DIALOGS
// ==========================================
// SVG Icons
const icons = {
folder: '<svg viewBox="0 0 24 24"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>',
check: '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg>',
copy: '<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
terminal: '<svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" y1="19" x2="20" y2="19"></line></svg>'
};
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 = `
<div class="path-modal">
<div class="path-modal-header">
<span class="path-modal-icon">${icons.folder}</span>
<h3>Folder Selected</h3>
</div>
<div class="path-modal-body">
<div class="selected-folder">
<strong>${dirName}</strong>
</div>
<p class="path-modal-note">
Confirm or edit the full path:
</p>
<div class="path-input-group" style="margin-top: 12px;">
<label>Full path:</label>
<input type="text" id="fullPathInput" value="${suggestedPath}" />
<button class="path-go-btn" id="pathGoBtn">Open</button>
</div>
</div>
<div class="path-modal-footer">
<button class="path-modal-close" id="pathCancelBtn">Cancel</button>
</div>
</div>
`;
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 = `
<div class="path-modal">
<div class="path-modal-header">
<span class="path-modal-icon">${icons.folder}</span>
<h3>Open Project</h3>
</div>
<div class="path-modal-body">
<div class="path-input-group" style="margin-top: 0;">
<label>Project path:</label>
<input type="text" id="fullPathInput" placeholder="D:/projects/my-project" />
<button class="path-go-btn" id="pathGoBtn">Open</button>
</div>
</div>
<div class="path-modal-footer">
<button class="path-modal-close" id="pathCancelBtn">Cancel</button>
</div>
</div>
`;
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 + ' <span>Copied!</span>';
setTimeout(() => { btn.innerHTML = icons.copy + ' <span>Copy</span>'; }, 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 = `
<div class="json-modal">
<div class="json-modal-header">
<div class="json-modal-title">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span>Task JSON</span>
</div>
<button class="json-modal-close" onclick="closeJsonModal(this)">&times;</button>
</div>
<div class="json-modal-body">
<pre class="json-modal-content">${escapeHtml(jsonContent)}</pre>
</div>
<div class="json-modal-footer">
<button class="btn-copy-json" onclick="copyJsonToClipboard(this)">Copy JSON</button>
</div>
</div>
`;
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 = '<pre class="whitespace-pre-wrap language-json">' + escapeHtml(formatted) + '</pre>';
} catch (e) {
// If not valid JSON, show as-is
previewEl.innerHTML = '<pre class="whitespace-pre-wrap">' + escapeHtml(normalizedContent) + '</pre>';
}
} else {
// Fallback: simple text with line breaks
previewEl.innerHTML = '<pre class="whitespace-pre-wrap">' + escapeHtml(normalizedContent) + '</pre>';
}
// 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');
}
}

View File

@@ -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);
}

View File

@@ -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');
});
}

View File

@@ -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 `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">No Context Data</div>
<div class="empty-text">No context-package.json found for this session.</div>
</div>
`;
}
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 `
<div class="context-tab-content">
<!-- Header Card -->
<div class="ctx-header-card">
<div class="ctx-header-content">
<h3 class="ctx-main-title">📦 Context Package</h3>
<button class="btn-view-modal" onclick="openMarkdownModal('context-package.json', window._currentContextJson, 'json')">
👁️ View JSON
</button>
</div>
</div>
<!-- Metadata Card -->
${metadata.task_description || metadata.session_id ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">📋</span>
<h4 class="ctx-card-title">Task Metadata</h4>
</div>
<div class="ctx-card-body">
${metadata.task_description ? `
<p class="ctx-description">${escapeHtml(metadata.task_description)}</p>
` : ''}
<div class="ctx-meta-row">
${metadata.session_id ? `
<div class="ctx-meta-chip">
<span class="ctx-meta-chip-label">SESSION</span>
<code class="ctx-meta-chip-value">${escapeHtml(metadata.session_id)}</code>
</div>
` : ''}
${metadata.complexity ? `
<div class="ctx-meta-chip">
<span class="ctx-meta-chip-label">COMPLEXITY</span>
<span class="ctx-complexity-badge ctx-complexity-${metadata.complexity}">${escapeHtml(metadata.complexity.toUpperCase())}</span>
</div>
` : ''}
${metadata.timestamp ? `
<div class="ctx-meta-chip">
<span class="ctx-meta-chip-label">CREATED</span>
<span class="ctx-meta-chip-value">${formatDate(metadata.timestamp)}</span>
</div>
` : ''}
</div>
${metadata.keywords && metadata.keywords.length > 0 ? `
<div class="ctx-keywords-row">
${metadata.keywords.map(kw => `<span class="ctx-keyword-tag">${escapeHtml(kw)}</span>`).join('')}
</div>
` : ''}
</div>
</div>
` : ''}
<!-- Architecture Patterns Card -->
${architecturePatterns.length > 0 ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">🏛️</span>
<h4 class="ctx-card-title">Architecture Patterns</h4>
<span class="ctx-count-badge">${architecturePatterns.length}</span>
</div>
<div class="ctx-card-body">
<div class="ctx-pattern-tags">
${architecturePatterns.map(p => `<span class="ctx-pattern-tag">${escapeHtml(p)}</span>`).join('')}
</div>
</div>
</div>
` : ''}
<!-- Tech Stack Card -->
${Object.keys(techStack).length > 0 ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">💻</span>
<h4 class="ctx-card-title">Technology Stack</h4>
</div>
<div class="ctx-card-body">
${renderTechStackCards(techStack)}
</div>
</div>
` : ''}
<!-- Coding Conventions Card -->
${Object.keys(codingConventions).length > 0 ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">📝</span>
<h4 class="ctx-card-title">Coding Conventions</h4>
</div>
<div class="ctx-card-body">
${renderCodingConventionsCards(codingConventions)}
</div>
</div>
` : ''}
<!-- Assets Section -->
${Object.keys(assets).length > 0 ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">📚</span>
<h4 class="ctx-card-title">Assets & Resources</h4>
</div>
<div class="ctx-card-body">
${renderAssetsCards(assets)}
</div>
</div>
` : ''}
<!-- Dependencies Section -->
${(dependencies.internal && dependencies.internal.length > 0) || (dependencies.external && dependencies.external.length > 0) ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">🔗</span>
<h4 class="ctx-card-title">Dependencies</h4>
</div>
<div class="ctx-card-body">
${renderDependenciesCards(dependencies)}
</div>
</div>
` : ''}
<!-- Test Context Section -->
${Object.keys(testContext).length > 0 ? `
<div class="ctx-card">
<div class="ctx-card-header">
<span class="ctx-card-icon">🧪</span>
<h4 class="ctx-card-title">Test Context</h4>
</div>
<div class="ctx-card-body">
${renderTestContextCards(testContext)}
</div>
</div>
` : ''}
<!-- Conflict Detection Section -->
${Object.keys(conflictDetection).length > 0 ? `
<div class="ctx-card ctx-card-warning">
<div class="ctx-card-header">
<span class="ctx-card-icon">⚠️</span>
<h4 class="ctx-card-title">Risk Analysis</h4>
${conflictDetection.risk_level ? `
<span class="ctx-risk-badge ctx-risk-${conflictDetection.risk_level}">${escapeHtml(conflictDetection.risk_level.toUpperCase())}</span>
` : ''}
</div>
<div class="ctx-card-body">
${renderConflictCards(conflictDetection)}
</div>
</div>
` : ''}
</div>
`;
}
// New card-based renderers
function renderTechStackCards(techStack) {
const sections = [];
if (techStack.languages) {
const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages];
sections.push(`
<div class="ctx-stack-section">
<span class="ctx-stack-label">Languages</span>
<div class="ctx-stack-tags">
${langs.map(l => `<span class="ctx-lang-tag">${escapeHtml(String(l))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.frameworks) {
const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks];
sections.push(`
<div class="ctx-stack-section">
<span class="ctx-stack-label">Frameworks</span>
<div class="ctx-stack-tags">
${frameworks.map(f => `<span class="ctx-framework-tag">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.frontend_frameworks) {
const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks];
sections.push(`
<div class="ctx-stack-section">
<span class="ctx-stack-label">Frontend</span>
<div class="ctx-stack-tags">
${ff.map(f => `<span class="ctx-frontend-tag">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.backend_frameworks) {
const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks];
sections.push(`
<div class="ctx-stack-section">
<span class="ctx-stack-label">Backend</span>
<div class="ctx-stack-tags">
${bf.map(f => `<span class="ctx-backend-tag">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.libraries && typeof techStack.libraries === 'object') {
Object.entries(techStack.libraries).forEach(([category, libList]) => {
if (Array.isArray(libList) && libList.length > 0) {
sections.push(`
<div class="ctx-stack-section">
<span class="ctx-stack-label">${escapeHtml(category)}</span>
<div class="ctx-stack-tags">
${libList.map(lib => `<span class="ctx-lib-tag">${escapeHtml(String(lib))}</span>`).join('')}
</div>
</div>
`);
}
});
}
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]) => `
<div class="ctx-conv-item">
<span class="ctx-conv-key">${escapeHtml(key)}</span>
<span class="ctx-conv-value">${formatLeafValue(subVal)}</span>
</div>
`).join('');
}
return `
<div class="ctx-conv-item">
<span class="ctx-conv-key">${escapeHtml(key)}</span>
<span class="ctx-conv-value">${formatLeafValue(val)}</span>
</div>
`;
}).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(`
<div class="ctx-conv-section">
<span class="ctx-conv-label">${escapeHtml(label)}</span>
<div class="ctx-conv-items">
${renderItems(val)}
</div>
</div>
`);
}
});
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(`
<div class="ctx-assets-category">
<div class="ctx-assets-cat-header">
<span class="ctx-assets-cat-icon">📄</span>
<span class="ctx-assets-cat-title">Documentation</span>
<span class="ctx-assets-cat-count">${assets.documentation.length}</span>
</div>
<div class="ctx-assets-card-grid">
${assets.documentation.map(doc => `
<div class="ctx-asset-card ctx-asset-doc">
<span class="ctx-asset-card-path">${escapeHtml(doc.path)}</span>
<span class="ctx-asset-card-badge ctx-relevance-badge">${(doc.relevance_score * 100).toFixed(0)}%</span>
</div>
`).join('')}
</div>
</div>
`);
}
// Source Code section - card grid layout
if (assets.source_code && assets.source_code.length > 0) {
sections.push(`
<div class="ctx-assets-category">
<div class="ctx-assets-cat-header">
<span class="ctx-assets-cat-icon">💻</span>
<span class="ctx-assets-cat-title">Source Code</span>
<span class="ctx-assets-cat-count">${assets.source_code.length}</span>
</div>
<div class="ctx-assets-card-grid">
${assets.source_code.map(src => `
<div class="ctx-asset-card ctx-asset-src">
<span class="ctx-asset-card-path">${escapeHtml(src.path)}</span>
${src.role ? `<span class="ctx-asset-card-badge ctx-role-badge">${escapeHtml(src.role.replace(/-/g, ' '))}</span>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Tests section - card grid layout
if (assets.tests && assets.tests.length > 0) {
sections.push(`
<div class="ctx-assets-category">
<div class="ctx-assets-cat-header">
<span class="ctx-assets-cat-icon">🧪</span>
<span class="ctx-assets-cat-title">Tests</span>
<span class="ctx-assets-cat-count">${assets.tests.length}</span>
</div>
<div class="ctx-assets-card-grid">
${assets.tests.map(test => `
<div class="ctx-asset-card ctx-asset-test">
<span class="ctx-asset-card-path">${escapeHtml(test.path)}</span>
${test.test_count ? `<span class="ctx-asset-card-badge ctx-test-badge">${test.test_count} tests</span>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderDependenciesCards(dependencies) {
const sections = [];
if (dependencies.internal && dependencies.internal.length > 0) {
sections.push(`
<div class="ctx-deps-section">
<div class="ctx-deps-header">
<span class="ctx-deps-label">Internal Dependencies</span>
<span class="ctx-deps-count">${dependencies.internal.length}</span>
</div>
<div class="ctx-deps-table">
<div class="ctx-deps-table-header">
<span class="ctx-deps-col-from">From</span>
<span class="ctx-deps-col-type">Type</span>
<span class="ctx-deps-col-to">To</span>
</div>
<div class="ctx-deps-table-body">
${dependencies.internal.map(dep => `
<div class="ctx-deps-row">
<span class="ctx-deps-col-from">${escapeHtml(dep.from)}</span>
<span class="ctx-deps-col-type">
<span class="ctx-deps-type-badge ctx-deps-type-${dep.type}">${escapeHtml(dep.type)}</span>
</span>
<span class="ctx-deps-col-to">${escapeHtml(dep.to)}</span>
</div>
`).join('')}
</div>
</div>
</div>
`);
}
if (dependencies.external && dependencies.external.length > 0) {
sections.push(`
<div class="ctx-deps-section">
<div class="ctx-deps-header">
<span class="ctx-deps-label">External Packages</span>
<span class="ctx-deps-count">${dependencies.external.length}</span>
</div>
<div class="ctx-deps-packages">
${dependencies.external.map(dep => `
<span class="ctx-pkg-tag">${escapeHtml(dep.package)}${dep.version ? `@${escapeHtml(dep.version)}` : ''}</span>
`).join('')}
</div>
</div>
`);
}
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(`
<div class="ctx-test-stats">
<div class="ctx-stat-box">
<span class="ctx-stat-value">${totalTests}</span>
<span class="ctx-stat-label">Total Tests</span>
</div>
${testContext.coverage_config?.target ? `
<div class="ctx-stat-box">
<span class="ctx-stat-value">${escapeHtml(testContext.coverage_config.target)}</span>
<span class="ctx-stat-label">Coverage Target</span>
</div>
` : ''}
</div>
`);
if (testContext.frameworks) {
const fw = testContext.frameworks;
sections.push(`
<div class="ctx-test-frameworks">
${fw.backend ? `
<div class="ctx-fw-card ctx-fw-installed">
<span class="ctx-fw-type">Backend</span>
<span class="ctx-fw-name">${escapeHtml(fw.backend.name || 'N/A')}</span>
</div>
` : ''}
${fw.frontend ? `
<div class="ctx-fw-card ${fw.frontend.name?.includes('NONE') ? 'ctx-fw-missing' : 'ctx-fw-installed'}">
<span class="ctx-fw-type">Frontend</span>
<span class="ctx-fw-name">${escapeHtml(fw.frontend.name || 'N/A')}</span>
</div>
` : ''}
</div>
`);
}
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(`
<div class="ctx-mitigation">
<span class="ctx-mitigation-label">Mitigation Strategy</span>
<ol class="ctx-mitigation-list">
${items.map(item => `<li>${escapeHtml(item.trim())}</li>`).join('')}
</ol>
</div>
`);
} else {
sections.push(`
<div class="ctx-mitigation">
<span class="ctx-mitigation-label">Mitigation Strategy</span>
<p class="ctx-mitigation-text">${escapeHtml(strategy)}</p>
</div>
`);
}
}
if (conflictDetection.risk_factors) {
const factors = conflictDetection.risk_factors;
if (factors.test_gaps?.length > 0) {
sections.push(`
<div class="ctx-risk-section">
<span class="ctx-risk-label">Test Gaps</span>
<ul class="ctx-risk-list">
${factors.test_gaps.map(gap => `<li>${escapeHtml(gap)}</li>`).join('')}
</ul>
</div>
`);
}
}
if (conflictDetection.affected_modules?.length > 0) {
sections.push(`
<div class="ctx-affected">
<span class="ctx-affected-label">Affected Modules</span>
<div class="ctx-affected-tags">
${conflictDetection.affected_modules.map(mod => `<span class="ctx-affected-tag">${escapeHtml(mod)}</span>`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderTechStackSection(techStack) {
const sections = [];
if (techStack.languages) {
const langs = Array.isArray(techStack.languages) ? techStack.languages : [techStack.languages];
sections.push(`
<div class="context-field">
<span class="context-label">Languages:</span>
<div class="flex flex-wrap gap-1 mt-1">
${langs.map(l => `<span class="badge badge-primary text-xs">${escapeHtml(String(l))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.frameworks) {
const frameworks = Array.isArray(techStack.frameworks) ? techStack.frameworks : [techStack.frameworks];
sections.push(`
<div class="context-field">
<span class="context-label">Frameworks:</span>
<div class="flex flex-wrap gap-1 mt-1">
${frameworks.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.frontend_frameworks) {
const ff = Array.isArray(techStack.frontend_frameworks) ? techStack.frontend_frameworks : [techStack.frontend_frameworks];
sections.push(`
<div class="context-field">
<span class="context-label">Frontend:</span>
<div class="flex flex-wrap gap-1 mt-1">
${ff.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
if (techStack.backend_frameworks) {
const bf = Array.isArray(techStack.backend_frameworks) ? techStack.backend_frameworks : [techStack.backend_frameworks];
sections.push(`
<div class="context-field">
<span class="context-label">Backend:</span>
<div class="flex flex-wrap gap-1 mt-1">
${bf.map(f => `<span class="badge badge-secondary text-xs">${escapeHtml(String(f))}</span>`).join('')}
</div>
</div>
`);
}
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(`
<div class="context-field">
<span class="context-label">${escapeHtml(category)}:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${libList.map(lib => `<li>${escapeHtml(String(lib))}</li>`).join('')}
</ul>
</div>
`);
}
});
}
}
return sections.join('');
}
function renderCodingConventions(conventions) {
const sections = [];
if (conventions.naming) {
sections.push(`
<div class="context-field">
<span class="context-label">Naming:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${Object.entries(conventions.naming).map(([key, val]) =>
`<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`
).join('')}
</ul>
</div>
`);
}
if (conventions.error_handling) {
sections.push(`
<div class="context-field">
<span class="context-label">Error Handling:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${Object.entries(conventions.error_handling).map(([key, val]) =>
`<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`
).join('')}
</ul>
</div>
`);
}
if (conventions.testing) {
sections.push(`
<div class="context-field">
<span class="context-label">Testing:</span>
<ul class="list-disc list-inside ml-4 mt-1 text-sm text-muted-foreground">
${Object.entries(conventions.testing).map(([key, val]) => {
if (Array.isArray(val)) {
return `<li><strong>${escapeHtml(key)}:</strong> ${val.map(v => escapeHtml(String(v))).join(', ')}</li>`;
}
return `<li><strong>${escapeHtml(key)}:</strong> ${escapeHtml(String(val))}</li>`;
}).join('')}
</ul>
</div>
`);
}
return sections.join('');
}
function renderAssetsSection(assets) {
const sections = [];
// Documentation
if (assets.documentation && assets.documentation.length > 0) {
sections.push(`
<div class="asset-category">
<h5 class="asset-category-title">📄 Documentation</h5>
<div class="asset-grid">
${assets.documentation.map(doc => `
<div class="asset-card">
<div class="asset-card-header">
<span class="asset-path">${escapeHtml(doc.path)}</span>
<span class="relevance-score" style="background: ${getRelevanceColor(doc.relevance_score)}">${(doc.relevance_score * 100).toFixed(0)}%</span>
</div>
<div class="asset-card-body">
<div class="asset-scope">${escapeHtml(doc.scope || '')}</div>
${doc.contains && doc.contains.length > 0 ? `
<div class="asset-tags">
${doc.contains.map(tag => `<span class="asset-tag">${escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`);
}
// Source Code
if (assets.source_code && assets.source_code.length > 0) {
sections.push(`
<div class="asset-category">
<h5 class="asset-category-title">💻 Source Code</h5>
<div class="asset-grid">
${assets.source_code.map(src => `
<div class="asset-card">
<div class="asset-card-header">
<span class="asset-path">${escapeHtml(src.path)}</span>
<span class="relevance-score" style="background: ${getRelevanceColor(src.relevance_score)}">${(src.relevance_score * 100).toFixed(0)}%</span>
</div>
<div class="asset-card-body">
<div class="asset-role-badge badge-${getRoleBadgeClass(src.role)}">${escapeHtml(src.role || '')}</div>
${src.exports && src.exports.length > 0 ? `
<div class="asset-meta"><strong>Exports:</strong> ${src.exports.map(e => `<code class="inline-code">${escapeHtml(e)}</code>`).join(', ')}</div>
` : ''}
${src.features && src.features.length > 0 ? `
<div class="asset-features">${src.features.map(f => `<span class="feature-tag">${escapeHtml(f)}</span>`).join('')}</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`);
}
// Tests
if (assets.tests && assets.tests.length > 0) {
sections.push(`
<div class="asset-category">
<h5 class="asset-category-title">🧪 Tests</h5>
<div class="asset-grid">
${assets.tests.map(test => `
<div class="asset-card">
<div class="asset-card-header">
<span class="asset-path">${escapeHtml(test.path)}</span>
${test.test_count ? `<span class="test-count-badge">${test.test_count} tests</span>` : ''}
</div>
<div class="asset-card-body">
<div class="asset-type">${escapeHtml(test.type || '')}</div>
${test.test_classes ? `<div class="asset-meta"><strong>Classes:</strong> ${escapeHtml(test.test_classes.join(', '))}</div>` : ''}
${test.coverage ? `<div class="asset-meta"><strong>Coverage:</strong> ${escapeHtml(test.coverage)}</div>` : ''}
</div>
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderDependenciesSection(dependencies) {
const sections = [];
// Internal Dependencies
if (dependencies.internal && dependencies.internal.length > 0) {
sections.push(`
<div class="dep-category">
<h5 class="dep-category-title">🔄 Internal Dependencies</h5>
<div class="dep-graph">
${dependencies.internal.slice(0, 10).map(dep => `
<div class="dep-item">
<div class="dep-from">${escapeHtml(dep.from)}</div>
<div class="dep-arrow">
<span class="dep-type-badge badge-${dep.type}">${escapeHtml(dep.type)}</span>
</div>
<div class="dep-to">${escapeHtml(dep.to)}</div>
</div>
`).join('')}
${dependencies.internal.length > 10 ? `<div class="dep-more">... and ${dependencies.internal.length - 10} more</div>` : ''}
</div>
</div>
`);
}
// External Dependencies
if (dependencies.external && dependencies.external.length > 0) {
sections.push(`
<div class="dep-category">
<h5 class="dep-category-title">📦 External Dependencies</h5>
<div class="dep-grid">
${dependencies.external.map(dep => `
<div class="dep-external-card">
<div class="dep-package-name">${escapeHtml(dep.package)}</div>
<div class="dep-version">${escapeHtml(dep.version || '')}</div>
<div class="dep-usage">${escapeHtml(dep.usage || '')}</div>
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}
function renderTestContextSection(testContext) {
const sections = [];
// Test Frameworks
if (testContext.frameworks) {
const frameworks = testContext.frameworks;
sections.push(`
<div class="test-category">
<h5 class="test-category-title">🛠 Test Frameworks</h5>
<div class="test-frameworks-grid">
${frameworks.backend ? `
<div class="framework-card framework-installed">
<div class="framework-header">
<span class="framework-label">Backend</span>
<span class="framework-name">${escapeHtml(frameworks.backend.name || 'N/A')}</span>
</div>
${frameworks.backend.plugins ? `
<div class="framework-plugins">${frameworks.backend.plugins.map(p => `<span class="plugin-tag">${escapeHtml(p)}</span>`).join('')}</div>
` : ''}
</div>
` : ''}
${frameworks.frontend ? `
<div class="framework-card ${frameworks.frontend.name && frameworks.frontend.name.includes('NONE') ? 'framework-missing' : 'framework-installed'}">
<div class="framework-header">
<span class="framework-label">Frontend</span>
<span class="framework-name">${escapeHtml(frameworks.frontend.name || 'N/A')}</span>
</div>
${frameworks.frontend.recommended ? `<div class="framework-recommended">Recommended: ${escapeHtml(frameworks.frontend.recommended)}</div>` : ''}
${frameworks.frontend.gap ? `<div class="framework-gap">⚠️ ${escapeHtml(frameworks.frontend.gap)}</div>` : ''}
</div>
` : ''}
</div>
</div>
`);
}
// 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(`
<div class="test-category">
<h5 class="test-category-title">📊 Test Statistics</h5>
<div class="test-stats-grid">
<div class="stat-card">
<div class="stat-value">${totalTests}</div>
<div class="stat-label">Total Tests</div>
</div>
<div class="stat-card">
<div class="stat-value">${totalClasses}</div>
<div class="stat-label">Test Classes</div>
</div>
${testContext.coverage_config && testContext.coverage_config.target ? `
<div class="stat-card">
<div class="stat-value">${escapeHtml(testContext.coverage_config.target)}</div>
<div class="stat-label">Coverage Target</div>
</div>
` : ''}
</div>
</div>
`);
}
// Test Markers
if (testContext.test_markers) {
sections.push(`
<div class="test-category">
<h5 class="test-category-title">🏷 Test Markers</h5>
<div class="test-markers-grid">
${Object.entries(testContext.test_markers).map(([marker, desc]) => `
<div class="marker-card">
<span class="marker-name">@${escapeHtml(marker)}</span>
<span class="marker-desc">${escapeHtml(desc)}</span>
</div>
`).join('')}
</div>
</div>
`);
}
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(`
<div class="risk-indicator" style="border-color: ${riskColor}">
<div class="risk-level" style="background: ${riskColor}">
${escapeHtml(riskLevel.toUpperCase())} RISK
</div>
${conflictDetection.mitigation_strategy ? `
<div class="risk-mitigation">
<strong>Mitigation Strategy:</strong> ${escapeHtml(conflictDetection.mitigation_strategy)}
</div>
` : ''}
</div>
`);
}
// Risk Factors
if (conflictDetection.risk_factors) {
const factors = conflictDetection.risk_factors;
sections.push(`
<div class="conflict-category">
<h5 class="conflict-category-title"> Risk Factors</h5>
<div class="risk-factors-list">
${factors.test_gaps && factors.test_gaps.length > 0 ? `
<div class="risk-factor">
<strong class="risk-factor-title">Test Gaps:</strong>
<ul class="risk-factor-items">
${factors.test_gaps.map(gap => `<li>${escapeHtml(gap)}</li>`).join('')}
</ul>
</div>
` : ''}
${factors.existing_implementations && factors.existing_implementations.length > 0 ? `
<div class="risk-factor">
<strong class="risk-factor-title">Existing Implementations:</strong>
<ul class="risk-factor-items">
${factors.existing_implementations.map(impl => `<li>${escapeHtml(impl)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
</div>
`);
}
// Affected Modules
if (conflictDetection.affected_modules && conflictDetection.affected_modules.length > 0) {
sections.push(`
<div class="conflict-category">
<h5 class="conflict-category-title">📦 Affected Modules</h5>
<div class="affected-modules-grid">
${conflictDetection.affected_modules.map(mod => `
<span class="affected-module-tag">${escapeHtml(mod)}</span>
`).join('')}
</div>
</div>
`);
}
// Historical Conflicts
if (conflictDetection.historical_conflicts && conflictDetection.historical_conflicts.length > 0) {
sections.push(`
<div class="conflict-category">
<h5 class="conflict-category-title">📜 Historical Lessons</h5>
<div class="historical-conflicts-list">
${conflictDetection.historical_conflicts.map(conflict => `
<div class="historical-conflict-card">
<div class="conflict-source">Source: ${escapeHtml(conflict.source || 'Unknown')}</div>
${conflict.lesson ? `<div class="conflict-lesson"><strong>Lesson:</strong> ${escapeHtml(conflict.lesson)}</div>` : ''}
${conflict.recommendation ? `<div class="conflict-recommendation"><strong>Recommendation:</strong> ${escapeHtml(conflict.recommendation)}</div>` : ''}
${conflict.challenge ? `<div class="conflict-challenge"><strong>Challenge:</strong> ${escapeHtml(conflict.challenge)}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return sections.join('');
}

View File

@@ -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 `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">No Summaries</div>
<div class="empty-text">No summaries found in .summaries/</div>
</div>
`;
}
// Store summaries in global variable for modal access
window._currentSummaries = summaries;
return `
<div class="summary-tab-content space-y-4">
${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 `
<div class="summary-item-card">
<div class="summary-item-header">
<h4 class="summary-item-title">📄 ${escapeHtml(s.name || 'Summary')}</h4>
<button class="btn-view-modal" onclick="openMarkdownModal('${escapeHtml(s.name || 'Summary')}', window._currentSummaries[${idx}].content, 'markdown');">
👁️ View
</button>
</div>
<div class="summary-item-preview">
<pre class="summary-preview-text">${escapeHtml(previewLines)}${hasMore ? '\n...' : ''}</pre>
</div>
</div>
`;
}).join('')}
</div>
`;
}
// ==========================================
// IMPL Plan Tab Rendering
// ==========================================
function renderImplPlanContent(implPlan) {
if (!implPlan) {
return `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">No IMPL Plan</div>
<div class="empty-text">No IMPL_PLAN.md found for this session.</div>
</div>
`;
}
// 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 `
<div class="impl-plan-tab-content">
<div class="impl-plan-card">
<div class="impl-plan-header">
<h3 class="impl-plan-title">📐 Implementation Plan</h3>
<button class="btn-view-modal" onclick="openMarkdownModal('IMPL_PLAN.md', window._currentImplPlan, 'markdown')">
👁️ View
</button>
</div>
<div class="impl-plan-preview">
<pre class="impl-plan-preview-text">${escapeHtml(previewLines)}${hasMore ? '\n...' : ''}</pre>
</div>
</div>
</div>
`;
}
// ==========================================
// Review Tab Rendering
// ==========================================
function renderReviewContent(review) {
if (!review || !review.dimensions) {
return `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">No Review Data</div>
<div class="empty-text">No review findings in .review/</div>
</div>
`;
}
const dimensions = Object.entries(review.dimensions);
if (dimensions.length === 0) {
return `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">No Findings</div>
<div class="empty-text">No review findings found.</div>
</div>
`;
}
return `
<div class="review-tab-content">
${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 `
<div class="review-dimension-section">
<div class="dimension-header">
<span class="dimension-name">${escapeHtml(dim)}</span>
<span class="dimension-count">${findings.length} finding${findings.length !== 1 ? 's' : ''}</span>
</div>
<div class="dimension-findings">
${findings.map(f => `
<div class="finding-item ${f.severity || 'medium'}">
<div class="finding-header">
<span class="finding-severity ${f.severity || 'medium'}">${f.severity || 'medium'}</span>
<span class="finding-title">${escapeHtml(f.title || 'Finding')}</span>
</div>
<p class="finding-description">${escapeHtml(f.description || '')}</p>
${f.file ? `<div class="finding-file">📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`}).join('')}
</div>
`;
}
// ==========================================
// Lite Context Tab Rendering
// ==========================================
function renderLiteContextContent(context, session) {
const plan = session.plan || {};
// If we have context from context-package.json
if (context) {
return `
<div class="context-tab-content">
<pre class="json-content">${escapeHtml(JSON.stringify(context, null, 2))}</pre>
</div>
`;
}
// Fallback: show context from plan
if (plan.focus_paths?.length || plan.summary) {
return `
<div class="context-tab-content">
${plan.summary ? `
<div class="context-section">
<h4>Summary</h4>
<p>${escapeHtml(plan.summary)}</p>
</div>
` : ''}
${plan.focus_paths?.length ? `
<div class="context-section">
<h4>Focus Paths</h4>
<div class="path-tags">
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
</div>
</div>
` : ''}
</div>
`;
}
return `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">No Context Data</div>
<div class="empty-text">No context-package.json found for this session.</div>
</div>
`;
}
// ==========================================
// 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(`
<div class="exploration-header">
<h4>${escapeHtml(manifest.task_description || 'Exploration Context')}</h4>
<div class="exploration-meta">
<span class="meta-item">Complexity: <strong>${escapeHtml(manifest.complexity || 'N/A')}</strong></span>
<span class="meta-item">Explorations: <strong>${manifest.exploration_count || 0}</strong></span>
</div>
</div>
`);
// 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(`
<div class="exploration-section collapsible-section">
<div class="collapsible-header" onclick="toggleSection(this)">
<span class="collapse-icon">▶</span>
<span class="section-label">${explorationTitles[angle] || angle}</span>
</div>
<div class="collapsible-content collapsed">
${renderExplorationAngle(angle, expData)}
</div>
</div>
`);
}
return `<div class="exploration-context">${sections.join('')}</div>`;
}
function renderExplorationAngle(angle, data) {
let content = [];
// Project structure (architecture)
if (data.project_structure) {
content.push(`
<div class="exp-field">
<label>Project Structure</label>
<p>${escapeHtml(data.project_structure)}</p>
</div>
`);
}
// Relevant files
if (data.relevant_files && data.relevant_files.length) {
content.push(`
<div class="exp-field">
<label>Relevant Files (${data.relevant_files.length})</label>
<div class="relevant-files-list">
${data.relevant_files.slice(0, 10).map(f => `
<div class="file-item-exp">
<div class="file-path"><code>${escapeHtml(f.path || '')}</code></div>
<div class="file-relevance">Relevance: ${(f.relevance * 100).toFixed(0)}%</div>
${f.rationale ? `<div class="file-rationale">${escapeHtml(f.rationale.substring(0, 200))}...</div>` : ''}
</div>
`).join('')}
${data.relevant_files.length > 10 ? `<div class="more-files">... and ${data.relevant_files.length - 10} more files</div>` : ''}
</div>
</div>
`);
}
// Patterns
if (data.patterns) {
content.push(`
<div class="exp-field">
<label>Patterns</label>
<p class="patterns-text">${escapeHtml(data.patterns)}</p>
</div>
`);
}
// Dependencies
if (data.dependencies) {
content.push(`
<div class="exp-field">
<label>Dependencies</label>
<p>${escapeHtml(data.dependencies)}</p>
</div>
`);
}
// Integration points
if (data.integration_points) {
content.push(`
<div class="exp-field">
<label>Integration Points</label>
<p>${escapeHtml(data.integration_points)}</p>
</div>
`);
}
// Constraints
if (data.constraints) {
content.push(`
<div class="exp-field">
<label>Constraints</label>
<p>${escapeHtml(data.constraints)}</p>
</div>
`);
}
// Clarification needs
if (data.clarification_needs && data.clarification_needs.length) {
content.push(`
<div class="exp-field">
<label>Clarification Needs</label>
<div class="clarification-list">
${data.clarification_needs.map(c => `
<div class="clarification-item">
<div class="clarification-question">${escapeHtml(c.question)}</div>
${c.options && c.options.length ? `
<div class="clarification-options">
${c.options.map((opt, i) => `
<span class="option-badge ${i === c.recommended ? 'recommended' : ''}">${escapeHtml(opt)}</span>
`).join('')}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
return content.join('') || '<p>No data available</p>';
}

View File

@@ -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 `
<!-- Task Header -->
<div class="drawer-task-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
<span class="task-status-badge ${task.status || 'pending'}">${task.status || 'pending'}</span>
</div>
<!-- Tab Navigation -->
<div class="drawer-tabs">
<button class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</button>
<button class="drawer-tab" data-tab="flowchart" onclick="switchDrawerTab('flowchart')">Flowchart</button>
<button class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</button>
<button class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw JSON</button>
</div>
<!-- Tab Content -->
<div class="drawer-tab-content">
<!-- Overview Tab (default) -->
<div class="drawer-panel active" data-tab="overview">
${renderPreAnalysisSteps(fc.pre_analysis)}
${renderImplementationStepsList(fc.implementation_approach)}
</div>
<!-- Flowchart Tab -->
<div class="drawer-panel" data-tab="flowchart">
<div id="flowchartContainer" class="flowchart-container"></div>
</div>
<!-- Files Tab -->
<div class="drawer-panel" data-tab="files">
${renderTargetFiles(fc.target_files)}
${fc.test_commands ? renderTestCommands(fc.test_commands) : ''}
</div>
<!-- Raw JSON Tab -->
<div class="drawer-panel" data-tab="raw">
<pre class="json-view">${escapeHtml(JSON.stringify(task, null, 2))}</pre>
</div>
</div>
`;
}
function renderLiteTaskDrawerContent(task, session) {
const rawTask = task._raw || task;
return `
<!-- Task Header -->
<div class="drawer-task-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
${rawTask.action ? `<span class="action-badge">${escapeHtml(rawTask.action)}</span>` : ''}
</div>
<!-- Tab Navigation -->
<div class="drawer-tabs">
<button class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</button>
<button class="drawer-tab" data-tab="implementation" onclick="switchDrawerTab('implementation')">Implementation</button>
<button class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</button>
<button class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw JSON</button>
</div>
<!-- Tab Content -->
<div class="drawer-tab-content">
<!-- Overview Tab (default) -->
<div class="drawer-panel active" data-tab="overview">
${renderLiteTaskOverview(rawTask)}
</div>
<!-- Implementation Tab -->
<div class="drawer-panel" data-tab="implementation">
${renderLiteTaskImplementation(rawTask)}
</div>
<!-- Files Tab -->
<div class="drawer-panel" data-tab="files">
${renderLiteTaskFiles(rawTask)}
</div>
<!-- Raw JSON Tab -->
<div class="drawer-panel" data-tab="raw">
<pre class="json-view">${escapeHtml(JSON.stringify(rawTask, null, 2))}</pre>
</div>
</div>
`;
}
// 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(`
<div class="drawer-section">
<h4 class="drawer-section-title">Description</h4>
<p class="task-description">${escapeHtml(task.description)}</p>
</div>
`);
}
// Modification Points
if (task.modification_points?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Modification Points</h4>
<div class="modification-points-list">
${task.modification_points.map(mp => `
<div class="mod-point-item">
<div class="mod-point-file">
<span class="file-icon">📄</span>
<code>${escapeHtml(mp.file || mp.path || '')}</code>
</div>
${mp.target ? `<div class="mod-point-target">Target: <code>${escapeHtml(mp.target)}</code></div>` : ''}
${mp.change ? `<div class="mod-point-change">${escapeHtml(mp.change)}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Implementation Steps
if (task.implementation?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Implementation Steps</h4>
<ol class="implementation-steps-list">
${task.implementation.map(step => `
<li class="impl-step-item">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</li>
`).join('')}
</ol>
</div>
`);
}
// Reference
if (task.reference) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Reference</h4>
${task.reference.pattern ? `<div class="ref-pattern"><strong>Pattern:</strong> ${escapeHtml(task.reference.pattern)}</div>` : ''}
${task.reference.files?.length ? `
<div class="ref-files">
<strong>Files:</strong>
<ul>
${task.reference.files.map(f => `<li><code>${escapeHtml(f)}</code></li>`).join('')}
</ul>
</div>
` : ''}
${task.reference.examples ? `<div class="ref-examples"><strong>Examples:</strong> ${escapeHtml(task.reference.examples)}</div>` : ''}
</div>
`);
}
// Acceptance Criteria
if (task.acceptance?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Acceptance Criteria</h4>
<ul class="acceptance-list">
${task.acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}
</ul>
</div>
`);
}
// Dependencies
if (task.depends_on?.length) {
sections.push(`
<div class="drawer-section">
<h4 class="drawer-section-title">Dependencies</h4>
<div class="dependencies-list">
${task.depends_on.map(dep => `<span class="dep-badge">${escapeHtml(dep)}</span>`).join(' ')}
</div>
</div>
`);
}
return sections.join('');
}
// Render lite task overview
function renderLiteTaskOverview(task) {
let sections = [];
// Description Card
if (task.description) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📝</span>
<h4 class="lite-card-title">Description</h4>
</div>
<div class="lite-card-body">
<p class="lite-description">${escapeHtml(task.description)}</p>
</div>
</div>
`);
}
// Scope Card
if (task.scope) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📂</span>
<h4 class="lite-card-title">Scope</h4>
</div>
<div class="lite-card-body">
<div class="lite-scope-box">
<code>${escapeHtml(task.scope)}</code>
</div>
</div>
</div>
`);
}
// Acceptance Criteria Card
if (task.acceptance && task.acceptance.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">✅</span>
<h4 class="lite-card-title">Acceptance Criteria</h4>
<span class="lite-count-badge">${task.acceptance.length}</span>
</div>
<div class="lite-card-body">
<ul class="lite-checklist">
${task.acceptance.map(a => `
<li class="lite-check-item">
<span class="lite-check-icon">○</span>
<span class="lite-check-text">${escapeHtml(a)}</span>
</li>
`).join('')}
</ul>
</div>
</div>
`);
}
// Reference Card
if (task.reference) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📚</span>
<h4 class="lite-card-title">Reference</h4>
</div>
<div class="lite-card-body">
${task.reference.pattern ? `
<div class="lite-ref-section">
<span class="lite-ref-label">Pattern:</span>
<span class="lite-ref-value">${escapeHtml(task.reference.pattern)}</span>
</div>
` : ''}
${task.reference.files && task.reference.files.length > 0 ? `
<div class="lite-ref-section">
<span class="lite-ref-label">Files:</span>
<div class="lite-ref-files">
${task.reference.files.map(f => `<code class="lite-file-tag">${escapeHtml(f)}</code>`).join('')}
</div>
</div>
` : ''}
${task.reference.examples ? `
<div class="lite-ref-section">
<span class="lite-ref-label">Examples:</span>
<span class="lite-ref-value">${escapeHtml(task.reference.examples)}</span>
</div>
` : ''}
</div>
</div>
`);
}
// Dependencies Card
if (task.depends_on && task.depends_on.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">🔗</span>
<h4 class="lite-card-title">Dependencies</h4>
</div>
<div class="lite-card-body">
<div class="lite-deps-tags">
${task.depends_on.map(dep => `<span class="lite-dep-tag">${escapeHtml(dep)}</span>`).join('')}
</div>
</div>
</div>
`);
}
return sections.length > 0 ? sections.join('') : '<div class="empty-section">No overview data</div>';
}
// Render lite task implementation steps
function renderLiteTaskImplementation(task) {
let sections = [];
// Implementation Steps Card
if (task.implementation && task.implementation.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📋</span>
<h4 class="lite-card-title">Implementation Steps</h4>
<span class="lite-count-badge">${task.implementation.length}</span>
</div>
<div class="lite-card-body">
<div class="lite-impl-steps">
${task.implementation.map((step, idx) => `
<div class="lite-impl-step">
<div class="lite-step-num">${idx + 1}</div>
<div class="lite-step-content">
<p class="lite-step-text">${escapeHtml(typeof step === 'string' ? step : step.step || JSON.stringify(step))}</p>
</div>
</div>
`).join('')}
</div>
</div>
</div>
`);
}
// Modification Points Card
if (task.modification_points && task.modification_points.length > 0) {
sections.push(`
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">🔧</span>
<h4 class="lite-card-title">Modification Points</h4>
<span class="lite-count-badge">${task.modification_points.length}</span>
</div>
<div class="lite-card-body">
<div class="lite-mod-points">
${task.modification_points.map(mp => `
<div class="lite-mod-card">
<div class="lite-mod-header">
<code class="lite-mod-file">${escapeHtml(mp.file || '')}</code>
</div>
${mp.target ? `
<div class="lite-mod-target">
<span class="lite-mod-label">Target:</span>
<span class="lite-mod-value">${escapeHtml(mp.target)}</span>
</div>
` : ''}
${mp.change ? `
<div class="lite-mod-change">${escapeHtml(mp.change)}</div>
` : ''}
</div>
`).join('')}
</div>
</div>
</div>
`);
}
return sections.length > 0 ? sections.join('') : '<div class="empty-section">No implementation data</div>';
}
// 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 '<div class="empty-section">No files specified</div>';
}
return `
<div class="drawer-section">
<h4 class="drawer-section-title">Target Files</h4>
<ul class="target-files-list">
${files.map(f => `
<li class="file-item">
<span class="file-icon">📄</span>
<code>${escapeHtml(f)}</code>
</li>
`).join('')}
</ul>
</div>
`;
}

View File

@@ -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 '<div class="empty-section">No pre-analysis steps</div>';
}
return `
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">🔍</span>
<h4 class="lite-card-title">Pre-Analysis Steps</h4>
<span class="lite-count-badge">${preAnalysis.length}</span>
</div>
<div class="lite-card-body">
<div class="lite-impl-steps">
${preAnalysis.map((item, idx) => `
<div class="lite-impl-step">
<div class="lite-step-num">${idx + 1}</div>
<div class="lite-step-content">
<p class="lite-step-text">${escapeHtml(item.step || item.action || 'Step ' + (idx + 1))}</p>
${item.action && item.action !== item.step ? `
<div class="lite-step-meta">
<span class="lite-step-label">Action:</span>
<span class="lite-step-value">${escapeHtml(item.action)}</span>
</div>
` : ''}
${item.commands?.length ? `
<div class="lite-step-commands">
${item.commands.map(c => `<code class="lite-cmd-tag">${escapeHtml(typeof c === 'string' ? c : JSON.stringify(c))}</code>`).join('')}
</div>
` : ''}
${item.output_to ? `
<div class="lite-step-meta">
<span class="lite-step-label">Output:</span>
<code class="lite-file-tag">${escapeHtml(item.output_to)}</code>
</div>
` : ''}
</div>
</div>
`).join('')}
</div>
</div>
</div>
`;
}
function renderImplementationStepsList(steps) {
if (!Array.isArray(steps) || steps.length === 0) {
return '<div class="empty-section">No implementation steps</div>';
}
return `
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📋</span>
<h4 class="lite-card-title">Implementation Approach</h4>
<span class="lite-count-badge">${steps.length}</span>
</div>
<div class="lite-card-body">
<div class="session-impl-steps">
${steps.map((step, idx) => {
const hasMods = step.modification_points?.length;
const hasFlow = step.logic_flow?.length;
return `
<div class="session-impl-step">
<div class="session-step-header">
<div class="lite-step-num">${step.step || idx + 1}</div>
<div class="session-step-title">${escapeHtml(step.title || 'Untitled Step')}</div>
</div>
${step.description ? `<div class="session-step-desc">${escapeHtml(step.description)}</div>` : ''}
${hasMods ? `
<div class="session-step-section">
<div class="session-section-label">
<span class="session-section-icon">🔧</span>
<span>Modifications</span>
<span class="lite-count-badge">${step.modification_points.length}</span>
</div>
<div class="session-mods-list">
${step.modification_points.map(mp => `
<div class="session-mod-item">
${typeof mp === 'string' ? `<code class="lite-file-tag">${escapeHtml(mp)}</code>` : `
<code class="lite-file-tag">${escapeHtml(mp.file || mp.path || '')}</code>
${mp.changes ? `<span class="session-mod-change">${escapeHtml(mp.changes)}</span>` : ''}
`}
</div>
`).join('')}
</div>
</div>
` : ''}
${hasFlow ? `
<div class="session-step-section">
<div class="session-section-label">
<span class="session-section-icon">⚡</span>
<span>Logic Flow</span>
<span class="lite-count-badge">${step.logic_flow.length}</span>
</div>
<div class="session-flow-list">
${step.logic_flow.map((lf, lfIdx) => `
<div class="session-flow-item">
<span class="session-flow-num">${lfIdx + 1}</span>
<span class="session-flow-text">${escapeHtml(typeof lf === 'string' ? lf : lf.action || JSON.stringify(lf))}</span>
</div>
`).join('')}
</div>
</div>
` : ''}
${step.depends_on?.length ? `
<div class="session-step-deps">
<span class="session-deps-label">Dependencies:</span>
<div class="lite-deps-tags">
${step.depends_on.map(d => `<span class="lite-dep-tag">${escapeHtml(d)}</span>`).join('')}
</div>
</div>
` : ''}
</div>
`}).join('')}
</div>
</div>
</div>
`;
}
function renderTargetFiles(files) {
if (!Array.isArray(files) || files.length === 0) {
return '<div class="empty-section">No target files</div>';
}
// Get current project path for building full paths
const projectPath = window.currentProjectPath || '';
return `
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">📁</span>
<h4 class="lite-card-title">Target Files</h4>
<span class="lite-count-badge">${files.length}</span>
</div>
<div class="lite-card-body">
<div class="session-files-list">
${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 `
<a href="${vscodeUri}" class="session-file-item" title="Open in VS Code: ${escapeHtml(fullPath)}">
<span class="session-file-icon">📄</span>
<code class="session-file-path">${escapeHtml(filePath)}</code>
<span class="session-file-action">↗</span>
</a>
`;
}).join('')}
</div>
</div>
</div>
`;
}
function renderTestCommands(testCommands) {
if (!testCommands || typeof testCommands !== 'object') return '';
const entries = Object.entries(testCommands);
if (entries.length === 0) return '';
return `
<div class="lite-card">
<div class="lite-card-header">
<span class="lite-card-icon">🧪</span>
<h4 class="lite-card-title">Test Commands</h4>
<span class="lite-count-badge">${entries.length}</span>
</div>
<div class="lite-card-body">
<div class="session-test-commands">
${entries.map(([key, val]) => `
<div class="session-test-item">
<span class="session-test-label">${escapeHtml(key)}</span>
<code class="session-test-cmd">${escapeHtml(typeof val === 'string' ? val : JSON.stringify(val))}</code>
</div>
`).join('')}
</div>
</div>
</div>
`;
}
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 `
<div class="task-detail" id="task-${sessionId}-${task.id}">
<div class="task-detail-header">
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<span class="task-status-badge ${task.status}">${task.status}</span>
<div class="task-header-actions">
<button class="btn-view-json" onclick="showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
</div>
</div>
<!-- Collapsible: Meta -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">meta</span>
<span class="section-preview">${escapeHtml((task.meta?.type || task.meta?.action || '') + (task.meta?.scope ? ' | ' + task.meta.scope : ''))}</span>
</div>
<div class="collapsible-content collapsed">
${renderDynamicFields(task.meta || rawTask, ['type', 'action', 'agent', 'scope', 'module', 'execution_group'])}
</div>
</div>
<!-- Collapsible: Context -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">context</span>
<span class="section-preview">${escapeHtml(getContextPreview(task.context, rawTask))}</span>
</div>
<div class="collapsible-content collapsed">
${renderContextFields(task.context, rawTask)}
</div>
</div>
<!-- Collapsible: Flow Control (with Flowchart) -->
<div class="collapsible-section">
<div class="collapsible-header">
<span class="collapse-icon">▶</span>
<span class="section-label">flow_control</span>
<span class="section-preview">${escapeHtml(getFlowControlPreview(task.flow_control, rawTask))}</span>
</div>
<div class="collapsible-content collapsed">
<div class="flowchart-container" id="flowchart-${sessionId}-${task.id}"></div>
${renderFlowControlDetails(task.flow_control, rawTask)}
</div>
</div>
</div>
`;
}
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 '<div class="field-value json-value-null">null</div>';
const entries = Object.entries(obj).filter(([k, v]) => v !== null && v !== undefined && k !== '_raw');
if (entries.length === 0) return '<div class="field-value json-value-null">Empty</div>';
// 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 `<div class="field-group">${entries.map(([key, value]) => renderFieldRow(key, value)).join('')}</div>`;
}
function renderFieldRow(key, value) {
return `
<div class="field-row">
<span class="field-label">${escapeHtml(key)}:</span>
<div class="field-value">${renderFieldValue(key, value)}</div>
</div>
`;
}
function renderFieldValue(key, value) {
if (value === null || value === undefined) {
return '<span class="json-value-null">null</span>';
}
if (typeof value === 'boolean') {
return `<span class="json-value-boolean">${value}</span>`;
}
if (typeof value === 'number') {
return `<span class="json-value-number">${value}</span>`;
}
if (typeof value === 'string') {
// Check if it's a path
if (key.includes('path') || key.includes('file') || value.includes('/') || value.includes('\\')) {
return `<span class="array-item path-item">${escapeHtml(value)}</span>`;
}
return `<span class="json-value-string">${escapeHtml(value)}</span>`;
}
if (Array.isArray(value)) {
if (value.length === 0) return '<span class="json-value-null">[]</span>';
// Check if array contains objects or strings
if (typeof value[0] === 'object') {
return `<div class="nested-array">${value.map((item, i) => `
<div class="array-object">
<div class="array-object-header">[${i + 1}]</div>
${renderDynamicFields(item)}
</div>
`).join('')}</div>`;
}
// Array of strings/primitives
const isPathArray = key.includes('path') || key.includes('file');
return `<div class="array-value">${value.map(v =>
`<span class="array-item ${isPathArray ? 'path-item' : ''}">${escapeHtml(String(v))}</span>`
).join('')}</div>`;
}
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(`
<div class="context-field">
<label>requirements:</label>
${description ? `<p style="margin-bottom: 8px;">${escapeHtml(description)}</p>` : ''}
${requirements.length > 0 ? `<ul>${requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>` : ''}
</div>
`);
}
// Focus paths / Modification points
const focusPaths = context?.focus_paths || [];
const modPoints = rawTask?.modification_points || [];
if (focusPaths.length > 0 || modPoints.length > 0) {
sections.push(`
<div class="context-field">
<label>${modPoints.length > 0 ? 'modification_points:' : 'focus_paths:'}</label>
${modPoints.length > 0 ? `
<div class="mod-points">
${modPoints.map(m => `
<div class="mod-point">
<span class="array-item path-item">${escapeHtml(m.file || m)}</span>
${m.target ? `<span class="mod-target">→ ${escapeHtml(m.target)}</span>` : ''}
${m.change ? `<p class="mod-change">${escapeHtml(m.change)}</p>` : ''}
</div>
`).join('')}
</div>
` : `
<div class="path-tags">${focusPaths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}</div>
`}
</div>
`);
}
// Acceptance criteria
const acceptance = context?.acceptance || rawTask?.acceptance || [];
if (acceptance.length > 0) {
sections.push(`
<div class="context-field">
<label>acceptance:</label>
<ul>${acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}</ul>
</div>
`);
}
// Dependencies
const depends = context?.depends_on || rawTask?.depends_on || [];
if (depends.length > 0) {
sections.push(`
<div class="context-field">
<label>depends_on:</label>
<div class="path-tags">${depends.map(d => `<span class="array-item depends-badge">${escapeHtml(d)}</span>`).join('')}</div>
</div>
`);
}
// Reference
const reference = rawTask?.reference;
if (reference) {
sections.push(`
<div class="context-field">
<label>reference:</label>
${renderDynamicFields(reference)}
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No context data</div>';
}
function renderFlowControlDetails(flowControl, rawTask) {
const sections = [];
// Pre-analysis
const preAnalysis = flowControl?.pre_analysis || rawTask?.pre_analysis || [];
if (preAnalysis.length > 0) {
sections.push(`
<div class="context-field" style="margin-top: 16px;">
<label>pre_analysis:</label>
<ul>${preAnalysis.map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
</div>
`);
}
// Target files
const targetFiles = flowControl?.target_files || rawTask?.target_files || [];
if (targetFiles.length > 0) {
sections.push(`
<div class="context-field">
<label>target_files:</label>
<div class="path-tags">${targetFiles.map(f => `<span class="path-tag">${escapeHtml(f)}</span>`).join('')}</div>
</div>
`);
}
return sections.join('');
}

View File

@@ -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' ? '🌙' : '☀️';
}

View File

@@ -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();
}
});
});

View File

@@ -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 = {};

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* 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);
}
}
}
}

View File

@@ -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 `
<div class="session-detail-page session-type-fix">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">🔧 ${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge test-fix">Fix</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Fix Progress Section -->
<div class="fix-progress-section">
<div class="fix-progress-header">
<h3>🔧 Fix Progress</h3>
<span class="phase-badge ${session.phase || 'execution'}">${session.phase || 'Execution'}</span>
</div>
<!-- Progress Bar -->
<div class="fix-progress-bar">
<div class="fix-progress-bar-fill" style="width: ${percentComplete}%"></div>
</div>
<div class="progress-text">
<strong>${fixedCount + failedCount}/${totalTasks}</strong> completed (${percentComplete.toFixed(1)}%)
</div>
<!-- Summary Cards -->
<div class="fix-summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value">${totalTasks}</div>
<div class="summary-label">Total Tasks</div>
</div>
<div class="summary-card fixed">
<div class="summary-icon">✅</div>
<div class="summary-value">${fixedCount}</div>
<div class="summary-label">Fixed</div>
</div>
<div class="summary-card failed">
<div class="summary-icon">❌</div>
<div class="summary-value">${failedCount}</div>
<div class="summary-label">Failed</div>
</div>
<div class="summary-card pending">
<div class="summary-icon">⏳</div>
<div class="summary-value">${pendingCount}</div>
<div class="summary-label">Pending</div>
</div>
</div>
<!-- Stage Timeline (if available) -->
${session.stages && session.stages.length > 0 ? `
<div class="stage-timeline">
${session.stages.map((stage, idx) => `
<div class="stage-item ${stage.status || 'pending'}">
<div class="stage-number">Stage ${idx + 1}</div>
<div class="stage-mode">${stage.execution_mode === 'parallel' ? '⚡ Parallel' : '➡️ Serial'}</div>
<div class="stage-groups">${stage.groups?.length || 0} groups</div>
</div>
`).join('')}
</div>
` : ''}
</div>
<!-- Fix Tasks Grid -->
<div class="fix-tasks-section">
<div class="tasks-header">
<h3>📋 Fix Tasks</h3>
<div class="task-filters">
<button class="filter-btn active" data-status="all" onclick="filterFixTasks('all')">All</button>
<button class="filter-btn" data-status="pending" onclick="filterFixTasks('pending')">Pending</button>
<button class="filter-btn" data-status="in_progress" onclick="filterFixTasks('in_progress')">In Progress</button>
<button class="filter-btn" data-status="fixed" onclick="filterFixTasks('fixed')">Fixed</button>
<button class="filter-btn" data-status="failed" onclick="filterFixTasks('failed')">Failed</button>
</div>
</div>
<div class="fix-tasks-grid" id="fixTasksGrid">
${renderFixTasksGrid(tasks)}
</div>
</div>
<!-- Session Info -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
</div>
</div>
`;
}
function renderFixTasksGrid(tasks) {
if (!tasks || tasks.length === 0) {
return `
<div class="empty-state">
<div class="empty-icon">📋</div>
<div class="empty-text">No fix tasks found</div>
</div>
`;
}
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 `
<div class="fix-task-card status-${statusClass}" data-status="${statusClass}">
<div class="task-card-header">
<span class="task-id-badge">${escapeHtml(task.task_id || task.id || 'N/A')}</span>
<span class="task-status-badge ${statusClass}">${statusText}</span>
</div>
<div class="task-card-title">${escapeHtml(task.title || 'Untitled Task')}</div>
${task.finding_title ? `<div class="task-finding">${escapeHtml(task.finding_title)}</div>` : ''}
${task.file ? `<div class="task-file">📄 ${escapeHtml(task.file)}${task.line ? ':' + task.line : ''}</div>` : ''}
<div class="task-card-meta">
${task.dimension ? `<span class="task-dimension">${escapeHtml(task.dimension)}</span>` : ''}
${task.attempts && task.attempts > 1 ? `<span class="task-attempts">🔄 ${task.attempts} attempts</span>` : ''}
${task.commit_hash ? `<span class="task-commit">💾 ${task.commit_hash.substring(0, 7)}</span>` : ''}
</div>
</div>
`;
}).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';
}
});
}

View File

@@ -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 = `
<div class="empty-state" style="grid-column: 1/-1;">
<div class="empty-icon">📭</div>
<div class="empty-title">No Sessions Found</div>
<div class="empty-text">No workflow sessions match your current filter.</div>
</div>
`;
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderSessionCard(session)).join('')}</div>`;
}
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' ? `<span class="session-type-badge ${sessionType}">${sessionType}</span>` : '';
// Store session data for modal
const sessionKey = `session-${session.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[sessionKey] = session;
return `
<div class="session-card" onclick="showSessionDetailPage('${sessionKey}')">
<div class="session-header">
<div class="session-title">${escapeHtml(session.session_id || 'Unknown')}</div>
<div class="session-badges">
${typeBadge}
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(date)}</span>
<span class="session-meta-item">📋 ${taskCount} tasks</span>
</div>
${taskCount > 0 ? `
<div class="progress-container">
<span class="progress-label">Progress</span>
<div class="progress-bar-wrapper">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<span class="progress-text">${completed}/${taskCount} (${progress}%)</span>
</div>
</div>
` : ''}
</div>
</div>
`;
}

View File

@@ -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 = `
<div class="empty-state">
<div class="empty-icon">⚡</div>
<div class="empty-title">No ${currentLiteType} Sessions</div>
<div class="empty-text">No sessions found in .workflow/.${currentLiteType}/</div>
</div>
`;
return;
}
container.innerHTML = `<div class="sessions-grid">${sessions.map(session => renderLiteTaskCard(session)).join('')}</div>`;
// 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 `
<div class="session-card lite-task-card" onclick="showLiteTaskDetailPage('${sessionKey}')" style="cursor: pointer;">
<div class="session-header">
<div class="session-title">${escapeHtml(session.id)}</div>
<span class="session-status ${session.type}">
${session.type === 'lite-plan' ? '📝 PLAN' : '🔧 FIX'}
</span>
</div>
<div class="session-body">
<div class="session-meta">
<span class="session-meta-item">📅 ${formatDate(session.createdAt)}</span>
<span class="session-meta-item">📋 ${tasks.length} tasks</span>
</div>
</div>
</div>
`;
}
// 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 = `
<div class="session-detail-page lite-task-detail-page">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToLiteTasks()">
<span class="back-icon">←</span>
<span>Back to ${session.type === 'lite-plan' ? 'Lite Plan' : 'Lite Fix'}</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${session.type === 'lite-plan' ? '📝' : '🔧'} ${escapeHtml(session.id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type}">${session.type}</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.createdAt)}</span>
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${tasks.length} tasks</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="tasks" onclick="switchLiteDetailTab('tasks')">
<span class="tab-icon">📋</span>
<span class="tab-text">Tasks</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="plan" onclick="switchLiteDetailTab('plan')">
<span class="tab-icon">📐</span>
<span class="tab-text">Plan</span>
</button>
<button class="detail-tab" data-tab="context" onclick="switchLiteDetailTab('context')">
<span class="tab-icon">📦</span>
<span class="tab-text">Context</span>
</button>
<button class="detail-tab" data-tab="summary" onclick="switchLiteDetailTab('summary')">
<span class="tab-icon">📝</span>
<span class="tab-text">Summary</span>
</button>
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="liteDetailTabContent">
${renderLiteTasksTab(session, tasks, completed, inProgress, pending)}
</div>
</div>
`;
// 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 `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
`;
}
return `
<div class="tasks-tab-content">
<div class="tasks-list" id="liteTasksListContent">
${tasks.map(task => renderLiteTaskDetailItem(session.id, task)).join('')}
</div>
</div>
`;
}
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 `
<div class="detail-task-item-full lite-task-item" onclick="openTaskDrawerForLite('${sessionId}', '${escapeHtml(task.id)}')" style="cursor: pointer;" title="Click to view details">
<div class="task-item-header-lite">
<span class="task-id-badge">${escapeHtml(task.id)}</span>
<span class="task-title">${escapeHtml(task.title || 'Untitled')}</span>
<button class="btn-view-json" onclick="event.stopPropagation(); showJsonModal('${taskJsonId}', '${escapeHtml(task.id)}')">{ } JSON</button>
</div>
<div class="task-item-meta-lite">
${action ? `<span class="meta-badge action">${escapeHtml(action)}</span>` : ''}
${scope ? `<span class="meta-badge scope">${escapeHtml(scope)}</span>` : ''}
${modCount > 0 ? `<span class="meta-badge mods">${modCount} mods</span>` : ''}
${implCount > 0 ? `<span class="meta-badge impl">${implCount} steps</span>` : ''}
${acceptCount > 0 ? `<span class="meta-badge accept">${acceptCount} acceptance</span>` : ''}
</div>
</div>
`;
}
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 `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">No Plan Data</div>
<div class="empty-text">No plan.json found for this session.</div>
</div>
`;
}
return `
<div class="plan-tab-content">
<!-- Summary -->
${plan.summary ? `
<div class="plan-section">
<h4 class="plan-section-title">📋 Summary</h4>
<p class="plan-summary-text">${escapeHtml(plan.summary)}</p>
</div>
` : ''}
<!-- Approach -->
${plan.approach ? `
<div class="plan-section">
<h4 class="plan-section-title">🎯 Approach</h4>
<p class="plan-approach-text">${escapeHtml(plan.approach)}</p>
</div>
` : ''}
<!-- Focus Paths -->
${plan.focus_paths?.length ? `
<div class="plan-section">
<h4 class="plan-section-title">📁 Focus Paths</h4>
<div class="path-tags">
${plan.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}
</div>
</div>
` : ''}
<!-- Metadata -->
<div class="plan-section">
<h4 class="plan-section-title"> Metadata</h4>
<div class="plan-meta-grid">
${plan.estimated_time ? `<div class="meta-item"><span class="meta-label">Estimated Time:</span> ${escapeHtml(plan.estimated_time)}</div>` : ''}
${plan.complexity ? `<div class="meta-item"><span class="meta-label">Complexity:</span> ${escapeHtml(plan.complexity)}</div>` : ''}
${plan.recommended_execution ? `<div class="meta-item"><span class="meta-label">Execution:</span> ${escapeHtml(plan.recommended_execution)}</div>` : ''}
</div>
</div>
<!-- Raw JSON -->
<div class="plan-section">
<h4 class="plan-section-title">{ } Raw JSON</h4>
<pre class="json-content">${escapeHtml(JSON.stringify(plan, null, 2))}</pre>
</div>
</div>
`;
}
async function loadAndRenderLiteContextTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
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 = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
}
}
async function loadAndRenderLiteSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
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 = `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">No Summaries</div>
<div class="empty-text">No summaries found in .summaries/</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
}
}

View File

@@ -0,0 +1,243 @@
// ==========================================
// PROJECT OVERVIEW VIEW
// ==========================================
function renderProjectOverview() {
const container = document.getElementById('mainContent');
const project = workflowData.projectOverview;
if (!project) {
container.innerHTML = `
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="text-6xl mb-4">📋</div>
<h3 class="text-xl font-semibold text-foreground mb-2">No Project Overview</h3>
<p class="text-muted-foreground mb-4">
Run <code class="px-2 py-1 bg-muted rounded text-sm font-mono">/workflow:init</code> to initialize project analysis
</p>
</div>
`;
return;
}
container.innerHTML = `
<!-- Project Header -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<div class="flex items-start justify-between mb-4">
<div>
<h2 class="text-2xl font-bold text-foreground mb-2">${escapeHtml(project.projectName)}</h2>
<p class="text-muted-foreground">${escapeHtml(project.description || 'No description available')}</p>
</div>
<div class="text-sm text-muted-foreground text-right">
<div>Initialized: ${formatDate(project.initializedAt)}</div>
<div class="mt-1">Mode: <span class="font-mono text-xs px-2 py-0.5 bg-muted rounded">${escapeHtml(project.metadata?.analysis_mode || 'unknown')}</span></div>
</div>
</div>
</div>
<!-- Technology Stack -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>💻</span> Technology Stack
</h3>
<!-- Languages -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Languages</h4>
<div class="flex flex-wrap gap-3">
${project.technologyStack.languages.map(lang => `
<div class="flex items-center gap-2 px-3 py-2 bg-background border border-border rounded-lg ${lang.primary ? 'ring-2 ring-primary' : ''}">
<span class="font-semibold text-foreground">${escapeHtml(lang.name)}</span>
<span class="text-xs text-muted-foreground">${lang.file_count} files</span>
${lang.primary ? '<span class="text-xs px-1.5 py-0.5 bg-primary text-primary-foreground rounded">Primary</span>' : ''}
</div>
`).join('') || '<span class="text-muted-foreground text-sm">No languages detected</span>'}
</div>
</div>
<!-- Frameworks -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.frameworks.map(fw => `
<span class="px-3 py-1.5 bg-success-light text-success rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No frameworks detected</span>'}
</div>
</div>
<!-- Build Tools -->
<div class="mb-5">
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Build Tools</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.build_tools.map(tool => `
<span class="px-3 py-1.5 bg-warning-light text-warning rounded-lg text-sm font-medium">${escapeHtml(tool)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No build tools detected</span>'}
</div>
</div>
<!-- Test Frameworks -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-3">Test Frameworks</h4>
<div class="flex flex-wrap gap-2">
${project.technologyStack.test_frameworks.map(fw => `
<span class="px-3 py-1.5 bg-accent text-accent-foreground rounded-lg text-sm font-medium">${escapeHtml(fw)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">No test frameworks detected</span>'}
</div>
</div>
</div>
<!-- Architecture -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>🏗️</span> Architecture
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<!-- Style -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Style</h4>
<div class="px-3 py-2 bg-background border border-border rounded-lg">
<span class="text-foreground font-medium">${escapeHtml(project.architecture.style)}</span>
</div>
</div>
<!-- Layers -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Layers</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.layers.map(layer => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(layer)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
<!-- Patterns -->
<div>
<h4 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">Patterns</h4>
<div class="flex flex-wrap gap-2">
${project.architecture.patterns.map(pattern => `
<span class="px-2 py-1 bg-muted text-foreground rounded text-sm">${escapeHtml(pattern)}</span>
`).join('') || '<span class="text-muted-foreground text-sm">None</span>'}
</div>
</div>
</div>
</div>
<!-- Key Components -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>⚙️</span> Key Components
</h3>
${project.keyComponents.length > 0 ? `
<div class="space-y-3">
${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: '<span class="px-2 py-0.5 text-xs font-semibold bg-destructive text-destructive-foreground rounded">High</span>',
medium: '<span class="px-2 py-0.5 text-xs font-semibold bg-warning text-foreground rounded">Medium</span>',
low: '<span class="px-2 py-0.5 text-xs font-semibold bg-muted text-muted-foreground rounded">Low</span>'
};
return `
<div class="p-4 ${importanceColors[comp.importance] || importanceColors.low} rounded-lg">
<div class="flex items-start justify-between mb-2">
<h4 class="font-semibold text-foreground">${escapeHtml(comp.name)}</h4>
${importanceBadges[comp.importance] || ''}
</div>
<p class="text-sm text-muted-foreground mb-2">${escapeHtml(comp.description)}</p>
<code class="text-xs font-mono text-primary">${escapeHtml(comp.path)}</code>
</div>
`;
}).join('')}
</div>
` : '<p class="text-muted-foreground text-sm">No key components identified</p>'}
</div>
<!-- Development Index -->
<div class="bg-card border border-border rounded-lg p-6 mb-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📝</span> Development History
</h3>
${renderDevelopmentIndex(project.developmentIndex)}
</div>
<!-- Statistics -->
<div class="bg-card border border-border rounded-lg p-6">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span>📊</span> Statistics
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-primary mb-1">${project.statistics.total_features || 0}</div>
<div class="text-sm text-muted-foreground">Total Features</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-3xl font-bold text-success mb-1">${project.statistics.total_sessions || 0}</div>
<div class="text-sm text-muted-foreground">Total Sessions</div>
</div>
<div class="text-center p-4 bg-background rounded-lg">
<div class="text-sm text-muted-foreground mb-1">Last Updated</div>
<div class="text-sm font-medium text-foreground">${formatDate(project.statistics.last_updated)}</div>
</div>
</div>
</div>
`;
}
function renderDevelopmentIndex(devIndex) {
if (!devIndex) return '<p class="text-muted-foreground text-sm">No development history available</p>';
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 '<p class="text-muted-foreground text-sm">No development history entries</p>';
}
return `
<div class="space-y-4">
${categories.map(cat => {
const entries = devIndex[cat.key] || [];
if (entries.length === 0) return '';
return `
<div>
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>${cat.icon}</span>
<span>${cat.label}</span>
<span class="text-xs px-2 py-0.5 ${cat.badgeClass} rounded-full">${entries.length}</span>
</h4>
<div class="space-y-2">
${entries.slice(0, 5).map(entry => `
<div class="p-3 bg-background border border-border rounded-lg hover:shadow-sm transition-shadow">
<div class="flex items-start justify-between mb-1">
<h5 class="font-medium text-foreground text-sm">${escapeHtml(entry.title)}</h5>
<span class="text-xs text-muted-foreground">${formatDate(entry.date)}</span>
</div>
${entry.description ? `<p class="text-sm text-muted-foreground mb-1">${escapeHtml(entry.description)}</p>` : ''}
<div class="flex items-center gap-2 text-xs">
${entry.sub_feature ? `<span class="px-2 py-0.5 bg-muted rounded">${escapeHtml(entry.sub_feature)}</span>` : ''}
${entry.status ? `<span class="px-2 py-0.5 ${entry.status === 'completed' ? 'bg-success-light text-success' : 'bg-warning-light text-warning'} rounded">${escapeHtml(entry.status)}</span>` : ''}
</div>
</div>
`).join('')}
${entries.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${entries.length - 5} more</div>` : ''}
</div>
</div>
`;
}).join('')}
</div>
`;
}

View File

@@ -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 `
<div class="session-detail-page session-type-review">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">🔍 ${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge review">Review</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Review Progress Section -->
<div class="review-progress-section">
<div class="review-progress-header">
<h3>📊 Review Progress</h3>
<span class="phase-badge ${session.phase || 'in-progress'}">${session.phase || 'In Progress'}</span>
</div>
<!-- Summary Cards -->
<div class="review-summary-grid">
<div class="summary-card">
<div class="summary-icon">📊</div>
<div class="summary-value">${totalFindings}</div>
<div class="summary-label">Total Findings</div>
</div>
<div class="summary-card critical">
<div class="summary-icon">🔴</div>
<div class="summary-value">${criticalCount}</div>
<div class="summary-label">Critical</div>
</div>
<div class="summary-card high">
<div class="summary-icon">🟠</div>
<div class="summary-value">${highCount}</div>
<div class="summary-label">High</div>
</div>
<div class="summary-card">
<div class="summary-icon">📋</div>
<div class="summary-value">${dimensions.length}</div>
<div class="summary-label">Dimensions</div>
</div>
</div>
<!-- Dimension Timeline -->
<div class="dimension-timeline" id="dimensionTimeline">
${dimensions.map((dim, idx) => `
<div class="dimension-item ${dim.status || 'pending'}" data-dimension="${dim.name}">
<div class="dimension-number">D${idx + 1}</div>
<div class="dimension-name">${escapeHtml(dim.name || 'Unknown')}</div>
<div class="dimension-stats">${dim.findings?.length || 0} findings</div>
</div>
`).join('')}
</div>
</div>
<!-- Findings Grid -->
<div class="review-findings-section">
<div class="findings-header">
<h3>🔍 Findings by Dimension</h3>
<div class="findings-filters">
<button class="filter-btn active" data-severity="all" onclick="filterReviewFindings('all')">All</button>
<button class="filter-btn" data-severity="critical" onclick="filterReviewFindings('critical')">Critical</button>
<button class="filter-btn" data-severity="high" onclick="filterReviewFindings('high')">High</button>
<button class="filter-btn" data-severity="medium" onclick="filterReviewFindings('medium')">Medium</button>
</div>
</div>
<div class="findings-grid" id="reviewFindingsGrid">
${renderReviewFindingsGrid(dimensions)}
</div>
</div>
<!-- Session Info -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
</div>
</div>
`;
}
function renderReviewFindingsGrid(dimensions) {
if (!dimensions || dimensions.length === 0) {
return `
<div class="empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-text">No review dimensions found</div>
</div>
`;
}
let html = '';
dimensions.forEach(dim => {
const findings = dim.findings || [];
if (findings.length === 0) return;
html += `
<div class="dimension-findings-group" data-dimension="${dim.name}">
<div class="dimension-group-header">
<span class="dimension-badge">${escapeHtml(dim.name)}</span>
<span class="dimension-count">${findings.length} findings</span>
</div>
<div class="findings-cards">
${findings.map(f => `
<div class="finding-card severity-${f.severity || 'medium'}" data-severity="${f.severity || 'medium'}">
<div class="finding-card-header">
<span class="severity-badge ${f.severity || 'medium'}">${f.severity || 'medium'}</span>
${f.fix_status ? `<span class="fix-status-badge status-${f.fix_status}">${f.fix_status}</span>` : ''}
</div>
<div class="finding-card-title">${escapeHtml(f.title || 'Finding')}</div>
<div class="finding-card-desc">${escapeHtml((f.description || '').substring(0, 100))}${f.description?.length > 100 ? '...' : ''}</div>
${f.file ? `<div class="finding-card-file">📄 ${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</div>` : ''}
</div>
`).join('')}
</div>
</div>
`;
});
return html || '<div class="empty-state"><div class="empty-text">No findings</div></div>';
}
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';
}
});
}

View File

@@ -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 = `
<div class="session-detail-page">
<!-- Header -->
<div class="detail-header">
<button class="btn-back" onclick="goBackToSessions()">
<span class="back-icon">←</span>
<span>Back to Sessions</span>
</button>
<div class="detail-title-row">
<h2 class="detail-session-id">${escapeHtml(session.session_id)}</h2>
<div class="detail-badges">
<span class="session-type-badge ${session.type || 'workflow'}">${session.type || 'workflow'}</span>
<span class="session-status ${isActive ? 'active' : 'archived'}">
${isActive ? 'ACTIVE' : 'ARCHIVED'}
</span>
</div>
</div>
</div>
<!-- Session Info Bar -->
<div class="detail-info-bar">
<div class="info-item">
<span class="info-label">Created:</span>
<span class="info-value">${formatDate(session.created_at)}</span>
</div>
${session.archived_at ? `
<div class="info-item">
<span class="info-label">Archived:</span>
<span class="info-value">${formatDate(session.archived_at)}</span>
</div>
` : ''}
<div class="info-item">
<span class="info-label">Project:</span>
<span class="info-value">${escapeHtml(session.project || '-')}</span>
</div>
<div class="info-item">
<span class="info-label">Tasks:</span>
<span class="info-value">${completed}/${tasks.length} completed</span>
</div>
</div>
<!-- Tab Navigation -->
<div class="detail-tabs">
<button class="detail-tab active" data-tab="tasks" onclick="switchDetailTab('tasks')">
<span class="tab-icon">📋</span>
<span class="tab-text">Tasks</span>
<span class="tab-count">${tasks.length}</span>
</button>
<button class="detail-tab" data-tab="context" onclick="switchDetailTab('context')">
<span class="tab-icon">📦</span>
<span class="tab-text">Context</span>
</button>
<button class="detail-tab" data-tab="summary" onclick="switchDetailTab('summary')">
<span class="tab-icon">📝</span>
<span class="tab-text">Summary</span>
</button>
<button class="detail-tab" data-tab="impl-plan" onclick="switchDetailTab('impl-plan')">
<span class="tab-icon">📐</span>
<span class="tab-text">IMPL Plan</span>
</button>
${session.hasReview ? `
<button class="detail-tab" data-tab="review" onclick="switchDetailTab('review')">
<span class="tab-icon">🔍</span>
<span class="tab-text">Review</span>
</button>
` : ''}
</div>
<!-- Tab Content -->
<div class="detail-tab-content" id="detailTabContent">
${renderTasksTab(session, tasks, completed, inProgress, pending)}
</div>
</div>
`;
}
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 `
<div class="tasks-tab-content">
<!-- Combined Stats & Actions Bar -->
<div class="task-toolbar">
<div class="task-stats-bar">
<span class="task-stat completed">✓ ${completed} completed</span>
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
<span class="task-stat pending">○ ${pending} pending</span>
</div>
<div class="toolbar-divider"></div>
<div class="task-bulk-actions">
<span class="bulk-label">Quick Actions:</span>
<button class="bulk-action-btn" onclick="bulkSetAllStatus('pending')" title="Set all tasks to pending">
<span class="bulk-icon">○</span> All Pending
</button>
<button class="bulk-action-btn" onclick="bulkSetAllStatus('in_progress')" title="Set all tasks to in progress">
<span class="bulk-icon">⟳</span> All In Progress
</button>
<button class="bulk-action-btn completed" onclick="bulkSetAllStatus('completed')" title="Set all tasks to completed">
<span class="bulk-icon">✓</span> All Completed
</button>
</div>
</div>
<div class="tasks-list" id="tasksListContent">
${showLoading ? `
<div class="tab-loading">Loading task details...</div>
` : (tasks.length === 0 ? `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Tasks</div>
<div class="empty-text">This session has no tasks defined.</div>
</div>
` : tasks.map(task => renderDetailTaskItem(task)).join(''))}
</div>
</div>
`;
}
async function loadFullTaskDetails() {
const session = sessionDataStore[currentSessionDetailKey];
if (!session || !window.SERVER_MODE || !session.path) return;
const tasksContainer = document.getElementById('tasksListContent');
tasksContainer.innerHTML = '<div class="tab-loading">Loading full task details...</div>';
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 = `
<div class="tab-empty-state">
<div class="empty-icon">📋</div>
<div class="empty-title">No Task Files</div>
<div class="empty-text">No IMPL-*.json files found in .task/</div>
</div>
`;
}
}
} catch (err) {
tasksContainer.innerHTML = `<div class="tab-error">Failed to load tasks: ${err.message}</div>`;
}
}
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 `
<div class="detail-task-item ${status} status-${status}" data-task-id="${escapeHtml(taskId)}">
<div class="task-item-header">
<span class="task-id-badge">${escapeHtml(taskId)}</span>
<span class="task-title" onclick="openTaskDrawer('${escapeHtml(taskId)}')" style="cursor: pointer; flex: 1;">
${escapeHtml(task.title || task.meta?.title || 'Untitled')}
</span>
<div class="task-status-control" onclick="event.stopPropagation()">
<select class="task-status-select ${status}" onchange="updateSingleTaskStatus('${escapeHtml(taskId)}', this.value)" data-current="${status}">
${statusOptions.map(opt => `
<option value="${opt}" ${opt === status ? 'selected' : ''}>${formatStatusLabel(opt)}</option>
`).join('')}
</select>
</div>
</div>
</div>
`;
}
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(`
<div class="context-field">
<label>description:</label>
<p>${escapeHtml(task.description)}</p>
</div>
`);
}
// Requirements
if (ctx.requirements?.length) {
sections.push(`
<div class="context-field">
<label>requirements:</label>
<ul>${ctx.requirements.map(r => `<li>${escapeHtml(r)}</li>`).join('')}</ul>
</div>
`);
}
// Focus paths
if (ctx.focus_paths?.length) {
sections.push(`
<div class="context-field">
<label>focus_paths:</label>
<div class="path-tags">${ctx.focus_paths.map(p => `<span class="path-tag">${escapeHtml(p)}</span>`).join('')}</div>
</div>
`);
}
// Modification points
if (task.modification_points?.length) {
sections.push(`
<div class="context-field">
<label>modification_points:</label>
<div class="mod-points">
${task.modification_points.map(m => `
<div class="mod-point">
<span class="array-item path-item">${escapeHtml(m.file || m)}</span>
${m.target ? `<span class="mod-target">→ ${escapeHtml(m.target)}</span>` : ''}
${m.change ? `<p class="mod-change">${escapeHtml(m.change)}</p>` : ''}
</div>
`).join('')}
</div>
</div>
`);
}
// Acceptance criteria
const acceptance = ctx.acceptance || task.acceptance || [];
if (acceptance.length) {
sections.push(`
<div class="context-field">
<label>acceptance:</label>
<ul>${acceptance.map(a => `<li>${escapeHtml(a)}</li>`).join('')}</ul>
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No context data</div>';
}
function renderFlowControl(task) {
const sections = [];
const fc = task.flow_control || {};
// Implementation approach
const steps = fc.implementation_approach || task.implementation || [];
if (steps.length) {
sections.push(`
<div class="context-field">
<label>implementation_approach:</label>
<ol class="impl-steps">
${steps.map(s => `<li>${escapeHtml(typeof s === 'string' ? s : s.step || s.action || JSON.stringify(s))}</li>`).join('')}
</ol>
</div>
`);
}
// Pre-analysis
const preAnalysis = fc.pre_analysis || task.pre_analysis || [];
if (preAnalysis.length) {
sections.push(`
<div class="context-field">
<label>pre_analysis:</label>
<ul>${preAnalysis.map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
</div>
`);
}
// Target files
const targetFiles = fc.target_files || task.target_files || [];
if (targetFiles.length) {
sections.push(`
<div class="context-field">
<label>target_files:</label>
<div class="path-tags">${targetFiles.map(f => `<span class="path-tag">${escapeHtml(f)}</span>`).join('')}</div>
</div>
`);
}
return sections.length > 0
? `<div class="context-fields">${sections.join('')}</div>`
: '<div class="field-value json-value-null">No flow control data</div>';
}
async function loadAndRenderContextTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading context data...</div>';
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 = `
<div class="tab-empty-state">
<div class="empty-icon">📦</div>
<div class="empty-title">Context Data</div>
<div class="empty-text">Context data will be loaded from context-package.json</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load context: ${err.message}</div>`;
}
}
async function loadAndRenderSummaryTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading summaries...</div>';
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 = `
<div class="tab-empty-state">
<div class="empty-icon">📝</div>
<div class="empty-title">Summaries</div>
<div class="empty-text">Session summaries will be loaded from .summaries/</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load summaries: ${err.message}</div>`;
}
}
async function loadAndRenderImplPlanTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading IMPL plan...</div>';
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 = `
<div class="tab-empty-state">
<div class="empty-icon">📐</div>
<div class="empty-title">IMPL Plan</div>
<div class="empty-text">IMPL plan will be loaded from IMPL_PLAN.md</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load IMPL plan: ${err.message}</div>`;
}
}
async function loadAndRenderReviewTab(session, contentArea) {
contentArea.innerHTML = '<div class="tab-loading">Loading review data...</div>';
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 = `
<div class="tab-empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-title">Review Data</div>
<div class="empty-text">Review data will be loaded from review files</div>
</div>
`;
} catch (err) {
contentArea.innerHTML = `<div class="tab-error">Failed to load review: ${err.message}</div>`;
}
}
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 = `
<div class="json-modal">
<div class="json-modal-header">
<div class="json-modal-title">
<span class="session-id-badge">${escapeHtml(session.session_id)}</span>
<span>Session JSON</span>
</div>
<button class="json-modal-close" onclick="closeJsonModal(this)">&times;</button>
</div>
<div class="json-modal-body">
<pre class="json-modal-content">${escapeHtml(JSON.stringify(session, null, 2))}</pre>
</div>
<div class="json-modal-footer">
<button class="json-modal-copy" onclick="copyJsonToClipboard(this)">Copy to Clipboard</button>
</div>
</div>
`;
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 = `
<span class="task-stat completed">✓ ${completed} completed</span>
<span class="task-stat in-progress">⟳ ${inProgress} in progress</span>
<span class="task-stat pending">○ ${pending} pending</span>
`;
}
}
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);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -228,6 +228,15 @@
</div>
</div>
</div>
<!-- Refresh Button -->
<button class="refresh-btn p-1.5 text-muted-foreground hover:text-foreground hover:bg-hover rounded" id="refreshWorkspace" title="Refresh workspace">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
<path d="M16 21h5v-5"/>
</svg>
</button>
</div>
<!-- Theme Toggle -->
@@ -291,12 +300,12 @@
<ul class="space-y-0.5">
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-plan" data-tooltip="Lite Plan Sessions">
<span>📝</span>
<span class="nav-text flex-1">lite-plan</span>
<span class="nav-text flex-1">Lite Plan</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeLitePlan">0</span>
</li>
<li class="nav-item flex items-center gap-2 mx-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-lite="lite-fix" data-tooltip="Lite Fix Sessions">
<span>🔧</span>
<span class="nav-text flex-1">lite-fix</span>
<span class="nav-text flex-1">Lite Fix</span>
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeLiteFix">0</span>
</li>
</ul>

View File

@@ -1,182 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CCW Dashboard</title>
<style>
{{CSS_CONTENT}}
</style>
</head>
<body>
<div class="app-container">
<!-- Top Bar -->
<header class="top-bar">
<div class="top-bar-left">
<button class="menu-toggle" id="menuToggle">☰</button>
<div class="logo">
<span class="logo-icon">⚡</span>
<span class="logo-text">Claude Code Workflow</span>
</div>
</div>
<!-- Path Selector -->
<div class="path-selector">
<label>Project:</label>
<div class="path-dropdown">
<button class="path-current" id="pathButton">
<span class="path-text" id="currentPath">{{PROJECT_PATH}}</span>
<span class="dropdown-arrow">▼</span>
</button>
<div class="path-menu" id="pathMenu">
<div class="menu-label">Recent Projects</div>
<div id="recentPaths">
<!-- Dynamic recent paths -->
</div>
<div class="path-actions">
<button class="path-browse" id="browsePath">
📂 Browse...
</button>
</div>
</div>
</div>
</div>
<!-- Header Actions -->
<div class="header-actions">
<button class="theme-toggle" id="themeToggle" title="Toggle theme">🌙</button>
</div>
</header>
<!-- Sidebar Overlay (mobile) -->
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<!-- Main Layout -->
<div class="main-layout">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<nav class="sidebar-nav">
<!-- Sessions Section -->
<div class="nav-section">
<div class="nav-section-header">
<span class="nav-icon">📁</span>
<span class="nav-section-title">Sessions</span>
</div>
<ul class="nav-items">
<li class="nav-item active tooltip" data-filter="all" data-tooltip="All Sessions">
<span class="nav-icon">📋</span>
<span class="nav-text">All</span>
<span class="badge" id="badgeAll">0</span>
</li>
<li class="nav-item tooltip" data-filter="active" data-tooltip="Active Sessions">
<span class="nav-icon">🟢</span>
<span class="nav-text">Active</span>
<span class="badge success" id="badgeActive">0</span>
</li>
<li class="nav-item tooltip" data-filter="archived" data-tooltip="Archived Sessions">
<span class="nav-icon">📦</span>
<span class="nav-text">Archived</span>
<span class="badge" id="badgeArchived">0</span>
</li>
</ul>
</div>
<!-- Lite Tasks Section -->
<div class="nav-section" id="liteTasksNav">
<div class="nav-section-header">
<span class="nav-icon">⚡</span>
<span class="nav-section-title">Lite Tasks</span>
</div>
<ul class="nav-items">
<li class="nav-item tooltip" data-lite="lite-plan" data-tooltip="Lite Plan Sessions">
<span class="nav-icon">📝</span>
<span class="nav-text">lite-plan</span>
<span class="badge" id="badgeLitePlan">0</span>
</li>
<li class="nav-item tooltip" data-lite="lite-fix" data-tooltip="Lite Fix Sessions">
<span class="nav-icon">🔧</span>
<span class="nav-text">lite-fix</span>
<span class="badge" id="badgeLiteFix">0</span>
</li>
</ul>
</div>
</nav>
<!-- Sidebar Footer -->
<div class="sidebar-footer">
<button class="sidebar-toggle" id="sidebarToggle">
<span class="toggle-icon">◀</span>
<span class="toggle-text">Collapse</span>
</button>
</div>
</aside>
<!-- Content Area -->
<main class="content">
<!-- Stats Grid -->
<section class="stats-grid">
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-value" id="statTotalSessions">0</div>
<div class="stat-label">Total Sessions</div>
</div>
<div class="stat-card">
<div class="stat-icon">🟢</div>
<div class="stat-value" id="statActiveSessions">0</div>
<div class="stat-label">Active Sessions</div>
</div>
<div class="stat-card">
<div class="stat-icon">📋</div>
<div class="stat-value" id="statTotalTasks">0</div>
<div class="stat-label">Total Tasks</div>
</div>
<div class="stat-card">
<div class="stat-icon">✅</div>
<div class="stat-value" id="statCompletedTasks">0</div>
<div class="stat-label">Completed Tasks</div>
</div>
</section>
<!-- Content Header -->
<div class="content-header">
<h2 class="content-title" id="contentTitle">All Sessions</h2>
<div class="search-box">
<input type="text" placeholder="Search..." id="searchInput">
</div>
</div>
<!-- Main Content Container -->
<section class="main-content" id="mainContent">
<!-- Dynamic content: sessions grid or session detail page -->
</section>
</main>
</div>
<!-- Footer -->
<footer class="bottom-bar">
<div>Generated: <span id="generatedAt">-</span></div>
<div>CCW Dashboard v1.0</div>
</footer>
<!-- Task Detail Drawer -->
<div class="task-detail-drawer" id="taskDetailDrawer">
<div class="drawer-header">
<h3 class="drawer-title" id="drawerTaskTitle">Task Details</h3>
<button class="drawer-close" onclick="closeTaskDrawer()">&times;</button>
</div>
<div class="drawer-content" id="drawerContent">
<!-- Dynamic content -->
</div>
</div>
<div class="drawer-overlay" id="drawerOverlay" onclick="closeTaskDrawer()"></div>
</div>
<!-- D3.js for Flowchart -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
{{JS_CONTENT}}
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,664 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Workflow Dashboard - Task Board</title>
<style>
:root {
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
--bg-card: #ffffff;
--text-primary: #1a202c;
--text-secondary: #718096;
--border-color: #e2e8f0;
--accent-color: #4299e1;
--success-color: #48bb78;
--warning-color: #ed8936;
--danger-color: #f56565;
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
[data-theme="dark"] {
--bg-primary: #1a202c;
--bg-secondary: #2d3748;
--bg-card: #2d3748;
--text-primary: #f7fafc;
--text-secondary: #a0aec0;
--border-color: #4a5568;
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3), 0 1px 2px 0 rgba(0, 0, 0, 0.2);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
transition: background-color 0.3s, color 0.3s;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: var(--bg-secondary);
box-shadow: var(--shadow);
padding: 20px;
margin-bottom: 30px;
border-radius: 8px;
}
h1 {
font-size: 2rem;
margin-bottom: 10px;
color: var(--accent-color);
}
.header-controls {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
margin-top: 15px;
}
.search-box {
flex: 1;
min-width: 250px;
position: relative;
}
.search-box input {
width: 100%;
padding: 10px 15px;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--bg-primary);
color: var(--text-primary);
font-size: 0.95rem;
}
.filter-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s;
background-color: var(--bg-card);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow);
}
.btn.active {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background-color: var(--bg-card);
padding: 20px;
border-radius: 8px;
box-shadow: var(--shadow);
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: var(--accent-color);
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9rem;
margin-top: 5px;
}
.section {
margin-bottom: 40px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-title {
font-size: 1.5rem;
font-weight: 600;
}
.sessions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
}
.session-card {
background-color: var(--bg-card);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 20px;
transition: all 0.3s;
}
.session-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.session-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 15px;
}
.session-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 5px;
}
.session-status {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-active {
background-color: #c6f6d5;
color: #22543d;
}
.status-archived {
background-color: #e2e8f0;
color: #4a5568;
}
[data-theme="dark"] .status-active {
background-color: #22543d;
color: #c6f6d5;
}
[data-theme="dark"] .status-archived {
background-color: #4a5568;
color: #e2e8f0;
}
.session-meta {
display: flex;
gap: 15px;
font-size: 0.85rem;
color: var(--text-secondary);
margin-bottom: 15px;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: var(--bg-primary);
border-radius: 4px;
overflow: hidden;
margin: 15px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--accent-color), var(--success-color));
transition: width 0.3s;
}
.tasks-list {
margin-top: 15px;
}
.task-item {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background-color: var(--bg-primary);
border-radius: 6px;
border-left: 3px solid var(--border-color);
transition: all 0.2s;
}
.task-item:hover {
transform: translateX(4px);
}
.task-item.completed {
border-left-color: var(--success-color);
opacity: 0.8;
}
.task-item.in_progress {
border-left-color: var(--warning-color);
}
.task-item.pending {
border-left-color: var(--text-secondary);
}
.task-checkbox {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid var(--border-color);
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.task-item.completed .task-checkbox {
background-color: var(--success-color);
border-color: var(--success-color);
}
.task-item.completed .task-checkbox::after {
content: '✓';
color: white;
font-size: 0.8rem;
font-weight: bold;
}
.task-item.in_progress .task-checkbox {
border-color: var(--warning-color);
background-color: var(--warning-color);
}
.task-item.in_progress .task-checkbox::after {
content: '⟳';
color: white;
font-size: 0.9rem;
}
.task-title {
flex: 1;
font-size: 0.9rem;
}
.task-id {
font-size: 0.75rem;
color: var(--text-secondary);
font-family: monospace;
margin-left: 10px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
.theme-toggle {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: var(--accent-color);
color: white;
border: none;
cursor: pointer;
font-size: 1.5rem;
box-shadow: var(--shadow-lg);
transition: all 0.3s;
z-index: 1000;
}
.theme-toggle:hover {
transform: scale(1.1);
}
@media (max-width: 768px) {
.sessions-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
h1 {
font-size: 1.5rem;
}
.header-controls {
flex-direction: column;
align-items: stretch;
}
.search-box {
width: 100%;
}
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
margin-left: 8px;
}
.badge-count {
background-color: var(--accent-color);
color: white;
}
.session-footer {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid var(--border-color);
font-size: 0.85rem;
color: var(--text-secondary);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🚀 Workflow Dashboard</h1>
<p style="color: var(--text-secondary);">Task Board - Active and Archived Sessions</p>
<div class="header-controls">
<div class="search-box">
<input type="text" id="searchInput" placeholder="🔍 Search tasks or sessions..." />
</div>
<div class="filter-group">
<button class="btn active" data-filter="all">All</button>
<button class="btn" data-filter="active">Active</button>
<button class="btn" data-filter="archived">Archived</button>
</div>
</div>
</header>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalSessions">0</div>
<div class="stat-label">Total Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="activeSessions">0</div>
<div class="stat-label">Active Sessions</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalTasks">0</div>
<div class="stat-label">Total Tasks</div>
</div>
<div class="stat-card">
<div class="stat-value" id="completedTasks">0</div>
<div class="stat-label">Completed Tasks</div>
</div>
</div>
<div class="section" id="activeSectionContainer">
<div class="section-header">
<h2 class="section-title">📋 Active Sessions</h2>
</div>
<div class="sessions-grid" id="activeSessions"></div>
</div>
<div class="section" id="archivedSectionContainer">
<div class="section-header">
<h2 class="section-title">📦 Archived Sessions</h2>
</div>
<div class="sessions-grid" id="archivedSessions"></div>
</div>
</div>
<button class="theme-toggle" id="themeToggle">🌙</button>
<script>
// Workflow data will be injected here
const workflowData = {{WORKFLOW_DATA}};
// Theme management
function initTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
}
function updateThemeIcon(theme) {
document.getElementById('themeToggle').textContent = theme === 'dark' ? '☀️' : '🌙';
}
// Statistics calculation
function updateStatistics() {
const stats = {
totalSessions: workflowData.activeSessions.length + workflowData.archivedSessions.length,
activeSessions: workflowData.activeSessions.length,
totalTasks: 0,
completedTasks: 0
};
workflowData.activeSessions.forEach(session => {
stats.totalTasks += session.tasks.length;
stats.completedTasks += session.tasks.filter(t => t.status === 'completed').length;
});
workflowData.archivedSessions.forEach(session => {
stats.totalTasks += session.taskCount || 0;
stats.completedTasks += session.taskCount || 0;
});
document.getElementById('totalSessions').textContent = stats.totalSessions;
document.getElementById('activeSessions').textContent = stats.activeSessions;
document.getElementById('totalTasks').textContent = stats.totalTasks;
document.getElementById('completedTasks').textContent = stats.completedTasks;
}
// Render session card
function createSessionCard(session, isActive) {
const card = document.createElement('div');
card.className = 'session-card';
card.dataset.sessionType = isActive ? 'active' : 'archived';
const completedTasks = isActive
? session.tasks.filter(t => t.status === 'completed').length
: (session.taskCount || 0);
const totalTasks = isActive ? session.tasks.length : (session.taskCount || 0);
const progress = totalTasks > 0 ? (completedTasks / totalTasks * 100) : 0;
let tasksHtml = '';
if (isActive && session.tasks.length > 0) {
tasksHtml = `
<div class="tasks-list">
${session.tasks.map(task => `
<div class="task-item ${task.status}">
<div class="task-checkbox"></div>
<div class="task-title">${task.title || 'Untitled Task'}</div>
<span class="task-id">${task.task_id || ''}</span>
</div>
`).join('')}
</div>
`;
}
card.innerHTML = `
<div class="session-header">
<div>
<h3 class="session-title">${session.session_id || 'Unknown Session'}</h3>
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-top: 5px;">
${session.project || ''}
</div>
</div>
<span class="session-status ${isActive ? 'status-active' : 'status-archived'}">
${isActive ? 'Active' : 'Archived'}
</span>
</div>
<div class="session-meta">
<span>📅 ${session.created_at || session.archived_at || 'N/A'}</span>
<span>📊 ${completedTasks}/${totalTasks} tasks</span>
</div>
${totalTasks > 0 ? `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<div style="text-align: center; font-size: 0.85rem; color: var(--text-secondary);">
${Math.round(progress)}% Complete
</div>
` : ''}
${tasksHtml}
${!isActive && session.archive_path ? `
<div class="session-footer">
📁 Archive: ${session.archive_path}
</div>
` : ''}
`;
return card;
}
// Render all sessions
function renderSessions(filter = 'all') {
const activeContainer = document.getElementById('activeSessions');
const archivedContainer = document.getElementById('archivedSessions');
activeContainer.innerHTML = '';
archivedContainer.innerHTML = '';
if (filter === 'all' || filter === 'active') {
if (workflowData.activeSessions.length === 0) {
activeContainer.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<p>No active sessions</p>
</div>
`;
} else {
workflowData.activeSessions.forEach(session => {
activeContainer.appendChild(createSessionCard(session, true));
});
}
}
if (filter === 'all' || filter === 'archived') {
if (workflowData.archivedSessions.length === 0) {
archivedContainer.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📦</div>
<p>No archived sessions</p>
</div>
`;
} else {
workflowData.archivedSessions.forEach(session => {
archivedContainer.appendChild(createSessionCard(session, false));
});
}
}
// Show/hide sections
document.getElementById('activeSectionContainer').style.display =
(filter === 'all' || filter === 'active') ? 'block' : 'none';
document.getElementById('archivedSectionContainer').style.display =
(filter === 'all' || filter === 'archived') ? 'block' : 'none';
}
// Search functionality
function setupSearch() {
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const cards = document.querySelectorAll('.session-card');
cards.forEach(card => {
const text = card.textContent.toLowerCase();
card.style.display = text.includes(query) ? 'block' : 'none';
});
});
}
// Filter functionality
function setupFilters() {
const filterButtons = document.querySelectorAll('[data-filter]');
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
renderSessions(btn.dataset.filter);
});
});
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initTheme();
updateStatistics();
renderSessions();
setupSearch();
setupFilters();
document.getElementById('themeToggle').addEventListener('click', toggleTheme);
});
</script>
</body>
</html>

9086
ccw/test-dashboard.html Normal file

File diff suppressed because it is too large Load Diff