// ========================================== // STORAGE MANAGER COMPONENT // ========================================== // Manages CCW centralized storage (~/.ccw/) // State let storageData = null; let storageLoading = false; /** * Initialize storage manager */ async function initStorageManager() { await loadStorageStats(); } /** * Load storage statistics from API */ async function loadStorageStats() { if (storageLoading) return; storageLoading = true; try { const res = await fetch('/api/storage/stats'); if (!res.ok) throw new Error('Failed to load storage stats'); storageData = await res.json(); renderStorageCard(); } catch (err) { console.error('Failed to load storage stats:', err); renderStorageCardError(err.message); } finally { storageLoading = false; } } /** * Render storage card in the dashboard */ function renderStorageCard() { const container = document.getElementById('storageCard'); if (!container || !storageData) return; const { location, totalSizeFormatted, projectCount, projects } = storageData; // Format relative time const formatTimeAgo = (isoString) => { if (!isoString) return 'Never'; const date = new Date(isoString); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return diffMins + 'm ago'; if (diffHours < 24) return diffHours + 'h ago'; if (diffDays < 30) return diffDays + 'd ago'; return date.toLocaleDateString(); }; // Build project tree (hierarchical view) let projectRows = ''; if (projects && projects.length > 0) { const tree = buildProjectTree(projects); projectRows = renderProjectTree(tree, 0, formatTimeAgo); // Initially hide all child rows (level > 0) setTimeout(() => { const allRows = document.querySelectorAll('.project-row'); allRows.forEach(row => { const level = parseInt(row.getAttribute('data-level')); if (level > 0) { row.style.display = 'none'; } }); }, 0); } else { projectRows = '\ \ No storage data yet\ \ '; } container.innerHTML = '\
\
\
\ \ Storage Manager\ ' + totalSizeFormatted + '\
\
\ \ \
\
\
\
\ \ ' + escapeHtml(location) + '\
\
\
\
' + projectCount + '
\
Projects
\
\
\
' + totalSizeFormatted + '
\
Total Size
\
\
\
' + getTotalRecords() + '
\
Records
\
\
\
\ \ \ \ \ \ \ \ \ \ \ \ ' + projectRows + '\ \
Project IDSizeHistoryLast Used
\
\
\ \
\
\
\ '; // Reinitialize Lucide icons if (typeof lucide !== 'undefined') { lucide.createIcons(); } } /** * Get total records across all projects */ function getTotalRecords() { if (!storageData || !storageData.projects) return 0; return storageData.projects.reduce((sum, p) => sum + (p.historyRecords || 0), 0); } /** * Build project tree from flat list * Converts flat project list to hierarchical tree structure */ function buildProjectTree(projects) { const tree = []; const map = new Map(); // Sort by path depth (shallowest first) const sorted = projects.slice().sort((a, b) => { const depthA = (a.id.match(/\//g) || []).length; const depthB = (b.id.match(/\//g) || []).length; return depthA - depthB; }); for (const project of sorted) { const segments = project.id.split('/'); if (segments.length === 1) { // Root level project const node = { ...project, children: [], isExpanded: false }; tree.push(node); map.set(project.id, node); } else { // Sub-project const parentId = segments.slice(0, -1).join('/'); const parent = map.get(parentId); if (parent) { const node = { ...project, children: [], isExpanded: false }; parent.children.push(node); map.set(project.id, node); } else { // Orphaned project (parent not found) - add to root const node = { ...project, children: [], isExpanded: false }; tree.push(node); map.set(project.id, node); } } } return tree; } /** * Render project tree recursively */ function renderProjectTree(tree, level = 0, formatTimeAgo) { if (!tree || tree.length === 0) return ''; let html = ''; for (const node of tree) { const hasChildren = node.children && node.children.length > 0; const indent = level * 20; const projectName = node.id.split('/').pop(); const historyBadge = node.historyRecords > 0 ? '' + node.historyRecords + '' : '-'; const toggleIcon = hasChildren ? '' : ''; html += '\ \ \
\ ' + (hasChildren ? '' : '') + '\ ' + escapeHtml(projectName) + '\
\ \ ' + escapeHtml(node.totalSizeFormatted) + '\ ' + historyBadge + '\ ' + formatTimeAgo(node.lastModified) + '\ \ \ \ \ '; // Render children (initially hidden) if (hasChildren) { html += renderProjectTree(node.children, level + 1, formatTimeAgo); } } return html; } /** * Toggle project node expansion */ function toggleProjectNode(projectId) { const row = document.querySelector('[data-project-id="' + projectId + '"]'); if (!row) return; const icon = row.querySelector('.toggle-icon'); const level = parseInt(row.getAttribute('data-level')); // Find all child rows let nextRow = row.nextElementSibling; const childRows = []; while (nextRow && nextRow.classList.contains('project-row')) { const nextLevel = parseInt(nextRow.getAttribute('data-level')); if (nextLevel <= level) break; childRows.push(nextRow); nextRow = nextRow.nextElementSibling; } // Toggle visibility const isExpanded = row.classList.contains('expanded'); if (isExpanded) { // Collapse row.classList.remove('expanded'); if (icon) icon.style.transform = 'rotate(0deg)'; childRows.forEach(child => { child.style.display = 'none'; }); } else { // Expand (only immediate children) row.classList.add('expanded'); if (icon) icon.style.transform = 'rotate(90deg)'; childRows.forEach(child => { const childLevel = parseInt(child.getAttribute('data-level')); if (childLevel === level + 1) { child.style.display = ''; } }); } // Reinitialize Lucide icons if (typeof lucide !== 'undefined') { lucide.createIcons(); } } /** * Render error state for storage card */ function renderStorageCardError(message) { const container = document.getElementById('storageCard'); if (!container) return; container.innerHTML = '\
\
\ \ Storage Manager\
\
\
\ \
\

' + escapeHtml(message) + '

\ \
\
\ '; if (typeof lucide !== 'undefined') { lucide.createIcons(); } } /** * Show storage configuration modal */ function showStorageConfig() { const content = '\ # Storage Configuration\n\ \n\ ## Current Location\n\ \n\ ```\n\ ' + (storageData?.location || '~/.ccw') + '\n\ ```\n\ \n\ ## Change Storage Location\n\ \n\ Set the `CCW_DATA_DIR` environment variable to change the storage location:\n\ \n\ ### Windows (PowerShell)\n\ ```powershell\n\ $env:CCW_DATA_DIR = "D:\\custom\\ccw-data"\n\ ```\n\ \n\ ### Windows (Command Prompt)\n\ ```cmd\n\ set CCW_DATA_DIR=D:\\custom\\ccw-data\n\ ```\n\ \n\ ### Linux/macOS\n\ ```bash\n\ export CCW_DATA_DIR="/custom/ccw-data"\n\ ```\n\ \n\ ### Permanent (add to shell profile)\n\ ```bash\n\ echo \'export CCW_DATA_DIR="/custom/ccw-data"\' >> ~/.bashrc\n\ ```\n\ \n\ > **Note:** Existing data will NOT be migrated automatically.\n\ > Manually copy the contents of the old directory to the new location.\n\ \n\ ## CLI Commands\n\ \n\ ```bash\n\ # Show storage info\n\ ccw cli storage\n\ \n\ # Clean all storage\n\ ccw cli storage clean --force\n\ \n\ # Clean specific project\n\ ccw cli storage clean --project . --force\n\ ```\n\ '; openMarkdownModal('Storage Configuration', content, 'markdown'); } /** * Clean storage for a specific project */ async function cleanProjectStorage(projectId) { const project = storageData?.projects?.find(p => p.id === projectId); const sizeInfo = project ? ' (' + project.totalSizeFormatted + ')' : ''; if (!confirm('Delete storage for project ' + projectId.substring(0, 8) + '...' + sizeInfo + '?\n\nThis will remove CLI history, memory, and cache for this project.')) { return; } try { const res = await csrfFetch('/api/storage/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId }) }); const result = await res.json(); if (result.success) { addGlobalNotification('success', 'Storage Cleaned', 'Freed ' + result.freedFormatted, 'storage'); await loadStorageStats(); } else { throw new Error(result.error || 'Failed to clean storage'); } } catch (err) { addGlobalNotification('error', 'Clean Failed', err.message, 'storage'); } } /** * Confirm and clean all storage */ async function cleanAllStorageConfirm() { const totalSize = storageData?.totalSizeFormatted || 'unknown'; const projectCount = storageData?.projectCount || 0; if (!confirm('Delete ALL CCW storage?\n\nThis will remove:\n- ' + projectCount + ' projects\n- ' + totalSize + ' of data\n\nThis action cannot be undone!')) { return; } // Double confirm for safety if (!confirm('Are you SURE? This will delete all CLI history, memory stores, and caches.')) { return; } try { const res = await csrfFetch('/api/storage/clean', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ all: true }) }); const result = await res.json(); if (result.success) { addGlobalNotification('success', 'All Storage Cleaned', 'Cleaned ' + result.projectsCleaned + ' projects, freed ' + result.freedFormatted, 'storage'); await loadStorageStats(); } else { throw new Error(result.error || 'Failed to clean storage'); } } catch (err) { addGlobalNotification('error', 'Clean Failed', err.message, 'storage'); } } /** * Get storage data (for external use) */ function getStorageData() { return storageData; }