Skip to main content

max / makenotwork

7.3 KB · 161 lines History Blame Raw
1 {% extends "base.html" %}
2
3 {% block title %}Admin: Scan Pipeline - Makenotwork{% endblock %}
4
5 {# htmx 4 dropped `hx-prompt` from core and ships it as an extension with the
6 same syntax. This is the only page that uses it, so it links the script here
7 rather than from the shell; the version is the extension's own, since a
8 vendored file changes only when htmx is bumped.
9
10 `defer` is load-bearing rather than a habit: the shell's htmx is deferred, so
11 an ordinary script here would run during parsing and call `registerExtension`
12 before htmx exists. Deferred scripts run in document order, and htmx's own
13 init waits a tick past that for exactly this. #}
14 {% block head %}
15 <script src="/static/hx-prompt.min.js?v=4.0.0-beta6" defer></script>
16 {% endblock %}
17
18 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} admin-page"{% endblock %}
19
20 {% block content %}
21 {% include "partials/site_header.html" %}
22
23 <div class="container">
24 {% include "partials/admin_nav.html" %}
25
26 <h1 class="page-title">Scan Pipeline</h1>
27
28 <section class="admin-scan-section">
29 <div class="admin-section-header">
30 <h2 class="subsection-title">Pipeline Health</h2>
31 <span class="dimmed text-sm">last 24h</span>
32 </div>
33 <div class="layer-health-grid">
34 {% for card in layer_health %}
35 <div class="layer-health-card layer-{{ card.status_badge }}">
36 <div class="layer-name">{{ card.layer }}</div>
37 <div class="layer-stat">
38 <span class="dimmed">success</span> {{ card.success_rate_pct }}%
39 </div>
40 <div class="layer-stat">
41 <span class="dimmed">error</span> {{ card.error_rate_pct }}%
42 </div>
43 <div class="layer-stat">
44 <span class="dimmed">fails</span> {{ card.fail_count }}
45 </div>
46 <div class="layer-stat text-xs">
47 last clean: {{ card.last_seen }}
48 </div>
49 </div>
50 {% endfor %}
51 </div>
52 </section>
53
54 <section class="admin-scan-section">
55 <div class="admin-section-header">
56 <h2 class="subsection-title">Active Queue</h2>
57 <span class="dimmed text-sm">auto-refresh every 10s</span>
58 </div>
59 <div class="stats-row"
60 hx-get="/admin/uploads/queue-summary"
61 hx-trigger="every 10s"
62 hx-swap="outerHTML">
63 <div class="stat-box">
64 <div class="number">{{ queue_pending }}</div>
65 <div class="label">Pending</div>
66 </div>
67 <div class="stat-box">
68 <div class="number">{{ queue_running }}</div>
69 <div class="label">Scanning</div>
70 </div>
71 </div>
72 </section>
73
74 <section class="admin-scan-section">
75 <div class="admin-section-header">
76 <h2 class="subsection-title">Audit Log</h2>
77 <a href="/admin/uploads/audit" class="text-sm">View full log</a>
78 </div>
79 <p class="dimmed text-sm">Every promote, quarantine, and re-scan is recorded.</p>
80 </section>
81
82 <section class="admin-scan-section">
83 <div class="admin-section-header">
84 <h2 class="subsection-title">Held for Review</h2>
85 <div>
86 <span class="dimmed text-sm">{{ total_held }} held</span>
87 {% if total_held > 0 %}
88 <button class="btn-secondary small"
89 hx-post="/api/admin/uploads/bulk/rescan"
90 hx-target="#uploads-table"
91 hx-swap="innerHTML"
92 hx-confirm="Re-scan every held file under the current pipeline? Workers will pick them up shortly.">
93 Re-scan all held
94 </button>
95 <button class="btn-secondary small"
96 hx-post="/api/admin/uploads/bulk/promote"
97 hx-target="#uploads-table"
98 hx-swap="innerHTML"
99 hx-prompt="Note explaining why every held file is safe to clear (required, audit-logged):">
100 Promote all held
101 </button>
102 {% endif %}
103 </div>
104 </div>
105 <div id="uploads-table">
106 {% include "partials/admin_upload_entries.html" %}
107 </div>
108 </section>
109
110 <section class="admin-scan-section">
111 <details class="scan-history-grid">
112 <summary>
113 Recent History
114 <span class="dimmed text-sm">last 7 days, {{ history_total }} scan{% if history_total != 1 %}s{% endif %}</span>
115 </summary>
116 {% if recent_history.is_empty() %}
117 <div class="dimmed text-sm py-section">No scans recorded in the last 7 days.</div>
118 {% else %}
119 <div class="scroll-x mt-section">
120 <table class="compact-table" aria-label="Recent scan results">
121 <thead>
122 <tr>
123 <th>Scanned</th>
124 <th>File</th>
125 <th>Status</th>
126 <th>Layers</th>
127 <th class="text-right">Size</th>
128 <th>SHA-256</th>
129 </tr>
130 </thead>
131 <tbody>
132 {% for row in recent_history %}
133 <tr>
134 <td class="text-sm nowrap">{{ row.scanned_at }}</td>
135 <td class="text-sm" title="{{ row.s3_key_short }}"><code>{{ row.s3_key_short }}</code></td>
136 <td class="text-sm">
137 <span class="layer-chip layer-chip-{% if row.scan_status == "clean" %}pass{% else if row.scan_status == "quarantined" %}fail{% else if row.scan_status == "held_for_review" %}error{% else %}skip{% endif %}">
138 {{ row.scan_status }}
139 </span>
140 </td>
141 <td class="layer-chips">
142 {% for chip in row.layers %}
143 <span class="layer-chip layer-chip-{{ chip.verdict }}"
144 title="{{ chip.layer }}: {{ chip.verdict }}{% if let Some(d) = chip.detail %}, {{ d }}{% endif %}">
145 {{ chip.layer }}
146 </span>
147 {% endfor %}
148 </td>
149 <td class="text-sm text-right nowrap">{{ row.size_bytes }}</td>
150 <td class="text-xs nowrap"><code>{{ row.sha256_short }}</code></td>
151 </tr>
152 {% endfor %}
153 </tbody>
154 </table>
155 </div>
156 {% endif %}
157 </details>
158 </section>
159 </div>
160 {% endblock %}
161