From 97d2980e955b00dfebcaaebf6c09c187b19f53b7 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 4 Apr 2026 20:33:08 +0300 Subject: [PATCH] feat: proxy provider UI, webhook URL fix, and dev-server key persistence - Add proxy provider selector (None/NPM) to settings page with radio cards - Show warning when switching to None about existing NPM routes - Conditionally hide SSL certificate picker and NPM credentials when provider is None - Fix webhook URL regenerate not updating UI (field name mismatch: url vs webhook_url) - Fix dev-server.sh to persist ENCRYPTION_KEY across restarts via data/.dev-key - Fix selected radio card background visibility in dark mode --- scripts/dev-server.sh | 16 ++++++- web/src/lib/api.ts | 8 ++-- web/src/lib/i18n/en.json | 9 +++- web/src/lib/i18n/ru.json | 9 +++- web/src/lib/types.ts | 1 + web/src/routes/settings/+page.svelte | 42 +++++++++++++++++-- .../routes/settings/credentials/+page.svelte | 4 ++ 7 files changed, 77 insertions(+), 12 deletions(-) diff --git a/scripts/dev-server.sh b/scripts/dev-server.sh index 7ed6100..8a88026 100644 --- a/scripts/dev-server.sh +++ b/scripts/dev-server.sh @@ -15,8 +15,20 @@ if [ -n "$PID" ] && [ "$PID" != "0" ]; then sleep 1 fi -# Generate a random encryption key if not set. -export ENCRYPTION_KEY="${ENCRYPTION_KEY:-$(openssl rand -hex 32)}" +# Use a stable encryption key for development. +# Generate once and save to data/.dev-key so encrypted tokens survive restarts. +if [ -z "${ENCRYPTION_KEY:-}" ]; then + KEY_FILE="./data/.dev-key" + mkdir -p ./data + if [ -f "$KEY_FILE" ]; then + ENCRYPTION_KEY=$(cat "$KEY_FILE") + else + ENCRYPTION_KEY=$(openssl rand -hex 32) + echo "$ENCRYPTION_KEY" > "$KEY_FILE" + echo "Generated new dev encryption key (saved to $KEY_FILE)" + fi +fi +export ENCRYPTION_KEY export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}" export LISTEN_ADDR="${PORT}" diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 2cd4f9c..0793d65 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -260,12 +260,12 @@ export function updateSettings(data: Partial): Promise { return put('/api/settings', data); } -export function getWebhookUrl(): Promise<{ url: string }> { - return get<{ url: string }>('/api/settings/webhook-url'); +export function getWebhookUrl(): Promise<{ webhook_url: string }> { + return get<{ webhook_url: string }>('/api/settings/webhook-url'); } -export function regenerateWebhookUrl(): Promise<{ url: string }> { - return post<{ url: string }>('/api/settings/webhook-url/regenerate'); +export function regenerateWebhookUrl(): Promise<{ webhook_url: string }> { + return post<{ webhook_url: string }>('/api/settings/webhook-url/regenerate'); } export function listNpmCertificates(): Promise { diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 689b52d..9fcbafe 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -238,7 +238,14 @@ "backup": "Backups", "appearance": "Appearance", "staleThreshold": "Stale threshold (days)", - "staleThresholdHelp": "Containers inactive for longer than this will be flagged as stale." + "staleThresholdHelp": "Containers inactive for longer than this will be flagged as stale.", + "proxyProvider": "Proxy Provider", + "proxyProviderHelp": "Select how reverse proxy routes are managed for deployed containers.", + "proxyNone": "None", + "proxyNoneDesc": "No proxy — containers are accessed directly by port", + "proxyNpm": "Nginx Proxy Manager", + "proxyNpmDesc": "Routes managed via NPM API (configure credentials below)", + "proxyNoneWarning": "Switching to None does not remove existing proxy routes from NPM. You can delete them manually from your NPM dashboard." }, "settingsGeneral": { "title": "General Settings", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 137ba47..153d2e2 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -238,7 +238,14 @@ "backup": "Резервные копии", "appearance": "Внешний вид", "staleThreshold": "Порог устаревания (дни)", - "staleThresholdHelp": "Контейнеры, неактивные дольше этого срока, будут помечены как устаревшие." + "staleThresholdHelp": "Контейнеры, неактивные дольше этого срока, будут помечены как устаревшие.", + "proxyProvider": "Провайдер прокси", + "proxyProviderHelp": "Выберите способ управления обратным прокси для развёрнутых контейнеров.", + "proxyNone": "Нет", + "proxyNoneDesc": "Без прокси — контейнеры доступны напрямую по порту", + "proxyNpm": "Nginx Proxy Manager", + "proxyNpmDesc": "Маршруты через NPM API (настройте учётные данные ниже)", + "proxyNoneWarning": "Переключение на «Нет» не удаляет существующие прокси-маршруты из NPM. Вы можете удалить их вручную в панели NPM." }, "settingsGeneral": { "title": "Общие настройки", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index d8c4a4c..168ac2a 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -115,6 +115,7 @@ export interface Settings { dns_provider: string; has_cloudflare_api_token: boolean; cloudflare_zone_id: string; + proxy_provider: string; backup_enabled: boolean; backup_interval_hours: number; backup_retention_count: number; diff --git a/web/src/routes/settings/+page.svelte b/web/src/routes/settings/+page.svelte index de2f535..b0ddc7a 100644 --- a/web/src/routes/settings/+page.svelte +++ b/web/src/routes/settings/+page.svelte @@ -28,6 +28,9 @@ let certPickerItems = $state([]); let loadingCerts = $state(false); + // Proxy provider state. + let proxyProvider = $state('npm'); + // DNS settings state. let wildcardDns = $state(true); let dnsProvider = $state(''); @@ -93,6 +96,7 @@ sslCertificateId = settings.ssl_certificate_id ?? 0; notificationUrl = settings.notification_url ?? ''; staleThresholdDays = String(settings.stale_threshold_days ?? 7); + proxyProvider = settings.proxy_provider ?? 'npm'; wildcardDns = settings.wildcard_dns ?? true; dnsProvider = settings.dns_provider ?? ''; hasCloudflareApiToken = settings.has_cloudflare_api_token ?? false; @@ -107,7 +111,7 @@ async function loadWebhookUrlValue() { try { const result = await getWebhookUrl(); - webhookUrl = result.url; + webhookUrl = result.webhook_url; } catch { /* may not be configured */ } } @@ -119,7 +123,8 @@ domain: domain.trim(), server_ip: serverIp.trim(), network: network.trim(), subdomain_pattern: subdomainPattern.trim(), polling_interval: pollingInterval.trim(), base_volume_path: baseVolumePath.trim(), notification_url: notificationUrl.trim(), - ssl_certificate_id: sslCertificateId, + proxy_provider: proxyProvider, + ssl_certificate_id: proxyProvider === 'npm' ? sslCertificateId : 0, stale_threshold_days: Math.max(1, parseInt(staleThresholdDays, 10) || 7), wildcard_dns: wildcardDns, dns_provider: wildcardDns ? '' : dnsProvider, @@ -141,7 +146,7 @@ regenerating = true; try { const result = await regenerateWebhookUrl(); - webhookUrl = result.url; + webhookUrl = result.webhook_url; toasts.success($t('settingsGeneral.regenerated')); } catch (err) { toasts.error(err instanceof Error ? err.message : $t('settingsGeneral.regenerateFailed')); @@ -293,7 +298,35 @@ - + +
+

{$t('settings.proxyProvider')}

+

{$t('settings.proxyProviderHelp')}

+
+ + +
+ {#if proxyProvider === 'none'} +
+

{$t('settings.proxyNoneWarning')}

+
+ {/if} +
+ + + {#if proxyProvider === 'npm'}
@@ -328,6 +361,7 @@
+ {/if}
diff --git a/web/src/routes/settings/credentials/+page.svelte b/web/src/routes/settings/credentials/+page.svelte index 2940908..52e7770 100644 --- a/web/src/routes/settings/credentials/+page.svelte +++ b/web/src/routes/settings/credentials/+page.svelte @@ -8,6 +8,7 @@ let loading = $state(true); let saving = $state(false); + let proxyProvider = $state('npm'); let npmUrl = $state(''); let npmEmail = $state(''); let npmPassword = $state(''); @@ -28,6 +29,7 @@ loading = true; try { const settings = await getSettings(); + proxyProvider = settings.proxy_provider ?? 'npm'; npmUrl = settings.npm_url ?? ''; npmEmail = settings.npm_email ?? ''; npmHasCredentials = !!(settings.npm_url && settings.npm_email); @@ -65,6 +67,7 @@ {#if loading}
{:else} + {#if proxyProvider === 'npm'}
@@ -113,6 +116,7 @@
{/if}
+ {/if}

{$t('settingsCredentials.registryTokens')}