Files
physicalCombinatorics/src/physcom_web/routes/domains.py
Andrew Simonson 8dfe3607b1 Add domain CRUD, energy density constraint, LLM status, reset results, score display fixes
Domain management:
- Add domain list/detail/form templates and full CRUD routes (domains.py)
- Add metric bound add/edit/delete via HTMX partials (_metrics_table.html)

Energy density constraint (Rule 6 in ConstraintResolver):
- Hard-block combos where power source provides <25% of platform's required Wh/kg
- Warn (conditional) when under-density but within 4x
- Solar Sail exempt (no stored energy); Airplane requires 400 Wh/kg, Spaceship 2000 Wh/kg
- Add energy_density_wh_kg provides to all 8 stored-energy power sources in seed data
- 3 new constraint resolver tests

LLM-complete status:
- Pipeline Pass 4 now sets combo status to llm_reviewed after successful LLM review
- update_combination_status guards against downgrading: scored won't overwrite
  llm_reviewed or reviewed; llm_reviewed won't overwrite reviewed
- Add badge-llm_reviewed CSS style (light blue)

Reset results:
- Repository.reset_domain_results() deletes combination_results, combination_scores,
  and pipeline_runs for a domain; pipeline re-evaluates on next run
- POST /results/<domain>/reset route with flash confirmation
- "Reset results" danger button with JS confirm dialog in results list

Fix composite score 0 displaying as --- (Jinja2 falsy 0.0 bug):
- Change `if r.composite_score` to `if r.composite_score is not none`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 11:13:00 -06:00

115 lines
4.4 KiB
Python

"""Domain CRUD routes."""
from __future__ import annotations
from flask import Blueprint, flash, redirect, render_template, request, url_for
from physcom.models.domain import Domain, MetricBound
from physcom_web.app import get_repo
bp = Blueprint("domains", __name__, url_prefix="/domains")
@bp.route("/")
def domain_list():
repo = get_repo()
domains = repo.list_domains()
return render_template("domains/list.html", domains=domains)
@bp.route("/new", methods=["GET", "POST"])
def domain_new():
repo = get_repo()
if request.method == "POST":
name = request.form["name"].strip()
description = request.form.get("description", "").strip()
if not name:
flash("Name is required.", "error")
return render_template("domains/form.html")
domain = repo.add_domain(Domain(name=name, description=description))
flash(f"Domain '{name}' created.", "success")
return redirect(url_for("domains.domain_detail", domain_id=domain.id))
return render_template("domains/form.html")
@bp.route("/<int:domain_id>", methods=["GET", "POST"])
def domain_detail(domain_id: int):
repo = get_repo()
domain = repo.get_domain_by_id(domain_id)
if not domain:
flash("Domain not found.", "error")
return redirect(url_for("domains.domain_list"))
if request.method == "POST":
name = request.form["name"].strip()
description = request.form.get("description", "").strip()
if not name:
flash("Name is required.", "error")
else:
repo.update_domain(domain_id, name, description)
flash(f"Domain '{name}' updated.", "success")
domain = repo.get_domain_by_id(domain_id)
return render_template("domains/detail.html", domain=domain)
@bp.route("/<int:domain_id>/delete", methods=["POST"])
def domain_delete(domain_id: int):
repo = get_repo()
domain = repo.get_domain_by_id(domain_id)
if domain:
repo.delete_domain(domain_id)
flash(f"Domain '{domain.name}' deleted.", "success")
return redirect(url_for("domains.domain_list"))
# ── Metric bound CRUD (HTMX partials) ────────────────────────
@bp.route("/<int:domain_id>/metrics/add", methods=["POST"])
def metric_add(domain_id: int):
repo = get_repo()
metric_name = request.form["metric_name"].strip()
unit = request.form.get("unit", "").strip()
try:
weight = float(request.form.get("weight", "1.0"))
norm_min = float(request.form.get("norm_min", "0.0"))
norm_max = float(request.form.get("norm_max", "1.0"))
except ValueError:
flash("Weight, norm_min, and norm_max must be numbers.", "error")
domain = repo.get_domain_by_id(domain_id)
return render_template("domains/_metrics_table.html", domain=domain)
if not metric_name:
flash("Metric name is required.", "error")
else:
mb = MetricBound(metric_name=metric_name, weight=weight, norm_min=norm_min, norm_max=norm_max, unit=unit)
repo.add_metric_bound(domain_id, mb)
flash("Metric added.", "success")
domain = repo.get_domain_by_id(domain_id)
return render_template("domains/_metrics_table.html", domain=domain)
@bp.route("/<int:domain_id>/metrics/<int:metric_id>/edit", methods=["POST"])
def metric_edit(domain_id: int, metric_id: int):
repo = get_repo()
try:
weight = float(request.form.get("weight", "1.0"))
norm_min = float(request.form.get("norm_min", "0.0"))
norm_max = float(request.form.get("norm_max", "1.0"))
except ValueError:
flash("Weight, norm_min, and norm_max must be numbers.", "error")
domain = repo.get_domain_by_id(domain_id)
return render_template("domains/_metrics_table.html", domain=domain)
unit = request.form.get("unit", "").strip()
repo.update_metric_bound(domain_id, metric_id, weight, norm_min, norm_max, unit)
flash("Metric updated.", "success")
domain = repo.get_domain_by_id(domain_id)
return render_template("domains/_metrics_table.html", domain=domain)
@bp.route("/<int:domain_id>/metrics/<int:metric_id>/delete", methods=["POST"])
def metric_delete(domain_id: int, metric_id: int):
repo = get_repo()
repo.delete_metric_bound(domain_id, metric_id)
flash("Metric removed.", "success")
domain = repo.get_domain_by_id(domain_id)
return render_template("domains/_metrics_table.html", domain=domain)