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>
451 lines
13 KiB
TypeScript
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);
|
|
}
|