diff --git a/src/components/Unraid/Dashboard/ArrayCard.tsx b/src/components/Unraid/Dashboard/ArrayCard.tsx
new file mode 100644
index 000000000..6a4f23b95
--- /dev/null
+++ b/src/components/Unraid/Dashboard/ArrayCard.tsx
@@ -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 (
+
+ |
+
+
+
+
+
+ {disk.name}
+
+
+ |
+
+
+ {disk.status.replace('DISK_', '')}
+
+ |
+
+ {formatBytes(disk.size)}
+ |
+
+ {disk.temp !== null ? (
+
+
+ 50 ? 'red' : disk.temp > 40 ? 'orange' : undefined}>
+ {disk.temp}°C
+
+
+ ) : (
+
+ {disk.spunDown ? 'Spun down' : '-'}
+
+ )}
+ |
+
+ {usedPercent ? (
+
+
+ ) : (
+ -
+ )}
+ |
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
Array
+
+ {formatBytes(array.capacity.used)} / {formatBytes(array.capacity.total)}
+
+
+
+
+
+ {array.state}
+
+ {isStarted && onStopArray && (
+
+
+
+
+
+ )}
+ {!isStarted && onStartArray && (
+
+
+
+
+
+ )}
+
+
+
+
+
+ {/* Array Capacity */}
+
+
+
+ Total Capacity
+
+
+ {usedPercent}% used
+
+
+
+
+ {/* Parity Check Status */}
+ {array.parityCheckStatus?.running && (
+
+
+
+ Parity Check in Progress
+
+
+ {array.parityCheckStatus.progress.toFixed(1)}% - {array.parityCheckStatus.errors} errors
+
+
+
+
+ )}
+
+ {/* Parity Disks */}
+ {array.parities.length > 0 && (
+ <>
+
+ Parity ({array.parities.length})
+
+
+
+
+ | Disk |
+ Status |
+ Size |
+ Temp |
+ Usage |
+
+
+
+ {array.parities.map((disk) => (
+
+ ))}
+
+
+ >
+ )}
+
+ {/* Data Disks */}
+ {array.disks.length > 0 && (
+ <>
+
+ Data Disks ({array.disks.length})
+
+
+
+
+ | Disk |
+ Status |
+ Size |
+ Temp |
+ Usage |
+
+
+
+ {array.disks.map((disk) => (
+
+ ))}
+
+
+ >
+ )}
+
+ {/* Cache Pools */}
+ {array.caches.length > 0 && (
+ <>
+
+ Cache Pools ({array.caches.length})
+
+ {array.caches.map((cache) => {
+ const cacheUsedPercent = ((cache.fsUsed / cache.fsSize) * 100).toFixed(1);
+ return (
+
+
+
+ {cache.name} ({cache.fsType})
+
+
+ {formatBytes(cache.fsUsed)} / {formatBytes(cache.fsSize)}
+
+
+
+ );
+ })}
+ >
+ )}
+
+
+ );
+}
+
+export default ArrayCard;
diff --git a/src/components/Unraid/Dashboard/DockerCard.tsx b/src/components/Unraid/Dashboard/DockerCard.tsx
new file mode 100644
index 000000000..d28097ef7
--- /dev/null
+++ b/src/components/Unraid/Dashboard/DockerCard.tsx
@@ -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 (
+ ({
+ borderBottom: `1px solid ${
+ theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]
+ }`,
+ '&:last-child': {
+ borderBottom: 'none',
+ },
+ })}>
+
+
+
+
+
+
+ {containerName}
+
+
+ {container.image}
+
+
+
+
+
+
+ {container.state}
+
+
+ {isRunning && onStop && (
+
+
+
+
+
+ )}
+
+ {!isRunning && onStart && (
+
+
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
Docker
+
+ {runningCount} running / {totalCount} total
+
+
+
+
+ {docker.networks.length} networks
+
+
+
+
+
+
+ {sortedContainers.length === 0 ? (
+
+ No containers
+
+ ) : (
+ sortedContainers.map((container) => (
+ onStartContainer(container.id) : undefined}
+ onStop={onStopContainer ? () => onStopContainer(container.id) : undefined}
+ isLoading={loadingContainers.includes(container.id)}
+ />
+ ))
+ )}
+
+
+
+ );
+}
+
+export default DockerCard;
diff --git a/src/components/Unraid/Dashboard/SystemInfoCard.tsx b/src/components/Unraid/Dashboard/SystemInfoCard.tsx
new file mode 100644
index 000000000..9c18391c2
--- /dev/null
+++ b/src/components/Unraid/Dashboard/SystemInfoCard.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
{vars.name}
+
+ {vars.description}
+
+
+
+
+ {registration.type}
+
+
+
+
+
+ {/* CPU Info */}
+
+
+
+
+
+
+ CPU
+
+
+
+ {info.cpu.brand}
+
+
+
+
+
+
+ Cores
+
+
+ {info.cpu.cores}
+
+
+
+
+ Threads
+
+
+ {info.cpu.threads}
+
+
+
+
+ Speed
+
+
+ {info.cpu.speed.toFixed(2)} GHz
+
+
+
+
+ {/* Memory Info */}
+
+
+
+ Memory
+
+
+ {formatBytes(info.memory.used)} / {formatBytes(info.memory.total)} ({memoryUsedPercent}%)
+
+
+
+
+ {/* OS Info */}
+
+
+
+
+
+
+ OS
+
+
+
+ Unraid {info.versions.unraid}
+
+
+
+ {/* Motherboard */}
+
+
+
+
+
+
+ Motherboard
+
+
+
+ {info.baseboard.manufacturer} {info.baseboard.model}
+
+
+
+ {/* Uptime */}
+
+
+
+
+
+
+ Uptime
+
+
+
+ {formatUptime(info.os.uptime)}
+
+
+
+
+ );
+}
+
+export default SystemInfoCard;
diff --git a/src/components/Unraid/Dashboard/VmsCard.tsx b/src/components/Unraid/Dashboard/VmsCard.tsx
new file mode 100644
index 000000000..eee9e2a69
--- /dev/null
+++ b/src/components/Unraid/Dashboard/VmsCard.tsx
@@ -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 (
+ ({
+ borderBottom: `1px solid ${
+ theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]
+ }`,
+ '&:last-child': {
+ borderBottom: 'none',
+ },
+ })}
+ >
+
+
+
+
+
+
+ {vm.name}
+
+
+
+ {vm.cpus} vCPU
+
+
+ •
+
+
+ {formatMemory(vm.memory)}
+
+
+
+
+
+
+
+ {vm.state}
+
+
+ {isRunning && onPause && (
+
+
+
+
+
+ )}
+
+ {isPaused && onResume && (
+
+
+
+
+
+ )}
+
+ {isRunning && onStop && (
+
+
+
+
+
+ )}
+
+ {isStopped && onStart && (
+
+
+
+
+
+ )}
+
+
+
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
Virtual Machines
+
+ {runningCount} running / {totalCount} total
+
+
+
+
+
+
+
+
+ {sortedVms.length === 0 ? (
+
+ No virtual machines
+
+ ) : (
+ sortedVms.map((vm) => (
+ 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)}
+ />
+ ))
+ )}
+
+
+
+ );
+}
+
+export default VmsCard;
diff --git a/src/components/Unraid/Dashboard/index.ts b/src/components/Unraid/Dashboard/index.ts
new file mode 100644
index 000000000..0eed4d04b
--- /dev/null
+++ b/src/components/Unraid/Dashboard/index.ts
@@ -0,0 +1,8 @@
+/**
+ * Unraid Dashboard Components
+ */
+
+export { SystemInfoCard } from './SystemInfoCard';
+export { ArrayCard } from './ArrayCard';
+export { DockerCard } from './DockerCard';
+export { VmsCard } from './VmsCard';
diff --git a/src/env.js b/src/env.js
index d85282e62..766bfa91f 100644
--- a/src/env.js
+++ b/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,
});
diff --git a/src/lib/unraid/client.ts b/src/lib/unraid/client.ts
new file mode 100644
index 000000000..88c6f5a56
--- /dev/null
+++ b/src/lib/unraid/client.ts
@@ -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(query: string, variables?: Record): Promise {
+ const response = await this.client.post>('/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(mutation: string, variables?: Record): Promise {
+ return this.query(mutation, variables);
+ }
+
+ // ============================================================================
+ // SYSTEM QUERIES
+ // ============================================================================
+
+ async getInfo(): Promise {
+ const data = await this.query<{ info: SystemInfo }>(INFO_QUERY);
+ return data.info;
+ }
+
+ async getVars(): Promise {
+ const data = await this.query<{ vars: ServerVars }>(VARS_QUERY);
+ return data.vars;
+ }
+
+ async getServer(): Promise {
+ const data = await this.query<{ server: Server }>(SERVER_QUERY);
+ return data.server;
+ }
+
+ async getRegistration(): Promise {
+ const data = await this.query<{ registration: Registration }>(REGISTRATION_QUERY);
+ return data.registration;
+ }
+
+ async getFlash(): Promise {
+ const data = await this.query<{ flash: Flash }>(FLASH_QUERY);
+ return data.flash;
+ }
+
+ // ============================================================================
+ // ARRAY QUERIES
+ // ============================================================================
+
+ async getArray(): Promise {
+ const data = await this.query<{ array: UnraidArray }>(ARRAY_QUERY);
+ return data.array;
+ }
+
+ async getDisks(): Promise {
+ const data = await this.query<{ disks: Disk[] }>(DISKS_QUERY);
+ return data.disks;
+ }
+
+ async getDisk(id: string): Promise {
+ const data = await this.query<{ disk: Disk }>(DISK_QUERY, { id });
+ return data.disk;
+ }
+
+ // ============================================================================
+ // DOCKER QUERIES
+ // ============================================================================
+
+ async getDocker(): Promise {
+ const data = await this.query<{ docker: Docker }>(DOCKER_QUERY);
+ return data.docker;
+ }
+
+ // ============================================================================
+ // VM QUERIES
+ // ============================================================================
+
+ async getVms(): Promise {
+ const data = await this.query<{ vms: VirtualMachine[] }>(VMS_QUERY);
+ return data.vms;
+ }
+
+ // ============================================================================
+ // SHARES QUERIES
+ // ============================================================================
+
+ async getShares(): Promise {
+ const data = await this.query<{ shares: Share[] }>(SHARES_QUERY);
+ return data.shares;
+ }
+
+ // ============================================================================
+ // NOTIFICATIONS QUERIES
+ // ============================================================================
+
+ async getNotifications(): Promise {
+ const data = await this.query<{ notifications: Notification[] }>(NOTIFICATIONS_QUERY);
+ return data.notifications;
+ }
+
+ // ============================================================================
+ // SERVICES QUERIES
+ // ============================================================================
+
+ async getServices(): Promise {
+ const data = await this.query<{ services: Service[] }>(SERVICES_QUERY);
+ return data.services;
+ }
+
+ // ============================================================================
+ // NETWORK QUERIES
+ // ============================================================================
+
+ async getNetwork(): Promise {
+ const data = await this.query<{ network: Network }>(NETWORK_QUERY);
+ return data.network;
+ }
+
+ // ============================================================================
+ // UPS QUERIES
+ // ============================================================================
+
+ async getUpsDevices(): Promise {
+ const data = await this.query<{ upsDevices: UpsDevice[] }>(UPS_DEVICES_QUERY);
+ return data.upsDevices;
+ }
+
+ // ============================================================================
+ // PLUGINS QUERIES
+ // ============================================================================
+
+ async getPlugins(): Promise {
+ const data = await this.query<{ plugins: Plugin[] }>(PLUGINS_QUERY);
+ return data.plugins;
+ }
+
+ // ============================================================================
+ // CUSTOMIZATION QUERIES
+ // ============================================================================
+
+ async getCustomization(): Promise {
+ const data = await this.query<{ customization: Customization }>(CUSTOMIZATION_QUERY);
+ return data.customization;
+ }
+
+ // ============================================================================
+ // DASHBOARD (COMPOSITE QUERY)
+ // ============================================================================
+
+ async getDashboard(): Promise {
+ const data = await this.query(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 {
+ 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 {
+ 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);
+}
diff --git a/src/lib/unraid/index.ts b/src/lib/unraid/index.ts
new file mode 100644
index 000000000..ddf36d738
--- /dev/null
+++ b/src/lib/unraid/index.ts
@@ -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';
diff --git a/src/lib/unraid/queries/index.ts b/src/lib/unraid/queries/index.ts
new file mode 100644
index 000000000..451fbd46a
--- /dev/null
+++ b/src/lib/unraid/queries/index.ts
@@ -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
+ }
+ }
+`;
diff --git a/src/lib/unraid/queries/mutations.ts b/src/lib/unraid/queries/mutations.ts
new file mode 100644
index 000000000..7e452898f
--- /dev/null
+++ b/src/lib/unraid/queries/mutations.ts
@@ -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
+ }
+ }
+`;
diff --git a/src/lib/unraid/queries/subscriptions.ts b/src/lib/unraid/queries/subscriptions.ts
new file mode 100644
index 000000000..3eb961850
--- /dev/null
+++ b/src/lib/unraid/queries/subscriptions.ts
@@ -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
+ }
+ }
+`;
diff --git a/src/lib/unraid/types.ts b/src/lib/unraid/types.ts
new file mode 100644
index 000000000..4b7600f03
--- /dev/null
+++ b/src/lib/unraid/types.ts
@@ -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 {
+ 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;
+}
diff --git a/src/pages/unraid/index.tsx b/src/pages/unraid/index.tsx
new file mode 100644
index 000000000..4a4e38864
--- /dev/null
+++ b/src/pages/unraid/index.tsx
@@ -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([]);
+ const [loadingVms, setLoadingVms] = useState([]);
+ 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: ,
+ });
+ refetch();
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'Error',
+ message: error.message,
+ color: 'red',
+ icon: ,
+ });
+ },
+ });
+
+ 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: ,
+ });
+ refetch();
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'Error',
+ message: error.message,
+ color: 'red',
+ icon: ,
+ });
+ },
+ });
+
+ 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: ,
+ });
+ refetch();
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'Error',
+ message: error.message,
+ color: 'red',
+ icon: ,
+ });
+ },
+ });
+
+ 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: ,
+ });
+ refetch();
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'Error',
+ message: error.message,
+ color: 'red',
+ icon: ,
+ });
+ },
+ });
+
+ 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: ,
+ });
+ 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: ,
+ });
+ 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: ,
+ });
+ 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: ,
+ });
+ refetch();
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'Error',
+ message: error.message,
+ color: 'red',
+ icon: ,
+ });
+ },
+ });
+
+ 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: ,
+ });
+ refetch();
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'Error',
+ message: error.message,
+ color: 'red',
+ icon: ,
+ });
+ },
+ });
+
+ // Loading state
+ if (isLoading) {
+ return (
+
+
+
+ Connecting to Unraid server...
+
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+ }
+ title="Connection Error"
+ color="red"
+ variant="filled"
+ >
+ {error.message}
+
+
+ );
+ }
+
+ // No data
+ if (!dashboard) {
+ return (
+
+ }
+ title="No Data"
+ color="yellow"
+ >
+ No data received from Unraid server. Please check your configuration.
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
Unraid Dashboard
+
+ {dashboard.vars.name} - Unraid {dashboard.info.versions.unraid}
+
+
+
+
+
+ {/* Dashboard Grid */}
+
+ {/* System Info */}
+
+
+
+
+ {/* Array */}
+
+ startArray.mutate()}
+ onStopArray={() => stopArray.mutate()}
+ isLoading={arrayLoading}
+ />
+
+
+ {/* Docker */}
+
+ startContainer.mutate({ id })}
+ onStopContainer={(id) => stopContainer.mutate({ id })}
+ loadingContainers={loadingContainers}
+ />
+
+
+ {/* VMs */}
+
+ 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}
+ />
+
+
+
+
+ );
+}
+
+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,
+ },
+ };
+};
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index 2ce6cc1e4..b8f446a3b 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -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
diff --git a/src/server/api/routers/unraid/router.ts b/src/server/api/routers/unraid/router.ts
new file mode 100644
index 000000000..43a3e0dbd
--- /dev/null
+++ b/src/server/api/routers/unraid/router.ts
@@ -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();
+ }),
+});
diff --git a/src/styles/global.scss b/src/styles/global.scss
index 6be988fc9..d899a3a9c 100644
--- a/src/styles/global.scss
+++ b/src/styles/global.scss
@@ -1,4 +1,5 @@
@import 'fily-publish-gridstack/dist/gridstack.min.css';
+@import './orchis/variables.css';
:root {
--gridstack-widget-width: 64;
diff --git a/src/styles/orchis/theme.ts b/src/styles/orchis/theme.ts
new file mode 100644
index 000000000..fcbb1d273
--- /dev/null
+++ b/src/styles/orchis/theme.ts
@@ -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 = [
+ '#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 = [
+ '#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 = [
+ '#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 = [
+ '#FFEBEE',
+ '#FFCDD2',
+ '#EF9A9A',
+ '#E57373',
+ '#EF5350',
+ '#F44336',
+ '#E53935', // Orchis error
+ '#D32F2F',
+ '#C62828',
+ '#B71C1C',
+];
+
+/**
+ * Orchis green for success
+ */
+const orchisGreen: Tuple = [
+ '#E8F5E9',
+ '#C8E6C9',
+ '#A5D6A7',
+ '#81C995', // Sea light
+ '#66BB6A',
+ '#4CAF50',
+ '#43A047',
+ '#388E3C',
+ '#0F9D58', // Sea dark (success)
+ '#1B5E20',
+];
+
+/**
+ * Orchis yellow for warnings
+ */
+const orchisYellow: Tuple = [
+ '#FFFDE7',
+ '#FFF9C4',
+ '#FFF59D',
+ '#FFF176',
+ '#FFEE58',
+ '#FFEB3B',
+ '#FDD835',
+ '#FBC02D', // Yellow light
+ '#F9A825',
+ '#FFD600', // Yellow dark (warning)
+];
+
+/**
+ * Orchis purple for accents
+ */
+const orchisPurple: Tuple = [
+ '#F3E5F5',
+ '#E1BEE7',
+ '#CE93D8',
+ '#BA68C8', // Purple light
+ '#AB47BC', // Purple dark
+ '#9C27B0',
+ '#8E24AA',
+ '#7B1FA2',
+ '#6A1B9A',
+ '#4A148C',
+];
+
+/**
+ * Orchis teal for secondary accents
+ */
+const orchisTeal: Tuple = [
+ '#E0F2F1',
+ '#B2DFDB',
+ '#80CBC4',
+ '#4DB6AC',
+ '#26A69A',
+ '#009688',
+ '#00897B',
+ '#00796B',
+ '#00695C',
+ '#004D40',
+];
+
+/**
+ * Orchis orange for highlights
+ */
+const orchisOrange: Tuple = [
+ '#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;
diff --git a/src/styles/orchis/variables.css b/src/styles/orchis/variables.css
new file mode 100644
index 000000000..7f9d57a9f
--- /dev/null
+++ b/src/styles/orchis/variables.css
@@ -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;
+}
diff --git a/src/tools/server/theme/theme.ts b/src/tools/server/theme/theme.ts
index 69d7b9643..b2b2223a9 100644
--- a/src/tools/server/theme/theme.ts
+++ b/src/tools/server/theme/theme.ts
@@ -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;