a11y: Web Interface Guidelines — 19 правок доступности и семантики

- base.html: lang динамический, meta theme-color, skip-link, убран дубль Dashboard
- Фильтры: labels на search/select, type=search, placeholder с …, autocomplete=off
- llm-retry: <span> → <button> во всех шаблонах (_llm_report, scan, package)
- sortable th: tabindex=0, role=button через JS + keyboard handler
- sort-icon: aria-hidden=true
- style.css: color-scheme:dark, prefers-reduced-motion, tabular-nums, touch-action
- app.js: clipboard fallback для не-HTTPS
This commit is contained in:
Marker689
2026-05-10 11:45:41 +03:00
parent 32dc179f25
commit dea9e0a6e4
11 changed files with 78 additions and 30 deletions

View File

@@ -1,14 +1,45 @@
// GuardDog Nexus — shared UI utilities // GuardDog Nexus — shared UI utilities
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('th.sortable').forEach(function (th) {
th.setAttribute('tabindex', '0');
th.setAttribute('role', 'button');
});
});
document.addEventListener('keydown', function (e) {
if ((e.key === 'Enter' || e.key === ' ') && e.target.matches('th.sortable')) {
e.preventDefault();
e.target.click();
}
});
function copyCode(btn, codeId) { function copyCode(btn, codeId) {
var el = document.getElementById(codeId); var el = document.getElementById(codeId);
if (!el) return; if (!el) return;
if (navigator.clipboard) {
navigator.clipboard.writeText(el.textContent).then(function () { navigator.clipboard.writeText(el.textContent).then(function () {
showCopied(btn);
});
} else {
// Fallback for non-HTTPS
var ta = document.createElement('textarea');
ta.value = el.textContent;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
showCopied(btn);
}
}
function showCopied(btn) {
btn.textContent = 'Copied!'; btn.textContent = 'Copied!';
btn.classList.add('copied'); btn.classList.add('copied');
setTimeout(function () { setTimeout(function () {
btn.textContent = 'Copy'; btn.textContent = 'Copy';
btn.classList.remove('copied'); btn.classList.remove('copied');
}, 2000); }, 2000);
});
} }

View File

@@ -1,5 +1,7 @@
/* GuardDog Nexus — Web UI styles */ /* GuardDog Nexus — Web UI styles */
html { color-scheme: dark; }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Status / severity colours */ /* Status / severity colours */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -35,10 +37,16 @@
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Tables */ /* Tables */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
table { font-size: 0.9rem; } table { font-size: 0.9rem; touch-action: manipulation; }
table.compact { font-size: 0.82rem; } table.compact { font-size: 0.82rem; }
table.compact th, table.compact th,
table.compact td { padding: 0.35rem 0.5rem; } table.compact td { padding: 0.35rem 0.5rem; }
table td:first-child { font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
.spinner { animation: none; }
.status-scanning { animation: none; }
}
@media (max-width: 768px) { @media (max-width: 768px) {
table { display: block; overflow-x: auto; white-space: nowrap; } table { display: block; overflow-x: auto; white-space: nowrap; }
@@ -182,9 +190,14 @@ table.compact td { padding: 0.35rem 0.5rem; }
font-size: 0.7rem; font-size: 0.7rem;
opacity: 0.5; opacity: 0.5;
cursor: pointer; cursor: pointer;
border: none;
border-bottom: 1px dashed; border-bottom: 1px dashed;
background: none;
padding: 0;
color: inherit;
} }
.llm-retry:hover { opacity: 0.8; } .llm-retry:hover { opacity: 0.8; }
.llm-retry:focus-visible { outline: 2px solid #2196f3; outline-offset: 2px; }
.llm-disclaimer { .llm-disclaimer {
margin-top: 0.6rem; margin-top: 0.6rem;

View File

@@ -5,11 +5,11 @@
<span class="llm-severity">{{ report.severity_rating }}</span> <span class="llm-severity">{{ report.severity_rating }}</span>
{% endif %} {% endif %}
{% if config.llm_enabled and not config.llm_auto_analyze %} {% if config.llm_enabled and not config.llm_auto_analyze %}
<span class="llm-retry" <button class="llm-retry"
hx-post="/api/v1/findings/{{ finding_id }}/analyze?retry=1" hx-post="/api/v1/findings/{{ finding_id }}/analyze?retry=1"
hx-target="closest .llm-report" hx-target="closest .llm-report"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</span> hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</button>
{% endif %} {% endif %}
</div> </div>
<p class="llm-summary">{{ report.summary }}</p> <p class="llm-summary">{{ report.summary }}</p>

View File

@@ -2,19 +2,19 @@
<thead> <thead>
<tr> <tr>
<th class="sortable {% if sort_by == 'name' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=name&sort_dir={% if sort_by == 'name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'name' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=name&sort_dir={% if sort_by == 'name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
{{ t('col_name', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_name', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th>{{ t('col_version', request.state.lang) }}</th> <th>{{ t('col_version', request.state.lang) }}</th>
<th>{{ t('col_ecosystem', request.state.lang) }}</th> <th>{{ t('col_ecosystem', request.state.lang) }}</th>
<th>{{ t('col_repo', request.state.lang) }}</th> <th>{{ t('col_repo', request.state.lang) }}</th>
<th class="sortable {% if sort_by == 'flagged' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=flagged&sort_dir={% if sort_by == 'flagged' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'flagged' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=flagged&sort_dir={% if sort_by == 'flagged' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
{{ t('col_flagged', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'flagged' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_flagged', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'flagged' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
{{ t('col_findings', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_findings', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'last_scanned_at' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=last_scanned_at&sort_dir={% if sort_by == 'last_scanned_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'last_scanned_at' %}active{% endif %}" hx-get="/packages?page=1&flagged={{ flagged_filter }}&search={{ search }}&sort_by=last_scanned_at&sort_dir={% if sort_by == 'last_scanned_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#packages-table-container" hx-swap="innerHTML">
{{ t('col_last_scan', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'last_scanned_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_last_scan', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'last_scanned_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@@ -2,21 +2,21 @@
<thead> <thead>
<tr> <tr>
<th class="sortable {% if sort_by == 'id' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=id&sort_dir={% if sort_by == 'id' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'id' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=id&sort_dir={% if sort_by == 'id' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
{{ t('col_id', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'id' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_id', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'id' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'package_name' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=package_name&sort_dir={% if sort_by == 'package_name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'package_name' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=package_name&sort_dir={% if sort_by == 'package_name' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
{{ t('col_package', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_package', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'package_name' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th>{{ t('col_version', request.state.lang) }}</th> <th>{{ t('col_version', request.state.lang) }}</th>
<th>{{ t('col_repo', request.state.lang) }}</th> <th>{{ t('col_repo', request.state.lang) }}</th>
<th class="sortable {% if sort_by == 'status' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=status&sort_dir={% if sort_by == 'status' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'status' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=status&sort_dir={% if sort_by == 'status' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
{{ t('col_status', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'status' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_status', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'status' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'total_findings' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=total_findings&sort_dir={% if sort_by == 'total_findings' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
{{ t('col_findings', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_findings', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'total_findings' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
<th class="sortable {% if sort_by == 'started_at' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=started_at&sort_dir={% if sort_by == 'started_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML"> <th class="sortable {% if sort_by == 'started_at' %}active{% endif %}" hx-get="/scans?page=1&flagged={{ flagged_filter }}&search={{ search }}&status={{ status_filter }}&sort_by=started_at&sort_dir={% if sort_by == 'started_at' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}" hx-target="#scans-table-container" hx-swap="innerHTML">
{{ t('col_time', request.state.lang) }} <span class="sort-icon">{% if sort_by == 'started_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span> {{ t('col_time', request.state.lang) }} <span class="sort-icon" aria-hidden="true">{% if sort_by == 'started_at' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% else %}↕{% endif %}</span>
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-theme="dark"> <html lang="{{ request.state.lang }}" data-theme="dark">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#13171f">
<title>{% block title %}GuardDog Nexus{% endblock %}</title> <title>{% block title %}GuardDog Nexus{% endblock %}</title>
<link rel="stylesheet" href="/static/pico.min.css"> <link rel="stylesheet" href="/static/pico.min.css">
<script src="/static/htmx.min.js"></script> <script src="/static/htmx.min.js"></script>
@@ -10,11 +11,11 @@
<script src="/static/app.js"></script> <script src="/static/app.js"></script>
</head> </head>
<body> <body>
<main class="container"> <a href="#main-content" style="position:absolute;left:-9999px;top:0;z-index:999;padding:0.5rem 1rem;background:#2a3140;color:#fff;">Skip to content</a>
<main class="container" id="main-content">
<nav class="sticky"> <nav class="sticky">
<ul><li><strong><a href="/">{{ t('nav_dashboard', request.state.lang) }}</a></strong></li></ul> <ul><li><strong><a href="/">{{ t('nav_dashboard', request.state.lang) }}</a></strong></li></ul>
<ul> <ul>
<li><a href="/">{{ t('nav_dashboard', request.state.lang) }}</a></li>
<li><a href="/scans">{{ t('nav_scans', request.state.lang) }}</a></li> <li><a href="/scans">{{ t('nav_scans', request.state.lang) }}</a></li>
<li><a href="/packages">{{ t('nav_packages', request.state.lang) }}</a></li> <li><a href="/packages">{{ t('nav_packages', request.state.lang) }}</a></li>
<li style="margin-left: 1rem;"> <li style="margin-left: 1rem;">

View File

@@ -1,5 +1,5 @@
{% if total_findings %} {% if total_findings %}
<div style="display:flex; gap:1.5rem; padding:0.3rem 0; margin-bottom:1rem; border-bottom:1px solid var(--pico-color-gray-500); font-size:0.82rem; opacity:0.8; flex-wrap:wrap;"> <div style="display:flex; gap:1.5rem; padding:0.3rem 0; margin-bottom:1rem; border-bottom:1px solid #2a3140; font-size:0.82rem; opacity:0.8; flex-wrap:wrap;">
<span>{{ t('total_scans_label', request.state.lang) }}: <strong>{{ total_scans }}</strong></span> <span>{{ t('total_scans_label', request.state.lang) }}: <strong>{{ total_scans }}</strong></span>
<span>{{ t('flagged_scans_label', request.state.lang) }}: <strong>{{ flagged_scans }}</strong></span> <span>{{ t('flagged_scans_label', request.state.lang) }}: <strong>{{ flagged_scans }}</strong></span>
<span>{{ t('col_findings', request.state.lang) }}: <strong>{{ total_findings }}</strong></span> <span>{{ t('col_findings', request.state.lang) }}: <strong>{{ total_findings }}</strong></span>

View File

@@ -64,11 +64,11 @@
<span class="llm-severity">{{ f.report.severity_rating }}</span> <span class="llm-severity">{{ f.report.severity_rating }}</span>
{% endif %} {% endif %}
{% if config.llm_enabled and not config.llm_auto_analyze %} {% if config.llm_enabled and not config.llm_auto_analyze %}
<span class="llm-retry" <button class="llm-retry"
hx-post="/api/v1/findings/{{ f.id }}/analyze?retry=1" hx-post="/api/v1/findings/{{ f.id }}/analyze?retry=1"
hx-target="closest .llm-report" hx-target="closest .llm-report"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</span> hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</button>
{% endif %} {% endif %}
</div> </div>
<p class="llm-summary">{{ f.report.summary }}</p> <p class="llm-summary">{{ f.report.summary }}</p>

View File

@@ -12,7 +12,8 @@
<div class="filter-bar"> <div class="filter-bar">
<input type="hidden" name="flagged" value="{{ flagged_filter }}"> <input type="hidden" name="flagged" value="{{ flagged_filter }}">
<input type="text" name="search" placeholder="{{ t('filter_search', request.state.lang) }}" value="{{ search }}" hx-get="/packages" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#packages-table-container" hx-swap="innerHTML" hx-include="[name=flagged]"> <label for="search-pkgs" style="display:none;">{{ t('filter_search', request.state.lang) }}</label>
<input type="search" id="search-pkgs" name="search" placeholder="{{ t('filter_search', request.state.lang) }}…" value="{{ search }}" hx-get="/packages" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#packages-table-container" hx-swap="innerHTML" hx-include="[name=flagged]" autocomplete="off">
<a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline"> <a href="?flagged={% if flagged_filter == '1' %}0{% else %}1{% endif %}" role="button" class="outline">
{% if flagged_filter == '1' %}{{ t('btn_show_all', request.state.lang) }}{% else %}{{ t('btn_flagged_only', request.state.lang) }}{% endif %} {% if flagged_filter == '1' %}{{ t('btn_show_all', request.state.lang) }}{% else %}{{ t('btn_flagged_only', request.state.lang) }}{% endif %}
</a> </a>

View File

@@ -60,11 +60,11 @@
<span class="llm-severity">{{ f.report.severity_rating }}</span> <span class="llm-severity">{{ f.report.severity_rating }}</span>
{% endif %} {% endif %}
{% if config.llm_enabled and not config.llm_auto_analyze %} {% if config.llm_enabled and not config.llm_auto_analyze %}
<span class="llm-retry" <button class="llm-retry"
hx-post="/api/v1/findings/{{ f.id }}/analyze?retry=1" hx-post="/api/v1/findings/{{ f.id }}/analyze?retry=1"
hx-target="closest .llm-report" hx-target="closest .llm-report"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</span> hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</button>
{% endif %} {% endif %}
</div> </div>
<p class="llm-summary">{{ f.report.summary }}</p> <p class="llm-summary">{{ f.report.summary }}</p>

View File

@@ -12,7 +12,9 @@
<div class="filter-bar"> <div class="filter-bar">
<input type="hidden" name="flagged" value="{{ flagged_filter }}"> <input type="hidden" name="flagged" value="{{ flagged_filter }}">
<input type="text" name="search" placeholder="{{ t('filter_search', request.state.lang) }}" value="{{ search }}" hx-get="/scans" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="#status-filter,[name=flagged]"> <label for="search-scans" style="display:none;">{{ t('filter_search', request.state.lang) }}</label>
<input type="search" id="search-scans" name="search" placeholder="{{ t('filter_search', request.state.lang) }}…" value="{{ search }}" hx-get="/scans" hx-trigger="input changed, keyup[entered] delay:300ms" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="#status-filter,[name=flagged]" autocomplete="off">
<label for="status-filter" style="display:none;">Status</label>
<select name="status" id="status-filter" hx-get="/scans" hx-trigger="change" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="[name=search],[name=flagged]"> <select name="status" id="status-filter" hx-get="/scans" hx-trigger="change" hx-target="#scans-table-container" hx-swap="innerHTML" hx-include="[name=search],[name=flagged]">
<option value="">{{ t('filter_all_statuses', request.state.lang) }}</option> <option value="">{{ t('filter_all_statuses', request.state.lang) }}</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>{{ t('filter_pending', request.state.lang) }}</option> <option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>{{ t('filter_pending', request.state.lang) }}</option>