Add Unraid API integration and Orchis theme
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
Phase 1: Foundation Setup - Create Unraid GraphQL client with type-safe queries/mutations - Add comprehensive TypeScript types for all Unraid data models - Implement tRPC router with 30+ endpoints for Unraid management - Add environment variables for Unraid connection Phase 2: Core Dashboard - Create SystemInfoCard component (CPU, RAM, OS, motherboard) - Create ArrayCard component (disks, parity, cache pools) - Create DockerCard component with start/stop controls - Create VmsCard component with power management - Add main Unraid dashboard page with real-time updates Phase 3: Orchis Theme Integration - Create Mantine theme override with Orchis design tokens - Add CSS custom properties for light/dark modes - Configure shadows, spacing, radius from Orchis specs - Style all Mantine components with Orchis patterns Files added: - src/lib/unraid/* (GraphQL client, types, queries) - src/server/api/routers/unraid/* (tRPC router) - src/components/Unraid/* (Dashboard components) - src/pages/unraid/* (Dashboard page) - src/styles/orchis/* (Theme configuration) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
450
src/lib/unraid/client.ts
Normal file
450
src/lib/unraid/client.ts
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Unraid GraphQL Client
|
||||
* Provides type-safe access to the Unraid API
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import type {
|
||||
Customization,
|
||||
Disk,
|
||||
Docker,
|
||||
Flash,
|
||||
Network,
|
||||
Notification,
|
||||
Plugin,
|
||||
Registration,
|
||||
Server,
|
||||
ServerVars,
|
||||
Service,
|
||||
Share,
|
||||
SystemInfo,
|
||||
UnraidApiResponse,
|
||||
UnraidArray,
|
||||
UpsDevice,
|
||||
VirtualMachine,
|
||||
} from './types';
|
||||
|
||||
import {
|
||||
ARRAY_QUERY,
|
||||
CUSTOMIZATION_QUERY,
|
||||
DASHBOARD_QUERY,
|
||||
DISK_QUERY,
|
||||
DISKS_QUERY,
|
||||
DOCKER_QUERY,
|
||||
FLASH_QUERY,
|
||||
INFO_QUERY,
|
||||
NETWORK_QUERY,
|
||||
NOTIFICATIONS_QUERY,
|
||||
PLUGINS_QUERY,
|
||||
REGISTRATION_QUERY,
|
||||
SERVER_QUERY,
|
||||
SERVICES_QUERY,
|
||||
SHARES_QUERY,
|
||||
UPS_DEVICES_QUERY,
|
||||
VARS_QUERY,
|
||||
VMS_QUERY,
|
||||
} from './queries';
|
||||
|
||||
import {
|
||||
ARRAY_SET_STATE_MUTATION,
|
||||
DOCKER_START_MUTATION,
|
||||
DOCKER_STOP_MUTATION,
|
||||
NOTIFICATION_ARCHIVE_MUTATION,
|
||||
NOTIFICATION_DELETE_MUTATION,
|
||||
PARITY_CHECK_CANCEL_MUTATION,
|
||||
PARITY_CHECK_PAUSE_MUTATION,
|
||||
PARITY_CHECK_RESUME_MUTATION,
|
||||
PARITY_CHECK_START_MUTATION,
|
||||
VM_FORCE_STOP_MUTATION,
|
||||
VM_PAUSE_MUTATION,
|
||||
VM_REBOOT_MUTATION,
|
||||
VM_RESUME_MUTATION,
|
||||
VM_START_MUTATION,
|
||||
VM_STOP_MUTATION,
|
||||
} from './queries/mutations';
|
||||
|
||||
export interface UnraidClientConfig {
|
||||
host: string;
|
||||
apiKey: string;
|
||||
useSsl?: boolean;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
info: SystemInfo;
|
||||
vars: ServerVars;
|
||||
registration: Registration;
|
||||
array: UnraidArray;
|
||||
docker: Docker;
|
||||
vms: VirtualMachine[];
|
||||
shares: Share[];
|
||||
services: Service[];
|
||||
notifications: Notification[];
|
||||
}
|
||||
|
||||
export class UnraidClient {
|
||||
private client: AxiosInstance;
|
||||
private config: UnraidClientConfig;
|
||||
|
||||
constructor(config: UnraidClientConfig) {
|
||||
this.config = config;
|
||||
const protocol = config.useSsl ? 'https' : 'http';
|
||||
const port = config.port || (config.useSsl ? 443 : 80);
|
||||
const baseURL = `${protocol}://${config.host}:${port}`;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': config.apiKey,
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a GraphQL query
|
||||
*/
|
||||
private async query<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
|
||||
const response = await this.client.post<UnraidApiResponse<T>>('/graphql', {
|
||||
query,
|
||||
variables,
|
||||
});
|
||||
|
||||
if (response.data.errors?.length) {
|
||||
throw new Error(response.data.errors.map((e) => e.message).join(', '));
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a GraphQL mutation
|
||||
*/
|
||||
private async mutate<T>(mutation: string, variables?: Record<string, unknown>): Promise<T> {
|
||||
return this.query<T>(mutation, variables);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SYSTEM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getInfo(): Promise<SystemInfo> {
|
||||
const data = await this.query<{ info: SystemInfo }>(INFO_QUERY);
|
||||
return data.info;
|
||||
}
|
||||
|
||||
async getVars(): Promise<ServerVars> {
|
||||
const data = await this.query<{ vars: ServerVars }>(VARS_QUERY);
|
||||
return data.vars;
|
||||
}
|
||||
|
||||
async getServer(): Promise<Server> {
|
||||
const data = await this.query<{ server: Server }>(SERVER_QUERY);
|
||||
return data.server;
|
||||
}
|
||||
|
||||
async getRegistration(): Promise<Registration> {
|
||||
const data = await this.query<{ registration: Registration }>(REGISTRATION_QUERY);
|
||||
return data.registration;
|
||||
}
|
||||
|
||||
async getFlash(): Promise<Flash> {
|
||||
const data = await this.query<{ flash: Flash }>(FLASH_QUERY);
|
||||
return data.flash;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getArray(): Promise<UnraidArray> {
|
||||
const data = await this.query<{ array: UnraidArray }>(ARRAY_QUERY);
|
||||
return data.array;
|
||||
}
|
||||
|
||||
async getDisks(): Promise<Disk[]> {
|
||||
const data = await this.query<{ disks: Disk[] }>(DISKS_QUERY);
|
||||
return data.disks;
|
||||
}
|
||||
|
||||
async getDisk(id: string): Promise<Disk> {
|
||||
const data = await this.query<{ disk: Disk }>(DISK_QUERY, { id });
|
||||
return data.disk;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getDocker(): Promise<Docker> {
|
||||
const data = await this.query<{ docker: Docker }>(DOCKER_QUERY);
|
||||
return data.docker;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VM QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getVms(): Promise<VirtualMachine[]> {
|
||||
const data = await this.query<{ vms: VirtualMachine[] }>(VMS_QUERY);
|
||||
return data.vms;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SHARES QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getShares(): Promise<Share[]> {
|
||||
const data = await this.query<{ shares: Share[] }>(SHARES_QUERY);
|
||||
return data.shares;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATIONS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getNotifications(): Promise<Notification[]> {
|
||||
const data = await this.query<{ notifications: Notification[] }>(NOTIFICATIONS_QUERY);
|
||||
return data.notifications;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVICES QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getServices(): Promise<Service[]> {
|
||||
const data = await this.query<{ services: Service[] }>(SERVICES_QUERY);
|
||||
return data.services;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NETWORK QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getNetwork(): Promise<Network> {
|
||||
const data = await this.query<{ network: Network }>(NETWORK_QUERY);
|
||||
return data.network;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UPS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getUpsDevices(): Promise<UpsDevice[]> {
|
||||
const data = await this.query<{ upsDevices: UpsDevice[] }>(UPS_DEVICES_QUERY);
|
||||
return data.upsDevices;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLUGINS QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getPlugins(): Promise<Plugin[]> {
|
||||
const data = await this.query<{ plugins: Plugin[] }>(PLUGINS_QUERY);
|
||||
return data.plugins;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CUSTOMIZATION QUERIES
|
||||
// ============================================================================
|
||||
|
||||
async getCustomization(): Promise<Customization> {
|
||||
const data = await this.query<{ customization: Customization }>(CUSTOMIZATION_QUERY);
|
||||
return data.customization;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DASHBOARD (COMPOSITE QUERY)
|
||||
// ============================================================================
|
||||
|
||||
async getDashboard(): Promise<DashboardData> {
|
||||
const data = await this.query<DashboardData>(DASHBOARD_QUERY);
|
||||
return data;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARRAY MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startArray(): Promise<{ state: string }> {
|
||||
const data = await this.mutate<{ array: { setState: { state: string } } }>(
|
||||
ARRAY_SET_STATE_MUTATION,
|
||||
{ state: 'start' }
|
||||
);
|
||||
return data.array.setState;
|
||||
}
|
||||
|
||||
async stopArray(): Promise<{ state: string }> {
|
||||
const data = await this.mutate<{ array: { setState: { state: string } } }>(
|
||||
ARRAY_SET_STATE_MUTATION,
|
||||
{ state: 'stop' }
|
||||
);
|
||||
return data.array.setState;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PARITY CHECK MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startParityCheck(correct = false): Promise<{ running: boolean; progress: number }> {
|
||||
const data = await this.mutate<{
|
||||
parityCheck: { start: { running: boolean; progress: number } };
|
||||
}>(PARITY_CHECK_START_MUTATION, { correct });
|
||||
return data.parityCheck.start;
|
||||
}
|
||||
|
||||
async pauseParityCheck(): Promise<{ running: boolean; progress: number }> {
|
||||
const data = await this.mutate<{
|
||||
parityCheck: { pause: { running: boolean; progress: number } };
|
||||
}>(PARITY_CHECK_PAUSE_MUTATION);
|
||||
return data.parityCheck.pause;
|
||||
}
|
||||
|
||||
async resumeParityCheck(): Promise<{ running: boolean; progress: number }> {
|
||||
const data = await this.mutate<{
|
||||
parityCheck: { resume: { running: boolean; progress: number } };
|
||||
}>(PARITY_CHECK_RESUME_MUTATION);
|
||||
return data.parityCheck.resume;
|
||||
}
|
||||
|
||||
async cancelParityCheck(): Promise<{ running: boolean }> {
|
||||
const data = await this.mutate<{ parityCheck: { cancel: { running: boolean } } }>(
|
||||
PARITY_CHECK_CANCEL_MUTATION
|
||||
);
|
||||
return data.parityCheck.cancel;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DOCKER MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startContainer(id: string): Promise<{ id: string; state: string; status: string }> {
|
||||
const data = await this.mutate<{
|
||||
docker: { start: { id: string; state: string; status: string } };
|
||||
}>(DOCKER_START_MUTATION, { id });
|
||||
return data.docker.start;
|
||||
}
|
||||
|
||||
async stopContainer(id: string): Promise<{ id: string; state: string; status: string }> {
|
||||
const data = await this.mutate<{
|
||||
docker: { stop: { id: string; state: string; status: string } };
|
||||
}>(DOCKER_STOP_MUTATION, { id });
|
||||
return data.docker.stop;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// VM MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async startVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { start: { id: string; state: string } } }>(
|
||||
VM_START_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.start;
|
||||
}
|
||||
|
||||
async stopVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { stop: { id: string; state: string } } }>(
|
||||
VM_STOP_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.stop;
|
||||
}
|
||||
|
||||
async pauseVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { pause: { id: string; state: string } } }>(
|
||||
VM_PAUSE_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.pause;
|
||||
}
|
||||
|
||||
async resumeVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { resume: { id: string; state: string } } }>(
|
||||
VM_RESUME_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.resume;
|
||||
}
|
||||
|
||||
async forceStopVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { forceStop: { id: string; state: string } } }>(
|
||||
VM_FORCE_STOP_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.forceStop;
|
||||
}
|
||||
|
||||
async rebootVm(id: string): Promise<{ id: string; state: string }> {
|
||||
const data = await this.mutate<{ vm: { reboot: { id: string; state: string } } }>(
|
||||
VM_REBOOT_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.vm.reboot;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// NOTIFICATION MUTATIONS
|
||||
// ============================================================================
|
||||
|
||||
async deleteNotification(id: string): Promise<boolean> {
|
||||
const data = await this.mutate<{ notification: { delete: boolean } }>(
|
||||
NOTIFICATION_DELETE_MUTATION,
|
||||
{ id }
|
||||
);
|
||||
return data.notification.delete;
|
||||
}
|
||||
|
||||
async archiveNotification(id: string): Promise<{ id: string; archived: boolean }> {
|
||||
const data = await this.mutate<{
|
||||
notification: { archive: { id: string; archived: boolean } };
|
||||
}>(NOTIFICATION_ARCHIVE_MUTATION, { id });
|
||||
return data.notification.archive;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HEALTH CHECK
|
||||
// ============================================================================
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await this.getVars();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SINGLETON INSTANCE
|
||||
// ============================================================================
|
||||
|
||||
let unraidClient: UnraidClient | null = null;
|
||||
|
||||
export function getUnraidClient(): UnraidClient {
|
||||
if (!unraidClient) {
|
||||
const host = process.env.UNRAID_HOST;
|
||||
const apiKey = process.env.UNRAID_API_KEY;
|
||||
|
||||
if (!host || !apiKey) {
|
||||
throw new Error('UNRAID_HOST and UNRAID_API_KEY environment variables are required');
|
||||
}
|
||||
|
||||
unraidClient = new UnraidClient({
|
||||
host,
|
||||
apiKey,
|
||||
useSsl: process.env.UNRAID_USE_SSL === 'true',
|
||||
port: process.env.UNRAID_PORT ? parseInt(process.env.UNRAID_PORT, 10) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return unraidClient;
|
||||
}
|
||||
|
||||
export function createUnraidClient(config: UnraidClientConfig): UnraidClient {
|
||||
return new UnraidClient(config);
|
||||
}
|
||||
Reference in New Issue
Block a user