diff --git a/api/dev/configs/api.json b/api/dev/configs/api.json index d44a115dd2..f4f76b8f55 100644 --- a/api/dev/configs/api.json +++ b/api/dev/configs/api.json @@ -1,5 +1,5 @@ { - "version": "4.28.2", + "version": "4.29.2", "extraOrigins": [], "sandbox": true, "ssoSubIds": [], diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-autostart.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker-autostart.service.spec.ts index adc98d497e..76a4300d8a 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker-autostart.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker-autostart.service.spec.ts @@ -2,10 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { - AutoStartEntry, - DockerAutostartService, -} from '@app/unraid-api/graph/resolvers/docker/docker-autostart.service.js'; +import { DockerAutostartService } from '@app/unraid-api/graph/resolvers/docker/docker-autostart.service.js'; import { DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; // Mock store getters diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-log.service.spec.ts b/api/src/unraid-api/graph/resolvers/docker/docker-log.service.spec.ts index 2280e8e3d8..24b2a85306 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker-log.service.spec.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker-log.service.spec.ts @@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AppError } from '@app/core/errors/app-error.js'; import { DockerLogService } from '@app/unraid-api/graph/resolvers/docker/docker-log.service.js'; -import { DockerContainerLogs } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; // Mock dependencies const mockExeca = vi.fn(); diff --git a/api/src/unraid-api/graph/resolvers/docker/docker-tailscale.service.ts b/api/src/unraid-api/graph/resolvers/docker/docker-tailscale.service.ts index 8402b9e3d5..7e7c63d476 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker-tailscale.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker-tailscale.service.ts @@ -91,7 +91,6 @@ export class DockerTailscaleService { ); const dnsName = rawStatus.Self.DNSName; - const actualHostname = dnsName ? dnsName.split('.')[0] : undefined; let relayName: string | undefined; if (rawStatus.Self.Relay && derpMap) { diff --git a/api/src/unraid-api/graph/resolvers/docker/docker.model.ts b/api/src/unraid-api/graph/resolvers/docker/docker.model.ts index ad4153ae48..a840f3b6de 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.model.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.model.ts @@ -2,7 +2,6 @@ import { Field, Float, GraphQLISODateTime, - ID, InputType, Int, ObjectType, 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 aad50b361d..445dc05a6b 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 @@ -13,11 +13,7 @@ import { DockerLogService } from '@app/unraid-api/graph/resolvers/docker/docker- import { DockerManifestService } from '@app/unraid-api/graph/resolvers/docker/docker-manifest.service.js'; import { DockerNetworkService } from '@app/unraid-api/graph/resolvers/docker/docker-network.service.js'; import { DockerPortService } from '@app/unraid-api/graph/resolvers/docker/docker-port.service.js'; -import { - ContainerPortType, - ContainerState, - DockerContainer, -} from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; +import { ContainerState, DockerContainer } from '@app/unraid-api/graph/resolvers/docker/docker.model.js'; import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js'; import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js'; 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 07f59d8787..87374db4bc 100644 --- a/api/src/unraid-api/graph/resolvers/docker/docker.service.ts +++ b/api/src/unraid-api/graph/resolvers/docker/docker.service.ts @@ -321,7 +321,7 @@ export class DockerService { await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY); this.logger.debug(`Invalidated container cache after pausing ${id}`); - let containers = await this.getContainers({ skipCache: true }); + let containers: DockerContainer[]; let updatedContainer: DockerContainer | undefined; for (let i = 0; i < 5; i++) { await sleep(500); @@ -349,7 +349,7 @@ export class DockerService { await this.cacheManager.del(DockerService.CONTAINER_CACHE_KEY); this.logger.debug(`Invalidated container cache after unpausing ${id}`); - let containers = await this.getContainers({ skipCache: true }); + let containers: DockerContainer[]; let updatedContainer: DockerContainer | undefined; for (let i = 0; i < 5; i++) { await sleep(500); diff --git a/api/src/unraid-api/organizer/organizer.ts b/api/src/unraid-api/organizer/organizer.ts index 381fb90b58..8ad9145fb0 100644 --- a/api/src/unraid-api/organizer/organizer.ts +++ b/api/src/unraid-api/organizer/organizer.ts @@ -683,7 +683,7 @@ export interface MoveItemsToPositionParams { * Combines moveEntriesToFolder with position-based insertion. */ export function moveItemsToPosition(params: MoveItemsToPositionParams): OrganizerView { - const { view, sourceEntryIds, destinationFolderId, position, resources } = params; + const { view, sourceEntryIds, destinationFolderId, position } = params; const movedView = moveEntriesToFolder({ view, sourceEntryIds, destinationFolderId }); @@ -743,7 +743,7 @@ export interface CreateFolderWithItemsParams { * Combines createFolder + moveItems + positioning in a single atomic operation. */ export function createFolderWithItems(params: CreateFolderWithItemsParams): OrganizerView { - const { view, folderId, folderName, parentId, sourceEntryIds = [], position, resources } = params; + const { view, folderId, folderName, parentId, sourceEntryIds = [], position } = params; let newView = createFolderInView({ view, diff --git a/web/src/components/Common/BaseTreeTable.vue b/web/src/components/Common/BaseTreeTable.vue index be69ff3144..4fd8a59d6c 100644 --- a/web/src/components/Common/BaseTreeTable.vue +++ b/web/src/components/Common/BaseTreeTable.vue @@ -299,9 +299,7 @@ function createCellWrapper( }); } -function wrapColumnHeaderRenderer( - header: ColumnHeaderRenderer | undefined -): ColumnHeaderRenderer | undefined { +function wrapColumnHeaderRenderer(header: ColumnHeaderRenderer | undefined): ColumnHeaderRenderer { if (typeof header === 'function') { return function wrappedHeaderRenderer(this: unknown, ...args: unknown[]) { const result = (header as (...args: unknown[]) => unknown).apply(this, args); @@ -481,7 +479,7 @@ const processedColumns = computed>[]>(() => { createSelectColumn(), ...props.columns.map((col, colIndex) => { const originalHeader = col.header as ColumnHeaderRenderer | undefined; - const header = wrapColumnHeaderRenderer(originalHeader) ?? originalHeader; + const header = wrapColumnHeaderRenderer(originalHeader); const cell = (col as { cell?: unknown }).cell ? ({ row }: { row: TableInstanceRow }) => { const cellFn = (col as { cell: (args: unknown) => VNode | string | number }).cell; diff --git a/web/src/components/Docker/SingleDockerLogViewer.vue b/web/src/components/Docker/SingleDockerLogViewer.vue index 824c991bb0..becf9ae302 100644 --- a/web/src/components/Docker/SingleDockerLogViewer.vue +++ b/web/src/components/Docker/SingleDockerLogViewer.vue @@ -68,6 +68,10 @@ function appendLogLines(newLines: Array<{ timestamp: string; message: string }>) state.lines = [...state.lines, ...added]; if (state.lines.length > MAX_LOG_LINES) { + const removed = state.lines.slice(0, state.lines.length - MAX_LOG_LINES); + for (const line of removed) { + state.lineKeys.delete(`${line.timestamp}|${line.message}`); + } state.lines = state.lines.slice(state.lines.length - MAX_LOG_LINES); } } diff --git a/web/src/components/Logs/SingleLogViewer.vue b/web/src/components/Logs/SingleLogViewer.vue index 9def976f28..54a5ac3295 100644 --- a/web/src/components/Logs/SingleLogViewer.vue +++ b/web/src/components/Logs/SingleLogViewer.vue @@ -281,7 +281,7 @@ const refreshLogContent = async () => { startLogSubscription(); }; -watch(() => props.logFilePath, refreshLogContent); +watch(() => props.logFilePath, refreshLogContent, { immediate: true }); defineExpose({ refreshLogContent }); diff --git a/web/src/components/Notifications/CriticalNotifications.standalone.vue b/web/src/components/Notifications/CriticalNotifications.standalone.vue index 783f7a099a..304ad430d0 100644 --- a/web/src/components/Notifications/CriticalNotifications.standalone.vue +++ b/web/src/components/Notifications/CriticalNotifications.standalone.vue @@ -142,14 +142,16 @@ const dismissNotification = async (notification: NotificationFragmentFragment) = const { onResult: onNotificationAdded } = useSubscription(notificationAddedSubscription); onNotificationAdded(({ data }) => { - if (!data) { + if (!data?.notificationAdded) { return; } - const notification = useFragment(NOTIFICATION_FRAGMENT, data.notificationAdded); + + // Access raw subscription data directly - don't call useFragment in async callback + const rawNotification = data.notificationAdded as unknown as NotificationFragmentFragment; if ( - !notification || - (notification.importance !== NotificationImportance.ALERT && - notification.importance !== NotificationImportance.WARNING) + !rawNotification || + (rawNotification.importance !== NotificationImportance.ALERT && + rawNotification.importance !== NotificationImportance.WARNING) ) { return; } @@ -160,7 +162,7 @@ onNotificationAdded(({ data }) => { return; } - if (notification.timestamp) { + if (rawNotification.timestamp) { // Trigger the global toast in tandem with the subscription update. const funcMapping: Record< NotificationImportance, @@ -170,16 +172,16 @@ onNotificationAdded(({ data }) => { [NotificationImportance.WARNING]: globalThis.toast.warning, [NotificationImportance.INFO]: globalThis.toast.info, }; - const toast = funcMapping[notification.importance]; + const toast = funcMapping[rawNotification.importance]; const createOpener = () => ({ label: 'Open', - onClick: () => notification.link && window.open(notification.link, '_blank', 'noopener'), + onClick: () => rawNotification.link && window.open(rawNotification.link, '_blank', 'noopener'), }); requestAnimationFrame(() => - toast(notification.title, { - description: notification.subject, - action: notification.link ? createOpener() : undefined, + toast(rawNotification.title, { + description: rawNotification.subject, + action: rawNotification.link ? createOpener() : undefined, }) ); } diff --git a/web/src/components/Wrapper/mount-engine.ts b/web/src/components/Wrapper/mount-engine.ts index e4ab478df4..c2da2f5b6e 100644 --- a/web/src/components/Wrapper/mount-engine.ts +++ b/web/src/components/Wrapper/mount-engine.ts @@ -75,6 +75,9 @@ function ensurePortalRoot(): string | undefined { } ensureUnapiScope(portalRoot); + if (isDarkModeActive()) { + portalRoot.classList.add('dark'); + } return `#${PORTAL_ROOT_ID}`; } diff --git a/web/src/composables/useContainerActions.ts b/web/src/composables/useContainerActions.ts index a0f13a4c07..e716491f35 100644 --- a/web/src/composables/useContainerActions.ts +++ b/web/src/composables/useContainerActions.ts @@ -39,12 +39,12 @@ export function useContainerActions(options: ContainerActionOptions const confirmStartStopOpen = ref(false); const confirmToStart = ref<{ name: string }[]>([]); const confirmToStop = ref<{ name: string }[]>([]); - let pendingStartStopIds: string[] = []; + const pendingStartStopIds = ref([]); const confirmPauseResumeOpen = ref(false); const confirmToPause = ref<{ name: string }[]>([]); const confirmToResume = ref<{ name: string }[]>([]); - let pendingPauseResumeIds: string[] = []; + const pendingPauseResumeIds = ref([]); function classifyStartStop(ids: string[]) { const toStart: { id: string; containerId: string; name: string }[] = []; @@ -201,7 +201,7 @@ export function useContainerActions(options: ContainerActionOptions const { toStart, toStop } = classifyStartStop(ids); const isMixed = toStart.length > 0 && toStop.length > 0; if (isMixed) { - pendingStartStopIds = ids; + pendingStartStopIds.value = ids; confirmToStart.value = toStart.map((i) => ({ name: i.name })); confirmToStop.value = toStop.map((i) => ({ name: i.name })); confirmStartStopOpen.value = true; @@ -216,15 +216,15 @@ export function useContainerActions(options: ContainerActionOptions } async function confirmStartStop(close: () => void) { - const { toStart, toStop } = classifyStartStop(pendingStartStopIds); - setRowsBusy(pendingStartStopIds, true); + const { toStart, toStop } = classifyStartStop(pendingStartStopIds.value); + setRowsBusy(pendingStartStopIds.value, true); try { await runStartStopBatch(toStart, toStop); onSuccess?.('Action completed'); } finally { - setRowsBusy(pendingStartStopIds, false); + setRowsBusy(pendingStartStopIds.value, false); confirmStartStopOpen.value = false; - pendingStartStopIds = []; + pendingStartStopIds.value = []; close(); } } @@ -234,7 +234,7 @@ export function useContainerActions(options: ContainerActionOptions const { toPause, toResume } = classifyPauseResume(ids); const isMixed = toPause.length > 0 && toResume.length > 0; if (isMixed) { - pendingPauseResumeIds = ids; + pendingPauseResumeIds.value = ids; confirmToPause.value = toPause.map((i) => ({ name: i.name })); confirmToResume.value = toResume.map((i) => ({ name: i.name })); confirmPauseResumeOpen.value = true; @@ -249,15 +249,15 @@ export function useContainerActions(options: ContainerActionOptions } async function confirmPauseResume(close: () => void) { - const { toPause, toResume } = classifyPauseResume(pendingPauseResumeIds); - setRowsBusy(pendingPauseResumeIds, true); + const { toPause, toResume } = classifyPauseResume(pendingPauseResumeIds.value); + setRowsBusy(pendingPauseResumeIds.value, true); try { await runPauseResumeBatch(toPause, toResume); onSuccess?.('Action completed'); } finally { - setRowsBusy(pendingPauseResumeIds, false); + setRowsBusy(pendingPauseResumeIds.value, false); confirmPauseResumeOpen.value = false; - pendingPauseResumeIds = []; + pendingPauseResumeIds.value = []; close(); } }