Delete remaining orphan Settings panels, Summary primitives, and presentation utils

Final sweep of the post-IA-migration cleanup. These files have zero
non-test consumers in the platform-first codebase:

Settings panels (consumed only by the deleted aggregate Settings shell):
- ConfiguredNodeTables.tsx
- MonitoredSystemLedgerPanel.tsx (+ test)
- SettingsSectionNav.tsx

Shared Summary primitives (only rendered by the deleted aggregate
summary surfaces — InfrastructureSummary, WorkloadsSummary,
StoragePageSummary):
- SummaryJumpToRowButton.tsx
- SummaryMetricCard.tsx
- SummarySynchronizedReadout.tsx

Platform-feature column config (only consumed by deleted
WorkloadsStateCards / WorkloadsSurface chrome):
- features/platformPage/appContainerColumns.ts

Presentation utilities (only consumed by the deleted Recovery surface,
problem-resource summary, throughput card, approval section, chart
series card, proxmox settings panel — every consumer of these utils
was deleted in earlier cleanups):
- utils/recoveryStatusPresentation.ts (+ test)
- utils/recoveryFilterChipPresentation.ts (+ test)
- utils/recoveryRecordPresentation.ts (+ test)
- utils/recoveryActionPresentation.ts (+ test)
- utils/recoveryEmptyStatePresentation.ts (+ test)
- utils/throughputPresentation.ts (+ test)
- utils/approvalPresentation.ts (+ test)
- utils/chartSeriesPresentation.ts (+ test)
- utils/problemResourcePresentation.ts (+ test)
- utils/proxmoxSettingsPresentation.ts (+ test)

Strips ?raw guardrail imports + assertions for all deleted sources
from SharedPrimitives.guardrails / frontendResourceTypeBoundaries /
UnifiedResourceTable.performance.contract. Drops the dead
configuredNodeTablesSource entry from a structural assertion array
and the stale shouldShowCephSummaryCard / getStoragePageBannerMessage
/ STORAGE_BANNER_ACTION_BUTTON_CLASS / getStoragePageBannerKind
expectations that were checking exports already removed in Cleanup H.

Baseline 10 pre-existing test failures unchanged.
This commit is contained in:
rcourtman
2026-05-26 19:47:27 +01:00
parent 579b7c1e62
commit ae65c5e9a2
31 changed files with 0 additions and 3014 deletions
@@ -12,8 +12,6 @@ import unifiedResourceTableStateSource from '@/components/Infrastructure/useUnif
import unifiedResourceTableViewportSyncSource from '@/components/Infrastructure/useUnifiedResourceTableViewportSync.ts?raw';
import unifiedResourceTableModelSource from '@/components/Infrastructure/unifiedResourceTableModel.ts?raw';
import unifiedResourceTableStateModelSource from '@/components/Infrastructure/unifiedResourceTableStateModel.ts?raw';
import infrastructureSummaryStateSource from '@/components/Infrastructure/useInfrastructureSummaryState.ts?raw';
import infrastructureSummaryModelSource from '@/components/Infrastructure/infrastructureSummaryModel.ts?raw';
import unifiedResourceHostTableCardSource from '@/components/Infrastructure/UnifiedResourceHostTableCard.tsx?raw';
import unifiedResourceServiceInfrastructureCardSource from '@/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx?raw';
import unifiedResourcePBSTableSectionSource from '@/components/Infrastructure/UnifiedResourcePBSTableSection.tsx?raw';
@@ -601,43 +599,6 @@ describe('UnifiedResourceTable performance contract', () => {
});
it('keeps infrastructure summary fetch runtime out of the render shell', () => {
expect(infrastructureSummaryStateSource).toContain('fetchInfrastructureSummaryAndCache');
expect(infrastructureSummaryStateSource).toContain('readInfrastructureSummaryCache');
expect(infrastructureSummaryStateSource).toContain('setInterval(');
expect(infrastructureSummaryStateSource).toContain('AbortController');
expect(infrastructureSummaryStateSource).toContain("eventBus.on('org_switched'");
expect(infrastructureSummaryStateSource).toContain('buildInfrastructureDisplaySeries');
expect(infrastructureSummaryStateSource).toContain('buildInfrastructureMetricSeries');
expect(infrastructureSummaryStateSource).toContain('buildInfrastructureEmptyMessage');
expect(infrastructureSummaryStateSource).not.toContain('useResources(');
expect(infrastructureSummaryStateSource).toContain(
'props.resources.filter((resource) => isWorkload(resource))',
);
expect(infrastructureSummaryStateSource).not.toContain(
'const match = allSeries.find((series) => series.id === focused);',
);
expect(infrastructureSummaryStateSource).not.toContain('displaySeries().map((series) => ({');
expect(infrastructureSummaryStateSource).not.toContain(
"isAwaitingFirstSample() ? 'Gathering first sample…' : 'Building trend history…'",
);
expect(infrastructureSummaryStateSource).not.toContain(
"fetchFailed() ? 'Trend data unavailable' : emptyHistoryLabel()",
);
expect(infrastructureSummaryModelSource).toContain(
'export function buildInfrastructureSummarySeries',
);
expect(infrastructureSummaryModelSource).toContain(
'export function combineResourceThroughputSeries',
);
expect(infrastructureSummaryModelSource).toContain(
'export function buildInfrastructureDisplaySeries',
);
expect(infrastructureSummaryModelSource).toContain(
'export function buildInfrastructureMetricSeries',
);
expect(infrastructureSummaryModelSource).toContain(
'export function shouldShowInfrastructureNetworkCard',
);
});
it('keeps contextual resource-row emphasis on the shared active-row presentation contract', () => {
@@ -1,607 +0,0 @@
import { Component, For, Show, createMemo } from 'solid-js';
import type { NodeConfigWithStatus } from '@/types/nodes';
import type { PBSInstance, PMGInstance } from '@/types/api';
import type { Resource } from '@/types/resource';
import { Card } from '@/components/shared/Card';
import { StatusDot } from '@/components/shared/StatusDot';
import { getClusterEndpointPresentation } from '@/utils/clusterEndpointPresentation';
import {
resolveConfiguredInstanceStatusIndicator,
resolveConfiguredPveNodeStatusIndicator,
} from '@/utils/configuredNodeStatusPresentation';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from '@/components/shared/Table';
import { getStatusIndicatorBadgeToneClasses } from '@/utils/status';
import { getConfiguredNodeCapabilityBadges } from '@/utils/configuredNodeCapabilityPresentation';
interface PveNodesTableProps {
nodes: NodeConfigWithStatus[];
stateNodes: Resource[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void;
onRefreshCluster?: (nodeId: string) => void;
}
export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
return (
<Card padding="none" tone="card" class="rounded-md">
<Table
wrapperClass="max-h-[600px] overflow-y-auto"
class="min-w-[max-content] w-full divide-y divide-border text-sm"
>
<TableHeader class="bg-surface-alt">
<TableRow>
<TableHead class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Node
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Credentials
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Capabilities
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Status
</TableHead>
<TableHead class="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-muted">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border bg-surface">
<For each={props.nodes}>
{(node) => {
const statusIndicator = createMemo(() =>
resolveConfiguredPveNodeStatusIndicator(node, props.stateNodes),
);
const capabilityBadges = createMemo(() =>
getConfiguredNodeCapabilityBadges(
node,
props.globalTemperatureMonitoringEnabled ?? true,
),
);
const clusterEndpoints = createMemo(() =>
'clusterEndpoints' in node && node.clusterEndpoints ? node.clusterEndpoints : [],
);
const clusterName = createMemo(() =>
'clusterName' in node && node.clusterName ? node.clusterName : 'Unknown',
);
return (
<TableRow class="even:bg-surface-alt hover:bg-blue-50 dark:hover:bg-blue-900 transition-colors">
<TableCell class="align-top py-3 pl-4 pr-3">
<div class="min-w-0 space-y-1">
<div class="flex items-start gap-3">
<StatusDot
variant={statusIndicator().variant}
size="md"
ariaHidden={true}
class="mt-1.5"
/>
<div class="min-w-0 flex-1">
<p class="font-medium text-base-content truncate" title={node.name}>{node.name}</p>
<p class="text-xs text-muted truncate" title={node.host}>{node.host}</p>
</div>
</div>
<Show when={node.type === 'pve' && 'isCluster' in node && node.isCluster}>
<div class="rounded-md border border-border bg-surface-alt px-3 py-2 space-y-2">
<div class="flex items-center gap-2 text-xs font-semibold text-base-content">
<span>{clusterName()} Cluster</span>
<span class="ml-auto text-[0.65rem] font-normal text-muted">
{clusterEndpoints().length} nodes
</span>
</div>
<Show when={clusterEndpoints().length > 0}>
<div class="flex flex-col gap-2">
<For each={clusterEndpoints()}>
{(endpoint) => {
const endpointPresentation =
getClusterEndpointPresentation(endpoint);
return (
<div
class={`rounded border px-3 py-2 text-[0.7rem] ${endpointPresentation.panelClass}`}
>
<div class="flex items-center gap-2 mb-1">
<span class="font-semibold">{endpoint.nodeName}</span>
<span class="text-[0.65rem] opacity-75">{endpoint.ip}</span>
</div>
<div class="flex flex-col gap-0.5 text-[0.65rem] opacity-90">
<div class="flex items-center gap-1.5">
<span class="w-16 font-medium">Proxmox:</span>
<span>{endpointPresentation.proxmoxLabel}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="w-16 font-medium">Pulse:</span>
<span>{endpointPresentation.pulseLabel}</span>
</div>
<Show
when={
endpointPresentation.pulseStatus === 'unreachable' &&
endpoint.pulseError
}
>
<div class="mt-1 pt-1 border-t border-current opacity-20">
<span class="font-medium">Error: </span>
<span class="opacity-80">{endpoint.pulseError}</span>
</div>
</Show>
</div>
</div>
);
}}
</For>
</div>
</Show>
<div class="flex items-center justify-between gap-2">
<p class="flex items-center gap-1 text-[0.7rem] text-muted">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
Automatic failover enabled
</p>
<Show when={props.onRefreshCluster}>
<button
type="button"
onClick={() => props.onRefreshCluster?.(node.id)}
class="flex min-h-10 sm:min-h-9 items-center gap-1 px-2.5 py-1.5 text-xs font-medium hover:text-muted bg-surface border border-border rounded hover:bg-surface-hover transition-colors"
title="Re-detect cluster membership (use if nodes were added to the Proxmox cluster)"
>
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
</svg>
Refresh
</button>
</Show>
</div>
</div>
</Show>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex flex-col gap-1">
<span class="text-xs text-muted">
{node.user ? `User: ${node.user}` : `Token: ${node.tokenName}`}
</span>
<Show when={node.source === 'agent'}>
<span class="inline-flex items-center gap-1 text-[0.65rem] px-1.5 py-0.5 bg-surface-hover text-base-content rounded w-fit">
<span class="h-1.5 w-1.5 rounded-full bg-slate-500"></span>
Agent-linked
</span>
</Show>
<Show when={node.source === 'script' || (!node.source && node.tokenName)}>
<span class="text-[0.65rem] px-1.5 py-0.5 bg-surface-alt text-muted rounded w-fit">
Direct-linked
</span>
</Show>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex flex-wrap gap-1">
<For each={capabilityBadges()}>
{(badge) => <span class={badge.className}>{badge.label}</span>}
</For>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3 whitespace-nowrap">
<span
class={`inline-flex items-center gap-2 text-xs font-medium ${getStatusIndicatorBadgeToneClasses(statusIndicator().variant)}`}
>
<StatusDot variant={statusIndicator().variant} size="sm" ariaHidden={true} />
{statusIndicator().label}
</span>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex items-center justify-end gap-1 sm:gap-2">
<button
type="button"
onClick={() => props.onTestConnection(node.id)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-muted hover:text-base-content"
title="Test connection"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</button>
<button
type="button"
onClick={() => props.onEdit(node)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-muted hover:text-base-content"
title="Edit node"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button
type="button"
onClick={() => props.onDelete(node)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
title="Delete node"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
</svg>
</button>
</div>
</TableCell>
</TableRow>
);
}}
</For>
</TableBody>
</Table>
</Card>
);
};
interface PbsNodesTableProps {
nodes: NodeConfigWithStatus[];
statePbs: PBSInstance[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void;
}
export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
return (
<Card padding="none" tone="card" class="rounded-md">
<Table class="min-w-[max-content] divide-y divide-border text-sm">
<TableHeader class="bg-surface-alt">
<TableRow>
<TableHead class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Node
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Credentials
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Capabilities
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Status
</TableHead>
<TableHead class="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-muted">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border bg-surface">
<For each={props.nodes}>
{(node) => {
const statusIndicator = createMemo(() =>
resolveConfiguredInstanceStatusIndicator(node, props.statePbs),
);
const capabilityBadges = createMemo(() =>
getConfiguredNodeCapabilityBadges(
node,
props.globalTemperatureMonitoringEnabled ?? true,
),
);
return (
<TableRow class="even:bg-surface-alt hover:bg-blue-50 dark:hover:bg-blue-900 transition-colors">
<TableCell class="align-top py-3 pl-4 pr-3">
<div class="min-w-0 space-y-1">
<div class="flex items-start gap-3">
<StatusDot
variant={statusIndicator().variant}
size="md"
ariaHidden={true}
class="mt-1.5"
/>
<div class="min-w-0 flex-1">
<p class="font-medium text-base-content truncate" title={node.name}>{node.name}</p>
<p class="text-xs text-muted truncate" title={node.host}>{node.host}</p>
</div>
</div>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex flex-col gap-1">
<span class="text-xs text-muted">
{node.user ? `User: ${node.user}` : `Token: ${node.tokenName}`}
</span>
<Show when={node.source === 'agent'}>
<span class="inline-flex items-center gap-1 text-[0.65rem] px-1.5 py-0.5 bg-surface-hover text-base-content rounded w-fit">
<span class="h-1.5 w-1.5 rounded-full bg-slate-500"></span>
Agent-linked
</span>
</Show>
<Show when={node.source === 'script' || (!node.source && node.tokenName)}>
<span class="text-[0.65rem] px-1.5 py-0.5 bg-surface-alt text-muted rounded w-fit">
Direct-linked
</span>
</Show>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex flex-wrap gap-1">
<For each={capabilityBadges()}>
{(badge) => <span class={badge.className}>{badge.label}</span>}
</For>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3 whitespace-nowrap">
<span
class={`inline-flex items-center gap-2 text-xs font-medium ${getStatusIndicatorBadgeToneClasses(statusIndicator().variant)}`}
>
<StatusDot variant={statusIndicator().variant} size="sm" ariaHidden={true} />
{statusIndicator().label}
</span>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex items-center justify-end gap-1 sm:gap-2">
<button
type="button"
onClick={() => props.onTestConnection(node.id)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-muted hover:text-base-content"
title="Test connection"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</button>
<button
type="button"
onClick={() => props.onEdit(node)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-muted hover:text-base-content"
title="Edit node"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button
type="button"
onClick={() => props.onDelete(node)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
title="Delete node"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
</svg>
</button>
</div>
</TableCell>
</TableRow>
);
}}
</For>
</TableBody>
</Table>
</Card>
);
};
interface PmgNodesTableProps {
nodes: NodeConfigWithStatus[];
statePmg: PMGInstance[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void;
}
export const PmgNodesTable: Component<PmgNodesTableProps> = (props) => {
return (
<Card padding="none" tone="card" class="rounded-md">
<Table class="min-w-[max-content] divide-y divide-border text-sm">
<TableHeader class="bg-surface-alt">
<TableRow>
<TableHead class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Node
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Credentials
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Capabilities
</TableHead>
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
Status
</TableHead>
<TableHead class="px-3 py-2 text-right text-xs font-semibold uppercase tracking-wide text-muted">
Actions
</TableHead>
</TableRow>
</TableHeader>
<TableBody class="divide-y divide-border bg-surface">
<For each={props.nodes}>
{(node) => {
const statusIndicator = createMemo(() =>
resolveConfiguredInstanceStatusIndicator(node, props.statePmg),
);
const capabilityBadges = createMemo(() =>
getConfiguredNodeCapabilityBadges(
node,
props.globalTemperatureMonitoringEnabled ?? true,
),
);
return (
<TableRow class="even:bg-surface-alt hover:bg-blue-50 dark:hover:bg-blue-900 transition-colors">
<TableCell class="align-top py-3 pl-4 pr-3">
<div class="min-w-0 space-y-1">
<div class="flex items-start gap-3">
<StatusDot
variant={statusIndicator().variant}
size="md"
ariaHidden={true}
class="mt-1.5"
/>
<div class="min-w-0 flex-1">
<p class="font-medium text-base-content truncate" title={node.name}>{node.name}</p>
<p class="text-xs text-muted truncate" title={node.host}>{node.host}</p>
</div>
</div>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex flex-col gap-1">
<span class="text-xs text-muted">
{node.user ? `User: ${node.user}` : `Token: ${node.tokenName}`}
</span>
<Show when={node.source === 'agent'}>
<span class="inline-flex items-center gap-1 text-[0.65rem] px-1.5 py-0.5 bg-surface-hover text-base-content rounded w-fit">
<span class="h-1.5 w-1.5 rounded-full bg-slate-500"></span>
Agent-linked
</span>
</Show>
<Show when={node.source === 'script' || (!node.source && node.tokenName)}>
<span class="text-[0.65rem] px-1.5 py-0.5 bg-surface-alt text-muted rounded w-fit">
Direct-linked
</span>
</Show>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex flex-wrap gap-1">
<For each={capabilityBadges()}>
{(badge) => <span class={badge.className}>{badge.label}</span>}
</For>
</div>
</TableCell>
<TableCell class="align-top px-3 py-3 whitespace-nowrap">
<span
class={`inline-flex items-center gap-2 text-xs font-medium ${getStatusIndicatorBadgeToneClasses(statusIndicator().variant)}`}
>
<StatusDot variant={statusIndicator().variant} size="sm" ariaHidden={true} />
{statusIndicator().label}
</span>
</TableCell>
<TableCell class="align-top px-3 py-3">
<div class="flex items-center justify-end gap-1 sm:gap-2">
<button
type="button"
onClick={() => props.onTestConnection(node.id)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-muted hover:text-base-content"
title="Test connection"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</button>
<button
type="button"
onClick={() => props.onEdit(node)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-muted hover:text-base-content"
title="Edit node"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button
type="button"
onClick={() => props.onDelete(node)}
class="min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 p-2.5 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
title="Delete node"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
</svg>
</button>
</div>
</TableCell>
</TableRow>
);
}}
</For>
</TableBody>
</Table>
</Card>
);
};
@@ -1,368 +0,0 @@
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
import SettingsPanel from '@/components/shared/SettingsPanel';
import { StatusDot } from '@/components/shared/StatusDot';
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from '@/components/shared/Table';
import { formatRelativeTime } from '@/utils/format';
import { MonitoredSystemLedgerAPI } from '@/api/monitoredSystemLedger';
import type {
MonitoredSystemLedgerEntry,
MonitoredSystemLedgerExplanationSurface,
} from '@/api/monitoredSystemLedger';
import { apiErrorCode, apiErrorDetailField } from '@/api/responseUtils';
import { getSimpleStatusIndicator } from '@/utils/status';
import {
formatMonitoredSystemGroupedSourcesLabel,
formatMonitoredSystemLedgerUnavailableMessage,
formatMonitoredSystemLatestIncludedSignalSentence,
formatMonitoredSystemSurfaceAttribution,
getMonitoredSystemLedgerErrorState,
getMonitoredSystemLedgerDescription,
getMonitoredSystemLedgerHiddenState,
getMonitoredSystemLedgerLoadingState,
getMonitoredSystemLedgerPolicyLoadingState,
getMonitoredSystemLedgerUnavailableState,
getMonitoredSystemCountingDetailsToggleLabel,
getMonitoredSystemLedgerPresentation,
} from '@/utils/monitoredSystemPresentation';
import { MonitoredSystemDefinitionDisclosure } from '@/components/Commercial/MonitoredSystemDefinitionDisclosure';
import {
presentationPolicyHidesCommercialSurfaces,
sessionPresentationPolicyResolved,
} from '@/stores/sessionPresentationPolicy';
interface MonitoredSystemLedgerPanelProps {
embedded?: boolean;
showCountingRulesByDefault?: boolean;
}
function latestIncludedSignalSummary(system: MonitoredSystemLedgerEntry): {
relative: string;
attribution: string;
} | null {
if (!system.latest_included_signal.at) {
return null;
}
return {
relative: formatRelativeTime(system.latest_included_signal.at, { compact: true }),
attribution: formatMonitoredSystemSurfaceAttribution(system.latest_included_signal),
};
}
function includedSurfaces(
system: MonitoredSystemLedgerEntry,
): MonitoredSystemLedgerExplanationSurface[] {
if (system.explanation.surfaces.length > 0) {
return system.explanation.surfaces;
}
return [
{
name: system.name,
type: system.type,
source: system.source,
},
];
}
export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProps = {}) {
const ledgerRequestKey = () =>
sessionPresentationPolicyResolved() && !presentationPolicyHidesCommercialSurfaces()
? 'visible'
: false;
const [explanation, { refetch }] = createResource(ledgerRequestKey, () =>
MonitoredSystemLedgerAPI.explain(),
);
const [expandedSystemKey, setExpandedSystemKey] = createSignal<string | null>(null);
const presentation = getMonitoredSystemLedgerPresentation();
const ledger = createMemo(() =>
explanation.state === 'errored' ? undefined : explanation()?.ledger,
);
const total = () => ledger()?.total ?? 0;
const systems = () => ledger()?.systems ?? [];
const totalSummary = () => `${total()} monitored ${total() === 1 ? 'system' : 'systems'}`;
const usageUnavailableReason = () => {
if (apiErrorCode(explanation.error) === 'monitored_system_usage_unavailable') {
return apiErrorDetailField(explanation.error, 'reason') ?? undefined;
}
return undefined;
};
const usageUnavailable = () =>
!ledger() && apiErrorCode(explanation.error) === 'monitored_system_usage_unavailable';
const genericError = () => Boolean(explanation.error) && !usageUnavailable();
const systemKey = (system: MonitoredSystemLedgerEntry, index: number) =>
`${system.name}:${system.type}:${index}`;
const toggleSystemExplanation = (key: string) => {
setExpandedSystemKey((current) => (current === key ? null : key));
};
const policyLoadingState = getMonitoredSystemLedgerPolicyLoadingState();
const hiddenState = getMonitoredSystemLedgerHiddenState();
const policyLoadingContent = (
<div class="rounded-lg border border-border bg-surface px-4 py-4 text-sm">
<p class="font-semibold text-base-content">{policyLoadingState.title}</p>
<p class="mt-1 text-muted">{policyLoadingState.message}</p>
</div>
);
const hiddenContent = (
<div class="rounded-lg border border-border bg-surface px-4 py-4 text-sm">
<p class="font-semibold text-base-content">{hiddenState.title}</p>
<p class="mt-1 text-muted">{hiddenState.message}</p>
</div>
);
const ledgerContent = (
<>
{/* Summary */}
<div class="space-y-1">
<div class="flex items-center justify-between">
<div class="space-y-1">
<h3 class="text-sm font-semibold text-base-content">{presentation.sectionTitle}</h3>
<MonitoredSystemDefinitionDisclosure
defaultOpen={props.showCountingRulesByDefault}
buttonClass="text-xs font-medium text-muted underline-offset-2 transition-colors hover:text-base-content hover:underline"
detailClass="max-w-xl text-xs text-muted"
/>
</div>
<Show when={ledger()}>
<div class="text-right">
<p class="text-sm font-medium text-base-content">{totalSummary()}</p>
</div>
</Show>
<Show when={!ledger() && usageUnavailable()}>
<span class="text-sm font-medium text-amber-700 dark:text-amber-300">
Verifying
</span>
</Show>
</div>
</div>
{/* Loading state */}
<Show when={explanation.loading && !usageUnavailable()}>
<p class="text-sm text-muted py-4 text-center">
{getMonitoredSystemLedgerLoadingState().text}
</p>
</Show>
{/* Usage-unavailable state */}
<Show when={usageUnavailable()}>
<div class="rounded-lg border border-amber-200 bg-amber-50 px-4 py-4 text-sm text-amber-900 dark:border-amber-900 dark:bg-amber-950/40 dark:text-amber-100">
<p class="font-semibold">{getMonitoredSystemLedgerUnavailableState().title}</p>
<p class="mt-1">
{formatMonitoredSystemLedgerUnavailableMessage(usageUnavailableReason())}
</p>
<button
type="button"
class="mt-3 text-xs font-medium underline-offset-2 transition-colors hover:underline disabled:opacity-50"
disabled={explanation.loading}
onClick={() => refetch()}
>
{explanation.loading
? getMonitoredSystemLedgerErrorState().retryingLabel
: getMonitoredSystemLedgerErrorState().retryLabel}
</button>
</div>
</Show>
{/* Error state */}
<Show when={genericError()}>
<div class="text-sm text-red-600 dark:text-red-400 py-4 text-center">
<p>{getMonitoredSystemLedgerErrorState().title}</p>
<button
type="button"
class="mt-2 text-xs text-primary hover:underline disabled:opacity-50"
disabled={explanation.loading}
onClick={() => refetch()}
>
{explanation.loading
? getMonitoredSystemLedgerErrorState().retryingLabel
: getMonitoredSystemLedgerErrorState().retryLabel}
</button>
</div>
</Show>
{/* Loaded content */}
<Show when={!explanation.loading && !genericError() && ledger()}>
<Show
when={systems().length > 0}
fallback={<p class="text-sm text-muted py-4 text-center">{presentation.emptyState}</p>}
>
<Table>
<TableHeader>
<TableRow>
<TableHead>{presentation.tableNameLabel}</TableHead>
<TableHead>{presentation.tableStatusLabel}</TableHead>
<TableHead>{presentation.tableLatestIncludedSignalLabel}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<For each={systems()}>
{(system: MonitoredSystemLedgerEntry, index) => {
const indicator = getSimpleStatusIndicator(system.status);
const key = systemKey(system, index());
const explanationID = `monitored-system-explanation-${index()}`;
const expanded = () => expandedSystemKey() === key;
const latestSignal = latestIncludedSignalSummary(system);
return (
<TableRow>
<TableCell>
<div class="space-y-1 whitespace-normal">
<span class="text-sm font-medium text-base-content">{system.name}</span>
<div class="flex flex-wrap items-center gap-1.5">
<span class="rounded-full border border-border bg-surface px-2 py-0.5 text-[11px] font-medium text-base-content">
{presentation.countedSystemBadgeLabel}
</span>
<span class="text-[11px] text-muted">
{formatMonitoredSystemGroupedSourcesLabel(
includedSurfaces(system).length,
)}
</span>
</div>
<p class="max-w-xl text-xs text-muted">{system.explanation.summary}</p>
<Show when={includedSurfaces(system).length > 0}>
<div class="flex flex-wrap gap-1">
<For each={includedSurfaces(system)}>
{(surface) => (
<span class="rounded-full bg-surface-alt px-2 py-0.5 text-[11px] text-muted">
{formatMonitoredSystemSurfaceAttribution(surface)}
</span>
)}
</For>
</div>
</Show>
<button
type="button"
class="block text-[11px] font-medium text-muted underline-offset-2 transition-colors hover:text-base-content hover:underline"
aria-expanded={expanded()}
aria-controls={explanationID}
onClick={() => toggleSystemExplanation(key)}
>
{getMonitoredSystemCountingDetailsToggleLabel(expanded())}
</button>
<Show when={expanded()}>
<div
id={explanationID}
class="space-y-2 rounded-md border border-border bg-surface px-3 py-2 text-xs text-muted"
>
<div class="space-y-1">
<p class="font-medium text-base-content">
{presentation.countingExplanationHeading}
</p>
<p class="whitespace-normal text-base-content">
{system.explanation.summary}
</p>
<Show when={system.explanation.reasons.length > 0}>
<ul class="space-y-1 whitespace-normal">
<For each={system.explanation.reasons}>
{(reason) => <li>{reason.summary}</li>}
</For>
</ul>
</Show>
</div>
<div class="space-y-1">
<p class="font-medium text-base-content">
{presentation.currentStatusHeading}
</p>
<p class="whitespace-normal text-base-content">
{system.status_explanation.summary}
</p>
<Show when={latestSignal}>
{(signal) => (
<p class="whitespace-normal text-base-content">
{formatMonitoredSystemLatestIncludedSignalSentence(signal())}
</p>
)}
</Show>
<Show when={system.status_explanation.reasons.length > 0}>
<ul class="space-y-1 whitespace-normal text-base-content">
<For each={system.status_explanation.reasons}>
{(reason) => <li>{reason.summary}</li>}
</For>
</ul>
</Show>
</div>
<Show when={system.explanation.surfaces.length > 0}>
<div class="space-y-1">
<p class="font-medium text-base-content">
{presentation.groupedSourcesHeading}
</p>
<ul class="space-y-1 whitespace-normal">
<For each={system.explanation.surfaces}>
{(surface) => (
<li>{formatMonitoredSystemSurfaceAttribution(surface)}</li>
)}
</For>
</ul>
</div>
</Show>
</div>
</Show>
</div>
</TableCell>
<TableCell>
<span class="inline-flex items-center gap-1.5">
<StatusDot variant={indicator.variant} size="sm" />
<span class="text-xs text-muted">{indicator.label}</span>
</span>
</TableCell>
<TableCell>
<Show
when={latestSignal}
fallback={
<span class="text-xs text-muted">
{presentation.noIncludedSignalLabel}
</span>
}
>
{(signal) => (
<div class="space-y-0.5">
<span class="block text-xs text-muted">{signal().relative}</span>
<span class="block text-[11px] text-muted whitespace-normal">
{signal().attribution}
</span>
</div>
)}
</Show>
</TableCell>
</TableRow>
);
}}
</For>
</TableBody>
</Table>
</Show>
</Show>
</>
);
const content = (
<Show when={sessionPresentationPolicyResolved()} fallback={policyLoadingContent}>
<Show when={!presentationPolicyHidesCommercialSurfaces()} fallback={hiddenContent}>
{ledgerContent}
</Show>
</Show>
);
if (props.embedded) {
return <div class="space-y-4">{content}</div>;
}
return (
<SettingsPanel
title={presentation.panelTitle}
description={getMonitoredSystemLedgerDescription()}
bodyClass="space-y-4"
>
{content}
</SettingsPanel>
);
}
@@ -1,69 +0,0 @@
import type { Component } from 'solid-js';
import { For } from 'solid-js';
import Server from 'lucide-solid/icons/server';
import HardDrive from 'lucide-solid/icons/hard-drive';
import Mail from 'lucide-solid/icons/mail';
type SettingsSection = 'pve' | 'pbs' | 'pmg';
interface SettingsSectionNavProps {
current: SettingsSection;
onSelect: (section: SettingsSection) => void;
class?: string;
}
const allSections: Array<{
id: SettingsSection;
label: string;
icon: typeof Server;
}> = [
{
id: 'pve',
label: 'Virtual Environment',
icon: Server,
},
{
id: 'pbs',
label: 'Backup Server',
icon: HardDrive,
},
{
id: 'pmg',
label: 'Mail Gateway',
icon: Mail,
},
];
export const SettingsSectionNav: Component<SettingsSectionNavProps> = (props) => {
return (
<div class={`border-b border-border ${props.class ?? ''}`} aria-label="Settings sections">
<div
class="flex flex-wrap items-center gap-6 overflow-x-auto scrollbar-hide"
style="-webkit-overflow-scrolling: touch;"
>
<For each={allSections}>
{(section) => {
const isActive = () => section.id === props.current;
const Icon = section.icon;
return (
<button
type="button"
onClick={() => props.onSelect(section.id)}
class={`inline-flex min-h-10 items-center gap-2 border-b-2 px-1 py-2 text-sm font-medium whitespace-nowrap transition-colors outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
isActive()
? 'border-blue-600 text-base-content'
: 'border-transparent text-muted hover:text-base-content'
}`}
aria-pressed={isActive()}
>
<Icon size={18} stroke-width={2} class="h-4 w-4" />
<span>{section.label}</span>
</button>
);
}}
</For>
</div>
</div>
);
};
@@ -1,529 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import type { JSX } from 'solid-js';
import type {
MonitoredSystemLedgerExplainResponse,
MonitoredSystemLedgerResponse,
} from '@/api/monitoredSystemLedger';
const explainMock = vi.fn<() => Promise<MonitoredSystemLedgerExplainResponse>>();
const presentationPolicyHidesCommercialSurfacesMock = vi.fn(() => false);
const sessionPresentationPolicyResolvedMock = vi.fn(() => true);
const explainResponse = (
ledger: MonitoredSystemLedgerResponse,
): MonitoredSystemLedgerExplainResponse => ({
ledger,
preview: null,
});
vi.mock('@/api/monitoredSystemLedger', () => ({
MonitoredSystemLedgerAPI: {
explain: () => explainMock(),
},
}));
vi.mock('@/stores/sessionPresentationPolicy', () => ({
presentationPolicyHidesCommercialSurfaces: () => presentationPolicyHidesCommercialSurfacesMock(),
sessionPresentationPolicyResolved: () => sessionPresentationPolicyResolvedMock(),
}));
vi.mock('@/components/shared/SettingsPanel', () => ({
default: (props: { children?: JSX.Element; title?: string; description?: string }) => (
<section data-testid="settings-panel">
<h2>{props.title}</h2>
<p>{props.description}</p>
{props.children}
</section>
),
}));
vi.mock('@/components/shared/StatusDot', () => ({
StatusDot: () => <span data-testid="status-dot" />,
}));
vi.mock('@/components/shared/Table', () => ({
Table: (props: { children?: JSX.Element }) => <table>{props.children}</table>,
TableHeader: (props: { children?: JSX.Element }) => <thead>{props.children}</thead>,
TableBody: (props: { children?: JSX.Element }) => <tbody>{props.children}</tbody>,
TableRow: (props: { children?: JSX.Element }) => <tr>{props.children}</tr>,
TableHead: (props: { children?: JSX.Element }) => <th>{props.children}</th>,
TableCell: (props: { children?: JSX.Element }) => <td>{props.children}</td>,
}));
vi.mock('@/utils/format', () => ({
formatRelativeTime: (v: string) => v,
}));
import { MonitoredSystemLedgerPanel } from '../MonitoredSystemLedgerPanel';
describe('MonitoredSystemLedgerPanel', () => {
beforeEach(() => {
presentationPolicyHidesCommercialSurfacesMock.mockReset();
sessionPresentationPolicyResolvedMock.mockReset();
presentationPolicyHidesCommercialSurfacesMock.mockReturnValue(false);
sessionPresentationPolicyResolvedMock.mockReturnValue(true);
});
afterEach(() => {
cleanup();
explainMock.mockReset();
});
it('waits for the session presentation policy before requesting ledger data', () => {
sessionPresentationPolicyResolvedMock.mockReturnValue(false);
render(() => <MonitoredSystemLedgerPanel />);
expect(explainMock).not.toHaveBeenCalled();
expect(screen.getByText('Checking monitored-system visibility')).toBeInTheDocument();
expect(screen.getByText(/before loading monitored-system usage details/i)).toBeInTheDocument();
});
it('hides monitored-system usage in demo mode without requesting the ledger', () => {
presentationPolicyHidesCommercialSurfacesMock.mockReturnValue(true);
render(() => <MonitoredSystemLedgerPanel />);
expect(explainMock).not.toHaveBeenCalled();
expect(screen.getByText('Monitored-system usage is hidden in demo mode')).toBeInTheDocument();
expect(screen.getByText(/instead of creating a demo license/i)).toBeInTheDocument();
expect(screen.queryByText('16 / 5')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Try again' })).not.toBeInTheDocument();
});
it('shows monitored-system loading copy while the ledger request is pending', () => {
explainMock.mockReturnValue(new Promise<MonitoredSystemLedgerExplainResponse>(() => {}));
render(() => <MonitoredSystemLedgerPanel />);
expect(screen.getByText('Loading monitored system usage…')).toBeInTheDocument();
});
it('shows error message with Retry button when API fails', async () => {
explainMock.mockRejectedValue(new Error('network error'));
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(
screen.getByText('Monitored system usage is temporarily unavailable.'),
).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
});
it('recovers from error when Retry is clicked and API succeeds', async () => {
explainMock.mockRejectedValue(new Error('network error'));
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(
screen.getByText('Monitored system usage is temporarily unavailable.'),
).toBeInTheDocument();
});
explainMock.mockResolvedValue(
explainResponse({
systems: [
{
name: 'host-1',
type: 'host',
status: 'online',
status_explanation: {
summary: 'All included top-level collection paths currently report online status.',
reasons: [],
},
latest_included_signal: {
name: 'host-1',
type: 'host',
source: 'agent',
at: '2026-01-01T00:00:00Z',
},
source: 'agent',
explanation: {
summary:
'Counts as one monitored system because Pulse sees one top-level host view from agent.',
reasons: [
{
kind: 'standalone',
signal: 'single-top-level-view',
summary: 'No overlapping top-level source matched this system.',
},
],
surfaces: [{ name: 'host-1', type: 'host', source: 'agent' }],
},
},
],
total: 1,
}),
);
fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
await waitFor(() => {
expect(screen.getByText('host-1')).toBeInTheDocument();
});
expect(
screen.queryByText('Monitored system usage is temporarily unavailable.'),
).not.toBeInTheDocument();
});
it('keeps Retry button visible on repeated failures', async () => {
explainMock.mockRejectedValue(new Error('network error'));
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(
screen.getByText('Monitored system usage is temporarily unavailable.'),
).toBeInTheDocument();
});
const callsBefore = explainMock.mock.calls.length;
fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
await waitFor(() => {
expect(
screen.getByText('Monitored system usage is temporarily unavailable.'),
).toBeInTheDocument();
});
expect(explainMock.mock.calls.length).toBeGreaterThan(callsBefore);
expect(screen.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
});
it('shows a verification state when canonical monitored-system usage is not settled', async () => {
explainMock.mockRejectedValue(
Object.assign(new Error('usage unavailable'), {
code: 'monitored_system_usage_unavailable',
details: { reason: 'supplemental_inventory_unsettled' },
}),
);
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(screen.getByText('Verifying monitored-system inventory')).toBeInTheDocument();
});
expect(screen.getByText('Verifying')).toBeInTheDocument();
expect(
screen.getByText(
'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
),
).toBeInTheDocument();
expect(screen.queryByText('0 / 5')).not.toBeInTheDocument();
expect(
screen.queryByText('Monitored system usage is temporarily unavailable.'),
).not.toBeInTheDocument();
});
it('renders ledger data on successful load', async () => {
explainMock.mockResolvedValue(
explainResponse({
systems: [
{
name: 'server-a',
type: 'host',
status: 'online',
status_explanation: {
summary: 'All included top-level collection paths currently report online status.',
reasons: [],
},
latest_included_signal: {
name: 'server-a',
type: 'host',
source: 'agent',
at: '2026-01-01T00:00:00Z',
},
source: 'agent',
explanation: {
summary:
'Counts as one monitored system because Pulse sees one top-level host view from agent.',
reasons: [
{
kind: 'standalone',
signal: 'single-top-level-view',
summary: 'No overlapping top-level source matched this system.',
},
],
surfaces: [{ name: 'server-a', type: 'host', source: 'agent' }],
},
},
{
name: 'server-b',
type: 'pbs-server',
status: 'offline',
status_explanation: {
summary:
'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
reasons: [
{
kind: 'source-offline',
name: 'server-b',
type: 'pbs-server',
source: 'pbs',
status: 'offline',
reported_at: '2026-01-01T23:55:00Z',
summary:
'PBS data for server-b is offline or disconnected (last reported 2026-01-01T23:55:00Z).',
},
],
},
latest_included_signal: {
name: 'server-b',
type: 'pbs-server',
source: 'pbs',
at: '2026-01-02T00:00:00Z',
},
source: 'pbs',
explanation: {
summary:
'Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.',
reasons: [
{
kind: 'shared-identity',
signal: 'machine-id',
summary: 'Merged host and PBS server views using shared machine identity.',
},
],
surfaces: [
{ name: 'server-b', type: 'pbs-server', source: 'pbs' },
{ name: 'server-b host', type: 'host', source: 'agent' },
],
},
},
],
total: 2,
}),
);
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(screen.getByText('server-a')).toBeInTheDocument();
});
expect(screen.getByText('Monitored System Ledger')).toBeInTheDocument();
expect(screen.getByText('Latest Included Signal')).toBeInTheDocument();
expect(screen.getByText('2026-01-02T00:00:00Z')).toBeInTheDocument();
expect(screen.getAllByText('server-b (PBS Server via PBS)').length).toBeGreaterThan(0);
expect(
screen.getByText(
'Review the top-level monitored systems Pulse has identified for reporting and support context.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'View counting rules' })).toBeInTheDocument();
expect(
screen.queryByText(
/a monitored system is a top-level monitored root such as a docker host, kubernetes cluster, proxmox node/i,
),
).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'View counting rules' }));
expect(
screen.getByText(
/a monitored system is a top-level monitored root such as a docker host, kubernetes cluster, proxmox node/i,
),
).toBeInTheDocument();
expect(screen.getByText('server-b')).toBeInTheDocument();
expect(screen.getByText('2 monitored systems')).toBeInTheDocument();
expect(screen.queryByText('Continuity review')).not.toBeInTheDocument();
expect(screen.getAllByRole('button', { name: 'View counting details' })).toHaveLength(2);
fireEvent.click(screen.getAllByRole('button', { name: 'View counting details' })[1]!);
expect(
screen.getAllByText(
'Counts as one monitored system because Pulse merged 2 top-level views into one canonical system using shared machine identity.',
),
).toHaveLength(2);
expect(screen.getAllByText('Counts as 1 monitored system')).toHaveLength(2);
expect(screen.getByText('2 grouped sources')).toBeInTheDocument();
expect(screen.getByText('Current status')).toBeInTheDocument();
expect(
screen.getByText(
'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'Latest included signal: server-b (PBS Server via PBS), reported 2026-01-02T00:00:00Z.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'PBS data for server-b is offline or disconnected (last reported 2026-01-01T23:55:00Z).',
),
).toBeInTheDocument();
expect(screen.getByText('Why this counts')).toBeInTheDocument();
expect(screen.getByText('Grouped sources')).toBeInTheDocument();
expect(screen.getAllByText('server-b host (Host via Agent)').length).toBeGreaterThan(0);
expect(
screen.queryByText('Monitored system usage is temporarily unavailable.'),
).not.toBeInTheDocument();
});
it('does not crash when explanation payload is missing', async () => {
explainMock.mockResolvedValue(
explainResponse({
systems: [
{
name: 'server-a',
type: 'host',
status: 'online',
status_explanation: {
summary: 'All included top-level collection paths currently report online status.',
reasons: [],
},
explanation: {
summary: 'Pulse counts this top-level collection path as one monitored system.',
reasons: [],
surfaces: [],
},
latest_included_signal: {
name: 'server-a',
type: 'host',
source: 'agent',
at: '2026-01-01T00:00:00Z',
},
source: 'agent',
},
],
total: 1,
}),
);
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(screen.getByText('server-a')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: 'View counting details' }));
expect(
screen.getAllByText('Pulse counts this top-level collection path as one monitored system.'),
).toHaveLength(2);
expect(
screen.getByText('All included top-level collection paths currently report online status.'),
).toBeInTheDocument();
expect(screen.queryByText('Grouped sources')).not.toBeInTheDocument();
});
it('shows a customer-facing fallback when no included signal is available', async () => {
explainMock.mockResolvedValue(
explainResponse({
systems: [
{
name: 'server-c',
type: 'host',
status: 'unknown',
status_explanation: {
summary:
'Pulse cannot determine a canonical runtime status for this monitored system yet.',
reasons: [],
},
explanation: {
summary: 'Pulse counts this top-level collection path as one monitored system.',
reasons: [],
surfaces: [],
},
latest_included_signal: {
name: 'server-c',
type: 'host',
source: 'agent',
at: '',
},
source: 'agent',
},
],
total: 1,
}),
);
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(screen.getByText('server-c')).toBeInTheDocument();
});
expect(screen.getByText('No included signal yet.')).toBeInTheDocument();
});
it('falls back to the canonical status summary for the row status when status explanation is missing', async () => {
explainMock.mockResolvedValue(
explainResponse({
systems: [
{
name: 'server-d',
type: 'host',
status: 'offline',
status_explanation: {
summary:
'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
reasons: [],
},
explanation: {
summary: 'Pulse counts this top-level collection path as one monitored system.',
reasons: [],
surfaces: [],
},
latest_included_signal: {
name: 'server-d',
type: 'host',
source: 'agent',
at: '2026-01-01T00:00:00Z',
},
source: 'agent',
},
],
total: 1,
}),
);
render(() => <MonitoredSystemLedgerPanel />);
await waitFor(() => {
expect(screen.getByText('server-d')).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: 'View counting details' }));
expect(
screen.getByText(
'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
),
).toBeInTheDocument();
});
it('opens counting rules by default for usage-focused billing arrivals', async () => {
explainMock.mockResolvedValue(
explainResponse({
systems: [],
total: 2,
}),
);
render(() => <MonitoredSystemLedgerPanel showCountingRulesByDefault />);
await waitFor(() => {
expect(screen.getByText('2 monitored systems')).toBeInTheDocument();
});
expect(screen.getByRole('button', { name: 'Hide counting rules' })).toHaveAttribute(
'aria-expanded',
'true',
);
expect(
screen.getByText(
/a monitored system is a top-level monitored root such as a docker host, kubernetes cluster, proxmox node/i,
),
).toBeInTheDocument();
});
});
@@ -58,7 +58,6 @@ import interactiveSparklineSource from '@/components/shared/InteractiveSparkline
import interactiveSparklineModelSource from '@/components/shared/interactiveSparklineModel.ts?raw';
import contextualFocusSource from '@/components/shared/contextualFocus.ts?raw';
import summaryCardInteractionSource from '@/components/shared/summaryCardInteraction.ts?raw';
import summaryJumpToRowButtonSource from '@/components/shared/SummaryJumpToRowButton.tsx?raw';
import summaryRowActionButtonSource from '@/components/shared/SummaryRowActionButton.tsx?raw';
import summaryInteractionA11ySource from '@/components/shared/summaryInteractionA11y.ts?raw';
import tableSource from '@/components/shared/Table.tsx?raw';
@@ -72,12 +71,9 @@ import animatedNumberModelSource from '@/components/shared/animatedNumberModel.t
import animatedNumberStateSource from '@/components/shared/useAnimatedNumberState.ts?raw';
import workloadsFilterSource from '@/components/Workloads/WorkloadsFilter.tsx?raw';
import infrastructureSourceManagerSource from '@/components/Settings/InfrastructureSourceManager.tsx?raw';
import configuredNodeTablesSource from '@/components/Settings/ConfiguredNodeTables.tsx?raw';
import selectionCardGroupSource from '@/components/shared/SelectionCardGroup.tsx?raw';
import selectionCardGroupModelSource from '@/components/shared/selectionCardGroupModel.ts?raw';
import summaryMetricCardSource from '@/components/shared/SummaryMetricCard.tsx?raw';
import summaryChartLayoutSource from '@/components/shared/summaryChartLayout.ts?raw';
import summarySynchronizedReadoutSource from '@/components/shared/SummarySynchronizedReadout.tsx?raw';
import tagBadgesSource from '@/components/shared/TagBadges.tsx?raw';
import tlsVerificationWarningBannerSource from '@/components/shared/TlsVerificationWarningBanner.tsx?raw';
import commandPaletteStateSource from '@/components/shared/useCommandPaletteState.ts?raw';
@@ -130,7 +126,6 @@ import storagePoolRowSource from '@/components/Storage/StoragePoolRow.tsx?raw';
import storageContentCardSource from '@/components/Storage/StorageContentCard.tsx?raw';
import storagePoolsTableSource from '@/components/Storage/StoragePoolsTable.tsx?raw';
import diskListSource from '@/components/Storage/DiskList.tsx?raw';
import workloadsSummarySource from '@/components/Workloads/WorkloadsSummary.tsx?raw';
import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.ts?raw';
import alertHistoryTableSectionSource from '@/features/alerts/AlertHistoryTableSection.tsx?raw';
import alertHistoryTableGroupRowSource from '@/features/alerts/AlertHistoryTableGroupRow.tsx?raw';
@@ -390,10 +385,6 @@ describe('shared primitive guardrails', () => {
expect(workloadSelectionStateSource).toContain('activeSummaryWorkloadGroupScope');
expect(workloadSelectionStateSource).not.toContain('const scrollTop = scroller?.scrollTop');
expect(summaryJumpToRowButtonSource).toContain('<span>Jump to row</span>');
expect(summaryJumpToRowButtonSource).toContain('props.onClick');
expect(summaryJumpToRowButtonSource).not.toContain('querySelector');
expect(summaryJumpToRowButtonSource).not.toContain('scrollIntoView');
expect(summaryTableFocusSource).toContain('export function useSummaryTableFocusBridge');
expect(summaryTableFocusSource).toContain('export function useSummaryPageInteractionState');
@@ -416,31 +407,16 @@ describe('shared primitive guardrails', () => {
expect(workloadsSummarySource).toContain('useSummaryContextualFocusState');
expect(workloadsSummarySource).toContain('chartHoverSync');
expect(workloadsSummarySource).toContain('hoveredGroupScope');
expect(workloadsSummarySource).toContain('filterSeriesForActiveScope');
expect(workloadsSummarySource).not.toContain('const interactiveWorkloadIds = createMemo');
});
it('keeps synchronized summary values on one shared card/readout contract', () => {
expect(summaryMetricCardSource).toContain('headerValue?: JSX.Element');
expect(summaryMetricCardSource).toContain("bodyLayout?: 'chart' | 'auto'");
expect(summaryMetricCardSource).toContain('props.headerValue');
expect(summaryMetricCardSource).not.toContain('data-summary-sync-readout');
expect(summarySynchronizedReadoutSource).toContain('export const SummarySynchronizedReadout');
expect(summarySynchronizedReadoutSource).toContain('data-summary-sync-readout="true"');
expect(summarySynchronizedReadoutSource).toContain('formatSummarySynchronizedReadoutTime');
expect(summarySynchronizedReadoutSource).not.toContain('Portal');
expect(interactiveSparklineModelSource).toContain(
'buildInteractiveSparklineSynchronizedReadout',
);
expect(densityMapModelSource).toContain('buildDensityMapSynchronizedReadout');
expect(workloadsSummarySource).toContain('SummarySynchronizedReadout');
expect(workloadsSummarySource).toContain('headerValue={');
});
it('keeps summary-linked table row emphasis on the shared active-row presentation contract', () => {
@@ -532,7 +508,6 @@ describe('shared primitive guardrails', () => {
storagePoolsTableSource,
diskListSource,
infrastructureSourceManagerSource,
configuredNodeTablesSource,
aiCostDashboardSource,
deployCandidatesStepSource,
deployConfirmStepSource,
@@ -575,7 +550,6 @@ describe('shared primitive guardrails', () => {
expect(storagePoolsTableSource).not.toContain('STORAGE_POOLS_SCROLL_WRAP_CLASS');
expect(diskListSource).not.toContain("from '@/components/shared/Card'");
expect(diskListSource).not.toContain('PHYSICAL_DISK_TABLE_SCROLL_CLASS');
expect(configuredNodeTablesSource).toContain('wrapperClass="max-h-[600px] overflow-y-auto"');
expect(storageContentCardSource).toContain('<StoragePoolsTable');
expect(pulseDataGridSource).toContain('wrapperClass="scrollbar-hide"');
expect(pulseDataGridSource).toContain('getPulseDataGridFrameClass');
@@ -1270,14 +1244,6 @@ describe('shared primitive guardrails', () => {
it('keeps summary density control inside the shared summary primitives', () => {
expect(summaryMetricCardSource).toContain("density?: 'default' | 'compact'");
expect(summaryMetricCardSource).toContain("props.density === 'compact'");
expect(summaryMetricCardSource).toContain("props.bodyLayout ?? 'chart'");
expect(summaryMetricCardSource).toContain(
"isCompact() ? 'mb-1 min-h-[20px]' : 'mb-1.5 min-h-[24px]'",
);
expect(summaryMetricCardSource).toContain('SUMMARY_CHART_SLOT_COMPACT_CLASS');
expect(summaryMetricCardSource).toContain('SUMMARY_CHART_SLOT_CLASS');
expect(summaryChartLayoutSource).toContain(
"export const SUMMARY_CHART_SLOT_CLASS = 'h-[136px] sm:h-[150px]'",
);
@@ -1287,9 +1253,6 @@ describe('shared primitive guardrails', () => {
expect(summaryChartLayoutSource).toContain(
"export const SUMMARY_CHART_PLOT_AREA_CLASS = 'h-[120px] sm:h-[134px]'",
);
expect(summaryMetricCardSource).toContain('!p-1.5 sm:!p-2');
expect(summaryMetricCardSource).not.toContain('Recovery Posture');
expect(summaryMetricCardSource).not.toContain('Freshness');
});
it('keeps sticky summary breakpoint behavior on the shared primitive', () => {
@@ -1,25 +0,0 @@
import type { Component } from 'solid-js';
interface SummaryJumpToRowButtonProps {
onClick: () => void;
}
export const SummaryJumpToRowButton: Component<SummaryJumpToRowButtonProps> = (props) => {
return (
<button
type="button"
onClick={props.onClick}
class="inline-flex items-center gap-1 rounded-full border border-border-subtle bg-surface-alt/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted transition-colors hover:bg-surface-hover hover:text-base-content"
>
<span>Jump to row</span>
<svg class="h-3 w-3" viewBox="0 0 16 16" fill="none" stroke="currentColor">
<path
d="M8 3v10m0 0-3-3m3 3 3-3"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
);
};
@@ -1,91 +0,0 @@
import { Show, type JSX } from 'solid-js';
import { Card } from './Card';
import { SparklineSkeleton } from './SparklineSkeleton';
import { SUMMARY_CHART_SLOT_CLASS, SUMMARY_CHART_SLOT_COMPACT_CLASS } from './summaryChartLayout';
import type { SummaryCardInteractionState } from './summaryCardInteraction';
export interface SummaryMetricCardProps {
/** Uppercase label at top-left of the card. */
label: string;
/** Optional secondary content to the right of the label. */
secondaryLabel?: JSX.Element;
/** Optional compact readout aligned to the header right edge. */
headerValue?: JSX.Element;
/** Shared summary cards default to chart-backed bodies unless explicitly auto-sized. */
bodyLayout?: 'chart' | 'auto';
/** Whether data has loaded at least once. Drives skeleton vs empty state. */
loaded: boolean;
/** Whether there is renderable data. When false, shows the fallback. */
hasData: boolean;
/** Message to show when loaded but no data. Defaults to "No history yet". */
emptyMessage?: string;
/** Chart or visualization content. */
children: JSX.Element;
/** Optional shared density override for compact monitoring surfaces. */
density?: 'default' | 'compact';
/** Optional interaction state for shared hover/focus summary behavior. */
interactionState?: SummaryCardInteractionState;
}
export function SummaryMetricCard(props: SummaryMetricCardProps) {
const isCompact = () => props.density === 'compact';
const bodyLayout = () => props.bodyLayout ?? 'chart';
const interactionState = () => props.interactionState ?? 'default';
const chartSlotClass = () =>
isCompact() ? SUMMARY_CHART_SLOT_COMPACT_CLASS : SUMMARY_CHART_SLOT_CLASS;
const interactionClass = () => {
switch (interactionState()) {
case 'active':
return 'border-sky-500/45 ring-2 ring-inset ring-sky-500/25 shadow-sm';
case 'inactive':
return 'opacity-50';
default:
return '';
}
};
return (
<div
data-summary-card-state={interactionState()}
class={`h-full rounded-md transition-all duration-150 ease-out ${interactionClass()}`.trim()}
>
<Card padding="sm" class={`h-full ${isCompact() ? '!p-1.5 sm:!p-2' : ''}`.trim()}>
<div class="flex flex-col h-full">
<div
class={`flex min-w-0 items-center gap-2 ${isCompact() ? 'mb-1 min-h-[20px]' : 'mb-1.5 min-h-[24px]'}`.trim()}
>
<div class="flex min-w-0 flex-1 items-center">
<span class="text-xs font-medium text-muted uppercase tracking-wide shrink-0">
{props.label}
</span>
{props.secondaryLabel}
</div>
<Show when={props.headerValue}>
<div class="ml-auto shrink-0">{props.headerValue}</div>
</Show>
</div>
<Show
when={props.hasData}
fallback={
props.loaded ? (
<div class="flex h-[56px] items-center text-sm text-muted">
{props.emptyMessage ?? 'No history yet'}
</div>
) : (
<SparklineSkeleton />
)
}
>
<div
class={`min-h-0 ${
bodyLayout() === 'chart' ? `${chartSlotClass()} shrink-0` : 'flex-1'
}`.trim()}
>
{props.children}
</div>
</Show>
</div>
</Card>
</div>
);
}
@@ -1,34 +0,0 @@
import type { Component } from 'solid-js';
export interface SummarySynchronizedReadoutProps {
empty?: boolean;
timestamp: number;
value: string;
}
export const formatSummarySynchronizedReadoutTime = (timestamp: number): string =>
new Date(timestamp).toLocaleString([], {
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false,
});
export const SummarySynchronizedReadout: Component<
SummarySynchronizedReadoutProps
> = (props) => (
<span
data-summary-sync-readout="true"
data-summary-sync-empty={props.empty === true ? 'true' : 'false'}
data-summary-sync-timestamp={props.timestamp}
class={`whitespace-nowrap text-[10px] leading-none ${
props.empty === true ? 'text-muted' : 'font-semibold text-base-content tabular-nums'
}`.trim()}
title={`Synced value at ${formatSummarySynchronizedReadoutTime(props.timestamp)}`}
>
{props.value}
</span>
);
export default SummarySynchronizedReadout;
@@ -1,35 +0,0 @@
import type { Resource } from '@/types/resource';
// Default-hidden columns for app-container workloads. `disk` and `tags` are
// existing hides; `backup` is added because the workload-table Backup column
// is driven exclusively by `resource.proxmox.lastBackup`, which Proxmox VE
// populates from its vzdump scheduler / PBS integration. App containers
// (Docker, Podman, TrueNAS apps) have no equivalent source — the cell is
// not just empty, the concept doesn't exist at this integration layer.
export const APP_CONTAINER_BASE_DEFAULT_HIDDEN_COLUMNS = ['disk', 'tags', 'backup'] as const;
export const APP_CONTAINER_COLUMN_LABEL_OVERRIDES = {
disk: 'Writable layer',
} as const;
const hasFiniteMetric = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value);
export function buildAppContainerDefaultHiddenColumnIds(
containers: readonly Resource[],
): string[] {
const hasNetworkIOTelemetry = containers.some(
(container) =>
hasFiniteMetric(container.network?.rxBytes) || hasFiniteMetric(container.network?.txBytes),
);
const hasDiskIOTelemetry = containers.some(
(container) =>
hasFiniteMetric(container.diskIO?.readRate) || hasFiniteMetric(container.diskIO?.writeRate),
);
return [
...APP_CONTAINER_BASE_DEFAULT_HIDDEN_COLUMNS,
...(hasNetworkIOTelemetry ? [] : ['netIo']),
...(hasDiskIOTelemetry ? [] : ['diskIo']),
];
}
@@ -1,18 +0,0 @@
import { describe, expect, it } from 'vitest';
import { getResourceApprovalLevelLabel } from '@/utils/approvalPresentation';
describe('approvalPresentation', () => {
it('formats canonical approval labels', () => {
expect(getResourceApprovalLevelLabel('none')).toBe('None');
expect(getResourceApprovalLevelLabel('dry_run_only')).toBe('Dry Run Only');
expect(getResourceApprovalLevelLabel('admin')).toBe('Admin');
expect(getResourceApprovalLevelLabel('mfa')).toBe('MFA');
});
it('falls back safely for unknown approval levels', () => {
expect(getResourceApprovalLevelLabel('strict')).toBe('Strict');
expect(getResourceApprovalLevelLabel('')).toBe('—');
expect(getResourceApprovalLevelLabel(undefined)).toBe('—');
});
});
@@ -1,15 +0,0 @@
import { describe, expect, it } from 'vitest';
import { getChartSeriesColor } from '@/utils/chartSeriesPresentation';
describe('chartSeriesPresentation', () => {
it('returns stable chart series colors by index', () => {
expect(getChartSeriesColor(0)).toBe('#3b82f6');
expect(getChartSeriesColor(1)).toBe('#8b5cf6');
expect(getChartSeriesColor(7)).toBe('#ef4444');
});
it('wraps the palette when the index exceeds the set length', () => {
expect(getChartSeriesColor(8)).toBe('#3b82f6');
expect(getChartSeriesColor(9)).toBe('#8b5cf6');
});
});
@@ -54,7 +54,6 @@ import searchTipsPopoverModelSource from '@/components/shared/searchTipsPopoverM
import tooltipSource from '@/components/shared/Tooltip.tsx?raw';
import tooltipModelSource from '@/components/shared/tooltipModel.ts?raw';
import upgradeLinkSource from '@/components/shared/UpgradeLink.tsx?raw';
import monitoredSystemLedgerPanelSource from '@/components/Settings/MonitoredSystemLedgerPanel.tsx?raw';
import monitoredSystemDefinitionDisclosureSource from '@/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx?raw';
import interactiveSparklineSource from '@/components/shared/InteractiveSparkline.tsx?raw';
import interactiveSparklineModelSource from '@/components/shared/interactiveSparklineModel.ts?raw';
@@ -137,13 +136,8 @@ import dockerRuntimeSettingsCardSource from '@/components/Settings/DockerRuntime
import discoveryTargetSource from '@/utils/discoveryTarget.ts?raw';
import recoveryOutcomePresentationSource from '@/utils/recoveryOutcomePresentation.ts?raw';
import recoveryArtifactModePresentationSource from '@/utils/recoveryArtifactModePresentation.ts?raw';
import recoveryFilterChipPresentationSource from '@/utils/recoveryFilterChipPresentation.ts?raw';
import recoveryIssuePresentationSource from '@/utils/recoveryIssuePresentation.ts?raw';
import recoveryActionPresentationSource from '@/utils/recoveryActionPresentation.ts?raw';
import recoveryDatePresentationSource from '@/utils/recoveryDatePresentation.ts?raw';
import recoveryRecordPresentationSource from '@/utils/recoveryRecordPresentation.ts?raw';
import recoveryEmptyStatePresentationSource from '@/utils/recoveryEmptyStatePresentation.ts?raw';
import recoveryStatusPresentationSource from '@/utils/recoveryStatusPresentation.ts?raw';
import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.ts?raw';
import recoveryTimelineChartPresentationSource from '@/utils/recoveryTimelineChartPresentation.ts?raw';
import recoveryTimelinePresentationSource from '@/utils/recoveryTimelinePresentation.ts?raw';
@@ -181,7 +175,6 @@ import stackedMemoryBarSource from '@/components/Workloads/StackedMemoryBar.tsx?
import stackedMemoryBarModelSource from '@/components/Workloads/stackedMemoryBarModel.ts?raw';
import stackedMemoryBarStateSource from '@/components/Workloads/useStackedMemoryBarState.ts?raw';
import thresholdSliderSource from '@/components/Workloads/ThresholdSlider.tsx?raw';
import problemResourcePresentationSource from '@/utils/problemResourcePresentation.ts?raw';
import guestRowSource from '@/components/Workloads/GuestRow.tsx?raw';
import guestRowCellsSource from '@/components/Workloads/GuestRowCells.tsx?raw';
import guestRowModelSource from '@/components/Workloads/guestRowModel.tsx?raw';
@@ -201,12 +194,10 @@ import orgScopeSource from '@/utils/orgScope.ts?raw';
import workloadsUtilitySource from '@/utils/workloads.ts?raw';
import infrastructureSummaryCacheSource from '@/utils/infrastructureSummaryCache.ts?raw';
import workloadEmptyStatePresentationSource from '@/utils/workloadEmptyStatePresentation.ts?raw';
import throughputPresentationSource from '@/utils/throughputPresentation.ts?raw';
import resourceChangeSummarySource from '@/components/Infrastructure/ResourceChangeSummary.tsx?raw';
import resourceChangePresentationSource from '@/utils/resourceChangePresentation.ts?raw';
import resourceCorrelationPresentationSource from '@/utils/resourceCorrelationPresentation.ts?raw';
import confidencePresentationSource from '@/utils/confidencePresentation.ts?raw';
import approvalPresentationSource from '@/utils/approvalPresentation.ts?raw';
import textPresentationSource from '@/utils/textPresentation.ts?raw';
import messageItemSource from '@/components/AI/Chat/MessageItem.tsx?raw';
import toolExecutionBlockSource from '@/components/AI/Chat/ToolExecutionBlock.tsx?raw';
@@ -280,7 +271,6 @@ import resourceDetailDrawerShellSource from '@/components/Infrastructure/Resourc
import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw';
import resourceDetailDrawerDebugSource from '@/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx?raw';
import resourceDetailSummarySource from '@/components/Infrastructure/ResourceDetailSummary.tsx?raw';
import infrastructureSummaryModelSource from '@/components/Infrastructure/infrastructureSummaryModel.ts?raw';
import resourceDetailDrawerDiscoveryModelSource from '@/components/Infrastructure/resourceDetailDiscoveryModel.ts?raw';
import resourceDetailMappersSource from '@/components/Infrastructure/resourceDetailMappers.ts?raw';
import resourceDetailDrawerHistoryStateSource from '@/components/Infrastructure/useResourceDetailDrawerHistoryState.ts?raw';
@@ -310,7 +300,6 @@ import pmgServiceHealthBadgeSource from '@/components/PMG/ServiceHealthBadge.tsx
import proxmoxSettingsModelSource from '@/components/Settings/proxmoxSettingsModel.ts?raw';
import infrastructureWorkspaceSource from '@/components/Settings/InfrastructureWorkspace.tsx?raw';
import infrastructureWorkspaceModelSource from '@/components/Settings/infrastructureWorkspaceModel.ts?raw';
import proxmoxSettingsPresentationSource from '@/utils/proxmoxSettingsPresentation.ts?raw';
import generalSettingsPanelSource from '@/components/Settings/GeneralSettingsPanel.tsx?raw';
import nodeModalAuthenticationSectionSource from '@/components/Settings/NodeModalAuthenticationSection.tsx?raw';
import nodeModalBasicInfoSectionSource from '@/components/Settings/NodeModalBasicInfoSection.tsx?raw';
@@ -447,7 +436,6 @@ import alertSeverityPresentationSource from '@/utils/alertSeverityPresentation.t
import alertTabsPresentationSource from '@/utils/alertTabsPresentation.ts?raw';
import alertGroupingPresentationSource from '@/utils/alertGroupingPresentation.ts?raw';
import alertSchedulePresentationSource from '@/utils/alertSchedulePresentation.ts?raw';
import configuredNodeTablesSource from '@/components/Settings/ConfiguredNodeTables.tsx?raw';
import configuredNodeCapabilityPresentationSource from '@/utils/configuredNodeCapabilityPresentation.ts?raw';
import configuredNodeStatusPresentationSource from '@/utils/configuredNodeStatusPresentation.ts?raw';
import rbacPresentationSource from '@/utils/rbacPresentation.ts?raw';
@@ -981,9 +969,6 @@ describe('frontend resource type boundaries', () => {
);
expect(recoveryIssuePresentationSource).toContain('export function getRecoveryIssueRailClass');
expect(recoveryTablePresentationSource).toContain('getAllFilterOptionLabel');
expect(recoveryFilterChipPresentationSource).toContain(
'export function getRecoveryFilterChipPresentation',
);
expect(recoveryTablePresentationSource).toContain(
'export function getRecoveryArtifactColumnHeaderClass',
);
@@ -1001,60 +986,6 @@ describe('frontend resource type boundaries', () => {
expect(recoveryDatePresentationSource).toContain('export function getRecoveryCompactAxisLabel');
expect(recoveryDatePresentationSource).toContain('export function formatRecoveryTimeOnly');
expect(recoveryDatePresentationSource).toContain('export function getRecoveryNiceAxisMax');
expect(recoveryActionPresentationSource).toContain(
'export function getRecoveryBreadcrumbLinkClass',
);
expect(recoveryActionPresentationSource).toContain(
'export function getRecoveryFilterPanelClearClass',
);
expect(recoveryActionPresentationSource).toContain(
'export function getRecoveryEmptyStateActionClass',
);
expect(recoveryActionPresentationSource).toContain(
'export function getRecoveryDrawerCloseButtonClass',
);
expect(recoveryRecordPresentationSource).toContain(
'export function getRecoveryRollupSubjectLabel',
);
expect(recoveryRecordPresentationSource).toContain(
'export function getRecoveryPointSubjectLabel',
);
expect(recoveryRecordPresentationSource).toContain(
'export function getRecoveryPointRepositoryLabel',
);
expect(recoveryRecordPresentationSource).toContain(
'export function getRecoveryPointDetailsSummary',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryProtectedItemsLoadingState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryProtectedItemsFailureState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryActivityLoadingState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryActivityEmptyState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryProtectedItemsEmptyState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryHistoryEmptyState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryPointsLoadingState',
);
expect(recoveryEmptyStatePresentationSource).toContain(
'export function getRecoveryPointsFailureState',
);
expect(recoveryRecordPresentationSource).toContain(
'export function getRecoveryPointTimestampMs',
);
expect(recoveryRecordPresentationSource).toContain(
'export function normalizeRecoveryModeQueryValue',
);
expect(recoveryTimelineChartPresentationSource).toContain(
'export function getRecoveryTimelineAxisLabelClass',
);
@@ -1064,18 +995,6 @@ describe('frontend resource type boundaries', () => {
expect(recoveryTimelineChartPresentationSource).toContain(
'export function getRecoveryTimelineLabelEvery',
);
expect(recoveryStatusPresentationSource).toContain(
'export function getRecoveryProtectedToggleClass',
);
expect(recoveryStatusPresentationSource).toContain(
'export function getRecoveryRollupStatusPillClass',
);
expect(recoveryStatusPresentationSource).toContain(
'export function getRecoveryRollupStatusPillLabel',
);
expect(recoveryStatusPresentationSource).toContain(
'export function getRecoverySpecialOutcomeTextClass',
);
expect(recoveryTablePresentationSource).toContain(
'export function getRecoveryArtifactRowClass',
);
@@ -1383,9 +1302,6 @@ describe('frontend resource type boundaries', () => {
);
expect(agentProfileSuggestionPresentationSource).toContain('titleCaseDelimitedLabel');
expect(agentProfileSuggestionPresentationSource).not.toContain('const titleize =');
expect(problemResourcePresentationSource).toContain(
'export function getProblemResourceStatusVariant',
);
expect(diskListSource).toContain('getTemperatureTextClass');
expect(diskListSource).not.toContain('const getTemperatureTone =');
expect(diskListSource).toContain('getPhysicalDiskHealthStatus');
@@ -1596,19 +1512,12 @@ describe('frontend resource type boundaries', () => {
expect(useEnhancedStorageBarModelSource).toContain('getStorageBarLabel');
expect(useEnhancedStorageBarModelSource).toContain('getStorageBarTooltipRows');
expect(useEnhancedStorageBarModelSource).toContain('getStorageBarZfsSummary');
expect(storagePagePresentationSource).toContain('export const shouldShowCephSummaryCard');
expect(storagePagePresentationSource).toContain('export const getStoragePageBannerMessage');
expect(storagePagePresentationSource).toContain('export const STORAGE_VIEW_OPTIONS');
expect(storagePagePresentationSource).toContain('export const getStoragePoolTableColumns');
expect(storagePagePresentationSource).toContain(
'export const STORAGE_BANNER_ACTION_BUTTON_CLASS',
);
expect(storagePagePresentationSource).toContain('export const getStorageTableHeading');
expect(storagePagePresentationSource).toContain('export const getStorageLoadingMessage');
expect(storagePageStatusSource).toContain('export const getStoragePageBannerKind');
expect(storagePageStatusSource).toContain('export const isStoragePoolLoading');
expect(storagePageStatusHookSource).toContain('export const useStoragePageStatus');
expect(storagePageStatusHookSource).toContain('getStoragePageBannerKind');
expect(storagePageStatusHookSource).toContain('isStoragePoolLoading');
expect(storagePageDataSource).toContain('export const useStoragePageData');
expect(storagePageDataSource).toContain('buildStorageRecords');
@@ -2312,8 +2221,6 @@ describe('frontend resource type boundaries', () => {
expect(resourceCorrelationPresentationSource).toContain('asTrimmedString');
expect(confidencePresentationSource).toContain('formatConfidencePercentage');
expect(confidencePresentationSource).toContain('formatConfidenceLabel');
expect(approvalPresentationSource).toContain('getResourceApprovalLevelLabel');
expect(throughputPresentationSource).toContain('formatThroughputRate');
expect(resourceDetailDrawerSource).toContain('formatIdentifierLabel');
expect(resourceDetailDrawerIdentityModelSource).toContain('formatIdentifierLabel');
expect(resourceDetailDrawerServiceModelSource).toContain('buildPbsVisibleJobBreakdown');
@@ -2369,10 +2276,6 @@ describe('frontend resource type boundaries', () => {
expect(aiChatSource).not.toContain('const normalizeMentionKeyPart =');
expect(chatIdentifiersSource).toContain('normalizeChatMentionKeyPart');
expect(chatIdentifiersSource).toContain('normalizeChatToolName');
expect(infrastructureSummaryModelSource).toContain('getNormalizedIdentityLookupVariants');
expect(infrastructureSummaryModelSource).toContain('buildInfrastructureDisplaySeries');
expect(infrastructureSummaryModelSource).toContain('buildInfrastructureMetricSeries');
expect(infrastructureSummaryModelSource).toContain('buildInfrastructureEmptyMessage');
expect(resourceIdentitySource).toContain('getNormalizedIdentityLookupVariants');
expect(stringUtilsSource).toContain('export const asTrimmedString');
expect(resourceIdentitySource).not.toContain(
@@ -2572,14 +2475,6 @@ describe('frontend resource type boundaries', () => {
expect(selectionCardGroupModelSource).toContain('resolveSelectionCardTone');
expect(selectionCardGroupModelSource).toContain('getSelectionCardButtonClass');
expect(selectionCardGroupModelSource).toContain("compact: 'grid grid-cols-2 gap-2'");
expect(monitoredSystemLedgerPanelSource).toContain('@/utils/monitoredSystemPresentation');
expect(monitoredSystemLedgerPanelSource).toContain('getMonitoredSystemLedgerPresentation');
expect(monitoredSystemLedgerPanelSource).toContain(
'getMonitoredSystemCountingDetailsToggleLabel',
);
expect(monitoredSystemLedgerPanelSource).toContain('getMonitoredSystemLedgerDescription');
expect(monitoredSystemLedgerPanelSource).not.toContain('No monitored systems counted.');
expect(monitoredSystemLedgerPanelSource).not.toContain('Current status');
expect(monitoredSystemDefinitionDisclosureSource).toContain(
'@/utils/monitoredSystemPresentation',
);
@@ -2640,9 +2535,6 @@ describe('frontend resource type boundaries', () => {
expect(collapsibleSearchInputModelSource).toContain('getCollapsibleSearchTriggerLabel');
expect(collapsibleSearchInputModelSource).toContain('shouldShowCollapsibleSearchExpanded');
expect(collapsibleSearchInputModelSource).toContain('getCollapsibleSearchRootClass');
expect(infrastructureSummaryModelSource).not.toContain(
'const asTrimmedString = (value: unknown): string | null => {',
);
expect(interactiveSparklineSource).toContain('useInteractiveSparklineState');
expect(interactiveSparklineSource).not.toContain('scheduleSparkline');
expect(interactiveSparklineSource).not.toContain('downsampleLTTB');
@@ -2949,15 +2841,6 @@ describe('frontend resource type boundaries', () => {
expect(proxmoxSettingsModelSource).toContain(
'export interface InfrastructurePlatformSettingsProps',
);
expect(proxmoxSettingsPresentationSource).toContain(
'export const PROXMOX_VARIANT_PRESENTATION',
);
expect(proxmoxSettingsPresentationSource).toContain(
'export function getProxmoxVariantPresentation',
);
expect(proxmoxSettingsPresentationSource).toContain(
'export function buildProxmoxDiscoveryPrefillNode',
);
expect(infrastructureWorkspaceSource).toContain(
'@/components/Settings/infrastructureWorkspaceModel'.replace('@/components/Settings/', './'),
);
@@ -3626,20 +3509,6 @@ describe('frontend resource type boundaries', () => {
expect(alertSchedulePresentationSource).toContain(
'export function getAlertQuietSuppressCheckboxClass',
);
expect(configuredNodeTablesSource).toContain('getConfiguredNodeCapabilityBadges');
expect(configuredNodeTablesSource).toContain('resolveConfiguredPveNodeStatusIndicator');
expect(configuredNodeTablesSource).toContain('resolveConfiguredInstanceStatusIndicator');
expect(configuredNodeTablesSource).toMatch(/<Table(?:\s|>)/);
expect(configuredNodeTablesSource).toContain('wrapperClass="max-h-[600px] overflow-y-auto"');
expect(configuredNodeTablesSource).not.toContain('<div class="overflow-x-auto">');
expect(configuredNodeTablesSource).not.toContain('<div class="overflow-auto max-h-[600px]">');
expect(configuredNodeTablesSource).not.toContain("'monitorVMs' in node");
expect(configuredNodeTablesSource).not.toContain("'monitorDatastores' in node");
expect(configuredNodeTablesSource).not.toContain(
'const resolveConfiguredNodeStatusIndicator =',
);
expect(configuredNodeTablesSource).not.toContain('const resolvePbsStatusIndicator =');
expect(configuredNodeTablesSource).not.toContain('const resolvePmgStatusIndicator =');
expect(configuredNodeCapabilityPresentationSource).toContain(
'export function getConfiguredNodeCapabilityBadges',
);
@@ -1,55 +0,0 @@
import { describe, expect, it } from 'vitest';
import type { Resource } from '@/types/resource';
import {
getProblemResourceIssueLabel,
getProblemResourceMemberLabel,
getProblemResourceStatusVariant,
isGenericProblemResourceDisplayName,
} from '@/utils/problemResourcePresentation';
function resource(overrides: Partial<Resource>): Resource {
return {
id: 'resource-1',
type: 'storage',
name: 'storage',
displayName: 'storage',
status: 'offline',
...overrides,
} as Resource;
}
describe('problemResourcePresentation', () => {
it('treats degraded dashboard problem resources as warning', () => {
expect(getProblemResourceStatusVariant(149)).toBe('warning');
});
it('treats offline or critical dashboard problem resources as danger', () => {
expect(getProblemResourceStatusVariant(150)).toBe('danger');
expect(getProblemResourceStatusVariant(200)).toBe('danger');
});
it('normalizes generic type-plus-status resource names to an issue label', () => {
const problemResource = resource({
id: 'storage-offline',
name: 'storage (offline)',
displayName: 'storage (offline)',
});
expect(isGenericProblemResourceDisplayName(problemResource, ['Offline'])).toBe(true);
expect(getProblemResourceIssueLabel(problemResource, ['Offline'])).toBe('the storage issue');
expect(getProblemResourceMemberLabel(problemResource, ['Offline'])).toBeNull();
});
it('preserves meaningful resource names for problem rows', () => {
const problemResource = resource({
id: 'vm-101',
type: 'vm',
name: 'database-vm',
displayName: 'database-vm',
});
expect(isGenericProblemResourceDisplayName(problemResource, ['Offline'])).toBe(false);
expect(getProblemResourceIssueLabel(problemResource, ['Offline'])).toBe('database-vm');
expect(getProblemResourceMemberLabel(problemResource, ['Offline'])).toBe('database-vm');
});
});
@@ -1,68 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
buildProxmoxDiscoveryPrefillNode,
getProxmoxVariantPresentation,
} from '@/utils/proxmoxSettingsPresentation';
describe('proxmoxSettingsPresentation', () => {
it('returns canonical variant presentation for proxmox node types', () => {
expect(getProxmoxVariantPresentation('pve')).toMatchObject({
title: 'Proxmox VE nodes',
addLabel: 'Add Proxmox VE Connection',
});
expect(getProxmoxVariantPresentation('pbs')).toMatchObject({
title: 'Proxmox Backup Server nodes',
addLabel: 'Add Backup Server Connection',
});
expect(getProxmoxVariantPresentation('pmg')).toMatchObject({
title: 'Proxmox Mail Gateway nodes',
addLabel: 'Add Mail Gateway Connection',
});
});
it('builds canonical discovery prefills for each proxmox node type', () => {
expect(
buildProxmoxDiscoveryPrefillNode({
type: 'pve',
ip: '10.0.0.2',
port: 8006,
hostname: 'pve1',
} as never),
).toMatchObject({
type: 'pve',
name: 'pve1',
host: 'https://10.0.0.2:8006',
monitorVMs: true,
monitorContainers: true,
monitorStorage: true,
});
expect(
buildProxmoxDiscoveryPrefillNode({
type: 'pbs',
ip: '10.0.0.3',
port: 8007,
hostname: 'pbs1',
} as never),
).toMatchObject({
type: 'pbs',
name: 'pbs1',
monitorDatastores: true,
monitorPruneJobs: true,
});
expect(
buildProxmoxDiscoveryPrefillNode({
type: 'pmg',
ip: '10.0.0.4',
port: 8006,
hostname: 'pmg1',
} as never),
).toMatchObject({
type: 'pmg',
name: 'pmg1',
monitorMailStats: true,
monitorQueues: true,
});
});
});
@@ -1,19 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
getRecoveryBreadcrumbLinkClass,
getRecoveryDrawerCloseButtonClass,
getRecoveryEmptyStateActionClass,
getRecoveryFilterPanelClearClass,
} from '@/utils/recoveryActionPresentation';
describe('recoveryActionPresentation', () => {
it('derives breadcrumb and filter clear link classes', () => {
expect(getRecoveryBreadcrumbLinkClass()).toContain('text-blue-600');
expect(getRecoveryFilterPanelClearClass()).toContain('text-blue-600');
});
it('derives empty-state action and drawer-close button classes', () => {
expect(getRecoveryEmptyStateActionClass()).toContain('rounded-md border');
expect(getRecoveryDrawerCloseButtonClass()).toContain('hover:bg-surface-hover');
});
});
@@ -1,54 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
getRecoveryActivityEmptyState,
getRecoveryActivityLoadingState,
getRecoveryHistoryEmptyState,
getRecoveryPointsFailureState,
getRecoveryPointsLoadingState,
getRecoveryProtectedItemsFailureState,
getRecoveryProtectedItemsLoadingState,
getRecoveryProtectedItemsEmptyState,
} from '@/utils/recoveryEmptyStatePresentation';
describe('recoveryEmptyStatePresentation', () => {
it('returns the protection coverage empty state copy', () => {
expect(getRecoveryProtectedItemsEmptyState()).toEqual({
title: 'No protection coverage yet',
description: 'Pulse has not observed recovery coverage for this org yet.',
});
});
it('returns the recovery loading-state copy', () => {
expect(getRecoveryProtectedItemsLoadingState()).toEqual({
text: 'Loading protection coverage...',
});
expect(getRecoveryActivityLoadingState()).toEqual({
text: 'Loading recovery activity...',
});
expect(getRecoveryPointsLoadingState()).toEqual({
text: 'Loading recovery points...',
});
});
it('returns the recovery failure-state copy', () => {
expect(getRecoveryProtectedItemsFailureState()).toEqual({
title: 'Failed to load protection coverage',
});
expect(getRecoveryPointsFailureState()).toEqual({
title: 'Failed to load recovery points',
});
});
it('returns the recovery activity empty-state copy', () => {
expect(getRecoveryActivityEmptyState()).toEqual({
text: 'No recovery activity in the selected window.',
});
});
it('returns the recovery-history filter empty state copy', () => {
expect(getRecoveryHistoryEmptyState()).toEqual({
title: 'No recovery history matches your filters',
description: 'Adjust your search, platform, method, status, or verification filters.',
});
});
});
@@ -1,22 +0,0 @@
import { describe, expect, it } from 'vitest';
import { getRecoveryFilterChipPresentation } from '@/utils/recoveryFilterChipPresentation';
describe('getRecoveryFilterChipPresentation', () => {
it('returns the canonical day chip presentation', () => {
expect(getRecoveryFilterChipPresentation('day')).toEqual({
clearButtonClass: 'rounded px-1 py-0.5 text-[10px] hover:bg-blue-100 dark:hover:bg-blue-900',
className:
'inline-flex max-w-full items-center gap-1 rounded border px-2 py-0.5 text-[10px] border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200',
label: 'Day',
});
});
it('returns the canonical namespace chip presentation', () => {
expect(getRecoveryFilterChipPresentation('namespace')).toMatchObject({
clearButtonClass:
'rounded px-1 py-0.5 text-[10px] hover:bg-indigo-100 dark:hover:bg-indigo-900',
label: 'Namespace / Group',
});
expect(getRecoveryFilterChipPresentation('namespace').className).toContain('border-indigo-200');
});
});
@@ -1,121 +0,0 @@
import { describe, expect, it } from 'vitest';
import type { ProtectionRollup, RecoveryPoint } from '@/types/recovery';
import type { Resource } from '@/types/resource';
import {
getRecoveryPointItemLabel,
getRecoveryPointItemSecondaryLabel,
getRecoveryPointDetailsSummary,
getRecoveryPointKindLabel,
getRecoveryPointModeLabel,
getRecoveryPointOutcomeLabel,
getRecoveryPointRepositoryLabel,
getRecoveryPointTimestampMs,
getRecoveryRollupItemLabel,
getRecoveryRollupItemSecondaryLabel,
normalizeRecoveryModeQueryValue,
} from '@/utils/recoveryRecordPresentation';
describe('recoveryRecordPresentation', () => {
it('derives rollup and point item labels from linked resources and refs', () => {
const resources = new Map<string, Resource>([
['res-1', { id: 'res-1', type: 'vm', name: 'db-01' } as Resource],
]);
const rollup = {
rollupId: 'rollup-1',
itemResourceId: 'res-1',
} as ProtectionRollup;
const point = {
id: 'point-1',
itemResourceId: 'res-1',
} as RecoveryPoint;
expect(getRecoveryRollupItemLabel(rollup, resources)).toBe('db-01');
expect(getRecoveryPointItemLabel(point, resources)).toBe('db-01');
});
it('falls back to refs, repository labels, and details summary', () => {
const resources = new Map<string, Resource>();
const point = {
id: 'point-2',
subjectRef: { namespace: 'prod', name: 'api' },
repositoryRef: { class: 'pbs', type: 'datastore' },
display: { detailsSummary: 'Immutable and encrypted' },
} as RecoveryPoint;
expect(getRecoveryPointItemLabel(point, resources)).toBe('prod/api');
expect(getRecoveryPointRepositoryLabel(point)).toBe('pbs:datastore');
expect(getRecoveryPointDetailsSummary(point)).toBe('Immutable and encrypted');
});
it('prefers governed display labels over opaque linked-resource ids', () => {
const resources = new Map<string, Resource>([
[
'res-2',
{
id: 'res-2',
name: 'res-2',
displayName: 'Payments API',
type: 'vm',
} as Resource,
],
[
'res-3',
{
id: 'res-3',
name: 'res-3',
type: 'vm',
} as Resource,
],
]);
const rollup = {
rollupId: 'res:res-3',
itemResourceId: 'res-3',
display: { itemLabel: 'Archive VM' },
} as ProtectionRollup;
const linkedPoint = {
id: 'point-3',
itemResourceId: 'res-2',
display: { itemLabel: 'billing-api' },
} as RecoveryPoint;
expect(getRecoveryRollupItemLabel(rollup, resources)).toBe('Archive VM');
expect(getRecoveryPointItemLabel(linkedPoint, resources)).toBe('Payments API');
});
it('derives secondary entity-id labels without replacing the primary name', () => {
const rollup = {
rollupId: 'rollup-2',
display: { itemLabel: 'Archive VM', itemType: 'vm', entityIdLabel: '101' },
} as ProtectionRollup;
const point = {
id: 'point-4',
display: { itemLabel: 'debian-go', itemType: 'system-container', entityIdLabel: '112' },
} as RecoveryPoint;
expect(getRecoveryRollupItemSecondaryLabel(rollup)).toBe('VMID 101');
expect(getRecoveryPointItemSecondaryLabel(point)).toBe('CTID 112');
});
it('derives timestamps and normalizes mode query values', () => {
const point = {
completedAt: '2026-03-09T12:00:00.000Z',
} as RecoveryPoint;
expect(getRecoveryPointTimestampMs(point)).toBeGreaterThan(0);
expect(normalizeRecoveryModeQueryValue('snapshot')).toBe('snapshot');
expect(normalizeRecoveryModeQueryValue('LOCAL')).toBe('local');
expect(normalizeRecoveryModeQueryValue('unknown')).toBe('all');
});
it('humanizes point kind, method, and outcome labels for operator-facing recovery details', () => {
expect(getRecoveryPointKindLabel('backup')).toBe('Backup');
expect(getRecoveryPointKindLabel('replica_sync')).toBe('Replica Sync');
expect(getRecoveryPointModeLabel('local')).toBe('Local Copy');
expect(getRecoveryPointModeLabel('zfs_send')).toBe('Zfs Send');
expect(getRecoveryPointOutcomeLabel('success')).toBe('Success');
expect(getRecoveryPointOutcomeLabel('FAILURE')).toBe('Failed');
expect(getRecoveryPointOutcomeLabel('')).toBe('Unknown');
});
});
@@ -1,42 +0,0 @@
import { describe, expect, it } from 'vitest';
import {
getRecoveryProtectedToggleClass,
getRecoveryRollupInventoryStatusLabel,
getRecoveryRollupInventoryStatusTextClass,
getRecoveryRollupInventoryStatusVariant,
getRecoveryRollupStatusPillClass,
getRecoveryRollupStatusPillLabel,
getRecoverySpecialOutcomeTextClass,
} from '@/utils/recoveryStatusPresentation';
describe('recoveryStatusPresentation', () => {
it('derives stale toggle classes', () => {
expect(getRecoveryProtectedToggleClass(true)).toContain('border-amber-300');
expect(getRecoveryProtectedToggleClass(false)).toContain('border-border');
});
it('derives rollup status pill labels and classes', () => {
expect(getRecoveryRollupStatusPillLabel('recent')).toBe('recent');
expect(getRecoveryRollupStatusPillClass('recent')).toContain('bg-blue-100/80');
expect(getRecoveryRollupStatusPillLabel('stale')).toBe('stale');
expect(getRecoveryRollupStatusPillClass('stale')).toContain('text-amber-700');
expect(getRecoveryRollupStatusPillLabel('never-succeeded')).toBe('never succeeded');
expect(getRecoveryRollupStatusPillClass('never-succeeded')).toContain('text-rose-700');
});
it('derives inventory status labels, tones, and dot variants', () => {
expect(getRecoveryRollupInventoryStatusLabel('healthy')).toBe('Healthy');
expect(getRecoveryRollupInventoryStatusLabel('stale')).toBe('Stale');
expect(getRecoveryRollupInventoryStatusLabel('never-succeeded')).toBe('Never succeeded');
expect(getRecoveryRollupInventoryStatusTextClass('stale')).toContain('text-amber-600');
expect(getRecoveryRollupInventoryStatusTextClass('failed')).toContain('text-red-600');
expect(getRecoveryRollupInventoryStatusTextClass('running')).toContain('text-blue-600');
expect(getRecoveryRollupInventoryStatusVariant('healthy')).toBe('success');
expect(getRecoveryRollupInventoryStatusVariant('stale')).toBe('warning');
expect(getRecoveryRollupInventoryStatusVariant('never-succeeded')).toBe('danger');
});
it('derives special outcome text classes', () => {
expect(getRecoverySpecialOutcomeTextClass('never')).toContain('text-amber-600');
});
});
@@ -1,13 +0,0 @@
import { describe, expect, it } from 'vitest';
import { formatThroughputRate } from '@/utils/throughputPresentation';
describe('throughputPresentation', () => {
it('formats bytes per second with canonical throughput labels', () => {
expect(formatThroughputRate(0)).toBe('0 B/s');
expect(formatThroughputRate(999)).toBe('999 B/s');
expect(formatThroughputRate(1_000)).toBe('1 KB/s');
expect(formatThroughputRate(1_500_000)).toBe('1.5 MB/s');
expect(formatThroughputRate(2_000_000_000)).toBe('2.0 GB/s');
});
});
@@ -1,18 +0,0 @@
import type { ResourceApprovalLevel } from '@/types/resource';
import { humanizeToken } from '@/utils/textPresentation';
const APPROVAL_LEVEL_LABELS: Record<ResourceApprovalLevel, string> = {
none: 'None',
dry_run_only: 'Dry Run Only',
admin: 'Admin',
mfa: 'MFA',
};
export function getResourceApprovalLevelLabel(level?: ResourceApprovalLevel | string): string {
if (!level) {
return '—';
}
const normalized = level.trim() as ResourceApprovalLevel;
return APPROVAL_LEVEL_LABELS[normalized] ?? humanizeToken(normalized, { fallback: '—' });
}
@@ -1,14 +0,0 @@
const CHART_SERIES_COLORS = [
'#3b82f6',
'#8b5cf6',
'#10b981',
'#f97316',
'#ec4899',
'#06b6d4',
'#f59e0b',
'#ef4444',
];
export function getChartSeriesColor(index: number): string {
return CHART_SERIES_COLORS[index % CHART_SERIES_COLORS.length];
}
@@ -1,60 +0,0 @@
import type { Resource } from '@/types/resource';
import { getPreferredResourceDisplayName } from '@/utils/resourceIdentity';
import { getResourceTypeLabel } from '@/utils/resourceTypePresentation';
import type { StatusIndicatorVariant } from '@/utils/status';
function normalizeProblemResourceValue(value?: string | null): string {
return (value ?? '').trim().toLowerCase().replace(/[-_]+/g, ' ');
}
function getProblemResourceTypeNoun(resource: Resource): string | null {
const typeLabel = getResourceTypeLabel(resource.type)?.trim();
if (!typeLabel) return null;
return typeLabel === typeLabel.toUpperCase() ? typeLabel : typeLabel.toLowerCase();
}
function getGenericProblemResourceNames(resource: Resource, problems: string[]): Set<string> {
const typeLabel = normalizeProblemResourceValue(getResourceTypeLabel(resource.type));
const rawType = normalizeProblemResourceValue(resource.type);
const normalizedProblems = problems.map(normalizeProblemResourceValue).filter(Boolean);
return new Set(
[typeLabel, rawType]
.filter(Boolean)
.flatMap((value) => [
value,
...normalizedProblems.map((problem) => `${value} (${problem})`),
]),
);
}
export function getProblemResourceStatusVariant(worstValue: number): StatusIndicatorVariant {
return worstValue >= 150 ? 'danger' : 'warning';
}
export function getProblemResourceDisplayName(resource: Resource): string {
return getPreferredResourceDisplayName(resource) || resource.id;
}
export function isGenericProblemResourceDisplayName(
resource: Resource,
problems: string[],
displayName = getProblemResourceDisplayName(resource),
): boolean {
return getGenericProblemResourceNames(resource, problems).has(
normalizeProblemResourceValue(displayName),
);
}
export function getProblemResourceIssueLabel(resource: Resource, problems: string[]): string {
const displayName = getProblemResourceDisplayName(resource);
const typeNoun = getProblemResourceTypeNoun(resource);
return typeNoun && isGenericProblemResourceDisplayName(resource, problems, displayName)
? `the ${typeNoun} issue`
: displayName;
}
export function getProblemResourceMemberLabel(resource: Resource, problems: string[]): string | null {
const displayName = getProblemResourceDisplayName(resource);
return isGenericProblemResourceDisplayName(resource, problems, displayName) ? null : displayName;
}
@@ -1,113 +0,0 @@
import Server from 'lucide-solid/icons/server';
import HardDrive from 'lucide-solid/icons/hard-drive';
import Mail from 'lucide-solid/icons/mail';
import type { Component } from 'solid-js';
import type { NodeConfig } from '@/types/nodes';
export type ProxmoxNodeType = 'pve' | 'pbs' | 'pmg';
export interface DiscoveredProxmoxServer {
type: ProxmoxNodeType;
ip: string;
port: number;
hostname?: string;
}
export interface ProxmoxVariantPresentation {
title: string;
addLabel: string;
emptyTitle: string;
emptyDescription: string;
scanningLabel: string;
emptyIcon: Component<{ class?: string }>;
nameFromServer: (server: DiscoveredProxmoxServer) => string;
titleFromServer: (server: DiscoveredProxmoxServer) => string;
}
export const PROXMOX_VARIANT_PRESENTATION: Record<ProxmoxNodeType, ProxmoxVariantPresentation> = {
pve: {
title: 'Proxmox VE nodes',
addLabel: 'Add Proxmox VE Connection',
emptyTitle: 'No Proxmox VE connections configured',
emptyDescription:
'Add a Proxmox VE connection when the unified agent is not available on the host',
scanningLabel: 'Scanning your network for Proxmox VE servers…',
emptyIcon: Server,
nameFromServer: (server) => server.hostname || `pve-${server.ip}`,
titleFromServer: (server) => server.hostname || `Proxmox VE at ${server.ip}`,
},
pbs: {
title: 'Proxmox Backup Server nodes',
addLabel: 'Add Backup Server Connection',
emptyTitle: 'No Proxmox Backup Server connections configured',
emptyDescription:
'Add a Proxmox Backup Server connection when the unified agent is not available on the host',
scanningLabel: 'Scanning your network for Proxmox Backup Servers…',
emptyIcon: HardDrive,
nameFromServer: (server) => server.hostname || `pbs-${server.ip}`,
titleFromServer: (server) => server.hostname || `Backup Server at ${server.ip}`,
},
pmg: {
title: 'Proxmox Mail Gateway nodes',
addLabel: 'Add Mail Gateway Connection',
emptyTitle: 'No Proxmox Mail Gateway connections configured',
emptyDescription:
'Add a Proxmox Mail Gateway connection when the unified agent is not available on the host',
scanningLabel: 'Scanning your network for Proxmox Mail Gateway servers…',
emptyIcon: Mail,
nameFromServer: (server) => server.hostname || `pmg-${server.ip}`,
titleFromServer: (server) => server.hostname || `Mail Gateway at ${server.ip}`,
},
};
export function getProxmoxVariantPresentation(type: ProxmoxNodeType): ProxmoxVariantPresentation {
return PROXMOX_VARIANT_PRESENTATION[type];
}
export function buildProxmoxDiscoveryPrefillNode(
server: DiscoveredProxmoxServer,
): Partial<NodeConfig> {
const variant = getProxmoxVariantPresentation(server.type);
const baseNode = {
type: server.type,
name: variant.nameFromServer(server),
host: `https://${server.ip}:${server.port}`,
verifySSL: false,
} as const;
switch (server.type) {
case 'pve':
return {
...baseNode,
user: '',
tokenName: '',
tokenValue: '',
monitorVMs: true,
monitorContainers: true,
monitorStorage: true,
monitorBackups: true,
monitorPhysicalDisks: false,
};
case 'pbs':
return {
...baseNode,
user: '',
tokenName: '',
tokenValue: '',
monitorDatastores: true,
monitorSyncJobs: true,
monitorVerifyJobs: true,
monitorPruneJobs: true,
monitorGarbageJobs: true,
};
case 'pmg':
return {
...baseNode,
user: '',
monitorMailStats: true,
monitorQueues: true,
monitorQuarantine: true,
monitorDomainStats: false,
};
}
}
@@ -1,15 +0,0 @@
export function getRecoveryBreadcrumbLinkClass(): string {
return 'text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 transition-colors';
}
export function getRecoveryFilterPanelClearClass(): string {
return 'text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300';
}
export function getRecoveryEmptyStateActionClass(): string {
return 'inline-flex items-center gap-2 rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content hover:bg-surface-hover';
}
export function getRecoveryDrawerCloseButtonClass(): string {
return 'rounded-md p-1 hover:text-base-content hover:bg-surface-hover';
}
@@ -1,49 +0,0 @@
export function getRecoveryProtectedItemsEmptyState() {
return {
title: 'No protection coverage yet',
description: 'Pulse has not observed recovery coverage for this org yet.',
} as const;
}
export function getRecoveryProtectedItemsLoadingState() {
return {
text: 'Loading protection coverage...',
} as const;
}
export function getRecoveryProtectedItemsFailureState() {
return {
title: 'Failed to load protection coverage',
} as const;
}
export function getRecoveryActivityLoadingState() {
return {
text: 'Loading recovery activity...',
} as const;
}
export function getRecoveryActivityEmptyState() {
return {
text: 'No recovery activity in the selected window.',
} as const;
}
export function getRecoveryHistoryEmptyState() {
return {
title: 'No recovery history matches your filters',
description: 'Adjust your search, platform, method, status, or verification filters.',
} as const;
}
export function getRecoveryPointsLoadingState() {
return {
text: 'Loading recovery points...',
} as const;
}
export function getRecoveryPointsFailureState() {
return {
title: 'Failed to load recovery points',
} as const;
}
@@ -1,57 +0,0 @@
import { getRecoveryLocationFacetLabel } from '@/utils/recoveryLocationPresentation';
export type RecoveryFilterChipKind =
| 'day'
| 'cluster'
| 'item-type'
| 'node'
| 'namespace'
| 'focused-item';
type RecoveryFilterChipPresentation = {
clearButtonClass: string;
className: string;
label: string;
};
const CHIP_BASE_CLASS = 'inline-flex max-w-full items-center gap-1 rounded border px-2 py-0.5 text-[10px]';
const CLEAR_BUTTON_BASE_CLASS = 'rounded px-1 py-0.5 text-[10px]';
const CHIP_PRESENTATION: Record<RecoveryFilterChipKind, RecoveryFilterChipPresentation> = {
cluster: {
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-cyan-100 dark:hover:bg-cyan-900`,
className: `${CHIP_BASE_CLASS} border-cyan-200 bg-cyan-50 text-cyan-700 dark:border-cyan-700 dark:bg-cyan-900 dark:text-cyan-200`,
label: getRecoveryLocationFacetLabel('cluster'),
},
day: {
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-blue-100 dark:hover:bg-blue-900`,
className: `${CHIP_BASE_CLASS} border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-200`,
label: 'Day',
},
'focused-item': {
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-amber-100 dark:hover:bg-amber-900`,
className: `${CHIP_BASE_CLASS} border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-700 dark:bg-amber-900 dark:text-amber-200`,
label: 'Focused Item',
},
'item-type': {
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-rose-100 dark:hover:bg-rose-900`,
className: `${CHIP_BASE_CLASS} border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-700 dark:bg-rose-900 dark:text-rose-200`,
label: 'Item Type',
},
namespace: {
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-indigo-100 dark:hover:bg-indigo-900`,
className: `${CHIP_BASE_CLASS} border-indigo-200 bg-indigo-50 text-indigo-700 dark:border-indigo-700 dark:bg-indigo-900 dark:text-indigo-200`,
label: getRecoveryLocationFacetLabel('namespace'),
},
node: {
clearButtonClass: `${CLEAR_BUTTON_BASE_CLASS} hover:bg-emerald-100 dark:hover:bg-emerald-900`,
className: `${CHIP_BASE_CLASS} border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-700 dark:bg-emerald-900 dark:text-emerald-200`,
label: getRecoveryLocationFacetLabel('node'),
},
};
export function getRecoveryFilterChipPresentation(
kind: RecoveryFilterChipKind,
): RecoveryFilterChipPresentation {
return CHIP_PRESENTATION[kind];
}
@@ -1,184 +0,0 @@
import type { ProtectionRollup, RecoveryPoint } from '@/types/recovery';
import type { Resource } from '@/types/resource';
import { getRecoveryArtifactModePresentation } from '@/utils/recoveryArtifactModePresentation';
import { normalizeRecoveryOutcome } from '@/utils/recoveryOutcomePresentation';
import { getPreferredInfrastructureDisplayName } from '@/utils/resourceIdentity';
import { titleCaseDelimitedLabel } from '@/utils/textPresentation';
export type RecoveryArtifactMode = 'snapshot' | 'local' | 'remote';
const getRecoveryLinkedResourceLabel = (
itemResourceId: string,
resourcesById: Map<string, Resource>,
): string => {
if (!itemResourceId) return '';
const resource = resourcesById.get(itemResourceId);
if (!resource) return '';
const label = getPreferredInfrastructureDisplayName(resource).trim();
if (!label) return '';
if (label.toLowerCase() === itemResourceId.toLowerCase()) return '';
return label;
};
export function getRecoveryRollupItemLabel(
rollup: ProtectionRollup,
resourcesById: Map<string, Resource>,
): string {
const itemResourceId = (rollup.itemResourceId || '').trim();
const displayLabel = String(rollup.display?.itemLabel || '').trim();
const linkedResourceLabel = getRecoveryLinkedResourceLabel(itemResourceId, resourcesById);
if (linkedResourceLabel) return linkedResourceLabel;
if (displayLabel) return displayLabel;
const ref = rollup.itemRef || rollup.subjectRef || null;
if (ref?.namespace && ref?.name) return `${ref.namespace}/${ref.name}`;
if (ref?.name) return ref.name;
if (itemResourceId) return itemResourceId;
return rollup.rollupId;
}
const getRecoveryEntityIdPrefix = (value: string | null | undefined): string => {
const normalized = String(value || '').trim().toLowerCase();
if (!normalized) return 'ID';
if (normalized.includes('vm')) return 'VMID';
if (normalized.includes('lxc') || normalized.includes('container')) return 'CTID';
return 'ID';
};
const getRecoveryItemSecondaryLabel = (
itemLabel: string,
itemType: string | null | undefined,
entityIdLabel: string | null | undefined,
): string => {
const raw = String(entityIdLabel || '').trim();
if (!raw) return '';
if (raw.toLowerCase() === itemLabel.trim().toLowerCase()) return '';
if (/^\d+$/.test(raw)) {
return `${getRecoveryEntityIdPrefix(itemType)} ${raw}`;
}
return raw;
};
export function getRecoveryRollupItemSecondaryLabel(rollup: ProtectionRollup): string {
return getRecoveryItemSecondaryLabel(
String(rollup.display?.itemLabel || '').trim(),
String(rollup.display?.itemType || rollup.itemRef?.type || rollup.subjectRef?.type || ''),
rollup.display?.entityIdLabel,
);
}
export function getRecoveryPointTimestampMs(point: RecoveryPoint): number {
const raw = String(point.completedAt || point.startedAt || '');
const parsed = Date.parse(raw);
return Number.isFinite(parsed) ? parsed : 0;
}
export function getRecoveryPointItemLabel(
point: RecoveryPoint,
resourcesById: Map<string, Resource>,
): string {
const itemResourceId = (point.itemResourceId || '').trim();
const displayLabel = String(point.display?.itemLabel || '').trim();
const linkedResourceLabel = getRecoveryLinkedResourceLabel(itemResourceId, resourcesById);
if (linkedResourceLabel) return linkedResourceLabel;
if (displayLabel) return displayLabel;
const ref = point.itemRef || point.subjectRef || null;
const namespace = String(ref?.namespace || '').trim();
const name = String(ref?.name || '').trim();
if (namespace && name) return `${namespace}/${name}`;
if (name) return name;
const id = String(ref?.id || '').trim();
if (id) return id;
if (itemResourceId) return itemResourceId;
return point.id;
}
export function getRecoveryPointItemSecondaryLabel(point: RecoveryPoint): string {
return getRecoveryItemSecondaryLabel(
String(point.display?.itemLabel || '').trim(),
String(point.display?.itemType || point.itemRef?.type || point.subjectRef?.type || ''),
point.display?.entityIdLabel,
);
}
export function getRecoveryRollupSubjectLabel(
rollup: ProtectionRollup,
resourcesById: Map<string, Resource>,
): string {
return getRecoveryRollupItemLabel(rollup, resourcesById);
}
export function getRecoveryPointSubjectLabel(
point: RecoveryPoint,
resourcesById: Map<string, Resource>,
): string {
return getRecoveryPointItemLabel(point, resourcesById);
}
export function getRecoveryPointRepositoryLabel(point: RecoveryPoint): string {
const displayLabel = String(point.display?.repositoryLabel || '').trim();
if (displayLabel) return displayLabel;
const repository = point.repositoryRef || null;
const repositoryName = String(repository?.name || '').trim();
const repositoryType = String(repository?.type || '').trim();
const repositoryClass = String(repository?.class || '').trim();
if (repositoryName) return repositoryName;
if (repositoryType && repositoryClass) return `${repositoryClass}:${repositoryType}`;
if (repositoryType) return repositoryType;
if (repositoryClass) return repositoryClass;
return '';
}
export function getRecoveryPointDetailsSummary(point: RecoveryPoint): string {
return String(point.display?.detailsSummary || '').trim();
}
export function getRecoveryPointKindLabel(value: string | null | undefined): string {
const normalized = (value || '').trim().toLowerCase();
if (!normalized) return 'n/a';
switch (normalized) {
case 'backup':
return 'Backup';
case 'snapshot':
return 'Snapshot';
case 'other':
return 'Other';
default:
return titleCaseDelimitedLabel(value ?? undefined, {
fallback: 'n/a',
preserveShortAllCaps: true,
});
}
}
export function getRecoveryPointModeLabel(value: string | null | undefined): string {
const normalized = normalizeRecoveryModeQueryValue(value);
if (normalized !== 'all') {
return getRecoveryArtifactModePresentation(normalized).label;
}
return titleCaseDelimitedLabel(value ?? undefined, {
fallback: 'n/a',
preserveShortAllCaps: true,
});
}
export function getRecoveryPointOutcomeLabel(value: string | null | undefined): string {
return titleCaseDelimitedLabel(normalizeRecoveryOutcome(value), {
fallback: 'Unknown',
preserveShortAllCaps: true,
});
}
export function normalizeRecoveryModeQueryValue(
value: string | null | undefined,
): 'all' | RecoveryArtifactMode {
const normalized = (value || '').trim().toLowerCase();
if (normalized === 'snapshot' || normalized === 'local' || normalized === 'remote') {
return normalized;
}
return 'all';
}
@@ -1,93 +0,0 @@
import type { RecoveryRollupInventoryStatus } from '@/utils/recoveryTablePresentation';
export type RecoveryRollupStatusPill = 'stale' | 'never-succeeded' | 'recent';
export function getRecoveryProtectedToggleClass(active: boolean): string {
return active
? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700 dark:bg-amber-900 dark:text-amber-100'
: 'border-border bg-surface text-base-content hover:bg-surface-hover';
}
export function getRecoveryRollupStatusPillClass(kind: RecoveryRollupStatusPill): string {
switch (kind) {
case 'recent':
return 'rounded-full bg-blue-100/80 px-1.5 py-px text-[9px] font-medium text-blue-700 dark:bg-blue-900/70 dark:text-blue-200';
case 'stale':
return 'whitespace-nowrap rounded px-1 py-0.5 text-[10px] font-medium text-amber-700 bg-amber-50 dark:text-amber-300 dark:bg-amber-900';
case 'never-succeeded':
return 'whitespace-nowrap rounded px-1 py-0.5 text-[10px] font-medium text-rose-700 bg-rose-100 dark:text-rose-200 dark:bg-rose-900';
}
}
export function getRecoveryRollupStatusPillLabel(kind: RecoveryRollupStatusPill): string {
switch (kind) {
case 'recent':
return 'recent';
case 'stale':
return 'stale';
case 'never-succeeded':
return 'never succeeded';
}
}
export function getRecoveryRollupInventoryStatusLabel(
status: RecoveryRollupInventoryStatus,
): string {
switch (status) {
case 'healthy':
return 'Healthy';
case 'warning':
return 'Warning';
case 'failed':
return 'Failed';
case 'running':
return 'Running';
case 'stale':
return 'Stale';
case 'never-succeeded':
return 'Never succeeded';
default:
return 'Unknown';
}
}
export function getRecoveryRollupInventoryStatusTextClass(
status: RecoveryRollupInventoryStatus,
): string {
switch (status) {
case 'healthy':
return 'text-emerald-600 dark:text-emerald-400';
case 'warning':
case 'stale':
return 'text-amber-600 dark:text-amber-400';
case 'failed':
case 'never-succeeded':
return 'text-red-600 dark:text-red-400';
case 'running':
return 'text-blue-600 dark:text-blue-400';
default:
return 'text-muted';
}
}
export function getRecoveryRollupInventoryStatusVariant(
status: RecoveryRollupInventoryStatus,
): 'success' | 'warning' | 'danger' | 'muted' {
switch (status) {
case 'healthy':
return 'success';
case 'warning':
case 'stale':
return 'warning';
case 'failed':
case 'never-succeeded':
return 'danger';
default:
return 'muted';
}
}
export function getRecoverySpecialOutcomeTextClass(kind: 'never'): string {
if (kind === 'never') return 'text-amber-600 dark:text-amber-400';
return 'text-muted';
}
@@ -1,19 +0,0 @@
const RATE_UNITS = [
{ threshold: 1e9, suffix: 'GB/s', precision: 1 },
{ threshold: 1e6, suffix: 'MB/s', precision: 1 },
{ threshold: 1e3, suffix: 'KB/s', precision: 0 },
] as const;
export function formatThroughputRate(bytesPerSecond: number): string {
if (!Number.isFinite(bytesPerSecond) || bytesPerSecond < 0) {
return '0 B/s';
}
for (const unit of RATE_UNITS) {
if (bytesPerSecond >= unit.threshold) {
return `${(bytesPerSecond / unit.threshold).toFixed(unit.precision)} ${unit.suffix}`;
}
}
return `${Math.round(bytesPerSecond)} B/s`;
}