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

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ ai_blueprint.md
plans/
# Claude / Anthropic Artifacts
CLAUDE.md
.claude/
claude.json

View File

@@ -1,15 +0,0 @@
# Claude Custom Instructions
## Project Context and Index
Before starting any coding task, you MUST always read the `ai_blueprint.md` file in the root directory. This file serves as the project index, architecture plan, and contains the actionable steps and versions designed by the Architect. Do not start modifying files until you have read and understood the context provided in `ai_blueprint.md`.
## Managing Documentation
Whenever you complete an implementation step outlined in `ai_blueprint.md` or make notable architectural changes, you MUST update the `README.md` and/or `ai_blueprint.md` to reflect those changes and bump version numbers if appropriate.
## Git Workflow
Every time you complete a task or make changes to files, you MUST automatically commit those changes to Git before waiting for the user's next prompt.
### Instructions:
1. Always run `git add .` to stage your changes.
2. Run `git commit -m "Auto-commit: [brief description of what was changed]"`
3. Do not ask for permission to commit, just perform the git commit automatically.

View File

@@ -34,10 +34,35 @@
<div class="card-body">
<h5 class="card-title">{{ p.name }}</h5>
<p class="card-text text-muted small">Created: {{ p.created_at.strftime('%Y-%m-%d') }}</p>
<a href="/project/{{ p.id }}" class="btn btn-outline-primary stretched-link">Open Project</a>
<div class="d-flex justify-content-between align-items-center mt-3">
<a href="/project/{{ p.id }}" class="btn btn-outline-primary">Open Project</a>
<button class="btn btn-outline-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteModal{{ p.id }}" title="Delete project">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Delete Modal for {{ p.name }} -->
<div class="modal fade" id="deleteModal{{ p.id }}" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ p.id }}/delete" method="POST">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title"><i class="fas fa-exclamation-triangle me-2"></i>Delete Project</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Permanently delete <strong>{{ p.name }}</strong> and all its runs and generated files?</p>
<p class="text-danger fw-bold mb-0">This cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="col-12 text-center py-5">
<h4 class="text-muted mb-3">No projects yet. Start writing!</h4>

View File

@@ -11,6 +11,11 @@
<button class="btn btn-sm btn-outline-info ms-2" data-bs-toggle="modal" data-bs-target="#cloneProjectModal" title="Clone/Fork Project" data-bs-toggle="tooltip">
<i class="fas fa-code-branch"></i>
</button>
{% if not locked %}
<button class="btn btn-sm btn-outline-danger ms-2" data-bs-toggle="modal" data-bs-target="#deleteProjectModal" title="Delete Project">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
<div class="mt-2">
<span class="badge bg-secondary">{{ bible.project_metadata.genre }}</span>
@@ -546,6 +551,26 @@
</div>
</div>
<!-- Delete Project Modal -->
<div class="modal fade" id="deleteProjectModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/delete" method="POST">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title"><i class="fas fa-exclamation-triangle me-2"></i>Delete Project</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>This will permanently delete <strong>{{ project.name }}</strong> and all its runs, files, and generated books.</p>
<p class="text-danger fw-bold">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Delete Project</button>
</div>
</form>
</div>
</div>
<!-- Full Bible JSON Modal -->
<div class="modal fade" id="fullBibleModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">

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):