feat(pipeline): extractions tab template

- Filterable extraction run history table (extractor + status dropdowns,
  HTMX live filter via 'change' trigger)
- Status badges with stale row highlighting (amber background)
- 'Mark Failed' button for stuck 'running' rows (with confirm dialog)
- 'Run All Extractors' trigger button
- Pagination via hx-vals

Subtask 3 of 6

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-25 12:53:36 +01:00
parent cac876e48f
commit 947a1a778e

View File

@@ -0,0 +1,135 @@
<!-- Pipeline Extractions Tab: filterable run history + mark-stale + trigger -->
<!-- Filter bar + trigger button -->
<div class="card mb-4" style="padding:0.875rem 1.25rem">
<div class="flex items-center justify-between gap-3 flex-wrap">
<form id="extraction-filters"
hx-get="{{ url_for('pipeline.pipeline_extractions') }}"
hx-target="#pipeline-tab-content"
hx-swap="innerHTML"
hx-trigger="change"
class="flex gap-3 items-end flex-wrap">
<div>
<label class="form-label">Extractor</label>
<select name="extractor" class="form-input" style="min-width:180px">
<option value="" {% if not extractor_filter %}selected{% endif %}>All extractors</option>
{% for name in extractors %}
<option value="{{ name }}" {% if extractor_filter == name %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</div>
<div>
<label class="form-label">Status</label>
<select name="status" class="form-input">
<option value="" {% if not status_filter %}selected{% endif %}>All statuses</option>
<option value="success" {% if status_filter == 'success' %}selected{% endif %}>Success</option>
<option value="failed" {% if status_filter == 'failed' %}selected{% endif %}>Failed</option>
<option value="running" {% if status_filter == 'running' %}selected{% endif %}>Running</option>
</select>
</div>
</form>
<form method="post" action="{{ url_for('pipeline.pipeline_trigger_extract') }}" class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="button" class="btn-outline btn-sm"
onclick="confirmAction('Enqueue a full extraction run? This will run all extractors in the background.', this.closest('form'))">
Run All Extractors
</button>
</form>
</div>
</div>
<!-- Run history -->
<div class="card">
<div class="flex justify-between items-center mb-3">
<p class="text-sm text-slate">
Showing {{ runs | length }} of {{ total }} run{{ 's' if total != 1 }}
{% if extractor_filter or status_filter %} (filtered){% endif %}
</p>
{% if total_pages > 1 %}
<div class="flex gap-2 items-center">
{% if page > 1 %}
<button class="btn-outline btn-sm"
hx-get="{{ url_for('pipeline.pipeline_extractions') }}"
hx-vals='{"page": "{{ page - 1 }}", "extractor": "{{ extractor_filter }}", "status": "{{ status_filter }}"}'
hx-target="#pipeline-tab-content" hx-swap="innerHTML">← Prev</button>
{% endif %}
<span class="text-xs text-slate">{{ page }} / {{ total_pages }}</span>
{% if page < total_pages %}
<button class="btn-outline btn-sm"
hx-get="{{ url_for('pipeline.pipeline_extractions') }}"
hx-vals='{"page": "{{ page + 1 }}", "extractor": "{{ extractor_filter }}", "status": "{{ status_filter }}"}'
hx-target="#pipeline-tab-content" hx-swap="innerHTML">Next →</button>
{% endif %}
</div>
{% endif %}
</div>
{% if runs %}
<div style="overflow-x:auto">
<table class="table">
<thead>
<tr>
<th style="width:60px">#</th>
<th>Extractor</th>
<th>Started</th>
<th>Duration</th>
<th>Status</th>
<th style="text-align:right">Files</th>
<th style="text-align:right">Size</th>
<th>Error</th>
<th></th>
</tr>
</thead>
<tbody>
{% for run in runs %}
<tr{% if run.is_stale %} style="background:#FFFBEB"{% endif %}>
<td class="mono text-xs text-slate">#{{ run.run_id }}</td>
<td class="text-sm font-medium">{{ run.extractor }}</td>
<td class="mono text-xs">{{ run.started_at[:16].replace('T', ' ') if run.started_at else '—' }}</td>
<td class="mono text-xs text-slate">{{ run.duration or '—' }}</td>
<td>
{% if run.is_stale %}
<span class="badge-warning">stale</span>
{% elif run.status == 'success' %}
<span class="badge-success">success</span>
{% elif run.status == 'failed' %}
<span class="badge-danger">failed</span>
{% else %}
<span class="badge" style="background:#EFF6FF;color:#1D4ED8">{{ run.status }}</span>
{% endif %}
</td>
<td class="text-right text-xs">{{ run.files_written or 0 }}</td>
<td class="text-right mono text-xs">{{ run.bytes_label }}</td>
<td style="max-width:200px">
{% if run.error_message %}
<span class="text-xs text-danger mono" style="word-break:break-all" title="{{ run.error_message }}">
{{ run.error_message[:60] }}{% if run.error_message|length > 60 %}…{% endif %}
</span>
{% endif %}
</td>
<td>
{% if run.status == 'running' %}
<form method="post"
action="{{ url_for('pipeline.pipeline_mark_stale', run_id=run.run_id) }}"
class="m-0">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="button" class="btn-danger btn-sm"
style="padding:2px 8px;font-size:11px"
onclick="confirmAction('Mark run #{{ run.run_id }} as failed? Only do this if the process is definitely dead.', this.closest('form'))">
Mark Failed
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-sm text-slate text-center" style="padding:2rem 0">
No extraction runs found{% if extractor_filter or status_filter %} matching the current filters{% endif %}.
</p>
{% endif %}
</div>