Compare commits

...

16 Commits

Author SHA1 Message Date
Pujit Mehrotra
f981129764 Merge 1f1516735e into ff2906e52a 2025-10-27 10:37:46 -04:00
Simon Fairweather
1f1516735e chore: Fix tests
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
74c53653e8 chore: Fix tests
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
9e04487f13 chore: Fix tests
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
01ecbbdf56 chore: Fix tests
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
5fda8fc1e2 chore: Code tidy
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
fa62d70ed8 chore: Code tidy
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
SimonFair
f494394825 Update api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
95adc92cd3 chore: Code tidy & remove cpu-power subscription/functions
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Pujit Mehrotra
be2d253060 chore: add local deploy script to unraid-shared 2025-10-27 10:37:07 -04:00
Simon Fairweather
9d63220146 Fix typo
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
f3da2a4caf Subscription changes
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Simon Fairweather
e78819d9b7 Add topology and temps
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
2025-10-27 10:37:07 -04:00
Pujit Mehrotra
3e2b1eff18 fix tests 2025-10-27 10:37:07 -04:00
Pujit Mehrotra
c58199b3ed fix types in tests 2025-10-27 10:37:07 -04:00
Pujit Mehrotra
2e8e4baa5a feat: add cpu power query & subscription 2025-10-27 10:37:07 -04:00
15 changed files with 448 additions and 12 deletions

View File

@@ -1401,6 +1401,17 @@ type CpuUtilization implements Node {
cpus: [CpuLoad!]!
}
type CpuPackages implements Node {
"""Total CPU package power draw (W)"""
totalPower: Float!
"""Power draw per package (W)"""
power: [Float!]
"""description: 'Temperature per package (°C)"""
temp: [Float!]
}
type InfoCpu implements Node {
id: PrefixedID!
@@ -1446,6 +1457,9 @@ type InfoCpu implements Node {
"""Number of physical processors"""
processors: Int
"""CPU packages information"""
packages: CpuPackages
"""CPU socket type"""
socket: String
@@ -2642,6 +2656,7 @@ type Subscription {
arraySubscription: UnraidArray!
logFile(path: String!): LogFileContent!
systemMetricsCpu: CpuUtilization!
systemMetricsCpuTelemetry: CpuPackages!
systemMetricsMemory: MemoryUtilization!
upsUpdates: UPSDevice!
}

View File

@@ -0,0 +1,211 @@
import { Injectable, Logger } from '@nestjs/common';
import { constants as fsConstants } from 'node:fs';
import { access, readdir, readFile } from 'node:fs/promises';
import { join } from 'path';
@Injectable()
export class CpuTopologyService {
private readonly logger = new Logger(CpuTopologyService.name);
private topologyCache: { id: number; cores: number[][] }[] | null = null;
// -----------------------------------------------------------------
// Read static CPU topology, per-package core thread pairs
// -----------------------------------------------------------------
async generateTopology(): Promise<number[][][]> {
const packages: Record<number, number[][]> = {};
const cpuDirs = await readdir('/sys/devices/system/cpu');
for (const dir of cpuDirs) {
if (!/^cpu\d+$/.test(dir)) continue;
const basePath = join('/sys/devices/system/cpu', dir, 'topology');
const pkgFile = join(basePath, 'physical_package_id');
const siblingsFile = join(basePath, 'thread_siblings_list');
try {
const [pkgIdStr, siblingsStrRaw] = await Promise.all([
readFile(pkgFile, 'utf8'),
readFile(siblingsFile, 'utf8'),
]);
const pkgId = parseInt(pkgIdStr.trim(), 10);
// expand ranges
const siblings = siblingsStrRaw
.trim()
.replace(/(\d+)-(\d+)/g, (_, start, end) =>
Array.from(
{ length: parseInt(end) - parseInt(start) + 1 },
(_, i) => parseInt(start) + i
).join(',')
)
.split(',')
.map((n) => parseInt(n, 10));
if (!packages[pkgId]) packages[pkgId] = [];
if (!packages[pkgId].some((arr) => arr.join(',') === siblings.join(','))) {
packages[pkgId].push(siblings);
}
} catch (err) {
console.warn('Topology read error for', dir, err);
}
}
// Sort cores within each package, and packages by their lowest core index
const result = Object.entries(packages)
.sort((a, b) => a[1][0][0] - b[1][0][0]) // sort packages by first CPU ID
.map(
([pkgId, cores]) => cores.sort((a, b) => a[0] - b[0]) // sort cores within package
);
return result;
}
// -----------------------------------------------------------------
// Dynamic telemetry (power + temperature)
// -----------------------------------------------------------------
private async getPackageTemps(): Promise<number[]> {
const temps: number[] = [];
try {
const hwmons = await readdir('/sys/class/hwmon');
for (const hwmon of hwmons) {
const path = join('/sys/class/hwmon', hwmon);
try {
const label = (await readFile(join(path, 'name'), 'utf8')).trim();
if (/coretemp|k10temp|zenpower/i.test(label)) {
const files = await readdir(path);
for (const f of files) {
if (f.startsWith('temp') && f.endsWith('_label')) {
const lbl = (await readFile(join(path, f), 'utf8')).trim().toLowerCase();
if (
lbl.includes('package id') ||
lbl.includes('tctl') ||
lbl.includes('tdie')
) {
const inputFile = join(path, f.replace('_label', '_input'));
try {
const raw = await readFile(inputFile, 'utf8');
temps.push(parseInt(raw.trim(), 10) / 1000);
} catch (err) {
this.logger.warn('Failed to read file', err);
}
}
}
}
}
} catch (err) {
this.logger.warn('Failed to read file', err);
}
}
} catch (err) {
this.logger.warn('Failed to read file', err);
}
return temps;
}
private async getPackagePower(): Promise<Record<number, Record<string, number>>> {
const basePath = '/sys/class/powercap';
const prefixes = ['intel-rapl', 'intel-rapl-mmio', 'amd-rapl'];
const raplPaths: string[] = [];
try {
const entries = await readdir(basePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isSymbolicLink() && prefixes.some((p) => entry.name.startsWith(p))) {
if (/:\d+:\d+/.test(entry.name)) continue;
raplPaths.push(join(basePath, entry.name));
}
}
} catch {
return {};
}
if (!raplPaths.length) return {};
const readEnergy = async (p: string): Promise<number | null> => {
try {
await access(join(p, 'energy_uj'), fsConstants.R_OK);
const raw = await readFile(join(p, 'energy_uj'), 'utf8');
return parseInt(raw.trim(), 10);
} catch {
return null;
}
};
const prevE = new Map<string, number>();
const prevT = new Map<string, bigint>();
for (const p of raplPaths) {
const val = await readEnergy(p);
if (val !== null) {
prevE.set(p, val);
prevT.set(p, process.hrtime.bigint());
}
}
await new Promise((res) => setTimeout(res, 100));
const results: Record<number, Record<string, number>> = {};
for (const p of raplPaths) {
const now = await readEnergy(p);
if (now === null) continue;
const prevVal = prevE.get(p);
const prevTime = prevT.get(p);
if (prevVal === undefined || prevTime === undefined) continue;
const diffE = now - prevVal;
const diffT = Number(process.hrtime.bigint() - prevTime);
if (diffT <= 0 || diffE < 0) continue;
const watts = (diffE * 1e-6) / (diffT * 1e-9);
const powerW = Math.round(watts * 100) / 100;
const nameFile = join(p, 'name');
let label = 'package';
try {
label = (await readFile(nameFile, 'utf8')).trim();
} catch (err) {
this.logger.warn('Failed to read file', err);
}
const pkgMatch = label.match(/package-(\d+)/i);
const pkgId = pkgMatch ? Number(pkgMatch[1]) : 0;
if (!results[pkgId]) results[pkgId] = {};
results[pkgId][label] = powerW;
}
for (const domains of Object.values(results)) {
const total = Object.values(domains).reduce((a, b) => a + b, 0);
(domains as any)['total'] = Math.round(total * 100) / 100;
}
return results;
}
async generateTelemetry(): Promise<{ id: number; power: number; temp: number }[]> {
const temps = await this.getPackageTemps();
const powerData = await this.getPackagePower();
const maxPkg = Math.max(temps.length - 1, ...Object.keys(powerData).map(Number), 0);
const result: {
id: number;
power: number;
temp: number;
}[] = [];
for (let pkgId = 0; pkgId <= maxPkg; pkgId++) {
const entry = powerData[pkgId] ?? {};
result.push({
id: pkgId,
power: entry.total ?? -1,
temp: temps[pkgId] ?? -1,
});
}
return result;
}
}

View File

@@ -39,6 +39,18 @@ export class CpuLoad {
percentSteal!: number;
}
@ObjectType()
export class CpuPackages {
@Field(() => Float, { description: 'Total CPU package power draw (W)' })
totalPower?: number;
@Field(() => [Float], { description: 'Power draw per package (W)' })
power?: number[];
@Field(() => [Float], { description: 'Temperature per package (°C)' })
temp?: number[];
}
@ObjectType({ implements: () => Node })
export class CpuUtilization extends Node {
@Field(() => Float, { description: 'Total CPU load in percent' })
@@ -100,4 +112,12 @@ export class InfoCpu extends Node {
@Field(() => [String], { nullable: true, description: 'CPU feature flags' })
flags?: string[];
@Field(() => [[[Int]]], {
description: 'Per-package array of core/thread pairs, e.g. [[[0,1],[2,3]], [[4,5],[6,7]]]',
})
topology!: number[][][];
@Field(() => CpuPackages)
packages!: CpuPackages;
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
@Module({
providers: [CpuService, CpuTopologyService],
exports: [CpuService, CpuTopologyService],
})
export class CpuModule {}

View File

@@ -1,5 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
vi.mock('systeminformation', () => ({
@@ -88,9 +89,27 @@ vi.mock('systeminformation', () => ({
describe('CpuService', () => {
let service: CpuService;
let cpuTopologyService: CpuTopologyService;
beforeEach(() => {
service = new CpuService();
cpuTopologyService = {
generateTopology: vi.fn().mockResolvedValue([
[
[0, 1],
[2, 3],
],
[
[4, 5],
[6, 7],
],
]),
generateTelemetry: vi.fn().mockResolvedValue([
{ power: 32.5, temp: 45.0 },
{ power: 33.0, temp: 46.0 },
]),
} as any;
service = new CpuService(cpuTopologyService);
});
describe('generateCpu', () => {
@@ -121,6 +140,21 @@ describe('CpuService', () => {
l3: 12582912,
},
flags: ['fpu', 'vme', 'de', 'pse', 'tsc', 'msr', 'pae', 'mce', 'cx8'],
packages: {
totalPower: 65.5,
power: [32.5, 33.0],
temp: [45.0, 46.0],
},
topology: [
[
[0, 1],
[2, 3],
],
[
[4, 5],
[6, 7],
],
],
});
});

View File

@@ -2,25 +2,50 @@ import { Injectable } from '@nestjs/common';
import { cpu, cpuFlags, currentLoad } from 'systeminformation';
import { CpuUtilization, InfoCpu } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.model.js';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import {
CpuPackages,
CpuUtilization,
InfoCpu,
} from '@app/unraid-api/graph/resolvers/info/cpu/cpu.model.js';
@Injectable()
export class CpuService {
constructor(private readonly cpuTopologyService: CpuTopologyService) {}
async generateCpu(): Promise<InfoCpu> {
const { cores, physicalCores, speedMin, speedMax, stepping, ...rest } = await cpu();
const { cores, physicalCores, speedMin, speedMax, stepping, processors, ...rest } = await cpu();
const flags = await cpuFlags()
.then((flags) => flags.split(' '))
.then((f) => f.split(' '))
.catch(() => []);
// Gather telemetry
const packageList = await this.cpuTopologyService.generateTelemetry();
const topology = await this.cpuTopologyService.generateTopology();
// Compute total power (2 decimals)
const totalPower =
Math.round(packageList.reduce((sum, pkg) => sum + (pkg.power ?? 0), 0) * 100) / 100;
// Build CpuPackages object
const packages: CpuPackages = {
totalPower,
power: packageList.map((pkg) => pkg.power ?? -1),
temp: packageList.map((pkg) => pkg.temp ?? -1),
};
return {
id: 'info/cpu',
...rest,
cores: physicalCores,
threads: cores,
processors,
flags,
stepping: Number(stepping),
speedmin: speedMin || -1,
speedmax: speedMax || -1,
packages,
topology,
};
}

View File

@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CpuModule } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.module.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
import { DevicesResolver } from '@app/unraid-api/graph/resolvers/info/devices/devices.resolver.js';
import { DevicesService } from '@app/unraid-api/graph/resolvers/info/devices/devices.service.js';
@@ -14,7 +15,7 @@ import { VersionsService } from '@app/unraid-api/graph/resolvers/info/versions/v
import { ServicesModule } from '@app/unraid-api/graph/services/services.module.js';
@Module({
imports: [ConfigModule, ServicesModule],
imports: [ConfigModule, ServicesModule, CpuModule],
providers: [
// Main resolver
InfoResolver,
@@ -25,7 +26,6 @@ import { ServicesModule } from '@app/unraid-api/graph/services/services.module.j
CoreVersionsResolver,
// Services
CpuService,
MemoryService,
DevicesService,
OsService,

View File

@@ -6,6 +6,7 @@ import { Test } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
import { DevicesResolver } from '@app/unraid-api/graph/resolvers/info/devices/devices.resolver.js';
import { DevicesService } from '@app/unraid-api/graph/resolvers/info/devices/devices.service.js';
@@ -28,6 +29,7 @@ describe('InfoResolver Integration Tests', () => {
InfoResolver,
DevicesResolver,
CpuService,
CpuTopologyService,
MemoryService,
DevicesService,
OsService,

View File

@@ -1,13 +1,15 @@
import { Module } from '@nestjs/common';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuModule } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.module.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
import { MetricsResolver } from '@app/unraid-api/graph/resolvers/metrics/metrics.resolver.js';
import { ServicesModule } from '@app/unraid-api/graph/services/services.module.js';
@Module({
imports: [ServicesModule],
providers: [MetricsResolver, CpuService, MemoryService],
imports: [ServicesModule, CpuModule],
providers: [MetricsResolver, MemoryService],
exports: [MetricsResolver],
})
export class MetricsModule {}

View File

@@ -5,6 +5,7 @@ import { Test } from '@nestjs/testing';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
import { MetricsResolver } from '@app/unraid-api/graph/resolvers/metrics/metrics.resolver.js';
@@ -22,6 +23,7 @@ describe('MetricsResolver Integration Tests', () => {
providers: [
MetricsResolver,
CpuService,
CpuTopologyService,
MemoryService,
SubscriptionTrackerService,
SubscriptionHelperService,

View File

@@ -3,6 +3,7 @@ import { Test } from '@nestjs/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
import { MetricsResolver } from '@app/unraid-api/graph/resolvers/metrics/metrics.resolver.js';
@@ -18,6 +19,7 @@ describe('MetricsResolver', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MetricsResolver,
CpuTopologyService,
{
provide: CpuService,
useValue: {
@@ -163,6 +165,7 @@ describe('MetricsResolver', () => {
const testModule = new MetricsResolver(
cpuService,
{} as any,
memoryService,
subscriptionTracker as any,
{} as any
@@ -170,7 +173,7 @@ describe('MetricsResolver', () => {
testModule.onModuleInit();
expect(subscriptionTracker.registerTopic).toHaveBeenCalledTimes(2);
expect(subscriptionTracker.registerTopic).toHaveBeenCalledTimes(3);
expect(subscriptionTracker.registerTopic).toHaveBeenCalledWith(
'CPU_UTILIZATION',
expect.any(Function),

View File

@@ -1,11 +1,12 @@
import { OnModuleInit } from '@nestjs/common';
import { Logger, OnModuleInit } from '@nestjs/common';
import { Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql';
import { AuthAction, Resource } from '@unraid/shared/graphql.model.js';
import { UsePermissions } from '@unraid/shared/use-permissions.directive.js';
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { CpuUtilization } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.model.js';
import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';
import { CpuPackages, CpuUtilization } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.model.js';
import { CpuService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.service.js';
import { MemoryUtilization } from '@app/unraid-api/graph/resolvers/info/memory/memory.model.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
@@ -15,8 +16,10 @@ import { SubscriptionTrackerService } from '@app/unraid-api/graph/services/subsc
@Resolver(() => Metrics)
export class MetricsResolver implements OnModuleInit {
private readonly logger = new Logger(MetricsResolver.name);
constructor(
private readonly cpuService: CpuService,
private readonly cpuTopologyService: CpuTopologyService,
private readonly memoryService: MemoryService,
private readonly subscriptionTracker: SubscriptionTrackerService,
private readonly subscriptionHelper: SubscriptionHelperService
@@ -33,6 +36,33 @@ export class MetricsResolver implements OnModuleInit {
1000
);
this.subscriptionTracker.registerTopic(
PUBSUB_CHANNEL.CPU_TELEMETRY,
async () => {
const packageList = (await this.cpuTopologyService.generateTelemetry()) ?? [];
// Compute total power with 2 decimals
const totalPower = Number(
packageList.reduce((sum, pkg) => sum + (pkg.power ?? 0), 0).toFixed(2)
);
const packages: CpuPackages = {
totalPower,
power: packageList.map((pkg) => pkg.power ?? -1),
temp: packageList.map((pkg) => pkg.temp ?? -1),
};
this.logger.debug(`CPU_TELEMETRY payload: ${JSON.stringify(packages)}`);
// Publish the payload
pubsub.publish(PUBSUB_CHANNEL.CPU_TELEMETRY, {
systemMetricsCpuTelemetry: packages,
});
this.logger.debug(`CPU_TELEMETRY payload2: ${JSON.stringify(packages)}`);
},
5000
);
// Register memory polling with 2 second interval
this.subscriptionTracker.registerTopic(
PUBSUB_CHANNEL.MEMORY_UTILIZATION,
@@ -77,6 +107,18 @@ export class MetricsResolver implements OnModuleInit {
return this.subscriptionHelper.createTrackedSubscription(PUBSUB_CHANNEL.CPU_UTILIZATION);
}
@Subscription(() => CpuPackages, {
name: 'systemMetricsCpuTelemetry',
resolve: (value) => value.systemMetricsCpuTelemetry,
})
@UsePermissions({
action: AuthAction.READ_ANY,
resource: Resource.INFO,
})
public async systemMetricsCpuTelemetrySubscription() {
return this.subscriptionHelper.createTrackedSubscription(PUBSUB_CHANNEL.CPU_TELEMETRY);
}
@Subscription(() => MemoryUtilization, {
name: 'systemMetricsMemory',
resolve: (value) => value.systemMetricsMemory,

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# Arguments
# $1: SSH server name (required)
# Check if the server name is provided
if [[ -z "$1" ]]; then
echo "Error: SSH server name is required."
echo "Usage: $0 <server_name>"
exit 1
fi
# Set server name from command-line argument
server_name="$1"
# Build the package
echo "Building unraid-shared package..."
pnpm build
if [ $? -ne 0 ]; then
echo "Build failed!"
exit 1
fi
# Source directory path
source_directory="./dist"
# Check if dist directory exists
if [ ! -d "$source_directory" ]; then
echo "The dist directory does not exist after build!"
exit 1
fi
# Destination directory path - deploy to node_modules/@unraid/shared/dist
destination_directory="/usr/local/unraid-api/node_modules/@unraid/shared"
# Create destination directory on remote server
ssh root@"${server_name}" "mkdir -p $destination_directory"
# Replace the value inside the rsync command with the user's input
rsync_command="rsync -avz --delete --progress --stats -e ssh \"$source_directory/\" \"root@${server_name}:$destination_directory/\""
echo "Executing the following command:"
echo "$rsync_command"
# Execute the rsync command and capture the exit code
eval "$rsync_command"
exit_code=$?
# Chown the directory
ssh root@"${server_name}" "chown -R root:root /usr/local/unraid-api/node_modules/@unraid/"
# Run unraid-api restart on remote host
ssh root@"${server_name}" 'INTROSPECTION=true LOG_LEVEL=trace unraid-api restart'
# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
afplay /System/Library/Sounds/Glass.aiff
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
# Linux
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
# Windows
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
fi
# Exit with the rsync command's exit code
exit $exit_code

View File

@@ -21,7 +21,8 @@
"build": "rimraf dist && tsc --project tsconfig.build.json",
"prepare": "npm run build",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"unraid:deploy": "./deploy.sh"
},
"keywords": [],
"author": "Lime Technology, Inc. <unraid.net>",

View File

@@ -5,6 +5,7 @@ export const GRAPHQL_PUBSUB_TOKEN = "GRAPHQL_PUBSUB";
export enum GRAPHQL_PUBSUB_CHANNEL {
ARRAY = "ARRAY",
CPU_UTILIZATION = "CPU_UTILIZATION",
CPU_TELEMETRY = "CPU_TELEMETRY",
DASHBOARD = "DASHBOARD",
DISPLAY = "DISPLAY",
INFO = "INFO",