Files
homarr/src/lib/unraid/client.ts
Kaloyan Danchev 83a8546521
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
Add Unraid API integration and Orchis theme
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>
2026-02-06 09:19:21 +02:00

451 lines
13 KiB
TypeScript

/**
* 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);
}