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">
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lastActivity: string;
|
lastActivity: string;
|
||||||
remainingSeconds: number;
|
remainingSeconds: number;
|
||||||
@@ -6,6 +8,27 @@
|
|||||||
|
|
||||||
let { lastActivity, remainingSeconds }: Props = $props();
|
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 {
|
function formatTime(seconds: number): string {
|
||||||
const m = Math.floor(seconds / 60);
|
const m = Math.floor(seconds / 60);
|
||||||
const s = seconds % 60;
|
const s = seconds % 60;
|
||||||
@@ -15,12 +38,14 @@
|
|||||||
function formatDate(iso: string): string {
|
function formatDate(iso: string): string {
|
||||||
return new Date(iso).toLocaleTimeString();
|
return new Date(iso).toLocaleTimeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let urgent = $derived(localRemaining < 60);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Idle Timer</h2>
|
<h2>Idle Timer</h2>
|
||||||
<div class="timer-display">
|
<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>
|
<span class="label">until idle check</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="last-activity">Last activity: {formatDate(lastActivity)}</p>
|
<p class="last-activity">Last activity: {formatDate(lastActivity)}</p>
|
||||||
@@ -38,6 +63,9 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.time.urgent {
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
.label {
|
.label {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -6,6 +6,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { services }: Props = $props();
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -19,7 +27,13 @@
|
|||||||
<span class="dot" class:healthy={service.healthy}></span>
|
<span class="dot" class:healthy={service.healthy}></span>
|
||||||
<div class="service-info">
|
<div class="service-info">
|
||||||
<span class="service-name">{service.name}</span>
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -50,7 +64,7 @@
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--text-dim);
|
background: var(--red);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.dot.healthy {
|
.dot.healthy {
|
||||||
@@ -64,8 +78,14 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
.service-host {
|
.service-target {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.service-meta {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const machineState = derived(status, ($s) => $s?.state ?? MachineState.OF
|
|||||||
|
|
||||||
let ws: WebSocket | null = null;
|
let ws: WebSocket | null = null;
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
function getWsUrl(): string {
|
function getWsUrl(): string {
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
@@ -20,13 +21,14 @@ function getWsUrl(): string {
|
|||||||
export function connectWs(): void {
|
export function connectWs(): void {
|
||||||
if (ws) return;
|
if (ws) return;
|
||||||
|
|
||||||
|
startPolling();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ws = new WebSocket(getWsUrl());
|
ws = new WebSocket(getWsUrl());
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
connected.set(true);
|
connected.set(true);
|
||||||
error.set(null);
|
error.set(null);
|
||||||
// Fetch full status on connect
|
|
||||||
refreshStatus();
|
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 {
|
export function disconnectWs(): void {
|
||||||
|
stopPolling();
|
||||||
if (reconnectTimer) {
|
if (reconnectTimer) {
|
||||||
clearTimeout(reconnectTimer);
|
clearTimeout(reconnectTimer);
|
||||||
reconnectTimer = null;
|
reconnectTimer = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user