Conversation
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
Caution Review failedThe pull request is closed. Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds end-to-end internationalization: locale bundles and many new locale files, extraction and sorting scripts, a web i18n-loader with window.LOCALE, JsonForms i18n adapter, migration of many components/tests to vue‑i18n, removal of legacy PHP translations, and type/schema changes to carry i18n metadata. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Browser
participant App as Web App
participant Loader as i18n-loader
participant I18n as Vue I18n
participant Window as window
Browser->>App: load page
App->>Loader: createI18nInstance()
Loader-->>App: i18n instance (DEFAULT)
App->>Loader: getWindowLocale()
Loader->>Window: read LOCALE
Window-->>Loader: "fr_FR" or undefined
App->>Loader: ensureLocale(i18n, requested?)
alt requested supported
Loader->>I18n: load & register messages, set locale
else fail or unsupported
Loader->>I18n: keep DEFAULT locale
end
App-->>Browser: render UI with active locale
sequenceDiagram
autonumber
actor Dev
participant Script as extract-translations
participant FS as File System
participant Parser as AST/SFC Parser
Dev->>Script: run i18n:extract
Script->>FS: glob source files (*.ts,*.js,*.vue)
Script->>Parser: parse & collect keys (t(), i18n.*, jsonforms.*)
Script->>FS: read `en.json`
Script->>FS: merge discovered keys, write `en.json` if changed
loop per non-en locale
Script->>FS: read locale.json
Script->>FS: add missing keys (from en or null) and write if changed
end
Script-->>Dev: report added/failed keys
sequenceDiagram
autonumber
participant API as API code
participant FormUtil as form-utils
participant JsonForms as JsonForms schema
API->>FormUtil: createLabeledControl(label, control, i18nKey?)
alt i18nKey provided
FormUtil->>JsonForms: emit LabelElement with i18n
FormUtil->>JsonForms: emit ControlElement with i18nKey
else
FormUtil->>JsonForms: emit elements without i18n metadata
end
JsonForms-->>API: schema annotated (optional i18n)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60–90 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (45)
Comment |
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
💡 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
|
|
||
| <div class="prose text-base leading-relaxed whitespace-normal opacity-75"> | ||
| <p> | ||
| {{ | ||
| t( | ||
| 'Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.' | ||
| ) | ||
| }} | ||
| {{ t('updateOs.update.receiveTheLatestAndGreatestFor') }} | ||
| </p> | ||
| <p v-if="available"> | ||
| {{ flashBackupCopy }} |
There was a problem hiding this comment.
Provide translation function to template
The template calls t('updateOs.update.receiveTheLatestAndGreatestFor'), but t is never defined in <script setup>. Here defineProps is assigned to props, so Vue does not auto-expose individual props to the template. At runtime this will throw ReferenceError: t is not defined, breaking the Update panel. Either destructure the prop (const { t } = defineProps…) or reference props.t from the template.
Useful? React with 👍 / 👎.
| <ArrowUturnDownIcon class="w-5 shrink-0" /> | ||
| <span class="inline-flex flex-wrap items-baseline justify-start gap-2 leading-none"> | ||
| <span class="text-xl"> | ||
| {{ t('Downgrade Unraid OS to {0}', [version]) }} | ||
| {{ t('updateOs.downgrade.downgradeUnraidOsTo', [version]) }} | ||
| </span> | ||
| <span | ||
| v-if="releaseDate && formattedReleaseDate !== 'Invalid Date'" | ||
| class="shrink text-base opacity-75" | ||
| > | ||
| {{ t('Original release date {0}', [formattedReleaseDate]) }} | ||
| {{ t('updateOs.downgrade.originalReleaseDate', [formattedReleaseDate]) }} | ||
| </span> | ||
| </span> | ||
| </h3> | ||
| <div class="prose text-base leading-relaxed whitespace-normal opacity-75"> | ||
| <p>{{ t(`Downgrades are only recommended if you're unable to solve a critical issue.`) }}</p> | ||
| <p>{{ t('updateOs.downgrade.downgradesAreOnlyRecommendedIfYou') }}</p> | ||
| <p> | ||
| {{ | ||
| t( | ||
| 'In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.' | ||
| ) | ||
| }} | ||
| {{ t('updateOs.downgrade.inTheRareEventYouNeed') }} | ||
| </p> | ||
| <p> | ||
| {{ | ||
| t( | ||
| 'Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.' | ||
| ) | ||
| }} | ||
| {{ t('updateOs.downgrade.downloadTheDiagnosticsZipThenPlease') }} | ||
| </p> |
There was a problem hiding this comment.
Destructure
t before using in downgrade template
The Downgrade view also assigns defineProps to props but the template invokes t('updateOs.downgrade…') directly. Without a const { t } = props or useI18n, t is undefined and the component will fail to render whenever the downgrade UI is shown. Expose the prop before using it or reference props.t in the template.
Useful? React with 👍 / 👎.
| :icon="EyeIcon" | ||
| :icon-right="ArrowTopRightOnSquareIcon" | ||
| :name="updateButton?.name" | ||
| :text="t('View Changelog')" | ||
| :text="t('updateOs.updateIneligible.viewChangelog')" | ||
| class="flex-none" |
There was a problem hiding this comment.
Fix undefined
t in update-ineligible CTA
Similar to the other Update components, UpdateIneligible.vue stores the props object in props but references t('updateOs.updateIneligible…') directly in the template. Vue will not inject t automatically in this case, so these bindings will throw when the component renders. Destructure t from props or use props.t to avoid the runtime error.
Useful? React with 👍 / 👎.
| > | ||
| {{ | ||
| available | ||
| ? t('Unraid {0} Available', [available]) | ||
| ? t('updateOs.status.unraidAvailable', [available]) | ||
| : availableWithRenewal | ||
| ? t('Up-to-date with eligible releases') | ||
| : t('Up-to-date') | ||
| ? t('updateOs.status.upToDateWithEligibleReleases') | ||
| : t('updateOs.status.upToDate') | ||
| }} | ||
| </Badge> | ||
| <Badge |
There was a problem hiding this comment.
Ensure
t is defined before using in status badges
The status component references t('updateOs.status…') throughout the template but only defines const props = defineProps… in script. Because the prop isn’t destructured, t isn’t available in the template scope and rendering this component raises a t is not defined error. Extract t from props or rewrite template references to props.t.
Useful? React with 👍 / 👎.
| > | ||
| <ResponsiveModalHeader> | ||
| <ResponsiveModalTitle> | ||
| {{ t('Unraid OS {0} Changelog', [currentRelease.version]) }} | ||
| {{ t('updateOs.changelogModal.unraidOsChangelog', [currentRelease.version]) }} | ||
| </ResponsiveModalTitle> | ||
| </ResponsiveModalHeader> | ||
|
|
There was a problem hiding this comment.
Changelog modal template uses undefined
t
ChangelogModal.vue sets const props = defineProps but the modal title uses {{ t('updateOs.changelogModal…') }}. Because t is only available on props, this expression will crash when opening the changelog modal. Destructure t (const { t } = props) or switch template bindings to props.t.
Useful? React with 👍 / 👎.
| <TooltipTrigger as-child> | ||
| <Button variant="ghost" @click="accountStore.updateOs()"> | ||
| <ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" /> | ||
| {{ t('More Options') }} | ||
| {{ t('updateOs.checkUpdateResponseModal.moreOptions') }} | ||
| </Button> |
There was a problem hiding this comment.
Check-update modal references
t without exposing it
CheckUpdateResponseModal.vue expects t via props but binds {{ t('updateOs.checkUpdateResponseModal…') }} in the template. Since the script assigns the props to const props and never destructures the translation function, Vue will throw when the modal renders. Destructure the prop or replace usages with props.t(...).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (6)
431-435: Critical: json_encode flags misused; UNESCAPED_UNICODE not appliedThe third argument to json_encode is depth, not an additional flag. Unicode will be escaped unintentionally. OR the flags in the second arg and omit the third arg.
Apply this diff:
- if ($urlEncode) { - return rawurlencode(json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE)); - } - return json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE); + if ($urlEncode) { + return rawurlencode(json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + return json_encode($this->getTranslations(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
78-78: Stray '>' in HTML outputThere’s an extra '>' inside the closing list item; results in a visible stray character in UI.
Apply this diff:
- '... 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>', + '... 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>' . '</ul>',
200-200: Typo in key: "It\s" → "It's"Backslash-s appears instead of an apostrophe in the key; inconsistent with the value and likely with frontend keys.
Apply this diff:
- 'It\s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'), + 'It\'s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'),
219-219: Mismatch: "Unraidnet" vs "Unraid.net"Dot missing in the translated string.
Apply this diff:
- 'Linked to Unraid.net account' => _('Linked to Unraidnet account'), + 'Linked to Unraid.net account' => _('Linked to Unraid.net account'),
82-82: Spelling: "Acklowledge" → "Acknowledge"User-facing typo.
Apply this diff:
- 'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'), + 'Acknowledge that you have made a Flash Backup to enable this action' => _('Acknowledge that you have made a Flash Backup to enable this action'),
273-273: Grammar: "Whether it new features" → "Whether it's new features"Minor copy fix.
Apply this diff:
- 'Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.' => _('Receive the latest and greatest for Unraid OS.') . ' ' . _('Whether it new features, security patches, or bug fixes – keeping your server up-to-date ensures the best experience that Unraid has to offer.'), + 'Receive the latest and greatest for Unraid OS. Whether it\'s new features, security patches, or bug fixes — keeping your server up-to-date ensures the best experience that Unraid has to offer.' => _('Receive the latest and greatest for Unraid OS.') . ' ' . _('Whether it\'s new features, security patches, or bug fixes — keeping your server up-to-date ensures the best experience that Unraid has to offer.'),web/src/components/Activation/WelcomeModal.standalone.vue (1)
40-44: SSR safety: guard window accessAccessing window during setup can crash on SSR. Guard it.
-const isLoginPage = computed(() => window.location.pathname.includes('login')); -// Initialize showModal based on conditions -const showModal = ref(isLoginPage.value || isInitialSetup.value); +const isClient = typeof window !== 'undefined'; +const isLoginPage = computed(() => (isClient ? window.location.pathname.includes('login') : false)); +// Initialize showModal based on conditions +const showModal = ref((isLoginPage.value) || isInitialSetup.value);api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts (1)
148-176: Fix CI: enum must be string[]; Resource[] breaks JsonSchema typingTypeScript fails because enum expects string[] but getAvailableResources() returns Resource[].
Apply this fix to return string[] safely and satisfy JsonSchema types:
@@ - private getAvailableResources(): Resource[] { - return Object.values(Resource); - } + // Ensure JSON Schema enums are string arrays + private getAvailableResources(): string[] { + return Object.values(Resource).filter((v): v is string => typeof v === 'string'); + }No changes needed where it’s used; the schema already declares items.type = 'string'.
Optionally do the same for getAvailableRoles() for consistency.
web/src/components/UpdateOs/CheckUpdateResponseModal.vue (1)
361-373: Avoid double-translation in extraLinks.
extraLinkssetstextto a translated string, but the template callst(item.text)again.- :text="t(item.text ?? '')" - :title="item.title ? t(item.title) : undefined" + :text="item.text ?? ''" + :title="item.title ? item.title : undefined"Alternatively, store keys in
extraLinksand translate in the template consistently.web/src/components/Logs/SingleLogViewer.vue (1)
403-407: Fix canLoadMore predicate: use computed valuePassing the computed itself returns a truthy ref, not a boolean. Use the ref’s value.
- { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore }, + { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore.value },
🧹 Nitpick comments (25)
web/src/components/sso/useSsoAuth.ts (1)
2-2: Add.jsextension to import for ESM compatibility.The import from
vue-i18nlacks the.jsextension required by the coding guidelines for TypeScript ESM compatibility.As per coding guidelines, apply this diff:
-import { useI18n } from 'vue-i18n'; +import { useI18n } from 'vue-i18n.js';web/src/components/Notifications/Item.vue (1)
125-125: Error message placement may not match expected behavior.The learning from PR #941 specifies that when delete or archive mutations fail, the error message should be shown below the action buttons. Currently, the error is displayed on line 125, which appears above the buttons div that starts on line 127.
Consider moving the error display to after the closing
</div>tag of the buttons section to match the expected behavior.Based on learnings.
Apply this diff to move the error below the action buttons:
- <p v-if="mutationError" class="text-red-600">{{ t('common.error') }}: {{ mutationError }}</p> - <div class="flex items-baseline justify-end gap-4"> <a v-if="link" @@ -148,6 +146,8 @@ <span class="text-sm">{{ t('notifications.item.delete') }}</span> </Button> </div> + + <p v-if="mutationError" class="text-red-600">{{ t('common.error') }}: {{ mutationError }}</p> </div> </template>plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php (1)
52-52: Use positional specifiers in translatable strings: %1$s, %2$sTo support languages that reorder placeholders, prefer %1$s, %2$s, etc., in translatable strings instead of bare %s or width-based %1s/%2s.
Example:
- '{0} {1} Key…' => sprintf(_('%1s %2s Key…'), '{0}', '{1}'), + '{0} {1} Key…' => sprintf(_('%1$s %2$s Key…'), '{0}', '{1}'), - '{1} Key {0} Successfully' => sprintf(_('%2s Key %1s Successfully'), '{0}', '{1}'), + '{1} Key {0} Successfully' => sprintf(_('%2$s Key %1$s Successfully'), '{0}', '{1}'),Also applies to: 60-60
api/src/i18n/ar.json (1)
1-1: Confirm BCP‑47 code choice if variants needed.Using generic "ar" is fine; if region-specific (e.g., ar-SA, ar-EG) are planned, document fallback/aliasing.
api/src/i18n/de.json (1)
1-1: LGTM: locale scaffold.No issues. Keep consistent formatting (minified JSON) across locales for smaller diffs.
api/src/i18n/ca.json (1)
1-1: LGTM: locale scaffold.All good. If you plan key ordering, ensure the sorter (if any) is applied on API locales too, not only web.
web/src/locales/hu.json (1)
1-603: LGTM! Comprehensive Hungarian locale addition.The locale file provides extensive translation coverage across all application namespaces. The use of placeholders (
{0},{n}) and HTML markup in translations is standard practice for i18n frameworks like vue-i18n.Optional: Consider HTML-free translations where possible.
Some translations embed HTML markup (e.g., lines 304, 349, 371), which can complicate maintenance if the markup structure needs to change across all locales. Where feasible, consider using component-level markup with translation keys for text content only. This is not blocking and can be addressed incrementally.
web/src/components/DownloadApiLogs.standalone.vue (1)
21-23: Consider consolidating multiple t() calls in paragraph.The paragraph uses three separate
t()calls (lines 21-23). While this provides translation flexibility, it may result in awkward sentence flow in some languages. Consider whether these could be combined into a single translation key with proper sentence structure, or if the current approach is intentional for translation purposes.web/src/components/UserProfile/DropdownContent.vue (1)
81-83: Good migration to namespaced i18n keys.Keys and positional params look consistent with locales. One nit: showKeyline computes a non-boolean (number/object). Consider coercing to boolean for clarity.
Example minimal change:
-const showKeyline = computed( - () => - (showConnectStatus.value && (keyActions.value?.length || links.value.length)) || - unraidConnectWelcome.value -); +const showKeyline = computed(() => + !!( + (showConnectStatus.value && (keyActions.value?.length || links.value.length)) || + unraidConnectWelcome.value + ) +);Also applies to: 93-94, 105-107, 118-120, 145-147, 162-164, 172-174, 196-198
web/src/components/Registration/KeyLinkedStatus.vue (1)
37-42: Use a localized fallback instead of 'Unknown'.Prefer t('common.unknown') over t('Unknown') to ensure proper localization.
-{{ t(keyLinkedOutput.text ?? 'Unknown') }} +{{ t(keyLinkedOutput.text ?? 'common.unknown') }}Apply the same change at both occurrences.
web/src/components/Activation/WelcomeModal.standalone.vue (1)
34-36: Prefer named params to avoid list-order bugsUse object params for clarity and to match typical message templates.
- partnerInfo.value?.partnerName - ? t('activation.welcomeModal.welcomeToYourNewSystemPowered', [partnerInfo.value?.partnerName]) + partnerInfo.value?.partnerName + ? t('activation.welcomeModal.welcomeToYourNewSystemPowered', { partnerName: partnerInfo.value?.partnerName }) : t('activation.welcomeModal.welcomeToUnraid')web/src/components/ApiKey/ApiKeyCreate.vue (1)
438-442: Remove redundant t() fallbackt always exists from useI18n(). Simplify and ensure all branches are localized.
- ? 'Authorize API Key Access' - : editingKey - ? t - ? t('apiKey.apiKeyCreate.editApiKey') - : 'Edit API Key' - : t - ? t('apiKey.apiKeyCreate.createApiKey') - : 'Create API Key' + ? t('apiKey.apiKeyCreate.authorizeAccess') + : editingKey + ? t('apiKey.apiKeyCreate.editApiKey') + : t('apiKey.apiKeyCreate.createApiKey')Note: add apiKey.apiKeyCreate.authorizeAccess to locales.
api/src/unraid-api/types/json-forms.ts (1)
17-22: Optional: allow params for schema-level i18nIf you plan parameterized labels, consider supporting an object.
-export type I18nJsonSchema = JsonSchema & { - i18n?: string; -}; +export type I18nJsonSchema = JsonSchema & { + i18n?: string | { key: string; params?: Record<string, unknown> }; +};web/src/components/ConnectSettings/ConnectSettings.standalone.vue (1)
118-118: Include punctuation in the translationAvoid appending “:” outside the i18n key so locales can control punctuation.
-<Label>{{ t('downloadApiLogs.downloadUnraidApiLogs') }}:</Label> +<Label>{{ t('downloadApiLogs.downloadUnraidApiLogsWithColon') }}</Label>Or include the colon in the existing key’s message.
web/src/components/UpdateOs/UpdateIneligible.vue (1)
53-59: Remove redundant conditional in watchEffect.Both branches do the same thing.
-watchEffect(() => { - if (availableWithRenewal.value) { - updateButton.value = updateOsActionsStore.updateCallbackButton(); - } else { - updateButton.value = updateOsActionsStore.updateCallbackButton(); - } -}); +watchEffect(() => { + updateButton.value = updateOsActionsStore.updateCallbackButton(); +});web/src/helpers/jsonforms-i18n.ts (1)
9-29: Tighten types for translate/translateError to avoid implicit any.Align with JsonForms expectations and our TS guidelines.
As per coding guidelines:
- translate: (id, defaultMessage, values) => { + translate: (id: string, defaultMessage?: string, values?: Record<string, unknown> | unknown[]) => { if (id && te(id)) { const result = t(id, values); return typeof result === 'string' ? result : String(result); } if (defaultMessage) { return defaultMessage; } return id; }, - translateError: (error) => { + translateError: (error: { keyword?: string; message?: string; params?: Record<string, unknown> }) => { const key = error.keyword ? `jsonforms.errors.${error.keyword}` : undefined; if (key && te(key)) { const translated = t(key, error.params ?? {}); return typeof translated === 'string' ? translated : String(translated); } return error.message ?? error.keyword ?? ''; },web/__test__/components/Wrapper/mount-engine.test.ts (1)
151-188: Test doesn’t verify JSON prop parsing behavior.The test asserts the attribute value, not that the component received parsed JSON. Consider asserting rendered output or adjusting the test component to accept/display structured input.
Example: change TestComponent prop type to [String, Object] and render JSON.stringify when object, then assert rendered text.
web/src/components/UpdateOs/CheckUpdateResponseModal.vue (1)
318-321: Untranslated literal: “Released on …”.Localize this string to keep UI fully i18n.
- <p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm"> - Released on {{ userFormattedReleaseDate }} - </p> + <p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm"> + {{ t('updateOs.checkUpdateResponseModal.releasedOn', [userFormattedReleaseDate]) }} + </p>Ensure the key exists in locales.
web/package.json (1)
73-73: Update vue-i18n-extract to the latest 2.x patch version
- Bump
vue-i18n-extractfrom 2.0.4 to 2.0.7web/src/components/Registration/UpdateExpirationAction.vue (1)
41-44: Avoid concatenating translation strings; use a single key with interpolationConcatenation breaks grammar in many locales. Prefer one key that includes the whole sentence with placeholders.
Example:
- Define a single message like registration.updateExpirationAction.eligibleForUpdatesReleasedOnOrAndExtend with a {date} placeholder.
- Use:
props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOrAndExtend', { date: formattedRegExp.value })web/src/components/UpdateOs/Update.vue (1)
66-82: Prefer full-sentence translation keys over assembling paragraphsBuilding paragraphs by concatenating multiple t() calls risks awkward spacing/grammar and complicates localization.
- Create complete strings per branch, e.g.:
- updateOs.update.flashBackup.copyAlreadyActivated
- updateOs.update.flashBackup.copyNotActivated
- updateOs.update.flashBackup.copyManualOnly
- Each key should include all sentences in the branch, with placeholders as needed.
web/src/components/Logs/LogViewer.standalone.vue (2)
7-7: VerifyconvertAPI and tighten typing forformatFileSize.Ensure
convert(bytes, 'B').to('best', 'metric')exists and returns{ quantity, unit }as expected. Current(best.quantity as number)hints missing types.Consider a small type guard to avoid casts:
+type BestSize = { quantity: number; unit: string }; +const isBestSize = (v: unknown): v is BestSize => + !!v && typeof (v as any).quantity === 'number' && typeof (v as any).unit === 'string'; - const best = convert(bytes, 'B').to('best', 'metric'); - const formattedValue = new Intl.NumberFormat(undefined, { + const res = convert(bytes, 'B').to('best', 'metric'); + if (!isBestSize(res)) throw new Error('Unexpected convert() result'); + const formattedValue = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 0, - }).format(best.quantity as number); + }).format(res.quantity); - - const unit = unitLabels.value[best.unit] ?? best.unit; + const unit = unitLabels.value[res.unit] ?? res.unit;If
convertdoesn’t guarantee “metric” symbols (e.g., returns kB vs KB), extendunitLabelsor normalize the unit key before lookup.Also applies to: 75-82, 91-104
218-219: Avoid exposing raw backend error messages to users.Interpolating
logFilesError.messagecan leak internal details. Prefer a generic message and log details to console/telemetry.- {{ t('logs.viewer.errorLoadingLogFiles', { error: logFilesError.message }) }} + {{ t('logs.viewer.errorLoadingLogFiles') }}web/src/helpers/i18n-loader.ts (2)
43-47: Normalize by comparing base language parts.Current check compares full candidate (e.g., en_US) to base (en) and never matches. Compare base segments.
- for (const candidate of supportedLocaleCache) { - if (candidate.toLowerCase() === base.toLowerCase()) { - return candidate; - } - } + for (const candidate of supportedLocaleCache) { + const candidateBase = candidate.split(/[-_]/)[0]; + if (candidateBase.toLowerCase() === base.toLowerCase()) { + return candidate; + } + }
101-115: Remove unsafe casts and unnecessary mutation ofavailableLocales.No need to coerce types or push into the array;
setLocaleMessagemakes the locale available.- const availableLocales = i18n.global.availableLocales as unknown as string[]; - - if (!availableLocales.includes(targetLocale)) { + const hasLocale = (i18n.global.availableLocales as readonly string[]).includes(targetLocale); + + if (!hasLocale) { const messages = await loadLocaleMessages(targetLocale); if (!messages) { i18n.global.locale.value = DEFAULT_LOCALE; return DEFAULT_LOCALE; } - i18n.global.setLocaleMessage(targetLocale as typeof DEFAULT_LOCALE, messages); - availableLocales.push(targetLocale); + i18n.global.setLocaleMessage(targetLocale, messages); } - i18n.global.locale.value = targetLocale as typeof DEFAULT_LOCALE; + i18n.global.locale.value = targetLocale; return targetLocale;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (102)
api/README.md(1 hunks)api/dev/configs/api.json(1 hunks)api/package.json(1 hunks)api/scripts/extract-translations.mjs(1 hunks)api/src/i18n/ar.json(1 hunks)api/src/i18n/ca.json(1 hunks)api/src/i18n/cs.json(1 hunks)api/src/i18n/da.json(1 hunks)api/src/i18n/de.json(1 hunks)api/src/i18n/en.json(1 hunks)api/src/i18n/es.json(1 hunks)api/src/i18n/fr.json(1 hunks)api/src/i18n/hu.json(1 hunks)api/src/i18n/it.json(1 hunks)api/src/i18n/ja.json(1 hunks)api/src/i18n/ko.json(1 hunks)api/src/i18n/nl.json(1 hunks)api/src/i18n/no.json(1 hunks)api/src/i18n/pl.json(1 hunks)api/src/i18n/pt.json(1 hunks)api/src/i18n/ro.json(1 hunks)api/src/i18n/ru.json(1 hunks)api/src/i18n/sv.json(1 hunks)api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts(13 hunks)api/src/unraid-api/graph/resolvers/settings/settings.service.ts(3 hunks)api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts(32 hunks)api/src/unraid-api/graph/utils/form-utils.ts(3 hunks)api/src/unraid-api/types/json-forms.ts(1 hunks)crowdin.yml(1 hunks)package.json(1 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php(1 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php(1 hunks)unraid-ui/src/components/brand/BrandButton.vue(3 hunks)unraid-ui/src/forms/AccordionLayout.vue(7 hunks)web/README.md(1 hunks)web/__test__/components/Wrapper/mount-engine.test.ts(3 hunks)web/package.json(4 hunks)web/scripts/extract-translations.mjs(1 hunks)web/scripts/sort-translations.mjs(1 hunks)web/src/components/Activation/ActivationModal.vue(3 hunks)web/src/components/Activation/WelcomeModal.standalone.vue(2 hunks)web/src/components/ApiKey/ApiKeyCreate.vue(4 hunks)web/src/components/ConnectSettings/ConnectSettings.standalone.vue(7 hunks)web/src/components/ConnectSettings/OidcDebugLogs.vue(3 hunks)web/src/components/DowngradeOs.standalone.vue(2 hunks)web/src/components/DownloadApiLogs.standalone.vue(5 hunks)web/src/components/HeaderOsVersion.standalone.vue(7 hunks)web/src/components/Logs/LogFilterInput.vue(4 hunks)web/src/components/Logs/LogViewer.standalone.vue(6 hunks)web/src/components/Logs/LogViewerToolbar.vue(5 hunks)web/src/components/Logs/SingleLogViewer.vue(9 hunks)web/src/components/Modal.vue(2 hunks)web/src/components/Notifications/Item.vue(4 hunks)web/src/components/Notifications/List.vue(4 hunks)web/src/components/Notifications/Sidebar.vue(10 hunks)web/src/components/Registration.standalone.vue(11 hunks)web/src/components/Registration/KeyLinkedStatus.vue(3 hunks)web/src/components/Registration/ReplaceCheck.vue(2 hunks)web/src/components/Registration/UpdateExpiration.vue(1 hunks)web/src/components/Registration/UpdateExpirationAction.vue(4 hunks)web/src/components/UpdateOs.standalone.vue(2 hunks)web/src/components/UpdateOs/CallbackButton.vue(1 hunks)web/src/components/UpdateOs/ChangelogModal.vue(3 hunks)web/src/components/UpdateOs/CheckUpdateResponseModal.vue(14 hunks)web/src/components/UpdateOs/Downgrade.vue(4 hunks)web/src/components/UpdateOs/IgnoredRelease.vue(1 hunks)web/src/components/UpdateOs/RawChangelogRenderer.vue(3 hunks)web/src/components/UpdateOs/Status.vue(11 hunks)web/src/components/UpdateOs/ThirdPartyDrivers.vue(1 hunks)web/src/components/UpdateOs/Update.vue(6 hunks)web/src/components/UpdateOs/UpdateIneligible.vue(4 hunks)web/src/components/UserProfile.standalone.vue(2 hunks)web/src/components/UserProfile/CallbackFeedback.vue(10 hunks)web/src/components/UserProfile/DropdownConnectStatus.vue(1 hunks)web/src/components/UserProfile/DropdownContent.vue(8 hunks)web/src/components/UserProfile/DropdownLaunchpad.vue(1 hunks)web/src/components/UserProfile/DropdownTrigger.vue(1 hunks)web/src/components/UserProfile/ServerState.vue(2 hunks)web/src/components/UserProfile/Trial.vue(2 hunks)web/src/components/UserProfile/UptimeExpire.vue(1 hunks)web/src/components/WanIpCheck.standalone.vue(2 hunks)web/src/components/Wrapper/mount-engine.ts(3 hunks)web/src/components/sso/SsoButtons.vue(1 hunks)web/src/components/sso/useSsoAuth.ts(1 hunks)web/src/composables/dateTime.ts(1 hunks)web/src/helpers/globals.d.ts(1 hunks)web/src/helpers/i18n-loader.ts(1 hunks)web/src/helpers/jsonforms-i18n.ts(1 hunks)web/src/locales/_template.json(0 hunks)web/src/locales/ar.json(1 hunks)web/src/locales/bn.json(1 hunks)web/src/locales/ca.json(1 hunks)web/src/locales/cs.json(1 hunks)web/src/locales/da.json(1 hunks)web/src/locales/de.json(1 hunks)web/src/locales/en.json(1 hunks)web/src/locales/en_US.json(0 hunks)web/src/locales/es.json(1 hunks)web/src/locales/fr.json(1 hunks)web/src/locales/hi.json(1 hunks)web/src/locales/hr.json(1 hunks)web/src/locales/hu.json(1 hunks)
💤 Files with no reviewable changes (2)
- web/src/locales/_template.json
- web/src/locales/en_US.json
🧰 Additional context used
📓 Path-based instructions (9)
**/components/**/*.vue
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests
Files:
web/src/components/Notifications/Item.vueweb/src/components/UpdateOs/Downgrade.vueweb/src/components/UserProfile/DropdownLaunchpad.vueweb/src/components/Activation/ActivationModal.vueweb/src/components/UserProfile/DropdownTrigger.vueweb/src/components/ApiKey/ApiKeyCreate.vueweb/src/components/UserProfile/CallbackFeedback.vueweb/src/components/UserProfile/DropdownContent.vueweb/src/components/Notifications/Sidebar.vueweb/src/components/HeaderOsVersion.standalone.vueweb/src/components/Registration/UpdateExpiration.vueweb/src/components/WanIpCheck.standalone.vueweb/src/components/Modal.vueweb/src/components/Registration/KeyLinkedStatus.vueweb/src/components/UpdateOs/CallbackButton.vueweb/src/components/UserProfile/ServerState.vueweb/src/components/UpdateOs/ThirdPartyDrivers.vueweb/src/components/Notifications/List.vueweb/src/components/DowngradeOs.standalone.vueweb/src/components/Registration.standalone.vueweb/src/components/UpdateOs/RawChangelogRenderer.vueweb/src/components/DownloadApiLogs.standalone.vueweb/src/components/UserProfile/Trial.vueunraid-ui/src/components/brand/BrandButton.vueweb/src/components/Registration/UpdateExpirationAction.vueweb/src/components/UpdateOs/ChangelogModal.vueweb/src/components/UpdateOs/Status.vueweb/src/components/UpdateOs/UpdateIneligible.vueweb/src/components/sso/SsoButtons.vueweb/src/components/Registration/ReplaceCheck.vueweb/src/components/Logs/LogFilterInput.vueweb/src/components/UpdateOs/CheckUpdateResponseModal.vueweb/src/components/UserProfile.standalone.vueweb/src/components/ConnectSettings/ConnectSettings.standalone.vueweb/src/components/UpdateOs/Update.vueweb/src/components/Logs/LogViewerToolbar.vueweb/src/components/Activation/WelcomeModal.standalone.vueweb/src/components/Logs/LogViewer.standalone.vueweb/src/components/UpdateOs/IgnoredRelease.vueweb/src/components/UserProfile/DropdownConnectStatus.vueweb/src/components/ConnectSettings/OidcDebugLogs.vueweb/src/components/Logs/SingleLogViewer.vueweb/src/components/UserProfile/UptimeExpire.vueweb/src/components/UpdateOs.standalone.vue
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/src/components/sso/useSsoAuth.tsapi/src/unraid-api/types/json-forms.tsweb/src/helpers/globals.d.tsweb/src/helpers/i18n-loader.tsapi/src/unraid-api/graph/resolvers/api-key/api-key-form.service.tsapi/src/unraid-api/graph/utils/form-utils.tsapi/src/unraid-api/graph/resolvers/settings/settings.service.tsweb/src/composables/dateTime.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/src/helpers/jsonforms-i18n.tsweb/src/components/Wrapper/mount-engine.tsapi/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
api/src/unraid-api/**
📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)
Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code
Files:
api/src/unraid-api/types/json-forms.tsapi/src/unraid-api/graph/resolvers/api-key/api-key-form.service.tsapi/src/unraid-api/graph/utils/form-utils.tsapi/src/unraid-api/graph/resolvers/settings/settings.service.tsapi/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)
Files:
api/src/unraid-api/types/json-forms.tsapi/src/unraid-api/graph/resolvers/api-key/api-key-form.service.tsapi/src/unraid-api/graph/utils/form-utils.tsapi/src/unraid-api/graph/resolvers/settings/settings.service.tsapi/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Files:
web/__test__/components/Wrapper/mount-engine.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections
Files:
web/__test__/components/Wrapper/mount-engine.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Files:
web/__test__/components/Wrapper/mount-engine.test.ts
web/__test__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Vue component tests under web/test; run with pnpm test
Files:
web/__test__/components/Wrapper/mount-engine.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.{test,spec}.{ts,tsx}: 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 component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test
Files:
web/__test__/components/Wrapper/mount-engine.test.ts
🧠 Learnings (15)
📚 Learning: 2024-11-01T17:52:55.311Z
Learnt from: pujitm
PR: unraid/api#941
File: web/components/Notifications/Item.vue:0-0
Timestamp: 2024-11-01T17:52:55.311Z
Learning: In the `web/components/Notifications/Item.vue` component, ensure that when delete or archive mutations fail, the notification remains displayed, and an error message is shown below the action buttons to inform the user.
Applied to files:
web/src/components/Notifications/Item.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports
Applied to files:
web/src/components/Notifications/Item.vueweb/src/components/Notifications/Sidebar.vueweb/src/components/Notifications/List.vueweb/src/components/ConnectSettings/ConnectSettings.standalone.vueweb/src/components/Logs/LogViewer.standalone.vueweb/src/components/Logs/SingleLogViewer.vue
📚 Learning: 2025-03-12T13:35:43.900Z
Learnt from: pujitm
PR: unraid/api#1211
File: web/codegen.ts:14-14
Timestamp: 2025-03-12T13:35:43.900Z
Learning: The JSON scalar type in web/codegen.ts was temporarily changed from 'string' to 'any' for compatibility with JsonForms integration. This change facilitates the implementation of the Connect settings web component.
Applied to files:
api/src/unraid-api/types/json-forms.ts
📚 Learning: 2025-09-02T19:35:14.427Z
Learnt from: elibosley
PR: unraid/api#1639
File: web/components/standalone-mount.ts:38-54
Timestamp: 2025-09-02T19:35:14.427Z
Learning: In the unraid/api project, exposing Apollo client and GraphQL utilities (apolloClient, gql, graphqlParse) on the global window object is intentional to allow developers to interact with GraphQL from the browser console without needing to be inside Vue components. Developer experience is prioritized over minimizing global exposure in this context.
Applied to files:
web/README.md
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Always run pnpm codegen for GraphQL in the web directory
Applied to files:
api/package.jsonweb/package.json
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory
Applied to files:
api/package.jsonweb/package.json
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#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/components/Logs/LogFilterInput.vue
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
PR: unraid/api#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/components/ConnectSettings/ConnectSettings.standalone.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
🧬 Code graph analysis (7)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php (1)
web/scripts/extract-translations.mjs (1)
locale(97-97)
web/src/helpers/i18n-loader.ts (1)
web/scripts/extract-translations.mjs (3)
rawLocale(96-96)rawLocale(129-129)locale(97-97)
web/scripts/extract-translations.mjs (1)
api/scripts/extract-translations.mjs (9)
raw(90-90)ignorePatterns(10-17)files(133-137)keys(54-54)keys(145-145)content(143-143)sourceFile(144-144)added(108-108)added(150-150)
api/scripts/extract-translations.mjs (1)
web/scripts/extract-translations.mjs (8)
ignorePatterns(32-39)sourceFile(58-58)keys(53-53)visit(60-68)raw(21-21)added(188-188)files(41-41)content(57-57)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
mountUnifiedApp(80-198)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/helpers/i18n-loader.ts (3)
createI18nInstance(59-69)ensureLocale(93-115)getWindowLocale(117-122)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
api/src/unraid-api/graph/utils/form-utils.ts (1)
createSimpleLabeledControl(7-55)
🪛 GitHub Actions: CI - Main (API)
package.json
[error] 1-1: Command failed with exit code 1.
api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts
[error] 152-152: Type '{ type: string; properties: { resources: { type: string; i18n: "jsonforms.apiKey.customPermissions.resources"; title: string; items: { type: string; enum: Resource[]; }; uniqueItems: true; minItems: number; default: Resource[]; }; actions: { ...; }; }; required: string[]; }' is not assignable to type 'JsonSchema4 | JsonSchema7 | JsonSchema4[] | JsonSchema7[] | undefined'.
api/package.json
[error] 1-1: Command failed with exit code 1.
web/__test__/components/Wrapper/mount-engine.test.ts
[error] 59-59: pnpm run type-check failed. TypeScript error TS2558: Expected 0-1 type arguments, but got 2. (test/components/Wrapper/mount-engine.test.ts:59:35)
web/package.json
[error] 1-1: Command failed with exit code 1.
| <DispatchRenderer | ||
| :schema="layout.schema" | ||
| :uischema="element as UISchemaElement" | ||
| :uischema="element as unknown as UISchemaElement" |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Verify the double cast necessity.
The cast element as unknown as UISchemaElement bypasses TypeScript's type checking and suggests that element may not fully satisfy UISchemaElement's requirements. This could hide runtime issues if required properties are missing.
Consider:
- Checking if
elementshould be typed asUISchemaElementfrom the start (e.g., by constraining the elements array type) - Verifying that all elements in the filtered array actually have the properties
DispatchRendererexpects - If the mismatch is deliberate (e.g., using a subset of UISchemaElement), documenting why the double cast is safe
Run the following script to verify how elements are typed and whether they consistently match UISchemaElement:
🏁 Script executed:
#!/bin/bash
# Description: Check the types of elements used in AccordionLayout and related components
# Search for UISchemaElement and BaseUISchemaElement usage patterns
ast-grep --pattern $'interface BaseUISchemaElement {
$$$
}'
# Search for where elements are defined/constrained
rg -nP --type=ts 'elements\s*:\s*\w+\[\]' unraid-ui/src/forms/
# Search for other uses of similar double casts in the codebase
rg -nP 'as unknown as UISchemaElement' unraid-ui/Length of output: 106
🏁 Script executed:
#!/bin/bash
sed -n '1,200p' unraid-ui/src/forms/AccordionLayout.vueLength of output: 5644
Remove the unnecessary double cast
Strongly type the computed elements as UISchemaElement[] (e.g. const elements = computed<UISchemaElement[]>(() => { … })) so that in the template you can use
:uischema="element"directly without as unknown as UISchemaElement.
🤖 Prompt for AI Agents
In unraid-ui/src/forms/AccordionLayout.vue around line 24, the template uses an
unnecessary double cast ":uischema=\"element as unknown as UISchemaElement\"";
strongly type the computed elements as UISchemaElement[] (e.g., define the
computed with computed<UISchemaElement[]>() in the script setup or component
code and ensure UISchemaElement is imported/available) so each item is already
typed and then replace the template binding with the direct
":uischema=\"element\"" and remove the double cast.
| const apiSourceRoot = path.resolve(process.cwd(), '../api/src'); | ||
| const ignorePatterns = [ | ||
| '**/__tests__/**', | ||
| '**/__test__/**', | ||
| '**/*.spec.ts', | ||
| '**/*.spec.js', | ||
| '**/*.test.ts', | ||
| '**/*.test.js', | ||
| ]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Verify path assumption for ../api/src
The script assumes it’s run with CWD at web/, resolving ../api/src. This may break under different runners.
Run this from the repo root to confirm expected layout and that locales exist:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[1] CWD:"
pwd
echo "[2] Check web locales dir:"
fd -t d -a '^locales$' web/src || true
fd -t f -a '\.json$' web/src/locales || true
echo "[3] Check api src dir as resolved by script:"
if [ -d "web" ]; then
(cd web && node -e "console.log(require('node:path').resolve(process.cwd(), '../api/src'))")
fi
fd -t d -a '^src$' api || true
fd -t f -a '\.ts$' api/src | head -n 5Length of output: 1648
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[4] Resolve api path from repo root:"
node -e "console.log(require('node:path').resolve(process.cwd(), '../api/src'))"
if [ -d "$(node -e "console.log(require('node:path').resolve(process.cwd(), '../api/src'))")" ]; then
echo "Exists"
else
echo "Does not exist"
fiLength of output: 74
Resolve apiSourceRoot relative to script file, not CWD
path.resolve(process.cwd(), '../api/src') breaks if the script is run from the repo root. Replace it with resolution based on the script’s directory, for example:
import { fileURLToPath } from 'url';
import path from 'path';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const apiSourceRoot = path.resolve(__dirname, '../../api/src');This ensures the correct path regardless of working directory.
🤖 Prompt for AI Agents
In web/scripts/extract-translations.mjs around lines 31 to 39, apiSourceRoot is
being resolved from process.cwd(), which breaks when the script is invoked from
different working directories; change resolution to be based on the script file
location by deriving __dirname from import.meta.url (using fileURLToPath and
path.dirname) and then resolve apiSourceRoot relative to that directory (e.g.,
path.resolve(__dirname, '../../api/src')) so the path is correct regardless of
CWD.
| <Label for="log-file-select">{{ t('logs.viewer.logFileLabel') }}</Label> | ||
| <Select | ||
| v-model="selectedLogFile" | ||
| :items="logFileOptions" | ||
| placeholder="Select a log file" | ||
| :placeholder="t('logs.viewer.selectLogFilePlaceholder')" | ||
| class="w-full" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
Fix label-to-control association for Selects (add ids).
Labels use for="log-file-select" and for="highlight-language" but the Selects lack matching id attributes. Screen readers won’t associate them.
Apply:
- <Select
+ <Select id="log-file-select"
v-model="selectedLogFile"
:items="logFileOptions"
:placeholder="t('logs.viewer.selectLogFilePlaceholder')"
class="w-full"
/>- <Select
+ <Select id="highlight-language"
v-model="highlightLanguage"
:items="highlightLanguageOptions"
:placeholder="t('logs.viewer.selectLanguagePlaceholder')"
class="w-full"
/>Also applies to: 189-197
🤖 Prompt for AI Agents
In web/src/components/Logs/LogViewer.standalone.vue around lines 168-175 and
189-197 the Label elements use for="log-file-select" and
for="highlight-language" but the corresponding Select components lack matching
id attributes; add id="log-file-select" to the Select bound to selectedLogFile
and add id="highlight-language" to the Select (or component) for highlight
language so the label for attributes correctly associate with their controls for
accessibility; ensure the id values exactly match the Label for values and keep
them unique within the page.
| :text="t('updateOs.update.viewAvailableUpdates')" | ||
| :title="!acknowledgeBackup ? t('updateOs.update.acklowledgeThatYouHaveMadeA') : ''" | ||
| class="flex-none" |
There was a problem hiding this comment.
Typo in translation key will break lookup
Key has a misspelling: acklowledge → acknowledge.
Apply this fix:
- :title="!acknowledgeBackup ? t('updateOs.update.acklowledgeThatYouHaveMadeA') : ''"
+ :title="!acknowledgeBackup ? t('updateOs.update.acknowledgeThatYouHaveMadeA') : ''"🤖 Prompt for AI Agents
In web/src/components/UpdateOs/Update.vue around lines 241 to 243 the
translation key used in the :title binding is misspelled as
"acklowledgeThatYouHaveMadeA" which will break lookup; change the key to the
correct spelling "acknowledgeThatYouHaveMadeA" (update the string in the :title
attribute and any other occurrences) so the i18n lookup resolves properly.
| if (availableWithRenewal.value) { | ||
| return props.t('Unraid OS {0} Released', [availableWithRenewal.value]); | ||
| return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]); | ||
| } | ||
| return props.t('License Key Updates Expired'); | ||
| return props.t('updateOs.updateIneligible.licenseKeyUpdatesExpired'); | ||
| }); |
There was a problem hiding this comment.
Incorrect interpolation value for heading; boolean passed to t().
availableWithRenewal.value is boolean. The heading should interpolate the release/version string from the update response.
Suggest using the version from updateOsResponse (or the release object) instead:
- if (availableWithRenewal.value) {
- return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]);
- }
+ if (availableWithRenewal.value) {
+ return props.t('headerOsVersion.unraidOsReleased', [updateOsResponse.value?.version ?? '']);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (availableWithRenewal.value) { | |
| return props.t('Unraid OS {0} Released', [availableWithRenewal.value]); | |
| return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]); | |
| } | |
| return props.t('License Key Updates Expired'); | |
| return props.t('updateOs.updateIneligible.licenseKeyUpdatesExpired'); | |
| }); | |
| if (availableWithRenewal.value) { | |
| return props.t( | |
| 'headerOsVersion.unraidOsReleased', | |
| [updateOsResponse.value?.version ?? ''] | |
| ); | |
| } | |
| return props.t('updateOs.updateIneligible.licenseKeyUpdatesExpired'); | |
| }); |
🤖 Prompt for AI Agents
web/src/components/UpdateOs/UpdateIneligible.vue lines 41-45: the code is
passing the boolean availableWithRenewal.value into the translation
interpolation, but the heading expects the release/version string from the
update response; replace the boolean with the actual version/release string from
updateOsResponse (or the release object) when calling props.t, and add a safe
null/undefined check (fall back to a sensible default string) so you pass a
string version like updateOsResponse.latestRelease.version (or the release.name)
rather than the boolean.
| @@ -0,0 +1,124 @@ | |||
| import { createI18n } from 'vue-i18n'; | |||
|
|
|||
| import { createHtmlEntityDecoder } from '~/helpers/i18n-utils'; | |||
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use .js extension for local TS ESM import.
Align with ESM compatibility guidance.
[As per coding guidelines]
-import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
+import { createHtmlEntityDecoder } from '~/helpers/i18n-utils.js';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createHtmlEntityDecoder } from '~/helpers/i18n-utils'; | |
| import { createHtmlEntityDecoder } from '~/helpers/i18n-utils.js'; |
🤖 Prompt for AI Agents
In web/src/helpers/i18n-loader.ts at line 3, the local ESM import lacks a .js
extension which breaks TS ESM runtime resolution; update the import specifier to
include the .js extension (e.g. import from '~/helpers/i18n-utils.js') so the
runtime can resolve the module, and ensure the referenced file is emitted with a
.js extension by your build setup.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
1 similar comment
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
web/src/locales/en.json (1)
304-306: HTML in translation strings may complicate maintenance and localization.Several translation values contain raw HTML markup (e.g.,
<p>,<em>,<ul>,<li>,<a>,<strong>). This approach can make translations harder to manage and may pose risks if user-supplied data is ever interpolated into these strings.Consider:
- Moving HTML structure to component templates and using separate translation keys for text fragments.
- Using markdown or a safe markup library if rich text is needed.
- Ensuring that any dynamic content interpolated into these strings is properly sanitized.
Also applies to: 349-350, 356-356, 371-396
web/__test__/utils/i18n.ts (1)
15-41: Consider escaping special regex characters in placeholder names.The
replaceParamsfunction constructs regular expressions from variable input (indexat line 24,placeholderat line 31). While the risk is low in a test context with controlled locale keys, if placeholder names ever contain regex special characters (e.g.,{foo.bar},{test[0]}), this could cause regex errors or unexpected behavior.To harden the implementation, escape special regex characters before constructing the RegExp:
+function escapeRegex(str: string | number): string { + return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + function replaceParams(template: string, params?: unknown): string { if (params === undefined || params === null) { return template; } let result = template; if (Array.isArray(params)) { params.forEach((value, index) => { - result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value)); + result = result.replace(new RegExp(`\\{${escapeRegex(index)}\\}`, 'g'), String(value)); }); return result; } if (typeof params === 'object') { Object.entries(params as AnyObject).forEach(([placeholder, value]) => { - result = result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), String(value)); + result = result.replace(new RegExp(`\\{${escapeRegex(placeholder)}\\}`, 'g'), String(value)); }); return result; } if (typeof params === 'number' || typeof params === 'string' || typeof params === 'boolean') { return result.replace(/\{0\}/g, String(params)); } return result; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (20)
api/src/i18n/bn.json(1 hunks)api/src/i18n/hi.json(1 hunks)api/src/i18n/hr.json(1 hunks)api/src/i18n/lv.json(1 hunks)api/src/i18n/uk.json(1 hunks)api/src/i18n/zh.json(1 hunks)plugin/plugins/dynamix.unraid.net.plg(0 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php(0 hunks)web/__test__/components/Activation/ActivationModal.test.ts(2 hunks)web/__test__/components/Activation/WelcomeModal.test.ts(3 hunks)web/__test__/components/CheckUpdateResponseModal.test.ts(2 hunks)web/__test__/components/DowngradeOs.test.ts(6 hunks)web/__test__/components/DownloadApiLogs.test.ts(7 hunks)web/__test__/components/HeaderOsVersion.test.ts(4 hunks)web/__test__/components/Logs/SingleLogViewer.test.ts(5 hunks)web/__test__/components/Registration.test.ts(2 hunks)web/__test__/components/SsoButton.test.ts(2 hunks)web/__test__/components/UpdateOs.test.ts(3 hunks)web/__test__/utils/i18n.ts(1 hunks)web/src/locales/en.json(1 hunks)
💤 Files with no reviewable changes (2)
- plugin/plugins/dynamix.unraid.net.plg
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
✅ Files skipped from review due to trivial changes (7)
- api/src/i18n/uk.json
- api/src/i18n/lv.json
- api/src/i18n/zh.json
- api/src/i18n/hi.json
- api/src/i18n/hr.json
- api/src/i18n/bn.json
- web/test/components/Logs/SingleLogViewer.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/utils/i18n.tsweb/__test__/components/HeaderOsVersion.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
web/__test__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Vue component tests under web/test; run with pnpm test
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/utils/i18n.tsweb/__test__/components/HeaderOsVersion.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.{test,spec}.{ts,tsx}: 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 component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
🧠 Learnings (14)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks
Applied to files:
web/__test__/components/Registration.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.tsweb/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.tsweb/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
Applied to files:
web/__test__/components/Activation/WelcomeModal.test.tsweb/__test__/components/DownloadApiLogs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils
Applied to files:
web/__test__/components/Activation/ActivationModal.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Applied to files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/DowngradeOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Applied to files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components
Applied to files:
web/__test__/components/DowngradeOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly
Applied to files:
web/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing
Applied to files:
web/__test__/components/DownloadApiLogs.test.tsweb/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to api/**/*.{test,spec}.{ts,tsx} : API test suite is Vitest; do not use Jest
Applied to files:
web/__test__/components/DownloadApiLogs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to {api,web}/**/*.{test,spec}.{ts,tsx} : Use mocks as nouns, not verbs
Applied to files:
web/__test__/components/DownloadApiLogs.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests
Applied to files:
web/__test__/components/DownloadApiLogs.test.ts
🧬 Code graph analysis (10)
web/__test__/components/Registration.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/components/Activation/WelcomeModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/components/Activation/ActivationModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/components/CheckUpdateResponseModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/components/SsoButton.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/components/DowngradeOs.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/__test__/components/DownloadApiLogs.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/__test__/components/UpdateOs.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/__test__/utils/i18n.ts (1)
web/src/helpers/i18n-loader.ts (1)
DEFAULT_LOCALE(124-124)
web/__test__/components/HeaderOsVersion.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
🪛 ast-grep (0.39.5)
web/__test__/utils/i18n.ts
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${index}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${placeholder}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (12)
web/__test__/components/Activation/ActivationModal.test.ts (1)
13-13: LGTM! Correct adoption of centralized i18n test utilities.The import of
testTranslateand its assignment tomockTcorrectly replaces the local translation mock with the centralized test utility, aligning with the PR's i18n testing strategy.Also applies to: 40-40
web/__test__/components/UpdateOs.test.ts (1)
12-12: LGTM! Proper spy wrapper for testTranslate.The pattern of wrapping
testTranslateinvi.fn()allows the test to track translation calls while using the real implementation from the centralized utilities. ThebeforeEachcorrectly resets the mock to ensure test isolation.Also applies to: 36-36, 71-71
web/__test__/components/Registration.test.ts (1)
19-19: LGTM! Clean async mock pattern.The async mock pattern preserves the original
vue-i18nmodule while overriding onlyuseI18nto returntestTranslate. This is a cleaner approach than fully replacing the module and aligns with the PR's centralized i18n testing strategy.Also applies to: 116-124
web/__test__/components/SsoButton.test.ts (1)
13-13: LGTM! Straightforward migration to centralized test utilities.The import and usage of
testTranslatecorrectly replaces the local translation mock with the centralized test utility.Also applies to: 33-36
web/__test__/components/HeaderOsVersion.test.ts (1)
20-20: LGTM! Complete i18n test setup.The test correctly imports and uses both
testTranslateandcreateTestI18n, providing a complete i18n context for the component. The async mock pattern preserves the original vue-i18n module while overriding the translation function, and the i18n plugin is consistently added to all mount operations.Also applies to: 64-72, 101-101, 156-156
web/__test__/components/DowngradeOs.test.ts (1)
13-13: LGTM! Consistent i18n setup across all tests.The test correctly imports and applies both
createTestI18nandtestTranslate, ensuring consistent i18n context across all mounting operations. The async mock pattern preserves the original module while providing the test translation function.Also applies to: 34-42, 79-79, 96-96, 124-124, 150-150
web/__test__/components/CheckUpdateResponseModal.test.ts (1)
9-9: LGTM! Correct type casting for prop-based i18n.The import of
testTranslateand type cast toComposerTranslationcorrectly adapts the test utility for use as a component prop, aligning with the component's prop-based i18n pattern.Also applies to: 11-11
web/__test__/components/DownloadApiLogs.test.ts (1)
12-12: LGTM! Complete i18n test migration with assertion improvements.The test correctly adopts both
createTestI18nandtestTranslate, providing a complete i18n context. The use oftestTranslate()in assertions (lines 62, 80, 83, 86, 107-109) is particularly good practice, as it verifies that translations are correctly applied while making tests resilient to translation text changes.Also applies to: 21-29, 41-41, 62-62, 68-68, 97-97
web/__test__/components/Activation/WelcomeModal.test.ts (2)
13-13: LGTM! Test updated correctly for i18n.The test now uses
testTranslatefrom the shared i18n test utilities, which provides real translation lookups againsten.json. The switch from inline mock totestTranslateensures tests validate actual translation keys and their interpolation logic.Also applies to: 40-40, 119-128
130-146: LGTM! Title assertions correctly validate translated output.Tests now assert against the actual translated strings returned by
testTranslate, including placeholder replacement for the partner name scenario. This approach ensures that translation keys remain valid and that parameter interpolation works as expected.web/__test__/utils/i18n.ts (2)
43-47: LGTM! testTranslate provides a practical test helper.The
testTranslatefunction effectively mimics the vue-i18nComposerTranslationsignature for test purposes. The type cast is acceptable here as it's a test utility that matches the expected interface.
49-58: LGTM! createTestI18n correctly initializes a test i18n instance.The factory function creates a properly configured vue-i18n instance with:
- Composition API mode (
legacy: false)- Consistent locale and fallback (
en_US)- Real locale messages from
en.jsonThis allows tests to use a real i18n setup rather than mocks.
| "userProfile.serverState.purchaseKey": "Purchase Key", | ||
| "userProfile.serverState.upgrade": "Upgrade", | ||
| "userProfile.serverState.upgradeKey": "Upgrade Key", | ||
| "userProfile.trial.errorCreatiingATrialKeyPlease": "Error creatiing a trial key. Please try again later.", |
There was a problem hiding this comment.
Typo in translation key.
Line 579 contains a typo: "userProfile.trial.errorCreatiingATrialKeyPlease" should be "errorCreatingATrialKeyPlease" (double 'i' in "Creatiing").
Apply this diff to fix the typo:
- "userProfile.trial.errorCreatiingATrialKeyPlease": "Error creatiing a trial key. Please try again later.",
+ "userProfile.trial.errorCreatingATrialKeyPlease": "Error creating a trial key. Please try again later.",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "userProfile.trial.errorCreatiingATrialKeyPlease": "Error creatiing a trial key. Please try again later.", | |
| "userProfile.trial.errorCreatingATrialKeyPlease": "Error creating a trial key. Please try again later.", |
🤖 Prompt for AI Agents
In web/src/locales/en.json around line 579 the translation key has a typo:
"userProfile.trial.errorCreatiingATrialKeyPlease" (double 'i' in "Creatiing");
update the key to "userProfile.trial.errorCreatingATrialKeyPlease" preserving
the existing value string exactly and ensure no other occurrences (search for
the old misspelled key and replace them if present) so references continue to
work.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
1 similar comment
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1739 +/- ##
==========================================
- Coverage 53.35% 52.64% -0.72%
==========================================
Files 860 866 +6
Lines 48278 49335 +1057
Branches 4929 4940 +11
==========================================
+ Hits 25759 25972 +213
- Misses 22450 23290 +840
- Partials 69 73 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
49-65: Verified: TS2558 fix applied.The previous TS2558 error has been resolved by using the correct Vitest function type annotation
vi.fn<() => string | undefined>()at line 59.web/scripts/extract-translations.mjs (1)
263-263: Path resolution still needs fixing.As flagged in a previous review,
apiSourceRootat line 263 is resolved relative toprocess.cwd(), which breaks when the script is invoked from different working directories.Resolve the path relative to the script file location instead:
+import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + async function collectJsonFormsKeys() { - const apiSourceRoot = path.resolve(process.cwd(), '../api/src'); + const apiSourceRoot = path.resolve(__dirname, '../../api/src'); const ignorePatterns = [
🧹 Nitpick comments (2)
web/__test__/components/Brand/Avatar.test.ts (1)
22-31: Clarify vue-i18n mock comment
Update the comment to reflect that this mock is for stores used in this component test, for example:- // Mock vue-i18n for store tests + // Mock vue-i18n for stores used in component testsThis mock remains necessary because
serverStoregetters calluseI18n.web/__test__/components/SsoButton.test.ts (1)
13-13: Remove unused import.
testTranslateis imported but never used in this test file. OnlycreateTestI18nis needed for the i18n plugin injection.Apply this diff:
-import { createTestI18n, testTranslate } from '../utils/i18n'; +import { createTestI18n } from '../utils/i18n';
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (23)
api/.eslintrc.ts(1 hunks)api/src/types/jsonforms-i18n.d.ts(1 hunks)api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts(32 hunks)api/src/unraid-api/graph/utils/form-utils.ts(4 hunks)web/__test__/components/Activation/ActivationModal.test.ts(3 hunks)web/__test__/components/Activation/ActivationSteps.test.ts(2 hunks)web/__test__/components/Auth.test.ts(5 hunks)web/__test__/components/Brand/Avatar.test.ts(6 hunks)web/__test__/components/KeyActions.test.ts(9 hunks)web/__test__/components/Modals.test.ts(0 hunks)web/__test__/components/Registration.test.ts(4 hunks)web/__test__/components/SsoButton.test.ts(10 hunks)web/__test__/components/UpdateOs.test.ts(9 hunks)web/__test__/components/Wrapper/mount-engine.test.ts(3 hunks)web/__test__/store/server.test.ts(1 hunks)web/package.json(5 hunks)web/scripts/extract-translations.mjs(1 hunks)web/src/components/Activation/ActivationModal.vue(5 hunks)web/src/components/Activation/ActivationSteps.vue(4 hunks)web/src/components/KeyActions.vue(1 hunks)web/src/components/Modals.standalone.vue(1 hunks)web/src/components/Notifications/List.vue(4 hunks)web/src/locales/en.json(1 hunks)
💤 Files with no reviewable changes (1)
- web/test/components/Modals.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- web/src/components/Notifications/List.vue
- web/test/components/UpdateOs.test.ts
- web/test/components/Activation/ActivationModal.test.ts
- api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
- web/src/locales/en.json
🧰 Additional context used
📓 Path-based instructions (11)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/__test__/components/Registration.test.tsapi/src/unraid-api/graph/utils/form-utils.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/KeyActions.test.tsapi/src/types/jsonforms-i18n.d.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
web/__test__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Vue component tests under web/test; run with pnpm test
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.{test,spec}.{ts,tsx}: 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 component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test
Files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
api/src/unraid-api/**
📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)
Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code
Files:
api/src/unraid-api/graph/utils/form-utils.ts
api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)
Files:
api/src/unraid-api/graph/utils/form-utils.tsapi/src/types/jsonforms-i18n.d.ts
**/components/**/*.vue
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests
Files:
web/src/components/KeyActions.vueweb/src/components/Activation/ActivationSteps.vueweb/src/components/Modals.standalone.vueweb/src/components/Activation/ActivationModal.vue
**/__test__/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/store/**/*.ts: For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed
Do not mock the store under test; allow the store to initialize with its natural default state
In store tests, verify action side effects and state changes, and assert that actions are called with the correct parameters
In store tests, mock external dependencies used by the store and verify interactions with those mocks
Test computed properties/getters by accessing them directly and verifying derived state
Thoroughly test async store actions: assert intermediate loading state, await completion, and check final state
Files:
web/__test__/store/server.test.ts
**/{store,stores}/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports
Files:
web/__test__/store/server.test.ts
🧠 Learnings (24)
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Activation/ActivationSteps.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/store/server.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use createTestingPinia() for mocking stores in components
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/store/server.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.tsweb/__test__/store/server.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Applied to files:
web/__test__/components/Registration.test.tsweb/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/SsoButton.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript import specifiers with .js extensions for ESM compatibility
Applied to files:
api/.eslintrc.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#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:
api/.eslintrc.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Test component behavior and output, not implementation details
Applied to files:
web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
Applied to files:
web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Applied to files:
web/__test__/components/Auth.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : For store tests, use createPinia() and setActivePinia
Applied to files:
web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed
Applied to files:
web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Applied to files:
web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Always run pnpm codegen for GraphQL in the web directory
Applied to files:
web/package.json
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory
Applied to files:
web/package.json
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store under test; allow the store to initialize with its natural default state
Applied to files:
web/__test__/store/server.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Let stores initialize with natural default state; don’t mock the store under test
Applied to files:
web/__test__/store/server.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests
Applied to files:
web/__test__/store/server.test.ts
🧬 Code graph analysis (9)
web/__test__/components/Registration.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/scripts/extract-translations.mjs (1)
api/scripts/extract-translations.mjs (8)
raw(90-90)content(143-143)sourceFile(144-144)keys(54-54)keys(145-145)firstArg(71-71)ignorePatterns(10-17)files(133-137)
web/__test__/components/Activation/ActivationSteps.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
web/__test__/components/KeyActions.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
mountUnifiedApp(80-198)
web/__test__/components/Auth.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/__test__/components/Brand/Avatar.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/__test__/components/SsoButton.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
web/__test__/store/server.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
🪛 GitHub Check: CodeQL
web/__test__/components/KeyActions.test.ts
[notice] 30-30: Unused variable, import, function or class
Unused variable t.
web/__test__/components/SsoButton.test.ts
[notice] 13-13: Unused variable, import, function or class
Unused import testTranslate.
web/__test__/store/server.test.ts
[notice] 22-22: Unused variable, import, function or class
Unused import createTestI18n.
🔇 Additional comments (25)
api/.eslintrc.ts (1)
44-48: LGTM! Correct ESM import extension rules.The updated configuration properly enforces ESM import conventions:
mjs: 'always'andcjs: 'always'ensure module variant extensions are explicitts: 'never'andtsx: 'never'correctly prevent TypeScript source extensions in imports (since imports resolve to compiled.jsfiles)This change aligns with the project's ESM setup and supports the new
.mjsscripts added in this PR (e.g.,extract-translations.mjs).Based on learnings: The codebase uses ESM and requires
.jsextensions in import statements for TypeScript files.web/__test__/components/Wrapper/mount-engine.test.ts (2)
80-87: LGTM: Proper mock isolation.Mock cleanup correctly uses
mockClear()for call history andmockReset()for implementation reset, ensuring test isolation.
412-427: LGTM: i18n setup tests verify correct behavior.Tests properly verify that
ensureLocaleis called with the expected locale (undefined for default, or the window locale when available), focusing on observable behavior rather than implementation details.web/__test__/components/Brand/Avatar.test.ts (2)
9-9: LGTM!The import of test i18n utilities is correctly placed and follows explicit import guidelines.
54-54: LGTM!The addition of
createTestI18n()to the global plugins array in each test mount is correct. This ensures the Avatar component has access to i18n functionality during testing, aligning with the test i18n utilities introduced in this PR.Also applies to: 72-72, 90-90, 109-109
web/src/components/KeyActions.vue (1)
3-3: LGTM! Clean i18n migration.The component correctly migrates from receiving
tas a prop to using the i18n composition API. The translation function is properly initialized and used in the template for bothaction.textandaction.title.Also applies to: 13-13, 60-61
web/package.json (2)
33-35: LGTM! Well-organized i18n workflow scripts.The new scripts properly chain extraction and sorting, providing a clean workflow for managing translations.
116-116: No duplicate 'convert' entry detected. Theconvertpackage is listed only underdependencies, not indevDependencies.Likely an incorrect or invalid review comment.
web/__test__/components/KeyActions.test.ts (1)
15-15: LGTM! Proper i18n test setup.The test correctly imports and uses
createTestI18n()to provide i18n context during component mounting. This aligns with the broader i18n testing strategy.Also applies to: 46-48
web/scripts/extract-translations.mjs (1)
485-514: Good protection against over-deletion.The script correctly preserves protected keys (jsonforms, descriptions, labels, TS translation keys) and
maybeDynamickeys to prevent breaking dynamic translations. This prevents the extractor from removing keys that are referenced indirectly or computed at runtime.web/__test__/components/Activation/ActivationSteps.test.ts (1)
10-10: LGTM! Consistent i18n test integration.The test properly adds
createTestI18n()to provide i18n context, consistent with the broader test migration strategy.Also applies to: 63-65
web/src/components/Modals.standalone.vue (1)
34-34: LGTM! ActivationModal migration complete.
ActivationModalnow uses the i18n composition API internally and no longer requires thetprop. The gradual migration approach (other modals still using prop-based translation) is appropriate.web/__test__/store/server.test.ts (1)
24-29: LGTM! Proper i18n mocking for store tests.The test correctly mocks
vue-i18n'suseI18nto returntestTranslate, enabling the store to resolve translations during tests without requiring full i18n infrastructure.Also applies to: 127-127
web/__test__/components/Registration.test.ts (1)
19-19: LGTM! Comprehensive i18n test integration.The test properly:
- Imports both
createTestI18nandtestTranslate- Mocks
vue-i18nwhile preserving the actual module exports- Injects
createTestI18n()into the component mount plugins- Uses
testTranslatefor translation resolutionThis follows the established testing pattern for i18n-enabled components.
Also applies to: 30-39, 127-127, 178-178
web/__test__/components/Auth.test.ts (2)
16-26: LGTM! Static analysis warning is a false positive.The i18n mock setup correctly uses the test utilities.
testTranslateis used within the mock factory (line 23) to provide thetfunction to components under test. The static analysis tool doesn't recognize usage withinvi.mockfactory functions.
73-73: LGTM! Consistent i18n plugin injection.All mount calls correctly inject the test i18n plugin via
createTestI18n(), providing translation context for the Auth component in tests. This aligns with the broader i18n testing strategy across the codebase.Also applies to: 97-97, 121-121, 142-142
web/__test__/components/SsoButton.test.ts (1)
149-149: LGTM! Consistent i18n plugin injection across all tests.All nine test cases correctly inject the test i18n plugin via
createTestI18n(), ensuring translation context is available for the SsoButtons component.Also applies to: 174-174, 203-203, 241-241, 290-290, 330-330, 374-374, 413-413, 468-468
web/src/components/Activation/ActivationSteps.vue (3)
2-3: LGTM! Necessary imports for i18n integration.The addition of
computedanduseI18nenables the component to dynamically generate localized step content.
44-44: LGTM! Standard i18n setup.Correctly initializes the translation function using the
useI18ncomposition API.
46-77: LGTM – translation keys verified
All activation.activationSteps.* keys are present in web/src/locales.web/src/components/Activation/ActivationModal.vue (4)
3-3: LGTM! Standard i18n composition API setup.The migration from prop-based translation to composition API is correct. This is a breaking change in the component's public API—it no longer accepts
tas a prop and instead uses the global i18n instance.Also applies to: 19-19
28-31: LGTM! Proper reactive translation setup.Title and description correctly use
computedwith translation keys for reactive localization.
32-51: LGTM! Documentation buttons properly localized.The
docsButtonscomputed correctly uses translation keys for button text while preserving all other button properties (links, icons, variants).
78-78: All ActivationModal translation keys verified. Button text properly localized; no missing keys.api/src/types/jsonforms-i18n.d.ts (1)
6-40: Consolidate redundant module augmentations.
We detected no imports of@jsonforms/core/(lib|src)/models/jsonSchema4(.js)?orjsonSchema7in the codebase, so it’s unclear which specifiers your consumers actually use. Verify which paths are imported (or re-exported via your package’sexports), then remove unused augmentations and keep only the canonical module(s).
| import '@jsonforms/core/lib/models/jsonSchema4'; | ||
| import '@jsonforms/core/lib/models/jsonSchema7'; | ||
| import '@jsonforms/core/src/models/jsonSchema4'; | ||
| import '@jsonforms/core/src/models/jsonSchema7'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Verify necessity of side-effect imports for module augmentation.
In TypeScript declaration files, module augmentation via declare module typically does not require explicit side-effect imports. These imports may be redundant.
Additionally, per the coding guidelines, TypeScript imports should use .js extensions for ESM compatibility. If these imports are necessary, consider:
-import '@jsonforms/core/lib/models/jsonSchema4';
-import '@jsonforms/core/lib/models/jsonSchema7';
-import '@jsonforms/core/src/models/jsonSchema4';
-import '@jsonforms/core/src/models/jsonSchema7';
+import '@jsonforms/core/lib/models/jsonSchema4.js';
+import '@jsonforms/core/lib/models/jsonSchema7.js';
+import '@jsonforms/core/src/models/jsonSchema4.js';
+import '@jsonforms/core/src/models/jsonSchema7.js';However, if these imports serve no purpose beyond TypeScript module resolution, they can likely be removed entirely.
As per coding guidelines.
Run the following script to check @jsonforms/core package structure and determine which module paths are actually exported:
🏁 Script executed:
#!/bin/bash
# Description: Examine @jsonforms/core package.json to identify exported module paths.
# Check if package.json exists and extract exports/main fields
fd -t f "package.json" api/node_modules/@jsonforms/core --exec cat {} \; | jq -r '.exports, .main, .module, .types'Length of output: 119
🏁 Script executed:
#!/bin/bash
# Fetch package metadata for @jsonforms/core from npm registry
npm view @jsonforms/core --json | jq -r '.exports, .main, .module, .types'Length of output: 61
Consolidate JSONForms module augmentations to .js ESM paths
- Remove imports and
declare moduleblocks for@jsonforms/core/src/models/*(these source‐path modules aren’t published). - Update remaining side-effect imports and
declare modulespecifiers to use.jsextensions, e.g.:@jsonforms/core/lib/models/jsonSchema4.js@jsonforms/core/lib/models/jsonSchema7.js
🤖 Prompt for AI Agents
In api/src/types/jsonforms-i18n.d.ts around lines 1 to 4, remove the side-effect
imports that reference non-published source paths
(@jsonforms/core/src/models/jsonSchema4 and jsonSchema7) and update the
remaining module augmentation imports and any declare module specifiers to use
the published ESM .js paths (e.g. @jsonforms/core/lib/models/jsonSchema4.js and
@jsonforms/core/lib/models/jsonSchema7.js); delete the two src/* import lines
and replace the lib/* import specifiers to include the .js extension so
TypeScript resolves the published ESM modules correctly.
| const labelElement = { | ||
| type: 'Label', | ||
| text: label, | ||
| options: { | ||
| description, | ||
| }, | ||
| } as LabelElement; | ||
|
|
||
| if (i18nKey) { | ||
| (labelElement as any).i18n = i18nKey; | ||
| } | ||
|
|
||
| const controlElement = { | ||
| type: 'Control', | ||
| scope: scope, | ||
| options: controlOptions, | ||
| } as ControlElement; | ||
|
|
||
| if (i18nKey) { | ||
| (controlElement as any).i18n = i18nKey; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Eliminate as any on the JSONForms elements.
We now violate the no-any rule while attaching i18n. Widen the element types so the property can be set without casting.
- const labelElement = {
- type: 'Label',
- text: label,
- options: {
- description,
- },
- } as LabelElement;
-
- if (i18nKey) {
- (labelElement as any).i18n = i18nKey;
- }
+ const labelElement: LabelElement & { i18n?: string } = {
+ type: 'Label',
+ text: label,
+ options: {
+ description,
+ },
+ };
+
+ if (i18nKey) {
+ labelElement.i18n = i18nKey;
+ }
- const controlElement = {
- type: 'Control',
- scope: scope,
- options: controlOptions,
- } as ControlElement;
-
- if (i18nKey) {
- (controlElement as any).i18n = i18nKey;
- }
+ const controlElement: ControlElement & { i18n?: string } = {
+ type: 'Control',
+ scope,
+ options: controlOptions,
+ };
+
+ if (i18nKey) {
+ controlElement.i18n = i18nKey;
+ }As per coding guidelines.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const labelElement = { | |
| type: 'Label', | |
| text: label, | |
| options: { | |
| description, | |
| }, | |
| } as LabelElement; | |
| if (i18nKey) { | |
| (labelElement as any).i18n = i18nKey; | |
| } | |
| const controlElement = { | |
| type: 'Control', | |
| scope: scope, | |
| options: controlOptions, | |
| } as ControlElement; | |
| if (i18nKey) { | |
| (controlElement as any).i18n = i18nKey; | |
| } | |
| const labelElement: LabelElement & { i18n?: string } = { | |
| type: 'Label', | |
| text: label, | |
| options: { | |
| description, | |
| }, | |
| }; | |
| if (i18nKey) { | |
| labelElement.i18n = i18nKey; | |
| } | |
| const controlElement: ControlElement & { i18n?: string } = { | |
| type: 'Control', | |
| scope, | |
| options: controlOptions, | |
| }; | |
| if (i18nKey) { | |
| controlElement.i18n = i18nKey; | |
| } |
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/utils/form-utils.ts around lines 22 to 42, the code
uses `as any` to attach an `i18n` property to JSONForms Label and Control
elements; instead, update the TypeScript types so `i18n` can be set without
casting. Add or import widened interfaces (e.g., declare interfaces
LocalizedLabelElement extends LabelElement { i18n?: string } and
LocalizedControlElement extends ControlElement { i18n?: string } or use
intersection types LabelElement & { i18n?: string } and ControlElement & {
i18n?: string }), then type `labelElement` and `controlElement` with those
widened types and set `.i18n = i18nKey` without any casting.
| const labelElement = { | ||
| type: 'Label', | ||
| text: label, | ||
| options: { ...labelOptions, description }, | ||
| } as LabelElement; | ||
|
|
||
| if (i18nKey) { | ||
| (labelElement as any).i18n = i18nKey; | ||
| } | ||
|
|
||
| const controlElement = { | ||
| type: 'Control', | ||
| scope: scope, | ||
| options: controlOptions, | ||
| } as ControlElement; | ||
|
|
||
| if (i18nKey) { | ||
| (controlElement as any).i18n = i18nKey; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Apply the same typed approach here.
This block repeats the as any pattern; please widen the element types instead.
- const labelElement = {
- type: 'Label',
- text: label,
- options: { ...labelOptions, description },
- } as LabelElement;
-
- if (i18nKey) {
- (labelElement as any).i18n = i18nKey;
- }
+ const labelElement: LabelElement & { i18n?: string } = {
+ type: 'Label',
+ text: label,
+ options: { ...labelOptions, description },
+ };
+
+ if (i18nKey) {
+ labelElement.i18n = i18nKey;
+ }
- const controlElement = {
- type: 'Control',
- scope: scope,
- options: controlOptions,
- } as ControlElement;
-
- if (i18nKey) {
- (controlElement as any).i18n = i18nKey;
- }
+ const controlElement: ControlElement & { i18n?: string } = {
+ type: 'Control',
+ scope,
+ options: controlOptions,
+ };
+
+ if (i18nKey) {
+ controlElement.i18n = i18nKey;
+ }As per coding guidelines.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const labelElement = { | |
| type: 'Label', | |
| text: label, | |
| options: { ...labelOptions, description }, | |
| } as LabelElement; | |
| if (i18nKey) { | |
| (labelElement as any).i18n = i18nKey; | |
| } | |
| const controlElement = { | |
| type: 'Control', | |
| scope: scope, | |
| options: controlOptions, | |
| } as ControlElement; | |
| if (i18nKey) { | |
| (controlElement as any).i18n = i18nKey; | |
| } | |
| const labelElement: LabelElement & { i18n?: string } = { | |
| type: 'Label', | |
| text: label, | |
| options: { ...labelOptions, description }, | |
| }; | |
| if (i18nKey) { | |
| labelElement.i18n = i18nKey; | |
| } | |
| const controlElement: ControlElement & { i18n?: string } = { | |
| type: 'Control', | |
| scope, | |
| options: controlOptions, | |
| }; | |
| if (i18nKey) { | |
| controlElement.i18n = i18nKey; | |
| } |
🤖 Prompt for AI Agents
In api/src/unraid-api/graph/utils/form-utils.ts around lines 83 to 101, the code
uses (labelElement as any).i18n and (controlElement as any).i18n; change this to
a typed approach by widening the element types: either add an optional i18n?:
string property to the LabelElement and ControlElement interfaces (preferred)
or, if you cannot modify the shared interfaces, use local intersection types
when creating the objects (e.g., const labelElement: LabelElement & { i18n?:
string } = ... and const controlElement: ControlElement & { i18n?: string } =
...) and then assign i18nKey without using any casts.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
web/src/locales/da.json (1)
466-466: Translate remaining English stringThis entry is still English; please supply the Danish translation (e.g., “Bekræftelse på opdatering af Unraid OS kræves”) to keep the locale consistent.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
web/src/locales/ar.json(1 hunks)web/src/locales/bn.json(1 hunks)web/src/locales/ca.json(1 hunks)web/src/locales/cs.json(1 hunks)web/src/locales/da.json(1 hunks)web/src/locales/de.json(1 hunks)web/src/locales/es.json(1 hunks)web/src/locales/fr.json(1 hunks)web/src/locales/hi.json(1 hunks)web/src/locales/hr.json(1 hunks)web/src/locales/hu.json(1 hunks)web/src/locales/it.json(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- web/src/locales/es.json
🚧 Files skipped from review as they are similar to previous changes (8)
- web/src/locales/ar.json
- web/src/locales/hu.json
- web/src/locales/ca.json
- web/src/locales/fr.json
- web/src/locales/bn.json
- web/src/locales/hr.json
- web/src/locales/de.json
- web/src/locales/cs.json
| "logs.filterLabel": "फिल्टर", | ||
| "logs.filterPlaceholder": "लॉग्स छानें...", | ||
| "logs.hideLogs": "लॉग्स छुपाएँ", | ||
| "logs.presets.auth": "प्रमाणीकरण", | ||
| "logs.presets.error": "त्रुटियाँ", | ||
| "logs.presets.none": "कोई फिल्टर नहीं", | ||
| "logs.presets.oidc": "OIDC लॉग्स", | ||
| "logs.presets.warning": "चेतावनियाँ", | ||
| "logs.quickFilterLabel": "त्वरित {लेबल}", | ||
| "logs.refreshLogs": "लॉग्स ताज़ा करें", |
There was a problem hiding this comment.
Restore the exact placeholder tokens.
{label} is part of the runtime interpolation contract; translating it to {लेबल} causes Vue I18n to fall back to the literal token so users will see braces instead of the dynamic value. Same issue in both lines. Please keep the placeholder name untouched.
- "logs.customFilterLabel": "कस्टम {लेबल}",
+ "logs.customFilterLabel": "कस्टम {label}",
...
- "logs.quickFilterLabel": "त्वरित {लेबल}",
+ "logs.quickFilterLabel": "त्वरित {label}",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "logs.filterLabel": "फिल्टर", | |
| "logs.filterPlaceholder": "लॉग्स छानें...", | |
| "logs.hideLogs": "लॉग्स छुपाएँ", | |
| "logs.presets.auth": "प्रमाणीकरण", | |
| "logs.presets.error": "त्रुटियाँ", | |
| "logs.presets.none": "कोई फिल्टर नहीं", | |
| "logs.presets.oidc": "OIDC लॉग्स", | |
| "logs.presets.warning": "चेतावनियाँ", | |
| "logs.quickFilterLabel": "त्वरित {लेबल}", | |
| "logs.refreshLogs": "लॉग्स ताज़ा करें", | |
| "logs.customFilterLabel": "कस्टम {label}", | |
| "logs.quickFilterLabel": "त्वरित {label}", |
🤖 Prompt for AI Agents
In web/src/locales/hi.json around lines 147 to 156, the placeholder tokens were
translated (e.g., "{लेबल}") which breaks runtime interpolation; restore the
exact placeholder names used in code (replace "{लेबल}" with "{label}" in any
affected strings such as "logs.quickFilterLabel" and any other occurrences) so
Vue I18n can substitute the dynamic value, preserving original token spelling
and braces exactly.
| "notifications.item.delete": "हटाएं", | ||
| "notifications.item.viewLink": "देखें", | ||
| "notifications.list.noNotifications": "यहां देखने के लिए कोई सूचनाएं नहीं हैं!", | ||
| "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {महत्व} सूचनाएं नहीं हैं!", |
There was a problem hiding this comment.
Keep placeholder identifiers untranslated.
Interpolated key {importance} was localized to {महत्व}, which breaks substitution in the UI. Restore the original token so the importance value renders properly.
- "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {महत्व} सूचनाएं नहीं हैं!",
+ "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {importance} सूचनाएं नहीं हैं!",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {महत्व} सूचनाएं नहीं हैं!", | |
| "notifications.list.noNotificationsWithImportance": "यहां देखने के लिए कोई {importance} सूचनाएं नहीं हैं!", |
🤖 Prompt for AI Agents
In web/src/locales/hi.json around line 209, the interpolation token
`{importance}` was translated to `{महत्व}`, breaking runtime substitution;
restore the original placeholder `{importance}` (keep placeholder identifiers
untranslated) so the UI can correctly substitute the importance value, and save
the file.
| "server.state.enoflash.message": "<p>आपके USB फ्लैश बूट डिवाइस तक पहुँचने में भौतिक समस्या है</p>", | ||
| "server.state.enokeyfile.heading": "चलो आपके हार्डवेयर को बाहर निकालें", | ||
| "server.state.enokeyfile.humanReadable": "कोई कीफाइल नहीं", | ||
| "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>", |
There was a problem hiding this comment.
Fix rel attribute typo in embedded link.
The HTML snippet misspells noreferrer, so the protective attribute is lost. Please correct the value to match the English source.
- "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",
+ "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>", | |
| "server.state.enokeyfile.message": "<p>नीचे एक विकल्प चुनें, फिर <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">आरंभ करें गाइड</a> का उपयोग करके 15 मिनट से भी कम समय में अपने एरे को कॉन्फ़िगर करने के लिए गाइड का पालन करें।</p>", |
🤖 Prompt for AI Agents
In web/src/locales/hi.json around line 323, the embedded link's rel attribute is
misspelled as "noreffer noopener"; update it to "noreferrer noopener" to match
the English source and restore the intended protective attributes.
| "server.state.enoflash.message": "<p>C'è un problema fisico nell'accesso al tuo dispositivo di avvio USB Flash</p>", | ||
| "server.state.enokeyfile.heading": "Liberiamo le tue risorse hardware", | ||
| "server.state.enokeyfile.humanReadable": "Nessun File Chiave", | ||
| "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>", |
There was a problem hiding this comment.
Restore rel="noreferrer" for security parity.
The typo rel="noreffer noopener" drops the noreferrer token, so referer headers will leak where the English source intentionally suppressed them. Please fix the attribute spelling.
- "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",
+ "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>", | |
| "server.state.enokeyfile.message": "<p>Scegli un'opzione qui sotto, quindi usa la nostra <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Guida Introduttiva</a> per configurare il tuo array in meno di 15 minuti.</p>", |
🤖 Prompt for AI Agents
In web/src/locales/it.json around line 323, the rel attribute in the anchor is
misspelled as "noreffer noopener"; update it to "noreferrer noopener" so the
HTML becomes rel="noreferrer noopener" (preserve the existing surrounding string
quoting and escape sequences).
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
web/__test__/components/Activation/ActivationModal.test.ts(3 hunks)web/__test__/components/KeyActions.test.ts(8 hunks)web/__test__/components/SsoButton.test.ts(10 hunks)web/__test__/store/server.test.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web/test/store/server.test.ts
- web/test/components/Activation/ActivationModal.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/KeyActions.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections
Files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/KeyActions.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/KeyActions.test.ts
{api,web}/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/KeyActions.test.ts
web/__test__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Vue component tests under web/test; run with pnpm test
Files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/KeyActions.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.{test,spec}.{ts,tsx}: 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 component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test
Files:
web/__test__/components/SsoButton.test.tsweb/__test__/components/KeyActions.test.ts
🧠 Learnings (11)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Applied to files:
web/__test__/components/SsoButton.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Applied to files:
web/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
Applied to files:
web/__test__/components/KeyActions.test.ts
🧬 Code graph analysis (2)
web/__test__/components/SsoButton.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
web/__test__/components/KeyActions.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
| import type { ServerStateDataAction, ServerStateDataActionType } from '~/types/server'; | ||
|
|
||
| import KeyActions from '~/components/KeyActions.vue'; | ||
| import { createTestI18n } from '../utils/i18n'; |
There was a problem hiding this comment.
Add the .js extension to the ESM import.
Our TypeScript files must use ESM-friendly specifiers. Importing ../utils/i18n without the .js suffix will break resolution under NodeNext/Bundler module resolution and violates the project’s TS guideline. Please switch the specifier to ../utils/i18n.js.
[As per coding guidelines]
-import { createTestI18n } from '../utils/i18n';
+import { createTestI18n } from '../utils/i18n.js';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createTestI18n } from '../utils/i18n'; | |
| import { createTestI18n } from '../utils/i18n.js'; |
🤖 Prompt for AI Agents
In web/__test__/components/KeyActions.test.ts around line 15, the ESM import
specifier "import { createTestI18n } from '../utils/i18n';" is missing the
required .js extension; update the import to use the ESM-friendly path
"../utils/i18n.js" so NodeNext/bundlers resolve the module correctly and comply
with the project's TypeScript ESM guidelines.
| import type { Mock, MockInstance } from 'vitest'; | ||
|
|
||
| import SsoButtons from '~/components/sso/SsoButtons.vue'; | ||
| import { createTestI18n } from '../utils/i18n'; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add the .js extension for ESM compatibility
Per our TS/ESM rules, specifiers must include the .js suffix so the compiled code resolves at runtime. Please update the import accordingly. As per coding guidelines.
-import { createTestI18n } from '../utils/i18n';
+import { createTestI18n } from '../utils/i18n.js';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createTestI18n } from '../utils/i18n'; | |
| import { createTestI18n } from '../utils/i18n.js'; |
🤖 Prompt for AI Agents
In web/__test__/components/SsoButton.test.ts around line 13, the import
specifier for createTestI18n is missing the required .js extension for ESM;
update the import to include the .js suffix (i.e., import from
'../utils/i18n.js') so compiled ESM code resolves at runtime and adjust any
related test/import paths if necessary.
…ripts - Added `crowdin.yml` for Crowdin integration to manage translation files. - Introduced `i18n:extract` scripts in both `api` and `web` packages to automate extraction of translation keys from source files. - Created initial English locale files in both `api/src/i18n/en.json` and `web/src/locales/en.json`. - Updated README files to include instructions for running the extraction scripts and details on the internationalization process. - Refactored i18n setup in the application to utilize the new extraction and loading mechanisms. This commit enhances the application's internationalization capabilities, allowing for easier management and addition of translations.
…ing of the script This commit cleans up the `extract-translations.mjs` file by removing an extraneous newline, improving the overall readability of the script without altering its functionality.
…mponents - Introduced i18n keys for API key and OIDC configuration forms, improving localization capabilities. - Updated form utility functions to support i18n integration for labels and descriptions. - Added a new helper for managing i18n state in Vue components, ensuring consistent translation handling. - Enhanced existing components to utilize the new i18n features, including ApiKeyCreate and ConnectSettings. - Updated English locale files with new keys for API settings and OIDC provider configurations. This commit significantly improves the application's internationalization, making it easier to manage translations and enhance user experience across different languages.
This commit adds a newline at the beginning of the `extract-translations.mjs` file to enhance the overall readability of the script. This change does not affect the functionality of the script.
- Added a new script `sort-translations.mjs` to automate the sorting of translation keys in locale JSON files, improving organization and maintainability. - Updated the `i18n:extract` command in `package.json` to include the new sorting step, ensuring translations are consistently ordered after extraction. - Refactored various components to utilize new i18n keys for improved localization, enhancing user experience across the application. This commit significantly improves the internationalization process, making it easier to manage and maintain translation files.
…nents - Integrated i18n support in ConnectSettings, updating labels and messages to utilize translation keys for improved user experience. - Refactored OidcDebugLogs and LogViewerToolbar components to leverage i18n for dynamic text rendering, enhancing consistency across the application. - Updated LogFilterInput to support localized preset filters and placeholders, improving accessibility and usability. - Added new translation keys to the English locale file for better management of UI text. These changes significantly improve the internationalization of the application, making it more user-friendly and adaptable to different languages.
…ponents - Integrated i18n support in LogViewer and SingleLogViewer, updating labels, messages, and error handling to utilize translation keys for improved user experience. - Refactored log file size formatting and loading messages to be dynamically translated, enhancing accessibility and usability. - Updated English locale file with new translation keys for log viewer functionalities, ensuring consistent internationalization across the application. These changes significantly improve the internationalization of the logging components, making them more user-friendly and adaptable to different languages.
- Changed translation file paths in `crowdin.yml` to use two-letter locale codes for consistency. - Added `convert` package version 5.12.0 to `pnpm-lock.yaml` and `package.json`. - Restored `glob` package version 11.0.3 in `package.json` after it was previously removed. These changes enhance the localization setup and ensure proper dependency management for the project.
…ences - Deleted the translations.php file from the dynamix.my.servers plugin, streamlining the localization process. - Updated backup and exit scripts to remove references to the deleted translations.php file, ensuring consistency across the application. - Enhanced test files to utilize a new test translation utility, improving internationalization support in various components. These changes simplify the localization setup and improve the overall maintainability of the codebase.
…ation - Introduced `i18n` property to JSON Schema 4 and 7 interfaces, enhancing localization capabilities for schemas. - Updated OIDC configuration to correct `i18n` references for allowed domains, emails, and user IDs, ensuring accurate localization. - Added `i18n` support in accordion layout configuration, improving internationalization for UI components. These changes enhance the internationalization framework, making it more robust and user-friendly across the application.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
1104-1111: Use distinct i18n keys for semantically different fields.Both
authorizationMode(lines 1104-1111) andauthorizationRuleMode(lines 1229-1236) use the same i18n keyOIDC_I18N.rules.mode, but they control different aspects:
authorizationMode: UI presentation (simple vs advanced interface)authorizationRuleMode: Rule evaluation logic (OR vs AND)These distinct concepts should have separate i18n keys to ensure accurate translations.
Add a new key to the OIDC_I18N constant:
const OIDC_I18N = { // ... rules: { mode: 'jsonforms.oidc.rules.mode', + evaluationMode: 'jsonforms.oidc.rules.evaluationMode', claim: 'jsonforms.oidc.rules.claim', operator: 'jsonforms.oidc.rules.operator', value: 'jsonforms.oidc.rules.value', collection: 'jsonforms.oidc.rules.collection', sectionTitle: 'jsonforms.oidc.rules.title', sectionDescription: 'jsonforms.oidc.rules.description', }, // ... } as const;Then update line 1235 to use the new key:
- i18nKey: OIDC_I18N.rules.mode, + i18nKey: OIDC_I18N.rules.evaluationMode,And update the schema property definition around line 794:
authorizationRuleMode: { type: 'string', - i18n: OIDC_I18N.rules.mode, + i18n: OIDC_I18N.rules.evaluationMode, title: 'Rule Mode',Also applies to: 1229-1236
web/src/components/Logs/SingleLogViewer.vue (2)
403-406: Bug: canLoadMore returns a ComputedRef, not booleanPass the boolean value, not the ref.
- { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore }, + { direction: 'top', distance: 200, canLoadMore: () => shouldLoadMore.value },
260-310: Avoid duplicate subscriptions; clean up on refreshstartLogSubscription is called on mount and again after refresh, but no unsubscribe is tracked. This can create multiple active subscriptions.
-const startLogSubscription = () => { +let stopSubscription: (() => void) | null = null; +const startLogSubscription = () => { if (!props.logFilePath) return; try { - subscribeToMore({ + // Stop previous subscription before starting a new one + stopSubscription?.(); + const maybeStop = subscribeToMore({ document: LOG_FILE_SUBSCRIPTION, variables: { path: props.logFilePath }, updateQuery: (prev, { subscriptionData }) => { ... }, }); + // Some apollo clients return an unsubscribe function; if available, keep it + if (typeof maybeStop === 'function') stopSubscription = maybeStop as () => void; state.isSubscriptionActive = true; } catch (error) { console.error('Error starting log subscription:', error); state.isSubscriptionActive = false; } }; +onUnmounted(() => stopSubscription?.());If subscribeToMore doesn’t return an unsubscribe function in this wrapper, store and call the result of client.subscribe/observable instead.
Also applies to: 319-328
♻️ Duplicate comments (11)
api/src/unraid-api/graph/utils/form-utils.ts (2)
22-42: Duplicate: Eliminateas anycasts on JSONForms elements.The
as anycasts on lines 31 and 41 still violate the coding guidelines ("Never use the any type"). The fix was already suggested in a previous review: widen the element types using intersection types so thei18nproperty can be set without casting.Apply the previously suggested diff:
- const labelElement = { - type: 'Label', - text: label, - options: { - description, - }, - } as LabelElement; - - if (i18nKey) { - (labelElement as any).i18n = i18nKey; - } + const labelElement: LabelElement & { i18n?: string } = { + type: 'Label', + text: label, + options: { + description, + }, + }; + + if (i18nKey) { + labelElement.i18n = i18nKey; + } - const controlElement = { - type: 'Control', - scope: scope, - options: controlOptions, - } as ControlElement; - - if (i18nKey) { - (controlElement as any).i18n = i18nKey; - } + const controlElement: ControlElement & { i18n?: string } = { + type: 'Control', + scope, + options: controlOptions, + }; + + if (i18nKey) { + controlElement.i18n = i18nKey; + }As per coding guidelines.
83-101: Duplicate: Apply the same typed approach here.This block repeats the
as anypattern on lines 90 and 100. Please widen the element types using intersection types as suggested in the previous review.Apply the previously suggested diff:
- const labelElement = { - type: 'Label', - text: label, - options: { ...labelOptions, description }, - } as LabelElement; - - if (i18nKey) { - (labelElement as any).i18n = i18nKey; - } + const labelElement: LabelElement & { i18n?: string } = { + type: 'Label', + text: label, + options: { ...labelOptions, description }, + }; + + if (i18nKey) { + labelElement.i18n = i18nKey; + } - const controlElement = { - type: 'Control', - scope: scope, - options: controlOptions, - } as ControlElement; - - if (i18nKey) { - (controlElement as any).i18n = i18nKey; - } + const controlElement: ControlElement & { i18n?: string } = { + type: 'Control', + scope, + options: controlOptions, + }; + + if (i18nKey) { + controlElement.i18n = i18nKey; + }As per coding guidelines.
web/src/components/Modal.vue (2)
100-100: Duplicate: Missing i18n key already flagged.This issue has been previously identified and verified. The key
modal.clickToCloseModalis missing from the locale files.
129-129: Duplicate: Missing i18n key already flagged.This issue has been previously identified and verified. The key
common.closeis missing from the locale files.unraid-ui/src/forms/AccordionLayout.vue (1)
24-24: Critical: Double cast worsens type safety.The cast changed from
element as UISchemaElementtoelement as unknown as UISchemaElement, which is MORE permissive and completely bypasses TypeScript's type checking. This is a regression from the previous version and conflicts with the past review suggestion.Additionally, there's a type inconsistency: your helper functions (
getAccordionTitle,getAccordionDescription) acceptBaseUISchemaElement(lines 130, 140), but here you're forcing a cast toUISchemaElement. This suggests either:
- Elements should be typed as
UISchemaElement[]if that's whatDispatchRendererrequires- Or
DispatchRenderershould acceptBaseUISchemaElementif that's the actual typeApply this diff to strongly type the
elementscomputed property and remove the unsafe cast:const elements = computed(() => { const allElements = props.uischema.elements || []; - // Filter elements based on visibility rules - return allElements.filter((element) => { + return allElements.filter((element): element is UISchemaElement => { const elementWithRule = element as BaseUISchemaElement; if (!elementWithRule.rule) { // No rule means always visible return true; } // ... rest of filter logic - }); + }) as UISchemaElement[]; });Then in the template, remove the cast:
<DispatchRenderer :schema="layout.schema" - :uischema="element as unknown as UISchemaElement" + :uischema="element" :path="layout.path || ''" :enabled="layout.enabled" :renderers="layout.renderers" :cells="layout.cells" />web/src/components/UpdateOs/UpdateIneligible.vue (1)
41-45: Fix incorrect boolean interpolation in heading.Line 42 passes
availableWithRenewal.value(a boolean) to the translation, but the heading expects the release version string. This will result in displaying "Unraid OS true Released" instead of "Unraid OS 7.0.0 Released".Apply this fix:
if (availableWithRenewal.value) { - return props.t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]); + return props.t('headerOsVersion.unraidOsReleased', [updateOsResponse.value?.version ?? '']); }web/src/locales/da.json (1)
323-323: Fix typo inrelattribute value.The
relattribute value contains"noreffer noopener"instead of"noreferrer noopener". This typo defeats the referrer protection and should be corrected.Apply this diff:
- "server.state.enokeyfile.message": "<p>Vælg en mulighed nedenfor, og brug vores <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreffer noopener\">Kom godt i gang Guide</a> til at konfigurere dit array på mindre end 15 minutter.</p>", + "server.state.enokeyfile.message": "<p>Vælg en mulighed nedenfor, og brug vores <a href=\"https://unraid.net/getting-started\" target=\"_blank\" rel=\"noreferrer noopener\">Kom godt i gang Guide</a> til at konfigurere dit array på mindre end 15 minutter.</p>",web/scripts/extract-translations.mjs (1)
173-176: Resolve paths relative to script file, not CWDCWD-based resolution breaks under different runners. Derive paths from import.meta.url. This was raised earlier.
-import process from 'node:process'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; ... -async function collectTsTranslationKeys() { - const sourceRoot = path.resolve(process.cwd(), 'src'); +async function collectTsTranslationKeys() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const projectRoot = path.resolve(__dirname, '..'); // web/ + const sourceRoot = path.resolve(projectRoot, 'src'); ... } ... -async function collectJsonFormsKeys() { - const apiSourceRoot = path.resolve(process.cwd(), '../api/src'); +async function collectJsonFormsKeys() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const projectRoot = path.resolve(__dirname, '..'); // web/ + const apiSourceRoot = path.resolve(projectRoot, '../api/src'); ... }Also applies to: 262-279
web/__test__/components/KeyActions.test.ts (1)
15-15: Add .js extension to ESM import.The import is missing the required
.jsextension for ESM compatibility. This was flagged in a previous review and should be corrected.As per coding guidelines:
-import { createTestI18n } from '../utils/i18n'; +import { createTestI18n } from '../utils/i18n.js';web/src/components/UpdateOs/Status.vue (1)
137-147: Previous “t is not defined” is resolved.
Propsnow includestand the template referencest(...)correctly. LGTM.web/src/components/Logs/LogViewer.standalone.vue (1)
168-175: A11y: Associate Labels with Selects (add matching ids).Labels have for="log-file-select" / "highlight-language" but the Selects lack ids. Add ids to ensure screen reader association.
@@ - <Label for="log-file-select">{{ t('logs.viewer.logFileLabel') }}</Label> - <Select + <Label for="log-file-select">{{ t('logs.viewer.logFileLabel') }}</Label> + <Select id="log-file-select" v-model="selectedLogFile" :items="logFileOptions" :placeholder="t('logs.viewer.selectLogFilePlaceholder')" class="w-full" /> @@ - <Label for="highlight-language">{{ t('logs.viewer.syntaxLabel') }}</Label> - <Select + <Label for="highlight-language">{{ t('logs.viewer.syntaxLabel') }}</Label> + <Select id="highlight-language" v-model="highlightLanguage" :items="highlightLanguageOptions" :placeholder="t('logs.viewer.selectLanguagePlaceholder')" class="w-full" />Also applies to: 189-197
🧹 Nitpick comments (8)
web/src/components/HeaderOsVersion.standalone.vue (2)
165-168: Verify interpolation style for titlesYou’re passing list params (array) to t(). Ensure the strings use positional placeholders (e.g., {0}). Consider named params for clarity:
- title: availableWithRenewal.value - ? t('headerOsVersion.unraidOsReleased', [availableWithRenewal.value]) - : t('headerOsVersion.unraidOsUpdateAvailable', [available.value]), + title: availableWithRenewal.value + ? t('headerOsVersion.unraidOsReleased', { version: availableWithRenewal.value }) + : t('headerOsVersion.unraidOsUpdateAvailable', { version: available.value }),
272-279: Localize remaining literal propLocalize “Open in new tab” for consistency.
- view-docs-label="Open in new tab" + :view-docs-label="t('common.openInNewTab')"If common.openInNewTab doesn’t exist, add it to locales.
web/src/components/Logs/SingleLogViewer.vue (1)
250-254: Replace alert() with non-blocking UI toastUse the app’s notification/toast system for errors; avoid leaking raw error strings in user-facing text.
- alert( - t('logs.singleViewer.errorDownloadingLogFile', { - error: error instanceof Error ? error.message : String(error), - }) - ); + // e.g., use a toast/notification composable + // showToast({ variant: 'destructive', title: t('logs.singleViewer.downloadFailedTitle'), description: t('logs.singleViewer.errorDownloadingLogFile') })web/scripts/extract-translations.mjs (2)
419-421: Include TSX/JSX in extractor patternsAvoid misreporting keys as unused when used in TSX/JSX.
- report = await createI18NReport({ - vueFiles: 'src/**/*.{vue,ts,js}', + report = await createI18NReport({ + vueFiles: 'src/**/*.{vue,ts,tsx,js,jsx}', languageFiles: 'src/locales/*.json', });
384-391: Add CI verification for extracted translation keys in en.jsonInclude a grouped jq check to avoid boolean-pipe errors and ensure all nested keys exist:
#!/usr/bin/env bash set -euo pipefail file="web/src/locales/en.json" jq -e ' has("headerOsVersion") and (.headerOsVersion | has("versionInformation","unraidOs","unraidApi","viewOsReleaseNotes","viewApiChangelog")) and has("logs") and (.logs | has("singleViewer","totalLines","watchingLogFileTooltip","download","downloading","refresh","loadingLogContent","errorLoadingLogContent","loadingMoreLines")) and has("downgradeOs") and (.downgradeOs | has("pleaseFinishTheInitiatedUpdateTo","downgradeUnraidOs")) ' "$file" >/dev/null echo "All required keys present in $file"Also apply this check at lines 529–536.
web/__test__/components/HeaderOsVersion.test.ts (2)
111-116: Use testTranslate for robust selectorAvoid hardcoding translated text. Use testTranslate to resolve the expected title.
- const versionButton = wrapper.find('[title="Version Information"]'); + const versionButton = wrapper.find(`[title="${testTranslate('headerOsVersion.versionInformation')}"]`);As per coding guidelines
64-72: Optional: rely on i18n plugin instead of mocking vue-i18nYou already install createTestI18n(). Consider removing the vue-i18n mock to avoid divergence between mock and plugin behavior.
-vi.mock('vue-i18n', async (importOriginal) => { - const actual = (await importOriginal()) as typeof import('vue-i18n'); - return { - ...actual, - useI18n: () => ({ - t: testTranslate, - }), - }; -}); ... - global: { - plugins: [testingPinia, createTestI18n()], - }, + global: { plugins: [testingPinia, createTestI18n()] }, ... - global: { - plugins: [testingPinia, createTestI18n()], - }, + global: { plugins: [testingPinia, createTestI18n()] },Based on learnings
Also applies to: 99-103, 155-157
web/src/components/UpdateOs/Status.vue (1)
53-55: Unifytusage (destructure from props).Avoid mixing
props.tin script andt(...)in template. Destructuretonce and use consistently.@@ -const { outputDateTimeReadableDiff: readableDiffRegExp, outputDateTimeFormatted: formattedRegExp } = - useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value); +const { outputDateTimeReadableDiff: readableDiffRegExp, outputDateTimeFormatted: formattedRegExp } = + useDateTimeHelper(dateTimeFormat.value, t, true, regExp.value); @@ - return { - text: regUpdatesExpired.value - ? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [formattedRegExp.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}` - : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [ - formattedRegExp.value, - ]), - title: regUpdatesExpired.value - ? props.t('registration.updateExpirationAction.ineligibleAsOf', [readableDiffRegExp.value]) - : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesFor', [ - readableDiffRegExp.value, - ]), - }; + return { + text: regUpdatesExpired.value + ? `${t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [formattedRegExp.value])} ${t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}` + : t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [ + formattedRegExp.value, + ]), + title: regUpdatesExpired.value + ? t('registration.updateExpirationAction.ineligibleAsOf', [readableDiffRegExp.value]) + : t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesFor', [ + readableDiffRegExp.value, + ]), + };Add once after props:
@@ const props = withDefaults(defineProps<Props>(), { downgradeNotAvailable: false, showExternalDowngrade: false, title: undefined, subtitle: undefined, }); + +const { t } = props;Also applies to: 61-71, 137-147
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (131)
api/.eslintrc.ts(1 hunks)api/README.md(1 hunks)api/package.json(1 hunks)api/scripts/extract-translations.mjs(1 hunks)api/src/i18n/ar.json(1 hunks)api/src/i18n/bn.json(1 hunks)api/src/i18n/ca.json(1 hunks)api/src/i18n/cs.json(1 hunks)api/src/i18n/da.json(1 hunks)api/src/i18n/de.json(1 hunks)api/src/i18n/en.json(1 hunks)api/src/i18n/es.json(1 hunks)api/src/i18n/fr.json(1 hunks)api/src/i18n/hi.json(1 hunks)api/src/i18n/hr.json(1 hunks)api/src/i18n/hu.json(1 hunks)api/src/i18n/it.json(1 hunks)api/src/i18n/ja.json(1 hunks)api/src/i18n/ko.json(1 hunks)api/src/i18n/lv.json(1 hunks)api/src/i18n/nl.json(1 hunks)api/src/i18n/no.json(1 hunks)api/src/i18n/pl.json(1 hunks)api/src/i18n/pt.json(1 hunks)api/src/i18n/ro.json(1 hunks)api/src/i18n/ru.json(1 hunks)api/src/i18n/sv.json(1 hunks)api/src/i18n/uk.json(1 hunks)api/src/i18n/zh.json(1 hunks)api/src/types/jsonforms-i18n.d.ts(1 hunks)api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts(13 hunks)api/src/unraid-api/graph/resolvers/settings/settings.service.ts(3 hunks)api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts(32 hunks)api/src/unraid-api/graph/utils/form-utils.ts(4 hunks)api/src/unraid-api/types/json-forms.ts(1 hunks)crowdin.yml(1 hunks)package.json(1 hunks)plugin/plugins/dynamix.unraid.net.plg(0 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php(1 hunks)plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php(0 hunks)unraid-ui/src/components/brand/BrandButton.vue(3 hunks)unraid-ui/src/forms/AccordionLayout.vue(7 hunks)web/README.md(1 hunks)web/__test__/components/Activation/ActivationModal.test.ts(3 hunks)web/__test__/components/Activation/ActivationSteps.test.ts(2 hunks)web/__test__/components/Activation/WelcomeModal.test.ts(3 hunks)web/__test__/components/Auth.test.ts(5 hunks)web/__test__/components/Brand/Avatar.test.ts(6 hunks)web/__test__/components/CheckUpdateResponseModal.test.ts(2 hunks)web/__test__/components/DowngradeOs.test.ts(6 hunks)web/__test__/components/DownloadApiLogs.test.ts(7 hunks)web/__test__/components/HeaderOsVersion.test.ts(4 hunks)web/__test__/components/KeyActions.test.ts(8 hunks)web/__test__/components/Logs/SingleLogViewer.test.ts(5 hunks)web/__test__/components/Modals.test.ts(0 hunks)web/__test__/components/Registration.test.ts(4 hunks)web/__test__/components/SsoButton.test.ts(10 hunks)web/__test__/components/UpdateOs.test.ts(9 hunks)web/__test__/components/Wrapper/mount-engine.test.ts(3 hunks)web/__test__/store/server.test.ts(1 hunks)web/__test__/utils/i18n.ts(1 hunks)web/package.json(5 hunks)web/scripts/extract-translations.mjs(1 hunks)web/scripts/sort-translations.mjs(1 hunks)web/src/components/Activation/ActivationModal.vue(5 hunks)web/src/components/Activation/ActivationSteps.vue(4 hunks)web/src/components/Activation/WelcomeModal.standalone.vue(2 hunks)web/src/components/ApiKey/ApiKeyCreate.vue(4 hunks)web/src/components/ConnectSettings/ConnectSettings.standalone.vue(7 hunks)web/src/components/ConnectSettings/OidcDebugLogs.vue(3 hunks)web/src/components/DowngradeOs.standalone.vue(2 hunks)web/src/components/DownloadApiLogs.standalone.vue(5 hunks)web/src/components/HeaderOsVersion.standalone.vue(7 hunks)web/src/components/KeyActions.vue(1 hunks)web/src/components/Logs/LogFilterInput.vue(4 hunks)web/src/components/Logs/LogViewer.standalone.vue(6 hunks)web/src/components/Logs/LogViewerToolbar.vue(5 hunks)web/src/components/Logs/SingleLogViewer.vue(9 hunks)web/src/components/Modal.vue(2 hunks)web/src/components/Modals.standalone.vue(1 hunks)web/src/components/Notifications/Item.vue(4 hunks)web/src/components/Notifications/List.vue(4 hunks)web/src/components/Notifications/Sidebar.vue(10 hunks)web/src/components/Registration.standalone.vue(11 hunks)web/src/components/Registration/KeyLinkedStatus.vue(3 hunks)web/src/components/Registration/ReplaceCheck.vue(2 hunks)web/src/components/Registration/UpdateExpiration.vue(1 hunks)web/src/components/Registration/UpdateExpirationAction.vue(4 hunks)web/src/components/UpdateOs.standalone.vue(2 hunks)web/src/components/UpdateOs/CallbackButton.vue(1 hunks)web/src/components/UpdateOs/ChangelogModal.vue(3 hunks)web/src/components/UpdateOs/CheckUpdateResponseModal.vue(14 hunks)web/src/components/UpdateOs/Downgrade.vue(4 hunks)web/src/components/UpdateOs/IgnoredRelease.vue(1 hunks)web/src/components/UpdateOs/RawChangelogRenderer.vue(3 hunks)web/src/components/UpdateOs/Status.vue(11 hunks)web/src/components/UpdateOs/ThirdPartyDrivers.vue(1 hunks)web/src/components/UpdateOs/Update.vue(6 hunks)web/src/components/UpdateOs/UpdateIneligible.vue(4 hunks)web/src/components/UserProfile.standalone.vue(2 hunks)web/src/components/UserProfile/CallbackFeedback.vue(10 hunks)web/src/components/UserProfile/DropdownConnectStatus.vue(1 hunks)web/src/components/UserProfile/DropdownContent.vue(8 hunks)web/src/components/UserProfile/DropdownLaunchpad.vue(1 hunks)web/src/components/UserProfile/DropdownTrigger.vue(1 hunks)web/src/components/UserProfile/ServerState.vue(2 hunks)web/src/components/UserProfile/Trial.vue(2 hunks)web/src/components/UserProfile/UptimeExpire.vue(1 hunks)web/src/components/WanIpCheck.standalone.vue(2 hunks)web/src/components/Wrapper/mount-engine.ts(3 hunks)web/src/components/sso/SsoButtons.vue(1 hunks)web/src/components/sso/useSsoAuth.ts(1 hunks)web/src/composables/dateTime.ts(1 hunks)web/src/helpers/globals.d.ts(1 hunks)web/src/helpers/i18n-loader.ts(1 hunks)web/src/helpers/jsonforms-i18n.ts(1 hunks)web/src/locales/_template.json(0 hunks)web/src/locales/ar.json(1 hunks)web/src/locales/bn.json(1 hunks)web/src/locales/ca.json(1 hunks)web/src/locales/cs.json(1 hunks)web/src/locales/da.json(1 hunks)web/src/locales/de.json(1 hunks)web/src/locales/en.json(1 hunks)web/src/locales/en_US.json(0 hunks)web/src/locales/es.json(1 hunks)web/src/locales/fr.json(1 hunks)web/src/locales/hi.json(1 hunks)web/src/locales/hr.json(1 hunks)web/src/locales/hu.json(1 hunks)web/src/locales/it.json(1 hunks)
💤 Files with no reviewable changes (5)
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php
- web/src/locales/_template.json
- web/test/components/Modals.test.ts
- plugin/plugins/dynamix.unraid.net.plg
- web/src/locales/en_US.json
✅ Files skipped from review due to trivial changes (8)
- api/src/i18n/hi.json
- web/src/components/DownloadApiLogs.standalone.vue
- api/src/i18n/nl.json
- api/src/i18n/zh.json
- api/src/i18n/uk.json
- web/src/locales/cs.json
- api/src/i18n/hr.json
- api/src/i18n/sv.json
🚧 Files skipped from review as they are similar to previous changes (72)
- web/src/components/UpdateOs/IgnoredRelease.vue
- api/src/i18n/ro.json
- api/src/i18n/cs.json
- web/src/components/UpdateOs/ThirdPartyDrivers.vue
- web/test/components/DownloadApiLogs.test.ts
- web/README.md
- api/src/i18n/en.json
- web/src/helpers/globals.d.ts
- web/test/components/SsoButton.test.ts
- api/src/i18n/bn.json
- package.json
- api/src/i18n/no.json
- web/test/components/Registration.test.ts
- api/src/i18n/ko.json
- web/src/components/UpdateOs/CheckUpdateResponseModal.vue
- web/src/components/UpdateOs/RawChangelogRenderer.vue
- web/src/components/sso/SsoButtons.vue
- web/src/locales/de.json
- web/src/components/Activation/ActivationModal.vue
- web/src/components/Registration/UpdateExpirationAction.vue
- web/src/locales/es.json
- api/src/i18n/ar.json
- web/src/components/ApiKey/ApiKeyCreate.vue
- web/src/locales/hi.json
- api/src/i18n/it.json
- web/src/locales/fr.json
- api/src/unraid-api/types/json-forms.ts
- web/src/components/UserProfile.standalone.vue
- web/src/locales/it.json
- api/src/i18n/ru.json
- api/src/types/jsonforms-i18n.d.ts
- api/src/i18n/de.json
- api/src/unraid-api/graph/resolvers/settings/settings.service.ts
- api/README.md
- web/src/components/Activation/WelcomeModal.standalone.vue
- api/src/i18n/es.json
- web/src/components/UserProfile/ServerState.vue
- web/test/components/Activation/WelcomeModal.test.ts
- web/test/components/DowngradeOs.test.ts
- web/src/components/UpdateOs/Update.vue
- web/src/locales/bn.json
- web/src/components/Notifications/List.vue
- web/src/locales/en.json
- api/scripts/extract-translations.mjs
- web/test/components/Activation/ActivationModal.test.ts
- web/src/components/Registration/ReplaceCheck.vue
- api/src/i18n/ca.json
- api/src/i18n/pl.json
- web/src/components/UserProfile/DropdownLaunchpad.vue
- web/src/components/Registration/KeyLinkedStatus.vue
- web/src/components/UserProfile/DropdownConnectStatus.vue
- web/src/components/UpdateOs/CallbackButton.vue
- web/test/store/server.test.ts
- web/src/components/UpdateOs/ChangelogModal.vue
- web/src/components/UpdateOs/Downgrade.vue
- crowdin.yml
- web/test/components/Wrapper/mount-engine.test.ts
- web/src/components/Registration.standalone.vue
- web/src/components/Notifications/Item.vue
- api/src/i18n/ja.json
- api/src/i18n/lv.json
- web/src/helpers/i18n-loader.ts
- web/src/helpers/jsonforms-i18n.ts
- web/test/components/Activation/ActivationSteps.test.ts
- web/src/components/UserProfile/DropdownTrigger.vue
- web/src/locales/ca.json
- web/src/components/KeyActions.vue
- web/src/locales/hr.json
- web/src/components/UserProfile/Trial.vue
- api/src/unraid-api/graph/resolvers/api-key/api-key-form.service.ts
- api/src/i18n/pt.json
- web/test/components/Logs/SingleLogViewer.test.ts
🧰 Additional context used
📓 Path-based instructions (9)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/HeaderOsVersion.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections
Files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/HeaderOsVersion.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/__test__/components/UpdateOs.test.tsweb/src/composables/dateTime.tsweb/src/components/Wrapper/mount-engine.tsapi/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsapi/src/unraid-api/graph/utils/form-utils.tsweb/__test__/components/Auth.test.tsweb/__test__/components/KeyActions.test.tsweb/src/components/sso/useSsoAuth.tsweb/__test__/components/HeaderOsVersion.test.tsweb/__test__/utils/i18n.ts
{api,web}/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/HeaderOsVersion.test.ts
web/__test__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Vue component tests under web/test; run with pnpm test
Files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/HeaderOsVersion.test.tsweb/__test__/utils/i18n.ts
web/__test__/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.{test,spec}.{ts,tsx}: 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 component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test
Files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/CheckUpdateResponseModal.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/KeyActions.test.tsweb/__test__/components/HeaderOsVersion.test.ts
**/components/**/*.vue
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests
Files:
web/src/components/Modal.vueweb/src/components/DowngradeOs.standalone.vueweb/src/components/WanIpCheck.standalone.vueweb/src/components/ConnectSettings/OidcDebugLogs.vueweb/src/components/Registration/UpdateExpiration.vueweb/src/components/Modals.standalone.vueweb/src/components/Logs/LogViewerToolbar.vueweb/src/components/Activation/ActivationSteps.vueweb/src/components/UpdateOs/Status.vueweb/src/components/UpdateOs/UpdateIneligible.vueweb/src/components/UserProfile/DropdownContent.vueweb/src/components/Logs/SingleLogViewer.vueweb/src/components/UpdateOs.standalone.vueweb/src/components/UserProfile/UptimeExpire.vueweb/src/components/Logs/LogFilterInput.vueweb/src/components/ConnectSettings/ConnectSettings.standalone.vueweb/src/components/HeaderOsVersion.standalone.vueweb/src/components/UserProfile/CallbackFeedback.vueunraid-ui/src/components/brand/BrandButton.vueweb/src/components/Notifications/Sidebar.vueweb/src/components/Logs/LogViewer.standalone.vue
api/src/unraid-api/**
📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)
Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code
Files:
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.tsapi/src/unraid-api/graph/utils/form-utils.ts
api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)
Files:
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.tsapi/src/unraid-api/graph/utils/form-utils.ts
🧠 Learnings (25)
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing
Applied to files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Auth.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils
Applied to files:
web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Applied to files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly
Applied to files:
web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Applied to files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Applied to files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/Auth.test.tsweb/__test__/components/HeaderOsVersion.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Applied to files:
web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Applied to files:
web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : In store tests, mock external dependencies used by the store and verify interactions with those mocks
Applied to files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Applied to files:
web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use vi.spyOn() to specify return values or behavior of methods under test
Applied to files:
web/__test__/components/UpdateOs.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components
Applied to files:
web/__test__/components/UpdateOs.test.tsweb/__test__/components/Brand/Avatar.test.tsweb/__test__/components/Auth.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use createTestingPinia() for mocking stores in components
Applied to files:
web/__test__/components/Brand/Avatar.test.tsweb/__test__/components/Auth.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : For store tests, use createPinia() and setActivePinia
Applied to files:
web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/store/**/*.ts : For Pinia store unit tests, initialize with setActivePinia(createPinia()) rather than createTestingPinia, unless testing features from createTestingPinia are explicitly needed
Applied to files:
web/__test__/components/Brand/Avatar.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Applied to files:
web/__test__/components/Brand/Avatar.test.tsweb/__test__/components/KeyActions.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Always run pnpm codegen for GraphQL in the web directory
Applied to files:
web/package.jsonapi/package.json
📚 Learning: 2025-07-21T14:00:25.140Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-07-21T14:00:25.140Z
Learning: Always run `pnpm codegen` for GraphQL code generation in the web directory
Applied to files:
web/package.json
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript import specifiers with .js extensions for ESM compatibility
Applied to files:
api/.eslintrc.tsweb/__test__/components/KeyActions.test.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#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:
api/.eslintrc.tsweb/__test__/components/KeyActions.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/{store,stores}/**/*.ts : In store files, explicitly import Vue reactivity utilities (e.g., computed, ref, watchEffect); do not rely on Nuxt auto-imports
Applied to files:
web/src/components/Logs/SingleLogViewer.vueweb/src/components/ConnectSettings/ConnectSettings.standalone.vueweb/src/components/Notifications/Sidebar.vueweb/src/components/Logs/LogViewer.standalone.vue
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
PR: unraid/api#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/components/Logs/LogFilterInput.vue
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
PR: unraid/api#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/components/ConnectSettings/ConnectSettings.standalone.vue
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Applied to files:
web/__test__/components/Auth.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
Applied to files:
web/__test__/components/KeyActions.test.ts
🧬 Code graph analysis (10)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php (1)
web/scripts/extract-translations.mjs (1)
locale(398-398)
web/__test__/components/UpdateOs.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/helpers/i18n-loader.ts (3)
createI18nInstance(59-69)ensureLocale(93-115)getWindowLocale(117-122)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
api/src/unraid-api/graph/utils/form-utils.ts (1)
createSimpleLabeledControl(7-55)
web/__test__/components/Brand/Avatar.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/__test__/components/CheckUpdateResponseModal.test.ts (1)
web/__test__/utils/i18n.ts (1)
testTranslate(43-47)
web/scripts/extract-translations.mjs (1)
api/scripts/extract-translations.mjs (7)
content(143-143)sourceFile(144-144)keys(54-54)keys(145-145)firstArg(71-71)ignorePatterns(10-17)files(133-137)
web/__test__/components/Auth.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
web/__test__/components/KeyActions.test.ts (1)
web/__test__/utils/i18n.ts (1)
createTestI18n(49-58)
web/__test__/components/HeaderOsVersion.test.ts (1)
web/__test__/utils/i18n.ts (2)
testTranslate(43-47)createTestI18n(49-58)
🪛 ast-grep (0.39.6)
web/__test__/utils/i18n.ts
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${index}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${placeholder}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
| // Get the root data from JSONForms context for rule evaluation | ||
| const rootData = jsonFormsContext?.core?.data || {}; | ||
| const formData = props.data || rootData; | ||
| const formData = (layout.value?.data as unknown) ?? rootData; |
There was a problem hiding this comment.
Unsafe cast bypasses type checking.
The cast (layout.value?.data as unknown) completely bypasses TypeScript's type safety and suggests a type mismatch in the useJsonFormsLayout composable's return type. This could hide runtime type errors.
Consider these alternatives:
- If
layout.value.datahas the correct type, remove the cast - If there's a genuine type mismatch, use proper type narrowing or type guards
- Document why the cast is necessary if it's intentional
Run the following script to examine the useJsonFormsLayout return type:
#!/bin/bash
# Description: Examine useJsonFormsLayout composable type definition
# Search for useJsonFormsLayout definition and return type
ast-grep --pattern $'function useJsonFormsLayout($$$) {
$$$
}'
# Search for useJsonFormsLayout type definitions
rg -nP --type=ts 'useJsonFormsLayout.*:\s*\{' node_modules/@jsonforms/vue/
# Check other usages of layout.value.data in the codebase
rg -nP --type=vue --type=ts -C3 'layout\.value\.data'🤖 Prompt for AI Agents
In unraid-ui/src/forms/AccordionLayout.vue around line 74, the expression "const
formData = (layout.value?.data as unknown) ?? rootData;" unsafely casts
layout.value.data to unknown which bypasses TypeScript checks; fix by removing
the cast and ensuring formData has the correct type: either update the
useJsonFormsLayout return type to include the proper data type, or narrow
layout.value with a type guard/conditional (e.g., check for existence and
expected shape) before assigning, or use a generic type parameter on
useJsonFormsLayout so you can write const formData = layout.value?.data ??
rootData while preserving typing; if the cast is intentional, add an inline
comment referencing why and where the type was validated.
| const flatMessages = enUS as unknown as Record<string, string>; | ||
|
|
||
| function resolveMessage(key: string): string | undefined { | ||
| return flatMessages[key]; | ||
| } | ||
|
|
||
| function replaceParams(template: string, params?: unknown): string { | ||
| if (params === undefined || params === null) { | ||
| return template; | ||
| } | ||
|
|
||
| let result = template; | ||
|
|
||
| if (Array.isArray(params)) { | ||
| params.forEach((value, index) => { | ||
| result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value)); | ||
| }); | ||
| return result; | ||
| } | ||
|
|
||
| if (typeof params === 'object') { | ||
| Object.entries(params as AnyObject).forEach(([placeholder, value]) => { | ||
| result = result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), String(value)); | ||
| }); | ||
| return result; | ||
| } | ||
|
|
||
| if (typeof params === 'number' || typeof params === 'string' || typeof params === 'boolean') { | ||
| return result.replace(/\{0\}/g, String(params)); | ||
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
Nested keys don’t resolve; replace custom resolver with composer t.
flatMessages = enUS as Record<string, string> will not resolve dotted paths (e.g., logs.viewer.title), causing fallbacks to the key and weakening tests. Also avoids dynamic RegExp construction.
Apply:
@@
-import enUS from '~/locales/en.json';
+import enUS from '~/locales/en.json';
@@
-const flatMessages = enUS as unknown as Record<string, string>;
-
-function resolveMessage(key: string): string | undefined {
- return flatMessages[key];
-}
-
-function replaceParams(template: string, params?: unknown): string {
- if (params === undefined || params === null) {
- return template;
- }
-
- let result = template;
-
- if (Array.isArray(params)) {
- params.forEach((value, index) => {
- result = result.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value));
- });
- return result;
- }
-
- if (typeof params === 'object') {
- Object.entries(params as AnyObject).forEach(([placeholder, value]) => {
- result = result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), String(value));
- });
- return result;
- }
-
- if (typeof params === 'number' || typeof params === 'string' || typeof params === 'boolean') {
- return result.replace(/\{0\}/g, String(params));
- }
-
- return result;
-}
+// Build a standalone composer to resolve nested keys and interpolation exactly like runtime.
+const testI18n = createI18n({
+ legacy: false,
+ locale: DEFAULT_LOCALE,
+ fallbackLocale: DEFAULT_LOCALE,
+ messages: { [DEFAULT_LOCALE]: enUS },
+});
@@
-export const testTranslate = ((key: string, params?: unknown) => {
- const message = resolveMessage(key);
- const template = message ?? key;
- return replaceParams(template, params);
-}) as unknown as import('vue-i18n').ComposerTranslation;
+export const testTranslate = testI18n.global.t as unknown as import('vue-i18n').ComposerTranslation;Also applies to: 43-48
🧰 Tools
🪛 ast-grep (0.39.6)
[warning] 23-23: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${index}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 30-30: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{${placeholder}\\}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
| ? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [outputDateTimeFormatted.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}` | ||
| : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [ | ||
| outputDateTimeFormatted.value, | ||
| ]), |
There was a problem hiding this comment.
Consolidate concatenated translation keys.
Concatenating two separate translation keys with template literals breaks proper internationalization, as different languages may require different word orders or sentence structures that cannot be achieved by simple concatenation.
Consider consolidating into a single translation key:
- ? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [outputDateTimeFormatted.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}`
+ ? props.t('registration.updateExpirationAction.eligibleForUpdatesWithExtensionPrompt', [outputDateTimeFormatted.value])The new key should contain the complete sentence with proper placeholders, allowing translators to reorder components as needed for their language.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ? `${props.t('registration.updateExpirationAction.eligibleForUpdatesReleasedOnOr', [outputDateTimeFormatted.value])} ${props.t('registration.updateExpirationAction.extendYourLicenseToAccessThe')}` | |
| : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [ | |
| outputDateTimeFormatted.value, | |
| ]), | |
| ? props.t('registration.updateExpirationAction.eligibleForUpdatesWithExtensionPrompt', [ | |
| outputDateTimeFormatted.value, | |
| ]) | |
| : props.t('registration.updateExpirationAction.eligibleForFreeFeatureUpdatesUntil', [ | |
| outputDateTimeFormatted.value, | |
| ]), |
🤖 Prompt for AI Agents
In web/src/components/Registration/UpdateExpiration.vue around lines 35 to 38,
the code concatenates two translation keys which breaks internationalization;
replace the concatenation with a single consolidated translation key that
contains the full sentence and placeholders (e.g., for the date and for the
phrase "extend your license..."), update the i18n resource files with that new
key (allowing translators to reorder placeholders as needed), and change the
component to call props.t once with the new key and pass
outputDateTimeFormatted.value and any other dynamic text as interpolation
parameters.
| import { client } from '~/helpers/create-apollo-client'; | ||
| import { createHtmlEntityDecoder } from '~/helpers/i18n-utils'; | ||
| import en_US from '~/locales/en_US.json'; | ||
| import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader'; |
There was a problem hiding this comment.
Add .js extension to the import specifier.
The import path should include the .js extension for ESM compatibility.
As per coding guidelines
Apply this diff:
-import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
+import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader'; | |
| import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js'; |
🤖 Prompt for AI Agents
In web/src/components/Wrapper/mount-engine.ts around line 9, the import
statement "import { createI18nInstance, ensureLocale, getWindowLocale } from
'~/helpers/i18n-loader';" must include the .js extension for ESM compatibility;
update the specifier to import from '~/helpers/i18n-loader.js' so the runtime
can resolve the module correctly.
| "userProfile.callbackFeedback.thenGoToToolsRegistrationTo": "Gå derefter til Værktøjer > Registrering for manuelt at installere det", | ||
| "userProfile.callbackFeedback.thisDowngradeWillRequireAReboot": "Denne nedgradering kræver en genstart", | ||
| "userProfile.callbackFeedback.thisUpdateWillRequireAReboot": "Denne opdatering kræver en genstart", | ||
| "userProfile.callbackFeedback.updateUnraidOsConfirmationRequired": "Confirmation of Update Unraid OS required", |
There was a problem hiding this comment.
Untranslated entry remains in English.
The value for userProfile.callbackFeedback.updateUnraidOsConfirmationRequired is still in English. Consider translating it to Danish for consistency (e.g., "Bekræftelse af opdatering af Unraid OS påkrævet").
🤖 Prompt for AI Agents
In web/src/locales/da.json around line 466 the value for
"userProfile.callbackFeedback.updateUnraidOsConfirmationRequired" is still in
English; replace the English string with the Danish translation (for example
"Bekræftelse af opdatering af Unraid OS påkrævet") so the locale file remains
fully translated and consistent with other entries.
- Introduced BigInt scalar type in code generation configurations for both API and web. - Updated GraphQL schema to reflect BigInt as a number type for input and output. - Enhanced DockerContainer and related types to utilize BigInt for sizeRootFs. - Improved documentation for sizeRootFs field to clarify its purpose and data type. - Ensured consistent export formatting in generated files.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
1 similar comment
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
- Updated component registry tests to mock the mount-engine module correctly, ensuring proper hoisting of mock functions. - Refactored mount-engine tests to await asynchronous operations for better reliability and accuracy in test execution. - Marked several test cases as skipped to reflect changes in the unified app architecture and focus on relevant tests. These improvements enhance the clarity and maintainability of the test suite, ensuring more reliable test outcomes.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
web/src/components/Wrapper/mount-engine.ts (1)
9-9: Add.jsextension to the import specifier.The import path should include the
.jsextension for ESM compatibility.As per coding guidelines
Apply this diff:
-import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader'; +import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader.js';
🧹 Nitpick comments (1)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
59-59: Optional: Simplify mock by removing redundant type parameter.The type parameter
<() => string | undefined>is redundant when an implementation with an explicit return type is provided. TypeScript can infer the type from the implementation.Apply this diff to simplify:
-const mockGetWindowLocale = vi.fn<() => string | undefined>(() => undefined); +const mockGetWindowLocale = vi.fn((): string | undefined => undefined);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
web/__test__/components/Wrapper/mount-engine.test.ts(17 hunks)web/__test__/components/component-registry.test.ts(6 hunks)web/src/components/Wrapper/auto-mount.ts(1 hunks)web/src/components/Wrapper/mount-engine.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
{**/*.test.ts,**/__test__/{components,store}/**/*.ts}: Use .rejects.toThrow() without arguments when asserting that async functions throw; avoid checking exact error message strings unless the message format is explicitly under test
Focus tests on observable behavior and outcomes, not implementation details such as exact error messages
Use await nextTick() for DOM update assertions and flushPromises() for complex async chains; always await async operations before asserting
Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Use vi.spyOn() to specify return values or behavior of methods under test
Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Do not rely on Nuxt auto-imports in tests; import required Vue utilities explicitly in test files
Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Component tests should use mount from @vue/test-utils
Stub complex child components that are not the focus of a component test
Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Use createTestingPinia() to mock Pinia stores when testing Vue components
Test that expected elements are rendered and verify component output rather than internals
Test component interactions (clicks, inputs) using await element.trigger and setValue, and verify emitted events via wrapper.emitted()
Prefer semantic queries like find('button') or CSS selectors; avoid relying on data-test IDs unless necessary
Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
In component tests that use real store implementations, configure createTestingPinia with stubActions: false when you need real action behavior
Use findComponent(ComponentName) for locating child components and findAll for collections
Files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/src/components/Wrapper/mount-engine.tsweb/__test__/components/component-registry.test.tsweb/src/components/Wrapper/auto-mount.ts
{api,web}/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{api,web}/**/*.{test,spec}.{ts,tsx}: For error tests, use .rejects.toThrow() without arguments; avoid asserting exact error messages unless that format is the subject
Focus tests on behavior, not implementation details
Avoid brittle tests tied to exact error or log wording
Use mocks as nouns, not verbs
Always await async operations before making assertions
Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
web/__test__/**
📄 CodeRabbit inference engine (CLAUDE.md)
Place Vue component tests under web/test; run with pnpm test
Files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
web/__test__/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.{test,spec}.{ts,tsx}: 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 component tests
Test component behavior and output, not implementation details
Use createTestingPinia() for mocking stores in components
Prefer semantic queries like find('button') over data-test IDs
Use await nextTick() for DOM updates before assertions
For store tests, use createPinia() and setActivePinia
Only use createTestingPinia when its special features are needed
Let stores initialize with natural default state; don’t mock the store under test
Files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
🧠 Learnings (15)
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Component tests should use mount from vue/test-utils
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Use mount from Vue Test Utils for component testing
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Use factory functions in vi.mock calls to define mocks and avoid hoisting pitfalls
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Remember that vi.mock calls are hoisted; avoid mixing mock declarations and module mocks incorrectly
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Place module mock declarations (vi.mock) at the top level of the test file to avoid hoisting issues
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in component tests (e.g., vi.mock for helper modules)
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Use createTestingPinia() to mock Pinia stores when testing Vue components
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to {**/*.test.ts,**/__test__/{components,store}/**/*.ts} : Reset/clear mocks between tests using vi.clearAllMocks() (and vi.resetAllMocks() when appropriate) to ensure isolation
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.tsweb/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/components/**/*.ts : Assert rendered text with wrapper.text(), check attributes via element.attributes(), and verify element existence with expect(element.exists()).toBe(true)
Applied to files:
web/__test__/components/Wrapper/mount-engine.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to **/*.{ts,tsx} : Use TypeScript import specifiers with .js extensions for ESM compatibility
Applied to files:
web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
PR: unraid/api#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/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to {api,web}/**/*.{test,spec}.{ts,tsx} : Place all mock declarations at the top level; use factory functions for module mocks; clear mocks between tests
Applied to files:
web/__test__/components/component-registry.test.ts
📚 Learning: 2025-09-12T01:36:59.838Z
Learnt from: CR
PR: unraid/api#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-12T01:36:59.838Z
Learning: Applies to web/__test__/**/*.{test,spec}.{ts,tsx} : Mock external dependencies and services in component tests
Applied to files:
web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: Applies to **/__test__/mocks/**/*.ts : Frequently used mocks can also be placed under __test__/mocks
Applied to files:
web/__test__/components/component-registry.test.ts
📚 Learning: 2025-08-11T15:10:28.150Z
Learnt from: CR
PR: unraid/api#0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-08-11T15:10:28.150Z
Learning: This Nuxt app is tested with vitest outside the Nuxt runtime; structure tests accordingly
Applied to files:
web/__test__/components/component-registry.test.ts
🧬 Code graph analysis (3)
web/__test__/components/Wrapper/mount-engine.test.ts (1)
web/src/components/Wrapper/mount-engine.ts (2)
mountUnifiedApp(80-198)autoMountAllComponents(201-203)
web/src/components/Wrapper/mount-engine.ts (1)
web/src/helpers/i18n-loader.ts (3)
createI18nInstance(59-69)ensureLocale(93-115)getWindowLocale(117-122)
web/src/components/Wrapper/auto-mount.ts (1)
web/src/components/Wrapper/mount-engine.ts (1)
autoMountAllComponents(201-203)
🔇 Additional comments (9)
web/__test__/components/component-registry.test.ts (2)
3-10: LGTM! Proper hoisted mock setup.The hoisted mocks for the mount-engine module follow best practices: using
vi.hoisted(), placing mocks at the top level, and including clear documentation. This ensures proper hoisting order and avoids common pitfalls.Based on learnings
141-210: Track re-enablement plan for skipped tests.Six tests have been skipped due to the unified app architecture transition. While the architectural reasoning is clear from the comments, there's no TODO, issue reference, or timeline for re-enabling these tests.
Consider:
- Adding a TODO comment with an issue link to track the work needed to adapt these tests to the unified architecture
- Documenting whether the hoisted mocks (lines 4-5) can be removed or are still needed by the active test (lines 132-139)
- Clarifying if these tests will be updated or replaced with new ones
Do you have an issue tracking the work to update these tests for the unified app architecture?
As per coding guidelines
web/src/components/Wrapper/auto-mount.ts (2)
48-52: LGTM! Proper error handling for async mount flows.The error handling correctly prevents unhandled promise rejections in both the DOMContentLoaded event path and the immediate mount scenario. The consistent logging approach makes debugging easier.
Also applies to: 55-57
62-62: LGTM! New public export aligns with i18n initialization strategy.Exposing
initializeGlobalDependenciesas a public export enables manual initialization scenarios, which aligns with the loader-based i18n approach introduced in this PR.web/src/components/Wrapper/mount-engine.ts (3)
22-22: LGTM! Type declaration aligns with loader-based i18n approach.The
Window.LOCALE?: stringdeclaration correctly replaces the previousLOCALE_DATAapproach, supporting the new i18n loader flow where the locale identifier is retrieved viagetWindowLocale().
30-34: LGTM! Async i18n setup eliminates race condition.Making
setupI18nasync and awaitingensureLocaleensures locale messages are fully loaded before mounting components, preventing the brief flash of English content that could occur with fire-and-forget locale loading. Line 88 correctly awaits the setup.Also applies to: 88-88
80-80: LGTM! Async mounting flow properly orchestrated.The async conversion of
mountUnifiedAppandautoMountAllComponentscorrectly propagates the async control flow, ensuring i18n and shared context are fully prepared before mounting. Error handling is properly delegated to callers inauto-mount.ts.Also applies to: 201-203
web/__test__/components/Wrapper/mount-engine.test.ts (2)
74-88: Excellent mock management and test isolation.The beforeEach properly resets all mocks and restores i18n state to defaults, ensuring test independence.
412-427: Well-structured i18n setup tests.The tests properly verify both default locale initialization and window-provided locale handling, ensuring the i18n-loader integration works correctly.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
- Removed the `t` prop from various components, transitioning to the `useI18n` composable for translation management. - Updated test files to utilize `createTestI18n` for improved internationalization consistency. - Enhanced modal and registration components to reflect the new translation approach, ensuring a cleaner and more maintainable codebase. These changes improve the overall integration of i18n across the application, promoting better localization practices.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
web/src/components/LocaleSwitcher.vue (2)
59-61: Remove redundant initialization inonMounted.The
currentLocaleis already initialized withlocale.valueat line 35, making thisonMountedhook redundant. The ref is created with the correct initial value during component setup.Apply this diff to remove the redundant code:
-// Initialize current locale on mount -onMounted(() => { - currentLocale.value = locale.value; -});
47-47: Consider the user experience impact of full page reloads.While the full page reload ensures all components pick up the new locale (as noted in the comment and UI message), it results in a jarring user experience. Modern SPAs typically handle locale changes reactively without reloads.
If the reload is necessary due to the mount engine architecture or legacy PHP component integration, ensure:
- The current page state can be recovered after reload (e.g., scroll position, form data)
- Users are aware that unsaved changes will be lost
- Consider adding a confirmation dialog if there's user input at risk
Verify whether the full reload is truly necessary, or if the i18n system can be configured to reactively update all mounted components through vue-i18n's built-in reactivity.
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
24-81: Several keys defined but unused.The following keys are defined in the constant but never referenced in the code:
provider.discoveryToggleprovider.userInfoEndpointrestrictions.sectionHelprules.sectionDescriptionbuttons.sectionTitlebuttons.sectionDescriptionConsider removing these unused keys or documenting if they're reserved for future use.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts(38 hunks)web/components.d.ts(1 hunks)web/public/test-pages/all-components.html(1 hunks)web/scripts/extract-translations.mjs(1 hunks)web/src/components/DevSettings.vue(2 hunks)web/src/components/LocaleSwitcher.vue(1 hunks)web/src/components/RClone/RCloneConfig.vue(2 hunks)web/src/components/Wrapper/component-registry.ts(1 hunks)web/src/locales/en.json(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- web/scripts/extract-translations.mjs
- web/src/locales/en.json
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Use TypeScript import specifiers with .js extensions for ESM compatibility
Never use the any type; prefer precise typing
Avoid type casting; model proper types from the start
Files:
web/components.d.tsapi/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.tsweb/src/components/Wrapper/component-registry.ts
**/components/**/*.vue
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Some Vue files may require explicit imports like ref or computed due to Nuxt auto-imports not applying in tests
Files:
web/src/components/DevSettings.vueweb/src/components/RClone/RCloneConfig.vueweb/src/components/LocaleSwitcher.vue
api/src/unraid-api/**
📄 CodeRabbit inference engine (.cursor/rules/api-rules.mdc)
Prefer adding new files to the Nest repo at api/src/unraid-api/ instead of legacy code
Files:
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
cache-manager v7 TTL values must be in milliseconds (e.g., 600000 for 10 minutes)
Files:
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
🧠 Learnings (2)
📚 Learning: 2025-02-18T17:36:22.670Z
Learnt from: pujitm
PR: unraid/api#1143
File: web/components/DummyServerSwitcher.vue:16-19
Timestamp: 2025-02-18T17:36:22.670Z
Learning: The DummyServerSwitcher component in web/components/DummyServerSwitcher.vue is a development tool for testing different server states, and includes intentional debug elements to aid development.
Applied to files:
web/src/components/DevSettings.vue
📚 Learning: 2025-04-02T21:21:29.168Z
Learnt from: elibosley
PR: unraid/api#1308
File: unraid-ui/src/components/common/loading/Error.vue:2-2
Timestamp: 2025-04-02T21:21:29.168Z
Learning: Components in the unraid-ui folder require explicit imports and are not autoloaded, unlike other parts of the project that may use Nuxt.js autoloading features.
Applied to files:
web/src/components/Wrapper/component-registry.ts
🧬 Code graph analysis (1)
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts (1)
api/src/unraid-api/graph/utils/form-utils.ts (1)
createSimpleLabeledControl(7-55)
🔇 Additional comments (6)
web/src/components/RClone/RCloneConfig.vue (1)
7-7: LGTM! JsonForms i18n integration is correctly implemented.The changes properly integrate internationalization support:
- Line 7 imports the composable helper
- Line 14 instantiates the i18n state
- Line 233 wires it into JsonForms
The implementation follows the standard JsonForms i18n pattern and aligns with the broader i18n effort described in the PR summary. All required imports are explicit per the coding guidelines.
Also applies to: 14-14, 233-233
web/src/components/DevSettings.vue (1)
6-6: LGTM!The LocaleSwitcher import and integration into DevSettings follows the established component pattern. The fixed width and spacing for the popover content provides a consistent layout for the developer settings UI.
Also applies to: 16-20
web/public/test-pages/all-components.html (1)
243-246: LGTM!The
<unraid-locale-switcher>web component is properly integrated into the test page, providing visibility for the new language selection functionality in the test environment.web/components.d.ts (1)
71-71: LGTM!The GlobalComponents type declaration for LocaleSwitcher follows the established pattern and enables proper TypeScript support for the component.
web/src/components/Wrapper/component-registry.ts (1)
124-128: LGTM!The LocaleSwitcher component registration follows the established pattern for web component mappings, with proper async loading and consistent selector naming.
web/src/components/LocaleSwitcher.vue (1)
2-3: LGTM!Explicit imports from
vueare correctly included, which is essential for components in this directory to work properly in test environments where Nuxt auto-imports may not apply.Based on coding guidelines
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
Outdated
Show resolved
Hide resolved
api/src/unraid-api/graph/resolvers/sso/core/oidc-config.service.ts
Outdated
Show resolved
Hide resolved
… tests - Removed the `t` prop from `ChangelogModal` and `CheckUpdateResponseModal`, transitioning to the `useI18n` composable for translation management. - Updated test files to consistently use `createTestI18n`, enhancing internationalization practices. - Adjusted modal components to reflect the new translation approach, promoting cleaner and more maintainable code. These changes continue to improve the integration of i18n across the application, ensuring better localization practices.
- Removed the `t` prop from multiple components, including `DowngradeOs`, `HeaderOsVersion`, and various modals, transitioning to the `useI18n` composable for improved translation management. - Updated components to ensure consistent use of the `useI18n` approach, enhancing code clarity and maintainability. - These changes further streamline internationalization practices across the application, promoting better localization and reducing prop dependencies.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
- Updated the OIDC_I18N structure to streamline the localization of providers and defaultAllowedOrigins by using direct keys instead of nested objects. - Adjusted the i18n references in the OidcConfigPersistence class to utilize the updated OIDC_I18N structure, enhancing consistency in translation handling. - Added LOCALE property to the global window type for improved locale management. These changes contribute to a more cohesive internationalization strategy within the OIDC configuration.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
- Eliminated the `t` prop from multiple components, including modals and user profile components, transitioning to a cleaner codebase without unnecessary dependencies. - Updated the sorting function in `sort-translations.mjs` to use a shared collator for consistent string comparison across locales. - These changes enhance the maintainability of the code and streamline internationalization practices throughout the application.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
Summary by CodeRabbit
New Features
Accessibility
Documentation
Refactor
Tests