Make UI language, timestamps and live feedback consistent
This commit is contained in:
@@ -3,8 +3,11 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { createLogRefresh, type LogResponse } from '$ui/log-refresh.js';
|
||||
import { stripGameColorCodes } from '$ui/log-text.js';
|
||||
import Tabs from './primitives/Tabs.svelte';
|
||||
import Button from './primitives/Button.svelte';
|
||||
import Freshness from './Freshness.svelte';
|
||||
import RelativeTime from './RelativeTime.svelte';
|
||||
const uid = $props.id();
|
||||
|
||||
interface TabDef {
|
||||
@@ -22,6 +25,7 @@
|
||||
|
||||
const active = $derived(tabs.find(tab => tab.key === $page.url.searchParams.get('log'))?.key ?? tabs[0]?.key ?? '');
|
||||
let data = $state<Record<string, LogResponse | null>>({});
|
||||
let checkedAt = $state<Record<string, number>>({});
|
||||
|
||||
function chooseLog(key: string) {
|
||||
const url = new URL($page.url); url.searchParams.set('log', key);
|
||||
@@ -37,11 +41,12 @@
|
||||
let stickToBottom = $state(true);
|
||||
|
||||
const refresh = createLogRefresh({
|
||||
onReset: () => { data = {}; refreshError = ''; copyStatus = ''; },
|
||||
onReset: () => { data = {}; checkedAt = {}; refreshError = ''; copyStatus = ''; },
|
||||
onLoading: value => { loading = value; },
|
||||
onError: message => { refreshError = message; },
|
||||
onData: (key, body) => {
|
||||
data = { ...data, [key]: body };
|
||||
checkedAt = { ...checkedAt, [key]: Date.now() };
|
||||
const currentServer = server;
|
||||
void tick().then(() => {
|
||||
if (server === currentServer && active === key && stickToBottom && !query && bodyEl) bodyEl.scrollTop = bodyEl.scrollHeight;
|
||||
@@ -97,15 +102,6 @@
|
||||
untrack(() => refresh.configure(target, auto, interval));
|
||||
});
|
||||
|
||||
function ago(iso: string | null | undefined): string {
|
||||
if (!iso) return 'never';
|
||||
const d = Date.now() - new Date(iso).getTime();
|
||||
if (d < 60_000) return `${Math.max(1, Math.floor(d / 1000))}s ago`;
|
||||
if (d < 3_600_000) return `${Math.floor(d / 60_000)}m ago`;
|
||||
if (d < 86_400_000) return `${Math.floor(d / 3_600_000)}h ago`;
|
||||
return `${Math.floor(d / 86_400_000)}d ago`;
|
||||
}
|
||||
|
||||
function fmtBytes(n: number | undefined): string {
|
||||
if (n === undefined || n === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
@@ -115,7 +111,7 @@
|
||||
}
|
||||
|
||||
let current = $derived(data[active] ?? null);
|
||||
const visibleLines = $derived((current?.lines ?? []).filter(line => !query || line.toLowerCase().includes(query.toLowerCase())));
|
||||
const visibleLines = $derived((current?.lines ?? []).map(stripGameColorCodes).filter(line => !query || line.toLowerCase().includes(query.toLowerCase())));
|
||||
</script>
|
||||
|
||||
<div class="log-panel">
|
||||
@@ -123,25 +119,26 @@
|
||||
<Tabs tabs={tabs.map(t => ({id:t.key,label:t.label}))} value={active} onchange={chooseLog} idPrefix={uid} panelId={uid + '-panel'} ariaLabel="Log files" size="sm" />
|
||||
<div class="right">
|
||||
{#if current?.mtime}
|
||||
<span class="meta">{fmtBytes(current.size)} · updated {ago(current.mtime)}</span>
|
||||
<span class="meta">{fmtBytes(current.size)} · File changed <RelativeTime value={current.mtime} /></span>
|
||||
{/if}
|
||||
<label class="auto">
|
||||
<input type="checkbox" checked={autoRefresh} onchange={toggleAuto} />
|
||||
Auto-refresh · {pollMs / 1000}s
|
||||
Auto-refresh
|
||||
</label>
|
||||
<button class="refresh" onclick={() => refresh.refresh()} disabled={loading}>
|
||||
{#if !autoRefresh}<button class="refresh" onclick={() => refresh.refresh()} disabled={loading}>
|
||||
{loading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-tools"><label class="log-search">Search loaded lines<input type="search" bind:value={query} placeholder="Find text in this log" /></label><details class="log-actions"><summary>More</summary><div class="action-items"><Button size="sm" onclick={copyLink}>Copy link</Button><Button size="sm" disabled={!current?.lines?.length} onclick={() => copy(visibleLines.join('\n'), 'Log text copied.')}>Copy {query ? 'matches' : 'log'}</Button><Button size="sm" disabled={!current?.lines?.length} onclick={download}>Download tail</Button></div></details></div>
|
||||
<Freshness updatedAt={checkedAt[active] ?? null} paused={!autoRefresh} intervalLabel={`Updates every ${pollMs / 1000} seconds`} error={refreshError ? 'Log updates interrupted.' : ''} onRetry={() => void refresh.refresh()} retryDisabled={loading} />
|
||||
<div class="log-tools"><label class="log-search">Search loaded lines<input type="search" bind:value={query} placeholder="Find text in this log" /></label><details class="log-actions"><summary>More</summary><div class="action-items"><Button size="sm" onclick={copyLink}>Copy link</Button><Button size="sm" disabled={!current?.lines?.length} onclick={() => copy(visibleLines.join('\n'), 'Log text copied.')}>Copy {query ? 'matches' : 'log'}</Button><Button size="sm" disabled={!current?.lines?.length} onclick={download}>Download raw tail</Button></div></details></div>
|
||||
{#if copyStatus}<p class="copy-status" role="status">{copyStatus}</p>{/if}
|
||||
<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">Couldn’t load logs. {current ? 'Showing the last loaded log.' : ''}<Button size="sm" onclick={() => refresh.refresh()} disabled={loading}>Retry</Button><details><summary>Technical details</summary>{refreshError}</details></div>{/if}
|
||||
{#if refreshError}<div class="ui-error" role="alert">Couldn’t load logs. {current ? 'Showing the last loaded log.' : ''}<details><summary>Technical details</summary>{refreshError}</details></div>{/if}
|
||||
{#if !current && !refreshError}
|
||||
<div class="empty">{loading ? 'loading…' : 'No log loaded. Select Refresh to load it.'}</div>
|
||||
<div class="empty">{loading ? 'Loading log…' : autoRefresh ? 'Waiting for the first log update.' : '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}
|
||||
|
||||
Reference in New Issue
Block a user