fix: per-event delete button, Docker network default, polling interval duration parsing
- Add per-event delete button (trash icon on hover) in event log entries - Set Docker network default to 'docker-watcher' in DB schema + migration for existing DBs - Parse Go duration strings (5m, 1h) to seconds in settings UI, convert back on save - Clear error when network is empty in deployer instead of hidden fallback
This commit is contained in:
@@ -286,6 +286,9 @@ func (d *Deployer) executeDeploy(
|
||||
d.logDeploy(deployID, "Image pulled successfully", "info")
|
||||
|
||||
// Step 2: Ensure network exists.
|
||||
if settings.Network == "" {
|
||||
return containerID, proxyRouteID, instanceID, fmt.Errorf("docker network not configured in settings")
|
||||
}
|
||||
networkID, err := d.docker.EnsureNetwork(ctx, settings.Network)
|
||||
if err != nil {
|
||||
return containerID, proxyRouteID, instanceID, fmt.Errorf("ensure network: %w", err)
|
||||
|
||||
@@ -109,6 +109,8 @@ func (s *Store) runMigrations() error {
|
||||
`ALTER TABLE settings ADD COLUMN traefik_cert_resolver TEXT NOT NULL DEFAULT 'letsencrypt'`,
|
||||
`ALTER TABLE settings ADD COLUMN traefik_network TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE settings ADD COLUMN traefik_api_url TEXT NOT NULL DEFAULT ''`,
|
||||
// Set default network for existing databases with empty network.
|
||||
`UPDATE settings SET network = 'docker-watcher' WHERE network = ''`,
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
@@ -198,7 +200,7 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
domain TEXT NOT NULL DEFAULT '',
|
||||
server_ip TEXT NOT NULL DEFAULT '',
|
||||
network TEXT NOT NULL DEFAULT '',
|
||||
network TEXT NOT NULL DEFAULT 'docker-watcher',
|
||||
subdomain_pattern TEXT NOT NULL DEFAULT 'stage-{stage}-{project}',
|
||||
notification_url TEXT NOT NULL DEFAULT '',
|
||||
npm_url TEXT NOT NULL DEFAULT '',
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
<script lang="ts">
|
||||
import type { EventLogEntry } from '$lib/types';
|
||||
import { t } from '$lib/i18n';
|
||||
import { IconTrash } from '$lib/components/icons';
|
||||
|
||||
interface Props {
|
||||
entry: EventLogEntry;
|
||||
isNew?: boolean;
|
||||
ondelete?: (id: number) => void;
|
||||
}
|
||||
|
||||
const { entry, isNew = false }: Props = $props();
|
||||
const { entry, isNew = false, ondelete }: Props = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
|
||||
@@ -82,6 +84,16 @@
|
||||
<span class="ml-auto shrink-0 text-[var(--text-tertiary)] tabular-nums" title={formatFull(entry.created_at)}>
|
||||
{timeAgo(entry.created_at)}
|
||||
</span>
|
||||
{#if ondelete}
|
||||
<button
|
||||
type="button"
|
||||
title={$t('common.delete')}
|
||||
onclick={() => ondelete(entry.id)}
|
||||
class="shrink-0 rounded p-0.5 text-[var(--text-tertiary)] opacity-0 group-hover:opacity-100 hover:text-[var(--color-danger)] transition-all"
|
||||
>
|
||||
<IconTrash size={12} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { fetchEventLog, fetchEventLogStats, clearAllEvents } from '$lib/api';
|
||||
import { fetchEventLog, fetchEventLogStats, clearAllEvents, deleteEvent } from '$lib/api';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { toasts } from '$lib/stores/toast';
|
||||
import { connectGlobalEvents, type SSEConnection, type EventLogSSEPayload } from '$lib/sse';
|
||||
@@ -275,6 +275,15 @@
|
||||
<EventLogEntryComponent
|
||||
{entry}
|
||||
isNew={newEventIds.has(entry.id)}
|
||||
ondelete={async (id) => {
|
||||
try {
|
||||
await deleteEvent(id);
|
||||
events = events.filter(e => e.id !== id);
|
||||
stats = { ...stats, total: Math.max(0, stats.total - 1) };
|
||||
} catch (err) {
|
||||
toasts.error(err instanceof Error ? err.message : 'Failed to delete event');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,28 @@
|
||||
|
||||
let errors = $state<Record<string, string>>({});
|
||||
|
||||
// Convert Go duration string (e.g., "5m", "300s", "1h") to seconds string.
|
||||
function parseDurationToSeconds(dur: string): string {
|
||||
if (!dur) return '60';
|
||||
const m = dur.match(/^(\d+)(s|m|h)?$/);
|
||||
if (!m) return dur;
|
||||
const n = parseInt(m[1], 10);
|
||||
switch (m[2]) {
|
||||
case 'h': return String(n * 3600);
|
||||
case 'm': return String(n * 60);
|
||||
default: return String(n);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert seconds to a Go duration string for the API.
|
||||
function secondsToDuration(sec: string | number): string {
|
||||
const n = parseInt(String(sec), 10);
|
||||
if (isNaN(n) || n <= 0) return '60s';
|
||||
if (n >= 3600 && n % 3600 === 0) return `${n / 3600}h`;
|
||||
if (n >= 60 && n % 60 === 0) return `${n / 60}m`;
|
||||
return `${n}s`;
|
||||
}
|
||||
|
||||
function validateDomain(value: string): string {
|
||||
if (!value.trim()) return '';
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(value.trim())) return $t('validation.invalidDomain');
|
||||
@@ -87,7 +109,7 @@
|
||||
serverIp = settings.server_ip ?? '';
|
||||
network = settings.network ?? '';
|
||||
subdomainPattern = settings.subdomain_pattern ?? '';
|
||||
pollingInterval = settings.polling_interval ?? '';
|
||||
pollingInterval = parseDurationToSeconds(settings.polling_interval ?? '60');
|
||||
baseVolumePath = settings.base_volume_path ?? '';
|
||||
notificationUrl = settings.notification_url ?? '';
|
||||
staleThresholdDays = String(settings.stale_threshold_days ?? 7);
|
||||
@@ -116,7 +138,7 @@
|
||||
try {
|
||||
const payload: Record<string, unknown> = {
|
||||
domain: domain.trim(), server_ip: serverIp.trim(), network: network.trim(),
|
||||
subdomain_pattern: subdomainPattern.trim(), polling_interval: String(pollingInterval ?? '').trim(),
|
||||
subdomain_pattern: subdomainPattern.trim(), polling_interval: secondsToDuration(pollingInterval),
|
||||
base_volume_path: baseVolumePath.trim(), notification_url: notificationUrl.trim(),
|
||||
proxy_provider: proxyProvider,
|
||||
stale_threshold_days: Math.max(1, parseInt(staleThresholdDays, 10) || 7),
|
||||
|
||||
Reference in New Issue
Block a user