Restore mobile alert history validation

This commit is contained in:
pulse-triage[bot]
2026-08-28 09:37:20 +01:00
parent 63acead96d
commit 11cbc303d6
7 changed files with 32 additions and 5 deletions
+7 -1
View File
@@ -31,7 +31,7 @@ On your TrueNAS system:
| Apps | TrueNAS Overview | Native app state, image/version, ports, volumes, networks, and runtime container details |
| ZFS Pools | Storage | Total/used/free capacity, pool status (ONLINE/DEGRADED/FAULTED) |
| ZFS Datasets | Storage | Used/available space, mount status, read-only flag |
| Physical Disks | Storage | Model, serial, size, transport type, rotational flag |
| Physical Disks | Storage | Model, serial, size, transport type, rotational flag, temperature, and native SMART failure evidence |
| ZFS Snapshots | Recovery | Dataset, creation time, size, referenced data |
| Replication Tasks | Recovery | Source/target datasets, direction, last run status |
| TrueNAS Alerts | Alerts | Native TrueNAS alert messages and severity levels |
@@ -47,6 +47,12 @@ TrueNAS resources are mapped into the unified resource model:
- **ZFS snapshots and replication** → appear on the **Recovery** page as recovery points.
- **TrueNAS alerts** → surfaced on the **Alerts** page alongside Proxmox and other platform alerts.
TrueNAS drive-health alerts that identify a specific disk remain disk-health
evidence after they are dismissed in TrueNAS. Dismissal acknowledges the
notification; Pulse continues to show the affected disk risk while TrueNAS
continues to report the underlying SMART condition. Other dismissed TrueNAS
alerts remain suppressed.
Resources from TrueNAS can be filtered using the **source** filter on any page.
## Multiple TrueNAS Systems
@@ -14,6 +14,7 @@ const shippedDocs = [
{ source: path.join(repoRoot, 'docs', 'PRIVACY.md'), target: 'PRIVACY.md' },
{ source: path.join(repoRoot, 'docs', 'CONFIGURATION.md'), target: 'CONFIGURATION.md' },
{ source: path.join(repoRoot, 'docs', 'PROXY_AUTH.md'), target: 'PROXY_AUTH.md' },
{ source: path.join(repoRoot, 'docs', 'TRUENAS.md'), target: 'TRUENAS.md' },
{ source: path.join(repoRoot, 'SECURITY.md'), target: 'SECURITY.md' },
{ source: path.join(repoRoot, 'TERMS.md'), target: 'TERMS.md' },
];
@@ -36,6 +36,7 @@ interface AlertResourceIncidentsPanelProps {
getResource?: (resourceId: string) => Resource | undefined;
onClose?: () => void;
showCloseAction?: boolean;
showTitle?: boolean;
}
export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelProps) {
@@ -59,9 +60,11 @@ export function AlertResourceIncidentsPanel(props: AlertResourceIncidentsPanelPr
<Card padding="md">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="text-sm font-semibold text-base-content">
{getAlertResourceIncidentPanelTitle()}
</h3>
<Show when={props.showTitle ?? true}>
<h3 class="text-sm font-semibold text-base-content">
{getAlertResourceIncidentPanelTitle()}
</h3>
</Show>
<p class="text-xs text-muted">
{resourceDisplayName()}
<Show when={incidents().length > 0}>
@@ -4,6 +4,10 @@ import { Match, Switch } from 'solid-js';
import { IncidentTimelinePanel } from '@/components/Alerts/IncidentTimelinePanel';
import { ActionIconButton } from '@/components/shared/Button';
import { Dialog } from '@/components/shared/Dialog';
import {
getAlertIncidentTimelineDialogTitle,
getAlertResourceIncidentPanelTitle,
} from '@/utils/alertIncidentPresentation';
import { AlertResourceIncidentsPanel } from './AlertResourceIncidentsPanel';
import type { AlertHistoryState } from './useAlertHistoryState';
@@ -26,7 +30,9 @@ export function MobileAlertHistoryInvestigationDialog(
props: MobileAlertHistoryInvestigationDialogProps,
) {
const dialogTitle = () =>
props.investigation.kind === 'timeline' ? 'Incident timeline' : 'Resource incidents';
props.investigation.kind === 'timeline'
? getAlertIncidentTimelineDialogTitle()
: getAlertResourceIncidentPanelTitle();
const dialogLabel = () => `${dialogTitle()} for ${props.investigation.alert.resourceName}`;
const rowKey = () => props.investigation.rowKey;
const alert = () => props.investigation.alert;
@@ -89,6 +95,7 @@ export function MobileAlertHistoryInvestigationDialog(
state={props.state}
onClose={props.onClose}
showCloseAction={false}
showTitle={false}
/>
</Match>
</Switch>
@@ -130,6 +130,7 @@ describe('AlertHistoryMobileList', () => {
expect(
screen.getByRole('dialog', { name: 'Resource incidents for pve-production-01' }),
).toBeInTheDocument();
expect(screen.getAllByRole('heading', { name: 'Resource incidents' })).toHaveLength(1);
});
it('keeps resource investigation scrolling outside the virtualized history row', async () => {
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
ALERT_INCIDENT_TIMELINE_DIALOG_TITLE,
ALERT_RESOURCE_INCIDENT_EMPTY_STATE,
ALERT_RESOURCE_INCIDENT_LOADING_STATE,
ALERT_RESOURCE_INCIDENT_NOTE_PLACEHOLDER,
@@ -11,6 +12,7 @@ import {
ALERT_RESOURCE_INCIDENT_SAVING_NOTE_LABEL,
getAlertHistoryStatusPresentation,
getAlertIncidentLevelBadgeClass,
getAlertIncidentTimelineDialogTitle,
getAlertResourceIncidentAcknowledgedByLabel,
getAlertResourceIncidentCountLabel,
getAlertResourceIncidentEmptyState,
@@ -102,6 +104,7 @@ describe('alertIncidentPresentation', () => {
});
it('returns canonical resource incident loading and empty-state copy', () => {
expect(ALERT_INCIDENT_TIMELINE_DIALOG_TITLE).toBe('Incident timeline');
expect(ALERT_RESOURCE_INCIDENT_PANEL_TITLE).toBe('Resource incidents');
expect(ALERT_RESOURCE_INCIDENT_LOADING_STATE).toBe('Loading incidents...');
expect(ALERT_RESOURCE_INCIDENT_EMPTY_STATE).toBe(
@@ -116,6 +119,7 @@ describe('alertIncidentPresentation', () => {
expect(ALERT_RESOURCE_INCIDENT_SAVE_NOTE_LABEL).toBe('Save Note');
expect(ALERT_RESOURCE_INCIDENT_SAVING_NOTE_LABEL).toBe('Saving...');
expect(getAlertResourceIncidentPanelTitle()).toBe('Resource incidents');
expect(getAlertIncidentTimelineDialogTitle()).toBe('Incident timeline');
expect(getAlertResourceIncidentLoadingState()).toEqual({
text: 'Loading incidents...',
});
@@ -15,6 +15,7 @@ export interface AlertHistoryStatusPresentation {
}
export const ALERT_RESOURCE_INCIDENT_PANEL_TITLE = 'Resource incidents';
export const ALERT_INCIDENT_TIMELINE_DIALOG_TITLE = 'Incident timeline';
export const ALERT_RESOURCE_INCIDENT_LOADING_STATE = 'Loading incidents...';
export const ALERT_RESOURCE_INCIDENT_EMPTY_STATE = 'No incidents recorded for this resource yet.';
export const ALERT_RESOURCE_INCIDENT_REFRESH_LABEL = 'Refresh';
@@ -121,6 +122,10 @@ export function getAlertResourceIncidentPanelTitle() {
return ALERT_RESOURCE_INCIDENT_PANEL_TITLE;
}
export function getAlertIncidentTimelineDialogTitle() {
return ALERT_INCIDENT_TIMELINE_DIALOG_TITLE;
}
export function getAlertResourceIncidentCountLabel(count: number) {
return `${count} incident${count === 1 ? '' : 's'}`;
}