Repository: unraid/api
Pull request #1334: chore: move to independent docker events
Author: login: elibosley
Created: 2025-04-04T15:13:48Z | Merged: 2025-04-07T14:47:13Z

- update dockerode to v4
- remove docker-event-emitter dependency

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a real-time Docker event monitoring service that improves container status updates.
  - Consolidated Docker functionalities within a dedicated module for enhanced management.

- **Refactor**
  - Streamlined Docker event handling to boost stability and simplify operations.
  - Updated the structure of the resolvers module to encapsulate Docker-related functionality.
  - Made certain methods public for easier access and interaction.

- **Chores**
  - Upgraded Docker-related dependencies and removed deprecated packages to improve performance.
  - Removed unused imports to clean up the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/api/package.json b/api/package.json
index 130814e4dd..5b1e6f63d6 100644
--- a/api/package.json
+++ b/api/package.json
@@ -87,8 +87,7 @@
         "cron": "3.5.0",
         "cross-fetch": "^4.0.0",
         "diff": "^7.0.0",
-        "docker-event-emitter": "^0.3.0",
-        "dockerode": "^3.3.5",
+        "dockerode": "^4.0.5",
         "dotenv": "^16.4.5",
         "execa": "^9.5.1",
         "exit-hook": "^4.0.0",
diff --git a/api/src/unraid-api/app/app.module.ts b/api/src/unraid-api/app/app.module.ts
index 76bee7c641..47dd873a59 100644
--- a/api/src/unraid-api/app/app.module.ts
+++ b/api/src/unraid-api/app/app.module.ts
@@ -7,7 +7,6 @@ import { LoggerModule } from 'nestjs-pino';
 
 import { apiLogger } from '@app/core/log.js';
 import { LOG_LEVEL } from '@app/environment.js';
-import { AuthInterceptor } from '@app/unraid-api/auth/auth.interceptor.js';
 import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
 import { AuthenticationGuard } from '@app/unraid-api/auth/authentication.guard.js';
 import { CronModule } from '@app/unraid-api/cron/cron.module.js';
diff --git a/api/src/unraid-api/graph/graph.module.ts b/api/src/unraid-api/graph/graph.module.ts
index f075b7e6e6..5f974c035a 100644
--- a/api/src/unraid-api/graph/graph.module.ts
+++ b/api/src/unraid-api/graph/graph.module.ts
@@ -1,9 +1,8 @@
 import type { ApolloDriverConfig } from '@nestjs/apollo';
 import { ApolloDriver } from '@nestjs/apollo';
-import { Module, UnauthorizedException } from '@nestjs/common';
+import { Module } from '@nestjs/common';
 import { GraphQLModule } from '@nestjs/graphql';
 
-import { ApolloServerPlugin } from '@apollo/server';
 import { NoUnusedVariablesRule, print } from 'graphql';
 import {
     DateTimeResolver,
@@ -12,7 +11,6 @@ import {
     URLResolver,
     UUIDResolver,
 } from 'graphql-scalars';
-import { AuthZService } from 'nest-authz';
 
 import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js';
 import { loadTypeDefs } from '@app/graphql/schema/loadTypesDefs.js';
@@ -31,8 +29,8 @@ import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
         GraphQLModule.forRootAsync<ApolloDriverConfig>({
             driver: ApolloDriver,
             imports: [PluginModule, AuthModule],
-            inject: [PluginService, AuthZService],
-            useFactory: async (pluginService: PluginService, authZService: AuthZService) => {
+            inject: [PluginService],
+            useFactory: async (pluginService: PluginService) => {
                 const plugins = await pluginService.getGraphQLConfiguration();
                 const authEnumTypeDefs = getAuthEnumTypeDefs();
                 const typeDefs = print(await loadTypeDefs([plugins.typeDefs, authEnumTypeDefs]));
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
new file mode 100644
index 0000000000..6dae1260d5
--- /dev/null
+++ b/api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
@@ -0,0 +1,254 @@
+import { Logger } from '@nestjs/common';
+import { Test, TestingModule } from '@nestjs/testing';
+import { PassThrough, Readable } from 'stream';
+
+import Docker from 'dockerode';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+// Mock chokidar
+vi.mock('chokidar', () => ({
+    watch: vi.fn().mockReturnValue({
+        on: vi.fn().mockReturnThis(),
+    }),
+}));
+
+// Mock @nestjs/common
+vi.mock('@nestjs/common', async () => {
+    const actual = await vi.importActual('@nestjs/common');
+    return {
+        ...actual,
+        Injectable: () => vi.fn(),
+        Logger: vi.fn().mockImplementation(() => ({
+            debug: vi.fn(),
+            error: vi.fn(),
+            log: vi.fn(),
+        })),
+    };
+});
+
+// Mock store getters
+vi.mock('@app/store/index.js', () => ({
+    getters: {
+        paths: vi.fn().mockReturnValue({
+            'var-run': '/var/run',
+            'docker-socket': '/var/run/docker.sock',
+        }),
+    },
+}));
+
+// Mock DockerService
+vi.mock('./docker.service.js', () => ({
+    DockerService: vi.fn().mockImplementation(() => ({
+        getDockerClient: vi.fn(),
+        debouncedContainerCacheUpdate: vi.fn(),
+    })),
+}));
+
+describe('DockerEventService', () => {
+    let service: DockerEventService;
+    let dockerService: DockerService;
+    let mockDockerClient: Docker;
+    let mockEventStream: PassThrough;
+    let mockLogger: Logger;
+    let module: TestingModule;
+
+    beforeEach(async () => {
+        // Create a mock Docker client
+        mockDockerClient = {
+            getEvents: vi.fn(),
+        } as unknown as Docker;
+
+        // Create a mock Docker service *instance*
+        const mockDockerServiceImpl = {
+            getDockerClient: vi.fn().mockReturnValue(mockDockerClient),
+            debouncedContainerCacheUpdate: vi.fn(),
+        };
+
+        // Create a mock event stream
+        mockEventStream = new PassThrough();
+
+        // Set up the mock Docker client to return our mock event stream
+        vi.spyOn(mockDockerClient, 'getEvents').mockResolvedValue(
+            mockEventStream as unknown as Readable
+        );
+
+        // Create a mock logger
+        mockLogger = new Logger(DockerEventService.name) as Logger;
+
+        // Use the mock implementation in the testing module
+        module = await Test.createTestingModule({
+            providers: [
+                DockerEventService,
+                {
+                    provide: DockerService,
+                    useValue: mockDockerServiceImpl,
+                },
+            ],
+        }).compile();
+
+        service = module.get<DockerEventService>(DockerEventService);
+        dockerService = module.get<DockerService>(DockerService);
+    });
+
+    afterEach(() => {
+        vi.clearAllMocks();
+        if (service['dockerEventStream']) {
+            service.stopEventStream();
+        }
+        module.close();
+    });
+
+    it('should be defined', () => {
+        expect(service).toBeDefined();
+    });
+
+    const waitForEventProcessing = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms));
+
+    it('should process Docker events correctly', async () => {
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const event = {
+            Type: 'container',
+            Action: 'start',
+            id: '123',
+            from: 'test-image',
+            time: Date.now(),
+            timeNano: Date.now() * 1000000,
+        };
+
+        mockEventStream.write(JSON.stringify(event) + '\n');
+
+        await waitForEventProcessing();
+
+        expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalled();
+    });
+
+    it('should ignore non-watched actions', async () => {
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const event = {
+            Type: 'container',
+            Action: 'unknown',
+            id: '123',
+            from: 'test-image',
+            time: Date.now(),
+            timeNano: Date.now() * 1000000,
+        };
+
+        mockEventStream.write(JSON.stringify(event) + '\n');
+
+        await waitForEventProcessing();
+
+        expect(dockerService.debouncedContainerCacheUpdate).not.toHaveBeenCalled();
+    });
+
+    it('should handle malformed JSON gracefully', async () => {
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const malformedJson = '{malformed json}\n';
+        mockEventStream.write(malformedJson);
+
+        const validEvent = { Type: 'container', Action: 'start', id: '456' };
+        mockEventStream.write(JSON.stringify(validEvent) + '\n');
+
+        await waitForEventProcessing();
+
+        expect(service.isActive()).toBe(true);
+        expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(1);
+    });
+
+    it('should handle multiple JSON bodies in a single chunk', async () => {
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const events = [
+            { Type: 'container', Action: 'start', id: '123', from: 'test-image-1' },
+            { Type: 'container', Action: 'stop', id: '456', from: 'test-image-2' },
+        ];
+
+        mockEventStream.write(events.map((event) => JSON.stringify(event)).join('\n') + '\n');
+
+        await waitForEventProcessing();
+
+        expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(2);
+    });
+
+    it('should handle mixed valid and invalid JSON in a single chunk', async () => {
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const validEvent = { Type: 'container', Action: 'start', id: '123', from: 'test-image' };
+        const invalidJson = '{malformed json}';
+
+        mockEventStream.write(JSON.stringify(validEvent) + '\n' + invalidJson + '\n');
+
+        await waitForEventProcessing();
+
+        expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(1);
+
+        expect(service.isActive()).toBe(true);
+    });
+
+    it('should handle empty lines in a chunk', async () => {
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const event = { Type: 'container', Action: 'start', id: '123', from: 'test-image' };
+
+        mockEventStream.write('\n\n' + JSON.stringify(event) + '\n\n');
+
+        await waitForEventProcessing();
+
+        expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(1);
+
+        expect(service.isActive()).toBe(true);
+    });
+
+    it('should handle stream errors gracefully', async () => {
+        const stopSpy = vi.spyOn(service, 'stopEventStream');
+
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true);
+
+        const testError = new Error('Stream error');
+        mockEventStream.emit('error', testError);
+
+        await waitForEventProcessing();
+
+        expect(service.isActive()).toBe(false);
+        expect(stopSpy).toHaveBeenCalled();
+    });
+
+    it('should clean up resources when stopped', async () => {
+        // Start the event stream
+        await service['setupDockerWatch']();
+        expect(service.isActive()).toBe(true); // Ensure it started
+
+        // Check if the stream exists before spying
+        const stream = service['dockerEventStream'];
+        let removeListenersSpy: any, destroySpy: any;
+        if (stream) {
+            removeListenersSpy = vi.spyOn(stream, 'removeAllListeners');
+            destroySpy = vi.spyOn(stream, 'destroy');
+        }
+
+        // Stop the event stream
+        service.stopEventStream();
+
+        // Verify that the service has stopped
+        expect(service.isActive()).toBe(false);
+        // Verify stream methods were called if the stream existed
+        if (removeListenersSpy) {
+            expect(removeListenersSpy).toHaveBeenCalled();
+        }
+        if (destroySpy) {
+            expect(destroySpy).toHaveBeenCalled();
+        }
+    });
+});
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
new file mode 100644
index 0000000000..b2dcb45abd
--- /dev/null
+++ b/api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
@@ -0,0 +1,198 @@
+import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+import { Readable } from 'stream';
+
+import { watch } from 'chokidar';
+import Docker from 'dockerode';
+
+import { getters } from '@app/store/index.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+enum DockerEventAction {
+    DIE = 'die',
+    KILL = 'kill',
+    OOM = 'oom',
+    PAUSE = 'pause',
+    RESTART = 'restart',
+    START = 'start',
+    STOP = 'stop',
+    UNPAUSE = 'unpause',
+    EXEC_CREATE = 'exec_create',
+    EXEC_START = 'exec_start',
+    EXEC_DIE = 'exec_die',
+}
+
+enum DockerEventType {
+    CONTAINER = 'container',
+}
+
+interface DockerEvent {
+    Action?: string;
+    status?: string;
+    from?: string;
+    Type?: string;
+    [key: string]: unknown;
+}
+
+@Injectable()
+export class DockerEventService implements OnModuleDestroy, OnModuleInit {
+    private client: Docker;
+    private dockerEventStream: Readable | null = null;
+    private readonly logger = new Logger(DockerEventService.name);
+
+    private watchedActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+        DockerEventAction.EXEC_CREATE,
+        DockerEventAction.EXEC_START,
+        DockerEventAction.EXEC_DIE,
+    ];
+
+    private containerActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+    ];
+
+    constructor(private readonly dockerService: DockerService) {
+        this.client = this.dockerService.getDockerClient();
+    }
+
+    async onModuleInit() {
+        this.setupVarRunWatch();
+    }
+
+    onModuleDestroy() {
+        this.stopEventStream();
+    }
+
+    private setupVarRunWatch() {
+        const paths = getters.paths();
+        watch(paths['var-run'], { ignoreInitial: false })
+            .on('add', async (path) => {
+                if (path === paths['docker-socket']) {
+                    this.logger.debug('Starting docker event watch');
+                    await this.setupDockerWatch();
+                }
+            })
+            .on('unlink', (path) => {
+                if (path === paths['docker-socket']) {
+                    this.stopEventStream();
+                }
+            });
+    }
+
+    /**
+     * Stop the Docker event stream
+     */
+    public stopEventStream(): void {
+        if (this.dockerEventStream) {
+            this.logger.debug('Stopping docker event stream');
+            this.dockerEventStream.removeAllListeners();
+            this.dockerEventStream.destroy();
+            this.dockerEventStream = null;
+        }
+    }
+
+    private async handleDockerEvent(event: unknown): Promise<void> {
+        if (typeof event !== 'object' || event === null) {
+            this.logger.error('Received non-object event', event);
+            return;
+        }
+
+        // Type assertion to DockerEvent
+        const dockerEvent = event as DockerEvent;
+
+        // Check if this is an action we're watching
+        const actionName = dockerEvent.Action || dockerEvent.status;
+        const shouldProcess = this.watchedActions.some(
+            (action) => typeof actionName === 'string' && actionName.startsWith(action)
+        );
+
+        if (shouldProcess) {
+            this.logger.debug(`[${dockerEvent.from}] ${dockerEvent.Type}->${actionName}`);
+
+            // For container lifecycle events, update the container cache
+            if (
+                dockerEvent.Type === DockerEventType.CONTAINER &&
+                typeof actionName === 'string' &&
+                this.containerActions.includes(actionName as DockerEventAction)
+            ) {
+                await this.dockerService.debouncedContainerCacheUpdate();
+            }
+        }
+    }
+
+    private async setupDockerWatch(): Promise<void> {
+        this.logger.debug('Setting up Docker event stream');
+
+        try {
+            const eventStream = await this.client.getEvents();
+            this.dockerEventStream = eventStream as unknown as Readable;
+
+            if (this.dockerEventStream) {
+                // Add error handlers to raw stream to prevent uncaught errors
+                this.dockerEventStream.on('error', (error) => {
+                    this.logger.error('Docker event stream error', error);
+                    this.stopEventStream();
+                });
+
+                this.dockerEventStream.on('end', () => {
+                    this.logger.debug('Docker event stream closed');
+                    this.stopEventStream();
+                });
+
+                // Set up data handler for line-by-line JSON parsing
+                this.dockerEventStream.on('data', async (chunk) => {
+                    try {
+                        // Split the chunk by newlines to handle multiple JSON bodies
+                        const jsonStrings = chunk
+                            .toString()
+                            .split('\n')
+                            .filter((line) => line.trim() !== '');
+
+                        for (const jsonString of jsonStrings) {
+                            try {
+                                const event = JSON.parse(jsonString);
+                                await this.handleDockerEvent(event);
+                            } catch (parseError) {
+                                this.logger
+                                    .error(`Failed to parse individual Docker event: ${parseError instanceof Error ? parseError.message : String(parseError)}
+                                Event data: ${jsonString}`);
+                            }
+                        }
+                    } catch (error) {
+                        this.logger.error(
+                            `Failed to process Docker event chunk: ${error instanceof Error ? error.message : String(error)}`
+                        );
+                        this.logger.verbose(`Full chunk: ${chunk.toString()}`);
+                    }
+                });
+
+                this.logger.debug('Docker event stream active');
+            }
+        } catch (error) {
+            this.logger.error(
+                `Failed to set up Docker event stream - ${error instanceof Error ? error.message : String(error)}`
+            );
+        }
+    }
+
+    /**
+     * Check if the Docker event service is currently running
+     * @returns True if the event stream is active
+     */
+    public isActive(): boolean {
+        return this.dockerEventStream !== null;
+    }
+}
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
new file mode 100644
index 0000000000..db5264e013
--- /dev/null
+++ b/api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts
@@ -0,0 +1,70 @@
+import { Test, TestingModule } from '@nestjs/testing';
+
+import { describe, expect, it, vi } from 'vitest';
+
+import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
+import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js';
+import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
+import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+describe('DockerModule', () => {
+    it('should compile the module', async () => {
+        const module = await Test.createTestingModule({
+            imports: [DockerModule],
+        })
+            .overrideProvider(DockerService)
+            .useValue({ getDockerClient: vi.fn() })
+            .compile();
+
+        expect(module).toBeDefined();
+    });
+
+    it('should provide DockerService', async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [
+                {
+                    provide: DockerService,
+                    useValue: {
+                        getDockerClient: vi.fn(),
+                        debouncedContainerCacheUpdate: vi.fn(),
+                    },
+                },
+            ],
+        }).compile();
+
+        const service = module.get<DockerService>(DockerService);
+        expect(service).toBeDefined();
+        expect(service).toHaveProperty('getDockerClient');
+    });
+
+    it('should provide DockerEventService', async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [
+                DockerEventService,
+                { provide: DockerService, useValue: { getDockerClient: vi.fn() } },
+            ],
+        }).compile();
+
+        const service = module.get<DockerEventService>(DockerEventService);
+        expect(service).toBeInstanceOf(DockerEventService);
+    });
+
+    it('should provide DockerResolver', async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [DockerResolver, { provide: DockerService, useValue: {} }],
+        }).compile();
+
+        const resolver = module.get<DockerResolver>(DockerResolver);
+        expect(resolver).toBeInstanceOf(DockerResolver);
+    });
+
+    it('should provide DockerMutationsResolver', async () => {
+        const module: TestingModule = await Test.createTestingModule({
+            providers: [DockerMutationsResolver, { provide: DockerService, useValue: {} }],
+        }).compile();
+
+        const resolver = module.get<DockerMutationsResolver>(DockerMutationsResolver);
+        expect(resolver).toBeInstanceOf(DockerMutationsResolver);
+    });
+});
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.module.ts b/api/src/unraid-api/graph/resolvers/docker/docker.module.ts
new file mode 100644
index 0000000000..28528fe298
--- /dev/null
+++ b/api/src/unraid-api/graph/resolvers/docker/docker.module.ts
@@ -0,0 +1,20 @@
+import { Module } from '@nestjs/common';
+
+import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
+import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
+import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+@Module({
+    providers: [
+        // Services
+        DockerService,
+        DockerEventService,
+
+        // Resolvers
+        DockerResolver,
+        DockerMutationsResolver,
+    ],
+    exports: [DockerService, DockerEventService],
+})
+export class DockerModule {}
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
index 1d380692e6..26d09c5115 100644
--- a/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
+++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
@@ -4,27 +4,9 @@ import { Test } from '@nestjs/testing';
 import Docker from 'dockerode';
 import { beforeEach, describe, expect, it, vi } from 'vitest';
 
-import type { DockerContainer } from '@app/graphql/generated/api/types.js';
 import { ContainerState } from '@app/graphql/generated/api/types.js';
 import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
 
-// Mock chokidar
-vi.mock('chokidar', () => ({
-    watch: vi.fn().mockReturnValue({
-        on: vi.fn().mockReturnValue({
-            on: vi.fn(),
-        }),
-    }),
-}));
-
-// Mock docker-event-emitter
-vi.mock('docker-event-emitter', () => ({
-    default: vi.fn().mockReturnValue({
-        on: vi.fn(),
-        start: vi.fn().mockResolvedValue(undefined),
-    }),
-}));
-
 // Mock pubsub
 vi.mock('@app/core/pubsub.js', () => ({
     pubsub: {
@@ -522,225 +504,4 @@ describe('DockerService', () => {
             });
         });
     });
-
-    describe('watchers', () => {
-        it('should setup docker watcher when docker socket is added', async () => {
-            // Mock the setupDockerWatch method
-            const setupDockerWatchSpy = vi.spyOn(service as any, 'setupDockerWatch');
-            setupDockerWatchSpy.mockResolvedValue({} as any);
-
-            // Get the watch function from chokidar
-            const { watch } = await import('chokidar');
-
-            // Mock the on method to simulate the add event
-            const mockOn = vi.fn().mockImplementation((event, callback) => {
-                if (event === 'add') {
-                    // Simulate the add event with the docker socket path
-                    callback('/var/run/docker.sock');
-                }
-                return { on: vi.fn() };
-            });
-
-            // Replace the watch function's on method
-            (watch as any).mockReturnValue({
-                on: mockOn,
-            });
-
-            // Call the setupVarRunWatch method
-            await (service as any).setupVarRunWatch();
-
-            // Verify that setupDockerWatch was called
-            expect(setupDockerWatchSpy).toHaveBeenCalled();
-        });
-
-        it('should stop docker watcher when docker socket is removed', async () => {
-            // Get the watch function from chokidar
-            const { watch } = await import('chokidar');
-
-            // Create a mock stop function
-            const mockStop = vi.fn();
-
-            // Set up the dockerWatcher before calling setupVarRunWatch
-            (service as any).dockerWatcher = { stop: mockStop };
-
-            // Mock the on method to simulate the unlink event
-            let unlinkCallback: (path: string) => void = () => {};
-            const mockOn = vi.fn().mockImplementation((event, callback) => {
-                if (event === 'unlink') {
-                    unlinkCallback = callback;
-                }
-                return { on: mockOn };
-            });
-
-            // Replace the watch function's on method
-            (watch as any).mockReturnValue({
-                on: mockOn,
-            });
-
-            // Call the setupVarRunWatch method
-            (service as any).setupVarRunWatch();
-
-            // Verify that the on method was called with 'unlink'
-            expect(mockOn).toHaveBeenCalledWith('unlink', expect.any(Function));
-            expect(unlinkCallback).toBeDefined();
-
-            // Trigger the unlink event
-            unlinkCallback('/var/run/docker.sock');
-
-            // Verify that the stop method was called
-            expect(mockStop).toHaveBeenCalled();
-            expect((service as any).dockerWatcher).toBeNull();
-            expect((service as any).containerCache).toEqual([]);
-        });
-
-        it('should setup docker watch correctly', async () => {
-            // Get the DockerEE import
-            const DockerEE = (await import('docker-event-emitter')).default;
-
-            // Mock the debouncedContainerCacheUpdate method
-            const debouncedContainerCacheUpdateSpy = vi.spyOn(
-                service as any,
-                'debouncedContainerCacheUpdate'
-            );
-            debouncedContainerCacheUpdateSpy.mockResolvedValue(undefined);
-
-            // Call the setupDockerWatch method
-            const result = await (service as any).setupDockerWatch();
-
-            // Verify that DockerEE was instantiated with the client
-            expect(DockerEE).toHaveBeenCalledWith(mockDockerInstance);
-
-            // Verify that the on method was called with the correct arguments
-            const dockerEEInstance = DockerEE();
-            expect(dockerEEInstance.on).toHaveBeenCalledWith('container', expect.any(Function));
-
-            // Verify that the start method was called
-            expect(dockerEEInstance.start).toHaveBeenCalled();
-
-            // Verify that debouncedContainerCacheUpdate was called
-            expect(debouncedContainerCacheUpdateSpy).toHaveBeenCalled();
-
-            // Verify that the result is the DockerEE instance
-            expect(result).toBe(dockerEEInstance);
-        });
-
-        it('should call debouncedContainerCacheUpdate when container event is received', async () => {
-            // Get the DockerEE import
-            const DockerEE = (await import('docker-event-emitter')).default;
-
-            // Mock the on method to capture the callback
-            const mockOnCallback = vi.fn();
-            const mockOn = vi.fn().mockImplementation((event, callback) => {
-                if (event === 'container') {
-                    mockOnCallback(callback);
-                }
-                return { on: vi.fn() };
-            });
-
-            // Replace the DockerEE constructor's on method
-            (DockerEE as any).mockReturnValue({
-                on: mockOn,
-                start: vi.fn().mockResolvedValue(undefined),
-            });
-
-            // Mock the debouncedContainerCacheUpdate method
-            const debouncedContainerCacheUpdateSpy = vi.spyOn(
-                service as any,
-                'debouncedContainerCacheUpdate'
-            );
-            debouncedContainerCacheUpdateSpy.mockResolvedValue(undefined);
-
-            // Call the setupDockerWatch method
-            await (service as any).setupDockerWatch();
-
-            // Get the callback function that was passed to the on method
-            const containerEventCallback = mockOnCallback.mock.calls[0][0];
-
-            // Call the callback with a container event
-            await containerEventCallback({
-                Type: 'container',
-                Action: 'start',
-                from: 'test-container',
-            });
-
-            // Verify that debouncedContainerCacheUpdate was called
-            expect(debouncedContainerCacheUpdateSpy).toHaveBeenCalled();
-        });
-
-        it('should not call debouncedContainerCacheUpdate for non-watched container events', async () => {
-            // Get the DockerEE import
-            const DockerEE = (await import('docker-event-emitter')).default;
-
-            // Mock the debouncedContainerCacheUpdate method
-            const debouncedContainerCacheUpdateSpy = vi.spyOn(
-                service as any,
-                'debouncedContainerCacheUpdate'
-            );
-            debouncedContainerCacheUpdateSpy.mockResolvedValue(undefined);
-
-            // Create a mock on function that captures the callback
-            let containerCallback: (data: {
-                Type: string;
-                Action: string;
-                from: string;
-            }) => void = () => {};
-            const mockOn = vi.fn().mockImplementation((event, callback) => {
-                if (event === 'container') {
-                    containerCallback = callback;
-                }
-                return { on: vi.fn() };
-            });
-
-            // Replace the DockerEE constructor's on method
-            (DockerEE as any).mockReturnValue({
-                on: mockOn,
-                start: vi.fn().mockResolvedValue(undefined),
-            });
-
-            // Call the setupDockerWatch method
-            await (service as any).setupDockerWatch();
-
-            // Reset the spy after setup
-            debouncedContainerCacheUpdateSpy.mockReset();
-
-            // Call the callback with a non-watched container event
-            await containerCallback({
-                Type: 'container',
-                Action: 'create',
-                from: 'test-container',
-            });
-
-            // Verify that debouncedContainerCacheUpdate was not called
-            expect(debouncedContainerCacheUpdateSpy).not.toHaveBeenCalled();
-        });
-
-        it('should call getContainers and publish appUpdateEvent in debouncedContainerCacheUpdate', async () => {
-            // Mock the client's listContainers method
-            const mockListContainers = vi.fn().mockResolvedValue([]);
-            (service as any).client = {
-                listContainers: mockListContainers,
-            };
-
-            // Mock the getContainers method
-            const getContainersSpy = vi.spyOn(service, 'getContainers');
-
-            // Get the pubsub import
-            const { pubsub, PUBSUB_CHANNEL } = await import('@app/core/pubsub.js');
-
-            // Call the debouncedContainerCacheUpdate method directly and wait for the debounce
-            service['debouncedContainerCacheUpdate']();
-            // Force the debounced function to execute immediately
-            await new Promise((resolve) => setTimeout(resolve, 600));
-
-            // Verify that getContainers was called with useCache: false
-            expect(getContainersSpy).toHaveBeenCalledWith({ useCache: false });
-
-            // Verify that pubsub.publish was called with the correct arguments
-            expect(pubsub.publish).toHaveBeenCalledWith('info', {
-                info: {
-                    apps: { installed: 0, running: 0 },
-                },
-            });
-        });
-    });
 });
diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
index fcefa0e828..9c9838ab16 100644
--- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
+++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts
@@ -2,13 +2,10 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
 import { readFile } from 'fs/promises';
 
 import camelCaseKeys from 'camelcase-keys';
-import { watch } from 'chokidar';
-import DockerEE from 'docker-event-emitter';
 import Docker from 'dockerode';
 import { debounce } from 'lodash-es';
 
 import type { ContainerPort, DockerContainer, DockerNetwork } from '@app/graphql/generated/api/types.js';
-import { dockerLogger } from '@app/core/log.js';
 import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
 import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
 import { sleep } from '@app/core/utils/misc/sleep.js';
@@ -28,38 +25,18 @@ export class DockerService implements OnModuleInit {
     private client: Docker;
     private containerCache: Array<DockerContainer> = [];
     private autoStarts: string[] = [];
-    private dockerWatcher: null | typeof DockerEE = null;
     private readonly logger = new Logger(DockerService.name);
 
     constructor() {
         this.client = this.getDockerClient();
     }
 
-    private getDockerClient() {
+    public getDockerClient() {
         return new Docker({
             socketPath: '/var/run/docker.sock',
         });
     }
 
-    private setupVarRunWatch() {
-        const paths = getters.paths();
-        watch(paths['var-run'], { ignoreInitial: false })
-            .on('add', async (path) => {
-                if (path === paths['docker-socket']) {
-                    dockerLogger.debug('Starting docker watch');
-                    this.dockerWatcher = await this.setupDockerWatch();
-                }
-            })
-            .on('unlink', (path) => {
-                if (path === paths['docker-socket'] && this.dockerWatcher) {
-                    dockerLogger.debug('Stopping docker watch');
-                    this.dockerWatcher?.stop?.();
-                    this.dockerWatcher = null;
-                    this.containerCache = [];
-                }
-            });
-    }
-
     get installed() {
         return this.containerCache.length;
     }
@@ -77,51 +54,24 @@ export class DockerService implements OnModuleInit {
         };
     }
 
-    private async setupDockerWatch(): Promise<DockerEE> {
-        // Only watch container events equal to start/stop
-        const watchedActions = ['die', 'kill', 'oom', 'pause', 'restart', 'start', 'stop', 'unpause'];
-
-        // Create docker event emitter instance
-        dockerLogger.debug('Creating docker event emitter instance');
-
-        const dee = new DockerEE(this.client);
-        // On Docker event update info with { apps: { installed, started } }
-        dee.on(
-            'container',
-            async (data: { Type: 'container'; Action: 'start' | 'stop'; from: string }) => {
-                // Only listen to container events
-                if (!watchedActions.includes(data.Action)) {
-                    return;
-                }
-                dockerLogger.debug(`[${data.from}] ${data.Type}->${data.Action}`);
-                await this.debouncedContainerCacheUpdate();
-            }
-        );
-        // Get docker container count on first start
-        await this.debouncedContainerCacheUpdate();
-        await dee.start();
-        dockerLogger.debug('Binding to docker events');
-        return dee;
-    }
-
     public async onModuleInit() {
-        this.setupVarRunWatch();
+        await this.debouncedContainerCacheUpdate();
     }
 
+    /**
+     * Docker auto start file
+     *
+     * @note Doesn't exist if array is offline.
+     * @see https://github.com/limetech/webgui/issues/502#issue-480992547
+     */
     public async getAutoStarts(): Promise<string[]> {
-        /**
-         * Docker auto start file
-         *
-         * @note Doesn't exist if array is offline.
-         * @see https://github.com/limetech/webgui/issues/502#issue-480992547
-         */
         const autoStartFile = await readFile(getters.paths()['docker-autostart'], 'utf8')
             .then((file) => file.toString())
             .catch(() => '');
         return autoStartFile.split('\n');
     }
 
-    private debouncedContainerCacheUpdate = debounce(async () => {
+    public debouncedContainerCacheUpdate = debounce(async () => {
         await this.getContainers({ useCache: false });
         await pubsub.publish(PUBSUB_CHANNEL.INFO, this.appUpdateEvent);
     }, 500);
diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts
index 6674a1332a..eed57c0a2c 100644
--- a/api/src/unraid-api/graph/resolvers/resolvers.module.ts
+++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts
@@ -13,9 +13,7 @@ import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resol
 import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js';
 import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js';
 import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js';
-import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
-import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
-import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js';
 import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resolver.js';
 import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js';
 import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
@@ -36,7 +34,7 @@ import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolv
 import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
 
 @Module({
-    imports: [AuthModule],
+    imports: [AuthModule, DockerModule],
     providers: [
         ApiKeyResolver,
         ArrayMutationsResolver,
@@ -49,9 +47,6 @@ import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js'
         ConnectSettingsService,
         DisksResolver,
         DisplayResolver,
-        DockerMutationsResolver,
-        DockerResolver,
-        DockerService,
         FlashResolver,
         InfoResolver,
         LogsResolver,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 095feccc21..7259cfdc2e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -137,12 +137,9 @@ importers:
       diff:
         specifier: ^7.0.0
         version: 7.0.0
-      docker-event-emitter:
-        specifier: ^0.3.0
-        version: 0.3.0(dockerode@3.3.5)
       dockerode:
-        specifier: ^3.3.5
-        version: 3.3.5
+        specifier: ^4.0.5
+        version: 4.0.5
       dotenv:
         specifier: ^16.4.5
         version: 16.4.7
@@ -2298,6 +2295,15 @@ packages:
     peerDependencies:
       graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
 
+  '@grpc/grpc-js@1.13.2':
+    resolution: {integrity: sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==}
+    engines: {node: '>=12.10.0'}
+
+  '@grpc/proto-loader@0.7.13':
+    resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==}
+    engines: {node: '>=6'}
+    hasBin: true
+
   '@headlessui/vue@1.7.23':
     resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==}
     engines: {node: '>=10'}
@@ -2400,6 +2406,9 @@ packages:
   '@jridgewell/trace-mapping@0.3.25':
     resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
 
+  '@js-sdsl/ordered-map@4.4.2':
+    resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
+
   '@jsdevtools/ono@7.1.3':
     resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
 
@@ -5865,17 +5874,12 @@ packages:
   dlv@1.1.3:
     resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
 
-  docker-event-emitter@0.3.0:
-    resolution: {integrity: sha512-QWpJsTOcLOiOctbCTH3T+w34Aw+zK6JzTh8xOqD/5/dDEhPhnCFmR8VzsCvTYAlTmkgxMUkRMTlBz1sGNZB5vg==}
-    peerDependencies:
-      dockerode: ^3.0.2
-
-  docker-modem@3.0.8:
-    resolution: {integrity: sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==}
+  docker-modem@5.0.6:
+    resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==}
     engines: {node: '>= 8.0'}
 
-  dockerode@3.3.5:
-    resolution: {integrity: sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==}
+  dockerode@4.0.5:
+    resolution: {integrity: sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA==}
     engines: {node: '>= 8.0'}
 
   doctrine@2.1.0:
@@ -7993,6 +7997,9 @@ packages:
   lodash-es@4.17.21:
     resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
 
+  lodash.camelcase@4.3.0:
+    resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+
   lodash.castarray@4.4.0:
     resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
 
@@ -8066,6 +8073,9 @@ packages:
   long@4.0.0:
     resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
 
+  long@5.3.1:
+    resolution: {integrity: sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==}
+
   longest@2.0.1:
     resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
     engines: {node: '>=0.10.0'}
@@ -9448,6 +9458,10 @@ packages:
   proto-list@1.2.4:
     resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
 
+  protobufjs@7.4.0:
+    resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==}
+    engines: {node: '>=12.0.0'}
+
   protocols@2.0.2:
     resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==}
 
@@ -9516,6 +9530,7 @@ packages:
     engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
     deprecated: |-
       You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
+
       (For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
 
   qs@6.13.0:
@@ -10485,8 +10500,8 @@ packages:
     resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
     engines: {node: '>=6'}
 
-  tar-fs@2.0.1:
-    resolution: {integrity: sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==}
+  tar-fs@2.1.2:
+    resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==}
 
   tar-stream@2.2.0:
     resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
@@ -11046,6 +11061,10 @@ packages:
     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
     engines: {node: '>= 0.4.0'}
 
+  uuid@10.0.0:
+    resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+    hasBin: true
+
   uuid@11.1.0:
     resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
     hasBin: true
@@ -11988,7 +12007,7 @@ snapshots:
       '@babel/traverse': 7.26.10
       '@babel/types': 7.26.10
       convert-source-map: 2.0.0
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -12359,7 +12378,7 @@ snapshots:
       '@babel/parser': 7.27.0
       '@babel/template': 7.26.9
       '@babel/types': 7.27.0
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -12371,7 +12390,7 @@ snapshots:
       '@babel/parser': 7.26.8
       '@babel/template': 7.26.8
       '@babel/types': 7.26.8
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -12689,7 +12708,7 @@ snapshots:
   '@eslint/config-array@0.19.2':
     dependencies:
       '@eslint/object-schema': 2.1.6
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -12726,7 +12745,7 @@ snapshots:
   '@eslint/eslintrc@3.3.1':
     dependencies:
       ajv: 6.12.6
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       espree: 10.3.0
       globals: 14.0.0
       ignore: 5.3.2
@@ -13325,12 +13344,12 @@ snapshots:
       '@types/js-yaml': 4.0.9
       '@whatwg-node/fetch': 0.10.3
       chalk: 4.1.2
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       dotenv: 16.4.7
       graphql: 16.10.0
       graphql-request: 6.1.0(graphql@16.10.0)
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@9.4.0)
+      https-proxy-agent: 7.0.6
       jose: 5.10.0
       js-yaml: 4.1.0
       lodash: 4.17.21
@@ -13442,6 +13461,18 @@ snapshots:
     dependencies:
       graphql: 16.10.0
 
+  '@grpc/grpc-js@1.13.2':
+    dependencies:
+      '@grpc/proto-loader': 0.7.13
+      '@js-sdsl/ordered-map': 4.4.2
+
+  '@grpc/proto-loader@0.7.13':
+    dependencies:
+      lodash.camelcase: 4.3.0
+      long: 5.3.1
+      protobufjs: 7.4.0
+      yargs: 17.7.2
+
   '@headlessui/vue@1.7.23(vue@3.5.13(typescript@5.8.2))':
     dependencies:
       '@tanstack/vue-virtual': 3.13.0(vue@3.5.13(typescript@5.8.2))
@@ -13540,6 +13571,8 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  '@js-sdsl/ordered-map@4.4.2': {}
+
   '@jsdevtools/ono@7.1.3': {}
 
   '@jsonforms/core@3.5.1':
@@ -13626,7 +13659,7 @@ snapshots:
     dependencies:
       consola: 3.4.2
       detect-libc: 2.0.3
-      https-proxy-agent: 7.0.6(supports-color@9.4.0)
+      https-proxy-agent: 7.0.6
       node-fetch: 2.7.0
       nopt: 8.1.0
       semver: 7.7.1
@@ -14408,7 +14441,7 @@ snapshots:
 
   '@pm2/pm2-version-check@1.0.4':
     dependencies:
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -15058,7 +15091,7 @@ snapshots:
   '@types/body-parser@1.19.5':
     dependencies:
       '@types/connect': 3.4.38
-      '@types/node': 22.13.13
+      '@types/node': 22.14.0
 
   '@types/bytes@3.1.5': {}
 
@@ -15068,11 +15101,11 @@ snapshots:
 
   '@types/connect@3.4.38':
     dependencies:
-      '@types/node': 22.13.13
+      '@types/node': 22.14.0
 
   '@types/conventional-commits-parser@5.0.1':
     dependencies:
-      '@types/node': 22.13.13
+      '@types/node': 22.14.0
     optional: true
 
   '@types/cors@2.8.17':
@@ -15219,14 +15252,14 @@ snapshots:
   '@types/send@0.17.4':
     dependencies:
       '@types/mime': 1.3.5
-      '@types/node': 22.13.13
+      '@types/node': 22.14.0
 
   '@types/sendmail@1.4.7': {}
 
   '@types/serve-static@1.15.7':
     dependencies:
       '@types/http-errors': 2.0.4
-      '@types/node': 22.13.13
+      '@types/node': 22.14.0
       '@types/send': 0.17.4
 
   '@types/ssh2@1.15.4':
@@ -15309,7 +15342,7 @@ snapshots:
       '@typescript-eslint/types': 8.28.0
       '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
       '@typescript-eslint/visitor-keys': 8.28.0
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       eslint: 9.23.0(jiti@2.4.2)
       typescript: 5.8.2
     transitivePeerDependencies:
@@ -15329,7 +15362,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
       '@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       eslint: 9.23.0(jiti@2.4.2)
       ts-api-utils: 2.1.0(typescript@5.8.2)
       typescript: 5.8.2
@@ -15357,7 +15390,7 @@ snapshots:
     dependencies:
       '@typescript-eslint/types': 8.28.0
       '@typescript-eslint/visitor-keys': 8.28.0
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       fast-glob: 3.3.3
       is-glob: 4.0.3
       minimatch: 9.0.5
@@ -15529,7 +15562,7 @@ snapshots:
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 1.0.2
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.6
@@ -17626,27 +17659,24 @@ snapshots:
 
   dlv@1.1.3: {}
 
-  docker-event-emitter@0.3.0(dockerode@3.3.5):
+  docker-modem@5.0.6:
     dependencies:
-      debug: 4.4.0(supports-color@9.4.0)
-      dockerode: 3.3.5
-    transitivePeerDependencies:
-      - supports-color
-
-  docker-modem@3.0.8:
-    dependencies:
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       readable-stream: 3.6.2
       split-ca: 1.0.1
       ssh2: 1.16.0
     transitivePeerDependencies:
       - supports-color
 
-  dockerode@3.3.5:
+  dockerode@4.0.5:
     dependencies:
       '@balena/dockerignore': 1.0.2
-      docker-modem: 3.0.8
-      tar-fs: 2.0.1
+      '@grpc/grpc-js': 1.13.2
+      '@grpc/proto-loader': 0.7.13
+      docker-modem: 5.0.6
+      protobufjs: 7.4.0
+      tar-fs: 2.1.2
+      uuid: 10.0.0
     transitivePeerDependencies:
       - supports-color
 
@@ -18283,7 +18313,7 @@ snapshots:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.6
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       escape-string-regexp: 4.0.0
       eslint-scope: 8.3.0
       eslint-visitor-keys: 4.2.0
@@ -18827,7 +18857,7 @@ snapshots:
     dependencies:
       basic-ftp: 5.0.5
       data-uri-to-buffer: 6.0.2
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19294,7 +19324,7 @@ snapshots:
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.3
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -19338,6 +19368,13 @@ snapshots:
       quick-lru: 5.1.1
       resolve-alpn: 1.2.1
 
+  https-proxy-agent@7.0.6:
+    dependencies:
+      agent-base: 7.1.3
+      debug: 4.4.0(supports-color@5.5.0)
+    transitivePeerDependencies:
+      - supports-color
+
   https-proxy-agent@7.0.6(supports-color@9.4.0):
     dependencies:
       agent-base: 7.1.3
@@ -19769,7 +19806,7 @@ snapshots:
   istanbul-lib-source-maps@5.0.6:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.25
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       istanbul-lib-coverage: 3.2.2
     transitivePeerDependencies:
       - supports-color
@@ -19856,7 +19893,7 @@ snapshots:
       form-data: 4.0.2
       html-encoding-sniffer: 4.0.0
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@9.4.0)
+      https-proxy-agent: 7.0.6
       is-potential-custom-element-name: 1.0.1
       nwsapi: 2.2.16
       parse5: 7.2.1
@@ -20126,6 +20163,8 @@ snapshots:
 
   lodash-es@4.17.21: {}
 
+  lodash.camelcase@4.3.0: {}
+
   lodash.castarray@4.4.0: {}
 
   lodash.defaults@4.2.0: {}
@@ -20182,6 +20221,8 @@ snapshots:
 
   long@4.0.0: {}
 
+  long@5.3.1: {}
+
   longest@2.0.1: {}
 
   loose-envify@1.4.0:
@@ -21106,10 +21147,10 @@ snapshots:
     dependencies:
       '@tootallnate/quickjs-emscripten': 0.23.0
       agent-base: 7.1.3
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       get-uri: 6.0.4
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@9.4.0)
+      https-proxy-agent: 7.0.6
       pac-resolver: 7.0.1
       socks-proxy-agent: 8.0.5
     transitivePeerDependencies:
@@ -21373,7 +21414,7 @@ snapshots:
 
   pm2-axon-rpc@0.7.1:
     dependencies:
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -21381,7 +21422,7 @@ snapshots:
     dependencies:
       amp: 0.3.1
       amp-message: 0.1.2
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       escape-string-regexp: 4.0.0
     transitivePeerDependencies:
       - supports-color
@@ -21398,7 +21439,7 @@ snapshots:
   pm2-sysmonit@1.2.8:
     dependencies:
       async: 3.2.6
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       pidusage: 2.0.21
       systeminformation: 5.25.11
       tx2: 1.0.5
@@ -21420,7 +21461,7 @@ snapshots:
       commander: 2.15.1
       croner: 4.1.97
       dayjs: 1.11.13
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       enquirer: 2.3.6
       eventemitter2: 5.0.1
       fclone: 1.0.11
@@ -21749,6 +21790,21 @@ snapshots:
 
   proto-list@1.2.4: {}
 
+  protobufjs@7.4.0:
+    dependencies:
+      '@protobufjs/aspromise': 1.1.2
+      '@protobufjs/base64': 1.1.2
+      '@protobufjs/codegen': 2.0.4
+      '@protobufjs/eventemitter': 1.1.0
+      '@protobufjs/fetch': 1.1.0
+      '@protobufjs/float': 1.0.2
+      '@protobufjs/inquire': 1.1.0
+      '@protobufjs/path': 1.1.2
+      '@protobufjs/pool': 1.1.0
+      '@protobufjs/utf8': 1.1.0
+      '@types/node': 22.14.0
+      long: 5.3.1
+
   protocols@2.0.2: {}
 
   proxy-addr@2.0.7:
@@ -21759,9 +21815,9 @@ snapshots:
   proxy-agent@6.4.0:
     dependencies:
       agent-base: 7.1.3
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6(supports-color@9.4.0)
+      https-proxy-agent: 7.0.6
       lru-cache: 7.18.3
       pac-proxy-agent: 7.1.0
       proxy-from-env: 1.1.0
@@ -22148,7 +22204,7 @@ snapshots:
 
   require-in-the-middle@5.2.0:
     dependencies:
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       module-details-from-path: 1.0.3
       resolve: 1.22.10
     transitivePeerDependencies:
@@ -22615,7 +22671,7 @@ snapshots:
   socks-proxy-agent@8.0.5:
     dependencies:
       agent-base: 7.1.3
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       socks: 2.8.4
     transitivePeerDependencies:
       - supports-color
@@ -22871,7 +22927,7 @@ snapshots:
   stylus@0.57.0:
     dependencies:
       css: 3.0.0
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       glob: 7.2.3
       safer-buffer: 2.1.2
       sax: 1.2.4
@@ -23022,7 +23078,7 @@ snapshots:
 
   tapable@2.2.1: {}
 
-  tar-fs@2.0.1:
+  tar-fs@2.1.2:
     dependencies:
       chownr: 1.1.4
       mkdirp-classic: 0.5.3
@@ -23560,6 +23616,8 @@ snapshots:
 
   utils-merge@1.0.1: {}
 
+  uuid@10.0.0: {}
+
   uuid@11.1.0: {}
 
   uuid@3.4.0: {}
@@ -23602,7 +23660,7 @@ snapshots:
   vite-node@3.0.9(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0):
     dependencies:
       cac: 6.7.14
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       es-module-lexer: 1.6.0
       pathe: 2.0.3
       vite: 6.2.3(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
@@ -23735,7 +23793,7 @@ snapshots:
     dependencies:
       '@rollup/pluginutils': 4.2.1
       chalk: 4.1.2
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       vite: 6.2.3(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
     optionalDependencies:
       '@swc/core': 1.11.13(@swc/helpers@0.5.15)
@@ -23798,7 +23856,7 @@ snapshots:
 
   vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.3(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)):
     dependencies:
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       globrex: 0.1.2
       tsconfck: 3.1.5(typescript@5.8.2)
     optionalDependencies:
@@ -23873,7 +23931,7 @@ snapshots:
       '@vitest/spy': 3.0.9
       '@vitest/utils': 3.0.9
       chai: 5.2.0
-      debug: 4.4.0(supports-color@9.4.0)
+      debug: 4.4.0(supports-color@5.5.0)
       expect-type: 1.2.0
       magic-string: 0.30.17
       pathe: 2.0.3


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🔭 Outside diff range comments (1)</summary><blockquote>

<details>
<summary>api/package.json (1)</summary><blockquote>

`51-136`: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Confirm docker-event-emitter removal**

According to the PR objectives and AI summary, the docker-event-emitter dependency has been removed. This aligns with the new `DockerEventService` implementation.

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify that all docker-event-emitter usage has been removed or replaced

echo "Searching for any remaining docker-event-emitter references..."
rg "docker-event-emitter" --type ts
```

Length of output: 370

---



</details>

**Action Required: Update Test Mocks for DockerEventEmitter**  
The removal from production code is confirmed in the dependencies list. However, the test file still mocks "docker-event-emitter":  
- **File:** `api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts`  
  - Remove or update the mock to reflect the changes from the new `DockerEventService` implementation.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (1)</summary><blockquote>

`102-124`: **Consider using more deterministic synchronization instead of time delays.**

Relying on `setTimeout` (via `await new Promise((resolve) => setTimeout(resolve, ...))`) to process stream data can potentially introduce test flakiness. A more robust approach is to wait on an event or a promise that confirms the event has been handled.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

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

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bc3ca92fb02387bc019bb001809df96974737b50 and 29f380f64f122a66d80ede15f1d7a690dcad0351.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `api/package.json` (1 hunks)
* `api/src/unraid-api/app/app.module.ts` (0 hunks)
* `api/src/unraid-api/graph/graph.module.ts` (2 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.module.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.service.ts` (2 hunks)
* `api/src/unraid-api/graph/resolvers/resolvers.module.ts` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* api/src/unraid-api/app/app.module.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary>

* `Injectable` (22-167)

</details>

</blockquote></details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/store/index.ts (1)</summary>

* `getters` (38-49)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.ts (1)</summary>

`1-20`: **Well-structured Docker module following NestJS conventions**

This new module provides a clean encapsulation of all Docker-related functionality by gathering the services and resolvers into a single module. The organization with commented sections for services and resolvers improves readability.

The exports of `DockerService` and `DockerEventService` make these components available to other modules that import `DockerModule`.

</details>
<details>
<summary>api/package.json (1)</summary>

`90-90`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify compatibility with dockerode v4 upgrade**

The dockerode package has been updated from v3 to v4, which is a major version update that may include breaking changes. The PR objective confirms this was intentional.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any breaking changes that might impact existing code

# Search for any dockerode usage in the codebase
echo "Searching for dockerode usage in the codebase..."
rg -A 3 "dockerode" --type ts

# Check the changelog or release notes for dockerode v4
echo "Checking dockerode v4 release notes..."
curl -s "https://api.github.com/repos/apocas/dockerode/releases/tags/v4.0.0" | jq '.body'
```

Length of output: 2742

---



</details>

**Dockerode v4 Upgrade: Preliminary Verification Complete**

- Dockerode usage is present in several files (e.g., `docker.service.ts`, `docker.service.spec.ts`, `docker-event.service.ts`, `docker-event.service.spec.ts`, `clients/docker.ts`), with no usage patterns immediately indicating incompatibility.
- The fetched release notes returned only the version string ("v4.0.0") without additional breaking change details.
- While the intentional upgrade appears sound, please ensure that comprehensive integration tests covering Docker interactions are executed to catch any subtle issues.

</details>
<details>
<summary>api/src/unraid-api/graph/graph.module.ts (1)</summary>

`32-33`: <details>
<summary>❓ Verification inconclusive</summary>

**Auth-related dependencies removed from GraphModule**

The AuthZService injection and parameter have been removed, simplifying this module's dependencies. This appears to be part of the refactoring for Docker events handling.

---

🏁 Script executed:

```shell
#!/bin/bash
# Verify if auth functionality has been moved elsewhere

# Check for AuthZService usage in Docker-related files
echo "Checking if AuthZService is used in Docker services:"
rg "AuthZService" --type ts api/src/unraid-api/graph/resolvers/docker/
```

Length of output: 185

---



</details>

**GraphModule Dependency Refactoring – Please Verify Global Auth References**

The removal of the AuthZService injection and parameter from `api/src/unraid-api/graph/graph.module.ts` (lines 32–33) appears correct for the Docker events refactoring. Our initial search in the Docker resolvers folder found no usage of AuthZService. However, since the check was limited to a specific directory, please verify—ideally via a global repository search—that no residual usage of AuthZService remains before finalizing this change.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/resolvers.module.ts (2)</summary>

`16-16`: **Added DockerModule import**

The import for DockerModule has been added, which will provide access to the encapsulated Docker functionality.

---

`37-37`: <details>
<summary>❓ Verification inconclusive</summary>

**Docker functionality now imported from DockerModule**

The module now imports DockerModule rather than directly including Docker-related providers. This improves modularity and encapsulation.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify that Docker providers were removed from the providers array

# Get the old version of the file to confirm the removed providers
echo "Checking the previous version of resolvers.module.ts for removed Docker providers:"
git show HEAD~1:api/src/unraid-api/graph/resolvers/resolvers.module.ts | grep -E "Docker(Service|Resolver|MutationsResolver)"
```

Length of output: 302

---



</details>

**DockerModule Usage Update – Please Verify Removal of Legacy Docker Providers**

The module now properly imports `DockerModule`, which is intended to replace directly included Docker providers for better modularity. However, our automated check did not yield evidence of Docker-related providers (e.g., DockerService, DockerResolver, DockerMutationsResolver) in the previous commit. Please manually verify that no legacy Docker providers remain in the `providers` array.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)</summary>

`12-56`: **All tests look well-structured and comprehensive.**

They correctly verify the module's successful compilation and ensure that each relevant service/resolver is provided. This coverage helps confirm that `DockerModule` is properly configured and exports the necessary providers.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts (2)</summary>

`53-96`: **Solid use of setup and teardown in this suite.**

Clearing mocks and calling `service.stopEventStream()` in `afterEach` ensures that tests start with a clean state. This approach minimizes the risk of cross-test contamination. Great job keeping tests atomic.

---

`136-196`: **Nice handling of multiple events per chunk.**

Splitting the chunk by newline and parsing each JSON object individually is both straightforward and effective. This ensures multiple events in a single chunk are processed accordingly without missing any.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary>

`55-75`: **Debounced container cache updates provide efficient batching.**

Calling `debouncedContainerCacheUpdate()` on module init (line 56) and exposing it as a public method (line 72) can help smooth out frequent updates. Keep in mind that debouncing may delay updates if frequent container actions occur in rapid succession, which likely matches the intended behavior. This approach looks well-thought-out.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (3)</summary>

`63-85`: **Good use of chokidar for Docker socket detection.**

Watching the `var-run` directory and reacting to the appearance/disappearance of the socket is a neat approach. This ensures the event stream is opened only when Docker is available, avoiding unnecessary errors or manual checks.

---

`99-114`: **Well-targeted container event filtering.**

Checking if `actionName` starts with the watched actions before updating the container cache is clear and readable. This ensures efficient event handling without the overhead of processing irrelevant actions.

---

`116-164`: **Robust event stream setup and error handling.**

Using listeners for errors, stream ends, and data parsing makes `setupDockerWatch()` resilient to malformed data or stream anomalies. Logging detailed messages helps with debugging if issues arise. This is a strong implementation practice.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts (1)</summary>

`527-550`: **Well-documented test migration with clear explanations**

The skipped tests clearly indicate what happened to the functionality they were testing. The comments provide valuable context that:
1. These tests have been moved to `docker-event.service.spec.ts`
2. The methods no longer exist in `DockerService`

This aligns with the PR objective of moving to independent Docker events by relocating the Docker event handling functionality from `DockerService` to a dedicated `DockerEventService`.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T15:19:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (2)</summary><blockquote>

`4-5`: **Consider reusing a shared TestingModule in beforeEach.**  
While each test individually creates and compiles a TestingModule, you might load the module once in a beforeEach hook and reuse it across your test cases. This can improve test performance and readability by reducing repetitive boilerplate.

---

`48-55`: **Add negative/edge case tests.**  
You already confirm that DockerMutationsResolver is provided, but consider adding tests validating scenarios where required dependencies are missing or incorrectly configured. Ensuring the module fails gracefully can boost resilience.

</blockquote></details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary><blockquote>

`74-77`: **Debounce usage to throttle frequent container updates.**  
The current code calls a 500ms debounce, which is usually sufficient. Consider verifying that this won't skip any critical container state transitions. If you do need real-time updates, you could reduce or remove the debounce. Otherwise, this is an effective approach to avoid excessive updates.

</blockquote></details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (3)</summary><blockquote>

`10-22`: **Ensure DockerEventAction covers full usage scenarios.**  
You have enumerated multiple container lifecycles. Confirm that all relevant actions for your environment are included. If new Docker actions appear in upcoming versions of Docker, consider an extensibility mechanism or fallback logic for unknown actions.

---

`99-114`: **Use stronger typing for events.**  
Currently, `handleDockerEvent` processes any JSON object as an event. If you have known event structures, consider using interfaces or type guards to make the event object safer to parse, reducing the chances of runtime errors.

---

`116-169`: **Add reconnection strategies for event streaming.**  
Should the stream disconnect unexpectedly, you may want logic that automatically attempts to reconnect the event stream. Right now, you stop everything on `end` and `error`. A reconnection approach can improve reliability for handling Docker restarts or transient socket unavailability.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2500f2156a40b05bfe6923109e7fcd5ccd2d90dd and 5ff652a59ee9b26f7bff62d28b7f8e801e93b3ca.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `api/package.json` (1 hunks)
* `api/src/unraid-api/app/app.module.ts` (0 hunks)
* `api/src/unraid-api/graph/graph.module.ts` (2 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.module.ts` (1 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts` (0 hunks)
* `api/src/unraid-api/graph/resolvers/docker/docker.service.ts` (2 hunks)
* `api/src/unraid-api/graph/resolvers/resolvers.module.ts` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* api/src/unraid-api/app/app.module.ts
* api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* api/src/unraid-api/graph/resolvers/docker/docker.module.ts
* api/package.json
* api/src/unraid-api/graph/graph.module.ts
* api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts
* api/src/unraid-api/graph/resolvers/resolvers.module.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary>

* `Injectable` (23-190)

</details>

</blockquote></details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary><blockquote>

<details>
<summary>api/src/store/index.ts (1)</summary>

* `getters` (38-49)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: Build Web App
* GitHub Check: Build API
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)</summary>

`12-19`: **Great coverage for module compilation.**  
Verifying that DockerModule compiles ensures that all necessary providers and imports are in place. This test is straightforward and is good to have in regression coverage.

</details>
<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (3)</summary>

`34-38`: **Review exposure of Docker client as public.**  
Changing `getDockerClient()` from private to public expands the Docker client usage beyond this service. This can be helpful, but also be mindful of potential misuse or concurrency concerns by external callers. Validate that external consumers handle the Docker client responsibly.

---

`58-59`: **Good call to debounce container cache update on module init.**  
You are now triggering `debouncedContainerCacheUpdate` at initialization, ensuring the container cache is populated upfront without spawning watchers. This appears consistent with your new approach.

---

`61-66`: **Docstring for getAutoStarts is helpful.**  
Explicitly citing the GitHub issue clarifies the dependency on an external file that may or may not exist. This is a neat reference for debugging or further enhancements.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T18:16:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (4)</summary><blockquote>

`3-4`: **Consolidate vitest imports**

Multiple imports from the same module can be combined into a single import statement for better readability.


```diff
-import { describe, expect, it } from 'vitest';
-import { vi } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Build API</summary>

[failure] 3-3: 
Replace `·}·from·'vitest';⏎import·{` with `,`                                                                                                                                                      

</details>
<details>
<summary>🪛 GitHub Check: Test API</summary>

[failure] 3-3: 
Replace `·}·from·'vitest';⏎import·{` with `,`                                                                                                                                                      

</details>

</details>

---

`24-40`: **Add verification for all mocked properties**

You've mocked `debouncedContainerCacheUpdate` but aren't verifying its presence in your test.


```diff
    const service = module.get<DockerService>(DockerService);
    expect(service).toBeDefined();
    expect(service).toHaveProperty('getDockerClient');
+   expect(service).toHaveProperty('debouncedContainerCacheUpdate');
```

---

`54-64`: **Fix provider formatting to match style guidelines**

Static analysis tools are flagging formatting issues with the provider array. Adjust the formatting to keep the test consistent with your style guidelines.


```diff
    const module: TestingModule = await Test.createTestingModule({
        providers: [
-           DockerResolver,
-           { provide: DockerService, useValue: {} },
+           DockerResolver, { provide: DockerService, useValue: {} },
        ],
    }).compile();
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Build API</summary>

[failure] 56-56: 
Replace `⏎················DockerResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerResolver,·{·provide:·DockerService,·useValue:·{}·}`                  

</details>
<details>
<summary>🪛 GitHub Check: Test API</summary>

[failure] 56-56: 
Replace `⏎················DockerResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerResolver,·{·provide:·DockerService,·useValue:·{}·}`                  

</details>

</details>

---

`66-76`: **Fix provider formatting to match style guidelines**

Static analysis tools are flagging formatting issues with the provider array. Adjust the formatting to keep the test consistent with your style guidelines.


```diff
    const module: TestingModule = await Test.createTestingModule({
        providers: [
-           DockerMutationsResolver,
-           { provide: DockerService, useValue: {} },
+           DockerMutationsResolver, { provide: DockerService, useValue: {} },
        ],
    }).compile();
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Build API</summary>

[failure] 68-68: 
Replace `⏎················DockerMutationsResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerMutationsResolver,·{·provide:·DockerService,·useValue:·{}·}`

</details>
<details>
<summary>🪛 GitHub Check: Test API</summary>

[failure] 68-68: 
Replace `⏎················DockerMutationsResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerMutationsResolver,·{·provide:·DockerService,·useValue:·{}·}`

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5ff652a59ee9b26f7bff62d28b7f8e801e93b3ca and 1c26e9bf5d870a2e99584b8757704ec5364d4a02.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: Build API</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts</summary>

[failure] 3-3: 
Replace `·}·from·'vitest';⏎import·{` with `,`                                                                                                                                                      

---

[failure] 56-56: 
Replace `⏎················DockerResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerResolver,·{·provide:·DockerService,·useValue:·{}·}`                  

---

[failure] 68-68: 
Replace `⏎················DockerMutationsResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerMutationsResolver,·{·provide:·DockerService,·useValue:·{}·}`

</details>

</details>
<details>
<summary>🪛 GitHub Check: Test API</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts</summary>

[failure] 3-3: 
Replace `·}·from·'vitest';⏎import·{` with `,`                                                                                                                                                      

---

[failure] 56-56: 
Replace `⏎················DockerResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerResolver,·{·provide:·DockerService,·useValue:·{}·}`                  

---

[failure] 68-68: 
Replace `⏎················DockerMutationsResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerMutationsResolver,·{·provide:·DockerService,·useValue:·{}·}`

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Build Web App
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts (1)</summary>

`12-77`: **Strong test coverage for the Docker module structure**

The test suite effectively verifies that the DockerModule compiles properly and provides all necessary Docker-related services and resolvers. This is critical when refactoring components to ensure the dependency injection system works as expected.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Build API</summary>

[failure] 56-56: 
Replace `⏎················DockerResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerResolver,·{·provide:·DockerService,·useValue:·{}·}`                  

---

[failure] 68-68: 
Replace `⏎················DockerMutationsResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerMutationsResolver,·{·provide:·DockerService,·useValue:·{}·}`

</details>
<details>
<summary>🪛 GitHub Check: Test API</summary>

[failure] 56-56: 
Replace `⏎················DockerResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerResolver,·{·provide:·DockerService,·useValue:·{}·}`                  

---

[failure] 68-68: 
Replace `⏎················DockerMutationsResolver,⏎················{·provide:·DockerService,·useValue:·{}·},⏎············` with `DockerMutationsResolver,·{·provide:·DockerService,·useValue:·{}·}`

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T18:30:36Z
user: login: mdatelle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T19:30:13Z
user: login: mdatelle
body: Just a small nit
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T19:44:33Z
user: login: elibosley
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T14:33:59Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:34:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (1)</summary><blockquote>

`126-128`: **Address Prettier/Linter spacing concerns.**

Static analysis indicates a required formatting fix here. Consider adding a line break or adjusting spacing to maintain consistency with lint rules.


Apply this diff to align with the lint suggestions:

```diff
- if (dockerEvent.Type === DockerEventType.CONTAINER && 
-     typeof actionName === 'string' && 
-     this.containerActions.includes(actionName as DockerEventAction)) {
+ if (
+   dockerEvent.Type === DockerEventType.CONTAINER &&
+   typeof actionName === 'string' &&
+   this.containerActions.includes(actionName as DockerEventAction)
+ ) {
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Test API</summary>

[failure] 126-126: 
Replace `dockerEvent.Type·===·DockerEventType.CONTAINER·&&·` with `⏎················dockerEvent.Type·===·DockerEventType.CONTAINER·&&`

---

[failure] 127-127: 
Delete `·`                                                                                                                            

---

[failure] 128-128: 
Insert `⏎············`                                                                                                                

</details>
<details>
<summary>🪛 GitHub Check: Build API</summary>

[failure] 126-126: 
Replace `dockerEvent.Type·===·DockerEventType.CONTAINER·&&·` with `⏎················dockerEvent.Type·===·DockerEventType.CONTAINER·&&`

---

[failure] 127-127: 
Delete `·`                                                                                                                            

---

[failure] 128-128: 
Insert `⏎············`                                                                                                                

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 1c9c77e4f3a43cc91fa0380908d54b173b767051 and c014d3064dddcede57b3e943466a4326841f9192.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: Test API</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts</summary>

[failure] 126-126: 
Replace `dockerEvent.Type·===·DockerEventType.CONTAINER·&&·` with `⏎················dockerEvent.Type·===·DockerEventType.CONTAINER·&&`

---

[failure] 127-127: 
Delete `·`                                                                                                                            

---

[failure] 128-128: 
Insert `⏎············`                                                                                                                

</details>

</details>
<details>
<summary>🪛 GitHub Check: Build API</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts</summary>

[failure] 126-126: 
Replace `dockerEvent.Type·===·DockerEventType.CONTAINER·&&·` with `⏎················dockerEvent.Type·===·DockerEventType.CONTAINER·&&`

---

[failure] 127-127: 
Delete `·`                                                                                                                            

---

[failure] 128-128: 
Insert `⏎············`                                                                                                                

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Build Web App
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (6)</summary>

`71-73`: **Good initialization approach.**

Invoking `setupVarRunWatch` in `onModuleInit` ensures the file watch is set up as soon as the module is initialized, which seems appropriate for automatically handling Docker events.

---

`79-93`: **Proper use of `ignoreInitial: false`.**

Using `ignoreInitial: false` with chokidar ensures that the Docker socket is recognized on startup, even if it already exists. This addresses potential race conditions where the socket might be present before the watch is established.

---

`98-105`: **Well-structured stream shutdown.**

Stopping and cleaning up the Docker event stream gracefully prevents leaks and uncaught event listeners. This is a good practice for ensuring stability.

---

`107-132`: **Robust event handling.**

The checks and type guarding around `event` ensure unexpected data is logged and handled gracefully. This helps maintain stability and traceability when invalid or malformed data is received.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: Test API</summary>

[failure] 126-126: 
Replace `dockerEvent.Type·===·DockerEventType.CONTAINER·&&·` with `⏎················dockerEvent.Type·===·DockerEventType.CONTAINER·&&`

---

[failure] 127-127: 
Delete `·`                                                                                                                            

---

[failure] 128-128: 
Insert `⏎············`                                                                                                                

</details>
<details>
<summary>🪛 GitHub Check: Build API</summary>

[failure] 126-126: 
Replace `dockerEvent.Type·===·DockerEventType.CONTAINER·&&·` with `⏎················dockerEvent.Type·===·DockerEventType.CONTAINER·&&`

---

[failure] 127-127: 
Delete `·`                                                                                                                            

---

[failure] 128-128: 
Insert `⏎············`                                                                                                                

</details>

</details>

---

`134-187`: **Comprehensive error handling for Docker event stream.**

The `try/catch` block around the event stream creation, coupled with callbacks that destroy the stream on errors or closures, provides resilient handling for unexpected disconnections or stream failures.

---

`193-195`: **Useful status check.**

The `isActive` method clearly indicates whether the service is actively streaming Docker events, which is helpful for health checks and debugging.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:39:33Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (4)</summary><blockquote>

`107-111`: **Consider defining a stronger event type.**  
You’re using an `unknown` type for the Docker event and casting it to a `DockerEvent` interface. This is acceptable in many cases, but you might minimize runtime checks by strictly typing the event or using runtime validation to ensure correctness.

---

`79-93`: **Validate repeated “add” events on the same socket.**  
If the socket is re-created multiple times, you’ll call `setupDockerWatch()` for each “add” event. Ensure this scenario is handled gracefully (e.g., by stopping an existing stream before starting another).

---

`140-142`: **Use a more direct type assertion for the event stream.**  
Here you’re casting `eventStream` to `unknown` then to `Readable`. If possible, check `instanceof Readable` or use a direct cast:  
```diff
- this.dockerEventStream = eventStream as unknown as Readable;
+ this.dockerEventStream = eventStream as Readable;
```
This can help reduce confusion about intermediate `unknown` casts.

---

`185-189`: **Consider auto-retry on setup failure.**  
When Docker is unavailable or an error occurs, you log and stop. Consider an optional backoff or retry mechanism to recover without requiring a file remove/add cycle.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro (Legacy)**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c014d3064dddcede57b3e943466a4326841f9192 and 48cb08d93e8904379456406f7006aeca7d22fcae.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (2)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker.service.ts (1)</summary>

* `Injectable` (23-190)

</details>
<details>
<summary>api/src/store/index.ts (1)</summary>

* `getters` (38-49)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: Build Web App
* GitHub Check: Build API
* GitHub Check: Test API
* GitHub Check: Cloudflare Pages

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts (2)</summary>

`117-120`: **Clarify partial vs. exact action matching.**  
Currently, `shouldProcess` uses `startsWith(action)` while the container actions check uses `includes(action)`. If your intent is to catch exact matches only, consider using `===` for consistency.  



Also applies to: 127-129

---

`71-73`: **Overall implementation looks solid!**  
Your approach for setting up `chokidar` watchers, stopping the event stream in `onModuleDestroy`, and deferring Docker operations to `DockerService` is clean and maintainable. Excellent job!  



Also applies to: 75-77

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:44:12Z

Review comments:
diff_hunk: @@ -0,0 +1,178 @@
+import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+import { Readable } from 'stream';
+
+import { watch } from 'chokidar';
+import Docker from 'dockerode';
+
+import { getters } from '@app/store/index.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+enum DockerEventAction {
+    DIE = 'die',
+    KILL = 'kill',
+    OOM = 'oom',
+    PAUSE = 'pause',
+    RESTART = 'restart',
+    START = 'start',
+    STOP = 'stop',
+    UNPAUSE = 'unpause',
+    EXEC_CREATE = 'exec_create',
+    EXEC_START = 'exec_start',
+    EXEC_DIE = 'exec_die',
+}
+
+enum DockerEventType {
+    CONTAINER = 'container',
+}
+
+@Injectable()
+export class DockerEventService implements OnModuleDestroy, OnModuleInit {
+    private client: Docker;
+    private dockerEventStream: Readable | null = null;
+    private readonly logger = new Logger(DockerEventService.name);
+
+    private watchedActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+        DockerEventAction.EXEC_CREATE,
+        DockerEventAction.EXEC_START,
+        DockerEventAction.EXEC_DIE,
+    ];
+
+    private containerActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+    ];
+
+    constructor(private readonly dockerService: DockerService) {
+        this.client = this.dockerService.getDockerClient();
+    }
+
+    async onModuleInit() {
+        this.setupVarRunWatch();
+    }
+
path: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Possible fallback if docker-socket is already present at startup.**  
At module init, you watch for the socket file to appear, but if the socket already exists when the app starts, you may miss the event. Consider checking for the file’s presence immediately before setting up the watch, ensuring you don’t skip any events.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-04T18:16:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,178 @@
+import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+import { Readable } from 'stream';
+
+import { watch } from 'chokidar';
+import Docker from 'dockerode';
+
+import { getters } from '@app/store/index.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+enum DockerEventAction {
+    DIE = 'die',
+    KILL = 'kill',
+    OOM = 'oom',
+    PAUSE = 'pause',
+    RESTART = 'restart',
+    START = 'start',
+    STOP = 'stop',
+    UNPAUSE = 'unpause',
+    EXEC_CREATE = 'exec_create',
+    EXEC_START = 'exec_start',
+    EXEC_DIE = 'exec_die',
+}
+
+enum DockerEventType {
+    CONTAINER = 'container',
+}
+
+@Injectable()
+export class DockerEventService implements OnModuleDestroy, OnModuleInit {
+    private client: Docker;
+    private dockerEventStream: Readable | null = null;
+    private readonly logger = new Logger(DockerEventService.name);
+
+    private watchedActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+        DockerEventAction.EXEC_CREATE,
+        DockerEventAction.EXEC_START,
+        DockerEventAction.EXEC_DIE,
+    ];
+
+    private containerActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+    ];
+
+    constructor(private readonly dockerService: DockerService) {
+        this.client = this.dockerService.getDockerClient();
+    }
+
+    async onModuleInit() {
+        this.setupVarRunWatch();
+    }
+
+    onModuleDestroy() {
+        this.stopEventStream();
+    }
+
+    private setupVarRunWatch() {
+        const paths = getters.paths();
+        watch(paths['var-run'], { ignoreInitial: false })
+            .on('add', async (path) => {
+                if (path === paths['docker-socket']) {
+                    this.logger.debug('Starting docker event watch');
+                    await this.setupDockerWatch();
+                }
+            })
+            .on('unlink', (path) => {
+                if (path === paths['docker-socket']) {
+                    this.stopEventStream();
+                }
+            });
+    }
+
+    /**
+     * Stop the Docker event stream
+     */
+    public stopEventStream(): void {
+        if (this.dockerEventStream) {
+            this.logger.debug('Stopping docker event stream');
+            this.dockerEventStream.removeAllListeners();
+            this.dockerEventStream.destroy();
+            this.dockerEventStream = null;
+        }
+    }
+
+    private async handleDockerEvent(event: any): Promise<void> {
path: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
user: login: mdatelle
body: Should remove this any.
created_at: 2025-04-04T19:30:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,178 @@
+import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+import { Readable } from 'stream';
+
+import { watch } from 'chokidar';
+import Docker from 'dockerode';
+
+import { getters } from '@app/store/index.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+enum DockerEventAction {
+    DIE = 'die',
+    KILL = 'kill',
+    OOM = 'oom',
+    PAUSE = 'pause',
+    RESTART = 'restart',
+    START = 'start',
+    STOP = 'stop',
+    UNPAUSE = 'unpause',
+    EXEC_CREATE = 'exec_create',
+    EXEC_START = 'exec_start',
+    EXEC_DIE = 'exec_die',
+}
+
+enum DockerEventType {
+    CONTAINER = 'container',
+}
+
+@Injectable()
+export class DockerEventService implements OnModuleDestroy, OnModuleInit {
+    private client: Docker;
+    private dockerEventStream: Readable | null = null;
+    private readonly logger = new Logger(DockerEventService.name);
+
+    private watchedActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+        DockerEventAction.EXEC_CREATE,
+        DockerEventAction.EXEC_START,
+        DockerEventAction.EXEC_DIE,
+    ];
+
+    private containerActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+    ];
+
+    constructor(private readonly dockerService: DockerService) {
+        this.client = this.dockerService.getDockerClient();
+    }
+
+    async onModuleInit() {
+        this.setupVarRunWatch();
+    }
+
path: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
user: login: elibosley
body: The ignoreInitial: false in here should cause this to trigger on app startup (it does on my dev server)
created_at: 2025-04-07T14:33:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,178 @@
+import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
+import { Readable } from 'stream';
+
+import { watch } from 'chokidar';
+import Docker from 'dockerode';
+
+import { getters } from '@app/store/index.js';
+import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
+
+enum DockerEventAction {
+    DIE = 'die',
+    KILL = 'kill',
+    OOM = 'oom',
+    PAUSE = 'pause',
+    RESTART = 'restart',
+    START = 'start',
+    STOP = 'stop',
+    UNPAUSE = 'unpause',
+    EXEC_CREATE = 'exec_create',
+    EXEC_START = 'exec_start',
+    EXEC_DIE = 'exec_die',
+}
+
+enum DockerEventType {
+    CONTAINER = 'container',
+}
+
+@Injectable()
+export class DockerEventService implements OnModuleDestroy, OnModuleInit {
+    private client: Docker;
+    private dockerEventStream: Readable | null = null;
+    private readonly logger = new Logger(DockerEventService.name);
+
+    private watchedActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+        DockerEventAction.EXEC_CREATE,
+        DockerEventAction.EXEC_START,
+        DockerEventAction.EXEC_DIE,
+    ];
+
+    private containerActions = [
+        DockerEventAction.DIE,
+        DockerEventAction.KILL,
+        DockerEventAction.OOM,
+        DockerEventAction.PAUSE,
+        DockerEventAction.RESTART,
+        DockerEventAction.START,
+        DockerEventAction.STOP,
+        DockerEventAction.UNPAUSE,
+    ];
+
+    constructor(private readonly dockerService: DockerService) {
+        this.client = this.dockerService.getDockerClient();
+    }
+
+    async onModuleInit() {
+        this.setupVarRunWatch();
+    }
+
path: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@elibosley You're absolutely right! I see the `ignoreInitial: false` option in your `setupVarRunWatch` method:

```typescript
private setupVarRunWatch() {
    this.logger.log('Setting up var run watch');
    const watcher = watch('/var/run/docker.sock', {
        ignoreInitial: false,
    });
    // ...
}
```

This setting will indeed trigger the watcher for existing files when the app starts up, which addresses the concern about the Docker socket already being present at startup. Thanks for the clarification!



---

🏁 Script executed:
```
#!/bin/bash
# Find the complete implementation of DockerEventService, particularly looking for setupVarRunWatch
cat api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
```

Length of output: 6286

---

🏁 Script executed:
```
#!/bin/bash
# Search for the setupVarRunWatch method implementation in DockerEventService
grep -A 20 "setupVarRunWatch" api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts
```

Length of output: 883

---



</details>

@elibosley You're absolutely right! I see in your implementation that you're using `ignoreInitial: false` in the chokidar watch options:

```typescript
private setupVarRunWatch() {
    const paths = getters.paths();
    watch(paths['var-run'], { ignoreInitial: false })
        .on('add', async (path) => {
            if (path === paths['docker-socket']) {
                this.logger.debug('Starting docker event watch');
                await this.setupDockerWatch();
            }
        })
        // ...
}
```

This configuration ensures that the watcher will emit events for existing files when it starts up, addressing the scenario where the Docker socket already exists at application startup. Thanks for clarifying!
---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: elibosley
PR: unraid/api#1334
File: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts:63-66
Timestamp: 2025-04-07T14:34:47.145Z
Learning: In DockerEventService, the chokidar file watcher is configured with `ignoreInitial: false` to ensure that existing files (like the Docker socket) are detected and processed at application startup, not just when files change after the watcher is initialized.
```

---

```
Learnt from: elibosley
PR: unraid/api#1334
File: api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts:63-66
Timestamp: 2025-04-07T14:34:47.145Z
Learning: In DockerEventService, the chokidar file watcher is configured with `ignoreInitial: false` to ensure that existing files (like the Docker socket) are detected and processed at application startup, not just when files change after the watcher is initialized.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-07T14:34:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-04T15:13:56Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request updates several Docker-related components. It upgrades the `dockerode` package and removes the `docker-event-emitter` dependency from `api/package.json`. The changes remove unused authentication imports from module files and refactor Docker event handling by introducing a new `DockerEventService` along with its test suite. Additionally, the `DockerModule` is introduced to encapsulate Docker services and resolvers, and the `DockerService` is simplified by removing watcher methods and exposing key methods publicly.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `api/package.json` | Upgraded `dockerode` from `^3.3.5` to `^4.0.5`; removed `docker-event-emitter` dependency. |
| `api/src/unraid-api/app/app.module.ts`<br>`api/src/unraid-api/graph/graph.module.ts` | Removed unused authentication and authorization imports (e.g., `AuthInterceptor`, `UnauthorizedException`, `AuthZService`); adjusted dependency injection in modules. |
| `api/src/unraid-api/graph/resolvers/docker/docker-event.service.ts`<br>`api/src/unraid-api/graph/resolvers/docker/docker-event.service.spec.ts` | Added new `DockerEventService` to monitor Docker events; introduced extensive tests covering event processing, error handling, and resource cleanup. |
| `api/src/unraid-api/graph/resolvers/docker/docker.module.ts`<br>`api/src/unraid-api/graph/resolvers/docker/docker.module.spec.ts` | Created a new `DockerModule` that registers Docker-related services and resolvers (including `DockerService`, `DockerEventService`, `DockerResolver`, and `DockerMutationsResolver`); added module tests. |
| `api/src/unraid-api/graph/resolvers/docker/docker.service.ts`<br>`api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts` | Simplified `DockerService` by removing watcher methods; made `getDockerClient` and `debouncedContainerCacheUpdate` publicly accessible; removed tests that have been moved to `DockerEventService`. |
| `api/src/unraid-api/graph/resolvers/resolvers.module.ts` | Updated module imports by replacing specific Docker providers with a single `DockerModule` import. |

## Possibly related PRs
- **unraid/api#1140:** Addresses dependency management for `dockerode` by reverting to an earlier version, directly conflicting with this upgrade.
- **unraid/api#1292:** Related to updates in Docker functionality and service management, focusing on the `dockerode` dependency and the removal of the `docker-event-emitter`.

## Suggested labels
`enhancement`

## Suggested reviewers
- pujitm
- mdatelle

## Poem
> In the code where Docker dreams,  
> A new service gleams, so bright it seems.  
> Dependencies updated, modules realigned,  
> Unused imports gracefully declined.  
> With tests in place and flow redefined,  
> Our code sings like a well-tuned rhyme! 🚀🎶

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=unraid/api&utm_content=1334):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-07T14:44:12Z
body: This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:
```
https://preview.dl.unraid.net/unraid-api/tag/1334/dynamix.unraid.net.plg
```
<!-- thollander/actions-comment-pull-request "prlink" -->
author_association: CONTRIBUTOR