Fix pausing log auto-refresh and cancel stale reads

This commit is contained in:
2026-10-04 17:19:15 -04:00
parent 2adadb0d05
commit 2413c29fc0
3 changed files with 268 additions and 48 deletions
+22 -48
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { tick, untrack, onMount } from 'svelte';
import { tick, untrack, onMount, onDestroy } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { toast } from '$ui/toast.js';
import { pollVisible } from '$ui/poll.js';
import { createLogRefresh, type LogResponse } from '$ui/log-refresh.js';
import Tabs from './primitives/Tabs.svelte';
import Button from './primitives/Button.svelte';
const uid = $props.id();
@@ -21,18 +21,6 @@
}
let { server, tabs, lines = 1000, pollMs = 5000 }: Props = $props();
interface LogResponse {
ok: boolean;
key?: string;
path?: string;
lines?: string[];
size?: number;
mtime?: string | null;
truncated?: boolean;
missing?: boolean;
reason?: string;
}
const active = $derived(tabs.find(tab => tab.key === $page.url.searchParams.get('log'))?.key ?? tabs[0]?.key ?? '');
let data = $state<Record<string, LogResponse | null>>({});
@@ -45,33 +33,21 @@
let loading = $state(false);
let autoRefresh = $state(true);
let refreshError = $state('');
const pending = new Set<string>();
let bodyEl = $state<HTMLPreElement | null>(null);
let stickToBottom = $state(true);
async function fetchLog(key: string): Promise<void> {
const currentServer = server;
const requestKey = currentServer + ':' + key;
if (pending.has(requestKey)) return;
pending.add(requestKey); loading = true;
try {
const res = await fetch(
`/api/servers/${encodeURIComponent(server)}/logs/${encodeURIComponent(key)}?lines=${lines}`
);
const body = (await res.json()) as LogResponse;
if (!res.ok || !body.ok) throw new Error(body.reason ?? 'Could not refresh log.');
if (server === currentServer) { data = { ...data, [key]: body }; if (key === active) refreshError = ''; }
} catch (err) {
if (server === currentServer && key === active) refreshError = (err as Error).message;
} finally {
pending.delete(requestKey);
if (server === currentServer && key === active) loading = false;
if (stickToBottom && !query && server === currentServer && key === active) {
await tick();
if (bodyEl) bodyEl.scrollTop = bodyEl.scrollHeight;
}
const refresh = createLogRefresh({
onLoading: value => { loading = value; },
onError: message => { refreshError = message; },
onData: (key, body) => {
data = { ...data, [key]: body };
const currentServer = server;
void tick().then(() => {
if (server === currentServer && active === key && stickToBottom && !query && bodyEl) bodyEl.scrollTop = bodyEl.scrollHeight;
});
}
}
});
onDestroy(() => refresh.dispose());
function onScroll(): void {
if (!bodyEl) return;
@@ -81,7 +57,10 @@
stickToBottom = nearBottom;
}
function toggleAuto() { autoRefresh = !autoRefresh; }
function toggleAuto(event: Event) {
autoRefresh = (event.currentTarget as HTMLInputElement).checked;
refresh.configure({ server, key: active, lines }, autoRefresh, pollMs);
}
async function jumpToLatest() {
query = ''; stickToBottom = true;
await tick(); if (bodyEl) bodyEl.scrollTop = bodyEl.scrollHeight;
@@ -111,15 +90,10 @@
}
$effect(() => { server; data = {}; refreshError = ''; });
$effect(() => {
server;
const key = active;
const target = { server, key: active, lines };
const auto = autoRefresh;
const interval = pollMs;
if (!key) return;
return untrack(() => {
if (auto) return pollVisible(() => fetchLog(key), interval);
void fetchLog(key);
});
untrack(() => refresh.configure(target, auto, interval));
});
function ago(iso: string | null | undefined): string {
@@ -154,7 +128,7 @@
<input type="checkbox" checked={autoRefresh} onchange={toggleAuto} />
Auto-refresh · {pollMs / 1000}s
</label>
<button class="refresh" onclick={() => fetchLog(active)} disabled={loading}>
<button class="refresh" onclick={() => refresh.refresh()} disabled={loading}>
{loading ? 'Refreshing…' : 'Refresh'}
</button>
</div>
@@ -163,9 +137,9 @@
<div class="follow-state"><span>{!autoRefresh ? 'Refresh paused' : 'Auto-refresh on'} · {query ? `${visibleLines.length} matching lines` : stickToBottom ? 'Following latest lines' : 'Reading earlier lines'}</span>{#if !stickToBottom || query}<Button size="sm" variant="ghost" onclick={jumpToLatest}>Jump to latest</Button>{/if}</div>
<div id={uid + '-panel'} role="tabpanel" aria-labelledby={uid + '-' + active}>
{#if refreshError}<div class="ui-error" role="alert">{refreshError} {current ? 'Showing the last loaded log.' : ''}<Button size="sm" onclick={() => fetchLog(active)} disabled={loading}>Retry</Button></div>{/if}
{#if refreshError}<div class="ui-error" role="alert">{refreshError} {current ? 'Showing the last loaded log.' : ''}<Button size="sm" onclick={() => refresh.refresh()} disabled={loading}>Retry</Button></div>{/if}
{#if !current && !refreshError}
<div class="empty">loading…</div>
<div class="empty">{loading ? 'loading…' : 'No log loaded. Select Refresh to load it.'}</div>
{:else if current && !current.ok}
<div class="error">error: {current.reason ?? 'unknown'}</div>
{:else if current?.missing}