Compare commits

...

8 Commits

Author SHA1 Message Date
renovate[bot]
85b947dc40 fix(deps): update apollo graphql packages 2025-12-31 14:40:03 +00:00
Eli Bosley
9ef1cf1eca feat: docker overview (#1855)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes v4.29.2

* **New Features**
* Added iframe mode support with UI adjustments for embedded deployments
  * Introduced modal portal system with dark mode integration

* **Bug Fixes**
* Fixed log line deduplication sync issue in Docker log viewer to
prevent orphaned entries

* **Improvements**
  * Enhanced table header rendering for consistent behavior and resizing
  * Improved notification subscription handling for better stability
  * Optimized log viewer initialization behavior
  * Simplified organizer API parameter handling

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Pujit Mehrotra <pujit@lime-technology.com>
2025-12-22 13:18:16 -05:00
Eli Bosley
a0745e15ca Revert "fix: revert replace docker overview table with web component (7.3+) (#1853)"
This reverts commit 560db880cc.
2025-12-19 15:28:19 -05:00
github-actions[bot]
c39b0b267c chore(main): release 4.29.2 (#1857)
🤖 I have created a release *beep* *boop*
---


## [4.29.2](https://github.com/unraid/api/compare/v4.29.1...v4.29.2)
(2025-12-19)


### Bug Fixes

* unraid-connect plugin not loaded when connect is installed
([#1856](https://github.com/unraid/api/issues/1856))
([73135b8](73135b8328))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 15:13:29 -05:00
Pujit Mehrotra
73135b8328 fix: unraid-connect plugin not loaded when connect is installed (#1856)
Previously, api plugins could only be installed as `peerDependencies` in
the api. This change allows them to be listed as `dependencies` as well.
This makes plugin loading (eg loading Connect) more robust.

Tests:

- [x] Re-logging on 7.3.0-beta.0.5
2025-12-19 15:06:52 -05:00
github-actions[bot]
e42d619b6d chore(main): release 4.29.1 (#1854)
🤖 I have created a release *beep* *boop*
---


## [4.29.1](https://github.com/unraid/api/compare/v4.29.0...v4.29.1)
(2025-12-19)


### Bug Fixes

* revert replace docker overview table with web component (7.3+)
([#1853](https://github.com/unraid/api/issues/1853))
([560db88](560db880cc))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 12:19:44 -05:00
Eli Bosley
560db880cc fix: revert replace docker overview table with web component (7.3+) (#1853)
Reverts unraid/api#1764
2025-12-19 12:12:41 -05:00
github-actions[bot]
d6055f102b chore(main): release 4.29.0 (#1849)
🤖 I have created a release *beep* *boop*
---


## [4.29.0](https://github.com/unraid/api/compare/v4.28.2...v4.29.0)
(2025-12-19)


### Features

* replace docker overview table with web component (7.3+)
([#1764](https://github.com/unraid/api/issues/1764))
([277ac42](277ac42046))


### Bug Fixes

* handle race condition between guid loading and license check
([#1847](https://github.com/unraid/api/issues/1847))
([8b155d1](8b155d1f1c))
* resolve issue with "Continue" button when updating
([#1852](https://github.com/unraid/api/issues/1852))
([d099e75](d099e7521d))
* update myservers config references to connect config references
([#1810](https://github.com/unraid/api/issues/1810))
([e1e3ea7](e1e3ea7eb6))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 11:53:48 -05:00
25 changed files with 252 additions and 572 deletions

View File

@@ -1 +1 @@
{".":"4.28.2"}
{".":"4.29.2"}

View File

@@ -1,5 +1,33 @@
# Changelog
## [4.29.2](https://github.com/unraid/api/compare/v4.29.1...v4.29.2) (2025-12-19)
### Bug Fixes
* unraid-connect plugin not loaded when connect is installed ([#1856](https://github.com/unraid/api/issues/1856)) ([73135b8](https://github.com/unraid/api/commit/73135b832801f5c76d60020161492e4770958c3d))
## [4.29.1](https://github.com/unraid/api/compare/v4.29.0...v4.29.1) (2025-12-19)
### Bug Fixes
* revert replace docker overview table with web component (7.3+) ([#1853](https://github.com/unraid/api/issues/1853)) ([560db88](https://github.com/unraid/api/commit/560db880cc138324f9ff8753f7209b683a84c045))
## [4.29.0](https://github.com/unraid/api/compare/v4.28.2...v4.29.0) (2025-12-19)
### Features
* replace docker overview table with web component (7.3+) ([#1764](https://github.com/unraid/api/issues/1764)) ([277ac42](https://github.com/unraid/api/commit/277ac420464379e7ee6739c4530271caf7717503))
### Bug Fixes
* handle race condition between guid loading and license check ([#1847](https://github.com/unraid/api/issues/1847)) ([8b155d1](https://github.com/unraid/api/commit/8b155d1f1c99bb19efbc9614e000d852e9f0c12d))
* resolve issue with "Continue" button when updating ([#1852](https://github.com/unraid/api/issues/1852)) ([d099e75](https://github.com/unraid/api/commit/d099e7521d2062bb9cf84f340e46b169dd2492c5))
* update myservers config references to connect config references ([#1810](https://github.com/unraid/api/issues/1810)) ([e1e3ea7](https://github.com/unraid/api/commit/e1e3ea7eb68cc6840f67a8aec937fd3740e75b28))
## [4.28.2](https://github.com/unraid/api/compare/v4.28.1...v4.28.2) (2025-12-16)

View File

@@ -1,5 +1,5 @@
{
"version": "4.28.2",
"version": "4.29.2",
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.28.2",
"version": "4.29.2",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
@@ -53,8 +53,8 @@
"unraid-api": "dist/cli.js"
},
"dependencies": {
"@apollo/client": "3.14.0",
"@apollo/server": "4.12.2",
"@apollo/client": "4.0.11",
"@apollo/server": "5.2.0",
"@as-integrations/fastify": "2.1.1",
"@fastify/cookie": "11.0.2",
"@fastify/helmet": "13.0.1",

View File

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

View File

@@ -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();

View File

@@ -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) {

View File

@@ -2,7 +2,6 @@ import {
Field,
Float,
GraphQLISODateTime,
ID,
InputType,
Int,
ObjectType,

View File

@@ -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';

View File

@@ -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);

View File

@@ -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,

View File

@@ -91,13 +91,9 @@ export class PluginService {
return name;
})
);
const { peerDependencies } = getPackageJson();
// All api plugins must be installed as peer dependencies of the unraid-api package
if (!peerDependencies) {
PluginService.logger.warn('Unraid-API peer dependencies not found; skipping plugins.');
return [];
}
const pluginTuples = Object.entries(peerDependencies).filter(
const { peerDependencies = {}, dependencies = {} } = getPackageJson();
const allDependencies = { ...peerDependencies, ...dependencies };
const pluginTuples = Object.entries(allDependencies).filter(
(entry): entry is [string, string] => {
const [pkgName, version] = entry;
return pluginNames.has(pkgName) && typeof version === 'string';

View File

@@ -1,7 +1,7 @@
{
"name": "unraid-monorepo",
"private": true,
"version": "4.28.2",
"version": "4.29.2",
"scripts": {
"build": "pnpm -r build",
"build:watch": "pnpm -r --parallel --filter '!@unraid/ui' build:watch",

View File

@@ -24,7 +24,7 @@
"license": "GPL-2.0-or-later",
"description": "Unraid Connect plugin for Unraid API",
"devDependencies": {
"@apollo/client": "3.14.0",
"@apollo/client": "4.0.11",
"@faker-js/faker": "10.0.0",
"@graphql-codegen/cli": "6.0.0",
"@graphql-typed-document-node/core": "3.2.0",
@@ -73,7 +73,7 @@
"node-cache": "5.1.2"
},
"peerDependencies": {
"@apollo/client": "3.14.0",
"@apollo/client": "4.0.11",
"@graphql-typed-document-node/core": "3.2.0",
"@jsonforms/core": "3.6.0",
"@nestjs/apollo": "13.1.0",

View File

@@ -54,7 +54,7 @@
"ws": "8.18.3"
},
"peerDependencies": {
"@apollo/client": "3.14.0",
"@apollo/client": "4.0.11",
"@graphql-tools/utils": "10.9.1",
"@jsonforms/core": "3.6.0",
"@nestjs/common": "11.1.6",

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/connect-plugin",
"version": "4.28.2",
"version": "4.29.2",
"private": true,
"dependencies": {
"commander": "14.0.0",

675
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/ui",
"version": "4.28.2",
"version": "4.29.2",
"private": true,
"license": "GPL-2.0-or-later",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/web",
"version": "4.28.2",
"version": "4.29.2",
"private": true,
"type": "module",
"license": "GPL-2.0-or-later",
@@ -98,7 +98,7 @@
"vue-tsc": "3.0.6"
},
"dependencies": {
"@apollo/client": "3.14.0",
"@apollo/client": "4.0.11",
"@floating-ui/dom": "1.7.4",
"@floating-ui/utils": "0.2.10",
"@floating-ui/vue": "1.1.9",

View File

@@ -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<TableColumn<TreeRow<T>>[]>(() => {
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<T> }) => {
const cellFn = (col as { cell: (args: unknown) => VNode | string | number }).cell;

View File

@@ -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);
}
}

View File

@@ -281,7 +281,7 @@ const refreshLogContent = async () => {
startLogSubscription();
};
watch(() => props.logFilePath, refreshLogContent);
watch(() => props.logFilePath, refreshLogContent, { immediate: true });
defineExpose({ refreshLogContent });
</script>

View File

@@ -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,
})
);
}

View File

@@ -75,6 +75,9 @@ function ensurePortalRoot(): string | undefined {
}
ensureUnapiScope(portalRoot);
if (isDarkModeActive()) {
portalRoot.classList.add('dark');
}
return `#${PORTAL_ROOT_ID}`;
}

View File

@@ -39,12 +39,12 @@ export function useContainerActions<T = unknown>(options: ContainerActionOptions
const confirmStartStopOpen = ref(false);
const confirmToStart = ref<{ name: string }[]>([]);
const confirmToStop = ref<{ name: string }[]>([]);
let pendingStartStopIds: string[] = [];
const pendingStartStopIds = ref<string[]>([]);
const confirmPauseResumeOpen = ref(false);
const confirmToPause = ref<{ name: string }[]>([]);
const confirmToResume = ref<{ name: string }[]>([]);
let pendingPauseResumeIds: string[] = [];
const pendingPauseResumeIds = ref<string[]>([]);
function classifyStartStop(ids: string[]) {
const toStart: { id: string; containerId: string; name: string }[] = [];
@@ -201,7 +201,7 @@ export function useContainerActions<T = unknown>(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<T = unknown>(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<T = unknown>(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<T = unknown>(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();
}
}