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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ ai_blueprint.md
|
||||
plans/
|
||||
|
||||
# Claude / Anthropic Artifacts
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
claude.json
|
||||
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user