This commit is contained in:
jarek committed 2026-03-02 07:59:58 +01:00
1 parent 4b430340db
commit 9c451aedf9
120 files changed
+7777 -6046

No files matched your search

+9 -18
View File
@@ -85,49 +85,40 @@
</Tabs.Trigger>
</Tabs.List>
<!-- General Tab -->
<Tabs.Content value="general" class="flex-1 min-h-0 overflow-y-auto">
<GeneralTab />
{#if activeTab === 'general'}<GeneralTab />{/if}
</Tabs.Content>
<!-- Environments Tab -->
<Tabs.Content value="environments" class="flex-1 min-h-0 overflow-y-auto">
<EnvironmentsTab {editEnvId} {newEnv} />
{#if activeTab === 'environments'}<EnvironmentsTab {editEnvId} {newEnv} />{/if}
</Tabs.Content>
<!-- Registries Tab -->
<Tabs.Content value="registries" class="flex-1 min-h-0 overflow-y-auto">
<RegistriesTab />
{#if activeTab === 'registries'}<RegistriesTab />{/if}
</Tabs.Content>
<!-- Git Tab -->
<Tabs.Content value="git" class="flex-1 min-h-0 overflow-y-auto">
<GitTab />
{#if activeTab === 'git'}<GitTab />{/if}
</Tabs.Content>
<!-- Config Sets Tab -->
<Tabs.Content value="config-sets" class="flex-1 min-h-0 overflow-y-auto">
<ConfigSetsTab />
{#if activeTab === 'config-sets'}<ConfigSetsTab />{/if}
</Tabs.Content>
<!-- Notifications Tab -->
<Tabs.Content value="notifications" class="flex-1 min-h-0 overflow-y-auto">
<NotificationsTab />
{#if activeTab === 'notifications'}<NotificationsTab />{/if}
</Tabs.Content>
<!-- Auth Tab -->
<Tabs.Content value="auth" class="flex-1 min-h-0 flex flex-col">
<AuthTab onTabChange={handleTabChange} />
{#if activeTab === 'auth'}<AuthTab onTabChange={handleTabChange} />{/if}
</Tabs.Content>
<!-- License Tab -->
<Tabs.Content value="license" class="flex-1 min-h-0 overflow-y-auto">
<LicenseTab />
{#if activeTab === 'license'}<LicenseTab />{/if}
</Tabs.Content>
<!-- About Tab -->
<Tabs.Content value="about" class="flex-1 min-h-0 overflow-y-auto">
<AboutTab />
{#if activeTab === 'about'}<AboutTab />{/if}
</Tabs.Content>
</Tabs.Root>
</div>
+15 -7
View File
@@ -195,8 +195,8 @@
storageDriver: string;
} | null;
runtime: {
bun: string | null;
bunRevision: string | null;
runtimeName: string;
runtimeVersion: string;
nodeVersion: string;
platform: string;
arch: string;
@@ -266,6 +266,7 @@
newDigest: string;
containerName: string;
isComposeManaged: boolean;
latestVersion?: string;
error?: string;
} | null>(null);
let updateCheckError = $state<string | null>(null);
@@ -299,11 +300,12 @@
if (data.updateAvailable) {
updateInfo = {
currentImage: data.currentImage,
newImage: data.currentImage,
newImage: data.newImage || data.currentImage,
currentDigest: data.currentDigest || '',
newDigest: data.newDigest || '',
containerName: data.containerName,
isComposeManaged: data.isComposeManaged
isComposeManaged: data.isComposeManaged,
latestVersion: data.latestVersion
};
}
} catch (err) {
@@ -634,9 +636,9 @@
</div>
<div class="text-sm pl-5">
<div class="flex items-center gap-2 flex-wrap">
{#if systemInfo.runtime.bun}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-500/15 text-amber-600 dark:text-amber-400 shadow-sm ring-1 ring-amber-500/20">
Bun {systemInfo.runtime.bun}
{#if systemInfo.runtime.runtimeVersion}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-500/15 text-green-600 dark:text-green-400 shadow-sm ring-1 ring-green-500/20">
{systemInfo.runtime.runtimeName} {systemInfo.runtime.runtimeVersion}
</span>
{/if}
<span class="text-muted-foreground/50">|</span>
@@ -744,6 +746,11 @@
<MessageSquarePlus class="w-4 h-4" />
Submit issue or idea
</a>
<span class="text-muted-foreground/50">|</span>
<a href="https://discord.gg/rMxW9Y5cQw" target="_blank" rel="noopener noreferrer" class="hover:underline inline-flex items-center gap-1.5 font-medium text-indigo-500 dark:text-indigo-400">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
Discord
</a>
{#if !$licenseStore.isLicensed}
<span class="text-muted-foreground/50">|</span>
<a href="https://buymeacoffee.com/dockhand" target="_blank" rel="noopener noreferrer" class="hover:underline inline-flex items-center gap-1.5 font-medium text-amber-600 dark:text-yellow-400">
@@ -962,6 +969,7 @@
newDigest={updateInfo.newDigest}
containerName={updateInfo.containerName}
isComposeManaged={updateInfo.isComposeManaged}
latestVersion={updateInfo.latestVersion}
onclose={() => showSelfUpdateDialog = false}
/>
{/if}
@@ -22,10 +22,13 @@
newDigest?: string;
containerName: string;
isComposeManaged?: boolean;
latestVersion?: string;
onclose: () => void;
}
let { open = $bindable(), currentImage, newImage, currentDigest = '', newDigest = '', containerName, isComposeManaged = false, onclose }: Props = $props();
let { open = $bindable(), currentImage, newImage, currentDigest = '', newDigest = '', containerName, isComposeManaged = false, latestVersion, onclose }: Props = $props();
const isVersionUpdate = $derived(currentImage !== newImage);
// Phase management
type Phase = 'confirm' | 'preparing' | 'updating' | 'restarting' | 'completed' | 'error';
@@ -210,7 +213,7 @@
try {
const response = await fetch('/api/self-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
body: JSON.stringify({ newImage })
});
@@ -511,19 +514,30 @@
{containerName}
</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Image</span>
<Badge variant="secondary" class="font-mono text-xs">{currentImage}</Badge>
</div>
{#if currentDigest || newDigest}
{#if isVersionUpdate}
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Current digest</span>
<span class="font-mono text-xs text-muted-foreground">{currentDigest ? currentDigest.replace('sha256:', '').slice(0, 12) : 'unknown'}</span>
<span class="text-muted-foreground">Current image</span>
<Badge variant="secondary" class="font-mono text-xs">{currentImage}</Badge>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">New digest</span>
<span class="font-mono text-xs text-amber-500">{newDigest ? newDigest.replace('sha256:', '').slice(0, 12) : 'unknown'}</span>
<span class="text-muted-foreground">New image</span>
<Badge variant="default" class="font-mono text-xs">{newImage}</Badge>
</div>
{:else}
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Image</span>
<Badge variant="secondary" class="font-mono text-xs">{currentImage}</Badge>
</div>
{#if currentDigest || newDigest}
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">Current digest</span>
<span class="font-mono text-xs text-muted-foreground">{currentDigest ? currentDigest.replace('sha256:', '').slice(0, 12) : 'unknown'}</span>
</div>
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">New digest</span>
<span class="font-mono text-xs text-amber-500">{newDigest ? newDigest.replace('sha256:', '').slice(0, 12) : 'unknown'}</span>
</div>
{/if}
{/if}
</div>
@@ -1,5 +1,6 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { readJobResponse } from '$lib/utils/sse-fetch';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import * as Dialog from '$lib/components/ui/dialog';
@@ -1114,18 +1115,9 @@
throw new Error('Failed to pull Grype image');
}
// Read SSE stream to completion
const reader = response.body?.getReader();
if (reader) {
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
if (text.includes('"status":"error"')) {
throw new Error('Pull failed');
}
}
const result = await readJobResponse(response);
if (result.success === false) {
throw new Error(result.error as string || 'Pull failed');
}
// Refresh scanner status after pull
@@ -1155,18 +1147,9 @@
throw new Error('Failed to pull Trivy image');
}
// Read SSE stream to completion
const reader = response.body?.getReader();
if (reader) {
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value, { stream: true });
if (text.includes('"status":"error"')) {
throw new Error('Pull failed');
}
}
const result = await readJobResponse(response);
if (result.success === false) {
throw new Error(result.error as string || 'Pull failed');
}
// Refresh scanner status after pull
@@ -177,25 +177,29 @@
testResult = 'idle';
try {
const config = getFormConfig();
// When editing with no password entered, use stored credentials via [id]/test
// to avoid sending blank password and getting "Missing credentials" from SMTP server
const useStoredCredentials = isEditing && formType === 'smtp' && !formSmtpPassword && notification?.id;
// For editing, if password is empty, we can't test without the original password
if (isEditing && formType === 'smtp' && !formSmtpPassword && notification?.config?.username) {
formError = 'Please enter the password to test the connection';
formTesting = false;
return;
let response: Response;
if (useStoredCredentials) {
response = await fetch(`/api/notifications/${notification!.id}/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
} else {
const config = getFormConfig();
response = await fetch('/api/notifications/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: formType,
name: formName.trim() || 'Test',
config
})
});
}
const response = await fetch('/api/notifications/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: formType,
name: formName.trim() || 'Test',
config
})
});
const data = await response.json();
if (data.success) {