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
This commit is contained in:
+4
-4
@@ -260,12 +260,12 @@ export function updateSettings(data: Partial<Settings>): Promise<Settings> {
|
||||
return put<Settings>('/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<NpmCertificate[]> {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Общие настройки",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
let certPickerItems = $state<EntityPickerItem[]>([]);
|
||||
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 @@
|
||||
<FormField label={$t('settingsGeneral.baseVolumePath')} name="baseVolumePath" bind:value={baseVolumePath} placeholder="/data" helpText={$t('settingsGeneral.baseVolumePathHelp')} />
|
||||
<FormField label={$t('settingsGeneral.notificationUrl')} name="notificationUrl" bind:value={notificationUrl} placeholder="https://notify.example.com/webhook" error={errors.notificationUrl ?? ''} helpText={$t('settingsGeneral.notificationUrlHelp')} />
|
||||
</div>
|
||||
<!-- SSL Certificate -->
|
||||
<!-- Proxy Provider -->
|
||||
<div class="mt-6 border-t border-[var(--border-primary)] pt-4">
|
||||
<h3 class="mb-1 text-sm font-semibold text-[var(--text-primary)]">{$t('settings.proxyProvider')}</h3>
|
||||
<p class="mb-3 text-xs text-[var(--text-tertiary)]">{$t('settings.proxyProviderHelp')}</p>
|
||||
<div class="flex gap-3">
|
||||
<label class="flex flex-1 cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors {proxyProvider === 'none' ? 'border-[var(--color-brand-500)] bg-[var(--surface-card-hover)]' : 'border-[var(--border-primary)] hover:bg-[var(--surface-card-hover)]'}">
|
||||
<input type="radio" bind:group={proxyProvider} value="none" class="mt-0.5 h-4 w-4 text-[var(--color-brand-600)] focus:ring-[var(--color-brand-500)]" />
|
||||
<div>
|
||||
<span class="text-sm font-medium text-[var(--text-primary)]">{$t('settings.proxyNone')}</span>
|
||||
<p class="text-xs text-[var(--text-tertiary)]">{$t('settings.proxyNoneDesc')}</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex flex-1 cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors {proxyProvider === 'npm' ? 'border-[var(--color-brand-500)] bg-[var(--surface-card-hover)]' : 'border-[var(--border-primary)] hover:bg-[var(--surface-card-hover)]'}">
|
||||
<input type="radio" bind:group={proxyProvider} value="npm" class="mt-0.5 h-4 w-4 text-[var(--color-brand-600)] focus:ring-[var(--color-brand-500)]" />
|
||||
<div>
|
||||
<span class="text-sm font-medium text-[var(--text-primary)]">{$t('settings.proxyNpm')}</span>
|
||||
<p class="text-xs text-[var(--text-tertiary)]">{$t('settings.proxyNpmDesc')}</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{#if proxyProvider === 'none'}
|
||||
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 dark:border-amber-700 dark:bg-amber-950/30 p-3">
|
||||
<p class="text-sm text-amber-800 dark:text-amber-300">{$t('settings.proxyNoneWarning')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- SSL Certificate (NPM only) -->
|
||||
{#if proxyProvider === 'npm'}
|
||||
<div class="mt-6 border-t border-[var(--border-primary)] pt-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
@@ -328,6 +361,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Stale Detection -->
|
||||
<div class="mt-6 border-t border-[var(--border-primary)] pt-4">
|
||||
|
||||
@@ -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}
|
||||
<div class="space-y-4"><Skeleton height="12rem" /></div>
|
||||
{:else}
|
||||
{#if proxyProvider === 'npm'}
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
@@ -113,6 +116,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<h3 class="text-sm font-semibold text-[var(--text-primary)]">{$t('settingsCredentials.registryTokens')}</h3>
|
||||
|
||||
Reference in New Issue
Block a user