From 58375e6db0da81da898a04e05e59c346a0111a33 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Wed, 7 Oct 2026 23:15:55 -0400 Subject: [PATCH] Separate provisioning jobs from activity log --- src/lib/activity.ts | 2 ++ src/lib/navigation.ts | 3 ++- src/lib/server/activity.ts | 3 +++ src/lib/ui/activity-history.ts | 3 ++- src/lib/ui/components/ActivityLog.svelte | 16 +++++++------ src/routes/activity/+page.server.ts | 2 +- src/routes/activity/+page.svelte | 7 ++---- tests/unit/activity.test.ts | 29 ++++++++++++++++++++++-- tests/unit/navigation.test.ts | 6 +++-- tests/unit/ui-activity.test.ts | 14 ++++++++++-- 10 files changed, 64 insertions(+), 21 deletions(-) diff --git a/src/lib/activity.ts b/src/lib/activity.ts index fe26882..b7cb220 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -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; diff --git a/src/lib/navigation.ts b/src/lib/navigation.ts index c06e92b..8e857be 100644 --- a/src/lib/navigation.ts +++ b/src/lib/navigation.ts @@ -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: [ diff --git a/src/lib/server/activity.ts b/src/lib/server/activity.ts index 0da97cd..dac5581 100644 --- a/src/lib/server/activity.ts +++ b/src/lib/server/activity.ts @@ -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 = {}; 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; } diff --git a/src/lib/ui/activity-history.ts b/src/lib/ui/activity-history.ts index 2b6bd35..610b98f 100644 --- a/src/lib/ui/activity-history.ts +++ b/src/lib/ui/activity-history.ts @@ -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); } diff --git a/src/lib/ui/components/ActivityLog.svelte b/src/lib/ui/components/ActivityLog.svelte index 366a00b..c5a4c74 100644 --- a/src/lib/ui/components/ActivityLog.svelte +++ b/src/lib/ui/components/ActivityLog.svelte @@ -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(untrack(() => initial)); @@ -27,12 +27,14 @@ let filtersOpen = $state(false); let checkedAt = $state(untrack(() => initial ? Date.now() : null)); let results = $state(); - 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 @@

Activity history

{#snippet filterFields(suffix: string)}
- + - {#if source === undefined}{/if} + {#if source === undefined}{/if} {#if server === undefined}{/if} @@ -123,7 +125,7 @@ {/each} - {:else if !error} + {:else if !error} {#snippet actions()}{#if filterCount}{/if}{/snippet} {/if} {#if data && !loading && loadedUrl === apiUrl && (data.total > 0 || data.offset > 0)} diff --git a/src/routes/activity/+page.server.ts b/src/routes/activity/+page.server.ts index 3bd71eb..a9e6a8b 100644 --- a/src/routes/activity/+page.server.ts +++ b/src/routes/activity/+page.server.ts @@ -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 }) }; } diff --git a/src/routes/activity/+page.svelte b/src/routes/activity/+page.svelte index fdde290..3cb57c6 100644 --- a/src/routes/activity/+page.svelte +++ b/src/routes/activity/+page.svelte @@ -1,6 +1,5 @@ @@ -8,10 +7,8 @@ Activity ยท Electric Cherry
- - {#snippet actions()}{#if data.user && data.user.role !== 'viewer'}{/if}{/snippet} - - + +