mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
Remove backup HTML template for workflow dashboard
This commit is contained in:
29
ccw/src/core/dashboard-generator-patch.js
Normal file
29
ccw/src/core/dashboard-generator-patch.js
Normal 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'
|
||||
];
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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
385
ccw/src/core/server.js.bak
Normal 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;
|
||||
}
|
||||
385
ccw/src/core/server_original.bak
Normal file
385
ccw/src/core/server_original.bak
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user