feat: Add project deletion; untrack CLAUDE.md from git

- Add DELETE /project/<id>/delete route with ownership check, active-run
  guard, filesystem cleanup (shutil.rmtree), and StoryState cascade delete
- Add delete button + confirmation modal to project page header
- Add delete button + per-project confirmation modal to dashboard cards
- Add CLAUDE.md to .gitignore and remove it from git tracking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 13:32:09 -05:00
parent d77ceb376d
commit f740174257
5 changed files with 83 additions and 17 deletions

View File

@@ -4,7 +4,7 @@ import shutil
from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from web.db import db, Project, Run, Persona
from web.db import db, Project, Run, Persona, StoryState
from web.helpers import is_project_locked
from core import config, utils
from ai import models as ai_models
@@ -392,6 +392,36 @@ def run_project(id):
return redirect(url_for('project.view_project', id=id))
@project_bp.route('/project/<int:id>/delete', methods=['POST'])
@login_required
def delete_project(id):
proj = db.session.get(Project, id) or Project.query.get_or_404(id)
if proj.user_id != current_user.id:
return "Unauthorized", 403
active = Run.query.filter_by(project_id=id).filter(Run.status.in_(['running', 'queued'])).first()
if active:
flash("Cannot delete a project with an active run. Stop the run first.", "danger")
return redirect(url_for('project.view_project', id=id))
# Delete filesystem folder
if proj.folder_path and os.path.exists(proj.folder_path):
try:
shutil.rmtree(proj.folder_path)
except Exception as e:
flash(f"Warning: could not delete project files: {e}", "warning")
# Delete StoryState records (no cascade on Project yet)
StoryState.query.filter_by(project_id=id).delete()
# Delete project (cascade handles Runs and LogEntries)
db.session.delete(proj)
db.session.commit()
flash("Project deleted.", "success")
return redirect(url_for('project.index'))
@project_bp.route('/project/<int:id>/review')
@login_required
def review_project(id):