From b55e1e5aadb9eb856efa45b039da79c8173a5b75 Mon Sep 17 00:00:00 2001 From: jarek Date: Sat, 21 Mar 2026 14:29:30 +0100 Subject: [PATCH] v1.0.22 --- VERSION | 2 +- docker-entrypoint-node.sh | 25 +- docker-entrypoint.sh | 18 +- package.json | 2 +- src/lib/components/AvatarCropper.svelte | 44 ++- src/lib/components/EnvironmentIcon.svelte | 23 ++ src/lib/components/ThemeSelector.svelte | 16 +- src/lib/components/TimezoneSelector.svelte | 17 +- src/lib/components/host-info.svelte | 71 ++++- src/lib/config/grid-columns.ts | 20 +- src/lib/data/changelog.json | 21 ++ src/lib/data/dependencies.json | 16 +- src/lib/server/db.ts | 23 +- src/lib/server/dns-dispatcher.ts | 70 +++-- src/lib/server/docker-validation.ts | 20 ++ src/lib/server/docker.ts | 182 +++++++++-- src/lib/server/env-icons.ts | 36 +++ src/lib/server/git.ts | 16 +- src/lib/server/notifications.ts | 50 +-- src/lib/server/scanner.ts | 91 ++++-- src/lib/stores/dashboard.ts | 36 ++- src/lib/stores/settings.ts | 55 +++- src/lib/stores/theme.ts | 4 +- src/lib/themes.ts | 1 - src/lib/types.ts | 4 +- src/lib/utils/clipboard.ts | 1 + src/lib/utils/icons.ts | 4 + src/routes/+layout.svelte | 12 +- src/routes/+page.svelte | 135 +++++++- src/routes/activity/+page.svelte | 15 +- .../api/auth/oidc/[id]/initiate/+server.ts | 4 +- src/routes/api/auth/oidc/[id]/test/+server.ts | 2 +- src/routes/api/auth/oidc/callback/+server.ts | 2 +- src/routes/api/containers/[id]/+server.ts | 7 + .../api/containers/[id]/exec/+server.ts | 4 + .../api/containers/[id]/files/+server.ts | 4 + .../containers/[id]/files/chmod/+server.ts | 4 + .../containers/[id]/files/content/+server.ts | 7 + .../containers/[id]/files/create/+server.ts | 4 + .../containers/[id]/files/delete/+server.ts | 4 + .../containers/[id]/files/download/+server.ts | 4 + .../containers/[id]/files/rename/+server.ts | 4 + .../containers/[id]/files/upload/+server.ts | 4 + .../api/containers/[id]/inspect/+server.ts | 4 + .../api/containers/[id]/logs/+server.ts | 4 + .../containers/[id]/logs/stream/+server.ts | 4 + .../api/containers/[id]/pause/+server.ts | 4 + .../api/containers/[id]/rename/+server.ts | 4 + .../api/containers/[id]/restart/+server.ts | 4 + .../api/containers/[id]/shells/+server.ts | 4 + .../api/containers/[id]/start/+server.ts | 4 + .../api/containers/[id]/stats/+server.ts | 4 + .../api/containers/[id]/stop/+server.ts | 4 + src/routes/api/containers/[id]/top/+server.ts | 4 + .../api/containers/[id]/unpause/+server.ts | 4 + .../api/containers/[id]/update/+server.ts | 4 + .../containers/batch-update-stream/+server.ts | 31 +- .../api/dashboard/preferences/+server.ts | 87 ++++-- src/routes/api/dependencies/+server.ts | 10 +- src/routes/api/environments/[id]/+server.ts | 4 + .../api/environments/[id]/icon/+server.ts | 68 ++++ .../api/environments/[id]/timezone/+server.ts | 18 +- .../api/git/repositories/[id]/+server.ts | 13 +- .../api/git/stacks/[id]/webhook/+server.ts | 8 +- src/routes/api/git/webhook/[id]/+server.ts | 8 +- src/routes/api/images/[id]/+server.ts | 4 + src/routes/api/images/[id]/export/+server.ts | 4 + src/routes/api/images/[id]/history/+server.ts | 4 + src/routes/api/images/[id]/tag/+server.ts | 4 + src/routes/api/legal/license/+server.ts | 4 +- src/routes/api/legal/privacy/+server.ts | 4 +- src/routes/api/networks/[id]/+server.ts | 7 + .../api/networks/[id]/connect/+server.ts | 7 + .../api/networks/[id]/disconnect/+server.ts | 7 + .../api/networks/[id]/inspect/+server.ts | 4 + src/routes/api/self-update/+server.ts | 27 +- src/routes/api/self-update/check/+server.ts | 21 ++ src/routes/api/settings/general/+server.ts | 54 +++- src/routes/api/settings/scanner/+server.ts | 25 +- src/routes/api/system/files/+server.ts | 47 ++- src/routes/api/volumes/[name]/+server.ts | 7 + .../api/volumes/[name]/browse/+server.ts | 4 + .../volumes/[name]/browse/content/+server.ts | 4 + .../volumes/[name]/browse/release/+server.ts | 4 + .../api/volumes/[name]/clone/+server.ts | 49 ++- .../api/volumes/[name]/export/+server.ts | 4 + .../api/volumes/[name]/inspect/+server.ts | 4 + src/routes/audit/+page.svelte | 15 +- src/routes/containers/+page.svelte | 60 +++- src/routes/containers/BatchUpdateModal.svelte | 50 +-- src/routes/dashboard/DraggableGrid.svelte | 24 +- .../dashboard/EnvironmentListView.svelte | 293 ++++++++++++++++++ src/routes/dashboard/EnvironmentTile.svelte | 14 +- src/routes/dashboard/dashboard-header.svelte | 8 +- src/routes/logs/+page.svelte | 21 +- src/routes/logs/LogViewer.svelte | 19 +- src/routes/logs/LogsPanel.svelte | 24 +- src/routes/schedules/+page.svelte | 5 +- .../settings/auth/roles/RoleModal.svelte | 5 +- .../settings/auth/roles/RolesSubTab.svelte | 5 +- .../environments/EnvironmentModal.svelte | 136 +++++++- .../environments/EnvironmentsTab.svelte | 5 +- .../environments/EventTypesEditor.svelte | 3 + src/routes/settings/general/GeneralTab.svelte | 141 ++++++--- .../settings/general/ScanResultsModal.svelte | 22 +- src/routes/stacks/FilesystemBrowser.svelte | 101 +++++- src/routes/stacks/ImportStackModal.svelte | 5 +- 107 files changed, 2194 insertions(+), 444 deletions(-) create mode 100644 src/lib/components/EnvironmentIcon.svelte create mode 100644 src/lib/server/docker-validation.ts create mode 100644 src/lib/server/env-icons.ts create mode 100644 src/routes/api/environments/[id]/icon/+server.ts create mode 100644 src/routes/dashboard/EnvironmentListView.svelte diff --git a/VERSION b/VERSION index 4b296b2..90c4f8c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.0.21 +v1.0.22 diff --git a/docker-entrypoint-node.sh b/docker-entrypoint-node.sh index 16e65ca..fe01600 100644 --- a/docker-entrypoint-node.sh +++ b/docker-entrypoint-node.sh @@ -10,10 +10,12 @@ PGID=${PGID:-1001} export BODY_SIZE_LIMIT=${BODY_SIZE_LIMIT:-2G} # Default command (--expose-gc allows forced GC from /api/debug/memory?gc=true) +# Custom CA: set NODE_EXTRA_CA_CERTS=/path/to/ca.crt (appends to built-in CAs) +# Enterprise (system CA store): set NODE_OPTIONS="--use-openssl-ca" if [ "$MEMORY_MONITOR" = "true" ]; then - DEFAULT_CMD="node --use-openssl-ca --dns-result-order=ipv4first --no-network-family-autoselection --expose-gc /app/server.js" + DEFAULT_CMD="node --dns-result-order=ipv4first --no-network-family-autoselection --expose-gc /app/server.js" else - DEFAULT_CMD="node --use-openssl-ca --dns-result-order=ipv4first --no-network-family-autoselection /app/server.js" + DEFAULT_CMD="node --dns-result-order=ipv4first --no-network-family-autoselection /app/server.js" fi # === Detect if running as root === @@ -100,14 +102,29 @@ else fi fi - chown -R "$RUN_USER":"$RUN_USER" /app/data 2>/dev/null || true + # === Directory Ownership === + # Only chown Dockhand's own subdirectories, not the entire /app/data tree. + # Recursive chown on /app/data breaks stack volumes mounted with relative paths + # (e.g. ./postgresql:/var/lib/postgresql) that need different ownership (#719). + DATA_DIR="${DATA_DIR:-/app/data}" + chown "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + for subdir in db stacks git-repos tmp icons snapshots scanner-cache; do + if [ -d "$DATA_DIR/$subdir" ]; then + chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR/$subdir" 2>/dev/null || true + fi + done if [ "$RUN_USER" = "dockhand" ]; then chown -R dockhand:dockhand /home/dockhand 2>/dev/null || true fi if [ -n "$DATA_DIR" ] && [ "$DATA_DIR" != "/app/data" ] && [ "$DATA_DIR" != "./data" ]; then mkdir -p "$DATA_DIR" - chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + chown "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + for subdir in db stacks git-repos tmp icons snapshots scanner-cache; do + if [ -d "$DATA_DIR/$subdir" ]; then + chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR/$subdir" 2>/dev/null || true + fi + done fi fi diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 8de5670..c224dbf 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -113,14 +113,28 @@ else fi # === Directory Ownership === - chown -R "$RUN_USER":"$RUN_USER" /app/data 2>/dev/null || true + # Only chown Dockhand's own subdirectories, not the entire /app/data tree. + # Recursive chown on /app/data breaks stack volumes mounted with relative paths + # (e.g. ./postgresql:/var/lib/postgresql) that need different ownership (#719). + DATA_DIR="${DATA_DIR:-/app/data}" + chown "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + for subdir in db stacks git-repos tmp icons snapshots scanner-cache; do + if [ -d "$DATA_DIR/$subdir" ]; then + chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR/$subdir" 2>/dev/null || true + fi + done if [ "$RUN_USER" = "dockhand" ]; then chown -R dockhand:dockhand /home/dockhand 2>/dev/null || true fi if [ -n "$DATA_DIR" ] && [ "$DATA_DIR" != "/app/data" ] && [ "$DATA_DIR" != "./data" ]; then mkdir -p "$DATA_DIR" - chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + chown "$RUN_USER":"$RUN_USER" "$DATA_DIR" 2>/dev/null || true + for subdir in db stacks git-repos tmp icons snapshots scanner-cache; do + if [ -d "$DATA_DIR/$subdir" ]; then + chown -R "$RUN_USER":"$RUN_USER" "$DATA_DIR/$subdir" 2>/dev/null || true + fi + done fi fi diff --git a/package.json b/package.json index f129cbc..af8de12 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dockhand", "private": true, - "version": "1.0.21", + "version": "1.0.22", "type": "module", "scripts": { "dev": "npx vite dev", diff --git a/src/lib/components/AvatarCropper.svelte b/src/lib/components/AvatarCropper.svelte index 96576de..71b8d9c 100644 --- a/src/lib/components/AvatarCropper.svelte +++ b/src/lib/components/AvatarCropper.svelte @@ -8,9 +8,26 @@ imageUrl: string; onCancel: () => void; onSave: (dataUrl: string) => void; + cropShape?: 'round' | 'rect'; + outputSize?: number; + outputFormat?: 'image/jpeg' | 'image/webp'; + outputQuality?: number; + title?: string; + saveLabel?: string; } - let { show, imageUrl, onCancel, onSave }: Props = $props(); + let { + show, + imageUrl, + onCancel, + onSave, + cropShape = 'round', + outputSize = 256, + outputFormat = 'image/jpeg', + outputQuality = 0.9, + title = 'Crop avatar', + saveLabel = 'Save avatar' + }: Props = $props(); // Cropper state let crop = $state({ x: 0, y: 0 }); @@ -144,9 +161,9 @@ return; } - // Set canvas size to output size (256x256 for avatar) - canvas.width = 256; - canvas.height = 256; + // Set canvas size to output size + canvas.width = outputSize; + canvas.height = outputSize; // Ensure we use a square crop area to avoid stretching // Center the square within the original crop area @@ -163,12 +180,12 @@ size, 0, 0, - 256, - 256 + outputSize, + outputSize ); // Convert to data URL - const dataUrl = canvas.toDataURL('image/jpeg', 0.9); + const dataUrl = canvas.toDataURL(outputFormat, outputQuality); resolve(dataUrl); }; @@ -204,16 +221,18 @@ handleCancel(); } } + + {#if show && imageUrl} -
+
-

Crop avatar

+

{title}

Drag to reposition. Use the slider to zoom.

@@ -226,7 +245,8 @@ bind:crop bind:zoom aspect={1} - cropShape="round" + minZoom={0.5} + cropShape={cropShape} showGrid={false} on:cropcomplete={onCropComplete} on:mediaLoaded={onMediaLoaded} @@ -239,7 +259,7 @@ - {saving ? 'Uploading...' : !imageLoaded ? 'Loading...' : 'Save avatar'} + {saving ? 'Uploading...' : !imageLoaded ? 'Loading...' : saveLabel}
diff --git a/src/lib/components/EnvironmentIcon.svelte b/src/lib/components/EnvironmentIcon.svelte new file mode 100644 index 0000000..669aedf --- /dev/null +++ b/src/lib/components/EnvironmentIcon.svelte @@ -0,0 +1,23 @@ + + +{#if isCustom} + +{:else if LucideIcon} + +{/if} diff --git a/src/lib/components/ThemeSelector.svelte b/src/lib/components/ThemeSelector.svelte index 9a65a60..f186a74 100644 --- a/src/lib/components/ThemeSelector.svelte +++ b/src/lib/components/ThemeSelector.svelte @@ -43,15 +43,17 @@ let selectedEditorFont = $state('system-mono'); onMount(async () => { - // Load monospace fonts for dropdown previews + // Load bundled monospace fonts for dropdown previews const fontsToLoad = monospaceFonts.filter(f => f.googleFont); if (fontsToLoad.length > 0) { - const families = fontsToLoad.map(f => `family=${f.googleFont}`).join('&'); - const link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = `https://fonts.googleapis.com/css2?${families}&display=swap`; - link.onload = () => { monoFontsLoaded = true; }; - document.head.appendChild(link); + let loaded = 0; + for (const font of fontsToLoad) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = `/fonts/${font.id}/font.css`; + link.onload = () => { if (++loaded >= fontsToLoad.length) monoFontsLoaded = true; }; + document.head.appendChild(link); + } } else { monoFontsLoaded = true; } diff --git a/src/lib/components/TimezoneSelector.svelte b/src/lib/components/TimezoneSelector.svelte index 72e9547..2596c17 100644 --- a/src/lib/components/TimezoneSelector.svelte +++ b/src/lib/components/TimezoneSelector.svelte @@ -29,7 +29,22 @@ 'Europe/Kyiv': 'Europe/Kiev', 'Asia/Ho_Chi_Minh': 'Asia/Saigon', 'America/Nuuk': 'America/Godthab', - 'Pacific/Kanton': 'Pacific/Enderbury' + 'Pacific/Kanton': 'Pacific/Enderbury', + 'Asia/Kolkata': 'Asia/Calcutta', + 'Asia/Kathmandu': 'Asia/Katmandu', + 'Asia/Yangon': 'Asia/Rangoon', + 'Asia/Kashgar': 'Asia/Urumqi', + 'Atlantic/Faroe': 'Atlantic/Faeroe', + 'Europe/Uzhgorod': 'Europe/Kiev', + 'Europe/Zaporozhye': 'Europe/Kiev', + 'America/Atikokan': 'America/Coral_Harbour', + 'America/Argentina/Buenos_Aires': 'America/Buenos_Aires', + 'America/Argentina/Catamarca': 'America/Catamarca', + 'America/Argentina/Cordoba': 'America/Cordoba', + 'America/Argentina/Jujuy': 'America/Jujuy', + 'America/Argentina/Mendoza': 'America/Mendoza', + 'Pacific/Pohnpei': 'Pacific/Ponape', + 'Pacific/Chuuk': 'Pacific/Truk' }; // Reverse map: canonical → modern alias names (for display hints) diff --git a/src/lib/components/host-info.svelte b/src/lib/components/host-info.svelte index ef3a4bf..746e243 100644 --- a/src/lib/components/host-info.svelte +++ b/src/lib/components/host-info.svelte @@ -1,11 +1,11 @@ diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 50a80f0..682502a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,7 +5,7 @@ + +
+ + { sortState = state; }} + onRowClick={(tile, e) => onrowclick?.(tile.id)} + rowHeight={36} + > + {#snippet cell(column, tile, rowState)} + {@const s = tile.stats} + {@const isOnline = s?.online === true} + {@const isOffline = s?.online === false} + + {#if column.id === 'status'} + {#if tile.loading && !s} + + {:else if isOnline} + + {:else if isOffline} + + {:else} + + {/if} + + {:else if column.id === 'name'} + {#if s} +
+ + {s.name} +
+ {:else if tile.info} +
+ + {tile.info.name} +
+ {:else} +
+ {/if} + + {:else if column.id === 'connection'} + {#if s} +
+ {#if s.connectionType === 'hawser-standard'} + + {:else if s.connectionType === 'hawser-edge'} + + {:else if s.connectionType === 'direct'} + + {:else} + + {/if} + {connectionLabel(s.connectionType)} +
+ {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'host'} + {#if s?.host && !isOffline} + + {s.host}{s.port ? `:${s.port}` : ''} + + {:else if s?.socketPath} + + {s.socketPath} + + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'containers'} + {#if s && !isOffline} + {s.containers.running} + / {s.containers.total} + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'updates'} + {#if s && !isOffline} + {#if s.containers.pendingUpdates > 0} +
+ + {s.containers.pendingUpdates} +
+ {:else} + 0 + {/if} + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'cpu'} + {#if s?.metrics && !isOffline} +
+
+
+
+ {formatPercent(s.metrics.cpuPercent)} +
+ {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'memory'} + {#if s?.metrics && !isOffline} +
+
+
+
+ {formatPercent(s.metrics.memoryPercent)} +
+ {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'images'} + {#if s && !isOffline} + {s.images.total} + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'volumes'} + {#if s && !isOffline} + {s.volumes.total} + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'stacks'} + {#if s && !isOffline} + {s.stacks.running} + / {s.stacks.total} + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'events'} + {#if s && !isOffline} + {s.events.today} + {:else if tile.loading} +
+ {:else} + - + {/if} + + {:else if column.id === 'labels'} + {#if s?.labels && s.labels.length > 0} +
+ {#each s.labels as label} + {@const colors = getLabelColors(label)} + + {label} + + {/each} +
+ {/if} + {/if} + {/snippet} + +
diff --git a/src/routes/dashboard/EnvironmentTile.svelte b/src/routes/dashboard/EnvironmentTile.svelte index 1f170f3..0a80a15 100644 --- a/src/routes/dashboard/EnvironmentTile.svelte +++ b/src/routes/dashboard/EnvironmentTile.svelte @@ -2,7 +2,7 @@ import * as Card from '$lib/components/ui/card'; import { Wifi, WifiOff, ShieldCheck, Activity, Cpu, Settings, Unplug, Icon, Route, UndoDot, CircleArrowUp, CircleFadingArrowUp, Loader2 } from 'lucide-svelte'; import { whale } from '@lucide/lab'; - import { getIconComponent } from '$lib/utils/icons'; + import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte'; import { goto } from '$app/navigation'; import { canAccess } from '$lib/stores/auth'; import type { EnvironmentStats } from '../api/dashboard/stats/+server'; @@ -31,8 +31,6 @@ let { stats, width = 1, height = 1, oneventsclick, showStacksBreakdown = true }: Props = $props(); - const EnvIcon = $derived(getIconComponent(stats.icon)); - // Specific tile size conditionals for easy customization const is1x1 = $derived(width === 1 && height === 1); const is1x2 = $derived(width === 1 && height === 2); @@ -64,7 +62,7 @@
- +
{#if stats.connectionType === 'socket' || !stats.connectionType} @@ -160,7 +158,7 @@
- +
{#if stats.connectionType === 'socket' || !stats.connectionType} @@ -263,7 +261,7 @@
- +
{#if stats.connectionType === 'socket' || !stats.connectionType} @@ -364,7 +362,7 @@
- +
{#if stats.connectionType === 'socket' || !stats.connectionType} @@ -468,7 +466,7 @@
- +
{#if stats.connectionType === 'socket' || !stats.connectionType} diff --git a/src/routes/dashboard/dashboard-header.svelte b/src/routes/dashboard/dashboard-header.svelte index 2bc35eb..95eeb2f 100644 --- a/src/routes/dashboard/dashboard-header.svelte +++ b/src/routes/dashboard/dashboard-header.svelte @@ -15,10 +15,9 @@ Loader2 } from 'lucide-svelte'; import { whale } from '@lucide/lab'; - import { getIconComponent } from '$lib/utils/icons'; + import EnvironmentIcon from '$lib/components/EnvironmentIcon.svelte'; import { goto } from '$app/navigation'; import { canAccess } from '$lib/stores/auth'; - import type { Component } from 'svelte'; type ConnectionType = 'socket' | 'direct' | 'hawser-standard' | 'hawser-edge'; @@ -71,7 +70,6 @@ (port ? `${host}:${port}` : host || 'Unknown host') ); - const EnvIcon = $derived(getIconComponent(icon)) as Component; const canEdit = $derived($canAccess('environments', 'edit')); function openSettings(e: MouseEvent) { @@ -88,7 +86,7 @@
- +
@@ -109,7 +107,7 @@
- +
{#if connectionType === 'socket' || !connectionType} diff --git a/src/routes/logs/+page.svelte b/src/routes/logs/+page.svelte index 9a97833..87b0362 100644 --- a/src/routes/logs/+page.svelte +++ b/src/routes/logs/+page.svelte @@ -10,7 +10,7 @@ import * as Select from '$lib/components/ui/select'; import { Checkbox } from '$lib/components/ui/checkbox'; import { ToggleGroup } from '$lib/components/ui/toggle-pill'; - import { RefreshCw, Search, ChevronDown, ChevronUp, Unplug, Copy, Download, WrapText, ArrowDownToLine, X, Sun, Moon, LayoutList, Square, Box, Wifi, WifiOff, Pause, Play, ScrollText, Star, GripVertical, Layers, Check, FolderHeart, Save, Trash2, MoreHorizontal } from 'lucide-svelte'; + import { RefreshCw, Search, ChevronDown, ChevronUp, Unplug, Copy, Download, WrapText, ArrowDownToLine, X, Sun, Moon, LayoutList, Square, Box, Wifi, WifiOff, Pause, Play, ScrollText, Star, GripVertical, Layers, Check, FolderHeart, Save, Trash2, MoreHorizontal, Eraser } from 'lucide-svelte'; import { copyToClipboard } from '$lib/utils/clipboard'; import PageHeader from '$lib/components/PageHeader.svelte'; import type { FavoriteGroup } from '../api/preferences/favorite-groups/+server'; @@ -1290,6 +1290,15 @@ import type { FavoriteGroup } from '../api/preferences/favorite-groups/+server'; } } + // Clear displayed logs + function clearLogs() { + logs = ''; + pendingText = ''; + mergedLogs = []; + mergedHtml = ''; + pendingLogs = []; + } + // Log search functions function toggleLogSearch() { logSearchActive = !logSearchActive; @@ -1960,6 +1969,9 @@ import type { FavoriteGroup } from '../api/preferences/favorite-groups/+server'; +
@@ -2137,6 +2149,13 @@ import type { FavoriteGroup } from '../api/preferences/favorite-groups/+server'; > +