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 @@
}
-
+
-
-