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:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user