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>
This commit is contained in:
68
src/physcom_web/templates/domains/_metrics_table.html
Normal file
68
src/physcom_web/templates/domains/_metrics_table.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<table id="metrics-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric</th>
|
||||
<th>Unit</th>
|
||||
<th>Weight</th>
|
||||
<th>Norm Min</th>
|
||||
<th>Norm Max</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mb in domain.metric_bounds %}
|
||||
<tr>
|
||||
<td>{{ mb.metric_name }}</td>
|
||||
<td>{{ mb.unit or '—' }}</td>
|
||||
<td>{{ mb.weight }}</td>
|
||||
<td>{{ mb.norm_min }}</td>
|
||||
<td>{{ mb.norm_max }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-sm"
|
||||
onclick="this.closest('tr').nextElementSibling.style.display='table-row'; this.closest('tr').style.display='none'">
|
||||
Edit
|
||||
</button>
|
||||
<form method="post"
|
||||
hx-post="{{ url_for('domains.metric_delete', domain_id=domain.id, metric_id=mb.metric_id) }}"
|
||||
hx-target="#metrics-section" hx-swap="innerHTML"
|
||||
class="inline-form">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Del</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="edit-row" style="display:none">
|
||||
<form method="post"
|
||||
hx-post="{{ url_for('domains.metric_edit', domain_id=domain.id, metric_id=mb.metric_id) }}"
|
||||
hx-target="#metrics-section" hx-swap="innerHTML">
|
||||
<td>{{ mb.metric_name }}</td>
|
||||
<td><input name="unit" value="{{ mb.unit or '' }}"></td>
|
||||
<td><input name="weight" type="number" step="any" value="{{ mb.weight }}" required></td>
|
||||
<td><input name="norm_min" type="number" step="any" value="{{ mb.norm_min }}" required></td>
|
||||
<td><input name="norm_max" type="number" step="any" value="{{ mb.norm_max }}" required></td>
|
||||
<td>
|
||||
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
||||
<button type="button" class="btn btn-sm"
|
||||
onclick="this.closest('tr').style.display='none'; this.closest('tr').previousElementSibling.style.display=''">
|
||||
Cancel
|
||||
</button>
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Add Metric</h3>
|
||||
<form method="post"
|
||||
hx-post="{{ url_for('domains.metric_add', domain_id=domain.id) }}"
|
||||
hx-target="#metrics-section" hx-swap="innerHTML"
|
||||
class="dep-add-form">
|
||||
<div class="form-row">
|
||||
<input name="metric_name" placeholder="metric name" required>
|
||||
<input name="unit" placeholder="unit">
|
||||
<input name="weight" type="number" step="any" placeholder="weight" value="1.0" required>
|
||||
<input name="norm_min" type="number" step="any" placeholder="norm min" value="0.0" required>
|
||||
<input name="norm_max" type="number" step="any" placeholder="norm max" value="1.0" required>
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
36
src/physcom_web/templates/domains/detail.html
Normal file
36
src/physcom_web/templates/domains/detail.html
Normal file
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ domain.name }} — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ domain.name }}</h1>
|
||||
<form method="post" action="{{ url_for('domains.domain_delete', domain_id=domain.id) }}" class="inline-form"
|
||||
onsubmit="return confirm('Delete this domain and all its pipeline runs and results?')">
|
||||
<button type="submit" class="btn btn-danger">Delete Domain</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('domains.domain_detail', domain_id=domain.id) }}">
|
||||
<div class="form-row">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ domain.name }}" required>
|
||||
</div>
|
||||
<div class="form-group" style="flex:2">
|
||||
<label for="description">Description</label>
|
||||
<input type="text" id="description" name="description" value="{{ domain.description }}">
|
||||
</div>
|
||||
<div class="form-group" style="align-self:end">
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h2>Metrics</h2>
|
||||
|
||||
<div id="metrics-section">
|
||||
{% include "domains/_metrics_table.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
24
src/physcom_web/templates/domains/form.html
Normal file
24
src/physcom_web/templates/domains/form.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}New Domain — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>New Domain</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name"
|
||||
value="{{ request.form.get('name', '') }}" required placeholder="e.g. Urban Transit">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" rows="3">{{ request.form.get('description', '') }}</textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<a href="{{ url_for('domains.domain_list') }}" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -2,14 +2,17 @@
|
||||
{% block title %}Domains — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Domains</h1>
|
||||
<div class="page-header">
|
||||
<h1>Domains</h1>
|
||||
<a href="{{ url_for('domains.domain_new') }}" class="btn btn-primary">New Domain</a>
|
||||
</div>
|
||||
|
||||
{% if not domains %}
|
||||
<p class="empty">No domains found. Seed data via CLI first.</p>
|
||||
<p class="empty">No domains found. <a href="{{ url_for('domains.domain_new') }}">Create one</a> or seed data via CLI.</p>
|
||||
{% else %}
|
||||
{% for d in domains %}
|
||||
<div class="card">
|
||||
<h2>{{ d.name }}</h2>
|
||||
<h2><a href="{{ url_for('domains.domain_detail', domain_id=d.id) }}">{{ d.name }}</a></h2>
|
||||
<p>{{ d.description }}</p>
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user