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:
@@ -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>
|
||||||
Reference in New Issue
Block a user