feat(web): add 5s polling and real-time idle timer countdown
- Add 5-second polling interval to refresh all dashboard data - IdleTimer now counts down every second client-side - Timer turns orange when < 60 seconds remain - ServiceList shows target URL, health status, and last check time - Unhealthy services now show red dot instead of gray Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
lastActivity: string;
|
||||
remainingSeconds: number;
|
||||
@@ -6,6 +8,27 @@
|
||||
|
||||
let { lastActivity, remainingSeconds }: Props = $props();
|
||||
|
||||
let localRemaining = $state(remainingSeconds);
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
$effect(() => {
|
||||
localRemaining = remainingSeconds;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
interval = setInterval(() => {
|
||||
if (localRemaining > 0) localRemaining--;
|
||||
}, 1000);
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
});
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
@@ -15,12 +38,14 @@
|
||||
function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleTimeString();
|
||||
}
|
||||
|
||||
let urgent = $derived(localRemaining < 60);
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
<h2>Idle Timer</h2>
|
||||
<div class="timer-display">
|
||||
<span class="time">{formatTime(remainingSeconds)}</span>
|
||||
<span class="time" class:urgent>{formatTime(localRemaining)}</span>
|
||||
<span class="label">until idle check</span>
|
||||
</div>
|
||||
<p class="last-activity">Last activity: {formatDate(lastActivity)}</p>
|
||||
@@ -38,6 +63,9 @@
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.time.urgent {
|
||||
color: var(--orange);
|
||||
}
|
||||
.label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
}
|
||||
|
||||
let { services }: Props = $props();
|
||||
|
||||
function timeAgo(iso: string | null): string {
|
||||
if (!iso) return 'never';
|
||||
const diff = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
||||
if (diff < 5) return 'just now';
|
||||
if (diff < 60) return `${diff}s ago`;
|
||||
return `${Math.floor(diff / 60)}m ago`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card">
|
||||
@@ -19,7 +27,13 @@
|
||||
<span class="dot" class:healthy={service.healthy}></span>
|
||||
<div class="service-info">
|
||||
<span class="service-name">{service.name}</span>
|
||||
<span class="service-host">{service.host}</span>
|
||||
<span class="service-target">{service.target}</span>
|
||||
<span class="service-meta">
|
||||
{service.healthy ? 'Healthy' : 'Unhealthy'}
|
||||
{#if service.lastCheck}
|
||||
· checked {timeAgo(service.lastCheck)}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -50,7 +64,7 @@
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-dim);
|
||||
background: var(--red);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot.healthy {
|
||||
@@ -64,8 +78,14 @@
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.service-host {
|
||||
.service-target {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-family: monospace;
|
||||
}
|
||||
.service-meta {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@ export const machineState = derived(status, ($s) => $s?.state ?? MachineState.OF
|
||||
|
||||
let ws: WebSocket | null = null;
|
||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function getWsUrl(): string {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
@@ -20,13 +21,14 @@ function getWsUrl(): string {
|
||||
export function connectWs(): void {
|
||||
if (ws) return;
|
||||
|
||||
startPolling();
|
||||
|
||||
try {
|
||||
ws = new WebSocket(getWsUrl());
|
||||
|
||||
ws.onopen = () => {
|
||||
connected.set(true);
|
||||
error.set(null);
|
||||
// Fetch full status on connect
|
||||
refreshStatus();
|
||||
};
|
||||
|
||||
@@ -78,7 +80,22 @@ export async function refreshStatus(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling(): void {
|
||||
if (pollTimer) return;
|
||||
pollTimer = setInterval(() => {
|
||||
refreshStatus();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function stopPolling(): void {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnectWs(): void {
|
||||
stopPolling();
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user