Use friendly UI labels and release feedback (Refs #120)
CI / checks (push) Successful in 6m39s
CI / checks (push) Successful in 6m39s
This commit is contained in:
@@ -19,7 +19,11 @@
|
||||
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
|
||||
import OperationHistory from '$ui/components/OperationHistory.svelte';
|
||||
import type { ActivityEvent } from '$lib/operation-flow.js';
|
||||
import { campaignServerStatus, type CampaignServerGroup } from '$ui/campaigns.js';
|
||||
import {
|
||||
campaignActionLabel,
|
||||
campaignServerStatus,
|
||||
type CampaignServerGroup
|
||||
} from '$ui/campaigns.js';
|
||||
import { serverIdentity } from '$ui/server-labels.js';
|
||||
|
||||
interface FileRow {
|
||||
@@ -86,6 +90,9 @@
|
||||
let filter = $state<'all' | CampaignServerGroup>('all');
|
||||
let expandedServers = $state<string[]>([]);
|
||||
let copyStatus = $state('');
|
||||
const title = $derived(
|
||||
detail ? `${campaignActionLabel(detail.deployment.action)} campaign` : 'Campaign'
|
||||
);
|
||||
const serverRows = $derived(
|
||||
(detail?.servers ?? []).map((server) => ({
|
||||
server,
|
||||
@@ -275,12 +282,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head
|
||||
><title
|
||||
>{detail ? `${detail.deployment.action} campaign · ${detail.deployment.id}` : 'Campaign'} | Electric
|
||||
Cherry</title
|
||||
></svelte:head
|
||||
>
|
||||
<svelte:head><title>{title} | Electric Cherry</title></svelte:head>
|
||||
{#if loadError}<p role="alert" class="err">
|
||||
{loadError} <button onclick={loadDetail}>Retry</button>
|
||||
</p>{/if}
|
||||
@@ -288,10 +290,10 @@
|
||||
{#if detail}
|
||||
{@const action = detail.deployment.action}
|
||||
<div class="page">
|
||||
<SectionHeader title={`${action.charAt(0).toUpperCase() + action.slice(1)} campaign`}>
|
||||
<SectionHeader {title}>
|
||||
{#snippet crumbs()}
|
||||
<Breadcrumbs
|
||||
items={[{ label: 'Campaigns', href: '/campaigns' }, { label: action }]}
|
||||
items={[{ label: 'Campaigns', href: '/campaigns' }, { label: title }]}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet actions()}
|
||||
|
||||
@@ -542,10 +542,15 @@
|
||||
>{/if}
|
||||
</div>
|
||||
<p>
|
||||
Published revision: <strong
|
||||
{data.runtime.release.error && data.runtime.release.revision !== null
|
||||
? 'Last known published revision'
|
||||
: 'Published revision'}:
|
||||
<strong
|
||||
>{data.runtime.release.revision
|
||||
? `r${data.runtime.release.revision}`
|
||||
: 'Unknown'}</strong
|
||||
: data.runtime.release.error
|
||||
? 'Unavailable'
|
||||
: 'Not recorded'}</strong
|
||||
>
|
||||
·
|
||||
<a href={PLUTONIUM_MANIFEST_URL} target="_blank" rel="noreferrer"
|
||||
@@ -559,11 +564,26 @@
|
||||
>
|
||||
</p>
|
||||
{#if data.runtime.release.checkedAt}<p class="muted">
|
||||
Checked <RelativeTime value={data.runtime.release.checkedAt} />
|
||||
</p>{/if}
|
||||
{#if data.runtime.release.error}<p class="ui-error" role="alert">
|
||||
{data.runtime.release.error} Displayed release information is cached.
|
||||
{data.runtime.release.error ? 'Last attempted' : 'Checked'}
|
||||
<RelativeTime value={data.runtime.release.checkedAt} />
|
||||
</p>{/if}
|
||||
{#if data.runtime.release.error}
|
||||
<p class="ui-error" role="alert">
|
||||
Could not refresh the published Plutonium release.
|
||||
{data.runtime.release.revision !== null
|
||||
? 'Showing the last known release; it may be out of date.'
|
||||
: 'The published revision is unavailable. No previous release information is available.'}
|
||||
{#if administrator}<Button
|
||||
size="sm"
|
||||
disabled={busy}
|
||||
onclick={() => act({ action: 'scan' })}>Retry checks</Button
|
||||
>{/if}
|
||||
</p>
|
||||
<details>
|
||||
<summary>Technical details</summary>
|
||||
<pre>{data.runtime.release.error}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
<p class="muted">
|
||||
Plutainer applies the available release at startup when auto-update is
|
||||
enabled. Maintenance verifies the running revision after recovery.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import ListFooter from '$ui/components/primitives/ListFooter.svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
import ProvisioningStatusBadge from '$ui/components/ProvisioningStatusBadge.svelte';
|
||||
import CopyValue from '$ui/components/CopyValue.svelte';
|
||||
|
||||
let detail = $state<WatchdogDetail | null>(null);
|
||||
let loadError = $state('');
|
||||
@@ -160,7 +161,6 @@
|
||||
title={incident
|
||||
? `${incident.kind === 'drift' ? 'File drift' : 'Server health'} · ${serverTitle(incident.server)}`
|
||||
: 'Maintenance finding'}
|
||||
subtitle={incident?.id}
|
||||
>
|
||||
{#snippet crumbs()}<Breadcrumbs
|
||||
items={[{ label: 'Maintenance', href: '/maintenance' }, { label: 'Incident' }]}
|
||||
@@ -253,6 +253,15 @@
|
||||
)}>Open server logs</a
|
||||
>
|
||||
</div>{/if}
|
||||
<details>
|
||||
<summary>Technical details</summary>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>Incident ID</dt>
|
||||
<dd><CopyValue value={incident.id} label="incident ID" /></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="recovery-title">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import type { AuthUser, UserRole } from '$lib/auth.js';
|
||||
import { confirmAction } from '$ui/confirm.js';
|
||||
import { auditPresentation } from '$ui/audit.js';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import Dialog from '$ui/components/primitives/Dialog.svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
@@ -101,24 +102,6 @@
|
||||
auditLoading = false;
|
||||
}
|
||||
}
|
||||
const actionLabels: Record<string, string> = {
|
||||
setup: 'Administrator created',
|
||||
login: 'Signed in',
|
||||
'login.failed': 'Sign-in failed',
|
||||
logout: 'Signed out',
|
||||
'sessions.revoked': 'Sessions ended',
|
||||
'user.created': 'User created',
|
||||
'user.updated': 'Access updated',
|
||||
'password.changed': 'Password changed'
|
||||
};
|
||||
function auditDetails(value: string): string {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return Object.keys(parsed).length ? JSON.stringify(parsed, null, 2) : '';
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
onMount(() => {
|
||||
if (canViewUsers) void Promise.all([load(), loadAudit()]);
|
||||
else {
|
||||
@@ -339,7 +322,7 @@
|
||||
<div class="audit-heading">
|
||||
<div>
|
||||
<h2 id="audit-title">Account activity</h2>
|
||||
<p class="muted">The latest 200 sign-in and account events.</p>
|
||||
<p class="muted">The latest 200 sign-in, account, and action events.</p>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" disabled={auditLoading} onclick={loadAudit}
|
||||
>Refresh activity</Button
|
||||
@@ -354,27 +337,27 @@
|
||||
<table>
|
||||
<thead
|
||||
><tr
|
||||
><th>Time</th><th>Event</th><th>By</th><th>Account</th><th
|
||||
><th>Time</th><th>Event</th><th>By</th><th>Account or action</th><th
|
||||
>Details</th
|
||||
></tr
|
||||
></thead
|
||||
><tbody>
|
||||
{#each events as event (event.id)}{@const details = auditDetails(
|
||||
event.details
|
||||
)}<tr
|
||||
{#each events as event (event.id)}{@const presentation =
|
||||
auditPresentation(event)}<tr
|
||||
><td class="event-time"
|
||||
><RelativeTime value={event.createdAt} /></td
|
||||
><td>{actionLabels[event.action] ?? event.action}</td><td
|
||||
><td>{presentation.label}</td><td
|
||||
>{event.actor ? `@${event.actor}` : 'Unauthenticated'}</td
|
||||
><td
|
||||
>{event.targetId
|
||||
? (users.find((user) => user.id === event.targetId)
|
||||
?.username ?? event.targetId)
|
||||
: '—'}</td
|
||||
>{presentation.requestAction ??
|
||||
(event.targetId
|
||||
? (users.find((user) => user.id === event.targetId)
|
||||
?.username ?? event.targetId)
|
||||
: '—')}</td
|
||||
><td
|
||||
>{#if details}<details>
|
||||
<summary>View details</summary>
|
||||
<pre>{details}</pre>
|
||||
>{#if presentation.details}<details>
|
||||
<summary>Technical details</summary>
|
||||
<pre>{presentation.details}</pre>
|
||||
</details>{:else}—{/if}</td
|
||||
></tr
|
||||
>{/each}
|
||||
|
||||
Reference in New Issue
Block a user