Add Unraid API integration and Orchis theme
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
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:
306
src/components/Unraid/Dashboard/ArrayCard.tsx
Normal file
306
src/components/Unraid/Dashboard/ArrayCard.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Array Card Component
|
||||
* Displays Unraid array status and disk information
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconDatabase,
|
||||
IconPlayerPlay,
|
||||
IconPlayerStop,
|
||||
IconTemperature,
|
||||
IconHardDrive,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import type { UnraidArray, ArrayDisk, ArrayState } from '~/lib/unraid/types';
|
||||
|
||||
interface ArrayCardProps {
|
||||
array: UnraidArray;
|
||||
onStartArray?: () => void;
|
||||
onStopArray?: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getStatusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'DISK_OK':
|
||||
return 'green';
|
||||
case 'DISK_INVALID':
|
||||
case 'DISK_WRONG':
|
||||
return 'red';
|
||||
case 'DISK_DSBL':
|
||||
case 'DISK_DSBL_NEW':
|
||||
return 'orange';
|
||||
case 'DISK_NEW':
|
||||
return 'blue';
|
||||
case 'DISK_NP':
|
||||
return 'gray';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
function getArrayStateColor(state: ArrayState): string {
|
||||
switch (state) {
|
||||
case 'STARTED':
|
||||
return 'green';
|
||||
case 'STOPPED':
|
||||
return 'red';
|
||||
default:
|
||||
return 'orange';
|
||||
}
|
||||
}
|
||||
|
||||
function DiskRow({ disk }: { disk: ArrayDisk }) {
|
||||
const usedPercent = disk.fsSize && disk.fsUsed
|
||||
? ((disk.fsUsed / disk.fsSize) * 100).toFixed(1)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
<ThemeIcon size="sm" variant="light" color={disk.spunDown ? 'gray' : 'blue'}>
|
||||
<IconHardDrive size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" weight={500}>
|
||||
{disk.name}
|
||||
</Text>
|
||||
</Group>
|
||||
</td>
|
||||
<td>
|
||||
<Badge size="xs" color={getStatusColor(disk.status)} variant="light">
|
||||
{disk.status.replace('DISK_', '')}
|
||||
</Badge>
|
||||
</td>
|
||||
<td>
|
||||
<Text size="sm">{formatBytes(disk.size)}</Text>
|
||||
</td>
|
||||
<td>
|
||||
{disk.temp !== null ? (
|
||||
<Group spacing={4}>
|
||||
<IconTemperature size={14} />
|
||||
<Text size="sm" color={disk.temp > 50 ? 'red' : disk.temp > 40 ? 'orange' : undefined}>
|
||||
{disk.temp}°C
|
||||
</Text>
|
||||
</Group>
|
||||
) : (
|
||||
<Text size="sm" color="dimmed">
|
||||
{disk.spunDown ? 'Spun down' : '-'}
|
||||
</Text>
|
||||
)}
|
||||
</td>
|
||||
<td style={{ width: 150 }}>
|
||||
{usedPercent ? (
|
||||
<Tooltip label={`${formatBytes(disk.fsUsed!)} / ${formatBytes(disk.fsSize!)}`}>
|
||||
<Progress
|
||||
value={parseFloat(usedPercent)}
|
||||
size="sm"
|
||||
radius="md"
|
||||
color={parseFloat(usedPercent) > 90 ? 'red' : parseFloat(usedPercent) > 75 ? 'orange' : 'blue'}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Text size="sm" color="dimmed">-</Text>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArrayCard({ array, onStartArray, onStopArray, isLoading }: ArrayCardProps) {
|
||||
const usedPercent = ((array.capacity.used / array.capacity.total) * 100).toFixed(1);
|
||||
const isStarted = array.state === 'STARTED';
|
||||
|
||||
return (
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Group position="apart">
|
||||
<Group>
|
||||
<ThemeIcon size="lg" radius="md" variant="light" color="blue">
|
||||
<IconDatabase size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Title order={4}>Array</Title>
|
||||
<Text size="xs" color="dimmed">
|
||||
{formatBytes(array.capacity.used)} / {formatBytes(array.capacity.total)}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group spacing="xs">
|
||||
<Badge color={getArrayStateColor(array.state)} variant="filled">
|
||||
{array.state}
|
||||
</Badge>
|
||||
{isStarted && onStopArray && (
|
||||
<Tooltip label="Stop Array">
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
onClick={onStopArray}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerStop size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isStarted && onStartArray && (
|
||||
<Tooltip label="Start Array">
|
||||
<ActionIcon
|
||||
color="green"
|
||||
variant="light"
|
||||
onClick={onStartArray}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerPlay size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
|
||||
<Stack spacing="md" mt="md">
|
||||
{/* Array Capacity */}
|
||||
<div>
|
||||
<Group position="apart" mb={5}>
|
||||
<Text size="sm" weight={500}>
|
||||
Total Capacity
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed">
|
||||
{usedPercent}% used
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={parseFloat(usedPercent)}
|
||||
size="lg"
|
||||
radius="md"
|
||||
color={parseFloat(usedPercent) > 90 ? 'red' : parseFloat(usedPercent) > 75 ? 'orange' : 'blue'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Parity Check Status */}
|
||||
{array.parityCheckStatus?.running && (
|
||||
<div>
|
||||
<Group position="apart" mb={5}>
|
||||
<Text size="sm" weight={500} color="orange">
|
||||
Parity Check in Progress
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed">
|
||||
{array.parityCheckStatus.progress.toFixed(1)}% - {array.parityCheckStatus.errors} errors
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={array.parityCheckStatus.progress}
|
||||
size="md"
|
||||
radius="md"
|
||||
color="orange"
|
||||
animate
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parity Disks */}
|
||||
{array.parities.length > 0 && (
|
||||
<>
|
||||
<Text size="sm" weight={600} mt="xs">
|
||||
Parity ({array.parities.length})
|
||||
</Text>
|
||||
<Table fontSize="sm" verticalSpacing={4}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Disk</th>
|
||||
<th>Status</th>
|
||||
<th>Size</th>
|
||||
<th>Temp</th>
|
||||
<th>Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{array.parities.map((disk) => (
|
||||
<DiskRow key={disk.id} disk={disk} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Data Disks */}
|
||||
{array.disks.length > 0 && (
|
||||
<>
|
||||
<Text size="sm" weight={600} mt="xs">
|
||||
Data Disks ({array.disks.length})
|
||||
</Text>
|
||||
<Table fontSize="sm" verticalSpacing={4}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Disk</th>
|
||||
<th>Status</th>
|
||||
<th>Size</th>
|
||||
<th>Temp</th>
|
||||
<th>Usage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{array.disks.map((disk) => (
|
||||
<DiskRow key={disk.id} disk={disk} />
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Cache Pools */}
|
||||
{array.caches.length > 0 && (
|
||||
<>
|
||||
<Text size="sm" weight={600} mt="xs">
|
||||
Cache Pools ({array.caches.length})
|
||||
</Text>
|
||||
{array.caches.map((cache) => {
|
||||
const cacheUsedPercent = ((cache.fsUsed / cache.fsSize) * 100).toFixed(1);
|
||||
return (
|
||||
<div key={cache.id}>
|
||||
<Group position="apart" mb={5}>
|
||||
<Text size="sm" weight={500}>
|
||||
{cache.name} ({cache.fsType})
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed">
|
||||
{formatBytes(cache.fsUsed)} / {formatBytes(cache.fsSize)}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={parseFloat(cacheUsedPercent)}
|
||||
size="sm"
|
||||
radius="md"
|
||||
color={parseFloat(cacheUsedPercent) > 90 ? 'red' : parseFloat(cacheUsedPercent) > 75 ? 'orange' : 'teal'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArrayCard;
|
||||
205
src/components/Unraid/Dashboard/DockerCard.tsx
Normal file
205
src/components/Unraid/Dashboard/DockerCard.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Docker Card Component
|
||||
* Displays Docker containers with start/stop controls
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Menu,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBrandDocker,
|
||||
IconPlayerPlay,
|
||||
IconPlayerStop,
|
||||
IconDots,
|
||||
IconBox,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import type { Docker, DockerContainer, ContainerState } from '~/lib/unraid/types';
|
||||
|
||||
interface DockerCardProps {
|
||||
docker: Docker;
|
||||
onStartContainer?: (id: string) => void;
|
||||
onStopContainer?: (id: string) => void;
|
||||
loadingContainers?: string[];
|
||||
}
|
||||
|
||||
function getStateColor(state: ContainerState): string {
|
||||
switch (state) {
|
||||
case 'RUNNING':
|
||||
return 'green';
|
||||
case 'EXITED':
|
||||
return 'red';
|
||||
case 'PAUSED':
|
||||
return 'yellow';
|
||||
case 'RESTARTING':
|
||||
return 'orange';
|
||||
case 'CREATED':
|
||||
return 'blue';
|
||||
case 'DEAD':
|
||||
return 'gray';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
function ContainerRow({
|
||||
container,
|
||||
onStart,
|
||||
onStop,
|
||||
isLoading,
|
||||
}: {
|
||||
container: DockerContainer;
|
||||
onStart?: () => void;
|
||||
onStop?: () => void;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const isRunning = container.state === 'RUNNING';
|
||||
const containerName = container.names[0]?.replace(/^\//, '') || 'Unknown';
|
||||
|
||||
return (
|
||||
<Group position="apart" py="xs" sx={(theme) => ({
|
||||
borderBottom: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]
|
||||
}`,
|
||||
'&:last-child': {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
})}>
|
||||
<Group spacing="sm">
|
||||
<ThemeIcon size="md" variant="light" color={getStateColor(container.state)}>
|
||||
<IconBox size={16} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text size="sm" weight={500} lineClamp={1}>
|
||||
{containerName}
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed" lineClamp={1}>
|
||||
{container.image}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Group spacing="xs">
|
||||
<Badge size="xs" color={getStateColor(container.state)} variant="light">
|
||||
{container.state}
|
||||
</Badge>
|
||||
|
||||
{isRunning && onStop && (
|
||||
<Tooltip label="Stop">
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onStop}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerStop size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!isRunning && onStart && (
|
||||
<Tooltip label="Start">
|
||||
<ActionIcon
|
||||
color="green"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onStart}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerPlay size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Menu shadow="md" width={150} position="bottom-end">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" size="sm">
|
||||
<IconDots size={14} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item disabled>View Logs</Menu.Item>
|
||||
<Menu.Item disabled>Edit</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item color="red" disabled>
|
||||
Remove
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function DockerCard({
|
||||
docker,
|
||||
onStartContainer,
|
||||
onStopContainer,
|
||||
loadingContainers = [],
|
||||
}: DockerCardProps) {
|
||||
const runningCount = docker.containers.filter((c) => c.state === 'RUNNING').length;
|
||||
const totalCount = docker.containers.length;
|
||||
|
||||
// Sort containers: running first, then by name
|
||||
const sortedContainers = [...docker.containers].sort((a, b) => {
|
||||
if (a.state === 'RUNNING' && b.state !== 'RUNNING') return -1;
|
||||
if (a.state !== 'RUNNING' && b.state === 'RUNNING') return 1;
|
||||
return (a.names[0] || '').localeCompare(b.names[0] || '');
|
||||
});
|
||||
|
||||
return (
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Group position="apart">
|
||||
<Group>
|
||||
<ThemeIcon size="lg" radius="md" variant="light" color="cyan">
|
||||
<IconBrandDocker size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Title order={4}>Docker</Title>
|
||||
<Text size="xs" color="dimmed">
|
||||
{runningCount} running / {totalCount} total
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge color="cyan" variant="light">
|
||||
{docker.networks.length} networks
|
||||
</Badge>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
|
||||
<ScrollArea.Autosize maxHeight={400} mt="xs">
|
||||
<Stack spacing={0}>
|
||||
{sortedContainers.length === 0 ? (
|
||||
<Text size="sm" color="dimmed" align="center" py="lg">
|
||||
No containers
|
||||
</Text>
|
||||
) : (
|
||||
sortedContainers.map((container) => (
|
||||
<ContainerRow
|
||||
key={container.id}
|
||||
container={container}
|
||||
onStart={onStartContainer ? () => onStartContainer(container.id) : undefined}
|
||||
onStop={onStopContainer ? () => onStopContainer(container.id) : undefined}
|
||||
isLoading={loadingContainers.includes(container.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea.Autosize>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default DockerCard;
|
||||
192
src/components/Unraid/Dashboard/SystemInfoCard.tsx
Normal file
192
src/components/Unraid/Dashboard/SystemInfoCard.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* System Info Card Component
|
||||
* Displays Unraid system information in the dashboard
|
||||
*/
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Progress,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconCpu,
|
||||
IconDeviceDesktop,
|
||||
IconServer,
|
||||
IconClock,
|
||||
IconBrandUbuntu,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import type { SystemInfo, ServerVars, Registration } from '~/lib/unraid/types';
|
||||
|
||||
interface SystemInfoCardProps {
|
||||
info: SystemInfo;
|
||||
vars: ServerVars;
|
||||
registration: Registration;
|
||||
}
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
export function SystemInfoCard({ info, vars, registration }: SystemInfoCardProps) {
|
||||
const memoryUsedPercent = ((info.memory.used / info.memory.total) * 100).toFixed(1);
|
||||
|
||||
return (
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Group position="apart">
|
||||
<Group>
|
||||
<ThemeIcon size="lg" radius="md" variant="light" color="blue">
|
||||
<IconServer size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Title order={4}>{vars.name}</Title>
|
||||
<Text size="xs" color="dimmed">
|
||||
{vars.description}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge
|
||||
color={registration.type === 'Pro' ? 'green' : registration.type === 'Plus' ? 'blue' : 'gray'}
|
||||
variant="filled"
|
||||
>
|
||||
{registration.type}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
|
||||
<Stack spacing="md" mt="md">
|
||||
{/* CPU Info */}
|
||||
<Group position="apart" noWrap>
|
||||
<Group spacing="xs">
|
||||
<ThemeIcon size="sm" radius="md" variant="light" color="violet">
|
||||
<IconCpu size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" weight={500}>
|
||||
CPU
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" color="dimmed" align="right" style={{ flex: 1 }}>
|
||||
{info.cpu.brand}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={3} spacing="xs">
|
||||
<div>
|
||||
<Text size="xs" color="dimmed">
|
||||
Cores
|
||||
</Text>
|
||||
<Text size="sm" weight={500}>
|
||||
{info.cpu.cores}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" color="dimmed">
|
||||
Threads
|
||||
</Text>
|
||||
<Text size="sm" weight={500}>
|
||||
{info.cpu.threads}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="xs" color="dimmed">
|
||||
Speed
|
||||
</Text>
|
||||
<Text size="sm" weight={500}>
|
||||
{info.cpu.speed.toFixed(2)} GHz
|
||||
</Text>
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* Memory Info */}
|
||||
<div>
|
||||
<Group position="apart" mb={5}>
|
||||
<Text size="sm" weight={500}>
|
||||
Memory
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed">
|
||||
{formatBytes(info.memory.used)} / {formatBytes(info.memory.total)} ({memoryUsedPercent}%)
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={parseFloat(memoryUsedPercent)}
|
||||
size="md"
|
||||
radius="md"
|
||||
color={parseFloat(memoryUsedPercent) > 80 ? 'red' : parseFloat(memoryUsedPercent) > 60 ? 'yellow' : 'blue'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* OS Info */}
|
||||
<Group position="apart" noWrap>
|
||||
<Group spacing="xs">
|
||||
<ThemeIcon size="sm" radius="md" variant="light" color="green">
|
||||
<IconBrandUbuntu size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" weight={500}>
|
||||
OS
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" color="dimmed">
|
||||
Unraid {info.versions.unraid}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{/* Motherboard */}
|
||||
<Group position="apart" noWrap>
|
||||
<Group spacing="xs">
|
||||
<ThemeIcon size="sm" radius="md" variant="light" color="orange">
|
||||
<IconDeviceDesktop size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" weight={500}>
|
||||
Motherboard
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" color="dimmed" lineClamp={1}>
|
||||
{info.baseboard.manufacturer} {info.baseboard.model}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{/* Uptime */}
|
||||
<Group position="apart" noWrap>
|
||||
<Group spacing="xs">
|
||||
<ThemeIcon size="sm" radius="md" variant="light" color="teal">
|
||||
<IconClock size={14} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" weight={500}>
|
||||
Uptime
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" color="dimmed">
|
||||
{formatUptime(info.os.uptime)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SystemInfoCard;
|
||||
277
src/components/Unraid/Dashboard/VmsCard.tsx
Normal file
277
src/components/Unraid/Dashboard/VmsCard.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* VMs Card Component
|
||||
* Displays Virtual Machines with power controls
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Card,
|
||||
Group,
|
||||
Menu,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconServer2,
|
||||
IconPlayerPlay,
|
||||
IconPlayerStop,
|
||||
IconPlayerPause,
|
||||
IconRefresh,
|
||||
IconDots,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import type { VirtualMachine, VmState } from '~/lib/unraid/types';
|
||||
|
||||
interface VmsCardProps {
|
||||
vms: VirtualMachine[];
|
||||
onStartVm?: (id: string) => void;
|
||||
onStopVm?: (id: string) => void;
|
||||
onPauseVm?: (id: string) => void;
|
||||
onResumeVm?: (id: string) => void;
|
||||
onRebootVm?: (id: string) => void;
|
||||
loadingVms?: string[];
|
||||
}
|
||||
|
||||
function getStateColor(state: VmState): string {
|
||||
switch (state) {
|
||||
case 'RUNNING':
|
||||
return 'green';
|
||||
case 'SHUTOFF':
|
||||
return 'red';
|
||||
case 'PAUSED':
|
||||
case 'PMSUSPENDED':
|
||||
return 'yellow';
|
||||
case 'SHUTDOWN':
|
||||
return 'orange';
|
||||
case 'IDLE':
|
||||
return 'blue';
|
||||
case 'CRASHED':
|
||||
return 'red';
|
||||
case 'NOSTATE':
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
function formatMemory(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function VmRow({
|
||||
vm,
|
||||
onStart,
|
||||
onStop,
|
||||
onPause,
|
||||
onResume,
|
||||
onReboot,
|
||||
isLoading,
|
||||
}: {
|
||||
vm: VirtualMachine;
|
||||
onStart?: () => void;
|
||||
onStop?: () => void;
|
||||
onPause?: () => void;
|
||||
onResume?: () => void;
|
||||
onReboot?: () => void;
|
||||
isLoading?: boolean;
|
||||
}) {
|
||||
const isRunning = vm.state === 'RUNNING';
|
||||
const isPaused = vm.state === 'PAUSED' || vm.state === 'PMSUSPENDED';
|
||||
const isStopped = vm.state === 'SHUTOFF' || vm.state === 'SHUTDOWN' || vm.state === 'NOSTATE';
|
||||
|
||||
return (
|
||||
<Group
|
||||
position="apart"
|
||||
py="xs"
|
||||
sx={(theme) => ({
|
||||
borderBottom: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]
|
||||
}`,
|
||||
'&:last-child': {
|
||||
borderBottom: 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Group spacing="sm">
|
||||
<ThemeIcon size="md" variant="light" color={getStateColor(vm.state)}>
|
||||
<IconServer2 size={16} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text size="sm" weight={500} lineClamp={1}>
|
||||
{vm.name}
|
||||
</Text>
|
||||
<Group spacing={4}>
|
||||
<Text size="xs" color="dimmed">
|
||||
{vm.cpus} vCPU
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed">
|
||||
•
|
||||
</Text>
|
||||
<Text size="xs" color="dimmed">
|
||||
{formatMemory(vm.memory)}
|
||||
</Text>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Group spacing="xs">
|
||||
<Badge size="xs" color={getStateColor(vm.state)} variant="light">
|
||||
{vm.state}
|
||||
</Badge>
|
||||
|
||||
{isRunning && onPause && (
|
||||
<Tooltip label="Pause">
|
||||
<ActionIcon
|
||||
color="yellow"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onPause}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerPause size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isPaused && onResume && (
|
||||
<Tooltip label="Resume">
|
||||
<ActionIcon
|
||||
color="green"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onResume}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerPlay size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isRunning && onStop && (
|
||||
<Tooltip label="Stop">
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onStop}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerStop size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isStopped && onStart && (
|
||||
<Tooltip label="Start">
|
||||
<ActionIcon
|
||||
color="green"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={onStart}
|
||||
loading={isLoading}
|
||||
>
|
||||
<IconPlayerPlay size={14} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Menu shadow="md" width={150} position="bottom-end">
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="subtle" size="sm">
|
||||
<IconDots size={14} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{isRunning && onReboot && (
|
||||
<Menu.Item
|
||||
icon={<IconRefresh size={14} />}
|
||||
onClick={onReboot}
|
||||
>
|
||||
Reboot
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item disabled>VNC Console</Menu.Item>
|
||||
<Menu.Item disabled>Edit</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item color="red" disabled>
|
||||
Force Stop
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export function VmsCard({
|
||||
vms,
|
||||
onStartVm,
|
||||
onStopVm,
|
||||
onPauseVm,
|
||||
onResumeVm,
|
||||
onRebootVm,
|
||||
loadingVms = [],
|
||||
}: VmsCardProps) {
|
||||
const runningCount = vms.filter((vm) => vm.state === 'RUNNING').length;
|
||||
const totalCount = vms.length;
|
||||
|
||||
// Sort VMs: running first, then by name
|
||||
const sortedVms = [...vms].sort((a, b) => {
|
||||
if (a.state === 'RUNNING' && b.state !== 'RUNNING') return -1;
|
||||
if (a.state !== 'RUNNING' && b.state === 'RUNNING') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Group position="apart">
|
||||
<Group>
|
||||
<ThemeIcon size="lg" radius="md" variant="light" color="violet">
|
||||
<IconServer2 size={20} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Title order={4}>Virtual Machines</Title>
|
||||
<Text size="xs" color="dimmed">
|
||||
{runningCount} running / {totalCount} total
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card.Section>
|
||||
|
||||
<ScrollArea.Autosize maxHeight={400} mt="xs">
|
||||
<Stack spacing={0}>
|
||||
{sortedVms.length === 0 ? (
|
||||
<Text size="sm" color="dimmed" align="center" py="lg">
|
||||
No virtual machines
|
||||
</Text>
|
||||
) : (
|
||||
sortedVms.map((vm) => (
|
||||
<VmRow
|
||||
key={vm.id}
|
||||
vm={vm}
|
||||
onStart={onStartVm ? () => onStartVm(vm.id) : undefined}
|
||||
onStop={onStopVm ? () => onStopVm(vm.id) : undefined}
|
||||
onPause={onPauseVm ? () => onPauseVm(vm.id) : undefined}
|
||||
onResume={onResumeVm ? () => onResumeVm(vm.id) : undefined}
|
||||
onReboot={onRebootVm ? () => onRebootVm(vm.id) : undefined}
|
||||
isLoading={loadingVms.includes(vm.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea.Autosize>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default VmsCard;
|
||||
8
src/components/Unraid/Dashboard/index.ts
Normal file
8
src/components/Unraid/Dashboard/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Unraid Dashboard Components
|
||||
*/
|
||||
|
||||
export { SystemInfoCard } from './SystemInfoCard';
|
||||
export { ArrayCard } from './ArrayCard';
|
||||
export { DockerCard } from './DockerCard';
|
||||
export { VmsCard } from './VmsCard';
|
||||
11
src/env.js
11
src/env.js
@@ -40,6 +40,12 @@ const env = createEnv({
|
||||
DOCKER_HOST: z.string().optional(),
|
||||
DOCKER_PORT: portSchema,
|
||||
DEMO_MODE: z.string().optional(),
|
||||
|
||||
// Unraid API
|
||||
UNRAID_HOST: z.string().optional(),
|
||||
UNRAID_API_KEY: z.string().optional(),
|
||||
UNRAID_USE_SSL: zodParsedBoolean().default('false'),
|
||||
UNRAID_PORT: portSchema,
|
||||
DISABLE_UPGRADE_MODAL: zodParsedBoolean().default('false'),
|
||||
HOSTNAME: z.string().optional(),
|
||||
|
||||
@@ -167,6 +173,11 @@ const env = createEnv({
|
||||
AUTH_SESSION_EXPIRY_TIME: process.env.AUTH_SESSION_EXPIRY_TIME,
|
||||
DEMO_MODE: process.env.DEMO_MODE,
|
||||
DISABLE_UPGRADE_MODAL: process.env.DISABLE_UPGRADE_MODAL,
|
||||
// Unraid API
|
||||
UNRAID_HOST: process.env.UNRAID_HOST,
|
||||
UNRAID_API_KEY: process.env.UNRAID_API_KEY,
|
||||
UNRAID_USE_SSL: process.env.UNRAID_USE_SSL,
|
||||
UNRAID_PORT: process.env.UNRAID_PORT,
|
||||
},
|
||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||
});
|
||||
|
||||
450
src/lib/unraid/client.ts
Normal file
450
src/lib/unraid/client.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Unraid GraphQL Client
|
||||
* Provides type-safe access to the Unraid API
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import type {
|
||||
Customization,
|
||||
Disk,
|
||||
Docker,
|
||||
Flash,
|
||||
Network,
|
||||
Notification,
|
||||
Plugin,
|
||||
Registration,
|
||||
Server,
|
||||
ServerVars,
|
||||
Service,
|
||||
Share,
|
||||
SystemInfo,
|
||||
UnraidApiResponse,
|
||||
UnraidArray,
|
||||
UpsDevice,
|
||||
VirtualMachine,
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
ARRAY_QUERY,
|
||||
CUSTOMIZATION_QUERY,
|
||||
DASHBOARD_QUERY,
|
||||
DISK_QUERY,
|
||||
DISKS_QUERY,
|
||||
DOCKER_QUERY,
|
||||
FLASH_QUERY,
|
||||
INFO_QUERY,
|
||||
NETWORK_QUERY,
|
||||
NOTIFICATIONS_QUERY,
|
||||
PLUGINS_QUERY,
|
||||
REGISTRATION_QUERY,
|
||||
SERVER_QUERY,
|
||||
SERVICES_QUERY,
|
||||
SHARES_QUERY,
|
||||
UPS_DEVICES_QUERY,
|
||||
VARS_QUERY,
|
||||
VMS_QUERY,
|
||||
} from './queries';
|
||||
|
||||
import {
|
||||
ARRAY_SET_STATE_MUTATION,
|
||||
DOCKER_START_MUTATION,
|
||||
DOCKER_STOP_MUTATION,
|
||||
NOTIFICATION_ARCHIVE_MUTATION,
|
||||
NOTIFICATION_DELETE_MUTATION,
|
||||
PARITY_CHECK_CANCEL_MUTATION,
|
||||
PARITY_CHECK_PAUSE_MUTATION,
|
||||
PARITY_CHECK_RESUME_MUTATION,
|
||||
PARITY_CHECK_START_MUTATION,
|
||||
VM_FORCE_STOP_MUTATION,
|
||||
VM_PAUSE_MUTATION,
|
||||
VM_REBOOT_MUTATION,
|
||||
VM_RESUME_MUTATION,
|
||||
VM_START_MUTATION,
|
||||
VM_STOP_MUTATION,
|
||||
} from './queries/mutations';
|
||||
|
||||
export interface UnraidClientConfig {
|
||||
host: string;
|
||||
apiKey: string;
|
||||
useSsl?: boolean;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
info: SystemInfo;
|
||||
vars: ServerVars;
|
||||
registration: Registration;
|
||||
array: UnraidArray;
|
||||
docker: Docker;
|
||||
vms: VirtualMachine[];
|
||||
shares: Share[];
|
||||
services: Service[];
|
||||
notifications: Notification[];
|
||||
}
|
||||
|
||||
export class UnraidClient {
|
||||
private client: AxiosInstance;
|
||||
private config: UnraidClientConfig;
|
||||
|
||||
constructor(config: UnraidClientConfig) {
|
||||
this.config = config;
|
||||
const protocol = config.useSsl ? 'https' : 'http';
|
||||
const port = config.port || (config.useSsl ? 443 : 80);
|
||||
const baseURL = `${protocol}://${config.host}:${port}`;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.apiKey,
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a GraphQL query
|
||||
*/
|
||||
private async query<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
|
||||
const response = await this.client.post<UnraidApiResponse<T>>('/graphql', {
|
||||
query,
|
||||
variables,
|
||||
});
|
||||
|
||||
if (response.data.errors?.length) {
|
||||
throw new Error(response.data.errors.map((e) => e.message).join(', '));
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a GraphQL mutation
|
||||
*/
|
||||
private async mutate<T>(mutation: string, variables?: Record<string, unknown>): Promise<T> {
|
||||
return this.query<T>(mutation, variables);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getInfo(): Promise<SystemInfo> {
|
||||
const data = await this.query<{ info: SystemInfo }>(INFO_QUERY);
|
||||
return data.info;
|
||||
}
|
||||
|
||||
async getVars(): Promise<ServerVars> {
|
||||
const data = await this.query<{ vars: ServerVars }>(VARS_QUERY);
|
||||
return data.vars;
|
||||
}
|
||||
|
||||
async getServer(): Promise<Server> {
|
||||
const data = await this.query<{ server: Server }>(SERVER_QUERY);
|
||||
return data.server;
|
||||
}
|
||||
|
||||
async getRegistration(): Promise<Registration> {
|
||||
const data = await this.query<{ registration: Registration }>(REGISTRATION_QUERY);
|
||||
return data.registration;
|
||||
}
|
||||
|
||||
async getFlash(): Promise<Flash> {
|
||||
const data = await this.query<{ flash: Flash }>(FLASH_QUERY);
|
||||
return data.flash;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getArray(): Promise<UnraidArray> {
|
||||
const data = await this.query<{ array: UnraidArray }>(ARRAY_QUERY);
|
||||
return data.array;
|
||||
}
|
||||
|
||||
async getDisks(): Promise<Disk[]> {
|
||||
const data = await this.query<{ disks: Disk[] }>(DISKS_QUERY);
|
||||
return data.disks;
|
||||
}
|
||||
|
||||
async getDisk(id: string): Promise<Disk> {
|
||||
const data = await this.query<{ disk: Disk }>(DISK_QUERY, { id });
|
||||
return data.disk;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getDocker(): Promise<Docker> {
|
||||
const data = await this.query<{ docker: Docker }>(DOCKER_QUERY);
|
||||
return data.docker;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getVms(): Promise<VirtualMachine[]> {
|
||||
const data = await this.query<{ vms: VirtualMachine[] }>(VMS_QUERY);
|
||||
return data.vms;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SHARES QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getShares(): Promise<Share[]> {
|
||||
const data = await this.query<{ shares: Share[] }>(SHARES_QUERY);
|
||||
return data.shares;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATIONS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getNotifications(): Promise<Notification[]> {
|
||||
const data = await this.query<{ notifications: Notification[] }>(NOTIFICATIONS_QUERY);
|
||||
return data.notifications;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVICES QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getServices(): Promise<Service[]> {
|
||||
const data = await this.query<{ services: Service[] }>(SERVICES_QUERY);
|
||||
return data.services;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getNetwork(): Promise<Network> {
|
||||
const data = await this.query<{ network: Network }>(NETWORK_QUERY);
|
||||
return data.network;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UPS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getUpsDevices(): Promise<UpsDevice[]> {
|
||||
const data = await this.query<{ upsDevices: UpsDevice[] }>(UPS_DEVICES_QUERY);
|
||||
return data.upsDevices;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGINS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getPlugins(): Promise<Plugin[]> {
|
||||
const data = await this.query<{ plugins: Plugin[] }>(PLUGINS_QUERY);
|
||||
return data.plugins;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOMIZATION QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getCustomization(): Promise<Customization> {
|
||||
const data = await this.query<{ customization: Customization }>(CUSTOMIZATION_QUERY);
|
||||
return data.customization;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DASHBOARD (COMPOSITE QUERY)
|
||||
// ============================================================================
|
||||
|
||||
async getDashboard(): Promise<DashboardData> {
|
||||
const data = await this.query<DashboardData>(DASHBOARD_QUERY);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startArray(): Promise<{ state: string }> {
|
||||
const data = await this.mutate<{ array: { setState: { state: string } } }>(
|
||||
ARRAY_SET_STATE_MUTATION,
|
||||
{ state: 'start' }
|
||||
);
|
||||
return data.array.setState;
|
||||
}
|
||||
|
||||
async stopArray(): Promise<{ state: string }> {
|
||||
const data = await this.mutate<{ array: { setState: { state: string } } }>(
|
||||
ARRAY_SET_STATE_MUTATION,
|
||||
{ state: 'stop' }
|
||||
);
|
||||
return data.array.setState;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PARITY CHECK MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startParityCheck(correct = false): Promise<{ running: boolean; progress: number }> {
|
||||
const data = await this.mutate<{
|
||||
parityCheck: { start: { running: boolean; progress: number } };
|
||||
}>(PARITY_CHECK_START_MUTATION, { correct });
|
||||
return data.parityCheck.start;
|
||||
}
|
||||
|
||||
async pauseParityCheck(): Promise<{ running: boolean; progress: number }> {
|
||||
const data = await this.mutate<{
|
||||
parityCheck: { pause: { running: boolean; progress: number } };
|
||||
}>(PARITY_CHECK_PAUSE_MUTATION);
|
||||
return data.parityCheck.pause;
|
||||
}
|
||||
|
||||
async resumeParityCheck(): Promise<{ running: boolean; progress: number }> {
|
||||
const data = await this.mutate<{
|
||||
parityCheck: { resume: { running: boolean; progress: number } };
|
||||
}>(PARITY_CHECK_RESUME_MUTATION);
|
||||
return data.parityCheck.resume;
|
||||
}
|
||||
|
||||
async cancelParityCheck(): Promise<{ running: boolean }> {
|
||||
const data = await this.mutate<{ parityCheck: { cancel: { running: boolean } } }>(
|
||||
PARITY_CHECK_CANCEL_MUTATION
|
||||
);
|
||||
return data.parityCheck.cancel;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startContainer(id: string): Promise<{ id: string; state: string; status: string }> {
|
||||
const data = await this.mutate<{
|
||||
docker: { start: { id: string; state: string; status: string } };
|
||||
}>(DOCKER_START_MUTATION, { id });
|
||||
return data.docker.start;
|
||||
}
|
||||
|
||||
async stopContainer(id: string): Promise<{ id: string; state: string; status: string }> {
|
||||
const data = await this.mutate<{
|
||||
docker: { stop: { id: string; state: string; status: string } };
|
||||
}>(DOCKER_STOP_MUTATION, { id });
|
||||
return data.docker.stop;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VM MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { start: { id: string; state: string } } }>(
|
||||
VM_START_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.start;
|
||||
}
|
||||
|
||||
async stopVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { stop: { id: string; state: string } } }>(
|
||||
VM_STOP_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.stop;
|
||||
}
|
||||
|
||||
async pauseVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { pause: { id: string; state: string } } }>(
|
||||
VM_PAUSE_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.pause;
|
||||
}
|
||||
|
||||
async resumeVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { resume: { id: string; state: string } } }>(
|
||||
VM_RESUME_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.resume;
|
||||
}
|
||||
|
||||
async forceStopVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { forceStop: { id: string; state: string } } }>(
|
||||
VM_FORCE_STOP_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.forceStop;
|
||||
}
|
||||
|
||||
async rebootVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { reboot: { id: string; state: string } } }>(
|
||||
VM_REBOOT_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.reboot;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATION MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async deleteNotification(id: string): Promise<boolean> {
|
||||
const data = await this.mutate<{ notification: { delete: boolean } }>(
|
||||
NOTIFICATION_DELETE_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.notification.delete;
|
||||
}
|
||||
|
||||
async archiveNotification(id: string): Promise<{ id: string; archived: boolean }> {
|
||||
const data = await this.mutate<{
|
||||
notification: { archive: { id: string; archived: boolean } };
|
||||
}>(NOTIFICATION_ARCHIVE_MUTATION, { id });
|
||||
return data.notification.archive;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HEALTH CHECK
|
||||
// ============================================================================
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await this.getVars();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SINGLETON INSTANCE
|
||||
// ============================================================================
|
||||
|
||||
let unraidClient: UnraidClient | null = null;
|
||||
|
||||
export function getUnraidClient(): UnraidClient {
|
||||
if (!unraidClient) {
|
||||
const host = process.env.UNRAID_HOST;
|
||||
const apiKey = process.env.UNRAID_API_KEY;
|
||||
|
||||
if (!host || !apiKey) {
|
||||
throw new Error('UNRAID_HOST and UNRAID_API_KEY environment variables are required');
|
||||
}
|
||||
|
||||
unraidClient = new UnraidClient({
|
||||
host,
|
||||
apiKey,
|
||||
useSsl: process.env.UNRAID_USE_SSL === 'true',
|
||||
port: process.env.UNRAID_PORT ? parseInt(process.env.UNRAID_PORT, 10) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return unraidClient;
|
||||
}
|
||||
|
||||
export function createUnraidClient(config: UnraidClientConfig): UnraidClient {
|
||||
return new UnraidClient(config);
|
||||
}
|
||||
19
src/lib/unraid/index.ts
Normal file
19
src/lib/unraid/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Unraid API Integration
|
||||
* ======================
|
||||
* Type-safe client for the Unraid GraphQL API
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { getUnraidClient } from '~/lib/unraid';
|
||||
*
|
||||
* const client = getUnraidClient();
|
||||
* const dashboard = await client.getDashboard();
|
||||
* ```
|
||||
*/
|
||||
|
||||
export * from './client';
|
||||
export * from './types';
|
||||
export * from './queries';
|
||||
export * from './queries/mutations';
|
||||
export * from './queries/subscriptions';
|
||||
536
src/lib/unraid/queries/index.ts
Normal file
536
src/lib/unraid/queries/index.ts
Normal file
@@ -0,0 +1,536 @@
|
||||
/**
|
||||
* Unraid GraphQL Queries
|
||||
* Based on Unraid API v4.29.2
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const INFO_QUERY = `
|
||||
query Info {
|
||||
info {
|
||||
cpu {
|
||||
manufacturer
|
||||
brand
|
||||
cores
|
||||
threads
|
||||
speed
|
||||
speedMax
|
||||
cache {
|
||||
l1d
|
||||
l1i
|
||||
l2
|
||||
l3
|
||||
}
|
||||
}
|
||||
memory {
|
||||
total
|
||||
free
|
||||
used
|
||||
active
|
||||
available
|
||||
buffers
|
||||
cached
|
||||
slab
|
||||
swapTotal
|
||||
swapUsed
|
||||
swapFree
|
||||
}
|
||||
os {
|
||||
platform
|
||||
distro
|
||||
release
|
||||
kernel
|
||||
arch
|
||||
hostname
|
||||
uptime
|
||||
}
|
||||
baseboard {
|
||||
manufacturer
|
||||
model
|
||||
version
|
||||
serial
|
||||
}
|
||||
versions {
|
||||
unraid
|
||||
api
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VARS_QUERY = `
|
||||
query Vars {
|
||||
vars {
|
||||
version
|
||||
name
|
||||
timezone
|
||||
description
|
||||
model
|
||||
protocol
|
||||
port
|
||||
localTld
|
||||
csrf
|
||||
uptime
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SERVER_QUERY = `
|
||||
query Server {
|
||||
server {
|
||||
owner
|
||||
guid
|
||||
wanip
|
||||
lanip
|
||||
localurl
|
||||
remoteurl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const REGISTRATION_QUERY = `
|
||||
query Registration {
|
||||
registration {
|
||||
type
|
||||
state
|
||||
keyFile
|
||||
expiration
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FLASH_QUERY = `
|
||||
query Flash {
|
||||
flash {
|
||||
guid
|
||||
vendor
|
||||
product
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const ARRAY_QUERY = `
|
||||
query Array {
|
||||
array {
|
||||
state
|
||||
capacity {
|
||||
total
|
||||
used
|
||||
free
|
||||
disks {
|
||||
total
|
||||
used
|
||||
free
|
||||
}
|
||||
}
|
||||
disks {
|
||||
id
|
||||
name
|
||||
device
|
||||
size
|
||||
status
|
||||
type
|
||||
temp
|
||||
numReads
|
||||
numWrites
|
||||
numErrors
|
||||
fsType
|
||||
fsFree
|
||||
fsUsed
|
||||
fsSize
|
||||
color
|
||||
spunDown
|
||||
transport
|
||||
rotational
|
||||
serial
|
||||
model
|
||||
}
|
||||
parities {
|
||||
id
|
||||
name
|
||||
device
|
||||
size
|
||||
status
|
||||
type
|
||||
temp
|
||||
numReads
|
||||
numWrites
|
||||
numErrors
|
||||
color
|
||||
spunDown
|
||||
transport
|
||||
rotational
|
||||
serial
|
||||
model
|
||||
}
|
||||
caches {
|
||||
id
|
||||
name
|
||||
fsType
|
||||
fsFree
|
||||
fsUsed
|
||||
fsSize
|
||||
devices {
|
||||
id
|
||||
name
|
||||
device
|
||||
size
|
||||
status
|
||||
temp
|
||||
spunDown
|
||||
serial
|
||||
model
|
||||
}
|
||||
}
|
||||
parityCheckStatus {
|
||||
running
|
||||
progress
|
||||
errors
|
||||
elapsed
|
||||
eta
|
||||
speed
|
||||
mode
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DISKS_QUERY = `
|
||||
query Disks {
|
||||
disks {
|
||||
id
|
||||
name
|
||||
device
|
||||
size
|
||||
vendor
|
||||
model
|
||||
serial
|
||||
firmware
|
||||
type
|
||||
interfaceType
|
||||
rotational
|
||||
temp
|
||||
smartStatus
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DISK_QUERY = `
|
||||
query Disk($id: String!) {
|
||||
disk(id: $id) {
|
||||
id
|
||||
name
|
||||
device
|
||||
size
|
||||
vendor
|
||||
model
|
||||
serial
|
||||
firmware
|
||||
type
|
||||
interfaceType
|
||||
rotational
|
||||
temp
|
||||
smartStatus
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const DOCKER_QUERY = `
|
||||
query Docker {
|
||||
docker {
|
||||
containers {
|
||||
id
|
||||
names
|
||||
image
|
||||
state
|
||||
status
|
||||
created
|
||||
ports {
|
||||
privatePort
|
||||
publicPort
|
||||
type
|
||||
ip
|
||||
}
|
||||
autoStart
|
||||
networkMode
|
||||
}
|
||||
networks {
|
||||
id
|
||||
name
|
||||
driver
|
||||
scope
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// VM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const VMS_QUERY = `
|
||||
query Vms {
|
||||
vms {
|
||||
id
|
||||
name
|
||||
state
|
||||
uuid
|
||||
description
|
||||
cpus
|
||||
memory
|
||||
autoStart
|
||||
icon
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// SHARES QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const SHARES_QUERY = `
|
||||
query Shares {
|
||||
shares {
|
||||
name
|
||||
comment
|
||||
free
|
||||
used
|
||||
size
|
||||
include
|
||||
exclude
|
||||
cache
|
||||
color
|
||||
floor
|
||||
splitLevel
|
||||
allocator
|
||||
export
|
||||
security
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATIONS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const NOTIFICATIONS_QUERY = `
|
||||
query Notifications {
|
||||
notifications {
|
||||
id
|
||||
title
|
||||
subject
|
||||
description
|
||||
importance
|
||||
type
|
||||
timestamp
|
||||
read
|
||||
archived
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// SERVICES QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const SERVICES_QUERY = `
|
||||
query Services {
|
||||
services {
|
||||
name
|
||||
online
|
||||
uptime
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const NETWORK_QUERY = `
|
||||
query Network {
|
||||
network {
|
||||
accessUrls {
|
||||
type
|
||||
name
|
||||
ipv4
|
||||
ipv6
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// UPS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const UPS_DEVICES_QUERY = `
|
||||
query UpsDevices {
|
||||
upsDevices {
|
||||
id
|
||||
name
|
||||
model
|
||||
status
|
||||
batteryCharge
|
||||
batteryRuntime
|
||||
load
|
||||
inputVoltage
|
||||
outputVoltage
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// PLUGINS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const PLUGINS_QUERY = `
|
||||
query Plugins {
|
||||
plugins {
|
||||
name
|
||||
version
|
||||
author
|
||||
url
|
||||
icon
|
||||
updateAvailable
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOMIZATION QUERIES
|
||||
// ============================================================================
|
||||
|
||||
export const CUSTOMIZATION_QUERY = `
|
||||
query Customization {
|
||||
customization {
|
||||
theme
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// DASHBOARD COMPOSITE QUERY
|
||||
// ============================================================================
|
||||
|
||||
export const DASHBOARD_QUERY = `
|
||||
query Dashboard {
|
||||
info {
|
||||
cpu {
|
||||
manufacturer
|
||||
brand
|
||||
cores
|
||||
threads
|
||||
speed
|
||||
}
|
||||
memory {
|
||||
total
|
||||
free
|
||||
used
|
||||
available
|
||||
}
|
||||
os {
|
||||
hostname
|
||||
uptime
|
||||
kernel
|
||||
}
|
||||
baseboard {
|
||||
manufacturer
|
||||
model
|
||||
}
|
||||
versions {
|
||||
unraid
|
||||
api
|
||||
}
|
||||
}
|
||||
vars {
|
||||
name
|
||||
version
|
||||
description
|
||||
}
|
||||
registration {
|
||||
type
|
||||
state
|
||||
}
|
||||
array {
|
||||
state
|
||||
capacity {
|
||||
total
|
||||
used
|
||||
free
|
||||
}
|
||||
disks {
|
||||
id
|
||||
name
|
||||
status
|
||||
temp
|
||||
size
|
||||
fsUsed
|
||||
fsFree
|
||||
spunDown
|
||||
}
|
||||
parities {
|
||||
id
|
||||
name
|
||||
status
|
||||
temp
|
||||
size
|
||||
}
|
||||
caches {
|
||||
id
|
||||
name
|
||||
fsUsed
|
||||
fsFree
|
||||
fsSize
|
||||
}
|
||||
parityCheckStatus {
|
||||
running
|
||||
progress
|
||||
errors
|
||||
eta
|
||||
}
|
||||
}
|
||||
docker {
|
||||
containers {
|
||||
id
|
||||
names
|
||||
state
|
||||
status
|
||||
autoStart
|
||||
}
|
||||
}
|
||||
vms {
|
||||
id
|
||||
name
|
||||
state
|
||||
autoStart
|
||||
}
|
||||
shares {
|
||||
name
|
||||
size
|
||||
used
|
||||
free
|
||||
}
|
||||
services {
|
||||
name
|
||||
online
|
||||
}
|
||||
notifications {
|
||||
id
|
||||
importance
|
||||
read
|
||||
}
|
||||
}
|
||||
`;
|
||||
328
src/lib/unraid/queries/mutations.ts
Normal file
328
src/lib/unraid/queries/mutations.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Unraid GraphQL Mutations
|
||||
* Based on Unraid API v4.29.2
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const ARRAY_SET_STATE_MUTATION = `
|
||||
mutation ArraySetState($state: String!) {
|
||||
array {
|
||||
setState(state: $state) {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ARRAY_ADD_DISK_MUTATION = `
|
||||
mutation ArrayAddDisk($slot: String!, $id: String!) {
|
||||
array {
|
||||
addDiskToArray(slot: $slot, id: $id) {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ARRAY_REMOVE_DISK_MUTATION = `
|
||||
mutation ArrayRemoveDisk($slot: String!) {
|
||||
array {
|
||||
removeDiskFromArray(slot: $slot) {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ARRAY_MOUNT_DISK_MUTATION = `
|
||||
mutation ArrayMountDisk($id: String!) {
|
||||
array {
|
||||
mountArrayDisk(id: $id) {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ARRAY_UNMOUNT_DISK_MUTATION = `
|
||||
mutation ArrayUnmountDisk($id: String!) {
|
||||
array {
|
||||
unmountArrayDisk(id: $id) {
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// PARITY CHECK MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const PARITY_CHECK_START_MUTATION = `
|
||||
mutation ParityCheckStart($correct: Boolean) {
|
||||
parityCheck {
|
||||
start(correct: $correct) {
|
||||
running
|
||||
progress
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARITY_CHECK_PAUSE_MUTATION = `
|
||||
mutation ParityCheckPause {
|
||||
parityCheck {
|
||||
pause {
|
||||
running
|
||||
progress
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARITY_CHECK_RESUME_MUTATION = `
|
||||
mutation ParityCheckResume {
|
||||
parityCheck {
|
||||
resume {
|
||||
running
|
||||
progress
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARITY_CHECK_CANCEL_MUTATION = `
|
||||
mutation ParityCheckCancel {
|
||||
parityCheck {
|
||||
cancel {
|
||||
running
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const DOCKER_START_MUTATION = `
|
||||
mutation DockerStart($id: String!) {
|
||||
docker {
|
||||
start(id: $id) {
|
||||
id
|
||||
state
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DOCKER_STOP_MUTATION = `
|
||||
mutation DockerStop($id: String!) {
|
||||
docker {
|
||||
stop(id: $id) {
|
||||
id
|
||||
state
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// VM MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const VM_START_MUTATION = `
|
||||
mutation VmStart($id: String!) {
|
||||
vm {
|
||||
start(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM_STOP_MUTATION = `
|
||||
mutation VmStop($id: String!) {
|
||||
vm {
|
||||
stop(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM_PAUSE_MUTATION = `
|
||||
mutation VmPause($id: String!) {
|
||||
vm {
|
||||
pause(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM_RESUME_MUTATION = `
|
||||
mutation VmResume($id: String!) {
|
||||
vm {
|
||||
resume(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM_FORCE_STOP_MUTATION = `
|
||||
mutation VmForceStop($id: String!) {
|
||||
vm {
|
||||
forceStop(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM_REBOOT_MUTATION = `
|
||||
mutation VmReboot($id: String!) {
|
||||
vm {
|
||||
reboot(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const VM_RESET_MUTATION = `
|
||||
mutation VmReset($id: String!) {
|
||||
vm {
|
||||
reset(id: $id) {
|
||||
id
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATION MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const NOTIFICATION_CREATE_MUTATION = `
|
||||
mutation NotificationCreate($input: CreateNotificationInput!) {
|
||||
notification {
|
||||
create(input: $input) {
|
||||
id
|
||||
title
|
||||
subject
|
||||
description
|
||||
importance
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NOTIFICATION_DELETE_MUTATION = `
|
||||
mutation NotificationDelete($id: String!) {
|
||||
notification {
|
||||
delete(id: $id)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NOTIFICATION_ARCHIVE_MUTATION = `
|
||||
mutation NotificationArchive($id: String!) {
|
||||
notification {
|
||||
archive(id: $id) {
|
||||
id
|
||||
archived
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NOTIFICATION_UNREAD_MUTATION = `
|
||||
mutation NotificationUnread($id: String!) {
|
||||
notification {
|
||||
unread(id: $id) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOMIZATION MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const SET_THEME_MUTATION = `
|
||||
mutation SetTheme($theme: ThemeName!) {
|
||||
customization {
|
||||
setTheme(theme: $theme) {
|
||||
theme
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// API KEY MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const API_KEY_CREATE_MUTATION = `
|
||||
mutation ApiKeyCreate($name: String!, $description: String) {
|
||||
apiKey {
|
||||
create(name: $name, description: $description) {
|
||||
id
|
||||
name
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const API_KEY_DELETE_MUTATION = `
|
||||
mutation ApiKeyDelete($id: String!) {
|
||||
apiKey {
|
||||
delete(id: $id)
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// CONNECT MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
export const CONNECT_SIGN_IN_MUTATION = `
|
||||
mutation ConnectSignIn {
|
||||
connectSignIn {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CONNECT_SIGN_OUT_MUTATION = `
|
||||
mutation ConnectSignOut {
|
||||
connectSignOut {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SETUP_REMOTE_ACCESS_MUTATION = `
|
||||
mutation SetupRemoteAccess($enable: Boolean!) {
|
||||
setupRemoteAccess(enable: $enable) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
164
src/lib/unraid/queries/subscriptions.ts
Normal file
164
src/lib/unraid/queries/subscriptions.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Unraid GraphQL Subscriptions
|
||||
* For real-time updates via WebSocket
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM METRICS SUBSCRIPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const CPU_METRICS_SUBSCRIPTION = `
|
||||
subscription CpuMetrics {
|
||||
systemMetricsCpu {
|
||||
cores
|
||||
average
|
||||
temperature
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CPU_TELEMETRY_SUBSCRIPTION = `
|
||||
subscription CpuTelemetry {
|
||||
systemMetricsCpuTelemetry {
|
||||
cores
|
||||
average
|
||||
temperature
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MEMORY_METRICS_SUBSCRIPTION = `
|
||||
subscription MemoryMetrics {
|
||||
systemMetricsMemory {
|
||||
total
|
||||
used
|
||||
free
|
||||
cached
|
||||
buffers
|
||||
percent
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY SUBSCRIPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const ARRAY_SUBSCRIPTION = `
|
||||
subscription ArrayUpdates {
|
||||
arraySubscription {
|
||||
state
|
||||
capacity {
|
||||
total
|
||||
used
|
||||
free
|
||||
}
|
||||
disks {
|
||||
id
|
||||
name
|
||||
status
|
||||
temp
|
||||
spunDown
|
||||
}
|
||||
parityCheckStatus {
|
||||
running
|
||||
progress
|
||||
errors
|
||||
eta
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARITY_HISTORY_SUBSCRIPTION = `
|
||||
subscription ParityHistory {
|
||||
parityHistorySubscription {
|
||||
date
|
||||
duration
|
||||
errors
|
||||
speed
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATION SUBSCRIPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const NOTIFICATION_ADDED_SUBSCRIPTION = `
|
||||
subscription NotificationAdded {
|
||||
notificationAdded {
|
||||
id
|
||||
title
|
||||
subject
|
||||
description
|
||||
importance
|
||||
type
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const NOTIFICATIONS_OVERVIEW_SUBSCRIPTION = `
|
||||
subscription NotificationsOverview {
|
||||
notificationsOverview {
|
||||
total
|
||||
unread
|
||||
alerts
|
||||
warnings
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// LOG SUBSCRIPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const LOG_FILE_SUBSCRIPTION = `
|
||||
subscription LogFile($path: String!) {
|
||||
logFile(path: $path) {
|
||||
line
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// UPS SUBSCRIPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const UPS_UPDATES_SUBSCRIPTION = `
|
||||
subscription UpsUpdates {
|
||||
upsUpdates {
|
||||
id
|
||||
status
|
||||
batteryCharge
|
||||
batteryRuntime
|
||||
load
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ============================================================================
|
||||
// SERVER SUBSCRIPTIONS
|
||||
// ============================================================================
|
||||
|
||||
export const OWNER_SUBSCRIPTION = `
|
||||
subscription OwnerUpdates {
|
||||
ownerSubscription {
|
||||
email
|
||||
username
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SERVERS_SUBSCRIPTION = `
|
||||
subscription ServersUpdates {
|
||||
serversSubscription {
|
||||
guid
|
||||
name
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
510
src/lib/unraid/types.ts
Normal file
510
src/lib/unraid/types.ts
Normal file
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Unraid GraphQL API Types
|
||||
* Based on live introspection from Unraid API v4.29.2
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// ENUMS
|
||||
// ============================================================================
|
||||
|
||||
export enum ArrayState {
|
||||
STARTED = 'STARTED',
|
||||
STOPPED = 'STOPPED',
|
||||
NEW_ARRAY = 'NEW_ARRAY',
|
||||
RECON_DISK = 'RECON_DISK',
|
||||
DISABLE_DISK = 'DISABLE_DISK',
|
||||
SWAP_DSBL = 'SWAP_DSBL',
|
||||
INVALID_EXPANSION = 'INVALID_EXPANSION',
|
||||
}
|
||||
|
||||
export enum ArrayDiskStatus {
|
||||
DISK_NP = 'DISK_NP',
|
||||
DISK_OK = 'DISK_OK',
|
||||
DISK_INVALID = 'DISK_INVALID',
|
||||
DISK_WRONG = 'DISK_WRONG',
|
||||
DISK_DSBL = 'DISK_DSBL',
|
||||
DISK_DSBL_NEW = 'DISK_DSBL_NEW',
|
||||
DISK_NEW = 'DISK_NEW',
|
||||
}
|
||||
|
||||
export enum ArrayDiskType {
|
||||
DATA = 'DATA',
|
||||
PARITY = 'PARITY',
|
||||
FLASH = 'FLASH',
|
||||
CACHE = 'CACHE',
|
||||
}
|
||||
|
||||
export enum ContainerState {
|
||||
RUNNING = 'RUNNING',
|
||||
EXITED = 'EXITED',
|
||||
PAUSED = 'PAUSED',
|
||||
RESTARTING = 'RESTARTING',
|
||||
CREATED = 'CREATED',
|
||||
DEAD = 'DEAD',
|
||||
}
|
||||
|
||||
export enum VmState {
|
||||
NOSTATE = 'NOSTATE',
|
||||
RUNNING = 'RUNNING',
|
||||
IDLE = 'IDLE',
|
||||
PAUSED = 'PAUSED',
|
||||
SHUTDOWN = 'SHUTDOWN',
|
||||
SHUTOFF = 'SHUTOFF',
|
||||
CRASHED = 'CRASHED',
|
||||
PMSUSPENDED = 'PMSUSPENDED',
|
||||
}
|
||||
|
||||
export enum DiskFsType {
|
||||
XFS = 'XFS',
|
||||
BTRFS = 'BTRFS',
|
||||
VFAT = 'VFAT',
|
||||
ZFS = 'ZFS',
|
||||
EXT4 = 'EXT4',
|
||||
NTFS = 'NTFS',
|
||||
}
|
||||
|
||||
export enum DiskInterfaceType {
|
||||
SAS = 'SAS',
|
||||
SATA = 'SATA',
|
||||
USB = 'USB',
|
||||
PCIE = 'PCIE',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
||||
export enum ThemeName {
|
||||
AZURE = 'azure',
|
||||
BLACK = 'black',
|
||||
GRAY = 'gray',
|
||||
WHITE = 'white',
|
||||
}
|
||||
|
||||
export enum NotificationImportance {
|
||||
NORMAL = 'NORMAL',
|
||||
WARNING = 'WARNING',
|
||||
ALERT = 'ALERT',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM INFO TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface SystemInfo {
|
||||
cpu: CpuInfo;
|
||||
memory: MemoryInfo;
|
||||
os: OsInfo;
|
||||
baseboard: BaseboardInfo;
|
||||
versions: VersionInfo;
|
||||
}
|
||||
|
||||
export interface CpuInfo {
|
||||
manufacturer: string;
|
||||
brand: string;
|
||||
cores: number;
|
||||
threads: number;
|
||||
speed: number;
|
||||
speedMax: number;
|
||||
cache: CpuCache;
|
||||
}
|
||||
|
||||
export interface CpuCache {
|
||||
l1d: number;
|
||||
l1i: number;
|
||||
l2: number;
|
||||
l3: number;
|
||||
}
|
||||
|
||||
export interface MemoryInfo {
|
||||
total: number;
|
||||
free: number;
|
||||
used: number;
|
||||
active: number;
|
||||
available: number;
|
||||
buffers: number;
|
||||
cached: number;
|
||||
slab: number;
|
||||
swapTotal: number;
|
||||
swapUsed: number;
|
||||
swapFree: number;
|
||||
}
|
||||
|
||||
export interface OsInfo {
|
||||
platform: string;
|
||||
distro: string;
|
||||
release: string;
|
||||
kernel: string;
|
||||
arch: string;
|
||||
hostname: string;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export interface BaseboardInfo {
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
version: string;
|
||||
serial: string;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
unraid: string;
|
||||
api: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface UnraidArray {
|
||||
state: ArrayState;
|
||||
capacity: ArrayCapacity;
|
||||
disks: ArrayDisk[];
|
||||
parities: ArrayDisk[];
|
||||
caches: ArrayCache[];
|
||||
parityCheckStatus: ParityCheckStatus | null;
|
||||
}
|
||||
|
||||
export interface ArrayCapacity {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
disks: {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ArrayDisk {
|
||||
id: string;
|
||||
name: string;
|
||||
device: string;
|
||||
size: number;
|
||||
status: ArrayDiskStatus;
|
||||
type: ArrayDiskType;
|
||||
temp: number | null;
|
||||
numReads: number;
|
||||
numWrites: number;
|
||||
numErrors: number;
|
||||
fsType: DiskFsType | null;
|
||||
fsFree: number | null;
|
||||
fsUsed: number | null;
|
||||
fsSize: number | null;
|
||||
color: string;
|
||||
spunDown: boolean;
|
||||
transport: DiskInterfaceType;
|
||||
rotational: boolean;
|
||||
serial: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface ArrayCache {
|
||||
id: string;
|
||||
name: string;
|
||||
devices: ArrayDisk[];
|
||||
fsType: DiskFsType;
|
||||
fsFree: number;
|
||||
fsUsed: number;
|
||||
fsSize: number;
|
||||
}
|
||||
|
||||
export interface ParityCheckStatus {
|
||||
running: boolean;
|
||||
progress: number;
|
||||
errors: number;
|
||||
elapsed: number;
|
||||
eta: number;
|
||||
speed: number;
|
||||
mode: 'check' | 'correct';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface DockerContainer {
|
||||
id: string;
|
||||
names: string[];
|
||||
image: string;
|
||||
state: ContainerState;
|
||||
status: string;
|
||||
created: number;
|
||||
ports: ContainerPort[];
|
||||
autoStart: boolean;
|
||||
networkMode: string;
|
||||
cpuPercent?: number;
|
||||
memoryUsage?: number;
|
||||
memoryLimit?: number;
|
||||
}
|
||||
|
||||
export interface ContainerPort {
|
||||
privatePort: number;
|
||||
publicPort?: number;
|
||||
type: 'tcp' | 'udp';
|
||||
ip?: string;
|
||||
}
|
||||
|
||||
export interface DockerNetwork {
|
||||
id: string;
|
||||
name: string;
|
||||
driver: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface Docker {
|
||||
containers: DockerContainer[];
|
||||
networks: DockerNetwork[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VM TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface VirtualMachine {
|
||||
id: string;
|
||||
name: string;
|
||||
state: VmState;
|
||||
uuid: string;
|
||||
description?: string;
|
||||
cpus: number;
|
||||
memory: number;
|
||||
autoStart: boolean;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface Vms {
|
||||
vms: VirtualMachine[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SHARES TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface Share {
|
||||
name: string;
|
||||
comment: string;
|
||||
free: number;
|
||||
used: number;
|
||||
size: number;
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
cache: string;
|
||||
color: string;
|
||||
floor: number;
|
||||
splitLevel: number;
|
||||
allocator: 'highwater' | 'fillup' | 'mostfree';
|
||||
export: string;
|
||||
security: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATION TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
title: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
importance: NotificationImportance;
|
||||
type: string;
|
||||
timestamp: string;
|
||||
read: boolean;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DISK TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface Disk {
|
||||
id: string;
|
||||
name: string;
|
||||
device: string;
|
||||
size: number;
|
||||
vendor: string;
|
||||
model: string;
|
||||
serial: string;
|
||||
firmware: string;
|
||||
type: string;
|
||||
interfaceType: DiskInterfaceType;
|
||||
rotational: boolean;
|
||||
temp: number | null;
|
||||
smartStatus: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVICES TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface Service {
|
||||
name: string;
|
||||
online: boolean;
|
||||
uptime: number | null;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVER VARIABLES
|
||||
// ============================================================================
|
||||
|
||||
export interface ServerVars {
|
||||
version: string;
|
||||
name: string;
|
||||
timezone: string;
|
||||
description: string;
|
||||
model: string;
|
||||
protocol: string;
|
||||
port: number;
|
||||
localTld: string;
|
||||
csrf: string;
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FLASH DRIVE
|
||||
// ============================================================================
|
||||
|
||||
export interface Flash {
|
||||
guid: string;
|
||||
vendor: string;
|
||||
product: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REGISTRATION / LICENSE
|
||||
// ============================================================================
|
||||
|
||||
export interface Registration {
|
||||
type: 'Basic' | 'Plus' | 'Pro' | 'Lifetime' | 'Trial' | 'Expired';
|
||||
state: string;
|
||||
keyFile: string | null;
|
||||
expiration: string | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK
|
||||
// ============================================================================
|
||||
|
||||
export interface Network {
|
||||
accessUrls: AccessUrl[];
|
||||
}
|
||||
|
||||
export interface AccessUrl {
|
||||
type: string;
|
||||
name: string;
|
||||
ipv4: string | null;
|
||||
ipv6: string | null;
|
||||
url: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVER
|
||||
// ============================================================================
|
||||
|
||||
export interface Server {
|
||||
owner: string | null;
|
||||
guid: string;
|
||||
wanip: string | null;
|
||||
lanip: string;
|
||||
localurl: string;
|
||||
remoteurl: string | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UPS
|
||||
// ============================================================================
|
||||
|
||||
export interface UpsDevice {
|
||||
id: string;
|
||||
name: string;
|
||||
model: string;
|
||||
status: string;
|
||||
batteryCharge: number;
|
||||
batteryRuntime: number;
|
||||
load: number;
|
||||
inputVoltage: number;
|
||||
outputVoltage: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGINS
|
||||
// ============================================================================
|
||||
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
url: string;
|
||||
icon: string | null;
|
||||
updateAvailable: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOMIZATION
|
||||
// ============================================================================
|
||||
|
||||
export interface Customization {
|
||||
theme: ThemeName;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API RESPONSE WRAPPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface UnraidApiResponse<T> {
|
||||
data: T;
|
||||
errors?: Array<{
|
||||
message: string;
|
||||
path?: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REAL-TIME METRICS (Subscriptions)
|
||||
// ============================================================================
|
||||
|
||||
export interface CpuMetrics {
|
||||
cores: number[];
|
||||
average: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface MemoryMetrics {
|
||||
total: number;
|
||||
used: number;
|
||||
free: number;
|
||||
cached: number;
|
||||
buffers: number;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MUTATION INPUTS
|
||||
// ============================================================================
|
||||
|
||||
export interface ArraySetStateInput {
|
||||
state: 'start' | 'stop';
|
||||
}
|
||||
|
||||
export interface DockerControlInput {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface VmPowerInput {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ParityCheckInput {
|
||||
action: 'start' | 'pause' | 'resume' | 'cancel';
|
||||
mode?: 'check' | 'correct';
|
||||
}
|
||||
|
||||
export interface NotificationInput {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CreateNotificationInput {
|
||||
title: string;
|
||||
subject: string;
|
||||
description: string;
|
||||
importance: NotificationImportance;
|
||||
}
|
||||
346
src/pages/unraid/index.tsx
Normal file
346
src/pages/unraid/index.tsx
Normal 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,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { tdarrRouter } from '~/server/api/routers/tdarr';
|
||||
import { unraidRouter } from '~/server/api/routers/unraid/router';
|
||||
import { createTRPCRouter } from '~/server/api/trpc';
|
||||
|
||||
import { appRouter } from './routers/app';
|
||||
@@ -55,6 +56,7 @@ export const rootRouter = createTRPCRouter({
|
||||
healthMonitoring: healthMonitoringRouter,
|
||||
tdarr: tdarrRouter,
|
||||
migrate: migrateRouter,
|
||||
unraid: unraidRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
399
src/server/api/routers/unraid/router.ts
Normal file
399
src/server/api/routers/unraid/router.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* Unraid tRPC Router
|
||||
* Provides API endpoints for Unraid management
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '~/server/api/trpc';
|
||||
import { getUnraidClient, createUnraidClient } from '~/lib/unraid';
|
||||
import type { UnraidClientConfig } from '~/lib/unraid';
|
||||
|
||||
// Input schemas
|
||||
const containerIdSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const vmIdSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const parityCheckSchema = z.object({
|
||||
correct: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
const notificationIdSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const diskIdSchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const connectionTestSchema = z.object({
|
||||
host: z.string(),
|
||||
apiKey: z.string(),
|
||||
useSsl: z.boolean().optional().default(false),
|
||||
port: z.number().optional(),
|
||||
});
|
||||
|
||||
export const unraidRouter = createTRPCRouter({
|
||||
// ============================================================================
|
||||
// CONNECTION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Test connection to Unraid server
|
||||
*/
|
||||
testConnection: publicProcedure.input(connectionTestSchema).mutation(async ({ input }) => {
|
||||
try {
|
||||
const client = createUnraidClient(input as UnraidClientConfig);
|
||||
const healthy = await client.healthCheck();
|
||||
return { success: healthy, error: null };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Check if Unraid is configured
|
||||
*/
|
||||
isConfigured: publicProcedure.query(() => {
|
||||
return {
|
||||
configured: !!(process.env.UNRAID_HOST && process.env.UNRAID_API_KEY),
|
||||
};
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// DASHBOARD / OVERVIEW
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get complete dashboard data
|
||||
*/
|
||||
dashboard: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getDashboard();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM INFO
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get system information
|
||||
*/
|
||||
info: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getInfo();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get server variables
|
||||
*/
|
||||
vars: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getVars();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get server details
|
||||
*/
|
||||
server: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getServer();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get registration/license info
|
||||
*/
|
||||
registration: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getRegistration();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get flash drive info
|
||||
*/
|
||||
flash: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getFlash();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get array status
|
||||
*/
|
||||
array: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getArray();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Start array
|
||||
*/
|
||||
startArray: protectedProcedure.mutation(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.startArray();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stop array
|
||||
*/
|
||||
stopArray: protectedProcedure.mutation(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.stopArray();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// DISKS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get all disks
|
||||
*/
|
||||
disks: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getDisks();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get single disk by ID
|
||||
*/
|
||||
disk: protectedProcedure.input(diskIdSchema).query(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.getDisk(input.id);
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// PARITY CHECK
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Start parity check
|
||||
*/
|
||||
startParityCheck: protectedProcedure.input(parityCheckSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.startParityCheck(input.correct);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Pause parity check
|
||||
*/
|
||||
pauseParityCheck: protectedProcedure.mutation(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.pauseParityCheck();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Resume parity check
|
||||
*/
|
||||
resumeParityCheck: protectedProcedure.mutation(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.resumeParityCheck();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Cancel parity check
|
||||
*/
|
||||
cancelParityCheck: protectedProcedure.mutation(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.cancelParityCheck();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get Docker containers and networks
|
||||
*/
|
||||
docker: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getDocker();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Start container
|
||||
*/
|
||||
startContainer: protectedProcedure.input(containerIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.startContainer(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stop container
|
||||
*/
|
||||
stopContainer: protectedProcedure.input(containerIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.stopContainer(input.id);
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// VMS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get virtual machines
|
||||
*/
|
||||
vms: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getVms();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Start VM
|
||||
*/
|
||||
startVm: protectedProcedure.input(vmIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.startVm(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Stop VM (graceful)
|
||||
*/
|
||||
stopVm: protectedProcedure.input(vmIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.stopVm(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Pause VM
|
||||
*/
|
||||
pauseVm: protectedProcedure.input(vmIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.pauseVm(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Resume VM
|
||||
*/
|
||||
resumeVm: protectedProcedure.input(vmIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.resumeVm(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Force stop VM
|
||||
*/
|
||||
forceStopVm: protectedProcedure.input(vmIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.forceStopVm(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Reboot VM
|
||||
*/
|
||||
rebootVm: protectedProcedure.input(vmIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.rebootVm(input.id);
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// SHARES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get shares
|
||||
*/
|
||||
shares: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getShares();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get notifications
|
||||
*/
|
||||
notifications: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getNotifications();
|
||||
}),
|
||||
|
||||
/**
|
||||
* Delete notification
|
||||
*/
|
||||
deleteNotification: protectedProcedure.input(notificationIdSchema).mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.deleteNotification(input.id);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Archive notification
|
||||
*/
|
||||
archiveNotification: protectedProcedure
|
||||
.input(notificationIdSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const client = getUnraidClient();
|
||||
return client.archiveNotification(input.id);
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// SERVICES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get services
|
||||
*/
|
||||
services: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getServices();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get network info
|
||||
*/
|
||||
network: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getNetwork();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// UPS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get UPS devices
|
||||
*/
|
||||
upsDevices: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getUpsDevices();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// PLUGINS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get installed plugins
|
||||
*/
|
||||
plugins: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getPlugins();
|
||||
}),
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOMIZATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get customization settings
|
||||
*/
|
||||
customization: protectedProcedure.query(async () => {
|
||||
const client = getUnraidClient();
|
||||
return client.getCustomization();
|
||||
}),
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
@import 'fily-publish-gridstack/dist/gridstack.min.css';
|
||||
@import './orchis/variables.css';
|
||||
|
||||
:root {
|
||||
--gridstack-widget-width: 64;
|
||||
|
||||
518
src/styles/orchis/theme.ts
Normal file
518
src/styles/orchis/theme.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
/**
|
||||
* Orchis Theme for Mantine
|
||||
* Based on the Orchis GTK Theme by vinceliuice
|
||||
* https://github.com/vinceliuice/Orchis-theme
|
||||
*/
|
||||
|
||||
import { MantineProviderProps, MantineThemeColors, Tuple } from '@mantine/core';
|
||||
|
||||
// ============================================================================
|
||||
// ORCHIS COLOR PALETTE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Orchis primary blue - the default accent color
|
||||
* Matches Google's Material Design blue
|
||||
*/
|
||||
const orchisBlue: Tuple<string, 10> = [
|
||||
'#E3F2FD', // 0 - lightest
|
||||
'#BBDEFB', // 1
|
||||
'#90CAF9', // 2
|
||||
'#64B5F6', // 3
|
||||
'#42A5F5', // 4
|
||||
'#2196F3', // 5
|
||||
'#1E88E5', // 6 - primary dark
|
||||
'#1976D2', // 7
|
||||
'#1565C0', // 8
|
||||
'#1A73E8', // 9 - Orchis primary
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis grey scale for surfaces
|
||||
*/
|
||||
const orchisGrey: Tuple<string, 10> = [
|
||||
'#FAFAFA', // 0 - grey-050
|
||||
'#F2F2F2', // 1 - grey-100
|
||||
'#EEEEEE', // 2 - grey-150
|
||||
'#DDDDDD', // 3 - grey-200
|
||||
'#BFBFBF', // 4 - grey-300
|
||||
'#9E9E9E', // 5 - grey-400
|
||||
'#727272', // 6 - grey-500
|
||||
'#464646', // 7 - grey-600
|
||||
'#2C2C2C', // 8 - grey-700
|
||||
'#212121', // 9 - grey-800
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis dark grey for dark mode
|
||||
*/
|
||||
const orchisDark: Tuple<string, 10> = [
|
||||
'#3C3C3C', // 0 - surface
|
||||
'#333333', // 1
|
||||
'#2C2C2C', // 2 - base
|
||||
'#262626', // 3
|
||||
'#242424', // 4 - base-alt
|
||||
'#212121', // 5 - background
|
||||
'#1A1A1A', // 6
|
||||
'#121212', // 7
|
||||
'#0F0F0F', // 8
|
||||
'#030303', // 9
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis red for errors/destructive
|
||||
*/
|
||||
const orchisRed: Tuple<string, 10> = [
|
||||
'#FFEBEE',
|
||||
'#FFCDD2',
|
||||
'#EF9A9A',
|
||||
'#E57373',
|
||||
'#EF5350',
|
||||
'#F44336',
|
||||
'#E53935', // Orchis error
|
||||
'#D32F2F',
|
||||
'#C62828',
|
||||
'#B71C1C',
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis green for success
|
||||
*/
|
||||
const orchisGreen: Tuple<string, 10> = [
|
||||
'#E8F5E9',
|
||||
'#C8E6C9',
|
||||
'#A5D6A7',
|
||||
'#81C995', // Sea light
|
||||
'#66BB6A',
|
||||
'#4CAF50',
|
||||
'#43A047',
|
||||
'#388E3C',
|
||||
'#0F9D58', // Sea dark (success)
|
||||
'#1B5E20',
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis yellow for warnings
|
||||
*/
|
||||
const orchisYellow: Tuple<string, 10> = [
|
||||
'#FFFDE7',
|
||||
'#FFF9C4',
|
||||
'#FFF59D',
|
||||
'#FFF176',
|
||||
'#FFEE58',
|
||||
'#FFEB3B',
|
||||
'#FDD835',
|
||||
'#FBC02D', // Yellow light
|
||||
'#F9A825',
|
||||
'#FFD600', // Yellow dark (warning)
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis purple for accents
|
||||
*/
|
||||
const orchisPurple: Tuple<string, 10> = [
|
||||
'#F3E5F5',
|
||||
'#E1BEE7',
|
||||
'#CE93D8',
|
||||
'#BA68C8', // Purple light
|
||||
'#AB47BC', // Purple dark
|
||||
'#9C27B0',
|
||||
'#8E24AA',
|
||||
'#7B1FA2',
|
||||
'#6A1B9A',
|
||||
'#4A148C',
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis teal for secondary accents
|
||||
*/
|
||||
const orchisTeal: Tuple<string, 10> = [
|
||||
'#E0F2F1',
|
||||
'#B2DFDB',
|
||||
'#80CBC4',
|
||||
'#4DB6AC',
|
||||
'#26A69A',
|
||||
'#009688',
|
||||
'#00897B',
|
||||
'#00796B',
|
||||
'#00695C',
|
||||
'#004D40',
|
||||
];
|
||||
|
||||
/**
|
||||
* Orchis orange for highlights
|
||||
*/
|
||||
const orchisOrange: Tuple<string, 10> = [
|
||||
'#FFF3E0',
|
||||
'#FFE0B2',
|
||||
'#FFCC80',
|
||||
'#FFB74D',
|
||||
'#FFA726',
|
||||
'#FF9800',
|
||||
'#FB8C00',
|
||||
'#F57C00',
|
||||
'#EF6C00',
|
||||
'#E65100',
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// MANTINE THEME CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
export const orchisColors: MantineThemeColors = {
|
||||
// Override default colors with Orchis palette
|
||||
blue: orchisBlue,
|
||||
gray: orchisGrey,
|
||||
dark: orchisDark,
|
||||
red: orchisRed,
|
||||
green: orchisGreen,
|
||||
yellow: orchisYellow,
|
||||
violet: orchisPurple,
|
||||
teal: orchisTeal,
|
||||
orange: orchisOrange,
|
||||
|
||||
// Custom Orchis-specific colors
|
||||
orchis: orchisBlue,
|
||||
surface: orchisGrey,
|
||||
};
|
||||
|
||||
export const orchisTheme: MantineProviderProps['theme'] = {
|
||||
// Color configuration
|
||||
colors: orchisColors,
|
||||
primaryColor: 'blue',
|
||||
primaryShade: { light: 9, dark: 5 },
|
||||
|
||||
// Typography
|
||||
fontFamily: '"M+ 1c", Roboto, Cantarell, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
fontFamilyMonospace: '"JetBrains Mono", "Fira Code", Consolas, monospace',
|
||||
headings: {
|
||||
fontFamily: 'Roboto, "M+ 1c", Cantarell, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
fontWeight: 700,
|
||||
},
|
||||
|
||||
// Border radius - Orchis uses 12px as default
|
||||
radius: {
|
||||
xs: 4,
|
||||
sm: 6,
|
||||
md: 12, // Default Orchis corner radius
|
||||
lg: 18, // Window radius
|
||||
xl: 24,
|
||||
},
|
||||
defaultRadius: 'md',
|
||||
|
||||
// Spacing - Orchis base is 6px
|
||||
spacing: {
|
||||
xs: 4,
|
||||
sm: 6,
|
||||
md: 12,
|
||||
lg: 18,
|
||||
xl: 24,
|
||||
},
|
||||
|
||||
// Shadows - Material Design elevation
|
||||
shadows: {
|
||||
xs: '0 1px 2px rgba(0,0,0,0.17)',
|
||||
sm: '0 2px 2px -2px rgba(0,0,0,0.3), 0 1px 2px -1px rgba(0,0,0,0.24), 0 1px 2px -1px rgba(0,0,0,0.17)',
|
||||
md: '0 3px 3px -2px rgba(0,0,0,0.2), 0 3px 3px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12)',
|
||||
lg: '0 3px 3px -1px rgba(0,0,0,0.2), 0 6px 6px 0 rgba(0,0,0,0.14), 0 1px 11px 0 rgba(0,0,0,0.12)',
|
||||
xl: '0 8px 6px -5px rgba(0,0,0,0.2), 0 16px 16px 2px rgba(0,0,0,0.14), 0 6px 18px 5px rgba(0,0,0,0.12)',
|
||||
},
|
||||
|
||||
// Transitions - Orchis uses 75ms base duration
|
||||
transitionTimingFunction: 'cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
|
||||
// Other
|
||||
cursorType: 'pointer',
|
||||
focusRing: 'auto',
|
||||
respectReducedMotion: true,
|
||||
white: '#FFFFFF',
|
||||
black: '#000000',
|
||||
|
||||
// Global styles
|
||||
globalStyles: (theme) => ({
|
||||
'*, *::before, *::after': {
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
color: theme.colorScheme === 'dark' ? theme.white : 'rgba(0, 0, 0, 0.87)',
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
// Orchis-style focus ring
|
||||
':focus-visible': {
|
||||
outline: `2px solid ${theme.colors.blue[theme.colorScheme === 'dark' ? 5 : 9]}`,
|
||||
outlineOffset: 2,
|
||||
},
|
||||
}),
|
||||
|
||||
// Component overrides for Orchis styling
|
||||
components: {
|
||||
Button: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
fontWeight: 500,
|
||||
transition: 'all 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Card: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
p: 'md',
|
||||
},
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.white,
|
||||
border: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'
|
||||
}`,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Paper: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.white,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Input: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
styles: (theme) => ({
|
||||
input: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)',
|
||||
border: 'none',
|
||||
transition: 'all 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
'&:hover': {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
|
||||
},
|
||||
'&:focus': {
|
||||
outline: `2px solid ${theme.colors.blue[theme.colorScheme === 'dark' ? 5 : 9]}`,
|
||||
outlineOffset: -2,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
TextInput: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
Select: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
Modal: {
|
||||
defaultProps: {
|
||||
radius: 'lg',
|
||||
overlayProps: {
|
||||
blur: 3,
|
||||
},
|
||||
},
|
||||
styles: (theme) => ({
|
||||
content: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.white,
|
||||
},
|
||||
header: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Menu: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
shadow: 'lg',
|
||||
},
|
||||
styles: (theme) => ({
|
||||
dropdown: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.white,
|
||||
border: 'none',
|
||||
},
|
||||
item: {
|
||||
borderRadius: 5, // menuitem-radius
|
||||
transition: 'background-color 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Tooltip: {
|
||||
defaultProps: {
|
||||
radius: 6,
|
||||
},
|
||||
styles: () => ({
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Tabs: {
|
||||
styles: (theme) => ({
|
||||
tab: {
|
||||
fontWeight: 500,
|
||||
transition: 'all 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
'&[data-active]': {
|
||||
borderColor: theme.colors.blue[theme.colorScheme === 'dark' ? 5 : 9],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Switch: {
|
||||
styles: (theme) => ({
|
||||
track: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.26)',
|
||||
transition: 'background-color 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
},
|
||||
thumb: {
|
||||
boxShadow:
|
||||
'0 2px 2px -2px rgba(0,0,0,0.3), 0 1px 2px -1px rgba(0,0,0,0.24), 0 1px 2px -1px rgba(0,0,0,0.17)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Progress: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
size: 6, // Orchis bar-size
|
||||
},
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
NavLink: {
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
borderRadius: '0 9999px 9999px 0', // Orchis sidebar row style
|
||||
fontWeight: 500,
|
||||
transition: 'all 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
'&[data-active]': {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
||||
color: theme.colors.blue[theme.colorScheme === 'dark' ? 5 : 9],
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Notification: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
Badge: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
Checkbox: {
|
||||
styles: (theme) => ({
|
||||
input: {
|
||||
cursor: 'pointer',
|
||||
borderRadius: 4,
|
||||
transition: 'all 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
},
|
||||
label: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Divider: {
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
borderColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Table: {
|
||||
styles: (theme) => ({
|
||||
root: {
|
||||
'& thead tr th': {
|
||||
color: theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.6)',
|
||||
fontWeight: 'normal',
|
||||
borderBottom: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'
|
||||
}`,
|
||||
},
|
||||
'& tbody tr td': {
|
||||
borderBottom: `1px solid ${
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)'
|
||||
}`,
|
||||
},
|
||||
'& tbody tr:hover': {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
ActionIcon: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
styles: () => ({
|
||||
root: {
|
||||
transition: 'all 75ms cubic-bezier(0.0, 0.0, 0.2, 1)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
Accordion: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
Alert: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
Popover: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
shadow: 'lg',
|
||||
},
|
||||
},
|
||||
|
||||
HoverCard: {
|
||||
defaultProps: {
|
||||
radius: 'md',
|
||||
shadow: 'lg',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default orchisTheme;
|
||||
155
src/styles/orchis/variables.css
Normal file
155
src/styles/orchis/variables.css
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Orchis Theme CSS Variables
|
||||
* Based on the Orchis GTK Theme by vinceliuice
|
||||
* https://github.com/vinceliuice/Orchis-theme
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ---- Primary / Accent ---- */
|
||||
--orchis-primary: #1A73E8;
|
||||
--orchis-primary-light: #3281EA;
|
||||
--orchis-on-primary: #FFFFFF;
|
||||
--orchis-on-primary-secondary: rgba(255, 255, 255, 0.7);
|
||||
|
||||
/* ---- Backgrounds (Light Mode) ---- */
|
||||
--orchis-background: #F2F2F2;
|
||||
--orchis-surface: #FFFFFF;
|
||||
--orchis-base: #FFFFFF;
|
||||
--orchis-base-alt: #FAFAFA;
|
||||
|
||||
/* ---- Text (Light Mode) ---- */
|
||||
--orchis-text: rgba(0, 0, 0, 0.87);
|
||||
--orchis-text-secondary: rgba(0, 0, 0, 0.6);
|
||||
--orchis-text-disabled: rgba(0, 0, 0, 0.38);
|
||||
|
||||
/* ---- Semantic Colors ---- */
|
||||
--orchis-error: #E53935;
|
||||
--orchis-warning: #FFD600;
|
||||
--orchis-success: #0F9D58;
|
||||
--orchis-info: #1A73E8;
|
||||
--orchis-link: #1A73E8;
|
||||
--orchis-link-visited: #AB47BC;
|
||||
|
||||
/* ---- Borders ---- */
|
||||
--orchis-border: rgba(0, 0, 0, 0.12);
|
||||
--orchis-border-solid: #E2E2E2;
|
||||
--orchis-divider: rgba(0, 0, 0, 0.12);
|
||||
|
||||
/* ---- Overlay States ---- */
|
||||
--orchis-overlay-hover: rgba(0, 0, 0, 0.08);
|
||||
--orchis-overlay-focus: rgba(0, 0, 0, 0.08);
|
||||
--orchis-overlay-active: rgba(0, 0, 0, 0.12);
|
||||
--orchis-overlay-checked: rgba(0, 0, 0, 0.10);
|
||||
--orchis-overlay-selected: rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* ---- Track / Fill ---- */
|
||||
--orchis-track: rgba(0, 0, 0, 0.26);
|
||||
--orchis-track-disabled: rgba(0, 0, 0, 0.15);
|
||||
--orchis-fill: rgba(0, 0, 0, 0.04);
|
||||
--orchis-secondary-fill: rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* ---- Tooltip ---- */
|
||||
--orchis-tooltip-bg: rgba(0, 0, 0, 0.9);
|
||||
|
||||
/* ---- Titlebar / Sidebar ---- */
|
||||
--orchis-titlebar: #FFFFFF;
|
||||
--orchis-titlebar-backdrop: #FAFAFA;
|
||||
--orchis-sidebar: #FAFAFA;
|
||||
|
||||
/* ---- Window Buttons ---- */
|
||||
--orchis-btn-close: #fd5f51;
|
||||
--orchis-btn-maximize: #38c76a;
|
||||
--orchis-btn-minimize: #fdbe04;
|
||||
|
||||
/* ---- Typography ---- */
|
||||
--orchis-font-family: "M+ 1c", Roboto, Cantarell, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
--orchis-font-size: 14px;
|
||||
--orchis-font-weight-button: 500;
|
||||
|
||||
/* ---- Spacing ---- */
|
||||
--orchis-space: 6px;
|
||||
--orchis-space-xs: 2px;
|
||||
--orchis-space-sm: 4px;
|
||||
--orchis-space-md: 6px;
|
||||
--orchis-space-lg: 12px;
|
||||
--orchis-space-xl: 18px;
|
||||
--orchis-space-xxl: 24px;
|
||||
|
||||
/* ---- Border Radius ---- */
|
||||
--orchis-radius: 12px;
|
||||
--orchis-radius-window: 18px;
|
||||
--orchis-radius-corner: 12px;
|
||||
--orchis-radius-menu: 11px;
|
||||
--orchis-radius-card: 11px;
|
||||
--orchis-radius-tooltip: 6px;
|
||||
--orchis-radius-menuitem: 5px;
|
||||
--orchis-radius-circular: 9999px;
|
||||
|
||||
/* ---- Shadows ---- */
|
||||
--orchis-shadow-z1: 0 2px 2px -2px rgba(0,0,0,0.3), 0 1px 2px -1px rgba(0,0,0,0.24), 0 1px 2px -1px rgba(0,0,0,0.17);
|
||||
--orchis-shadow-z2: 0 3px 2px -3px rgba(0,0,0,0.3), 0 2px 2px -1px rgba(0,0,0,0.24), 0 1px 3px 0 rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z3: 0 3px 3px -2px rgba(0,0,0,0.2), 0 3px 3px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z4: 0 2px 2px -1px rgba(0,0,0,0.2), 0 4px 4px 0 rgba(0,0,0,0.14), 0 1px 6px 0 rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z6: 0 3px 3px -1px rgba(0,0,0,0.2), 0 6px 6px 0 rgba(0,0,0,0.14), 0 1px 11px 0 rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z8: 0 5px 5px -3px rgba(0,0,0,0.2), 0 8px 8px 1px rgba(0,0,0,0.14), 0 3px 9px 2px rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z12: 0 7px 7px -4px rgba(0,0,0,0.2), 0 12px 12px 2px rgba(0,0,0,0.14), 0 5px 13px 4px rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z16: 0 8px 6px -5px rgba(0,0,0,0.2), 0 16px 16px 2px rgba(0,0,0,0.14), 0 6px 18px 5px rgba(0,0,0,0.12);
|
||||
--orchis-shadow-z24: 0 11px 11px -7px rgba(0,0,0,0.2), 0 24px 24px 3px rgba(0,0,0,0.14), 0 9px 28px 8px rgba(0,0,0,0.12);
|
||||
|
||||
/* ---- Transitions ---- */
|
||||
--orchis-duration: 75ms;
|
||||
--orchis-duration-short: 150ms;
|
||||
--orchis-duration-ripple: 225ms;
|
||||
--orchis-ease: cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
--orchis-ease-out: cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||
--orchis-ease-in: cubic-bezier(0.4, 0.0, 1, 1);
|
||||
--orchis-transition: all 75ms cubic-bezier(0.0, 0.0, 0.2, 1);
|
||||
|
||||
/* ---- Component Sizes ---- */
|
||||
--orchis-size-small: 24px;
|
||||
--orchis-size-medium: 36px;
|
||||
--orchis-size-large: 48px;
|
||||
--orchis-icon-size: 16px;
|
||||
--orchis-icon-size-md: 24px;
|
||||
--orchis-icon-size-lg: 32px;
|
||||
--orchis-bar-size: 6px;
|
||||
}
|
||||
|
||||
/* ---- Dark Mode Overrides ---- */
|
||||
[data-mantine-color-scheme="dark"],
|
||||
.mantine-ColorScheme-root[data-mantine-color-scheme="dark"] {
|
||||
--orchis-primary: #3281EA;
|
||||
--orchis-background: #212121;
|
||||
--orchis-surface: #3C3C3C;
|
||||
--orchis-base: #2C2C2C;
|
||||
--orchis-base-alt: #242424;
|
||||
|
||||
--orchis-text: #FFFFFF;
|
||||
--orchis-text-secondary: rgba(255, 255, 255, 0.7);
|
||||
--orchis-text-disabled: rgba(255, 255, 255, 0.5);
|
||||
|
||||
--orchis-error: #F44336;
|
||||
--orchis-warning: #FBC02D;
|
||||
--orchis-success: #81C995;
|
||||
--orchis-link: #3281EA;
|
||||
--orchis-link-visited: #BA68C8;
|
||||
|
||||
--orchis-border: rgba(255, 255, 255, 0.12);
|
||||
--orchis-border-solid: #3D3D3D;
|
||||
--orchis-divider: rgba(255, 255, 255, 0.12);
|
||||
|
||||
--orchis-overlay-hover: rgba(255, 255, 255, 0.08);
|
||||
--orchis-overlay-focus: rgba(255, 255, 255, 0.08);
|
||||
--orchis-overlay-active: rgba(255, 255, 255, 0.12);
|
||||
--orchis-overlay-checked: rgba(255, 255, 255, 0.10);
|
||||
--orchis-overlay-selected: rgba(255, 255, 255, 0.06);
|
||||
|
||||
--orchis-track: rgba(255, 255, 255, 0.3);
|
||||
--orchis-track-disabled: rgba(255, 255, 255, 0.15);
|
||||
--orchis-fill: rgba(255, 255, 255, 0.04);
|
||||
--orchis-secondary-fill: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--orchis-titlebar: #2C2C2C;
|
||||
--orchis-titlebar-backdrop: #3C3C3C;
|
||||
--orchis-sidebar: #242424;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { MantineProviderProps } from '@mantine/core';
|
||||
|
||||
export const theme: MantineProviderProps['theme'] = {};
|
||||
import { orchisTheme } from '~/styles/orchis/theme';
|
||||
|
||||
// Use Orchis theme as the base theme
|
||||
export const theme: MantineProviderProps['theme'] = orchisTheme;
|
||||
|
||||
Reference in New Issue
Block a user