Skip to content

[WIP] extract mothership to connect plugin #1367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 69 commits into
base: main
Choose a base branch
from
Draft

Conversation

pujitm
Copy link
Member

@pujitm pujitm commented Apr 23, 2025

Summary by CodeRabbit

  • New Features

    • Introduced modular Connect API plugin with dedicated modules for connection, mothership, remote access, and system/network services.
    • Added robust configuration management with migration support and dynamic remote access handling (static, UPnP, disabled).
    • Implemented event-driven login/logout, connection status, and mothership event handling.
    • Enhanced network and URL resolution services for accurate server access and allowed origins.
    • Added health checks, cloud connectivity verification, and improved error handling and logging.
    • Provided utilities for file existence checks, data parsing, and JSONForms integration.
    • Added GraphQL directives and decorators for permissions management.
  • Bug Fixes

    • Improved remote access timeout detection and error reporting.
    • Fixed theme detection and session management in the web GUI for localhost requests.
  • Refactor

    • Centralized configuration and state management via dependency injection and services.
    • Replaced Redux store usage with NestJS ConfigService and event emitter patterns.
    • Migrated shared types, models, and utilities to a new shared package.
    • Updated imports to use shared modules for consistency.
  • Documentation

    • Added detailed README and usage instructions for the Connect API plugin and shared packages.
  • Chores

    • Updated dependencies, scripts, and build configurations for development and deployment.
    • Added new test suites; disabled or removed obsolete tests and listeners.
  • Style

    • Standardized code formatting, import ordering, and naming conventions.
  • Revert

    • Disabled or removed legacy modules, actions, listeners, and GraphQL schema related to remote access and cloud checks.

Copy link
Contributor

coderabbitai bot commented Apr 23, 2025

Walkthrough

This update refactors Unraid's API and Connect plugin, modularizing connect, mothership, and remote access logic into new NestJS modules and services. It removes legacy Redux store code, replaces direct state management with ConfigService, and centralizes shared types and utilities in a new @unraid/shared package. The API's GraphQL schema, resolvers, and permission directives are restructured for maintainability and extensibility.

Changes

Files/Groups Change Summary
api/src/store/modules/*, api/src/store/actions/*, api/src/store/listeners/*, api/src/store/getters/*, api/src/store/modules/dynamic-remote-access.ts, api/src/store/modules/remote-graphql.ts, api/src/store/modules/cache.ts, api/src/store/index.ts, api/src/store/root-reducer.ts, api/src/store/store-sync.ts Removed Redux slices, actions, listeners, and selectors for connect, remote access, and cache; rewired store composition.
api/src/graphql/resolvers/query/cloud/*, api/src/graphql/resolvers/subscription/remote-graphql/*, api/src/unraid-api/graph/resolvers/cloud/cloud.model.ts, api/src/unraid-api/graph/resolvers/connect/connect.module.ts, api/src/unraid-api/graph/resolvers/network/network.resolver.spec.ts, api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts, api/src/unraid-api/graph/resolvers/owner/owner.resolver.spec.ts, api/src/unraid-api/graph/resolvers/servers/server.resolver.ts, api/src/__test__/graphql/resolvers/subscription/network.test.ts, api/src/__test__/graphql/resolvers/query/cloud/check-mothership-authentication.test.ts Deleted legacy GraphQL resolvers, models, and tests for cloud/connect/remote access.
api/src/unraid-api/graph/graph.module.ts, api/src/unraid-api/graph/resolvers/*, api/src/unraid-api/graph/services/services.resolver.ts, api/src/unraid-api/plugin/plugin.module.ts, api/src/unraid-api/graph/validate-schema.ts Refactored GraphQL modules, moved to shared types, added schema validation script, updated dependency injection.
api/src/unraid-api/plugin/global-deps.module.ts, api/src/unraid-api/app/pubsub.module.ts, api/src/unraid-api/config/legacy-config.module.ts, api/src/unraid-api/config/config.loader.ts, api/src/unraid-api/config/store-sync.service.ts, api/src/unraid-api/config/factory/* Introduced global dependency and config modules, legacy config loader, and store sync service.
api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts, api/src/unraid-api/graph/services/services.resolver.ts Switched from global store to injected ConfigService for config access.
api/src/unraid-api/cli/sso/add-sso-user.command.ts, api/src/unraid-api/cli/sso/remove-sso-user.command.ts Temporarily disabled SSO user add/remove commands.
api/src/core/pubsub.ts Switched to shared GraphQL pubsub channels.
api/src/unraid-api/graph/sandbox-plugin.ts Hardcoded sandbox mode to enabled.
api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts Added RClone mutation fields.
api/src/unraid-api/graph/resolvers/flash-backup/flash-backup.resolver.ts Removed RCloneService injection.
api/src/unraid-api/plugin/plugin.service.ts Improved plugin import error handling and notifications.
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/*, api/src/unraid-api/unraid-file-modifier/modifications/patches/* Refactored PHP theme and session logic, added SSO support, updated timestamps.
api/src/unraid-api/auth/*, api/src/unraid-api/cli/apikey/*, api/src/unraid-api/graph/resolvers/*, api/src/unraid-api/rest/rest.controller.ts, api/src/unraid-api/graph/services/service.model.ts Switched to shared types for Role, Resource, and PrefixedID.
api/src/types/my-servers-config.ts Changed dynamicRemoteAccessType field from enum to string.
api/src/upnp/helpers.ts Made upnpClient a named export.
api/vite.config.ts, api/tsconfig.json, .vscode/settings.json, api/.vscode/settings.json Updated build, dev, and IDE settings for shared packages and source maps.
api/scripts/build.ts Vendors workspace packages during build.
plugin/builder/cli/common-environment.ts, plugin/builder/utils/version.ts, plugin/docker-compose.yml, plugin/.gitignore, plugin/Dockerfile Enhanced versioning, build, and Docker support.
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php Reads connect config from new JSON file, updates default values.
packages/unraid-api-plugin-connect/* Introduced modular NestJS connect plugin: new modules, services, models, event handlers, jobs, resolvers, helpers, tests, and config.
packages/unraid-shared/* Added shared package for types, directives, tokens, utilities, and network models.
api/generated-schema.graphql Removed connect and remote access types, queries, and mutations from schema; added minimal types for RClone and server profiles.
api/dev/states/myservers.cfg Downgraded config version and reset minigraph status.

Changes Table (Abbreviated)

File/Group Summary
api/src/store/* Removed Redux modules, actions, listeners, and selectors for connect/remote access/cache.
api/src/graphql/resolvers/query/cloud/* Deleted legacy cloud/connect GraphQL resolvers and models.
api/src/unraid-api/graph/resolvers/connect/* Removed legacy connect module.
api/src/unraid-api/graph/resolvers/cloud/* Removed legacy cloud resolver and model.
api/src/unraid-api/graph/*, plugin/*, packages/* Introduced modular NestJS plugin architecture, shared types, and new services.
api/generated-schema.graphql Pruned connect/remote access types and queries from schema.
packages/unraid-api-plugin-connect/* Added new connect, mothership, remote access modules, services, resolvers, and tests.
packages/unraid-shared/* Added shared types, directives, tokens, and utilities.
... ...

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant ConnectPlugin
    participant ConfigService
    participant MothershipModule
    participant RemoteAccessModule
    participant Shared

    Client->>API: GraphQL Query/Mutation
    API->>ConnectPlugin: Delegate to Connect Resolver/Service
    ConnectPlugin->>ConfigService: Read/Update Config
    ConnectPlugin->>MothershipModule: Setup/Manage Mothership Connection
    ConnectPlugin->>RemoteAccessModule: Enable/Disable Remote Access
    ConnectPlugin->>Shared: Use shared types/utilities
    MothershipModule->>ConfigService: Update connection metadata
    RemoteAccessModule->>ConfigService: Update remote access state
    API-->>Client: Return GraphQL Response
Loading

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • mdatelle
  • zackspear
  • elibosley

Poem

Refactored, modular, and shared anew,
The API’s heart beats clean and true.
Redux is gone, ConfigService reigns,
Plugins and motherships ride new lanes.
Shared types unite what once was split—
A tapestry woven, commit by commit!
🚀✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 9

🧹 Nitpick comments (9)
api/src/unraid-api/config/store-sync.service.ts (1)

8-23: Consider optimizing store sync performance

The service correctly implements OnModuleDestroy for proper cleanup and synchronizes the Redux store state to NestJS config. However, it currently syncs the entire store state on every change without any error handling.

Consider these improvements:

  1. Add error handling for store subscriptions
  2. Consider selective synchronization if the store is large
  3. Consider debouncing frequent updates
constructor(private configService: ConfigService) {
-    this.unsubscribe = store.subscribe(() => {
-        this.configService.set('store', store.getState());
-        this.logger.verbose('Synced store to NestJS Config');
+    this.unsubscribe = store.subscribe(() => {
+        try {
+            this.configService.set('store', store.getState());
+            this.logger.verbose('Synced store to NestJS Config');
+        } catch (error) {
+            this.logger.error('Failed to sync store to NestJS Config', error);
+        }
    });
}
api/src/unraid-api/config/legacy-config.module.ts (1)

8-15: Consider adding documentation for the loaders

The module effectively sets up the integration between legacy and NestJS config systems, but could benefit from JSDoc comments explaining the purpose of the loader functions.

@Module({
    imports: [ConfigModule.forRoot({
        isGlobal: true,
+       // Load environment variables and legacy Redux store state into NestJS ConfigService
        load: [loadAppEnvironment, loadLegacyStore],
    })],
    providers: [StoreSyncService],
    exports: [StoreSyncService],
})
export class LegacyConfigModule {}
api/src/unraid-api/app/pubsub.module.ts (1)

38-43: Consider tightening the EventEmitter memory‑leak guard

verboseMemoryLeak: true is great, but you might want to cap maxListeners to a sane default (e.g. 20) so leaking handlers fail fast in production instead of only logging warnings.

 EventEmitterModule.forRoot({
     wildcard: true,
     verboseMemoryLeak: true,
+    maxListeners: 20,
 }),
packages/unraid-api-plugin-connect/src/pubsub/user.service.ts (1)

33-40: Use an informational log level for normal logout flow

logger.error() implies an unexpected failure, yet logout is a normal, user‑initiated action. Recommend downgrading to log or warn to keep error channels meaningful.

-this.logger.error('Logging out user: %s', reason ?? 'No reason provided');
+this.logger.log('User logout: %s', reason ?? 'No reason provided');
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

114-118: URL protocol replacement is brittle

replace('http', 'ws') mistakenly produces wss://ws://s:// for https URLs. Safer:

-url: this.mothershipGraphqlLink.replace('http', 'ws'),
+url: this.mothershipGraphqlLink.replace(/^http/, 'ws'),

or use new URL():

const url = new URL(this.mothershipGraphqlLink);
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
packages/unraid-api-plugin-connect/src/config.persistence.ts (1)

23-26: Avoid magic‑string key "connect.config"

That literal appears in three separate places; a single typo would silently break persistence. Consider extracting a constant:

export const CONNECT_CFG = 'connect.config' as const;

then reuse CONNECT_CFG everywhere.
Besides safety it improves intent‑clarity for future readers.

Also applies to: 57-58, 100-104

packages/unraid-api-plugin-connect/src/config.entity.ts (1)

37-40: Minor doc typo

// Remote Access Configurationx → stray “x”.

-// Remote Access Configurationx
+// Remote‑access configuration
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (2)

45-60: Implement OnModuleInit for clarity

onModuleInit is present but the class does not implement the marker
interface. Adding implements OnModuleInit (and the import) makes the
lifecycle expectation explicit and prevents accidental renaming.

-import { Injectable, Logger } from '@nestjs/common';
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
 ...
-export class MothershipConnectionService {
+export class MothershipConnectionService implements OnModuleInit {

98-102: Narrow headers/params types after validation

After the isLoaded guard you know the values are defined, yet state
keeps the original (possibly undefined) types. A quick cast (or a
helper) lets TypeScript enforce non‑undefined on the returned object,
eliminating future ! assertions.

Not urgent, but worth tightening.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 026b0b3 and 5d4c8ba.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (24)
  • .vscode/settings.json (1 hunks)
  • api/.vscode/settings.json (1 hunks)
  • api/package.json (1 hunks)
  • api/src/consts.ts (0 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/app/app.module.ts (1 hunks)
  • api/src/unraid-api/app/pubsub.module.ts (1 hunks)
  • api/src/unraid-api/cli/pm2.service.ts (1 hunks)
  • api/src/unraid-api/cli/restart.command.ts (1 hunks)
  • api/src/unraid-api/cli/start.command.ts (1 hunks)
  • api/src/unraid-api/cli/stop.command.ts (1 hunks)
  • api/src/unraid-api/config/config.loader.ts (1 hunks)
  • api/src/unraid-api/config/legacy-config.module.ts (1 hunks)
  • api/src/unraid-api/config/store-sync.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/.prettierrc.cjs (1 hunks)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/config.entity.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/config.persistence.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/index.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/delay-function.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/pubsub/consts.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/pubsub/user.service.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • api/src/consts.ts
🧰 Additional context used
🧬 Code Graph Analysis (6)
api/src/unraid-api/config/config.loader.ts (1)
api/src/store/index.ts (1)
  • store (6-12)
api/src/unraid-api/config/legacy-config.module.ts (2)
api/src/unraid-api/app/app.module.ts (1)
  • Module (20-68)
api/src/unraid-api/config/config.loader.ts (2)
  • loadAppEnvironment (14-19)
  • loadLegacyStore (29-33)
api/src/unraid-api/config/store-sync.service.ts (1)
api/src/store/index.ts (1)
  • store (6-12)
packages/unraid-api-plugin-connect/src/index.ts (2)
packages/unraid-api-plugin-generator/src/templates/index.ts (1)
  • adapter (7-7)
packages/unraid-api-plugin-connect/src/config.entity.ts (1)
  • configFeature (145-150)
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (2)
packages/unraid-api-plugin-connect/src/pubsub/user.service.ts (1)
  • Injectable (12-53)
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)
  • Injectable (25-300)
packages/unraid-api-plugin-connect/src/config.entity.ts (3)
packages/unraid-api-plugin-connect/src/helpers/my-servers-config.ts (1)
  • MyServersConfig (29-53)
packages/unraid-api-plugin-generator/src/templates/config.entity.ts (1)
  • configFeature (16-20)
packages/unraid-api-plugin-connect/src/config.demo.ts (1)
  • ConnectConfig (3-6)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (21)
api/package.json (1)

70-70: Addition of event-emitter module looks good.

This dependency is correctly added to support the new event-driven architecture being introduced with the PubSubModule.

api/src/environment.ts (1)

99-100:

✅ Verification successful

Path constants appropriately moved from consts.ts to environment.ts.

The PM2 path constants are now correctly centralized with other environment-related configuration.


🏁 Script executed:

#!/bin/bash
# Verify that references to these constants have been updated throughout the codebase
rg -A 1 "import.*PM2_PATH|import.*ECOSYSTEM_PATH" --type ts

Length of output: 846


All PM2_PATH and ECOSYSTEM_PATH imports updated

Verified that every reference now points to @app/environment.js:

  • api/src/unraid-api/cli/stop.command.ts
  • api/src/unraid-api/cli/start.command.ts
  • api/src/unraid-api/cli/restart.command.ts
  • api/src/unraid-api/cli/pm2.service.ts

LGTM.

api/src/unraid-api/config/config.loader.ts (1)

14-19: Environment extraction implementation is clean and effective.

The function correctly filters out functions and undefined values, providing a clean configuration object.

api/src/unraid-api/cli/start.command.ts (1)

4-4: Import path correctly updated to reflect constant relocation.

The import now properly references the new location of ECOSYSTEM_PATH in the environment.ts file.

api/src/unraid-api/cli/restart.command.ts (1)

3-3: Import path updated correctly

The import path for ECOSYSTEM_PATH has been properly updated from @app/consts.js to @app/environment.js, aligning with the centralization of environment-related constants.

api/src/unraid-api/cli/stop.command.ts (1)

3-3: Import path updated correctly

The import path for ECOSYSTEM_PATH has been properly updated from @app/consts.js to @app/environment.js, consistent with the centralization of environment constants.

api/src/unraid-api/cli/pm2.service.ts (1)

9-9: Import path consolidated correctly

The imports for PM2_HOME and PM2_PATH have been correctly consolidated to a single import from @app/environment.js, consistent with the centralization of environment constants.

api/src/unraid-api/app/app.module.ts (1)

11-11:

✅ Verification successful

New modules properly integrated

The integration of LegacyConfigModule and PubSubModule in the AppModule is correctly implemented. These modules enhance the application with legacy configuration synchronization and event handling capabilities.

Let's verify these modules are correctly initialized and don't cause any issues on application startup:

Also applies to: 14-14, 22-23


🏁 Script executed:

#!/bin/bash
# Search for any initialization errors related to these new modules in the codebase
rg -A 3 -B 3 "PubSubModule|LegacyConfigModule" --type ts
rg -A 3 "Error.*PubSub|Error.*LegacyConfig" --type ts

Length of output: 2113


LegacyConfigModule and PubSubModule integration verified – no errors found

  • Definitions located in
    • api/src/unraid-api/config/legacy-config.module.ts
    • api/src/unraid-api/app/pubsub.module.ts
  • Both modules are correctly imported and listed in the imports array of
    • api/src/unraid-api/app/app.module.ts
  • No occurrences of initialization errors (Error.*PubSub or Error.*LegacyConfig) were found across the codebase

All checks pass. Approving the integration of these modules.

api/.vscode/settings.json (1)

6-8: Import style configuration added

The addition of non-relative import preferences for both TypeScript and JavaScript is a good standardization practice. This will help maintain consistent import paths across the codebase.

.vscode/settings.json (1)

12-14: Import style configuration added at root level

Adding the non-relative import preferences at the root level complements the same settings in the API directory, ensuring consistent import style throughout the entire project.

api/src/unraid-api/config/store-sync.service.ts (1)

1-6: Imports look good

The imports include the necessary NestJS decorators and the Redux store, with appropriate type imports.

api/src/unraid-api/config/legacy-config.module.ts (2)

1-2: Informative comment

The comment clearly explains the purpose of this module as a bridge between legacy config and NestJS config system.


3-15: Module structure looks good

The module correctly imports ConfigModule and configures it with appropriate loaders. It properly provides and exports the StoreSyncService for use in other modules.

packages/unraid-api-plugin-connect/package.json (1)

19-25: Dependency updates align with new GraphQL and event handling features

The added dependencies support the new GraphQL client service with WebSocket capabilities, the PubSub event bus system, and connection management features mentioned in the PR description. Good choice of specific versions that should work well together.

Also applies to: 32-34, 36-40, 43-48, 52-58

packages/unraid-api-plugin-connect/src/mothership/delay-function.ts (1)

1-22: Well-implemented retry delay function with best practices

This utility function implements exponential backoff with optional jitter, which are excellent practices for network retry logic. The jitter helps prevent the "thundering herd" problem in distributed systems, and the exponential backoff ensures retries don't overwhelm the system.

The code is well-commented and has sensible defaults. The use of optional parameters with destructuring makes the function flexible and easy to use.

packages/unraid-api-plugin-connect/src/index.ts (1)

1-7: Style changes align with new Prettier configuration

The changes to import style, indentation, and constructor formatting align with the new Prettier configuration while maintaining the same functionality. Good job enforcing consistent code style across the codebase.

Also applies to: 8-8, 10-13, 15-21

packages/unraid-api-plugin-connect/.prettierrc.cjs (1)

1-38: Well-structured Prettier configuration improves code consistency

The Prettier configuration is comprehensive and logically organized:

  • Base formatting rules are clear and consistent
  • Import grouping is logical with good separation between Nest.js/Node.js, third-party, and application code
  • Type imports are prioritized within each group
  • Parser plugins handle TypeScript and decorators correctly

This will significantly improve code readability and maintainability across the project.

packages/unraid-api-plugin-connect/src/pubsub/consts.ts (1)

1-8: Comment acknowledges maintenance concerns with shared constants

The comment clearly explains the current limitation in the build/release system and indicates this approach is temporary. This transparency is good for future developers.

packages/unraid-api-plugin-connect/src/pubsub/user.service.ts (1)

21-23: Token name mismatch could break DI

GRAPHQL_PUB_SUB_TOKEN comes from ./consts.ts, whereas the API side exports (in this PR) GRAPHQL_PUB_SUB. If the literal string values ever diverge, Nest will fail to resolve the dependency at runtime.

Either:

  1. Re‑export the constant from the API package and import it here, or
  2. Keep a shared tokens.ts package to guarantee single‑source‑of‑truth.

This avoids fragile “same string, different file” coupling.

packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

169-176: appErrorLink swallows errors silently

The custom ApolloLink completes the observer without calling observer.error, meaning callers never know a request failed. If intentional, add a comment; otherwise pass the error downstream:

-    error: (error) => {
-        this.logger.warn('Apollo error, will not retry: %s', error?.message);
-        observer.complete();
-    },
+    error: (error) => {
+        this.logger.warn('Apollo error, propagating: %s', error?.message);
+        observer.error?.(error);
+    },
packages/unraid-api-plugin-connect/src/config.entity.ts (1)

115-116: ⚠️ Potential issue

Typo breaks validation – MinigraphStatusMinigraphStatus

@IsEnum(MinigraphStatus) references the wrong identifier (missing “a”).
At compile‑time this is an undefined symbol → build failure.

-@IsEnum(MinigraphStatus)
+@IsEnum(MinigraphStatus)

Likely an incorrect or invalid review comment.

Comment on lines +29 to +33
export const loadLegacyStore = () => {
return {
store: store.getState(),
};
};
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Legacy store loading is clear but consider serialization implications.

The function correctly provides the initial store state but doesn't handle synchronization (which appears to be managed by StoreSyncService per the summary). Consider adding validation to ensure the store state contains only serializable values.


🏁 Script executed:

#!/bin/bash
# Check if the store uses any middleware for serialization checks
rg "serializableCheck" --type ts

Length of output: 93


Enforce serialization checks when loading the legacy store

The application currently disables Redux’s serializable-value middleware (serializableCheck: false in api/src/store/index.ts), which means non‑serializable data could slip into your config loader. To guard against this, add a runtime validation step (or re‑enable the middleware) before returning the state:

• File: api/src/unraid-api/config/config.loader.ts

export const loadLegacyStore = () => {
  const state = store.getState();
  // runtime serialization guard
  try {
    JSON.stringify(state);
  } catch {
    throw new Error(
      'loadLegacyStore failed: legacy store contains non-serializable values'
    );
  }
  return { store: state };
};

• Alternatively, re-enable serializableCheck in api/src/store/index.ts or validate against a zod/schema before shipping the state.

Comment on lines 100 to 105
if (this.wsClient) {
this.clearClientEventHandlers();
this.wsClient.terminate();
await this.wsClient.dispose();
this.wsClient = null;
}
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

Client.terminate() is not part of graphql‑ws API

terminate() exists on the underlying low‑level WebSocket, not on the Client returned by createClient(). Calling it will raise TypeError: client.terminate is not a function.

-if (this.wsClient) {
-    this.clearClientEventHandlers();
-    this.wsClient.terminate();
-    await this.wsClient.dispose();
-    this.wsClient = null;
-}
+if (this.wsClient) {
+    this.clearClientEventHandlers();
+    await this.wsClient.dispose(); // disposes and closes the socket internally
+    this.wsClient = 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
if (this.wsClient) {
this.clearClientEventHandlers();
this.wsClient.terminate();
await this.wsClient.dispose();
this.wsClient = null;
}
if (this.wsClient) {
this.clearClientEventHandlers();
await this.wsClient.dispose(); // disposes and closes the socket internally
this.wsClient = null;
}

Comment on lines 32 to 48
async onModuleInit() {
this.logger.debug(`Config path: ${this.configPath}`);
await this.loadOrMigrateConfig();
// Persist changes to the config.
const HALF_SECOND = 500;
this.configService.changes$.pipe(debounceTime(HALF_SECOND)).subscribe({
next: async ({ newValue, oldValue, path }) => {
if (path.startsWith('connect.config')) {
this.logger.debug(`Config changed: ${path} from ${oldValue} to ${newValue}`);
await this.persist();
}
},
error: (err) => {
this.logger.error('Error receiving config changes:', err);
},
});
}
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

Potential subscription leak – keep a reference and dispose on module destroy

onModuleInit creates a subscription to this.configService.changes$ but the ConnectConfigPersister never unsubscribes from it.
If the provider is ever re‑started (hot‑reload, testing, etc.) the old subscription remains alive, producing duplicate work and memory leaks.

+private readonly subscriptions: Subscription[] = [];
 ...
- this.configService.changes$
+ const sub = this.configService.changes$
     .pipe(debounceTime(HALF_SECOND))
     .subscribe(/* … */);
+ this.subscriptions.push(sub);

and in onModuleDestroy:

+ for (const sub of this.subscriptions) sub.unsubscribe();

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

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: 6

♻️ Duplicate comments (2)
packages/unraid-api-plugin-connect/src/pubsub/consts.ts (1)

10-24: Enums still diverge from the API – verify before merge

Previous reviews flagged that API-side PUBSUB_CHANNEL includes more entries (e.g. DASHBOARD, NOTIFICATION). Syncing both sides avoids silent event loss.

If alignment is planned for a later commit, please confirm and add a TODO with a ticket reference.

packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

99-104: ⚠️ Potential issue

Client.terminate() is not part of graphql-ws API

Calling it throws TypeError: client.terminate is not a function. Rely solely on dispose() (which closes the underlying socket) or call dispose first and skip terminate():

-            this.clearClientEventHandlers();
-            this.wsClient.terminate();
-            await this.wsClient.dispose();
+            this.clearClientEventHandlers();
+            await this.wsClient.dispose(); // graceful shutdown
🧹 Nitpick comments (4)
packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)

60-65: Guard against nullish roots & improve type-safety

getNestedValue throws if obj is null | undefined. A quick guard keeps its “safe accessor” promise and avoids the as any escape-hatch:

 export function getNestedValue<TObj extends object, TPath extends string>(
-    obj: TObj,
+    obj: TObj | null | undefined,
     path: TPath
 ): Get<TObj, TPath> {
-    return path.split('.').reduce((acc, part) => acc?.[part], obj as any) as Get<TObj, TPath>;
+    if (obj == null) return undefined as Get<TObj, TPath>;
+    return path
+        .split('.')
+        .reduce((acc: any, part) => acc?.[part], obj) as Get<TObj, TPath>;
 }
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

291-298: Unused events parameter – dead code

clearClientEventHandlers ignores its events argument and instead drains disposalQueue. Drop the parameter to avoid confusion:

-private clearClientEventHandlers(
-    events: Array<'connected' | 'connecting' | 'error' | 'ping'> = [
-        'connected',
-        'connecting',
-        'error',
-        'ping',
-    ]
-): void {
+private clearClientEventHandlers(): void {
packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1)

30-38: Bubble-up errors from client initialisation

createClientInstance() may throw (e.g., unreachable server, malformed headers).
Catching and logging here prevents unhandled rejections and makes retries explicit:

-await this.clientService.createClientInstance();
+try {
+    await this.clientService.createClientInstance();
+} catch (err) {
+    this.logger.error('Failed to create GraphQL client: %o', err);
+    // leave status in ERROR_RETRYING so the retry logic can kick in
+    this.connectionService.setConnectionStatus({
+        status: MinigraphStatus.ERROR_RETRYING,
+        error: (err as Error).message,
+    });
+}
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (1)

165-171: Normalise header names to lowercase for Node.js

Node automatically lower-cases headers, but providing mixed-case keys can lead to duplicates
(User-Agent vs user-agent) if another layer sets them too.
Conventionally send them in lower-case:

-            'User-Agent': `unraid-api/${state.apiVersion}`,
+            'user-agent': `unraid-api/${state.apiVersion}`,
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 7a318e8 and b82edb1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • api/src/unraid-api/config/factory/api-state.model.ts (1 hunks)
  • api/src/unraid-api/config/factory/api-state.register.ts (1 hunks)
  • api/src/unraid-api/config/factory/api-state.service.ts (1 hunks)
  • api/src/unraid-api/config/factory/config.injection.ts (1 hunks)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/utils.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/index.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/pubsub/consts.ts (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • api/src/unraid-api/config/factory/config.injection.ts
  • api/src/unraid-api/config/factory/api-state.model.ts
  • api/src/unraid-api/config/factory/api-state.register.ts
  • api/src/unraid-api/config/factory/api-state.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/unraid-api-plugin-connect/src/index.ts
  • packages/unraid-api-plugin-connect/package.json
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (3)
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (1)
  • Injectable (50-210)
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)
  • Injectable (27-305)
packages/unraid-api-plugin-connect/src/pubsub/consts.ts (1)
  • GRAPHQL_PUB_SUB_TOKEN (17-17)
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (3)
packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1)
  • Injectable (11-76)
packages/unraid-api-plugin-connect/src/config.persistence.ts (1)
  • Injectable (17-172)
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)
  • Injectable (27-305)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

190-193: ⚠️ Potential issue

ConfigService has no set method

@nestjs/config exposes get / getOrThrow; mutating config at runtime is unsupported. Persist the timeout elsewhere (e.g. dedicated service or in-memory state).

⛔ Skipped due to learnings
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom ConfigService implementation that includes a `set()` method for runtime configuration mutation, unlike the standard @nestjs/config package which only provides getter methods.
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard @nestjs/config package which only provides getter methods.

Comment on lines 123 to 139
const apolloClient = new ApolloClient({
link: ApolloLink.from([appErrorLink, retryLink, errorLink, wsLink]),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
},
});

this.initEventHandlers();
return apolloClient;
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

Mirror assignment inside createGraphqlClient (defensive)

Even after fixing createClientInstance, assigning here eliminates future foot-guns if someone bypasses that helper:

-        const apolloClient = new ApolloClient({
+        this.apolloClient = new ApolloClient({-        });
-
-        this.initEventHandlers();
-        return apolloClient;
+        });
+
+        this.initEventHandlers();
+        return this.apolloClient;
📝 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
const apolloClient = new ApolloClient({
link: ApolloLink.from([appErrorLink, retryLink, errorLink, wsLink]),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
},
});
this.initEventHandlers();
return apolloClient;
this.apolloClient = new ApolloClient({
link: ApolloLink.from([appErrorLink, retryLink, errorLink, wsLink]),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
},
});
this.initEventHandlers();
return this.apolloClient;

Comment on lines 65 to 66
@OnEvent(EVENTS.LOGOUT, { async: true, prependListener: true })
async logout({ reason }: { reason?: string }) {
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

prependListener is not a recognised option for @OnEvent in NestJS

@OnEvent accepts options such as async, once, prepend, etc. – but not prependListener.
Using an unknown property is silently ignored at runtime and will trip the type-checker (unless noImplicitAny is off).

-@OnEvent(EVENTS.LOGOUT, { async: true, prependListener: true })
+@OnEvent(EVENTS.LOGOUT, { async: true, prepend: true })
📝 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
@OnEvent(EVENTS.LOGOUT, { async: true, prependListener: true })
async logout({ reason }: { reason?: string }) {
@OnEvent(EVENTS.LOGOUT, { async: true, prepend: true })
async logout({ reason }: { reason?: string }) {

Comment on lines 203 to 205
resetMetadata() {
this.setMetadata({ status: MinigraphStatus.PRE_INIT });
}
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

resetMetadata should also clear stale errors

Leaving the error field unchanged keeps the connection in an error state even
after a deliberate reset.

-    resetMetadata() {
-        this.setMetadata({ status: MinigraphStatus.PRE_INIT });
-    }
+    resetMetadata() {
+        this.setMetadata({ status: MinigraphStatus.PRE_INIT, error: 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
resetMetadata() {
this.setMetadata({ status: MinigraphStatus.PRE_INIT });
}
resetMetadata() {
this.setMetadata({ status: MinigraphStatus.PRE_INIT, error: null });
}

Comment on lines 68 to 72
private updateMetadata(data: Partial<ConnectionMetadata>) {
this.configService.set('connect.mothership', {
...this.configService.get<ConnectionMetadata>('connect.mothership'),
...data,
});
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

Crash-proof updateMetadata when the config key is unset

Spreading ...undefined throws TypeError: Cannot convert undefined or null to object
on the very first call (before resetMetadata() is executed).

-        this.configService.set('connect.mothership', {
-            ...this.configService.get<ConnectionMetadata>('connect.mothership'),
-            ...data,
-        });
+        const existing =
+            this.configService.get<ConnectionMetadata>('connect.mothership') ?? {};
+        this.configService.set('connect.mothership', {
+            ...existing,
+            ...data,
+        });
📝 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 updateMetadata(data: Partial<ConnectionMetadata>) {
this.configService.set('connect.mothership', {
...this.configService.get<ConnectionMetadata>('connect.mothership'),
...data,
});
private updateMetadata(data: Partial<ConnectionMetadata>) {
const existing =
this.configService.get<ConnectionMetadata>('connect.mothership') ?? {};
this.configService.set('connect.mothership', {
...existing,
...data,
});
}

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: 8

🔭 Outside diff range comments (3)
api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (3)

12-28: 🛠️ Refactor suggestion

Auto-creating a root session for localhost is risky

Granting root privileges solely on REMOTE_ADDR === 127.0.0.1 / ::1 ignores common reverse-proxy setups (::ffff:127.0.0.1, Docker networks, etc.) and may be exploitable if traffic is forwarded.

Consider:

  • Checking HTTP_X_FORWARDED_FOR / proxy headers or disabling the shortcut when a proxy is detected.
  • Allowing an opt-in admin setting rather than unconditional elevation.

61-65: ⚠️ Potential issue

Logo anchor removed but closing tag remains elsewhere

The <a> wrapper around the SVG was deleted but its closing tag is now missing, leaving malformed HTML. Either restore the anchor or remove the closing </a> (and adjust styles accordingly).


88-110: ⚠️ Potential issue

Bell-icon logic references removed elements

#bell class manipulation remains, but the element was removed in the header section, which will cause jQuery to operate on an empty set.

Either:

  • Re-introduce the bell element, or
  • Delete the JS that mutates it.

Misalignment will break alert-count visual feedback.

♻️ Duplicate comments (6)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1)

781-804: Same nchanPaused timing caveat as in the fixture file

The snapshot reproduces the timer-reload script verbatim, so apply the same guard or variable-hoisting fix here to keep the snapshot in sync.

packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (2)

124-129: ⚠️ Potential issue

terminate() is still not part of graphql-ws Client – will throw at runtime

This exact issue was raised previously and remains unresolved: the Client returned by createClient() does not expose a terminate() method. Calling it will crash with TypeError: client.terminate is not a function.

-            this.clearClientEventHandlers();
-            this.wsClient.terminate();
-            await this.wsClient.dispose();
+            this.clearClientEventHandlers();
+            await this.wsClient.dispose();   // closes the socket internally

110-113: ⚠️ Potential issue

apolloClient still never cached – singleton pattern broken

createClientInstance() returns a fresh client but never persists it; subsequent calls get null and rebuild a second WebSocket. The same feedback was given earlier and is still pending.

-    async createClientInstance(): Promise<ApolloClient<NormalizedCacheObject>> {
-        return this.getClient() ?? this.createGraphqlClient();
+    async createClientInstance(): Promise<ApolloClient<NormalizedCacheObject>> {
+        if (!this.apolloClient) {
+            this.apolloClient = this.createGraphqlClient();
+        }
+        return this.apolloClient;
     }
…
-        const apolloClient = new ApolloClient({
+        this.apolloClient = new ApolloClient({
             link: ApolloLink.from([appErrorLink, retryLink, errorLink, wsLink]),
             cache: new InMemoryCache(),
             …
         });
-        this.initEventHandlers();
-        return apolloClient;
+        this.initEventHandlers();
+        return this.apolloClient;

Also applies to: 148-165

packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1)

72-73: ⚠️ Potential issue

Invalid option prependListener – NestJS ignores it

@OnEvent supports prepend, not prependListener. Using an unknown property is silently ignored and defeats the ordering you intend. This was pointed out earlier but is still present.

-@OnEvent(EVENTS.LOGOUT, { async: true, prependListener: true })
+@OnEvent(EVENTS.LOGOUT, { async: true, prepend: true })
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (2)

68-72: ⚠️ Potential issue

Crash risk: spreading undefined when config key is unset

If connect.mothership is missing, this.configService.get() returns undefined; spreading it throws TypeError: Cannot convert undefined or null to object.

-        this.configService.set('connect.mothership', {
-            ...this.configService.get<ConnectionMetadata>('connect.mothership'),
-            ...data,
-        });
+        const existing =
+            this.configService.get<ConnectionMetadata>('connect.mothership') ?? {};
+        this.configService.set('connect.mothership', {
+            ...existing,
+            ...data,
+        });

207-209: 🛠️ Refactor suggestion

resetMetadata() leaves stale error message

Reset only touches the status; the previous error (and timestamps) survive the reset, so the UI still shows an error even after a manual clear.

-    resetMetadata() {
-        this.setMetadata({ status: MinigraphStatus.PRE_INIT });
+    resetMetadata() {
+        this.setMetadata({ status: MinigraphStatus.PRE_INIT, error: null, lastPing: null, selfDisconnectedSince: null });
     }
🧹 Nitpick comments (4)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php (1)

1299-1321: pageFocusFunction() may explode silently

Inside nchanFocusStart() the code calls an optional pageFocusFunction() inside a try … catch {} block whose catch is empty.
While this avoids a hard crash, it also hides real problems and makes debugging harder.

If the function is truly optional, test for its existence instead of swallowing all errors:

-    try {
-      pageFocusFunction();
-    } catch(error) {}
+    if (typeof pageFocusFunction === 'function') {
+      pageFocusFunction();
+    }

The check is cheaper and prevents unrelated runtime exceptions from being ignored.

packages/unraid-api-plugin-connect/src/mothership/mothership-graphql.handler.ts (1)

1-4: Remove unused imports to avoid dead-code noise

OnEvent and OnModuleDestroy are imported but not used. Until they are needed, drop them to keep the file lint-clean.

-import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
-import { OnEvent } from '@nestjs/event-emitter';
+import { Inject, Injectable, Logger } from '@nestjs/common';
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (2)

316-323: Unused parameter – dead code & misleading API

clearClientEventHandlers(events = …) accepts an events array but never uses it; the function simply drains this.disposalQueue. Either remove the parameter or honour it by filtering the disposables.


212-229: Unbounded retry may hammer the server

RetryLink sets max: Infinity; for network errors that are not invalid-API-key related the client will retry forever, even after hours of outage. Consider capping the total attempts or time-window to avoid flooding logs and wasting bandwidth.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between b82edb1 and 194cc51.

⛔ Files ignored due to path filters (5)
  • packages/unraid-api-plugin-connect/src/graphql/generated/client/fragment-masking.ts is excluded by !**/generated/**
  • packages/unraid-api-plugin-connect/src/graphql/generated/client/gql.ts is excluded by !**/generated/**
  • packages/unraid-api-plugin-connect/src/graphql/generated/client/graphql.ts is excluded by !**/generated/**
  • packages/unraid-api-plugin-connect/src/graphql/generated/client/index.ts is excluded by !**/generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • api/dev/Unraid.net/myservers.cfg (1 hunks)
  • api/package.json (1 hunks)
  • api/src/consts.ts (0 hunks)
  • api/src/environment.ts (1 hunks)
  • api/src/unraid-api/app/pubsub.module.ts (1 hunks)
  • api/src/unraid-api/cli/start.command.ts (1 hunks)
  • api/src/unraid-api/config/legacy-config.module.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php (3 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (3 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (2 hunks)
  • packages/unraid-api-plugin-connect/codegen.ts (1 hunks)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/graphql/event.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/graphql/remote-response.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/parse-graphql.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/index.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/internal.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership-graphql.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership-subscription.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • api/src/consts.ts
✅ Files skipped from review due to trivial changes (9)
  • api/src/unraid-api/unraid-file-modifier/modifications/test/fixtures/downloaded/.login.php.last-download-time
  • api/src/unraid-api/unraid-file-modifier/modifications/test/fixtures/downloaded/auth-request.php.last-download-time
  • api/src/unraid-api/unraid-file-modifier/modifications/test/fixtures/downloaded/Notifications.page.last-download-time
  • api/src/unraid-api/unraid-file-modifier/modifications/test/fixtures/downloaded/DefaultPageLayout.php.last-download-time
  • packages/unraid-api-plugin-connect/src/graphql/remote-response.ts
  • api/dev/Unraid.net/myservers.cfg
  • api/src/environment.ts
  • packages/unraid-api-plugin-connect/codegen.ts
  • packages/unraid-api-plugin-connect/src/graphql/event.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • api/package.json
  • api/src/unraid-api/cli/start.command.ts
  • api/src/unraid-api/config/legacy-config.module.ts
  • packages/unraid-api-plugin-connect/package.json
  • api/src/unraid-api/app/pubsub.module.ts
  • packages/unraid-api-plugin-connect/src/index.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts (3)
packages/unraid-api-plugin-connect/src/mothership/connection.service.ts (1)
  • Injectable (50-222)
packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1)
  • Injectable (12-83)
packages/unraid-api-plugin-connect/src/mothership/mothership-subscription.handler.ts (1)
  • Injectable (29-215)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (4)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php (1)

770-795:

❌ Incorrect review comment

Reload-timer helpers work, but guard against an undefined nchanPaused

setTimerReload() (and the click handler) access nchanPaused, yet that variable is declared later in a separate <script> block.
If the page author (or a future refactor) ever moves the var nchanPaused = false; block below the interval’s first tick, the callback will throw ReferenceError: nchanPaused is not defined.

A tiny defensive check eliminates that risk without altering behaviour:

-          if (nchanPaused === false && ! dialogOpen() ) {
+          if ((window.nchanPaused === false || typeof window.nchanPaused === 'undefined')
+              && !dialogOpen()) {
             location.reload();
           }

(or simply hoist var nchanPaused = false; above this <script>).

Please confirm that nchanPaused is guaranteed to be defined before the first interval fires (especially when Load is set to small values such as 1).


🏁 Script executed:

#!/bin/bash
# Locate all occurrences of nchanPaused to verify its declaration position
rg -n "nchanPaused"

Length of output: 2645


nchanPaused is defined before any timer callback—no change needed
The inline <script> for var nchanPaused = false; runs immediately after the reload-timer script, and even with a 1-minute interval the variable is guaranteed to exist before the first tick. This is a test fixture, not production code, so no defensive check or hoisting is required.

Likely an incorrect or invalid review comment.

packages/unraid-api-plugin-connect/src/mothership/mothership-graphql.handler.ts (1)

12-23: Class is empty – clarify intent or mark TODO

MothershipGraphqlHandler is registered as a provider but contains no hooks or behaviour yet.
If this is intentional (PR is WIP), add a short // TODO: comment so future readers know implementation is forthcoming; otherwise, flesh out the methods now to avoid an inert provider in production.

api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (1)

38-53: openNotifier still references jGrowl setup variables

The jGrowl call block was stripped, yet $.jGrowl assets may still be loaded, and other code later (bell badge updates) assumes those side-effects. Verify that:

  1. jquery.jgrowl.js & CSS are no longer loaded — otherwise unused bytes are shipped.
  2. All downstream code querying .jGrowl-notification is removed to avoid dead selectors.
packages/unraid-api-plugin-connect/src/mothership/internal.client.ts (1)

79-87: Polyfill fetch for Node < 18 or older deployment images.

HttpLink relies on a global fetch. In Node ≤ 16 this is absent, and many Docker base images are still on those versions.
Consider importing cross-fetch (or similar) and passing it explicitly to HttpLink to guarantee portability.

import fetch from 'cross-fetch';
// ...
const httpLink = new HttpLink({
    uri: httpUri,
    fetch,                // ✅ always defined
    headers: {
        /* … */
    },
});

Comment on lines 3 to 6
interface ParsedQuery {
query?: string;
variables?: Record<string, string>;
}
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

Broaden the variable type to support all valid GraphQL inputs

variables may legitimately contain numbers, booleans, nested objects, arrays, etc.
Using Record<string, string> rejects those cases at compile time.

-    variables?: Record<string, string>;
+    variables?: Record<string, unknown>;

This change keeps type–safety while permitting every value shape that GraphQL allows.

📝 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
interface ParsedQuery {
query?: string;
variables?: Record<string, string>;
}
interface ParsedQuery {
query?: string;
variables?: Record<string, unknown>;
}

Comment on lines 8 to 20
export const parseGraphQLQuery = (body: string): QueryOptions => {
try {
const parsedBody: ParsedQuery = JSON.parse(body);
if (parsedBody.query && parsedBody.variables && typeof parsedBody.variables === 'object') {
return {
query: gql(parsedBody.query),
variables: parsedBody.variables,
};
}
throw new Error('Invalid Body');
} catch (error) {
throw new Error('Invalid Body Provided');
}
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

Relax validation and surface parsing errors

  1. A query without variables ({ query: "..."} ) is perfectly legal, but the current guard fails it.
  2. Swallowing the original JSON.parse error makes troubleshooting harder.
-        if (parsedBody.query && parsedBody.variables && typeof parsedBody.variables === 'object') {
+        if (parsedBody.query && (!parsedBody.variables || typeof parsedBody.variables === 'object')) {
             return {
                 query: gql(parsedBody.query),
-                variables: parsedBody.variables,
+                // variables is optional in Apollo `QueryOptions`
+                ...(parsedBody.variables ? { variables: parsedBody.variables } : {}),
             };
         }
-        throw new Error('Invalid Body');
+        throw new Error('Invalid body – missing "query" string');
     } catch (error) {
-        throw new Error('Invalid Body Provided');
+        /* re-throw with context to aid debugging */
+        const reason = error instanceof Error ? error.message : String(error);
+        throw new Error(`Body could not be parsed: ${reason}`);
     }

This keeps the contract intact while being more permissive and developer-friendly.

📝 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
export const parseGraphQLQuery = (body: string): QueryOptions => {
try {
const parsedBody: ParsedQuery = JSON.parse(body);
if (parsedBody.query && parsedBody.variables && typeof parsedBody.variables === 'object') {
return {
query: gql(parsedBody.query),
variables: parsedBody.variables,
};
}
throw new Error('Invalid Body');
} catch (error) {
throw new Error('Invalid Body Provided');
}
export const parseGraphQLQuery = (body: string): QueryOptions => {
try {
const parsedBody: ParsedQuery = JSON.parse(body);
if (parsedBody.query && (!parsedBody.variables || typeof parsedBody.variables === 'object')) {
return {
query: gql(parsedBody.query),
// variables is optional in Apollo `QueryOptions`
...(parsedBody.variables ? { variables: parsedBody.variables } : {}),
};
}
throw new Error('Invalid body – missing "query" string');
} catch (error) {
/* re-throw with context to aid debugging */
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`Body could not be parsed: ${reason}`);
}
};

Comment on lines 139 to 142
public clearClient() {
this.client?.stop();
this.client = null;
}
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

ApolloClient.stop() may be undefined – confirm version compatibility.

stop() exists in Apollo Client v2 but was removed in v3+.
If the project has already upgraded to v3 (many other imports suggest this), this call will throw TypeError: client.stop is not a function.

-        this.client?.stop();
+        // v3 does not expose `stop`; fall back to terminating WebSocket connections if needed
+        (this.client as any)?.stop?.(); // optional chaining keeps this safe

Alternatively, keep a reference to the WsLink client returned from createClient(...) and dispose it directly via dispose().

Comment on lines 35 to 41
private getNonSocketPortOverride() {
const port = this.configService.get<string | number | undefined>('PORT');
if (!port || port.toString().includes('.sock')) {
return undefined;
}
return Number(port);
}
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

Guard against non-numeric PORT values to avoid NaN.

Number(port) will silently yield NaN for unexpected input (e.g. 'abc'). Down-stream callers treat the result as a valid number, which could generate malformed URLs (http://127.0.0.1:NaN/graphql) and connection failures.

-        const port = this.configService.get<string | number | undefined>('PORT');
-        if (!port || port.toString().includes('.sock')) {
+        const portValue = this.configService.get<string | number | undefined>('PORT');
+        if (!portValue || portValue.toString().includes('.sock')) {
             return undefined;
         }
-        return Number(port);
+        const numericPort = Number(portValue);
+        return Number.isFinite(numericPort) ? numericPort : undefined;
📝 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 getNonSocketPortOverride() {
const port = this.configService.get<string | number | undefined>('PORT');
if (!port || port.toString().includes('.sock')) {
return undefined;
}
return Number(port);
}
private getNonSocketPortOverride() {
- const port = this.configService.get<string | number | undefined>('PORT');
- if (!port || port.toString().includes('.sock')) {
- return undefined;
- }
- return Number(port);
+ const portValue = this.configService.get<string | number | undefined>('PORT');
+ if (!portValue || portValue.toString().includes('.sock')) {
+ return undefined;
+ }
+ const numericPort = Number(portValue);
+ return Number.isFinite(numericPort) ? numericPort : undefined;
}

Comment on lines 174 to 184
async subscribeToMothershipEvents(client = this.mothershipClient.getClient()) {
if (!client) {
this.logger.error('Mothership client unavailable. State might not be loaded.');
return;
}
const subscription = client.subscribe({
query: EVENTS_SUBSCRIPTION,
fetchPolicy: 'no-cache',
});
this.mothershipSubscription = subscription.subscribe({
next: (event) => {
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

Missing error / complete handlers for mothership event stream.

If the upstream subscription errors out the local handler silently dies, leaving the system in an inconsistent state.
Consider supplying error and complete callbacks to attempt automatic resubscription or update connection status.

Comment on lines 58 to 74
clearStaleSubscriptions({ maxAgeMs }: { maxAgeMs: number }) {
const totalSubscriptions = this.subscriptions.size;
let numOfStaleSubscriptions = 0;
const now = Date.now();
this.subscriptions
.entries()
.filter(([, { lastPing }]) => {
return now - lastPing > maxAgeMs;
})
.forEach(([sha256]) => {
this.removeSubscription(sha256);
numOfStaleSubscriptions++;
});
this.logger.debug(
`Cleared ${numOfStaleSubscriptions}/${totalSubscriptions} subscriptions (older than ${maxAgeMs}ms)`
);
}
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

Map.entries() returns an iterator – current chain will crash.

Iterators don’t expose filter/forEach directly. The current implementation will throw TypeError: iterator.filter is not a function.

-        this.subscriptions
-            .entries()
-            .filter(([, { lastPing }]) => {
-                return now - lastPing > maxAgeMs;
-            })
-            .forEach(([sha256]) => {
+        Array.from(this.subscriptions.entries())
+            .filter(([, { lastPing }]) => now - lastPing > maxAgeMs)
+            .forEach(([sha256]) => {
                 this.removeSubscription(sha256);
                 numOfStaleSubscriptions++;
             });
📝 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
clearStaleSubscriptions({ maxAgeMs }: { maxAgeMs: number }) {
const totalSubscriptions = this.subscriptions.size;
let numOfStaleSubscriptions = 0;
const now = Date.now();
this.subscriptions
.entries()
.filter(([, { lastPing }]) => {
return now - lastPing > maxAgeMs;
})
.forEach(([sha256]) => {
this.removeSubscription(sha256);
numOfStaleSubscriptions++;
});
this.logger.debug(
`Cleared ${numOfStaleSubscriptions}/${totalSubscriptions} subscriptions (older than ${maxAgeMs}ms)`
);
}
clearStaleSubscriptions({ maxAgeMs }: { maxAgeMs: number }) {
const totalSubscriptions = this.subscriptions.size;
let numOfStaleSubscriptions = 0;
const now = Date.now();
Array.from(this.subscriptions.entries())
.filter(([, { lastPing }]) => now - lastPing > maxAgeMs)
.forEach(([sha256]) => {
this.removeSubscription(sha256);
numOfStaleSubscriptions++;
});
this.logger.debug(
`Cleared ${numOfStaleSubscriptions}/${totalSubscriptions} subscriptions (older than ${maxAgeMs}ms)`
);
}

Comment on lines 28 to 34
async clear() {
this.timeoutCheckerJob.stop();
this.subscriptionHandler.stopMothershipSubscription();
await this.clientService.clearInstance();
this.connectionService.resetMetadata();
this.subscriptionHandler.clearAllSubscriptions();
}
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

Await the subscription & timeout teardown to avoid race conditions

stopMothershipSubscription() returns a Promise (per previous file), but it’s called without await. The handler could proceed to reset metadata while the subscription is still active, causing unexpected events.

-        this.subscriptionHandler.stopMothershipSubscription();
+        await this.subscriptionHandler.stopMothershipSubscription();
📝 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
async clear() {
this.timeoutCheckerJob.stop();
this.subscriptionHandler.stopMothershipSubscription();
await this.clientService.clearInstance();
this.connectionService.resetMetadata();
this.subscriptionHandler.clearAllSubscriptions();
}
async clear() {
this.timeoutCheckerJob.stop();
await this.subscriptionHandler.stopMothershipSubscription();
await this.clientService.clearInstance();
this.connectionService.resetMetadata();
this.subscriptionHandler.clearAllSubscriptions();
}

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: 2

♻️ Duplicate comments (2)
packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts (1)

59-65: stop() throws if the interval was never registered.

SchedulerRegistry.getInterval() throws a NotFoundException when the key is missing.
Because start() calls stop() pre-emptively, the very first start() will crash the service.

-        const interval = this.schedulerRegistry.getInterval(this.jobName);
-        if (isDefined(interval)) {
-            clearInterval(interval);
-            this.schedulerRegistry.deleteInterval(this.jobName);
-        }
+        try {
+            const interval = this.schedulerRegistry.getInterval(this.jobName);
+            clearInterval(interval);
+            this.schedulerRegistry.deleteInterval(this.jobName);
+        } catch {
+            // Interval was not registered – nothing to stop
+            this.logger.debug(`No active interval found for ${this.jobName}`);
+        }
packages/unraid-api-plugin-connect/src/config.entity.ts (1)

204-210: ⚠️ Potential issue

Generic type mismatch – ConnectConfig does not declare mothership or dynamicRemoteAccess

registerAs<ConnectConfig>('connect', …) returns an object with mothership and dynamicRemoteAccess properties, yet ConnectConfig (from config.demo.ts) only has demo.
This violates the generic contract and will surface as a TypeScript error.

Options:

  1. Extend ConnectConfig to include the new properties, or
  2. Replace the generic with an updated interface that matches the returned shape.
-export const configFeature = registerAs<ConnectConfig>('connect', () => ({
+export const configFeature = registerAs('connect', () => ({

(or update the ConnectConfig interface to include the new properties).

🧹 Nitpick comments (4)
packages/unraid-api-plugin-connect/src/remote-access/dynamic-remote-access-state.service.ts (1)

13-15: Consider handling missing configuration more gracefully

The getOrThrow method will throw an exception if the configuration key doesn't exist. Consider whether a more graceful fallback might be appropriate here, such as using get with a default value.

- return this.configService.getOrThrow<DynamicRemoteAccessState>('connect.dynamicRemoteAccess');
+ return this.configService.get<DynamicRemoteAccessState>(
+   'connect.dynamicRemoteAccess', 
+   makeDisabledDynamicRemoteAccessState()
+ );
packages/unraid-api-plugin-connect/src/config.entity.ts (3)

42-42: Fix typo in comment

-    // Remote Access Configurationx
+    // Remote Access Configuration

117-148: Ensure ConnectionMetadata has proper initialization defaults

The ConnectionMetadata class has several nullable fields, but only status is initialized in configFeature. Consider adding default values for all fields or creating a factory function similar to makeDisabledDynamicRemoteAccessState().


173-194: Consider adding URL validation for IP addresses

For the ipv4 and ipv6 fields, consider adding regex pattern validation to ensure they match valid IP address formats.

@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
+@Matches(/^(\d{1,3}\.){3}\d{1,3}$/, { message: 'Must be a valid IPv4 address' })
ipv4!: string | null | undefined;

@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
+@Matches(/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/, { message: 'Must be a valid IPv6 address' })
ipv6!: string | null | undefined;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 194cc51 and c876997.

📒 Files selected for processing (4)
  • packages/unraid-api-plugin-connect/src/config.entity.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/consts.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/remote-access/dynamic-remote-access-state.service.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/unraid-api-plugin-connect/src/helpers/consts.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
packages/unraid-api-plugin-connect/src/remote-access/dynamic-remote-access-state.service.ts (3)
packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts (1)
  • Injectable (12-71)
packages/unraid-api-plugin-connect/src/helpers/consts.ts (1)
  • ONE_MINUTE_MS (1-1)
packages/unraid-api-plugin-connect/src/config.entity.ts (1)
  • makeDisabledDynamicRemoteAccessState (196-202)
packages/unraid-api-plugin-connect/src/config.entity.ts (3)
packages/unraid-api-plugin-connect/src/helpers/my-servers-config.ts (1)
  • MyServersConfig (29-53)
packages/unraid-api-plugin-generator/src/templates/config.entity.ts (1)
  • configFeature (16-20)
packages/unraid-api-plugin-connect/src/config.demo.ts (1)
  • ConnectConfig (3-6)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/unraid-api-plugin-connect/src/remote-access/dynamic-remote-access-state.service.ts (1)

17-26: LGTM! Timeout checking logic works as expected.

The implementation correctly checks if the last ping is older than one minute and disables dynamic remote access if needed.

packages/unraid-api-plugin-connect/src/config.entity.ts (1)

150-171: LGTM! Well-structured state entity with validation

The DynamicRemoteAccessState class is well-structured with appropriate validation decorators and field definitions.

Comment on lines 46 to 50
async checkForTimeouts() {
this.subscriptionHandler.clearStaleSubscriptions({ maxAgeMs: THREE_MINUTES_MS });
this.checkMothershipClientTimeout();
this.dynamicRemoteAccess.checkForTimeout();
}
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

Consider handling potential errors in checkForTimeouts

Since this method is called in a periodic interval, any unhandled errors could cause the interval to stop working. Consider adding try/catch blocks around each method call.

async checkForTimeouts() {
+   try {
        this.subscriptionHandler.clearStaleSubscriptions({ maxAgeMs: THREE_MINUTES_MS });
        this.checkMothershipClientTimeout();
        this.dynamicRemoteAccess.checkForTimeout();
+   } catch (error) {
+       this.logger.error('Error checking for timeouts', error);
+   }
}

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: 5

♻️ Duplicate comments (1)
packages/unraid-api-plugin-connect/src/config.persistence.ts (1)

37-48: Subscription never unsubscribed (repeat feedback)
Previous review already highlighted the potential leak; the code still doesn’t keep the Subscription reference or clean it up in onModuleDestroy().

🧹 Nitpick comments (11)
packages/unraid-api-plugin-connect/src/system/nginx.service.ts (1)

8-16: Minor inconsistency in error message

The error message states "Failed to restart Nginx" but the command is actually running reload.

-            this.logger.warn('Failed to restart Nginx with error: ', err);
+            this.logger.warn('Failed to reload Nginx with error: ', err);
packages/unraid-api-plugin-connect/src/system/dns.service.ts (1)

9-17: Consider adding a comment explaining what the UpdateDNS.php script does

The service calls an external PHP script, but without any context about its purpose or expected behavior.

    async update() {
+       // Updates DNS settings by running the Dynamix plugin's PHP script
        try {
            await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
            return true;
        } catch (err: unknown) {
            this.logger.warn('Failed to call Update DNS with error: ', err);
            return false;
        }
    }
packages/unraid-api-plugin-connect/src/system/network.service.ts (1)

13-16: Consider handling the return values

Both nginxService.reload() and dnsService.update() return booleans indicating success/failure, but this method doesn't use those values.

    async reloadNetworkStack() {
-        await this.nginxService.reload();
-        await this.dnsService.update();
+        const nginxResult = await this.nginxService.reload();
+        const dnsResult = await this.dnsService.update();
+        return nginxResult && dnsResult;
    }
packages/unraid-api-plugin-connect/package.json (3)

2-18: Update package description

The description still says "Example Health plugin for Unraid API" but this package is for the Connect plugin.

    "license": "GPL-2.0-or-later",
-    "description": "Example Health plugin for Unraid API",
+    "description": "Connect plugin for Unraid API",

13-13: Hardcoded staging URL in codegen script

The codegen script has a hardcoded URL to the staging environment. Consider making this configurable.

-        "codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.ts"
+        "codegen": "graphql-codegen --config codegen.ts"

Then set the URL in the codegen.ts file or use an .env file for environment-specific configuration.


19-77: Lots of duplicated dependencies

Many packages appear in both devDependencies and peerDependencies with identical version constraints. Consider reviewing if all of these need to be in both sections.

Typically, peerDependencies are for packages that your library expects to be provided by the consuming application, while devDependencies are only needed during development.

packages/unraid-api-plugin-connect/src/remote-access/static-remote-access.service.ts (1)

15-18: getRemoteAccessUrl is a stub that can break callers
beginRemoteAccess() returns the result of this method, yet the current implementation always yields null. Any consumer expecting a usable URL will mis-behave.
Consider at least returning a sensible placeholder or throwing NotImplementedException until the full logic is in place.

packages/unraid-api-plugin-connect/src/system/upnp.service.ts (1)

143-148: Nit: prefer early return to avoid nested blocks
A quick readability win—exit early when no WAN port can be determined.

-const wanPortToUse = await this.getWanPortToUse(args);
-if (!wanPortToUse) {
-  await this.disableUpnp();
-  ...
-}
+const wanPortToUse = await this.getWanPortToUse(args);
+if (!wanPortToUse) {
+  await this.disableUpnp();
+  this.logger.error('No WAN port found. Disabled UPNP.');
+  throw new Error('No WAN port found. Disabled UPNP.');
+}
packages/unraid-api-plugin-connect/src/config.entity.ts (3)

42-42: Fix typo in comment

There's a typo in the comment: "Remote Access Configurationx" should be "Remote Access Configuration".

-    // Remote Access Configurationx
+    // Remote Access Configuration

107-109: Commented code should be removed

These commented lines appear to be no longer needed since the connection status tracking has been moved to the dedicated ConnectionMetadata class.

-    // @Field(() => MinigraphStatus)
-    // @IsEnum(MinigraphStatus)
-    // minigraph!: MinigraphStatus;

210-213: Consider using more specific types instead of any

The store: any reduces type safety. Consider using a more specific type if possible.

-    store: any;
+    store: Record<string, unknown>;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between c876997 and 80d7152.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/config.demo.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/config.entity.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/config.persistence.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/consts.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/dependency-injection.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/remote-access/static-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/dns.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/network.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/nginx.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/upnp.service.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/unraid-api-plugin-connect/src/config.demo.ts
  • packages/unraid-api-plugin-connect/src/helpers/dependency-injection.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/unraid-api-plugin-connect/src/mothership/timeout-checker.job.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
packages/unraid-api-plugin-connect/src/system/nginx.service.ts (3)
packages/unraid-api-plugin-connect/src/remote-access/static-remote-access.service.ts (1)
  • Injectable (6-36)
packages/unraid-api-plugin-connect/src/system/dns.service.ts (1)
  • Injectable (5-18)
packages/unraid-api-plugin-connect/src/system/network.service.ts (1)
  • Injectable (6-17)
packages/unraid-api-plugin-connect/src/system/dns.service.ts (3)
packages/unraid-api-plugin-connect/src/remote-access/static-remote-access.service.ts (1)
  • Injectable (6-36)
packages/unraid-api-plugin-connect/src/system/nginx.service.ts (1)
  • Injectable (4-17)
packages/unraid-api-plugin-connect/src/system/network.service.ts (1)
  • Injectable (6-17)
packages/unraid-api-plugin-connect/src/config.entity.ts (3)
packages/unraid-api-plugin-connect/src/helpers/my-servers-config.ts (1)
  • MyServersConfig (29-53)
packages/unraid-api-plugin-connect/src/config.demo.ts (1)
  • ConnectDemoConfig (3-6)
packages/unraid-api-plugin-generator/src/templates/config.entity.ts (1)
  • configFeature (16-20)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test API
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (7)
packages/unraid-api-plugin-connect/src/helpers/consts.ts (1)

1-3: Nice implementation of time constants!

The constants are well-structured, using calculation rather than hardcoded values and featuring clear naming conventions that indicate the units (MS for milliseconds, SECS for seconds). Using ONE_MINUTE_MS to calculate THREE_MINUTES_MS is especially good practice.

Consider adding JSDoc comments to these constants to provide context for their intended usage across the Connect Plugin.

packages/unraid-api-plugin-connect/src/system/nginx.service.ts (2)

1-2: Good use of imports

Imports are clean and appropriately specified.


4-7: Good implementation of logger

The service correctly uses NestJS injection and implements a private logger with the class name.

packages/unraid-api-plugin-connect/src/system/dns.service.ts (1)

1-7: Good class structure

The service is properly decorated as injectable and implements a class-specific logger.

packages/unraid-api-plugin-connect/src/system/network.service.ts (1)

1-11: Good dependency injection

The service correctly injects its dependencies through the constructor.

packages/unraid-api-plugin-connect/src/config.entity.ts (2)

204-208: Type definition resolves previous issue

This ConnectConfig type correctly includes the mothership property, which resolves the generic type mismatch issue mentioned in the previous review.


215-237: Verify all config properties are initialized

The config feature properly initializes the new structure with default values. Ensure that all properties in the various classes have proper defaults to prevent runtime errors.

Comment on lines 25 to 34
async beginRemoteAccess() {
const { dynamicRemoteAccessType } = this.configService.getOrThrow<MyServersConfig>('connect.config');
if (dynamicRemoteAccessType !== DynamicRemoteAccessType.STATIC) {
this.logger.error('Invalid Dynamic Remote Access Type: %s', dynamicRemoteAccessType);
return null;
}
this.logger.log('Enabling Static Remote Access');
this.configService.set('connect.config.wanaccess', true);
await this.networkService.reloadNetworkStack();
return this.getRemoteAccessUrl();
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

Error handling is one-sided
reloadNetworkStack() can throw but the promise chain isn’t wrapped in try/catch, so the service may bubble an unhandled rejection and leave the config in an inconsistent state (wanaccess=true while the network stack never reloaded).
Wrap the mutation & reload in a guarded block and roll back on failure.

try {
   this.configService.set('connect.config.wanaccess', true);
   await this.networkService.reloadNetworkStack();
   return this.getRemoteAccessUrl();
-} 
+} catch (err) {
+   this.logger.error('Static RA enable failed', err);
+   this.configService.set('connect.config.wanaccess', false);
+   throw 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
async beginRemoteAccess() {
const { dynamicRemoteAccessType } = this.configService.getOrThrow<MyServersConfig>('connect.config');
if (dynamicRemoteAccessType !== DynamicRemoteAccessType.STATIC) {
this.logger.error('Invalid Dynamic Remote Access Type: %s', dynamicRemoteAccessType);
return null;
}
this.logger.log('Enabling Static Remote Access');
this.configService.set('connect.config.wanaccess', true);
await this.networkService.reloadNetworkStack();
return this.getRemoteAccessUrl();
async beginRemoteAccess() {
const { dynamicRemoteAccessType } = this.configService.getOrThrow<MyServersConfig>('connect.config');
if (dynamicRemoteAccessType !== DynamicRemoteAccessType.STATIC) {
this.logger.error('Invalid Dynamic Remote Access Type: %s', dynamicRemoteAccessType);
return null;
}
this.logger.log('Enabling Static Remote Access');
try {
this.configService.set('connect.config.wanaccess', true);
await this.networkService.reloadNetworkStack();
return this.getRemoteAccessUrl();
} catch (err) {
this.logger.error('Static RA enable failed', err);
this.configService.set('connect.config.wanaccess', false);
throw err;
}
}

Comment on lines 137 to 161
async enableUpnp(args?: { sslPort?: number; wanPort?: number }) {
const { sslPort, wanPort } = args ?? {};
if (wanPort !== this.#wanPort || this.#localPort !== sslPort) {
await this.removeUpnpLease();
}

// todo: start the renewal job

const wanPortToUse = await this.getWanPortToUse(args);
this.#wanPort = wanPortToUse;
const localPortToUse = sslPort ?? this.#localPort;
if (wanPortToUse && localPortToUse) {
await this.renewUpnpLease({
publicPort: wanPortToUse,
privatePort: localPortToUse,
});
} else {
await this.disableUpnp();
this.logger.error('No WAN port found %o. Disabled UPNP.', {
wanPort: wanPortToUse,
localPort: localPortToUse,
});
throw new Error('No WAN port found. Disabled UPNP.');
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Race condition & missing renewal job
enableUpnp() mutates internal state before renewUpnpLease() resolves. If the latter fails, the class remains in a half-enabled state. Also, the TODO about starting a renewal task is crucial—without it the lease expires after an hour.
Wrap the call in try/catch and revert the fields on error; schedule the renewal before returning.


Implement atomic enableUpnp with rollback and schedule renewal

The current implementation in
packages/unraid-api-plugin-connect/src/system/upnp.service.ts (lines 137–161)
mutates this.#wanPort (and by extension this.#localPort) before renewUpnpLease() succeeds, leaving the service in a half-enabled state if renewal fails. Additionally, the // todo: start the renewal job comment indicates that leases aren’t being refreshed, so they’ll expire after their TTL.

Please:

  • Wrap the call to renewUpnpLease({ publicPort, privatePort }) in a try/catch.
    • On error, revert this.#wanPort and this.#localPort to their previous values and rethrow or handle the failure gracefully.
  • Replace the // todo: start the renewal job with an actual scheduled renewal (e.g., a setInterval or background timer) so the lease is refreshed before expiration.

This ensures that:

  1. State changes only persist on success.
  2. Leases are automatically renewed.

Comment on lines 117 to 123
const excludedPorts = new Set(mappings?.map((val) => val.public.port));
// Generate a random port between minPort and maxPort up to maxAttempts times
for (let i = 0; i < maxAttempts; i++) {
const port = Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort;
if (!excludedPorts.has(port)) {
return port;
}
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

new Set(undefined) will crash – guard against missing mappings
If getMappings() returns undefined, the spread into new Set() throws TypeError.

-const excludedPorts = new Set(mappings?.map((val) => val.public.port));
+const excludedPorts = new Set(
+  (mappings ?? []).map((val) => val.public.port),
+);
📝 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
const excludedPorts = new Set(mappings?.map((val) => val.public.port));
// Generate a random port between minPort and maxPort up to maxAttempts times
for (let i = 0; i < maxAttempts; i++) {
const port = Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort;
if (!excludedPorts.has(port)) {
return port;
}
const excludedPorts = new Set(
(mappings ?? []).map((val) => val.public.port),
);
// Generate a random port between minPort and maxPort up to maxAttempts times
for (let i = 0; i < maxAttempts; i++) {
const port = Math.floor(Math.random() * (maxPort - minPort + 1)) + minPort;
if (!excludedPorts.has(port)) {
return port;

Comment on lines 57 to 66
async persist(config = this.configService.get<MyServersConfig>('connect.config')) {
try {
if (isEqual(config, await this.loadConfig())) {
this.logger.verbose(`Config is unchanged, skipping persistence`);
return false;
}
} catch (error) {
this.logger.error(`Error loading config (will overwrite file):`, error);
}
const data = JSON.stringify(config, null, 2);
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

persist() writes "undefined" when config is missing
If connect.config hasn’t been set, config is undefined; JSON.stringify(undefined) serialises to the literal string "undefined" which isn’t valid JSON for later reads.
Guard against empty configs.

async persist(config = this.configService.get<MyServersConfig>('connect.config')) {
+  if (config == null) {
+    this.logger.warn('No config in memory – skipping persistence');
+    return false;
+  }
   try {
📝 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
async persist(config = this.configService.get<MyServersConfig>('connect.config')) {
try {
if (isEqual(config, await this.loadConfig())) {
this.logger.verbose(`Config is unchanged, skipping persistence`);
return false;
}
} catch (error) {
this.logger.error(`Error loading config (will overwrite file):`, error);
}
const data = JSON.stringify(config, null, 2);
async persist(config = this.configService.get<MyServersConfig>('connect.config')) {
if (config == null) {
this.logger.warn('No config in memory – skipping persistence');
return false;
}
try {
if (isEqual(config, await this.loadConfig())) {
this.logger.verbose(`Config is unchanged, skipping persistence`);
return false;
}
} catch (error) {
this.logger.error(`Error loading config (will overwrite file):`, error);
}
const data = JSON.stringify(config, null, 2);

// Remote Access Configurationx
@Field(() => Boolean)
@IsBoolean()
wanaccess!: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify type change compatibility

The wanaccess field changed from string to boolean. This is a more appropriate type but verify this change won't break existing code that expects a string.


🏁 Script executed:

#!/bin/bash
# Find all usages of wanaccess in the codebase
rg "wanaccess" --type ts

Length of output: 2689


Type change for wanaccess requires updates across the codebase

We switched wanaccess from a string to a boolean in packages/unraid-api-plugin-connect/src/config.entity.ts. However, many modules still treat it as a string ('yes'/'no'/''), which will break at runtime. Please update the following locations to use a boolean instead of string:

  • packages/unraid-api-plugin-connect/src/helpers/my-servers-config.ts: wanaccess: string
  • api/src/types/my-servers-config.ts: Zod schema (wanaccess: z.string())
  • Store modules & actions:
    • api/src/store/modules/config.ts: initial state (wanaccess: '') and assignments ('yes'/'no')
    • api/src/store/actions/setup-remote-access.ts: setting 'yes'/'no'
  • Store listeners:
    • api/src/store/listeners/upnp-listener.ts: wanaccess === 'yes'
    • api/src/store/listeners/wan-access-change-listener.ts: comparisons against ''
  • GraphQL resolver:
    • api/src/unraid-api/graph/resolvers/connect/connect-settings.service.ts: getters.config().remote.wanaccess === 'yes'
  • Core types & utils:
    • api/src/core/types/ini.ts: wanaccess: string
    • Tests:
      • api/src/test/store/modules/config.test.ts (expect.any(String), toBe('yes'))
      • api/src/test/core/utils/files/config-file-normalizer.test.ts (empty/string values)

Ensure schemas, comparisons, defaults, and test assertions all use boolean (or map string values at the boundary) to align with this change.

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: 2

♻️ Duplicate comments (1)
packages/unraid-api-plugin-connect/src/system/upnp.service.ts (1)

125-126: new Set(undefined) will still explode – previous feedback not applied
Passing undefined to the Set constructor raises TypeError.
Use an empty array fallback as suggested earlier.

-const excludedPorts = new Set(mappings?.map((val) => val.public.port));
+const excludedPorts = new Set((mappings ?? []).map((val) => val.public.port));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between 80d7152 and df3be4c.

📒 Files selected for processing (2)
  • packages/unraid-api-plugin-connect/src/helpers/dependency-injection.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/upnp.service.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/unraid-api-plugin-connect/src/helpers/dependency-injection.ts (1)

2-3: Looks good – thanks for centralising the tokens
Clear constant names, all-caps convention is consistent with the rest of the helpers.

packages/unraid-api-plugin-connect/src/system/upnp.service.ts (1)

175-183: Cron frequency vs TTL – intentional?
The job renews every 30 min while the TTL is 60 min. That’s fine, but if you ever shorten the TTL remember to adjust the cron to trigger sooner than half the TTL to avoid race conditions.

Comment on lines 37 to 39
get renewalJob(): ReturnType<typeof this.scheduleRegistry.getCronJob> {
return this.scheduleRegistry.getCronJob(UPNP_RENEWAL_JOB_TOKEN);
}
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

Getter may throw when the job hasn’t been registered yet
SchedulerRegistry#getCronJob throws if the job name is unknown.
Accessing renewalJob before the first @Cron registration (e.g. during application bootstrap or in tests) will blow up the service.

Consider:

-getCronJob(UPNP_RENEWAL_JOB_TOKEN);
+this.scheduleRegistry.doesExist('cron', UPNP_RENEWAL_JOB_TOKEN)
+  ? this.scheduleRegistry.getCronJob(UPNP_RENEWAL_JOB_TOKEN)
+  : undefined;

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

Comment on lines 150 to 152
const wanPortToUse = await this.getWanPortToUse(args);
this.#wanPort = wanPortToUse;
const localPortToUse = sslPort ?? this.#localPort;
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

State is mutated before the mapping is confirmed – possible half-enabled state
this.#wanPort is set before createUpnpMapping() succeeds.
If createUpnpMapping throws, the service ends up disabled (#enabled is still false) but with a non-undefined #wanPort, making later logic confusing.

Suggestion:

-const wanPortToUse = await this.getWanPortToUse(args);
-this.#wanPort = wanPortToUse;
+const wanPortToUse = await this.getWanPortToUse(args);

and only assign inside createUpnpMapping on success (it already does so).

Optionally wrap the call in try/catch and roll back #localPort as well.

📝 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
const wanPortToUse = await this.getWanPortToUse(args);
this.#wanPort = wanPortToUse;
const localPortToUse = sslPort ?? this.#localPort;
const wanPortToUse = await this.getWanPortToUse(args);
const localPortToUse = sslPort ?? this.#localPort;

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: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro (Legacy)

📥 Commits

Reviewing files that changed from the base of the PR and between df3be4c and 3c510a9.

📒 Files selected for processing (1)
  • packages/unraid-api-plugin-connect/src/remote-access/upnp-remote-access.service.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test API
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/unraid-api-plugin-connect/src/remote-access/upnp-remote-access.service.ts (2)

1-6: Imports look good

The imports are clear and appropriate for the service's functionality.


8-17: Service declaration and logger setup are well-structured

The class correctly uses NestJS dependency injection and establishes a properly scoped logger instance.

Comment on lines 18 to 21
async stop() {
await this.upnpService.disableUpnp();
await this.networkService.reloadNetworkStack();
}
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

Add error handling to the stop() method

The method calls two async operations but doesn't handle potential errors.

async stop() {
+   try {
        await this.upnpService.disableUpnp();
        await this.networkService.reloadNetworkStack();
+       return true;
+   } catch (error) {
+       this.logger.error('Failed to stop UPnP remote access: %O', error);
+       return false;
+   }
}

Comment on lines 23 to 38
async begin() {
const sslPort = this.configService.get<string | undefined>('store.emhttp.var.portssl');
try {
await this.upnpService.createOrRenewUpnpLease({
sslPort: Number(sslPort),
});
await this.networkService.reloadNetworkStack();
} catch (error) {
this.logger.error(
'Failed to begin UPNP Remote Access using port %s: %O',
String(sslPort),
error
);
this.stop();
}
}
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

Improve error handling and input validation in the begin() method

The method retrieves and uses an SSL port without proper validation:

  1. There's no check if sslPort is undefined before converting to Number
  2. No validation that the port is in a valid range
  3. The call to this.stop() is not awaited, potentially allowing unhandled promise rejections
async begin() {
    const sslPort = this.configService.get<string | undefined>('store.emhttp.var.portssl');
+   if (!sslPort) {
+       this.logger.error('SSL port is not configured');
+       return;
+   }
+   const numericPort = Number(sslPort);
+   if (isNaN(numericPort) || numericPort <= 0 || numericPort > 65535) {
+       this.logger.error(`Invalid SSL port: ${sslPort}`);
+       return;
+   }
    try {
        await this.upnpService.createOrRenewUpnpLease({
-           sslPort: Number(sslPort),
+           sslPort: numericPort,
        });
        await this.networkService.reloadNetworkStack();
    } catch (error) {
        this.logger.error(
            'Failed to begin UPNP Remote Access using port %s: %O',
            String(sslPort),
            error
        );
-       this.stop();
+       await this.stop().catch(stopError => {
+           this.logger.error('Error while stopping UPnP after initial failure: %O', stopError);
+       });
    }
}
📝 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
async begin() {
const sslPort = this.configService.get<string | undefined>('store.emhttp.var.portssl');
try {
await this.upnpService.createOrRenewUpnpLease({
sslPort: Number(sslPort),
});
await this.networkService.reloadNetworkStack();
} catch (error) {
this.logger.error(
'Failed to begin UPNP Remote Access using port %s: %O',
String(sslPort),
error
);
this.stop();
}
}
async begin() {
const sslPort = this.configService.get<string | undefined>('store.emhttp.var.portssl');
if (!sslPort) {
this.logger.error('SSL port is not configured');
return;
}
const numericPort = Number(sslPort);
if (isNaN(numericPort) || numericPort <= 0 || numericPort > 65535) {
this.logger.error(`Invalid SSL port: ${sslPort}`);
return;
}
try {
await this.upnpService.createOrRenewUpnpLease({
sslPort: numericPort,
});
await this.networkService.reloadNetworkStack();
} catch (error) {
this.logger.error(
'Failed to begin UPNP Remote Access using port %s: %O',
String(sslPort),
error
);
await this.stop().catch(stopError => {
this.logger.error('Error while stopping UPnP after initial failure: %O', stopError);
});
}
}

@pujitm pujitm force-pushed the refactor/mothership branch from 8888587 to 5a063ef Compare June 3, 2025 14:43
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

🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts (1)

14-14: Address the architectural question.

The comment raises a valid concern about whether this resolver should remain here or move to the connect plugin. This decision impacts the overall architecture and should be resolved.

Consider the following factors:

  • Is owner information always needed regardless of connect plugin status?
  • Does this create a dependency that could affect modularity?
  • How does this align with the plugin architecture goals?

Please clarify the architectural decision or create an issue to track this design question.

plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php (3)

182-184: Add validation for JSON parsing.

The code reads and parses JSON but doesn't validate if the JSON is well-formed. If the file contains invalid JSON, json_decode() will return null, and the ?: operator will use an empty array, which is good. However, consider logging when JSON parsing fails for debugging purposes.

-$connectConfig = file_exists($connectJsonPath) ? @json_decode(file_get_contents($connectJsonPath), true) : [];
+$connectConfig = [];
+if (file_exists($connectJsonPath)) {
+    $jsonContent = @file_get_contents($connectJsonPath);
+    $connectConfig = @json_decode($jsonContent, true);
+    if (json_last_error() !== JSON_ERROR_NONE) {
+        error_log("Failed to parse connect.json: " . json_last_error_msg());
+        $connectConfig = [];
+    }
+}

210-210: SSO enabled logic could be more explicit.

The current logic !empty($connectConfig['ssoSubIds'] ?? '') works but could be clearer about what constitutes "enabled" SSO.

-$this->ssoEnabled = !empty($connectConfig['ssoSubIds'] ?? '');
+$this->ssoEnabled = !empty($connectConfig['ssoSubIds'] ?? null);

This makes it clearer that we're checking for the existence of non-empty ssoSubIds rather than falling back to an empty string.


182-210: Consider backwards compatibility with existing INI config.

The migration to JSON config appears to completely replace the INI-based config for remote connection settings. Consider adding a fallback mechanism that reads from the old INI config if the new JSON config doesn't exist or is empty.

This would ensure a smoother migration path:

 $connectJsonPath = '/boot/config/plugins/dynamix.my.servers/configs/connect.json';
 $connectConfig = file_exists($connectJsonPath) ? @json_decode(file_get_contents($connectJsonPath), true) : [];
+
+// Fallback to INI config if JSON config is not available
+if (empty($connectConfig) && !empty($this->myServersFlashCfg)) {
+    $connectConfig = [
+        'apikey' => $this->myServersFlashCfg['remote']['apikey'] ?? '',
+        'avatar' => $this->myServersFlashCfg['remote']['avatar'] ?? '',
+        'email' => $this->myServersFlashCfg['remote']['email'] ?? '',
+        // Add other mappings as needed
+    ];
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4929b87 and 8888587.

📒 Files selected for processing (3)
  • api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/connect/connect-api-key.service.ts (1 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/unraid-api-plugin-connect/src/connect/connect-api-key.service.ts
🧰 Additional context used
🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts

[failure] 11-11:
Delete ';⏎import·{·ConfigService·}·from·'@nestjs/config

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Test API
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (6)
api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts (4)

3-3: Good migration to shared module.

The import change from local path to @unraid/shared aligns well with the architectural refactoring to centralize shared types and utilities.


17-17: Correct NestJS dependency injection pattern.

The ConfigService injection follows proper NestJS patterns and correctly replaces the previous store access approach.


12-12: ⚠️ Potential issue

Fix import formatting issue.

Static analysis flagged a formatting issue with this import statement.

Apply this fix:

-import { Owner } from '@app/unraid-api/graph/resolvers/owner/owner.model.js';
-import { ConfigService } from '@nestjs/config';
+import { Owner } from '@app/unraid-api/graph/resolvers/owner/owner.model.js';
+import { ConfigService } from '@nestjs/config';
⛔ Skipped due to learnings
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom ConfigService implementation that includes a `set()` method for runtime configuration mutation, unlike the standard @nestjs/config package which only provides getter methods.
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard @nestjs/config package which only provides getter methods.

25-38:

✅ Verification successful

Verify configuration structure consistency.

The migration from store access to ConfigService looks correct, but ensure the connect.config structure matches expectations across the codebase.

Run this script to verify the configuration structure is consistent:


🏁 Script executed:

#!/bin/bash
# Description: Verify connect.config usage patterns across the codebase
# Expected: Consistent usage of connect.config.username and connect.config.avatar

echo "=== Searching for connect.config usage ==="
rg -A 3 "connect\.config" --type ts

echo "=== Searching for config.username usage ==="
rg -A 2 "config\.username" --type ts

echo "=== Searching for config.avatar usage ==="
rg -A 2 "config\.avatar" --type ts

Length of output: 14454


connect.config usage verified
Verified that connect.config.username and connect.config.avatar are both set in connect-settings.service.ts and consistently retrieved (including in owner.resolver.ts). No discrepancies found—this migration is good to merge.

plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php (2)

206-207: Logic for hasRemoteApikey and registered properties looks correct.

The separation of concerns is well implemented - hasRemoteApikey checks for the presence of an API key, while registered requires both an API key and the connect plugin to be installed.


189-190:

✅ Verification successful

Data type change may break backwards compatibility.

The default values for wanaccess and upnpEnabled changed from string "no" to boolean false. This could break other parts of the system that expect string values when checking these configuration options.

Also applies to: 195-196


🏁 Script executed:

#!/bin/bash
# Search for code that might expect string values for these config options
rg -A 3 -B 3 'wanaccess.*=.*"no"'
rg -A 3 -B 3 'upnpEnabled.*=.*"no"'
rg -A 3 -B 3 'wanaccess.*===.*"no"'
rg -A 3 -B 3 'upnpEnabled.*===.*"no"'

Length of output: 1118


🏁 Script executed:

#!/bin/bash
# Search for usage of boolean defaults in PHP code to catch any string comparisons
rg -A3 -B3 "\$connectConfig\['wanaccess'\]" -t php
rg -A3 -B3 "\$connectConfig\['upnpEnabled'\]" -t php

Length of output: 117


🏁 Script executed:

#!/bin/bash
# List all occurrences of these keys across the repo for context
rg -n "wanaccess" -C5
rg -n "upnpEnabled" -C5

Length of output: 69491


Defaults-to-boolean false is compatible—no breakage found

I searched through the PHP plugin and supporting JS/TypeScript code and found:

• In Connect.page, both wanaccess and upnpEnabled are only ever checked via === 'yes', so a boolean false (from the new default) behaves identically to the old "no".
• There are no other strict comparisons against "no" anywhere else in the PHP or JS/TS code.

No changes are needed here.

@pujitm pujitm force-pushed the refactor/mothership branch from 5a063ef to fa59b6f Compare June 3, 2025 14:53
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: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa59b6f and 1f7b0ca.

📒 Files selected for processing (5)
  • api/src/core/log.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/config.entity.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/connect/connect.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/unraid-api-plugin-connect/src/connect/connect.module.ts
  • packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
api/src/core/log.ts (1)
api/src/environment.ts (1)
  • LOG_LEVEL (85-89)
packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (2)
packages/unraid-api-plugin-connect/src/mothership/mothership.handler.ts (1)
  • Injectable (12-86)
packages/unraid-api-plugin-connect/src/config.entity.ts (2)
  • ConfigType (207-210)
  • emptyMyServersConfig (212-227)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Test API
  • GitHub Check: Build API
🔇 Additional comments (16)
api/src/core/log.ts (2)

4-4: LGTM! Proper import of LOG_LEVEL constant.

The import correctly brings in the LOG_LEVEL constant from the environment configuration, which is used in the serializers section.


46-50: Good security enhancement with additional sensitive data redaction.

The new redact paths properly handle the additional sensitive tokens introduced in the connect plugin refactor:

  • apikey and localApiKey for API authentication
  • accesstoken, idtoken, refreshtoken for OAuth/SSO tokens

These align with the sensitive fields defined in the MyServersConfig class.

packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (4)

1-11: Well-structured NestJS service with proper dependencies.

The service follows good practices:

  • Proper dependency injection with ConfigService
  • Clear separation of concerns with event handling
  • Immutable config key definition

13-15: Safe configuration retrieval with proper error handling.

Using getOrThrow ensures the service fails fast if the required configuration is missing, which is appropriate for a critical service dependency.


23-30: Smart user reset logic that preserves non-identity data.

The destructuring approach correctly separates infrastructure settings (wanaccess, wanport, upnpEnabled, ssoSubIds) from user identity data, ensuring only user-specific fields are cleared on logout.


32-35: Proper event-driven logout handling.

The async event listener correctly calls the resetUser method, maintaining consistency with the broader logout flow seen in MothershipHandler.

packages/unraid-api-plugin-connect/src/config.entity.ts (10)

1-17: Comprehensive import of validation and GraphQL decorators.

The imports properly bring in all necessary validation, transformation, and GraphQL decorators for the robust type system being implemented.


20-32: Improved enum definitions with clearer state representation.

The updated enums provide better granularity:

  • MinigraphStatus now includes PRE_INIT and ERROR_RETRYING for more precise connection state tracking
  • DynamicRemoteAccessType simplified to three clear options: STATIC, UPNP, DISABLED

34-112: Well-structured MyServersConfig class with comprehensive validation.

The class improvements include:

  • Proper GraphQL and validation decorators for all fields
  • Type safety with boolean for wanaccess (addressing past review concerns)
  • Nullable wanport for optional configuration
  • Strong validation for sensitive fields like email and SSO IDs
  • Clear field grouping with comments

The SSO ID regex validation is particularly good for security.


114-145: Appropriate separation of connection metadata concerns.

The ConnectionMetadata class properly encapsulates mothership connection state with:

  • Clear status tracking with MinigraphStatus enum
  • Optional error and timing fields for debugging
  • Proper nullable types for optional metadata

147-168: Good URL access object modeling.

The AccessUrlObject class properly models network access with:

  • Support for both IPv4 and IPv6
  • URL type classification from shared package
  • Optional name field for user-friendly identification

170-191: Comprehensive dynamic remote access state tracking.

The DynamicRemoteAccessState class provides good state management for remote access with proper typing and validation.


193-199: Clean factory function for default state.

The helper function properly uses plainToInstance for type-safe object creation with appropriate default values.


201-210: Proper type composition for configuration hierarchy.

The type definitions correctly compose the configuration structure, extending demo config and providing proper typing for the nested configuration object.


212-227: Comprehensive default configuration factory.

The emptyMyServersConfig function provides appropriate defaults for all fields, ensuring consistent initialization across the system.


229-236: Proper configuration registration with type safety.

The config feature registration correctly initializes all components:

  • Mothership with PRE_INIT status
  • Dynamic remote access in disabled state
  • Default MyServersConfig instance

This provides a clean foundation for the configuration system.

Comment on lines +54 to +63
serializers: {
context: (context) => {
// For DEBUG and ERROR levels, return full context
if (LOG_LEVEL === 'DEBUG' || LOG_LEVEL === 'ERROR') {
return context;
}
// For other levels, only return the context key if it exists
return context?.context || context;
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Context serializer logic looks good but verify LOG_LEVEL constant usage.

The conditional serialization based on log level is a smart approach for performance and security. However, ensure the LOG_LEVEL constant usage is consistent.


🏁 Script executed:

#!/bin/bash
# Verify LOG_LEVEL usage consistency across the codebase
echo "Checking LOG_LEVEL usage in log.ts and environment.ts..."
rg "LOG_LEVEL" --type ts -A 2 -B 2

Length of output: 5560


Unify LOG_LEVEL source in core/log.ts

The mapping to Pino levels currently reads directly from process.env.LOG_LEVEL while elsewhere you use the imported LOG_LEVEL constant. This inconsistency can lead to mismatches if defaults or casing logic change in environment.ts.

• api/src/core/log.ts (around the const level = … definition)

Suggested diff:

- const level =
-   levels[
-     levels.indexOf(process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number])
-   ] ?? 'info';
+ const level =
+   levels[
+     levels.indexOf(LOG_LEVEL.toLowerCase() as (typeof levels)[number])
+   ] ?? 'info';
📝 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
serializers: {
context: (context) => {
// For DEBUG and ERROR levels, return full context
if (LOG_LEVEL === 'DEBUG' || LOG_LEVEL === 'ERROR') {
return context;
}
// For other levels, only return the context key if it exists
return context?.context || context;
},
},
const level =
levels[
levels.indexOf(
- process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number]
+ LOG_LEVEL.toLowerCase() as (typeof levels)[number]
)
] ?? 'info';
🤖 Prompt for AI Agents
In api/src/core/log.ts around lines 54 to 63, the LOG_LEVEL constant is used
inconsistently by reading directly from process.env.LOG_LEVEL in some places
while using the imported LOG_LEVEL constant elsewhere. To fix this, unify the
LOG_LEVEL source by always using the imported LOG_LEVEL constant throughout the
file, including in the serializers and level mapping logic, to ensure consistent
casing and default handling as defined in environment.ts.

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: 5

♻️ Duplicate comments (3)
packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

125-130: ⚠️ Potential issue

wsClient.terminate() is still invoked – API mismatch
terminate() exists on the raw WebSocket, not on graphql-ws’s Client. Calling it throws TypeError: client.terminate is not a function.

-            this.clearClientEventHandlers();
-            this.wsClient.terminate();
-            await this.wsClient.dispose();
+            this.clearClientEventHandlers();
+            await this.wsClient.dispose(); // disposes & closes internally
packages/unraid-api-plugin-connect/src/system/url-resolver.service.ts (2)

306-314: IPv6 URL is stored in ipv4 field – same bug as previously reported
The IPv6 URL resolved from lanIp6 is still assigned to ipv4 instead of ipv6.

-                ipv4: lanIp6Url,
+                ipv6: lanIp6Url,

Same issue was flagged in the last review – please amend.


127-156: 🛠️ Refactor suggestion

Handle raw IPv6 literals – wrap with square brackets before constructing the URL

new URL() rejects bare IPv6 literals such as https://2001:db8::1.
Detect IPv6 literals and wrap them in [] so the constructor receives an RFC-compliant host part.

-        const urlString = `${httpMode}${url}${portToUse}`;
+        const isIpv6Literal = url.includes(':') && !url.startsWith('[');
+        const hostPart = isIpv6Literal ? `[${url}]` : url;
+        const urlString = `${httpMode}${hostPart}${portToUse}`;

This fixes failures when the nginx config contains an IPv6 address instead of a hostname.

🧹 Nitpick comments (3)
packages/unraid-api-plugin-connect/src/helpers/consts.ts (1)

1-6: Well-structured time constants with minor consistency improvement opportunity.

The constants are mathematically correct and follow clear naming conventions. Good reuse pattern where THREE_MINUTES_MS builds on ONE_MINUTE_MS, and ONE_DAY_SECS/FIVE_DAYS_SECS build on previous constants.

Consider improving consistency by making ONE_HOUR_SECS build on ONE_MINUTE_SECS:

-export const ONE_HOUR_SECS = 60 * 60;
+export const ONE_HOUR_SECS = 60 * ONE_MINUTE_SECS;

This maintains the established pattern of building constants from previously defined ones.

packages/unraid-api-plugin-connect/src/system/network.service.ts (1)

25-45: Normalise & de-duplicate origins more robustly

map((origin) => (origin.endsWith('/') ? origin.slice(0, -1) : origin)) trims only the trailing slash and keeps case differences (HTTP://… vs http://…) or leading/trailing spaces, causing silent duplicates. A stricter normaliser would:

-].map((origin) => (origin.endsWith('/') ? origin.slice(0, -1) : origin));
+].map((origin) =>
+    origin.trim().replace(/\/$/, '').toLowerCase(),
+);

That guarantees canonical form before the Set is built.

packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (1)

18-27: getExtraOrigins() may emit duplicates & invalid URIs

After .split(',') you could:

  1. map((o) => o.trim()) to remove accidental whitespace around commas (newline/tab not just spaces).
  2. new Set() to guarantee uniqueness.
  3. Use URL constructor for basic validation instead of startsWith.
-            return extraOrigins
-                .replaceAll(' ', '')
-                .split(',')
-                .filter((origin) => origin.startsWith('http://') || origin.startsWith('https://'));
+            return [...new Set(
+                extraOrigins
+                    .split(',')
+                    .map((o) => o.trim())
+                    .filter((o) => {
+                        try { new URL(o); return true; } catch { return false; }
+                    }),
+            )];
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f7b0ca and 2e203e1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (25)
  • api/src/__test__/store/modules/config.test.ts (1 hunks)
  • api/src/graphql/resolvers/query/cloud/check-api.ts (0 hunks)
  • api/src/graphql/resolvers/query/cloud/check-cloud.ts (0 hunks)
  • api/src/graphql/resolvers/query/cloud/check-minigraphql.ts (0 hunks)
  • api/src/store/getters/index.ts (0 hunks)
  • api/src/store/index.ts (1 hunks)
  • api/src/store/modules/cache.ts (0 hunks)
  • api/src/store/root-reducer.ts (0 hunks)
  • api/src/unraid-api/graph/resolvers/cloud/cloud.model.ts (0 hunks)
  • api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts (0 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (0 hunks)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/connect/connect-api-key.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/consts.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helpers/my-servers-config.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/cloud.model.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership-subscription.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/mothership/mothership.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/cloud.resolver.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/service/cloud.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/network.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/system.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/system/url-resolver.service.ts (1 hunks)
💤 Files with no reviewable changes (9)
  • api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts
  • api/src/graphql/resolvers/query/cloud/check-api.ts
  • api/src/store/root-reducer.ts
  • api/src/graphql/resolvers/query/cloud/check-minigraphql.ts
  • api/src/store/modules/cache.ts
  • api/src/unraid-api/graph/resolvers/cloud/cloud.model.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/store/getters/index.ts
  • api/src/graphql/resolvers/query/cloud/check-cloud.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/unraid-api-plugin-connect/src/model/cloud.model.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/unraid-api-plugin-connect/src/mothership/mothership.module.ts
  • packages/unraid-api-plugin-connect/src/system/system.module.ts
  • api/src/store/index.ts
  • api/src/test/store/modules/config.test.ts
  • packages/unraid-api-plugin-connect/package.json
  • packages/unraid-api-plugin-connect/src/connect/connect-api-key.service.ts
  • packages/unraid-api-plugin-connect/src/mothership/mothership-subscription.handler.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/unraid-api-plugin-connect/src/system/url-resolver.service.ts (6)
packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (1)
  • Injectable (8-57)
packages/unraid-api-plugin-connect/src/system/network.service.ts (1)
  • Injectable (8-46)
packages/unraid-api-plugin-connect/src/connect/connect-settings.service.ts (1)
  • Injectable (37-521)
packages/unraid-api-plugin-connect/src/remote-access/static-remote-access.service.ts (1)
  • Injectable (8-35)
packages/unraid-api-plugin-connect/src/remote-access/upnp-remote-access.service.ts (1)
  • Injectable (9-42)
packages/unraid-api-plugin-connect/src/config.entity.ts (1)
  • ConfigType (207-210)
🔇 Additional comments (7)
packages/unraid-api-plugin-connect/src/helpers/my-servers-config.ts (5)

3-3: Good addition for GraphQL integration.

The import aligns well with the NestJS GraphQL integration mentioned in the summary.


5-11: Excellent conversion to explicit string enums.

Converting from implicit to explicit string literal enums improves type safety and makes enum values explicit, which is a best practice for maintainability and debugging.


13-17: Consistent enum pattern applied.

The same explicit string literal pattern is properly applied to DynamicRemoteAccessType, maintaining consistency across the codebase.


19-22: Proper GraphQL enum registration.

The registerEnumType call is correctly implemented with appropriate metadata for GraphQL schema generation.


25-47: Clean formatting improvements.

The type definitions maintain their structure while achieving consistent indentation and string literal style. This improves code readability without affecting functionality.

Also applies to: 52-55

packages/unraid-api-plugin-connect/src/connect/connect-config.service.ts (1)

43-51: ⚠️ Potential issue

resetUser() silently overwrites non-identity fields

The spread order means every property in identity (defaults) overrides the current config, potentially reverting unrelated settings the user changed after login. Invert the merge order:

-        this.configService.set(this.configKey, {
-            ...this.getConfig(),
-            ...identity,
-        });
+        this.configService.set(this.configKey, {
+            ...identity,
+            ...this.getConfig(), // keep latest user settings
+        });

(or update individual keys).

Likely an incorrect or invalid review comment.

packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts (1)

215-221: ⚠️ Potential issue

ConfigService has no .set() – compilation will fail

@nestjs/config’s ConfigService exposes read-only getters. The call below will error at compile time:

this.configService.set('connect.mothership.timeout', getDelay);

Persist the timeout another way (e.g., dedicated state service or in-memory variable).

⛔ Skipped due to learnings
Learnt from: pujitm
PR: unraid/api#1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom ConfigService implementation that includes a `set()` method for runtime configuration mutation, unlike the standard @nestjs/config package which only provides getter methods.

Comment on lines 17 to 20
async reloadNetworkStack() {
await this.nginxService.reload();
await this.dnsService.update();
}
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

Shield reloadNetworkStack() with error handling or run services in parallel

Both reloads are awaited sequentially; if nginxService.reload() rejects, dnsService.update() never runs and the error bubbles out unhandled.
Either guard each await in a try / catch and log, or fire them concurrently:

-    async reloadNetworkStack() {
-        await this.nginxService.reload();
-        await this.dnsService.update();
-    }
+    async reloadNetworkStack() {
+        await Promise.allSettled([
+            this.nginxService.reload(),
+            this.dnsService.update(),
+        ]);
+    }

This prevents partial-state updates and shortens total latency.

📝 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
async reloadNetworkStack() {
await this.nginxService.reload();
await this.dnsService.update();
}
async reloadNetworkStack() {
await Promise.allSettled([
this.nginxService.reload(),
this.dnsService.update(),
]);
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/system/network.service.ts around lines
17 to 20, the reloadNetworkStack method awaits nginxService.reload() and
dnsService.update() sequentially without error handling, causing
dnsService.update() to be skipped if the first fails. Modify the method to run
both service calls concurrently using Promise.all and add try/catch to handle
and log any errors, ensuring both services attempt to reload regardless of
individual failures and reducing total execution time.

Comment on lines +37 to 45
apiKey: { valid: true },
minigraphql,
cloud,
allowedOrigins: getAllowedOrigins(),
allowedOrigins: this.networkService.getAllowedOrigins(),
error:
`${apiKey.error ? `API KEY: ${apiKey.error}` : ''}${
`${
cloud.error ? `NETWORK: ${cloud.error}` : ''
}${minigraphql.error ? `CLOUD: ${minigraphql.error}` : ''}` || undefined,
};
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

apiKey hard-codes “valid: true” & error message concatenation is brittle

  1. Returning { valid: true } regardless of the actual key state may mislead consumers. Consider delegating this to cloudService so the resolver reflects real validity.

  2. Error string construction lacks delimiters and may yield "NETWORK: …CLOUD: …" without spacing. Simpler:

- error:
-     `${ cloud.error ? `NETWORK: ${cloud.error}` : '' }${
-         minigraphql.error ? `CLOUD: ${minigraphql.error}` : ''}` || undefined,
+ error: [cloud.error && `NETWORK: ${cloud.error}`,
+         minigraphql.error && `CLOUD: ${minigraphql.error}`]
+        .filter(Boolean)
+        .join(' | ') || undefined,

This avoids empty substrings and is easier to read.

📝 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
apiKey: { valid: true },
minigraphql,
cloud,
allowedOrigins: getAllowedOrigins(),
allowedOrigins: this.networkService.getAllowedOrigins(),
error:
`${apiKey.error ? `API KEY: ${apiKey.error}` : ''}${
`${
cloud.error ? `NETWORK: ${cloud.error}` : ''
}${minigraphql.error ? `CLOUD: ${minigraphql.error}` : ''}` || undefined,
};
apiKey: { valid: true },
minigraphql,
cloud,
allowedOrigins: this.networkService.getAllowedOrigins(),
error: [
cloud.error && `NETWORK: ${cloud.error}`,
minigraphql.error && `CLOUD: ${minigraphql.error}`,
]
.filter(Boolean)
.join(' | ') || undefined,
};
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/resolver/cloud.resolver.ts around
lines 37 to 45, the apiKey field is hardcoded to { valid: true } which does not
reflect the actual validity state and may mislead consumers; update this to
retrieve the real validity status from cloudService instead. Additionally, the
error message concatenation combines network and cloud errors without
delimiters, causing readability issues; refactor the error construction to join
only existing error messages with a clear separator like a space or comma to
avoid empty substrings and improve clarity.

Comment on lines 224 to 231
max: Infinity,
retryIf: (error, operation) => {
const { retryCount = 0 } = operation.getContext();
// i.e. retry api key errors up to 3 times (4 attempts total)
return !this.isInvalidApiKeyError(error) || retryCount < MAX_AUTH_RETRIES;
},
},
});
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

Infinite retry loop may hang the service

attempts.max = Infinity combined with exponential back-off means an unreachable Mothership keeps the process alive forever. Consider a sane upper bound or expose it via config.

🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/mothership/graphql.client.ts around
lines 224 to 231, the retry configuration sets max attempts to Infinity, which
can cause an infinite retry loop and hang the service if the Mothership is
unreachable. Fix this by replacing Infinity with a reasonable upper limit for
max retries, such as a configurable constant or a fixed number, to prevent
endless retries and allow the process to fail gracefully after a set number of
attempts.

Comment on lines +205 to +207
const mothershipGqlUri = this.configService.getOrThrow<string>('MOTHERSHIP_GRAPHQL_LINK');
const hostname = new URL(mothershipGqlUri).host;
const lookup = promisify(lookupDNS);
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

Use .hostname instead of .host to strip port before DNS look-up

new URL(mothershipGqlUri).host includes the port if present (e.g. api.example.com:443), causing DNS resolution to fail.
Swap to .hostname so only the domain part is sent to dns.lookup / dns.resolve.

-        const hostname = new URL(mothershipGqlUri).host;
+        const hostname = new URL(mothershipGqlUri).hostname;
📝 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
const mothershipGqlUri = this.configService.getOrThrow<string>('MOTHERSHIP_GRAPHQL_LINK');
const hostname = new URL(mothershipGqlUri).host;
const lookup = promisify(lookupDNS);
// File: packages/unraid-api-plugin-connect/src/service/cloud.service.ts
const mothershipGqlUri = this.configService.getOrThrow<string>('MOTHERSHIP_GRAPHQL_LINK');
- const hostname = new URL(mothershipGqlUri).host;
+ const hostname = new URL(mothershipGqlUri).hostname;
const lookup = promisify(lookupDNS);
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/cloud.service.ts around lines
205 to 207, replace the use of `.host` with `.hostname` on the URL object to
ensure the port is excluded from the DNS lookup. This means changing `new
URL(mothershipGqlUri).host` to `new URL(mothershipGqlUri).hostname` so that only
the domain name is passed to the DNS resolution function, preventing lookup
failures caused by including the port.

Comment on lines 214 to 219
if (!local.includes(network))
throw new Error(
`Local and network resolvers showing different IP for "${hostname}". [local="${
local ?? 'NOT FOUND'
}"] [network="${network ?? 'NOT FOUND'}"]`
);
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

Equality check should be strict, not substring

!local.includes(network) gives false positives (e.g. "1.2.3.4" includes "2.3").
Use strict comparison to ensure both resolvers agree on the exact IP.

-        if (!local.includes(network))
+        if (local !== network)
📝 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
if (!local.includes(network))
throw new Error(
`Local and network resolvers showing different IP for "${hostname}". [local="${
local ?? 'NOT FOUND'
}"] [network="${network ?? 'NOT FOUND'}"]`
);
if (local !== network)
throw new Error(
`Local and network resolvers showing different IP for "${hostname}". [local="${
local ?? 'NOT FOUND'
}"] [network="${network ?? 'NOT FOUND'}"]`
);
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/cloud.service.ts around lines
214 to 219, replace the substring check using local.includes(network) with a
strict equality check local !== network to ensure both resolvers return exactly
the same IP address. This prevents false positives where one IP string might be
a substring of the other.

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: 1

🧹 Nitpick comments (1)
packages/unraid-api-plugin-connect/src/service/cloud.service.test.ts (1)

10-31: Consider improving mock isolation and test categorization.

The mock setup is functional but has some areas for improvement:

  1. The test is labeled as "integration" but uses mocked dependencies, making it more of a unit test
  2. Consider using proper mocking libraries like vi.mock() for better type safety and isolation

Here's a suggested improvement for better mock isolation:

describe('CloudService.hardCheckCloud (unit)', () => {
    let service: CloudService;
-   let configService: any;
-   let mothership: any; 
-   let connectConfig: any;
+   let configService: jest.Mocked<ConfigService>;
+   let mothership: jest.Mocked<MothershipService>;
+   let connectConfig: jest.Mocked<ConnectConfigService>;

    beforeEach(() => {
-       configService = {
-           getOrThrow: (key: string) => {
-               if (key === 'MOTHERSHIP_GRAPHQL_LINK') return MOTHERSHIP_GRAPHQL_LINK;
-               if (key === 'API_VERSION') return API_VERSION;
-               throw new Error('Unknown key');
-           },
-       };
+       configService = {
+           getOrThrow: vi.fn().mockImplementation((key: string) => {
+               if (key === 'MOTHERSHIP_GRAPHQL_LINK') return MOTHERSHIP_GRAPHQL_LINK;
+               if (key === 'API_VERSION') return API_VERSION;
+               throw new Error('Unknown key');
+           }),
+       } as any;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e203e1 and c6fd8ac.

📒 Files selected for processing (5)
  • api/src/__test__/graphql/resolvers/query/cloud/check-mothership-authentication.test.ts (0 hunks)
  • api/src/graphql/resolvers/query/cloud/check-dns.ts (0 hunks)
  • api/src/graphql/resolvers/query/cloud/check-mothership-authentication.ts (0 hunks)
  • api/src/graphql/resolvers/query/cloud/create-response.ts (0 hunks)
  • packages/unraid-api-plugin-connect/src/service/cloud.service.test.ts (1 hunks)
💤 Files with no reviewable changes (4)
  • api/src/graphql/resolvers/query/cloud/check-dns.ts
  • api/src/test/graphql/resolvers/query/cloud/check-mothership-authentication.test.ts
  • api/src/graphql/resolvers/query/cloud/create-response.ts
  • api/src/graphql/resolvers/query/cloud/check-mothership-authentication.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/unraid-api-plugin-connect/src/service/cloud.service.test.ts (1)
api/src/environment.ts (2)
  • MOTHERSHIP_GRAPHQL_LINK (90-94)
  • API_VERSION (65-65)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
packages/unraid-api-plugin-connect/src/service/cloud.service.test.ts (1)

1-8: LGTM! Clean imports and well-defined test constants.

The imports are appropriate for Vitest testing, and the test constants provide clear separation between test data and actual values.

Comment on lines 33 to 50
it('fails to authenticate with mothership with no credentials', async () => {
try {
await expect(
service['hardCheckCloud'](API_VERSION, BAD)
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
);
await expect(
service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
} catch (error) {
if (error instanceof Error && error.message.includes('Timeout')) {
// Test succeeds on timeout
return;
}
throw error;
}
});
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

Address testing of private methods and improve error handling.

This test has several concerns that should be addressed:

  1. Testing private methods: Accessing service['hardCheckCloud'] tests implementation details rather than public behavior
  2. Timeout handling: The catch block that allows tests to pass on timeout is a workaround that masks real issues
  3. Limited coverage: Only tests failure scenarios

Consider these improvements:

-   it('fails to authenticate with mothership with no credentials', async () => {
-       try {
-           await expect(
-               service['hardCheckCloud'](API_VERSION, BAD)
-           ).rejects.toThrowErrorMatchingInlineSnapshot(
-               `[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
-           );
-           await expect(
-               service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
-           ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
-       } catch (error) {
-           if (error instanceof Error && error.message.includes('Timeout')) {
-               // Test succeeds on timeout
-               return;
-           }
-           throw error;
-       }
-   });
+   it('fails to authenticate with mothership with bad credentials', async () => {
+       await expect(
+           service['hardCheckCloud'](API_VERSION, BAD)
+       ).rejects.toThrow('Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.');
+   });
+
+   it('fails to authenticate with mothership with invalid API key', async () => {
+       await expect(
+           service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
+       ).rejects.toThrow('Invalid credentials');
+   });

Alternative approach: Test the public interface instead of private methods by testing methods that call hardCheckCloud internally.

📝 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
it('fails to authenticate with mothership with no credentials', async () => {
try {
await expect(
service['hardCheckCloud'](API_VERSION, BAD)
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
);
await expect(
service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
} catch (error) {
if (error instanceof Error && error.message.includes('Timeout')) {
// Test succeeds on timeout
return;
}
throw error;
}
});
it('fails to authenticate with mothership with bad credentials', async () => {
await expect(
service['hardCheckCloud'](API_VERSION, BAD)
).rejects.toThrow('Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.');
});
it('fails to authenticate with mothership with invalid API key', async () => {
await expect(
service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
).rejects.toThrow('Invalid credentials');
});
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/cloud.service.test.ts around
lines 33 to 50, avoid directly testing the private method 'hardCheckCloud' by
instead writing tests against the public methods that internally invoke it.
Remove the catch block that swallows timeout errors to ensure real issues are
not masked. Expand test coverage to include successful authentication scenarios
and other relevant cases to improve overall test robustness.

@pujitm pujitm force-pushed the refactor/mothership branch from 78d97b7 to de68fc5 Compare June 3, 2025 21:33
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 (2)
packages/unraid-api-plugin-connect/src/service/cloud.service.ts (2)

203-203: Use .hostname instead of .host to strip port before DNS look-up

new URL(mothershipGqlUri).host includes the port if present (e.g. api.example.com:443), causing DNS resolution to fail. Swap to .hostname so only the domain part is sent to dns.lookup / dns.resolve.

-        const hostname = new URL(mothershipGqlUri).host;
+        const hostname = new URL(mothershipGqlUri).hostname;

211-211: Equality check should be strict, not substring

!local.includes(network) gives false positives (e.g. "1.2.3.4" includes "2.3"). Use strict comparison to ensure both resolvers agree on the exact IP.

-        if (!local.includes(network)) {
+        if (local !== network) {
🧹 Nitpick comments (3)
packages/unraid-api-plugin-connect/src/service/cloud.service.ts (3)

43-43: Consider dependency injection for cache to improve testability.

The static cache makes unit testing difficult since it's shared across all instances and can't be easily mocked.

Consider injecting the cache as a dependency:

@Injectable()
export class CloudService {
-    static cache = new NodeCache() as TypedCache<CacheSchema>;
+    private readonly cache: TypedCache<CacheSchema>;

    private readonly logger = new Logger(CloudService.name);
    constructor(
        private readonly configService: ConfigService<ConfigType>,
        private readonly mothership: MothershipConnectionService,
-        private readonly connectConfig: ConnectConfigService
+        private readonly connectConfig: ConnectConfigService,
+        cache?: TypedCache<CacheSchema>
    ) {
+        this.cache = cache ?? new NodeCache() as TypedCache<CacheSchema>;
    }

75-86: Cache reference inconsistency.

The method uses CloudService.cache but should use this.cache if the cache becomes an instance property (related to the testability suggestion above).

If implementing the cache injection suggestion, update cache references:

-        const cachedCloudCheck = CloudService.cache.get('cloudCheck');
+        const cachedCloudCheck = this.cache.get('cloudCheck');

And similarly for line 85:

-        CloudService.cache.set('cloudCheck', cloudCheck, ttl);
+        this.cache.set('cloudCheck', cloudCheck, ttl);

188-191: Improve type safety and logic for IP validation.

The type check typeof validIp !== 'string' is redundant since validIp is already typed as string from the nullish coalescing with empty string.

Simplify the logic:

-            const validIp = local ?? network ?? '';
-            if (typeof validIp !== 'string') {
-                return '';
-            }
+            const validIp = local || network || '';
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c6fd8ac and de68fc5.

📒 Files selected for processing (1)
  • packages/unraid-api-plugin-connect/src/service/cloud.service.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages

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: 33

♻️ Duplicate comments (4)
packages/unraid-api-plugin-connect/package.json (2)

17-21: Update metadata for Connect plugin
The description still refers to “Example Health plugin” and keywords is empty. Update these fields to accurately describe the Connect plugin.

-  "keywords": [],
-  "description": "Example Health plugin for Unraid API",
+  "keywords": ["unraid", "plugin", "connect", "graphql"],
+  "description": "Unraid Connect Plugin for Mothership GraphQL integration",

21-61: Relocate runtime libraries to dependencies
Several packages (@apollo/client, class-transformer, graphql, rxjs, etc.) are imported at runtime but currently live in devDependencies. Move them into dependencies to ensure they’re installed in production.

--- package.json
@@ "devDependencies": {
-    "@apollo/client": "^3.11.8",
-    "class-transformer": "^0.5.1",
-    "graphql": "^16.9.0",
-    "graphql-scalars": "^1.23.0",
-    "graphql-subscriptions": "^3.0.0",
-    "graphql-ws": "^6.0.0",
-    "rxjs": "^7.8.2",
@@ "dependencies": {
-    "@unraid/shared": "workspace:*",
-    "ip": "^2.0.1",
-    "node-cache": "^5.1.2"
+    "@unraid/shared": "workspace:*",
+    "@apollo/client": "^3.11.8",
+    "class-transformer": "^0.5.1",
+    "graphql": "^16.9.0",
+    "graphql-scalars": "^1.23.0",
+    "graphql-subscriptions": "^3.0.0",
+    "graphql-ws": "^6.0.0",
+    "ip": "^2.0.1",
+    "node-cache": "^5.1.2",
+    "rxjs": "^7.8.2"
packages/unraid-api-plugin-connect/src/service/cloud.service.ts (2)

202-204: ⚠️ Potential issue

Use .hostname instead of .host to strip port before DNS look-up

new URL(mothershipGqlUri).host includes the port if present (e.g. api.example.com:443), causing DNS resolution to fail.
Swap to .hostname so only the domain part is sent to dns.lookup / dns.resolve.

-        const hostname = new URL(mothershipGqlUri).host;
+        const hostname = new URL(mothershipGqlUri).hostname;

211-211: ⚠️ Potential issue

Equality check should be strict, not substring

!local.includes(network) gives false positives (e.g. "1.2.3.4" includes "2.3").
Use strict comparison to ensure both resolvers agree on the exact IP.

-        if (!local.includes(network)) {
+        if (local !== network) {
🧹 Nitpick comments (39)
packages/unraid-api-plugin-connect/package.json (3)

11-11: Implement a real test script
The test script is currently a stub. Since vitest is already added in devDependencies, consider replacing it with:

"test": "vitest"

6-9: Ensure correct casing of README
You included "readme.md" in files, but the standard file is named README.md. Confirm the actual filename or adjust for case sensitivity to avoid missing files.


67-95: Review scope of peerDependencies
The peerDependencies mirror most devDependencies, which may force consumers to install many packages they don't need. Consider pruning to only the runtime packages that consumers must provide, or mark optional where appropriate.

packages/unraid-api-plugin-connect/justfile (2)

27-27: Add fallback for bc dependency.

The script assumes bc is available for percentage calculations, but this may not be installed on all systems.

-            percentage=$(echo "scale=1; $lines * 100 / $total" | bc)
+            if command -v bc >/dev/null 2>&1; then
+                percentage=$(echo "scale=1; $lines * 100 / $total" | bc)
+            else
+                percentage=$(awk "BEGIN {printf \"%.1f\", $lines * 100 / $total}")
+            fi

33-33: Fix comment inconsistency.

The comment says "Top 10 largest files" but the command uses head -n 11. While this is technically correct (because wc -l adds a total line), the comment should be clarified.

-    echo -e "${GREEN}Top 10 largest files:${NC}"
-    find . -type f -name "*.ts" -not -path "*/test/*" -not -path "*/graphql/generated/*" | xargs wc -l | sort -nr | head -n 11
+    echo -e "${GREEN}Top 10 largest files:${NC}"
+    find . -type f -name "*.ts" -not -path "*/test/*" -not -path "*/graphql/generated/*" | xargs wc -l | sort -nr | head -n 10 | grep -v "total$"
packages/unraid-api-plugin-connect/src/test/cloud.service.test.ts (1)

5-8: Extract test constants to improve maintainability.

Hard-coded test values should be extracted as named constants for better readability and maintenance.

+const TEST_MOTHERSHIP_URL = 'https://mothership.unraid.net/ws';
+const TEST_API_VERSION = 'TEST_VERSION';
+const INVALID_API_KEY = 'BAD_API_KEY';
+const INVALID_CREDENTIALS = 'BAD';
+
-const MOTHERSHIP_GRAPHQL_LINK = 'https://mothership.unraid.net/ws';
-const API_VERSION = 'TEST_VERSION';
-const BAD_API_KEY = 'BAD_API_KEY';
-const BAD = 'BAD';
packages/unraid-api-plugin-connect/src/service/dns.service.ts (1)

11-11: Consider making file paths configurable.

Hard-coded system paths reduce portability and testability. Consider extracting these as configuration constants.

+const PHP_BINARY_PATH = '/usr/bin/php';
+const UPDATE_DNS_SCRIPT_PATH = '/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php';
+
-            await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
+            await execa(PHP_BINARY_PATH, [UPDATE_DNS_SCRIPT_PATH]);
packages/unraid-api-plugin-connect/src/service/nginx.service.ts (1)

10-10: Consider making Nginx control script path configurable.

Hard-coded system paths should be extracted as configuration for better maintainability and testing.

+const NGINX_CONTROL_SCRIPT = '/etc/rc.d/rc.nginx';
+
-            await execa('/etc/rc.d/rc.nginx', ['reload']);
+            await execa(NGINX_CONTROL_SCRIPT, ['reload']);
packages/unraid-api-plugin-connect/src/helper/delay-function.ts (1)

4-4: Consider reducing the default initial delay.

The default initial delay of 10 seconds is quite aggressive for most use cases and may impact user experience.

-    const { initial = 10_000, jitter = true, max = Infinity } = delayOptions ?? {};
+    const { initial = 1_000, jitter = true, max = 60_000 } = delayOptions ?? {};
packages/unraid-api-plugin-connect/src/readme.md (1)

20-27: Specify language for the code block.

The code block should specify TypeScript for proper syntax highlighting and linting.

-```
+```typescript
 import { ApiModule } from '@unraid-api-plugin-connect/src';
 
 @Module({
   imports: [ApiModule],
 })
 export class AppModule {}
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

20-20: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

packages/unraid-api-plugin-connect/src/resolver/health.resolver.ts (1)

10-14: Health check returns demo data instead of actual health status.

Consider implementing actual health check logic rather than returning a demo constant.

 @Query(() => String)
 health() {
-    // You can replace the return value with your actual health check logic
-    return demoShared;
+    // Implement actual health check logic
+    return 'healthy';
 }
packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (1)

30-30: Consider adding error handling for missing configuration.

Using getOrThrow will throw an exception if the 'connect' configuration is missing. While this might be intentional, consider whether a GraphQL resolver should handle this more gracefully.

Consider wrapping the configuration access in a try-catch block to provide a more user-friendly error:

 @ResolveField(() => DynamicRemoteAccessStatus)
 public dynamicRemoteAccess(): DynamicRemoteAccessStatus {
-    const state = this.configService.getOrThrow<ConnectConfig>('connect');
+    try {
+        const state = this.configService.getOrThrow<ConnectConfig>('connect');
+    } catch (error) {
+        this.logger.error('Connect configuration not found', error);
+        throw new Error('Connect service is not properly configured');
+    }
     return {
packages/unraid-api-plugin-connect/src/event-handler/connect-login.handler.ts (2)

32-32: Consider making the url field configurable.

The url field is hardcoded to an empty string. This might need to be populated with actual data.

Would you like me to investigate what the expected value for this url field should be and help implement the proper logic to populate it?


17-17: Remove unnecessary async modifier.

The event handler is marked as async: true but the method doesn't need to be async since it only awaits a single operation at the end.

-    @OnEvent(EVENTS.LOGIN, { async: true })
+    @OnEvent(EVENTS.LOGIN)
packages/unraid-api-plugin-connect/src/service/internal.client.ts (1)

64-72: Instantiate the WebSocket subclass only once
Every call to getWebsocketWithMothershipHeaders() creates a new class definition, which prevents WS-level connection pooling and slightly inflates memory. Cache the constructor instead.

-    private getWebsocketWithMothershipHeaders() {
+    private websocketCtor?: typeof WebSocket;
+
+    private getWebsocketWithMothershipHeaders() {
+        if (this.websocketCtor) return this.websocketCtor;
         const getHeaders = () => this.connectionService.getMothershipWebsocketHeaders();
-        return class WebsocketWithMothershipHeaders extends WebSocket {
+        this.websocketCtor = class WebsocketWithMothershipHeaders extends WebSocket {
             constructor(address: string | URL, protocols?: string | string[]) {
                 super(address, protocols, {
                     headers: getHeaders(),
                 });
             }
-        };
+        };
+        return this.websocketCtor;
     }
packages/unraid-api-plugin-connect/src/service/network.service.ts (1)

40-44: Minor: micro-optimise de-duplication
new Set(sink) already drops duplicates; the preceding .map() can run after the Set to avoid doing the trailing-slash check twice for duplicates. Micro-level, but essentially free.

packages/unraid-api-plugin-connect/src/job/timeout-checker.job.ts (1)

24-29: Timeout logic ignores the “never pinged” case
If a client connects but no ping is ever received, lastPing is undefined, and the timeout is silently skipped. Consider treating undefined as timed-out after the same interval or when CONNECTED persists for > threshold without a ping.

packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts (1)

24-33: Extract the config key into a constant

'connect.config.wanaccess' is repeated here and in stopRemoteAccess(). Defining it once (e.g. const WAN_ACCESS_KEY = 'connect.config.wanaccess';) avoids typos and eases future refactors.

packages/unraid-api-plugin-connect/src/service/upnp.service.ts (1)

136-144: Parameter mismatch obscures intent

getWanPortToUse only documents wanPort, yet the caller forwards { sslPort, wanPort }.
Although structurally allowed, it confuses readers and tooling (e.g. schema generators).

-private async getWanPortToUse(args?: { wanPort?: number }) {
+private async getWanPortToUse(args?: { wanPort?: number }) {

…and pass only the field you need:

-const wanPortToUse = await this.getWanPortToUse(args);
+const wanPortToUse = await this.getWanPortToUse({ wanPort });
packages/unraid-api-plugin-connect/src/event-handler/mothership.handler.ts (1)

61-66: Remove inline question or formalise it

The comment “// Question: do we include MinigraphStatus.ERROR_RETRYING here?” is leftover scaffolding and will confuse future readers. Either:

  1. Extend the array to include the status, or
  2. Open a tracked issue and reference it, or
  3. Delete the comment.

Untended TODO-style markers are noise in production code.

packages/unraid-api-plugin-connect/src/test/url-resolver.service.test.ts (1)

190-196: Hard-coded expected URL assumes trailing slash

.toString() on URL guarantees the trailing slash, but many internal helpers use .href vs .origin. If the implementation switches, these tests will fail for non-functional reasons.

Consider comparing new URL(...).origin instead of the full string to make tests less brittle.

packages/unraid-api-plugin-connect/src/service/url-resolver.service.ts (1)

229-238: getAllowedServerIps() silently discards IPv6 addresses

Current logic prefers IPv4 and skips IPv6 when both exist.

-            if ((curr.ipv4 && curr.ipv6) || curr.ipv4) {
-                acc.push(curr.ipv4.toString());
-            } else if (curr.ipv6) {
-                acc.push(curr.ipv6.toString());
-            }
+            if (curr.ipv4) acc.push(curr.ipv4.toString());
+            if (curr.ipv6) acc.push(curr.ipv6.toString());
packages/unraid-api-plugin-connect/src/service/dynamic-remote-access.service.ts (1)

78-86: Remove unnecessary null check for required parameter

The allowedUrl parameter is required in the input type, so the null check is redundant.

-            await this.stopRemoteAccess();
-            if (input.allowedUrl) {
-                this.setAllowedUrl({
-                    ipv4: input.allowedUrl.ipv4?.toString() ?? null,
-                    ipv6: input.allowedUrl.ipv6?.toString() ?? null,
-                    type: input.allowedUrl.type,
-                    name: input.allowedUrl.name,
-                });
-            }
+            await this.stopRemoteAccess();
+            this.setAllowedUrl({
+                ipv4: input.allowedUrl.ipv4?.toString() ?? null,
+                ipv6: input.allowedUrl.ipv6?.toString() ?? null,
+                type: input.allowedUrl.type,
+                name: input.allowedUrl.name,
+            });
packages/unraid-api-plugin-connect/src/service/graphql.client.ts (3)

59-63: Remove redundant configuration checks

The configuration values are already validated through the getter methods. These duplicate calls serve no purpose.

     async onModuleInit(): Promise<void> {
         await this.createClientInstance();
-        this.configService.getOrThrow('API_VERSION');
-        this.configService.getOrThrow('MOTHERSHIP_GRAPHQL_LINK');
     }

318-325: Remove unused parameter

The events parameter is defined but never used in the method body.

-    private clearClientEventHandlers(
-        events: Array<'connected' | 'connecting' | 'error' | 'ping'> = [
-            'connected',
-            'connecting',
-            'error',
-            'ping',
-        ]
-    ): void {
+    private clearClientEventHandlers(): void {

140-142: Consider more robust protocol replacement

While replacing 'http' with 'ws' works for most cases, it could cause issues with URLs containing 'http' elsewhere in the string.

-        url: this.mothershipGraphqlLink.replace('http', 'ws'),
+        url: this.mothershipGraphqlLink.replace(/^https?:/, 'ws:'),
packages/unraid-api-plugin-connect/src/service/cloud.service.ts (3)

43-43: Consider making cache instance-based instead of static

Using a static cache could cause issues in testing scenarios or if multiple service instances are needed. Consider injecting the cache or making it an instance property.

-    static cache = new NodeCache() as TypedCache<CacheSchema>;
+    private readonly cache = new NodeCache() as TypedCache<CacheSchema>;

Then update all references from CloudService.cache to this.cache throughout the file.


77-78: Clean up commented code

Remove commented-out code to improve readability. If this code is needed for debugging, use proper logging levels instead.

-        if (cachedCloudCheck) {
-            // this.logger.verbose('Cache hit for cloud check %O', cachedCloudCheck);
-            return cachedCloudCheck;
-        }
+        if (cachedCloudCheck) {
+            return cachedCloudCheck;
+        }
-        if (!local.includes(network)) {
-            // Question: should we actually throw an error, or just log a warning?
-            //
-            // This is usually due to cloudflare's load balancing.
-            // if `dig +short mothership.unraid.net` shows both IPs, then this should be safe to ignore.
-            // this.logger.warn(
-            //     `Local and network resolvers showing different IP for "${hostname}". [local="${
-            //         local ?? 'NOT FOUND'
-            //     }"] [network="${network ?? 'NOT FOUND'}"].`
-            // );
-
+        if (local !== network) {

Also applies to: 212-220


188-191: Remove unnecessary type check

The DNS methods return strings, and the nullish coalescing ensures validIp is always a string. This check is redundant.

             const validIp = local ?? network ?? '';
-            if (typeof validIp !== 'string') {
-                return '';
-            }
             cache.set('cloudIp', validIp, 12 * ONE_HOUR_SECS); // 12 hours ttl
packages/unraid-api-plugin-connect/src/service/connect-api-key.service.ts (2)

13-13: Remove unused static field.

The validRoles static field is declared but never used in this class.

-    private static readonly validRoles: Set<Role> = new Set(Object.values(Role));

71-83: Improve error handling consistency.

The createLocalConnectApiKey method catches errors and returns null, while getOrCreateLocalApiKey throws an error. This inconsistency could lead to confusion. Consider either throwing in both cases or returning null in both cases.

 public async createLocalConnectApiKey(): Promise<ApiKeyWithSecret | null> {
     try {
         return await this.create({
             name: 'Connect',
             description: 'API key for Connect user',
             roles: [Role.CONNECT],
             overwrite: true,
         });
     } catch (err) {
-        this.logger.error(`Failed to create local API key for Connect user: ${err}`);
-        return null;
+        const errorMessage = `Failed to create local API key for Connect user: ${err}`;
+        this.logger.error(errorMessage);
+        throw new Error(errorMessage);
     }
 }
packages/unraid-api-plugin-connect/src/resolver/connect-settings.resolver.ts (1)

91-101: Consider improving the restart delay mechanism.

Using setTimeout with a hardcoded 300ms delay for API restart seems fragile. Consider using a more robust event-driven approach or at least make the delay configurable.

 const currentSettings = await this.connectSettingsService.getCurrentSettings();
 if (restartRequired) {
-    setTimeout(async () => {
-        // Send restart out of band to avoid blocking the return of this resolver
-        this.logger.log('Restarting API');
-        await this.connectSettingsService.restartApi();
-    }, 300);
+    // Emit an event for API restart to be handled asynchronously
+    this.eventEmitter.emit('api.restart.required', { 
+        reason: 'API settings updated',
+        delay: 300 // Make this configurable
+    });
 }
packages/unraid-api-plugin-connect/src/model/config.entity.ts (4)

38-38: Fix typo in comment.

-    // Remote Access Configurationx
+    // Remote Access Configuration

95-101: Enhance SSO ID validation message.

The validation message could be more specific about the allowed format.

 @Matches(/^[a-zA-Z0-9-]+$/, {
     each: true,
-    message: 'Each SSO ID must be alphanumeric with dashes',
+    message: 'Each SSO ID must contain only letters, numbers, and hyphens',
 })

103-106: Remove commented-out code.

If the minigraph field is no longer needed, remove it entirely rather than leaving it commented out.

-    // Connection Status
-    // @Field(() => MinigraphStatus)
-    // @IsEnum(MinigraphStatus)
-    // minigraph!: MinigraphStatus;
-

207-210: Improve type safety for ConfigType.

The store: any type and Record<string, string> intersection reduce type safety. Consider defining more specific types if possible.

 export type ConfigType = ConnectConfig & {
     connect: ConnectConfig;
-    store: any;
-} & Record<string, string>;
+    store: Record<string, unknown>;
+} & Record<string, string | number | boolean | null>;
packages/unraid-api-plugin-connect/src/service/connection.service.ts (1)

102-118: Correct log-message label for clarity
The warning refers to METADATA_CHANGED, but the emitted token is EVENTS.MOTHERSHIP_CONNECTION_STATUS_CHANGED, which can confuse log searches.

-                        this.logger.warn('Failed to emit METADATA_CHANGED event');
+                        this.logger.warn('Failed to emit MOTHERSHIP_CONNECTION_STATUS_CHANGED event');
packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (2)

249-266: Avoid unconditional restart flag from updateRemoteAccess
The method always returns true, so any call marks restartRequired even when nothing changed. Compute deltas instead:

-        // Use the dynamic remote access service to handle the transition
-        await this.remoteAccess.enableDynamicRemoteAccess({
+        const restartNeeded = await this.remoteAccess.enableDynamicRemoteAccess({
             ...
-        });
-        
-        return true;
+        });
+
+        return restartNeeded;

200-207: Check event-emitter result for diagnostics
Unlike MothershipConnectionService, you ignore the boolean returned by emit. Add a check to aid troubleshooting.

-                // Emit login event
-                this.eventEmitter.emit(EVENTS.LOGIN, {
+                const emitted = this.eventEmitter.emit(EVENTS.LOGIN, {
                     ...
                 });
+                if (!emitted) {
+                    this.logger.warn('LOGIN event emitted with no listeners');
+                }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de68fc5 and 0096f49.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (44)
  • packages/unraid-api-plugin-connect/justfile (1 hunks)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/event-handler/connect-login.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/event-handler/mothership.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helper/delay-function.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helper/generic-consts.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helper/nest-tokens.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/index.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/job/timeout-checker.job.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/cloud.model.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/config.demo.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/config.entity.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/connect.model.ts (3 hunks)
  • packages/unraid-api-plugin-connect/src/model/my-servers-config.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/module/connect.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/module/mothership.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/module/remote-access.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/module/system.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/readme.md (1 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/cloud.resolver.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/connect-settings.resolver.ts (6 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/health.resolver.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/network.resolver.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/service/cloud.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/config.persistence.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/connect-api-key.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/connect-config.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (8 hunks)
  • packages/unraid-api-plugin-connect/src/service/connection.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/dns.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/dynamic-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/graphql.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/internal.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/mothership-subscription.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/network.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/nginx.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/sso-user.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/upnp-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/upnp.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/url-resolver.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/test/cloud.service.test.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/test/url-resolver.service.test.ts (1 hunks)
✅ Files skipped from review due to trivial changes (8)
  • packages/unraid-api-plugin-connect/src/module/remote-access.module.ts
  • packages/unraid-api-plugin-connect/src/module/system.module.ts
  • packages/unraid-api-plugin-connect/src/module/mothership.module.ts
  • packages/unraid-api-plugin-connect/src/helper/generic-consts.ts
  • packages/unraid-api-plugin-connect/src/model/config.demo.ts
  • packages/unraid-api-plugin-connect/src/module/connect.module.ts
  • packages/unraid-api-plugin-connect/src/helper/nest-tokens.ts
  • packages/unraid-api-plugin-connect/src/model/my-servers-config.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/unraid-api-plugin-connect/src/index.ts
  • packages/unraid-api-plugin-connect/src/resolver/cloud.resolver.ts
  • packages/unraid-api-plugin-connect/src/model/cloud.model.ts
🧰 Additional context used
🧬 Code Graph Analysis (9)
packages/unraid-api-plugin-connect/src/test/cloud.service.test.ts (1)
api/src/environment.ts (2)
  • MOTHERSHIP_GRAPHQL_LINK (90-94)
  • API_VERSION (65-65)
packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (4)
packages/unraid-api-plugin-connect/src/resolver/health.resolver.ts (1)
  • Resolver (6-27)
packages/unraid-api-plugin-connect/src/resolver/connect-settings.resolver.ts (1)
  • Resolver (27-165)
web/composables/gql/graphql.ts (1)
  • Connect (438-445)
packages/unraid-api-plugin-connect/src/model/config.entity.ts (2)
  • ConfigType (207-210)
  • ConnectConfig (201-205)
packages/unraid-api-plugin-connect/src/resolver/health.resolver.ts (2)
packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (1)
  • Resolver (11-42)
packages/unraid-shared/src/index.ts (1)
  • demoShared (2-2)
packages/unraid-api-plugin-connect/src/model/connect.model.ts (1)
web/composables/gql/graphql.ts (2)
  • ConnectSettings (447-456)
  • Node (1030-1032)
packages/unraid-api-plugin-connect/src/test/url-resolver.service.test.ts (1)
packages/unraid-api-plugin-connect/src/model/config.entity.ts (1)
  • ConfigType (207-210)
packages/unraid-api-plugin-connect/src/resolver/connect-settings.resolver.ts (3)
packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (1)
  • Resolver (11-42)
packages/unraid-api-plugin-connect/src/resolver/network.resolver.ts (1)
  • Resolver (14-40)
web/composables/gql/graphql.ts (1)
  • ConnectSettings (447-456)
packages/unraid-api-plugin-connect/src/service/connect-api-key.service.ts (5)
packages/unraid-api-plugin-connect/src/service/connect-config.service.ts (1)
  • Injectable (8-57)
packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (1)
  • Injectable (37-521)
packages/unraid-shared/src/services/api-key.ts (1)
  • ApiKeyService (5-61)
packages/unraid-shared/src/tokens.ts (1)
  • API_KEY_SERVICE_TOKEN (2-2)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • AuthActionVerb (14-14)
packages/unraid-api-plugin-connect/src/service/cloud.service.ts (8)
web/composables/gql/graphql.ts (2)
  • CloudResponse (415-420)
  • MinigraphqlResponse (912-917)
packages/unraid-api-plugin-connect/src/service/connect-config.service.ts (1)
  • Injectable (8-57)
packages/unraid-api-plugin-connect/src/service/connect-api-key.service.ts (1)
  • Injectable (10-106)
packages/unraid-api-plugin-connect/src/service/connection.service.ts (1)
  • Injectable (50-222)
packages/unraid-api-plugin-connect/src/service/internal.client.ts (1)
  • Injectable (16-143)
packages/unraid-api-plugin-connect/src/service/mothership-subscription.handler.ts (1)
  • Injectable (29-216)
packages/unraid-api-plugin-connect/src/model/config.entity.ts (1)
  • ConfigType (207-210)
packages/unraid-api-plugin-connect/src/helper/generic-consts.ts (2)
  • ONE_MINUTE_SECS (5-5)
  • ONE_HOUR_SECS (6-6)
packages/unraid-api-plugin-connect/src/service/sso-user.service.ts (9)
packages/unraid-api-plugin-connect/src/service/connect-config.service.ts (1)
  • Injectable (8-57)
packages/unraid-api-plugin-connect/src/service/connect-api-key.service.ts (1)
  • Injectable (10-106)
packages/unraid-api-plugin-connect/src/service/config.persistence.ts (1)
  • Injectable (17-172)
packages/unraid-api-plugin-connect/src/service/dns.service.ts (1)
  • Injectable (5-18)
packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (1)
  • Injectable (37-521)
packages/unraid-api-plugin-connect/src/service/network.service.ts (1)
  • Injectable (8-46)
packages/unraid-api-plugin-connect/src/service/nginx.service.ts (1)
  • Injectable (4-17)
packages/unraid-api-plugin-connect/src/model/config.entity.ts (1)
  • ConfigType (207-210)
packages/unraid-api-plugin-connect/src/model/my-servers-config.ts (1)
  • MyServersConfig (24-48)
🪛 Biome (1.9.4)
packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts

[error] 227-231: This code is unreachable

... because this statement will throw an exception beforehand

(lint/correctness/noUnreachable)

🪛 markdownlint-cli2 (0.17.2)
packages/unraid-api-plugin-connect/src/readme.md

20-20: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build API
🔇 Additional comments (14)
packages/unraid-api-plugin-connect/justfile (1)

3-5: LGTM! Clean default recipe implementation.

The default recipe correctly uses just --list to show available recipes when called without arguments.

packages/unraid-api-plugin-connect/src/resolver/network.resolver.ts (1)

3-12: LGTM! Clean refactor to dependency injection and shared packages.

The migration to shared packages and dependency injection follows NestJS best practices and improves code modularity.

Also applies to: 16-16, 32-32

packages/unraid-api-plugin-connect/src/resolver/health.resolver.ts (1)

24-24:

❓ Verification inconclusive

Verify that configuration changes persist.

Ensure that ConfigService.set() persists changes rather than just updating in-memory state.


🏁 Script executed:

#!/bin/bash
# Description: Check if ConfigService has persistence mechanism
# Expected: Find configuration persistence implementation

ast-grep --pattern 'class $_ {
  $$$
  set($_, $_) {
    $$$
  }
  $$$
}'

Length of output: 75


Let’s locate and inspect the actual ConfigService implementation to see how set() works:


🏁 Script executed:

#!/bin/bash
# 1. Find where ConfigService is defined
echo "Searching for ConfigService class..."
rg "class ConfigService" -n --glob "*.ts"

# 2. Inspect its `set(...)` method for persistence logic
file=$(rg -l "class ConfigService" -n --glob "*.ts" | head -n1)
if [ -n "$file" ]; then
  echo "Found implementation in: $file"
  echo "Showing context around the `set(` method:"
  rg -n -B3 -A5 "set(" "$file"
else
  echo "❌ No ConfigService class found."
fi

Length of output: 283


Let’s confirm where ConfigService comes from in your resolver:

#!/bin/bash
# Locate health.resolver.ts and show its ConfigService import
file=$(fd -g "health.resolver.ts" | head -n1)
if [ -n "$file" ]; then
  echo "File: $file"
  rg -n "import.*ConfigService" "$file"
else
  echo "❌ health.resolver.ts not found"
fi

Let’s pinpoint where ConfigService is coming from:

#!/bin/bash
# 1. Locate health.resolver.ts
resolver=$(fd -g "health.resolver.ts" | head -n1)

# 2. Show its import of ConfigService
if [ -n "$resolver" ]; then
  echo "In $resolver:"
  rg -n "import.*ConfigService" "$resolver"
else
  echo "❌ health.resolver.ts not found"
fi

# 3. Find all ConfigService imports across the repo
echo ""
echo "All ConfigService imports:"
rg -n "import.*ConfigService" -g "*.ts"

Verify ConfigService.set behavior and error handling

  • ConfigService isn’t defined in this repo (imported externally). Confirm that its set() method writes to persistent storage and doesn’t only affect in-memory state.
  • Add appropriate error handling or fallbacks around ConfigService.set('connect.demo', newValue) to surface any failures.
packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (1)

30-33:

✅ Verification successful

Verify configuration structure change for dynamicRemoteAccessType.

The path to access dynamicRemoteAccessType changed from state.config.remote.dynamicRemoteAccessType to state.config.dynamicRemoteAccessType. This appears to be a breaking change in the configuration structure.

Run the following script to verify if this configuration path change is consistent across the codebase:


🏁 Script executed:

#!/bin/bash
# Description: Check for any remaining references to the old configuration path

# Search for old path pattern
rg -A 3 "config\.remote\.dynamicRemoteAccessType"

# Search for new path pattern to confirm migration
rg -A 3 "config\.dynamicRemoteAccessType"

Length of output: 2302


All references updated to new config path
The only occurrence of config.remote.dynamicRemoteAccessType is in a commented-out block (api/src/store/actions/shutdown-api-event.ts). All active code now uses config.dynamicRemoteAccessType. No further action required.

packages/unraid-api-plugin-connect/src/event-handler/connect-login.handler.ts (1)

5-6: ⚠️ Potential issue

Import GRAPHQL_PUBSUB_CHANNEL from the correct location.

The GRAPHQL_PUBSUB_CHANNEL constant is used but not imported. It should be imported from the helper file.

Add the missing import:

-import { GRAPHQL_PUBSUB_TOKEN } from '../helper/nest-tokens.js';
-import { EVENTS } from '../helper/nest-tokens.js';
+import { GRAPHQL_PUBSUB_TOKEN, GRAPHQL_PUBSUB_CHANNEL, EVENTS } from '../helper/nest-tokens.js';

Likely an incorrect or invalid review comment.

packages/unraid-api-plugin-connect/src/model/connect.model.ts (1)

297-301: Good implementation of the Node interface.

The explicit id field with proper decorators correctly implements the Node interface requirements.

packages/unraid-api-plugin-connect/src/service/internal.client.ts (2)

4-8: Ensure fetch is always defined in the runtime
node < 18 does not expose a global fetch; production images often pin older LTS versions. A missing global will crash the service the first time the client is created.

@@
-import { HttpLink } from '@apollo/client/link/http/index.js';
+import { HttpLink } from '@apollo/client/link/http/index.js';
+import fetch from 'cross-fetch';          // Polyfill guarantees availability

If you are 100 % certain the container runs on Node 18+, please ignore; otherwise importing a tiny polyfill is the safer default.

Also applies to: 79-82


82-86: Questionable Origin header value
Origin: /var/run/unraid-cli.sock is not a valid origin and will be rejected by many CORS checks. If the intent is to bypass the browser, consider omitting the header entirely or using a RFC-compliant scheme (e.g. http://localhost).

packages/unraid-api-plugin-connect/src/service/config.persistence.ts (1)

57-66: Skip persistence when config is undefined

configService.get<MyServersConfig>('connect.config') may return undefined on a mis-configured system; JSON-stringifying undefined produces "undefined", creating an invalid file.

-    async persist(config = this.configService.get<MyServersConfig>('connect.config')) {
+    async persist(config = this.configService.get<MyServersConfig>('connect.config')) {
+        if (!config) {
+            this.logger.warn('No config present – skipping persistence');
+            return false;
+        }
packages/unraid-api-plugin-connect/src/test/url-resolver.service.test.ts (1)

204-206: Accessing IPv6 result via ipv4 field is misleading

The test asserts:

lanIp6Url?.ipv4?.toString()

If the service really returns an IPv6 address in an ipv4 slot, that is a design smell; if not, the test passes by mistake.
Adjust the contract (e.g. distinct ipv6 field) or the test expectation to avoid hiding issues.

packages/unraid-api-plugin-connect/src/service/sso-user.service.ts (1)

17-20: ssoSubIds is typed as string in the model but treated as string[]

MyServersConfig.ssoSubIds is currently declared as a single string (see my-servers-config.ts).
Either migrate the config schema to an array or split/join when reading/writing; otherwise runtime shape and TypeScript types mismatch.

packages/unraid-api-plugin-connect/src/service/dynamic-remote-access.service.ts (1)

1-140: Service implementation looks solid!

The dynamic remote access service properly manages state transitions, timeout detection, and cleanup. The separation of concerns between static and UPnP remote access services is well-designed.

packages/unraid-api-plugin-connect/src/service/connect-api-key.service.ts (1)

85-105:

✅ Verification successful

Verify API key lookups are case-sensitive.

The method searches for an API key with the exact name 'Connect'. Ensure that the API key service's findByField method performs case-sensitive comparisons, as a key named 'connect' or 'CONNECT' would not be found.


🏁 Script executed:

#!/bin/bash
# Description: Check if API key lookups in the codebase are case-sensitive

# Search for findByField implementations to understand the lookup behavior
ast-grep --pattern 'findByField($_, $_) {
  $$$
}'

# Also search for any existing API keys with similar names
rg -i "name.*connect" --type ts -A 3 -B 3

Length of output: 52086


🏁 Script executed:

#!/bin/bash
# Locate the implementation of findByField in the ApiKeyService to inspect case sensitivity
rg -n "findByField" -A 20 -B 5 api/src/unraid-api/auth/api-key.service.ts

Length of output: 4934


findByField lookups are already case-sensitive
The findByField implementation in api/src/unraid-api/auth/api-key.service.ts (lines 277–281) uses strict === on string fields, so searching for 'Connect' only matches that exact casing. Since all Connect keys are created with the same capitalization, no changes are required.

packages/unraid-api-plugin-connect/src/resolver/connect-settings.resolver.ts (1)

120-123: LGTM! Clean event-driven logout implementation.

The change from direct store dispatch to event emission is a good architectural improvement that decouples the resolver from the state management implementation.

echo -e "${GREEN}Lines by directory:${NC}"
cd src
# First pass to get total lines
total=$(find . -type f -name "*.ts" -not -path "*/test/*" -not -path "*/graphql/generated/*" | xargs wc -l | tail -n 1 | awk '{print $1}')
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

Improve robustness of file counting logic.

The current approach using xargs wc -l | tail -n 1 can fail when no files are found, even with error redirection.

-    total=$(find . -type f -name "*.ts" -not -path "*/test/*" -not -path "*/graphql/generated/*" | xargs wc -l | tail -n 1 | awk '{print $1}')
+    total=$(find . -type f -name "*.ts" -not -path "*/test/*" -not -path "*/graphql/generated/*" -print0 | xargs -0 wc -l 2>/dev/null | tail -n 1 | awk '{print $1}' || echo "0")

Similarly for line 25:

-        lines=$(find "$dir" -type f -name "*.ts" -not -path "*/graphql/generated/*" | xargs wc -l 2>/dev/null | tail -n 1 | awk '{print $1}')
+        lines=$(find "$dir" -type f -name "*.ts" -not -path "*/graphql/generated/*" -print0 | xargs -0 wc -l 2>/dev/null | tail -n 1 | awk '{print $1}' || echo "0")

Also applies to: 25-25

🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/justfile at lines 21 and 25, the current
file counting logic using `xargs wc -l | tail -n 1` can fail if no matching
files are found. To fix this, modify the commands to handle the case when no
files are found gracefully, such as by using `find` with `-print0` and `xargs
-0` combined with a fallback or conditional check to avoid errors and ensure the
total line count is correctly set to zero if no files match.

echo -e "${BLUE}Counting TypeScript lines in src/ (excluding test/ and graphql/generated/)...${NC}"
echo
echo -e "${GREEN}Lines by directory:${NC}"
cd src
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

Add error handling for missing src directory.

The script changes to the src directory without checking if it exists, which could cause the entire script to fail.

+    if [ ! -d "src" ]; then
+        echo -e "${RED}Error: src directory not found${NC}"
+        exit 1
+    fi
     cd src
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/justfile at line 19, the script changes
directory to 'src' without verifying its existence, which may cause failure if
'src' is missing. Add a check before the 'cd src' command to confirm that the
'src' directory exists, and handle the error gracefully by either printing a
clear error message and exiting or by skipping the directory change to prevent
the script from failing unexpectedly.

const BAD_API_KEY = 'BAD_API_KEY';
const BAD = 'BAD';

describe('CloudService.hardCheckCloud (integration)', () => {
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

Consider testing public interface instead of private methods.

Testing private methods directly breaks encapsulation and makes tests brittle to implementation changes. Consider testing the public methods that use hardCheckCloud internally.

🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/test/cloud.service.test.ts at line 10,
the test is directly targeting the private method hardCheckCloud, which breaks
encapsulation and can lead to brittle tests. Refactor the test to remove direct
calls to hardCheckCloud and instead test the public methods of CloudService that
internally use hardCheckCloud, ensuring the tests verify behavior through the
public interface only.

Comment on lines +33 to +50
it('fails to authenticate with mothership with no credentials', async () => {
try {
await expect(
service['hardCheckCloud'](API_VERSION, BAD)
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
);
await expect(
service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
} catch (error) {
if (error instanceof Error && error.message.includes('Timeout')) {
// Test succeeds on timeout
return;
}
throw error;
}
});
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

Clarify timeout handling logic and add positive test cases.

The timeout handling appears to treat timeouts as test success, which is confusing. Additionally, this test only covers failure scenarios.

Consider:

  1. Adding explicit timeout configuration if network tests are expected
  2. Adding positive test cases with valid credentials
  3. Clarifying why timeouts are considered successful test outcomes
-        } catch (error) {
-            if (error instanceof Error && error.message.includes('Timeout')) {
-                // Test succeeds on timeout
-                return;
-            }
-            throw error;
-        }
+        } catch (error) {
+            if (error instanceof Error && error.message.includes('Timeout')) {
+                // Skip test on timeout - network unavailable
+                console.warn('Test skipped due to network timeout');
+                return;
+            }
+            throw error;
+        }
📝 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
it('fails to authenticate with mothership with no credentials', async () => {
try {
await expect(
service['hardCheckCloud'](API_VERSION, BAD)
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
);
await expect(
service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
} catch (error) {
if (error instanceof Error && error.message.includes('Timeout')) {
// Test succeeds on timeout
return;
}
throw error;
}
});
it('fails to authenticate with mothership with no credentials', async () => {
try {
await expect(
service['hardCheckCloud'](API_VERSION, BAD)
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
);
await expect(
service['hardCheckCloud'](API_VERSION, BAD_API_KEY)
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
} catch (error) {
if (error instanceof Error && error.message.includes('Timeout')) {
// Skip test on timeout - network unavailable
console.warn('Test skipped due to network timeout');
return;
}
throw error;
}
});
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/test/cloud.service.test.ts between
lines 33 and 50, the test treats timeouts as successful outcomes without clear
explanation and only covers failure cases. To fix this, explicitly configure
timeout settings for network calls if applicable, clarify in comments why
timeouts are considered successful, and add positive test cases that verify
successful authentication with valid credentials to ensure comprehensive test
coverage.

Comment on lines +9 to +17
async update() {
try {
await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
return true;
} catch (err: unknown) {
this.logger.warn('Failed to call Update DNS with error: ', err);
return false;
}
}
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

Add input validation and improve error handling.

The method lacks proper documentation and error context for callers.

+    /**
+     * Updates DNS settings by executing the Unraid dynamix UpdateDNS script.
+     * @returns Promise<boolean> - true if DNS update succeeded, false otherwise
+     */
     async update() {
         try {
             await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
             return true;
         } catch (err: unknown) {
-            this.logger.warn('Failed to call Update DNS with error: ', err);
+            this.logger.warn('Failed to update DNS settings', { error: err });
             return false;
         }
     }
📝 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
async update() {
try {
await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
return true;
} catch (err: unknown) {
this.logger.warn('Failed to call Update DNS with error: ', err);
return false;
}
}
/**
* Updates DNS settings by executing the Unraid dynamix UpdateDNS script.
* @returns Promise<boolean> - true if DNS update succeeded, false otherwise
*/
async update() {
try {
await execa('/usr/bin/php', ['/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php']);
return true;
} catch (err: unknown) {
this.logger.warn('Failed to update DNS settings', { error: err });
return false;
}
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/dns.service.ts between lines 9
and 17, add input validation if applicable and enhance error handling by
including more detailed error context in the catch block. Also, add a clear
method-level comment describing the purpose, behavior, and return value of the
update method to improve code documentation and maintainability.

Comment on lines +135 to +147
if (!url || url === '') {
throw new Error('No URL Provided');
}

if (port) {
portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://';
} else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://';
} else {
throw new Error(`No ports specified for URL: ${url}`);
}
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

IPv6 hosts need brackets – new URL() mis-parses raw IPv6 literals

new URL('http://2001:db8::1') throws because 2001:db8::1 is read as a scheme.
Wrap bare IPv6 addresses in [].

-        const urlString = `${httpMode}${url}${portToUse}`;
+        const host = /:/.test(url) && !url.startsWith('[') ? `[${url}]` : url;
+        const urlString = `${httpMode}${host}${portToUse}`;
📝 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
if (!url || url === '') {
throw new Error('No URL Provided');
}
if (port) {
portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://';
} else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://';
} else {
throw new Error(`No ports specified for URL: ${url}`);
}
if (!url || url === '') {
throw new Error('No URL Provided');
}
if (port) {
portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://';
} else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://';
} else {
throw new Error(`No ports specified for URL: ${url}`);
}
// Wrap bare IPv6 literals in brackets so new URL() parses them correctly
const host = /:/.test(url) && !url.startsWith('[') ? `[${url}]` : url;
const urlString = `${httpMode}${host}${portToUse}`;
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/url-resolver.service.ts around
lines 135 to 147, the code constructs URLs using raw IPv6 addresses without
brackets, causing new URL() to misinterpret the address as a scheme. To fix
this, detect if the host is an IPv6 literal and wrap it in square brackets
before concatenating with the protocol and port. This ensures new URL() parses
the address correctly.

Comment on lines +190 to +199
if (nginx.sslMode === 'yes') {
return this.getUrlForField({
url: nginx[field] as string,
portSsl: nginx.httpsPort,
});
}

if (nginx.sslMode === 'auto') {
throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode auto`);
}
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

sslMode: "no" not handled – valid configs will falsely error

When sslEnabled is true but sslMode is "no", the function falls through to the generic error branch.

-            if (nginx.sslMode === 'yes') {
+            if (nginx.sslMode === 'yes') {
                 ...
+            } else if (nginx.sslMode === 'no') {
+                return this.getUrlForField({
+                    url: nginx[field] as string,
+                    port: nginx.httpPort,
+                });
             }
📝 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
if (nginx.sslMode === 'yes') {
return this.getUrlForField({
url: nginx[field] as string,
portSsl: nginx.httpsPort,
});
}
if (nginx.sslMode === 'auto') {
throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode auto`);
}
if (nginx.sslMode === 'yes') {
return this.getUrlForField({
url: nginx[field] as string,
portSsl: nginx.httpsPort,
});
} else if (nginx.sslMode === 'no') {
return this.getUrlForField({
url: nginx[field] as string,
port: nginx.httpPort,
});
}
if (nginx.sslMode === 'auto') {
throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode auto`);
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/url-resolver.service.ts around
lines 190 to 199, the code does not handle the case when sslMode is "no" while
sslEnabled is true, causing valid configurations to incorrectly throw an error.
Add a condition to properly handle sslMode "no" by returning the appropriate URL
or behavior instead of falling through to the error branch. Ensure all valid
sslMode values are explicitly handled to avoid false errors.

Comment on lines +32 to +35
// If there's no change, no need to update
if (newUserSet.symmetricDifference(currentUserSet).size === 0) {
return false;
}
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

Set.symmetricDifference is non-standard – code will not compile

Standard JS Set has no symmetricDifference() method.

Simple replacement:

-        if (newUserSet.symmetricDifference(currentUserSet).size === 0) {
+        const changed =
+            [...newUserSet].filter((x) => !currentUserSet.has(x)).length > 0 ||
+            [...currentUserSet].filter((x) => !newUserSet.has(x)).length > 0;
+        if (!changed) {
             return false;
         }
📝 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
// If there's no change, no need to update
if (newUserSet.symmetricDifference(currentUserSet).size === 0) {
return false;
}
// If there's no change, no need to update
const changed =
[...newUserSet].filter((x) => !currentUserSet.has(x)).length > 0 ||
[...currentUserSet].filter((x) => !newUserSet.has(x)).length > 0;
if (!changed) {
return false;
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/sso-user.service.ts around
lines 32 to 35, the code uses a non-standard Set method symmetricDifference
which does not exist in JavaScript and causes compilation errors. Replace the
symmetricDifference call by manually computing the symmetric difference using
standard Set operations: create a new Set containing elements in either
newUserSet or currentUserSet but not in both, by combining the differences of
each set from the other, then check if the size of this new Set is zero to
determine if there is no change.

Comment on lines +193 to +201
getConnectionState() {
const state = this.configService.get<ConnectionMetadata>('connect.mothership');
if (!state) {
this.logger.error(
'connect.mothership config is not present! Preventing fatal crash; mothership is in Error state.'
);
}
return state;
}
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

Return a sane default when metadata is missing
getConnectionState() logs the missing config but still returns undefined; callers expecting a ConnectionMetadata object will crash on access. Return a minimal PRE_INIT object instead.

-        if (!state) {
-            this.logger.error(
-                'connect.mothership config is not present! Preventing fatal crash; mothership is in Error state.'
-            );
-        }
-        return state;
+        if (!state) {
+            this.logger.error(
+                'connect.mothership config is not present! Defaulting to PRE_INIT state.'
+            );
+            return { status: MinigraphStatus.PRE_INIT } as ConnectionMetadata;
+        }
+        return state;
📝 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
getConnectionState() {
const state = this.configService.get<ConnectionMetadata>('connect.mothership');
if (!state) {
this.logger.error(
'connect.mothership config is not present! Preventing fatal crash; mothership is in Error state.'
);
}
return state;
}
getConnectionState() {
const state = this.configService.get<ConnectionMetadata>('connect.mothership');
if (!state) {
this.logger.error(
'connect.mothership config is not present! Defaulting to PRE_INIT state.'
);
return { status: MinigraphStatus.PRE_INIT } as ConnectionMetadata;
}
return state;
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/connection.service.ts around
lines 193 to 201, the getConnectionState() method returns undefined when the
connect.mothership config is missing, which can cause callers to crash. Modify
the method to return a minimal ConnectionMetadata object with a PRE_INIT state
as a default instead of undefined, ensuring callers always receive a valid
object.

Comment on lines 225 to 231
private async setSandboxMode(sandboxEnabled: boolean): Promise<boolean> {
const { store, getters } = await import('@app/store/index.js');
const currentSandbox = getters.config().local.sandbox;
throw new Error('Not implemented');
const currentSandbox = this.configService.get('store.config.local.sandbox');
const sandbox = sandboxEnabled ? 'yes' : 'no';
if (currentSandbox === sandbox) return false;
store.dispatch(updateUserConfig({ local: { sandbox } }));
this.configService.set('store.config.local.sandbox', sandbox);
return true;
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

setSandboxMode is unreachable – breaks settings sync
The method throws "Not implemented" but is invoked from syncSettings(). Any update to sandbox mode will crash the API.

-        throw new Error('Not implemented');
-        const currentSandbox = this.configService.get('store.config.local.sandbox');
-        const sandbox = sandboxEnabled ? 'yes' : 'no';
-        if (currentSandbox === sandbox) return false;
-        this.configService.set('store.config.local.sandbox', sandbox);
-        return true;
+        const currentSandbox = this.configService.get('store.config.local.sandbox');
+        const sandbox = sandboxEnabled ? 'yes' : 'no';
+        if (currentSandbox === sandbox) {
+            return false;
+        }
+        this.configService.set('store.config.local.sandbox', sandbox);
+        return true;
📝 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 async setSandboxMode(sandboxEnabled: boolean): Promise<boolean> {
const { store, getters } = await import('@app/store/index.js');
const currentSandbox = getters.config().local.sandbox;
throw new Error('Not implemented');
const currentSandbox = this.configService.get('store.config.local.sandbox');
const sandbox = sandboxEnabled ? 'yes' : 'no';
if (currentSandbox === sandbox) return false;
store.dispatch(updateUserConfig({ local: { sandbox } }));
this.configService.set('store.config.local.sandbox', sandbox);
return true;
private async setSandboxMode(sandboxEnabled: boolean): Promise<boolean> {
const currentSandbox = this.configService.get('store.config.local.sandbox');
const sandbox = sandboxEnabled ? 'yes' : 'no';
if (currentSandbox === sandbox) {
return false;
}
this.configService.set('store.config.local.sandbox', sandbox);
return true;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 227-231: This code is unreachable

... because this statement will throw an exception beforehand

(lint/correctness/noUnreachable)

🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts
between lines 225 and 231, the setSandboxMode method currently throws a "Not
implemented" error, making it unreachable and causing crashes when called from
syncSettings. Remove the throw statement and implement the method logic to check
the current sandbox mode, compare it with the desired state, update the config
if different, and return a boolean indicating if a change was made.

pujitm added 2 commits June 4, 2025 14:59
@pujitm pujitm force-pushed the refactor/mothership branch from 6e2ea8a to b42d854 Compare June 4, 2025 19:25
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: 1

♻️ Duplicate comments (5)
packages/unraid-api-plugin-connect/src/service/config.persistence.ts (2)

23-26: Guard against missing PATHS_CONFIG_MODULES

get('PATHS_CONFIG_MODULES')! will throw a TypeError if the env-var is unset. Return a clear error instead so startup diagnostics are obvious.


32-48: Subscription leak – store the RxJS subscription and dispose on shutdown

The changes$ subscription is never unsubscribed. For long-running processes that restart modules (e.g. hot-reload in dev) this causes memory leaks.

packages/unraid-api-plugin-connect/src/service/cloud.service.ts (2)

202-204: Use .hostname instead of .host to strip port before DNS look-up

new URL(mothershipGqlUri).host includes the port if present (e.g. api.example.com:443), causing DNS resolution to fail. Swap to .hostname so only the domain part is sent to dns.lookup / dns.resolve.


211-211: Equality check should be strict, not substring

!local.includes(network) gives false positives (e.g. "1.2.3.4" includes "2.3"). Use strict comparison to ensure both resolvers agree on the exact IP.

packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (1)

225-232: ⚠️ Potential issue

Fix unreachable code in setSandboxMode method.

The method throws "Not implemented" making the subsequent code unreachable. This breaks settings sync when sandbox mode is updated, as noted in previous reviews.

-        throw new Error('Not implemented');
-        const currentSandbox = this.configService.get('store.config.local.sandbox');
+        const currentSandbox = this.configService.get('store.config.local.sandbox');
         const sandbox = sandboxEnabled ? 'yes' : 'no';
         if (currentSandbox === sandbox) return false;
         this.configService.set('store.config.local.sandbox', sandbox);
         return true;
🧰 Tools
🪛 Biome (1.9.4)

[error] 227-231: This code is unreachable

... because this statement will throw an exception beforehand

(lint/correctness/noUnreachable)

🧹 Nitpick comments (8)
packages/unraid-api-plugin-connect/src/service/dynamic-remote-access.service.ts (2)

46-52: Consider adding timeout check frequency control

The timeout check logic is correct, but there's no indication of how frequently checkForTimeout() is called. Consider documenting the expected call frequency or implementing a scheduled job.


79-86: Simplify URL transformation logic

The allowedUrl transformation can be simplified and made more readable.

-            this.setAllowedUrl({
-                ipv4: input.allowedUrl.ipv4?.toString() ?? null,
-                ipv6: input.allowedUrl.ipv6?.toString() ?? null,
-                type: input.allowedUrl.type,
-                name: input.allowedUrl.name,
-            });
+            this.setAllowedUrl(input.allowedUrl);

The setAllowedUrl method already handles the transformation and defaults appropriately.

packages/unraid-api-plugin-connect/src/service/graphql.client.ts (1)

61-62: Remove redundant configuration calls

These getOrThrow calls are redundant since the same values are already accessed in the class getters and constructor.

-        this.configService.getOrThrow('API_VERSION');
-        this.configService.getOrThrow('MOTHERSHIP_GRAPHQL_LINK');
packages/unraid-api-plugin-connect/src/service/cloud.service.ts (2)

43-43: Consider instance-based cache instead of static

Using a static cache shared across all service instances could lead to unexpected behavior in testing or multi-tenant scenarios. Consider using an instance-based cache.

-    static cache = new NodeCache() as TypedCache<CacheSchema>;
+    private cache = new NodeCache() as TypedCache<CacheSchema>;

And update all references from CloudService.cache to this.cache.


188-191: Improve type safety for IP validation

The type check typeof validIp !== 'string' is redundant since validIp is already typed as string from the nullish coalescing. Consider simplifying the logic.

-        const validIp = local ?? network ?? '';
-        if (typeof validIp !== 'string') {
-            return '';
-        }
+        const validIp = local || network || '';
packages/unraid-api-plugin-connect/src/model/connect-config.model.ts (2)

38-38: Fix typo in comment.

There's a typo in the comment: "Configurationx" should be "Configuration".

-    // Remote Access Configurationx
+    // Remote Access Configuration

104-106: Clean up commented code.

Consider removing the commented-out minigraph status field if it's no longer needed, or add a TODO comment explaining why it's temporarily disabled.

-    // Connection Status
-    // @Field(() => MinigraphStatus)
-    // @IsEnum(MinigraphStatus)
-    // minigraph!: MinigraphStatus;
packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (1)

150-151: Clean up commented code.

Remove the commented-out import and function call if they're no longer needed.

-        // const { writeConfigSync } = await import('@app/store/sync/config-disk-sync.js');
-        // writeConfigSync('flash');
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0096f49 and b42d854.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • api/package.json (4 hunks)
  • api/scripts/build.ts (3 hunks)
  • packages/unraid-api-plugin-connect/src/event-handler/mothership.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/index.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/job/timeout-checker.job.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/cloud.model.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/connect-config.model.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/model/my-servers-config.model.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts (2 hunks)
  • packages/unraid-api-plugin-connect/src/service/cloud.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/config.persistence.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/connect-config.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (8 hunks)
  • packages/unraid-api-plugin-connect/src/service/connection.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/dynamic-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/graphql.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/internal.client.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/sso-user.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/upnp-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/upnp.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/url-resolver.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/test/url-resolver.service.test.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • packages/unraid-api-plugin-connect/src/service/upnp-remote-access.service.ts
  • packages/unraid-api-plugin-connect/src/model/my-servers-config.model.ts
🚧 Files skipped from review as they are similar to previous changes (15)
  • packages/unraid-api-plugin-connect/src/resolver/connect.resolver.ts
  • packages/unraid-api-plugin-connect/src/job/timeout-checker.job.ts
  • api/package.json
  • packages/unraid-api-plugin-connect/src/service/connect-config.service.ts
  • packages/unraid-api-plugin-connect/src/index.ts
  • packages/unraid-api-plugin-connect/src/service/internal.client.ts
  • packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts
  • packages/unraid-api-plugin-connect/src/event-handler/mothership.handler.ts
  • packages/unraid-api-plugin-connect/src/service/upnp.service.ts
  • packages/unraid-api-plugin-connect/src/test/url-resolver.service.test.ts
  • packages/unraid-api-plugin-connect/src/service/url-resolver.service.ts
  • packages/unraid-api-plugin-connect/src/model/cloud.model.ts
  • packages/unraid-api-plugin-connect/src/service/sso-user.service.ts
  • api/scripts/build.ts
  • packages/unraid-api-plugin-connect/src/service/connection.service.ts
🧰 Additional context used
🪛 Biome (1.9.4)
packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts

[error] 227-231: This code is unreachable

... because this statement will throw an exception beforehand

(lint/correctness/noUnreachable)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
🔇 Additional comments (6)
packages/unraid-api-plugin-connect/src/service/config.persistence.ts (1)

57-57:

❌ Incorrect review comment

Verify config parameter type consistency

The method parameter defaults to this.configService.get<MyServersConfig>('connect.config') but the method signature accepts any MyServersConfig. Ensure the configuration path is consistent across the service.


🏁 Script executed:

#!/bin/bash
# Search for all usages of connect.config vs connect configuration keys
rg -A 3 -B 3 "connect\.config|'connect'" --type ts

Length of output: 24078


🏁 Script executed:

#!/bin/bash
# Display the beginning of config.persistence.ts to inspect the persist method signature
sed -n '1,200p' packages/unraid-api-plugin-connect/src/service/config.persistence.ts

Length of output: 6893


No inconsistencies in persist signature or config key usage
The persist method’s default parameter is correctly inferred as MyServersConfig (via get<MyServersConfig>), and every read/write in this service consistently uses the 'connect.config' key. You can safely ignore the suggested type/path mismatch.

Likely an incorrect or invalid review comment.

packages/unraid-api-plugin-connect/src/service/graphql.client.ts (2)

118-123: Potential race condition in client cleanup

The comment indicates a known race condition where apolloClient can become null during shutdown. Consider using proper synchronization or state management.

-        if (this.apolloClient) {
-            await this.apolloClient.clearStore();
-            // some race condition causes apolloClient to be null here upon api shutdown?
-            this.apolloClient?.stop();
-            this.apolloClient = null;
-        }
+        const client = this.apolloClient;
+        if (client) {
+            this.apolloClient = null;
+            await client.clearStore();
+            client.stop();
+        }

213-231:

✅ Verification successful

Verify retry logic handles edge cases

The retry logic has complex conditions. Ensure the retryCount context is properly initialized and the infinite retry limit is intentional.


🏁 Script executed:

#!/bin/bash
# Search for other retry implementations to verify consistency
rg -A 10 -B 5 "RetryLink|retryIf|attempts.*max" --type ts

Length of output: 7186


Retry logic confirmation

The retryCount is safely initialized to 0 via the default in
const { retryCount = 0 } = operation.getContext(), and it’s updated on each back‐off in your delay callback. Using max: Infinity is intentional to allow unlimited retries for non-auth errors, while invalid API key errors are still capped at MAX_AUTH_RETRIES. No changes needed.

packages/unraid-api-plugin-connect/src/model/connect-config.model.ts (1)

1-236: Well-structured model definitions.

The configuration models are comprehensive and well-designed with appropriate validation decorators, GraphQL integration, and type safety. The use of class-validator and class-transformer provides robust validation and serialization capabilities.

packages/unraid-api-plugin-connect/src/service/connect-settings.service.ts (2)

39-45: Excellent dependency injection refactoring.

The constructor properly injects ConfigService and other services, replacing direct Redux store access with clean dependency injection. This improves testability and follows NestJS best practices.


173-207: Good event-driven architecture implementation.

The signIn method properly uses ConfigService for state management and EventEmitter2 for event publishing, replacing Redux actions with a cleaner event-driven approach.

Comment on lines +88 to +94
} catch (error) {
this.logger.error(error);
const message = error instanceof Error ? error.message : 'Unknown Error';
this.setErrorMessage(message);
return error;
}
}
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

Error handling returns error object instead of throwing

The method catches errors but returns them instead of throwing, which is inconsistent with typical async error handling patterns. Consider whether callers expect thrown errors or returned error objects.

🤖 Prompt for AI Agents
In
packages/unraid-api-plugin-connect/src/service/dynamic-remote-access.service.ts
around lines 88 to 94, the catch block currently returns the caught error object
instead of throwing it, which is inconsistent with standard async error
handling. Modify the catch block to throw the error after logging and setting
the error message, ensuring that the error propagates properly to callers
expecting thrown exceptions.

Copy link
Contributor

github-actions bot commented Jun 4, 2025

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/PR1367/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: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b42d854 and e4497e1.

📒 Files selected for processing (13)
  • .github/workflows/main.yml (0 hunks)
  • api/src/environment.ts (2 hunks)
  • api/src/index.ts (2 hunks)
  • api/src/store/actions/shutdown-api-event.ts (0 hunks)
  • api/src/store/listeners/listener-middleware.ts (0 hunks)
  • api/src/store/listeners/upnp-listener.ts (2 hunks)
  • api/src/store/listeners/wan-access-change-listener.ts (0 hunks)
  • packages/unraid-api-plugin-connect/package.json (1 hunks)
  • packages/unraid-api-plugin-connect/src/event-handler/wan-access.handler.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/helper/nest-tokens.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/module/remote-access.module.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts (1 hunks)
  • packages/unraid-api-plugin-connect/src/service/upnp-remote-access.service.ts (1 hunks)
💤 Files with no reviewable changes (4)
  • .github/workflows/main.yml
  • api/src/store/actions/shutdown-api-event.ts
  • api/src/store/listeners/listener-middleware.ts
  • api/src/store/listeners/wan-access-change-listener.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • api/src/store/listeners/upnp-listener.ts
  • api/src/index.ts
  • packages/unraid-api-plugin-connect/src/module/remote-access.module.ts
  • api/src/environment.ts
  • packages/unraid-api-plugin-connect/src/service/upnp-remote-access.service.ts
  • packages/unraid-api-plugin-connect/package.json
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/unraid-api-plugin-connect/src/event-handler/wan-access.handler.ts (2)
packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts (1)
  • Injectable (9-34)
packages/unraid-api-plugin-connect/src/service/upnp-remote-access.service.ts (1)
  • Injectable (10-43)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (4)
packages/unraid-api-plugin-connect/src/event-handler/wan-access.handler.ts (1)

16-18: Good cleanup implementation.

The automatic disabling of WAN access on module destruction ensures proper cleanup and prevents dangling network configurations.

packages/unraid-api-plugin-connect/src/helper/nest-tokens.ts (1)

1-15: Excellent centralization of tokens and events.

Clean organization with proper separation of job tokens, GraphQL tokens, and event constants. The consistent naming convention and use of enums reduces magic strings throughout the codebase.

packages/unraid-api-plugin-connect/src/service/static-remote-access.service.ts (2)

19-21: Clean event-driven design.

Good separation of concerns - the service emits events rather than directly managing config and network state.


23-33: Solid validation and error handling.

Good configuration validation with early return for invalid types. The logging provides helpful debugging information when the dynamic remote access type doesn't match expectations.

Comment on lines +20 to +24
@OnEvent(EVENTS.ENABLE_WAN_ACCESS, { async: true })
async enableWanAccess() {
this.configService.set('connect.config.wanaccess', true);
await this.networkService.reloadNetworkStack();
}
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

Add error handling for network stack reload failures.

If reloadNetworkStack() fails, the config will show WAN access as enabled while the network stack may not reflect this state, creating system inconsistency.

 @OnEvent(EVENTS.ENABLE_WAN_ACCESS, { async: true })
 async enableWanAccess() {
     this.configService.set('connect.config.wanaccess', true);
-    await this.networkService.reloadNetworkStack();
+    try {
+        await this.networkService.reloadNetworkStack();
+    } catch (error) {
+        // Rollback config on failure to maintain consistency
+        this.configService.set('connect.config.wanaccess', false);
+        throw error;
+    }
 }
📝 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
@OnEvent(EVENTS.ENABLE_WAN_ACCESS, { async: true })
async enableWanAccess() {
this.configService.set('connect.config.wanaccess', true);
await this.networkService.reloadNetworkStack();
}
@OnEvent(EVENTS.ENABLE_WAN_ACCESS, { async: true })
async enableWanAccess() {
this.configService.set('connect.config.wanaccess', true);
try {
await this.networkService.reloadNetworkStack();
} catch (error) {
// Rollback config on failure to maintain consistency
this.configService.set('connect.config.wanaccess', false);
throw error;
}
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/event-handler/wan-access.handler.ts
around lines 20 to 24, the enableWanAccess method lacks error handling for the
asynchronous reloadNetworkStack call. To fix this, wrap the await
this.networkService.reloadNetworkStack() call in a try-catch block. In the catch
block, revert the configService setting for 'connect.config.wanaccess' to false
and log or handle the error appropriately to maintain system consistency.

Comment on lines +26 to +30
@OnEvent(EVENTS.DISABLE_WAN_ACCESS, { async: true })
async disableWanAccess() {
this.configService.set('connect.config.wanaccess', false);
await this.networkService.reloadNetworkStack();
}
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

Add error handling for network stack reload failures.

If reloadNetworkStack() fails, the config will show WAN access as disabled while the network stack may still have WAN access enabled, creating system inconsistency.

 @OnEvent(EVENTS.DISABLE_WAN_ACCESS, { async: true })
 async disableWanAccess() {
     this.configService.set('connect.config.wanaccess', false);
-    await this.networkService.reloadNetworkStack();
+    try {
+        await this.networkService.reloadNetworkStack();
+    } catch (error) {
+        // Rollback config on failure to maintain consistency
+        this.configService.set('connect.config.wanaccess', true);
+        throw error;
+    }
 }
📝 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
@OnEvent(EVENTS.DISABLE_WAN_ACCESS, { async: true })
async disableWanAccess() {
this.configService.set('connect.config.wanaccess', false);
await this.networkService.reloadNetworkStack();
}
@OnEvent(EVENTS.DISABLE_WAN_ACCESS, { async: true })
async disableWanAccess() {
this.configService.set('connect.config.wanaccess', false);
try {
await this.networkService.reloadNetworkStack();
} catch (error) {
// Rollback config on failure to maintain consistency
this.configService.set('connect.config.wanaccess', true);
throw error;
}
}
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/event-handler/wan-access.handler.ts
around lines 26 to 30, the disableWanAccess method lacks error handling for the
asynchronous reloadNetworkStack call. To fix this, wrap the await
this.networkService.reloadNetworkStack() call in a try-catch block. In the catch
block, log the error and consider reverting the configService.set call or
handling the failure to maintain system consistency between the config and
actual network state.

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.

1 participant