feat: add base shell, notes panel, back-navigation with stale propagation

Implements Task 4: base.html DaisyUI/Alpine shell, notes autosave panel,
nav.py phase switching with downstream stale marking, notes.py save/get
endpoints, state debug endpoint, and stub /triage route for test support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 16:08:21 +02:00
parent 203737cc3f
commit bcfee45bd7
6 changed files with 235 additions and 3 deletions
+21 -1
View File
@@ -1,3 +1,23 @@
from flask import Blueprint
from flask import Blueprint, current_app, render_template, request
from app.state import load_state
bp = Blueprint("albums", __name__)
@bp.get("/triage")
def triage():
album_id = request.args.get("album_id", "")
notes_content = ""
phase_stale = []
if album_id:
state = load_state(album_id, current_app)
if state:
notes_content = state.notes
phase_stale = state.phase_stale
return render_template(
"base.html",
current_phase="triage",
album_id=album_id,
notes_content=notes_content,
phase_stale=phase_stale,
)
+49 -1
View File
@@ -1,3 +1,51 @@
from flask import Blueprint
from flask import Blueprint, current_app, jsonify, redirect, request
from app.state import load_state, save_state
bp = Blueprint("nav", __name__)
STALE_DOWNSTREAM = {
"triage": ["curate", "group", "write"],
"curate": ["group", "write"],
"group": ["write"],
"write": [],
"export": [],
}
@bp.post("/nav/phase")
def goto_phase():
body = request.get_json()
target = body["target_phase"]
state = load_state(body["album_id"], current_app)
if state is None:
return jsonify({"error": "no state"}), 404
# Mark downstream completed phases and the current phase as stale
downstream = STALE_DOWNSTREAM.get(target, [])
candidates = set(downstream) & (set(state.phases_completed) | {state.phase})
newly_stale = [p for p in candidates if p not in state.phase_stale]
state.phase_stale = list(set(state.phase_stale + newly_stale))
state.phase = target
save_state(state, current_app)
return jsonify({"ok": True, "phase": target})
@bp.post("/nav/dismiss-stale")
def dismiss_stale():
album_id = request.form["album_id"]
phase = request.form["phase"]
state = load_state(album_id, current_app)
if state:
state.phase_stale = [p for p in state.phase_stale if p != phase]
save_state(state, current_app)
return redirect(f"/{phase}?album_id={album_id}")
@bp.get("/state/<album_id>")
def get_state(album_id):
"""Debug/test endpoint — returns full state JSON."""
state = load_state(album_id, current_app)
if state is None:
return jsonify({"error": "no state"}), 404
from dataclasses import asdict
return jsonify(asdict(state))
+23 -1
View File
@@ -1,3 +1,25 @@
from flask import Blueprint
from flask import Blueprint, current_app, jsonify, request
from app.state import load_state, save_state
bp = Blueprint("notes", __name__)
PHASE_ORDER = ["triage", "curate", "group", "write", "export"]
@bp.post("/notes/save")
def save_notes():
body = request.get_json()
state = load_state(body["album_id"], current_app)
if state is None:
return jsonify({"error": "no state"}), 404
state.notes = body["notes"]
save_state(state, current_app)
return jsonify({"ok": True})
@bp.get("/notes/<album_id>")
def get_notes(album_id):
state = load_state(album_id, current_app)
if state is None:
return jsonify({"error": "no state"}), 404
return jsonify({"notes": state.notes})