Compare commits
2
Commits
82e07ea33e
...
09396ab584
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09396ab584 | ||
|
|
eceb38a6bc |
@@ -17,7 +17,7 @@ Campaigns retain managed-file deployment evidence; Collections define lineups.
|
|||||||
when the running revision is unverified.
|
when the running revision is unverified.
|
||||||
- **Policies & schedules** controls health recovery, configuration drift, automatic
|
- **Policies & schedules** controls health recovery, configuration drift, automatic
|
||||||
Plutonium updates, and the optional daily restart schedule independently.
|
Plutonium updates, and the optional daily restart schedule independently.
|
||||||
- **Activity** uses the shared Activity log, scoped to maintenance and recovery.
|
- **Activity** shows retained history scoped to maintenance and recovery.
|
||||||
Filter retained findings, update results, and scheduled restarts in one timeline.
|
Filter retained findings, update results, and scheduled restarts in one timeline.
|
||||||
Follow a finding for evidence or an operation for execution progress. Use
|
Follow a finding for evidence or an operation for execution progress. Use
|
||||||
Overview’s **Active work & attention** to cancel waiting work or retry failed
|
Overview’s **Active work & attention** to cancel waiting work or retry failed
|
||||||
|
|||||||
@@ -525,6 +525,10 @@ local credentials already exist.
|
|||||||
|
|
||||||
## Host and game-file API
|
## Host and game-file API
|
||||||
|
|
||||||
|
Open **Fleet → Hosts → Operation history** for retained operations across all
|
||||||
|
hosts, including deleted hosts. A host's **Operation history** action scopes the
|
||||||
|
same history to that host. Filters and pagination retain the selected scope.
|
||||||
|
|
||||||
- `GET /api/hosts/:id`: host details and deletion blockers.
|
- `GET /api/hosts/:id`: host details and deletion blockers.
|
||||||
- `GET /api/hosts/operations?page=1&filter=important`: operation history across
|
- `GET /api/hosts/operations?page=1&filter=important`: operation history across
|
||||||
hosts, newest first, with 25 summaries per page and filtered total/page counts.
|
hosts, newest first, with 25 summaries per page and filtered total/page counts.
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# Provisioning
|
# Provisioning
|
||||||
|
|
||||||
Open **Activity → Activity log** for retained lifecycle, host, campaign, and
|
Open **Activity → Provisioning** for server queues, bulk cancellation, lifecycle
|
||||||
maintenance history. Use **Manage servers** from that page or command search
|
actions, and retained lifecycle history. **Fleet → Collections** owns reusable lineups and collection launch
|
||||||
for server queues, bulk cancellation, and lifecycle actions. **Fleet → Collections** owns reusable lineups and collection launch
|
|
||||||
progress; their individual operations remain visible in Provisioning.
|
progress; their individual operations remain visible in Provisioning.
|
||||||
Provisioning owns the queues, safety checks, execution, and
|
Provisioning owns the queues, safety checks, execution, and
|
||||||
recovery for all six actions. Create a server using **Create server**. For an existing
|
recovery for all six actions. Create a server using **Create server**. For an existing
|
||||||
@@ -305,6 +304,21 @@ query parameters. Collection launch links at `/provisioning/collections/:id` or
|
|||||||
query parameters. `/api/deployments` remains compatible, including old campaign
|
query parameters. `/api/deployments` remains compatible, including old campaign
|
||||||
detail endpoints. No history or IDs are renamed or discarded.
|
detail endpoints. No history or IDs are renamed or discarded.
|
||||||
|
|
||||||
|
The standalone Activity log has been retired. Use a server's **History** tab for
|
||||||
|
its combined history, **Activity → Maintenance → Activity** for maintenance and
|
||||||
|
recovery, **Releases → Campaigns** or **Releases → Betas → History** for release
|
||||||
|
history, and **Fleet → Hosts → Operation history** for retained host operations.
|
||||||
|
Host operation history remains available after a host is deleted.
|
||||||
|
|
||||||
|
Legacy `/activity` and `/operations` bookmarks redirect to Provisioning history
|
||||||
|
by default. Server-scoped bookmarks open the server's History tab; host-scoped
|
||||||
|
bookmarks open host operation history. Source-only bookmarks open the matching
|
||||||
|
Campaigns, Betas, Maintenance, host operations, or autoscaling provisioning
|
||||||
|
history. Shared history filters are retained when opening server or Maintenance
|
||||||
|
history; pagination is reset when switching to a different kind of history.
|
||||||
|
`GET /api/activity` and the shared history views remain available. Historical
|
||||||
|
records, event collection, and retention are unchanged.
|
||||||
|
|
||||||
Start and Stop use durable `instance.start` and `instance.stop` host-agent
|
Start and Stop use durable `instance.start` and `instance.stop` host-agent
|
||||||
operations. Rebuild uses `instance.rebuild` and replaces the instance files.
|
operations. Rebuild uses `instance.rebuild` and replaces the instance files.
|
||||||
See [native host management](native-hosts.md) for command delivery and recovery.
|
See [native host management](native-hosts.md) for command delivery and recovery.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const historyFilters = ['host', 'type', 'source', 'state', 'days', 'q', 'limit', 'offset'];
|
||||||
|
|
||||||
|
/** Route retired activity bookmarks to the existing history that owns their scope. */
|
||||||
|
export function activityRedirectHref(url: URL): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const copy = (keys: string[]) => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = url.searchParams.get(key);
|
||||||
|
if (value) params.set(key, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const server = url.searchParams.get('server')?.trim();
|
||||||
|
const host = url.searchParams.get('host')?.trim();
|
||||||
|
const source = url.searchParams.get('source');
|
||||||
|
let path: string;
|
||||||
|
let hash = '';
|
||||||
|
if (server) {
|
||||||
|
path = `/servers/${encodeURIComponent(server)}`;
|
||||||
|
params.set('tab', 'history');
|
||||||
|
copy(historyFilters);
|
||||||
|
hash = url.hash;
|
||||||
|
} else if (host || source === 'host') {
|
||||||
|
path = '/hosts/operations';
|
||||||
|
if (host) params.set('host', host);
|
||||||
|
} else if (source === 'maintenance' || source === 'recovery') {
|
||||||
|
path = '/maintenance';
|
||||||
|
params.set('view', 'activity');
|
||||||
|
copy(historyFilters);
|
||||||
|
hash = url.hash;
|
||||||
|
} else if (source === 'campaign') {
|
||||||
|
path = '/campaigns';
|
||||||
|
copy(['q']);
|
||||||
|
} else if (source === 'beta') {
|
||||||
|
path = '/betas';
|
||||||
|
params.set('view', 'history');
|
||||||
|
copy(['q']);
|
||||||
|
} else {
|
||||||
|
path = '/provisioning';
|
||||||
|
params.set('view', 'history');
|
||||||
|
if (source === 'autoscaling') params.set('source', source);
|
||||||
|
}
|
||||||
|
copy(['returnTo']);
|
||||||
|
return path + (params.size ? `?${params}` : '') + hash;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { activityRedirectHref } from './activity-redirect.js';
|
||||||
|
|
||||||
const origin = 'https://electric-cherry.invalid';
|
const origin = 'https://electric-cherry.invalid';
|
||||||
const pagePath =
|
const pagePath =
|
||||||
/^\/(?:$|activity|operations|autoscaling|account|bank|capacity|metrics|server-keys|settings(?:\/users)?|servers\/[^/]+|hosts(?:\/[^/]+)?|collections(?:\/launches\/[^/]+)?|(?:scripts|configs|plugins|betas|campaigns|maintenance|provisioning|matches)(?:\/[^/]+)?|game-files|leaderboard(?:\/players\/[^/]+(?:\/[^/]+)?)?)$/;
|
/^\/(?:$|activity|operations|autoscaling|account|bank|capacity|metrics|server-keys|settings(?:\/users)?|servers\/[^/]+|hosts(?:\/[^/]+)?|collections(?:\/launches\/[^/]+)?|(?:scripts|configs|plugins|betas|campaigns|maintenance|provisioning|matches)(?:\/[^/]+)?|game-files|leaderboard(?:\/players\/[^/]+(?:\/[^/]+)?)?)$/;
|
||||||
@@ -16,6 +18,8 @@ export function safeReturnHref(value: string | null | undefined, fallback: strin
|
|||||||
try {
|
try {
|
||||||
const url = new URL(value, origin);
|
const url = new URL(value, origin);
|
||||||
if (url.origin !== origin || !pagePath.test(url.pathname)) return fallback;
|
if (url.origin !== origin || !pagePath.test(url.pathname)) return fallback;
|
||||||
|
if (url.pathname === '/activity' || url.pathname === '/operations')
|
||||||
|
return activityRedirectHref(url);
|
||||||
return url.pathname + url.search + url.hash;
|
return url.pathname + url.search + url.hash;
|
||||||
} catch {
|
} catch {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -45,8 +49,6 @@ export function returnLabel(href: string): string {
|
|||||||
if (path.startsWith('/hosts/')) return 'Back to host';
|
if (path.startsWith('/hosts/')) return 'Back to host';
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
'': 'Servers',
|
'': 'Servers',
|
||||||
activity: 'Activity',
|
|
||||||
operations: 'Activity',
|
|
||||||
autoscaling: 'Autoscaling',
|
autoscaling: 'Autoscaling',
|
||||||
collections: 'Collections',
|
collections: 'Collections',
|
||||||
hosts: 'Hosts',
|
hosts: 'Hosts',
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ export const navigationGroups: NavigationGroup[] = [
|
|||||||
label: 'Hosts',
|
label: 'Hosts',
|
||||||
href: '/hosts',
|
href: '/hosts',
|
||||||
shortcut: 'o',
|
shortcut: 'o',
|
||||||
matches: (path) =>
|
matches: (path) => path === '/hosts' || path.startsWith('/hosts/')
|
||||||
path === '/hosts' || /^\/hosts\/(?!operations(?:\/|$))/.test(path)
|
|
||||||
},
|
},
|
||||||
{ label: 'Capacity', href: '/capacity' },
|
{ label: 'Capacity', href: '/capacity' },
|
||||||
{ label: 'Autoscaling', href: '/autoscaling' },
|
{ label: 'Autoscaling', href: '/autoscaling' },
|
||||||
@@ -52,13 +51,6 @@ export const navigationGroups: NavigationGroup[] = [
|
|||||||
id: 'activity',
|
id: 'activity',
|
||||||
label: 'Activity',
|
label: 'Activity',
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
label: 'Activity log',
|
|
||||||
href: '/activity',
|
|
||||||
shortcut: 'd',
|
|
||||||
matches: (path) =>
|
|
||||||
path === '/activity' || path === '/operations' || path === '/hosts/operations'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Provisioning',
|
label: 'Provisioning',
|
||||||
href: '/provisioning',
|
href: '/provisioning',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { resolveConfirmation } from '$ui/confirm.js';
|
import { resolveConfirmation } from '$ui/confirm.js';
|
||||||
import '$ui/styles/components.css';
|
import '$ui/styles/components.css';
|
||||||
import { enableViewTransitions } from '$ui/motion/viewTransition.js';
|
import { enableViewTransitions } from '$ui/motion/viewTransition.js';
|
||||||
import { canReadAdmin, type AuthUser } from '$lib/auth.js';
|
import type { AuthUser } from '$lib/auth.js';
|
||||||
// Side-effect import — puts @fontsource CSS on Vite's module graph so
|
// Side-effect import — puts @fontsource CSS on Vite's module graph so
|
||||||
// the referenced woff/woff2 files get emitted into the build.
|
// the referenced woff/woff2 files get emitted into the build.
|
||||||
import '$ui/styles/fonts.js';
|
import '$ui/styles/fonts.js';
|
||||||
@@ -294,16 +294,6 @@
|
|||||||
<div class="account-links" id="account-links">
|
<div class="account-links" id="account-links">
|
||||||
<div class="account-name">{accountName}</div>
|
<div class="account-name">{accountName}</div>
|
||||||
<a href="/account">My account</a>
|
<a href="/account">My account</a>
|
||||||
{#if sessionReady}<button onclick={openHelp}
|
|
||||||
>Keyboard shortcuts <span aria-hidden="true">?</span
|
|
||||||
></button
|
|
||||||
>{/if}
|
|
||||||
{#if canReadAdmin(data.user.role) && sessionReady}<a
|
|
||||||
href="/settings/users"
|
|
||||||
>{data.user.role === 'admin'
|
|
||||||
? 'Manage users'
|
|
||||||
: 'View users'}</a
|
|
||||||
>{/if}
|
|
||||||
<button disabled={signingOut} onclick={signOut}
|
<button disabled={signingOut} onclick={signOut}
|
||||||
>{signingOut ? 'Signing out…' : 'Sign out'}</button
|
>{signingOut ? 'Signing out…' : 'Sign out'}</button
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { parseActivityQuery } from '$lib/activity.js';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { activityHistory } from '$lib/server/activity.js';
|
import { activityRedirectHref } from '$lib/activity-redirect.js';
|
||||||
|
|
||||||
export function load({ url }: { url: URL }) {
|
export function load({ url }: { url: URL }) {
|
||||||
return {
|
redirect(308, activityRedirectHref(url));
|
||||||
activity: activityHistory({
|
|
||||||
...parseActivityQuery(url.searchParams),
|
|
||||||
excludeProvisioning: true,
|
|
||||||
excludeMaintenance: true
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ActivityLog from '$ui/components/ActivityLog.svelte';
|
|
||||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
|
||||||
let { data }: { data: import('./$types').PageData } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head><title>Activity · Electric Cherry</title></svelte:head>
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<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>
|
|
||||||
.page {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-6);
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -86,7 +86,8 @@
|
|||||||
<svelte:head><title>Hosts · Electric Cherry</title></svelte:head>
|
<svelte:head><title>Hosts · Electric Cherry</title></svelte:head>
|
||||||
<section class="page">
|
<section class="page">
|
||||||
<SectionHeader title="Hosts">
|
<SectionHeader title="Hosts">
|
||||||
{#snippet actions()}<Button href="/capacity?rebalance=1">Rebalance</Button
|
{#snippet actions()}<Button href="/hosts/operations">Operation history</Button><Button
|
||||||
|
href="/capacity?rebalance=1">Rebalance</Button
|
||||||
>{#if canManage}<Button variant="primary" onclick={() => (addOpen = true)}
|
>{#if canManage}<Button variant="primary" onclick={() => (addOpen = true)}
|
||||||
>Add host</Button
|
>Add host</Button
|
||||||
>{/if}{/snippet}
|
>{/if}{/snippet}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
<section class="page" aria-busy={changingPage}>
|
<section class="page" aria-busy={changingPage}>
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Host operation history"
|
title="Host operation history"
|
||||||
eyebrow="Operations"
|
eyebrow="Hosts"
|
||||||
subtitle="Remote operations, newest first. Filtered views retain their host and server context."
|
subtitle="Remote operations, newest first. Filtered views retain their host and server context."
|
||||||
>
|
>
|
||||||
{#snippet actions()}<Freshness
|
{#snippet actions()}<Freshness
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
/>{/snippet}
|
/>{/snippet}
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
{#if data.scope.operation || $page.url.searchParams.has('returnTo')}<BackLink
|
{#if data.scope.operation || $page.url.searchParams.has('returnTo')}<BackLink
|
||||||
fallback={data.scope.operation ? '/activity' : '/hosts'}
|
fallback={data.scope.operation ? allOperationsHref : '/hosts'}
|
||||||
/>{/if}
|
/>{/if}
|
||||||
{#if data.scope.operation}<div class="selected-operation">
|
{#if data.scope.operation}<div class="selected-operation">
|
||||||
<div><strong>Selected host operation</strong><code>{data.scope.operation}</code></div>
|
<div><strong>Selected host operation</strong><code>{data.scope.operation}</code></div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { activityRedirectHref } from '$lib/activity-redirect.js';
|
||||||
|
|
||||||
export function load() {
|
export function load({ url }: { url: URL }) {
|
||||||
redirect(308, '/activity');
|
redirect(308, activityRedirectHref(url));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -837,11 +837,6 @@
|
|||||||
{:else if activeSection === 'matches'}
|
{:else if activeSection === 'matches'}
|
||||||
{#key name}<MatchHistory server={name} />{/key}
|
{#key name}<MatchHistory server={name} />{/key}
|
||||||
{:else if activeSection === 'history'}
|
{:else if activeSection === 'history'}
|
||||||
<div class="history-links">
|
|
||||||
<a href={contextHref(`/activity?server=${encodeURIComponent(name)}`, $page.url)}
|
|
||||||
>Open in Activity</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{#key name}<ActivityLog server={name} />{/key}
|
{#key name}<ActivityLog server={name} />{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -866,11 +861,6 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
scroll-margin-top: 80px;
|
scroll-margin-top: 80px;
|
||||||
}
|
}
|
||||||
.history-links {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.server-actions {
|
.server-actions {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -475,7 +475,7 @@ describe('unified retained Activity', () => {
|
|||||||
expect(api.activityHistory({ source: 'campaign' }).total).toBe(1);
|
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();
|
const api = await setup();
|
||||||
api.operation('a');
|
api.operation('a');
|
||||||
api.operation('b', { state: 'failed' });
|
api.operation('b', { state: 'failed' });
|
||||||
@@ -483,23 +483,19 @@ describe('unified retained Activity', () => {
|
|||||||
api.maintenance(1, 'failed');
|
api.maintenance(1, 'failed');
|
||||||
api.incident('finding', 'health', 'attention');
|
api.incident('finding', 'health', 'attention');
|
||||||
const url = new URL(
|
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 { GET } = await import('../../src/routes/api/activity/+server.js');
|
||||||
const { load } = await import('../../src/routes/activity/+page.server.js');
|
|
||||||
const response = GET({ url });
|
const response = GET({ url });
|
||||||
expect(response.headers.get('cache-control')).toBe('no-store');
|
expect(response.headers.get('cache-control')).toBe('no-store');
|
||||||
expect(await response.json()).toEqual(JSON.parse(JSON.stringify(load({ url }).activity)));
|
const body = await response.json();
|
||||||
expect(load({ url }).activity.rows[0].id).toBe('host:failed-check');
|
expect(body).toEqual(
|
||||||
expect(
|
JSON.parse(JSON.stringify(api.activityHistory(parseActivityQuery(url.searchParams))))
|
||||||
load({
|
);
|
||||||
url: new URL('http://localhost/activity?excludeProvisioning=false')
|
expect(body.rows[0].id).toBe('host:failed-check');
|
||||||
}).activity.rows.some((row) => row.id.startsWith('provisioning:'))
|
const history = await GET({ url: new URL('http://localhost/api/activity') }).json();
|
||||||
).toBe(false);
|
expect(history.rows.map((row: { id: string }) => row.id)).toEqual(
|
||||||
expect(
|
expect.arrayContaining(['provisioning:a', 'provisioning:b', 'host:failed-check'])
|
||||||
load({
|
);
|
||||||
url: new URL('http://localhost/activity?excludeMaintenance=false')
|
|
||||||
}).activity.rows.map((row) => row.id)
|
|
||||||
).toEqual(['host:failed-check']);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,24 @@ import {
|
|||||||
} from '../../src/lib/ui/command-palette.js';
|
} from '../../src/lib/ui/command-palette.js';
|
||||||
|
|
||||||
describe('command palette destinations', () => {
|
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', () => {
|
it('lets Super Viewers search administration pages without offering operations', () => {
|
||||||
const results = palettePageResults('super_viewer', '');
|
const results = palettePageResults('super_viewer', '');
|
||||||
expect(results.filter((item) => item.group === 'Actions')).toEqual([]);
|
expect(results.filter((item) => item.group === 'Actions')).toEqual([]);
|
||||||
|
|||||||
@@ -20,13 +20,25 @@ describe('navigation context', () => {
|
|||||||
expect(returnLabel('/collections?view=history&launches=30')).toBe('Back to launch history');
|
expect(returnLabel('/collections?view=history&launches=30')).toBe('Back to launch history');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('retains Activity filters and a selected result when opening a detail', () => {
|
it('retains Maintenance filters and a selected result when opening a detail', () => {
|
||||||
const source = '/activity?source=maintenance&state=failed&offset=25#operation-12';
|
const source = '/maintenance?view=activity&state=failed&offset=25#operation-12';
|
||||||
const target = new URL(contextHref('/provisioning/12', source), 'http://localhost');
|
const target = new URL(contextHref('/provisioning/12', source), 'http://localhost');
|
||||||
expect(safeReturnHref(target.searchParams.get('returnTo'), '/')).toBe(source);
|
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', () => {
|
it('preserves the selected diagnostic tab on both sides of a contextual link', () => {
|
||||||
const url = new URL(
|
const url = new URL(
|
||||||
contextHref(
|
contextHref(
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ describe('application navigation', () => {
|
|||||||
['/servers/t5_zombie_theater_1', 'fleet', '/'],
|
['/servers/t5_zombie_theater_1', 'fleet', '/'],
|
||||||
['/collections/launches/42', 'fleet', '/collections'],
|
['/collections/launches/42', 'fleet', '/collections'],
|
||||||
['/hosts/42', 'fleet', '/hosts'],
|
['/hosts/42', 'fleet', '/hosts'],
|
||||||
['/hosts/operations', 'activity', '/activity'],
|
['/hosts/operations', 'fleet', '/hosts'],
|
||||||
['/provisioning', 'activity', '/provisioning'],
|
['/provisioning', 'activity', '/provisioning'],
|
||||||
['/provisioning/job-1', 'activity', '/provisioning'],
|
['/provisioning/job-1', 'activity', '/provisioning'],
|
||||||
['/deployments/job-1', 'activity', '/provisioning'],
|
['/deployments/job-1', 'activity', '/provisioning'],
|
||||||
['/activity', 'activity', '/activity'],
|
|
||||||
['/betas/new', 'releases', '/betas'],
|
['/betas/new', 'releases', '/betas'],
|
||||||
['/campaigns/42', 'releases', '/campaigns'],
|
['/campaigns/42', 'releases', '/campaigns'],
|
||||||
['/configs/42', 'releases', '/configs'],
|
['/configs/42', 'releases', '/configs'],
|
||||||
@@ -33,14 +32,15 @@ describe('application navigation', () => {
|
|||||||
).toEqual([href]);
|
).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(
|
expect(
|
||||||
navigationGroups
|
navigationGroups
|
||||||
.find((group) => group.id === 'activity')
|
.find((group) => group.id === 'activity')
|
||||||
?.items.map((item) => item.label)
|
?.items.map((item) => item.label)
|
||||||
).toEqual(['Activity log', 'Provisioning', 'Maintenance']);
|
).toEqual(['Provisioning', 'Maintenance']);
|
||||||
const { load } = await import('../../src/routes/operations/+page.server.js');
|
const items = navigationGroups.flatMap((group) => group.items);
|
||||||
expect(load).toThrow(expect.objectContaining({ status: 308, location: '/activity' }));
|
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', () => {
|
it('keeps administration and its shortcuts out of operator and viewer navigation', () => {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { beforeEach, afterEach, expect, it, vi } from 'vitest';
|
import { beforeEach, afterEach, expect, it, vi } from 'vitest';
|
||||||
import { render } from 'svelte/server';
|
import { render } from 'svelte/server';
|
||||||
import type { ComponentProps } from 'svelte';
|
|
||||||
import { writable, type Writable } from 'svelte/store';
|
import { writable, type Writable } from 'svelte/store';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import ActivityLog from '../../src/lib/ui/components/ActivityLog.svelte';
|
import ActivityLog from '../../src/lib/ui/components/ActivityLog.svelte';
|
||||||
import ActivityPage from '../../src/routes/activity/+page.svelte';
|
|
||||||
import {
|
import {
|
||||||
activityApiHref,
|
activityApiHref,
|
||||||
activityHistoryHref,
|
activityHistoryHref,
|
||||||
@@ -13,7 +11,9 @@ import {
|
|||||||
} from '../../src/lib/ui/activity-history.js';
|
} from '../../src/lib/ui/activity-history.js';
|
||||||
import type { ActivityPage as ActivityReport, ActivityRow } from '../../src/lib/activity.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('$app/navigation', () => ({ goto: vi.fn() }));
|
||||||
vi.mock('$ui/stores/fleet.js', () => ({
|
vi.mock('$ui/stores/fleet.js', () => ({
|
||||||
fleet: writable({ servers: [], operations: [], receivedAt: 0 })
|
fleet: writable({ servers: [], operations: [], receivedAt: 0 })
|
||||||
@@ -43,7 +43,7 @@ const report = (): ActivityReport => ({
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
query: { limit: 25, 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}`) });
|
(page as unknown as Writable<{ url: URL }>).set({ url: new URL(`http://localhost${path}`) });
|
||||||
}
|
}
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -62,7 +62,7 @@ it.each([
|
|||||||
['90', '90', 'Last 90 days'],
|
['90', '90', 'Last 90 days'],
|
||||||
['14', '14', 'Last 14 days']
|
['14', '14', 'Last 14 days']
|
||||||
])('selects the current date range for days=%s on desktop and mobile', (days, value, label) => {
|
])('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;
|
const body = render(ActivityLog, { props: { initial: report() } }).body;
|
||||||
for (const suffix of ['', '-mobile']) {
|
for (const suffix of ['', '-mobile']) {
|
||||||
const select = body.match(
|
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('tab')).toBe(false);
|
||||||
expect(params.has('returnTo')).toBe(false);
|
expect(params.has('returnTo')).toBe(false);
|
||||||
expect(
|
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();
|
).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', () => {
|
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 };
|
const scope = { excludeProvisioning: true };
|
||||||
expect(new URL(activityApiHref(url, scope), url).searchParams.get('excludeProvisioning')).toBe(
|
expect(new URL(activityApiHref(url, scope), url).searchParams.get('excludeProvisioning')).toBe(
|
||||||
'true'
|
'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', () => {
|
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 };
|
const logScope = { excludeProvisioning: true, excludeMaintenance: true };
|
||||||
expect(
|
expect(
|
||||||
new URL(activityApiHref(url, logScope), url).searchParams.get('excludeMaintenance')
|
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', () => {
|
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();
|
const data = report();
|
||||||
data.offset = 25;
|
data.offset = 25;
|
||||||
const body = render(ActivityLog, { props: { initial: data } }).body;
|
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('shape-circle');
|
||||||
expect(body).toContain('15 s ago');
|
expect(body).toContain('15 s ago');
|
||||||
expect(body).toContain('datetime="2026-10-07T17:59:45Z"');
|
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('state%3Dsucceeded');
|
||||||
expect(body).toContain('offset%3D25');
|
expect(body).toContain('offset%3D25');
|
||||||
expect(body).toContain('26–26 of 52');
|
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', () => {
|
it('preserves deleted server evidence without linking to a replacement instance', () => {
|
||||||
|
scope('/maintenance?view=activity');
|
||||||
const data = report();
|
const data = report();
|
||||||
data.rows = [{ ...row, serverDeletedAt: '2026-10-07T17:59:46Z' }];
|
data.rows = [{ ...row, serverDeletedAt: '2026-10-07T17:59:46Z' }];
|
||||||
const body = render(ActivityLog, { props: { initial: data } }).body;
|
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', () => {
|
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();
|
const data = report();
|
||||||
data.rows = [];
|
data.rows = [];
|
||||||
data.offset = 100;
|
data.offset = 100;
|
||||||
|
|||||||
@@ -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 { render } from 'svelte/server';
|
||||||
import type { ComponentProps } from 'svelte';
|
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 HostOperations from '../../src/routes/hosts/operations/+page.svelte';
|
||||||
|
import Hosts from '../../src/routes/hosts/+page.svelte';
|
||||||
import type { HostOperationSummary } from '../../src/lib/hosts.js';
|
import type { HostOperationSummary } from '../../src/lib/hosts.js';
|
||||||
|
|
||||||
vi.mock('$app/stores', () => ({
|
vi.mock('$app/stores', () => ({
|
||||||
@@ -11,6 +13,14 @@ vi.mock('$app/stores', () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn() }));
|
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 = {
|
const operation: HostOperationSummary = {
|
||||||
id: 'retained-op',
|
id: 'retained-op',
|
||||||
hostId: 'host-1',
|
hostId: 'host-1',
|
||||||
@@ -29,6 +39,58 @@ const operation: HostOperationSummary = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('host operation history presentation', () => {
|
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&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', () => {
|
it('uses readable operation, server and state labels while keeping identifiers in technical detail', () => {
|
||||||
const body = render(HostOperations, {
|
const body = render(HostOperations, {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ describe('sidebar navigation', () => {
|
|||||||
expect(body).toContain('Main navigation');
|
expect(body).toContain('Main navigation');
|
||||||
expect(body).toMatch(/<a[^>]*href="\/campaigns"[^>]*aria-current="page"/);
|
expect(body).toMatch(/<a[^>]*href="\/campaigns"[^>]*aria-current="page"/);
|
||||||
expect(body).toContain('href="/betas"');
|
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="/metrics"');
|
||||||
expect(body).toContain('href="/bank"');
|
expect(body).toContain('href="/bank"');
|
||||||
expect(body).not.toContain('href="/settings"');
|
expect(body).not.toContain('href="/settings"');
|
||||||
@@ -120,6 +122,8 @@ describe('application shell', () => {
|
|||||||
expect(body).toContain('aria-label="Keyboard shortcuts"');
|
expect(body).toContain('aria-label="Keyboard shortcuts"');
|
||||||
expect(body).not.toContain('class="primary-navigation"');
|
expect(body).not.toContain('class="primary-navigation"');
|
||||||
expect(body).not.toContain('variant="secondary"');
|
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', () => {
|
it('preserves the viewer notice and hides navigation while a password change is required', () => {
|
||||||
|
|||||||
@@ -164,7 +164,9 @@ it('keeps Super Viewer diagnostics readable without offering probes or lifecycle
|
|||||||
|
|
||||||
it('uses the shared Activity view for server history', () => {
|
it('uses the shared Activity view for server history', () => {
|
||||||
const body = pageBody('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('Loading campaign history');
|
||||||
expect(body).not.toContain('>Provisioning history</a>');
|
expect(body).not.toContain('>Provisioning history</a>');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user