feat: group sessions by project on landing page
- Add loadSessionsAndProjects() to fetch sessions and projects in parallel - Store projects in window.projectsMap for quick lookup - Group sessions by projectId, separating assigned and unassigned - Render collapsible project sections with icon, name, and session count - Add toggleProjectSection() to collapse/expand sections (▼/▶) - Display project badges on sessions when assigned to a project - Unassigned sessions shown in separate section at bottom Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
checkAuth();
|
||||
initializeProjectInput();
|
||||
loadSessions();
|
||||
loadSessionsAndProjects();
|
||||
});
|
||||
|
||||
// Check authentication
|
||||
@@ -100,9 +100,9 @@ async function createProject(projectName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all sessions and render in table
|
||||
* Load all sessions and projects, then render grouped by project
|
||||
*/
|
||||
async function loadSessions() {
|
||||
async function loadSessionsAndProjects() {
|
||||
const tbody = document.getElementById('projects-tbody');
|
||||
const emptyState = document.getElementById('projects-empty');
|
||||
const table = document.getElementById('projects-table');
|
||||
@@ -112,42 +112,54 @@ async function loadSessions() {
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; padding: 40px;">Loading...</td></tr>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/claude/api/claude/sessions');
|
||||
// Fetch both sessions and projects in parallel
|
||||
const [sessionsRes, projectsRes] = await Promise.all([
|
||||
fetch('/claude/api/claude/sessions'),
|
||||
fetch('/api/projects')
|
||||
]);
|
||||
|
||||
if (!res.ok) throw new Error('Request failed');
|
||||
if (!sessionsRes.ok) throw new Error('Failed to load sessions');
|
||||
if (!projectsRes.ok) throw new Error('Failed to load projects');
|
||||
|
||||
const data = await res.json();
|
||||
const sessionsData = await sessionsRes.json();
|
||||
const projectsData = await projectsRes.json();
|
||||
|
||||
// Combine active and historical sessions
|
||||
const allSessions = [
|
||||
...(data.active || []).map(s => ({...s, type: 'active'})),
|
||||
...(data.historical || []).map(s => ({...s, type: 'historical'}))
|
||||
...(sessionsData.active || []).map(s => ({...s, type: 'active'})),
|
||||
...(sessionsData.historical || []).map(s => ({...s, type: 'historical'}))
|
||||
];
|
||||
|
||||
renderProjectsTable(allSessions);
|
||||
// Store projects in global map for quick lookup
|
||||
window.projectsMap = new Map(
|
||||
(projectsData.projects || []).map(p => [p.id, p])
|
||||
);
|
||||
|
||||
renderSessionsGroupedByProject(allSessions);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
console.error('Error loading sessions and projects:', error);
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #ff6b6b;">Failed to load</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render projects table with session data
|
||||
* Load all sessions and render in table (legacy function for backward compatibility)
|
||||
*/
|
||||
function renderProjectsTable(sessions) {
|
||||
async function loadSessions() {
|
||||
await loadSessionsAndProjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render sessions grouped by project
|
||||
*/
|
||||
function renderSessionsGroupedByProject(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) {
|
||||
@@ -159,17 +171,148 @@ function renderProjectsTable(sessions) {
|
||||
if (table) table.style.display = 'table';
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
|
||||
// Group sessions by projectId
|
||||
const grouped = {
|
||||
byProject: new Map(), // projectId -> {project, sessions: []}
|
||||
unassigned: [] // sessions without projectId
|
||||
};
|
||||
|
||||
sessions.forEach(session => {
|
||||
const row = createProjectRow(session);
|
||||
tbody.appendChild(row);
|
||||
const projectId = session.metadata?.projectId;
|
||||
|
||||
if (projectId && window.projectsMap?.has(projectId)) {
|
||||
const project = window.projectsMap.get(projectId);
|
||||
|
||||
if (!grouped.byProject.has(projectId)) {
|
||||
grouped.byProject.set(projectId, {
|
||||
project: project,
|
||||
sessions: []
|
||||
});
|
||||
}
|
||||
|
||||
grouped.byProject.get(projectId).sessions.push(session);
|
||||
} else {
|
||||
grouped.unassigned.push(session);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort sessions within each group by last activity
|
||||
grouped.byProject.forEach(group => {
|
||||
group.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;
|
||||
});
|
||||
});
|
||||
|
||||
grouped.unassigned.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;
|
||||
});
|
||||
|
||||
// Render project sections
|
||||
grouped.byProject.forEach((group, projectId) => {
|
||||
const section = createProjectSection(group.project, group.sessions);
|
||||
tbody.appendChild(section);
|
||||
});
|
||||
|
||||
// Render unassigned sessions if any
|
||||
if (grouped.unassigned.length > 0) {
|
||||
const section = createUnassignedSection(grouped.unassigned);
|
||||
tbody.appendChild(section);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single project row for the table
|
||||
* Render projects table with session data (legacy function for backward compatibility)
|
||||
*/
|
||||
function createProjectRow(session) {
|
||||
function renderProjectsTable(sessions) {
|
||||
renderSessionsGroupedByProject(sessions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a collapsible project section
|
||||
*/
|
||||
function createProjectSection(project, sessions) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'project-section';
|
||||
container.dataset.projectId = project.id;
|
||||
|
||||
// Create section header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'project-section-header';
|
||||
header.onclick = () => toggleProjectSection(project.id);
|
||||
|
||||
const icon = project.icon || '📁';
|
||||
const sessionCount = sessions.length;
|
||||
|
||||
header.innerHTML = `
|
||||
<span class="project-section-toggle">▼</span>
|
||||
<span class="project-section-icon">${escapeHtml(icon)}</span>
|
||||
<span class="project-section-name">${escapeHtml(project.name)}</span>
|
||||
<span class="project-section-count">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</span>
|
||||
`;
|
||||
|
||||
// Create sessions container
|
||||
const sessionsContainer = document.createElement('div');
|
||||
sessionsContainer.className = 'project-section-sessions';
|
||||
|
||||
// Render sessions within this project
|
||||
sessions.forEach(session => {
|
||||
const row = createSessionRow(session);
|
||||
sessionsContainer.appendChild(row);
|
||||
});
|
||||
|
||||
container.appendChild(header);
|
||||
container.appendChild(sessionsContainer);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create unassigned sessions section
|
||||
*/
|
||||
function createUnassignedSection(sessions) {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'project-section unassigned-section';
|
||||
|
||||
// Create section header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'project-section-header';
|
||||
header.onclick = () => toggleProjectSection('unassigned');
|
||||
|
||||
const sessionCount = sessions.length;
|
||||
|
||||
header.innerHTML = `
|
||||
<span class="project-section-toggle">▼</span>
|
||||
<span class="project-section-icon">📄</span>
|
||||
<span class="project-section-name">Unassigned Sessions</span>
|
||||
<span class="project-section-count">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</span>
|
||||
`;
|
||||
|
||||
// Create sessions container
|
||||
const sessionsContainer = document.createElement('div');
|
||||
sessionsContainer.className = 'project-section-sessions';
|
||||
|
||||
// Render unassigned sessions
|
||||
sessions.forEach(session => {
|
||||
const row = createSessionRow(session);
|
||||
sessionsContainer.appendChild(row);
|
||||
});
|
||||
|
||||
container.appendChild(header);
|
||||
container.appendChild(sessionsContainer);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single session row
|
||||
*/
|
||||
function createSessionRow(session) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'session-row';
|
||||
tr.dataset.sessionId = session.id;
|
||||
|
||||
const projectName = getProjectName(session);
|
||||
@@ -177,9 +320,16 @@ function createProjectRow(session) {
|
||||
const status = getStatus(session);
|
||||
const statusClass = getStatusClass(session);
|
||||
|
||||
// Check if session has a project assigned
|
||||
const projectId = session.metadata?.projectId;
|
||||
const hasProject = projectId && window.projectsMap?.has(projectId);
|
||||
const projectBadge = hasProject ?
|
||||
`<span class="session-project-badge">${escapeHtml(window.projectsMap.get(projectId).name)}</span>` : '';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td data-label="Project">
|
||||
<h3 class="project-name">${escapeHtml(projectName)}</h3>
|
||||
${projectBadge}
|
||||
</td>
|
||||
<td data-label="Last Activity">
|
||||
<span class="last-activity">${relativeTime}</span>
|
||||
@@ -212,6 +362,39 @@ function createProjectRow(session) {
|
||||
return tr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle project section collapse/expand
|
||||
*/
|
||||
function toggleProjectSection(projectId) {
|
||||
const section = projectId === 'unassigned' ?
|
||||
document.querySelector('.unassigned-section') :
|
||||
document.querySelector(`.project-section[data-project-id="${projectId}"]`);
|
||||
|
||||
if (!section) return;
|
||||
|
||||
const toggle = section.querySelector('.project-section-toggle');
|
||||
const sessionsContainer = section.querySelector('.project-section-sessions');
|
||||
|
||||
if (section.classList.contains('collapsed')) {
|
||||
// Expand
|
||||
section.classList.remove('collapsed');
|
||||
toggle.textContent = '▼';
|
||||
sessionsContainer.style.display = '';
|
||||
} else {
|
||||
// Collapse
|
||||
section.classList.add('collapsed');
|
||||
toggle.textContent = '▶';
|
||||
sessionsContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single project row for the table (legacy function for backward compatibility)
|
||||
*/
|
||||
function createProjectRow(session) {
|
||||
return createSessionRow(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract project name from session metadata
|
||||
*/
|
||||
@@ -295,13 +478,13 @@ function showProjectMenu(event, session) {
|
||||
menu.querySelector('[data-action="duplicate"]').addEventListener('click', async () => {
|
||||
closeProjectMenu();
|
||||
await duplicateProject(session);
|
||||
loadSessions();
|
||||
loadSessionsAndProjects();
|
||||
});
|
||||
|
||||
menu.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||
closeProjectMenu();
|
||||
await deleteProject(session);
|
||||
loadSessions();
|
||||
loadSessionsAndProjects();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -494,7 +677,7 @@ async function deleteProject(session) {
|
||||
|
||||
// Refresh sessions
|
||||
function refreshSessions() {
|
||||
loadSessions();
|
||||
loadSessionsAndProjects();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user