Skip to content

Conversation

@pujitm
Copy link
Member

@pujitm pujitm commented Oct 27, 2025

Summary by CodeRabbit

  • New Features
    • CPU package topology view showing core/thread groupings per package
    • Per-package power and temperature metrics with total CPU power
    • Real-time CPU telemetry subscription streaming live power/temperature updates
  • Tests
    • Updated tests to validate topology and per-package telemetry fields
  • Chores
    • Added deployment script and package script to deploy shared package remotely

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 27, 2025

Walkthrough

Adds CPU package topology discovery and per-package telemetry. Introduces CpuTopologyService that reads sysfs (hwmon, RAPL) for temps and power, integrates it into CpuService and MetricsResolver, adds CpuPackages GraphQL type and subscription, registers new CpuModule, and adds a deployment script for the shared package.

Changes

Cohort / File(s) Change Summary
GraphQL Schema & Types
api/generated-schema.graphql, api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts
New CpuPackages GraphQL type (totalPower, power[], temp[]). InfoCpu extended with topology (3D int array) and packages. Subscription systemMetricsCpuTelemetry: CpuPackages! added to schema.
CPU Topology Service
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
New NestJS injectable CpuTopologyService that discovers per-package core/thread topology from /sys/devices/system/cpu, reads hwmon temps and RAPL power domains, provides generateTopology() and generateTelemetry(), includes caching, logging and error handling.
CPU Service Integration & Tests
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts, api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
CpuService now injects CpuTopologyService; generateCpu() returns enhanced payload with processors, topology, and packages (totalPower, power[], temp[]). Tests updated to mock topology/telemetry and assert new structure; constructor signature changed in service.
Cpu Module & Module Wiring
api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts, api/src/unraid-api/graph/resolvers/info/info.module.ts, api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
New CpuModule providing CpuService and CpuTopologyService. InfoModule and MetricsModule import CpuModule and remove direct CpuService provider references.
Metrics Resolver, Subscription & Tests
api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts, api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts, api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
MetricsResolver now injects CpuTopologyService, adds a 5s polling registration for CPU telemetry, computes CpuPackages payload, logs and publishes on CPU_TELEMETRY. Adds GraphQL subscription resolver for systemMetricsCpuTelemetry. Tests/registers new provider and update expectations.
Integration Tests Update
api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
Test module now registers CpuTopologyService as a provider.
Pubsub Channel
packages/unraid-shared/src/pubsub/graphql.pubsub.ts
Added CPU_TELEMETRY = "CPU_TELEMETRY" enum member to GRAPHQL_PUBSUB_CHANNEL.
Deployment Script & Package
packages/unraid-shared/deploy.sh, packages/unraid-shared/package.json
New deployment script deploy.sh for rsync-based remote deploy, service restart and notifications; package.json gains unraid:deploy script and a minor formatting change.

Sequence Diagram(s)

sequenceDiagram
    participant MetricsResolver
    participant PubSub
    participant CpuTopologyService
    participant Sysfs as Sysfs (hwmon / powercap)
    participant Subscriber

    rect rgba(200,230,201,0.6)
    MetricsResolver->>MetricsResolver: Init CPU_TELEMETRY polling (5s)
    end

    loop Every 5s
        MetricsResolver->>CpuTopologyService: generateTelemetry()
        CpuTopologyService->>Sysfs: Read RAPL energy samples (/sys/class/powercap)
        Sysfs-->>CpuTopologyService: energy_uj samples
        CpuTopologyService->>Sysfs: Read hwmon temps (/sys/class/hwmon)
        Sysfs-->>CpuTopologyService: temp labels/values
        CpuTopologyService-->>MetricsResolver: per-package {id,power,temp}[]
        MetricsResolver->>MetricsResolver: build CpuPackages (totalPower,power[],temp[])
        MetricsResolver->>PubSub: publish CPU_TELEMETRY payload
    end

    Subscriber->>PubSub: Subscribe systemMetricsCpuTelemetry
    PubSub-->>Subscriber: Deliver CpuPackages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Review areas needing extra attention:
    • CpuTopologyService sysfs parsing, range expansion, deduplication and caching logic.
    • RAPL energy sampling math and unit conversions, and handling missing/partial data.
    • Integration points: DI changes across modules (CpuModule, InfoModule, MetricsModule) and updated constructors.
    • Subscription polling lifecycle and potential resource/timer cleanup.
    • Tests updated to mock new service—verify mocks mirror real behavior and edge cases.

Poem

🐰
I peeked in sysfs late at night,
Counting cores by pale LED light,
Temps and watts now sing in rows,
Packages told what each one knows.
Hops of telemetry — metrics bright!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "[testing, don't merge] feat: add cpu power query & subscription" is directly related to the main changes in the changeset. The pull request adds CPU power and temperature telemetry capabilities through both a GraphQL query field (InfoCpu.packages) and a new subscription (systemMetricsCpuTelemetry). The title clearly captures these two key features—power query and subscription—along with a testing/draft indicator. While the PR also adds CPU topology data, the title adequately summarizes the primary focus without needing to enumerate every detail, as the criteria allow.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cpu-power2

Comment @coderabbitai help to get the list of available commands and usage tips.

@@ -1,13 +1,15 @@
import { Module } from '@nestjs/common';

import { CpuTopologyService } from '@app/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.js';

Check notice

Code scanning / CodeQL

Unused variable, import, function or class Note

Unused import CpuTopologyService.

Copilot Autofix

AI 4 days ago

To fix this issue, simply remove the unused import statement for CpuTopologyService in api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts. No reference to this import exists in the code, so its removal will not affect the functionality or structure of the module. Remove only the import on line 3, leaving other lines unchanged.

Suggested changeset 1
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts b/api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
--- a/api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
+++ b/api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
@@ -1,6 +1,5 @@
 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';
EOF
@@ -1,6 +1,5 @@
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';
Copilot is powered by AI and may make mistakes. Always verify output.
@codecov
Copy link

codecov bot commented Oct 27, 2025

Codecov Report

❌ Patch coverage is 52.41935% with 118 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.68%. Comparing base (34075e4) to head (2db3bf5).

Files with missing lines Patch % Lines
...i/graph/resolvers/info/cpu/cpu-topology.service.ts 48.83% 88 Missing ⚠️
...id-api/graph/resolvers/metrics/metrics.resolver.ts 30.30% 23 Missing ⚠️
...c/unraid-api/graph/resolvers/info/cpu/cpu.model.ts 46.15% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1763      +/-   ##
==========================================
- Coverage   52.69%   52.68%   -0.01%     
==========================================
  Files         865      867       +2     
  Lines       49343    49582     +239     
  Branches     4952     4982      +30     
==========================================
+ Hits        26000    26121     +121     
- Misses      23270    23388     +118     
  Partials       73       73              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (8)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts (1)

121-122: Reconsider required packages field.

The packages field is marked as required (!), but telemetry collection might fail in some environments. Consider making this field nullable to handle cases where package power/temperature data is unavailable.

Apply this diff if telemetry can be unavailable:

-    @Field(() => CpuPackages)
+    @Field(() => CpuPackages, { nullable: true })
     packages!: CpuPackages;
api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts (1)

22-22: Consider providing a proper mock for CpuTopologyService.

While adding CpuTopologyService to the providers is necessary, consider providing a proper mock implementation rather than relying on the empty object at line 168. This will make the test more robust and easier to maintain.

For example:

{
    provide: CpuTopologyService,
    useValue: {
        generateTelemetry: vi.fn().mockResolvedValue({
            totalPower: 100,
            power: [50, 50],
            temp: [45, 45],
        }),
    },
},
api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts (3)

8-8: Wiring CpuTopologyService in the test module is appropriate.

Needed to satisfy CpuService constructor DI in this integration setup. Optionally, import CpuModule instead of listing CPU providers to mirror production wiring.

Also applies to: 26-26


124-141: Avoid as any in test spy return; type the payload.

Model the return value to match MemoryService.generateMemoryLoad() and keep types intact.

-            vi.spyOn(memoryService, 'generateMemoryLoad').mockImplementation(async () => {
+            vi.spyOn(memoryService, 'generateMemoryLoad').mockImplementation(async () => {
                 executionCount++;
                 await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate slow operation
-                return {
+                const payload: Awaited<ReturnType<MemoryService['generateMemoryLoad']>> = {
                     id: 'memory-utilization',
                     total: 16000000000,
                     used: 8000000000,
                     free: 8000000000,
                     available: 8000000000,
                     active: 4000000000,
                     buffcache: 2000000000,
                     percentTotal: 50,
                     swapTotal: 0,
                     swapUsed: 0,
                     swapFree: 0,
                     percentSwapTotal: 0,
-                } as any;
+                };
+                return payload;
             });

112-117: Deflake time-based polling tests with fake timers or injectable intervals.

Real setTimeout (50–2100ms) slows CI and can flake. Use vi.useFakeTimers() and advance timers, or make poll intervals injectable and set to 10–20ms in tests.

Example:

vi.useFakeTimers();
trackerService.subscribe(PUBSUB_CHANNEL.CPU_UTILIZATION);
await vi.advanceTimersByTimeAsync(1000); // or injected short interval

Also applies to: 160-162, 185-187

api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (2)

22-25: Run topology and telemetry in parallel to cut latency.

-        const packageList = await this.cpuTopologyService.generateTelemetry();
-        const topology = await this.cpuTopologyService.generateTopology();
+        const [packageList, topology] = await Promise.all([
+            this.cpuTopologyService.generateTelemetry(),
+            this.cpuTopologyService.generateTopology(),
+        ]);

19-20: Filter empty flag tokens.

split(' ') on empty string yields ['']. Filter falsy tokens.

-        const flags = await cpuFlags()
-            .then((f) => f.split(' '))
+        const flags = await cpuFlags()
+            .then((f) => f.split(' ').filter(Boolean))
             .catch(() => []);
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1)

35-44: Consider extracting range expansion logic to a helper function.

The range expansion logic is complex and could benefit from extraction for clarity and testability.

Example refactor:

private expandCpuRange(rangeStr: string): number[] {
    return rangeStr
        .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));
}

Then use it in generateTopology:

const siblings = this.expandCpuRange(siblingsStrRaw);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff2906e and 1f15167.

📒 Files selected for processing (15)
  • api/generated-schema.graphql (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts (4 hunks)
  • packages/unraid-shared/deploy.sh (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • packages/unraid-shared/src/pubsub/graphql.pubsub.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts
  • packages/unraid-shared/src/pubsub/graphql.pubsub.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

api/**/*.{test,spec}.{ts,tsx}: API test suite is Vitest; do not use Jest
Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts
🧠 Learnings (1)
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
PR: unraid/api#1575
File: packages/unraid-shared/src/services/socket-config.service.spec.ts:10-13
Timestamp: 2025-08-09T01:03:29.676Z
Learning: Vitest is used for all testing across all repositories in the unraid organization, not Jest. Always use `vi` for mocking utilities, not `jest`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
🧬 Code graph analysis (6)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts (1)
api/src/unraid-api/graph/resolvers/info/info.model.ts (1)
  • ObjectType (13-44)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (2)
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1)
  • Module (10-15)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-37)
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1)
  • Injectable (12-70)
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (2)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (1)
  • Module (6-10)
api/src/unraid-api/graph/services/services.module.ts (1)
  • Module (8-13)
api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts (3)
api/src/core/pubsub.ts (1)
  • pubsub (12-12)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • Subscription (2046-2058)
web/composables/gql/graphql.ts (1)
  • Subscription (2032-2044)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1)
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1)
  • Injectable (6-211)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts

[notice] 3-3: Unused variable, import, function or class
Unused import CpuTopologyService.

🔇 Additional comments (17)
packages/unraid-shared/package.json (1)

24-25: LGTM!

The trailing comma addition and new deployment script are clean additions. The script follows npm conventions and properly references the deployment script.

packages/unraid-shared/deploy.sh (1)

53-53: Verify debug settings are appropriate.

The restart command uses INTROSPECTION=true and LOG_LEVEL=trace, which may produce excessive logging in production environments. Consider whether these debug settings should be configurable or removed for production deployments.

api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts (1)

176-176: Update expectation correctly reflects new topic.

The change from 2 to 3 registered topics correctly reflects the addition of the CPU_TELEMETRY channel. This is consistent with the changes in the codebase.

api/src/unraid-api/graph/resolvers/info/info.module.ts (1)

18-18: LGTM: Clean module composition.

Adding CpuModule to the imports properly encapsulates CPU-related services and follows NestJS best practices for module organization.

packages/unraid-shared/src/pubsub/graphql.pubsub.ts (1)

8-8: LGTM!

The addition of CPU_TELEMETRY channel is clean and follows the existing naming convention. The placement maintains alphabetical ordering within the enum.

api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts (1)

9-9: LGTM!

The import and registration of CpuTopologyService properly reflects the new dependency structure. This ensures the integration tests accurately exercise the full CPU service stack including topology functionality.

api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1)

11-13: Module wiring LGTM.

Importing CpuModule and dropping direct CpuService provider is correct DI usage.

api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (1)

6-10: LGTM.

Clean module encapsulation; exports enable downstream injection. ESM .js specifiers correct.

api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1)

14-15: DI addition LGTM.

Constructor injection of CpuTopologyService is clean and testable.

api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts (1)

143-157: Expectation updates LGTM.

Validates packages and topology structure precisely.

api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts (3)

1-26: LGTM!

Logger instance and dependency injection are properly configured. The imports follow ESM guidelines with .js extensions as required.


39-53: Good defensive coding and consistent patterns.

The telemetry polling implementation follows the established pattern from other subscriptions, with appropriate defensive coding (?? [], -1 for missing values) and proper rounding for power values.


110-120: LGTM!

The subscription follows the same pattern as existing subscriptions with proper permissions and resolve configuration.

api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (4)

146-146: Verify power measurement timing is appropriate for subscription interval.

The 100ms delay for power measurement means each telemetry call blocks for at least 100ms. With a 5-second subscription interval (from metrics.resolver.ts line 63), this is acceptable. However, if the interval were reduced or this method called more frequently, it could become a bottleneck.


1-9: LGTM!

Imports follow ESM guidelines with .js extensions, and the service is properly decorated with @Injectable().


67-104: Good defensive temperature parsing.

The method handles multiple hwmon drivers (coretemp, k10temp, zenpower) and gracefully handles missing files with appropriate error logging.


188-210: LGTM!

The telemetry aggregation logic correctly combines temperature and power data, with consistent use of -1 for missing values.

Comment on lines +1404 to +1413
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!]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix malformed description and verify Node implementation.

Line 1411 has a malformed description: """description: 'Temperature per package (°C)""" should be """Temperature per package (°C)""".

Additionally, CpuPackages implements Node (line 1404), which requires an id: PrefixedID! field, but this field is not present in the TypeScript model (cpu.model.ts lines 42-52). This will cause runtime errors.

To fix the TypeScript model, add the id field:

@ObjectType({ description: 'CPU package telemetry data', implements: () => Node })
export class CpuPackages extends Node {
    @Field(() => Float, { description: 'Total CPU package power draw (W)', nullable: true })
    totalPower?: number;

    @Field(() => [Float], { description: 'Power draw per package (W)', nullable: true })
    power?: number[];

    @Field(() => [Float], { description: 'Temperature per package (°C)', nullable: true })
    temp?: number[];
}

Or if CpuPackages shouldn't implement Node, remove it from the GraphQL ObjectType decorator in the TypeScript file and regenerate the schema.

🤖 Prompt for AI Agents
In api/generated-schema.graphql around lines 1404 to 1413, the GraphQL
description for temp is malformed and the type declares CpuPackages implements
Node but the corresponding TypeScript model (cpu.model.ts lines 42-52) lacks the
required id: PrefixedID! field; fix by correcting the triple-quoted description
to """Temperature per package (°C)""" and then either add the missing id field
to the CpuPackages TypeScript class (extend/implement Node and declare id:
PrefixedID with appropriate @Field() decorator) or remove the Node
implementation from the TypeScript @ObjectType decorator and regenerate the
schema so the GraphQL and TypeScript models stay consistent.

export class CpuTopologyService {
private readonly logger = new Logger(CpuTopologyService.name);

private topologyCache: { id: number; cores: number[][] }[] | null = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused topologyCache field.

The topologyCache field is declared but never read or written. Either implement caching or remove this dead code.

Apply this diff if caching is not needed:

     private readonly logger = new Logger(CpuTopologyService.name);
-
-    private topologyCache: { id: number; cores: number[][] }[] | null = null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private topologyCache: { id: number; cores: number[][] }[] | null = null;
private readonly logger = new Logger(CpuTopologyService.name);
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts around
line 10, remove the unused topologyCache field declaration (private
topologyCache: { id: number; cores: number[][] }[] | null = null;) since it is
never read or written; simply delete that line and run the TypeScript
compiler/linter to ensure no references remain and commit the change.

packages[pkgId].push(siblings);
}
} catch (err) {
console.warn('Topology read error for', dir, err);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use logger instance instead of console.warn.

Line 51 uses console.warn while lines 90, 97, 101, and 170 correctly use this.logger.warn. This inconsistency should be resolved.

Apply this diff:

             } catch (err) {
-                console.warn('Topology read error for', dir, err);
+                this.logger.warn('Topology read error for', dir, err);
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.warn('Topology read error for', dir, err);
} catch (err) {
this.logger.warn('Topology read error for', dir, err);
}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts around
line 51, replace the console.warn call with this.logger.warn to match the other
warning usages; ensure the method uses the class logger (this.logger.warn) and
pass the same message and variables (dir and err) to the logger so it stays
consistent with lines 90, 97, 101, and 170.


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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid type casting; use proper typing instead.

The (domains as any)['total'] cast violates the coding guideline to never use the any type and to avoid type casting.

As per coding guidelines.

Define a proper type for the domains object:

type DomainPower = Record<string, number>;

Then update the function signature and usage:

-    private async getPackagePower(): Promise<Record<number, Record<string, number>>> {
+    private async getPackagePower(): Promise<Record<number, DomainPower>> {
         // ... existing code ...
         
         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;
+            domains['total'] = Math.round(total * 100) / 100;
         }
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts around
line 182, avoid the (domains as any)['total'] cast: define a proper type (e.g.
type DomainPower = Record<string, number>) and use it for the domains
variable/function signature so domains is typed as DomainPower; then assign
domains['total'] = Math.round(total * 100) / 100 without any casting and update
any callers to pass/expect DomainPower.

Comment on lines +42 to +52
@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[];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add description to ObjectType and verify nullability.

The CpuPackages type is missing a description in the @ObjectType decorator. Additionally, verify the nullability semantics: the fields are optional (?) in TypeScript but appear as required in parts of the generated GraphQL schema (line 1404-1413 in generated-schema.graphql), creating an inconsistency.

Apply this diff to add a description:

-@ObjectType()
+@ObjectType({ description: 'CPU package telemetry data' })
 export class CpuPackages {

Additionally, confirm whether these fields should be optional or required. If telemetry collection can fail, consider keeping them optional and ensuring the GraphQL schema reflects this.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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({ description: 'CPU package telemetry data' })
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[];
}
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts around lines 42 to
52, add a description to the @ObjectType decorator (e.g., "Per-package CPU
telemetry: power and temperature") and make the GraphQL nullability match the
TypeScript optional fields by updating each @Field to include nullable: true
(keeping the TypeScript ? on totalPower, power, temp). This ensures the
generated schema shows these fields as optional when telemetry can fail.

Comment on lines +26 to +29
// Compute total power (2 decimals)
const totalPower =
Math.round(packageList.reduce((sum, pkg) => sum + (pkg.power ?? 0), 0) * 100) / 100;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix totalPower when a package has unknown power (-1).

pkg.power ?? 0 includes -1 in the sum, producing incorrect totals. Treat negatives/unknown as 0.

-        const totalPower =
-            Math.round(packageList.reduce((sum, pkg) => sum + (pkg.power ?? 0), 0) * 100) / 100;
+        const totalPower =
+            Math.round(
+                packageList.reduce(
+                    (sum, pkg) => sum + ((pkg.power != null && pkg.power >= 0) ? pkg.power : 0),
+                    0
+                ) * 100
+            ) / 100;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Compute total power (2 decimals)
const totalPower =
Math.round(packageList.reduce((sum, pkg) => sum + (pkg.power ?? 0), 0) * 100) / 100;
// Compute total power (2 decimals)
const totalPower =
Math.round(
packageList.reduce(
(sum, pkg) => sum + ((pkg.power != null && pkg.power >= 0) ? pkg.power : 0),
0
) * 100
) / 100;
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts around lines 26 to
29, the totalPower calculation currently treats pkg.power of -1 as a valid
value; change the reducer to treat any negative/unknown power as 0 by using
Math.max(0, Number(pkg.power ?? 0)) (or equivalent) when summing, then apply the
same rounding to two decimals as before so negative sentinel values are excluded
from the total.

Comment on lines +4 to 5
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused CpuService import.

With the introduction of CpuModule (line 4), the direct import of CpuService (line 5) is no longer needed since CpuModule now provides CpuService. This import should be removed to avoid confusion.

Apply this diff:

 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';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 { CpuModule } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.module.js';
import { DevicesResolver } from '@app/unraid-api/graph/resolvers/info/devices/devices.resolver.js';
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/info/info.module.ts around lines 4 to 5,
remove the unused import of CpuService (the second import line) because
CpuModule now provides CpuService; delete that import statement and run the
linter/build to ensure no remaining references to CpuService exist in this file.

Comment on lines +3 to 6
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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused imports to satisfy CodeQL and keep module clean.

CpuTopologyService and CpuService are no longer referenced here; drop both imports. This also aligns with moving CPU providers behind CpuModule.

Based on static analysis hints.

-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';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 { CpuModule } from '@app/unraid-api/graph/resolvers/info/cpu/cpu.module.js';
import { MemoryService } from '@app/unraid-api/graph/resolvers/info/memory/memory.service.js';
🧰 Tools
🪛 GitHub Check: CodeQL

[notice] 3-3: Unused variable, import, function or class
Unused import CpuTopologyService.

🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts around lines 3
to 6, remove the two unused imports CpuTopologyService and CpuService which are
no longer referenced; keep the remaining imports (CpuModule and MemoryService)
intact to satisfy CodeQL and align CPU providers behind CpuModule.

Comment on lines +54 to +61
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)}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate debug log.

Line 61 logs the identical payload already logged at line 54. This creates unnecessary log noise.

Apply this diff:

                 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)}`);
             },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)}`);
this.logger.debug(`CPU_TELEMETRY payload: ${JSON.stringify(packages)}`);
// Publish the payload
pubsub.publish(PUBSUB_CHANNEL.CPU_TELEMETRY, {
systemMetricsCpuTelemetry: packages,
});
},
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts around lines
54 to 61, there is a duplicate debug log of the same CPU_TELEMETRY payload
(logged at line 54 and again at line 61); remove the second
this.logger.debug(...) call (the one at line 61) so the payload is only logged
once when publishing, keeping the first debug before pubsub.publish and leaving
the rest of the method unchanged.

Comment on lines +40 to +46
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid eval with user input.

Building the rsync command as a string and executing it with eval introduces a command injection risk. Even though server_name is intended to be a trusted SSH hostname, this pattern is fragile.

Apply this diff to execute rsync directly:

-# 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=$?
+# Execute rsync and capture the exit code
+echo "Syncing $source_directory/ to root@${server_name}:$destination_directory/"
+rsync -avz --delete --progress --stats -e ssh "$source_directory/" "root@${server_name}:$destination_directory/"
+exit_code=$?

Committable suggestion skipped: line range outside the PR's diff.

pujitm and others added 15 commits October 27, 2025 11:05
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1763/dynamix.unraid.net.plg

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1)

3-6: Drop unused CPU service/topology imports; rely on CpuModule.

These imports are unused here. Keep CpuModule in imports; remove direct imports to satisfy CodeQL and keep module clean.

Apply this diff:

-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';
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts (1)

94-113: Replace as any mock; type it and include id in telemetry.

Avoid any and match the real CpuTopologyService signature to prevent type drift.

Apply:

-    let cpuTopologyService: CpuTopologyService;
+    let cpuTopologyService: CpuTopologyService;

     beforeEach(() => {
-        cpuTopologyService = {
-            generateTopology: vi.fn().mockResolvedValue([
+        const cpuTopologyMock: Pick<CpuTopologyService, 'generateTopology' | 'generateTelemetry'> = {
+            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);
+            generateTelemetry: vi.fn().mockResolvedValue([
+                { id: 0, power: 32.5, temp: 45.0 },
+                { id: 1, power: 33.0, temp: 46.0 },
+            ]),
+        };
+
+        cpuTopologyService = cpuTopologyMock as unknown as CpuTopologyService;
+        service = new CpuService(cpuTopologyService);
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1)

26-29: Correct totalPower: exclude unknown (-1) readings.

Current reducer includes -1, producing under-reported totals.

Apply:

-        const totalPower =
-            Math.round(packageList.reduce((sum, pkg) => sum + (pkg.power ?? 0), 0) * 100) / 100;
+        const totalPower =
+            Math.round(
+                packageList.reduce(
+                    (sum, pkg) => sum + ((pkg.power != null && pkg.power >= 0) ? pkg.power : 0),
+                    0
+                ) * 100
+            ) / 100;
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (3)

10-10: Remove unused topologyCache field.

The topologyCache field is declared but never read or written throughout the service.

Apply this diff:

     private readonly logger = new Logger(CpuTopologyService.name);
-
-    private topologyCache: { id: number; cores: number[][] }[] | null = null;

51-51: Use logger instance instead of console.warn.

Line 51 uses console.warn while lines 90, 97, 101, and 170 correctly use this.logger.warn.

Apply this diff:

             } catch (err) {
-                console.warn('Topology read error for', dir, err);
+                this.logger.warn('Topology read error for', dir, err);
             }

182-182: Avoid type casting; use proper typing instead.

The (domains as any)['total'] cast violates the coding guideline to avoid type casting and never use the any type.

As per coding guidelines, define a proper type for the domains object:

type DomainPower = Record<string, number>;

Then update the function signature:

-    private async getPackagePower(): Promise<Record<number, Record<string, number>>> {
+    private async getPackagePower(): Promise<Record<number, DomainPower>> {
         // ... existing code ...
         
         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;
+            domains['total'] = Math.round(total * 100) / 100;
         }
🧹 Nitpick comments (6)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts (1)

162-178: Avoid as any in systeminformation mock; use Partial typing.

Keep tests type-safe without any.

Apply:

-            vi.mocked(cpu).mockResolvedValueOnce({
+            vi.mocked(cpu).mockResolvedValueOnce({
                 manufacturer: 'Intel',
                 brand: 'Core i7-9700K',
                 vendor: 'Intel',
                 family: '6',
                 model: '158',
                 stepping: '12',
                 revision: '',
                 voltage: '1.2V',
                 speed: 3.6,
                 cores: 16,
                 physicalCores: 8,
                 processors: 1,
                 socket: 'LGA1151',
                 cache: { l1d: 32768, l1i: 32768, l2: 262144, l3: 12582912 },
-            } as any);
+            } as Partial<import('systeminformation').Systeminformation.CpuData> as unknown as import('systeminformation').Systeminformation.CpuData);
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (3)

22-25: Harden topology/telemetry collection with graceful fallbacks.

A top‑level fs failure in CpuTopologyService (e.g., initial readdir) will reject and break the resolver. Catch and default.

Apply:

-        // Gather telemetry
-        const packageList = await this.cpuTopologyService.generateTelemetry();
-        const topology = await this.cpuTopologyService.generateTopology();
+        // Gather telemetry with graceful fallbacks
+        const [packageList, topology] = await Promise.all([
+            this.cpuTopologyService.generateTelemetry().catch(() => [] as { power: number; temp: number }[]),
+            this.cpuTopologyService.generateTopology().catch(() => [] as number[][][]),
+        ]);

Optionally set packages.totalPower to -1 when packageList.length === 0.


19-20: Make flags parsing robust to extra whitespace.

Trim and split by whitespace; drop empties.

Apply:

-        const flags = await cpuFlags()
-            .then((f) => f.split(' '))
+        const flags = await cpuFlags()
+            .then((f) => f.trim().split(/\s+/).filter(Boolean))
             .catch(() => []);

44-45: Optional: safer stepping parse.

Avoid NaN on empty strings; default to -1.

Apply:

-            stepping: Number(stepping),
+            stepping: Number.isFinite(Number(stepping)) && String(stepping).trim() !== '' ? Number(stepping) : -1,
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (2)

67-104: Consider more specific error messages for observability.

Lines 90, 97, and 101 all log "Failed to read file" without indicating which operation or path failed, making debugging more difficult.

Consider including context in the error messages:

// Example for line 90
this.logger.warn(`Failed to read temp input file ${inputFile}`, err);

// Example for line 97
this.logger.warn(`Failed to read hwmon path ${path}`, err);

// Example for line 101
this.logger.warn('Failed to read /sys/class/hwmon directory', err);

146-146: Consider extracting the sampling interval as a named constant.

The 100ms delay is a magic number. Extracting it would improve readability and make it easier to adjust if needed.

private static readonly POWER_SAMPLE_INTERVAL_MS = 100;

// Then in the method:
await new Promise((res) => setTimeout(res, CpuTopologyService.POWER_SAMPLE_INTERVAL_MS));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f15167 and 2db3bf5.

📒 Files selected for processing (15)
  • api/generated-schema.graphql (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.module.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts (3 hunks)
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts (4 hunks)
  • packages/unraid-shared/deploy.sh (1 hunks)
  • packages/unraid-shared/package.json (1 hunks)
  • packages/unraid-shared/src/pubsub/graphql.pubsub.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • api/src/unraid-api/graph/resolvers/info/info.module.ts
  • api/src/unraid-api/graph/resolvers/info/info.resolver.integration.spec.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.integration.spec.ts
  • api/generated-schema.graphql
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.spec.ts
  • packages/unraid-shared/deploy.sh
  • packages/unraid-shared/package.json
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.model.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.resolver.ts
🧰 Additional context used
📓 Path-based instructions (6)
api/src/unraid-api/**

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code

Files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start

Files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
  • packages/unraid-shared/src/pubsub/graphql.pubsub.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)

Files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
  • api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts
  • api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts
api/**/*.{test,spec}.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)

api/**/*.{test,spec}.{js,jsx,ts,tsx}: Use Vitest for tests in the api; do not use Jest
Prefer not to mock simple dependencies in tests
For error testing, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless the message format is the subject under test

Files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
api/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

api/**/*.{test,spec}.{ts,tsx}: API test suite is Vitest; do not use Jest
Prefer not to mock simple dependencies in API tests

Files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
{api,web}/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests

Files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
🧠 Learnings (2)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls

Applied to files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
PR: unraid/api#1575
File: packages/unraid-shared/src/services/socket-config.service.spec.ts:10-13
Timestamp: 2025-08-09T01:03:29.676Z
Learning: Vitest is used for all testing across all repositories in the unraid organization, not Jest. Always use `vi` for mocking utilities, not `jest`.

Applied to files:

  • api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.spec.ts
🧬 Code graph analysis (4)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (2)
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1)
  • Module (10-15)
api/src/unraid-api/graph/resolvers/info/info.module.ts (1)
  • Module (17-37)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (2)
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1)
  • Injectable (6-211)
api/src/unraid-api/cli/generated/graphql.ts (1)
  • InfoCpu (855-892)
api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.service.ts (1)
  • Injectable (12-70)
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts (1)
api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (1)
  • Module (6-10)
🪛 GitHub Check: CodeQL
api/src/unraid-api/graph/resolvers/metrics/metrics.module.ts

[notice] 3-3: Unused variable, import, function or class
Unused import CpuTopologyService.

🔇 Additional comments (3)
packages/unraid-shared/src/pubsub/graphql.pubsub.ts (1)

8-8: New CPU_TELEMETRY channel — looks good. Please verify wiring.

Enum addition is fine. Ensure all publishers/subscribers use this constant and any filters/auth align with other channels.

api/src/unraid-api/graph/resolvers/info/cpu/cpu.module.ts (1)

6-10: CpuModule wiring — LGTM.

Providers/exports are correct, ESM import specifiers use .js as required.

api/src/unraid-api/graph/resolvers/info/cpu/cpu-topology.service.ts (1)

188-210: LGTM!

The telemetry aggregation logic correctly combines temperature and power data, handles missing values appropriately with -1 sentinels, and ensures all packages are represented.

@pujitm pujitm closed this Oct 27, 2025
@pujitm pujitm deleted the feat/cpu-power2 branch October 27, 2025 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants