mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
Refactor code structure for improved readability and maintainability
This commit is contained in:
328
ccw/src/core/server.js
Normal file
328
ccw/src/core/server.js
Normal file
@@ -0,0 +1,328 @@
|
||||
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: {} },
|
||||
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 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 = { dimensions: {} };
|
||||
if (existsSync(reviewDir)) {
|
||||
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 content = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8'));
|
||||
result.review.dimensions[dimName] = content.findings || content;
|
||||
} 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: {} },
|
||||
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