Separate provisioning jobs from activity log
This commit is contained in:
@@ -34,6 +34,7 @@ export interface ActivityRow {
|
||||
}
|
||||
|
||||
export interface ActivityQuery {
|
||||
excludeProvisioning?: boolean;
|
||||
server?: string;
|
||||
host?: string;
|
||||
type?: ActivityType;
|
||||
@@ -67,6 +68,7 @@ export function parseActivityQuery(params: URLSearchParams): ActivityQuery {
|
||||
limit: integer(params.get('limit'), 25, 1, 100),
|
||||
offset: integer(params.get('offset'), 0, 0, 1_000_000_000)
|
||||
};
|
||||
if (params.get('excludeProvisioning') === 'true') query.excludeProvisioning = true;
|
||||
for (const key of ['server', 'host', 'q'] as const) {
|
||||
const value = params.get(key)?.trim().slice(0, key === 'q' ? 200 : 255);
|
||||
if (value) query[key] = value;
|
||||
|
||||
@@ -20,7 +20,8 @@ export const navigationGroups: NavigationGroup[] = [
|
||||
{ label: 'Campaigns', href: '/campaigns', shortcut: 'a' }
|
||||
] },
|
||||
{ id: 'activity', label: 'Activity', items: [
|
||||
{ label: 'Activity log', href: '/activity', shortcut: 'd', matches: path => path === '/activity' || path === '/operations' || path === '/hosts/operations' || /^\/(provisioning|deployments)(\/|$)/.test(path) },
|
||||
{ label: 'Activity log', href: '/activity', shortcut: 'd', matches: path => path === '/activity' || path === '/operations' || path === '/hosts/operations' },
|
||||
{ label: 'Provisioning', href: '/provisioning', matches: path => /^\/(provisioning|deployments)(\/|$)/.test(path) },
|
||||
{ label: 'Maintenance', href: '/maintenance', shortcut: 'w', matches: path => /^\/(maintenance|watchdog)(\/|$)/.test(path) }
|
||||
] },
|
||||
{ id: 'players', label: 'Players', items: [
|
||||
|
||||
@@ -151,6 +151,9 @@ export function activityHistory(input: ActivityQuery = {}, now = Date.now()): Ac
|
||||
initialized.add(handle);
|
||||
}
|
||||
const conditions: string[] = [];
|
||||
// Keep lifecycle jobs in Provisioning, including jobs triggered by automation.
|
||||
// Apply this before totals and pagination; resource histories can still include them.
|
||||
if (query.excludeProvisioning) conditions.push("route <> 'provisioning'");
|
||||
const bindings: Record<string, string | number> = {};
|
||||
for (const key of ['server', 'type', 'state'] as const) if (query[key]) { conditions.push(`${key}=@${key}`); bindings[key] = query[key]; }
|
||||
if (query.host) { conditions.push('(hostId=@host OR secondaryHostId=@host)'); bindings.host = query.host; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseActivityQuery, type ActivityQuery, type ActivitySource } from '../activity.js';
|
||||
import { createTimeFormatter } from '../time-zone.js';
|
||||
|
||||
export interface ActivityScope { server?: string; source?: ActivitySource }
|
||||
export interface ActivityScope { server?: string; source?: ActivitySource; excludeProvisioning?: boolean }
|
||||
const keys = ['server', 'host', 'type', 'source', 'state', 'days', 'q', 'limit', 'offset'] as const;
|
||||
|
||||
/** Scoped views always retain their server/source, including after reset or a pasted URL. */
|
||||
@@ -9,6 +9,7 @@ export function activityQuery(url: URL, scope: ActivityScope = {}): ActivityQuer
|
||||
const params = new URLSearchParams(url.searchParams);
|
||||
if (scope.server !== undefined) params.set('server', scope.server);
|
||||
if (scope.source !== undefined) params.set('source', scope.source);
|
||||
if (scope.excludeProvisioning !== undefined) params.set('excludeProvisioning', String(scope.excludeProvisioning));
|
||||
return parseActivityQuery(params);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Freshness from './Freshness.svelte';
|
||||
import HistoricalServerIdentity from './HistoricalServerIdentity.svelte';
|
||||
|
||||
let { server, source, initial }: { server?: string; source?: ActivitySource; initial?: ActivityPage } = $props();
|
||||
let { server, source, initial, excludeProvisioning = false }: { server?: string; source?: ActivitySource; initial?: ActivityPage; excludeProvisioning?: boolean } = $props();
|
||||
const id = $props.id();
|
||||
const headingId = `${id}-activity-heading`;
|
||||
let data = $state<ActivityPage | undefined>(untrack(() => initial));
|
||||
@@ -27,12 +27,14 @@
|
||||
let filtersOpen = $state(false);
|
||||
let checkedAt = $state<number | null>(untrack(() => initial ? Date.now() : null));
|
||||
let results = $state<HTMLOListElement>();
|
||||
let search = $state(untrack(() => activityQuery($page.url, { server, source }).q ?? ''));
|
||||
let search = $state(untrack(() => activityQuery($page.url, { server, source, excludeProvisioning }).q ?? ''));
|
||||
let pendingUrl: URL | undefined;
|
||||
let navigationId = 0;
|
||||
const filters = $derived(activityQuery($page.url, { server, source }));
|
||||
const apiUrl = $derived(activityApiHref($page.url, { server, source }));
|
||||
const filters = $derived(activityQuery($page.url, { server, source, excludeProvisioning }));
|
||||
const apiUrl = $derived(activityApiHref($page.url, { server, source, excludeProvisioning }));
|
||||
let loadedUrl = $state(untrack(() => initial ? apiUrl : ''));
|
||||
const typeOptions = $derived(Object.entries(activityTypes).filter(([key]) => !excludeProvisioning || !['create', 'start', 'rebuild', 'stop', 'remove'].includes(key)));
|
||||
const sourceOptions = $derived(Object.entries(activitySources).filter(([key]) => !excludeProvisioning || !['provisioning', 'autoscaling'].includes(key)));
|
||||
const filterCount = $derived([filters.type, filters.state, source === undefined ? filters.source : '', server === undefined ? filters.server : '', filters.host, filters.days, filters.q].filter(Boolean).length);
|
||||
const serverOptions = $derived.by(() => {
|
||||
const names = new Set([...$fleet.servers.map(item => item.name), ...(data?.rows.flatMap(row => row.server ? [row.server] : []) ?? [])]);
|
||||
@@ -89,9 +91,9 @@
|
||||
<h2 class="sr-only" id={headingId}>Activity history</h2>
|
||||
{#snippet filterFields(suffix: string)}
|
||||
<div class="filters">
|
||||
<label for={`activity-type${suffix}`}>Type<select id={`activity-type${suffix}`} value={filters.type ?? ''} onchange={event => update({ type: event.currentTarget.value })}><option value="">All types</option>{#each Object.entries(activityTypes) as [value, label]}<option {value}>{label}</option>{/each}</select></label>
|
||||
<label for={`activity-type${suffix}`}>Type<select id={`activity-type${suffix}`} value={filters.type ?? ''} onchange={event => update({ type: event.currentTarget.value })}><option value="">All types</option>{#each typeOptions as [value, label]}<option {value}>{label}</option>{/each}</select></label>
|
||||
<label for={`activity-state${suffix}`}>State<select id={`activity-state${suffix}`} value={filters.state ?? ''} onchange={event => update({ state: event.currentTarget.value })}><option value="">All states</option>{#each Object.entries(activityStates) as [value, label]}<option {value}>{label}</option>{/each}</select></label>
|
||||
{#if source === undefined}<label for={`activity-source${suffix}`}>Source<select id={`activity-source${suffix}`} value={filters.source ?? ''} onchange={event => update({ source: event.currentTarget.value })}><option value="">All sources</option>{#each Object.entries(activitySources) as [value, label]}<option {value}>{label}</option>{/each}</select></label>{/if}
|
||||
{#if source === undefined}<label for={`activity-source${suffix}`}>Source<select id={`activity-source${suffix}`} value={filters.source ?? ''} onchange={event => update({ source: event.currentTarget.value })}><option value="">All sources</option>{#each sourceOptions as [value, label]}<option {value}>{label}</option>{/each}</select></label>{/if}
|
||||
{#if server === undefined}<label for={`activity-server${suffix}`}>Server<select id={`activity-server${suffix}`} value={filters.server ?? ''} onchange={event => update({ server: event.currentTarget.value })}><option value="">All servers</option>{#each serverOptions as name}<option value={name}>{serverTitle(name)}</option>{/each}</select></label>{/if}
|
||||
<label for={`activity-host${suffix}`}>Host<select id={`activity-host${suffix}`} value={filters.host ?? ''} onchange={event => update({ host: event.currentTarget.value })}><option value="">All hosts</option>{#each hostOptions as [value, label]}<option {value}>{label}</option>{/each}</select></label>
|
||||
<label for={`activity-days${suffix}`}>Date range<select id={`activity-days${suffix}`} value={filters.days ?? 0} onchange={event => update({ days: event.currentTarget.value })}><option value="0">All retained history</option><option value="1">Last 24 hours</option><option value="7">Last 7 days</option><option value="30">Last 30 days</option><option value="90">Last 90 days</option>{#if filters.days && ![1, 7, 30, 90].includes(filters.days)}<option value={filters.days}>Last {filters.days} days</option>{/if}</select></label>
|
||||
@@ -123,7 +125,7 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{:else if !error}<EmptyState title={filterCount ? 'No matching activity' : 'No activity recorded yet'} description={filterCount ? 'Try another type, state, server, host, or date range.' : 'Server operations, file changes, maintenance, and host work appear here.'}>
|
||||
{:else if !error}<EmptyState title={filterCount ? 'No matching activity' : 'No activity recorded yet'} description={filterCount ? 'Try another type, state, server, host, or date range.' : excludeProvisioning ? 'File changes, maintenance, and host work appear here.' : 'Server operations, file changes, maintenance, and host work appear here.'}>
|
||||
{#snippet actions()}{#if filterCount}<Button size="sm" onclick={reset}>Clear filters</Button>{/if}{/snippet}
|
||||
</EmptyState>{/if}
|
||||
{#if data && !loading && loadedUrl === apiUrl && (data.total > 0 || data.offset > 0)}
|
||||
|
||||
@@ -2,5 +2,5 @@ import { parseActivityQuery } from '$lib/activity.js';
|
||||
import { activityHistory } from '$lib/server/activity.js';
|
||||
|
||||
export function load({ url }: { url: URL }) {
|
||||
return { activity: activityHistory(parseActivityQuery(url.searchParams)) };
|
||||
return { activity: activityHistory({ ...parseActivityQuery(url.searchParams), excludeProvisioning: true }) };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ActivityLog from '$ui/components/ActivityLog.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
let { data }: { data: import('./$types').PageData } = $props();
|
||||
</script>
|
||||
@@ -8,10 +7,8 @@
|
||||
<svelte:head><title>Activity · Electric Cherry</title></svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<SectionHeader title="Activity" subtitle="Server operations, releases, maintenance, and host work in one history.">
|
||||
{#snippet actions()}{#if data.user && data.user.role !== 'viewer'}<Button href="/provisioning">Manage servers</Button>{/if}{/snippet}
|
||||
</SectionHeader>
|
||||
<ActivityLog initial={data.activity} />
|
||||
<SectionHeader title="Activity" subtitle="Releases, maintenance, and host activity. Server jobs are in Provisioning." />
|
||||
<ActivityLog initial={data.activity} excludeProvisioning />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user