feat(aria): replace countdown toggle with labeled auto-refresh switch

Replace cryptic SVG countdown circle with a clear labeled toggle switch
in ThreatEvents, DevicePosture, and PrecursorAlerts. State is persisted
to localStorage per-component (aria_threats_autoRefresh, aria_posture_autoRefresh,
aria_alerts_autoRefresh).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 10:03:16 -04:00
parent d8619c4ecb
commit dd10b8754a
3 changed files with 129 additions and 114 deletions
+43 -38
View File
@@ -122,16 +122,17 @@
// Auto-refresh
const REFRESH_SECONDS = 60;
const CIRCUMFERENCE = 2 * Math.PI * 14;
let secondsLeft = REFRESH_SECONDS;
let refreshing = false;
let autoRefreshEnabled = true;
const _AR_KEY = 'aria_posture_autoRefresh';
let autoRefreshEnabled: boolean = typeof localStorage !== 'undefined'
? (localStorage.getItem(_AR_KEY) ?? 'true') === 'true'
: true;
let tickInterval: ReturnType<typeof setInterval>;
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
}
@@ -166,32 +167,13 @@
<p>Per-device security compliance — disk encryption, image integrity, OS currency</p>
</div>
<div class="header-controls">
<label class="auto-refresh-toggle" title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s` : 'Auto-refresh disabled'}>
<input type="checkbox" checked={autoRefreshEnabled} on:change={toggleAutoRefresh} />
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-label">Auto-refresh{autoRefreshEnabled ? ` (${secondsLeft}s)` : ''}</span>
</label>
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
on:click={toggleAutoRefresh}
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
aria-label="Toggle auto-refresh">
<svg width="72" height="72" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
{#if autoRefreshEnabled}
<circle cx="18" cy="18" r="14" fill="none"
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
{:else}
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
{/if}
{#if refreshing}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
{:else if autoRefreshEnabled}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
{:else}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
{/if}
</svg>
</button>
</div>
</div>
@@ -570,6 +552,39 @@
.toolbar { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.auto-refresh-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
flex-shrink: 0;
}
.auto-refresh-toggle input { display: none; }
.toggle-track {
width: 36px; height: 20px;
background: rgba(255,255,255,0.25);
border-radius: 10px;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.auto-refresh-toggle input:checked + .toggle-track { background: #34d399; }
.toggle-thumb {
position: absolute;
top: 3px; left: 3px;
width: 14px; height: 14px;
background: white;
border-radius: 50%;
transition: transform 0.2s;
}
.auto-refresh-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
.toggle-label {
color: rgba(255,255,255,0.9);
font-size: 0.8rem;
white-space: nowrap;
min-width: 110px;
}
.btn-manual-refresh {
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
color: white; border-radius: 50%; width: 36px; height: 36px;
@@ -578,16 +593,6 @@
}
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
.refresh-countdown {
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
}
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
.refresh-countdown.paused { opacity: 0.5; }
.refresh-countdown.paused:hover { opacity: 0.8; }
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
/* Table card — canonical */
.table-card {
+43 -38
View File
@@ -110,16 +110,17 @@
// Auto-refresh
const REFRESH_SECONDS = 60;
const CIRCUMFERENCE = 2 * Math.PI * 14;
let secondsLeft = REFRESH_SECONDS;
let refreshing = false;
let autoRefreshEnabled = true;
const _AR_KEY = 'aria_alerts_autoRefresh';
let autoRefreshEnabled: boolean = typeof localStorage !== 'undefined'
? (localStorage.getItem(_AR_KEY) ?? 'true') === 'true'
: true;
let tickInterval: ReturnType<typeof setInterval>;
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
}
@@ -162,32 +163,13 @@
<p>Multi-phase attack sequence detection — jackpotting and skimming precursors</p>
</div>
<div class="header-controls">
<label class="auto-refresh-toggle" title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s` : 'Auto-refresh disabled'}>
<input type="checkbox" checked={autoRefreshEnabled} on:change={toggleAutoRefresh} />
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-label">Auto-refresh{autoRefreshEnabled ? ` (${secondsLeft}s)` : ''}</span>
</label>
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
on:click={toggleAutoRefresh}
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
aria-label="Toggle auto-refresh">
<svg width="72" height="72" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
{#if autoRefreshEnabled}
<circle cx="18" cy="18" r="14" fill="none"
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
{:else}
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
{/if}
{#if refreshing}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
{:else if autoRefreshEnabled}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
{:else}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
{/if}
</svg>
</button>
</div>
</div>
@@ -551,6 +533,39 @@
.toolbar-count { font-size: var(--font-size-body-sm); color: #6b7280; }
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.auto-refresh-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
flex-shrink: 0;
}
.auto-refresh-toggle input { display: none; }
.toggle-track {
width: 36px; height: 20px;
background: rgba(255,255,255,0.25);
border-radius: 10px;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.auto-refresh-toggle input:checked + .toggle-track { background: #34d399; }
.toggle-thumb {
position: absolute;
top: 3px; left: 3px;
width: 14px; height: 14px;
background: white;
border-radius: 50%;
transition: transform 0.2s;
}
.auto-refresh-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
.toggle-label {
color: rgba(255,255,255,0.9);
font-size: 0.8rem;
white-space: nowrap;
min-width: 110px;
}
.btn-manual-refresh {
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
color: white; border-radius: 50%; width: 36px; height: 36px;
@@ -559,16 +574,6 @@
}
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
.refresh-countdown {
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
}
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
.refresh-countdown.paused { opacity: 0.5; }
.refresh-countdown.paused:hover { opacity: 0.8; }
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
/* Table card — canonical */
.table-card {
+43 -38
View File
@@ -85,16 +85,17 @@
// Auto-refresh
const REFRESH_SECONDS = 60;
const CIRCUMFERENCE = 2 * Math.PI * 14;
let secondsLeft = REFRESH_SECONDS;
let refreshing = false;
let autoRefreshEnabled = true;
const _AR_KEY = 'aria_threats_autoRefresh';
let autoRefreshEnabled: boolean = typeof localStorage !== 'undefined'
? (localStorage.getItem(_AR_KEY) ?? 'true') === 'true'
: true;
let tickInterval: ReturnType<typeof setInterval>;
$: dashOffset = CIRCUMFERENCE * (1 - secondsLeft / REFRESH_SECONDS);
function toggleAutoRefresh() {
autoRefreshEnabled = !autoRefreshEnabled;
if (typeof localStorage !== 'undefined') localStorage.setItem(_AR_KEY, String(autoRefreshEnabled));
if (autoRefreshEnabled) secondsLeft = REFRESH_SECONDS;
}
@@ -128,32 +129,13 @@
<p>IOC match events detected across the fleet</p>
</div>
<div class="header-controls">
<label class="auto-refresh-toggle" title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s` : 'Auto-refresh disabled'}>
<input type="checkbox" checked={autoRefreshEnabled} on:change={toggleAutoRefresh} />
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-label">Auto-refresh{autoRefreshEnabled ? ` (${secondsLeft}s)` : ''}</span>
</label>
<button class="btn-manual-refresh" on:click={doRefresh} disabled={refreshing}
title="Refresh now" aria-label="Refresh">{refreshing ? '⟳' : '↻'}</button>
<button class="refresh-countdown" class:paused={!autoRefreshEnabled}
on:click={toggleAutoRefresh}
title={autoRefreshEnabled ? `Auto-refresh in ${secondsLeft}s — click to pause` : 'Auto-refresh paused — click to resume'}
aria-label="Toggle auto-refresh">
<svg width="72" height="72" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="3"/>
{#if autoRefreshEnabled}
<circle cx="18" cy="18" r="14" fill="none"
stroke={secondsLeft <= 5 ? '#f87171' : 'rgba(255,255,255,0.85)'}
stroke-width="3" stroke-dasharray={CIRCUMFERENCE} stroke-dashoffset={dashOffset}
stroke-linecap="round" transform="rotate(-90 18 18)" class:pulsing={secondsLeft <= 5}/>
{:else}
<circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,255,255,0.3)"
stroke-width="3" stroke-dasharray="4 3" transform="rotate(-90 18 18)"/>
{/if}
{#if refreshing}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">⟳</text>
{:else if autoRefreshEnabled}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.9)" font-family="sans-serif">{secondsLeft}</text>
{:else}
<text x="18" y="22" text-anchor="middle" font-size="9" fill="rgba(255,255,255,0.5)" font-family="sans-serif">⏸</text>
{/if}
</svg>
</button>
</div>
</div>
@@ -290,6 +272,39 @@
}
.header-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }
.auto-refresh-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
flex-shrink: 0;
}
.auto-refresh-toggle input { display: none; }
.toggle-track {
width: 36px; height: 20px;
background: rgba(255,255,255,0.25);
border-radius: 10px;
position: relative;
transition: background 0.2s;
flex-shrink: 0;
}
.auto-refresh-toggle input:checked + .toggle-track { background: #34d399; }
.toggle-thumb {
position: absolute;
top: 3px; left: 3px;
width: 14px; height: 14px;
background: white;
border-radius: 50%;
transition: transform 0.2s;
}
.auto-refresh-toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(16px); }
.toggle-label {
color: rgba(255,255,255,0.9);
font-size: 0.8rem;
white-space: nowrap;
min-width: 110px;
}
.btn-manual-refresh {
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
color: white; border-radius: 50%; width: 36px; height: 36px;
@@ -298,16 +313,6 @@
}
.btn-manual-refresh:hover:not(:disabled) { background: rgba(255,255,255,0.28); }
.btn-manual-refresh:disabled { opacity: 0.5; cursor: not-allowed; }
.refresh-countdown {
flex-shrink: 0; background: none; border: none; cursor: pointer; padding: 0;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
opacity: 0.9; transition: opacity 0.15s, transform 0.15s;
}
.refresh-countdown:hover { opacity: 1; transform: scale(1.08); }
.refresh-countdown.paused { opacity: 0.5; }
.refresh-countdown.paused:hover { opacity: 0.8; }
@keyframes countdown-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
:global(.pulsing) { animation: countdown-pulse 0.8s ease-in-out infinite; }
.page-body {
display: flex; flex: 1; overflow: hidden; min-height: 0; margin: 0 0 16px;