Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Apr 24, 2025

Summary by CodeRabbit

  • New Features
    • Added comprehensive activation code customization service with dynamic theming, partner branding, and UI updates.
    • Introduced new GraphQL types and public queries for activation code, partner info, and theme data.
    • Implemented new web UI stores and components for activation modal, partner logos, and theme management.
  • Improvements
    • Removed legacy activation code scripts, PHP components, and plugin references, streamlining activation logic.
    • Enhanced configuration and environment support for activation and theming features.
    • Improved error handling, validation, and type safety in activation and customization modules.
  • Bug Fixes
    • Fixed color code validation and path handling in customization service.
  • Chores
    • Added pre-commit linting hooks and related configuration.
    • Cleaned up test and development environment files.
  • Tests
    • Added extensive tests covering activation customization service initialization, data handling, and file modifications.
    • Removed obsolete tests related to legacy activation code store.
  • Refactor
    • Migrated activation and partner branding logic from legacy scripts and PHP to TypeScript services and GraphQL resolvers.
    • Reorganized store and component architecture for activation-related features.
  • Style
    • Updated UI components for improved branding, theming, accessibility, and layout consistency.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Apr 24, 2025

Walkthrough

This update introduces a comprehensive activation code and customization system for Unraid, spanning backend, frontend, and plugin layers. The backend adds new GraphQL types, queries, services, and modules for activation code handling, theming, and partner branding. File modification classes are introduced for injecting banners, logos, and modal dialogs into the Unraid web GUI. The frontend is refactored to use new Pinia stores and GraphQL queries for activation code and partner info, updating modal and logo display logic. Legacy Bash/PHP activation scripts and related plugin code are removed, consolidating customization logic within the API. Supporting configuration, environment, and test files are updated accordingly.

Changes

Files/Groups Change Summary
api/src/unraid-api/graph/resolvers/customization/*, api/src/unraid-api/graph/resolvers/customization/theme.model.ts Added Customization module, resolver, service, DTOs, and theme model for activation code and theming support.
api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts, case-model-copier.modification.ts, set-password-modal.modification.ts New file modification classes for copying partner logo, case model, and injecting set-password modal.
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts, patches/default-page-layout.patch Updated to remove Unraid logo, add transformers, and patch HTML for branding changes.
api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts, log-viewer.modification.ts Changed class exports from named to default.
api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts, patches/auth-request.patch, __test__/snapshots/auth-request.php.modified.snapshot.php Updated logo path in whitelist and patch logic for dynamic asset path.
api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts Switched to dynamic import for modifications using import.meta.glob.
api/src/unraid-api/unraid-file-modifier/modifications/__test__/* Minor test fixture changes, newlines and snapshot updates.
api/src/core/types/ini.ts Extended Display interface with theming and header color fields.
api/src/store/modules/paths.ts, api/src/utils.ts Added asset path helpers and new path constants for customization assets.
api/src/store/modules/emhttp.ts Introduced explicit types for state file payloads and thunk results.
api/src/core/utils/clients/emcmd.ts Added retry logic for CSRF token, improved error handling, and updated signature.
api/src/unraid-api/graph/resolvers/base.model.ts Added ACTIVATION_CODE to Resource enum.
api/generated-schema.graphql, web/composables/gql/graphql.ts, web/composables/gql/gql.ts Added GraphQL types, enums, and queries for activation code, customization, and theme.
api/src/unraid-api/auth/authentication.guard.ts, public.decorator.ts Added public endpoint decorator and guard logic for unauthenticated routes.
api/src/unraid-api/graph/resolvers/info/info.model.ts, graphql/resolvers/query/info.ts Updated theme field to use new ThemeName enum.
api/src/environment.ts Removed unused DRY_RUN export.
api/.env.development, api/.env.test Added activation and password path environment variables.
api/dev/activation/*, api/dev/dynamix/dynamix.cfg, api/dev/ident.cfg, api/dev/states/var.ini, api/dev/configs/connect.json Added/updated dev assets and configuration files for activation and theming.
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts Added DTOs for activation code and partner info with validation.
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts Added comprehensive tests for customization service logic.
web/components/Activation/ActivationPartnerLogo.vue, ActivationPartnerLogoImg.vue, graphql/activationCode.query.ts, store/activationCodeData.ts, store/activationCodeModal.ts Added/updated components and Pinia stores for activation code and partner info.
web/components/Activation/ActivationModal.vue, WelcomeModal.ce.vue Refactored to use new stores, updated modal and logo logic, removed Konami code handler.
web/components/HeaderOsVersion.ce.vue Updated to use partner info for logo link, changed layout.
web/layouts/default.vue, web/pages/welcome.vue, web/pages/index.vue Integrated modal components and debug panels, removed legacy logo.
web/store/theme.ts Added async theme loading from GraphQL, exposed CSS var setter.
web/store/purchase.ts, web/__test__/store/purchase.test.ts Included activation code data in purchase payload and tests.
web/store/server.ts, web/types/server.ts Updated to use new activation code data store and types.
web/_data/serverState.ts Removed legacy activation code data from dummy server state.
web/helpers/create-apollo-client.ts Removed conditional client link logic.
web/components/DummyServerSwitcher.vue Simplified component logic, removed unused types and handlers.
web/components/Activation/PartnerLogo.vue, web/store/activationCode.ts Deleted legacy activation code and partner logo components/stores.
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/scripts/activation_code_setup, activation_code_remove, include/activation-code-extractor.php, include/partner-logo.php Deleted legacy Bash/PHP activation setup and extraction scripts.
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php Removed activation code detection logic from server state.
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/welcome-modal.php Removed server state injection into welcome modal.
plugin/plugins/dynamix.unraid.net.plg Removed activation/partner setup script calls from plugin install/remove.
.cursor/rules/api-rules.mdc, .cursor/rules/default.mdc Updated file glob patterns and added comment rule.
package.json, .husky/_/pre-commit Added git hooks and lint-staged config for pre-commit linting.
unraid-ui/src/components/common/stepper/StepperItem.vue Adjusted disabled opacity for stepper item.
api/src/customization/activation-code.dto.ts Added empty placeholder file.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebUI
    participant API
    participant CustomizationService
    participant FileModifier
    participant FS

    User->>WebUI: Loads page
    WebUI->>API: GraphQL query (customization, partner info, theme)
    API->>CustomizationService: Fetch activation code and partner info
    CustomizationService->>FS: Read activation JSON, assets, config
    CustomizationService-->>API: Return activation code, partner info, theme
    API-->>WebUI: Respond with customization data
    WebUI->>WebUI: Update modal, logo, theme using Pinia stores
    User->>WebUI: Interacts with activation modal
Loading

Possibly related PRs

  • unraid/api#973: Refactor and replacement of legacy activation code handling scripts with a new NestJS-based CustomizationService and GraphQL resolvers, directly related at feature and code level.
  • unraid/api#1352: Adds environment variables and modular configuration management infrastructure, related in establishing modular config system.
  • unraid/api#1288: Modifies file modifier classes related to OEM/activation code customizations, touching similar files but different aspects.

Suggested reviewers

  • zackspear
  • pujitm

Poem

In Unraid's halls, a banner flies,
With partner logos, colors rise.
Activation codes now lead the way,
Theming shines both night and day.
Old scripts retire, new stores awake—
Customization’s here, make no mistake!
🖼️✨


🪧 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.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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 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: 5

🔭 Outside diff range comments (4)
api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (2)

20-23: ⚠️ Potential issue

Escape literal dots in JWT regex

The . characters in the regex currently match any character, so malformed tokens such as abcXdefYghi would still pass the format check. Escape the dots to ensure you only allow the JWT’s three-part structure separated by literal periods.

-        if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
+        if (!preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $password)) {

25-30: 🛠️ Refactor suggestion

Passing the token via the command line leaks secrets

exec("/etc/rc.d/rc.unraid-api sso validate-token $safePassword …") puts the full JWT in the process list, making it visible via ps to any local user. Instead, pipe the token on stdin or use an environment variable that is cleared immediately after use.

Example sketch:

$process = proc_open(
    '/etc/rc.d/rc.unraid-api sso validate-token -',
    [['pipe','r'], ['pipe','w'], ['pipe','w']],
    $pipes
);
fwrite($pipes[0], $password);
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
$code   = proc_close($process);
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (2)

15-18: ⚠️ Potential issue

Escape dots in JWT regex to avoid false positives

In the pattern
'/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/'
the dot (.) matches any character, so an attacker could smuggle arbitrary content between the token parts and still pass the check.

-        if (!preg_match('/^[A-Za-z0-9-_]+.[A-Za-z0-9-_]+.[A-Za-z0-9-_]+$/', $password)) {
+        if (!preg_match('/^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$/', $password)) {

Escaping the dots limits matches to the canonical header.payload.signature structure.


15-16: 🛠️ Refactor suggestion

Hard-coding “> 800” may reject valid tokens

Most JWTs are ≤ 1 kB and many are ~300 B. Relying on strlen($password) > 800 as the sole indicator of an SSO token risks false negatives (shorter but valid tokens) and fragile behaviour if token sizes change. Consider detecting tokens exclusively via the regex or a dedicated Authorization: prefix.

🧹 Nitpick comments (10)
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1)

5-10: Consider typing and enhancing the sanitizeString function

The sanitization function has correct logic for removing potentially harmful characters, but it could benefit from more specific typing.

-const sanitizeString = (value: any): any => {
+const sanitizeString = (value: unknown): string | unknown => {
    if (typeof value === 'string') {
        return value.replace(/[\\"']/g, ''); // Remove backslashes, double quotes, and single quotes
    }
    return value;
};
api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts (1)

16-21: Accessing the Redux store inside the constructor may race with boot-strapping

If CaseModelCopierModification is instantiated before the paths slice is populated, paths.activationBase will be undefined, causing path.join(undefined, 'assets') to throw. Lazily read the store in apply() or inject the PathsService to guarantee initialization.

-    constructor() {
-        const paths = store.getState().paths;
-        this.assetsDir = path.join(paths.activationBase, 'assets');
-        this.webguiImagesDir = paths.webguiImagesBase;
-        this.logger.debug('CaseModelCopierModification initialized with paths from store.');
-    }
+    constructor() {
+        this.logger.debug('CaseModelCopierModification constructed.');
+    }
+
+    private initPaths() {
+        const paths = store.getState().paths;
+        this.assetsDir       ??= path.join(paths.activationBase, 'assets');
+        this.webguiImagesDir ??= paths.webguiImagesBase;
+    }
api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts (1)

36-38: Specify the symlink type for Windows compatibility

fs.symlink() defaults to “file” on POSIX but requires an explicit 'file' type on Windows; otherwise the call may fail under NTFS. Adding the third argument keeps the code portable.

-                await fs.symlink(partnerLogo, linkDest);
+                await fs.symlink(partnerLogo, linkDest, 'file');
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (1)

498-501: Declare cookieEnabled with let/const

Implicit globals hinder maintainability and can collide with other scripts:

-        cookieEnabled = document.cookie.indexOf("cookietest=")!=-1;
+        const cookieEnabled = document.cookie.indexOf("cookietest=") !== -1;
api/src/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.ts (3)

13-18: Use overridePath consistently when reading the file

generatePatch() honours overridePath only when creating the diff, but still reads this.filePath. Reading the override first prevents mismatched content in test scenarios.

-        const fileContent = await readFile(this.filePath, 'utf-8');
+        const sourcePath = overridePath ?? this.filePath;
+        const fileContent = await readFile(sourcePath, 'utf-8');

21-36: Gracefully handle missing target file

readFile() will reject if the file is absent (e.g. older Unraid versions), bubbling an unhandled promise rejection. Wrap with try/catch and return shouldApply: false plus the error reason to keep the modifier service resilient.


38-47: Body-tag search is case-sensitive

</body> is matched literally; many legacy Unraid files use </BODY>. A case-insensitive search (/i flag or toLowerCase()) would make the injection more robust.

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

33-36: Add alt-text when replacing the logo for accessibility

The original <a> element rendered an inline SVG (self-describing). The new include outputs an <img> tag without guaranteed alt text. Ensure partner-logo.php provides meaningful alt so screen-reader users aren’t left in the dark.

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

1-29: New service for handling activation-based customizations.

A well-structured service class has been created to manage Unraid system customizations based on activation data. The class properly uses dependency injection and logging.

However, there's an unnecessary empty constructor that can be removed:

-constructor() {}
🧰 Tools
🪛 Biome (1.9.4)

[error] 29-29: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)


72-81: Formatting issue in log message.

The log message exceeds the line length limits according to static analysis.

-                    this.logger.log(`Activation directory ${this.activationDir} not found. Skipping activation setup.`);
+                    this.logger.log(
+                        `Activation directory ${this.activationDir} not found. Skipping activation setup.`
+                    );
🧰 Tools
🪛 GitHub Check: Build API

[failure] 77-77:
Replace Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup. with ⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················

🪛 GitHub Check: Test API

[failure] 77-77:
Replace Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup. with ⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c5a394e and 843aed0.

📒 Files selected for processing (26)
  • .cursor/rules/api-rules.mdc (1 hunks)
  • api/dev/Unraid.net/myservers.cfg (1 hunks)
  • api/dev/ident.cfg (1 hunks)
  • api/src/__test__/store/modules/paths.test.ts (2 hunks)
  • api/src/customization/activation-code.dto.ts (1 hunks)
  • api/src/store/modules/paths.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (14 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 (1 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__/generic-modification.spec.ts (2 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (14 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (2 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/log-viewer.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (4 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (3)
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch is used to remove the old jGrowl notification system from Unraid pages, as notifications are handled by a new system implemented on a different page.
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:6-20
Timestamp: 2025-01-31T22:01:41.842Z
Learning: The default-page-layout.patch removes the old jGrowl notification system and is complemented by the unraid-toaster component implementation. The new system is added through the DefaultPageLayout modification which inserts the toaster component with proper position configuration based on user preferences.
Learnt from: elibosley
PR: unraid/api#1101
File: api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch:30-54
Timestamp: 2025-01-31T22:01:02.725Z
Learning: The removal of jGrowl notifications from DefaultPageLayout.php is intentional as notifications are now handled on a separate page as part of the architectural design to override existing Unraid pages.
🧬 Code Graph Analysis (5)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts (1)
api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts (1)
  • SSOFileModification (8-98)
api/src/unraid-api/unraid-file-modifier/modifications/set-password-modal.modification.ts (1)
api/src/unraid-api/unraid-file-modifier/file-modification.ts (1)
  • ShouldApplyWithReason (8-11)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (2)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (6)
  • readFromFile (71-83)
  • appendToFile (85-94)
  • writeToFile (96-105)
  • cleanupFails (115-139)
  • isValidTimeStamp (108-113)
  • verifyUsernamePassword (141-153)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php (1)
  • _ (108-108)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/.login.php.modified.snapshot.php (2)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (6)
  • readFromFile (20-32)
  • appendToFile (34-43)
  • writeToFile (45-54)
  • cleanupFails (64-88)
  • isValidTimeStamp (57-62)
  • verifyUsernamePassword (90-102)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php (1)
  • _ (108-108)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)
api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1)
  • ActivationCodeDto (12-76)
packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
  • fileExists (5-8)
api/src/store/index.ts (1)
  • getters (18-29)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (12-41)
🪛 GitHub Check: Build API
api/src/customization/activation-code.dto.ts

[failure] 1-1:
Delete ·


[failure] 1-1:
Too many blank lines at the beginning of file. Max of 0 allowed


[failure] 1-1:
Newline required at end of file but not found

api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts

[failure] 4-4:
Delete


[failure] 4-4:
More than 1 blank line not allowed

api/src/store/modules/paths.ts

[failure] 22-22:
Replace 'dynamixCaseModelConfig' with dynamixCaseModelConfig


[failure] 78-78:
Replace 'activationBase' with activationBase


[failure] 79-79:
Replace 'webguiImagesBase' with webguiImagesBase


[failure] 80-80:
Replace 'identConfig' with identConfig

api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[failure] 77-77:
Replace Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup. with ⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················

🪛 GitHub Check: Test API
api/src/customization/activation-code.dto.ts

[failure] 1-1:
Delete ·


[failure] 1-1:
Too many blank lines at the beginning of file. Max of 0 allowed


[failure] 1-1:
Newline required at end of file but not found

api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts

[failure] 4-4:
Delete


[failure] 4-4:
More than 1 blank line not allowed

api/src/store/modules/paths.ts

[failure] 22-22:
Replace 'dynamixCaseModelConfig' with dynamixCaseModelConfig


[failure] 78-78:
Replace 'activationBase' with activationBase


[failure] 79-79:
Replace 'webguiImagesBase' with webguiImagesBase


[failure] 80-80:
Replace 'identConfig' with identConfig

api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[failure] 77-77:
Replace Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup. with ⏎························Activation·directory·${this.activationDir}·not·found.·Skipping·activation·setup.⏎····················

🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[error] 29-29: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (41)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/auth-request.php.last-download-time (1)

1-1: Approve fixture timestamp update.
The timestamp was bumped to reflect the latest download snapshot. No action needed.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/Notifications.page.last-download-time (1)

1-1: Approve fixture timestamp update.
Updated notification fixture timestamp to match the new UI changes. All good.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php.last-download-time (1)

1-1: Approve fixture timestamp update.
The DefaultPageLayout fixture timestamp is updated to align with UI customization tests. Looks good.

api/dev/Unraid.net/myservers.cfg (1)

2-2: Verify version change.
[api].version was downgraded from "4.6.6" to "4.4.1". Please confirm this is intentional and compatible with downstream systems.

api/src/unraid-api/unraid-file-modifier/modifications/log-rotate.modification.ts (1)

10-10: Export style changed to default export

The change from named export to default export aligns with the broader refactoring mentioned in the summary, where modification classes are now dynamically imported using import.meta.glob.

api/dev/ident.cfg (1)

1-36: Configuration file looks good

This new configuration file contains appropriate default values for system and network settings. It will be used by the CustomizationService to modify identity parameters based on activation data.

api/src/__test__/store/modules/paths.test.ts (1)

19-36: Test updated to include new path keys

The snapshot test has been correctly updated to include the new path keys added to the store.

api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1)

12-76: Well-structured DTO with appropriate validations

The DTO is well-designed with proper validations for each field. The use of @IsOptional() for all fields makes sense for a configuration where not all fields may be provided. The string sanitization ensures that potentially harmful characters are removed.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php.last-download-time (1)

1-1: Timestamp updated for login.php fixture.

This timestamp update indicates the login.php test fixture was refreshed, likely reflecting the broader authentication system changes mentioned in the summary.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts (2)

11-11: Import style aligned with module export change.

The import statement has been updated to use a default import for LogRotateModification, reflecting changes in the export style of that class.


39-39: Property order adjusted for consistency.

The order of properties in the SSOFileModification test case has been adjusted for better readability, with the class definition now appearing before the file URL.

api/src/unraid-api/unraid-file-modifier/modifications/log-viewer.modification.ts (1)

10-10: Changed to default export for consistency.

The export has been changed from a named export to a default export to align with a broader pattern change in file modification classes. This supports the dynamic loading of modifications implemented in the unraid-file-modifier.service.ts file.

.cursor/rules/api-rules.mdc (2)

3-3: Expanded glob pattern to include nested directories.

The glob pattern has been broadened from api/* to api/**/*,api/* to include all files recursively within the api directory, ensuring new nested files are properly covered by these rules.


11-11: Added new guideline for dependency mocking.

Added a sensible guideline to prefer not mocking simple dependencies, which should lead to more robust tests.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/DefaultPageLayout.php (3)

695-695: Partner logo replacement implemented correctly

The static Unraid logo link has been replaced with a PHP include for a partner logo. This is a good implementation that enables dynamic partner branding.


1294-1299: Improved page refresh behavior for extended viewing

The visibility change handler now conditionally checks if $myPage['Load'] is set and reloads the page when necessary, rather than always calling nchanFocusStart(). This provides better control over page reloading behavior for extended viewing sessions.


1325-1325: Modern notification component added

A new web component <uui-toaster> has been added to replace the legacy jGrowl notification system. This component properly inherits the notification position configuration from the existing settings.

api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (1)

39-49: Improved modification loading with dynamic imports

The implementation now uses import.meta.glob to dynamically load all modification files, making the codebase more maintainable and extendable. This follows the open/closed principle - new modifications can be added without changing this service.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1)

695-695: Snapshot reflects partner logo replacement

The snapshot correctly reflects the partner logo PHP include replacement, ensuring tests will pass with the new implementation.

api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (2)

35-45: Well-implemented partner logo injection method

The injectPartnerLogo method correctly handles the replacement of the Unraid logo with a partner logo from the dynamix.my.servers plugin. The method includes proper checks to prevent duplicate replacements.


52-52: Transformation pipeline properly updated

The injectPartnerLogo method has been correctly added to the transformation pipeline in the applyToSource method.

api/src/unraid-api/unraid-file-modifier/modifications/patches/sso.patch (1)

20-21: Re-evaluate the 800-character threshold

Hard-coding strlen($password) > 800 risks rejecting valid (shorter) JWTs and accepting junk that simply exceeds the limit. Consider dropping the length gate altogether or using a far smaller sanity check (e.g. > 200) informed by real-world token sizes.

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

92-93: Verify $notify['position'] is always defined

<uui-toaster … position="<?= ($notify['position'] === 'center') ? 'top-center' : $notify['position'] ?>">

If $notify isn’t in scope at this point, PHP will emit a notice and default to null, producing an empty attribute. Please confirm the variable is set earlier or guard with isset($notify['position']) ? … : 'top-center'.


70-78: Residual JS may still reference the removed bell icon

The patch removes the #bell element and part of its update logic, but other scripts (e.g. openNotifier()) might still touch #bell or .nav-user. Scan the page scripts to ensure all references are gone to prevent runtime errors.

api/src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded/.login.php (8)

5-10: Improved session management logic.

The session initialization code has been refactored with proper formatting and explicit checks, making it more robust when verifying if an existing session is present.


20-54: Improved file handling functions with type hints.

The file utility functions have been enhanced with proper PHP return type hints and consistent formatting, making the code more maintainable and providing better IDE support.


64-88: Enhanced cleanupFails function with better formatting.

The function has been improved with consistent formatting while maintaining its original logic for cleaning up failed login attempts.


114-116: Simplified credential input fields.

The input fields have been simplified to only capture username and password, aligning with the removal of 2FA functionality.


125-135: Simplified login verification flow.

The login verification now only checks username and password credentials, with improved error handling and formatting.


427-438: Updated login form markup.

The login form has been simplified to remove 2FA-related elements while improving formatting and error message display.


446-459: Added cookie validation checks.

New JavaScript code has been added to verify that cookies are enabled in the browser, with a clear error message for users when cookies are disabled.


90-102:

Details

❌ Incorrect review comment

Security impact: Two-factor authentication has been removed.

The authentication function has been simplified to only verify username and password, removing the previous two-factor authentication capability. This is a significant change to the security model.


🏁 Script executed:

#!/bin/bash
# Check if there are any references to two-factor authentication in the codebase
# that might be affected by this change

echo "Checking for 2FA references in the codebase..."
rg -i "two-?factor|2fa|totp|authenticator" --type php

echo "Checking for SSO implementation that might replace 2FA..."
rg -i "sso|single sign-?on" --type php

Length of output: 4264


Test fixture only – no impact on production 2FA

The file you’re looking at lives under modifications/__test__/__fixtures__/downloaded/ and represents the raw, downloaded login script used in tests. Removing two-factor logic here does not alter any production authentication code or security model. You can safely ignore this fixture change—production 2FA remains untouched.

Likely an incorrect or invalid review comment.

api/src/unraid-api/graph/resolvers/customization/customization.service.ts (9)

31-44: Well-implemented first boot setup check.

The method creates or checks for a flag file to determine if first boot setup has already run, with proper error handling and logging.


46-67: Path initialization with proper error handling.

The service properly initializes path properties with dynamic imports and includes error checking for critical paths.


103-141: Well-implemented activation data retrieval and validation.

The service properly retrieves and validates activation data using class-transformer and class-validator, with comprehensive error handling.


143-176: Comprehensive customization application workflow.

The method orchestrates the application of all customizations with proper error handling and logging.


178-201: Partner banner setup with proper file handling.

The method correctly copies the partner banner file with appropriate existence checks and error handling.


203-246: Display settings application using Redux store data.

The service properly retrieves current settings from the Redux store and applies new settings from activation data.


248-300: Case model configuration with proper error handling.

The method properly sets up a custom case model based on activation data, with appropriate file existence checks.


302-351: Server identity updates with proper API integration.

The service updates server identity settings and properly triggers system updates using the emcmd utility.


354-407: Generic INI configuration file update helper.

A well-implemented helper method for updating INI configuration files, with proper handling of missing files, section creation, and value updates.

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

🧹 Nitpick comments (10)
api/src/__test__/store/modules/paths.test.ts (1)

7-8: Trim the redundant key-snapshot

Keeping both a full-key snapshot (Object.keys(paths) => toMatchSnapshot()) and the explicit toMatchObject assertion below doubles the maintenance load without adding safety.
If a new key is introduced you will already fail the toMatchObject block (or have to update it intentionally). Consider dropping the first snapshot to reduce future churn.

-    expect(Object.keys(paths)).toMatchSnapshot();
api/src/unraid-api/auth/authentication.guard.ts (1)

74-81: Guard short-circuit is correct but consider typing

getAllAndOverride<boolean> can return undefined; comparing with true is a tad clearer and prevents accidental truthy values:

-        if (isPublic) {
+        if (isPublic === true) {
             return true;
         }
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (2)

1-8: Remove unused imports

Field, ObjectType and GraphQLError are imported but never referenced, which will trigger the linter and bloats bundle size.

-import { Field, ObjectType, Query, Resolver } from '@nestjs/graphql';
-import { GraphQLError } from 'graphql';
+import { Query, Resolver } from '@nestjs/graphql';

46-71: getPartnerInfo – tighten boolean computation & double I/O

  1. hasPartnerLogo is given a path but later treated as a boolean. Rename to logoPath (or cast with Boolean(...)) for clarity.

  2. customizationService.getPartnerLogoWebguiPath() is executed in both getActivationData and here. If the service hits the filesystem each time, you incur two reads per request. Either memoise inside the service (preferred) or reuse the earlier result.

Example patch:

-        const hasPartnerLogo = await this.customizationService.getPartnerLogoWebguiPath(); // Returns boolean
+        const logoPath = await this.customizationService.getPartnerLogoWebguiPath();
return {
    hasPartnerLogo: Boolean(logoPath),
    partnerName: activationData.partnerName,
};
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)

637-642: Snapshot sprawl

Snapshot-testing the entire file content after every INI manipulation will quickly balloon the __snapshots__ directory and make diffs noisy. Prefer explicit assertions on the critical keys or use toMatchInlineSnapshot() with a trimmed expectation.

api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (2)

9-14: Basic string sanitization

The sanitization removes potentially dangerous characters (backslashes and quotes) which helps prevent basic injection attacks. For added security, consider using a more comprehensive sanitization library for user inputs.


17-30: Hex color normalization and validation

The function handles both validation and normalization well by adding the # prefix when needed. However, it silently returns an empty string for invalid colors rather than throwing an error or returning null.

Consider making the invalid case handling more explicit:

- return ''; // Return empty string if not a valid hex color after potential modification
+ return ''; // Return empty string if not a valid hex color (could consider returning null to be more explicit)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)

28-28: Remove unnecessary constructor

The empty constructor doesn't add any value and can be removed.

- constructor() {}
🧰 Tools
🪛 Biome (1.9.4)

[error] 28-28: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)


45-99: Comprehensive module initialization

The onModuleInit method handles path initialization, directory checking, and conditional application of customizations with robust error handling.

Add path validation for caseModelCfg and identCfg similar to how you check configFile:

this.caseModelCfg = paths.dynamixCaseModelConfig;
+ if (!this.caseModelCfg) {
+   this.logger.warn("Could not resolve case model config path from store.");
+ }
this.identCfg = paths.identConfig;
+ if (!this.identCfg) {
+   this.logger.warn("Could not resolve identity config path from store.");
+ }

382-436: Flexible configuration file update utility

The updateCfgFile method provides a flexible way to update INI-style configuration files, with good error handling and section-level updates.

For additional robustness when writing config files, consider creating parent directories if they don't exist:

// Write the updated content back to the file
+ const dirPath = path.dirname(filePath);
+ await fs.mkdir(dirPath, { recursive: true });
await fs.writeFile(filePath, newContent + '\n'); // Ensure trailing newline
📜 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 843aed0 and 6684f88.

⛔ Files ignored due to path filters (4)
  • api/dev/activation/assets/case-model.png is excluded by !**/*.png
  • api/dev/activation/assets/logo.svg is excluded by !**/*.svg
  • api/src/__test__/store/modules/__snapshots__/paths.test.ts.snap is excluded by !**/*.snap
  • api/src/unraid-api/graph/resolvers/customization/__snapshots__/customization.service.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (21)
  • .cursor/rules/api-rules.mdc (1 hunks)
  • .cursor/rules/default.mdc (1 hunks)
  • api/.env.development (1 hunks)
  • api/.env.test (1 hunks)
  • api/dev/activation/.done (1 hunks)
  • api/dev/activation/activation_code_12345.activationcode (1 hunks)
  • api/generated-schema.graphql (4 hunks)
  • api/src/__test__/store/modules/paths.test.ts (1 hunks)
  • api/src/store/modules/paths.ts (2 hunks)
  • api/src/unraid-api/auth/authentication.guard.ts (4 hunks)
  • api/src/unraid-api/auth/public.decorator.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/base.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts (2 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts (1 hunks)
✅ Files skipped from review due to trivial changes (11)
  • .cursor/rules/default.mdc
  • api/src/unraid-api/graph/resolvers/docker/docker.model.ts
  • api/dev/activation/.done
  • api/.env.test
  • api/src/unraid-api/auth/public.decorator.ts
  • api/src/unraid-api/graph/resolvers/resolvers.module.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/.env.development
  • api/dev/activation/activation_code_12345.activationcode
  • api/src/unraid-api/graph/resolvers/base.model.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • .cursor/rules/api-rules.mdc
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
  • api/src/store/modules/paths.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
api/src/unraid-api/auth/authentication.guard.ts (1)
api/src/unraid-api/auth/public.decorator.ts (1)
  • IS_PUBLIC_KEY (3-3)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (5)
api/src/unraid-api/graph/directives/use-permissions.directive.ts (3)
  • UsePermissions (69-85)
  • AuthActionVerb (14-14)
  • AuthPossession (14-14)
api/src/unraid-api/auth/public.decorator.ts (1)
  • Public (4-4)
api/src/store/modules/paths.ts (1)
  • paths (112-116)
api/src/store/index.ts (1)
  • store (6-12)
packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
  • fileExists (5-8)
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[error] 28-28: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

⏰ 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 (18)
api/src/__test__/store/modules/paths.test.ts (1)

21-30: Use stringContaining for every dynamically-resolved dev path

'myservers-keepalive' is asserted against the literal string ./dev/Unraid.net/fb_keepalive, while most siblings use expect.stringContaining(...).
Should the base directory ever change (e.g., api/dev vs ./dev), this single hard-coded check will break. Align it with the other flexible assertions:

-        'myservers-keepalive': './dev/Unraid.net/fb_keepalive',
+        'myservers-keepalive': expect.stringContaining('Unraid.net/fb_keepalive'),
api/src/unraid-api/auth/authentication.guard.ts (1)

44-46: Constructor injection looks good

Injecting Reflector and calling super() is the recommended NestJS pattern; no issues spotted here.

api/generated-schema.graphql (4)

759-759: Appropriate addition of ACTIVATION_CODE to Resource enum

The new ACTIVATION_CODE resource type has been correctly added to the Resource enum, which will enable permission-based access control for activation code features.


1209-1209: Good addition of specification reference

Adding the @specifiedBy directive to the JSONObject scalar improves documentation by providing a reference to the official ECMA-404 specification.


1392-1424: Well-structured DTOs for activation code features

The three new object types (PublicPartnerInfoDto, ActivationCodeDto, and Customization) are properly defined with appropriate fields, types, and descriptions. The structure adequately supports the activation code and partner branding features.


1457-1458: Appropriate query endpoints for activation data

The two new query fields getActivationData and partnerInfo provide logical access points for retrieving customization data and partner information, respectively.

api/src/unraid-api/graph/resolvers/customization/activation-code.dto.ts (4)

7-7: Simple but effective hex color validation

This helper function correctly validates standard hex color formats with the # prefix.


32-40: Clean DTO for partner info

This DTO is correctly structured with appropriate field types and nullability settings.


42-103: Well-organized ActivationCodeDto with validation

The DTO implements thorough validation and sanitization for all fields. The use of class-validator decorators and transformation functions ensures that data is properly validated and cleansed.

The color fields correctly use the sanitization helper, and the theme/banner fields have appropriate enum validation.


105-129: Well-structured Customization class

Good organization with clear field descriptions and appropriate validation. The relative path approach for assets is a clean solution.

api/src/unraid-api/graph/resolvers/customization/customization.service.ts (8)

30-43: Good implementation of boot setup flag mechanism

The method correctly creates and checks for the first boot setup flag, with appropriate logging and error handling.


101-119: Effective activation JSON file detection

The method properly handles directory access and file finding with good error handling.


121-141: Well-implemented activation data retrieval

The method correctly retrieves, parses, and validates the activation data, with proper error handling and documentation.


159-192: Comprehensive customization application orchestration

The method cleanly coordinates the application of all customization types with appropriate checks and error handling.


194-218: Partner banner setup with safe file operations

The method correctly handles file existence checks and copying with appropriate error handling and logging.


220-276: Display settings application with color formatting

The method correctly handles display settings application, including proper color format conversion (removing # from hex colors as required by the config).


278-329: Case model configuration with fallback options

The method intelligently handles different sources for the case model, with appropriate file checks and fallback options.


331-380: Server identity application with emcmd integration

The method correctly updates both the config file and triggers the emcmd utility to apply changes. Good state retrieval from the store and parameter preparation.

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 (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)

36-46: Mocking store doesn't override store.getState()

The mock implementation for the store doesn't correctly replace the getState() method, which means the tests could be accessing the real store state instead of the mock paths. This issue was previously identified in a review comment.

vi.mock('@app/store/index.js', async () => {
    const actual = await vi.importActual('@app/store/index.js');
    return {
        ...actual,
-        getters: {
-            paths: vi.fn(() => mockPaths),
-            dynamix: vi.fn(() => mockDynamixState),
-            emhttp: vi.fn(() => mockEmhttpState),
-        },
+        store: {
+            getState: () => ({ 
+                paths: mockPaths,
+                dynamix: mockDynamixState,
+                emhttp: mockEmhttpState 
+            }),
+        },
    };
});

132-141: should initialize paths from store is a false positive

The test succeeds even if onModuleInit() fails to read from the store because you're manually assigning properties before the test runs.


121-126: 🛠️ Refactor suggestion

Remove manual property assignment after service initialization

After fixing the store mock to properly override getState(), you should be able to remove these manual property assignments. The service should initialize these values from the mocked store during onModuleInit().

// Remove these lines after fixing the store mock
- (service as any).activationDir = activationDir;
- (service as any).hasRunFirstBootSetup = doneFlag;
- (service as any).configFile = userDynamixCfg;
- (service as any).caseModelCfg = caseModelCfg;
- (service as any).identCfg = identCfg;
🧹 Nitpick comments (7)
web/components/Activation/store/activationCodeData.ts (2)

4-4: Verify import path case consistency

The import path uses lowercase 'activationcode.query' which is inconsistent with typical camelCase naming conventions. This might cause issues on case-sensitive file systems.

-import { ACTIVATION_CODE_QUERY } from '~/components/Activation/graphql/activationcode.query';
+import { ACTIVATION_CODE_QUERY } from '~/components/Activation/graphql/activationCode.query';

17-18: Remove extra empty line

There's an unnecessary empty line that can be removed for better code style.

  return {
    activationCode,
    isFreshInstall,
-    
  };
web/components/Activation/ActivationModal.vue (1)

14-14: Verify import path case consistency

The import path uses lowercase 'activationcode.query' which is inconsistent with typical camelCase naming conventions.

-import { PARTNER_INFO_QUERY } from '~/components/Activation/graphql/activationcode.query';
+import { PARTNER_INFO_QUERY } from '~/components/Activation/graphql/activationCode.query';
web/components/Activation/WelcomeModal.ce.vue (1)

12-19: Consider moving the GraphQL query to a separate file

The GraphQL query is defined inline here but imported from a separate file in other components. Consider moving this to a shared query file for consistency.

Consider moving this query to ~/components/Activation/graphql/activationCode.query.ts and importing it like other components do:

-const PARTNER_INFO_QUERY = gql`
-  query PartnerInfo {
-    partnerInfo {
-      hasPartnerLogo
-      partnerName
-    }
-  }
-`;
+import { PARTNER_INFO_QUERY } from '~/components/Activation/graphql/activationCode.query';
web/components/Activation/store/activationCodeModal.ts (1)

35-63: Consider moving sequenceIndex inside the store function

The sequenceIndex variable is defined outside the store function, which could potentially cause issues if multiple store instances are created.

  /**
   * Listen for konami code sequence to close the modal
   */
  const keySequence = [
    'ArrowUp',
    'ArrowUp',
    'ArrowDown',
    'ArrowDown',
    'ArrowLeft',
    'ArrowRight',
    'ArrowLeft',
    'ArrowRight',
    'b',
    'a',
  ];
- let sequenceIndex = 0;
+ const sequenceIndex = ref(0);

  const handleKeydown = (event: KeyboardEvent) => {
    if (event.key === keySequence[sequenceIndex]) {
-     sequenceIndex++;
+     sequenceIndex.value++;
    } else {
-     sequenceIndex = 0;
+     sequenceIndex.value = 0;
    }

-   if (sequenceIndex === keySequence.length) {
+   if (sequenceIndex.value === keySequence.length) {
      setActivationModalHidden(true);
      window.location.href = '/Tools/Registration';
    }
  };
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)

57-62: Consider adding comment about authentication dependency

The resolver method for partnerInfo relies on the parent query's authentication. A comment noting this relationship would help future maintainers understand why there's no explicit @Public() decorator here compared to the standalone query.

@ResolveField(() => PublicPartnerInfo, { nullable: true, name: 'partnerInfo' })
// No @Public() decorator here - relies on parent query authentication
+// This resolver is accessed through the authenticated `customization` query
+// and inherits its authentication context, so no @Public() decorator is needed
async resolvePartnerInfo(): Promise<PublicPartnerInfo | null> {
    return this._getPublicPartnerInfoInternal();
}
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)

222-243: Implement the skipped test for error handling

The skipped test for error handling during activation setup should be implemented to ensure proper error handling coverage.

Consider implementing this test by:

  1. Setting up mocks to simulate a specific error case
  2. Asserting on the expected error logs and behavior
  3. Verifying that subsequent steps are still attempted

Remove the .skip once implemented.

📜 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 6684f88 and 3f71df9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • web/__test__/store/activationCode.test.ts (1 hunks)
  • web/components/Activation/ActivationModal.vue (2 hunks)
  • web/components/Activation/ActivationPartnerLogo.vue (1 hunks)
  • web/components/Activation/ActivationPartnerLogoImg.vue (1 hunks)
  • web/components/Activation/WelcomeModal.ce.vue (4 hunks)
  • web/components/Activation/graphql/activationCode.query.ts (1 hunks)
  • web/components/Activation/store/activationCodeData.ts (1 hunks)
  • web/components/Activation/store/activationCodeModal.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (17 hunks)
  • web/store/activationCode.ts (0 hunks)
  • web/store/server.ts (1 hunks)
  • web/types/server.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • web/store/activationCode.ts
✅ Files skipped from review due to trivial changes (2)
  • web/types/server.ts
  • web/components/Activation/graphql/activationCode.query.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/generated-schema.graphql
🧰 Additional context used
🧠 Learnings (1)
web/components/Activation/store/activationCodeData.ts (4)
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.
Learnt from: zackspear
PR: unraid/api#0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.
🧬 Code Graph Analysis (6)
web/components/Activation/store/activationCodeData.ts (2)
web/components/Activation/graphql/activationCode.query.ts (1)
  • ACTIVATION_CODE_QUERY (12-32)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • activationCode (70-72)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (5)
web/composables/gql/graphql.ts (4)
  • Customization (447-454)
  • PublicPartnerInfo (1090-1095)
  • Query (1097-1133)
  • ActivationCode (46-58)
api/src/store/index.ts (1)
  • store (6-12)
packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
  • fileExists (5-8)
api/src/unraid-api/auth/public.decorator.ts (1)
  • Public (4-4)
api/src/unraid-api/graph/directives/use-permissions.directive.ts (3)
  • UsePermissions (69-85)
  • AuthActionVerb (14-14)
  • AuthPossession (14-14)
web/composables/gql/gql.ts (1)
api/src/graphql/generated/client/gql.ts (1)
  • graphql (54-56)
web/components/Activation/store/activationCodeModal.ts (3)
web/consts.ts (1)
  • ACTIVATION_CODE_MODAL_HIDDEN_STORAGE_KEY (1-1)
web/components/Activation/store/activationCodeData.ts (1)
  • useActivationCodeDataStore (9-19)
web/store/callbackActions.ts (1)
  • useCallbackActionsStore (26-184)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
  • PublicPartnerInfo (1090-1095)
  • ActivationCode (46-58)
  • Customization (447-454)
web/composables/gql/graphql.ts (2)
api/src/unraid-api/graph/directives/use-permissions.directive.ts (2)
  • AuthActionVerb (14-14)
  • AuthPossession (14-14)
web/types/server.ts (1)
  • Server (70-121)
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[error] 28-28: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (37)
web/components/Activation/ActivationPartnerLogoImg.vue (1)

8-8: Import path updated to match new modular store structure.

The import path has been updated to use the new dedicated activation code modal store location, which aligns with the broader architectural changes where activation code functionality has been modularized.

web/store/server.ts (2)

48-48: Import path updated to match new modular store structure.

The import path has been updated to use the new dedicated activation code modal store location, which aligns with the broader architectural changes where activation code functionality has been modularized.


1116-1119:

Details

❌ Incorrect review comment

Code still references activation code store but not activationCodeData.

The setServer function still attempts to set activation code data using the old pattern, even though according to the AI summary, this data structure has been refactored. Consider whether this activation code data setting logic is still needed or should be updated to use the new store structure.


🏁 Script executed:

#!/bin/bash
# Verify if activationCodeData is still being set elsewhere in the codebase
rg "activationCodeData" --type ts --type vue

Length of output: 72


Let’s rerun the search without restricting file types to ensure we catch any occurrences:


🏁 Script executed:

#!/bin/bash
# Search for all references to activationCodeData across the repo
rg "activationCodeData" -n .

Length of output: 1752


Let’s check where useActivationCodeStore comes from and inspect the surrounding code in web/store/server.ts:


🏁 Script executed:

#!/bin/bash
# Locate any definitions or references to useActivationCodeStore
rg "useActivationCodeStore" -n .

# Show imports and top of server.ts to see where activationCodeStore is coming from
sed -n '1,50p' web/store/server.ts

# Show context around lines 1110–1130 in server.ts
sed -n '1100,1130p' web/store/server.ts

Length of output: 3697


Ignore outdated activation code refactor suggestion

The call to

useActivationCodeStore().setData(data.activationCodeData)

in web/store/server.ts is valid:

  • ActivationCodeData remains defined in web/types/server.ts
  • useActivationCodeStore (from activationCodeModal.ts) still exposes setData and delegates to the underlying activationCodeData store
  • tests in web/__test__/store/activationCode.test.ts cover this behavior

No changes are needed here.

Likely an incorrect or invalid review comment.

web/components/Activation/store/activationCodeData.ts (1)

9-19: The store implements clean reactive access to activation code data

The store correctly uses Apollo's useQuery to fetch data and exposes computed properties for activation code and fresh installation status. This is a good approach for making this data reactively available to components.

web/components/Activation/ActivationModal.vue (4)

3-3: Good refactoring to use GraphQL instead of store-based data

The imports have been updated to use Apollo's useQuery for data fetching and the new modal store for visibility management, which is a cleaner separation of concerns.

Also applies to: 11-14


22-22: Correctly migrated to the new modal store

The component now uses the dedicated activation code modal store, which follows better separation of concerns.


25-32: Good implementation of reactive partner data

The component now fetches partner info via GraphQL and handles loading/error states gracefully with fallback values, which is a more robust approach than the previous implementation.


70-70: Template correctly uses computed partner data

The template updates properly use the computed partner data properties for conditional rendering and prop passing.

Also applies to: 78-79

web/components/Activation/WelcomeModal.ce.vue (3)

21-31: Good error handling for the GraphQL query

The component properly handles the query execution and includes error logging, which is important for debugging.


33-34: Well-structured computed properties

The computed properties correctly extract and provide fallback values for partner data.


98-102: Good UX consideration with disabled button

Disabling the button while partner info is loading prevents user interaction with incomplete data.

web/components/Activation/store/activationCodeModal.ts (4)

1-11: Review module-level Pinia initialization

As noted in the activationCodeData.ts review, there are conflicting approaches to Pinia initialization in web component contexts. Verify this is the intended approach for your architecture.

Since this is the same pattern used in activationCodeData.ts, the same verification applies.


13-33: Good implementation of modal visibility logic

The store correctly implements the modal visibility logic based on multiple conditions with clear comments explaining the requirements. Using session storage for persistence is appropriate for this use case.


65-71: Good lifecycle management for event listeners

The event listeners are properly added on mount and removed on unmount, following best practices for resource management.


73-77: Clean API with minimal exports

The store exports only what's needed - the visibility state and a method to update it. This follows good encapsulation principles.

api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (4)

18-45: Well-structured resolver with clear separation of concerns

The CustomizationResolver class has a clean design, delegating complex operations to the CustomizationService. The empty object return in the customization() query with separate field resolvers is a good approach for GraphQL performance, allowing each field to be resolved only when requested.


47-55: Good use of @public() decorator for unauthenticated access

The publicPartnerInfo query properly implements a public endpoint with conditional access based on password presence. This allows for exposing minimal branding information without requiring authentication when appropriate.


64-72: Appropriate permission control for sensitive data

The activationCode field resolver correctly uses @UsePermissions to restrict access to activation code data based on user permissions. The resource type and action verb are clearly defined.


74-84: Well-designed public access for UI assets

Making the caseIcon and partnerLogo resolvers public allows the UI to display branding elements without requiring authentication, which is appropriate for these visual elements.

api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)

303-321: Good validation test for hex color transformation

This test effectively verifies that invalid hex colors are transformed to empty strings rather than causing validation failures. This helps ensure robustness in handling potentially malformed input data.


323-339: Comprehensive test for hex color normalization

The test for prepending '#' to valid hex colors ensures the transformer correctly normalizes different input formats. This is important for maintaining consistent color representation in the system.


358-589: Well-organized test suite with thorough edge case coverage

The customization methods tests cover a wide range of scenarios including:

  • Asset presence/absence
  • Error handling during file operations
  • Various combinations of customization data
  • Edge cases in hex color handling

This comprehensive coverage helps ensure the service behaves correctly across different inputs and system states.

api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (5)

7-14: Good input sanitization implementation

The helper function to sanitize strings removes potentially dangerous characters like backslashes and quotes, which helps prevent injection attacks and ensures data integrity.


16-30: Robust hex color validation and normalization

The sanitizeAndValidateHexColor function handles multiple formats (with or without '#') and provides graceful fallback to empty string for invalid inputs. This ensures consistent data representation and prevents validation errors.


32-40: PublicPartnerInfo has proper access control design

The PublicPartnerInfo class exposes only minimal information (partner name and logo flag) which is appropriate for public access. The @IsBoolean() validation ensures type safety.


42-103: Comprehensive validation for ActivationCode fields

The ActivationCode class properly:

  1. Validates and sanitizes all fields
  2. Uses @IsIn constraints for enum-like fields
  3. Provides sensible default for showBannerGradient
  4. Applies the custom hex color transformation to color fields

This ensures that only valid, sanitized data enters the system.


105-132: Well-designed Customization container class

The Customization class effectively aggregates related data (activation code, partner info, file paths) with clear field descriptions. The @Transform decorators ensure input sanitization for all string fields.

web/composables/gql/gql.ts (4)

16-18: Added PartnerInfo query to Documents type

This new entry correctly adds the PartnerInfo query to the Documents type, allowing type-safe access to partner information.


44-46: Added PartnerInfo to documents object

The query document is properly added to the documents object for runtime usage.


87-90: New graphql function overload for PartnerInfo query

The function overload provides type-safe access to the PartnerInfo query, ensuring proper type checking at compile time.


17-18: Complete and correct integration of ActivationCode query

The ActivationCode query is properly integrated into:

  1. The Documents type
  2. The documents object
  3. The graphql function overloads

This provides complete type safety for accessing activation code data from the GraphQL API.

Also applies to: 45-46, 91-94

api/src/unraid-api/graph/resolvers/customization/customization.service.ts (5)

28-29: Remove the no-op constructor

The class does not declare any dependencies and the body is empty, so the constructor can be safely removed to satisfy noUselessConstructor linting and keep the class lean.
[ suggest_nitpick ]

-    constructor() {}
+
+    // No constructor needed – Nest will create an instance automatically
🧰 Tools
🪛 Biome (1.9.4)

[error] 28-28: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)


49-60: Guard against missing path values to avoid early-crash scenarios

paths.activationBase (and other path look-ups) may return undefined if the store is mis-configured. Attempting fs.access(undefined) later will throw an opaque ENOENT. Fail fast with a descriptive error (or a sane default) to aid troubleshooting.
[ flag_critical_issue ]

-        this.activationDir = paths.activationBase;
+        this.activationDir = paths.activationBase;
+        if (!this.activationDir) {
+            throw new Error(
+                "Store did not provide 'activationBase'. CustomizationService cannot continue."
+            );
+        }

237-246: Drop the redundant dynamic import of getters

getters is already imported at module scope (line 11). Re-importing it on every call adds overhead and shadows the earlier binding. Use the existing reference instead.
[ suggest_optional_refactor ]

-        const { getters } = await import('@app/store/index.js');
+        // `getters` is already in scope – reuse it to avoid an extra dynamic import.

256-261: Preserve explicit “falsey” values for showBannerGradient

The current truthy check will ignore valid values such as "0" or "false". Test for undefined/null instead so that the caller can explicitly disable the gradient.
[ suggest_essential_refactor ]

-        if (showBannerGradient) settingsToUpdate['showBannerGradient'] = showBannerGradient;
+        if (showBannerGradient !== undefined && showBannerGradient !== null) {
+            settingsToUpdate['showBannerGradient'] = showBannerGradient;
+        }

343-349: Reuse the existing getters import in applyServerIdentity

As with applyDisplaySettings, the extra dynamic import is unnecessary. It can also lead to subtle bugs if the two imports diverge (e.g., during hot-reload in tests).
[ suggest_optional_refactor ]

-        const { getters } = await import('@app/store/index.js');
+        // `getters` already imported at module scope
web/composables/gql/graphql.ts (1)

421-425: Consider enforcing non-nullability for container port numbers

privatePort / publicPort now use the custom Port scalar but remain nullable. If a port is always present when the container exposes it, consider marking the fields as non-nullable to improve type-safety throughout the UI.
[ suggest_nitpick ]

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)
web/components/Activation/WelcomeModal.ce.vue (2)

40-42: Fix missing DOM selector for confirmPassword

The query window.document.querySelector('#confirmPassword') returns null—there's no element with id="confirmPassword" in this component or elsewhere. This will cause runtime errors.

Either:

  • Add id="confirmPassword" to the corresponding input element
  • Update the selector to match an actual field ID
  • Use Vue's ref system instead of querySelector for more reliable DOM access:
- const confirmPasswordField = window.document.querySelector('#confirmPassword');
- 
- if (confirmPasswordField) {
+ const showFontSizeAdjustment = true;
+ 
+ if (showFontSizeAdjustment) {

70-71: Ensure ActivationPartnerLogo accepts a name prop

The component is used without a name prop here, but in other places it receives one (e.g., ActivationModal.vue).

Either:

  • Add the prop to this instance: <ActivationPartnerLogo :name="partnerInfo?.partnerName" />
  • Or ensure the component properly handles the absence of a name prop
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)

4-4: Remove unused import or actually use IsUrl.

IsUrl is imported but never applied, which is a small cleanliness issue and may also signal a missing validation on partnerUrl. Either drop the import or (preferably) add the decorator to the partnerUrl field.

-import { IsBoolean, IsIn, IsOptional, IsString, IsUrl } from 'class-validator';
+import { IsBoolean, IsIn, IsOptional, IsString } from 'class-validator';

—or—

     @IsOptional()
-    @IsString()
+    @IsUrl({ require_tld: false }, { message: 'partnerUrl must be a valid URL' })

34-36: Add validation to partnerName for consistency.

partnerName lacks any class-validator decorators, unlike the rest of the scalar fields. Adding @IsOptional() and @IsString() keeps the DTO internally consistent and prevents accidental non-string values from sneaking through.

-    @Field(() => String, { nullable: true })
-    partnerName?: string;
+    @Field(() => String, { nullable: true })
+    @IsOptional()
+    @IsString()
+    @Transform(({ value }) => sanitizeString(value))
+    partnerName?: string;

17-30: Rethink returning '' for invalid hex colours.

The transformer coerces invalid values to an empty string, which still satisfies @IsString() and therefore passes validation. Consider returning undefined (so the property remains truly “unset”) or throwing an error so the client is immediately informed.

-    return ''; // Return empty string if not a valid hex color after potential modification
+    return undefined; // Preserve "unset" semantics for invalid colours

This prevents downstream code from having to filter out falsy-but-defined values.

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

6-8: Remove unused validateOrReject import.

validateOrReject is imported but never referenced, adding a tiny bit of noise.

-import { plainToInstance } from 'class-transformer';
-import { validateOrReject } from 'class-validator';
+import { plainToInstance } from 'class-transformer';
📜 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 3f71df9 and d50839d.

📒 Files selected for processing (13)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • web/__test__/store/activationCode.test.ts (0 hunks)
  • web/components/Activation/ActivationPartnerLogo.vue (1 hunks)
  • web/components/Activation/ActivationPartnerLogoImg.vue (1 hunks)
  • web/components/Activation/WelcomeModal.ce.vue (4 hunks)
  • web/components/Activation/graphql/activationCode.query.ts (1 hunks)
  • web/components/Activation/store/activationCodeData.ts (1 hunks)
  • web/components/Activation/store/activationCodeModal.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (17 hunks)
💤 Files with no reviewable changes (1)
  • web/test/store/activationCode.test.ts
✅ Files skipped from review due to trivial changes (1)
  • web/components/Activation/ActivationPartnerLogo.vue
🚧 Files skipped from review as they are similar to previous changes (8)
  • web/components/Activation/graphql/activationCode.query.ts
  • web/components/Activation/store/activationCodeData.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • web/components/Activation/store/activationCodeModal.ts
  • web/components/Activation/ActivationPartnerLogoImg.vue
  • api/generated-schema.graphql
  • web/composables/gql/gql.ts
  • web/composables/gql/graphql.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
  • PublicPartnerInfo (1088-1096)
  • ActivationCode (46-59)
  • Customization (448-452)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
web/components/Activation/WelcomeModal.ce.vue (2)

6-8: Good refactoring to use central activation store

The imports reflect the architectural shift to use a centralized GraphQL-based activation store, which aligns with the PR objective of moving activation logic into the API.


76-76: Improved UX with loading state

Using the loading state to disable the button during API operations is a good practice that prevents multiple submissions and provides visual feedback to users.

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

♻️ Duplicate comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (4)

35-45: ⚠️ Potential issue

Mock for store doesn't properly override getState()

Your current mock implementation for the store adds a getters property, but doesn't mock store.getState(), which is what the CustomizationService likely calls. This will prevent your tests from properly isolating the service from the real store.

vi.mock('@app/store/index.js', async () => {
    const actual = await vi.importActual('@app/store/index.js');
    return {
        ...actual,
-        getters: {
-            paths: vi.fn(() => mockPaths),
-            dynamix: vi.fn(() => mockDynamixState),
-            emhttp: vi.fn(() => mockEmhttpState),
-        },
+        store: {
+            getState: () => ({ 
+                paths: mockPaths,
+                dynamix: mockDynamixState,
+                emhttp: mockEmhttpState 
+            }),
+        },
    };
});

16-19: ⚠️ Potential issue

Duplicate mocks for emcmd still present

The duplicate mock declaration issue from the previous review hasn't been fully addressed. You have two vi.mock('@app/core/utils/clients/emcmd.js') calls - the first one at line 17 and the second one at line 47. Since Vitest hoists mocks, only the first one (which is an auto-mock) takes effect, making your detailed mock implementation at lines 47-69 ineffective.

-vi.mock('@app/core/utils/clients/emcmd.js');
-
 ... 

Remove the first simple mock to ensure your detailed implementation is used.


145-154: ⚠️ Potential issue

Test doesn't properly verify path initialization

This test is a false positive as mentioned in previous review comments. You're manually setting the service's properties in the beforeEach block (lines 123-127), then calling onModuleInit() and asserting the values are what you manually set. This doesn't verify that the method actually initializes paths from the store.

it('should initialize paths from store', async () => {
+    // Reset the manually assigned properties
+    (service as any).activationDir = undefined;
+    (service as any).configFile = undefined;
+    (service as any).caseModelCfg = undefined;
+    (service as any).identCfg = undefined;
+
    await service.onModuleInit();
+    
    expect((service as any).activationDir).toBe(activationDir);
    expect((service as any).configFile).toBe(userDynamixCfg);
    expect(loggerLogSpy).toHaveBeenCalledWith(
        'CustomizationService initialized with paths from store.'
    );
});

106-139: 🛠️ Refactor suggestion

Stop manually assigning service properties in beforeEach

You're manually setting service properties after instantiation in the beforeEach block. This creates artificial test conditions and makes tests like "should initialize paths from store" meaningless since you're bypassing the actual initialization logic.

beforeEach(async () => {
    vi.clearAllMocks(); // Clear mocks before each test

    // Spy on logger methods
    loggerDebugSpy = vi.spyOn(Logger.prototype, 'debug').mockImplementation(() => {});
    loggerLogSpy = vi.spyOn(Logger.prototype, 'log').mockImplementation(() => {});
    loggerWarnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => {});
    loggerErrorSpy = vi.spyOn(Logger.prototype, 'error').mockImplementation(() => {});

    const module: TestingModule = await Test.createTestingModule({
        providers: [CustomizationService],
    }).compile();

    service = module.get<CustomizationService>(CustomizationService);

-    // Re-assign paths manually in beforeEach AFTER mocks are cleared and service instantiated
-    // This simulates the dynamic import within onModuleInit
-    (service as any).activationDir = activationDir;
-    (service as any).hasRunFirstBootSetup = doneFlag;
-    (service as any).configFile = userDynamixCfg;
-    (service as any).caseModelCfg = caseModelCfg;
-    (service as any).identCfg = identCfg;

    // Mock fileExists needed by customization methods
    vi.mocked(fileExists).mockImplementation(async (p) => {
        // Assume assets exist by default for these tests unless overridden
        return (
            p === partnerBannerSource ||
            p === bannerAssetPath ||
            p === caseModelAssetPath ||
            p === bannerDestPath
        );
    });
});

Instead, fix the store mock to provide these values through the getState() method and let onModuleInit() set them properly.

🧹 Nitpick comments (3)
web/pages/welcome.vue (1)

35-35: Label doesn't match the displayed value.

The label "Manually Hidden" is displaying the isVisible value, which is confusing since these represent opposite states.

-<li>Manually Hidden (`activationModalHidden`): {{ isVisible }}</li>
+<li>Manually Hidden (`activationModalHidden`): {{ !isVisible }}</li>
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)

856-885: Clean up commented out expectations

There are several expectations marked with "REMOVED" comments (lines 857-859, 881-884). These should be completely removed rather than left as commented code, as they clutter the file and could cause confusion.

        // Other steps should still run
        expect(loggerLogSpy).toHaveBeenCalledWith('Setting up partner banner...');
        expect(loggerLogSpy).toHaveBeenCalledWith('Applying display settings...');
        expect(loggerLogSpy).toHaveBeenCalledWith('Applying server identity...');

-        // Overall error from applyActivationCustomizations' catch block
-        // REMOVED: expect(loggerErrorSpy).toHaveBeenCalledWith('Error during activation setup:', readError);
-
-        // NO overall error logged because the writeFile error is caught internally
        expect(loggerErrorSpy).not.toHaveBeenCalledWith(
            'Error during activation setup:',
            expect.any(Error)
        ); // This line should remain

65-69: Improve emcmd mock with proper typings

Your emcmd mock returns a generic object with minimal fields. This is brittle and could lead to TypeScript errors. Consider using a more robust approach with proper type assertions.

return {
    ...actual,
-    emcmd: vi.fn(async () => mockResponse), // Return the mock response object
+    emcmd: vi.fn().mockImplementation(async () => mockResponse as import('@app/core/utils/clients/emcmd.js').Response<string>),
};
📜 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 d50839d and ceda719.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • .cursor/rules/default.mdc (1 hunks)
  • api/dev/Unraid.net/myservers.cfg (1 hunks)
  • api/dev/states/var.ini (1 hunks)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • package.json (1 hunks)
  • web/__test__/store/server.test.ts (0 hunks)
  • web/components/Activation/ActivationModal.vue (3 hunks)
  • web/components/Activation/store/activationCodeData.ts (1 hunks)
  • web/components/Activation/store/activationCodeModal.ts (1 hunks)
  • web/components/DummyServerSwitcher.vue (1 hunks)
  • web/helpers/create-apollo-client.ts (2 hunks)
  • web/layouts/default.vue (2 hunks)
  • web/nuxt.config.ts (1 hunks)
  • web/pages/welcome.vue (1 hunks)
  • web/store/server.ts (3 hunks)
💤 Files with no reviewable changes (1)
  • web/test/store/server.test.ts
✅ Files skipped from review due to trivial changes (6)
  • package.json
  • web/nuxt.config.ts
  • api/dev/Unraid.net/myservers.cfg
  • api/dev/states/var.ini
  • web/components/DummyServerSwitcher.vue
  • web/layouts/default.vue
🚧 Files skipped from review as they are similar to previous changes (6)
  • .cursor/rules/default.mdc
  • web/components/Activation/store/activationCodeData.ts
  • web/components/Activation/ActivationModal.vue
  • web/components/Activation/store/activationCodeModal.ts
  • api/generated-schema.graphql
  • web/store/server.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
web/pages/welcome.vue (3)

3-8: Imports look good and align with PR objectives.

These imports properly bring in the necessary activation-related stores and components that support moving activation logic to the API.


12-16: Good store initialization with proper reactivity handling.

Using storeToRefs is the correct approach to maintain reactivity when destructuring properties from Pinia stores.


28-28: Good addition of the ModalsCe component.

This integrates well with the overall activation system refactor mentioned in the PR summary.

web/helpers/create-apollo-client.ts (2)

1-1: Import cleanup improves code hygiene.

The removal of ApolloLink and Observable imports aligns with the elimination of the disableClientLink functionality, keeping imports to only what's needed.


82-82:

Details

✅ Verification successful

Apollo link chain simplified.

The Apollo client link chain has been streamlined by removing the disableClientLink from the composition. This change aligns with moving activation code logic to the API as stated in the PR objectives.


🏁 Script executed:

#!/bin/bash
# Verify that no code in the codebase still relies on the removed disableClientLink
rg "disableClientLink" --type ts

Length of output: 32


🏁 Script executed:

#!/bin/bash
# Re-verify that no code references disableClientLink across all file types
rg -n "disableClientLink"

Length of output: 25


Link chain simplification verified
No remaining references to disableClientLink were found across the codebase. The Apollo link chain removal is safe and aligns with the PR objectives.

api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (4)

95-105: LGTM: Well-structured mock activation data

The mock activation data is well-structured and matches the expected ActivationCode interface. This makes the tests more realistic and improves their value.


337-453: LGTM: Comprehensive tests for getActivationData

The test suite for the getActivationData method is well-designed and covers all important scenarios:

  • Missing activation directory
  • No activation code file
  • Directory access errors
  • File read errors
  • Invalid JSON
  • Hex color handling (both with and without # prefix)
  • Successfully validating and returning the DTO

This comprehensive approach significantly increases confidence in the code's reliability.


726-941: LGTM: Thorough error handling tests

The separate test suite for applyActivationCustomizations specifically focuses on error handling scenarios, which is excellent practice. The tests verify that:

  1. The service gracefully handles a missing activation directory
  2. Errors in one customization step don't prevent other steps from running
  3. Each method properly logs its specific errors

This approach ensures the service remains resilient even when parts of the customization process fail.


943-1096: LGTM: Excellent unit tests for updateCfgFile utility

The tests for the updateCfgFile utility function are particularly well-designed, covering:

  1. Creating a new file with/without sections
  2. Merging updates with existing content
  3. Adding new sections to existing files
  4. Handling read/write errors

This thorough testing of a utility function demonstrates good attention to detail and will prevent regressions.

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 (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)

122-128: Re-assignment of service properties masks initialization issues.

Manually reassigning service properties after service instantiation might make tests pass even if the actual initialization in onModuleInit() is broken. Instead, consider testing the initialization directly by asserting values before and after calling onModuleInit().


155-181: Complex approach for testing missing config path.

The test for handling a missing dynamix user config path is unnecessarily complex. Rather than importing the mock module and modifying its return value, consider using the approach from other tests where you directly mock the store's getState method.

🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)

41-45: Consider using IsUrl validator for partner URL.

The partnerUrl field is currently only validated as a string. If this is intended to be a valid URL, consider adding the @IsUrl() validator to ensure it's properly formatted.

@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
+@IsUrl({ require_tld: false, allow_protocol_relative_urls: true })
@Transform(({ value }) => sanitizeString(value))
partnerUrl?: string;
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

237-238: Redundant dynamic import of getters.

The getters are already imported at the top of the file, making this dynamic import unnecessary. Consider removing it.

-const { getters } = await import('@app/store/index.js');

312-313: Consider implementing Redux store updates.

The TODO comment indicates a potential need to dispatch actions to update the Redux store. Consider implementing this feature for immediate UI feedback when display settings change.

Would you like me to provide an implementation for dispatching Redux actions after updating the config file?

📜 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 ceda719 and 7b73737.

📒 Files selected for processing (6)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • web/_data/serverState.ts (2 hunks)
  • web/store/server.ts (4 hunks)
  • web/types/server.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • web/types/server.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/store/server.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
  • PublicPartnerInfo (1088-1096)
  • ActivationCode (46-59)
  • Customization (448-452)
🪛 Biome (1.9.4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[error] 28-28: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (13)
web/_data/serverState.ts (3)

9-10: Improved import organization.

Moving the Pinia import to its own line at the top of the file follows best practices for import organization.


200-206: Removal of activation code data from server state.

The removal of activation code data from the OEM activation object aligns with the PR objective of moving activation code logic into the API. This is a good architectural change that promotes separation of concerns by transferring this data management to dedicated stores with GraphQL queries.


208-212: Improved Pinia store formatting.

The formatting changes to the useDummyServerStore make the code more consistent and readable while maintaining the same functionality.

api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (4)

9-14: Well-implemented string sanitization.

The sanitizeString function effectively removes potentially dangerous characters (backslashes and quotes) from string values, which helps prevent injection attacks.


17-30: Good implementation of hex color validation.

The sanitizeAndValidateHexColor function properly:

  1. Sanitizes input with existing sanitizeString
  2. Adds a leading # if missing
  3. Validates the hex color format
  4. Returns empty string for invalid inputs

This robustly handles different formats of hex color input.


96-122: Comprehensive color handling for header and background.

The color fields are properly:

  1. Nullable in GraphQL
  2. Optional in validation
  3. Validated as strings
  4. Transformed using the custom sanitizeAndValidateHexColor function

The implementation ensures proper hex color format while gracefully handling invalid inputs.


123-128: Good theme constraint using IsIn validator.

The theme field is properly constrained to a set of allowed values using the @IsIn() validator, preventing invalid theme values.

api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)

404-422: Comprehensive test for hex color prepending.

Good test case that verifies the system correctly prepends # to hex colors that are provided without it, ensuring display settings are consistently applied regardless of input format.


599-622: Good test for handling empty color strings.

This test ensures that empty strings resulting from invalid hex colors are properly filtered out when applying display settings, preventing errors from propagating through the system.


943-1096: Excellent standalone tests for updateCfgFile utility.

These tests comprehensively verify the behavior of the private utility method, covering:

  1. Creating new files with and without sections
  2. Merging updates with existing content
  3. Adding new sections to existing files
  4. Error handling for both read and write operations

This approach of testing private methods in isolation is valuable for complex utilities.

api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)

126-157: Well-implemented activation data retrieval with caching.

The getActivationData() method effectively:

  1. Returns cached data if available
  2. Locates JSON files with the correct extension
  3. Parses and validates the data using class-transformer and class-validator
  4. Gracefully handles errors with appropriate logging
  5. Only caches valid data

This provides a robust foundation for the activation features.


236-316: Thorough display settings application with edge case handling.

The applyDisplaySettings() method:

  1. Checks for activation data availability
  2. Maps properties with custom transformations (removing # from colors)
  3. Verifies banner file existence before setting banner=image
  4. Skips empty fields
  5. Handles errors gracefully
  6. Provides detailed debug logging

The structured approach makes the code maintainable and robust.


417-472: Well-implemented INI file update utility.

The updateCfgFile helper method:

  1. Handles both section-based and root-level updates
  2. Creates files if they don't exist
  3. Preserves existing settings not being updated
  4. Properly stringifies INI format with trailing newline
  5. Includes comprehensive error handling and logging

This reusable utility provides a solid foundation for all configuration file updates.

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 (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)

16-19: Duplicate mocks for emcmd need to be consolidated

There are two vi.mock calls for the same module. Since Vitest hoists mocks, only the first statement is honored, making the detailed implementation at lines 47-69 ineffective.

Apply this diff to fix the issue:

-vi.mock('@app/core/utils/clients/emcmd.js');
-
 // Mocks
 vi.mock('fs/promises');
+// Mock emcmd with custom implementation
 vi.mock('@app/core/utils/clients/emcmd.js');

35-45: Store mock doesn't correctly override store.getState()

The current mock only adds getters but doesn't replace store.getState(), which is what the service actually calls. This makes tests potentially unreliable.

Apply this diff to properly mock the store:

vi.mock('@app/store/index.js', async () => {
    const actual = await vi.importActual('@app/store/index.js');
    return {
        ...actual,
-        getters: {
-            paths: vi.fn(() => mockPaths),
-            dynamix: vi.fn(() => mockDynamixState),
-            emhttp: vi.fn(() => mockEmhttpState),
-        },
+        store: {
+            getState: () => ({
+                paths: mockPaths,
+                dynamix: mockDynamixState,
+                emhttp: mockEmhttpState
+            }),
+        }
    };
});

146-161: Test for missing config path is unnecessarily complex

This test modifies mockPaths directly and then restores it, which is fragile and harder to maintain.

Consider simplifying by using a temporary store mock override:

it('should log error if dynamix user config path is missing', async () => {
-    // Temporarily modify mockPaths to simulate missing user config path
-    const originalPaths = { ...mockPaths };
-    mockPaths['dynamix-config'] = [mockPaths['dynamix-config'][0]]; // Only keep default config
-
+    // Save original mock to restore later
+    const originalGetState = vi.mocked(store.getState);
+    
+    // Mock store to return paths without dynamix-config[1]
+    vi.mocked(store.getState).mockReturnValueOnce({
+        ...originalGetState(),
+        paths: {
+            ...mockPaths,
+            'dynamix-config': [mockPaths['dynamix-config'][0]],
+        }
+    });

    await service.onModuleInit();

    expect(loggerErrorSpy).toHaveBeenCalledWith(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
    expect(fs.writeFile).not.toHaveBeenCalledWith(doneFlag, 'true');

-    // Restore original paths
-    mockPaths['dynamix-config'] = originalPaths['dynamix-config'];
+    // Restore original mock
+    vi.mocked(store.getState).mockImplementation(originalGetState);
});
🧹 Nitpick comments (5)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (5)

193-198: Test could be more specific about .done flag check

The test mocks fileExists to return true for .done, but the assertion only checks that fs.readdir wasn't called, which is an indirect verification.

Add an explicit log message assertion to verify the skip reason:

it('should skip customizations if .done flag exists', async () => {
    vi.mocked(fileExists).mockImplementation(async (p) => p === doneFlag); // .done file exists

    await service.onModuleInit();

    expect(fs.readdir).not.toHaveBeenCalled(); // Should not read activation dir for JSON
+   expect(loggerLogSpy).toHaveBeenCalledWith(expect.stringContaining("First boot setup already complete"));
});

209-215: Mock for fs.readFile could be simplified using a map

The current implementation uses a series of if-else conditions which becomes harder to maintain as more cases are added.

Use a map to make the mock more maintainable:

vi.mocked(fs.readFile).mockImplementation(async (p) => {
-    if (p === activationJsonPath) return JSON.stringify(mockActivationData);
-    if (p === userDynamixCfg) return ini.stringify({}); // Mock empty dynamix cfg
-    if (p === identCfg) return ini.stringify({}); // Mock empty ident cfg
-    if (p === caseModelCfg) throw { code: 'ENOENT' }; // Mock case model cfg doesn't exist
-    throw new Error(`Unexpected readFile: ${p}`);
+    const mockFiles = {
+        [activationJsonPath]: JSON.stringify(mockActivationData),
+        [userDynamixCfg]: ini.stringify({}),
+        [identCfg]: ini.stringify({})
+    };
+    
+    if (p === caseModelCfg) throw { code: 'ENOENT' };
+    if (p in mockFiles) return mockFiles[p];
+    throw new Error(`Unexpected readFile: ${p}`);
});

379-380: Use more specific assertion for error message

Using expect.any(SyntaxError) is less specific than checking for the actual error message content.

Use a more specific assertion:

expect(loggerErrorSpy).toHaveBeenCalledWith(
    `Error processing activation file ${activationJsonPath}:`,
-    expect.any(SyntaxError)
+    expect.objectContaining({ 
+        message: expect.stringContaining('Unexpected token') 
+    })
);

812-813: Commented out expectation without explanation

There's a commented out expectation without clear explanation why it was removed. This creates confusion about what was intended.

Either add a comment explaining why the expectation was removed or remove the line entirely to avoid confusion.


949-951: Default mock behavior could be improved with common error pattern

The default mocking for fs.readFile only throws for one specific path, which could lead to unexpected behavior for other paths.

Create a consistent default behavior:

vi.mocked(fs.readFile).mockImplementation(async (p) => {
    if (p === filePath) {
        const err = new Error('ENOENT') as NodeJS.ErrnoException;
        err.code = 'ENOENT';
        throw err; // Default: file not found for read
    }
-    throw new Error(`Unexpected readFile call in updateCfgFile tests: ${p}`);
+    // Default behavior for any other path
+    const err = new Error(`Unexpected readFile call: ${p}`) as NodeJS.ErrnoException;
+    err.code = 'ENOENT';
+    throw err;
});
📜 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 7b73737 and 3c401be.

⛔ Files ignored due to path filters (1)
  • api/src/__test__/store/modules/__snapshots__/paths.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.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 (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)

992-1006: Test for updateCfgFile with sections is thorough and well-designed

The test effectively verifies the method correctly merges updates with existing content while preserving other sections and unmodified keys.

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

🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)

238-239: Use the already imported getters instead of reimporting.

You're reimporting getters from '@app/store/index.js' when it's already imported at the top of the file (line 11) and used elsewhere. This duplicate import is unnecessary.

-const { getters } = await import('@app/store/index.js');
+// Use the already imported getters from line 11

368-369: Use the already imported getters instead of reimporting.

Same issue as in the applyDisplaySettings method - you're reimporting getters when it's already available.

-const { getters } = await import('@app/store/index.js');
+// Use the already imported getters from line 11

186-194: Consider removing redundant directory check.

This directory check is redundant since it's already performed in onModuleInit, as acknowledged in your comment. Consider removing it to simplify the code.

-// Check if activation dir exists (redundant if onModuleInit succeeded, but safe)
-try {
-    await fs.access(this.activationDir);
-} catch (dirError: any) {
-    if (dirError.code === 'ENOENT') {
-        this.logger.warn('Activation directory disappeared after init? Skipping.');
-        return;
-    }
-    throw dirError; // Rethrow other errors
-}

419-473: Consider adding file locking mechanism for config updates.

The updateCfgFile method directly modifies configuration files without any locking mechanism. This could lead to race conditions if multiple processes are modifying these files concurrently. Consider implementing a simple file locking strategy.

You could use a package like proper-lockfile to implement file locking:

import * as lockfile from 'proper-lockfile';

private async updateCfgFile(
    filePath: string,
    section: string | null,
    updates: Record<string, string>
) {
    let configData: any = {};
    let release;
    try {
        // Acquire a lock on the file
        release = await lockfile.lock(filePath, { retries: 3 });
        
        try {
            const content = await fs.readFile(filePath, 'utf-8');
            configData = ini.parse(content);
        } catch (error: any) {
            // Handle errors as before
        }
        
        // Update config as before
        
        // Write changes
        const newContent = ini.stringify(configData);
        await fs.writeFile(filePath, newContent + '\n');
        this.logger.log(`Config file ${filePath} updated successfully.`);
    } catch (error) {
        this.logger.error(`Error updating config file ${filePath}:`, error);
        throw error;
    } finally {
        // Release the lock if it was acquired
        if (release) await release();
    }
}
📜 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 f58272e and 65efd6d.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • web/components/Activation/WelcomeModal.ce.vue (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components/Activation/WelcomeModal.ce.vue
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)

1-15: The overall architecture looks good.

The service is well-structured with clear responsibilities for managing system customizations based on activation data. The code is organized logically with appropriate use of NestJS patterns and dependency management.


121-152: Great implementation of the activation data retrieval.

The caching strategy and error handling in this method are well implemented. The use of class-transformer and class-validator ensures that the activation data is properly validated before use.


251-285: Good use of typed mappings for configuration transformation.

The type-safe approach to mapping activation data properties to configuration keys is well-designed. The transformation functions and optional value handling make the code robust and maintainable.

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 (1)
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1)

32-42: Added method to remove default Unraid logo from page layout.

This method removes the default Unraid logo anchor tag by replacing it with an empty string, making room for partner branding to be injected elsewhere. The implementation includes a check to ensure the replacement only happens when necessary.

There appears to be unnecessary whitespace in the replaceString declaration. Consider removing the extra newlines and whitespace:

-    const replaceString =
-        '';
+    const replaceString = '';
🧰 Tools
🪛 GitHub Check: Test API

[failure] 35-35:
Delete ⏎···········

🪛 GitHub Check: Build API

[failure] 35-35:
Delete ⏎···········

📜 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 adc4b3d and 0655092.

⛔ Files ignored due to path filters (1)
  • api/dev/webGui/images/UN-logotype-gradient.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/DefaultPageLayout.php.modified.snapshot.php (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch (4 hunks)
  • api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (1 hunks)
  • plugin/plugins/dynamix.unraid.net.plg (0 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php (0 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/partner-logo.php (0 hunks)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php (0 hunks)
  • web/components/Activation/ActivationModal.vue (2 hunks)
  • web/components/HeaderOsVersion.ce.vue (2 hunks)
  • web/nuxt.config.ts (3 hunks)
  • web/pages/index.vue (2 hunks)
💤 Files with no reviewable changes (4)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/partner-logo.php
  • plugin/plugins/dynamix.unraid.net.plg
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/activation-code-extractor.php
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php
✅ Files skipped from review due to trivial changes (1)
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts
  • web/components/Activation/ActivationModal.vue
  • web/nuxt.config.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/test/snapshots/DefaultPageLayout.php.modified.snapshot.php
  • api/src/unraid-api/unraid-file-modifier/modifications/partner-logo-copier.modification.ts
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/default-page-layout.patch
  • api/generated-schema.graphql
🧰 Additional context used
🪛 GitHub Check: Test API
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts

[failure] 35-35:
Delete ⏎···········

🪛 GitHub Check: Build API
api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts

[failure] 35-35:
Delete ⏎···········

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (8)
web/pages/index.vue (2)

3-3: Logo component import removed to support the dynamic partner branding.

The removal of BrandLogo from imports aligns with the broader changes moving to dynamic partner branding handled via the new activation code system.


114-116: Logo element removed from header.

The static logo and link have been removed, which corresponds with the import changes. The container class was also updated to remove horizontal padding (px-4). This change streamlines the UI to support dynamic partner branding implemented elsewhere.

web/components/HeaderOsVersion.ce.vue (5)

12-12: Added import for the new activation code data store.

This import adds support for dynamic partner information through the activation code system.


20-20: Added access to partner information from the new store.

The partnerInfo reactive reference is destructured from the new activation code data store.


25-30: Added computed property for partner URL.

Good implementation of the logoUrl computed property with proper fallback to the default Unraid URL when partner URL is not available.


73-76: Added dynamic logo link in the header.

The logo now links to either the partner URL or the default Unraid URL based on the activation code data. The image source still points to the Unraid logo image with responsive width classes.


77-115: Restructured OS version and update status badges.

The existing OS version badge and update status controls are now contained within a flex container below the logo. The functionality remains unchanged, but the visual organization is improved.

api/src/unraid-api/unraid-file-modifier/modifications/default-page-layout.modification.ts (1)

49-49: Added logo injection step to transformation pipeline.

The injectPartnerLogo method has been incorporated into the transformation pipeline in applyToSource.

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)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

44-52: 🛠️ Refactor suggestion

Provide a default value for the configFile path.

The error handling for the missing configFile path could be improved. Currently, initialization simply stops if the path is missing, which may lead to unpredictable behavior later.

Consider adding a default path instead of returning early:

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}

310-310: 🛠️ Refactor suggestion

Address the TODO comment about Redux store updates.

This TODO comment suggests that the state update might not be immediately reflected in the Redux store.

- // @todo: Consider dispatching an action to update Redux store if needed immediately
+ // Update Redux store to reflect the changed display settings
+ try {
+   // Import actions and dispatch to ensure store is updated
+   const { actions } = await import('@app/store/index.js');
+   actions.dynamix.update({ display: settingsToUpdate });
+   this.logger.log('Redux store updated with new display settings.');
+ } catch (storeError) {
+   this.logger.warn('Failed to update Redux store:', storeError);
+   // Non-fatal error, continue
+ }
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)

83-84: Improve error handling specificity.

The comment mentions a specific error case but the code checks for two conditions.

- if (error.code === 'ENOENT' && error.path === this.activationDir) {
+ // Check specifically for activation directory not found errors
+ if (error.code === 'ENOENT' && error.path === this.activationDir) {

214-231: Simplify error handling in setupPartnerBanner.

The nested try-catch structure is overly complex since the file existence check doesn't throw errors.

private async setupPartnerBanner() {
    this.logger.log('Setting up partner banner...');
    const paths = getters.paths();
    const partnerBannerSource = paths.partnerBannerSource;
    const partnerBannerTarget = paths.partnerBannerTarget;

-   try {
-       // Always overwrite if partner banner exists
-       if (await fileExists(partnerBannerSource)) {
-           this.logger.log(`Partner banner found at ${partnerBannerSource}, overwriting original.`);
-           try {
-               await fs.copyFile(partnerBannerSource, partnerBannerTarget);
-               this.logger.log('Partner banner copied over the original banner.');
-           } catch (copyError: any) {
-               this.logger.warn(
-                   `Failed to replace the original banner with the partner banner: ${copyError.message}`
-               );
-           }
-       } else {
-           this.logger.log('Partner banner file not found, skipping banner setup.');
-       }
-   } catch (error) {
-       this.logger.error('Error setting up partner banner:', error);
-   }
+   // Always overwrite if partner banner exists
+   if (await fileExists(partnerBannerSource)) {
+       this.logger.log(`Partner banner found at ${partnerBannerSource}, overwriting original.`);
+       try {
+           await fs.copyFile(partnerBannerSource, partnerBannerTarget);
+           this.logger.log('Partner banner copied over the original banner.');
+       } catch (copyError: any) {
+           this.logger.warn(
+               `Failed to replace the original banner with the partner banner: ${copyError.message}`
+           );
+       }
+   } else {
+       this.logger.log('Partner banner file not found, skipping banner setup.');
+   }
}

338-358: Handle case when no custom model exists more explicitly.

The current implementation skips writing to the config file if no custom model is found, but the logic could be more explicit.

Consider making this behavior more explicit:

let modelToSet: string | null = null;

// Check if the custom image file exists in assets
if (await fileExists(caseModelSource)) {
    // Use the *target* filename which CaseModelCopierModification will create
    modelToSet = path.basename(paths.caseModelTarget); // e.g., 'case-model.png'
    this.logger.log('Custom case model file found in assets, config will be set.');
} else {
    this.logger.log('No custom case model file found in assets.');
+   // Explicitly note that we're keeping the existing model (if any)
+   this.logger.log('Keeping existing case model configuration.');
}

// If a model was determined, write it to the config file
if (modelToSet) {
    try {
        await fs.writeFile(this.caseModelCfg, modelToSet);
        this.logger.log(`Case model set to ${modelToSet} in ${this.caseModelCfg}`);
    } catch (writeError: any) {
        // Added type annotation
        this.logger.error(`Failed to write case model config: ${writeError.message}`);
    }
}

400-417: Separate error handling for external operations.

The method calls several external operations but handles all errors in a single try-catch block. It would be better to handle each operation's errors separately.

Consider separating the error handling for each external operation:

try {
    // Update ident.cfg first
    await this.updateCfgFile(this.identCfg, null, paramsToUpdate);
    this.logger.log(`Server identity updated in ${this.identCfg}`);

+   try {
        // Trigger emhttp update via emcmd
        const updateParams = { ...paramsToUpdate, changeNames: 'Apply' };
        this.logger.log(`Calling emcmd with params:`, updateParams);
        await emcmd(updateParams);
        this.logger.log('emcmd executed successfully.');
+   } catch (emcmdError) {
+       this.logger.error('Error executing emcmd:', emcmdError);
+       // Continue with other operations
+   }

+   try {
        // Reload nginx and update DNS
        await store.dispatch(reloadNginxAndUpdateDNS());
        this.logger.log('Nginx reloaded and DNS updated successfully.');
+   } catch (reloadError) {
+       this.logger.error('Error reloading nginx and updating DNS:', reloadError);
+   }
} catch (error) {
    this.logger.error('Error applying server identity:', error);
}
📜 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 6b7639e and 450a03e.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • web/components/Activation/WelcomeModal.ce.vue (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/components/Activation/WelcomeModal.ce.vue
⏰ 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 (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

119-123: LGTM! Good use of caching for activation data.

The caching implementation for activation data is well-executed and will help performance.


419-473: Well-implemented INI file updating function.

The updateCfgFile method is well-structured and includes thorough error handling for file operations. The comments explaining the INI library's behavior are particularly helpful.

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)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

44-52: 🛠️ Refactor suggestion

Provide a default value for missing configFile path.

The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}

323-323: 🛠️ Refactor suggestion

Address the TODO comment about Redux store updates.

This TODO comment suggests that the state update might not be immediately reflected in the Redux store.

- // @todo: Consider dispatching an action to update Redux store if needed immediately
+ // Update Redux store to reflect the changed display settings
+ try {
+   const { actions } = await import('@app/store/index.js');
+   store.dispatch(actions.dynamix.updateDisplay(settingsToUpdate));
+   this.logger.log('Redux store updated with new display settings.');
+ } catch (storeError) {
+   this.logger.warn('Failed to update Redux store:', storeError);
+   // Non-fatal error, continue
+ }
🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)

38-42: Consider caching the password check result.

For better performance, consider caching the result of this check since the password status rarely changes during runtime.

private async isPasswordSet(): Promise<boolean> {
+    // Use static variable to cache result
+    if (this.passwordCheckResult !== undefined) return this.passwordCheckResult;
    const paths = store.getState().paths;
    const hasPasswd = await fileExists(paths.passwd);
+    this.passwordCheckResult = hasPasswd;
    return hasPasswd;
}
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

27-36: Use better flag creation strategy.

The current implementation might face race conditions if called concurrently. Consider using a more atomic approach.

async createOrGetFirstBootSetupFlag(): Promise<boolean> {
    await fs.mkdir(this.activationDir, { recursive: true });
-   if (await fileExists(this.hasRunFirstBootSetup)) {
-       this.logger.log('First boot setup flag file already exists.');
-       return true; // Indicate setup was already done based on flag presence
-   }
-   await fs.writeFile(this.hasRunFirstBootSetup, 'true');
-   this.logger.log('First boot setup flag file created.');
-   return false; // Indicate setup was just marked as done
+   try {
+       // Try to create file with exclusive flag
+       await fs.writeFile(this.hasRunFirstBootSetup, 'true', { flag: 'wx' });
+       this.logger.log('First boot setup flag file created.');
+       return false; // Indicate setup was just marked as done
+   } catch (error: any) {
+       if (error.code === 'EEXIST') {
+           this.logger.log('First boot setup flag file already exists.');
+           return true; // Indicate setup was already done based on flag presence
+       }
+       throw error; // Re-throw unexpected errors
+   }
}

173-184: Consider using a more efficient approach for base64 conversion.

For large logo files, reading the entire file into memory as base64 could be memory intensive.

public async getPartnerLogoRaw(): Promise<string | null> {
    const path = getters.paths().partnerLogoTarget;
    if (await fileExists(path)) {
-       const fileContent = await fs.readFile(path, 'base64');
-       return fileContent;
+       // Use stream-based approach for more efficient memory usage
+       return new Promise((resolve, reject) => {
+           try {
+               const readStream = createReadStream(path);
+               const chunks: Buffer[] = [];
+               readStream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
+               readStream.on('error', (err) => reject(err));
+               readStream.on('end', () => {
+                   const buffer = Buffer.concat(chunks);
+                   resolve(buffer.toString('base64'));
+               });
+           } catch (error) {
+               reject(error);
+           }
+       });
    }
    return null;
}
📜 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 450a03e and 303541e.

📒 Files selected for processing (10)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • web/components/Activation/ActivationPartnerLogoImg.vue (1 hunks)
  • web/components/Activation/WelcomeModal.ce.vue (3 hunks)
  • web/components/Activation/graphql/activationCode.query.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (17 hunks)
  • web/store/theme.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • web/components/Activation/ActivationPartnerLogoImg.vue
  • web/components/Activation/WelcomeModal.ce.vue
  • web/components/Activation/graphql/activationCode.query.ts
  • api/generated-schema.graphql
  • web/composables/gql/gql.ts
🧰 Additional context used
🧬 Code Graph Analysis (2)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
  • PublicPartnerInfo (1089-1099)
  • ActivationCode (46-59)
  • Customization (448-452)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (5)
web/composables/gql/graphql.ts (4)
  • Customization (448-452)
  • PublicPartnerInfo (1089-1099)
  • Query (1101-1138)
  • ActivationCode (46-59)
api/src/store/index.ts (1)
  • store (6-12)
packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
  • fileExists (5-8)
api/src/unraid-api/graph/directives/use-permissions.directive.ts (3)
  • UsePermissions (69-85)
  • AuthActionVerb (14-14)
  • AuthPossession (14-14)
api/src/unraid-api/auth/public.decorator.ts (1)
  • Public (4-4)
🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts

[failure] 34-34:
Replace await·this.customizationService.getPartnerLogoRaw( with (await·this.customizationService.getPartnerLogoRaw()

🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts

[failure] 34-34:
Replace await·this.customizationService.getPartnerLogoRaw( with (await·this.customizationService.getPartnerLogoRaw()

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (18)
web/store/theme.ts (1)

127-127: Good addition - exposes the setCssVars function for external use.

This change makes the setCssVars function accessible outside the store, which supports the new activation code-driven theming features. The function was previously only used internally (triggered by the watcher on line 115), but now external components can trigger CSS variable updates directly when needed.

api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (4)

25-36: LGTM! Well-structured method for partner info retrieval.

The method efficiently combines partner logo existence check and activation data into a single PublicPartnerInfo object.

🧰 Tools
🪛 GitHub Check: Test API

[failure] 34-34:
Replace await·this.customizationService.getPartnerLogoRaw( with (await·this.customizationService.getPartnerLogoRaw()

🪛 GitHub Check: Build API

[failure] 34-34:
Replace await·this.customizationService.getPartnerLogoRaw( with (await·this.customizationService.getPartnerLogoRaw()


51-54: Good implementation of authenticated query.

The resolver properly returns an empty object and relies on field resolvers for population, which follows GraphQL best practices.


56-64: Well-designed public endpoint with security check.

Good use of the @public() decorator combined with password check to conditionally expose partner info only when no password is set.


66-79: Appropriate authorization handling for different fields.

The implementation correctly applies permissions to the sensitive activationCode field while leaving partnerInfo accessible, matching the security requirements.

api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (5)

7-14: Good security practice with string sanitization.

The sanitizeString function properly removes potential injection vectors by filtering out quotes and backslashes.


17-30: Well-implemented hex color validation and normalization.

The function handles different color formats well, ensuring consistent output with proper validation.


32-66: LGTM! Robust model design with proper validation.

The PublicPartnerInfo class uses appropriate decorators for validation and transformation, ensuring data safety.


124-131: Nice handling of boolean transformation.

Good approach for handling boolean values that might come in different formats (boolean or string).


133-137: Good use of enum validation.

The @isin decorator ensures the theme value is restricted to valid options.

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

58-91: Well-structured initialization logic.

Good pattern for handling potential directory existence issues and appropriately applying customizations only on first boot.


377-430: LGTM! Thorough implementation of server identity updates.

The method handles all necessary updates including configuration files, emhttp updates, and system services.

web/composables/gql/graphql.ts (6)

25-26: Good addition of Port scalar type.

Adding a specific scalar for TCP ports improves type safety and documentation.


46-59: Well-structured ActivationCode type.

The type includes all necessary fields for customization with appropriate nullability.


279-292: Good practice using enums for authorization verbs and possession.

Using enums provides better type safety and documentation than string literals.


1043-1061: Improved organization with ParityCheckMutations.

Grouping related mutations into a dedicated type improves API organization and maintainability.


1089-1099: Well-documented PublicPartnerInfo type.

Good use of field descriptions to document the purpose and format of each field.


1720-1728: LGTM! Well-structured GraphQL queries.

The queries properly fetch the data needed for the activation code features.

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)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

45-53: 🛠️ Refactor suggestion

Provide a default value for missing configFile path.

The code logs an error when configFile path is missing but then returns without providing a fallback. This could lead to runtime errors when the path is used later.

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/boot/config/plugins/dynamix/dynamix.cfg'; // Use default path
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}

305-307: 🛠️ Refactor suggestion

Address the TODO comment about Redux store updates.

The TODO comment indicates the Redux store might need to be updated after applying display settings.

await this.updateCfgFile(this.configFile, 'display', settingsToUpdate);
this.logger.log('Display settings updated in config file.');
- // @todo: Consider dispatching an action to update Redux store if needed immediately
+ // Update Redux store to reflect the changed display settings
+ try {
+   const { actions } = await import('@app/store/index.js');
+   actions.dynamix.update({ display: settingsToUpdate });
+   this.logger.log('Redux store updated with new display settings.');
+ } catch (storeError) {
+   this.logger.warn('Failed to update Redux store:', storeError);
+   // Non-fatal error, continue with customization
+ }
🧹 Nitpick comments (7)
api/src/utils.ts (2)

281-292: Consider adding input validation for path conversion.

The function handles standard cases correctly, but lacks validation for edge cases like empty strings, paths not starting with the expected prefix, or paths containing unexpected characters.

export const convertWebGuiPathToAssetPath = (webGuiPath: string): string => {
+   // Validate input
+   if (!webGuiPath || typeof webGuiPath !== 'string') {
+       return '';
+   }

+   // Ensure path has expected prefix
+   if (!webGuiPath.startsWith('/usr/local/emhttp/')) {
+       return webGuiPath; // Return unchanged if not matching expected pattern
+   }

    // Strip the leading /usr/local/emhttp/ from the path
    const assetPath = webGuiPath.replace('/usr/local/emhttp/', '/');
    return assetPath;
};
🧰 Tools
🪛 GitHub Check: Build API

[failure] 292-292:
Insert


[failure] 292-292:
Newline required at end of file but not found

🪛 GitHub Check: Test API

[failure] 292-292:
Insert


[failure] 292-292:
Newline required at end of file but not found


4-16: Fix linting issues for code style consistency.

There are several linting errors flagged by the static analysis:

  1. Extra blank lines at various places in the file
  2. Missing trailing newline at the end of the file
// Remove extra blank lines
-

-

import strftime from 'strftime';

-

-

// Fix missing trailing newline at the end of the file
    return assetPath;
};
+

Also applies to: 292-292

🧰 Tools
🪛 GitHub Check: Build API

[failure] 4-4:
Replace ⏎⏎import·strftime·from·'strftime';⏎⏎ with import·strftime·from·'strftime';


[failure] 4-4:
More than 1 blank line not allowed


[failure] 8-8:
More than 1 blank line not allowed


[failure] 11-11:
Delete ⏎⏎⏎⏎


[failure] 13-13:
More than 1 blank line not allowed

🪛 GitHub Check: Test API

[failure] 4-4:
Replace ⏎⏎import·strftime·from·'strftime';⏎⏎ with import·strftime·from·'strftime';


[failure] 4-4:
More than 1 blank line not allowed


[failure] 8-8:
More than 1 blank line not allowed


[failure] 11-11:
Delete ⏎⏎⏎⏎


[failure] 13-13:
More than 1 blank line not allowed

api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)

152-152: Remove extra blank line.

There's an extra blank line that's triggering linting errors.

    }
}

-

public async getCaseIconWebguiPath(): Promise<string | null> {
🧰 Tools
🪛 GitHub Check: Build API

[failure] 152-152:
Delete


[failure] 152-152:
More than 1 blank line not allowed

🪛 GitHub Check: Test API

[failure] 152-152:
Delete


[failure] 152-152:
More than 1 blank line not allowed


153-159: Add input validation for path resolution method.

The method returns null if the file doesn't exist, but should also handle potential issues with the path itself.

public async getCaseIconWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
+   if (!paths || !paths.caseModelSource || !paths.caseModelTarget) {
+       this.logger.warn('Missing required case model paths');
+       return null;
+   }
    if (await fileExists(paths.caseModelSource)) {
        return convertWebGuiPathToAssetPath(paths.caseModelTarget);
    }
    return null;
}

161-167: Add input validation for partner logo path resolution.

Similar to the case icon method, this should handle missing paths.

public async getPartnerLogoWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
+   if (!paths || !paths.partnerLogoSource || !paths.partnerLogoTarget) {
+       this.logger.warn('Missing required partner logo paths');
+       return null;
+   }
    if (await fileExists(paths.partnerLogoSource)) {
        return convertWebGuiPathToAssetPath(paths.partnerLogoTarget);
    }
    return null;
}

416-426: Improve error handling in updateCfgFile method.

The method handles file not found errors but should be more explicit about file permissions or other I/O errors.

private async updateCfgFile(
    filePath: string,
    section: string | null,
    updates: Record<string, string>
) {
    let configData: any = {}; // Use 'any' for flexibility with ini structure
    try {
        const content = await fs.readFile(filePath, 'utf-8');
        // Parse the INI file content. Note: ini library parses values as strings by default.
        // It might interpret numbers/booleans if not quoted, but our values are always quoted.
        configData = ini.parse(content);
    } catch (error: any) {
        if (error.code === 'ENOENT') {
            this.logger.log(`Config file ${filePath} not found, will create it.`);
            // Initialize configData as an empty object if file doesn't exist
+       } else if (error.code === 'EACCES') {
+           this.logger.error(`Permission denied reading config file ${filePath}`);
+           throw new Error(`Permission denied reading config file ${filePath}: ${error.message}`);
        } else {
            this.logger.error(`Error reading config file ${filePath}:`, error);
            throw error; // Re-throw other errors
        }
    }
web/composables/gql/graphql.ts (1)

1691-1692: Clarify the difference between domain and domains.

The Vms type has both domain and domains fields, which could cause confusion. Consider documenting the difference between these fields or consolidating them if they serve the same purpose.

📜 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 303541e and 90b59a0.

📒 Files selected for processing (12)
  • api/generated-schema.graphql (4 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/auth-request.php.modified.snapshot.php (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts (2 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/auth-request.patch (1 hunks)
  • api/src/utils.ts (2 hunks)
  • web/components/Activation/ActivationPartnerLogoImg.vue (1 hunks)
  • web/components/Activation/graphql/activationCode.query.ts (1 hunks)
  • web/composables/gql/gql.ts (3 hunks)
  • web/composables/gql/graphql.ts (17 hunks)
✅ Files skipped from review due to trivial changes (1)
  • api/src/unraid-api/unraid-file-modifier/modifications/patches/auth-request.patch
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/components/Activation/ActivationPartnerLogoImg.vue
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • web/components/Activation/graphql/activationCode.query.ts
  • api/generated-schema.graphql
  • web/composables/gql/gql.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts (2)
api/src/utils.ts (1)
  • convertWebGuiPathToAssetPath (288-292)
api/src/store/index.ts (1)
  • getters (18-29)
🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[failure] 152-152:
Delete


[failure] 152-152:
More than 1 blank line not allowed

api/src/utils.ts

[failure] 4-4:
Replace ⏎⏎import·strftime·from·'strftime';⏎⏎ with import·strftime·from·'strftime';


[failure] 4-4:
More than 1 blank line not allowed


[failure] 8-8:
More than 1 blank line not allowed


[failure] 11-11:
Delete ⏎⏎⏎⏎


[failure] 13-13:
More than 1 blank line not allowed


[failure] 292-292:
Insert


[failure] 292-292:
Newline required at end of file but not found

🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/customization/customization.service.ts

[failure] 152-152:
Delete


[failure] 152-152:
More than 1 blank line not allowed

api/src/utils.ts

[failure] 4-4:
Replace ⏎⏎import·strftime·from·'strftime';⏎⏎ with import·strftime·from·'strftime';


[failure] 4-4:
More than 1 blank line not allowed


[failure] 8-8:
More than 1 blank line not allowed


[failure] 11-11:
Delete ⏎⏎⏎⏎


[failure] 13-13:
More than 1 blank line not allowed


[failure] 292-292:
Insert


[failure] 292-292:
Newline required at end of file but not found

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (14)
api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots/auth-request.php.modified.snapshot.php (1)

18-18: LGTM! Updated whitelist entry matches expected changes.

The whitelist entry has been correctly updated from partner-logo.svg to UN-logotype-gradient.svg, aligning with the changes to use dynamic path resolution in the auth-request modification.

api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts (1)

9-9: Good approach using dynamic path resolution.

The changes convert a hardcoded path to a dynamic, store-driven approach using the new utility function. This enhances maintainability and flexibility for managing partner logo paths.

Also applies to: 43-47

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

1-470: Well-structured service with good separation of concerns.

The CustomizationService is well-organized with clear separation between initialization, data retrieval, and applying different types of customizations. The error handling is comprehensive and the logging is thorough.

🧰 Tools
🪛 GitHub Check: Build API

[failure] 152-152:
Delete


[failure] 152-152:
More than 1 blank line not allowed

🪛 GitHub Check: Test API

[failure] 152-152:
Delete


[failure] 152-152:
More than 1 blank line not allowed

web/composables/gql/graphql.ts (11)

25-26: Well-defined Port scalar type.

This new scalar type for TCP ports correctly defines the input and output types as numbers, providing better type safety for port-related operations.


39-44: Good replacement for direct URL usage.

The new AccessUrlInput type provides a more structured approach than directly using the URL scalar, with appropriate nullable fields for flexibility.


46-59: Clear activation code type structure.

The new ActivationCode type has well-named fields that clearly represent the customization options available through activation codes.


279-292: Well-documented authentication enums.

The new authentication-related enums have good documentation and clear, descriptive values.


424-425: Improved type safety for ContainerPort.

Making these fields nullable and utilizing the new Port scalar type improves the type safety of the API.


572-579: Added cache control capability.

The new skipCache parameter gives clients better control over Docker data retrieval behavior.


448-452: Coherent customization structure.

The new Customization and PublicPartnerInfo types, along with their associated query fields, create a logical structure for accessing activation code data and partner information.

Also applies to: 1089-1097, 1107-1107, 1126-1126


1218-1218: Resource enum extension.

Adding ACTIVATION_CODE to the Resource enum aligns with the new activation code feature.


1718-1726: Well-defined TypeScript query types.

The new TypeScript type definitions for PartnerInfo and ActivationCode queries match the schema changes and are properly structured.


1114-1115: New utility endpoints.

The addition of getDemo and health endpoints provides useful functionality for testing and monitoring.


823-823:

Details

✅ Verification successful

Verify potential breaking changes in mutation restructuring.

The VM and parity check mutations have been restructured from direct fields on the Mutation type to grouped types. While this improves organization, it's a potential breaking change for existing clients.

Run the following script to check for client code that might be affected:

Also applies to: 835-835, 1043-1061, 1624-1675


🏁 Script executed:

#!/bin/bash
# Look for any code that directly references VM or parity check mutations at the top level

# Check for references to VM mutations
echo "Checking for VM mutation references that might be affected:"
rg -i "mutation.*\b(start|stop|pause|resume|reset|reboot|forceStop)(Vm|VirtualMachine)" --type ts

# Check for references to parity check mutations
echo -e "\nChecking for parity check mutation references that might be affected:"
rg -i "mutation.*\b(start|pause|resume|cancel)(ParityCheck)" --type ts

Length of output: 456


No internal usage of legacy root-level VM or parityCheck mutations detected. The grep scan for direct references (e.g. startVm, stopVm, cancelParityCheck) returned no matches, so this restructuring won’t break internal code.

• Affected file: web/composables/gql/graphql.ts (lines 823, 835, 1043–1061, 1624–1675) now exposes mutations under vm and parityCheck groups.
• No TypeScript or GraphQL documents in this repo reference the old top-level mutation fields.
• External clients should be notified to update their GraphQL calls to the new grouped fields and documentation/CHANGELOG updated accordingly.

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)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

229-306: 🛠️ Refactor suggestion

Address the TODO comment about Redux store updates.

This TODO comment suggests that the state update might not be immediately reflected in the Redux store. Either implement the dispatch to update the Redux store or document why it's not needed.

await this.updateCfgFile(this.configFile, 'display', settingsToUpdate);
this.logger.log('Display settings updated in config file.');
- // @todo: Consider dispatching an action to update Redux store if needed immediately
+ // Update Redux store to reflect the changed display settings
+ try {
+   const dynamixStore = getters.dynamix();
+   if (dynamixStore?.display) {
+     store.dispatch({
+       type: 'dynamix/updateDisplay',
+       payload: settingsToUpdate
+     });
+     this.logger.log('Redux store updated with new display settings.');
+   }
+ } catch (storeError) {
+   this.logger.warn('Failed to update Redux store:', storeError);
+   // Non-fatal error, continue
+ }

39-53: 🛠️ Refactor suggestion

Provide a default value for missing configFile path.

The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

119-145: Consider adding validation error details in the logs.

When validation fails, it would be helpful to log the specific validation errors to aid in troubleshooting.

try {
    const fileContent = await fs.readFile(activationJsonPath, 'utf-8');
    const activationDataRaw = JSON.parse(fileContent);

    const activationDataDto = plainToClass(ActivationCode, activationDataRaw);
-   await validateOrReject(activationDataDto);
+   try {
+       await validateOrReject(activationDataDto);
+   } catch (validationErrors) {
+       if (Array.isArray(validationErrors)) {
+           this.logger.error('Validation errors in activation data:', 
+               validationErrors.map(err => Object.values(err.constraints || {})).flat());
+       }
+       throw validationErrors;
+   }

    // Cache the validated data
    this.activationData = activationDataDto;
    this.logger.debug('Activation data fetched and cached.');
    return this.activationData;

152-166: Consider adding path validation to prevent path traversal.

The public methods that return web GUI paths should validate that the paths are within the expected bounds to prevent path traversal issues.

public async getCaseIconWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.caseModelSource)) {
-       return convertWebGuiPathToAssetPath(paths.caseModelTarget);
+       const assetPath = convertWebGuiPathToAssetPath(paths.caseModelTarget);
+       // Validate path is a subpath of the expected webgui directory
+       if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid webgui path detected: ${paths.caseModelTarget}`);
+           return null;
+       }
+       return assetPath;
    }
    return null;
}
📜 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 90b59a0 and 1765b89.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • api/src/utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/utils.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (6)
web/composables/gql/graphql.ts (1)
  • ActivationCode (46-59)
packages/unraid-api-plugin-connect/src/helpers/utils.ts (1)
  • fileExists (5-8)
api/src/store/modules/paths.ts (4)
  • paths (112-116)
  • partnerBannerSource (101-103)
  • partnerBannerTarget (104-106)
  • caseModelSource (95-97)
api/src/store/index.ts (2)
  • getters (18-29)
  • store (6-12)
api/src/utils.ts (1)
  • convertWebGuiPathToAssetPath (280-284)
api/src/core/utils/clients/emcmd.ts (1)
  • emcmd (12-41)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (11)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (11)

1-27: Well-organized imports and class setup.

The imports are logically grouped and the class is well-structured with proper type definitions for member variables.


28-37: LGTM! First boot setup flag implementation is robust.

The method correctly creates the activation directory if needed, checks for the existence of the flag file, and appropriately creates it if missing.


54-92: Robust initialization and activation directory handling.

The initialization process correctly sets up paths, checks for directory existence, and handles the first boot setup flag. The error handling is comprehensive with appropriate logging.


94-112: Good path resolution with proper error handling.

The method correctly finds the activation JSON file with appropriate error handling and logging.


146-150: Comprehensive error handling in activation data processing.

The error handling for the activation data processing is well-implemented with appropriate logging and return values.


168-201: Robust customization application flow.

The method appropriately checks for activation data, verifies directory existence, and applies customizations in a logical sequence with comprehensive error handling.


203-227: Well-implemented partner banner setup.

The banner setup correctly handles file existence checks, copying, and error handling.


307-309: Good error handling for display settings.

The error handling for display settings is appropriate with informative error logging.


311-357: Well-implemented case model configuration.

The method effectively manages the case model configuration with appropriate checks, error handling, and logging.


359-412: Comprehensive server identity implementation.

The server identity is properly applied with appropriate error handling, emcmd execution, and nginx/DNS updates.


414-468: Well-designed config file update utility.

The updateCfgFile utility method is well-designed to handle both section-specific and root-level updates to INI files with proper error handling.

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)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

45-53: Provide a default value for missing configFile path.

The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}

28-37: 🛠️ Refactor suggestion

Enhance createOrGetFirstBootSetupFlag method to handle uninitialized paths.

This method relies on this.activationDir being initialized, but could fail if called before onModuleInit. Consider accepting an optional parameter or checking for initialization.

-async createOrGetFirstBootSetupFlag(): Promise<boolean> {
+async createOrGetFirstBootSetupFlag(activationDirOverride?: string): Promise<boolean> {
+   const dirToUse = activationDirOverride || this.activationDir;
+   if (!dirToUse) {
+       throw new Error('Activation directory path not initialized');
+   }
-   await fs.mkdir(this.activationDir, { recursive: true });
-   if (await fileExists(this.hasRunFirstBootSetup)) {
+   await fs.mkdir(dirToUse, { recursive: true });
+   const flagPath = activationDirOverride ? 
+       path.join(dirToUse, '.done') : 
+       this.hasRunFirstBootSetup;
+   if (await fileExists(flagPath)) {
        this.logger.log('First boot setup flag file already exists.');
        return true; // Indicate setup was already done based on flag presence
    }
-   await fs.writeFile(this.hasRunFirstBootSetup, 'true');
+   await fs.writeFile(flagPath, 'true');
    this.logger.log('First boot setup flag file created.');
    return false; // Indicate setup was just marked as done
}
🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (4)

177-186: Simplify redundant directory check.

This directory check is redundant since it's already performed in onModuleInit. Consider removing it or adding a comment explaining why it's necessary to check again.

- // Check if activation dir exists (redundant if onModuleInit succeeded, but safe)
- try {
-     await fs.access(this.activationDir);
- } catch (dirError: any) {
-     if (dirError.code === 'ENOENT') {
-         this.logger.warn('Activation directory disappeared after init? Skipping.');
-         return;
-     }
-     throw dirError; // Rethrow other errors
- }

243-267: Move type definition outside method for reusability.

The DisplayMapping type is defined within the method scope, limiting its reusability. Consider moving it to the class level or exporting it from a shared types file.

+// Define types at the class or module level
+type DisplayMapping = {
+    key: string;
+    transform?: (v: any) => string;
+    skipIfEmpty?: boolean;
+};

@Injectable()
export class CustomizationService implements OnModuleInit {
    // ... existing properties
    
    private async applyDisplaySettings() {
        // ... existing code
        
        // Map activation data properties to their corresponding config keys
-       type DisplayMapping = {
-           key: string;
-           transform?: (v: any) => string;
-           skipIfEmpty?: boolean;
-       };
        
        const displayMappings: Record<string, DisplayMapping> = {
            // ... existing mappings
        };

269-277: Replace forEach with a safer reduce operation.

Using forEach with side effects can be error-prone. Consider using reduce for a more functional approach that is easier to test and reason about.

- Object.entries(displayMappings).forEach(([prop, mapping]) => {
-     const value = this.activationData?.[prop];
-     if (value !== undefined && value !== null) {
-         const transformedValue = mapping.transform ? mapping.transform(value) : value;
-         if (!mapping.skipIfEmpty || transformedValue) {
-             settingsToUpdate[mapping.key] = transformedValue;
-         }
-     }
- });
+ const settingsToUpdate = Object.entries(displayMappings).reduce((acc, [prop, mapping]) => {
+     const value = this.activationData?.[prop];
+     if (value !== undefined && value !== null) {
+         const transformedValue = mapping.transform ? mapping.transform(value) : value;
+         if (!mapping.skipIfEmpty || transformedValue) {
+             acc[mapping.key] = transformedValue;
+         }
+     }
+     return acc;
+ }, {} as Record<string, string>);

419-433: Improve error handling for config file reading.

The error handling when reading the config file could be more robust. Consider explicitly checking for file existence before attempting to read it.

let configData: any = {}; // Use 'any' for flexibility with ini structure
try {
+   // Check if file exists first
+   if (await fileExists(filePath)) {
        const content = await fs.readFile(filePath, 'utf-8');
        // Parse the INI file content. Note: ini library parses values as strings by default.
        // It might interpret numbers/booleans if not quoted, but our values are always quoted.
        configData = ini.parse(content);
+   } else {
+       this.logger.log(`Config file ${filePath} not found, will create it.`);
+   }
} catch (error: any) {
-   if (error.code === 'ENOENT') {
-       this.logger.log(`Config file ${filePath} not found, will create it.`);
-       // Initialize configData as an empty object if file doesn't exist
-   } else {
-       this.logger.error(`Error reading config file ${filePath}:`, error);
-       throw error; // Re-throw other errors
-   }
+   this.logger.error(`Error reading config file ${filePath}:`, error);
+   throw error; // Re-throw errors
}
📜 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 1765b89 and 09369f5.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/customization/customization.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

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 (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (3)

17-18: ⚠️ Potential issue

Duplicate vi.mock – second factory silently ignored

vi.mock('@app/core/utils/clients/emcmd.js') is declared twice.
Because Vitest hoists mocks, the first (auto-mock) wins and the factory at lines 47-68 is never applied, leaving emcmd an empty stub.

-vi.mock('@app/core/utils/clients/emcmd.js');
-
-
-vi.mock('@app/core/utils/clients/emcmd.js', async () => {
+vi.mock('@app/core/utils/clients/emcmd.js', async () => {

Remove the first call (or merge them) so your custom mock is actually used.

Also applies to: 47-48


121-128: 🛠️ Refactor suggestion

Manual re-assignment hides initialization bugs

Reassigning internal paths after service construction bypasses the very behaviour the test is meant to verify (initialisation from the store). It also forces every test to repeat this boilerplate.

Better: stub store.getState() (or the relevant getters) to return mockPaths and let onModuleInit populate these fields naturally.

This was pointed out in a previous review and is still unresolved.


786-789: ⚠️ Potential issue

Spies registered after the call never fire

setupPartnerBannerSpy and applyDisplaySettingsSpy are created after applyActivationCustomizations() has already executed, so they will always report “not called” even if the methods actually ran.

Move the spy creation before invoking the method:

-await (service as any).applyActivationCustomizations();
-const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner');
-const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings');
+const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner');
+const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings');
+await (service as any).applyActivationCustomizations();

This makes the assertion meaningful.

📜 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 09369f5 and 7fc5585.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
🧰 Additional context used
🪛 GitHub Check: Test API
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts

[failure] 701-701:
Replace 'Error·applying·server·identity:·%o',·emcmdError with ⏎················'Error·applying·server·identity:·%o',⏎················emcmdError⏎············

🪛 GitHub Check: Build API
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts

[failure] 701-701:
Replace 'Error·applying·server·identity:·%o',·emcmdError with ⏎················'Error·applying·server·identity:·%o',⏎················emcmdError⏎············

🪛 GitHub Actions: CI - Main (API)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts

[error] 701-701: Prettier formatting error: Replace 'Error applying server identity: %o', emcmdError with a properly formatted multiline string as per prettier/prettier rule.

⏰ 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: 3

♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)

121-128: Manual path re-assignment masks init bugs
This test suite still overwrites activationDir, configFile, etc., after creating the service instance. As noted in the previous review, doing so defeats the purpose of verifying that onModuleInit correctly reads the store.

Please fix the store mock instead of mutating internal state.


15-19: ⚠️ Potential issue

Duplicate mock declarations – only the first one takes effect

vi.mock('@app/core/utils/clients/emcmd.js') is declared twice (lines 17 and 47).
Vitest hoists mocks; the factory on lines 47-68 is silently ignored, leaving emcmd as an empty stub and invalidating several assertions.

Delete the first call:

-vi.mock('@app/core/utils/clients/emcmd.js');

Retain the factory version to keep the typed mock response.

📜 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 7fc5585 and 0db8f76.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Build and Deploy Plugin / Build and Deploy Plugin
  • 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: 0

♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (2)

17-18: ⚠️ Potential issue

Duplicate vi.mock for emcmd – the custom factory is silently ignored

Vitest hoists mocks; the first vi.mock('@app/core/utils/clients/emcmd.js') (line 17) wins and the factory in lines 50-71 is never used, leaving emcmd as an empty stub and breaking assertions that rely on the mocked response.

-vi.mock('@app/core/utils/clients/emcmd.js');

Delete the first call (or merge the two) so the factory takes effect.

Also applies to: 50-71


124-131: 🛠️ Refactor suggestion

Manual property re-assignment hides real-world failures

The service is supposed to derive activationDir, configFile, etc. from the mocked store during onModuleInit, yet the tests overwrite those fields right after instantiation. This masks bugs in the initialisation path.

Instead, fix the store mock so the service picks up the correct values automatically and drop these lines altogether.

-// Re-assign paths manually in beforeEach AFTER mocks are cleared and service instantiated
-(service as any).activationDir = activationDir;
-(service as any).hasRunFirstBootSetup = doneFlag;
-(service as any).configFile = userDynamixCfg;
-(service as any).caseModelCfg = caseModelCfg;
-(service as any).identCfg = identCfg;
🧹 Nitpick comments (4)
package.json (3)

29-33: Re-evaluate onlyBuiltDependencies entries

Dev‐only tools like simple-git-hooks typically shouldn’t be listed here—it may bloat your production install. Consider restricting this array to true runtime dependencies (e.g., core-js, es5-ext, protobufjs) and removing dev-only packages.


48-50: Configure simple-git-hooks pre-commit hook

The pre-commit hook is correctly defined. You might also consider adding hooks like commit-msg for message linting or pre-push for additional checks.


51-55: Extend lint-staged file patterns

Your current globs target JS/TS only. To cover the full monorepo, consider including .vue, .json, or other relevant formats so all staged files get linted.

api/src/core/utils/clients/emcmd.ts (1)

12-15: Consider caching the dynamic import to avoid per-call overhead

import() is cached by Node, but you still pay the await cost on every invocation.
If emcmd is called frequently, the extra micro-await adds latency. A tiny wrapper keeps the laziness you want while paying the price only once:

-export const emcmd = async (commands: LooseObject) => {
-    const { getters } = await import('@app/store/index.js');
+let cachedGetters: typeof import('@app/store/index.js').getters | null = null;
+export const emcmd = async (commands: LooseObject) => {
+    if (!cachedGetters) {
+        ({ getters: cachedGetters } = await import('@app/store/index.js'));
+    }
+    const getters = cachedGetters!;

Nice and simple, no global side-effects.

📜 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 0db8f76 and b1b1ac2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • .husky/_/pre-commit (1 hunks)
  • api/src/core/utils/clients/emcmd.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • package.json (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .husky/_/pre-commit
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
⏰ 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 (2)
package.json (2)

15-16: Verify new scripts and postinstall behavior

Confirm that sync-webgui-repo handles errors and is idempotent, and ensure that running simple-git-hooks in the postinstall hook doesn’t conflict with other install scripts or sequences.


44-47: Add lint-staged and simple-git-hooks as devDependencies

Good move to centralize pre-commit tooling. These devDependencies align well with the new Git-hook setup.

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 (6)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (6)

16-19: Duplicate mocks for emcmd - only the first one is honored

There are two separate mock declarations for the same module. Vitest hoists mocks, so the first mock (auto-mock) is used and the detailed factory mock is ignored.

-vi.mock('@app/core/utils/clients/emcmd.js');

Keep the factory mock for proper response handling.


38-48: Mocking store does not override store.getState()

The current mock only adds a getters field but doesn't provide a getState() implementation, which is what the service actually uses.

vi.mock('@app/store/index.js', async () => {
    const actual = await vi.importActual('@app/store/index.js');
    return {
        ...actual,
+       store: {
+           getState: () => ({ paths: mockPaths }),
+       },
        getters: {
            paths: vi.fn(() => mockPaths),
            dynamix: vi.fn(() => mockDynamixState),
            emhttp: vi.fn(() => mockEmhttpState),
        },
    };
});

125-131: Manual path re-assignment makes tests less reliable

Service paths are manually reassigned after instantiation, which means tests aren't accurately verifying initialization logic. This approach hides potential initialization issues.

- // Re-assign paths manually in beforeEach AFTER mocks are cleared and service instantiated
- // This simulates the dynamic import within onModuleInit
- (service as any).activationDir = activationDir;
- (service as any).hasRunFirstBootSetup = doneFlag;
- (service as any).configFile = userDynamixCfg;
- (service as any).caseModelCfg = caseModelCfg;
- (service as any).identCfg = identCfg;

Fix the store mock as suggested above instead.


148-164: Complex approach for testing missing config path

The test for handling a missing dynamix user config path is overly complex. You're temporarily modifying the mockPaths object and then restoring it, which is fragile.

Consider using the mockImplementationOnce approach instead to temporarily change the behavior for a single test:

it('should log error if dynamix user config path is missing', async () => {
-    // Temporarily modify mockPaths to simulate missing user config path
-    const originalPaths = { ...mockPaths };
-    mockPaths['dynamix-config'] = [mockPaths['dynamix-config'][0]]; // Only keep default config
+    // Save original mock to restore later
+    const originalGetState = vi.mocked(store.getState);
+    
+    // Mock store to return paths without dynamix-config[1]
+    vi.mocked(store.getState).mockReturnValueOnce({
+        paths: {
+            ...mockPaths,
+            'dynamix-config': [mockPaths['dynamix-config'][0]],
+        }
+    });

    await service.onModuleInit();

    expect(loggerErrorSpy).toHaveBeenCalledWith(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
    // Expect subsequent operations that rely on configFile to fail or not run
    expect(fs.writeFile).not.toHaveBeenCalledWith(doneFlag, 'true'); // Setup should bail early

-    // Restore original paths
-    mockPaths['dynamix-config'] = originalPaths['dynamix-config'];
+    // Restore original mock
+    vi.mocked(store.getState).mockImplementation(originalGetState);
});

820-826: Ineffective test setup - spies registered after method call

The spies on setupPartnerBanner and applyDisplaySettings are registered after applyActivationCustomizations has already been called, making the assertions ineffective.

+    // Set up spies BEFORE calling the method
+    const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner');
+    const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings');

    await (service as any).applyActivationCustomizations();

    expect(loggerWarnSpy).toHaveBeenCalledWith(
        'Activation directory disappeared after init? Skipping.'
    );
    // Ensure no customization methods were called implicitly by checking their side effects
-    const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner');
-    const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings');
    expect(setupPartnerBannerSpy).not.toHaveBeenCalled();
    expect(applyDisplaySettingsSpy).not.toHaveBeenCalled();

701-724: Prettier formatting will break CI for error message

The long single-line error expectation will fail Prettier formatting checks in CI.

-expect(loggerErrorSpy).toHaveBeenCalledWith('Error applying server identity: %o', emcmdError);
+expect(loggerErrorSpy).toHaveBeenCalledWith(
+  'Error applying server identity: %o',
+  emcmdError,
+);

Similar formatting issues may exist elsewhere in the file.

🧹 Nitpick comments (4)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (4)

4-4: Consider adding URL validation for partnerUrl

You're importing various validators from class-validator but only using IsString for partnerUrl fields. Consider adding IsUrl validation to ensure valid URLs are provided.

- import { IsBoolean, IsIn, IsOptional, IsString, IsUrl } from 'class-validator';
+ import { IsBoolean, IsIn, IsOptional, IsString, IsUrl } from 'class-validator';

Then apply it to the partnerUrl fields:

  @Field(() => String, { nullable: true })
  @IsOptional()
  @IsString()
+ @IsUrl()
  @Transform(({ value }) => sanitizeString(value))
  partnerUrl?: string;

6-7: Hex color validation function could be more robust

The current regex only matches exact 3 or 6 character hex colors. Consider adding support for 4 and 8 character hex codes (which include alpha channel) or using a library like color for more robust validation.


18-31: Enhance hex color validation with more informative error handling

The current implementation silently returns an empty string for invalid colors. Consider adding a warning log or throwing a specific error that can be caught and handled appropriately.

const sanitizeAndValidateHexColor = (value: any): string => {
    let sanitized = sanitizeString(value);
    if (typeof sanitized === 'string') {
        // Check if it's a 3 or 6 character hex string without '#'
        if (/^([0-9A-F]{3}){1,2}$/i.test(sanitized)) {
            sanitized = `#${sanitized}`; // Prepend '#'
        }
        // Now validate if it's a standard hex color format
        if (isHexColor(sanitized)) {
            return sanitized;
        }
+       // Log or output more information about the invalid input
+       console.warn(`Invalid hex color format: ${value}`);
    }
    return ''; // Return empty string if not a valid hex color after potential modification
};

115-122: Consider adding type safety for boolean transformation

The current transformation converts string 'yes' to boolean true, but doesn't handle other truthy/falsy string values like 'true', '1', etc. Consider using a more comprehensive approach for boolean conversion.

@Field(() => Boolean, { nullable: true })
@IsOptional()
@Transform(({ value }) => {
    if (typeof value === 'boolean') return value;
    const sanitized = sanitizeString(value);
-   return sanitized === 'yes';
+   return ['yes', 'true', '1', 'on'].includes(sanitized.toLowerCase());
})
showBannerGradient?: boolean = true;
📜 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 5a107a1 and ed10f9c.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1)
web/composables/gql/graphql.ts (3)
  • PublicPartnerInfo (1089-1097)
  • ActivationCode (46-59)
  • Customization (448-452)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (7)
api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (3)

82-83: Good use of length limitation for serverName

The 15-character limit for serverName is good practice, as it prevents potential buffer overflows or UI issues in downstream systems.


124-128: Good use of IsIn validator for theme property

Using @IsIn validator with specific allowed theme values ensures that only valid themes can be applied, preventing errors in the UI.


131-138: Customization class provides a clean aggregation structure

The Customization class effectively aggregates related data models, making the GraphQL schema more organized and easier to query.

api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (4)

395-412: Good test for handling invalid hex colors

The test correctly verifies that invalid hex colors are transformed to empty strings, which allows validation to pass while preventing invalid data from propagating.


414-430: Excellent test for hex color normalization

This test properly verifies that hex colors provided without the '#' prefix are correctly normalized, ensuring consistent data format.


474-482: Comprehensive test for partner banner setup

The test thoroughly validates the banner setup process, including file copying and appropriate logging.


959-1112: Well-structured unit tests for the updateCfgFile utility

The test suite for updateCfgFile is thorough and covers various scenarios:

  • Creating new files with and without sections
  • Merging updates with existing content
  • Adding new sections to existing files
  • Error handling for file operations

This ensures the configuration management is robust and reliable.

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 (12)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (7)

46-54: Provide a default value for missing configFile path.

The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}

153-159: Add path validation to prevent path traversal issues.

The method getCaseIconWebguiPath should validate paths to ensure they're within expected bounds before converting them to web asset paths.

public async getCaseIconWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.caseModelSource)) {
+       // Validate path is within expected bounds
+       if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.caseModelTarget);
    }
    return null;
}

231-240: Remove redundant dynamic import.

The code re-imports getters despite it already being statically imported at the top of the file. This adds unnecessary I/O and module initialization overhead.

-const { getters } = await import('@app/store/index.js');
 if (!this.activationData) {
     this.logger.warn('No activation data available for display settings.');
     return;
 }

 this.logger.log('Applying display settings...');
-const currentDisplaySettings = getters.dynamix()?.display || {};
+const currentDisplaySettings = getters.dynamix()?.display || {};
 this.logger.debug('Current display settings from store:', currentDisplaySettings);

250-267: Add type safety for activation data properties.

The code assumes the activation data properties are of the expected types for the transformations. Consider adding explicit type checks for better safety.

const displayMappings: Record<string, DisplayMapping> = {
-   header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true },
+   header: { 
+       key: 'header', 
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', 
+       skipIfEmpty: true 
+   },
-   headermetacolor: {
-       key: 'headermetacolor',
-       transform: (v: string) => v.replace('#', ''),
-       skipIfEmpty: true,
-   },
+   headermetacolor: {
+       key: 'headermetacolor',
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '',
+       skipIfEmpty: true,
+   },
    // Apply similar changes to other properties

283-290: Banner existence check targets wrong file.

The code verifies partnerBannerSource, i.e. the file before it is copied. If the copy fails, we would still force banner=image, resulting in a broken UI.

-const partnerBannerSource = paths.partnerBannerSource;
-...
-if (await fileExists(partnerBannerSource)) {
+const partnerBannerTarget = paths.partnerBannerTarget;
+...
+if (await fileExists(partnerBannerTarget)) {

420-467: Missing directory-existence guard may break first boot.

updateCfgFile blindly calls fs.writeFile(filePath, …); if the parent directory is absent the write will throw ENOENT.

+await fs.mkdir(path.dirname(filePath), { recursive: true });
 await fs.writeFile(filePath, newContent + '\n');

161-167: 🛠️ Refactor suggestion

Add path validation to prevent path traversal issues in getPartnerLogoWebguiPath.

Similar to the getCaseIconWebguiPath method, this function should validate paths to ensure they're within expected bounds.

public async getPartnerLogoWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.partnerLogoSource)) {
+       // Validate path is within expected bounds
+       if (!paths.partnerLogoTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.partnerLogoTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.partnerLogoTarget);
    }
    return null;
}
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (5)

16-19: Duplicate mocks for emcmd – only the first one is honoured.

vi.mock('@app/core/utils/clients/emcmd.js') is declared twice. Vitest hoists mocks, so the first statement (auto-mock) wins and the factory at lines 50-71 is silently ignored, leaving emcmd as an empty stub.

-vi.mock('@app/core/utils/clients/emcmd.js');
-
 ... 

38-48: Mocking store does not override store.getState().

CustomizationService calls store.getState() via getters, but this mock only adds a getters field. Provide a minimal stub for getState:

vi.mock('@app/store/index.js', async () => {
    const actual = await vi.importActual('@app/store/index.js');
    return {
        ...actual,
+       store: {
+           getState: () => ({ paths: mockPaths }),
+       },
        getters: {
            paths: vi.fn(() => mockPaths),
            dynamix: vi.fn(() => mockDynamixState),
            emhttp: vi.fn(() => mockEmhttpState),
        },
    };
});

131-137: Manual path re-assignment makes tests less reliable.

The service paths are manually reassigned after instantiation, which means the tests aren't accurately verifying initialization from the store. This approach is error-prone and hides initialization issues.

Remove these manual assignments and fix the store mock instead, as suggested in the previous comment.


826-832: Ineffective test setup - spies registered after method call.

The spies on setupPartnerBanner and applyDisplaySettings are registered after applyActivationCustomizations has already been called, making the assertions ineffective.

+    // Set up spies BEFORE calling the method
+    const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner');
+    const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings');

    await (service as any).applyActivationCustomizations();

    expect(loggerWarnSpy).toHaveBeenCalledWith(
        'Activation directory disappeared after init? Skipping.'
    );
    // Ensure no customization methods were called implicitly by checking their side effects
-    const setupPartnerBannerSpy = vi.spyOn(service as any, 'setupPartnerBanner');
-    const applyDisplaySettingsSpy = vi.spyOn(service as any, 'applyDisplaySettings');
    expect(setupPartnerBannerSpy).not.toHaveBeenCalled();
    expect(applyDisplaySettingsSpy).not.toHaveBeenCalled();

701-731: Fix the multi-line expect statement formatting.

CI fails because this long .toHaveBeenCalledWith line exceeds the formatter rules.

-expect(loggerErrorSpy).toHaveBeenCalledWith('Error applying server identity: %o', emcmdError);
+expect(loggerErrorSpy).toHaveBeenCalledWith(
+   'Error applying server identity: %o',
+   emcmdError
+);
🧹 Nitpick comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

359-411: Consider separating server identity application from UI reload.

The applyServerIdentity method combines two distinct operations: updating server identity configuration and reloading the web UI. This violates the Single Responsibility Principle. Consider separating these operations for better maintainability.

private async applyServerIdentity() {
    const { getters, store } = await import('@app/store/index.js');
    if (!this.activationData) {
        this.logger.warn('No activation data available for server identity setup.');
        return;
    }

    this.logger.log('Applying server identity...');
    
    // ... existing code ...
    
    try {
        // Update ident.cfg first
        await this.updateCfgFile(this.identCfg, null, paramsToUpdate);
        this.logger.log(`Server identity updated in ${this.identCfg}`);

        // Trigger emhttp update via emcmd
        const updateParams = { ...paramsToUpdate, changeNames: 'Apply' };
        this.logger.log(`Calling emcmd with params: %o`, updateParams);

        await sleep(10000);
        await emcmd(updateParams);

        this.logger.log('emcmd executed successfully.');
+       
+       await this.reloadWebUI();
+    } catch (error) {
+        this.logger.error('Error applying server identity: %o', error);
+    }
+}
+
+// Separate method for UI reload
+private async reloadWebUI() {
+    try {
+        const { store } = await import('@app/store/index.js');
        await store.dispatch(reloadNginxAndUpdateDNS());
        this.logger.log('Nginx reloaded and DNS updated successfully.');
    } catch (error) {
-        this.logger.error('Error applying server identity: %o', error);
+        this.logger.error('Error reloading web UI: %o', error);
    }
}

402-404: Document the reason for the 10-second sleep.

The code includes a significant 10-second sleep before executing the emcmd command. Without explanation, this delay seems excessive and could confuse future developers.

+       // A 10-second delay is needed before executing emcmd to ensure
+       // that system has enough time to process previous configuration changes
        await sleep(10000);
        await emcmd(updateParams);
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)

139-147: Enhance fileExists mock for more robust tests.

The current fileExists mock implementation only covers a limited set of specific paths. Consider a more comprehensive approach that handles all possible paths that might be checked during test execution.

// Mock fileExists needed by customization methods
vi.mocked(fileExists).mockImplementation(async (p) => {
    // Assume assets exist by default for these tests unless overridden
-   return (
-       p === partnerBannerSource ||
-       p === bannerAssetPath ||
-       p === caseModelAssetPath ||
-       p === bannerDestPath
-   );
+   // Handle specific test-critical paths explicitly
+   if ([partnerBannerSource, bannerAssetPath, caseModelAssetPath, bannerDestPath].includes(p)) {
+       return true;
+   }
+   
+   // Handle common path patterns
+   if (p.includes('/assets/') && !p.includes('.done')) {
+       return true; // Most asset files exist by default
+   }
+   
+   if (p === doneFlag) {
+       return false; // .done flag doesn't exist by default
+   }
+   
+   // Log unexpected paths to help identify missed mocks
+   console.log(`Unmocked fileExists call for path: ${p}`);
+   return false; // Default to false for safety
});
📜 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 ed10f9c and d7d673a.

📒 Files selected for processing (4)
  • api/src/core/utils/clients/emcmd.ts (2 hunks)
  • api/src/environment.ts (0 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • api/src/environment.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/core/utils/clients/emcmd.ts
⏰ 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

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 (5)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (5)

46-54: Provide a default value for missing configFile path.

The code correctly logs an error when the configFile path is missing, but still proceeds with initialization. It would be better to either throw an error or use a default path to prevent unexpected failures in downstream operations.

this.configFile = paths['dynamix-config']?.[1];
if (!this.configFile) {
    this.logger.error(
        "Could not resolve user dynamix config path (paths['dynamix-config'][1]) from store."
    );
-   // Handle error appropriately - maybe throw, or use a default?
-   // For now, we'll let subsequent operations fail if configFile is needed.
-   return; // Stop initialization if critical path is missing
+   this.configFile = '/etc/local/emhttp/dynamix.cfg'; // Use a sensible default
+   this.logger.warn(`Using default config path: ${this.configFile}`);
}

153-159: Add path validation to prevent path traversal issues.

Methods like getCaseIconWebguiPath should validate paths to prevent potential path traversal vulnerabilities when converting paths.

public async getCaseIconWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.caseModelSource)) {
+       // Validate path is within expected bounds
+       if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.caseModelTarget);
    }
    return null;
}

249-266: Add type safety for activation data properties.

The code assumes the activation data properties are of the expected types for the transformations. Consider adding explicit type checks for better safety.

const displayMappings: Record<string, DisplayMapping> = {
-   header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true },
+   header: { 
+       key: 'header', 
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', 
+       skipIfEmpty: true 
+   },
-   headermetacolor: {
-       key: 'headermetacolor',
-       transform: (v: string) => v.replace('#', ''),
-       skipIfEmpty: true,
-   },
+   headermetacolor: {
+       key: 'headermetacolor',
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '',
+       skipIfEmpty: true,
+   },
    // Apply similar changes to other properties

282-289: Banner existence check targets wrong file.

The code verifies partnerBannerSource, i.e. the file before it is copied. If the copy fails (disk full, permissions, etc.) we would still force banner=image, resulting in a broken UI.

-const partnerBannerSource = paths.partnerBannerSource;
-...
-if (await fileExists(partnerBannerSource)) {
+const partnerBannerTarget = paths.partnerBannerTarget;
+...
+if (await fileExists(partnerBannerTarget)) {

412-459: Missing directory-existence guard may break first boot.

updateCfgFile blindly calls fs.writeFile(filePath, …); if the parent directory is absent (/mock/user on fresh installs, read-only mounts, etc.) the write will throw ENOENT.

+await fs.mkdir(path.dirname(filePath), { recursive: true });
 await fs.writeFile(filePath, newContent + '\n');
🧹 Nitpick comments (2)
api/src/core/utils/clients/emcmd.ts (1)

2-2: Unnecessary import of ArrayDisk.

The ArrayDisk import on line 11 appears to be unused in this file and can be safely removed.

-import { ArrayDisk } from '@app/unraid-api/graph/resolvers/array/array.model.js';
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)

368-375: Consider optimizing state access pattern.

You're retrieving values from the Redux store in multiple places. Consider extracting this into a reusable helper function or memoizing the values to avoid repeated access.

+private getIdentityValues() {
+    const currentEmhttpState = getters.emhttp();
+    return {
+        currentName: currentEmhttpState?.var?.name || '',
+        currentSysModel: currentEmhttpState?.var?.sysModel || '',
+        currentComment: currentEmhttpState?.var?.comment || ''
+    };
+}

private async applyServerIdentity() {
    // ...
-    const currentEmhttpState = getters.emhttp();
-    const currentName = currentEmhttpState?.var?.name || '';
-    const currentSysModel = currentEmhttpState?.var?.sysModel || '';
-    const currentComment = currentEmhttpState?.var?.comment || '';
+    const { currentName, currentSysModel, currentComment } = this.getIdentityValues();
    // ...
}
📜 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 b8f2904 and 017d2d5.

📒 Files selected for processing (3)
  • api/src/core/utils/clients/emcmd.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Test API
  • GitHub Check: Build API
  • GitHub Check: Build Web App
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
api/src/core/utils/clients/emcmd.ts (4)

20-20: Dynamic import pattern is good for preventing circular dependencies.

The dynamic import of getters helps prevent potential circular dependencies.


23-25: Good error handling for missing socket path.

Adding an explicit check for socketPath improves error detection and provides a clear error message.


27-54: Robust CSRF token retry implementation.

The retry logic with exponential backoff is well-implemented. It provides resilience when tokens aren't immediately available during system startup.


56-57: HTTP method changed from GET to POST.

Changing from got.get to got.post may have implications for server-side handlers.

Verify that server-side handlers for /update.htm properly handle POST requests with the same parameter structure that was previously sent with GET.

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

399-399: Good use of waitForToken parameter.

The emcmd function is called with { waitForToken: true }, ensuring that the CSRF token is available before attempting the command execution. This aligns well with the changes made to the emcmd utility.

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)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)

278-285: Banner existence check still targets source instead of target

applyDisplaySettings verifies paths.partnerBannerSource after the copy, which does not guarantee that the banner was actually written to the Web-GUI images directory. The resilient check is to look at paths.partnerBannerTarget (or the computed bannerDestPath) so the UI only switches to banner=image when the destination file is present.

-const partnerBannerSource = paths.partnerBannerSource;
-...
-if (await fileExists(partnerBannerSource)) {
-    settingsToUpdate['banner'] = 'image';
-    this.logger.debug(`Webgui banner exists at ${partnerBannerSource}, setting banner=image.`);
+const partnerBannerTarget = paths.partnerBannerTarget;
+...
+if (await fileExists(partnerBannerTarget)) {
+    settingsToUpdate['banner'] = 'image';
+    this.logger.debug(`Webgui banner exists at ${partnerBannerTarget}, setting banner=image.`);
 }
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)

130-136: Manual path re-assignment hides initialisation bugs

Re-populating activationDir, configFile, … after instantiating the service means the tests still pass even if onModuleInit fails to read the store – exactly the false-positive noted in previous reviews. Refactor the mock store so onModuleInit picks up accurate paths and drop this manual reassignment.

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1)

536-550: Assertion hard-codes every field – consider objectContaining

These lines assert the full object produced by applyDisplaySettings. Any future field (e.g. a new display option) will break the test unnecessarily. Prefer:

expect(updateSpy).toHaveBeenCalledWith(
  userDynamixCfg,
  'display',
  expect.objectContaining({
    header: '112233',
    theme: 'black',
    banner: 'image',
  })
);

to keep the test focused on behaviour, not implementation details.

📜 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 6d17eea and 9e72161.

📒 Files selected for processing (3)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/unraid-file-modifier/modifications/case-model-copier.modification.ts
⏰ 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: 0

♻️ Duplicate comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

266-283: Add type safety for activation data properties

The code assumes the activation data properties are of the expected types for the transformations. Consider adding explicit type checks for better safety.

const displayMappings: Record<string, DisplayMapping> = {
-   header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true },
+   header: { 
+       key: 'header', 
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', 
+       skipIfEmpty: true 
+   },
-   headermetacolor: {
-       key: 'headermetacolor',
-       transform: (v: string) => v.replace('#', ''),
-       skipIfEmpty: true,
-   },
+   headermetacolor: {
+       key: 'headermetacolor',
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '',
+       skipIfEmpty: true,
+   },
    // Apply similar changes to other properties

337-345: 🛠️ Refactor suggestion

Ensure parent directory exists before writing case-model.cfg

Add a directory existence check before writing to the case model configuration file to prevent potential errors on first-boot systems.

-const modelToSet = path.basename(paths.caseModelTarget); // e.g., 'case-model.png'
-await fs.writeFile(this.caseModelCfg, modelToSet);
+const modelToSet = path.basename(paths.caseModelTarget); // e.g., 'case-model.png'
+await fs.mkdir(path.dirname(this.caseModelCfg), { recursive: true });
+await fs.writeFile(this.caseModelCfg, modelToSet);
🧹 Nitpick comments (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

170-176: Consider path validation for case icon path

While the method works correctly, it would be safer to validate the returned path to prevent potential path traversal issues.

public async getCaseIconWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.caseModelSource)) {
+       // Validate path is within expected bounds
+       if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.caseModelTarget);
    }
    return null;
}

178-184: Consider path validation for partner logo path

Similar to the case icon method, adding path validation would improve security.

public async getPartnerLogoWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.partnerLogoSource)) {
+       // Validate path is within expected bounds
+       if (!paths.partnerLogoTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.partnerLogoTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.partnerLogoTarget);
    }
    return null;
}
📜 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 9e72161 and 6903775.

📒 Files selected for processing (12)
  • api/dev/activation/applied.txt (1 hunks)
  • api/dev/dynamix/dynamix.cfg (1 hunks)
  • api/src/unraid-api/auth/authentication.guard.ts (4 hunks)
  • api/src/unraid-api/auth/public.decorator.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
  • api/src/utils.ts (1 hunks)
  • web/components/Activation/store/activationCodeModal.ts (1 hunks)
  • web/pages/index.vue (3 hunks)
  • web/pages/welcome.vue (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • api/dev/activation/applied.txt
  • api/dev/dynamix/dynamix.cfg
🚧 Files skipped from review as they are similar to previous changes (8)
  • api/src/unraid-api/auth/public.decorator.ts
  • api/src/unraid-api/auth/authentication.guard.ts
  • web/pages/index.vue
  • api/src/utils.ts
  • web/pages/welcome.vue
  • api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts
  • web/components/Activation/store/activationCodeModal.ts
  • api/src/unraid-api/graph/resolvers/customization/activation-code.model.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (11)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (5)

31-40: Well-implemented first boot setup flag management

This method correctly creates a directory (if missing) and manages a flag file to track first boot setup status. The return value provides useful context for the caller.


47-54: Good handling of missing configuration path

The code correctly logs an error when the critical configuration path is missing and returns early to prevent undefined behavior in subsequent operations.


95-113: Robust method for finding activation JSON file

This method handles all error cases properly - both when the activation directory doesn't exist and when there are issues reading its content.


137-168: Well-implemented activation data retrieval with caching

The caching of activation data prevents redundant file reads and parsing, which is a nice performance optimization. The error handling is thorough and validation is properly implemented.


401-455: Well-implemented configuration file update utility

The updateCfgFile method is robust, handling file existence checks, section creation if needed, and proper merging of updated values. The error handling is thorough and the implementation preserves existing configuration.

api/src/unraid-api/graph/resolvers/customization/customization.service.spec.ts (6)

141-143: Good test cleanup with timer reset

Proper cleanup of timers after each test prevents potential timing issues between tests.


201-252: Comprehensive happy path test

This test thoroughly verifies the entire customization flow, including flag creation, activation data loading, and the application of all customization types. The assertions are detailed and cover all relevant aspects.


254-324: Thorough error handling test

This test effectively verifies that errors in one part of the customization process don't prevent other parts from being applied, which is a critical requirement for resilience.


393-411: Good test for hex color transformation

This test verifies that invalid hex colors are properly handled by the transformations, which is important for robustness.


413-429: Useful test for hex color normalization

This test ensures that hex colors without the # prefix are correctly normalized, verifying important business logic in the service.


954-1107: Excellent standalone tests for updateCfgFile

These tests thoroughly verify the behavior of the private utility method, covering creation, updates, merging, and error handling scenarios.

@elibosley elibosley marked this pull request as ready for review April 28, 2025 19:57
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 (2)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2)

115-124: Optimize getPublicPartnerInfo by caching redundant function call.

The method calls getPartnerLogoWebguiPath() twice, which could lead to unnecessary file system operations.

public async getPublicPartnerInfo(): Promise<PublicPartnerInfo | null> {
    const activationData = await this.getActivationData();
+   const partnerLogoUrl = await this.getPartnerLogoWebguiPath();

    return {
-       hasPartnerLogo: (await this.getPartnerLogoWebguiPath()) !== null,
+       hasPartnerLogo: partnerLogoUrl !== null,
        partnerName: activationData?.partnerName,
        partnerUrl: activationData?.partnerUrl,
-       partnerLogoUrl: await this.getPartnerLogoWebguiPath(),
+       partnerLogoUrl: partnerLogoUrl,
    };
}

266-283: Add type safety for activation data properties.

The transform functions don't check input types, which could cause runtime errors if activation data doesn't match expected types.

const displayMappings: Record<string, DisplayMapping> = {
-   header: { key: 'header', transform: (v: string) => v.replace('#', ''), skipIfEmpty: true },
+   header: { 
+       key: 'header', 
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '', 
+       skipIfEmpty: true 
+   },
-   headermetacolor: {
-       key: 'headermetacolor',
-       transform: (v: string) => v.replace('#', ''),
-       skipIfEmpty: true,
-   },
+   headermetacolor: {
+       key: 'headermetacolor',
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '',
+       skipIfEmpty: true,
+   },
-   background: {
-       key: 'background',
-       transform: (v: string) => v.replace('#', ''),
-       skipIfEmpty: true,
-   },
+   background: {
+       key: 'background',
+       transform: (v: string) => typeof v === 'string' ? v.replace('#', '') : '',
+       skipIfEmpty: true,
+   },
    showBannerGradient: {
        key: 'showBannerGradient',
-       transform: (v: boolean) => (v ? 'yes' : 'no'),
+       transform: (v: boolean) => (typeof v === 'boolean' ? (v ? 'yes' : 'no') : 'no'),
    },
    theme: { key: 'theme' },
};
📜 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 6903775 and 1f3dde0.

📒 Files selected for processing (1)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build Web App
  • GitHub Check: Build API
  • GitHub Check: Cloudflare Pages
  • GitHub Check: Test API
🔇 Additional comments (3)
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (3)

170-176: Add path validation to prevent path traversal issues.

The method should validate that the provided path is within expected bounds to prevent path traversal issues.

public async getCaseIconWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.caseModelSource)) {
+       // Validate path is within expected bounds
+       if (!paths.caseModelTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.caseModelTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.caseModelTarget);
    }
    return null;
}

178-184: Add path validation for partner logo paths.

Similar to the case icon path, validate the path to prevent potential path traversal vulnerabilities.

public async getPartnerLogoWebguiPath(): Promise<string | null> {
    const paths = getters.paths();
    if (await fileExists(paths.partnerLogoSource)) {
+       // Validate path is within expected bounds
+       if (!paths.partnerLogoTarget.startsWith('/usr/local/emhttp/')) {
+           this.logger.warn(`Invalid target path: ${paths.partnerLogoTarget}`);
+           return null;
+       }
        return convertWebGuiPathToAssetPath(paths.partnerLogoTarget);
    }
    return null;
}

1-458: Code implementation looks comprehensive and well-structured.

Overall, the CustomizationService implementation is thorough with good error handling, proper logging, and careful file operations. The service correctly handles initialization, activation data retrieval, customization application, and config file updates.

@zackspear
Copy link
Contributor

Set password screen has some issues

  • Partner Logo not coming through
  • Partner Name not displaying
  • Stepper, non-active icons wrong color
Screenshot 2025-04-28 at 16 10 43

However, when I progress pass this and land on the activate modal - the logo, name, and icons are correct.

So I wonder if the API finally kicked here and set the values
Screenshot 2025-04-28 at 16 10 58

@elibosley elibosley force-pushed the feat/activation-code-logic-in-api branch from 28f9c5e to 9e6e3c2 Compare May 1, 2025 03:46
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 (3)
web/components/Activation/ActivationPartnerLogoImg.vue (2)

16-21: Add alt text for accessibility.

The image element is missing an alt attribute, which is important for accessibility. Even if the image is decorative, you should include an empty alt attribute.

 <img
   v-if="partnerInfo?.partnerLogoUrl"
   :src="partnerInfo?.partnerLogoUrl"
+  :alt="partnerInfo?.partnerName || ''"
   class="w-72"
   :class="{ invert: darkMode && partnerInfo.hasPartnerLogo }"
 />

16-21: Add error handling for the partner logo image.

Consider adding error handling for the image to gracefully handle cases where the image fails to load.

 <img
   v-if="partnerInfo?.partnerLogoUrl"
   :src="partnerInfo?.partnerLogoUrl"
   class="w-72"
   :class="{ invert: darkMode && partnerInfo.hasPartnerLogo }"
+  @error="handleImageError"
 />

With a corresponding method in the script section:

const handleImageError = (e) => {
  console.error('Failed to load partner logo image', e);
  // Optional: Set a flag to hide the image or show a fallback
};
web/pages/welcome.vue (1)

35-35: Misleading label in debug panel.

The label "Manually Hidden" is displaying the isVisible value, which is confusing since they represent opposite states. The label suggests it should show whether the modal is hidden, but it's showing if it's visible.

-        <li>Manually Hidden (`activationModalHidden`): {{ isVisible }}</li>
+        <li>Is Visible (`isVisible`): {{ isVisible }}</li>
📜 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 d2e8e25 and a4087de.

📒 Files selected for processing (5)
  • api/dev/Unraid.net/myservers.cfg (1 hunks)
  • api/dev/activation/activation_code_12345.activationcode (1 hunks)
  • api/dev/dynamix/dynamix.cfg (1 hunks)
  • web/components/Activation/ActivationPartnerLogoImg.vue (1 hunks)
  • web/pages/welcome.vue (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • api/dev/Unraid.net/myservers.cfg
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/dev/activation/activation_code_12345.activationcode
  • api/dev/dynamix/dynamix.cfg
🧰 Additional context used
🧠 Learnings (1)
web/pages/welcome.vue (1)
Learnt from: elibosley
PR: unraid/api#1369
File: web/pages/welcome.vue:29-43
Timestamp: 2025-04-28T17:28:19.796Z
Learning: The pages in the web/pages/ directory are debug views only and not published to production. Debug panels and console logs are acceptable in these views.
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build API
  • GitHub Check: Build Web App
  • GitHub Check: Test API
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
web/components/Activation/ActivationPartnerLogoImg.vue (1)

1-13: Great component structure and implementation.

The component is well structured using Vue 3 Composition API and properly leverages Pinia stores for reactive state management. The documentation about the black image requirement for dark mode inversion is helpful for future maintainers.

web/pages/welcome.vue (4)

2-11: Good refactoring to Pinia stores.

The refactoring from server state to Pinia stores is well-implemented. The imports and store references are properly structured, and the use of storeToRefs ensures reactivity while preventing unnecessary re-renders.


13-20: Clear and effective modal control function.

The showActivationModal function is well-documented and properly implements the override for the default modal visibility logic.


27-28: Successful integration of new components.

The removal of server state props from the WelcomeModalCe component and the addition of the new ModalsCe component aligns well with the broader refactoring to API-driven activation code data.


29-43: Appropriate debug panel for development view.

The debug panel provides useful information for development and testing. Based on the retrieved learning, this is acceptable since pages in the web/pages/ directory are debug views only and not published to production.

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 (1)
api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (1)

45-72: Good implementation of flexible module loading.

The implementation supports both default exports and named exports, making it more accommodating to different coding styles. The approach properly filters non-class exports and validates that discovered classes extend FileModification.

Consider adding a warning if multiple eligible classes are found in a single file, as currently only the first one is used:

 if (exportedKeys.length > 0) {
     const firstExportKey = exportedKeys[0];
+    if (exportedKeys.length > 1) {
+        this.logger.warn(`Multiple exported classes found in ${path}, using ${firstExportKey}`);
+    }
     const ExportedClass = module[firstExportKey];
📜 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 a4087de and 8adb0d6.

📒 Files selected for processing (1)
  • api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (2 hunks)
⏰ 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 (4)
api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts (4)

39-42: Dynamic file loading improves modularity.

The implementation of dynamic module loading using import.meta.glob is a significant improvement over manual imports. This approach automatically discovers all modification files following the *.modification.ts pattern, making the system more maintainable and extensible.


44-44: Helpful debug logging.

Adding debug logging for the number of discovered modifications improves observability and helps with troubleshooting.


104-104: Improved error logging.

Using the %o format specifier for error objects provides more detailed error information, which is helpful for debugging.


37-75:

Details

✅ Verification successful

Verify new modifications are loaded correctly.

The dynamic loading mechanism assumes a specific file naming pattern and export structure. Make sure all modification files follow this pattern and test that they're properly discovered and instantiated.


🏁 Script executed:

#!/bin/bash
# Check for all modification files and their export patterns
echo "Finding all modification files:"
find api/src/unraid-api/unraid-file-modifier/modifications -name "*.modification.ts" | sort

echo "\nExamining export patterns in modification files:"
for file in $(find api/src/unraid-api/unraid-file-modifier/modifications -name "*.modification.ts"); do
  echo "\n$file exports:"
  grep -E "export (default|class)" $file
done

Length of output: 5596


All modifications follow the expected export pattern and are loaded correctly

Inspection of the nine *.modification.ts files under api/src/unraid-api/unraid-file-modifier/modifications shows eight using export default class … and one (PartnerLogoCopierModification) using a named export class …. The loader’s logic covers both cases—loading the default export first, then falling back to the named export—so all modules will be discovered and instantiated as intended.

@github-actions
Copy link
Contributor

github-actions bot commented May 1, 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/PR1369/dynamix.unraid.net.plg

@elibosley elibosley merged commit 39e83b2 into main May 1, 2025
9 checks passed
@elibosley elibosley deleted the feat/activation-code-logic-in-api branch May 1, 2025 21:40
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.

5 participants