Files
hiveops-aria/frontend/src/components/DevicePosture.svelte
T
johannes 726ccb4ecf
CD - Develop / build-and-deploy (push) Failing after 13m2s
fix(aria): correct V3 column names and switch filters to select dropdowns
V3 migration had wrong column names for threat_ioc (source, confidence)
and precursor_sequence_alert (auto_response_taken). Fixed column names
and added phases_detected TEXT[] conversion. PrecursorAlerts and
DevicePosture filter sidebar now uses <select> dropdowns per template
pattern (filter-sidebar structure kept; pill buttons removed).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 16:29:22 -04:00

621 lines
28 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from 'svelte';
import { ariaApi, type DeviceSecurityPosture, type OsEolStatus } from '../lib/api';
import { addToast } from '../lib/stores';
import Pagination from './common/Pagination.svelte';
let postures: DeviceSecurityPosture[] = [];
let totalElements = 0;
let totalPages = 0;
let currentPage = 0;
const pageSize = 25;
let loading = true;
// Filter sidebar
let filterSidebarCollapsed = false;
let filterScore: '' | 'low' | 'medium' = '';
let filterEol: OsEolStatus | '' = '';
$: hasActiveFilters = !!filterScore || !!filterEol;
// Detail panel
let panelOpen = false;
let panelDevice: DeviceSecurityPosture | null = null;
async function load(page = 0) {
loading = true;
try {
const params: Record<string, any> = { page, size: pageSize };
if (filterScore) params.scoreFilter = filterScore;
if (filterEol) params.osEolStatus = filterEol;
const res = await ariaApi.getPostures(params);
postures = res.data.content;
totalElements = res.data.page.totalElements;
totalPages = res.data.page.totalPages;
currentPage = res.data.page.number;
} catch {
addToast('error', 'Load Failed', 'Could not load device posture data');
} finally {
loading = false;
}
}
function clearAllFilters() {
filterScore = '';
filterEol = '';
currentPage = 0;
load(0);
}
function openPanel(device: DeviceSecurityPosture) {
panelDevice = device;
panelOpen = true;
}
function scoreColor(score: number | undefined): string {
if (score == null) return 'unknown';
if (score >= 80) return 'good';
if (score >= 50) return 'medium';
return 'bad';
}
function eolLabel(s: OsEolStatus | string): string {
if (s === 'SUPPORTED') return 'Supported';
if (s === 'EOL') return 'EOL';
return 'Unknown';
}
function boolCell(v: boolean | undefined): string {
if (v == null) return '—';
return v ? '✓' : '✗';
}
function boolClass(v: boolean | undefined): string {
if (v == null) return 'neutral';
return v ? 'ok' : 'bad';
}
function formatDate(iso: string | undefined): string {
if (!iso) return '—';
return new Date(iso).toLocaleString('en-US', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
}
function imageMismatch(d: DeviceSecurityPosture): boolean {
return !!(d.goldImageHash && d.currentImageHash && d.goldImageHash !== d.currentImageHash);
}
onMount(() => load(0));
</script>
<div class="page-wrapper">
<div class="header">
<div class="header-text">
<h1>Device Posture</h1>
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
</div>
</div>
<div class="page-body">
<!-- LEFT: Collapsible filter sidebar -->
<div class="filter-sidebar" class:sidebar-collapsed={filterSidebarCollapsed}>
<div class="filter-sidebar-toggle-bar" class:bar-filters-active={hasActiveFilters}>
{#if !filterSidebarCollapsed}
<span class="filter-sidebar-title">Filters</span>
<div class="toggle-bar-right">
<span class="sidebar-stat-pill">{totalElements}</span>
<button class="sidebar-toggle-btn"
on:click={() => filterSidebarCollapsed = true}
title="Collapse filters"></button>
</div>
{:else}
<button class="sidebar-toggle-btn"
on:click={() => filterSidebarCollapsed = false}
title="Expand filters"></button>
{/if}
</div>
{#if !filterSidebarCollapsed}
<div class="filter-sidebar-content">
{#if hasActiveFilters}
<button class="sidebar-clear-all-btn" on:click={clearAllFilters}> Clear all</button>
{/if}
<div class="filter-section">
<label class="filter-section-label">Score</label>
<select class="filter-select" bind:value={filterScore}
on:change={() => { currentPage = 0; load(0); }}>
<option value="">All Scores</option>
<option value="low">Low (&lt;50)</option>
<option value="medium">Below Good (&lt;80)</option>
</select>
</div>
<div class="filter-section">
<label class="filter-section-label">OS Status</label>
<select class="filter-select" bind:value={filterEol}
on:change={() => { currentPage = 0; load(0); }}>
<option value="">All</option>
<option value="EOL">EOL</option>
<option value="SUPPORTED">Supported</option>
<option value="UNKNOWN">Unknown</option>
</select>
</div>
</div>
{/if}
</div>
<!-- RIGHT: active chips + content frame -->
<div class="page-right">
{#if hasActiveFilters}
<div class="active-filters">
<span class="active-filters-label">Filters:</span>
{#if filterScore}
<span class="filter-tag" style="border-color: #d97706">
<span class="filter-tag-dot" style="background-color: #d97706"></span>
Score: {filterScore === 'low' ? 'Low (<50)' : 'Below Good (<80)'}
<button class="filter-tag-remove" on:click={() => { filterScore = ''; currentPage = 0; load(0); }}>&times;</button>
</span>
{/if}
{#if filterEol}
<span class="filter-tag" style="border-color: {filterEol === 'EOL' ? '#dc2626' : filterEol === 'SUPPORTED' ? '#16a34a' : '#6b7280'}">
<span class="filter-tag-dot" style="background-color: {filterEol === 'EOL' ? '#dc2626' : filterEol === 'SUPPORTED' ? '#16a34a' : '#6b7280'}"></span>
OS: {eolLabel(filterEol)}
<button class="filter-tag-remove" on:click={() => { filterEol = ''; currentPage = 0; load(0); }}>&times;</button>
</span>
{/if}
</div>
{/if}
<div class="content-frame">
<div class="page-main">
<div class="toolbar">
<span class="toolbar-count">{totalElements} device{totalElements !== 1 ? 's' : ''}</span>
<div class="toolbar-right">
<button class="btn-refresh" on:click={() => load(currentPage)}>Refresh</button>
</div>
</div>
<div class="table-card">
<Pagination
page={currentPage} {totalPages} {totalElements} size={pageSize}
on:pageChange={e => { currentPage = e.detail.page; load(currentPage); }}
on:pageSizeChange={e => { currentPage = 0; load(0); }}
/>
<div class="table-container">
{#if loading}
<div class="table-msg">Loading…</div>
{:else if postures.length === 0}
<div class="table-msg">
{hasActiveFilters ? 'No devices match the current filters.' : 'No posture data yet. Devices report posture via the ARIA signals module.'}
</div>
{:else}
<table>
<thead>
<tr>
<th>Device</th>
<th>OS Version</th>
<th>OS Status</th>
<th>Disk Enc.</th>
<th>Audit Policy</th>
<th>Whitelist</th>
<th>Image</th>
<th>Score</th>
<th>Last Check</th>
<th></th>
</tr>
</thead>
<tbody>
{#each postures as device (device.deviceId)}
<tr on:click={() => openPanel(device)}>
<td class="mono-cell">{device.deviceAgentId ?? device.deviceId}</td>
<td class="os-cell">{device.osVersion ?? '—'}</td>
<td>
<span class="eol-badge eol-{(device.osEolStatus ?? 'UNKNOWN').toLowerCase()}">
{eolLabel(device.osEolStatus)}
</span>
</td>
<td class="bool-cell {boolClass(device.diskEncryptionEnabled)}">{boolCell(device.diskEncryptionEnabled)}</td>
<td class="bool-cell {boolClass(device.auditPolicyCompliant)}">{boolCell(device.auditPolicyCompliant)}</td>
<td class="bool-cell {boolClass(device.softwareWhitelistEnabled)}">{boolCell(device.softwareWhitelistEnabled)}</td>
<td>
{#if imageMismatch(device)}
<span class="img-badge drift">Drift</span>
{:else if device.goldImageHash}
<span class="img-badge ok">Match</span>
{:else}
<span class="img-badge unknown"></span>
{/if}
</td>
<td>
<div class="score-wrap">
<div class="score-bar">
<div class="score-fill score-{scoreColor(device.postureScore)}"
style="width: {device.postureScore ?? 0}%"></div>
</div>
<span class="score-text score-text-{scoreColor(device.postureScore)}">
{device.postureScore ?? '—'}
</span>
</div>
</td>
<td class="date-cell">{formatDate(device.lastPostureCheckAt)}</td>
<td class="actions" on:click|stopPropagation>
<button class="btn-sm btn-secondary-sm" on:click={() => openPanel(device)}>Detail</button>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{#if panelOpen && panelDevice}
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="panel-overlay" on:click={() => { panelOpen = false; panelDevice = null; }}></div>
<div class="panel" role="dialog" aria-modal="true">
<div class="panel-header">
<div class="panel-header-text">
<div class="panel-title">{panelDevice.deviceAgentId ?? `Device ${panelDevice.deviceId}`}</div>
<div class="panel-subtitle">Institution: {panelDevice.institutionKey ?? '—'}</div>
</div>
<button class="panel-close" on:click={() => { panelOpen = false; panelDevice = null; }} aria-label="Close"></button>
</div>
<div class="panel-body">
<div class="panel-score-row">
<div class="big-score score-text-{scoreColor(panelDevice.postureScore)}">
{panelDevice.postureScore ?? '—'}
</div>
<div class="big-score-label">Posture Score</div>
<div class="score-bar big-bar">
<div class="score-fill score-{scoreColor(panelDevice.postureScore)}"
style="width: {panelDevice.postureScore ?? 0}%"></div>
</div>
</div>
<div class="check-list">
<div class="check-row">
<span class="check-label">Disk Encryption</span>
<span class="check-val {boolClass(panelDevice.diskEncryptionEnabled)}">
{boolCell(panelDevice.diskEncryptionEnabled)}
{#if panelDevice.diskEncryptionEnabled === false}<span class="penalty">25</span>{/if}
</span>
</div>
<div class="check-row">
<span class="check-label">Audit Policy</span>
<span class="check-val {boolClass(panelDevice.auditPolicyCompliant)}">
{boolCell(panelDevice.auditPolicyCompliant)}
{#if panelDevice.auditPolicyCompliant === false}<span class="penalty">20</span>{/if}
</span>
</div>
<div class="check-row">
<span class="check-label">Software Whitelist</span>
<span class="check-val {boolClass(panelDevice.softwareWhitelistEnabled)}">
{boolCell(panelDevice.softwareWhitelistEnabled)}
{#if panelDevice.softwareWhitelistEnabled === false}<span class="penalty">10</span>{/if}
</span>
</div>
<div class="check-row">
<span class="check-label">OS Version</span>
<span class="check-val neutral">{panelDevice.osVersion ?? '—'}</span>
</div>
<div class="check-row">
<span class="check-label">OS EOL Status</span>
<span class="check-val {panelDevice.osEolStatus === 'EOL' ? 'bad' : panelDevice.osEolStatus === 'SUPPORTED' ? 'ok' : 'neutral'}">
{eolLabel(panelDevice.osEolStatus)}
{#if panelDevice.osEolStatus === 'EOL'}<span class="penalty">20</span>{/if}
</span>
</div>
<div class="check-divider"></div>
<div class="check-row">
<span class="check-label">Gold Image Hash</span>
<span class="check-val neutral hash">{panelDevice.goldImageHash ? panelDevice.goldImageHash.substring(0, 20) + '…' : '—'}</span>
</div>
<div class="check-row">
<span class="check-label">Current Hash</span>
<span class="check-val {imageMismatch(panelDevice) ? 'bad' : 'neutral'} hash">
{panelDevice.currentImageHash ? panelDevice.currentImageHash.substring(0, 20) + '…' : '—'}
{#if imageMismatch(panelDevice)}<span class="penalty">20 drift</span>{/if}
</span>
</div>
<div class="check-divider"></div>
<div class="check-row">
<span class="check-label">Last Posture Check</span>
<span class="check-val neutral">{formatDate(panelDevice.lastPostureCheckAt)}</span>
</div>
<div class="check-row">
<span class="check-label">Gold Validated At</span>
<span class="check-val neutral">{formatDate(panelDevice.goldImageValidatedAt)}</span>
</div>
<div class="check-row">
<span class="check-label">Record Updated</span>
<span class="check-val neutral">{formatDate(panelDevice.updatedAt)}</span>
</div>
</div>
</div>
<div class="panel-footer">
<button class="btn btn-secondary" on:click={() => { panelOpen = false; panelDevice = null; }}>Close</button>
</div>
</div>
{/if}
<style>
.page-wrapper {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
padding: 24px 24px 0;
}
.page-body {
display: flex;
flex: 1;
overflow: hidden;
min-height: 0;
margin: 0 0 16px;
}
.page-right {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.content-frame {
flex: 1;
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
margin-top: 8px;
background: white;
}
/* Filter sidebar — canonical from template */
.filter-sidebar {
width: 220px; min-width: 220px;
background: #f8f9fa;
border-right: 1px solid #dee2e6;
display: flex; flex-direction: column;
overflow: hidden;
transition: width 0.2s ease, min-width 0.2s ease;
flex-shrink: 0;
}
.filter-sidebar.sidebar-collapsed { width: 40px; min-width: 40px; }
.filter-sidebar-toggle-bar {
display: flex; align-items: center; justify-content: space-between;
padding: 0.55rem 0.65rem;
border-bottom: 1px solid #dee2e6;
background: #eef2f7;
flex-shrink: 0; min-height: 36px;
}
.bar-filters-active { background: #dbeafe !important; border-bottom-color: #93c5fd !important; }
.bar-filters-active .filter-sidebar-title { color: #1e40af !important; }
.toggle-bar-right { display: flex; align-items: center; gap: 0.35rem; margin-left: auto; }
.filter-sidebar-title { font-size: var(--font-size-label); font-weight: 700; color: #374151; text-transform: uppercase; letter-spacing: 0.5px; }
.sidebar-stat-pill { font-size: var(--font-size-tiny); background: #e0e7ff; border: 1px solid #c7d2fe; color: #3730a3; border-radius: 10px; padding: 1px 8px; white-space: nowrap; }
.sidebar-toggle-btn { background: none; border: 1px solid #d1d5db; border-radius: 4px; padding: 1px 6px; cursor: pointer; font-size: 0.82rem; color: #6b7280; line-height: 1.4; flex-shrink: 0; margin-left: auto; }
.sidebar-toggle-btn:hover { background: #e5e7eb; border-color: #9ca3af; }
.filter-sidebar-content { display: flex; flex-direction: column; gap: 0.75rem; padding: 0.75rem 0.65rem; overflow-y: auto; flex: 1; }
.filter-section { display: flex; flex-direction: column; gap: 0.3rem; }
.filter-section-label { font-size: var(--font-size-tiny); font-weight: 700; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
.filter-select { padding: 0.4rem 0.6rem; border: 1px solid #d1d5db; border-radius: 6px; font-size: var(--font-size-body-sm); width: 100%; background: white; color: #374151; }
.filter-select:focus { outline: none; border-color: #3b82f6; }
.sidebar-clear-all-btn { border: none; background: #fee2e2; color: #dc2626; border-radius: 4px; padding: 0.3rem 0.6rem; cursor: pointer; font-size: var(--font-size-tiny); font-weight: 600; text-align: left; }
.sidebar-clear-all-btn:hover { background: #fecaca; }
/* Active filter chips */
.active-filters {
display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem;
margin-bottom: 8px; padding: 0.5rem 0.75rem; flex-shrink: 0;
}
.active-filters-label { font-size: var(--font-size-tiny); font-weight: 600; color: #555; margin-right: 0.25rem; }
.filter-tag {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.3rem 0.6rem; background: white;
border: 1px solid #ddd; border-left-width: 3px;
border-radius: 4px; font-size: var(--font-size-tiny); font-weight: 500; color: #333;
}
.filter-tag-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.filter-tag-remove {
display: inline-flex; align-items: center; justify-content: center;
width: 18px; height: 18px; border: none; background: #e5e7eb;
border-radius: 50%; cursor: pointer; font-size: var(--font-size-tiny);
color: #666; line-height: 1; padding: 0; margin-left: 0.15rem;
}
.filter-tag-remove:hover { background: #f44336; color: white; }
/* Toolbar */
.page-main {
flex: 1; overflow-y: auto; padding: 16px 24px 24px;
min-width: 0; display: flex; flex-direction: column;
}
.toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; flex-shrink: 0; }
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
.toolbar-right { display: flex; gap: 0.5rem; }
.btn-refresh {
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
border-radius: 6px; padding: 6px 12px; font-size: var(--font-size-body-sm); font-weight: 500; cursor: pointer;
}
.btn-refresh:hover { background: #e5e7eb; }
/* Table card — canonical */
.table-card {
background: white; border: 1px solid #e5e7eb; border-radius: 8px;
overflow: hidden; flex: 1; display: flex; flex-direction: column; min-height: 0;
}
.table-container { flex: 1; overflow: auto; }
.table-msg { padding: 2rem; text-align: center; color: #6b7280; font-size: var(--font-size-body-sm); }
table { width: 100%; border-collapse: collapse; }
thead { background: #f9fafb; position: sticky; top: 0; z-index: 5; }
th {
color: #374151; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.3px;
font-weight: 600; border-bottom: 2px solid #e5e7eb; padding: 0.6rem 0.75rem; text-align: left; white-space: nowrap;
}
td { border-bottom: 1px solid #f3f4f6; color: #1f2937; padding: 0.55rem 0.75rem; font-size: var(--font-size-label); vertical-align: middle; }
tbody tr:hover { background: #eff6ff; cursor: pointer; }
tbody tr:last-child td { border-bottom: none; }
.mono-cell { font-family: monospace; font-size: 0.78rem; color: #374151; }
.os-cell { font-size: 0.78rem; color: #4b5563; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.date-cell { white-space: nowrap; color: #6b7280; }
.actions { white-space: nowrap; }
.bool-cell { font-size: 0.9rem; font-weight: 700; text-align: center; }
.bool-cell.ok { color: #16a34a; }
.bool-cell.bad { color: #dc2626; }
.bool-cell.neutral { color: #9ca3af; }
.eol-badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 0.72rem; font-weight: 700; }
.eol-badge.eol-supported { background: #dcfce7; color: #16a34a; }
.eol-badge.eol-eol { background: #fee2e2; color: #dc2626; }
.eol-badge.eol-unknown { background: #f3f4f6; color: #6b7280; }
.img-badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 0.72rem; font-weight: 700; }
.img-badge.ok { background: #dcfce7; color: #16a34a; }
.img-badge.drift { background: #fee2e2; color: #dc2626; }
.img-badge.unknown { background: #f3f4f6; color: #9ca3af; }
.score-wrap { display: flex; align-items: center; gap: 8px; }
.score-bar { width: 60px; height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; flex-shrink: 0; }
.score-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
.score-fill.score-good { background: #16a34a; }
.score-fill.score-medium { background: #d97706; }
.score-fill.score-bad { background: #dc2626; }
.score-fill.score-unknown { background: #9ca3af; }
.score-text { font-size: 0.82rem; font-weight: 700; }
.score-text-good { color: #16a34a; }
.score-text-medium { color: #d97706; }
.score-text-bad { color: #dc2626; }
.score-text-unknown { color: #9ca3af; }
.btn-sm { border: none; border-radius: 5px; padding: 0.28rem 0.65rem; cursor: pointer; font-size: 0.78rem; font-weight: 500; }
.btn-secondary-sm { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; }
.btn-secondary-sm:hover { background: #e5e7eb; }
/* Slide-in panel — canonical */
.panel-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 999; cursor: pointer; }
.panel {
position: fixed; top: 0; right: 0; width: 480px; height: 100vh;
background: white; z-index: 1000; display: flex; flex-direction: column;
box-shadow: -4px 0 20px rgba(0,0,0,0.15); animation: panelIn 0.25s ease; overflow: hidden;
}
@keyframes panelIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
.panel-header {
background: linear-gradient(180deg, #081651 0%, #1c49b8 100%);
padding: 1rem 1.25rem; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
}
.panel-title { color: white; font-size: 1rem; font-weight: 700; font-family: monospace; }
.panel-subtitle { color: rgba(255,255,255,0.65); font-size: 0.78rem; margin-top: 2px; }
.panel-close {
background: rgba(255,255,255,0.15); border: none; color: white;
width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
font-size: 0.85rem; display: flex; align-items: center; justify-content: center;
flex-shrink: 0; transition: background 0.15s;
}
.panel-close:hover { background: rgba(255,255,255,0.3); }
.panel-body { flex: 1; overflow-y: auto; padding: 1.5rem; display: flex; flex-direction: column; gap: 1rem; }
.panel-footer { border-top: 1px solid #e5e7eb; padding: 1rem 1.5rem; display: flex; justify-content: flex-end; gap: 0.75rem; flex-shrink: 0; background: #f9fafb; }
.panel-score-row {
display: flex; flex-direction: column; align-items: center; gap: 6px;
padding: 1rem; background: #f9fafb; border-radius: 8px;
}
.big-score { font-size: 3rem; font-weight: 800; line-height: 1; }
.big-score-label { font-size: 0.75rem; color: #9ca3af; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.big-bar { width: 100%; height: 8px; margin-top: 4px; }
.check-list { display: flex; flex-direction: column; gap: 10px; }
.check-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.check-label { font-size: 0.82rem; color: #6b7280; flex-shrink: 0; width: 150px; }
.check-val { font-size: 0.88rem; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.check-val.ok { color: #16a34a; }
.check-val.bad { color: #dc2626; }
.check-val.neutral { color: #374151; font-weight: 400; }
.check-val.hash { font-family: monospace; font-size: 0.78rem; font-weight: 400; }
.penalty {
display: inline-block; background: #fee2e2; color: #dc2626;
font-size: 0.65rem; font-weight: 700; padding: 1px 6px; border-radius: 8px;
}
.check-divider { height: 1px; background: #f3f4f6; margin: 4px 0; }
/* Dark mode */
:global(.dark-mode) .content-frame { border-color: #374151; background: #1f2937; }
:global(.dark-mode) .filter-sidebar { background: #1f2937; border-right-color: #374151; }
:global(.dark-mode) .filter-sidebar-toggle-bar { background: #111827; border-bottom-color: #374151; }
:global(.dark-mode) .bar-filters-active { background: #1d4ed8 !important; border-bottom-color: #60a5fa !important; }
:global(.dark-mode) .bar-filters-active .filter-sidebar-title { color: #bfdbfe !important; }
:global(.dark-mode) .filter-sidebar-title { color: #9ca3af; }
:global(.dark-mode) .sidebar-toggle-btn { border-color: #4b5563; color: #9ca3af; }
:global(.dark-mode) .sidebar-toggle-btn:hover { background: #374151; }
:global(.dark-mode) .filter-select { background: #283040; border-color: #4b5563; color: #e2e8f0; }
:global(.dark-mode) .sidebar-stat-pill { background: #1e2d4a; border-color: #3730a3; color: #93c5fd; }
:global(.dark-mode) .sidebar-clear-all-btn { background: #3b1a1a; color: #f87171; }
:global(.dark-mode) .active-filters { background: #111827; border-color: #374151; }
:global(.dark-mode) .active-filters-label { color: #9ca3af; }
:global(.dark-mode) .filter-tag { background: #1f2937; border-color: #374151; color: #e5e5e5; }
:global(.dark-mode) .filter-tag-remove { background: #374151; color: #e5e7eb; }
:global(.dark-mode) .toolbar-count { color: #9ca3af; }
:global(.dark-mode) .btn-refresh { background: #374151; color: #e5e7eb; border-color: #4b5563; }
:global(.dark-mode) .table-card { background: #1f2937; }
:global(.dark-mode) thead { background: #111827; border-bottom-color: #374151; }
:global(.dark-mode) th { background: #111827; color: #9ca3af; border-bottom-color: #374151; }
:global(.dark-mode) td { color: #e5e7eb; border-bottom-color: #374151; }
:global(.dark-mode) tbody tr { background: #1f2937; }
:global(.dark-mode) tbody tr:hover { background: #374151; }
:global(.dark-mode) .mono-cell { color: #9ca3af; }
:global(.dark-mode) .os-cell { color: #94a3b8; }
:global(.dark-mode) .date-cell { color: #9ca3af; }
:global(.dark-mode) .table-msg { color: #9ca3af; }
:global(.dark-mode) .score-bar { background: #374151; }
:global(.dark-mode) .btn-secondary-sm { background: #374151; color: #e5e7eb; border-color: #4b5563; }
:global(.dark-mode) .eol-badge.eol-supported { background: #052e16; color: #86efac; }
:global(.dark-mode) .eol-badge.eol-eol { background: #450a0a; color: #fca5a5; }
:global(.dark-mode) .eol-badge.eol-unknown { background: #1e293b; color: #94a3b8; }
:global(.dark-mode) .img-badge.ok { background: #052e16; color: #86efac; }
:global(.dark-mode) .img-badge.drift { background: #450a0a; color: #fca5a5; }
:global(.dark-mode) .img-badge.unknown { background: #1e293b; color: #94a3b8; }
:global(.dark-mode) .panel { background: #1e293b; }
:global(.dark-mode) .panel-footer { background: #162032; border-top-color: #334155; }
:global(.dark-mode) .panel-score-row { background: #111827; }
:global(.dark-mode) .check-val.neutral { color: #cbd5e1; }
:global(.dark-mode) .check-divider { background: #374151; }
</style>