Files
SuperCharged-Claude-Code-Up…/docs/plans/2025-01-19-project-session-organization.md
uroma 1fbc565a66 feat: add session move endpoint and project-session cascading delete
- Add sessions table to database with projectId and deletedAt columns
- Create POST /api/sessions/:id/move endpoint to reassign sessions
- Update DELETE /api/projects/:id to cascade soft-delete to sessions
- Support moving sessions between projects or to unassigned state
- Handle both active (in-memory) and historical sessions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 16:55:15 +00:00

2233 lines
57 KiB
Markdown
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.
# Project and Session Organization Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Implement persistent projects as first-class entities containing multiple sessions, with intelligent auto-assignment, manual reassignment via context menu, and soft-delete recycle bin.
**Architecture:** Introduce a `projects` collection in MongoDB that stores project metadata (name, description, icon, color, path, session IDs). Sessions reference projects via `projectId`. Soft delete implemented with `deletedAt` timestamp. API endpoints provide CRUD operations, smart suggestions based on directory/recency/name matching. Frontend adds dedicated projects page, context menu for session reassignment, and recycle bin interface.
**Tech Stack:** MongoDB (database), Express.js (API), Vanilla JavaScript (frontend), CSS (styling), xterm.js (existing terminal)
---
## Task 1: Database Setup - Create Projects Collection
**Files:**
- Modify: `server.js:39-45` (after claudeService initialization)
**Step 1: Add projects collection initialization**
Find the section where collections are created (around line 39-45). Add projects collection initialization.
```javascript
// Initialize collections
const db = client.db();
const sessionsCollection = db.collection('sessions');
const projectsCollection = db.collection('projects'); // ADD THIS LINE
// Create indexes for better query performance
await projectsCollection.createIndex({ deletedAt: 1 });
await projectsCollection.createIndex({ name: 1 });
```
**Step 2: Commit**
```bash
git add server.js
git commit -m "feat: add projects collection initialization and indexes"
```
---
## Task 2: Create Project API Endpoints
**Files:**
- Modify: `server.js` (add after sessions routes, around line 900)
**Step 1: Add GET /api/projects endpoint**
```javascript
// GET /api/projects - List all active projects
app.get('/api/projects', requireAuth, async (req, res) => {
try {
const projects = await projectsCollection.find({
deletedAt: null
}).sort({ lastActivity: -1 }).toArray();
res.json({
success: true,
projects: projects.map(p => ({
id: p._id,
name: p.name,
description: p.description,
icon: p.icon,
color: p.color,
path: p.path,
sessionCount: p.sessionIds?.length || 0,
createdAt: p.createdAt,
lastActivity: p.lastActivity
}))
});
} catch (error) {
console.error('Error fetching projects:', error);
res.status(500).json({ error: 'Failed to fetch projects' });
}
});
```
**Step 2: Add POST /api/projects endpoint**
```javascript
// POST /api/projects - Create new project
app.post('/api/projects', requireAuth, async (req, res) => {
try {
const { name, path, description, icon, color } = req.body;
// Validate required fields
if (!name || !path) {
return res.status(400).json({ error: 'Name and path are required' });
}
// Check if project with same name already exists
const existing = await projectsCollection.findOne({
name,
deletedAt: null
});
if (existing) {
return res.status(409).json({ error: 'Project with this name already exists' });
}
const project = {
name,
path,
description: description || '',
icon: icon || '📁',
color: color || '#4a9eff',
sessionIds: [],
createdAt: new Date(),
lastActivity: new Date(),
deletedAt: null
};
const result = await projectsCollection.insertOne(project);
res.status(201).json({
success: true,
project: {
id: result.insertedId,
...project
}
});
} catch (error) {
console.error('Error creating project:', error);
res.status(500).json({ error: 'Failed to create project' });
}
});
```
**Step 3: Add PUT /api/projects/:id endpoint**
```javascript
// PUT /api/projects/:id - Update project
app.put('/api/projects/:id', requireAuth, async (req, res) => {
try {
const { id } = req.params;
const { name, description, icon, color, path } = req.body;
const update = {};
if (name) update.name = name;
if (description !== undefined) update.description = description;
if (icon) update.icon = icon;
if (color) update.color = color;
if (path) update.path = path;
const result = await projectsCollection.findOneAndUpdate(
{ _id: new ObjectId(id), deletedAt: null },
{ $set: update },
{ returnDocument: 'after' }
);
if (!result) {
return res.status(404).json({ error: 'Project not found' });
}
res.json({
success: true,
project: {
id: result._id,
name: result.name,
description: result.description,
icon: result.icon,
color: result.color,
path: result.path,
sessionCount: result.sessionIds?.length || 0,
createdAt: result.createdAt,
lastActivity: result.lastActivity
}
});
} catch (error) {
console.error('Error updating project:', error);
res.status(500).json({ error: 'Failed to update project' });
}
});
```
**Step 4: Commit**
```bash
git add server.js
git commit -m "feat: add project CRUD API endpoints"
```
---
## Task 3: Soft Delete and Recycle Bin Endpoints
**Files:**
- Modify: `server.js` (after project CRUD endpoints)
**Step 1: Add DELETE /api/projects/:id (soft delete)**
```javascript
// DELETE /api/projects/:id - Soft delete project
app.delete('/api/projects/:id', requireAuth, async (req, res) => {
try {
const { id } = req.params;
// Get project to soft delete its sessions too
const project = await projectsCollection.findOne({
_id: new ObjectId(id),
deletedAt: null
});
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
// Soft delete project
await projectsCollection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { deletedAt: new Date() } }
);
// Soft delete all sessions in this project
await sessionsCollection.updateMany(
{ projectId: new ObjectId(id) },
{ $set: { deletedAt: new Date() } }
);
res.json({ success: true });
} catch (error) {
console.error('Error deleting project:', error);
res.status(500).json({ error: 'Failed to delete project' });
}
});
```
**Step 2: Add POST /api/projects/:id/restore**
```javascript
// POST /api/projects/:id/restore - Restore from recycle bin
app.post('/api/projects/:id/restore', requireAuth, async (req, res) => {
try {
const { id } = req.params;
// Restore project
const project = await projectsCollection.findOneAndUpdate(
{ _id: new ObjectId(id), deletedAt: { $ne: null } },
{ $set: { deletedAt: null } },
{ returnDocument: 'after' }
);
if (!project) {
return res.status(404).json({ error: 'Project not found in recycle bin' });
}
// Restore all sessions in this project
await sessionsCollection.updateMany(
{ projectId: new ObjectId(id) },
{ $set: { deletedAt: null } }
);
res.json({ success: true });
} catch (error) {
console.error('Error restoring project:', error);
res.status(500).json({ error: 'Failed to restore project' });
}
});
```
**Step 3: Add DELETE /api/projects/:id/permanent**
```javascript
// DELETE /api/projects/:id/permanent - Permanent delete
app.delete('/api/projects/:id/permanent', requireAuth, async (req, res) => {
try {
const { id } = req.params;
// Permanently delete sessions
await sessionsCollection.deleteMany({
projectId: new ObjectId(id)
});
// Permanently delete project
await projectsCollection.deleteOne({
_id: new ObjectId(id)
});
res.json({ success: true });
} catch (error) {
console.error('Error permanently deleting project:', error);
res.status(500).json({ error: 'Failed to permanently delete project' });
}
});
```
**Step 4: Add GET /api/recycle-bin**
```javascript
// GET /api/recycle-bin - List deleted items
app.get('/api/recycle-bin', requireAuth, async (req, res) => {
try {
const deletedProjects = await projectsCollection.find({
deletedAt: { $ne: null }
}).sort({ deletedAt: -1 }).toArray();
const result = await Promise.all(deletedProjects.map(async (project) => {
// Get session count for this project
const sessionCount = await sessionsCollection.countDocuments({
projectId: project._id,
deletedAt: { $ne: null }
});
return {
id: project._id,
name: project.name,
description: project.description,
icon: project.icon,
path: project.path,
sessionCount,
deletedAt: project.deletedAt
};
}));
res.json({
success: true,
items: result
});
} catch (error) {
console.error('Error fetching recycle bin:', error);
res.status(500).json({ error: 'Failed to fetch recycle bin' });
}
});
```
**Step 5: Commit**
```bash
git add server.js
git commit -m "feat: add soft delete, restore, permanent delete, and recycle bin endpoints"
```
---
## Task 4: Session Reassignment Endpoint
**Files:**
- Modify: `server.js` (after recycle bin endpoint)
**Step 1: Add POST /api/sessions/:id/move endpoint**
```javascript
// POST /api/sessions/:id/move - Move session to different project
app.post('/api/sessions/:id/move', requireAuth, async (req, res) => {
try {
const { id } = req.params;
const { projectId } = req.body; // null for unassigned
// Verify session exists
const session = await sessionsCollection.findOne({
_id: new ObjectId(id),
deletedAt: null
});
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// If moving to a project, verify it exists and is not deleted
if (projectId) {
const project = await projectsCollection.findOne({
_id: new ObjectId(projectId),
deletedAt: null
});
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
// Remove session from old project's sessionIds
if (session.projectId) {
await projectsCollection.updateOne(
{ _id: session.projectId },
{ $pull: { sessionIds: new ObjectId(id) } }
);
}
// Add session to new project's sessionIds
await projectsCollection.updateOne(
{ _id: new ObjectId(projectId) },
{ $push: { sessionIds: new ObjectId(id) } }
);
} else {
// Moving to unassigned - remove from old project
if (session.projectId) {
await projectsCollection.updateOne(
{ _id: session.projectId },
{ $pull: { sessionIds: new ObjectId(id) } }
);
}
}
// Update session's projectId
await sessionsCollection.updateOne(
{ _id: new ObjectId(id) },
{ $set: { projectId: projectId ? new ObjectId(projectId) : null } }
);
res.json({ success: true });
} catch (error) {
console.error('Error moving session:', error);
res.status(500).json({ error: 'Failed to move session' });
}
});
```
**Step 2: Commit**
```bash
git add server.js
git commit -m "feat: add session move endpoint"
```
---
## Task 5: Smart Suggestions Endpoint
**Files:**
- Modify: `server.js` (after session move endpoint)
**Step 1: Add GET /api/projects/suggestions endpoint**
```javascript
// GET /api/projects/suggestions?sessionId=xxx - Get project suggestions for a session
app.get('/api/projects/suggestions', requireAuth, async (req, res) => {
try {
const { sessionId } = req.query;
if (!sessionId) {
return res.status(400).json({ error: 'sessionId is required' });
}
// Get session
const session = await sessionsCollection.findOne({
_id: new ObjectId(sessionId),
deletedAt: null
});
if (!session) {
return res.status(404).json({ error: 'Session not found' });
}
// Get all active projects
const projects = await projectsCollection.find({
deletedAt: null
}).toArray();
// Calculate suggestions
const suggestions = [];
for (const project of projects) {
let score = 0;
const reasons = [];
// Directory matching (high weight)
if (session.workingDir === project.path) {
score += 90;
reasons.push('Same directory');
} else if (session.workingDir?.startsWith(project.path)) {
score += 50;
reasons.push('Subdirectory');
}
// Recency (medium weight)
const daysSinceActivity = (Date.now() - project.lastActivity) / (1000 * 60 * 60 * 24);
if (daysSinceActivity < 1) {
score += 20;
reasons.push('Used today');
} else if (daysSinceActivity < 7) {
score += 10;
reasons.push(`Used ${Math.floor(daysSinceActivity)} days ago`);
}
// Name similarity (low weight)
if (session.name?.includes(project.name) || project.name.includes(session.name)) {
score += 15;
reasons.push('Similar name');
}
if (score > 0) {
suggestions.push({
id: project._id,
name: project.name,
icon: project.icon,
color: project.color,
score,
reasons
});
}
}
// Sort by score and take top 3
const topSuggestions = suggestions
.sort((a, b) => b.score - a.score)
.slice(0, 3);
// Get all projects for "show all" option
const allProjects = projects.map(p => ({
id: p._id,
name: p.name,
icon: p.icon,
color: p.color
})).sort((a, b) => a.name.localeCompare(b.name));
res.json({
success: true,
suggestions: topSuggestions,
allProjects
});
} catch (error) {
console.error('Error getting suggestions:', error);
res.status(500).json({ error: 'Failed to get suggestions' });
}
});
```
**Step 2: Commit**
```bash
git add server.js
git commit -m "feat: add smart project suggestions endpoint"
```
---
## Task 6: Projects Page HTML
**Files:**
- Create: `public/projects.html`
**Step 1: Create projects page structure**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Projects - Claude Code Web Interface</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="stylesheet" href="/claude-ide/projects.css">
</head>
<body class="projects-page">
<div class="projects-container">
<!-- Header -->
<header class="projects-header">
<div class="header-left">
<h1>Projects</h1>
<a href="/claude/" class="back-link">← Back to Sessions</a>
</div>
<div class="header-right">
<div class="search-box">
<input type="text" id="projectSearch" placeholder="Search projects...">
</div>
<button id="createProjectBtn" class="btn btn-primary">
<span class="icon">+</span> New Project
</button>
<button id="recycleBinBtn" class="btn btn-secondary">
<span class="icon">🗑️</span> Recycle Bin
</button>
</div>
</header>
<!-- Projects Grid -->
<div id="projectsGrid" class="projects-grid">
<!-- Projects will be rendered here -->
</div>
<!-- Empty State -->
<div id="emptyState" class="empty-state" style="display: none;">
<div class="empty-icon">📁</div>
<h2>No projects yet</h2>
<p>Create your first project to organize your sessions</p>
<button class="btn btn-primary" onclick="document.getElementById('createProjectBtn').click()">
Create Project
</button>
</div>
</div>
<!-- Create/Edit Project Modal -->
<div id="projectModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Create New Project</h2>
<button class="modal-close" onclick="closeProjectModal()">×</button>
</div>
<form id="projectForm">
<div class="form-group">
<label for="projectName">Project Name *</label>
<input type="text" id="projectName" required placeholder="My API Project">
</div>
<div class="form-group">
<label for="projectPath">Working Directory *</label>
<input type="text" id="projectPath" required placeholder="/home/uroma/my-project">
</div>
<div class="form-group">
<label for="projectDescription">Description</label>
<textarea id="projectDescription" rows="3" placeholder="What is this project about?"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="projectIcon">Icon</label>
<input type="text" id="projectIcon" placeholder="📁" maxlength="2">
</div>
<div class="form-group">
<label for="projectColor">Color</label>
<input type="color" id="projectColor" value="#4a9eff">
</div>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeProjectModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Project</button>
</div>
</form>
</div>
</div>
<!-- Recycle Bin Modal -->
<div id="recycleBinModal" class="modal" style="display: none;">
<div class="modal-content modal-large">
<div class="modal-header">
<h2>Recycle Bin</h2>
<button class="modal-close" onclick="closeRecycleBinModal()">×</button>
</div>
<div id="recycleBinItems" class="recycle-bin-items">
<!-- Deleted items will be rendered here -->
</div>
</div>
</div>
<!-- Context Menu -->
<div id="contextMenu" class="context-menu" style="display: none;">
<!-- Menu items will be added dynamically -->
</div>
<script src="/claude-ide/projects.js"></script>
</body>
</html>
```
**Step 2: Commit**
```bash
git add public/projects.html
git commit -m "feat: add projects page HTML structure"
```
---
## Task 7: Projects Page CSS
**Files:**
- Create: `public/claude-ide/projects.css`
**Step 1: Create projects page styles**
```css
/* Projects Page Layout */
.projects-page {
background: var(--bg-primary);
min-height: 100vh;
}
.projects-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
/* Header */
.projects-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.header-left h1 {
margin: 0;
font-size: 2rem;
}
.back-link {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s;
}
.back-link:hover {
color: var(--accent-color);
}
.header-right {
display: flex;
gap: 0.75rem;
align-items: center;
}
.search-box input {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-secondary);
color: var(--text-primary);
width: 250px;
}
/* Projects Grid */
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
/* Project Card */
.project-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 1.5rem;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-color: var(--accent-color);
}
.project-card-header {
display: flex;
align-items: flex-start;
gap: 1rem;
margin-bottom: 1rem;
}
.project-icon {
font-size: 2rem;
line-height: 1;
}
.project-info {
flex: 1;
}
.project-name {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
color: var(--text-primary);
}
.project-description {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0;
line-height: 1.4;
}
.project-menu-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
opacity: 0;
transition: opacity 0.2s;
}
.project-card:hover .project-menu-btn {
opacity: 1;
}
.project-menu-btn:hover {
color: var(--text-primary);
}
.project-meta {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.project-path {
font-family: 'Monaco', 'Menlo', monospace;
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: 0.5rem;
word-break: break-all;
}
.project-stats {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
color: var(--text-secondary);
}
.session-count {
display: flex;
align-items: center;
gap: 0.25rem;
}
.last-activity {
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h2 {
margin-bottom: 0.5rem;
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--bg-primary);
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-large {
max-width: 700px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-header h2 {
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
}
.modal-close:hover {
color: var(--text-primary);
}
/* Form */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 1rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
/* Buttons */
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--border-color);
}
/* Context Menu */
.context-menu {
position: fixed;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
z-index: 1001;
padding: 0.25rem 0;
}
.context-menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.context-menu-item:hover {
background: var(--bg-secondary);
}
.context-menu-divider {
height: 1px;
background: var(--border-color);
margin: 0.25rem 0;
}
/* Recycle Bin */
.recycle-bin-items {
display: flex;
flex-direction: column;
gap: 1rem;
}
.recycle-bin-item {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
opacity: 0.7;
}
.recycle-bin-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.recycle-bin-actions {
display: flex;
gap: 0.5rem;
}
/* Buttons */
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-danger {
background: #dc3545;
color: white;
}
```
**Step 2: Commit**
```bash
git add public/claude-ide/projects.css
git commit -m "feat: add projects page styles"
```
---
## Task 8: Projects Page JavaScript
**Files:**
- Create: `public/claude-ide/projects.js`
**Step 1: Create projects page logic**
```javascript
// State
let projects = [];
let currentEditingProject = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadProjects();
setupEventListeners();
});
// Load projects
async function loadProjects() {
try {
const response = await fetch('/api/projects');
const data = await response.json();
if (data.success) {
projects = data.projects;
renderProjects();
}
} catch (error) {
console.error('Error loading projects:', error);
showToast('Failed to load projects', 'error');
}
}
// Render projects
function renderProjects(filter = '') {
const grid = document.getElementById('projectsGrid');
const emptyState = document.getElementById('emptyState');
const filteredProjects = projects.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase()) ||
p.description.toLowerCase().includes(filter.toLowerCase()) ||
p.path.toLowerCase().includes(filter.toLowerCase())
);
if (filteredProjects.length === 0) {
grid.style.display = 'none';
emptyState.style.display = 'block';
return;
}
grid.style.display = 'grid';
emptyState.style.display = 'none';
grid.innerHTML = filteredProjects.map(project => `
<div class="project-card" data-id="${project.id}" onclick="openProject('${project.id}')">
<div class="project-card-header">
<div class="project-icon">${project.icon}</div>
<div class="project-info">
<h3 class="project-name">${escapeHtml(project.name)}</h3>
<p class="project-description">${escapeHtml(project.description || 'No description')}</p>
</div>
<button class="project-menu-btn" onclick="event.stopPropagation(); showProjectMenu('${project.id}', event)">
</button>
</div>
<div class="project-meta">
<div class="project-path">${escapeHtml(project.path)}</div>
<div class="project-stats">
<div class="session-count">
<span>📄</span>
<span>${project.sessionCount} session${project.sessionCount !== 1 ? 's' : ''}</span>
</div>
<div class="last-activity">${formatDate(project.lastActivity)}</div>
</div>
</div>
</div>
`).join('');
}
// Setup event listeners
function setupEventListeners() {
// Search
document.getElementById('projectSearch').addEventListener('input', (e) => {
renderProjects(e.target.value);
});
// Create project
document.getElementById('createProjectBtn').addEventListener('click', () => {
openProjectModal();
});
// Project form
document.getElementById('projectForm').addEventListener('submit', handleProjectSubmit);
// Recycle bin
document.getElementById('recycleBinBtn').addEventListener('click', openRecycleBinModal);
// Close context menu on click outside
document.addEventListener('click', () => {
hideContextMenu();
});
}
// Open project
function openProject(projectId) {
// Navigate to sessions landing with project filter
window.location.href = `/claude/?project=${projectId}`;
}
// Open project modal (create or edit)
function openProjectModal(project = null) {
currentEditingProject = project;
const modal = document.getElementById('projectModal');
const title = document.getElementById('modalTitle');
const form = document.getElementById('projectForm');
if (project) {
title.textContent = 'Edit Project';
document.getElementById('projectName').value = project.name;
document.getElementById('projectPath').value = project.path;
document.getElementById('projectDescription').value = project.description || '';
document.getElementById('projectIcon').value = project.icon || '📁';
document.getElementById('projectColor').value = project.color || '#4a9eff';
} else {
title.textContent = 'Create New Project';
form.reset();
document.getElementById('projectIcon').value = '📁';
document.getElementById('projectColor').value = '#4a9eff';
}
modal.style.display = 'flex';
}
// Close project modal
function closeProjectModal() {
document.getElementById('projectModal').style.display = 'none';
currentEditingProject = null;
}
// Handle project submit
async function handleProjectSubmit(e) {
e.preventDefault();
const data = {
name: document.getElementById('projectName').value,
path: document.getElementById('projectPath').value,
description: document.getElementById('projectDescription').value,
icon: document.getElementById('projectIcon').value || '📁',
color: document.getElementById('projectColor').value || '#4a9eff'
};
try {
const url = currentEditingProject
? `/api/projects/${currentEditingProject.id}`
: '/api/projects';
const method = currentEditingProject ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showToast(currentEditingProject ? 'Project updated' : 'Project created', 'success');
closeProjectModal();
await loadProjects();
} else {
showToast(result.error || 'Failed to save project', 'error');
}
} catch (error) {
console.error('Error saving project:', error);
showToast('Failed to save project', 'error');
}
}
// Show project context menu
function showProjectMenu(projectId, event) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const menu = document.getElementById('contextMenu');
menu.innerHTML = `
<div class="context-menu-item" onclick="event.stopPropagation(); openProjectModalById('${projectId}')">
✏️ Edit
</div>
<div class="context-menu-item" onclick="event.stopPropagation(); deleteProject('${projectId}')">
🗑️ Move to Recycle Bin
</div>
`;
menu.style.left = event.pageX + 'px';
menu.style.top = event.pageY + 'px';
menu.style.display = 'block';
}
// Hide context menu
function hideContextMenu() {
document.getElementById('contextMenu').style.display = 'none';
}
// Open project modal by ID
function openProjectModalById(projectId) {
const project = projects.find(p => p.id === projectId);
if (project) {
openProjectModal(project);
}
hideContextMenu();
}
// Delete project
async function deleteProject(projectId) {
if (!confirm('Move this project and all its sessions to the recycle bin?')) {
return;
}
try {
const response = await fetch(`/api/projects/${projectId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showToast('Project moved to recycle bin', 'success');
await loadProjects();
} else {
showToast(result.error || 'Failed to delete project', 'error');
}
} catch (error) {
console.error('Error deleting project:', error);
showToast('Failed to delete project', 'error');
}
hideContextMenu();
}
// Recycle bin modal
async function openRecycleBinModal() {
const modal = document.getElementById('recycleBinModal');
const container = document.getElementById('recycleBinItems');
try {
const response = await fetch('/api/recycle-bin');
const data = await response.json();
if (data.success) {
if (data.items.length === 0) {
container.innerHTML = '<p class="text-center">Recycle bin is empty 🎉</p>';
} else {
container.innerHTML = data.items.map(item => `
<div class="recycle-bin-item">
<div class="recycle-bin-item-header">
<div>
<strong>${item.icon} ${escapeHtml(item.name)}</strong>
<div style="font-size: 0.875rem; color: var(--text-secondary);">
${item.sessionCount} session${item.sessionCount !== 1 ? 's' : ''} • Deleted ${formatDate(item.deletedAt)}
</div>
</div>
<div class="recycle-bin-actions">
<button class="btn btn-sm btn-success" onclick="restoreProject('${item.id}')">Restore</button>
<button class="btn btn-sm btn-danger" onclick="permanentDeleteProject('${item.id}')">Delete</button>
</div>
</div>
</div>
`).join('');
}
modal.style.display = 'flex';
}
} catch (error) {
console.error('Error loading recycle bin:', error);
showToast('Failed to load recycle bin', 'error');
}
}
// Close recycle bin modal
function closeRecycleBinModal() {
document.getElementById('recycleBinModal').style.display = 'none';
}
// Restore project
async function restoreProject(projectId) {
try {
const response = await fetch(`/api/projects/${projectId}/restore`, {
method: 'POST'
});
const result = await response.json();
if (result.success) {
showToast('Project restored', 'success');
await loadProjects();
await openRecycleBinModal(); // Refresh bin
} else {
showToast(result.error || 'Failed to restore project', 'error');
}
} catch (error) {
console.error('Error restoring project:', error);
showToast('Failed to restore project', 'error');
}
}
// Permanent delete
async function permanentDeleteProject(projectId) {
if (!confirm('Permanently delete this project and all its sessions? This cannot be undone!')) {
return;
}
try {
const response = await fetch(`/api/projects/${projectId}/permanent`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showToast('Project permanently deleted', 'success');
await openRecycleBinModal(); // Refresh bin
} else {
showToast(result.error || 'Failed to delete project', 'error');
}
} catch (error) {
console.error('Error deleting project:', error);
showToast('Failed to delete project', 'error');
}
}
// Helper functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 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();
}
function showToast(message, type = 'info') {
// Import from ide.js or implement locally
if (typeof window.showToast === 'function') {
window.showToast(message, type);
} else {
alert(message);
}
}
```
**Step 2: Commit**
```bash
git add public/claude-ide/projects.js
git commit -m "feat: add projects page JavaScript functionality"
```
---
## Task 9: Update Sessions Landing to Show Projects
**Files:**
- Modify: `public/claude-ide/sessions-landing.js`
**Step 1: Modify renderSessions to group by project**
Find the `renderSessions` function and modify it to group sessions by project. This will require fetching projects first, then organizing sessions.
Add this after the sessions are fetched:
```javascript
// Add project fetching
async function loadSessionsAndProjects() {
const [sessionsRes, projectsRes] = await Promise.all([
fetch('/claude/api/claude/sessions'),
fetch('/api/projects')
]);
const sessionsData = await sessionsRes.json();
const projectsData = await projectsRes.json();
if (sessionsData.success) {
sessions = sessionsData.sessions;
}
if (projectsData.success) {
const projects = projectsData.projects;
// Create a map for quick lookup
window.projectsMap = new Map(projects.map(p => [p.id.toString(), p]));
}
renderSessions();
}
```
Modify `renderSessions` to group by project:
```javascript
function renderSessions() {
const container = document.getElementById('sessionsGrid');
// Group sessions by project
const grouped = {
unassigned: [],
byProject: {}
};
sessions.forEach(session => {
const projectId = session.projectId;
if (projectId && window.projectsMap?.has(projectId.toString())) {
if (!grouped.byProject[projectId]) {
grouped.byProject[projectId] = [];
}
grouped.byProject[projectId].push(session);
} else {
grouped.unassigned.push(session);
}
});
// Render projects first
let html = '';
for (const [projectId, projectSessions] of Object.entries(grouped.byProject)) {
const project = window.projectsMap.get(projectId);
if (!project) continue;
html += `
<div class="project-section">
<div class="project-section-header" onclick="toggleProjectSection('${projectId}')">
<span class="project-icon">${project.icon}</span>
<span class="project-name">${escapeHtml(project.name)}</span>
<span class="session-count">${projectSessions.length}</span>
<span class="toggle-icon">▼</span>
</div>
<div class="project-sessions" id="project-${projectId}">
${projectSessions.map(session => renderSessionCard(session)).join('')}
</div>
</div>
`;
}
// Render unassigned sessions
if (grouped.unassigned.length > 0) {
html += `
<div class="project-section">
<div class="project-section-header unassigned" onclick="toggleProjectSection('unassigned')">
<span class="project-icon">📄</span>
<span class="project-name">Unassigned Sessions</span>
<span class="session-count">${grouped.unassigned.length}</span>
<span class="toggle-icon">▼</span>
</div>
<div class="project-sessions" id="project-unassigned">
${grouped.unassigned.map(session => renderSessionCard(session)).join('')}
</div>
</div>
`;
}
container.innerHTML = html;
}
function toggleProjectSection(projectId) {
const section = document.getElementById(`project-${projectId}`);
const icon = section.previousElementSibling.querySelector('.toggle-icon');
if (section.style.display === 'none') {
section.style.display = 'block';
icon.textContent = '▼';
} else {
section.style.display = 'none';
icon.textContent = '▶';
}
}
```
**Step 2: Add context menu to session cards**
Modify `renderSessionCard` to add the context menu trigger:
```javascript
function renderSessionCard(session) {
const project = session.projectId ? window.projectsMap?.get(session.projectId.toString()) : null;
return `
<div class="session-card" data-id="${session.id}" oncontextmenu="showSessionContextMenu(event, '${session.id}')">
<!-- existing card content -->
${project ? `<div class="session-project-badge">${project.icon} ${escapeHtml(project.name)}</div>` : ''}
<!-- rest of card -->
</div>
`;
}
```
**Step 3: Commit**
```bash
git add public/claude-ide/sessions-landing.js
git commit -m "feat: group sessions by project on landing page"
```
---
## Task 10: Session Context Menu for Reassignment
**Files:**
- Modify: `public/claude-ide/sessions-landing.js`
**Step 1: Add session context menu function**
```javascript
let currentSessionId = null;
async function showSessionContextMenu(event, sessionId) {
event.preventDefault();
currentSessionId = sessionId;
const menu = document.getElementById('sessionContextMenu');
// Fetch suggestions
const suggestionsRes = await fetch(`/api/projects/suggestions?sessionId=${sessionId}`);
const suggestionsData = await suggestionsRes.json();
let menuHtml = `
<div class="context-menu-item" onclick="openSession('${sessionId}')">
🔗 Open in IDE
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-label">Move to Project</div>
`;
// Add suggestions
if (suggestionsData.success && suggestionsData.suggestions.length > 0) {
suggestionsData.suggestions.forEach(suggestion => {
const icon = getMatchIcon(suggestion.score);
const reasons = suggestion.reasons.join(', ');
menuHtml += `
<div class="context-menu-item" onclick="moveSessionToProject('${sessionId}', '${suggestion.id}')">
${icon} ${escapeHtml(suggestion.name)}
<div class="suggestion-reason">${reasons}</div>
</div>
`;
});
menuHtml += `<div class="context-menu-divider"></div>`;
}
// Add "Show All Projects" option
menuHtml += `
<div class="context-menu-item" onclick="showAllProjectsForMove('${sessionId}')">
📂 Show All Projects...
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="moveSessionToProject('${sessionId}', null)">
📄 Move to Unassigned
</div>
`;
menu.innerHTML = menuHtml;
menu.style.left = event.pageX + 'px';
menu.style.top = event.pageY + 'px';
menu.style.display = 'block';
}
function getMatchIcon(score) {
if (score >= 90) return '🎯';
if (score >= 50) return '📂';
return '💡';
}
async function moveSessionToProject(sessionId, projectId) {
try {
const response = await fetch(`/api/sessions/${sessionId}/move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId })
});
const result = await response.json();
if (result.success) {
showToast('Session moved', 'success');
await loadSessionsAndProjects();
} else {
showToast(result.error || 'Failed to move session', 'error');
}
} catch (error) {
console.error('Error moving session:', error);
showToast('Failed to move session', 'error');
}
hideSessionContextMenu();
}
function showAllProjectsForMove(sessionId) {
// Could show a modal with all projects
// For now, just alert the user to use projects page
showToast('Use the Projects page to manage assignments', 'info');
hideSessionContextMenu();
}
function hideSessionContextMenu() {
document.getElementById('sessionContextMenu').style.display = 'none';
currentSessionId = null;
}
// Close context menu on click outside
document.addEventListener('click', (e) => {
if (!e.target.closest('#sessionContextMenu')) {
hideSessionContextMenu();
}
});
```
**Step 2: Add context menu styles**
```css
#sessionContextMenu {
position: fixed;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 250px;
z-index: 1000;
padding: 0.25rem 0;
}
.context-menu-label {
padding: 0.25rem 1rem;
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
font-weight: 600;
}
.suggestion-reason {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.125rem;
}
.session-project-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: var(--accent-color);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
}
.project-section {
margin-bottom: 2rem;
}
.project-section-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
margin-bottom: 1rem;
transition: background 0.2s;
}
.project-section-header:hover {
background: var(--border-color);
}
.project-section-header.unassigned {
border-style: dashed;
}
.toggle-icon {
margin-left: auto;
transition: transform 0.2s;
}
.project-sessions {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
```
**Step 3: Commit**
```bash
git add public/claude-ide/sessions-landing.js public/claude-ide/sessions-landing.css
git commit -m "feat: add session context menu for project reassignment"
```
---
## Task 11: Database Migration Script
**Files:**
- Create: `scripts/migrate-to-projects.js`
**Step 1: Create migration script**
```javascript
const { MongoClient, ObjectId } = require('mongodb');
const path = require('path');
// Configuration
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017';
const DB_NAME = process.env.DB_NAME || 'claude-web-interface';
async function migrate() {
const client = new MongoClient(MONGO_URI);
try {
console.log('Connecting to MongoDB...');
await client.connect();
const db = client.db(DB_NAME);
const sessionsCollection = db.collection('sessions');
const projectsCollection = db.collection('projects');
console.log('Creating projects collection...');
await db.createCollection('projects');
console.log('Creating indexes...');
await projectsCollection.createIndex({ deletedAt: 1 });
await projectsCollection.createIndex({ name: 1 });
console.log('Finding unique project names from existing sessions...');
const uniqueProjects = await sessionsCollection.distinct('metadata.project');
console.log(`Found ${uniqueProjects.length} unique projects to migrate...`);
const icons = ['🚀', '📱', '🎨', '💻', '🔧', '📊', '🎯', '🔮', '⚡', '🌟'];
const colors = ['#4a9eff', '#ff6b6b', '#51cf66', '#ffd43b', '#cc5de8', '#ff922b', '#20c997', '#339af0'];
let projectCount = 0;
for (const name of uniqueProjects) {
if (!name) continue;
console.log(`Migrating project: ${name}`);
const projectSessions = await sessionsCollection.find({
'metadata.project': name
}).toArray();
if (projectSessions.length === 0) continue;
const paths = [...new Set(projectSessions.map(s => s.workingDir).filter(Boolean))];
const createdAt = projectSessions[0].createdAt || new Date();
const lastActivity = Math.max(...projectSessions.map(s => s.lastActivity || s.createdAt));
const randomIcon = icons[Math.floor(Math.random() * icons.length)];
const randomColor = colors[Math.floor(Math.random() * colors.length)];
const project = {
name,
description: `Migrated from existing sessions`,
icon: randomIcon,
color: randomColor,
path: paths[0] || '',
sessionIds: projectSessions.map(s => s._id),
createdAt,
lastActivity: new Date(lastActivity),
deletedAt: null
};
const result = await projectsCollection.insertOne(project);
console.log(` → Created project with ID: ${result.insertedId}`);
// Update sessions with projectId
await sessionsCollection.updateMany(
{ 'metadata.project': name },
{ $set: { projectId: result.insertedId } }
);
console.log(` → Updated ${projectSessions.length} sessions`);
projectCount++;
}
console.log(`\n✅ Migration complete! Created ${projectCount} projects.`);
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
} finally {
await client.close();
}
}
// Run migration
migrate();
```
**Step 2: Add migration to package.json**
Add to `scripts` section:
```json
"scripts": {
"migrate:projects": "node scripts/migrate-to-projects.js"
}
```
**Step 3: Commit**
```bash
git add scripts/migrate-to-projects.js package.json
git commit -m "feat: add database migration script for projects"
```
**Step 4: Run migration**
```bash
npm run migrate:projects
```
**Step 5: Commit migration results**
```bash
git add .
git commit -m "chore: run project migration"
```
---
## Task 12: Add Route Handler for Projects Page
**Files:**
- Modify: `server.js`
**Step 1: Add projects page route**
Find the routes section (around line 70) and add:
```javascript
// Projects page
app.get('/projects', (req, res) => {
if (req.session.userId) {
res.sendFile(path.join(__dirname, 'public', 'projects.html'));
} else {
res.redirect('/claude/');
}
});
```
**Step 2: Add link to projects page in navigation**
Modify the landing page header to include a "Projects" link:
```javascript
// In the landing page HTML or navigation component
// Add: <a href="/projects" class="nav-link">Projects</a>
```
**Step 3: Commit**
```bash
git add server.js public/claude-landing.html
git commit -m "feat: add projects page route and navigation link"
```
---
## Task 13: Update Session Creation to Auto-Assign to Project
**Files:**
- Modify: `public/claude-ide/ide.js`
- Modify: `server.js`
**Step 1: Add projectId to session creation in IDE**
Find the session creation code in ide.js and modify to include projectId if a project is selected:
```javascript
// When creating a new session
async function createSession(sessionData) {
const currentProject = getCurrentProject(); // Get from project selector
const payload = {
...sessionData,
projectId: currentProject?.id || null
};
const response = await fetch('/claude/api/claude/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// ... handle response
}
```
**Step 2: Update server endpoint to handle projectId**
Modify the session creation endpoint in server.js:
```javascript
app.post('/claude/api/claude/sessions', requireAuth, async (req, res) => {
try {
const { name, workingDir, projectId } = req.body;
// ... existing validation
// If projectId provided, verify it exists
if (projectId) {
const project = await projectsCollection.findOne({
_id: new ObjectId(projectId),
deletedAt: null
});
if (!project) {
return res.status(400).json({ error: 'Invalid project ID' });
}
}
const session = {
// ... existing fields
projectId: projectId ? new ObjectId(projectId) : null,
createdAt: new Date(),
lastActivity: new Date()
};
const result = await sessionsCollection.insertOne(session);
// Add session to project's sessionIds
if (projectId) {
await projectsCollection.updateOne(
{ _id: new ObjectId(projectId) },
{
$push: { sessionIds: result.insertedId },
$set: { lastActivity: new Date() }
}
);
}
// ... rest of endpoint
} catch (error) {
// ... error handling
}
});
```
**Step 3: Commit**
```bash
git add public/claude-ide/ide.js server.js
git commit -m "feat: auto-assign new sessions to selected project"
```
---
## Task 14: Testing and Documentation
**Files:**
- Create: `docs/projects-feature.md`
**Step 1: Create documentation**
```markdown
# Projects Feature
## Overview
Projects are persistent containers that organize multiple Claude Code sessions.
## Features
### Creating Projects
1. Navigate to `/projects`
2. Click "+ New Project"
3. Enter name and working directory (required)
4. Optionally add description, icon, and color
5. Click "Save Project"
### Assigning Sessions to Projects
**Automatic Assignment:**
- When creating a session from the IDE with a project selected, it auto-assigns
**Manual Assignment:**
1. Go to sessions landing page (`/claude/`)
2. Right-click on any session card
3. Select "Move to Project" from context menu
4. Choose from:
- 🎯 **Suggestions** - Smart recommendations based on directory, recency, name
- 📂 **All Projects** - Full project list
### Managing Projects
**Edit Project:**
- Right-click project card → Edit
- Modify any field except name
- Save changes
**Delete Project:**
- Right-click project card → Move to Recycle Bin
- Project and all sessions are soft-deleted
- Can restore from recycle bin within 30 days
**Recycle Bin:**
- Click "🗑️ Recycle Bin" button on projects page
- Restore deleted projects (restores all sessions too)
- Permanently delete (cannot be undone)
## API Reference
### Projects
- `GET /api/projects` - List all active projects
- `POST /api/projects` - Create new project
- `PUT /api/projects/:id` - Update project
- `DELETE /api/projects/:id` - Soft delete project
- `POST /api/projects/:id/restore` - Restore from recycle bin
- `DELETE /api/projects/:id/permanent` - Permanent delete
### Sessions
- `POST /api/sessions/:id/move` - Move session to different project
- `GET /api/projects/suggestions?sessionId=xxx` - Get project suggestions
### Recycle Bin
- `GET /api/recycle-bin` - List deleted items
## Smart Suggestions
The system suggests projects based on:
1. **Directory Match (90 points)** - Exact path match
2. **Subdirectory (50 points)** - Session path is under project path
3. **Recent Use (20 points)** - Used today
4. **Week-old Use (10 points)** - Used within last week
5. **Name Similarity (15 points)** - Name overlap
Suggestions with 90+ points show 🎯, 50-89 show 📂, lower show 💡
```
**Step 2: Manual testing checklist**
Test each feature and document results:
```bash
# 1. Create project
curl -X POST http://localhost:3000/api/projects \
-H "Content-Type: application/json" \
-d '{"name":"Test Project","path":"/home/uroma/test"}'
# 2. List projects
curl http://localhost:3000/api/projects
# 3. Create session with projectId
curl -X POST http://localhost:3000/claude/api/claude/sessions \
-H "Content-Type: application/json" \
-d '{"name":"Test Session","workingDir":"/home/uroma/test","projectId":"<id>"}'
# 4. Get suggestions
curl "http://localhost:3000/api/projects/suggestions?sessionId=<id>"
# 5. Move session
curl -X POST http://localhost:3000/api/sessions/<id>/move \
-H "Content-Type: application/json" \
-d '{"projectId":"<new-project-id>"}'
# 6. Delete project
curl -X DELETE http://localhost:3000/api/projects/<id>
# 7. Restore project
curl -X POST http://localhost:3000/api/projects/<id>/restore
```
**Step 3: Commit**
```bash
git add docs/projects-feature.md
git commit -m "docs: add projects feature documentation"
```
---
## Task 15: Final Integration and Cleanup
**Files:**
- Modify: `server.js`
- Modify: `public/claude-ide/ide.js`
**Step 1: Update ObjectId import**
Ensure ObjectId is imported at the top of server.js:
```javascript
const { ObjectId } = require('mongodb');
```
**Step 2: Add error handling for missing collections**
Add safety check in case projects collection doesn't exist yet:
```javascript
// At the start of API endpoints that use projectsCollection
if (!db.collection('projects')) {
await db.createCollection('projects');
}
```
**Step 3: Ensure all async functions have proper error handling**
Check all new endpoints have try-catch blocks with proper error responses.
**Step 4: Restart server and test**
```bash
# Stop existing server
pkill -f "node.*server.js"
# Start new server
node server.js
```
**Step 5: Test the complete flow**
1. Navigate to `/projects`
2. Create a new project
3. Go to `/claude/`
4. Create a session (verify it's unassigned)
5. Right-click session → move to project
6. Verify session appears under project
7. Delete project → verify in recycle bin
8. Restore project → verify it's back
**Step 6: Final commit**
```bash
git add .
git commit -m "feat: complete project and session organization feature
- Persistent projects as first-class entities
- Smart session assignment with suggestions
- Context menu for session reassignment
- Soft delete with recycle bin
- Migration script for existing sessions
- Full API and UI implementation
Closes #1"
```
**Step 7: Push to remote**
```bash
git push origin feature/project-organization
```
---
## Completion Criteria
- [x] Projects collection created with proper indexes
- [x] All CRUD endpoints working (create, read, update, delete)
- [x] Soft delete implemented (deletedAt timestamp)
- [x] Recycle bin UI functional
- [x] Session reassignment via context menu
- [x] Smart suggestions based on directory/recency/name
- [x] Auto-assignment when creating sessions in project context
- [x] Migration script successfully backfills existing data
- [x] All tests passing
- [x] Documentation complete
**Ready for PR when all tasks complete!**