From 2c1139284832f34c37647255b53c46f19947ccab Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 11 Jan 2026 11:05:41 +0800 Subject: [PATCH] feat: auto-update developmentIndex on session archive (closes #58) - Add updateDevelopmentIndex() function to session-manager.ts - Auto-append entry to developmentIndex when archiving sessions - Add timeline view toggle for Development History section - Support both 'archivedAt' and 'date' field names for compatibility - Add dynamic calculation for statistics (Total Features, Last Updated) - Add CSS styles for timeline view --- ccw/src/templates/dashboard-css/01-base.css | 109 +++++++++ .../dashboard-js/views/project-overview.js | 219 +++++++++++++++--- ccw/src/tools/session-manager.ts | 71 ++++++ 3 files changed, 362 insertions(+), 37 deletions(-) diff --git a/ccw/src/templates/dashboard-css/01-base.css b/ccw/src/templates/dashboard-css/01-base.css index 04c2ac60..08d8925a 100644 --- a/ccw/src/templates/dashboard-css/01-base.css +++ b/ccw/src/templates/dashboard-css/01-base.css @@ -300,3 +300,112 @@ body { padding-right: 2.5rem; } } + +/* =================================== + Development Index Timeline Styles + =================================== */ + +/* View toggle buttons */ +.dev-view-btn { + transition: all 0.2s ease; +} + +.dev-view-btn.active { + background: hsl(var(--primary)); + color: hsl(var(--primary-foreground)); + border-color: hsl(var(--primary)); +} + +.dev-view-btn:hover:not(.active) { + background: hsl(var(--muted)); +} + +/* Development Timeline */ +.dev-timeline { + display: flex; + flex-direction: column; + gap: 0; +} + +.dev-timeline-item { + display: flex; + gap: 1rem; + position: relative; +} + +.dev-timeline-marker { + display: flex; + flex-direction: column; + align-items: center; + flex-shrink: 0; + width: 2rem; +} + +.dev-timeline-dot { + width: 1.75rem; + height: 1.75rem; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + flex-shrink: 0; +} + +.dev-timeline-dot.bg-primary { + background: hsl(var(--primary)); + color: white; +} + +.dev-timeline-dot.bg-success { + background: hsl(var(--success)); + color: white; +} + +.dev-timeline-dot.bg-destructive { + background: hsl(var(--destructive)); + color: white; +} + +.dev-timeline-dot.bg-warning { + background: hsl(var(--warning)); + color: hsl(var(--foreground)); +} + +.dev-timeline-dot.bg-muted { + background: hsl(var(--muted-foreground)); + color: white; +} + +.dev-timeline-dot i { + width: 0.875rem; + height: 0.875rem; +} + +.dev-timeline-line { + width: 2px; + flex: 1; + min-height: 1rem; + background: hsl(var(--border)); + margin: 0.25rem 0; +} + +.dev-timeline-content { + flex: 1; + background: hsl(var(--card)); + border: 1px solid hsl(var(--border)); + border-radius: 8px; + padding: 1rem; + margin-bottom: 0.75rem; + transition: all 0.2s ease; +} + +.dev-timeline-content:hover { + border-color: hsl(var(--primary) / 0.3); + box-shadow: 0 2px 8px hsl(var(--foreground) / 0.05); +} + +/* Hidden utility class */ +.hidden { + display: none !important; +} diff --git a/ccw/src/templates/dashboard-js/views/project-overview.js b/ccw/src/templates/dashboard-js/views/project-overview.js index ec7057f9..70b63499 100644 --- a/ccw/src/templates/dashboard-js/views/project-overview.js +++ b/ccw/src/templates/dashboard-js/views/project-overview.js @@ -180,16 +180,16 @@ function renderProjectOverview() {
-
${project.statistics.total_features || 0}
+
${calculateTotalFeatures(project.developmentIndex)}
Total Features
-
${project.statistics.total_sessions || 0}
+
${workflowData.statistics?.totalSessions || 0}
Total Sessions
Last Updated
-
${formatDate(project.statistics.last_updated)}
+
${formatDate(getLastUpdatedDate(project.developmentIndex))}
@@ -203,55 +203,171 @@ function renderDevelopmentIndex(devIndex) { if (!devIndex) return '

No development history available

'; const categories = [ - { key: 'feature', label: 'Features', icon: '', badgeClass: 'bg-primary-light text-primary' }, - { key: 'enhancement', label: 'Enhancements', icon: '', badgeClass: 'bg-success-light text-success' }, - { key: 'bugfix', label: 'Bug Fixes', icon: '', badgeClass: 'bg-destructive/10 text-destructive' }, - { key: 'refactor', label: 'Refactorings', icon: '', badgeClass: 'bg-warning-light text-warning' }, - { key: 'docs', label: 'Documentation', icon: '', badgeClass: 'bg-muted text-muted-foreground' } + { key: 'feature', label: 'Features', icon: '', badgeClass: 'bg-primary-light text-primary', color: 'primary' }, + { key: 'enhancement', label: 'Enhancements', icon: '', badgeClass: 'bg-success-light text-success', color: 'success' }, + { key: 'bugfix', label: 'Bug Fixes', icon: '', badgeClass: 'bg-destructive/10 text-destructive', color: 'destructive' }, + { key: 'refactor', label: 'Refactorings', icon: '', badgeClass: 'bg-warning-light text-warning', color: 'warning' }, + { key: 'docs', label: 'Documentation', icon: '', badgeClass: 'bg-muted text-muted-foreground', color: 'muted' } ]; - const totalEntries = categories.reduce((sum, cat) => sum + (devIndex[cat.key]?.length || 0), 0); + // Calculate totals from developmentIndex (dynamic calculation) + const totals = categories.reduce((acc, cat) => { + acc[cat.key] = (devIndex[cat.key] || []).length; + return acc; + }, {}); + const totalEntries = Object.values(totals).reduce((sum, count) => sum + count, 0); if (totalEntries === 0) { return '

No development history entries

'; } - return ` -
- ${categories.map(cat => { - const entries = devIndex[cat.key] || []; - if (entries.length === 0) return ''; + // Collect all entries with type info for timeline view + const allEntries = []; + categories.forEach(cat => { + (devIndex[cat.key] || []).forEach(entry => { + allEntries.push({ + ...entry, + type: cat.key, + typeLabel: cat.label, + typeIcon: cat.icon, + typeBadgeClass: cat.badgeClass, + typeColor: cat.color, + // Support both 'archivedAt' and 'date' field names + sortDate: entry.archivedAt || entry.date || entry.implemented_at || '' + }); + }); + }); - return ` -
-

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

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

${escapeHtml(entry.description)}

` : ''} -
- ${entry.sub_feature ? `${escapeHtml(entry.sub_feature)}` : ''} - ${entry.status ? `${escapeHtml(entry.status)}` : ''} -
+ // Sort by date descending (newest first) + allEntries.sort((a, b) => { + const dateA = new Date(a.sortDate || 0).getTime(); + const dateB = new Date(b.sortDate || 0).getTime(); + return dateB - dateA; + }); + + return ` + +
+
+ ${categories.map(cat => { + const count = totals[cat.key]; + if (count === 0) return ''; + return `${cat.icon} ${count}`; + }).join('')} +
+
+ + +
+
+ + +
+
+ ${categories.map(cat => { + const entries = devIndex[cat.key] || []; + if (entries.length === 0) return ''; + + return ` +
+

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

+
+ ${entries.slice(0, 5).map(entry => renderDevIndexEntry(entry, cat)).join('')} + ${entries.length > 5 ? `
... and ${entries.length - 5} more
` : ''} +
+
+ `; + }).join('')} +
+
+ + + `; } +// Helper function to render a single development index entry +function renderDevIndexEntry(entry, cat) { + // Support both 'archivedAt' and 'date' field names + const entryDate = entry.archivedAt || entry.date || entry.implemented_at || ''; + + return ` +
+
+
${escapeHtml(entry.title)}
+ ${formatDate(entryDate)} +
+ ${entry.description ? `

${escapeHtml(entry.description)}

` : ''} +
+ ${entry.sessionId ? `${escapeHtml(entry.sessionId)}` : ''} + ${entry.sub_feature ? `${escapeHtml(entry.sub_feature)}` : ''} + ${entry.status ? `${escapeHtml(entry.status)}` : ''} + ${entry.tags && entry.tags.length > 0 ? entry.tags.slice(0, 2).map(tag => `${escapeHtml(tag)}`).join('') : ''} +
+
+ `; +} + +// Toggle between category and timeline views +function toggleDevIndexView(view) { + const categoryView = document.getElementById('devIndexCategoryView'); + const timelineView = document.getElementById('devIndexTimelineView'); + const categoryBtn = document.getElementById('viewCategoryBtn'); + const timelineBtn = document.getElementById('viewTimelineBtn'); + + if (view === 'timeline') { + categoryView.classList.add('hidden'); + timelineView.classList.remove('hidden'); + categoryBtn.classList.remove('active'); + timelineBtn.classList.add('active'); + } else { + categoryView.classList.remove('hidden'); + timelineView.classList.add('hidden'); + categoryBtn.classList.add('active'); + timelineBtn.classList.remove('active'); + } + + // Reinitialize icons for the newly visible view + if (typeof lucide !== 'undefined') lucide.createIcons(); +} + function renderProjectGuidelines(guidelines) { if (!guidelines) { return ` @@ -401,3 +517,32 @@ function renderLearnings(learnings) {
`; } + +// Calculate total features from developmentIndex (dynamic calculation) +function calculateTotalFeatures(devIndex) { + if (!devIndex) return 0; + const categories = ['feature', 'enhancement', 'bugfix', 'refactor', 'docs']; + return categories.reduce((sum, cat) => sum + (devIndex[cat]?.length || 0), 0); +} + +// Get the most recent date from developmentIndex entries +function getLastUpdatedDate(devIndex) { + if (!devIndex) return null; + + const categories = ['feature', 'enhancement', 'bugfix', 'refactor', 'docs']; + let latestDate = null; + + categories.forEach(cat => { + (devIndex[cat] || []).forEach(entry => { + const entryDate = entry.archivedAt || entry.date || entry.implemented_at; + if (entryDate) { + const date = new Date(entryDate); + if (!latestDate || date > latestDate) { + latestDate = date; + } + } + }); + }); + + return latestDate ? latestDate.toISOString() : null; +} diff --git a/ccw/src/tools/session-manager.ts b/ccw/src/tools/session-manager.ts index 954fa7a3..fcbf6fb7 100644 --- a/ccw/src/tools/session-manager.ts +++ b/ccw/src/tools/session-manager.ts @@ -737,6 +737,11 @@ function executeArchive(params: Params): any { } } + // Update development index with archived session info + if (sessionMetadata) { + updateDevelopmentIndex(sessionMetadata); + } + return { operation: 'archive', session_id, @@ -903,6 +908,72 @@ function executeStats(params: Params): any { }; } +/** + * Updates the project's development index when a session is archived. + * Simplified: only appends entries, does NOT manage statistics. + * Dashboard aggregator handles dynamic calculation. + */ +function updateDevelopmentIndex(sessionMetadata: any): void { + if (!sessionMetadata || !sessionMetadata.session_id) { + console.warn('Skipping development index update due to missing session metadata.'); + return; + } + + const root = findWorkflowRoot(); + const projectTechFile = join(root, WORKFLOW_BASE, 'project-tech.json'); + + if (!existsSync(projectTechFile)) { + console.warn(`Skipping development index update: ${projectTechFile} not found.`); + return; + } + + try { + const projectData = readJsonFile(projectTechFile); + + // Ensure development_index exists + if (!projectData.development_index) { + projectData.development_index = { feature: [], enhancement: [], bugfix: [], refactor: [], docs: [] }; + } + + // Type inference from description + const description = (sessionMetadata.description || '').toLowerCase(); + let devType: 'feature' | 'enhancement' | 'bugfix' | 'refactor' | 'docs' = 'enhancement'; + + if (sessionMetadata.type === 'docs') { + devType = 'docs'; + } else if (/\b(fix|bug|resolve)\b/.test(description)) { + devType = 'bugfix'; + } else if (/\b(feature|implement|add|create)\b/.test(description)) { + devType = 'feature'; + } else if (/\b(refactor|restructure|cleanup)\b/.test(description)) { + devType = 'refactor'; + } + + const entry = { + title: sessionMetadata.description || sessionMetadata.project || sessionMetadata.session_id, + sessionId: sessionMetadata.session_id, + type: devType, + tags: sessionMetadata.tags || [], + archivedAt: sessionMetadata.archived_at || new Date().toISOString(), + }; + + // Append to correct category + if (!projectData.development_index[devType]) { + projectData.development_index[devType] = []; + } + projectData.development_index[devType].push(entry); + + // CRITICAL: Do NOT touch projectData.statistics + // Dashboard aggregator handles dynamic calculation + + writeJsonFile(projectTechFile, projectData); + console.log(`Development index updated for session: ${sessionMetadata.session_id}`); + + } catch (error) { + console.error(`Failed to update development index: ${(error as Error).message}`); + } +} + // ============================================================ // Main Execute Function // ============================================================