/**
* 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;
}