Exclude maintenance history from activity log

This commit is contained in:
2026-10-07 23:31:21 -04:00
parent c0c34d8735
commit 584084c09c
8 changed files with 76 additions and 13 deletions
+2
View File
@@ -35,6 +35,7 @@ export interface ActivityRow {
export interface ActivityQuery {
excludeProvisioning?: boolean;
excludeMaintenance?: boolean;
server?: string;
host?: string;
type?: ActivityType;
@@ -69,6 +70,7 @@ export function parseActivityQuery(params: URLSearchParams): ActivityQuery {
offset: integer(params.get('offset'), 0, 0, 1_000_000_000)
};
if (params.get('excludeProvisioning') === 'true') query.excludeProvisioning = true;
if (params.get('excludeMaintenance') === 'true') query.excludeMaintenance = 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;
+4 -1
View File
@@ -130,6 +130,8 @@ normalized AS (
type SqlRow = Omit<ActivityRow, 'source' | 'sources' | 'href'> & { sources: string; route: string; recordId: string; secondaryHostId: string | null };
const sourceOrder: ActivitySource[] = ['maintenance', 'recovery', 'campaign', 'beta', 'autoscaling', 'provisioning', 'host'];
// Use the same membership rule for Maintenance history and its exclusion from the main log.
const maintenanceMembershipSql = "(instr(sources,'|maintenance|')>0 OR instr(sources,'|recovery|')>0)";
function present(row: SqlRow): ActivityRow {
const sources = sourceOrder.filter(source => row.sources.includes(`|${source}|`));
const href = row.route === 'maintenance-job' ? `/maintenance?view=overview&job=${encodeURIComponent(row.recordId)}#job-${row.recordId}`
@@ -154,12 +156,13 @@ export function activityHistory(input: ActivityQuery = {}, now = Date.now()): Ac
// 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' AND type <> 'restart'");
if (query.excludeMaintenance) conditions.push(`NOT ${maintenanceMembershipSql}`);
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; }
if (query.source) {
// Maintenance is the user-facing umbrella; Recovery remains available as a narrower filter.
conditions.push(query.source === 'maintenance' ? "(instr(sources,'|maintenance|')>0 OR instr(sources,'|recovery|')>0)" : 'instr(sources,@source)>0');
conditions.push(query.source === 'maintenance' ? maintenanceMembershipSql : 'instr(sources,@source)>0');
if (query.source !== 'maintenance') bindings.source = `|${query.source}|`;
}
if (query.days) { conditions.push('julianday(at)>=julianday(@since)'); bindings.since = new Date(now - query.days * 86_400_000).toISOString(); }
+2 -1
View File
@@ -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; excludeProvisioning?: boolean }
export interface ActivityScope { server?: string; source?: ActivitySource; excludeProvisioning?: boolean; excludeMaintenance?: 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. */
@@ -10,6 +10,7 @@ export function activityQuery(url: URL, scope: ActivityScope = {}): ActivityQuer
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));
if (scope.excludeMaintenance !== undefined) params.set('excludeMaintenance', String(scope.excludeMaintenance));
return parseActivityQuery(params);
}
+15 -7
View File
@@ -17,7 +17,7 @@
import Freshness from './Freshness.svelte';
import HistoricalServerIdentity from './HistoricalServerIdentity.svelte';
let { server, source, initial, excludeProvisioning = false }: { server?: string; source?: ActivitySource; initial?: ActivityPage; excludeProvisioning?: boolean } = $props();
let { server, source, initial, excludeProvisioning = false, excludeMaintenance = false }: { server?: string; source?: ActivitySource; initial?: ActivityPage; excludeProvisioning?: boolean; excludeMaintenance?: boolean } = $props();
const id = $props.id();
const headingId = `${id}-activity-heading`;
let data = $state<ActivityPage | undefined>(untrack(() => initial));
@@ -27,14 +27,22 @@
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, excludeProvisioning }).q ?? ''));
let search = $state(untrack(() => activityQuery($page.url, { server, source, excludeProvisioning, excludeMaintenance }).q ?? ''));
let pendingUrl: URL | undefined;
let navigationId = 0;
const filters = $derived(activityQuery($page.url, { server, source, excludeProvisioning }));
const apiUrl = $derived(activityApiHref($page.url, { server, source, excludeProvisioning }));
const filters = $derived(activityQuery($page.url, { server, source, excludeProvisioning, excludeMaintenance }));
const apiUrl = $derived(activityApiHref($page.url, { server, source, excludeProvisioning, excludeMaintenance }));
let loadedUrl = $state(untrack(() => initial ? apiUrl : ''));
const typeOptions = $derived(Object.entries(activityTypes).filter(([key]) => !excludeProvisioning || !['create', 'start', 'restart', 'rebuild', 'stop', 'remove'].includes(key)));
const sourceOptions = $derived(Object.entries(activitySources).filter(([key]) => !excludeProvisioning || !['provisioning', 'autoscaling'].includes(key)));
const typeOptions = $derived(Object.entries(activityTypes).filter(([key]) =>
(!excludeProvisioning || !['create', 'start', 'restart', 'rebuild', 'stop', 'remove'].includes(key))
&& (!excludeMaintenance || !['update', 'health'].includes(key))));
const sourceOptions = $derived(Object.entries(activitySources).filter(([key]) =>
(!excludeProvisioning || !['provisioning', 'autoscaling'].includes(key))
&& (!excludeMaintenance || !['maintenance', 'recovery'].includes(key))));
const historyDescription = $derived(excludeMaintenance
? 'File changes and host work appear here.'
: excludeProvisioning ? 'File changes, maintenance, and host work appear here.'
: 'Server operations, file changes, maintenance, and host work appear here.');
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] : []) ?? [])]);
@@ -125,7 +133,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.' : excludeProvisioning ? 'File changes, maintenance, and host work appear here.' : '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.' : historyDescription}>
{#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)}
+1 -1
View File
@@ -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), excludeProvisioning: true }) };
return { activity: activityHistory({ ...parseActivityQuery(url.searchParams), excludeProvisioning: true, excludeMaintenance: true }) };
}
+2 -2
View File
@@ -7,8 +7,8 @@
<svelte:head><title>Activity · Electric Cherry</title></svelte:head>
<div class="page">
<SectionHeader title="Activity" subtitle="Releases, maintenance, and host activity. Server jobs are in Provisioning." />
<ActivityLog initial={data.activity} excludeProvisioning />
<SectionHeader title="Activity" subtitle="Releases and host activity. Server jobs are in Provisioning; maintenance and recovery are in Maintenance history." />
<ActivityLog initial={data.activity} excludeProvisioning excludeMaintenance />
</div>
<style>