Unify retained operation history in Activity
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import ActivityLog from '$ui/components/ActivityLog.svelte';
|
||||
import BackLink from '$ui/components/BackLink.svelte';
|
||||
import { contextHref } from '$lib/navigation-context.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { tick, untrack } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { MaintenanceOverview, MaintenancePolicy } from '$lib/maintenance.js';
|
||||
@@ -38,6 +39,15 @@
|
||||
let scheduleLoaded = false;
|
||||
let displayedZone = $state('');
|
||||
let request = 0;
|
||||
const focusedJob = $derived(/^[1-9]\d*$/.test($page.url.searchParams.get('job') ?? '') ? $page.url.searchParams.get('job') : null);
|
||||
let scrolledJob: string | null = null;
|
||||
$effect(() => {
|
||||
if (!focusedJob) { scrolledJob = null; return; }
|
||||
if (data?.runtime.jobs.some(job => String(job.id) === focusedJob) && scrolledJob !== focusedJob) {
|
||||
scrolledJob = focusedJob;
|
||||
void tick().then(() => document.getElementById(`job-${focusedJob}`)?.scrollIntoView({ block: 'start' }));
|
||||
}
|
||||
});
|
||||
let checkedAt = $state<number | null>(null);
|
||||
const tabs = [{ id: 'overview', label: 'Overview' }, { id: 'policies', label: 'Policies & schedules' }, { id: 'activity', label: 'Activity' }];
|
||||
const policies: { id: MaintenancePolicy; title: string; description: string }[] = [
|
||||
@@ -71,7 +81,7 @@
|
||||
const isLatest = (item: (typeof activities)[number]) => !item.job || !data?.runtime.jobs.some(j => j.server === item.server && j.kind === item.job!.kind && j.id > item.job!.id);
|
||||
const attention = $derived(activities.filter(a => !a.server_deleted_at && a.state === 'attention' && isLatest(a)).length);
|
||||
const waiting = $derived(activities.filter(a => !a.server_deleted_at && a.state === 'active').length);
|
||||
const matching = $derived(activities.filter(a =>
|
||||
const matching = $derived(activities.filter(a => focusedJob ? a.key === `job-${focusedJob}` :
|
||||
(kind === 'all' || a.kind === kind) &&
|
||||
(!server || `${a.server} ${serverTitle(a.server)}`.toLowerCase().includes(server.toLowerCase())) &&
|
||||
(view === 'overview' ? !a.server_deleted_at && a.state !== 'done' && isLatest(a) : status === 'all' || a.state === status)
|
||||
@@ -103,12 +113,12 @@
|
||||
checkedAt = Date.now();
|
||||
if (!scheduleLoaded) { scheduleEnabled = body.runtime.schedule.enabled; scheduleLoaded = true; }
|
||||
}
|
||||
async function load() {
|
||||
async function load(job = focusedJob) {
|
||||
if (busy || refreshing) return;
|
||||
refreshing = true;
|
||||
const current = ++request;
|
||||
try {
|
||||
const response = await fetch('/api/maintenance');
|
||||
const response = await fetch(`/api/maintenance${job ? `?job=${encodeURIComponent(job)}` : ''}`);
|
||||
if (!response.ok) throw new Error('Could not refresh maintenance.');
|
||||
const body = await response.json();
|
||||
if (current === request) { receive(body); error = ''; }
|
||||
@@ -126,7 +136,7 @@
|
||||
} catch (cause) { error = (cause as Error).message; }
|
||||
finally { busy = false; }
|
||||
}
|
||||
onMount(() => pollVisible(load, 5000));
|
||||
$effect(() => { const job = focusedJob; return untrack(() => pollVisible(() => load(job), 5000)); });
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Maintenance | Electric Cherry</title></svelte:head>
|
||||
@@ -137,7 +147,7 @@
|
||||
{#if data}<Button variant="ghost" disabled={busy} onclick={() => act({ action: 'pause', paused: !data!.policies.paused })}>{data.policies.paused ? 'Resume maintenance' : 'Pause all maintenance'}</Button>{/if}
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
{#if error}<p class="ui-error" role="alert">{error} {data ? 'Showing the last successful refresh.' : ''} <Button size="sm" onclick={load} disabled={busy}>Retry refresh</Button></p>{/if}
|
||||
{#if error}<p class="ui-error" role="alert">{error} {data ? 'Showing the last successful refresh.' : ''} <Button size="sm" onclick={() => load()} disabled={busy}>Retry refresh</Button></p>{/if}
|
||||
{#if !error}<Freshness updatedAt={checkedAt} intervalLabel="Updates every 5 seconds" />{/if}
|
||||
{#if notice}<p role="status">{notice}</p>{/if}
|
||||
{#if data?.policies.paused}<div class="paused"><strong>All maintenance is paused.</strong> Checks and verification continue. New actions, scheduled runs, and queued maintenance are held. Submitted operations finish recovery. Campaigns, betas, and manual server operations continue.</div>{/if}
|
||||
@@ -167,6 +177,9 @@
|
||||
{#if data.runtime.schedule.nextAt}<p>Next run: {$timeFormat.dateTime(data.runtime.schedule.nextAt)}{data.policies.paused ? ' · Held while maintenance is paused' : ''}</p>{/if}
|
||||
<p class="muted">Disabling the schedule stops future runs. Existing requests remain in Activity, where unsubmitted restarts can be cancelled.</p>
|
||||
</section>
|
||||
{:else if view === 'activity'}
|
||||
<ActivityLog source="maintenance" />
|
||||
<p class="muted">Use <a href="/maintenance?view=overview#activity">active work and attention</a> to cancel waiting maintenance or retry a failed verification after inspection.</p>
|
||||
{:else}
|
||||
{#if view === 'overview'}
|
||||
<div class="summary"><span><strong>{attention}</strong> {attention === 1 ? 'needs' : 'need'} attention</span><span><strong>{waiting}</strong> active</span><span><strong>{outdated.length}</strong> outdated</span><span><strong>{unknown.length}</strong> {unknown.length === 1 ? 'version' : 'versions'} unverified</span></div>
|
||||
@@ -223,7 +236,8 @@
|
||||
</section>
|
||||
{/if}
|
||||
<section id="activity">
|
||||
<h2>{view === 'overview' ? 'Active work & attention' : 'Maintenance activity'}</h2>
|
||||
<h2>{focusedJob ? `Maintenance request #${focusedJob}` : view === 'overview' ? 'Active work & attention' : 'Maintenance activity'}</h2>
|
||||
{#if focusedJob}<p class="muted">Selected retained request. <a href="/maintenance?view=overview#activity">Show active work and attention</a></p>{/if}
|
||||
<div class="activity">{#each matching.slice(0, limit) as item (item.key)}
|
||||
<article class="card" id={item.key}>
|
||||
<div class="row-heading"><h3>{item.title} · <HistoricalServerIdentity name={item.server} deletedAt={item.server_deleted_at} /></h3><Badge variant={item.state === 'attention' ? 'danger' : item.state === 'active' ? 'info' : 'neutral'}>{item.label}</Badge></div>
|
||||
@@ -234,7 +248,7 @@
|
||||
{#if item.job?.state === 'failed' && !item.server_deleted_at && item.job.operation_state === 'done'}<Button size="sm" disabled={busy || data.policies.paused} onclick={() => act({ action: 'retry', id: item.job!.id })}>Retry after inspection</Button>{/if}
|
||||
</div>
|
||||
</article>
|
||||
{:else}<EmptyState title={view === 'overview' ? 'No active maintenance in this view' : 'No matching activity'} description="Findings, scheduled restarts, and update verification appear here." />{/each}</div>
|
||||
{:else}<EmptyState title={focusedJob ? 'Maintenance request unavailable' : view === 'overview' ? 'No active maintenance in this view' : 'No matching activity'} description={focusedJob ? 'This request may have expired under the history retention settings.' : 'Findings, scheduled restarts, and update verification appear here.'} />{/each}</div>
|
||||
<ListFooter total={matching.length} bind:limit onchange={() => saveContext(false)} label="activities" />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user