Add Unraid API integration and Orchis theme
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled

Phase 1: Foundation Setup
- Create Unraid GraphQL client with type-safe queries/mutations
- Add comprehensive TypeScript types for all Unraid data models
- Implement tRPC router with 30+ endpoints for Unraid management
- Add environment variables for Unraid connection

Phase 2: Core Dashboard
- Create SystemInfoCard component (CPU, RAM, OS, motherboard)
- Create ArrayCard component (disks, parity, cache pools)
- Create DockerCard component with start/stop controls
- Create VmsCard component with power management
- Add main Unraid dashboard page with real-time updates

Phase 3: Orchis Theme Integration
- Create Mantine theme override with Orchis design tokens
- Add CSS custom properties for light/dark modes
- Configure shadows, spacing, radius from Orchis specs
- Style all Mantine components with Orchis patterns

Files added:
- src/lib/unraid/* (GraphQL client, types, queries)
- src/server/api/routers/unraid/* (tRPC router)
- src/components/Unraid/* (Dashboard components)
- src/pages/unraid/* (Dashboard page)
- src/styles/orchis/* (Theme configuration)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kaloyan Danchev
2026-02-06 09:19:21 +02:00
parent e881ec6cb5
commit 83a8546521
19 changed files with 4431 additions and 1 deletions

346
src/pages/unraid/index.tsx Normal file
View File

@@ -0,0 +1,346 @@
/**
* Unraid Dashboard Page
* Main overview page for Unraid server management
*/
import { useState } from 'react';
import {
Alert,
Container,
Grid,
Group,
Loader,
Stack,
Text,
ThemeIcon,
Title,
useMantineTheme,
} from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconServer, IconAlertCircle, IconCheck } from '@tabler/icons-react';
import { GetServerSidePropsContext } from 'next';
import { SystemInfoCard, ArrayCard, DockerCard, VmsCard } from '~/components/Unraid/Dashboard';
import { getServerAuthSession } from '~/server/auth';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { api } from '~/utils/api';
export default function UnraidDashboardPage() {
const theme = useMantineTheme();
const [loadingContainers, setLoadingContainers] = useState<string[]>([]);
const [loadingVms, setLoadingVms] = useState<string[]>([]);
const [arrayLoading, setArrayLoading] = useState(false);
// Fetch dashboard data
const {
data: dashboard,
isLoading,
error,
refetch,
} = api.unraid.dashboard.useQuery(undefined, {
refetchInterval: 10000, // Refresh every 10 seconds
});
// Mutations
const startContainer = api.unraid.startContainer.useMutation({
onMutate: ({ id }) => setLoadingContainers((prev) => [...prev, id]),
onSettled: (_, __, { id }) =>
setLoadingContainers((prev) => prev.filter((cid) => cid !== id)),
onSuccess: () => {
notifications.show({
title: 'Container Started',
message: 'Container started successfully',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
onError: (error) => {
notifications.show({
title: 'Error',
message: error.message,
color: 'red',
icon: <IconAlertCircle size={16} />,
});
},
});
const stopContainer = api.unraid.stopContainer.useMutation({
onMutate: ({ id }) => setLoadingContainers((prev) => [...prev, id]),
onSettled: (_, __, { id }) =>
setLoadingContainers((prev) => prev.filter((cid) => cid !== id)),
onSuccess: () => {
notifications.show({
title: 'Container Stopped',
message: 'Container stopped successfully',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
onError: (error) => {
notifications.show({
title: 'Error',
message: error.message,
color: 'red',
icon: <IconAlertCircle size={16} />,
});
},
});
const startVm = api.unraid.startVm.useMutation({
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
onSuccess: () => {
notifications.show({
title: 'VM Started',
message: 'Virtual machine started successfully',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
onError: (error) => {
notifications.show({
title: 'Error',
message: error.message,
color: 'red',
icon: <IconAlertCircle size={16} />,
});
},
});
const stopVm = api.unraid.stopVm.useMutation({
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
onSuccess: () => {
notifications.show({
title: 'VM Stopped',
message: 'Virtual machine stopped successfully',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
onError: (error) => {
notifications.show({
title: 'Error',
message: error.message,
color: 'red',
icon: <IconAlertCircle size={16} />,
});
},
});
const pauseVm = api.unraid.pauseVm.useMutation({
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
onSuccess: () => {
notifications.show({
title: 'VM Paused',
message: 'Virtual machine paused successfully',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
});
const resumeVm = api.unraid.resumeVm.useMutation({
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
onSuccess: () => {
notifications.show({
title: 'VM Resumed',
message: 'Virtual machine resumed successfully',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
});
const rebootVm = api.unraid.rebootVm.useMutation({
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
onSuccess: () => {
notifications.show({
title: 'VM Rebooting',
message: 'Virtual machine is rebooting',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
});
const startArray = api.unraid.startArray.useMutation({
onMutate: () => setArrayLoading(true),
onSettled: () => setArrayLoading(false),
onSuccess: () => {
notifications.show({
title: 'Array Starting',
message: 'Array is starting...',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
onError: (error) => {
notifications.show({
title: 'Error',
message: error.message,
color: 'red',
icon: <IconAlertCircle size={16} />,
});
},
});
const stopArray = api.unraid.stopArray.useMutation({
onMutate: () => setArrayLoading(true),
onSettled: () => setArrayLoading(false),
onSuccess: () => {
notifications.show({
title: 'Array Stopping',
message: 'Array is stopping...',
color: 'green',
icon: <IconCheck size={16} />,
});
refetch();
},
onError: (error) => {
notifications.show({
title: 'Error',
message: error.message,
color: 'red',
icon: <IconAlertCircle size={16} />,
});
},
});
// Loading state
if (isLoading) {
return (
<Container size="xl" py="xl">
<Stack align="center" spacing="md">
<Loader size="xl" />
<Text color="dimmed">Connecting to Unraid server...</Text>
</Stack>
</Container>
);
}
// Error state
if (error) {
return (
<Container size="xl" py="xl">
<Alert
icon={<IconAlertCircle size={16} />}
title="Connection Error"
color="red"
variant="filled"
>
{error.message}
</Alert>
</Container>
);
}
// No data
if (!dashboard) {
return (
<Container size="xl" py="xl">
<Alert
icon={<IconAlertCircle size={16} />}
title="No Data"
color="yellow"
>
No data received from Unraid server. Please check your configuration.
</Alert>
</Container>
);
}
return (
<Container size="xl" py="xl">
<Stack spacing="xl">
{/* Header */}
<Group position="apart">
<Group>
<ThemeIcon size={48} radius="md" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }}>
<IconServer size={28} />
</ThemeIcon>
<div>
<Title order={1}>Unraid Dashboard</Title>
<Text color="dimmed" size="sm">
{dashboard.vars.name} - Unraid {dashboard.info.versions.unraid}
</Text>
</div>
</Group>
</Group>
{/* Dashboard Grid */}
<Grid>
{/* System Info */}
<Grid.Col md={6} lg={4}>
<SystemInfoCard
info={dashboard.info}
vars={dashboard.vars}
registration={dashboard.registration}
/>
</Grid.Col>
{/* Array */}
<Grid.Col md={6} lg={8}>
<ArrayCard
array={dashboard.array}
onStartArray={() => startArray.mutate()}
onStopArray={() => stopArray.mutate()}
isLoading={arrayLoading}
/>
</Grid.Col>
{/* Docker */}
<Grid.Col md={6}>
<DockerCard
docker={dashboard.docker}
onStartContainer={(id) => startContainer.mutate({ id })}
onStopContainer={(id) => stopContainer.mutate({ id })}
loadingContainers={loadingContainers}
/>
</Grid.Col>
{/* VMs */}
<Grid.Col md={6}>
<VmsCard
vms={dashboard.vms}
onStartVm={(id) => startVm.mutate({ id })}
onStopVm={(id) => stopVm.mutate({ id })}
onPauseVm={(id) => pauseVm.mutate({ id })}
onResumeVm={(id) => resumeVm.mutate({ id })}
onRebootVm={(id) => rebootVm.mutate({ id })}
loadingVms={loadingVms}
/>
</Grid.Col>
</Grid>
</Stack>
</Container>
);
}
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,
},
};
};