Compare commits

..

2 Commits

Author SHA1 Message Date
f740174257 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>
2026-02-22 13:32:09 -05:00
d77ceb376d feat: Save bible snapshot alongside each run on start
Copies bible.json as bible_snapshot.json into the run folder before
generation begins, preserving the exact blueprint used for that run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:29:55 -05:00
6 changed files with 95 additions and 18 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ ai_blueprint.md
plans/ plans/
# Claude / Anthropic Artifacts # Claude / Anthropic Artifacts
CLAUDE.md
.claude/ .claude/
claude.json 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"> <div class="card-body">
<h5 class="card-title">{{ p.name }}</h5> <h5 class="card-title">{{ p.name }}</h5>
<p class="card-text text-muted small">Created: {{ p.created_at.strftime('%Y-%m-%d') }}</p> <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>
</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 %} {% else %}
<div class="col-12 text-center py-5"> <div class="col-12 text-center py-5">
<h4 class="text-muted mb-3">No projects yet. Start writing!</h4> <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"> <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> <i class="fas fa-code-branch"></i>
</button> </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>
<div class="mt-2"> <div class="mt-2">
<span class="badge bg-secondary">{{ bible.project_metadata.genre }}</span> <span class="badge bg-secondary">{{ bible.project_metadata.genre }}</span>
@@ -546,6 +551,26 @@
</div> </div>
</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 --> <!-- Full Bible JSON Modal -->
<div class="modal fade" id="fullBibleModal" tabindex="-1"> <div class="modal fade" id="fullBibleModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-dialog modal-lg modal-dialog-scrollable">

View File

@@ -4,7 +4,7 @@ import shutil
from datetime import datetime from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, flash from flask import Blueprint, render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user 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 web.helpers import is_project_locked
from core import config, utils from core import config, utils
from ai import models as ai_models from ai import models as ai_models
@@ -392,6 +392,36 @@ def run_project(id):
return redirect(url_for('project.view_project', id=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') @project_bp.route('/project/<int:id>/review')
@login_required @login_required
def review_project(id): def review_project(id):

View File

@@ -281,7 +281,18 @@ def generate_book_task(run_id, project_path, bible_path, allow_copy=True, feedba
except Exception as e: except Exception as e:
utils.log("SYSTEM", f" -> Failed to copy {item}: {e}") utils.log("SYSTEM", f" -> Failed to copy {item}: {e}")
# 2. Run Generation # 2. Save Bible Snapshot alongside this run
run_dir_early = os.path.join(project_path, "runs", f"run_{run_id}")
os.makedirs(run_dir_early, exist_ok=True)
if os.path.exists(bible_path):
snapshot_path = os.path.join(run_dir_early, "bible_snapshot.json")
try:
shutil.copy2(bible_path, snapshot_path)
utils.log("SYSTEM", f"Bible snapshot saved to run folder.")
except Exception as _e:
utils.log("SYSTEM", f"WARNING: Could not save bible snapshot: {_e}")
# 3. Run Generation
from cli.engine import run_generation from cli.engine import run_generation
run_generation(bible_path, specific_run_id=run_id) run_generation(bible_path, specific_run_id=run_id)