/** * Sessions Landing Page JavaScript */ // Load sessions on page load document.addEventListener('DOMContentLoaded', () => { checkAuth(); initializeProjectInput(); loadSessions(); }); // Check authentication async function checkAuth() { try { const res = await fetch('/claude/api/auth/status'); if (!res.ok) { throw new Error('Request failed'); } const data = await res.json(); if (!data.authenticated) { // Redirect to login if not authenticated window.location.href = '/claude/login.html'; } } catch (error) { console.error('Auth check failed:', error); } } /** * Initialize project input field in hero section */ function initializeProjectInput() { const input = document.getElementById('project-input'); const status = document.getElementById('input-status'); if (!input) return; // Auto-focus on page load input.focus(); input.addEventListener('input', () => { const projectName = input.value.trim(); const hasInvalidChars = !validateProjectName(projectName); if (hasInvalidChars && projectName.length > 0) { status.textContent = 'Invalid characters'; status.classList.add('error'); } else { status.textContent = ''; status.classList.remove('error'); } }); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const projectName = input.value.trim(); if (projectName && validateProjectName(projectName)) { createProject(projectName); } } }); } /** * Create a new project and navigate to IDE */ async function createProject(projectName) { try { showLoadingOverlay('Creating project...'); const res = await fetch('/claude/api/claude/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ metadata: { type: 'chat', source: 'web-ide', project: projectName } }) }); if (!res.ok) throw new Error('Request failed'); const data = await res.json(); if (data.success) { // Minimum display time for smooth UX await new Promise(resolve => setTimeout(resolve, 300)); window.location.href = `/claude/ide?session=${data.session.id}`; } } catch (error) { console.error('Error creating project:', error); hideLoadingOverlay(); showToast('Failed to create project', 'error'); } } /** * Load all sessions and render in table */ async function loadSessions() { const tbody = document.getElementById('projects-tbody'); const emptyState = document.getElementById('projects-empty'); const table = document.getElementById('projects-table'); if (!tbody) return; tbody.innerHTML = 'Loading...'; try { const res = await fetch('/claude/api/claude/sessions'); if (!res.ok) throw new Error('Request failed'); const data = await res.json(); const allSessions = [ ...(data.active || []).map(s => ({...s, type: 'active'})), ...(data.historical || []).map(s => ({...s, type: 'historical'})) ]; renderProjectsTable(allSessions); } catch (error) { console.error('Error loading sessions:', error); tbody.innerHTML = 'Failed to load'; } } /** * Render projects table with session data */ function renderProjectsTable(sessions) { const tbody = document.getElementById('projects-tbody'); const emptyState = document.getElementById('projects-empty'); const table = document.getElementById('projects-table'); if (!tbody) return; // Sort by last activity (newest first) sessions.sort((a, b) => { const dateA = new Date(a.lastActivity || a.createdAt || a.created_at); const dateB = new Date(b.lastActivity || b.createdAt || b.created_at); return dateB - dateA; }); tbody.innerHTML = ''; if (sessions.length === 0) { if (table) table.style.display = 'none'; if (emptyState) emptyState.style.display = 'block'; return; } if (table) table.style.display = 'table'; if (emptyState) emptyState.style.display = 'none'; sessions.forEach(session => { const row = createProjectRow(session); tbody.appendChild(row); }); } /** * Create a single project row for the table */ function createProjectRow(session) { const tr = document.createElement('tr'); tr.dataset.sessionId = session.id; const projectName = getProjectName(session); const relativeTime = getRelativeTime(session); const status = getStatus(session); const statusClass = getStatusClass(session); tr.innerHTML = `

${escapeHtml(projectName)}

${relativeTime} ${status} `; tr.addEventListener('click', (e) => { if (!e.target.closest('.btn-menu')) { continueToSession(session.id); } }); tr.querySelector('.btn-continue').addEventListener('click', (e) => { e.stopPropagation(); continueToSession(session.id); }); tr.querySelector('.btn-menu').addEventListener('click', (e) => { e.stopPropagation(); showProjectMenu(e, session); }); return tr; } /** * Extract project name from session metadata */ function getProjectName(session) { return session.metadata?.project || session.metadata?.projectName || session.workingDir?.split('/').pop() || 'Session ' + session.id.substring(0, 8); } /** * Get relative time string for session */ function getRelativeTime(session) { const date = new Date(session.lastActivity || session.createdAt || session.created_at); const now = new Date(); const diffMins = Math.floor((now - date) / 60000); const diffHours = Math.floor((now - date) / 3600000); const diffDays = Math.floor((now - date) / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } /** * Get status text for session */ function getStatus(session) { if (session.status === 'running') return 'Active'; return 'Done'; } /** * Get status CSS class for session */ function getStatusClass(session) { if (session.status === 'running') return 'active'; return 'done'; } /** * Navigate to IDE with session */ async function continueToSession(sessionId) { showLoadingOverlay('Opening workspace...'); await new Promise(resolve => setTimeout(resolve, 300)); window.location.href = `/claude/ide?session=${sessionId}`; } /** * Show project menu dropdown */ function showProjectMenu(event, session) { closeProjectMenu(); const menu = document.createElement('div'); menu.className = 'dropdown-menu'; menu.id = 'project-menu'; menu.innerHTML = ` `; const rect = event.target.getBoundingClientRect(); menu.style.position = 'fixed'; menu.style.top = `${rect.bottom + 8}px`; menu.style.right = `${window.innerWidth - rect.right}px`; document.body.appendChild(menu); menu.querySelector('[data-action="rename"]').addEventListener('click', () => { closeProjectMenu(); renameProject(session); }); menu.querySelector('[data-action="duplicate"]').addEventListener('click', async () => { closeProjectMenu(); await duplicateProject(session); loadSessions(); }); menu.querySelector('[data-action="delete"]').addEventListener('click', async () => { closeProjectMenu(); await deleteProject(session); loadSessions(); }); setTimeout(() => { document.addEventListener('click', closeProjectMenu, { once: true }); }, 10); } /** * Close project menu dropdown */ function closeProjectMenu() { document.getElementById('project_menu')?.remove(); } /** * Rename project (inline edit) */ function renameProject(session) { const row = document.querySelector(`tr[data-session-id="${session.id}"]`); if (!row) return; const nameCell = row.querySelector('.project-name'); if (!nameCell) return; const currentName = nameCell.textContent; // Only block truly historical sessions (loaded from disk, not in memory) // Active and terminated sessions in memory can be renamed if (session.type === 'historical') { showToast('Cannot rename historical sessions', 'warning'); return; } // Create input element const input = document.createElement('input'); input.type = 'text'; input.className = 'project-name-input'; input.value = currentName; input.maxLength = 50; // Replace text with input nameCell.innerHTML = ''; nameCell.appendChild(input); input.focus(); input.select(); // Handle save const saveRename = async () => { const newName = input.value.trim(); // Validation if (!newName) { nameCell.textContent = currentName; showToast('Project name cannot be empty', 'error'); return; } if (!validateProjectName(newName)) { nameCell.textContent = currentName; showToast('Project name contains invalid characters', 'error'); return; } if (newName === currentName) { nameCell.textContent = currentName; return; } // Show saving state input.disabled = true; input.style.opacity = '0.6'; try { const res = await fetch(`/claude/api/claude/sessions/${session.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ metadata: { ...session.metadata, project: newName } }) }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || 'Failed to rename'); } const data = await res.json(); if (data.success) { nameCell.textContent = newName; showToast('Project renamed successfully', 'success'); } else { throw new Error(data.error || 'Failed to rename'); } } catch (error) { console.error('Error renaming project:', error); nameCell.textContent = currentName; showToast(error.message || 'Failed to rename project', 'error'); } }; // Handle cancel const cancelRename = () => { nameCell.textContent = currentName; }; // Event listeners input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { input.blur(); // Trigger save on blur } else if (e.key === 'Escape') { cancelRename(); } }); input.addEventListener('blur', () => { // Small delay to allow Enter key to process first setTimeout(() => { if (document.activeElement !== input) { saveRename(); } }, 100); }); } /** * Duplicate project/session */ async function duplicateProject(session) { try { showLoadingOverlay('Duplicating project...'); const res = await fetch(`/claude/api/claude/sessions/${session.id}/duplicate`, { method: 'POST' }); if (!res.ok) throw new Error('Request failed'); const data = await res.json(); if (data.success) { hideLoadingOverlay(); showToast('Project duplicated successfully', 'success'); } else { throw new Error(data.error || 'Failed to duplicate'); } } catch (error) { console.error('Error duplicating project:', error); hideLoadingOverlay(); showToast('Failed to duplicate project', 'error'); } } /** * Delete project/session with confirmation */ async function deleteProject(session) { const projectName = getProjectName(session); if (!confirm(`Are you sure you want to delete "${projectName}"? This action cannot be undone.`)) { return; } try { showLoadingOverlay('Deleting project...'); const res = await fetch(`/claude/api/claude/sessions/${session.id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Request failed'); const data = await res.json(); if (data.success) { hideLoadingOverlay(); showToast('Project deleted successfully', 'success'); } else { throw new Error(data.error || 'Failed to delete'); } } catch (error) { console.error('Error deleting project:', error); hideLoadingOverlay(); showToast('Failed to delete project', 'error'); } } // Refresh sessions function refreshSessions() { loadSessions(); } /** * Validate project name for invalid characters * @param {string} name - The project name to validate * @returns {boolean} - True if valid, false if contains invalid characters */ function validateProjectName(name) { const invalidChars = /[\/\\<>:"|?*]/; return !invalidChars.test(name); } /** * Show loading overlay * @param {string} message - Optional custom message (default: "Loading...") */ function showLoadingOverlay(message = 'Loading...') { let overlay = document.getElementById('loading-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'loading-overlay'; overlay.className = 'loading-overlay'; overlay.innerHTML = `

${escapeHtml(message)}

`; document.body.appendChild(overlay); } else { // Update message if provided const textElement = overlay.querySelector('.loading-text'); if (textElement) { textElement.textContent = message; } } overlay.classList.remove('hidden'); setTimeout(() => { overlay.classList.add('visible'); }, 10); } /** * Hide loading overlay */ function hideLoadingOverlay() { const overlay = document.getElementById('loading-overlay'); if (overlay) { overlay.classList.remove('visible'); setTimeout(() => { overlay.classList.add('hidden'); }, 300); } } /** * Show toast notification * @param {string} message - The message to display * @param {string} type - The type of toast: 'success', 'error', 'info' * @param {number} duration - Duration in milliseconds (default: 3000) */ function showToast(message, type = 'info', duration = 3000) { // Remove existing toasts const existingToasts = document.querySelectorAll('.toast-notification'); existingToasts.forEach(toast => toast.remove()); // Create toast element const toast = document.createElement('div'); toast.className = `toast-notification toast-${type}`; toast.innerHTML = ` ${getToastIcon(type)} ${escapeHtml(message)} `; document.body.appendChild(toast); // Trigger animation setTimeout(() => { toast.classList.add('visible'); }, 10); // Auto remove after duration setTimeout(() => { toast.classList.remove('visible'); setTimeout(() => { toast.remove(); }, 300); }, duration); } /** * Get toast icon based on type */ function getToastIcon(type) { const icons = { success: '✓', error: '✕', info: 'ℹ', warning: '⚠' }; return icons[type] || icons.info; } /** * Escape HTML to prevent XSS */ function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }