Skip to content

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Nov 13, 2025

Summary

  • install the pinia-plugin-persistedstate integration directly inside the theme store and hydrate cached themes before applying CSS variables
  • fall back to the active/global Pinia instance while ensuring persisted state is only wired once per store instance
  • update the theme store tests to reset the shared Pinia state between runs and rely on the plugin-backed persistence

Testing

  • pnpm --filter web test test/store/theme.test.ts

Codex Task

Summary by CodeRabbit

  • New Features
    • Theme preferences now persist across sessions and are restored on return.
  • Behavior Change
    • Theme switching may now update the URL/address bar to reflect the selected theme.
  • Chores
    • Added persistence integration and a public storage key constant for theme persistence.
  • Tests
    • Updated/added tests covering hydration from storage and persistence of server-provided themes.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds persisted Pinia state for the theme store (new THEME_STORAGE_KEY), registers pinia-plugin-persistedstate on a shared global Pinia instance, refactors theme store wiring to support hydration/sanitization, updates component calls, expands tests to exercise hydration and persistence, and adds the plugin dependency.

Changes

Cohort / File(s) Summary
Theme store & API
web/src/store/theme.ts
Adds THEME_STORAGE_KEY, persistence config (persist key/pick, afterHydrate), sanitizeTheme and mapPublicTheme helpers, refactors to a base store + wrapper useThemeStore that resolves Pinia context and re-applies hydrated theme. Exports THEME_STORAGE_KEY and ThemeSource type.
Global Pinia setup
web/src/store/globalPinia.ts
Introduces shared globalPinia instance and registers pinia-plugin-persistedstate via globalPinia.use(...).
Tests (store)
web/__test__/store/theme.test.ts
Reworks tests to use globalPinia, adds lazy createStore helper, manages globalPinia state and DOM classList between tests, uses THEME_STORAGE_KEY for localStorage assertions, and adds cases for hydration from cache and persistence of server-provided theme.
Component callsites
web/src/components/DevThemeSwitcher.standalone.vue
Removes the second boolean argument from themeStore.setTheme calls (no explicit skip flag passed).
Package manifest
web/package.json
Adds dependency pinia-plugin-persistedstate @ 4.7.1.

Sequence Diagram(s)

sequenceDiagram
    participant Component as App / Component
    participant Wrapper as useThemeStore (wrapper)
    participant Store as theme store (base)
    participant GlobalPinia as globalPinia + plugin
    participant LS as localStorage

    Component->>Wrapper: request theme store (optional pinia)
    Wrapper->>GlobalPinia: resolve Pinia (arg or shared)
    Wrapper->>Store: instantiate or return store
    GlobalPinia->>LS: plugin reads `THEME_STORAGE_KEY`
    alt persisted state exists
        LS-->>GlobalPinia: return persisted state
        GlobalPinia->>Store: hydrate store (afterHydrate)
        Store->>Store: sanitize/map hydrated publicTheme
        Store->>Document: apply theme (classList, CSS vars)
    else
        Store->>Store: use defaults
    end
    Component->>Store: setTheme(...)
    Store->>GlobalPinia: state mutation triggers persist
    GlobalPinia->>LS: write `THEME_STORAGE_KEY`
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas to inspect closely:

  • The wrapper pattern in web/src/store/theme.ts for correct Pinia resolution and TypeScript typing.
  • sanitizeTheme / mapPublicTheme logic and afterHydrate flow for partial or malformed server payloads.
  • Test lifecycle in web/__test__/store/theme.test.ts (globalPinia reset, DOM classList management, localStorage cleanup).
  • Plugin registration in web/src/store/globalPinia.ts for unintended global side effects.

Poem

🐰
I nibbled keys and stitched the store,
Hid themes where crumbs fall on the floor.
At boot they bloom, from cache they leap,
Warmed and saved for dreams and sleep.
A happy hop — the theme will keep!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: persisting theme CSS to eliminate visual flashes on page header during load.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92daef8 and f04aacb.

📒 Files selected for processing (1)
  • web/src/store/theme.ts (3 hunks)

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

@elibosley elibosley force-pushed the codex/cache-theme-css-for-faster-load branch from 3b7b043 to e6397e9 Compare November 13, 2025 20:06
@elibosley elibosley changed the title Simplify theme persistence with plugin hook feat: use persisted theme css to fix flashes on header Nov 13, 2025
@chatgpt-codex-connector
Copy link

💡 Codex Review

persistThemeState({
app: piniaWithApp._a,
pinia,
store: store as unknown as PiniaPluginContext['store'],
options: {
persist: {
key: THEME_STORAGE_KEY,
pick: ['theme'],
afterHydrate: ({ store: hydratedStore }) => {
const themeStore = hydratedStore as ThemeStorePersistedShape;
themeStore.setTheme(themeStore.theme);
},

P1 Badge Fix persisted-state config to avoid storing hasServerTheme

The persisted-state plugin is wired with pick: ['theme'] to limit what gets saved, but pinia-plugin-persistedstate only supports paths (or paths inside a strategy) and silently ignores unknown keys. Because of that the entire store, including hasServerTheme, is serialized and restored on reload. When the store rehydrates with hasServerTheme === true, the subsequent call to setTheme(themeStore.theme) early-returns because the default source is local, so the cached theme is never re-sanitized or applied if the GraphQL request fails. This leaves CSS variables stuck at their initial defaults and prevents local theme overrides until the server responds. Switching the option to paths: ['theme'] (or an equivalent configuration) ensures only the theme object is persisted and allows the hydration hook to run reliably.

ℹ️ 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".

@codecov
Copy link

codecov bot commented Nov 13, 2025

Codecov Report

❌ Patch coverage is 84.37500% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.27%. Comparing base (c264a18) to head (f04aacb).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
web/src/store/theme.ts 85.10% 28 Missing ⚠️
web/src/components/DevThemeSwitcher.standalone.vue 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1784      +/-   ##
==========================================
+ Coverage   52.23%   52.27%   +0.03%     
==========================================
  Files         872      872              
  Lines       50219    50267      +48     
  Branches     5009     5019      +10     
==========================================
+ Hits        26234    26278      +44     
- Misses      23909    23914       +5     
+ Partials       76       75       -1     

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

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/store/theme.ts (1)

33-52: Remove unsupported manual plugin invocation; use standard pinia.use() registration

The manual per-store invocation of createPersistedState with a hand-crafted PiniaPluginContext is not a documented or supported pattern in pinia-plugin-persistedstate@4.7.1. The official approach is to register the plugin via pinia.use(createPersistedState(...)), and the non-null assertion on app doesn't address the underlying architectural problem.

The app parameter is not required for client-side localStorage/sessionStorage usage, so your persistence should work fine with the standard registration approach. Replace the custom ensureThemePersistence logic in useThemeStore with standard plugin registration in your Pinia setup, and remove the manual context construction in web/src/store/theme.ts:54–81.

🧹 Nitpick comments (2)
web/src/components/DevThemeSwitcher.standalone.vue (1)

54-56: Updated setTheme calls align with new API; consider relying on store’s CSS var watcher

Removing the old boolean flag and calling themeStore.setTheme({ name: ... }) matches the new (data, options?) signature and keeps this component focused on its own URL/localStorage handling. Given the store now watches theme with { immediate: true } and calls setCssVars itself, the explicit themeStore.setCssVars() calls here are redundant; you could drop them later if you want to cut a bit of duplicated work.

Also applies to: 103-105

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

30-52: Test setup uses shared globalPinia instance; official Pinia v3 docs recommend fresh instances per test

The Pinia v3 testing documentation recommends calling setActivePinia(createPinia()) so useX() picks a fresh instance in each test rather than reusing a shared instance. While manual cleanup (deleting state, disposing the store, clearing localStorage) works and provides some isolation, creating a fresh Pinia instance in beforeEach would better align with official best practices and reduce the risk of cross-test contamination.

Consider refactoring beforeEach to use:

beforeEach(() => {
  const pinia = createPinia();
  setActivePinia(pinia);
  store = undefined;
  // ... rest of setup
});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c264a18 and e6397e9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • web/__test__/store/theme.test.ts (10 hunks)
  • web/package.json (1 hunks)
  • web/src/components/DevThemeSwitcher.standalone.vue (2 hunks)
  • web/src/store/theme.ts (8 hunks)
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
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.
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.
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.
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.
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.
📚 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/components/DevThemeSwitcher.standalone.vue
  • 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/components/DevThemeSwitcher.standalone.vue
📚 Learning: 2025-02-05T14:43:25.062Z
Learnt from: elibosley
Repo: unraid/api PR: 1120
File: plugin/package.json:1-8
Timestamp: 2025-02-05T14:43:25.062Z
Learning: The repository uses Renovate for automated dependency updates, making strict version pinning in package.json less critical as updates are handled automatically through PRs.

Applied to files:

  • web/package.json
📚 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/__test__/store/theme.test.ts
  • 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/__test__/store/theme.test.ts
  • 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/__test__/store/theme.test.ts
  • 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/__test__/store/theme.test.ts
  • 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/__test__/store/theme.test.ts
  • web/src/store/theme.ts
📚 Learning: 2025-01-22T18:34:06.925Z
Learnt from: elibosley
Repo: unraid/api PR: 1068
File: api/src/unraid-api/auth/api-key.service.ts:122-137
Timestamp: 2025-01-22T18:34:06.925Z
Learning: The store in app/store is implemented using Redux's configureStore, where dispatch operations for config updates are synchronous in-memory state updates that cannot fail, making transaction-like patterns unnecessary.

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

  • 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
🧬 Code graph analysis (1)
web/__test__/store/theme.test.ts (1)
web/src/store/theme.ts (2)
  • useThemeStore (330-337)
  • THEME_STORAGE_KEY (33-33)
🪛 GitHub Actions: CI - Main (API)
web/src/store/theme.ts

[error] 64-64: Type 'App | undefined' is not assignable to type 'App'.

🔇 Additional comments (4)
web/package.json (1)

134-136: pinia-plugin-persistedstate dependency looks appropriate; confirm version compatibility

Adding pinia-plugin-persistedstate as a runtime dependency alongside pinia makes sense for the new persistence behavior. Please just double‑check that 4.7.1 is explicitly compatible with pinia@3.0.3 in this repo’s environment (ESM, Vite, Nuxt module usage, etc.), and bump if needed via your normal Renovate flow.

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

239-276: Persistence tests correctly exercise hydration and caching behavior

The new tests that (1) preload localStorage under THEME_STORAGE_KEY and verify the store hydrates that theme, and (2) assert that server‑sourced themes end up serialized as { theme: serverTheme } under the same key, are well aligned with the store’s pick: ['theme'] configuration. This should give good protection against regressions in both cache shape and persistence flow.

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

82-98: Theme sanitization and source-aware setTheme logic look solid

sanitizeTheme provides a strict, typed normalization layer for both server and persisted data, and mapPublicTheme cleanly translates the GraphQL shape into that internal Theme structure. The new ThemeSource‑aware setTheme implementation correctly:

  • Marks server themes via hasServerTheme,
  • Prevents subsequent “local” updates from overriding a server theme unless devOverride is enabled, and
  • Always writes a fully sanitized Theme into state.

This should significantly reduce the risk of malformed data (from cache or API) breaking CSS or class computations.

Also applies to: 124-147, 179-198


309-315: All external setTheme call sites have been properly updated to the new signature

Verification confirms that the entire codebase has successfully migrated to the new setTheme(data?: Partial<Theme>, options?: { source?: ThemeSource }) signature. A search across 21 call sites found zero instances of the old boolean second argument pattern. All callers now properly use either no arguments, a data object, or the new options object format.

@elibosley elibosley requested a review from pujitm November 13, 2025 20:48
Copy link
Member

@pujitm pujitm left a comment

Choose a reason for hiding this comment

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

qq about Object.assign, lgtm otherwise

setDevOverride,
};
});
Object.assign(useThemeStore, baseUseThemeStore);
Copy link
Member

Choose a reason for hiding this comment

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

I haven't seen this application of Object.assign before -- what's supposed to happen here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Honestly was a lot of code remnants from AI refactor + the fact we used to pass pinia in manually. Removed all of this logic in favor of much simplified flows.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
web/__test__/store/theme.test.ts (1)

5-6: Good alignment of test harness with globalPinia and DOM state

Using createApp + app.use(globalPinia) together with setActivePinia(globalPinia) ensures tests exercise the same shared Pinia instance as production. Clearing localStorage and deleting globalPinia.state.value.theme before each run, then disposing the store and unmounting the app afterward, avoids stale persisted state or watchers leaking across tests. The extra delete globalPinia.state.value.theme on top of $dispose() is a bit redundant but makes the reset explicit and is fine to keep.

Also applies to: 12-15, 31-37, 38-48, 64-69, 79-85

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

170-272: CSS variable/class application is well-structured; consider a safe fallback for unknown themes

Centralizing DOM updates in setCssVars with a single requestAnimationFrame callback and targeting both documentElement and .unapi roots is a good fit for your web-component setup. Dynamic variables and has-custom-* classes are applied/cleaned predictably, and the tests asserting calls to document.documentElement.style.setProperty and activeColorVariables confirm the behavior.

One minor robustness tweak: if a new theme name is introduced on the server before defaultColors is updated, defaultColors[selectedTheme] could be undefined, and spreading it would throw. You can cheaply guard against that by falling back to a known palette (e.g., the default/white theme):

-        // Store active variables for reference (from defaultColors for compatibility)
-        const customTheme = { ...defaultColors[selectedTheme] };
-        activeColorVariables.value = customTheme;
+        // Store active variables for reference (from defaultColors for compatibility)
+        const baseTheme = defaultColors[selectedTheme] ?? defaultColors.white;
+        const customTheme = { ...baseTheme };
+        activeColorVariables.value = customTheme;

The double application of the 'dark' class (via themeClasses and then explicitly) is mostly cosmetic; you could simplify it later if desired.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6397e9 and 92daef8.

📒 Files selected for processing (3)
  • web/__test__/store/theme.test.ts (10 hunks)
  • web/src/store/globalPinia.ts (1 hunks)
  • web/src/store/theme.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (11)
📓 Common learnings
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.
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.
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.
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.
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.
📚 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/globalPinia.ts
  • web/__test__/store/theme.test.ts
  • 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/globalPinia.ts
  • web/__test__/store/theme.test.ts
  • 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/globalPinia.ts
  • web/__test__/store/theme.test.ts
  • 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/globalPinia.ts
  • web/__test__/store/theme.test.ts
  • 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/globalPinia.ts
  • web/__test__/store/theme.test.ts
  • web/src/store/theme.ts
📚 Learning: 2025-01-22T18:34:06.925Z
Learnt from: elibosley
Repo: unraid/api PR: 1068
File: api/src/unraid-api/auth/api-key.service.ts:122-137
Timestamp: 2025-01-22T18:34:06.925Z
Learning: The store in app/store is implemented using Redux's configureStore, where dispatch operations for config updates are synchronous in-memory state updates that cannot fail, making transaction-like patterns unnecessary.

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

  • 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
🧬 Code graph analysis (2)
web/__test__/store/theme.test.ts (2)
web/src/store/theme.ts (2)
  • useThemeStore (307-310)
  • THEME_STORAGE_KEY (32-32)
web/src/store/globalPinia.ts (1)
  • globalPinia (6-6)
web/src/store/theme.ts (1)
web/src/store/globalPinia.ts (1)
  • globalPinia (6-6)
🔇 Additional comments (7)
web/__test__/store/theme.test.ts (2)

79-85: createStore helper simplifies store reuse and verifies persistence wiring

The createStore helper ensures each test reuses a single useThemeStore instance per run and centralizes initialization logic. Asserting that typeof store.$persist === 'function' is a nice way to lock in the contract that the persistedstate plugin is correctly attached to the theme store when used via globalPinia.

Also applies to: 88-92, 106-107, 121-122, 148-149, 165-166, 181-182, 212-213


244-281: Server theme precedence test is needed to complete coverage

The two tests at lines 244–281 correctly exercise cache hydration and server persistence. However, they don't verify the precedence behavior when both a cached theme and server publicTheme exist—per the store implementation, the server theme should override the cache, but this scenario isn't tested.

The review suggestion to mock result.value.publicTheme and seed localStorage simultaneously is valid: it would confirm that server data takes precedence during initialization. Because the mock currently returns result: ref(null), the initial if (result.value?.publicTheme) block in the store never fires with real data in any test.

You should add a test that:

  • Seeds a cached theme in localStorage
  • Mocks useQuery to return a non-null result.value.publicTheme
  • Verifies the store resolves to the server theme, not the cached one
web/src/store/theme.ts (4)

32-63: Theme storage key and sanitizeTheme provide a solid normalization layer

Exposing THEME_STORAGE_KEY and normalizing all theme inputs through sanitizeTheme is a clean way to keep Theme fully populated and well-typed, regardless of whether the source is persisted JSON or GraphQL. The field-by-field fallbacks to DEFAULT_THEME tighten robustness against partial or malformed data.


129-165: Getters and setTheme semantics align with previous behavior and new sources

The darkMode and bannerGradient getters remain straightforward and consistent with the existing UI semantics. The updated setTheme now cleanly distinguishes between 'server' and 'local' sources, uses sanitizeTheme to merge partial updates, and respects hasServerTheme/devOverride to prevent accidental overwrites of server-provided themes. This matches the intended flow without introducing surprising side effects.


275-305: Persist config and useThemeStore wrapper correctly tie into globalPinia

The persist block with pick: ['theme'] and afterHydrate hook ensures hydrated data flows through setTheme() normalization and CSS logic. The wrapper's fallback chain pinia ?? getActivePinia() ?? globalPinia provides flexibility while safely defaulting to the globally registered instance. All current callers use the no-argument form, avoiding the potential persistence gap when a custom Pinia without piniaPluginPersistedstate would be passed—though this remains a valid concern for future API misuse.


75-128: No precedence issue exists; server theme protection is already in place via early-return logic

The code correctly prevents the hydrate-override scenario through the gate at lines 171–173 of setTheme. When a server theme is applied, hasServerTheme is set to true; subsequent calls from afterHydrate (which use the default source: 'local') return early without overwriting. This ensures server themes always take precedence, regardless of initialization order.

While a combined cache+server test would provide explicit documentation of this behavior, the existing separate tests for cache and server scenarios, combined with the protective logic, make the implementation sound.

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

3-7: Persistedstate plugin registration on globalPinia is correct and verified

The code properly wires pinia-plugin-persistedstate on a single shared globalPinia instance before calling setActivePinia(globalPinia) at module load (line 11, confirmed by script output). This ensures all stores—including the theme store—persist without extra setup. All ~30 useThemeStore() calls across the codebase route through this shared instance, and no production code creates custom Pinia instances.

- Updated the theme store to export the useThemeStore directly, simplifying its usage.
- Removed the baseUseThemeStore function and associated global Pinia references, enhancing clarity and maintainability.
@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/PR1784/dynamix.unraid.net.plg

@elibosley elibosley merged commit 854b403 into main Nov 13, 2025
12 of 13 checks passed
@elibosley elibosley deleted the codex/cache-theme-css-for-faster-load branch November 13, 2025 21:24
elibosley pushed a commit that referenced this pull request Nov 17, 2025
🤖 I have created a release *beep* *boop*
---


## [4.26.0](v4.25.3...v4.26.0)
(2025-11-17)


### Features

* add cpu power query & subscription
([#1745](#1745))
([d7aca81](d7aca81))
* add schema publishing to apollo studio
([#1772](#1772))
([7e13202](7e13202))
* add workflow_dispatch trigger to schema publishing workflow
([818e7ce](818e7ce))
* apollo studio readme link
([c4cd0c6](c4cd0c6))
* **cli:** make `unraid-api plugins remove` scriptable
([#1774](#1774))
([64eb9ce](64eb9ce))
* use persisted theme css to fix flashes on header
([#1784](#1784))
([854b403](854b403))


### Bug Fixes

* **api:** decode html entities before parsing notifications
([#1768](#1768))
([42406e7](42406e7))
* **connect:** disable api plugin if unraid plugin is absent
([#1773](#1773))
([c264a18](c264a18))
* detection of flash backup activation state
([#1769](#1769))
([d18eaf2](d18eaf2))
* re-add missing header gradient styles
([#1787](#1787))
([f8a6785](f8a6785))
* respect OS safe mode in plugin loader
([#1775](#1775))
([92af3b6](92af3b6))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants