Files
Claude-Code-Workflow/ccw/src/templates/dashboard-js/components/navigation.js
catlog22 d9f1d14d5e feat: add CCW Loop System for automated iterative workflow execution
Implements a complete loop execution system with multi-loop parallel support,
dashboard monitoring, and comprehensive security validation.

Core features:
- Loop orchestration engine (loop-manager, loop-state-manager)
- Multi-loop parallel execution with independent state management
- REST API endpoints for loop control (pause, resume, stop, retry)
- WebSocket real-time status updates
- Dashboard Loop Monitor view with live updates
- Security: path traversal protection and sandboxed JavaScript evaluation

Test coverage:
- 42 comprehensive tests covering multi-loop, API, WebSocket, security
- Security validation for success_condition injection attacks
- Edge case handling and end-to-end workflow tests
2026-01-21 22:55:24 +08:00

421 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Navigation and Routing
// Manages navigation events, active state, content title updates, search, and path selector
// View lifecycle management
var currentViewDestroy = null;
// 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.dataset.path = path;
// Path text
const pathText = document.createElement('span');
pathText.className = 'path-text';
pathText.textContent = path;
pathText.addEventListener('click', () => selectPath(path));
item.appendChild(pathText);
// Delete button (only for non-current paths)
if (path !== projectPath) {
const deleteBtn = document.createElement('button');
deleteBtn.className = 'path-delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.title = 'Remove from recent';
deleteBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await removeRecentPathFromList(path);
});
item.appendChild(deleteBtn);
}
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();
});
}
// Cleanup function for view transitions
function cleanupPreviousView() {
// Call current view's destroy function if exists
if (currentViewDestroy) {
currentViewDestroy();
currentViewDestroy = null;
}
// Cleanup graph explorer
if (currentView === 'graph-explorer' && typeof window.cleanupGraphExplorer === 'function') {
window.cleanupGraphExplorer();
}
// Hide storage card when leaving cli-manager
var storageCard = document.getElementById('storageCard');
if (storageCard) {
storageCard.style.display = 'none';
}
}
// Navigation
function initNavigation() {
document.querySelectorAll('.nav-item[data-filter]').forEach(item => {
item.addEventListener('click', () => {
cleanupPreviousView();
setActiveNavItem(item);
currentFilter = item.dataset.filter;
currentLiteType = null;
currentView = 'sessions';
currentSessionDetailKey = null;
updateContentTitle();
showStatsAndSearch();
renderSessions();
});
});
// Lite Tasks Navigation
document.querySelectorAll('.nav-item[data-lite]').forEach(item => {
item.addEventListener('click', () => {
cleanupPreviousView();
setActiveNavItem(item);
currentLiteType = item.dataset.lite;
currentFilter = null;
currentView = 'liteTasks';
currentSessionDetailKey = null;
updateContentTitle();
showStatsAndSearch();
renderLiteTasks();
});
});
// View Navigation (Project Overview, MCP Manager, etc.)
document.querySelectorAll('.nav-item[data-view]').forEach(item => {
item.addEventListener('click', () => {
cleanupPreviousView();
setActiveNavItem(item);
currentView = item.dataset.view;
currentFilter = null;
currentLiteType = null;
currentSessionDetailKey = null;
updateContentTitle();
// Route to appropriate view
if (currentView === 'mcp-manager') {
renderMcpManager();
} else if (currentView === 'project-overview') {
renderProjectOverview();
} else if (currentView === 'explorer') {
renderExplorer();
} else if (currentView === 'cli-manager') {
renderCliManager();
} else if (currentView === 'cli-history') {
renderCliHistoryView();
// Register destroy function for cli-history view
if (typeof window.destroyCliHistoryView === 'function') {
currentViewDestroy = window.destroyCliHistoryView;
}
} else if (currentView === 'hook-manager') {
renderHookManager();
} else if (currentView === 'memory') {
renderMemoryView();
} else if (currentView === 'prompt-history') {
renderPromptHistoryView();
} else if (currentView === 'skills-manager') {
renderSkillsManager();
} else if (currentView === 'rules-manager') {
renderRulesManager();
} else if (currentView === 'claude-manager') {
renderClaudeManager();
// Register destroy function for claude-manager view
if (typeof window.initClaudeManager === 'function') {
window.initClaudeManager();
}
} else if (currentView === 'graph-explorer') {
renderGraphExplorer();
} else if (currentView === 'help') {
renderHelpView();
} else if (currentView === 'core-memory') {
if (typeof renderCoreMemoryView === 'function') {
renderCoreMemoryView();
} else {
console.error('renderCoreMemoryView not defined - please refresh the page');
}
} else if (currentView === 'codexlens-manager') {
if (typeof renderCodexLensManager === 'function') {
renderCodexLensManager();
} else {
console.error('renderCodexLensManager not defined - please refresh the page');
}
} else if (currentView === 'api-settings') {
if (typeof renderApiSettings === 'function') {
renderApiSettings();
} else {
console.error('renderApiSettings not defined - please refresh the page');
}
} else if (currentView === 'issue-manager') {
if (typeof renderIssueManager === 'function') {
renderIssueManager();
} else {
console.error('renderIssueManager not defined - please refresh the page');
}
} else if (currentView === 'issue-discovery') {
if (typeof renderIssueDiscovery === 'function') {
renderIssueDiscovery();
} else {
console.error('renderIssueDiscovery not defined - please refresh the page');
}
} else if (currentView === 'loop-monitor') {
if (typeof renderLoopMonitor === 'function') {
renderLoopMonitor();
// Register destroy function for cleanup
currentViewDestroy = window.destroyLoopMonitor;
} else {
console.error('renderLoopMonitor not defined - please refresh the page');
}
}
});
});
}
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 = t('title.projectOverview');
} else if (currentView === 'mcp-manager') {
titleEl.textContent = t('title.mcpManagement');
} else if (currentView === 'explorer') {
titleEl.textContent = t('title.fileExplorer');
} else if (currentView === 'cli-manager') {
titleEl.textContent = t('title.cliTools');
} else if (currentView === 'cli-history') {
titleEl.textContent = t('title.cliHistory');
} else if (currentView === 'hook-manager') {
titleEl.textContent = t('title.hookManager');
} else if (currentView === 'memory') {
titleEl.textContent = t('title.memoryModule');
} else if (currentView === 'prompt-history') {
titleEl.textContent = t('title.promptHistory');
} else if (currentView === 'skills-manager') {
titleEl.textContent = t('title.skillsManager');
} else if (currentView === 'rules-manager') {
titleEl.textContent = t('title.rulesManager');
} else if (currentView === 'claude-manager') {
titleEl.textContent = t('title.claudeManager');
} else if (currentView === 'graph-explorer') {
titleEl.textContent = t('title.graphExplorer');
} else if (currentView === 'help') {
titleEl.textContent = t('title.helpGuide');
} else if (currentView === 'core-memory') {
titleEl.textContent = t('title.coreMemory');
} else if (currentView === 'codexlens-manager') {
titleEl.textContent = t('title.codexLensManager');
} else if (currentView === 'api-settings') {
titleEl.textContent = t('title.apiSettings');
} else if (currentView === 'issue-manager') {
titleEl.textContent = t('title.issueManager');
} else if (currentView === 'issue-discovery') {
titleEl.textContent = t('title.issueDiscovery');
} else if (currentView === 'loop-monitor') {
titleEl.textContent = t('title.loopMonitor') || 'Loop Monitor';
} else if (currentView === 'liteTasks') {
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions'), 'multi-cli-plan': t('title.multiCliPlanSessions') || 'Multi-CLI Plan Sessions' };
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
} else if (currentView === 'multiCliDetail') {
titleEl.textContent = t('title.multiCliDetail') || 'Multi-CLI Discussion Detail';
} else if (currentView === 'sessionDetail') {
titleEl.textContent = t('title.sessionDetail');
} else if (currentView === 'liteTaskDetail') {
titleEl.textContent = t('title.liteTaskDetail');
} else {
const names = { 'all': t('title.allSessions'), 'active': t('title.activeSessions'), 'archived': t('title.archivedSessions') };
titleEl.textContent = names[currentFilter] || t('title.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) {
// Clear and repopulate stores
Object.keys(sessionDataStore).forEach(k => delete sessionDataStore[k]);
Object.keys(liteTaskDataStore).forEach(k => delete liteTaskDataStore[k]);
// Populate stores
[...(data.activeSessions || []), ...(data.archivedSessions || [])].forEach(s => {
const sessionKey = `session-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
sessionDataStore[sessionKey] = s;
});
[...(data.liteTasks?.litePlan || []), ...(data.liteTasks?.liteFix || [])].forEach(s => {
const sessionKey = `lite-${s.session_id}`.replace(/[^a-zA-Z0-9-]/g, '-');
liteTaskDataStore[sessionKey] = 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(t('toast.workspaceRefreshed'), 'success');
}
} else {
// Non-server mode: just reload page
window.location.reload();
}
} catch (error) {
console.error('Refresh failed:', error);
showRefreshToast(t('toast.refreshFailed', { error: 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 (using ID selectors to match dashboard.html structure)
const litePlanCount = document.getElementById('badgeLitePlan');
const liteFixCount = document.getElementById('badgeLiteFix');
const multiCliPlanCount = document.getElementById('badgeMultiCliPlan');
if (litePlanCount) litePlanCount.textContent = data.liteTasks?.litePlan?.length || 0;
if (liteFixCount) liteFixCount.textContent = data.liteTasks?.liteFix?.length || 0;
if (multiCliPlanCount) multiCliPlanCount.textContent = data.liteTasks?.multiCliPlan?.length || 0;
}
// ========== Navigation Badge Aggregation ==========
/**
* Update a single badge element by ID
* @param {string} badgeId - Element ID
* @param {number|undefined} count - Badge count value
*/
function updateBadgeById(badgeId, count) {
const badge = document.getElementById(badgeId);
if (badge && count !== undefined) {
badge.textContent = count;
}
}
/**
* Fetch and update all navigation badges at once
* Called on dashboard initialization and path switch
*/
async function updateAllNavigationBadges() {
if (!window.SERVER_MODE) return;
try {
const response = await fetch('/api/nav-status?path=' + encodeURIComponent(projectPath));
if (!response.ok) {
console.warn('[Nav Status] Failed to fetch:', response.status);
return;
}
const status = await response.json();
// Update each badge
updateBadgeById('badgeIssues', status.issues?.count);
updateBadgeById('badgeDiscovery', status.discoveries?.count);
updateBadgeById('badgeSkills', status.skills?.count);
updateBadgeById('badgeRules', status.rules?.count);
updateBadgeById('badgeClaude', status.claude?.count);
updateBadgeById('badgeHooks', status.hooks?.count);
console.log('[Nav Status] Badges updated:', status);
} catch (err) {
console.error('[Nav Status] Error fetching status:', err);
// Graceful degradation - badges will update when user visits each page
}
}
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);
// Increase display time to 3.5 seconds for better visibility
setTimeout(() => {
toast.classList.add('fade-out');
setTimeout(() => toast.remove(), 300);
}, 3500);
}