diff --git a/frontend-modern/public/docs/TRUENAS.md b/frontend-modern/public/docs/TRUENAS.md index 3ef257642..7eb4400b3 100644 --- a/frontend-modern/public/docs/TRUENAS.md +++ b/frontend-modern/public/docs/TRUENAS.md @@ -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 diff --git a/frontend-modern/scripts/sync-public-docs.mjs b/frontend-modern/scripts/sync-public-docs.mjs index 361dac3f5..5eb107d16 100644 --- a/frontend-modern/scripts/sync-public-docs.mjs +++ b/frontend-modern/scripts/sync-public-docs.mjs @@ -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' }, ]; diff --git a/frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx b/frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx index 36976e79b..00af74fec 100644 --- a/frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx +++ b/frontend-modern/src/features/alerts/AlertResourceIncidentsPanel.tsx @@ -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
-

- {getAlertResourceIncidentPanelTitle()} -

+ +

+ {getAlertResourceIncidentPanelTitle()} +

+

{resourceDisplayName()} 0}> diff --git a/frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx b/frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx index 2f7656067..2847fc236 100644 --- a/frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx +++ b/frontend-modern/src/features/alerts/MobileAlertHistoryInvestigationDialog.tsx @@ -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} /> diff --git a/frontend-modern/src/features/alerts/__tests__/AlertHistoryMobileList.test.tsx b/frontend-modern/src/features/alerts/__tests__/AlertHistoryMobileList.test.tsx index 6059aceef..ab1b171d1 100644 --- a/frontend-modern/src/features/alerts/__tests__/AlertHistoryMobileList.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/AlertHistoryMobileList.test.tsx @@ -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 () => { diff --git a/frontend-modern/src/utils/__tests__/alertIncidentPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertIncidentPresentation.test.ts index fdb8ba6fd..0d06a9572 100644 --- a/frontend-modern/src/utils/__tests__/alertIncidentPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertIncidentPresentation.test.ts @@ -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...', }); diff --git a/frontend-modern/src/utils/alertIncidentPresentation.ts b/frontend-modern/src/utils/alertIncidentPresentation.ts index b37a78feb..2ad6cd045 100644 --- a/frontend-modern/src/utils/alertIncidentPresentation.ts +++ b/frontend-modern/src/utils/alertIncidentPresentation.ts @@ -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'}`; }