Remove standalone Activity log page (Refs #153)

This commit is contained in:
2026-10-13 15:39:05 -04:00
parent 82e07ea33e
commit eceb38a6bc
21 changed files with 260 additions and 132 deletions
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { load as activity } from '../../src/routes/activity/+page.server.js';
import { load as operations } from '../../src/routes/operations/+page.server.js';
describe.each([
['/activity', activity],
['/operations', operations]
] as const)('retired %s page', (path, load) => {
it.each([
['', '/provisioning?view=history'],
['?source=provisioning', '/provisioning?view=history'],
['?source=autoscaling', '/provisioning?view=history&source=autoscaling'],
['?source=unknown&offset=100', '/provisioning?view=history'],
['?server=town', '/servers/town?tab=history'],
['?server=town%20%2F%201', '/servers/town%20%2F%201?tab=history'],
[
'?server=town&host=host-1&source=campaign&state=failed&days=7&q=files&limit=10&offset=20',
'/servers/town?tab=history&host=host-1&source=campaign&state=failed&days=7&q=files&limit=10&offset=20'
],
[
'?server=town&tab=overview&excludeProvisioning=true&excludeMaintenance=true#operation-1',
'/servers/town?tab=history#operation-1'
],
['?host=host%20%2F%201&offset=50', '/hosts/operations?host=host+%2F+1'],
['?server=%20&host=deleted-host', '/hosts/operations?host=deleted-host'],
['?source=host', '/hosts/operations'],
['?source=campaign&q=files&offset=50', '/campaigns?q=files'],
['?source=beta&q=release&offset=50', '/betas?view=history&q=release'],
[
'?source=maintenance&type=update&state=failed&offset=25',
'/maintenance?view=activity&type=update&source=maintenance&state=failed&offset=25'
],
['?source=recovery', '/maintenance?view=activity&source=recovery'],
['?returnTo=%2Fhosts', '/provisioning?view=history&returnTo=%2Fhosts']
])('redirects %s directly to %s', (query, location) => {
expect(() => load({ url: new URL(`https://ec.test${path}${query}`) })).toThrow(
expect.objectContaining({ status: 308, location })
);
});
});
+11 -15
View File
@@ -475,7 +475,7 @@ describe('unified retained Activity', () => {
expect(api.activityHistory({ source: 'campaign' }).total).toBe(1);
});
it('uses the same query contract for API and page loaders with no-store API responses', async () => {
it('keeps the shared history query contract and no-store API responses', async () => {
const api = await setup();
api.operation('a');
api.operation('b', { state: 'failed' });
@@ -483,23 +483,19 @@ describe('unified retained Activity', () => {
api.maintenance(1, 'failed');
api.incident('finding', 'health', 'attention');
const url = new URL(
'http://localhost/activity?state=failed&limit=1&excludeProvisioning=true&excludeMaintenance=true'
'http://localhost/api/activity?state=failed&limit=1&excludeProvisioning=true&excludeMaintenance=true'
);
const { GET } = await import('../../src/routes/api/activity/+server.js');
const { load } = await import('../../src/routes/activity/+page.server.js');
const response = GET({ url });
expect(response.headers.get('cache-control')).toBe('no-store');
expect(await response.json()).toEqual(JSON.parse(JSON.stringify(load({ url }).activity)));
expect(load({ url }).activity.rows[0].id).toBe('host:failed-check');
expect(
load({
url: new URL('http://localhost/activity?excludeProvisioning=false')
}).activity.rows.some((row) => row.id.startsWith('provisioning:'))
).toBe(false);
expect(
load({
url: new URL('http://localhost/activity?excludeMaintenance=false')
}).activity.rows.map((row) => row.id)
).toEqual(['host:failed-check']);
const body = await response.json();
expect(body).toEqual(
JSON.parse(JSON.stringify(api.activityHistory(parseActivityQuery(url.searchParams))))
);
expect(body.rows[0].id).toBe('host:failed-check');
const history = await GET({ url: new URL('http://localhost/api/activity') }).json();
expect(history.rows.map((row: { id: string }) => row.id)).toEqual(
expect.arrayContaining(['provisioning:a', 'provisioning:b', 'host:failed-check'])
);
});
});
+18
View File
@@ -8,6 +8,24 @@ import {
} from '../../src/lib/ui/command-palette.js';
describe('command palette destinations', () => {
it('omits the retired Activity log while retaining the pages that own history', () => {
for (const role of ['admin', 'operator', 'viewer', 'super_viewer'] as const) {
const destinations = palettePageResults(role, '').map((item) => item.href);
expect(destinations).not.toContain('/activity');
expect(destinations).not.toContain('/operations');
expect(palettePageResults(role, 'Activity log')).toEqual([]);
expect(destinations).toEqual(
expect.arrayContaining([
'/provisioning',
'/maintenance',
'/campaigns',
'/betas',
'/hosts'
])
);
}
});
it('lets Super Viewers search administration pages without offering operations', () => {
const results = palettePageResults('super_viewer', '');
expect(results.filter((item) => item.group === 'Actions')).toEqual([]);
+15 -3
View File
@@ -20,13 +20,25 @@ describe('navigation context', () => {
expect(returnLabel('/collections?view=history&launches=30')).toBe('Back to launch history');
});
it('retains Activity filters and a selected result when opening a detail', () => {
const source = '/activity?source=maintenance&state=failed&offset=25#operation-12';
it('retains Maintenance filters and a selected result when opening a detail', () => {
const source = '/maintenance?view=activity&state=failed&offset=25#operation-12';
const target = new URL(contextHref('/provisioning/12', source), 'http://localhost');
expect(safeReturnHref(target.searchParams.get('returnTo'), '/')).toBe(source);
expect(returnLabel(source)).toBe('Back to Activity');
expect(returnLabel(source)).toBe('Back to Maintenance');
});
it.each(['/activity', '/operations'])(
'replaces retired %s return links with their current history',
(path) => {
const source = `${path}?server=town&host=host-1&state=failed&offset=25`;
const expected = '/servers/town?tab=history&host=host-1&state=failed&offset=25';
expect(safeReturnHref(source, '/')).toBe(expected);
const target = new URL(contextHref('/provisioning/12', source), 'http://localhost');
expect(target.searchParams.get('returnTo')).toBe(expected);
expect(returnLabel(expected)).toBe('Back to server');
}
);
it('preserves the selected diagnostic tab on both sides of a contextual link', () => {
const url = new URL(
contextHref(
+6 -6
View File
@@ -11,11 +11,10 @@ describe('application navigation', () => {
['/servers/t5_zombie_theater_1', 'fleet', '/'],
['/collections/launches/42', 'fleet', '/collections'],
['/hosts/42', 'fleet', '/hosts'],
['/hosts/operations', 'activity', '/activity'],
['/hosts/operations', 'fleet', '/hosts'],
['/provisioning', 'activity', '/provisioning'],
['/provisioning/job-1', 'activity', '/provisioning'],
['/deployments/job-1', 'activity', '/provisioning'],
['/activity', 'activity', '/activity'],
['/betas/new', 'releases', '/betas'],
['/campaigns/42', 'releases', '/campaigns'],
['/configs/42', 'releases', '/configs'],
@@ -33,14 +32,15 @@ describe('application navigation', () => {
).toEqual([href]);
});
it('keeps Operations reachable from Activity and redirects legacy overview links', async () => {
it('keeps Provisioning and Maintenance in Activity without the retired log or shortcut', () => {
expect(
navigationGroups
.find((group) => group.id === 'activity')
?.items.map((item) => item.label)
).toEqual(['Activity log', 'Provisioning', 'Maintenance']);
const { load } = await import('../../src/routes/operations/+page.server.js');
expect(load).toThrow(expect.objectContaining({ status: 308, location: '/activity' }));
).toEqual(['Provisioning', 'Maintenance']);
const items = navigationGroups.flatMap((group) => group.items);
expect(items.some((item) => ['/activity', '/operations'].includes(item.href))).toBe(false);
expect(items.some((item) => item.shortcut === 'd')).toBe(false);
});
it('keeps administration and its shortcuts out of operator and viewer navigation', () => {
+17 -42
View File
@@ -1,10 +1,8 @@
import { beforeEach, afterEach, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import type { ComponentProps } from 'svelte';
import { writable, type Writable } from 'svelte/store';
import { page } from '$app/stores';
import ActivityLog from '../../src/lib/ui/components/ActivityLog.svelte';
import ActivityPage from '../../src/routes/activity/+page.svelte';
import {
activityApiHref,
activityHistoryHref,
@@ -13,7 +11,9 @@ import {
} from '../../src/lib/ui/activity-history.js';
import type { ActivityPage as ActivityReport, ActivityRow } from '../../src/lib/activity.js';
vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/activity') }) }));
vi.mock('$app/stores', () => ({
page: writable({ url: new URL('http://localhost/servers/town?tab=history') })
}));
vi.mock('$app/navigation', () => ({ goto: vi.fn() }));
vi.mock('$ui/stores/fleet.js', () => ({
fleet: writable({ servers: [], operations: [], receivedAt: 0 })
@@ -43,7 +43,7 @@ const report = (): ActivityReport => ({
offset: 0,
query: { limit: 25, offset: 0 }
});
function scope(path = '/activity') {
function scope(path = '/servers/town?tab=history') {
(page as unknown as Writable<{ url: URL }>).set({ url: new URL(`http://localhost${path}`) });
}
beforeEach(() => {
@@ -62,7 +62,7 @@ it.each([
['90', '90', 'Last 90 days'],
['14', '14', 'Last 14 days']
])('selects the current date range for days=%s on desktop and mobile', (days, value, label) => {
scope(days ? `/activity?days=${days}` : '/activity');
scope(days ? `/servers/town?tab=history&days=${days}` : '/servers/town?tab=history');
const body = render(ActivityLog, { props: { initial: report() } }).body;
for (const suffix of ['', '-mobile']) {
const select = body.match(
@@ -88,7 +88,8 @@ it('sends only supported normalized filters and enforces scoped identities', ()
expect(params.has('tab')).toBe(false);
expect(params.has('returnTo')).toBe(false);
expect(
activityQuery(new URL('https://example.test/activity?type=unknown&days=0')).type
activityQuery(new URL('https://example.test/servers/town?tab=history&type=unknown&days=0'))
.type
).toBeUndefined();
});
@@ -112,7 +113,9 @@ it('resets pagination when filtering and preserves the containing page, hash, an
});
it('keeps provisioning excluded from log polling after filtering or clearing filters', () => {
const url = new URL('http://localhost/activity?type=create&excludeProvisioning=false');
const url = new URL(
'http://localhost/servers/town?tab=history&type=create&excludeProvisioning=false'
);
const scope = { excludeProvisioning: true };
expect(new URL(activityApiHref(url, scope), url).searchParams.get('excludeProvisioning')).toBe(
'true'
@@ -125,7 +128,9 @@ it('keeps provisioning excluded from log polling after filtering or clearing fil
});
it('keeps maintenance excluded from log polling while preserving the Maintenance history scope', () => {
const url = new URL('http://localhost/activity?source=maintenance&excludeMaintenance=false');
const url = new URL(
'http://localhost/servers/town?tab=history&source=maintenance&excludeMaintenance=false'
);
const logScope = { excludeProvisioning: true, excludeMaintenance: true };
expect(
new URL(activityApiHref(url, logScope), url).searchParams.get('excludeMaintenance')
@@ -155,39 +160,8 @@ it('shows relative recent timestamps and preserves accurate older dates and inva
);
});
it('renders one page heading with provisioning management and filters removed', () => {
const body = render(ActivityPage, {
props: {
data: {
activity: report(),
user: { role: 'admin' },
setupRequired: false
} as ComponentProps<typeof ActivityPage>['data']
}
}).body;
expect(body.match(/<h1[ >]/g)).toHaveLength(1);
expect(body).not.toContain('Manage servers');
expect(body).not.toContain('href="/provisioning"');
expect(body).not.toContain('<option value="create"');
expect(body).not.toContain('<option value="provisioning"');
expect(body).not.toContain('<option value="restart"');
for (const value of ['update', 'health', 'maintenance', 'recovery'])
expect(body).not.toContain(`<option value="${value}"`);
expect(body).toContain('<option value="drift"');
const viewer = render(ActivityPage, {
props: {
data: {
activity: report(),
user: { role: 'viewer' },
setupRequired: false
} as ComponentProps<typeof ActivityPage>['data']
}
}).body;
expect(viewer).not.toContain('Manage servers');
});
it('renders named operation links, shared source memberships, friendly resources, and one freshness indicator', () => {
scope('/activity?state=succeeded&offset=25');
scope('/servers/town?tab=history&state=succeeded&offset=25');
const data = report();
data.offset = 25;
const body = render(ActivityLog, { props: { initial: data } }).body;
@@ -199,7 +173,7 @@ it('renders named operation links, shared source memberships, friendly resources
expect(body).toContain('shape-circle');
expect(body).toContain('15 s ago');
expect(body).toContain('datetime="2026-10-07T17:59:45Z"');
expect(body).toContain('returnTo=%2Factivity');
expect(body).toContain('returnTo=%2Fservers%2Ftown%3Ftab%3Dhistory');
expect(body).toContain('state%3Dsucceeded');
expect(body).toContain('offset%3D25');
expect(body).toContain('26–26 of 52');
@@ -209,6 +183,7 @@ it('renders named operation links, shared source memberships, friendly resources
});
it('preserves deleted server evidence without linking to a replacement instance', () => {
scope('/maintenance?view=activity');
const data = report();
data.rows = [{ ...row, serverDeletedAt: '2026-10-07T17:59:46Z' }];
const body = render(ActivityLog, { props: { initial: data } }).body;
@@ -239,7 +214,7 @@ it('offers one mobile filter sheet with distinct IDs and hides fixed scope contr
});
it('offers useful empty and out-of-range page recovery', () => {
scope('/activity?q=missing&offset=100');
scope('/servers/town?tab=history&q=missing&offset=100');
const data = report();
data.rows = [];
data.offset = 100;
+64 -2
View File
@@ -1,8 +1,10 @@
import { describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render } from 'svelte/server';
import type { ComponentProps } from 'svelte';
import { writable } from 'svelte/store';
import { writable, type Writable } from 'svelte/store';
import { page } from '$app/stores';
import HostOperations from '../../src/routes/hosts/operations/+page.svelte';
import Hosts from '../../src/routes/hosts/+page.svelte';
import type { HostOperationSummary } from '../../src/lib/hosts.js';
vi.mock('$app/stores', () => ({
@@ -11,6 +13,14 @@ vi.mock('$app/stores', () => ({
}));
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn() }));
function setPage(path: string) {
(page as unknown as Writable<unknown>).set({
url: new URL(`http://localhost${path}`),
data: { user: { role: 'viewer' } }
});
}
beforeEach(() => setPage('/hosts/operations'));
const operation: HostOperationSummary = {
id: 'retained-op',
hostId: 'host-1',
@@ -29,6 +39,58 @@ const operation: HostOperationSummary = {
};
describe('host operation history presentation', () => {
it('keeps fleet-wide operation history reachable even when all hosts have been deleted', () => {
setPage('/hosts');
const body = render(Hosts, {
props: {
data: { user: null, setupRequired: false, hosts: [], serverNow: Date.now() }
}
}).body;
expect(body).toContain('href="/hosts/operations"');
expect(body).toContain('Operation history');
});
it.each([
[
'/hosts/operations?operation=retained-op&host=host-1&filter=all',
'/hosts/operations?host=host-1&amp;filter=all',
'Back to host operation history'
],
[
'/hosts/operations?operation=retained-op&returnTo=%2Fservers%2Ftown%3Ftab%3Dhistory',
'/servers/town?tab=history',
'Back to server'
],
[
'/hosts/operations?operation=retained-op&returnTo=%2Factivity%3Fhost%3Dhost-1',
'/hosts/operations?host=host-1',
'Back to host operation history'
]
])('returns from a selected operation to existing history: %s', (path, href, label) => {
setPage(path);
const body = render(HostOperations, {
props: {
data: {
user: null,
setupRequired: false,
history: {
operations: [],
page: 1,
pageSize: 25,
totalPages: 1,
total: 0,
filter: 'all'
},
scope: { operation: operation.id, host: operation.hostId },
hostName: operation.hostName
}
}
}).body;
expect(body).toContain(`href="${href}"`);
expect(body).toContain(label);
expect(body).not.toContain('href="/activity');
});
it('uses readable operation, server and state labels while keeping identifiers in technical detail', () => {
const body = render(HostOperations, {
props: {
+5 -1
View File
@@ -31,7 +31,9 @@ describe('sidebar navigation', () => {
expect(body).toContain('Main navigation');
expect(body).toMatch(/<a[^>]*href="\/campaigns"[^>]*aria-current="page"/);
expect(body).toContain('href="/betas"');
expect(body).toContain('href="/activity"');
expect(body).not.toContain('href="/activity"');
expect(body).toContain('href="/provisioning"');
expect(body).toContain('href="/maintenance"');
expect(body).toContain('href="/metrics"');
expect(body).toContain('href="/bank"');
expect(body).not.toContain('href="/settings"');
@@ -120,6 +122,8 @@ describe('application shell', () => {
expect(body).toContain('aria-label="Keyboard shortcuts"');
expect(body).not.toContain('class="primary-navigation"');
expect(body).not.toContain('variant="secondary"');
expect(body).not.toContain('g d');
expect(body).not.toContain('Activity log');
});
it('preserves the viewer notice and hides navigation while a password change is required', () => {
+3 -1
View File
@@ -164,7 +164,9 @@ it('keeps Super Viewer diagnostics readable without offering probes or lifecycle
it('uses the shared Activity view for server history', () => {
const body = pageBody('history');
expect(body).toContain('Open in Activity');
expect(body).toContain('Activity filters');
expect(body).not.toContain('Open in Activity');
expect(body).not.toContain('href="/activity');
expect(body).not.toContain('Loading campaign history');
expect(body).not.toContain('>Provisioning history</a>');
});