Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
d07aa42
table > select & status badge
pujitm Sep 17, 2025
a32dd21
enhance: add ssh retry logic and SSH options in deploy script
Ajit-Mehrotra Nov 21, 2025
29312d7
refactor: replace LoadingSpinner and LoadingError with nuxtUI components
Ajit-Mehrotra Nov 21, 2025
a8459c7
fix: address notification pane infinite network request and improve e…
Ajit-Mehrotra Nov 24, 2025
7679d71
refactor: update notification components to use UIcon and UButton for…
Ajit-Mehrotra Nov 25, 2025
e80ea79
refactor: enhance app configuration and notification components for i…
Ajit-Mehrotra Dec 2, 2025
88766ad
refactor(ui): replace vue-sonner toasts with nuxtui toasts
Ajit-Mehrotra Dec 2, 2025
4798c89
fix(notifications api): implement async generator for improve filteri…
Ajit-Mehrotra Dec 3, 2025
80bbf2d
fix(notification): address filenames greater than 255 bytes
Ajit-Mehrotra Dec 5, 2025
9f27eb7
refactor(notifications): sync toast position with legacy settings
Ajit-Mehrotra Dec 5, 2025
1000976
fix(app.config): update toast max duration comment for compatibility …
Ajit-Mehrotra Dec 5, 2025
c21ecc7
fix(notifications): override notification bell hover to maintain cons…
Ajit-Mehrotra Dec 9, 2025
67ac307
fix(notifications): add break-word styling for long words in subject …
Ajit-Mehrotra Dec 9, 2025
be1e2f5
fix(notifications): add elipses to long single-word toast titles
Ajit-Mehrotra Dec 9, 2025
fe74e53
fix(notifications): show full title on notification pane and truncate…
Ajit-Mehrotra Dec 9, 2025
f690c23
fix(notifications): add support for bottom-center'd nuxtui toasts
Ajit-Mehrotra Dec 9, 2025
f7fe959
chore(gitignore): add .dev-scripts to gitignore for local dev scripts
Ajit-Mehrotra Dec 9, 2025
875aa09
fix(notifications): fix color of notification button's active state
Ajit-Mehrotra Dec 9, 2025
6a04a06
fix(notification): add min width to allow flex title to shrink and wr…
Ajit-Mehrotra Dec 9, 2025
e3bf571
fix(notifications): resolve ID mismatches, counter bugs, and paginati…
Ajit-Mehrotra Dec 11, 2025
2122e6d
feat(notifications): add notify script for backup and restore on plug…
Ajit-Mehrotra Dec 12, 2025
ddd6b0b
Revert "fix(notifications): add support for bottom-center'd nuxtui to…
Ajit-Mehrotra Dec 12, 2025
4a19d4d
fix(notifications): enable user overrides for the header text color
Ajit-Mehrotra Dec 12, 2025
4c42b4b
fix(notifications): update notify script's filename sanitization to i…
Ajit-Mehrotra Dec 13, 2025
53ee465
fix: resolve no-undef lint errors for auto-imported composables in Vu…
Ajit-Mehrotra Dec 15, 2025
6458457
feat(components): add DockerContainerStatCell and USlideover componen…
Ajit-Mehrotra Dec 15, 2025
d10e054
feat(notifications): replace docker toast usage with nuxtui toast com…
Ajit-Mehrotra Dec 15, 2025
fd1a046
feat(notifications): integrate nuxtui toast composable for success me…
Ajit-Mehrotra Dec 15, 2025
7969e44
refactor(navigation): replace window.location with navigate helper fo…
Ajit-Mehrotra Dec 15, 2025
e61657c
refactor(notification): use "size" for square css widths and height
Ajit-Mehrotra Dec 15, 2025
c18de73
refactor(notifications): center notification error button text
Ajit-Mehrotra Dec 15, 2025
ad78a02
feat(errors): add dbgApolloError function for enhanced GraphQL error …
Ajit-Mehrotra Dec 15, 2025
c2d2fbe
feat(notifications): introduce constants for notification icons and c…
Ajit-Mehrotra Dec 15, 2025
e817d6a
fix(notifications): add onResult handler to update online status base…
Ajit-Mehrotra Dec 15, 2025
4abc79a
fix(notifications): sync badges on api recovery
Ajit-Mehrotra Dec 15, 2025
17f0176
fix(notifications): set 'top-right' as default toast location
Ajit-Mehrotra Dec 15, 2025
4c49baf
chore(dependencies): update @nuxt/ui to version 4.2.1 from 4.0.0 alpha
Ajit-Mehrotra Dec 16, 2025
1e5decc
Revert "Revert "fix(notifications): add support for bottom-center'd n…
Ajit-Mehrotra Dec 16, 2025
88e76d7
feat(notifications): update nuxtui to 4.2.1 and add new toast features
Ajit-Mehrotra Dec 16, 2025
a8578bf
refactor(notifications): move ToastPosition type to a separate file a…
Ajit-Mehrotra Dec 16, 2025
3c9c04d
wip(css-modification): add DefaultBaseCssModification class for CSS s…
Ajit-Mehrotra Dec 18, 2025
5628bf9
fix(color): fix bell icon color in header to work on all themes
Ajit-Mehrotra Dec 23, 2025
2f88228
fix(theme): adjust logo overlap on azure/gray themes on unraid versio…
Ajit-Mehrotra Dec 23, 2025
902307a
feat(notifications): add file-modification scripts for nuxtui notific…
Ajit-Mehrotra Dec 24, 2025
b456678
fix: update file-modification location for default-base.css
Ajit-Mehrotra Dec 29, 2025
716789f
fix: add generated files + patch + snapshot files
Ajit-Mehrotra Dec 29, 2025
8ef00ce
chore: lint
Ajit-Mehrotra Dec 29, 2025
9ed71e5
chore: rebase cleanup
Ajit-Mehrotra Dec 30, 2025
ad2a5cc
test(notifications): Update various api and web component tests
Ajit-Mehrotra Dec 30, 2025
443335e
fix: update header background file modification
Ajit-Mehrotra Dec 30, 2025
00f015f
test(notifications): update patch files
Ajit-Mehrotra Dec 30, 2025
1c4f33e
Revert "fix: update header background file modification"
Ajit-Mehrotra Dec 30, 2025
b0ff85b
Revert "test(notifications): update patch files"
Ajit-Mehrotra Dec 30, 2025
9504b4a
fix(css): file-modifications for backwards compatability with 7.0
Ajit-Mehrotra Dec 30, 2025
18f3227
fix: default-gray theme's header background color via a file-modifier.
Ajit-Mehrotra Dec 31, 2025
bff05e0
fix(web): remove incorrect toaster mapping causing empty notification
Ajit-Mehrotra Jan 2, 2026
86ec429
fix(css): add @layer in default-base file modifier to ensure css spec…
Ajit-Mehrotra Jan 2, 2026
4ee5506
test: update snapshots and patch files for default-base.css file-modi…
Ajit-Mehrotra Jan 2, 2026
f23e5bc
fix: clean up rebase
Ajit-Mehrotra Jan 2, 2026
41aa09f
build(scripts): enhance local docker plugin development workflow
Ajit-Mehrotra Jan 2, 2026
736c24c
chore: prevent automatic formatting of .page files (interpretted as .…
Ajit-Mehrotra Jan 2, 2026
e140863
feat(toast): finish migration to nuxtui toasts (finished migrating ne…
Ajit-Mehrotra Jan 2, 2026
aa2c6eb
chore: remove legacy Toaster.vue and related components from monorepo
Ajit-Mehrotra Jan 2, 2026
eae2651
test: update file modification tests
Ajit-Mehrotra Jan 3, 2026
d8f3e92
fix: remove unused import
Ajit-Mehrotra Jan 3, 2026
de860cb
fix: add file-modification for font-awesome.css
Ajit-Mehrotra Jan 4, 2026
acb5427
fix(notification): fix improper Notifications.page and plg file
Ajit-Mehrotra Jan 4, 2026
2b0f8d0
fix(notifications): update notification.page file-modifications and t…
Ajit-Mehrotra Jan 4, 2026
41ae0e2
fix(notifications): update 7.0 file-modifications for Notifications.p…
Ajit-Mehrotra Jan 4, 2026
6a088db
fix(webgui): fix translation cache invalidation logic
Ajit-Mehrotra Jan 4, 2026
33d32ff
fix(notifcations): update display position helper text to reflect not…
Ajit-Mehrotra Jan 4, 2026
0858b4f
fix(file-modifications): fix translation cache invalidation for Unrai…
Ajit-Mehrotra Jan 4, 2026
c14237d
fix(web): use API-formatted timestamp for notifications to respect us…
Ajit-Mehrotra Jan 5, 2026
82b6819
feat(notifications): improve item.vue layout and add relative timestamps
Ajit-Mehrotra Jan 5, 2026
c93d4f2
chore: remove unused dependency from unraid-ui's package.json
Ajit-Mehrotra Jan 5, 2026
5c742e4
AI(no-idea): honestly have no idea why this fixes it cuz the build wa…
Ajit-Mehrotra Jan 5, 2026
7d39b78
fix(notification): update notify script file-modification to work on …
Ajit-Mehrotra Jan 5, 2026
265a407
fix(notifications): update notification api service and sidebar.vue t…
Ajit-Mehrotra Jan 5, 2026
197213a
fixup! fix(notifications): update notification api service and sideba…
Ajit-Mehrotra Jan 5, 2026
933d341
fixup! fix(notifications): update notification api service and sideba…
Ajit-Mehrotra Jan 6, 2026
ac03da2
fixup! fix(notifications): update notification api service and sideba…
Ajit-Mehrotra Jan 6, 2026
f748f5a
fix(notifications): address code review feedback and improve stability
Ajit-Mehrotra Jan 6, 2026
e9b53c8
refactor(notifications): address coderabbit review feedback
Ajit-Mehrotra Jan 6, 2026
11bfdc3
fixup! refactor(notifications): address coderabbit review feedback
Ajit-Mehrotra Jan 6, 2026
95de2e4
fixup! refactor(notifications): address coderabbit review feedback
Ajit-Mehrotra Jan 6, 2026
07cbde3
Revert "AI(no-idea): honestly have no idea why this fixes it cuz the …
Ajit-Mehrotra Jan 6, 2026
89ad63a
build: fix type-check failures and SSR guards
Ajit-Mehrotra Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ api/dev/Unraid.net/myservers.cfg

# local Mise settings
.mise.toml
mise.toml

# Compiled test pages (generated from Nunjucks templates)
web/public/test-pages/*.html

# local scripts for testing and development
.dev-scripts/
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"files.associations": {
"*.page": "php"
},
"intelephense.format.enable": false,
}
Comment on lines +1 to +6
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove trailing comma in JSON object.

The JSON file has a syntax error: a trailing comma after "intelephense.format.enable": false, before the closing brace. JSON does not permit trailing commas, and this will cause VS Code to fail parsing the settings file.

🔎 Proposed fix
{
    "files.associations": {
        "*.page": "php"
    },
-   "intelephense.format.enable": false,
+   "intelephense.format.enable": false
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"files.associations": {
"*.page": "php"
},
"intelephense.format.enable": false,
}
{
"files.associations": {
"*.page": "php"
},
"intelephense.format.enable": false
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 5-6: Expected a property but instead found '}'.

Expected a property here.

(parse)

🤖 Prompt for AI Agents
In .vscode/settings.json around lines 1 to 6 there is a trailing comma after the
"intelephense.format.enable": false entry which makes the JSON invalid; remove
the trailing comma so the last property in the object does not end with a comma
and ensure the file is valid JSON (no other trailing commas or syntax errors).

2 changes: 1 addition & 1 deletion @tailwind-shared/base-utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@ unraid-sso-button.unapi {
--text-7xl: 4.5rem;
--text-8xl: 6rem;
--text-9xl: 8rem;
}
}
2 changes: 1 addition & 1 deletion @tailwind-shared/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
@import './css-variables.css';
@import './unraid-theme.css';
@import './theme-variants.css';
@import './base-utilities.css';
@import './base-utilities.css';
2 changes: 1 addition & 1 deletion @tailwind-shared/theme-variants.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@
/* Dark Mode Overrides */
.dark {
--color-border: #383735;
}
}
4 changes: 1 addition & 3 deletions api/dev/configs/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,5 @@
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],
"plugins": [
"unraid-api-plugin-connect"
]
"plugins": []
}
8 changes: 8 additions & 0 deletions api/generated-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,13 @@ type NotificationCounts {
total: Int!
}

type NotificationSettings {
position: String!
expand: Boolean!
duration: Int!
max: Int!
}

type NotificationOverview {
unread: NotificationCounts!
archive: NotificationCounts!
Expand Down Expand Up @@ -1438,6 +1445,7 @@ type Notifications implements Node {
Deduplicated list of unread warning and alert notifications, sorted latest first.
"""
warningsAndAlerts: [Notification!]!
settings: NotificationSettings!
}

input NotificationFilter {
Expand Down
2 changes: 2 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,15 @@
"// Testing": "",
"test": "NODE_ENV=test vitest run",
"test:watch": "NODE_ENV=test vitest --ui",
"test:modifications:update": "rm -rf src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded src/unraid-api/unraid-file-modifier/modifications/patches /Users/ajitmehrotra/Projects/Unraid/api/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots && NODE_ENV=test vitest run src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts -u",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Remove hardcoded absolute path from script.

The script contains a hardcoded absolute path /Users/ajitmehrotra/Projects/Unraid/api/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots that will fail on other developers' machines and in CI environments. The other paths in the command are already relative and correct.

🔎 Proposed fix
-        "test:modifications:update": "rm -rf src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded src/unraid-api/unraid-file-modifier/modifications/patches /Users/ajitmehrotra/Projects/Unraid/api/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots && NODE_ENV=test vitest run src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts -u",
+        "test:modifications:update": "rm -rf src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded src/unraid-api/unraid-file-modifier/modifications/patches src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots && NODE_ENV=test vitest run src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts -u",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"test:modifications:update": "rm -rf src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded src/unraid-api/unraid-file-modifier/modifications/patches /Users/ajitmehrotra/Projects/Unraid/api/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots && NODE_ENV=test vitest run src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts -u",
"test:modifications:update": "rm -rf src/unraid-api/unraid-file-modifier/modifications/__test__/__fixtures__/downloaded src/unraid-api/unraid-file-modifier/modifications/patches src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots && NODE_ENV=test vitest run src/unraid-api/unraid-file-modifier/modifications/__test__/generic-modification.spec.ts -u",
🤖 Prompt for AI Agents
In api/package.json around line 42, the npm script contains a hardcoded absolute
path
(/Users/ajitmehrotra/Projects/Unraid/api/api/src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots)
which will break on other machines/CI; replace that absolute path with the
corresponding relative path used by the other entries (e.g.
src/unraid-api/unraid-file-modifier/modifications/__test__/snapshots) so all
removed paths are relative and cross-environment compatible, then run the script
locally to verify it still deletes the intended fixtures/snapshots.

"coverage": "NODE_ENV=test vitest run --coverage",
"// Docker": "",
"container:build": "./scripts/dc.sh build dev",
"container:start": "pnpm run container:stop && ./scripts/dc.sh run --rm --service-ports dev",
"container:stop": "./scripts/dc.sh stop dev",
"container:test": "./scripts/dc.sh run --rm builder pnpm run test",
"container:enter": "./scripts/dc.sh exec dev /bin/bash",
"docker:build-and-run": "pnpm --filter @unraid/connect-plugin docker:build-and-run",
"// Migration Scripts": "",
"migration:codefirst": "tsx ./src/unraid-api/graph/migration-script.ts"
},
Expand Down
23 changes: 23 additions & 0 deletions api/src/__test__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,29 @@ describe('formatDatetime', () => {
}
);
});

describe('Unraid PHP-style date formats (conversion)', () => {
const phpFormats = [
['d-m-Y', '14-02-2024'],
['m-d-Y', '02-14-2024'],
['Y-m-d', '2024-02-14'],
];

const phpTimeFormats = [
['h:i A', '12:34 PM'],
['H:i', '12:34'],
];

it.each(phpFormats)('converts and formats date with %s', (format, expected) => {
const result = formatDatetime(testDate, { dateFormat: format });
expect(result).toContain(expected);
});

it.each(phpTimeFormats)('converts and formats time with %s', (format, expected) => {
const result = formatDatetime(testDate, { timeFormat: format, dateFormat: 'Y-m-d' });
expect(result).toContain(expected);
});
});
});

describe('csvStringToArray', () => {
Expand Down
7 changes: 5 additions & 2 deletions api/src/core/types/ini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ interface Notify {
plugin: string;
docker_notify: string;
report: string;
/** @deprecated (will remove in future release). Date format: DD-MM-YYYY, MM-DD-YYY, or YYYY-MM-DD */
/** Date format: DD-MM-YYYY, MM-DD-YYY, or YYYY-MM-DD */
date: 'd-m-Y' | 'm-d-Y' | 'Y-m-d';
/**
* @deprecated (will remove in future release). Time format:
* Time format:
* - `hi: A` => 12 hr
* - `H:i` => 24 hr (default)
*/
Expand All @@ -93,6 +93,9 @@ interface Notify {
system: string;
version: string;
docker_update: string;
expand?: string | boolean;
duration?: string | number;
max?: string | number;
}

interface Ssmtp {
Expand Down
38 changes: 38 additions & 0 deletions api/src/unraid-api/cli/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,17 @@ export type CpuLoad = {
percentUser: Scalars['Float']['output'];
};

export type CpuPackages = Node & {
__typename?: 'CpuPackages';
id: Scalars['PrefixedID']['output'];
/** Power draw per package (W) */
power: Array<Scalars['Float']['output']>;
/** Temperature per package (°C) */
temp: Array<Scalars['Float']['output']>;
/** Total CPU package power draw (W) */
totalPower: Scalars['Float']['output'];
};

export type CpuUtilization = Node & {
__typename?: 'CpuUtilization';
/** CPU load for each core */
Expand Down Expand Up @@ -591,6 +602,19 @@ export type Customization = {
theme: Theme;
};

/** Customization related mutations */
export type CustomizationMutations = {
__typename?: 'CustomizationMutations';
/** Update the UI theme (writes dynamix.cfg) */
setTheme: Theme;
};


/** Customization related mutations */
export type CustomizationMutationsSetThemeArgs = {
theme: ThemeName;
};

export type DeleteApiKeyInput = {
ids: Array<Scalars['PrefixedID']['input']>;
};
Expand Down Expand Up @@ -1065,6 +1089,7 @@ export type InfoCpu = Node & {
manufacturer?: Maybe<Scalars['String']['output']>;
/** CPU model */
model?: Maybe<Scalars['String']['output']>;
packages: CpuPackages;
/** Number of physical processors */
processors?: Maybe<Scalars['Int']['output']>;
/** CPU revision */
Expand All @@ -1081,6 +1106,8 @@ export type InfoCpu = Node & {
stepping?: Maybe<Scalars['Int']['output']>;
/** Number of CPU threads */
threads?: Maybe<Scalars['Int']['output']>;
/** Per-package array of core/thread pairs, e.g. [[[0,1],[2,3]], [[4,5],[6,7]]] */
topology: Array<Array<Array<Scalars['Int']['output']>>>;
/** CPU vendor */
vendor?: Maybe<Scalars['String']['output']>;
/** CPU voltage */
Expand Down Expand Up @@ -1422,6 +1449,7 @@ export type Mutation = {
createDockerFolderWithItems: ResolvedOrganizerV1;
/** Creates a new notification record */
createNotification: Notification;
customization: CustomizationMutations;
/** Deletes all archived notifications on server. */
deleteArchivedNotifications: NotificationOverview;
deleteDockerEntries: ResolvedOrganizerV1;
Expand Down Expand Up @@ -1659,6 +1687,14 @@ export type NotificationOverview = {
unread: NotificationCounts;
};

export type NotificationSettings = {
__typename?: 'NotificationSettings';
duration: Scalars['Int']['output'];
expand: Scalars['Boolean']['output'];
max: Scalars['Int']['output'];
position: Scalars['String']['output'];
};

export enum NotificationType {
ARCHIVE = 'ARCHIVE',
UNREAD = 'UNREAD'
Expand All @@ -1670,6 +1706,7 @@ export type Notifications = Node & {
list: Array<Notification>;
/** A cached overview of the notifications in the system & their severity. */
overview: NotificationOverview;
settings: NotificationSettings;
/** Deduplicated list of unread warning and alert notifications, sorted latest first. */
warningsAndAlerts: Array<Notification>;
};
Expand Down Expand Up @@ -2269,6 +2306,7 @@ export type Subscription = {
parityHistorySubscription: ParityCheck;
serversSubscription: Server;
systemMetricsCpu: CpuUtilization;
systemMetricsCpuTelemetry: CpuPackages;
systemMetricsMemory: MemoryUtilization;
upsUpdates: UpsDevice;
};
Expand Down
32 changes: 18 additions & 14 deletions api/src/unraid-api/config/api-config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,25 @@ export const loadApiConfig = async () => {
const defaultConfig = createDefaultConfig();
const apiHandler = new ApiConfigPersistence(new ConfigService()).getFileHandler();

const diskConfig: Partial<ApiConfig> = await apiHandler.loadConfig();
// Hack: cleanup stale connect plugin entry if necessary
if (!isConnectPluginInstalled()) {
diskConfig.plugins = diskConfig.plugins?.filter(
(plugin) => plugin !== 'unraid-api-plugin-connect'
);
await apiHandler.writeConfigFile(diskConfig as ApiConfig);
}
try {
const diskConfig: Partial<ApiConfig> = await apiHandler.loadConfig();
// Hack: cleanup stale connect plugin entry if necessary
if (!isConnectPluginInstalled() && diskConfig.plugins) {
diskConfig.plugins = diskConfig.plugins?.filter(
(plugin) => plugin !== 'unraid-api-plugin-connect'
);
await apiHandler.writeConfigFile(diskConfig as ApiConfig);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe type cast from Partial to ApiConfig.

The cast assumes diskConfig contains all required ApiConfig fields, but it's typed as Partial<ApiConfig>. If the disk config is incomplete, this could cause issues downstream.

Consider either:

  1. Merging with defaultConfig before the write operation to ensure all required fields are present
  2. Using a type guard to validate the config structure before casting
🔎 Proposed fix
 if (!isConnectPluginInstalled() && diskConfig.plugins) {
     diskConfig.plugins = diskConfig.plugins.filter(
         (plugin) => plugin !== 'unraid-api-plugin-connect'
     );
-    await apiHandler.writeConfigFile(diskConfig as ApiConfig);
+    
+    const configToWrite: ApiConfig = {
+        ...defaultConfig,
+        ...diskConfig,
+        version: API_VERSION,
+    };
+    await apiHandler.writeConfigFile(configToWrite);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await apiHandler.writeConfigFile(diskConfig as ApiConfig);
const configToWrite: ApiConfig = {
...defaultConfig,
...diskConfig,
version: API_VERSION,
};
await apiHandler.writeConfigFile(configToWrite);
🤖 Prompt for AI Agents
In @api/src/unraid-api/config/api-config.module.ts at line 39, The diskConfig
variable is typed Partial<ApiConfig> but is being unsafely cast to ApiConfig
when calling apiHandler.writeConfigFile(diskConfig as ApiConfig); ensure the
config is complete before writing by either merging diskConfig with a default
ApiConfig (e.g., defaultConfig) and passing the merged result to
apiHandler.writeConfigFile, or add a runtime type guard/validation function that
checks required fields on diskConfig and only casts and writes after validation;
update the call site to use the merged/validated object instead of the direct
cast.

}

return {
...defaultConfig,
...diskConfig,
// diskConfig's version may be older, but we still want to use the correct version
version: API_VERSION,
};
return {
...defaultConfig,
...diskConfig,
// diskConfig's version may be older, but we still want to use the correct version
version: API_VERSION,
};
} catch (e) {
return defaultConfig;
}
Comment on lines +32 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error logging and improve error handling.

The try/catch block silently swallows all errors and returns the default config. While graceful degradation is appropriate per the learnings about Unraid API handling missing configs, errors should be logged for debugging and operational visibility.

Additionally, the writeConfigFile call on line 39 could fail and throw an error that would be caught by the outer catch block, potentially hiding the actual config that was successfully loaded.

🔎 Proposed fix
 try {
     const diskConfig: Partial<ApiConfig> = await apiHandler.loadConfig();
-    // Hack: cleanup stale connect plugin entry if necessary
-    if (!isConnectPluginInstalled() && diskConfig.plugins) {
-        diskConfig.plugins = diskConfig.plugins?.filter(
-            (plugin) => plugin !== 'unraid-api-plugin-connect'
-        );
-        await apiHandler.writeConfigFile(diskConfig as ApiConfig);
+    
+    if (!isConnectPluginInstalled() && diskConfig.plugins) {
+        diskConfig.plugins = diskConfig.plugins.filter(
+            (plugin) => plugin !== 'unraid-api-plugin-connect'
+        );
+        
+        try {
+            await apiHandler.writeConfigFile(diskConfig as ApiConfig);
+        } catch (writeError) {
+            console.error('Failed to write cleaned config, proceeding with loaded config:', writeError);
+        }
     }
 
     return {
         ...defaultConfig,
         ...diskConfig,
-        // diskConfig's version may be older, but we still want to use the correct version
         version: API_VERSION,
     };
 } catch (e) {
+    console.error('Failed to load API config, using defaults:', e);
     return defaultConfig;
 }

Based on learnings, the Unraid API handles missing configs gracefully with fallbacks.

🤖 Prompt for AI Agents
In @api/src/unraid-api/config/api-config.module.ts around lines 32 - 50, The
try/catch currently swallows all errors and can hide failures from
apiHandler.loadConfig and apiHandler.writeConfigFile; change it to log errors
and avoid letting writeConfigFile failures discard a successfully loaded
diskConfig: call apiHandler.loadConfig() inside the try as you do, but wrap the
writeConfigFile(...) call in its own try/catch that logs the error (use the
module's logger) so a failing write does not trigger the outer fallback; in the
outer catch log the caught exception (include error details) before returning
defaultConfig; reference apiHandler.loadConfig, isConnectPluginInstalled,
apiHandler.writeConfigFile, defaultConfig and API_VERSION to locate the relevant
logic.

};

/**
Expand Down
13 changes: 13 additions & 0 deletions api/src/unraid-api/config/api-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ describe('ApiConfigPersistence', () => {
describe('loadApiConfig', () => {
beforeEach(async () => {
vi.clearAllMocks();
vi.spyOn(ApiConfigPersistence.prototype, 'getFileHandler').mockReturnValue({
loadConfig: vi.fn().mockResolvedValue({}),
readConfigFile: vi.fn().mockResolvedValue({}),
writeConfigFile: vi.fn().mockResolvedValue(true),
updateConfig: vi.fn().mockResolvedValue(true),
} as any);
});

it('should return default config with current API_VERSION', async () => {
Expand All @@ -209,6 +215,13 @@ describe('loadApiConfig', () => {
});

it('should handle errors gracefully and return defaults', async () => {
vi.spyOn(ApiConfigPersistence.prototype, 'getFileHandler').mockReturnValue({
loadConfig: vi.fn().mockRejectedValue(new Error('Config load failed')),
readConfigFile: vi.fn().mockResolvedValue({}),
writeConfigFile: vi.fn(),
updateConfig: vi.fn(),
} as any);

const result = await loadApiConfig();

expect(result).toEqual({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DOCKER_SERVICE_TOKEN = Symbol('DOCKER_SERVICE');
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Field, InputType, Int, ObjectType, registerEnumType } from '@nestjs/graphql';

import { Node } from '@unraid/shared/graphql.model.js';
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';

export enum NotificationType {
UNREAD = 'UNREAD',
Expand Down Expand Up @@ -99,6 +99,31 @@ export class NotificationCounts {
total!: number;
}

@ObjectType('NotificationSettings')
export class NotificationSettings {
@Field()
@IsString()
@IsNotEmpty()
position!: string;

@Field(() => Boolean)
@IsBoolean()
@IsNotEmpty()
expand!: boolean;

@Field(() => Int)
@IsInt()
@Min(1)
@IsNotEmpty()
duration!: number;

@Field(() => Int)
@IsInt()
@Min(1)
@IsNotEmpty()
max!: number;
}

@ObjectType('NotificationOverview')
export class NotificationOverview {
@Field(() => NotificationCounts)
Expand Down Expand Up @@ -170,4 +195,8 @@ export class Notifications extends Node {
})
@IsNotEmpty()
warningsAndAlerts!: Notification[];

@Field(() => NotificationSettings)
@IsNotEmpty()
settings!: NotificationSettings;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
NotificationImportance,
NotificationOverview,
Notifications,
NotificationSettings,
NotificationType,
} from '@app/unraid-api/graph/resolvers/notifications/notifications.model.js';
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
Expand Down Expand Up @@ -41,6 +42,11 @@ export class NotificationsResolver {
return this.notificationsService.getOverview();
}

@ResolveField(() => NotificationSettings)
public settings(): NotificationSettings {
return this.notificationsService.getSettings();
}

@ResolveField(() => [Notification])
public async list(
@Args('filter', { type: () => NotificationFilter })
Expand Down
Loading
Loading