pytest-api-core 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,469 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="{{ theme }}">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>{{ report_title }}</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f8fafc; --surface: #ffffff; --border: #e2e8f0;
10
+ --text: #1e293b; --text-muted: #64748b;
11
+ --pass: #22c55e; --fail: #ef4444; --error: #f97316; --skip: #94a3b8;
12
+ --code-bg: #0f172a; --code-text: #e2e8f0;
13
+ --shadow: 0 1px 3px rgba(0,0,0,.1);
14
+ --method-get: #3b82f6; --method-post: #22c55e; --method-put: #f59e0b;
15
+ --method-patch: #a855f7; --method-delete: #ef4444; --method-other: #64748b;
16
+ }
17
+ [data-theme="dark"] {
18
+ --bg: #0f172a; --surface: #1e293b; --border: #334155;
19
+ --text: #f1f5f9; --text-muted: #94a3b8;
20
+ --code-bg: #020617; --code-text: #e2e8f0;
21
+ --shadow: 0 1px 3px rgba(0,0,0,.4);
22
+ }
23
+ * { box-sizing: border-box; margin: 0; padding: 0; }
24
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
25
+ background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }
26
+ header { background: var(--surface); border-bottom: 1px solid var(--border);
27
+ padding: 16px 24px; display: flex; align-items: center; justify-content: space-between; }
28
+ header h1 { font-size: 20px; font-weight: 700; letter-spacing: -.3px; }
29
+ header small { color: var(--text-muted); margin-left: 10px; font-weight: 400; font-size: 12px; }
30
+ .theme-btn { background: none; border: 1px solid var(--border); border-radius: 6px;
31
+ padding: 6px 12px; cursor: pointer; color: var(--text); font-size: 13px; }
32
+ .main { max-width: 1280px; margin: 24px auto; padding: 0 24px; }
33
+
34
+ /* ── Summary card ───────────────────────────────────────────────────── */
35
+ .summary-grid { display: grid; grid-template-columns: auto 1fr; gap: 24px;
36
+ align-items: center; background: var(--surface); border-radius: 12px;
37
+ padding: 24px; box-shadow: var(--shadow); margin-bottom: 24px; }
38
+ .donut-wrap { display: flex; flex-direction: column; align-items: center; gap: 8px; }
39
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; }
40
+ .stat-card { background: var(--bg); border-radius: 8px; padding: 14px 16px;
41
+ border: 1px solid var(--border); text-align: center; }
42
+ .stat-card .stat-value { font-size: 28px; font-weight: 700; line-height: 1; }
43
+ .stat-card .stat-label { font-size: 11px; text-transform: uppercase;
44
+ letter-spacing: .8px; color: var(--text-muted); margin-top: 4px; }
45
+ .stat-card.pass .stat-value { color: var(--pass); }
46
+ .stat-card.fail .stat-value { color: var(--fail); }
47
+ .stat-card.error .stat-value { color: var(--error); }
48
+ .stat-card.skip .stat-value { color: var(--skip); }
49
+
50
+ /* ── Controls ───────────────────────────────────────────────────────── */
51
+ .controls { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
52
+ .search-box { flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid var(--border);
53
+ border-radius: 6px; background: var(--surface); color: var(--text); font-size: 13px; }
54
+ .filter-btn { padding: 7px 16px; border: 1px solid var(--border); border-radius: 6px;
55
+ background: var(--surface); color: var(--text-muted); cursor: pointer;
56
+ font-size: 13px; transition: all .15s; }
57
+ .filter-btn.active { background: var(--text); color: var(--bg); border-color: var(--text); }
58
+
59
+ /* ── Results table ──────────────────────────────────────────────────── */
60
+ table { width: 100%; border-collapse: collapse; background: var(--surface);
61
+ border-radius: 12px; overflow: hidden; box-shadow: var(--shadow); }
62
+ thead tr { background: var(--bg); border-bottom: 2px solid var(--border); }
63
+ th { padding: 10px 14px; text-align: left; font-size: 11px; text-transform: uppercase;
64
+ letter-spacing: .7px; color: var(--text-muted); }
65
+ td { padding: 10px 14px; border-bottom: 1px solid var(--border); vertical-align: top; }
66
+ .result-row:hover { background: var(--bg); }
67
+ .result-row.failed { border-left: 3px solid var(--fail); }
68
+ .result-row.error { border-left: 3px solid var(--error); }
69
+ .result-row.passed { border-left: 3px solid var(--pass); }
70
+ .result-row.skipped { border-left: 3px solid var(--skip); }
71
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
72
+ font-size: 11px; font-weight: 700; letter-spacing: .5px; }
73
+ .badge-pass { background: #dcfce7; color: #15803d; }
74
+ .badge-fail { background: #fee2e2; color: #dc2626; }
75
+ .badge-error { background: #ffedd5; color: #c2410c; }
76
+ .badge-skip { background: #f1f5f9; color: #475569; }
77
+ .test-name { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px;
78
+ max-width: 600px; overflow-wrap: break-word; }
79
+ .duration { color: var(--text-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
80
+ .marker { display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 10px;
81
+ font-size: 10px; background: #ede9fe; color: #6d28d9; }
82
+ .test-chevron { display: inline-block; font-size: 10px; color: #64748b;
83
+ transition: transform .2s; margin-right: 8px; vertical-align: middle; }
84
+ .test-chevron.open { transform: rotate(90deg); }
85
+
86
+ /* ── Detail panel ───────────────────────────────────────────────────── */
87
+ .detail-row td { padding: 0; }
88
+ .detail-body { background: var(--code-bg); padding: 16px; display: flex;
89
+ flex-direction: column; gap: 12px; }
90
+ .detail-section { }
91
+ .detail-label { font-size: 10px; text-transform: uppercase; letter-spacing: .8px;
92
+ color: #64748b; margin-bottom: 6px; font-weight: 600; }
93
+ .log-label { color: #f59e0b; }
94
+ .code-block { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px;
95
+ color: var(--code-text); white-space: pre-wrap; overflow-wrap: break-word;
96
+ max-height: 300px; overflow-y: auto; }
97
+ .log-block { color: #fde68a; }
98
+
99
+ /* ── HTTP call banner ───────────────────────────────────────────────── */
100
+ .http-banner { border: 1px solid #334155; border-radius: 8px; overflow: hidden; }
101
+ .http-headline { display: flex; align-items: center; gap: 10px; padding: 10px 14px;
102
+ background: #1e293b; cursor: pointer; user-select: none; }
103
+ .http-headline:hover { background: #253347; }
104
+ .method-badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
105
+ font-size: 11px; font-weight: 800; letter-spacing: .6px; color: #fff; }
106
+ .method-GET { background: var(--method-get); }
107
+ .method-POST { background: var(--method-post); }
108
+ .method-PUT { background: var(--method-put); }
109
+ .method-PATCH { background: var(--method-patch); }
110
+ .method-DELETE { background: var(--method-delete); }
111
+ .method-HEAD, .method-OPTIONS { background: var(--method-other); }
112
+ .http-url { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px;
113
+ color: #e2e8f0; flex: 1; overflow-wrap: anywhere; }
114
+ .status-chip { padding: 2px 10px; border-radius: 12px; font-size: 11px; font-weight: 700; }
115
+ .status-2xx { background: #14532d; color: #86efac; }
116
+ .status-3xx { background: #1e3a5f; color: #93c5fd; }
117
+ .status-4xx { background: #7f1d1d; color: #fca5a5; }
118
+ .status-5xx { background: #7c2d12; color: #fdba74; }
119
+ .elapsed-chip { font-size: 11px; color: #94a3b8; white-space: nowrap; }
120
+ .http-chevron { font-size: 10px; color: #64748b; transition: transform .2s; }
121
+ .http-chevron.open { transform: rotate(90deg); }
122
+
123
+ .http-body { display: none; background: var(--code-bg); }
124
+ .http-body.open { display: block; }
125
+ .http-tabs { display: flex; gap: 0; border-bottom: 1px solid #334155; }
126
+ .http-tab { padding: 6px 14px; font-size: 11px; color: #64748b; cursor: pointer;
127
+ border-bottom: 2px solid transparent; }
128
+ .http-tab.active { color: #e2e8f0; border-bottom-color: #3b82f6; }
129
+ .http-tab-content { display: none; padding: 12px 14px; }
130
+ .http-tab-content.active { display: block; }
131
+ .http-kv-table { width: 100%; border-collapse: collapse; }
132
+ .http-kv-table th { color: #cbd5e1; font-size: 10px; text-transform: uppercase;
133
+ letter-spacing: .6px; padding: 6px 10px; text-align: left; font-weight: 600; }
134
+ .http-kv-table td { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px;
135
+ color: #cbd5e1; padding: 6px 10px; vertical-align: top; word-break: break-all; }
136
+ .http-kv-table tr:nth-child(even) td { background: rgba(255,255,255,.05); }
137
+ .http-kv-table td:first-child { color: #94c7ff; font-weight: 500; width: 35%; }
138
+ .http-body-pre { font-family: "SF Mono", "Fira Code", monospace; font-size: 12px;
139
+ color: #cbd5e1; white-space: pre-wrap; overflow-wrap: break-word;
140
+ max-height: 300px; overflow-y: auto; padding: 8px; }
141
+
142
+ /* ── Assertion banners ──────────────────────────────────────────────── */
143
+ .assert-list { display: flex; flex-direction: column; gap: 4px; }
144
+ .assert-row { display: flex; align-items: flex-start; gap: 8px; padding: 6px 10px;
145
+ border-radius: 6px; font-size: 12px; }
146
+ .assert-pass { background: rgba(34,197,94,.1); border: 1px solid rgba(34,197,94,.25); }
147
+ .assert-fail { background: rgba(239,68,68,.12); border: 1px solid rgba(239,68,68,.35); }
148
+ .assert-icon { font-size: 13px; line-height: 1.4; flex-shrink: 0; }
149
+ .assert-name { font-family: "SF Mono", "Fira Code", monospace; color: #e2e8f0;
150
+ font-weight: 600; flex-shrink: 0; min-width: 140px; }
151
+ .assert-detail { color: #94a3b8; font-family: "SF Mono", "Fira Code", monospace; }
152
+ .assert-detail .exp { color: #86efac; }
153
+ .assert-detail .got { color: #fca5a5; }
154
+
155
+ footer { text-align: center; padding: 24px; color: var(--text-muted); font-size: 12px; }
156
+ </style>
157
+ </head>
158
+ <body>
159
+ <header>
160
+ <div>
161
+ <h1>{{ report_header }}
162
+ <small>Generated {{ generated_at }} &nbsp;|&nbsp; Duration: {{ duration }}</small>
163
+ </h1>
164
+ </div>
165
+ <button class="theme-btn" onclick="toggleTheme()">🌙 Dark</button>
166
+ </header>
167
+
168
+ <div class="main">
169
+ <!-- Summary -->
170
+ <div class="summary-grid">
171
+ <div class="donut-wrap">
172
+ {{ donut_svg | safe }}
173
+ <div style="font-size:12px;color:var(--text-muted)">
174
+ Pass rate: <strong>{{ pass_rate }}%</strong>
175
+ </div>
176
+ </div>
177
+ <div class="stats-grid">
178
+ <div class="stat-card">
179
+ <div class="stat-value">{{ total }}</div><div class="stat-label">Total</div>
180
+ </div>
181
+ <div class="stat-card pass">
182
+ <div class="stat-value">{{ passed }}</div><div class="stat-label">Passed</div>
183
+ </div>
184
+ <div class="stat-card fail">
185
+ <div class="stat-value">{{ failed }}</div><div class="stat-label">Failed</div>
186
+ </div>
187
+ <div class="stat-card error">
188
+ <div class="stat-value">{{ error }}</div><div class="stat-label">Errors</div>
189
+ </div>
190
+ <div class="stat-card skip">
191
+ <div class="stat-value">{{ skipped }}</div><div class="stat-label">Skipped</div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Filters -->
197
+ <div class="controls">
198
+ <input class="search-box" type="text" placeholder="Search tests…"
199
+ oninput="filterTable()" id="searchBox"/>
200
+ <button class="filter-btn active" onclick="setFilter('all', this)">All</button>
201
+ <button class="filter-btn" onclick="setFilter('passed', this)">Passed</button>
202
+ <button class="filter-btn" onclick="setFilter('failed', this)">Failed</button>
203
+ <button class="filter-btn" onclick="setFilter('error', this)">Error</button>
204
+ <button class="filter-btn" onclick="setFilter('skipped', this)">Skipped</button>
205
+ <div style="flex: 1"></div>
206
+ <button class="filter-btn" onclick="expandAll()">▼ Expand All</button>
207
+ <button class="filter-btn" onclick="collapseAll()">▲ Collapse All</button>
208
+ </div>
209
+
210
+ <!-- Results table -->
211
+ <table id="resultsTable">
212
+ <thead>
213
+ <tr>
214
+ <th style="width:90px">Status</th>
215
+ <th>Test ID</th>
216
+ <th style="width:90px">Duration</th>
217
+ <th style="width:80px">Calls</th>
218
+ </tr>
219
+ </thead>
220
+ <tbody id="tableBody">
221
+ {% for rec in records %}
222
+ {% set idx = loop.index0 %}
223
+ {% set has_detail = rec.stdout or rec.stderr or rec.logs or rec.longrepr or rec.api_calls or rec.assertions %}
224
+ <tr class="result-row {{ rec.outcome }}"
225
+ {% if has_detail %}onclick="toggleDetail('detail-{{ idx }}')"
226
+ style="cursor:pointer"{% endif %}
227
+ data-outcome="{{ rec.outcome }}">
228
+ <td>
229
+ <span class="badge badge-{{ rec.outcome[:4] if rec.outcome != 'unknown' else 'skip' }}
230
+ {%- if rec.outcome == 'passed' %} badge-pass
231
+ {%- elif rec.outcome == 'failed' %} badge-fail
232
+ {%- elif rec.outcome == 'error' %} badge-error
233
+ {%- elif rec.outcome == 'skipped' %} badge-skip
234
+ {%- else %} badge-skip{% endif %}">
235
+ {{ rec.outcome | upper }}
236
+ </span>
237
+ </td>
238
+ <td class="test-name">
239
+ {% if has_detail %}
240
+ <span class="test-chevron" id="chev-detail-{{ idx }}">▶</span>
241
+ {% endif %}
242
+ {{ rec.node_id }}
243
+ {% for m in rec.markers %}
244
+ <span class="marker">{{ m }}</span>
245
+ {% endfor %}
246
+ </td>
247
+ <td class="duration">{{ "%.3f" | format(rec.duration) }}s</td>
248
+ <td style="color:var(--text-muted);font-size:12px">
249
+ {{ rec.api_calls | length }}
250
+ </td>
251
+ </tr>
252
+ {% if has_detail %}
253
+ <tr id="detail-{{ idx }}" class="detail-row" style="display:none">
254
+ <td colspan="4">
255
+ <div class="detail-body">
256
+
257
+ {# ── HTTP call banners ──────────────────────────────────── #}
258
+ {% for call in rec.api_calls %}
259
+ {% set cid = "call-" ~ idx ~ "-" ~ loop.index0 %}
260
+ {% set sc = call.status | string %}
261
+ {% if sc.startswith('2') %}{% set sc_cls = "status-2xx" %}
262
+ {% elif sc.startswith('3') %}{% set sc_cls = "status-3xx" %}
263
+ {% elif sc.startswith('4') %}{% set sc_cls = "status-4xx" %}
264
+ {% else %}{% set sc_cls = "status-5xx" %}{% endif %}
265
+ <div class="http-banner">
266
+ <div class="http-headline" onclick="toggleHttp('{{ cid }}', this)">
267
+ <span class="method-badge method-{{ call.method }}">{{ call.method }}</span>
268
+ <span class="http-url">{{ call.url }}</span>
269
+ <span class="status-chip {{ sc_cls }}">{{ call.status }}</span>
270
+ <span class="elapsed-chip">{{ call.elapsed_ms }} ms</span>
271
+ <span class="http-chevron" id="chev-{{ cid }}">▶</span>
272
+ </div>
273
+ <div class="http-body" id="{{ cid }}">
274
+ <div class="http-tabs">
275
+ <div class="http-tab active" onclick="switchTab(this, 'req-h-{{ cid }}')">Req Headers</div>
276
+ <div class="http-tab" onclick="switchTab(this, 'req-b-{{ cid }}')">Req Body</div>
277
+ <div class="http-tab" onclick="switchTab(this, 'res-h-{{ cid }}')">Res Headers</div>
278
+ <div class="http-tab" onclick="switchTab(this, 'res-b-{{ cid }}')">Res Body</div>
279
+ </div>
280
+
281
+ <div id="req-h-{{ cid }}" class="http-tab-content active">
282
+ <table class="http-kv-table">
283
+ <tr><th>Header</th><th>Value</th></tr>
284
+ {% for k, v in call.req_headers.items() %}
285
+ <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
286
+ {% else %}
287
+ <tr><td colspan="2" style="color:#64748b">—</td></tr>
288
+ {% endfor %}
289
+ </table>
290
+ </div>
291
+
292
+ <div id="req-b-{{ cid }}" class="http-tab-content">
293
+ <pre class="http-body-pre">{{ call.req_body if call.req_body else "—" }}</pre>
294
+ </div>
295
+
296
+ <div id="res-h-{{ cid }}" class="http-tab-content">
297
+ <table class="http-kv-table">
298
+ <tr><th>Header</th><th>Value</th></tr>
299
+ {% for k, v in call.res_headers.items() %}
300
+ <tr><td>{{ k }}</td><td>{{ v }}</td></tr>
301
+ {% else %}
302
+ <tr><td colspan="2" style="color:#64748b">—</td></tr>
303
+ {% endfor %}
304
+ </table>
305
+ </div>
306
+
307
+ <div id="res-b-{{ cid }}" class="http-tab-content">
308
+ <pre class="http-body-pre">{{ call.res_body if call.res_body else "—" }}</pre>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ {% endfor %}
313
+
314
+ {# ── Assertion banners ──────────────────────────────────── #}
315
+ {% if rec.assertions %}
316
+ <div class="detail-section">
317
+ <div class="detail-label">ASSERTIONS</div>
318
+ <div class="assert-list">
319
+ {% for a in rec.assertions %}
320
+ <div class="assert-row {% if a.passed %}assert-pass{% else %}assert-fail{% endif %}">
321
+ <span class="assert-icon">{% if a.passed %}✅{% else %}❌{% endif %}</span>
322
+ <span class="assert-name">{{ a.name }}</span>
323
+ <span class="assert-detail">
324
+ {% if a.expected is defined and a.actual is defined %}
325
+ expected=<span class="exp">{{ a.expected }}</span>
326
+ &nbsp; actual=<span class="got {% if not a.passed %}got{% endif %}">{{ a.actual }}</span>
327
+ {% elif a.key is defined %}
328
+ key=<span class="exp">{{ a.key }}</span>
329
+ {% if a.expected is defined %}
330
+ expected=<span class="exp">{{ a.expected }}</span>
331
+ &nbsp; actual=<span class="{% if not a.passed %}got{% else %}exp{% endif %}">{{ a.actual }}</span>
332
+ {% endif %}
333
+ {% elif a.actual is defined %}
334
+ actual=<span class="{% if a.passed %}exp{% else %}got{% endif %}">{{ a.actual }}</span>
335
+ {% elif a.max_ms is defined %}
336
+ max={{ a.max_ms }}ms &nbsp; actual=<span class="{% if a.passed %}exp{% else %}got{% endif %}">{{ a.actual_ms }}ms</span>
337
+ {% endif %}
338
+ </span>
339
+ </div>
340
+ {% endfor %}
341
+ </div>
342
+ </div>
343
+ {% endif %}
344
+
345
+ {# ── Captured logs ──────────────────────────────────────── #}
346
+ {% if rec.logs %}
347
+ <div class="detail-section">
348
+ <div class="detail-label log-label">LOGS</div>
349
+ <pre class="code-block log-block">{{ rec.logs }}</pre>
350
+ </div>
351
+ {% endif %}
352
+
353
+ {# ── Stdout / stderr ────────────────────────────────────── #}
354
+ {% if rec.stdout %}
355
+ <div class="detail-section">
356
+ <div class="detail-label">STDOUT</div>
357
+ <pre class="code-block">{{ rec.stdout }}</pre>
358
+ </div>
359
+ {% endif %}
360
+ {% if rec.stderr %}
361
+ <div class="detail-section">
362
+ <div class="detail-label">STDERR</div>
363
+ <pre class="code-block">{{ rec.stderr }}</pre>
364
+ </div>
365
+ {% endif %}
366
+
367
+ {# ── Failure traceback ──────────────────────────────────── #}
368
+ {% if rec.longrepr %}
369
+ <div class="detail-section">
370
+ <div class="detail-label" style="color:#f87171">FAILURE</div>
371
+ <pre class="code-block" style="color:#fca5a5">{{ rec.longrepr }}</pre>
372
+ </div>
373
+ {% endif %}
374
+
375
+ </div>
376
+ </td>
377
+ </tr>
378
+ {% endif %}
379
+ {% endfor %}
380
+ </tbody>
381
+ </table>
382
+ </div>
383
+
384
+ <footer>pytest-api-core &mdash; API Test Report</footer>
385
+
386
+ <script>
387
+ var currentFilter = 'all';
388
+
389
+ function toggleDetail(id) {
390
+ var el = document.getElementById(id);
391
+ var chev = document.getElementById('chev-' + id);
392
+ if (!el) return;
393
+ var isOpen = el.style.display !== 'none';
394
+ el.style.display = isOpen ? 'none' : 'table-row';
395
+ if (chev) chev.classList.toggle('open', !isOpen);
396
+ }
397
+
398
+ function expandAll() {
399
+ document.querySelectorAll('.detail-row').forEach(function(row) {
400
+ var parentRow = row.previousElementSibling;
401
+ if (parentRow && parentRow.style.display !== 'none') {
402
+ row.style.display = 'table-row';
403
+ var id = row.getAttribute('id');
404
+ var chev = document.getElementById('chev-' + id);
405
+ if (chev) chev.classList.add('open');
406
+ }
407
+ });
408
+ }
409
+
410
+ function collapseAll() {
411
+ document.querySelectorAll('.detail-row').forEach(function(row) {
412
+ row.style.display = 'none';
413
+ var id = row.getAttribute('id');
414
+ var chev = document.getElementById('chev-' + id);
415
+ if (chev) chev.classList.remove('open');
416
+ });
417
+ }
418
+
419
+ function toggleHttp(id, headline) {
420
+ var body = document.getElementById(id);
421
+ var chev = document.getElementById('chev-' + id);
422
+ if (!body) return;
423
+ var open = body.classList.toggle('open');
424
+ if (chev) chev.classList.toggle('open', open);
425
+ }
426
+
427
+ function switchTab(tabEl, contentId) {
428
+ var banner = tabEl.closest('.http-banner');
429
+ banner.querySelectorAll('.http-tab').forEach(function(t) { t.classList.remove('active'); });
430
+ banner.querySelectorAll('.http-tab-content').forEach(function(c) { c.classList.remove('active'); });
431
+ tabEl.classList.add('active');
432
+ var el = document.getElementById(contentId);
433
+ if (el) el.classList.add('active');
434
+ }
435
+
436
+ function setFilter(outcome, btn) {
437
+ currentFilter = outcome;
438
+ document.querySelectorAll('.filter-btn').forEach(function(b) { b.classList.remove('active'); });
439
+ btn.classList.add('active');
440
+ applyFilters();
441
+ }
442
+
443
+ function filterTable() { applyFilters(); }
444
+
445
+ function applyFilters() {
446
+ var query = document.getElementById('searchBox').value.toLowerCase();
447
+ document.querySelectorAll('#tableBody .result-row').forEach(function(row) {
448
+ var matchOutcome = currentFilter === 'all' || row.dataset.outcome === currentFilter;
449
+ var matchSearch = !query || row.innerText.toLowerCase().includes(query);
450
+ var show = matchOutcome && matchSearch;
451
+ row.style.display = show ? '' : 'none';
452
+ var onclick = row.getAttribute('onclick') || '';
453
+ var m = onclick.match(/'([^']+)'/);
454
+ if (m) {
455
+ var detail = document.getElementById(m[1]);
456
+ if (detail && !show) detail.style.display = 'none';
457
+ }
458
+ });
459
+ }
460
+
461
+ function toggleTheme() {
462
+ var html = document.documentElement;
463
+ var isDark = html.getAttribute('data-theme') === 'dark';
464
+ html.setAttribute('data-theme', isDark ? 'light' : 'dark');
465
+ document.querySelector('.theme-btn').textContent = isDark ? '🌙 Dark' : '☀️ Light';
466
+ }
467
+ </script>
468
+ </body>
469
+ </html>