Add async pipeline with progress monitoring, resumability, and result transparency

Pipeline engine rewritten with combo-first loop: each combination is processed
through all requested passes before moving to the next, with incremental DB
saves after every step (crash-safe). Blocked combos now get result rows so they
appear in the results page with constraint violation reasons.

New pipeline_runs table tracks run lifecycle (pending/running/completed/failed/
cancelled). Web route launches pipeline in a background thread with its own DB
connection. HTMX polling partial shows live progress with per-pass breakdown.

Also: status guard prevents reviewed->scored downgrade, save_combination loads
existing status on dedup for correct resume, per-metric scores show domain
bounds + units + position bars, ensure_metric backfills units on existing rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Simonson, Andrew
2026-02-18 15:30:52 -06:00
parent 8118a62242
commit d2028a642b
17 changed files with 1263 additions and 217 deletions

View File

@@ -40,18 +40,8 @@ def entity_new():
dimensions=repo.list_dimensions())
@bp.route("/<int:entity_id>")
@bp.route("/<int:entity_id>", methods=["GET", "POST"])
def entity_detail(entity_id: int):
repo = get_repo()
entity = repo.get_entity(entity_id)
if not entity:
flash("Entity not found.", "error")
return redirect(url_for("entities.entity_list"))
return render_template("entities/detail.html", entity=entity)
@bp.route("/<int:entity_id>/edit", methods=["GET", "POST"])
def entity_edit(entity_id: int):
repo = get_repo()
entity = repo.get_entity(entity_id)
if not entity:
@@ -62,13 +52,17 @@ def entity_edit(entity_id: int):
description = request.form.get("description", "").strip()
if not name:
flash("Name is required.", "error")
return render_template("entities/form.html", entity=entity,
dimensions=repo.list_dimensions())
repo.update_entity(entity_id, name, description)
flash(f"Entity '{name}' updated.", "success")
return redirect(url_for("entities.entity_detail", entity_id=entity_id))
return render_template("entities/form.html", entity=entity,
dimensions=repo.list_dimensions())
else:
repo.update_entity(entity_id, name, description)
flash(f"Entity '{name}' updated.", "success")
entity = repo.get_entity(entity_id)
return render_template("entities/detail.html", entity=entity)
@bp.route("/<int:entity_id>/edit")
def entity_edit(entity_id: int):
"""Legacy route — redirect to detail page."""
return redirect(url_for("entities.entity_detail", entity_id=entity_id))
@bp.route("/<int:entity_id>/delete", methods=["POST"])