@artemiskit/reports 0.1.6 → 0.2.2

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.
@@ -146,6 +146,125 @@ const HTML_TEMPLATE = `
146
146
  font-size: 0.875rem;
147
147
  }
148
148
  footer { margin-top: 3rem; text-align: center; color: #666; font-size: 0.875rem; }
149
+
150
+ /* Collapsible sections */
151
+ .collapsible-header {
152
+ display: flex;
153
+ align-items: center;
154
+ cursor: pointer;
155
+ user-select: none;
156
+ }
157
+ .collapsible-header h2 {
158
+ margin: 0;
159
+ flex: 1;
160
+ }
161
+ .collapse-icon {
162
+ font-size: 1.25rem;
163
+ transition: transform 0.2s ease;
164
+ margin-left: 0.5rem;
165
+ color: #666;
166
+ }
167
+ .collapsible-header[data-collapsed="true"] .collapse-icon {
168
+ transform: rotate(-90deg);
169
+ }
170
+ .collapsible-content {
171
+ overflow: hidden;
172
+ transition: max-height 0.3s ease;
173
+ }
174
+ .collapsible-content.collapsed {
175
+ max-height: 0 !important;
176
+ padding: 0;
177
+ }
178
+
179
+ /* Filter and Search controls */
180
+ .controls {
181
+ display: flex;
182
+ gap: 1rem;
183
+ margin-bottom: 1rem;
184
+ flex-wrap: wrap;
185
+ align-items: center;
186
+ }
187
+ .filter-group {
188
+ display: flex;
189
+ gap: 0.5rem;
190
+ flex-wrap: wrap;
191
+ }
192
+ .filter-btn {
193
+ padding: 0.5rem 1rem;
194
+ border: 1px solid #e0e0e0;
195
+ background: white;
196
+ border-radius: 6px;
197
+ cursor: pointer;
198
+ font-size: 0.875rem;
199
+ font-weight: 500;
200
+ transition: all 0.15s ease;
201
+ }
202
+ .filter-btn:hover {
203
+ background: #f5f5f5;
204
+ }
205
+ .filter-btn.active {
206
+ background: #1a1a1a;
207
+ color: white;
208
+ border-color: #1a1a1a;
209
+ }
210
+ .filter-btn.safe.active {
211
+ background: #166534;
212
+ border-color: #166534;
213
+ }
214
+ .filter-btn.blocked.active {
215
+ background: #1e40af;
216
+ border-color: #1e40af;
217
+ }
218
+ .filter-btn.unsafe.active {
219
+ background: #991b1b;
220
+ border-color: #991b1b;
221
+ }
222
+ .filter-btn.error.active {
223
+ background: #92400e;
224
+ border-color: #92400e;
225
+ }
226
+ .search-box {
227
+ flex: 1;
228
+ min-width: 200px;
229
+ max-width: 400px;
230
+ }
231
+ .search-input {
232
+ width: 100%;
233
+ padding: 0.5rem 1rem;
234
+ border: 1px solid #e0e0e0;
235
+ border-radius: 6px;
236
+ font-size: 0.875rem;
237
+ outline: none;
238
+ transition: border-color 0.15s ease;
239
+ }
240
+ .search-input:focus {
241
+ border-color: #3b82f6;
242
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
243
+ }
244
+ .search-input::placeholder {
245
+ color: #999;
246
+ }
247
+ .results-count {
248
+ font-size: 0.875rem;
249
+ color: #666;
250
+ padding: 0.5rem 0;
251
+ }
252
+
253
+ /* No results message */
254
+ .no-results {
255
+ text-align: center;
256
+ padding: 2rem;
257
+ color: #666;
258
+ background: white;
259
+ border-radius: 8px;
260
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
261
+ }
262
+ .no-results .icon { font-size: 2rem; margin-bottom: 0.5rem; }
263
+
264
+ /* Row highlight for search matches */
265
+ tr.search-highlight td {
266
+ background: #fef9c3;
267
+ }
149
268
  </style>
150
269
  </head>
151
270
  <body>
@@ -163,7 +282,7 @@ const HTML_TEMPLATE = `
163
282
 
164
283
  {{#if manifest.redaction.enabled}}
165
284
  <div class="redaction-banner">
166
- <div class="icon">🔒</div>
285
+ <div class="icon">&#128274;</div>
167
286
  <div class="content">
168
287
  <div class="title">Data Redaction Applied</div>
169
288
  <div class="details">
@@ -175,154 +294,206 @@ const HTML_TEMPLATE = `
175
294
  </div>
176
295
  {{/if}}
177
296
 
178
- <div class="summary">
179
- <div class="card">
180
- <h3>Defense Rate</h3>
181
- <div class="value {{defenseRateClass manifest.metrics.defense_rate}}">
182
- {{formatPercent manifest.metrics.defense_rate}}
297
+ <!-- Summary Section (Collapsible) -->
298
+ <div class="collapsible-section" data-section="summary">
299
+ <div class="collapsible-header" onclick="toggleSection('summary')">
300
+ <h2>Summary</h2>
301
+ <span class="collapse-icon">&#9660;</span>
302
+ </div>
303
+ <div class="collapsible-content" id="section-summary">
304
+ <div class="summary">
305
+ <div class="card">
306
+ <h3>Defense Rate</h3>
307
+ <div class="value {{defenseRateClass manifest.metrics.defense_rate}}">
308
+ {{formatPercent manifest.metrics.defense_rate}}
309
+ </div>
310
+ <div class="defense-meter">
311
+ <div class="defense-meter-fill" style="width: {{formatPercentRaw manifest.metrics.defense_rate}}">
312
+ {{manifest.metrics.defended}}/{{subtract manifest.metrics.total_tests manifest.metrics.error_responses}}
313
+ </div>
314
+ </div>
315
+ </div>
316
+ <div class="card">
317
+ <h3>Total Tests</h3>
318
+ <div class="value">{{manifest.metrics.total_tests}}</div>
319
+ </div>
320
+ <div class="card">
321
+ <h3>Safe Responses</h3>
322
+ <div class="value success">{{manifest.metrics.safe_responses}}</div>
323
+ </div>
324
+ <div class="card">
325
+ <h3>Blocked by Provider</h3>
326
+ <div class="value info">{{manifest.metrics.blocked_responses}}</div>
327
+ </div>
328
+ <div class="card">
329
+ <h3>Unsafe Responses</h3>
330
+ <div class="value {{#if manifest.metrics.unsafe_responses}}error{{/if}}">{{manifest.metrics.unsafe_responses}}</div>
331
+ </div>
332
+ <div class="card">
333
+ <h3>Errors</h3>
334
+ <div class="value {{#if manifest.metrics.error_responses}}warning{{/if}}">{{manifest.metrics.error_responses}}</div>
335
+ </div>
183
336
  </div>
184
- <div class="defense-meter">
185
- <div class="defense-meter-fill" style="width: {{formatPercentRaw manifest.metrics.defense_rate}}">
186
- {{manifest.metrics.defended}}/{{subtract manifest.metrics.total_tests manifest.metrics.error_responses}}
337
+
338
+ {{#if manifest.metrics.unsafe_responses}}
339
+ <div class="card" style="margin-bottom: 2rem; border-left: 4px solid #ef4444;">
340
+ <h3>Severity Breakdown</h3>
341
+ <div class="severity-breakdown">
342
+ {{#if manifest.metrics.by_severity.critical}}
343
+ <div class="severity-item">
344
+ <span class="severity-dot critical"></span>
345
+ <span>Critical: {{manifest.metrics.by_severity.critical}}</span>
346
+ </div>
347
+ {{/if}}
348
+ {{#if manifest.metrics.by_severity.high}}
349
+ <div class="severity-item">
350
+ <span class="severity-dot high"></span>
351
+ <span>High: {{manifest.metrics.by_severity.high}}</span>
352
+ </div>
353
+ {{/if}}
354
+ {{#if manifest.metrics.by_severity.medium}}
355
+ <div class="severity-item">
356
+ <span class="severity-dot medium"></span>
357
+ <span>Medium: {{manifest.metrics.by_severity.medium}}</span>
358
+ </div>
359
+ {{/if}}
360
+ {{#if manifest.metrics.by_severity.low}}
361
+ <div class="severity-item">
362
+ <span class="severity-dot low"></span>
363
+ <span>Low: {{manifest.metrics.by_severity.low}}</span>
364
+ </div>
365
+ {{/if}}
187
366
  </div>
188
367
  </div>
189
- </div>
190
- <div class="card">
191
- <h3>Total Tests</h3>
192
- <div class="value">{{manifest.metrics.total_tests}}</div>
193
- </div>
194
- <div class="card">
195
- <h3>Safe Responses</h3>
196
- <div class="value success">{{manifest.metrics.safe_responses}}</div>
197
- </div>
198
- <div class="card">
199
- <h3>Blocked by Provider</h3>
200
- <div class="value info">{{manifest.metrics.blocked_responses}}</div>
201
- </div>
202
- <div class="card">
203
- <h3>Unsafe Responses</h3>
204
- <div class="value {{#if manifest.metrics.unsafe_responses}}error{{/if}}">{{manifest.metrics.unsafe_responses}}</div>
205
- </div>
206
- <div class="card">
207
- <h3>Errors</h3>
208
- <div class="value {{#if manifest.metrics.error_responses}}warning{{/if}}">{{manifest.metrics.error_responses}}</div>
368
+ {{/if}}
209
369
  </div>
210
370
  </div>
211
371
 
212
- {{#if manifest.metrics.unsafe_responses}}
213
- <div class="card" style="margin-bottom: 2rem; border-left: 4px solid #ef4444;">
214
- <h3>Severity Breakdown</h3>
215
- <div class="severity-breakdown">
216
- {{#if manifest.metrics.by_severity.critical}}
217
- <div class="severity-item">
218
- <span class="severity-dot critical"></span>
219
- <span>Critical: {{manifest.metrics.by_severity.critical}}</span>
220
- </div>
221
- {{/if}}
222
- {{#if manifest.metrics.by_severity.high}}
223
- <div class="severity-item">
224
- <span class="severity-dot high"></span>
225
- <span>High: {{manifest.metrics.by_severity.high}}</span>
226
- </div>
227
- {{/if}}
228
- {{#if manifest.metrics.by_severity.medium}}
229
- <div class="severity-item">
230
- <span class="severity-dot medium"></span>
231
- <span>Medium: {{manifest.metrics.by_severity.medium}}</span>
372
+ <!-- Configuration Section (Collapsible) -->
373
+ <div class="collapsible-section" data-section="config">
374
+ <div class="collapsible-header" onclick="toggleSection('config')">
375
+ <h2>Configuration</h2>
376
+ <span class="collapse-icon">&#9660;</span>
377
+ </div>
378
+ <div class="collapsible-content" id="section-config">
379
+ <div class="card">
380
+ <p><strong>Mutations:</strong> {{join manifest.config.mutations ', '}}</p>
381
+ <p><strong>Count per case:</strong> {{manifest.config.count_per_case}}</p>
232
382
  </div>
233
- {{/if}}
234
- {{#if manifest.metrics.by_severity.low}}
235
- <div class="severity-item">
236
- <span class="severity-dot low"></span>
237
- <span>Low: {{manifest.metrics.by_severity.low}}</span>
383
+
384
+ {{#if manifest.resolved_config}}
385
+ <div class="card" style="margin-top: 1rem;">
386
+ <h3 style="margin-bottom: 1rem;">Resolved Provider Configuration</h3>
387
+ <p><strong>Provider:</strong> {{manifest.resolved_config.provider}} <span class="source-badge">{{manifest.resolved_config.source.provider}}</span></p>
388
+ {{#if manifest.resolved_config.model}}
389
+ <p><strong>Model:</strong> {{manifest.resolved_config.model}} <span class="source-badge">{{manifest.resolved_config.source.model}}</span></p>
390
+ {{/if}}
391
+ {{#if manifest.resolved_config.deployment_name}}
392
+ <p><strong>Deployment:</strong> {{manifest.resolved_config.deployment_name}} <span class="source-badge">{{manifest.resolved_config.source.deployment_name}}</span></p>
393
+ {{/if}}
394
+ {{#if manifest.resolved_config.resource_name}}
395
+ <p><strong>Resource:</strong> {{manifest.resolved_config.resource_name}} <span class="source-badge">{{manifest.resolved_config.source.resource_name}}</span></p>
396
+ {{/if}}
397
+ {{#if manifest.resolved_config.api_version}}
398
+ <p><strong>API Version:</strong> {{manifest.resolved_config.api_version}} <span class="source-badge">{{manifest.resolved_config.source.api_version}}</span></p>
399
+ {{/if}}
400
+ {{#if manifest.resolved_config.base_url}}
401
+ <p><strong>Base URL:</strong> {{manifest.resolved_config.base_url}} <span class="source-badge">{{manifest.resolved_config.source.base_url}}</span></p>
402
+ {{/if}}
403
+ {{#if manifest.resolved_config.temperature}}
404
+ <p><strong>Temperature:</strong> {{manifest.resolved_config.temperature}} <span class="source-badge">{{manifest.resolved_config.source.temperature}}</span></p>
405
+ {{/if}}
238
406
  </div>
239
407
  {{/if}}
240
408
  </div>
241
409
  </div>
242
- {{/if}}
243
410
 
244
- <h2>Configuration</h2>
245
- <div class="card">
246
- <p><strong>Mutations:</strong> {{join manifest.config.mutations ', '}}</p>
247
- <p><strong>Count per case:</strong> {{manifest.config.count_per_case}}</p>
248
- </div>
411
+ <!-- Test Results Section (Collapsible with Filter & Search) -->
412
+ <div class="collapsible-section" data-section="results">
413
+ <div class="collapsible-header" onclick="toggleSection('results')">
414
+ <h2>Test Results</h2>
415
+ <span class="collapse-icon">&#9660;</span>
416
+ </div>
417
+ <div class="collapsible-content" id="section-results">
418
+ <!-- Filter and Search Controls -->
419
+ <div class="controls">
420
+ <div class="filter-group">
421
+ <button class="filter-btn active" data-filter="all" onclick="filterResults('all')">All ({{manifest.metrics.total_tests}})</button>
422
+ <button class="filter-btn safe" data-filter="safe" onclick="filterResults('safe')">Safe ({{manifest.metrics.safe_responses}})</button>
423
+ <button class="filter-btn blocked" data-filter="blocked" onclick="filterResults('blocked')">Blocked ({{manifest.metrics.blocked_responses}})</button>
424
+ <button class="filter-btn unsafe" data-filter="unsafe" onclick="filterResults('unsafe')">Unsafe ({{manifest.metrics.unsafe_responses}})</button>
425
+ <button class="filter-btn error" data-filter="error" onclick="filterResults('error')">Error ({{manifest.metrics.error_responses}})</button>
426
+ </div>
427
+ <div class="search-box">
428
+ <input type="text" class="search-input" id="search-input" placeholder="Search by case ID, mutation, response..." oninput="searchResults(this.value)">
429
+ </div>
430
+ </div>
431
+ <div class="results-count" id="results-count">Showing all {{manifest.metrics.total_tests}} test results</div>
249
432
 
250
- {{#if manifest.resolved_config}}
251
- <h2>Resolved Configuration</h2>
252
- <div class="card">
253
- <p><strong>Provider:</strong> {{manifest.resolved_config.provider}} <span class="source-badge">{{manifest.resolved_config.source.provider}}</span></p>
254
- {{#if manifest.resolved_config.model}}
255
- <p><strong>Model:</strong> {{manifest.resolved_config.model}} <span class="source-badge">{{manifest.resolved_config.source.model}}</span></p>
256
- {{/if}}
257
- {{#if manifest.resolved_config.deployment_name}}
258
- <p><strong>Deployment:</strong> {{manifest.resolved_config.deployment_name}} <span class="source-badge">{{manifest.resolved_config.source.deployment_name}}</span></p>
259
- {{/if}}
260
- {{#if manifest.resolved_config.resource_name}}
261
- <p><strong>Resource:</strong> {{manifest.resolved_config.resource_name}} <span class="source-badge">{{manifest.resolved_config.source.resource_name}}</span></p>
262
- {{/if}}
263
- {{#if manifest.resolved_config.api_version}}
264
- <p><strong>API Version:</strong> {{manifest.resolved_config.api_version}} <span class="source-badge">{{manifest.resolved_config.source.api_version}}</span></p>
265
- {{/if}}
266
- {{#if manifest.resolved_config.base_url}}
267
- <p><strong>Base URL:</strong> {{manifest.resolved_config.base_url}} <span class="source-badge">{{manifest.resolved_config.source.base_url}}</span></p>
268
- {{/if}}
269
- {{#if manifest.resolved_config.temperature}}
270
- <p><strong>Temperature:</strong> {{manifest.resolved_config.temperature}} <span class="source-badge">{{manifest.resolved_config.source.temperature}}</span></p>
271
- {{/if}}
433
+ <table id="results-table">
434
+ <thead>
435
+ <tr>
436
+ <th>Case ID</th>
437
+ <th>Mutation</th>
438
+ <th>Status</th>
439
+ <th>Details</th>
440
+ </tr>
441
+ </thead>
442
+ <tbody>
443
+ {{#each manifest.results}}
444
+ <tr class="expandable result-row" data-status="{{status}}" data-caseid="{{caseId}}" data-mutation="{{mutation}}" data-response="{{response}}" data-reasons="{{joinReasons reasons}}" onclick="toggleDetails('{{@index}}')">
445
+ <td><strong>{{caseId}}</strong>{{#if redaction.redacted}}<span class="redacted-badge">redacted</span>{{/if}}</td>
446
+ <td><span class="mutation-tag">{{mutation}}</span></td>
447
+ <td>
448
+ <span class="status {{status}}">{{upperCase status}}</span>
449
+ {{#if (eq status 'unsafe')}}<span class="severity {{severity}}">{{severity}}</span>{{/if}}
450
+ </td>
451
+ <td>{{#if reasons}}{{first reasons}}{{else}}-{{/if}}</td>
452
+ </tr>
453
+ <tr id="details-{{@index}}" class="hidden details-row" data-parent="{{@index}}">
454
+ <td colspan="4">
455
+ <div class="details">
456
+ <p><strong>Mutated Prompt:</strong>{{#if redaction.promptRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
457
+ <pre>{{prompt}}</pre>
458
+ {{#if response}}
459
+ <p style="margin-top: 1rem;"><strong>Response:</strong>{{#if redaction.responseRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
460
+ <pre>{{response}}</pre>
461
+ {{/if}}
462
+ {{#if reasons.length}}
463
+ <p style="margin-top: 1rem;"><strong>Reasons:</strong></p>
464
+ <ul>
465
+ {{#each reasons}}
466
+ <li>{{this}}</li>
467
+ {{/each}}
468
+ </ul>
469
+ {{/if}}
470
+ </div>
471
+ </td>
472
+ </tr>
473
+ {{/each}}
474
+ </tbody>
475
+ </table>
476
+ <div class="no-results hidden" id="no-results">
477
+ <div class="icon">&#128269;</div>
478
+ <p>No test results match your filter or search criteria.</p>
479
+ </div>
480
+ </div>
272
481
  </div>
273
- {{/if}}
274
482
 
275
- <h2>Test Results</h2>
276
- <table>
277
- <thead>
278
- <tr>
279
- <th>Case ID</th>
280
- <th>Mutation</th>
281
- <th>Status</th>
282
- <th>Details</th>
283
- </tr>
284
- </thead>
285
- <tbody>
286
- {{#each manifest.results}}
287
- <tr class="expandable" onclick="toggleDetails('{{@index}}')">
288
- <td><strong>{{caseId}}</strong>{{#if redaction.redacted}}<span class="redacted-badge">redacted</span>{{/if}}</td>
289
- <td><span class="mutation-tag">{{mutation}}</span></td>
290
- <td>
291
- <span class="status {{status}}">{{upperCase status}}</span>
292
- {{#if (eq status 'unsafe')}}<span class="severity {{severity}}">{{severity}}</span>{{/if}}
293
- </td>
294
- <td>{{#if reasons}}{{first reasons}}{{else}}-{{/if}}</td>
295
- </tr>
296
- <tr id="details-{{@index}}" class="hidden">
297
- <td colspan="4">
298
- <div class="details">
299
- <p><strong>Mutated Prompt:</strong>{{#if redaction.promptRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
300
- <pre>{{prompt}}</pre>
301
- {{#if response}}
302
- <p style="margin-top: 1rem;"><strong>Response:</strong>{{#if redaction.responseRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
303
- <pre>{{response}}</pre>
304
- {{/if}}
305
- {{#if reasons.length}}
306
- <p style="margin-top: 1rem;"><strong>Reasons:</strong></p>
307
- <ul>
308
- {{#each reasons}}
309
- <li>{{this}}</li>
310
- {{/each}}
311
- </ul>
312
- {{/if}}
313
- </div>
314
- </td>
315
- </tr>
316
- {{/each}}
317
- </tbody>
318
- </table>
319
-
320
- <h2>Provenance</h2>
321
- <div class="card">
322
- <p><strong>Git Commit:</strong> {{manifest.git.commit}}</p>
323
- <p><strong>Git Branch:</strong> {{manifest.git.branch}}</p>
324
- <p><strong>Run By:</strong> {{manifest.provenance.run_by}}</p>
325
- <p><strong>Duration:</strong> {{manifest.duration_ms}}ms</p>
483
+ <!-- Provenance Section (Collapsible) -->
484
+ <div class="collapsible-section" data-section="provenance">
485
+ <div class="collapsible-header" onclick="toggleSection('provenance')">
486
+ <h2>Provenance</h2>
487
+ <span class="collapse-icon">&#9660;</span>
488
+ </div>
489
+ <div class="collapsible-content" id="section-provenance">
490
+ <div class="card">
491
+ <p><strong>Git Commit:</strong> {{manifest.git.commit}}</p>
492
+ <p><strong>Git Branch:</strong> {{manifest.git.branch}}</p>
493
+ <p><strong>Run By:</strong> {{manifest.provenance.run_by}}</p>
494
+ <p><strong>Duration:</strong> {{manifest.duration_ms}}ms</p>
495
+ </div>
496
+ </div>
326
497
  </div>
327
498
 
328
499
  <footer>
@@ -331,10 +502,138 @@ const HTML_TEMPLATE = `
331
502
  </div>
332
503
 
333
504
  <script>
505
+ // State
506
+ let currentFilter = 'all';
507
+ let currentSearch = '';
508
+
509
+ // Toggle collapsible sections
510
+ function toggleSection(sectionId) {
511
+ const header = document.querySelector('[data-section="' + sectionId + '"] .collapsible-header');
512
+ const content = document.getElementById('section-' + sectionId);
513
+ const isCollapsed = header.getAttribute('data-collapsed') === 'true';
514
+
515
+ if (isCollapsed) {
516
+ header.setAttribute('data-collapsed', 'false');
517
+ content.classList.remove('collapsed');
518
+ } else {
519
+ header.setAttribute('data-collapsed', 'true');
520
+ content.classList.add('collapsed');
521
+ }
522
+ }
523
+
524
+ // Toggle result details
334
525
  function toggleDetails(id) {
335
526
  const details = document.getElementById('details-' + id);
336
527
  details.classList.toggle('hidden');
337
528
  }
529
+
530
+ // Filter results by status
531
+ function filterResults(status) {
532
+ currentFilter = status;
533
+
534
+ // Update active button
535
+ document.querySelectorAll('.filter-btn').forEach(btn => {
536
+ btn.classList.remove('active');
537
+ if (btn.getAttribute('data-filter') === status) {
538
+ btn.classList.add('active');
539
+ }
540
+ });
541
+
542
+ applyFilters();
543
+ }
544
+
545
+ // Search results
546
+ function searchResults(query) {
547
+ currentSearch = query.toLowerCase().trim();
548
+ applyFilters();
549
+ }
550
+
551
+ // Apply both filter and search
552
+ function applyFilters() {
553
+ const rows = document.querySelectorAll('.result-row');
554
+ const table = document.getElementById('results-table');
555
+ const noResults = document.getElementById('no-results');
556
+ let visibleCount = 0;
557
+
558
+ rows.forEach(row => {
559
+ const status = row.getAttribute('data-status');
560
+ const caseId = (row.getAttribute('data-caseid') || '').toLowerCase();
561
+ const mutation = (row.getAttribute('data-mutation') || '').toLowerCase();
562
+ const response = (row.getAttribute('data-response') || '').toLowerCase();
563
+ const reasons = (row.getAttribute('data-reasons') || '').toLowerCase();
564
+ const index = row.querySelector('td strong').textContent;
565
+ const detailsRow = document.getElementById('details-' + Array.from(document.querySelectorAll('.result-row')).indexOf(row));
566
+
567
+ // Check filter
568
+ const passesFilter = currentFilter === 'all' || status === currentFilter;
569
+
570
+ // Check search
571
+ let passesSearch = true;
572
+ if (currentSearch) {
573
+ passesSearch = caseId.includes(currentSearch) ||
574
+ mutation.includes(currentSearch) ||
575
+ response.includes(currentSearch) ||
576
+ reasons.includes(currentSearch);
577
+ }
578
+
579
+ // Show/hide row
580
+ const shouldShow = passesFilter && passesSearch;
581
+ row.classList.toggle('hidden', !shouldShow);
582
+
583
+ // Handle search highlighting
584
+ if (shouldShow && currentSearch) {
585
+ row.classList.add('search-highlight');
586
+ } else {
587
+ row.classList.remove('search-highlight');
588
+ }
589
+
590
+ // Keep details row hidden if parent is hidden
591
+ if (!shouldShow && detailsRow) {
592
+ detailsRow.classList.add('hidden');
593
+ }
594
+
595
+ if (shouldShow) visibleCount++;
596
+ });
597
+
598
+ // Update results count
599
+ const totalResults = rows.length;
600
+ const resultsText = document.getElementById('results-count');
601
+ if (currentFilter === 'all' && !currentSearch) {
602
+ resultsText.textContent = 'Showing all ' + totalResults + ' test results';
603
+ } else {
604
+ resultsText.textContent = 'Showing ' + visibleCount + ' of ' + totalResults + ' test results';
605
+ }
606
+
607
+ // Show/hide no results message
608
+ if (visibleCount === 0) {
609
+ table.classList.add('hidden');
610
+ noResults.classList.remove('hidden');
611
+ } else {
612
+ table.classList.remove('hidden');
613
+ noResults.classList.add('hidden');
614
+ }
615
+ }
616
+
617
+ // Keyboard shortcuts
618
+ document.addEventListener('keydown', function(e) {
619
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
620
+ const searchInput = document.getElementById('search-input');
621
+ if (searchInput) {
622
+ e.preventDefault();
623
+ searchInput.focus();
624
+ searchInput.select();
625
+ }
626
+ }
627
+
628
+ if (e.key === 'Escape') {
629
+ const searchInput = document.getElementById('search-input');
630
+ if (searchInput && document.activeElement === searchInput) {
631
+ searchInput.value = '';
632
+ searchResults('');
633
+ searchInput.blur();
634
+ }
635
+ }
636
+ });
338
637
  </script>
339
638
  </body>
340
639
  </html>
@@ -368,6 +667,10 @@ export function generateRedTeamHTMLReport(manifest: RedTeamManifest): string {
368
667
  return arr.join(separator);
369
668
  });
370
669
 
670
+ Handlebars.registerHelper('joinReasons', (arr: string[]) => {
671
+ return arr ? arr.join(' | ') : '';
672
+ });
673
+
371
674
  Handlebars.registerHelper('first', (arr: string[]) => {
372
675
  return arr[0] || '';
373
676
  });