Retain host and server scope during investigations

This commit is contained in:
2026-10-05 19:05:07 -04:00
parent 056fbd7517
commit cd577b646c
12 changed files with 95 additions and 29 deletions
+7 -5
View File
@@ -1,19 +1,21 @@
import { redirect } from '@sveltejs/kit';
import { listHostOperationHistory } from '$lib/server/hosts/index.js';
import { isHostOperationFilter } from '$lib/hosts.js';
import { getHost, listHostOperationHistory } from '$lib/server/hosts/index.js';
import { hostOperationScope, isHostOperationFilter } from '$lib/hosts.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = ({ url }) => {
const requestedPage = url.searchParams.get('page');
const requestedFilter = url.searchParams.get('filter');
const filter = isHostOperationFilter(requestedFilter) ? requestedFilter : 'important';
const history = listHostOperationHistory(Number(requestedPage ?? 1), filter);
const scope = hostOperationScope(url.searchParams);
const history = listHostOperationHistory(Number(requestedPage ?? 1), filter, scope);
if ((requestedPage !== null && requestedPage !== String(history.page))
|| (requestedFilter !== null && requestedFilter !== filter)) {
const params = new URLSearchParams();
const params = new URLSearchParams(url.searchParams);
params.delete('page');
if (requestedFilter !== null) params.set('filter', filter);
if (history.page !== 1) params.set('page', String(history.page));
redirect(307, `/hosts/operations${params.size ? `?${params}` : ''}`);
}
return { history };
return { history, scope, hostName: scope.host ? getHost(scope.host)?.name ?? scope.host : null };
};
+14 -8
View File
@@ -1,11 +1,13 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { beforeNavigate } from '$app/navigation';
import { navigating } from '$app/stores';
import { navigating, page } from '$app/stores';
import type { HostOperationFilter, HostOperationHistoryPage } from '$lib/hosts.js';
import type { PageData } from './$types';
import { pollVisible } from '$ui/poll.js';
import { timeFormat } from '$ui/stores/time-zone.js';
import BackLink from '$ui/components/BackLink.svelte';
import { contextHref } from '$lib/navigation-context.js';
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
import Button from '$ui/components/primitives/Button.svelte';
@@ -25,9 +27,10 @@
{ id: 'all', label: 'All operations', description: 'Every recorded remote operation.', empty: 'No remote operations yet.' }
];
const selectedFilter = $derived(filters.find(filter => filter.id === history.filter)!);
function pageHref(page: number, filter: HostOperationFilter = history.filter) {
const params = new URLSearchParams({ filter });
if (page !== 1) params.set('page', String(page));
function pageHref(pageNumber: number, filter: HostOperationFilter = history.filter) {
const params = new URLSearchParams($page.url.searchParams);
params.set('filter', filter); params.delete('page');
if (pageNumber !== 1) params.set('page', String(pageNumber));
return `/hosts/operations?${params}`;
}
function cancel() { sequence++; request?.abort(); request = null; refreshing = false; }
@@ -40,7 +43,7 @@
request = controller;
refreshing = true;
try {
const params = new URLSearchParams({ page: String(history.page), filter: history.filter });
const params = new URLSearchParams({ ...data.scope, page: String(history.page), filter: history.filter });
const response = await fetch(`/api/hosts/operations?${params}`, {
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15000), ...(signal ? [signal] : [])])
});
@@ -60,12 +63,14 @@
onMount(() => { const stop = pollVisible(refresh, 15000, false); return () => { stop(); cancel(); }; });
</script>
<svelte:head><title>Operation history · Hosts · Electric Cherry</title></svelte:head>
<svelte:head><title>Host operation history · Electric Cherry</title></svelte:head>
<section class="page" aria-busy={changingPage}>
<SectionHeader title="Operation history" eyebrow="Hosts" subtitle="Remote operations across all hosts, newest first.">
<SectionHeader title="Host operation history" eyebrow="Operations" subtitle="Remote operations, newest first. Filtered views retain their host and server context.">
{#snippet actions()}<Button disabled={refreshing || changingPage} onclick={() => void refresh()}>{refreshing ? 'Refreshing…' : 'Refresh'}</Button>{/snippet}
</SectionHeader>
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/hosts" />{/if}
{#if data.scope.host || data.scope.server}<div class="scope" role="status"><span>Showing {data.hostName ? `host: ${data.hostName}` : ''}{data.scope.host && data.scope.server ? ' · ' : ''}{data.scope.server ? `server: ${data.scope.server}` : ''}</span><a href={`/hosts/operations?filter=${history.filter}`}>Clear scope</a></div>{/if}
<div class="history-filters">
<nav class="filter-links" aria-label="Operation history filters">
{#each filters as filter}
@@ -86,7 +91,7 @@
<tbody>{#each history.operations as operation (operation.id)}
<tr>
<td class="operation">{operation.kind}<small>{operation.id}</small></td>
<td>{#if operation.hostName !== null}<a href={`/hosts/${operation.hostId}`}>{operation.hostName}</a>{:else}Deleted host<small>{operation.hostId}</small>{/if}</td>
<td>{#if operation.hostName !== null}<a href={contextHref(`/hosts/${operation.hostId}`, $page.url)}>{operation.hostName}</a>{:else}Deleted host<small>{operation.hostId}</small>{/if}</td>
<td class="target">
<span>{operation.serverName ?? operation.instanceId ?? 'Host-level'}</span>
{#if operation.targetPath}<small class="target-path">{operation.targetPath}</small>{/if}
@@ -112,6 +117,7 @@
</section>
<style>
.scope { display:flex; flex-wrap:wrap; gap:16px; color:var(--text-secondary); font-size:var(--text-sm); }
.page { display: grid; gap: 24px; min-width: 0; }
.history-filters { min-width: 0; }
.history-filters p { margin: 10px 0 0; color: var(--text-secondary); font-size: var(--text-sm); }