Fix type mismatches across Unraid UI pages (SystemInfo, ServerVars, Notification properties), replace unavailable Mantine components (ScrollArea.Autosize, IconHardDrive), correct Orchis theme types, add missing tRPC endpoints (users, syslog, notification actions), and configure docker-compose for Traefik reverse proxy on dockerproxy network with unmarr.xtrm-lab.org routing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
384 lines
11 KiB
TypeScript
384 lines
11 KiB
TypeScript
/**
|
|
* Diagnostics Page
|
|
* Generate and download Unraid diagnostic reports
|
|
*/
|
|
|
|
import { useState } from 'react';
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Card,
|
|
Container,
|
|
Divider,
|
|
Group,
|
|
List,
|
|
Loader,
|
|
Paper,
|
|
Progress,
|
|
Stack,
|
|
Text,
|
|
ThemeIcon,
|
|
Title,
|
|
} from '@mantine/core';
|
|
import { notifications } from '@mantine/notifications';
|
|
import {
|
|
IconBug,
|
|
IconAlertCircle,
|
|
IconCheck,
|
|
IconDownload,
|
|
IconRefresh,
|
|
IconCpu,
|
|
IconDatabase,
|
|
IconServer,
|
|
IconNetwork,
|
|
IconBrandDocker,
|
|
} from '@tabler/icons-react';
|
|
import { GetServerSidePropsContext } from 'next';
|
|
|
|
import { UnraidLayout } from '~/components/Unraid/Layout';
|
|
import { getServerAuthSession } from '~/server/auth';
|
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
|
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
|
import { api } from '~/utils/api';
|
|
|
|
interface DiagnosticCheck {
|
|
name: string;
|
|
icon: React.ElementType;
|
|
status: 'pending' | 'running' | 'success' | 'warning' | 'error';
|
|
message?: string;
|
|
}
|
|
|
|
export default function DiagnosticsPage() {
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
const [progress, setProgress] = useState(0);
|
|
const [checks, setChecks] = useState<DiagnosticCheck[]>([
|
|
{ name: 'System Information', icon: IconServer, status: 'pending' },
|
|
{ name: 'CPU & Memory', icon: IconCpu, status: 'pending' },
|
|
{ name: 'Array Status', icon: IconDatabase, status: 'pending' },
|
|
{ name: 'Network Configuration', icon: IconNetwork, status: 'pending' },
|
|
{ name: 'Docker Containers', icon: IconBrandDocker, status: 'pending' },
|
|
]);
|
|
|
|
const {
|
|
data: info,
|
|
isLoading,
|
|
error,
|
|
} = api.unraid.info.useQuery();
|
|
|
|
const {
|
|
data: array,
|
|
} = api.unraid.array.useQuery();
|
|
|
|
const {
|
|
data: docker,
|
|
} = api.unraid.docker.useQuery();
|
|
|
|
const runDiagnostics = async () => {
|
|
setIsGenerating(true);
|
|
setProgress(0);
|
|
|
|
// Simulate running diagnostics
|
|
const steps = checks.length;
|
|
for (let i = 0; i < steps; i++) {
|
|
setChecks((prev) =>
|
|
prev.map((check, idx) =>
|
|
idx === i ? { ...check, status: 'running' } : check
|
|
)
|
|
);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
|
|
// Simulate random results
|
|
const statuses: Array<'success' | 'warning' | 'error'> = ['success', 'success', 'success', 'warning'];
|
|
const status = statuses[Math.floor(Math.random() * statuses.length)];
|
|
|
|
setChecks((prev) =>
|
|
prev.map((check, idx) =>
|
|
idx === i
|
|
? {
|
|
...check,
|
|
status,
|
|
message:
|
|
status === 'warning'
|
|
? 'Minor issues detected'
|
|
: status === 'error'
|
|
? 'Problems found'
|
|
: 'All checks passed',
|
|
}
|
|
: check
|
|
)
|
|
);
|
|
|
|
setProgress(((i + 1) / steps) * 100);
|
|
}
|
|
|
|
setIsGenerating(false);
|
|
notifications.show({
|
|
title: 'Diagnostics Complete',
|
|
message: 'Diagnostic report has been generated',
|
|
color: 'green',
|
|
icon: <IconCheck size={16} />,
|
|
});
|
|
};
|
|
|
|
const resetDiagnostics = () => {
|
|
setProgress(0);
|
|
setChecks((prev) =>
|
|
prev.map((check) => ({
|
|
...check,
|
|
status: 'pending',
|
|
message: undefined,
|
|
}))
|
|
);
|
|
};
|
|
|
|
const getStatusColor = (status: DiagnosticCheck['status']) => {
|
|
switch (status) {
|
|
case 'success':
|
|
return 'green';
|
|
case 'warning':
|
|
return 'yellow';
|
|
case 'error':
|
|
return 'red';
|
|
case 'running':
|
|
return 'blue';
|
|
default:
|
|
return 'gray';
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<UnraidLayout>
|
|
<Container size="lg" py="xl">
|
|
<Stack align="center" spacing="md">
|
|
<Loader size="xl" />
|
|
<Text color="dimmed">Loading system information...</Text>
|
|
</Stack>
|
|
</Container>
|
|
</UnraidLayout>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<UnraidLayout>
|
|
<Container size="lg" py="xl">
|
|
<Alert icon={<IconAlertCircle size={16} />} title="Error" color="red">
|
|
{error.message}
|
|
</Alert>
|
|
</Container>
|
|
</UnraidLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<UnraidLayout>
|
|
<Container size="lg" py="xl">
|
|
<Stack spacing="xl">
|
|
{/* Header */}
|
|
<Group position="apart">
|
|
<Group>
|
|
<ThemeIcon size={48} radius="md" variant="light" color="red">
|
|
<IconBug size={28} />
|
|
</ThemeIcon>
|
|
<div>
|
|
<Title order={2}>Diagnostics</Title>
|
|
<Text color="dimmed" size="sm">
|
|
System health checks and diagnostic reports
|
|
</Text>
|
|
</div>
|
|
</Group>
|
|
|
|
<Group>
|
|
<Button
|
|
variant="light"
|
|
leftIcon={<IconRefresh size={16} />}
|
|
onClick={resetDiagnostics}
|
|
disabled={isGenerating}
|
|
>
|
|
Reset
|
|
</Button>
|
|
<Button
|
|
leftIcon={<IconBug size={16} />}
|
|
onClick={runDiagnostics}
|
|
loading={isGenerating}
|
|
>
|
|
Run Diagnostics
|
|
</Button>
|
|
</Group>
|
|
</Group>
|
|
|
|
{/* Progress */}
|
|
{progress > 0 && (
|
|
<Card shadow="sm" radius="md" withBorder>
|
|
<Group position="apart" mb="xs">
|
|
<Text weight={500}>Diagnostic Progress</Text>
|
|
<Text size="sm" color="dimmed">
|
|
{Math.round(progress)}%
|
|
</Text>
|
|
</Group>
|
|
<Progress
|
|
value={progress}
|
|
color={progress === 100 ? 'green' : 'blue'}
|
|
size="lg"
|
|
radius="xl"
|
|
animate={isGenerating}
|
|
/>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Diagnostic Checks */}
|
|
<Card shadow="sm" radius="md" withBorder>
|
|
<Title order={4} mb="md">
|
|
System Checks
|
|
</Title>
|
|
|
|
<Stack spacing="md">
|
|
{checks.map((check, idx) => (
|
|
<div key={check.name}>
|
|
<Group position="apart">
|
|
<Group spacing="sm">
|
|
<ThemeIcon
|
|
size="md"
|
|
variant="light"
|
|
color={getStatusColor(check.status)}
|
|
>
|
|
{check.status === 'running' ? (
|
|
<Loader size={14} color="blue" />
|
|
) : (
|
|
<check.icon size={16} />
|
|
)}
|
|
</ThemeIcon>
|
|
<div>
|
|
<Text weight={500}>{check.name}</Text>
|
|
{check.message && (
|
|
<Text size="xs" color="dimmed">
|
|
{check.message}
|
|
</Text>
|
|
)}
|
|
</div>
|
|
</Group>
|
|
|
|
<Text
|
|
size="sm"
|
|
weight={500}
|
|
color={getStatusColor(check.status)}
|
|
transform="uppercase"
|
|
>
|
|
{check.status}
|
|
</Text>
|
|
</Group>
|
|
{idx < checks.length - 1 && <Divider mt="md" />}
|
|
</div>
|
|
))}
|
|
</Stack>
|
|
</Card>
|
|
|
|
{/* System Summary */}
|
|
<Card shadow="sm" radius="md" withBorder>
|
|
<Title order={4} mb="md">
|
|
System Summary
|
|
</Title>
|
|
|
|
<Stack spacing="md">
|
|
<Group position="apart">
|
|
<Text color="dimmed">Unraid Version</Text>
|
|
<Text weight={500}>{info?.versions.unraid || 'Unknown'}</Text>
|
|
</Group>
|
|
<Divider />
|
|
<Group position="apart">
|
|
<Text color="dimmed">Linux Kernel</Text>
|
|
<Text weight={500}>{info?.os.kernel || 'Unknown'}</Text>
|
|
</Group>
|
|
<Divider />
|
|
<Group position="apart">
|
|
<Text color="dimmed">Array Status</Text>
|
|
<Text weight={500} color={array?.state === 'STARTED' ? 'green' : 'red'}>
|
|
{array?.state || 'Unknown'}
|
|
</Text>
|
|
</Group>
|
|
<Divider />
|
|
<Group position="apart">
|
|
<Text color="dimmed">Docker Containers</Text>
|
|
<Text weight={500}>
|
|
{docker?.containers.filter((c) => c.state === 'RUNNING').length || 0} running /{' '}
|
|
{docker?.containers.length || 0} total
|
|
</Text>
|
|
</Group>
|
|
<Divider />
|
|
<Group position="apart">
|
|
<Text color="dimmed">CPU</Text>
|
|
<Text weight={500}>{info?.cpu.brand || 'Unknown'}</Text>
|
|
</Group>
|
|
<Divider />
|
|
<Group position="apart">
|
|
<Text color="dimmed">Total RAM</Text>
|
|
<Text weight={500}>
|
|
{info?.memory?.total
|
|
? `${Math.round(info.memory.total / 1024 / 1024 / 1024)} GB`
|
|
: 'Unknown'}
|
|
</Text>
|
|
</Group>
|
|
</Stack>
|
|
</Card>
|
|
|
|
{/* Download Report */}
|
|
<Card shadow="sm" radius="md" withBorder>
|
|
<Title order={4} mb="md">
|
|
Diagnostic Report
|
|
</Title>
|
|
|
|
<Text size="sm" color="dimmed" mb="md">
|
|
Generate a comprehensive diagnostic report for troubleshooting. This includes system
|
|
logs, configuration files, and hardware information.
|
|
</Text>
|
|
|
|
<List size="sm" spacing="xs" mb="md">
|
|
<List.Item>System configuration and settings</List.Item>
|
|
<List.Item>Hardware information (CPU, RAM, disks)</List.Item>
|
|
<List.Item>Network configuration</List.Item>
|
|
<List.Item>Docker container status</List.Item>
|
|
<List.Item>Recent system logs</List.Item>
|
|
<List.Item>Plugin information</List.Item>
|
|
</List>
|
|
|
|
<Button
|
|
leftIcon={<IconDownload size={16} />}
|
|
variant="light"
|
|
disabled
|
|
>
|
|
Download Diagnostic Report
|
|
</Button>
|
|
</Card>
|
|
</Stack>
|
|
</Container>
|
|
</UnraidLayout>
|
|
);
|
|
}
|
|
|
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
|
const session = await getServerAuthSession(context);
|
|
const translations = await getServerSideTranslations(
|
|
['common'],
|
|
context.locale,
|
|
context.req,
|
|
context.res
|
|
);
|
|
|
|
const result = checkForSessionOrAskForLogin(
|
|
context,
|
|
session,
|
|
() => session?.user != undefined
|
|
);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
|
|
return {
|
|
props: {
|
|
...translations,
|
|
},
|
|
};
|
|
};
|