Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Nov 24, 2025

  • Added PHP logic to determine if the current theme is dark and set a CSS variable accordingly.
  • Introduced a new function to retrieve the dark mode state from the CSS variable in JavaScript.
  • Updated the theme store to initialize dark mode based on the CSS variable, ensuring consistent theme application across the application.

This improves user experience by ensuring the correct theme is applied based on user preferences.

Summary by CodeRabbit

  • New Features

    • Persist and set UI theme from the server/dev endpoint so theme selections can be saved.
  • Bug Fixes

    • More reliable dark-mode detection and application on startup so themes and DOM dark state are consistent.
  • Improvements

    • Dark-mode exposed via CSS variables for consistent theming.
    • Header/banner gradients only apply when banner enabled; responsive gradient stop for small screens.
    • Modals, teleported containers and mounted elements inherit dark mode.
    • Minor responsive layout tweak on the user profile container.
  • Tests

    • Updated tests to validate CSS-driven dark-mode and banner-gradient behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

- Added PHP logic to determine if the current theme is dark and set a CSS variable accordingly.
- Introduced a new function to retrieve the dark mode state from the CSS variable in JavaScript.
- Updated the theme store to initialize dark mode based on the CSS variable, ensuring consistent theme application across the application.

This improves user experience by ensuring the correct theme is applied based on user preferences.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Backend emits theme-related CSS variables and conditional banner gradient vars; frontend bootstraps theme from those CSS vars, exposes computed getters and a lazy fetchTheme, and synchronizes the dark class to document, .unapi elements and teleported/modal containers; API and dev tooling add theme mutation and persistence.

Changes

Cohort / File(s) Summary
Backend theme vars
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
Emits --theme-dark-mode and --theme-name; introduces shouldShowBanner gating and conditionally sets header/banner gradient vars only when banner + gradient enabled; rewrites gradient construction and stop positioning.
Frontend theme store & getters
web/src/store/theme.ts
Switches to useLazyQuery + fetchTheme; reads/writes CSS vars (--theme-name, --theme-dark-mode, --banner-gradient, --banner-gradient-stop); adds computed themeName, darkMode, bannerGradient; bootstraps theme from DOM/CSS; applyDarkClass/syncDarkClass propagate dark and persist var.
Dev UI / theme switching
web/src/components/DevThemeSwitcher.standalone.vue, web/test-pages/partials/styles.njk, web/vite-plugin-serve-static.ts
Adds multi-source theme resolution, persistence endpoints (/dev/theme GET/POST), normalization (ThemeName/ALLOWED_THEMES), cookie/localStorage persistence, dev-time script to set CSS vars and dark class; DevThemeSwitcher now uses GraphQL mutation to persist theme.
API — GraphQL mutation + service
api/generated-schema.graphql, api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts, api/src/unraid-api/graph/resolvers/customization/customization.service.ts, api/src/unraid-api/graph/resolvers/customization/customization.module.ts, api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts, api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
Adds CustomizationMutations container and setTheme(theme: ThemeName!) mutation; implements CustomizationMutationsResolver guarded by permissions; adds CustomizationService.setTheme() to write dynamix config, reload from disk, update in-memory config and return updated Theme; wires resolver into module exports.
Wrapper / teleport dark propagation
web/src/components/Wrapper/mount-engine.ts, unraid-ui/src/composables/useTeleport.ts
On mount/teleport container creation, detect dark state (DOM class or --theme-dark-mode == '1') and add dark class to mounted/teleported containers to keep modals consistent.
CSS banner responsiveness
web/src/assets/main.css
Adds --banner-gradient-stop default 30% and media-query override 60% (max-width:768px) used for header/banner gradient stop position.
Tests updated for CSS-var-driven theme
web/__test__/store/theme.test.ts, web/__test__/components/ColorSwitcher.test.ts, web/__test__/components/UserProfile.test.ts
Add useLazyQuery mocks; tests mock getComputedStyle and root CSS vars (--theme-dark-mode, --banner-gradient); replace direct nested theme mutations with setTheme(...) in tests; guard cleanup.
Other small changes
web/src/components/UserProfile.standalone.vue, web/.prettierignore, api/dev/configs/api.json, api/generated-schema.graphql
UI layout tweak (responsive left padding); add dist/ to .prettierignore; bump api.json version; schema adjustments (new enum/type references noted).

Sequence Diagram(s)

sequenceDiagram
    participant PHP as Backend (PHP)
    participant RootCSS as :root CSS vars
    participant Frontend as Frontend (store + components)
    participant DOM as Document / .unapi
    participant GraphQL as Apollo (lazy)
    participant API as Server GraphQL
    participant FS as Dynamix config file

    PHP->>RootCSS: emit `--theme-name`, `--theme-dark-mode` and (cond.) banner gradient vars
    RootCSS-->>Frontend: read via getComputedStyle(:root)
    Frontend->>DOM: apply/remove 'dark' on documentElement, body, `.unapi` and teleported containers
    Frontend->>GraphQL: fetchTheme() (lazy) -> GraphQL returns theme payload
    Frontend->>Frontend: mapPublicTheme / merge with CSS-derived values
    Frontend->>API: GraphQL mutation setTheme(theme)
    API->>FS: write theme into dynamix config file
    API->>API: reload dynamix config from disk and update in-memory config
    API-->>Frontend: updated Theme result
    Frontend->>RootCSS: write `--theme-dark-mode` when toggled
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay attention to CSS-var parsing/normalization and fallback handling in web/src/store/theme.ts.
  • Verify correctness and timing of useLazyQuery usage and fetchTheme lifecycle (onResult wiring).
  • Review permission guard and error handling for setTheme mutation and its filesystem interactions in CustomizationService.setTheme.
  • Confirm banner/header gradient gating (shouldShowBanner + showBannerGradient) prevents leaking CSS variables when banner disabled.
  • Check dark-class propagation to teleported containers and .unapi elements for race conditions (first-time creation vs later toggles).
  • Review dev middleware /dev/theme endpoint (input validation, method handling) and its integration with vite dev server.

Poem

🐰 I hop the root to read the sign,
A little var: dark = 1 or 0, so fine,
Backend paints the banner hue,
Frontend listens, syncs the view,
My whiskers twitch — the theme is mine!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: enhance dark mode support in theme handling' accurately summarizes the main changes across the PR, which broadly focus on improving dark mode detection, initialization, and CSS variable-based theme management throughout multiple layers of the application.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dark-mode

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 832e9d0 and 1063831.

📒 Files selected for processing (2)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (1 hunks)
  • web/src/store/theme.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
  • web/src/store/theme.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/src/store/theme.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/store/theme.ts
web/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Files:

  • web/src/store/theme.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/src/store/theme.ts
**/store/**/*.ts

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

Ensure Vue reactivity imports like computed, ref, and watchEffect are added to store files even with Nuxt auto-imports enabled

Files:

  • web/src/store/theme.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
  • web/src/store/theme.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.

Applied to files:

  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
  • web/src/store/theme.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
  • web/src/store/theme.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.

Applied to files:

  • web/src/store/theme.ts
🧬 Code graph analysis (1)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (2)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • theme (58-60)
api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts (1)
  • vars (18-23)
🔇 Additional comments (3)
web/src/store/theme.ts (2)

53-60: LGTM!

The function correctly reads the CSS variable set by PHP and converts it to a boolean, with appropriate SSR handling.


67-67: LGTM!

Setting the CSS variable alongside the dark class creates a bidirectional sync, allowing the variable to stay in sync with runtime theme changes.

plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (1)

212-214: Dark theme lists are synchronized.

Verification confirms both the PHP (web-components-extractor.php, line 212) and TypeScript (web/src/store/theme.ts, line 13) dark theme lists contain identical values ['gray', 'black']. The lists are currently in sync, and dark mode detection is consistent between backend and frontend.

- Removed the previous test for applying dark mode classes to .unapi elements.
- Added a new test to verify that dark mode is initialized based on a CSS variable when the store is created.
- Mocked getComputedStyle to simulate dark mode and checked that the appropriate classes are added to document elements.

This enhances the test coverage for theme handling in the application.
@codecov
Copy link

codecov bot commented Nov 24, 2025

Codecov Report

❌ Patch coverage is 25.80645% with 322 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.85%. Comparing base (832e9d0) to head (8655575).

Files with missing lines Patch % Lines
web/vite-plugin-serve-static.ts 0.00% 167 Missing ⚠️
web/src/components/DevThemeSwitcher.standalone.vue 0.00% 130 Missing ⚠️
...h/resolvers/customization/customization.service.ts 27.27% 8 Missing ⚠️
.../customization/customization.mutations.resolver.ts 63.15% 7 Missing ⚠️
web/src/store/theme.ts 95.12% 4 Missing ⚠️
...-api/graph/resolvers/mutation/mutation.resolver.ts 50.00% 2 Missing ⚠️
unraid-ui/src/composables/useTeleport.ts 71.42% 2 Missing ⚠️
...aid-api/graph/resolvers/mutation/mutation.model.ts 75.00% 1 Missing ⚠️
web/src/components/Wrapper/mount-engine.ts 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1808      +/-   ##
==========================================
- Coverage   52.04%   51.85%   -0.19%     
==========================================
  Files         874      875       +1     
  Lines       50372    50699     +327     
  Branches     5017     5049      +32     
==========================================
+ Hits        26214    26290      +76     
- Misses      24083    24334     +251     
  Partials       75       75              

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

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

- Removed the unnecessary assignment of the store variable in the theme store test.
- This change streamlines the test setup while maintaining the verification of dark mode class application.

This update enhances the clarity and efficiency of the test code.
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)
web/__test__/store/theme.test.ts (1)

200-225: Dark‑mode init test looks solid; consider simplifying mock cleanup and comments

The new test correctly verifies initialization from --theme-dark-mode and the resulting dark classes on documentElement and body. Two small cleanups:

  • Since afterEach already calls vi.restoreAllMocks(), the explicit vi.restoreAllMocks() inside this test is redundant and can unexpectedly reset other spies (like requestAnimationFrame) mid‑test. Relying on the suite‑level cleanup keeps the lifecycle clearer.
  • The inline comments here add little beyond what the code already conveys and slightly conflict with the “no unnecessary comments” guideline; you can safely drop them.

Example minimal version:

-    it('should initialize dark mode from CSS variable on store creation', () => {
-      // Mock getComputedStyle to return dark mode
+    it('should initialize dark mode from CSS variable on store creation', () => {
       const originalGetComputedStyle = window.getComputedStyle;
       vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
         const style = originalGetComputedStyle(el);
         if (el === document.documentElement) {
           return {
             ...style,
             getPropertyValue: (prop: string) => {
               if (prop === '--theme-dark-mode') {
                 return '1';
               }
               return style.getPropertyValue(prop);
             },
           } as CSSStyleDeclaration;
         }
         return style;
       });
 
       createStore();
 
-      // Should have added dark class to documentElement
       expect(document.documentElement.classList.add).toHaveBeenCalledWith('dark');
       expect(document.body.classList.add).toHaveBeenCalledWith('dark');
-
-      vi.restoreAllMocks();
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1063831 and 297a6a5.

📒 Files selected for processing (1)
  • web/__test__/store/theme.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/__test__/store/theme.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use .rejects.toThrow() without arguments to test that functions throw errors, not exact error message strings

Files:

  • web/__test__/store/theme.test.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/__test__/store/theme.test.ts
web/__test__/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries like find('button') rather than data-test IDs
Use createPinia() and setActivePinia when testing Store files, only use createTestingPinia if you specifically need its testing features

Files:

  • web/__test__/store/theme.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/__test__/store/theme.test.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/__test__/store/theme.test.ts
**/*.test.ts

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

**/*.test.ts: Use .rejects.toThrow() without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Use vi.mock() for module-level mocks
Specify return values for component methods with vi.spyOn()
Reset mocks between tests with vi.clearAllMocks()
Always await async operations before making assertions

Files:

  • web/__test__/store/theme.test.ts
**/__test__/store/**/*.ts

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

**/__test__/store/**/*.ts: Use createPinia and setActivePinia when testing Pinia store files
Use createPinia() instead of createTestingPinia() for most Pinia store tests
Only use createTestingPinia if you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when using createPinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing

Files:

  • web/__test__/store/theme.test.ts
**/store/**/*.ts

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

Ensure Vue reactivity imports like computed, ref, and watchEffect are added to store files even with Nuxt auto-imports enabled

Files:

  • web/__test__/store/theme.test.ts
🧠 Learnings (18)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing

Applied to files:

  • web/__test__/store/theme.test.ts

- Updated the WebComponentsExtractor to set the theme name and conditionally apply gradient variables based on banner visibility.
- Improved the useTeleport and mount-engine functions to inherit dark mode from the document, ensuring consistency across components.
- Refactored the theme store to initialize dark mode based on CSS variables and streamline theme management, including lazy loading of theme queries.

These changes improve the user experience by ensuring that the correct theme and dark mode settings are applied consistently throughout the application.
@elibosley elibosley marked this pull request as ready for review November 25, 2025 15:16
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +116 to 120
// Lazy query - only executes when explicitly called
const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
});

Choose a reason for hiding this comment

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

P1 Badge Trigger theme query after switching to useLazyQuery

useThemeStore now instantiates useLazyQuery for GET_THEME_QUERY, but load()/fetchTheme() are never called anywhere (rg "fetchTheme" web only returns this definition), so the theme query no longer runs on startup. This regresses the previous eager fetch and leaves server-provided theme values (colors, banner flags, etc.) at defaults unless some other code happens to reload the page, meaning users’ configured theme is never applied in a single-page session.

Useful? React with 👍 / 👎.

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 (4)
unraid-ui/src/composables/useTeleport.ts (1)

12-19: Dark class is applied at container creation but won't update dynamically.

The virtual container is a singleton created once. If the theme changes after the container is created (e.g., via dev tools), the dark class won't be updated. This may be acceptable if theme changes always trigger a page refresh in production, but could cause visual inconsistencies during development.

Consider adding a MutationObserver or exposing a method to sync the dark class if dynamic theme switching without refresh is needed.

web/src/store/theme.ts (1)

243-247: Consider extracting DOM initialization to a named function.

The inline initialization logic works, but extracting it to a named function (similar to bootstrapDarkClass) would improve readability and make the initialization sequence clearer.

+ const initializeThemeFromDom = () => {
+   const domThemeName = themeName.value;
+   if (domThemeName && domThemeName !== DEFAULT_THEME.name) {
+     theme.value.name = domThemeName;
+   }
+ };
+
  // Initialize dark mode from CSS variable set by PHP or any pre-applied .dark class
  bootstrapDarkClass();
+ initializeThemeFromDom();

- // Initialize theme from DOM on store creation
- const domThemeName = themeName.value;
- if (domThemeName && domThemeName !== DEFAULT_THEME.name) {
-   theme.value.name = domThemeName;
- }
web/__test__/store/theme.test.ts (2)

21-27: Mock onResult should accept and store a callback.

The store calls onResult(callback) to register a result handler. The current mock returns vi.fn() which accepts any args but doesn't do anything with them. For tests that need to simulate query results, you'll need a more sophisticated mock.

If you need to test query result handling in the future:

useLazyQuery: () => {
  const callbacks: Array<(result: { data: unknown }) => void> = [];
  return {
    load: vi.fn(),
    result: ref(null),
    loading: ref(false),
    onResult: vi.fn((cb) => callbacks.push(cb)),
    onError: vi.fn(),
    // Helper for tests to trigger result callbacks
    _triggerResult: (data: unknown) => callbacks.forEach(cb => cb({ data })),
  };
},

174-196: Test passes for the wrong reason - tests flag check, not CSS variable behavior.

This test expects undefined when the CSS variable is empty, but it actually returns undefined because theme.value.banner and theme.value.bannerGradient are both false by default. The CSS variable mock is never reached.

Consider clarifying the test name or adjusting to actually test the CSS variable fallback behavior.

- it('should return undefined when bannerGradient CSS variable is not set', () => {
+ it('should return undefined when banner flags are disabled', () => {
+   const store = createStore();
+   // Default theme has banner: false, bannerGradient: false
+   expect(store.bannerGradient).toBeUndefined();
+ });
+
+ it('should return undefined when CSS variable is not set but banner flags are enabled', () => {
    const originalGetComputedStyle = window.getComputedStyle;
    vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
      // ... existing mock that returns empty string
    });

    const store = createStore();
+   store.setTheme({ banner: true, bannerGradient: true });
    expect(store.bannerGradient).toBeUndefined();

    vi.restoreAllMocks();
  });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 297a6a5 and 55742ac.

📒 Files selected for processing (5)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (2 hunks)
  • unraid-ui/src/composables/useTeleport.ts (1 hunks)
  • web/__test__/store/theme.test.ts (3 hunks)
  • web/src/components/Wrapper/mount-engine.ts (1 hunks)
  • web/src/store/theme.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • unraid-ui/src/composables/useTeleport.ts
  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
web/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/store/theme.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • unraid-ui/src/composables/useTeleport.ts
  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • unraid-ui/src/composables/useTeleport.ts
  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use .rejects.toThrow() without arguments to test that functions throw errors, not exact error message strings

Files:

  • web/__test__/store/theme.test.ts
web/__test__/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries like find('button') rather than data-test IDs
Use createPinia() and setActivePinia when testing Store files, only use createTestingPinia if you specifically need its testing features

Files:

  • web/__test__/store/theme.test.ts
**/*.test.ts

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

**/*.test.ts: Use .rejects.toThrow() without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Use vi.mock() for module-level mocks
Specify return values for component methods with vi.spyOn()
Reset mocks between tests with vi.clearAllMocks()
Always await async operations before making assertions

Files:

  • web/__test__/store/theme.test.ts
**/__test__/store/**/*.ts

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

**/__test__/store/**/*.ts: Use createPinia and setActivePinia when testing Pinia store files
Use createPinia() instead of createTestingPinia() for most Pinia store tests
Only use createTestingPinia if you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when using createPinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing

Files:

  • web/__test__/store/theme.test.ts
**/store/**/*.ts

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

Ensure Vue reactivity imports like computed, ref, and watchEffect are added to store files even with Nuxt auto-imports enabled

Files:

  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
🧠 Learnings (36)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • unraid-ui/src/composables/useTeleport.ts
  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
📚 Learning: 2025-02-20T15:52:58.297Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test computed properties in Pinia stores by accessing them directly

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
Repo: unraid/api PR: 1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/src/store/theme.ts
🧬 Code graph analysis (1)
web/__test__/store/theme.test.ts (1)
api/src/store/index.ts (1)
  • store (10-16)
🪛 GitHub Actions: CI - Main (API)
web/__test__/store/theme.test.ts

[error] 150-170: Assertion failed: expected bannerGradient to have a gradient string, but received undefined in theme store tests.

🪛 GitHub Check: Test API
web/src/store/theme.ts

[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > enables gradient automatically when banner is enabled
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when switches change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when color inputs change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when theme selection changes
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > renders all form elements correctly
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24

🔇 Additional comments (4)
web/src/store/theme.ts (2)

180-196: Computed correctly guards CSS variable access with theme flags.

The logic properly checks banner and bannerGradient flags before reading the CSS variable. The pipeline failure in tests is due to tests not setting these flags to true before asserting the gradient value.


232-241: Good: immediate: false resolves the timing issue.

The watch no longer triggers immediately on initialization, which prevents the race condition flagged in the past review. The bootstrapDarkClass() call at line 114 handles the initial dark mode setup separately.

web/src/components/Wrapper/mount-engine.ts (1)

182-189: LGTM - Dark mode class propagation is correctly applied to mounted elements.

The detection logic correctly checks both the DOM class and CSS variable, consistent with the patterns in theme.ts. This ensures mounted web components inherit the dark mode styling.

web/__test__/store/theme.test.ts (1)

37-38: Variable store is correctly used throughout tests.

The past review comment about an unused variable appears to be outdated or a false positive. The store variable is properly used for cleanup in afterEach and populated via createStore().

Comment on lines +117 to 120
const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update existing Apollo mocks to include useLazyQuery.

The static analysis shows test failures in ColorSwitcher.test.ts because the Apollo mock doesn't export useLazyQuery. Any test file that mocks @vue/apollo-composable needs to include both useQuery and useLazyQuery.

The affected test file needs to update its mock:

 vi.mock('@vue/apollo-composable', () => ({
   useQuery: () => ({
     result: ref(null),
     loading: ref(false),
     onResult: vi.fn(),
     onError: vi.fn(),
   }),
+  useLazyQuery: () => ({
+    load: vi.fn(),
+    result: ref(null),
+    loading: ref(false),
+    onResult: vi.fn(),
+    onError: vi.fn(),
+  }),
 }));

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

🧰 Tools
🪛 GitHub Check: Test API

[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > enables gradient automatically when banner is enabled
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when switches change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when color inputs change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when theme selection changes
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > renders all form elements correctly
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24

🤖 Prompt for AI Agents
In web/src/store/theme.ts around lines 117 to 120 the code uses useLazyQuery
from @vue/apollo-composable but the test mocks for that module only export
useQuery; update the test mocks that stub @vue/apollo-composable (e.g.,
ColorSwitcher.test.ts or the shared test mock file) to export both useQuery and
useLazyQuery with equivalent mock implementations and return shapes (including
load, onResult, onError, and reactive result/error/loading properties) so tests
that import useLazyQuery behave like the real composable.

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)
unraid-ui/src/composables/useTeleport.ts (1)

12-19: Inconsistent dark mode detection across files.

This detection logic differs from web/src/components/Wrapper/mount-engine.ts (lines 184-186), which checks the --theme-dark-mode CSS variable in addition to the documentElement class. The --theme-dark-mode variable is set by the theme store and serves as the source of truth for dark mode state.

Not checking the CSS variable here could cause the teleported container to miss dark mode in scenarios where the variable is set but the DOM classes haven't been applied yet.

Update the detection logic to align with mount-engine.ts:

-    const isDark =
-      document.documentElement.classList.contains('dark') ||
-      document.body?.classList.contains('dark') ||
-      Boolean(document.querySelector('.unapi.dark'));
+    const isDark =
+      document.documentElement.classList.contains('dark') ||
+      getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1';
web/src/store/theme.ts (1)

116-120: Critical: Theme query never executes.

The switch to useLazyQuery requires an explicit call to load() (exposed as fetchTheme), but fetchTheme is never invoked anywhere in the store initialization or by external code. This means the server theme is never fetched, and users will only see the default white theme regardless of their configured preferences.

Call fetchTheme() after the store initialization completes:

   // Initialize theme from DOM on store creation
   const domThemeName = themeName.value;
   if (domThemeName && domThemeName !== DEFAULT_THEME.name) {
     theme.value.name = domThemeName;
   }
+
+  // Fetch server theme to sync with backend
+  fetchTheme();

   return {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 297a6a5 and 55742ac.

📒 Files selected for processing (5)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (2 hunks)
  • unraid-ui/src/composables/useTeleport.ts (1 hunks)
  • web/__test__/store/theme.test.ts (3 hunks)
  • web/src/components/Wrapper/mount-engine.ts (1 hunks)
  • web/src/store/theme.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • unraid-ui/src/composables/useTeleport.ts
  • web/src/store/theme.ts
  • web/__test__/store/theme.test.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/store/theme.ts
  • web/__test__/store/theme.test.ts
web/src/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • web/src/store/theme.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • unraid-ui/src/composables/useTeleport.ts
  • web/src/store/theme.ts
  • web/__test__/store/theme.test.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/src/components/Wrapper/mount-engine.ts
  • unraid-ui/src/composables/useTeleport.ts
  • web/src/store/theme.ts
  • web/__test__/store/theme.test.ts
**/store/**/*.ts

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

Ensure Vue reactivity imports like computed, ref, and watchEffect are added to store files even with Nuxt auto-imports enabled

Files:

  • web/src/store/theme.ts
  • web/__test__/store/theme.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use .rejects.toThrow() without arguments to test that functions throw errors, not exact error message strings

Files:

  • web/__test__/store/theme.test.ts
web/__test__/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries like find('button') rather than data-test IDs
Use createPinia() and setActivePinia when testing Store files, only use createTestingPinia if you specifically need its testing features

Files:

  • web/__test__/store/theme.test.ts
**/*.test.ts

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

**/*.test.ts: Use .rejects.toThrow() without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Use vi.mock() for module-level mocks
Specify return values for component methods with vi.spyOn()
Reset mocks between tests with vi.clearAllMocks()
Always await async operations before making assertions

Files:

  • web/__test__/store/theme.test.ts
**/__test__/store/**/*.ts

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

**/__test__/store/**/*.ts: Use createPinia and setActivePinia when testing Pinia store files
Use createPinia() instead of createTestingPinia() for most Pinia store tests
Only use createTestingPinia if you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when using createPinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing

Files:

  • web/__test__/store/theme.test.ts
🧠 Learnings (34)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • unraid-ui/src/composables/useTeleport.ts
  • web/src/store/theme.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
Repo: unraid/api PR: 1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components

Applied to files:

  • web/src/store/theme.ts
  • web/__test__/store/theme.test.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-02-20T15:52:58.297Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test computed properties in Pinia stores by accessing them directly

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
🪛 GitHub Check: Test API
web/src/store/theme.ts

[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > enables gradient automatically when banner is enabled
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when switches change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when color inputs change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when theme selection changes
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24


[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > renders all form elements correctly
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24

🔇 Additional comments (7)
web/src/store/theme.ts (6)

41-85: Well-structured helper functions.

The DOM availability checks and CSS variable reading helpers are well-organized. The syncDarkClass function correctly propagates the dark class to documentElement, body, and all .unapi elements, ensuring consistency across the application.


113-114: Good initialization pattern.

Calling bootstrapDarkClass() during store initialization ensures that dark mode is properly applied based on CSS variables or existing DOM state when the store is created.


158-168: CSS-variable-driven getters align with PR objectives.

The themeName and darkMode computed properties correctly read from CSS variables set by PHP (--theme-name and --theme-dark-mode), with appropriate fallbacks. This ensures consistent theme state between server-rendered HTML and client-side JavaScript.


180-196: Banner gradient logic correctly respects dark mode.

The implementation correctly returns undefined when darkMode is active, aligning with the learned behavior that banners should not show gradients in dark mode. The CSS variable reading includes proper normalization to handle empty, null, or undefined values.

Based on learnings, the header design intentionally differs between light and dark modes.


234-241: Watch with immediate: false prevents initialization conflict.

Setting immediate: false correctly prevents the watcher from overriding the dark class applied by bootstrapDarkClass() during initialization. The watcher now only applies the dark class when the theme name actually changes (e.g., via dev tools), which is the intended behavior per the comment.


254-257: Computed theme wrapper ensures CSS variable takes precedence.

Returning a computed object that overrides name with themeName.value ensures the theme name always reflects the CSS variable set by PHP, even if the internal theme ref has a different value. This maintains the CSS variable as the source of truth for theme name.

web/__test__/store/theme.test.ts (1)

100-294: Comprehensive test coverage for CSS-variable-driven theme.

The new tests effectively validate the CSS-variable-driven theme initialization and computed properties:

  • Dark mode detection from --theme-dark-mode (both '1' and '0' values)
  • Banner gradient reading from --banner-gradient
  • Proper handling of missing/empty CSS variables
  • Bootstrap dark class application on store creation

The mocking strategy using getComputedStyle is appropriate and aligns with the implementation.

Comment on lines +21 to +27
useLazyQuery: () => ({
load: vi.fn(),
result: ref(null),
loading: ref(false),
onResult: vi.fn(),
onError: vi.fn(),
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Update other test files to include useLazyQuery mock.

The useLazyQuery mock added here is correct, but static analysis reveals that other test files (e.g., __test__/components/ColorSwitcher.test.ts) are failing because their @vue/apollo-composable mocks don't export useLazyQuery.

Update all test files that mock @vue/apollo-composable to include useLazyQuery. Run the following script to identify files that need updating:

#!/bin/bash
# Find all test files that mock @vue/apollo-composable but don't include useLazyQuery

rg -l "vi\.mock.*@vue/apollo-composable" web/__test__ | while read -r file; do
  if ! grep -q "useLazyQuery" "$file"; then
    echo "Missing useLazyQuery mock: $file"
  fi
done

For each file found, add useLazyQuery to the mock:

 vi.mock('@vue/apollo-composable', () => ({
   useQuery: () => ({
     result: ref(null),
     loading: ref(false),
     onResult: vi.fn(),
     onError: vi.fn(),
   }),
+  useLazyQuery: () => ({
+    load: vi.fn(),
+    result: ref(null),
+    loading: ref(false),
+    onResult: vi.fn(),
+    onError: vi.fn(),
+  }),
 }));
🤖 Prompt for AI Agents
In web/__test__/store/theme.test.ts around lines 21 to 27 the mock for
@vue/apollo-composable includes useLazyQuery but other test files that mock
@vue/apollo-composable are missing it; run the provided grep script to locate
all such test files, and for each file add a useLazyQuery export to the mock
that matches the shape used here (return an object with load: vi.fn(), result:
ref(null), loading: ref(false), onResult: vi.fn(), onError: vi.fn()); update
imports if needed and run tests to confirm no missing-export errors remain.

Comment on lines +182 to +189
// Apply dark mode class if active
// Check both documentElement class and CSS variable (set by PHP)
const isDarkMode =
document.documentElement.classList.contains('dark') ||
getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1';
if (isDarkMode) {
element.classList.add('dark');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent dark mode detection across files.

This implementation checks documentElement.classList and the CSS variable --theme-dark-mode, but unraid-ui/src/composables/useTeleport.ts (lines 13-16) uses a different detection strategy: it checks documentElement.classList, body.classList, and the presence of .unapi.dark elements, but does not check the --theme-dark-mode CSS variable.

This inconsistency could result in the teleported container and mounted elements having different dark mode states under certain conditions.

Consider consolidating the detection logic into a shared utility function that both files can use. For example:

// helpers/dark-mode.ts
export const isDarkModeActive = (): boolean => {
  if (typeof document === 'undefined') return false;
  return (
    document.documentElement.classList.contains('dark') ||
    getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1'
  );
};

Then use it in both files:

+import { isDarkModeActive } from '~/helpers/dark-mode';

-    const isDarkMode =
-      document.documentElement.classList.contains('dark') ||
-      getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1';
-    if (isDarkMode) {
+    if (isDarkModeActive()) {
       element.classList.add('dark');
     }

- Updated the WebComponentsExtractor to utilize a CSS variable for the banner gradient stop, allowing for responsive adjustments.
- Modified theme store tests to reflect the new gradient format, ensuring consistency in gradient rendering.
- Added CSS rules to define the banner gradient stop, with a media query for different screen sizes.

These changes improve the flexibility and responsiveness of the banner gradient in the application.
- Introduced a new test to verify the correct application of the banner gradient when the theme is set.
- This enhancement ensures that the theme store's functionality for banner gradient customization is properly validated.

These changes improve the robustness of the theme store tests.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

1 similar comment
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Refactored UserProfile tests to replace direct theme property manipulations with the setTheme method, ensuring consistency in theme state management.
- Mocked getComputedStyle to simulate CSS variables for banner gradient and dark mode, enhancing test reliability.
- Improved test coverage for theme-related functionality, particularly around banner visibility and description rendering.

These changes enhance the robustness and maintainability of the UserProfile component tests.
- Updated ColorSwitcher and UserProfile tests to set CSS variables directly on the document element, improving test reliability and consistency.
- Refactored cleanup logic in theme store tests to handle unmounting more gracefully, ensuring robust test teardown.

These changes enhance the maintainability and clarity of the theme-related tests.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

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)
web/__test__/store/theme.test.ts (1)

106-128: Consider extracting repeated getComputedStyle mock to a helper.

The mock pattern for getComputedStyle is repeated across multiple tests with slight variations. A helper function could reduce duplication:

const mockCssVariables = (variables: Record<string, string>) => {
  const originalGetComputedStyle = window.getComputedStyle;
  vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => {
    const style = originalGetComputedStyle(el);
    if (el === document.documentElement) {
      return {
        ...style,
        getPropertyValue: (prop: string) => variables[prop] ?? style.getPropertyValue(prop),
      } as CSSStyleDeclaration;
    }
    return style;
  });
};

Usage: mockCssVariables({ '--theme-dark-mode': '1' })

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8799ac9 and a930dee.

📒 Files selected for processing (3)
  • web/__test__/components/ColorSwitcher.test.ts (3 hunks)
  • web/__test__/components/UserProfile.test.ts (3 hunks)
  • web/__test__/store/theme.test.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use .rejects.toThrow() without arguments to test that functions throw errors, not exact error message strings

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
web/__test__/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries like find('button') rather than data-test IDs
Use createPinia() and setActivePinia when testing Store files, only use createTestingPinia if you specifically need its testing features

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
**/*.test.ts

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

**/*.test.ts: Use .rejects.toThrow() without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Use vi.mock() for module-level mocks
Specify return values for component methods with vi.spyOn()
Reset mocks between tests with vi.clearAllMocks()
Always await async operations before making assertions

Files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
**/__test__/store/**/*.ts

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

**/__test__/store/**/*.ts: Use createPinia and setActivePinia when testing Pinia store files
Use createPinia() instead of createTestingPinia() for most Pinia store tests
Only use createTestingPinia if you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when using createPinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing

Files:

  • web/__test__/store/theme.test.ts
**/store/**/*.ts

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

Ensure Vue reactivity imports like computed, ref, and watchEffect are added to store files even with Nuxt auto-imports enabled

Files:

  • web/__test__/store/theme.test.ts
**/__test__/components/**/*.ts

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

**/__test__/components/**/*.ts: Use mount from Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Mock external dependencies and services in Vue component tests
Test component behavior and output, not implementation details
Verify that the expected elements are rendered in Vue components
Test component interactions such as clicks and inputs
Check for expected prop handling and event emissions in Vue components
Use createTestingPinia() for mocking stores in Vue components
Use semantic queries like find('button') or find('[data-test="id"]') but prefer not to use data test ID's when finding elements
Find Vue components with findComponent(ComponentName)
Use findAll to check for multiple elements in Vue components
Assert on rendered text content with wrapper.text()
Assert on element attributes with element.attributes()
Verify element existence with expect(element.exists()).toBe(true)
Check component state through rendered output
Trigger events with await element.trigger('click')
Set input values with await input.setValue('value')
Test emitted events with wrapper.emitted()
Use await nextTick() for DOM updates in Vue component tests
Use flushPromises() for complex promise chains in Vue component tests

Files:

  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
🧠 Learnings (37)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-02-20T15:52:58.297Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify proper error handling in Pinia store tests

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test computed properties in Pinia stores by accessing them directly

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `await nextTick()` for DOM updates in Vue component tests

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`

Applied to files:

  • web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing

Applied to files:

  • web/__test__/store/theme.test.ts
  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use `createPinia` and `setActivePinia` when testing Pinia store files

Applied to files:

  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use `createPinia()` instead of `createTestingPinia()` for most Pinia store tests

Applied to files:

  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Override specific Pinia action implementations when needed in tests

Applied to files:

  • web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Assert on rendered text content with `wrapper.text()`

Applied to files:

  • web/__test__/components/UserProfile.test.ts
  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check component state through rendered output

Applied to files:

  • web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing

Applied to files:

  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions such as clicks and inputs

Applied to files:

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

Applied to files:

  • web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Only use `createTestingPinia` if you specifically need its testing features for Pinia stores

Applied to files:

  • web/__test__/components/ColorSwitcher.test.ts
🧬 Code graph analysis (2)
web/__test__/store/theme.test.ts (1)
api/src/store/index.ts (1)
  • store (10-16)
web/__test__/components/UserProfile.test.ts (1)
web/src/store/theme.ts (1)
  • useThemeStore (105-263)
🔇 Additional comments (12)
web/__test__/components/UserProfile.test.ts (3)

191-210: LGTM! Well-structured theme initialization.

The CSS variable setup and setTheme initialization properly align with the new CSS-variable-driven theme resolution. This approach ensures tests reflect production behavior where theme state is derived from CSS variables set by PHP.


336-357: Clean refactor to use public API.

The spread pattern { ...themeStore.theme, descriptionShow: ... } correctly preserves existing theme state while updating specific properties, replacing direct state mutations with proper setTheme calls.


368-398: LGTM!

Banner tests correctly use setTheme to toggle flags and verify bannerGradient computed behavior with CSS variables.

web/__test__/components/ColorSwitcher.test.ts (3)

25-31: Good addition of useLazyQuery mock.

This addresses the missing mock issue flagged in past reviews. The mock shape correctly mirrors the useLazyQuery return type with load, result, loading, onResult, and onError.


64-67: CSS variable setup enables proper theme store initialization.

Setting --theme-dark-mode and --banner-gradient before creating the store ensures the CSS-variable-driven computed properties initialize correctly during tests.


84-89: Improved cleanup robustness.

The guards prevent potential errors during teardown when modalDiv is already removed or consoleWarnSpy wasn't created.

web/__test__/store/theme.test.ts (6)

21-27: useLazyQuery mock added correctly.

This aligns with the mock added in ColorSwitcher.test.ts and addresses the past review comment about consistency across test files.


65-71: Defensive cleanup pattern.

The try-catch handles the case where the app was never mounted, preventing test failures during teardown.


106-152: Thorough CSS variable-based darkMode tests.

These tests properly verify the darkMode computed property reads from --theme-dark-mode CSS variable and returns the correct boolean value.


154-186: Good test for bannerGradient with CSS variable.

The test correctly sets banner: true and bannerGradient: true via setTheme before asserting the computed value, addressing the issue flagged in past reviews.


188-210: Correctly tests undefined gradient when CSS variable is empty.

This validates the edge case where --banner-gradient is not set or empty.


279-308: LGTM! Validates dark mode initialization from CSS variable.

This test confirms that bootstrapDarkClass correctly applies the dark class to documentElement and body when the CSS variable --theme-dark-mode is set to '1' at store creation time.

@elibosley elibosley requested a review from pujitm November 26, 2025 15:34
- Introduced CustomizationMutations to handle theme updates via GraphQL.
- Implemented setTheme mutation to update the UI theme and persist changes in the configuration.
- Updated the generated GraphQL schema to include the new mutations and theme enum.
- Enhanced the CustomizationService to manage theme persistence and in-memory updates.
- Updated the frontend to support theme selection and synchronization with the server.

These changes improve the user experience by allowing dynamic theme management and ensuring consistent application of user preferences.
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (8)
web/src/components/DevThemeSwitcher.standalone.vue (3)

66-77: Remove unnecessary async keyword.

This function doesn't contain any await expressions, so the async modifier is unnecessary overhead.

-const resolveInitialTheme = async (): Promise<ThemeName> => {
+const resolveInitialTheme = (): ThemeName => {
   const candidates = [readCssTheme(), readCookieTheme(), readLocalStorageTheme(), theme.value?.name];

If this is intentionally async for future extensibility, consider adding a comment to clarify.


96-102: Redundant watch for query loading state.

The queryLoading ref from useQuery can be used directly instead of copying it to another ref via a watcher.

Remove the redundant watch and isQueryLoading ref:

-const isQueryLoading = ref(false);
...
-watch(
-  () => queryLoading.value,
-  (loading) => {
-    isQueryLoading.value = loading;
-  },
-  { immediate: true }
-);

Then in the template, use queryLoading directly:

-    :disabled="isSaving || isQueryLoading"
+    :disabled="isSaving || queryLoading"

191-216: Clarify the persist parameter semantics.

The persist parameter name is misleading—local persistence to cookie/localStorage always happens (line 198), while persist only controls the GraphQL server mutation.

Consider renaming for clarity:

 const applyThemeSelection = async (
   themeName: string | null | undefined,
-  { persist = false, skipStore = false }: { persist?: boolean; skipStore?: boolean } = {}
+  { persistToServer = false, skipStore = false }: { persistToServer?: boolean; skipStore?: boolean } = {}
 ) => {
   // ...
-  if (persist) {
+  if (persistToServer) {
web/test-pages/partials/styles.njk (1)

2-5: Dev theme bootstrap matches CSS-var/dark-mode flow; confirm azure dark-mode mapping

The dev-only logic looks good overall:

  • activeTheme precedence (resolvedThemequery.theme'white') and the explicit allowlist guard prevent invalid theme names from leaking into CSS vars.
  • The script keeps the <link id="dev-theme-css-link"> href in sync with the normalized theme.
  • --theme-name, --theme-dark-mode (as '1'/'0'), and data-theme on documentElement align with how the theme store expects to bootstrap state. Based on learnings, this should integrate cleanly with existing CSS-variable-driven theming.
  • syncDarkClass updating html, body, and .unapi mirrors how the app propagates the dark class to containers.

Two small follow-ups:

  1. isDark only treats 'black' and 'gray' as dark:

    var isDark = theme === 'black' || theme === 'gray';

    Please double-check whether azure should also be considered dark for your design system. If yes, add it here so --theme-dark-mode and the dark class stay consistent.

  2. The theme allowlist, themeCssMap keys, and isDark predicate all repeat the same set of theme names. If you find yourself tweaking themes often, consider centralizing this into a single map/object and deriving the allowlist and isDark from it to avoid drift.

Also applies to: 26-66

web/vite-plugin-serve-static.ts (4)

10-39: Tighten normalizeTheme typing to avoid unnecessary cast

The runtime logic is fine, but casting (theme ?? '').toLowerCase() directly to ThemeName is a bit loose for TS and goes against the “exact theme types, minimal casting” pattern used elsewhere in the theme system (see previous learnings). You can keep the same behavior while letting TypeScript infer the union from ALLOWED_THEMES:

-const normalizeTheme = (theme?: string | null): ThemeName => {
-  const normalized = (theme ?? '').toLowerCase() as ThemeName;
-  return ALLOWED_THEMES.includes(normalized) ? normalized : 'white';
-};
+const normalizeTheme = (theme?: string | null): ThemeName => {
+  const normalized = (theme ?? '').toLowerCase();
+  return ALLOWED_THEMES.find((t) => t === normalized) ?? 'white';
+};

This preserves guarantees that only whitelisted values flow out, without relying on a broad cast. Based on learnings, this matches the preference for precise theme typings.


40-58: Harden parseCookies against malformed encodings

The implementation is solid for normal cookies, but decodeURIComponent will throw if a cookie value contains invalid percent-encoding, which would currently take down the dev request.

If you want this helper to be more defensive (even in dev), consider wrapping the decode in a try/catch and skipping values that can’t be decoded:

-    acc[trimmedName] = decodeURIComponent(rest.join('=').trim());
+    const rawValue = rest.join('=').trim();
+    try {
+      acc[trimmedName] = decodeURIComponent(rawValue);
+    } catch {
+      // Ignore malformed cookie values
+    }

That way a single bad cookie header doesn’t break /test-pages rendering.


70-109: INI parsing and theme read behavior are minimal but adequate

parseIniSection is intentionally minimal (single-line key=value, no multi-line or advanced INI features) and readThemeFromConfig gracefully falls back to null on any error, which is appropriate for a dev helper.

One behavioral note: if [display] exists but has no theme, normalizeTheme(displaySection.theme) effectively defaults to 'white'. That’s probably fine for dev, but it does mean a misconfigured file silently falls back rather than surfacing an error. If you ever want stricter behavior, you could explicitly check for Object.hasOwn(displaySection, 'theme') before normalizing.


307-318: Theme resolution precedence is clear; confirm it matches intended UX

The resolution chain:

const cookies = parseCookies(req.headers.cookie);
const cookieTheme = cookies['unraid_dev_theme'];
const queryTheme = requestUrl.searchParams.get('theme');
const cfgTheme = readThemeFromConfig();
const resolvedTheme = normalizeTheme(cfgTheme || queryTheme || cookieTheme);

gives precedence config > query param > cookie, then defaults via normalizeTheme. That’s a reasonable choice for dev, but it does mean a remembered config will override ad‑hoc query testing unless config is cleared/changed.

If that’s the intended behavior for local testing, this looks good. If you expect query params to always win during manual testing, you may want to reorder to query > cookie > config.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a930dee and 8655575.

📒 Files selected for processing (11)
  • api/dev/configs/api.json (1 hunks)
  • api/generated-schema.graphql (2 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts (1 hunks)
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (2 hunks)
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (2 hunks)
  • web/.prettierignore (1 hunks)
  • web/src/components/DevThemeSwitcher.standalone.vue (3 hunks)
  • web/test-pages/partials/styles.njk (2 hunks)
  • web/vite-plugin-serve-static.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
api/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

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

Files:

  • api/dev/configs/api.json
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • api/generated-schema.graphql
**/*

📄 CodeRabbit inference engine (.cursor/rules/default.mdc)

Never add comments unless they are needed for clarity of function

Files:

  • api/dev/configs/api.json
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • web/src/components/DevThemeSwitcher.standalone.vue
  • web/vite-plugin-serve-static.ts
  • web/test-pages/partials/styles.njk
  • api/generated-schema.graphql
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with .js extensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks

Files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • web/vite-plugin-serve-static.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use the any type. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start

Files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
  • web/vite-plugin-serve-static.ts
api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)

Files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
web/**/*

📄 CodeRabbit inference engine (CLAUDE.md)

Always run pnpm codegen for GraphQL code generation in the web directory

Files:

  • web/src/components/DevThemeSwitcher.standalone.vue
  • web/vite-plugin-serve-static.ts
  • web/test-pages/partials/styles.njk
🧠 Learnings (18)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)

Applied to files:

  • web/.prettierignore
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/* : Always run `pnpm codegen` for GraphQL code generation in the web directory

Applied to files:

  • web/.prettierignore
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files

Applied to files:

  • web/.prettierignore
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.

Applied to files:

  • web/.prettierignore
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.{query.ts,mutation.ts} : All GraphQL under `web/` must follow the `.query.ts` and `.mutation.ts` naming convention

Applied to files:

  • web/.prettierignore
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files

Applied to files:

  • web/.prettierignore
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.

Applied to files:

  • web/.prettierignore
  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details

Applied to files:

  • web/.prettierignore
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.

Applied to files:

  • api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
  • api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-06T20:59:06.096Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:87-89
Timestamp: 2024-11-06T20:59:06.096Z
Learning: Error handling in mutations like `addRoleForUser` in `auth.resolver.ts` is performed at the service level, so additional error handling in the resolver is unnecessary.

Applied to files:

  • api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.

Applied to files:

  • api/src/unraid-api/graph/resolvers/customization/customization.service.ts
  • web/src/components/DevThemeSwitcher.standalone.vue
  • web/vite-plugin-serve-static.ts
  • web/test-pages/partials/styles.njk
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.

Applied to files:

  • api/src/unraid-api/graph/resolvers/customization/customization.module.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.

Applied to files:

  • web/src/components/DevThemeSwitcher.standalone.vue
  • web/vite-plugin-serve-static.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 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.

Applied to files:

  • web/src/components/DevThemeSwitcher.standalone.vue
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.

Applied to files:

  • web/test-pages/partials/styles.njk
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts (3)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
  • Resolver (14-50)
packages/unraid-shared/src/use-permissions.directive.ts (1)
  • UsePermissions (81-128)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
  • theme (58-60)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
web/composables/gql/graphql.ts (1)
  • Mutation (1199-1239)
🔇 Additional comments (19)
web/.prettierignore (1)

5-5: Adding dist/ to Prettier ignore list is sensible.

The change appropriately excludes the compiled/build output directory from Prettier formatting, which is a best practice to prevent formatting of generated files. The entry follows the existing naming convention with other directory-based ignores in the file.

web/src/components/DevThemeSwitcher.standalone.vue (7)

1-27: LGTM!

Good use of the generated ThemeName enum for type-safe theme options, aligning with the project's preference for exact TypeScript type definitions. Based on learnings, this approach ensures better type safety.


34-64: LGTM!

The helper functions are SSR-safe with appropriate guards for document and window. Cookie parsing correctly handles values containing = by using rest.join('=').


142-150: LGTM!

Cookie persistence with reasonable expiration and SameSite=Lax. The omission of the Secure flag is acceptable for a dev-only component that may run on localhost.


152-163: LGTM!

The CSS variable names --theme-name and --theme-dark-mode follow the concise naming pattern as per established conventions. The dark class synchronization across root, body, and .unapi elements ensures consistent theming. Based on learnings, this aligns with the project's approach to CSS variable naming.


227-244: LGTM!

The lifecycle management correctly:

  • Sets dev override on mount before applying theme
  • Watches store changes with skipStore: true to prevent circular updates
  • Separates user-initiated changes (with server persistence) from external sync

247-286: LGTM!

Template correctly binds to currentTheme and disables the select during saving/loading operations. The scoped styles appropriately handle the disabled state.


29-32: No case mismatch exists — review comment is incorrect.

The ThemeName enum values are lowercase strings ('azure', 'black', 'gray', 'white'), not uppercase. On line 27, themeValues is populated from themeOptions.map((option) => option.value), where each option.value is a ThemeName enum member with a lowercase string value. The normalizeTheme function correctly converts input to lowercase and checks membership against themeValues, which contains lowercase strings. The logic functions as intended—no fix is needed.

api/dev/configs/api.json (1)

2-2: Version bump looks consistent

Only the dev API config version was updated; structure and other fields are unchanged. As long as 4.27.2 matches the API build you’re shipping, this is good to go.

api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (1)

27-30: CustomizationMutations container wiring matches existing mutation pattern

Defining CustomizationMutations and exposing it via RootMutations.customization mirrors the existing nested mutation containers (array/docker/vm/apiKey/parityCheck/rclone). Naming and descriptions are consistent.

Also applies to: 62-63

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

1-25: setTheme mutation resolver is well-scoped and permissioned

The resolver cleanly delegates to CustomizationService.setTheme, uses ThemeName as the argument type and returns Theme, and applies UsePermissions({ action: AuthAction.UPDATE_ANY, resource: Resource.CUSTOMIZATIONS }) in line with existing mutation patterns. No additional error handling is needed here since it’s handled in the service.

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

6-6: RootMutations.customization entry correctly exposes customization mutations

The new @Mutation(() => CustomizationMutations, { name: 'customization' }) method matches the existing container-style mutations and cleanly returns a new CustomizationMutations instance. Import updates are correct.

Also applies to: 41-44

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

3-3: CustomizationModule now cleanly wires mutations resolver and exports the service

Adding CustomizationMutationsResolver to providers and exporting CustomizationService aligns this module with how other resolver/service modules are structured. No issues.

Also applies to: 8-9

api/generated-schema.graphql (1)

947-963: Schema additions for customization/theming align with resolvers; ensure dependent codegen is updated

The generated SDL updates look internally consistent:

  • type CustomizationMutations with setTheme(theme: ThemeName!): Theme! matches CustomizationMutationsResolver.setTheme.
  • enum ThemeName { azure black gray white } aligns with the service logic that maps dynamix.display.theme to ThemeName.
  • Mutation.customization: CustomizationMutations! matches the new RootMutationsResolver.customization() entry.

Since this file is generated, the main follow-up is to ensure all dependent codegen is refreshed (e.g., web GraphQL TypeScript types via pnpm codegen in web/) so clients see ThemeName, CustomizationMutations, and the customization mutation root.

Also applies to: 2461-2461

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

12-15: Argument shape is correct; loadDynamixConfigFromDiskSync receives the right data structure

The review comment's concern about the loader argument is based on a misunderstanding. The code is correct as-is:

  • Line 50 in onModuleInit: this.configFile = paths['dynamix-config']?.[1] extracts a single file path string for use with updateCfgFile.
  • Line 478 in setTheme: loadDynamixConfigFromDiskSync(paths['dynamix-config']) correctly passes the full array to the loader.

The function signature requires configPaths: readonly (string | undefined | null)[] (an array), and paths['dynamix-config'] is exactly that. This matches the existing pattern in api/src/store/index.ts line 32, which also passes the full array: loadDynamixConfigFromDiskSync(configPaths) where configPaths = store.getState().paths['dynamix-config'] ?? [].

Passing this.configFile (a single string) would violate the type contract and break the function.

Optional: add error handling for reload/dispatch resilience

If the disk read or dispatch fails after updateCfgFile succeeds, the mutation currently rejects even though the config on disk is updated. Wrapping the reload/dispatch in a try-catch that logs but proceeds to getTheme() would make the endpoint more resilient, though this is optional.

web/vite-plugin-serve-static.ts (4)

60-69: Config path resolution order looks reasonable for dev

The candidate resolution favors explicit env vars (DEV_DYNAMIX_CFG, PATHS_DYNAMIX_CONFIG) before falling back to repo-relative paths, which is a sane precedence for a dev-only tool. The existsSync probe in findDynamixConfigPath is simple and should behave predictably across the small candidate list.

No changes needed from my side; just confirm this order matches how you expect to override local configs during development.


111-152: Config write logic is correct; consider concurrency edge cases only if needed

The writeThemeToConfig read–modify–write flow correctly:

  • Preserves existing sections.
  • Updates an existing theme= in [display] if present.
  • Otherwise inserts theme= into an existing [display] or creates the section at the end.

For a Vite dev server this is plenty. The only theoretical gap is concurrent writes (multiple /dev/theme POSTs) racing on the same file, but that’s unlikely and probably not worth guarding against here.

No required changes; just be aware of the race window in case this ever gets reused outside dev tooling.


153-162: Request body reader is fine for dev; no size limits

readRequestBody is straightforward and adequate for the small JSON payload expected on /dev/theme. If this ever grows beyond a tiny dev interface, you might want to:

  • Impose a max body size and reject larger payloads.
  • Handle req.aborted explicitly.

For now, I’d keep it as-is.


230-276: /dev/theme endpoint behavior looks correct and robust

The middleware cleanly handles:

  • GET → returns { theme: ThemeName | null } JSON from config.
  • POST → reads JSON body, validates theme as a string, normalizes against ALLOWED_THEMES, persists via writeThemeToConfig, and returns the result plus theme.
  • Other methods → 405 with an Allow header.
  • Malformed JSON → 400 with a structured error.

This aligns well with the PR’s goal of a small dev-only surface for theme persistence, and keeps responses consistently JSON-typed for the main flows.

No behavior changes to suggest here.

Comment on lines +104 to +140
type SetThemeMutationResult = {
customization: {
setTheme: {
name: ThemeName;
showBannerImage: boolean;
showBannerGradient: boolean;
headerBackgroundColor?: string | null;
showHeaderDescription: boolean;
headerPrimaryTextColor?: string | null;
headerSecondaryTextColor?: string | null;
};
};
};
type SetThemeMutationVariables = {
theme: ThemeName;
};
const SET_THEME_MUTATION = gql`
mutation setTheme($theme: ThemeName!) {
customization {
setTheme(theme: $theme) {
name
showBannerImage
showBannerGradient
headerBackgroundColor
showHeaderDescription
headerPrimaryTextColor
headerSecondaryTextColor
}
}
}
`;
const { mutate: setThemeMutation } = useMutation<SetThemeMutationResult, SetThemeMutationVariables>(
SET_THEME_MUTATION
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Script executed:

# Check if SetThemeMutation types exist in generated graphql files
rg -n "SetThemeMutation|SetTheme" --type ts web/composables/gql/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Explore .standalone.vue files to understand the pattern
fd -e "standalone.vue" web/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

stderr:

[fd error]: The search pattern 'web/' contains a path-separation character ('/') and will not lead to any search results.

If you want to search for all files inside the 'web/' directory, use a match-all pattern:

  fd . 'web/'

Instead, if you want your pattern to match the full file path, use:

  fd --full-path 'web/'

</shell_result>


Script executed:

# Check the current DevThemeSwitcher.standalone.vue file content
wc -l web/src/components/DevThemeSwitcher.standalone.vue

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

286 web/src/components/DevThemeSwitcher.standalone.vue

</shell_result>


Script executed:

# Find .standalone.vue files correctly
fd -e standalone.vue . web/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

web/src/components/Activation/WelcomeModal.standalone.vue
web/src/components/ApiKeyAuthorize.standalone.vue
web/src/components/ApiKeyPage.standalone.vue
web/src/components/ApiStatus/ApiStatus.standalone.vue
web/src/components/Auth.standalone.vue
web/src/components/CallbackHandler.standalone.vue
web/src/components/ColorSwitcher.standalone.vue
web/src/components/ConnectSettings/ConnectSettings.standalone.vue
web/src/components/DevModalTest.standalone.vue
web/src/components/DevThemeSwitcher.standalone.vue
web/src/components/DowngradeOs.standalone.vue
web/src/components/HeaderOsVersion.standalone.vue
web/src/components/LayoutViews/Detail/DetailTest.standalone.vue
web/src/components/Logs/LogViewer.standalone.vue
web/src/components/Modals.standalone.vue
web/src/components/Registration.standalone.vue
web/src/components/SsoButton.standalone.vue
web/src/components/TestThemeSwitcher.standalone.vue
web/src/components/ThemeSwitcher.standalone.vue
web/src/components/UpdateOs/TestUpdateModal.standalone.vue
web/src/components/UpdateOs.standalone.vue
web/src/components/UserProfile.standalone.vue
web/src/components/WanIpCheck.standalone.vue

</shell_result>


Script executed:

# Check what files exist in web/composables/gql
ls -la web/composables/gql/ 2>/dev/null || echo "Directory not found"

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

total 196
drwxr-xr-x 2 jailuser jailuser    120 Nov 26 21:30 .
drwxr-xr-x 3 jailuser jailuser     60 Nov 26 21:30 ..
-rw-r--r-- 1 jailuser jailuser   3934 Nov 26 21:30 fragment-masking.ts
-rw-r--r-- 1 jailuser jailuser  42524 Nov 26 21:30 gql.ts
-rw-r--r-- 1 jailuser jailuser 145290 Nov 26 21:30 graphql.ts
-rw-r--r-- 1 jailuser jailuser     59 Nov 26 21:30 index.ts

</shell_result>


Script executed:

# Read lines 104-140 of the file
sed -n '104,140p' web/src/components/DevThemeSwitcher.standalone.vue

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

type SetThemeMutationResult = {
  customization: {
    setTheme: {
      name: ThemeName;
      showBannerImage: boolean;
      showBannerGradient: boolean;
      headerBackgroundColor?: string | null;
      showHeaderDescription: boolean;
      headerPrimaryTextColor?: string | null;
      headerSecondaryTextColor?: string | null;
    };
  };
};

type SetThemeMutationVariables = {
  theme: ThemeName;
};

const SET_THEME_MUTATION = gql`
  mutation setTheme($theme: ThemeName!) {
    customization {
      setTheme(theme: $theme) {
        name
        showBannerImage
        showBannerGradient
        headerBackgroundColor
        showHeaderDescription
        headerPrimaryTextColor
        headerSecondaryTextColor
      }
    }
  }
`;

const { mutate: setThemeMutation } = useMutation<SetThemeMutationResult, SetThemeMutationVariables>(
  SET_THEME_MUTATION
);

</shell_result>


Script executed:

# Search for SetThemeMutation or SetTheme in the generated graphql types
rg -n "SetTheme" web/composables/gql/graphql.ts | head -20

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

</shell_result>


Script executed:

# Check ColorSwitcher.standalone.vue to see if it uses generated types or inline
grep -n "Mutation" web/src/components/ColorSwitcher.standalone.vue | head -20

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

</shell_result>


Script executed:

# Check TestThemeSwitcher.standalone.vue for comparison
grep -n "Mutation" web/src/components/TestThemeSwitcher.standalone.vue | head -20

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

</shell_result>


Script executed:

# Look for mutation type definitions in another standalone component
rg -n "type.*Mutation" web/src/components/ColorSwitcher.standalone.vue

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Search for setTheme (lowercase) in generated files
rg -n "setTheme" web/composables/gql/graphql.ts | head -10

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

</shell_result>


Script executed:

# Check the structure of graphql.ts to understand if SetThemeMutation should exist
head -100 web/composables/gql/graphql.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
  /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */
  BigInt: { input: any; output: any; }
  /** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
  DateTime: { input: string; output: string; }
  /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
  JSON: { input: any; output: any; }
  /** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */
  Port: { input: number; output: number; }
  /**
   *
   * ### Description:
   *
   * ID scalar type that prefixes the underlying ID with the server identifier on output and strips it on input.
   *
   * We use this scalar type to ensure that the ID is unique across all servers, allowing the same underlying resource ID to be used across different server instances.
   *
   * #### Input Behavior:
   *
   * When providing an ID as input (e.g., in arguments or input objects), the server identifier prefix ('<serverId>:') is optional.
   *
   * - If the prefix is present (e.g., '123:456'), it will be automatically stripped, and only the underlying ID ('456') will be used internally.
   * - If the prefix is absent (e.g., '456'), the ID will be used as-is.
   *
   * This makes it flexible for clients, as they don't strictly need to know or provide the server ID.
   *
   * #### Output Behavior:
   *
   * When an ID is returned in the response (output), it will *always* be prefixed with the current server's unique identifier (e.g., '123:456').
   *
   * #### Example:
   *
   * Note: The server identifier is '123' in this example.
   *
   * ##### Input (Prefix Optional):
   * ```graphql
   * # Both of these are valid inputs resolving to internal ID '456'
   * {
   *   someQuery(id: "123:456") { ... }
   *   anotherQuery(id: "456") { ... }
   * }
   * ```
   *
   * ##### Output (Prefix Always Added):
   * ```graphql
   * # Assuming internal ID is '456'
   * {
   *   "data": {
   *     "someResource": {
   *       "id": "123:456"
   *     }
   *   }
   * }
   * ```
   *
   */
  PrefixedID: { input: string; output: string; }
  /** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */
  URL: { input: URL; output: URL; }
};

export type AccessUrl = {
  __typename?: 'AccessUrl';
  ipv4?: Maybe<Scalars['URL']['output']>;
  ipv6?: Maybe<Scalars['URL']['output']>;
  name?: Maybe<Scalars['String']['output']>;
  type: UrlType;
};

export type AccessUrlInput = {
  ipv4?: InputMaybe<Scalars['URL']['input']>;
  ipv6?: InputMaybe<Scalars['URL']['input']>;
  name?: InputMaybe<Scalars['String']['input']>;
  type: UrlType;
};

export type AccessUrlObject = {
  __typename?: 'AccessUrlObject';
  ipv4?: Maybe<Scalars['String']['output']>;
  ipv6?: Maybe<Scalars['String']['output']>;
  name?: Maybe<Scalars['String']['output']>;
  type: UrlType;
};

export type AccessUrlObjectInput = {

</shell_result>


Script executed:

# Look at ColorSwitcher.standalone.vue content
cat web/src/components/ColorSwitcher.standalone.vue

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

<script lang="ts" setup>
import { reactive, watch } from 'vue';

import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
  Input,
  Label,
  Select,
  Switch,
} from '@unraid/ui';
import { defaultColors } from '~/themes/default';

import type { Theme } from '~/themes/types';

import { useThemeStore } from '~/store/theme';

const themeStore = useThemeStore();

// Form state
const form = reactive({
  selectedTheme: 'white',
  gradient: false,
  description: true,
  banner: true,
  textPrimary: '',
  textSecondary: '',
  bgColor: '',
});

// Watch for changes and update theme
watch([form], () => {
  // Enable gradient if banner is enabled
  if (form.banner && !form.gradient) {
    form.gradient = true;
  }

  const themeToSet: Theme = {
    banner: form.banner,
    bannerGradient: form.gradient,
    descriptionShow: form.description,
    textColor: form.textPrimary ?? defaultColors[form.selectedTheme]['--header-text-primary']!,
    metaColor: form.textSecondary ?? defaultColors[form.selectedTheme]['--header-text-secondary']!,
    bgColor: form.bgColor ?? defaultColors[form.selectedTheme]['--header-background-color']!,
    name: form.selectedTheme,
  };
  themeStore.setTheme(themeToSet);
});

const items = [
  { value: 'white', label: 'Light' },
  { value: 'black', label: 'Dark' },
  { value: 'azure', label: 'Azure' },
  { value: 'gray', label: 'Gray' },
];
</script>

<template>
  <Accordion>
    <AccordionItem value="color-theme-customization">
      <AccordionTrigger>Color Theme Customization</AccordionTrigger>
      <AccordionContent>
        <div class="border-muted flex flex-col gap-2 border-2 border-solid p-2">
          <h1 class="text-lg">Color Theme Customization</h1>

          <Label for="theme-select">Theme</Label>
          <Select v-model="form.selectedTheme" :items="items" placeholder="Select a theme" />

          <Label for="primary-text-color">Header Primary Text Color</Label>
          <Input id="primary-text-color" v-model="form.textPrimary" />

          <Label for="secondary-text-color">Header Secondary Text Color</Label>
          <Input id="secondary-text-color" v-model="form.textSecondary" />

          <Label for="background-color">Header Background Color</Label>
          <Input id="background-color" v-model="form.bgColor" />

          <Label for="gradient">Gradient</Label>
          <Switch id="gradient" v-model:checked="form.gradient" />

          <Label for="description">Description</Label>
          <Switch id="description" v-model:checked="form.description" />

          <Label for="banner">Banner</Label>
          <Switch id="banner" v-model:checked="form.banner" />
        </div>
      </AccordionContent>
    </AccordionItem>
  </Accordion>
</template>

</shell_result>


Script executed:

# Search for mutation definitions in graphql.ts
rg -n "mutation|Mutation" web/composables/gql/graphql.ts | head -30

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

163:/** API Key related mutations */
164:export type ApiKeyMutations = {
165:  __typename?: 'ApiKeyMutations';
179:/** API Key related mutations */
180:export type ApiKeyMutationsAddRoleArgs = {
185:/** API Key related mutations */
186:export type ApiKeyMutationsCreateArgs = {
191:/** API Key related mutations */
192:export type ApiKeyMutationsDeleteArgs = {
197:/** API Key related mutations */
198:export type ApiKeyMutationsRemoveRoleArgs = {
203:/** API Key related mutations */
204:export type ApiKeyMutationsUpdateArgs = {
304:export type ArrayMutations = {
305:  __typename?: 'ArrayMutations';
321:export type ArrayMutationsAddDiskToArrayArgs = {
326:export type ArrayMutationsClearArrayDiskStatisticsArgs = {
331:export type ArrayMutationsMountArrayDiskArgs = {
336:export type ArrayMutationsRemoveDiskFromArrayArgs = {
341:export type ArrayMutationsSetStateArgs = {
346:export type ArrayMutationsUnmountArrayDiskArgs = {
713:export type DockerMutations = {
714:  __typename?: 'DockerMutations';
722:export type DockerMutationsStartArgs = {
727:export type DockerMutationsStopArgs = {
1199:export type Mutation = {
1200:  __typename?: 'Mutation';
1203:  apiKey: ApiKeyMutations;
1208:  array: ArrayMutations;
1219:  docker: DockerMutations;

</shell_result>


Script executed:

# Search for setTheme in gql.ts
rg -n "setTheme" web/composables/gql/gql.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Check if non-standalone ThemeSwitcher exists
ls -la web/src/components/ThemeSwitcher.vue 2>/dev/null || echo "Not found"

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

Not found

</shell_result>


Script executed:

# Check the full DevThemeSwitcher.standalone.vue to understand context
head -50 web/src/components/DevThemeSwitcher.standalone.vue

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useMutation, useQuery } from '@vue/apollo-composable';

import gql from 'graphql-tag';

import type { GetThemeQuery } from '~/composables/gql/graphql';

import { ThemeName } from '~/composables/gql/graphql';
import { DARK_UI_THEMES, GET_THEME_QUERY, useThemeStore } from '~/store/theme';

const themeStore = useThemeStore();

const themeOptions: Array<{ value: ThemeName; label: string }> = [
  { value: ThemeName.WHITE, label: 'White' },
  { value: ThemeName.BLACK, label: 'Black' },
  { value: ThemeName.GRAY, label: 'Gray' },
  { value: ThemeName.AZURE, label: 'Azure' },
];

const STORAGE_KEY_THEME = 'unraid:test:theme';
const THEME_COOKIE_KEY = 'unraid_dev_theme';

const { theme } = storeToRefs(themeStore);

const themeValues = new Set<ThemeName>(themeOptions.map((option) => option.value));

const normalizeTheme = (value?: string | ThemeName | null): ThemeName | null => {
  const normalized = (value ?? '').toString().toLowerCase();
  return themeValues.has(normalized as ThemeName) ? (normalized as ThemeName) : null;
};

const readCookieTheme = (): string | null => {
  if (typeof document === 'undefined') {
    return null;
  }

  const cookies = document.cookie?.split(';') ?? [];
  for (const cookie of cookies) {
    const [name, ...rest] = cookie.split('=');
    if (name?.trim() === THEME_COOKIE_KEY) {
      return decodeURIComponent(rest.join('=').trim());
    }
  }

  return null;
};

const readLocalStorageTheme = (): string | null => {

</shell_result>


Script executed:

# Search for SetThemeMutation export in graphql.ts
rg -n "export type SetTheme" web/composables/gql/graphql.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Check ~/store/theme.ts
cat web/store/theme.ts | head -100

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stderr:

cat: web/store/theme.ts: No such file or directory

</shell_result>


Script executed:

# Search for setTheme string definition in gql.ts (the query/mutation definitions)
rg -A5 "setTheme" web/composables/gql/gql.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Find theme.ts file
fd -e "theme.ts" web/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

stderr:

[fd error]: The search pattern 'web/' contains a path-separation character ('/') and will not lead to any search results.

If you want to search for all files inside the 'web/' directory, use a match-all pattern:

  fd . 'web/'

Instead, if you want your pattern to match the full file path, use:

  fd --full-path 'web/'

</shell_result>


Script executed:

# Search for GET_THEME_QUERY to find where it's defined
rg -n "GET_THEME_QUERY" web/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

web/src/components/DevThemeSwitcher.standalone.vue:11:import { DARK_UI_THEMES, GET_THEME_QUERY, useThemeStore } from '~/store/theme';
web/src/components/DevThemeSwitcher.standalone.vue:84:  GET_THEME_QUERY,
web/src/store/theme.ts:15:export const GET_THEME_QUERY = graphql(`
web/src/store/theme.ts:117:  const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, {

</shell_result>


Script executed:

# Search for customization mutations in graphql.ts
rg -n "customization|setTheme" web/composables/gql/graphql.ts | head -20

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

1648:  customization?: Maybe<Customization>;
2567:export type ActivationCodeQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', regState?: RegistrationState | null }, customization?: { __typename?: 'Customization', activationCode?: { __typename?: 'ActivationCode', code?: string | null, partnerName?: string | null, serverName?: string | null, sysModel?: string | null, comment?: string | null, header?: string | null, headermetacolor?: string | null, background?: string | null, showBannerGradient?: boolean | null, theme?: string | null } | null, partnerInfo?: { __typename?: 'PublicPartnerInfo', hasPartnerLogo: boolean, partnerName?: string | null, partnerUrl?: string | null, partnerLogoUrl?: string | null } | null } | null };
2830:export const ActivationCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActivationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regState"}}]}},{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"header"}},{"kind":"Field","name":{"kind":"Name","value":"headermetacolor"}},{"kind":"Field","name":{"kind":"Name","value":"background"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"theme"}}]}},{"kind":"Field","name":{"kind":"Name","value":"partnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ActivationCodeQuery, ActivationCodeQueryVariables>;

</shell_result>


Script executed:

# List what's exported from gql.ts
rg -n "^export" web/composables/gql/gql.ts | head -40

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

119:export function graphql(source: string): unknown;
124:export function graphql(source: "\n  query PartnerInfo {\n    publicPartnerInfo {\n      hasPartnerLogo\n      partnerName\n      partnerUrl\n      partnerLogoUrl\n    }\n  }\n"): (typeof documents)["\n  query PartnerInfo {\n    publicPartnerInfo {\n      hasPartnerLogo\n      partnerName\n      partnerUrl\n      partnerLogoUrl\n    }\n  }\n"];
128:export function graphql(source: "\n  query PublicWelcomeData {\n    publicPartnerInfo {\n      hasPartnerLogo\n      partnerName\n      partnerUrl\n      partnerLogoUrl\n    }\n    isInitialSetup\n  }\n"): (typeof documents)["\n  query PublicWelcomeData {\n    publicPartnerInfo {\n      hasPartnerLogo\n      partnerName\n      partnerUrl\n      partnerLogoUrl\n    }\n    isInitialSetup\n  }\n"];
132:export function graphql(source: "\n  query ActivationCode {\n    vars {\n      regState\n    }\n    customization {\n      activationCode {\n        code\n        partnerName\n        serverName\n        sysModel\n        comment\n        header\n        headermetacolor\n        background\n        showBannerGradient\n        theme\n      }\n      partnerInfo {\n        hasPartnerLogo\n        partnerName\n        partnerUrl\n        partnerLogoUrl\n      }\n    }\n  }\n"): (typeof documents)["\n  query ActivationCode {\n    vars {\n      regState\n    }\n    customization {\n      activationCode {\n        code\n        partnerName\n        serverName\n        sysModel\n        comment\n        header\n        headermetacolor\n        background\n        showBannerGradient\n        theme\n      }\n      partnerInfo {\n        hasPartnerLogo\n        partnerName\n        partnerUrl\n        partnerLogoUrl\n      }\n    }\n  }\n"];
136:export function graphql(source: "\n  query GetApiKeyCreationFormSchema {\n    getApiKeyCreationFormSchema {\n      id\n      dataSchema\n      uiSchema\n      values\n    }\n  }\n"): (typeof documents)["\n  query GetApiKeyCreationFormSchema {\n    getApiKeyCreationFormSchema {\n      id\n      dataSchema\n      uiSchema\n      values\n    }\n  }\n"];
140:export function graphql(source: "\n  fragment ApiKey on ApiKey {\n    id\n    key\n    name\n    description\n    createdAt\n    roles\n    permissions {\n      resource\n      actions\n    }\n  }\n"): (typeof documents)["\n  fragment ApiKey on ApiKey {\n    id\n    key\n    name\n    description\n    createdAt\n    roles\n    permissions {\n      resource\n      actions\n    }\n  }\n"];
144:export function graphql(source: "\n  query ApiKeys {\n    apiKeys {\n      ...ApiKey\n    }\n  }\n"): (typeof documents)["\n  query ApiKeys {\n    apiKeys {\n      ...ApiKey\n    }\n  }\n"];
148:export function graphql(source: "\n  mutation CreateApiKey($input: CreateApiKeyInput!) {\n    apiKey {\n      create(input: $input) {\n        ...ApiKey\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation CreateApiKey($input: CreateApiKeyInput!) {\n    apiKey {\n      create(input: $input) {\n        ...ApiKey\n      }\n    }\n  }\n"];
152:export function graphql(source: "\n  mutation UpdateApiKey($input: UpdateApiKeyInput!) {\n    apiKey {\n      update(input: $input) {\n        ...ApiKey\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation UpdateApiKey($input: UpdateApiKeyInput!) {\n    apiKey {\n      update(input: $input) {\n        ...ApiKey\n      }\n    }\n  }\n"];
156:export function graphql(source: "\n  mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n    apiKey {\n      delete(input: $input)\n    }\n  }\n"): (typeof documents)["\n  mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n    apiKey {\n      delete(input: $input)\n    }\n  }\n"];
160:export function graphql(source: "\n  query ApiKeyMeta {\n    apiKeyPossibleRoles\n    apiKeyPossiblePermissions {\n      resource\n      actions\n    }\n  }\n"): (typeof documents)["\n  query ApiKeyMeta {\n    apiKeyPossibleRoles\n    apiKeyPossiblePermissions {\n      resource\n      actions\n    }\n  }\n"];
164:export function graphql(source: "\n  query PreviewEffectivePermissions($roles: [Role!], $permissions: [AddPermissionInput!]) {\n    previewEffectivePermissions(roles: $roles, permissions: $permissions) {\n      resource\n      actions\n    }\n  }\n"): (typeof documents)["\n  query PreviewEffectivePermissions($roles: [Role!], $permissions: [AddPermissionInput!]) {\n    previewEffectivePermissions(roles: $roles, permissions: $permissions) {\n      resource\n      actions\n    }\n  }\n"];
168:export function graphql(source: "\n  query GetPermissionsForRoles($roles: [Role!]!) {\n    getPermissionsForRoles(roles: $roles) {\n      resource\n      actions\n    }\n  }\n"): (typeof documents)["\n  query GetPermissionsForRoles($roles: [Role!]!) {\n    getPermissionsForRoles(roles: $roles) {\n      resource\n      actions\n    }\n  }\n"];
172:export function graphql(source: "\n  query Unified {\n    settings {\n      unified {\n        id\n        dataSchema\n        uiSchema\n        values\n      }\n    }\n  }\n"): (typeof documents)["\n  query Unified {\n    settings {\n      unified {\n        id\n        dataSchema\n        uiSchema\n        values\n      }\n    }\n  }\n"];
176:export function graphql(source: "\n  mutation UpdateConnectSettings($input: JSON!) {\n    updateSettings(input: $input) {\n      restartRequired\n      values\n    }\n  }\n"): (typeof documents)["\n  mutation UpdateConnectSettings($input: JSON!) {\n    updateSettings(input: $input) {\n      restartRequired\n      values\n    }\n  }\n"];
180:export function graphql(source: "\n  query LogFiles {\n    logFiles {\n      name\n      path\n      size\n      modifiedAt\n    }\n  }\n"): (typeof documents)["\n  query LogFiles {\n    logFiles {\n      name\n      path\n      size\n      modifiedAt\n    }\n  }\n"];
184:export function graphql(source: "\n  query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n    logFile(path: $path, lines: $lines, startLine: $startLine) {\n      path\n      content\n      totalLines\n      startLine\n    }\n  }\n"): (typeof documents)["\n  query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n    logFile(path: $path, lines: $lines, startLine: $startLine) {\n      path\n      content\n      totalLines\n      startLine\n    }\n  }\n"];
188:export function graphql(source: "\n  subscription LogFileSubscription($path: String!) {\n    logFile(path: $path) {\n      path\n      content\n      totalLines\n    }\n  }\n"): (typeof documents)["\n  subscription LogFileSubscription($path: String!) {\n    logFile(path: $path) {\n      path\n      content\n      totalLines\n    }\n  }\n"];
192:export function graphql(source: "\n  fragment NotificationFragment on Notification {\n    id\n    title\n    subject\n    description\n    importance\n    link\n    type\n    timestamp\n    formattedTimestamp\n  }\n"): (typeof documents)["\n  fragment NotificationFragment on Notification {\n    id\n    title\n    subject\n    description\n    importance\n    link\n    type\n    timestamp\n    formattedTimestamp\n  }\n"];
196:export function graphql(source: "\n  fragment NotificationCountFragment on NotificationCounts {\n    total\n    info\n    warning\n    alert\n  }\n"): (typeof documents)["\n  fragment NotificationCountFragment on NotificationCounts {\n    total\n    info\n    warning\n    alert\n  }\n"];
200:export function graphql(source: "\n  query Notifications($filter: NotificationFilter!) {\n    notifications {\n      id\n      list(filter: $filter) {\n        ...NotificationFragment\n      }\n    }\n  }\n"): (typeof documents)["\n  query Notifications($filter: NotificationFilter!) {\n    notifications {\n      id\n      list(filter: $filter) {\n        ...NotificationFragment\n      }\n    }\n  }\n"];
204:export function graphql(source: "\n  mutation ArchiveNotification($id: PrefixedID!) {\n    archiveNotification(id: $id) {\n      ...NotificationFragment\n    }\n  }\n"): (typeof documents)["\n  mutation ArchiveNotification($id: PrefixedID!) {\n    archiveNotification(id: $id) {\n      ...NotificationFragment\n    }\n  }\n"];
208:export function graphql(source: "\n  mutation ArchiveAllNotifications {\n    archiveAll {\n      unread {\n        total\n      }\n      archive {\n        info\n        warning\n        alert\n        total\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation ArchiveAllNotifications {\n    archiveAll {\n      unread {\n        total\n      }\n      archive {\n        info\n        warning\n        alert\n        total\n      }\n    }\n  }\n"];
212:export function graphql(source: "\n  mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n    deleteNotification(id: $id, type: $type) {\n      archive {\n        total\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n    deleteNotification(id: $id, type: $type) {\n      archive {\n        total\n      }\n    }\n  }\n"];
216:export function graphql(source: "\n  mutation DeleteAllNotifications {\n    deleteArchivedNotifications {\n      archive {\n        total\n      }\n      unread {\n        total\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation DeleteAllNotifications {\n    deleteArchivedNotifications {\n      archive {\n        total\n      }\n      unread {\n        total\n      }\n    }\n  }\n"];
220:export function graphql(source: "\n  query Overview {\n    notifications {\n      id\n      overview {\n        unread {\n          info\n          warning\n          alert\n          total\n        }\n        archive {\n          total\n        }\n      }\n    }\n  }\n"): (typeof documents)["\n  query Overview {\n    notifications {\n      id\n      overview {\n        unread {\n          info\n          warning\n          alert\n          total\n        }\n        archive {\n          total\n        }\n      }\n    }\n  }\n"];
224:export function graphql(source: "\n  mutation RecomputeOverview {\n    recalculateOverview {\n      archive {\n        ...NotificationCountFragment\n      }\n      unread {\n        ...NotificationCountFragment\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation RecomputeOverview {\n    recalculateOverview {\n      archive {\n        ...NotificationCountFragment\n      }\n      unread {\n        ...NotificationCountFragment\n      }\n    }\n  }\n"];
228:export function graphql(source: "\n  subscription NotificationAddedSub {\n    notificationAdded {\n      ...NotificationFragment\n    }\n  }\n"): (typeof documents)["\n  subscription NotificationAddedSub {\n    notificationAdded {\n      ...NotificationFragment\n    }\n  }\n"];
232:export function graphql(source: "\n  subscription NotificationOverviewSub {\n    notificationsOverview {\n      archive {\n        ...NotificationCountFragment\n      }\n      unread {\n        ...NotificationCountFragment\n      }\n    }\n  }\n"): (typeof documents)["\n  subscription NotificationOverviewSub {\n    notificationsOverview {\n      archive {\n        ...NotificationCountFragment\n      }\n      unread {\n        ...NotificationCountFragment\n      }\n    }\n  }\n"];
236:export function graphql(source: "\n  mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n    rclone {\n      createRCloneRemote(input: $input) {\n        name\n        type\n        parameters\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n    rclone {\n      createRCloneRemote(input: $input) {\n        name\n        type\n        parameters\n      }\n    }\n  }\n"];
240:export function graphql(source: "\n  mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n    rclone {\n      deleteRCloneRemote(input: $input)\n    }\n  }\n"): (typeof documents)["\n  mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n    rclone {\n      deleteRCloneRemote(input: $input)\n    }\n  }\n"];
244:export function graphql(source: "\n  query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n    rclone {\n      configForm(formOptions: $formOptions) {\n        id\n        dataSchema\n        uiSchema\n      }\n    }\n  }\n"): (typeof documents)["\n  query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n    rclone {\n      configForm(formOptions: $formOptions) {\n        id\n        dataSchema\n        uiSchema\n      }\n    }\n  }\n"];
248:export function graphql(source: "\n  query ListRCloneRemotes {\n    rclone {\n      remotes {\n        name\n        type\n        parameters\n        config\n      }\n    }\n  }\n"): (typeof documents)["\n  query ListRCloneRemotes {\n    rclone {\n      remotes {\n        name\n        type\n        parameters\n        config\n      }\n    }\n  }\n"];
252:export function graphql(source: "\n  query InfoVersions {\n    info {\n      id\n      os {\n        id\n        hostname\n      }\n      versions {\n        id\n        core {\n          unraid\n          api\n        }\n      }\n    }\n  }\n"): (typeof documents)["\n  query InfoVersions {\n    info {\n      id\n      os {\n        id\n        hostname\n      }\n      versions {\n        id\n        core {\n          unraid\n          api\n        }\n      }\n    }\n  }\n"];
256:export function graphql(source: "\n  query OidcProviders {\n    settings {\n      sso {\n        oidcProviders {\n          id\n          name\n          clientId\n          issuer\n          authorizationEndpoint\n          tokenEndpoint\n          jwksUri\n          scopes\n          authorizationRules {\n            claim\n            operator\n            value\n          }\n          authorizationRuleMode\n          buttonText\n          buttonIcon\n        }\n      }\n    }\n  }\n"): (typeof documents)["\n  query OidcProviders {\n    settings {\n      sso {\n        oidcProviders {\n          id\n          name\n          clientId\n          issuer\n          authorizationEndpoint\n          tokenEndpoint\n          jwksUri\n          scopes\n          authorizationRules {\n            claim\n            operator\n            value\n          }\n          authorizationRuleMode\n          buttonText\n          buttonIcon\n        }\n      }\n    }\n  }\n"];
260:export function graphql(source: "\n  query PublicOidcProviders {\n    publicOidcProviders {\n      id\n      name\n      buttonText\n      buttonIcon\n      buttonVariant\n      buttonStyle\n    }\n  }\n"): (typeof documents)["\n  query PublicOidcProviders {\n    publicOidcProviders {\n      id\n      name\n      buttonText\n      buttonIcon\n      buttonVariant\n      buttonStyle\n    }\n  }\n"];
264:export function graphql(source: "\n  query serverInfo {\n    info {\n      os {\n        hostname\n      }\n    }\n    vars {\n      comment\n    }\n  }\n"): (typeof documents)["\n  query serverInfo {\n    info {\n      os {\n        hostname\n      }\n    }\n    vars {\n      comment\n    }\n  }\n"];
268:export function graphql(source: "\n  mutation ConnectSignIn($input: ConnectSignInInput!) {\n    connectSignIn(input: $input)\n  }\n"): (typeof documents)["\n  mutation ConnectSignIn($input: ConnectSignInInput!) {\n    connectSignIn(input: $input)\n  }\n"];
272:export function graphql(source: "\n  mutation SignOut {\n    connectSignOut\n  }\n"): (typeof documents)["\n  mutation SignOut {\n    connectSignOut\n  }\n"];
276:export function graphql(source: "\n  query IsSSOEnabled {\n    isSSOEnabled\n  }\n"): (typeof documents)["\n  query IsSSOEnabled {\n    isSSOEnabled\n  }\n"];

</shell_result>


Script executed:

# Search for setTheme in gql.ts more broadly
grep -n "setTheme" web/composables/gql/gql.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Check theme.ts file content
cat web/src/store/theme.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useLazyQuery } from '@vue/apollo-composable';

import { defaultColors } from '~/themes/default';

import type { GetThemeQuery } from '~/composables/gql/graphql';
import type { Theme, ThemeVariables } from '~/themes/types';

import { graphql } from '~/composables/gql/gql';

// Themes that should apply the .dark class (dark UI themes)
export const DARK_UI_THEMES = ['gray', 'black'] as const;

export const GET_THEME_QUERY = graphql(`
  query getTheme {
    publicTheme {
      name
      showBannerImage
      showBannerGradient
      headerBackgroundColor
      showHeaderDescription
      headerPrimaryTextColor
      headerSecondaryTextColor
    }
  }
`);

const DEFAULT_THEME: Theme = {
  name: 'white',
  banner: false,
  bannerGradient: false,
  bgColor: '',
  descriptionShow: false,
  metaColor: '',
  textColor: '',
};

type ThemeSource = 'local' | 'server';

const isDomAvailable = () => typeof document !== 'undefined';

const getCssVar = (name: string): string => {
  if (!isDomAvailable()) return '';
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
};

const readDomThemeName = () => getCssVar('--theme-name');

const readDomDarkFlag = (): boolean | null => {
  if (!isDomAvailable()) return null;
  const cssVar = getCssVar('--theme-dark-mode');
  if (cssVar === '1') return true;
  if (cssVar === '0') return false;
  return null;
};

const hasDarkClassApplied = (): boolean => {
  if (!isDomAvailable()) return false;
  if (document.documentElement.classList.contains('dark')) return true;
  if (document.body?.classList.contains('dark')) return true;
  return Boolean(document.querySelector('.unapi.dark'));
};

const syncDarkClass = (method: 'add' | 'remove') => {
  if (!isDomAvailable()) return;
  document.documentElement.classList[method]('dark');
  document.body?.classList[method]('dark');
  document.querySelectorAll('.unapi').forEach((el) => el.classList[method]('dark'));
};

const applyDarkClass = (isDark: boolean) => {
  if (!isDomAvailable()) return;
  const method: 'add' | 'remove' = isDark ? 'add' : 'remove';
  syncDarkClass(method);
  document.documentElement.style.setProperty('--theme-dark-mode', isDark ? '1' : '0');
};

const bootstrapDarkClass = () => {
  const domFlag = readDomDarkFlag();
  const shouldBeDark = domFlag ?? hasDarkClassApplied();
  if (shouldBeDark) {
    applyDarkClass(true);
  }
};

const sanitizeTheme = (data: Partial<Theme> | null | undefined): Theme | null => {
  if (!data || typeof data !== 'object') {
    return null;
  }

  return {
    name: typeof data.name === 'string' ? data.name : DEFAULT_THEME.name,
    banner: typeof data.banner === 'boolean' ? data.banner : DEFAULT_THEME.banner,
    bannerGradient:
      typeof data.bannerGradient === 'boolean' ? data.bannerGradient : DEFAULT_THEME.bannerGradient,
    bgColor: typeof data.bgColor === 'string' ? data.bgColor : DEFAULT_THEME.bgColor,
    descriptionShow:
      typeof data.descriptionShow === 'boolean' ? data.descriptionShow : DEFAULT_THEME.descriptionShow,
    metaColor: typeof data.metaColor === 'string' ? data.metaColor : DEFAULT_THEME.metaColor,
    textColor: typeof data.textColor === 'string' ? data.textColor : DEFAULT_THEME.textColor,
  };
};

export const useThemeStore = defineStore('theme', () => {
  // State
  const theme = ref<Theme>({ ...DEFAULT_THEME });

  const activeColorVariables = ref<ThemeVariables>(defaultColors.white);
  const hasServerTheme = ref(false);
  const devOverride = ref(false);

  // Initialize dark mode from CSS variable set by PHP or any pre-applied .dark class
  bootstrapDarkClass();

  // Lazy query - only executes when explicitly called
  const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, {
    fetchPolicy: 'cache-and-network',
    nextFetchPolicy: 'cache-first',
  });

  const mapPublicTheme = (publicTheme?: GetThemeQuery['publicTheme'] | null): Theme | null => {
    if (!publicTheme) {
      return null;
    }

    return sanitizeTheme({
      name: publicTheme.name?.toLowerCase(),
      banner: publicTheme.showBannerImage,
      bannerGradient: publicTheme.showBannerGradient,
      bgColor: publicTheme.headerBackgroundColor ?? undefined,
      descriptionShow: publicTheme.showHeaderDescription,
      metaColor: publicTheme.headerSecondaryTextColor ?? undefined,
      textColor: publicTheme.headerPrimaryTextColor ?? undefined,
    });
  };

  const applyThemeFromQuery = (publicTheme?: GetThemeQuery['publicTheme'] | null) => {
    const sanitized = mapPublicTheme(publicTheme);
    if (!sanitized) {
      return;
    }

    setTheme(sanitized, { source: 'server' });
  };

  onResult(({ data }) => {
    if (data?.publicTheme) {
      applyThemeFromQuery(data.publicTheme);
    }
  });

  onError((err) => {
    console.warn('Failed to load theme from server, keeping existing theme:', err);
  });

  // Getters - read from DOM CSS variables set by PHP
  const themeName = computed<string>(() => {
    if (!isDomAvailable()) return DEFAULT_THEME.name;
    const name = readDomThemeName() || theme.value.name;
    return name || DEFAULT_THEME.name;
  });

  const darkMode = computed<boolean>(() => {
    if (!isDomAvailable()) return false;
    const flag = readDomDarkFlag();
    return flag ?? hasDarkClassApplied();
  });

  const readBannerGradientVar = (): string => {
    const raw = getCssVar('--banner-gradient');
    if (!raw) return '';
    const normalized = raw.trim().toLowerCase();
    if (!normalized || normalized === 'null' || normalized === 'none' || normalized === 'undefined') {
      return '';
    }
    return raw;
  };

  const bannerGradient = computed(() => {
    if (darkMode.value) {
      return undefined;
    }
    const { banner, bannerGradient } = theme.value;
    if (!banner || !bannerGradient) {
      return undefined;
    }
    const gradient = readBannerGradientVar();
    // Only return gradient if CSS variable is set and not empty
    // CSS variable is only set by PHP when both banner and gradient are enabled
    if (gradient) {
      return `background-image: ${gradient};`;
    }
    // No fallback - only use CSS variable set by PHP
    return undefined;
  });

  // Actions
  function setTheme(data?: Partial<Theme>, options: { source?: ThemeSource } = {}) {
    if (data) {
      const { source = 'local' } = options;

      if (source === 'server') {
        hasServerTheme.value = true;
      } else if (hasServerTheme.value && !devOverride.value) {
        return;
      }

      const sanitized = sanitizeTheme({
        ...theme.value,
        ...data,
      });

      if (sanitized) {
        theme.value = sanitized;
        const fallbackTheme = defaultColors[sanitized.name as keyof typeof defaultColors];
        activeColorVariables.value = {
          ...(fallbackTheme ?? defaultColors.white),
        };
      }
    }
  }

  const setDevOverride = (enabled: boolean) => {
    devOverride.value = enabled;
  };

  const fetchTheme = () => {
    load();
  };

  // Only apply dark class when theme changes (for dev tools that don't refresh)
  // In production, PHP sets the dark class and page refreshes on theme change
  watch(
    () => theme.value.name,
    (themeName) => {
      const isDark = DARK_UI_THEMES.includes(themeName as (typeof DARK_UI_THEMES)[number]);
      applyDarkClass(isDark);
    },
    { immediate: false }
  );

  // Initialize theme from DOM on store creation
  const domThemeName = themeName.value;
  if (domThemeName && domThemeName !== DEFAULT_THEME.name) {
    theme.value.name = domThemeName;
  }

  return {
    // state
    activeColorVariables,
    bannerGradient,
    darkMode,
    theme: computed(() => ({
      ...theme.value,
      name: themeName.value,
    })),
    // actions
    setTheme,
    setDevOverride,
    fetchTheme,
  };
});

</shell_result>


Script executed:

# Search in graphql.ts for SetTheme or customization mutations
grep -n "SetTheme\|CustomizationMutations\|customization.*mutation" web/composables/gql/graphql.ts | head -20

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

</shell_result>


Script executed:

# Search for Customization in graphql.ts to find setTheme mutation
rg -n "Customization" web/composables/gql/graphql.ts | head -20

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

584:export type Customization = {
585:  __typename?: 'Customization';
1648:  customization?: Maybe<Customization>;
2567:export type ActivationCodeQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', regState?: RegistrationState | null }, customization?: { __typename?: 'Customization', activationCode?: { __typename?: 'ActivationCode', code?: string | null, partnerName?: string | null, serverName?: string | null, sysModel?: string | null, comment?: string | null, header?: string | null, headermetacolor?: string | null, background?: string | null, showBannerGradient?: boolean | null, theme?: string | null } | null, partnerInfo?: { __typename?: 'PublicPartnerInfo', hasPartnerLogo: boolean, partnerName?: string | null, partnerUrl?: string | null, partnerLogoUrl?: string | null } | null } | null };

</shell_result>


Script executed:

# Look for mutation definitions in the schema or codegen config
find . -name "*.graphql" -o -name "codegen.ts" -o -name "codegen.yml" | head -10

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

./api/codegen.ts
./api/legacy/generated-schema-legacy.graphql
./api/generated-schema.graphql
./packages/unraid-api-plugin-connect/codegen.ts
./web/codegen.ts

</shell_result>


Script executed:

# Check if there are any .mutation.ts files in web directory
fd -e mutation.ts web/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

stderr:

[fd error]: The search pattern 'web/' contains a path-separation character ('/') and will not lead to any search results.

If you want to search for all files inside the 'web/' directory, use a match-all pattern:

  fd . 'web/'

Instead, if you want your pattern to match the full file path, use:

  fd --full-path 'web/'

</shell_result>


Script executed:

# Get the full Customization type definition
rg -A 30 "^export type Customization = {" web/composables/gql/graphql.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 2

stderr:

regex parse error:
    ^export type Customization = {
                                 ^
error: unclosed counted repetition

</shell_result>


Script executed:

# Check web/codegen.ts
cat web/codegen.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  overwrite: true,
  documents: ['./**/**/*.ts'],
  ignoreNoDocuments: false,
  config: {
    namingConvention: {
      enumValues: 'change-case-all#upperCase',
      transformUnderscore: true,
    },
    scalars: {
      DateTime: 'string',
      Long: 'number',
      JSON: 'any',
      URL: 'URL',
      Port: 'number',
      UUID: 'string',
      PrefixedID: 'string',
      BigInt: 'number',
    },
  },
  generates: {
    'src/composables/gql/': {
      preset: 'client',
      config: {
        useTypeImports: true,
      },
      schema: '../api/generated-schema.graphql',
      plugins: [
        {
          add: {
            content: '/* eslint-disable */',
          },
        },
      ],
    },
  },
};

export default config;

</shell_result>


Script executed:

# Find .mutation.ts files correctly
fd --full-path ".*mutation\.ts" web/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

web/src/components/ApiKey/apikey.mutation.ts

</shell_result>


Script executed:

# Check the GraphQL schema for setTheme mutation
grep -n "setTheme" api/generated-schema.graphql

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

950:  setTheme(

</shell_result>


Script executed:

# Check the apikey.mutation.ts file to understand the pattern
cat web/src/components/ApiKey/apikey.mutation.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

import { graphql } from '~/composables/gql/gql';

export const CREATE_API_KEY = graphql(/* GraphQL */ `
  mutation CreateApiKey($input: CreateApiKeyInput!) {
    apiKey {
      create(input: $input) {
        ...ApiKey
      }
    }
  }
`);

export const UPDATE_API_KEY = graphql(/* GraphQL */ `
  mutation UpdateApiKey($input: UpdateApiKeyInput!) {
    apiKey {
      update(input: $input) {
        ...ApiKey
      }
    }
  }
`);

export const DELETE_API_KEY = graphql(/* GraphQL */ `
  mutation DeleteApiKey($input: DeleteApiKeyInput!) {
    apiKey {
      delete(input: $input)
    }
  }
`);

</shell_result>


Script executed:

# Check if DevThemeSwitcher has a .mutation.ts file
ls -la web/src/components/DevThemeSwitcher.mutation.ts 2>/dev/null || echo "Not found"

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

Not found

</shell_result>


Script executed:

# Look at the setTheme definition in the GraphQL schema
grep -B 5 -A 10 "setTheme" api/generated-schema.graphql

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

}

"""Customization related mutations"""
type CustomizationMutations {
  """Update the UI theme (writes dynamix.cfg)"""
  setTheme(
    """Theme to apply"""
    theme: ThemeName!
  ): Theme!
}

"""The theme name"""
enum ThemeName {
  azure
  black
  gray

</shell_result>


Script executed:

# Check what the full Customization type looks like in the schema
grep -B 5 -A 30 "type Customization" api/generated-schema.graphql

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

  roles: [Role!]
  permissions: [AddPermissionInput!]
}

"""Customization related mutations"""
type CustomizationMutations {
  """Update the UI theme (writes dynamix.cfg)"""
  setTheme(
    """Theme to apply"""
    theme: ThemeName!
  ): Theme!
}

"""The theme name"""
enum ThemeName {
  azure
  black
  gray
  white
}

"""
Parity check related mutations, WIP, response types and functionaliy will change
"""
type ParityCheckMutations {
  """Start a parity check"""
  start(correct: Boolean!): JSON!

  """Pause a parity check"""
  pause: JSON!

  """Resume a parity check"""
  resume: JSON!

  """Cancel a parity check"""
  cancel: JSON!
--
  background: String
  showBannerGradient: Boolean
  theme: String
}

type Customization {
  activationCode: ActivationCode
  partnerInfo: PublicPartnerInfo
  theme: Theme!
}

type Theme {
  """The theme name"""
  name: ThemeName!

  """Whether to show the header banner image"""
  showBannerImage: Boolean!

  """Whether to show the banner gradient"""
  showBannerGradient: Boolean!

  """Whether to show the description in the header"""
  showHeaderDescription: Boolean!

  """The background color of the header"""
  headerBackgroundColor: String

  """The text color of the header"""
  headerPrimaryTextColor: String

  """The secondary text color of the header"""
  headerSecondaryTextColor: String
}

type ExplicitStatusItem {
  name: String!

</shell_result>


Script executed:

# Check the entire mutation definition section in DevThemeSwitcher.standalone.vue (lines 104-140)
sed -n '100,145p' web/src/components/DevThemeSwitcher.standalone.vue

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

  },
  { immediate: true }
);

type SetThemeMutationResult = {
  customization: {
    setTheme: {
      name: ThemeName;
      showBannerImage: boolean;
      showBannerGradient: boolean;
      headerBackgroundColor?: string | null;
      showHeaderDescription: boolean;
      headerPrimaryTextColor?: string | null;
      headerSecondaryTextColor?: string | null;
    };
  };
};

type SetThemeMutationVariables = {
  theme: ThemeName;
};

const SET_THEME_MUTATION = gql`
  mutation setTheme($theme: ThemeName!) {
    customization {
      setTheme(theme: $theme) {
        name
        showBannerImage
        showBannerGradient
        headerBackgroundColor
        showHeaderDescription
        headerPrimaryTextColor
        headerSecondaryTextColor
      }
    }
  }
`;

const { mutate: setThemeMutation } = useMutation<SetThemeMutationResult, SetThemeMutationVariables>(
  SET_THEME_MUTATION
);

const persistThemePreference = (themeName: ThemeName) => {
  const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
  document.cookie = `${THEME_COOKIE_KEY}=${encodeURIComponent(themeName)}; path=/; SameSite=Lax; expires=${expires}`;
  try {

</shell_result>


Script executed:

# Search for any other theme mutation definitions in the codebase
rg -n "setTheme" web/ --type ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

web/src/store/themeInitializer.ts:32:      await themeStore.setTheme();
web/src/store/themeInitializer.ts:58:export function resetThemeInitialization(): void {
web/src/store/themeInitializer.ts:67:    reset: resetThemeInitialization,
web/src/store/theme.ts:144:    setTheme(sanitized, { source: 'server' });
web/src/store/theme.ts:199:  function setTheme(data?: Partial<Theme>, options: { source?: ThemeSource } = {}) {
web/src/store/theme.ts:259:    setTheme,
web/src/store/server.ts:152:      themeStore.setTheme(newVal);
web/__test__/store/server.test.ts:275:    setTheme: vi.fn(),
web/__test__/store/theme.test.ts:176:      store.setTheme({ banner: true, bannerGradient: true });
web/__test__/store/theme.test.ts:227:      store.setTheme(newTheme);
web/__test__/store/theme.test.ts:234:      store.setTheme({ ...store.theme, name: 'black' });
web/__test__/store/theme.test.ts:240:      store.setTheme({ ...store.theme, name: 'white' });
web/__test__/store/theme.test.ts:250:      store.setTheme({
web/__test__/store/theme.test.ts:268:      store.setTheme({
web/__test__/components/ColorSwitcher.test.ts:147:    expect(themeStore.setTheme).toHaveBeenCalledTimes(2);
web/__test__/components/ColorSwitcher.test.ts:149:    expect(themeStore.setTheme).toHaveBeenLastCalledWith({
web/__test__/components/ColorSwitcher.test.ts:189:    expect(themeStore.setTheme).toHaveBeenCalledTimes(2);
web/__test__/components/ColorSwitcher.test.ts:190:    expect(themeStore.setTheme).toHaveBeenCalledWith(expect.objectContaining({ textColor: '#ff0000' }));
web/__test__/components/ColorSwitcher.test.ts:196:    expect(themeStore.setTheme).toHaveBeenCalledTimes(3);
web/__test__/components/ColorSwitcher.test.ts:197:    expect(themeStore.setTheme).toHaveBeenCalledWith(expect.objectContaining({ metaColor: '#00ff00' }));
web/__test__/components/ColorSwitcher.test.ts:203:    expect(themeStore.setTheme).toHaveBeenCalledTimes(4);
web/__test__/components/ColorSwitcher.test.ts:204:    expect(themeStore.setTheme).toHaveBeenCalledWith(expect.objectContaining({ bgColor: '#0000ff' }));
web/__test__/components/ColorSwitcher.test.ts:206:    expect(themeStore.setTheme).toHaveBeenLastCalledWith({
web/__test__/components/ColorSwitcher.test.ts:242:    expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/ColorSwitcher.test.ts:249:    expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/ColorSwitcher.test.ts:256:    expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/ColorSwitcher.test.ts:293:    expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/Activation/WelcomeModal.test.ts:63:  setTheme: vi.fn(),
web/__test__/components/Activation/WelcomeModal.test.ts:203:  it('calls setTheme on mount', () => {
web/__test__/components/Activation/WelcomeModal.test.ts:206:    expect(mockThemeStore.setTheme).toHaveBeenCalled();
web/__test__/components/Activation/WelcomeModal.test.ts:214:    mockThemeStore.setTheme.mockRejectedValueOnce(new Error('Theme error'));
web/__test__/components/component-registry.test.ts:70:  resetThemeInitialization: vi.fn(),
web/__test__/components/UserProfile.test.ts:201:    // Set the theme using setTheme method
web/__test__/components/UserProfile.test.ts:202:    themeStore.setTheme({
web/__test__/components/UserProfile.test.ts:336:    themeStore.setTheme({ ...themeStore.theme, descriptionShow: true });
web/__test__/components/UserProfile.test.ts:344:    themeStore.setTheme({ ...themeStore.theme, descriptionShow: false });
web/__test__/components/UserProfile.test.ts:351:    themeStore.setTheme({ ...themeStore.theme, descriptionShow: true });
web/__test__/components/UserProfile.test.ts:371:    themeStore.setTheme({
web/__test__/components/UserProfile.test.ts:381:    themeStore.setTheme({
web/__test__/components/UserProfile.test.ts:390:    themeStore.setTheme({
web/__test__/components/ThemeSwitcher.test.ts:184:        expect(consoleDebugSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Theme is already set');
web/__test__/components/ThemeSwitcher.test.ts:208:        expect(consoleDebugSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Submitting form');
web/__test__/components/ThemeSwitcher.test.ts:222:        expect(consoleLogSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Theme updated, reloading…');
web/__test__/components/ThemeSwitcher.test.ts:233:        const componentThrownErrorMsg = '[ThemeSwitcher.setTheme] Failed to update theme';
web/__test__/components/ThemeSwitcher.test.ts:260:          '[ThemeSwitcher.setTheme] Failed to update theme',

</shell_result>


Script executed:

# Check if DevThemeSwitcher has related files
ls -la web/src/components/DevThemeSwitcher*

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

-rw-r--r-- 1 jailuser jailuser 7724 Nov 26 21:30 web/src/components/DevThemeSwitcher.standalone.vue

</shell_result>


Script executed:

# Run codegen to see what would be generated if we had the mutation in a .mutation.ts file
# First, check the current codegen output for any mutation-related exports
rg "SetTheme" web/composables/gql/

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 1

</shell_result>


Script executed:

# Check if the inline mutation matches the GraphQL schema definition
# Compare the structure of SET_THEME_MUTATION with the schema
cat api/generated-schema.graphql | grep -A 20 "setTheme"

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

  setTheme(
    """Theme to apply"""
    theme: ThemeName!
  ): Theme!
}

"""The theme name"""
enum ThemeName {
  azure
  black
  gray
  white
}

"""
Parity check related mutations, WIP, response types and functionaliy will change
"""
type ParityCheckMutations {
  """Start a parity check"""
  start(correct: Boolean!): JSON!

</shell_result>


Script executed:

# Look at how other components handle mutations - check ThemeSwitcher.test.ts
grep -B 5 -A 10 "setTheme.*Theme updated" web/__test__/components/ThemeSwitcher.test.ts

Repository: unraid/api


<shell_result>
Repository: unraid/api
Exit code: 0

stdout:

        expect(mockFormUrlPostRes).toHaveBeenCalledTimes(1);
        expect(select.attributes('disabled')).toBeDefined();
        expect(mockLocation.reload).not.toHaveBeenCalled();

        expect(mockResCallback).toHaveBeenCalledTimes(1);
        expect(consoleLogSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Theme updated, reloading…');

        await vi.advanceTimersByTimeAsync(1000);

        expect(mockLocation.reload).toHaveBeenCalledTimes(1);
      });

      it('handles error during WebguiUpdate call', async () => {
        const currentTheme = 'azure';
        const newTheme = 'black';
        const updateError = new Error('Network Error');

</shell_result>


Extract the mutation definition to a separate .mutation.ts file and use generated types.

The inline mutation type definitions violate the .mutation.ts file pattern required in this codebase. Per coding guidelines, GraphQL mutations must be placed in dedicated .mutation.ts files.

Create web/src/components/DevThemeSwitcher.mutation.ts:

+import { graphql } from '~/composables/gql/gql';
+
+export const SET_THEME_MUTATION = graphql(`
+  mutation setTheme($theme: ThemeName!) {
+    customization {
+      setTheme(theme: $theme) {
+        name
+        showBannerImage
+        showBannerGradient
+        headerBackgroundColor
+        showHeaderDescription
+        headerPrimaryTextColor
+        headerSecondaryTextColor
+      }
+    }
+  }
+`);

Then in DevThemeSwitcher.standalone.vue, import the generated types and mutation:

-type SetThemeMutationResult = {
-  customization: {
-    setTheme: {
-      name: ThemeName;
-      showBannerImage: boolean;
-      showBannerGradient: boolean;
-      headerBackgroundColor?: string | null;
-      showHeaderDescription: boolean;
-      headerPrimaryTextColor?: string | null;
-      headerSecondaryTextColor?: string | null;
-    };
-  };
-};
-
-type SetThemeMutationVariables = {
-  theme: ThemeName;
-};
-
-const SET_THEME_MUTATION = gql`
-  mutation setTheme($theme: ThemeName!) {
-    customization {
-      setTheme(theme: $theme) {
-        name
-        showBannerImage
-        showBannerGradient
-        headerBackgroundColor
-        showHeaderDescription
-        headerPrimaryTextColor
-        headerSecondaryTextColor
-      }
-    }
-  }
-`;
+import type { SetThemeMutation, SetThemeMutationVariables } from '~/composables/gql/graphql';
+import { SET_THEME_MUTATION } from './DevThemeSwitcher.mutation';

After creating the .mutation.ts file, run pnpm codegen to generate the types automatically.

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

🤖 Prompt for AI Agents
In web/src/components/DevThemeSwitcher.standalone.vue around lines 104 to 140
you have inline GraphQL mutation and local TypeScript types; move the GraphQL
mutation string into a new file web/src/components/DevThemeSwitcher.mutation.ts,
export the gql mutation from that file, remove the inline type defs and mutation
from the .vue file, run pnpm codegen to generate the operation types, then
import the generated types and the exported mutation into
DevThemeSwitcher.standalone.vue and call useMutation with those generated types
and the imported mutation.

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.

2 participants