This commit is contained in:
jarek committed 2026-03-21 14:29:30 +01:00
1 parent aefa5e7925
commit b55e1e5aad
107 files changed
+2193 -443

No files matched your search

+52 -8
View File
@@ -265,6 +265,7 @@
let updateCheckStatus = $state<'idle' | 'checking' | 'found' | 'none' | 'error'>('idle');
let updateCheckProgress = $state({ checked: 0, total: 0 });
let updateCheckBtnEl = $state<HTMLButtonElement | null>(null);
let failedUpdateChecks = $state<Array<{ containerName: string; imageName: string; error: string }>>([]);
let showBatchUpdateModal = $state(false);
const batchUpdateContainerIds = $derived($containerStore.pendingUpdateIds);
const batchUpdateContainerNames = $derived($containerStore.pendingUpdateNames);
@@ -424,9 +425,24 @@
}, 3000));
}
function showFailedChecksToast(failed: typeof failedUpdateChecks, prefix: string) {
const details = failed.map(f => `• ${f.containerName}: ${f.error}`).join('\n');
toast.warning(`${prefix} (${failed.length} failed to check)`, {
description: details,
descriptionClass: 'whitespace-pre-line',
class: '!w-[28rem] !max-w-[28rem]',
duration: Infinity,
action: {
label: 'OK',
onClick: () => {}
}
});
}
async function checkForUpdates() {
updateCheckStatus = 'checking';
updateCheckProgress = { checked: 0, total: 0 };
failedUpdateChecks = [];
// Lock button width to prevent layout shift
if (updateCheckBtnEl) {
@@ -455,18 +471,24 @@
if (updateCheckBtnEl) updateCheckBtnEl.style.minWidth = '';
const containersWithUpdates = data.results.filter((r: any) => r.hasUpdate);
const failedChecks = data.results.filter((r: any) => r.error && !r.hasUpdate).length;
const failedSuffix = failedChecks > 0 ? ` (${failedChecks} failed to check)` : '';
const failed = data.results.filter((r: any) => r.error && !r.hasUpdate);
failedUpdateChecks = failed.map((r: any) => ({
containerName: r.containerName,
imageName: r.imageName,
error: r.error
}));
if (containersWithUpdates.length === 0) {
updateCheckStatus = 'none';
containerStore.setPendingUpdates([], new Map());
if (failedChecks > 0) {
toast.warning(`All containers are up to date${failedSuffix}`);
if (failed.length > 0) {
updateCheckStatus = 'none';
showFailedChecksToast(failedUpdateChecks, 'All containers are up to date');
pendingTimeouts.push(setTimeout(() => { updateCheckStatus = 'idle'; }, 3000));
} else {
updateCheckStatus = 'none';
toast.success('All containers are up to date');
pendingTimeouts.push(setTimeout(() => { updateCheckStatus = 'idle'; }, 3000));
}
pendingTimeouts.push(setTimeout(() => { updateCheckStatus = 'idle'; }, 3000));
return;
}
@@ -476,7 +498,11 @@
new Map(containersWithUpdates.map((r: any) => [r.containerId, r.containerName]))
);
updateCheckStatus = 'found';
toast.info(`${containersWithUpdates.length} update(s) available${failedSuffix}`);
if (failed.length > 0) {
showFailedChecksToast(failedUpdateChecks, `${containersWithUpdates.length} update(s) available`);
} else {
toast.info(`${containersWithUpdates.length} update(s) available`);
}
} catch (error) {
updateCheckStatus = 'error';
pendingTimeouts.push(setTimeout(() => { updateCheckStatus = 'idle'; }, 3000));
@@ -1823,7 +1849,25 @@
{/if}
</div>
{:else if column.id === 'ip'}
<code class="text-xs">{getContainerIp(container.networks)}</code>
{@const networkEntries = container.networks ? Object.entries(container.networks) : []}
{@const primaryIp = getContainerIp(container.networks)}
{#if networkEntries.length > 1 && primaryIp !== '-'}
<Tooltip.Root>
<Tooltip.Trigger>
<span class="inline-flex items-center gap-1">
<code class="text-xs">{primaryIp}</code>
<span class="text-2xs px-1 py-0.5 rounded bg-muted text-muted-foreground font-medium">+{networkEntries.length - 1}</span>
</span>
</Tooltip.Trigger>
<Tooltip.Content side="top" class="max-w-none">
{#each networkEntries as [name, net]}
<div class="font-mono text-xs">{name}: {net.ipAddress || 'no IP'}</div>
{/each}
</Tooltip.Content>
</Tooltip.Root>
{:else}
<code class="text-xs">{primaryIp}</code>
{/if}
{:else if column.id === 'ports'}
{#if ports.length > 0}
{@const compactPorts = $appSettings.compactPorts}
+2 -48
View File
@@ -3,7 +3,7 @@
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Progress } from '$lib/components/ui/progress';
import * as Tooltip from '$lib/components/ui/tooltip';
import { CircleArrowUp, Loader2, AlertCircle, CheckCircle2, XCircle, ChevronDown, ChevronRight, ExternalLink } from 'lucide-svelte';
import { appendEnvParam } from '$lib/stores/environment';
import type { VulnerabilityCriteria } from '$lib/server/db';
@@ -68,7 +68,6 @@
error?: string;
pullLogs: PullLogEntry[];
scanLogs: ScanLogEntry[];
scanResult?: ScanResult;
scannerResults?: ScannerResult[];
vulnerabilities?: VulnerabilityEntry[];
blockReason?: string;
@@ -223,7 +222,6 @@
if (data.message && data.scannerResults && data.scannerResults.length > 1) {
containerProgress.scanLogs.push({ message: data.message });
}
containerProgress.scanResult = data.scanResult;
containerProgress.scannerResults = data.scannerResults;
containerProgress.vulnerabilities = data.vulnerabilities;
progress = [...progress];
@@ -234,7 +232,6 @@
if (existingIndex >= 0) {
progress[existingIndex].step = 'blocked';
progress[existingIndex].success = false;
progress[existingIndex].scanResult = data.scanResult;
progress[existingIndex].scannerResults = data.scannerResults;
progress[existingIndex].blockReason = data.blockReason;
progress = [...progress];
@@ -465,52 +462,9 @@ const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2,
{/if}
</div>
<!-- Scan result badges - show per scanner when available -->
<!-- Scan result badges per scanner -->
{#if item.scannerResults && item.scannerResults.length > 0}
<ScannerSeverityPills results={item.scannerResults} />
{:else if item.scanResult}
<div class="flex items-center gap-1 text-xs shrink-0">
{#if item.scanResult.critical > 0}
<Tooltip.Root>
<Tooltip.Trigger>
<Badge variant="destructive" class="px-1.5 py-0 cursor-help">C:{item.scanResult.critical}</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{item.scanResult.critical} Critical vulnerabilities</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if item.scanResult.high > 0}
<Tooltip.Root>
<Tooltip.Trigger>
<Badge variant="destructive" class="px-1.5 py-0 bg-orange-500 cursor-help">H:{item.scanResult.high}</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{item.scanResult.high} High severity vulnerabilities</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if item.scanResult.medium > 0}
<Tooltip.Root>
<Tooltip.Trigger>
<Badge variant="secondary" class="px-1.5 py-0 bg-amber-500 text-white cursor-help">M:{item.scanResult.medium}</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{item.scanResult.medium} Medium severity vulnerabilities</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if item.scanResult.low > 0}
<Tooltip.Root>
<Tooltip.Trigger>
<Badge variant="secondary" class="px-1.5 py-0 cursor-help">L:{item.scanResult.low}</Badge>
</Tooltip.Trigger>
<Tooltip.Content>
<p>{item.scanResult.low} Low severity vulnerabilities</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
</div>
{/if}
{#if item.success === true}