From 8ee4fe4d68e20ab3045baed35f6e29ac4eb66f17 Mon Sep 17 00:00:00 2001 From: jarek Date: Mon, 16 Feb 2026 08:46:56 +0100 Subject: [PATCH] 1.0.18 --- Dockerfile | 10 +- package.json | 2 +- src/lib/components/BatchOperationModal.svelte | 12 +- src/lib/components/CommandPalette.svelte | 4 +- .../ui/error-dialog/error-dialog.svelte | 21 +- src/lib/data/changelog.json | 21 +- src/lib/data/dependencies.json | 8 +- src/lib/server/db.ts | 3 +- src/lib/server/docker.ts | 77 ++- src/lib/server/git.ts | 92 ++- src/lib/server/host-path.ts | 2 +- src/lib/server/notifications.ts | 10 +- .../scheduler/tasks/env-update-check.ts | 15 +- src/lib/server/scheduler/tasks/image-prune.ts | 14 +- src/lib/server/stack-scanner.ts | 4 +- src/lib/server/stacks.ts | 61 +- .../server/subprocesses/event-subprocess.ts | 26 +- .../server/subprocesses/metrics-subprocess.ts | 6 + src/lib/utils/clipboard.ts | 34 + .../api/containers/check-updates/+server.ts | 17 +- .../api/dashboard/stats/stream/+server.ts | 12 +- .../api/environments/[id]/test/+server.ts | 33 +- .../api/git/stacks/[id]/webhook/+server.ts | 27 +- src/routes/api/git/webhook/[id]/+server.ts | 27 +- src/routes/api/notifications/+server.ts | 10 +- src/routes/api/self-update/+server.ts | 410 +++++++++++ src/routes/api/self-update/check/+server.ts | 142 ++++ .../api/self-update/progress/+server.ts | 108 +++ .../api/stacks/[name]/env/raw/+server.ts | 2 +- src/routes/containers/+page.svelte | 55 +- .../containers/ContainerInspectModal.svelte | 38 +- src/routes/images/+page.svelte | 47 +- src/routes/logs/+page.svelte | 7 +- src/routes/logs/LogViewer.svelte | 7 +- src/routes/logs/LogsPanel.svelte | 7 +- src/routes/networks/+page.svelte | 7 +- src/routes/profile/MfaSetupModal.svelte | 28 +- .../registry/CopyToRegistryModal.svelte | 19 +- src/routes/settings/about/AboutTab.svelte | 101 ++- .../settings/about/SelfUpdateDialog.svelte | 654 ++++++++++++++++++ .../environments/EnvironmentModal.svelte | 60 +- src/routes/stacks/+page.svelte | 30 +- src/routes/stacks/GitStackModal.svelte | 46 +- src/routes/stacks/ImportStackModal.svelte | 3 +- src/routes/stacks/PathBarItem.svelte | 16 +- src/routes/stacks/StackModal.svelte | 33 +- src/routes/terminal/Terminal.svelte | 7 +- src/routes/terminal/TerminalEmulator.svelte | 7 +- svelte.config.js | 5 +- 49 files changed, 2126 insertions(+), 261 deletions(-) create mode 100644 src/lib/utils/clipboard.ts create mode 100644 src/routes/api/self-update/+server.ts create mode 100644 src/routes/api/self-update/check/+server.ts create mode 100644 src/routes/api/self-update/progress/+server.ts create mode 100644 src/routes/settings/about/SelfUpdateDialog.svelte diff --git a/Dockerfile b/Dockerfile index c51b0cd..a0e3299 100644 --- a/Dockerfile +++ b/Dockerfile @@ -54,6 +54,7 @@ RUN APKO_ARCH=$([ "$TARGETARCH" = "arm64" ] && echo "aarch64" || echo "x86_64") " - postgresql-client" \ " - git" \ " - openssh-client" \ + " - openssh-keygen" \ " - curl" \ " - tini" \ " - su-exec" \ @@ -86,7 +87,9 @@ ARG TARGETARCH WORKDIR /app # Install build dependencies -RUN apt-get update && apt-get install -y --no-install-recommends jq git curl unzip ca-certificates && rm -rf /var/lib/apt/lists/* +# libnss-wrapper: needed for git SSH with arbitrary UIDs on read-only containers (getpwuid workaround) +RUN apt-get update && apt-get install -y --no-install-recommends jq git curl unzip ca-certificates libnss-wrapper && rm -rf /var/lib/apt/lists/* \ + && cp "$(dpkg -L libnss-wrapper | grep 'libnss_wrapper\.so$')" /usr/local/lib/libnss_wrapper.so # Copy package files and install ALL dependencies (needed for build) COPY package.json bun.lock* bunfig.toml ./ @@ -95,7 +98,7 @@ RUN bun install --frozen-lockfile # Copy source code and build COPY . . -# Build with parallelism - dedicated build VM has 16 CPUs and 32GB RAM +# Build the application RUN NODE_OPTIONS="--max-old-space-size=8192 --max-semi-space-size=128" bun run build # Prepare production node_modules (do this in builder where we have compilers) @@ -130,6 +133,9 @@ COPY --from=os-builder /work/rootfs/ / # For regular builds, this contains the standard oven/bun binary COPY --from=app-builder /usr/local/bin/bun /usr/bin/bun +# Copy libnss_wrapper for git SSH with arbitrary UIDs (same cross-copy pattern as Bun above) +COPY --from=app-builder /usr/local/lib/libnss_wrapper.so /usr/lib/libnss_wrapper.so + WORKDIR /app # Set up environment variables diff --git a/package.json b/package.json index ea8dd09..d569215 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dockhand", "private": true, - "version": "1.0.17", + "version": "1.0.18", "type": "module", "scripts": { "dev": "bunx --bun vite dev", diff --git a/src/lib/components/BatchOperationModal.svelte b/src/lib/components/BatchOperationModal.svelte index fa25134..304b0a9 100644 --- a/src/lib/components/BatchOperationModal.svelte +++ b/src/lib/components/BatchOperationModal.svelte @@ -5,6 +5,14 @@ import { Check, X, Loader2, Circle, Ban } from 'lucide-svelte'; import { onDestroy } from 'svelte'; + function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + } + const progressText: Record = { remove: 'removing', start: 'starting', @@ -30,6 +38,7 @@ items: Array<{ id: string; name: string }>; envId?: number; options?: Record; + totalSize?: number; onClose: () => void; onComplete: () => void; } @@ -42,6 +51,7 @@ items, envId, options = {}, + totalSize, onClose, onComplete }: Props = $props(); @@ -233,7 +243,7 @@ {#if isRunning} Processing {items.length} {entityType}... {:else if isComplete} - Completed: {successCount} succeeded{#if failCount > 0}, {failCount} failed{/if}{#if cancelledCount > 0}, {cancelledCount} cancelled{/if} + Completed: {successCount} succeeded{#if failCount > 0}, {failCount} failed{/if}{#if cancelledCount > 0}, {cancelledCount} cancelled{/if}{#if totalSize && successCount > 0} ({formatBytes(totalSize)}){/if} {:else} Preparing to {operation} {items.length} {entityType}... {/if} diff --git a/src/lib/components/CommandPalette.svelte b/src/lib/components/CommandPalette.svelte index 9934c7f..58a51f9 100644 --- a/src/lib/components/CommandPalette.svelte +++ b/src/lib/components/CommandPalette.svelte @@ -1,5 +1,5 @@ + + { if (!isOpen) handleClose(); }}> + { if (!canClose) e.preventDefault(); }}> + + + + {#if phase === 'confirm'} + Update Dockhand + {:else} + Updating Dockhand + {/if} + + {#if phase !== 'confirm'} + + {#if activeStep} + {activeStep.label}... + ({completedCount}/{ALL_STEPS.length}) + {:else if phase === 'completed'} + Update complete + {:else if phase === 'error'} + Update failed + {:else} + Preparing... + {/if} + + {/if} + + + {#if phase === 'confirm'} + +
+
+
+ Container + + + {containerName} + +
+
+ Image + {currentImage} +
+ {#if currentDigest || newDigest} +
+ Current digest + {currentDigest ? currentDigest.replace('sha256:', '').slice(0, 12) : 'unknown'} +
+
+ New digest + {newDigest ? newDigest.replace('sha256:', '').slice(0, 12) : 'unknown'} +
+ {/if} +
+ + {#if loadingNotes} +
+ + Loading release notes... +
+ {:else if releaseNotes.length > 0} +
+
+

What's new

+
+
+ {#each releaseNotes as entry} +
+
+ v{entry.version} + {entry.date} +
+
    + {#each entry.changes as change} + {@const ChangeIcon = getChangeIcon(change.type)} +
  • + + {change.text} +
  • + {/each} +
+
+ {/each} +
+
+ {/if} + + {#if isComposeManaged} +
+

+ Note: This container is managed by Docker Compose. After update it will continue to work but may lose Compose tracking. Use docker compose pull && docker compose up -d for Compose-aware updates. +

+
+ {/if} +
+ + + + + + + {:else} + +
+ +
+
+ Progress + {completedCount}/{ALL_STEPS.length} +
+ +
+ + + {#if visibleSteps.length > 0} +
+ {#each visibleSteps as step (step.id)} + {@const StepIcon = getIconComponent(step.status)} + {@const hasLogs = step.logs.length > 0} +
+ +
+ +
+
{step.label}
+
+ {#if step.status === 'completed'} + + {:else if step.status === 'error'} + + {/if} +
+ + + {#if hasLogs} +
+ {#each step.logs as line} +
{line}
+ {/each} +
+ {/if} +
+ {/each} +
+ {/if} + + + {#if phase === 'error' && errorMessage} +
+ + {errorMessage} +
+ {/if} +
+ + + {#if phase === 'completed'} + + {:else if phase === 'error'} + + {:else} + + {/if} + + {/if} +
+
diff --git a/src/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte index 78114f6..4cda99b 100644 --- a/src/routes/settings/environments/EnvironmentModal.svelte +++ b/src/routes/settings/environments/EnvironmentModal.svelte @@ -57,7 +57,8 @@ X, Tags, ChevronDown, - ChevronRight + ChevronRight, + XCircle } from 'lucide-svelte'; import * as Tooltip from '$lib/components/ui/tooltip'; import * as Alert from '$lib/components/ui/alert'; @@ -70,6 +71,7 @@ import { TogglePill, ToggleGroup } from '$lib/components/ui/toggle-pill'; import { ShieldOff } from 'lucide-svelte'; import { focusFirstInput } from '$lib/utils'; + import { copyToClipboard } from '$lib/utils/clipboard'; import { authStore, canAccess } from '$lib/stores/auth'; import { licenseStore } from '$lib/stores/license'; import { formatDateTime, formatDate } from '$lib/stores/settings'; @@ -321,8 +323,8 @@ let hawserTokenLoading = $state(false); let generatingToken = $state(false); let generatedToken = $state(null); // Full token shown once after generation - let copySuccess = $state(false); - let copyCmdSuccess = $state(false); + let copySuccess = $state<'ok' | 'error' | null>(null); + let copyCmdSuccess = $state<'ok' | 'error' | null>(null); // For add mode - auto-generated token stored until save let pendingToken = $state(null); @@ -1268,17 +1270,17 @@ await generateHawserToken(envId); } - function copyToken(token: string) { - navigator.clipboard.writeText(token); - copySuccess = true; - setTimeout(() => { copySuccess = false; }, 2000); + async function copyToken(token: string) { + const ok = await copyToClipboard(token); + copySuccess = ok ? 'ok' : 'error'; + setTimeout(() => { copySuccess = null; }, 2000); } - function copyCommand(token: string) { + async function copyCommand(token: string) { const cmd = `DOCKHAND_SERVER_URL=${getConnectionUrl()} TOKEN=${token} hawser`; - navigator.clipboard.writeText(cmd); - copyCmdSuccess = true; - setTimeout(() => { copyCmdSuccess = false; }, 2000); + const ok = await copyToClipboard(cmd); + copyCmdSuccess = ok ? 'ok' : 'error'; + setTimeout(() => { copyCmdSuccess = null; }, 2000); } function getConnectionUrl() { @@ -1883,7 +1885,14 @@ class="font-mono text-xs flex-1" />