diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index f3311f3..e507ebe 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -269,11 +269,18 @@ "settings": { "title": "Settings", "general": "General", + "integrations": "Integrations", + "dns": "DNS", + "maintenance": "Maintenance", "registries": "Registries", "credentials": "Credentials", "authentication": "Authentication", "backup": "Backups", "appearance": "Appearance", + "groupMain": "Overview", + "groupProxy": "Routing", + "groupSystem": "System", + "groupSecurity": "Security", "staleThreshold": "Stale threshold (days)", "staleThresholdHelp": "Containers inactive for longer than this will be flagged as stale.", "dockerCleanup": "Docker Image Cleanup", @@ -310,6 +317,10 @@ "settingsGeneral": { "title": "General Settings", "globalConfig": "Global Configuration", + "globalConfigDesc": "Core infrastructure: the base domain, network, and polling cadence Tinyforge uses to orchestrate containers.", + "configureNpm": "Nginx Proxy Manager is selected.", + "configureTraefik": "Traefik is selected.", + "configureLink": "Configure provider", "domain": "Domain", "domainHelp": "Base domain for subdomain routing (e.g., example.com → stage-dev-app.example.com)", "serverIp": "Server IP (Docker Host)", @@ -1079,5 +1090,21 @@ "previewFull": "Full timestamp", "previewDate": "Date only", "previewHint": "Timestamps like the event log will look exactly like this." + }, + "settingsDns": { + "title": "DNS Configuration", + "description": "Choose whether routes rely on a wildcard record or per-subdomain records managed by a DNS provider." + }, + "settingsIntegrations": { + "title": "Integrations", + "outgoing": "Outgoing notifications", + "outgoingDesc": "Where Tinyforge posts deploy and alert events. Paste a webhook URL (Apprise, Discord, Slack, your own handler).", + "incoming": "Incoming webhook" + }, + "settingsMaintenance": { + "title": "Maintenance", + "thresholds": "Thresholds", + "thresholdsDesc": "Tune when Tinyforge flags stale containers and warns about unused image disk usage.", + "dangerZone": "Danger zone" } } diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index ee89dae..8ab2903 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -269,11 +269,18 @@ "settings": { "title": "Настройки", "general": "Общие", + "integrations": "Интеграции", + "dns": "DNS", + "maintenance": "Обслуживание", "registries": "Реестры", "credentials": "Учётные данные", "authentication": "Аутентификация", "backup": "Резервные копии", "appearance": "Внешний вид", + "groupMain": "Обзор", + "groupProxy": "Маршрутизация", + "groupSystem": "Система", + "groupSecurity": "Безопасность", "staleThreshold": "Порог устаревания (дни)", "staleThresholdHelp": "Контейнеры, неактивные дольше этого срока, будут помечены как устаревшие.", "dockerCleanup": "Очистка Docker-образов", @@ -310,6 +317,10 @@ "settingsGeneral": { "title": "Общие настройки", "globalConfig": "Глобальная конфигурация", + "globalConfigDesc": "Базовая инфраструктура: домен, сеть и интервал опроса, используемые Tinyforge для оркестрации контейнеров.", + "configureNpm": "Выбран Nginx Proxy Manager.", + "configureTraefik": "Выбран Traefik.", + "configureLink": "Настроить провайдера", "domain": "Домен", "domainHelp": "Базовый домен для маршрутизации (напр., example.com → stage-dev-app.example.com)", "serverIp": "IP сервера (Docker Host)", @@ -1079,5 +1090,21 @@ "previewFull": "Полная метка времени", "previewDate": "Только дата", "previewHint": "Метки времени в логе событий будут выглядеть именно так." + }, + "settingsDns": { + "title": "Настройка DNS", + "description": "Выберите, использовать ли wildcard-запись или отдельные поддомены, управляемые DNS-провайдером." + }, + "settingsIntegrations": { + "title": "Интеграции", + "outgoing": "Исходящие уведомления", + "outgoingDesc": "Куда Tinyforge отправляет события деплоев и алертов. Укажите webhook-URL (Apprise, Discord, Slack, свой обработчик).", + "incoming": "Входящий вебхук" + }, + "settingsMaintenance": { + "title": "Обслуживание", + "thresholds": "Пороги", + "thresholdsDesc": "Настройте, когда Tinyforge помечает контейнеры как устаревшие и предупреждает о неиспользуемых образах.", + "dangerZone": "Опасная зона" } } diff --git a/web/src/routes/settings/+layout.svelte b/web/src/routes/settings/+layout.svelte index 62536f8..02c70a7 100644 --- a/web/src/routes/settings/+layout.svelte +++ b/web/src/routes/settings/+layout.svelte @@ -3,35 +3,69 @@ import { page } from '$app/stores'; import { getSettings } from '$lib/api'; import { t } from '$lib/i18n'; - import { IconSettings, IconDatabase, IconShield, IconHardDrive, IconWifi } from '$lib/components/icons'; + import { + IconSettings, + IconDatabase, + IconShield, + IconHardDrive, + IconWifi, + IconGlobe, + IconRefresh, + IconServer + } from '$lib/components/icons'; import ForgeHero from '$lib/components/ForgeHero.svelte'; - interface Props { - children: Snippet; - } - + interface Props { children: Snippet; } let { children }: Props = $props(); + let proxyProvider = $state('npm'); - // Load the proxy provider setting to show/hide tabs. $effect(() => { getSettings().then((s) => { proxyProvider = s.proxy_provider ?? 'npm'; }).catch(() => {}); }); - const baseItems = [ - { href: '/settings', labelKey: 'settings.general', icon: 'general', always: true }, - { href: '/settings/registries', labelKey: 'settings.registries', icon: 'registries', always: true }, - { href: '/settings/npm', labelKey: 'settings.npm', icon: 'npm', provider: 'npm' }, - { href: '/settings/traefik', labelKey: 'settings.traefik', icon: 'traefik', provider: 'traefik' }, - { href: '/settings/auth', labelKey: 'settings.authentication', icon: 'auth', always: true }, - { href: '/settings/backup', labelKey: 'settings.backup', icon: 'backup', always: true } + type NavGroup = 'main' | 'proxy' | 'system' | 'security'; + + interface NavItem { + href: string; + labelKey: string; + icon: string; + group: NavGroup; + provider?: 'npm' | 'traefik'; + } + + // Sidebar layout: grouped, with clear separators. Provider-specific items + // (NPM / Traefik) only appear under "Proxy" when that provider is active. + const baseItems: NavItem[] = [ + { href: '/settings', labelKey: 'settings.general', icon: 'general', group: 'main' }, + { href: '/settings/integrations', labelKey: 'settings.integrations', icon: 'integrations', group: 'main' }, + + { href: '/settings/registries', labelKey: 'settings.registries', icon: 'registries', group: 'proxy' }, + { href: '/settings/npm', labelKey: 'settings.npm', icon: 'npm', group: 'proxy', provider: 'npm' }, + { href: '/settings/traefik', labelKey: 'settings.traefik', icon: 'traefik', group: 'proxy', provider: 'traefik' }, + { href: '/settings/dns', labelKey: 'settings.dns', icon: 'dns', group: 'proxy' }, + + { href: '/settings/maintenance', labelKey: 'settings.maintenance', icon: 'maintenance', group: 'system' }, + { href: '/settings/backup', labelKey: 'settings.backup', icon: 'backup', group: 'system' }, + + { href: '/settings/auth', labelKey: 'settings.authentication', icon: 'auth', group: 'security' } ]; - const navItems = $derived( - baseItems.filter((item) => item.always || item.provider === proxyProvider) - ); + const navItems = $derived(baseItems.filter((i) => !i.provider || i.provider === proxyProvider)); + + // Order of group rendering + headings. + const groupOrder: { key: NavGroup; labelKey: string }[] = [ + { key: 'main', labelKey: 'settings.groupMain' }, + { key: 'proxy', labelKey: 'settings.groupProxy' }, + { key: 'system', labelKey: 'settings.groupSystem' }, + { key: 'security', labelKey: 'settings.groupSecurity' } + ]; + + const grouped = $derived(groupOrder + .map((g) => ({ ...g, items: navItems.filter((i) => i.group === g.key) })) + .filter((g) => g.items.length > 0)); let currentPath = $derived($page.url.pathname); @@ -41,7 +75,7 @@ } -
+
- -