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>
115 lines
4.4 KiB
Python
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)
|