Add Flask web UI, Docker Compose, core engine + tests
- physcom core: CLI, 5-pass pipeline, SQLite repo, 37 tests - physcom_web: Flask app with HTMX for entity/domain/pipeline/results CRUD - Docker Compose: web + cli services sharing a named volume for the DB - Clean up local settings to use wildcard permissions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
35
src/physcom_web/templates/base.html
Normal file
35
src/physcom_web/templates/base.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}PhysCom{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="{{ url_for('entities.entity_list') }}" class="nav-brand">PhysCom</a>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('entities.entity_list') }}">Entities</a></li>
|
||||
<li><a href="{{ url_for('domains.domain_list') }}">Domains</a></li>
|
||||
<li><a href="{{ url_for('pipeline.pipeline_form') }}">Pipeline</a></li>
|
||||
<li><a href="{{ url_for('results.results_index') }}">Results</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-container">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash flash-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
34
src/physcom_web/templates/domains/list.html
Normal file
34
src/physcom_web/templates/domains/list.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Domains — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Domains</h1>
|
||||
|
||||
{% if not domains %}
|
||||
<p class="empty">No domains found. Seed data via CLI first.</p>
|
||||
{% else %}
|
||||
<div class="card-grid">
|
||||
{% for d in domains %}
|
||||
<div class="card">
|
||||
<h2>{{ d.name }}</h2>
|
||||
<p>{{ d.description }}</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Metric</th><th>Weight</th><th>Min</th><th>Max</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mb in d.metric_bounds %}
|
||||
<tr>
|
||||
<td>{{ mb.metric_name }}</td>
|
||||
<td>{{ mb.weight }}</td>
|
||||
<td>{{ mb.norm_min }}</td>
|
||||
<td>{{ mb.norm_max }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
80
src/physcom_web/templates/entities/_dep_table.html
Normal file
80
src/physcom_web/templates/entities/_dep_table.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<table id="dep-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Category</th>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
<th>Unit</th>
|
||||
<th>Type</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dep-table-body">
|
||||
{% for dep in entity.dependencies %}
|
||||
<tr>
|
||||
<td>{{ dep.category }}</td>
|
||||
<td>{{ dep.key }}</td>
|
||||
<td>{{ dep.value }}</td>
|
||||
<td>{{ dep.unit or '—' }}</td>
|
||||
<td><span class="badge badge-{{ dep.constraint_type }}">{{ dep.constraint_type }}</span></td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-sm"
|
||||
onclick="this.closest('tr').querySelector('.edit-row').style.display='table-row'; this.closest('tr').style.display='none'">
|
||||
Edit
|
||||
</button>
|
||||
<form method="post"
|
||||
hx-post="{{ url_for('entities.dep_delete', entity_id=entity.id, dep_id=dep.id) }}"
|
||||
hx-target="#dep-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('entities.dep_edit', entity_id=entity.id, dep_id=dep.id) }}"
|
||||
hx-target="#dep-section" hx-swap="innerHTML">
|
||||
<td><input name="category" value="{{ dep.category }}" required></td>
|
||||
<td><input name="key" value="{{ dep.key }}" required></td>
|
||||
<td><input name="value" value="{{ dep.value }}" required></td>
|
||||
<td><input name="unit" value="{{ dep.unit or '' }}"></td>
|
||||
<td>
|
||||
<select name="constraint_type">
|
||||
{% for ct in ['requires', 'provides', 'range_min', 'range_max', 'excludes'] %}
|
||||
<option value="{{ ct }}" {{ 'selected' if dep.constraint_type == ct }}>{{ ct }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</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 Dependency</h3>
|
||||
<form method="post"
|
||||
hx-post="{{ url_for('entities.dep_add', entity_id=entity.id) }}"
|
||||
hx-target="#dep-section" hx-swap="innerHTML"
|
||||
class="dep-add-form">
|
||||
<div class="form-row">
|
||||
<input name="category" placeholder="category" required>
|
||||
<input name="key" placeholder="key" required>
|
||||
<input name="value" placeholder="value" required>
|
||||
<input name="unit" placeholder="unit">
|
||||
<select name="constraint_type">
|
||||
<option value="requires">requires</option>
|
||||
<option value="provides">provides</option>
|
||||
<option value="range_min">range_min</option>
|
||||
<option value="range_max">range_max</option>
|
||||
<option value="excludes">excludes</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
28
src/physcom_web/templates/entities/detail.html
Normal file
28
src/physcom_web/templates/entities/detail.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ entity.name }} — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>{{ entity.name }}</h1>
|
||||
<div>
|
||||
<a href="{{ url_for('entities.entity_edit', entity_id=entity.id) }}" class="btn">Edit</a>
|
||||
<form method="post" action="{{ url_for('entities.entity_delete', entity_id=entity.id) }}" class="inline-form"
|
||||
onsubmit="return confirm('Delete this entity?')">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<dl>
|
||||
<dt>Dimension</dt><dd>{{ entity.dimension }}</dd>
|
||||
<dt>Description</dt><dd>{{ entity.description or '—' }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h2>Dependencies</h2>
|
||||
|
||||
<div id="dep-section">
|
||||
{% include "entities/_dep_table.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
38
src/physcom_web/templates/entities/form.html
Normal file
38
src/physcom_web/templates/entities/form.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ 'Edit' if entity else 'Add' }} Entity — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ 'Edit' if entity else 'New' }} Entity</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="dimension">Dimension</label>
|
||||
{% if entity %}
|
||||
<input type="text" id="dimension" name="dimension" value="{{ entity.dimension }}" readonly>
|
||||
{% else %}
|
||||
<input type="text" id="dimension" name="dimension" list="dim-list"
|
||||
value="{{ request.form.get('dimension', '') }}" required placeholder="e.g. platform">
|
||||
<datalist id="dim-list">
|
||||
{% for d in dimensions %}
|
||||
<option value="{{ d.name }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name"
|
||||
value="{{ entity.name if entity else request.form.get('name', '') }}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" rows="3">{{ entity.description if entity else request.form.get('description', '') }}</textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'Save' if entity else 'Create' }}</button>
|
||||
<a href="{{ url_for('entities.entity_detail', entity_id=entity.id) if entity else url_for('entities.entity_list') }}" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
43
src/physcom_web/templates/entities/list.html
Normal file
43
src/physcom_web/templates/entities/list.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Entities — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Entities</h1>
|
||||
<a href="{{ url_for('entities.entity_new') }}" class="btn btn-primary">+ Add Entity</a>
|
||||
</div>
|
||||
|
||||
{% if not grouped %}
|
||||
<p class="empty">No entities found. <a href="{{ url_for('entities.entity_new') }}">Add one</a> or seed data via CLI.</p>
|
||||
{% else %}
|
||||
{% for dimension, entities in grouped.items() %}
|
||||
<div class="card">
|
||||
<h2>{{ dimension }}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Deps</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in entities %}
|
||||
<tr>
|
||||
<td>{{ e.id }}</td>
|
||||
<td><a href="{{ url_for('entities.entity_detail', entity_id=e.id) }}">{{ e.name }}</a></td>
|
||||
<td>{{ e.description }}</td>
|
||||
<td>{{ e.dependencies|length }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('entities.entity_edit', entity_id=e.id) }}" class="btn btn-sm">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
60
src/physcom_web/templates/pipeline/run.html
Normal file
60
src/physcom_web/templates/pipeline/run.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Run Pipeline — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Run Pipeline</h1>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('pipeline.pipeline_run') }}">
|
||||
<div class="form-group">
|
||||
<label for="domain">Domain</label>
|
||||
<select name="domain" id="domain" required>
|
||||
<option value="">— select —</option>
|
||||
{% for d in domains %}
|
||||
<option value="{{ d.name }}">{{ d.name }} — {{ d.description }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Passes</legend>
|
||||
<div class="checkbox-row">
|
||||
{% for p in [1, 2, 3, 4, 5] %}
|
||||
<label>
|
||||
<input type="checkbox" name="passes" value="{{ p }}"
|
||||
{{ 'checked' if p <= 3 }}>
|
||||
Pass {{ p }}
|
||||
{% if p == 1 %}(Constraints)
|
||||
{% elif p == 2 %}(Estimation)
|
||||
{% elif p == 3 %}(Scoring)
|
||||
{% elif p == 4 %}(LLM Review)
|
||||
{% elif p == 5 %}(Human Review)
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="threshold">Score Threshold</label>
|
||||
<input type="number" name="threshold" id="threshold" value="0.1" step="0.01" min="0" max="1">
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Dimensions</legend>
|
||||
<div class="checkbox-row">
|
||||
{% for d in dimensions %}
|
||||
<label>
|
||||
<input type="checkbox" name="dimensions" value="{{ d.name }}" checked>
|
||||
{{ d.name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Run Pipeline</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
7
src/physcom_web/templates/results/_review_done.html
Normal file
7
src/physcom_web/templates/results/_review_done.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="card">
|
||||
<p class="flash flash-success">Review saved.</p>
|
||||
<dl>
|
||||
{% if novelty_flag %}<dt>Novelty</dt><dd>{{ novelty_flag }}</dd>{% endif %}
|
||||
{% if human_notes %}<dt>Notes</dt><dd>{{ human_notes }}</dd>{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
22
src/physcom_web/templates/results/_review_form.html
Normal file
22
src/physcom_web/templates/results/_review_form.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<div class="card">
|
||||
<form method="post"
|
||||
hx-post="{{ url_for('results.submit_review', domain_name=domain.name, combo_id=combo.id) }}"
|
||||
hx-target="#review-section" hx-swap="innerHTML">
|
||||
<div class="form-group">
|
||||
<label for="novelty_flag">Novelty Flag</label>
|
||||
<select name="novelty_flag" id="novelty_flag">
|
||||
<option value="">— none —</option>
|
||||
<option value="novel" {{ 'selected' if result and result.novelty_flag == 'novel' }}>novel</option>
|
||||
<option value="exists" {{ 'selected' if result and result.novelty_flag == 'exists' }}>exists</option>
|
||||
<option value="researched" {{ 'selected' if result and result.novelty_flag == 'researched' }}>researched</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="human_notes">Notes</label>
|
||||
<textarea name="human_notes" id="human_notes" rows="3">{{ result.human_notes if result and result.human_notes else '' }}</textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Review</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
82
src/physcom_web/templates/results/detail.html
Normal file
82
src/physcom_web/templates/results/detail.html
Normal file
@@ -0,0 +1,82 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Combination #{{ combo.id }} — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Combination #{{ combo.id }}</h1>
|
||||
<a href="{{ url_for('results.results_domain', domain_name=domain.name) }}" class="btn">Back to list</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<dl>
|
||||
<dt>Domain</dt><dd>{{ domain.name }}</dd>
|
||||
<dt>Status</dt><dd><span class="badge badge-{{ combo.status }}">{{ combo.status }}</span></dd>
|
||||
{% if combo.block_reason %}
|
||||
<dt>Block Reason</dt><dd>{{ combo.block_reason }}</dd>
|
||||
{% endif %}
|
||||
{% if result %}
|
||||
<dt>Composite Score</dt><dd class="score-cell">{{ "%.4f"|format(result.composite_score) }}</dd>
|
||||
<dt>Pass Reached</dt><dd>{{ result.pass_reached }}</dd>
|
||||
{% if result.novelty_flag %}
|
||||
<dt>Novelty</dt><dd>{{ result.novelty_flag }}</dd>
|
||||
{% endif %}
|
||||
{% if result.llm_review %}
|
||||
<dt>LLM Review</dt><dd>{{ result.llm_review }}</dd>
|
||||
{% endif %}
|
||||
{% if result.human_notes %}
|
||||
<dt>Human Notes</dt><dd>{{ result.human_notes }}</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<h2>Entities</h2>
|
||||
<div class="card-grid">
|
||||
{% for e in combo.entities %}
|
||||
<div class="card">
|
||||
<h3><a href="{{ url_for('entities.entity_detail', entity_id=e.id) }}">{{ e.name }}</a></h3>
|
||||
<p class="subtitle">{{ e.dimension }}</p>
|
||||
<p>{{ e.description }}</p>
|
||||
<table class="compact">
|
||||
<thead><tr><th>Key</th><th>Value</th><th>Type</th></tr></thead>
|
||||
<tbody>
|
||||
{% for dep in e.dependencies %}
|
||||
<tr>
|
||||
<td>{{ dep.key }}</td>
|
||||
<td>{{ dep.value }}{{ ' ' + dep.unit if dep.unit else '' }}</td>
|
||||
<td><span class="badge badge-{{ dep.constraint_type }}">{{ dep.constraint_type }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if scores %}
|
||||
<h2>Per-Metric Scores</h2>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Metric</th><th>Raw Value</th><th>Normalized</th><th>Method</th><th>Confidence</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in scores %}
|
||||
<tr>
|
||||
<td>{{ s.metric_name }}</td>
|
||||
<td>{{ "%.2f"|format(s.raw_value) if s.raw_value is not none else '—' }}</td>
|
||||
<td class="score-cell">{{ "%.4f"|format(s.normalized_score) if s.normalized_score is not none else '—' }}</td>
|
||||
<td>{{ s.estimation_method or '—' }}</td>
|
||||
<td>{{ "%.2f"|format(s.confidence) if s.confidence is not none else '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>Human Review</h2>
|
||||
<div id="review-section">
|
||||
{% include "results/_review_form.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
69
src/physcom_web/templates/results/list.html
Normal file
69
src/physcom_web/templates/results/list.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Results — PhysCom{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Results</h1>
|
||||
|
||||
<div class="form-row" style="margin-bottom:1rem">
|
||||
{% for d in domains %}
|
||||
<a href="{{ url_for('results.results_domain', domain_name=d.name) }}"
|
||||
class="btn {{ 'btn-primary' if domain and domain.name == d.name else '' }}">
|
||||
{{ d.name }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if domain and results is not none %}
|
||||
<div class="card">
|
||||
<h2>{{ domain.name }} <span class="subtitle">{{ domain.description }}</span></h2>
|
||||
|
||||
{% if statuses %}
|
||||
<div class="filter-row">
|
||||
<span>Filter:</span>
|
||||
<a href="{{ url_for('results.results_domain', domain_name=domain.name) }}"
|
||||
class="btn btn-sm {{ '' if status_filter else 'btn-primary' }}">All</a>
|
||||
{% for s, cnt in statuses.items() %}
|
||||
<a href="{{ url_for('results.results_domain', domain_name=domain.name, status=s) }}"
|
||||
class="btn btn-sm {{ 'btn-primary' if status_filter == s else '' }}">
|
||||
{{ s }} ({{ cnt }})
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not results %}
|
||||
<p class="empty">No results yet. <a href="{{ url_for('pipeline.pipeline_form') }}">Run the pipeline</a> first.</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Score</th>
|
||||
<th>Entities</th>
|
||||
<th>Status</th>
|
||||
<th>Novelty</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in results %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td class="score-cell">{{ "%.4f"|format(r.composite_score) }}</td>
|
||||
<td>{{ r.combination.entities|map(attribute='name')|join(' + ') }}</td>
|
||||
<td><span class="badge badge-{{ r.combination.status }}">{{ r.combination.status }}</span></td>
|
||||
<td>{{ r.novelty_flag or '—' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('results.result_detail', domain_name=domain.name, combo_id=r.combination.id) }}"
|
||||
class="btn btn-sm">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif not domain %}
|
||||
<p class="empty">Select a domain above to view results.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user