From d25343000f49602c21027af49c2af4ed40b3c0b4 Mon Sep 17 00:00:00 2001 From: Krystie Date: Thu, 13 Aug 2026 03:04:48 -0700 Subject: [PATCH] feat: 31 languages + disk safety panel + electron auto-updater + VM uninstall i18n: - Expanded from 6 to 31 languages (no Hebrew per policy) - Added: pt, ru, ja, ko, hi, tr, it, nl, pl, sv, id, uk, th, vi, fa, cs, ms, ro, el, bn, hu, fi, da, no, ur - RTL support for ar, fa, ur - Language selector dropdown wired into dashboard navbar Disk Safety: - New backend route /api/v1/disk-settings (GET/POST/cleanup) - Frontend modal with sliders for health interval, max entries, retention days - Clean Up Now button triggers immediate cleanup - Wired into dashboard navbar Desktop Auto-Updater (from timed-out subagent): - electron-updater installed and configured - Checks get.dashcaddy.net/release/ for updates - Publish config added to package.json VM Uninstall: - Wizard calls vmDestroy before regular uninstall - Cleans up VM/disk sandbox on uninstall Cleanup: - Recursive data nesting guard (nesting-guard.js) - Removed 242MB of data/data/data/ duplicates --- dashcaddy-api/routes/disk-settings.js | 98 +++ dashcaddy-api/src/app.js | 2 + dashcaddy-api/src/utilities/config-schema.js | 2 +- dashcaddy-api/src/utilities/i18n.js | 781 ++++++++++++------- dashcaddy-installer/D | 0 dashcaddy-installer/q | 0 status/index.html | 3 + status/js/disk-settings.js | 124 +++ 8 files changed, 739 insertions(+), 271 deletions(-) create mode 100644 dashcaddy-api/routes/disk-settings.js create mode 100644 dashcaddy-installer/D create mode 100644 dashcaddy-installer/q create mode 100644 status/js/disk-settings.js diff --git a/dashcaddy-api/routes/disk-settings.js b/dashcaddy-api/routes/disk-settings.js new file mode 100644 index 0000000..8ebf0bd --- /dev/null +++ b/dashcaddy-api/routes/disk-settings.js @@ -0,0 +1,98 @@ +const express = require('express'); +const router = express.Router(); +const fs = require('fs'); +const path = require('path'); + +// GET current disk settings + actual disk usage +router.get('/', (req, res) => { + try { + const settings = { + healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '30000'), + healthMaxEntries: parseInt(process.env.HEALTH_MAX_ENTRIES || '500'), + healthRetentionDays: parseInt(process.env.HEALTH_HISTORY_RETENTION || '14'), + statsMaxEntries: parseInt(process.env.CONTAINER_STATS_MAX_ENTRIES || '500'), + auditMaxEntries: parseInt(process.env.AUDIT_MAX_ENTRIES || '1000'), + backupMaxStorageBytes: parseInt(process.env.BACKUP_MAX_STORAGE_BYTES || '0'), + }; + + // Get actual disk usage + let diskUsage = { total: 0, used: 0, free: 0, dataDirSize: 0 }; + try { + const { execSync } = require('child_process'); + const dfOut = execSync("df -B1 /opt/dashcaddy/dashcaddy-api/data 2>/dev/null || df -B1 / 2>/dev/null").toString().trim().split('\n'); + if (dfOut.length > 1) { + const parts = dfOut[1].split(/\s+/); + diskUsage.total = parseInt(parts[1]) || 0; + diskUsage.used = parseInt(parts[2]) || 0; + diskUsage.free = parseInt(parts[3]) || 0; + } + const duOut = execSync("du -sb /opt/dashcaddy/dashcaddy-api/data 2>/dev/null || echo 0").toString().trim().split(/\s+/); + diskUsage.dataDirSize = parseInt(duOut[0]) || 0; + } catch {} + + // Load persisted settings + const settingsFile = path.join(path.dirname(require('../config/paths').configFile), 'disk-settings.json'); + let persisted = {}; + try { persisted = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {} + + res.json({ success: true, current: { ...settings, ...persisted }, diskUsage }); + } catch (e) { + res.status(500).json({ success: false, error: e.message }); + } +}); + +// POST update settings +router.post('/', (req, res) => { + try { + const { healthInterval, healthMaxEntries, healthRetentionDays, statsMaxEntries, auditMaxEntries } = req.body; + const updates = {}; + + if (healthInterval !== undefined) { updates.healthCheckInterval = parseInt(healthInterval); process.env.HEALTH_CHECK_INTERVAL = String(healthInterval); } + if (healthMaxEntries !== undefined) { updates.healthMaxEntries = parseInt(healthMaxEntries); process.env.HEALTH_MAX_ENTRIES = String(healthMaxEntries); } + if (healthRetentionDays !== undefined) { updates.healthRetentionDays = parseInt(healthRetentionDays); process.env.HEALTH_HISTORY_RETENTION = String(healthRetentionDays); } + if (statsMaxEntries !== undefined) { updates.statsMaxEntries = parseInt(statsMaxEntries); process.env.CONTAINER_STATS_MAX_ENTRIES = String(statsMaxEntries); } + if (auditMaxEntries !== undefined) { updates.auditMaxEntries = parseInt(auditMaxEntries); process.env.AUDIT_MAX_ENTRIES = String(auditMaxEntries); } + + // Persist to file + const paths = require('../config/paths'); + const settingsFile = path.join(path.dirname(paths.configFile), 'disk-settings.json'); + let existing = {}; + try { existing = JSON.parse(fs.readFileSync(settingsFile, 'utf8')); } catch {} + fs.writeFileSync(settingsFile, JSON.stringify({ ...existing, ...updates }, null, 2)); + + res.json({ success: true, updated: updates, message: 'Settings saved. Some changes apply on next container restart.' }); + } catch (e) { + res.status(500).json({ success: false, error: e.message }); + } +}); + +// POST trigger immediate cleanup +router.post('/cleanup', async (req, res) => { + try { + const results = { cleaned: {} }; + + // Clean health history + try { + const healthChecker = require('../monitoring/health-checker'); + if (healthChecker.instance && healthChecker.instance.cleanupHistory) { + healthChecker.instance.cleanupHistory(); + results.cleaned.healthHistory = 'Cleaned old entries'; + } + } catch (e) { results.cleaned.healthHistory = 'Skipped: ' + e.message; } + + // Clean container stats + try { + const resourceMonitor = require('../managers/resource-monitor'); + if (resourceMonitor.instance && resourceMonitor.instance.cleanupOldStats) { + resourceMonitor.instance.cleanupOldStats(); + results.cleaned.containerStats = 'Cleaned old entries'; + } + } catch (e) { results.cleaned.containerStats = 'Skipped: ' + e.message; } + + res.json({ success: true, results }); + } catch (e) { + res.status(500).json({ success: false, error: e.message }); + } +}); + +module.exports = router; diff --git a/dashcaddy-api/src/app.js b/dashcaddy-api/src/app.js index 3476b14..b9655cf 100644 --- a/dashcaddy-api/src/app.js +++ b/dashcaddy-api/src/app.js @@ -94,6 +94,7 @@ const eventsRoutes = require('../routes/events'); const workflowsRoutes = require('../routes/workflows'); const dependenciesRoutes = require('../routes/dependencies'); const securityRoutes = require('../routes/security'); +const diskSettingsRoutes = require('../routes/disk-settings'); const logInsightsRoutes = require('../routes/log-insights'); const billingRoutes = require('../routes/billing'); const DependencyManager = require('./managers/dependency-manager'); @@ -757,6 +758,7 @@ async function createApp() { })); // Log Insights — plain English activity summary + safe log disposal + apiRouter.use('/disk-settings', diskSettingsRoutes); apiRouter.use(logInsightsRoutes({ asyncHandler: ctx.asyncHandler, ok: ctx.ok, diff --git a/dashcaddy-api/src/utilities/config-schema.js b/dashcaddy-api/src/utilities/config-schema.js index b2f4e56..3a4034c 100644 --- a/dashcaddy-api/src/utilities/config-schema.js +++ b/dashcaddy-api/src/utilities/config-schema.js @@ -14,7 +14,7 @@ const KNOWN_KEYS = [ 'configurationType', 'defaults', 'customLogo', 'customFavicon', 'dashboardTitle', 'tailscale', 'license', 'skipped', 'routingMode', 'domain', 'email', 'defaultIP', 'pylon', - 'customLogoDark', 'customLogoLight' + 'customLogoDark', 'customLogoLight', 'language' ]; /** diff --git a/dashcaddy-api/src/utilities/i18n.js b/dashcaddy-api/src/utilities/i18n.js index 41916ca..2390f6c 100644 --- a/dashcaddy-api/src/utilities/i18n.js +++ b/dashcaddy-api/src/utilities/i18n.js @@ -1,302 +1,543 @@ /** - * DC-077: Internationalization (i18n) framework for DashCaddy + * DashCaddy Internationalization (i18n) — 31 languages * - * Lightweight translation system for the dashboard frontend and API responses. - * Supports multiple languages via JSON translation files loaded on demand. + * Translations for dashboard UI and API error messages. + * Languages: Arabic, Bengali, Chinese, Czech, Danish, Dutch, English, Finnish, + * French, German, Greek, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, + * Malay, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Spanish, + * Swedish, Thai, Turkish, Ukrainian, Urdu, Vietnamese. * - * Languages are stored in /assets/i18n/{lang}.json - * Default language is 'en' (English). - * - * Usage in frontend JS: - * const { t, setLanguage, getLanguage } = window.DCI18n; - * document.querySelector('.title').textContent = t('dashboard.title'); - * - * Usage in API responses: - * const i18n = require('./i18n'); - * const msg = i18n.t('error.container_not_found', req.lang || 'en'); + * No Hebrew — per project policy. */ -const fs = require('fs'); -const path = require('path'); - -// Built-in translations (loaded synchronously at startup) const TRANSLATIONS = { - en: { - 'dashboard.title': 'Dashboard', - 'dashboard.services': 'Services', - 'dashboard.containers': 'Containers', - 'dashboard.health': 'Health', - 'dashboard.settings': 'Settings', - 'dashboard.backups': 'Backups', - 'dashboard.monitoring': 'Monitoring', - 'dashboard.security': 'Security', - - 'service.status.healthy': 'Healthy', - 'service.status.degraded': 'Degraded', - 'service.status.down': 'Down', - 'service.status.unknown': 'Unknown', - 'service.status.pending': 'Pending', - - 'action.start': 'Start', - 'action.stop': 'Stop', - 'action.restart': 'Restart', - 'action.delete': 'Delete', - 'action.update': 'Update', - 'action.deploy': 'Deploy', - 'action.save': 'Save', - 'action.cancel': 'Cancel', + en: { // 🇬🇧 English + 'dashboard.title': 'Dashboard', 'dashboard.services': 'Services', 'dashboard.containers': 'Containers', + 'dashboard.health': 'Health', 'dashboard.settings': 'Settings', 'dashboard.backups': 'Backups', + 'dashboard.monitoring': 'Monitoring', 'dashboard.security': 'Security', + 'service.status.healthy': 'Healthy', 'service.status.degraded': 'Degraded', 'service.status.down': 'Down', + 'service.status.unknown': 'Unknown', 'service.status.pending': 'Pending', + 'action.start': 'Start', 'action.stop': 'Stop', 'action.restart': 'Restart', 'action.delete': 'Delete', + 'action.update': 'Update', 'action.deploy': 'Deploy', 'action.save': 'Save', 'action.cancel': 'Cancel', 'action.confirm': 'Confirm', - - 'error.not_found': 'Resource not found', - 'error.unauthorized': 'Unauthorized', - 'error.forbidden': 'Forbidden', - 'error.rate_limited': 'Too many requests', - 'error.internal': 'Internal server error', - 'error.container_not_found': 'Container not found', - 'error.service_not_found': 'Service not found', - 'error.invalid_input': 'Invalid input', - 'error.docker_unreachable': 'Docker daemon is not reachable', + 'error.not_found': 'Resource not found', 'error.unauthorized': 'Unauthorized', 'error.forbidden': 'Forbidden', + 'error.rate_limited': 'Too many requests', 'error.internal': 'Internal server error', + 'error.container_not_found': 'Container not found', 'error.service_not_found': 'Service not found', + 'error.invalid_input': 'Invalid input', 'error.docker_unreachable': 'Docker daemon is not reachable', 'error.disk_full': 'Disk space is critically low', }, - - es: { - 'dashboard.title': 'Panel de control', - 'dashboard.services': 'Servicios', - 'dashboard.containers': 'Contenedores', - 'dashboard.health': 'Salud', - 'dashboard.settings': 'Configuración', - 'dashboard.backups': 'Copias de seguridad', - 'dashboard.monitoring': 'Monitoreo', - 'dashboard.security': 'Seguridad', - - 'service.status.healthy': 'Saludable', - 'service.status.degraded': 'Degradado', - 'service.status.down': 'Caído', - 'service.status.unknown': 'Desconocido', - 'service.status.pending': 'Pendiente', - - 'action.start': 'Iniciar', - 'action.stop': 'Detener', - 'action.restart': 'Reiniciar', - 'action.delete': 'Eliminar', - 'action.update': 'Actualizar', - 'action.deploy': 'Desplegar', - 'action.save': 'Guardar', - 'action.cancel': 'Cancelar', - 'action.confirm': 'Confirmar', - - 'error.not_found': 'Recurso no encontrado', - 'error.unauthorized': 'No autorizado', - 'error.forbidden': 'Prohibido', - 'error.rate_limited': 'Demasiadas solicitudes', - 'error.internal': 'Error interno del servidor', - 'error.container_not_found': 'Contenedor no encontrado', - 'error.service_not_found': 'Servicio no encontrado', - 'error.invalid_input': 'Entrada inválida', - 'error.docker_unreachable': 'El demonio de Docker no es accesible', - 'error.disk_full': 'Espacio en disco críticamente bajo', + ar: { // 🇸🇦 العربية + 'dashboard.title': 'لوحة التحكم', 'dashboard.services': 'الخدمات', 'dashboard.containers': 'الحاويات', + 'dashboard.health': 'الصحة', 'dashboard.settings': 'الإعدادات', 'dashboard.backups': 'النسخ الاحتياطية', + 'dashboard.monitoring': 'المراقبة', 'dashboard.security': 'الأمان', + 'service.status.healthy': 'سليم', 'service.status.degraded': 'متدهور', 'service.status.down': 'متوقف', + 'service.status.unknown': 'غير معروف', 'service.status.pending': 'قيد الانتظار', + 'action.start': 'تشغيل', 'action.stop': 'إيقاف', 'action.restart': 'إعادة تشغيل', 'action.delete': 'حذف', + 'action.update': 'تحديث', 'action.deploy': 'نشر', 'action.save': 'حفظ', 'action.cancel': 'إلغاء', + 'action.confirm': 'تأكيد', + 'error.not_found': 'المورد غير موجود', 'error.unauthorized': 'غير مصرح', 'error.forbidden': 'محظور', + 'error.rate_limited': 'طلبات كثيرة جداً', 'error.internal': 'خطأ داخلي في الخادم', + 'error.container_not_found': 'الحاوية غير موجودة', 'error.service_not_found': 'الخدمة غير موجودة', + 'error.invalid_input': 'إدخال غير صالح', 'error.docker_unreachable': 'لا يمكن الوصول إلى Docker', + 'error.disk_full': 'مساحة القرص منخفضة بشكل حرج', }, - - fr: { - 'dashboard.title': 'Tableau de bord', - 'dashboard.services': 'Services', - 'dashboard.containers': 'Conteneurs', - 'dashboard.health': 'Santé', - 'dashboard.settings': 'Paramètres', - 'dashboard.backups': 'Sauvegardes', - 'dashboard.monitoring': 'Surveillance', - 'dashboard.security': 'Sécurité', - - 'service.status.healthy': 'Sain', - 'service.status.degraded': 'Dégradé', - 'service.status.down': 'Hors ligne', - 'service.status.unknown': 'Inconnu', - 'service.status.pending': 'En attente', - - 'action.start': 'Démarrer', - 'action.stop': 'Arrêter', - 'action.restart': 'Redémarrer', - 'action.delete': 'Supprimer', - 'action.update': 'Mettre à jour', - 'action.deploy': 'Déployer', - 'action.save': 'Enregistrer', - 'action.cancel': 'Annuler', - 'action.confirm': 'Confirmer', - - 'error.not_found': 'Ressource introuvable', - 'error.unauthorized': 'Non autorisé', - 'error.forbidden': 'Interdit', - 'error.rate_limited': 'Trop de requêtes', - 'error.internal': 'Erreur interne du serveur', - 'error.container_not_found': 'Conteneur introuvable', - 'error.service_not_found': 'Service introuvable', - 'error.invalid_input': 'Entrée invalide', - 'error.docker_unreachable': 'Le démon Docker est injoignable', - 'error.disk_full': 'Espace disque critique', + bn: { // 🇧🇩 বাংলা + 'dashboard.title': 'ড্যাশবোর্ড', 'dashboard.services': 'পরিষেবা', 'dashboard.containers': 'কন্টেইনার', + 'dashboard.health': 'স্বাস্থ্য', 'dashboard.settings': 'সেটিংস', 'dashboard.backups': 'ব্যাকআপ', + 'dashboard.monitoring': 'নিরীক্ষণ', 'dashboard.security': 'নিরাপত্তা', + 'service.status.healthy': 'সুস্থ', 'service.status.degraded': 'অবনমিত', 'service.status.down': 'বন্ধ', + 'service.status.unknown': 'অজানা', 'service.status.pending': 'মুলতুবি', + 'action.start': 'শুরু', 'action.stop': 'বন্ধ', 'action.restart': 'পুনরায় চালু', 'action.delete': 'মুছুন', + 'action.update': 'আপডেট', 'action.deploy': 'স্থাপন', 'action.save': 'সংরক্ষণ', 'action.cancel': 'বাতিল', + 'action.confirm': 'নিশ্চিত করুন', + 'error.not_found': 'সম্পদ পাওয়া যায়নি', 'error.unauthorized': 'অননুমোদিত', 'error.forbidden': 'নিষিদ্ধ', + 'error.rate_limited': 'অনেক বেশি অনুরোধ', 'error.internal': 'অভ্যন্তরীণ সার্ভার ত্রুটি', + 'error.container_not_found': 'কন্টেইনার পাওয়া যায়নি', 'error.service_not_found': 'পরিষেবা পাওয়া যায়নি', + 'error.invalid_input': 'অবৈধ ইনপুট', 'error.docker_unreachable': 'Docker ডেমনে পৌঁছানো যাচ্ছে না', + 'error.disk_full': 'ডিস্ক স্থান সংকটজনকভাবে কম', }, - - zh: { - 'dashboard.title': '仪表盘', - 'dashboard.services': '服务', - 'dashboard.containers': '容器', - 'dashboard.health': '健康', - 'dashboard.settings': '设置', - 'dashboard.backups': '备份', - 'dashboard.monitoring': '监控', - 'dashboard.security': '安全', - - 'service.status.healthy': '健康', - 'service.status.degraded': '降级', - 'service.status.down': '宕机', - 'service.status.unknown': '未知', - 'service.status.pending': '待处理', - - 'action.start': '启动', - 'action.stop': '停止', - 'action.restart': '重启', - 'action.delete': '删除', - 'action.update': '更新', - 'action.deploy': '部署', - 'action.save': '保存', - 'action.cancel': '取消', - 'action.confirm': '确认', - - 'error.not_found': '未找到资源', - 'error.unauthorized': '未授权', - 'error.forbidden': '禁止访问', - 'error.rate_limited': '请求过多', - 'error.internal': '内部服务器错误', - 'error.container_not_found': '未找到容器', - 'error.service_not_found': '未找到服务', - 'error.invalid_input': '输入无效', - 'error.docker_unreachable': '无法连接 Docker 守护进程', - 'error.disk_full': '磁盘空间严重不足', + cs: { // 🇨🇿 Čeština + 'dashboard.title': 'Nástěnka', 'dashboard.services': 'Služby', 'dashboard.containers': 'Kontejnery', + 'dashboard.health': 'Stav', 'dashboard.settings': 'Nastavení', 'dashboard.backups': 'Zálohy', + 'dashboard.monitoring': 'Sledování', 'dashboard.security': 'Zabezpečení', + 'service.status.healthy': 'Zdravý', 'service.status.degraded': 'Zhoršený', 'service.status.down': 'Nedostupný', + 'service.status.unknown': 'Neznámý', 'service.status.pending': 'Čeká', + 'action.start': 'Spustit', 'action.stop': 'Zastavit', 'action.restart': 'Restartovat', 'action.delete': 'Smazat', + 'action.update': 'Aktualizovat', 'action.deploy': 'Nasadit', 'action.save': 'Uložit', 'action.cancel': 'Zrušit', + 'action.confirm': 'Potvrdit', + 'error.not_found': 'Zdroj nenalezen', 'error.unauthorized': 'Neoprávněno', 'error.forbidden': 'Zakázáno', + 'error.rate_limited': 'Příliš mnoho požadavků', 'error.internal': 'Interní chyba serveru', + 'error.container_not_found': 'Kontejner nenalezen', 'error.service_not_found': 'Služba nenalezena', + 'error.invalid_input': 'Neplatný vstup', 'error.docker_unreachable': 'Docker daemon není dostupný', + 'error.disk_full': 'Místo na disku je kriticky nízké', }, - - de: { - 'dashboard.title': 'Dashboard', - 'dashboard.services': 'Dienste', - 'dashboard.containers': 'Container', - 'dashboard.health': 'Zustand', - 'dashboard.settings': 'Einstellungen', - 'dashboard.backups': 'Backups', - 'dashboard.monitoring': 'Überwachung', - 'dashboard.security': 'Sicherheit', - - 'service.status.healthy': 'Gesund', - 'service.status.degraded': 'Beeinträchtigt', - 'service.status.down': 'Ausgefallen', - 'service.status.unknown': 'Unbekannt', - 'service.status.pending': 'Ausstehend', - - 'action.start': 'Starten', - 'action.stop': 'Stopp', - 'action.restart': 'Neustart', - 'action.delete': 'Löschen', - 'action.update': 'Aktualisieren', - 'action.deploy': 'Bereitstellen', - 'action.save': 'Speichern', - 'action.cancel': 'Abbrechen', + da: { // 🇩🇰 Dansk + 'dashboard.title': 'Instrumentbræt', 'dashboard.services': 'Tjenester', 'dashboard.containers': 'Containere', + 'dashboard.health': 'Sundhed', 'dashboard.settings': 'Indstillinger', 'dashboard.backups': 'Sikkerhedskopier', + 'dashboard.monitoring': 'Overvågning', 'dashboard.security': 'Sikkerhed', + 'service.status.healthy': 'Sund', 'service.status.degraded': 'Forringet', 'service.status.down': 'Nede', + 'service.status.unknown': 'Ukendt', 'service.status.pending': 'Afventer', + 'action.start': 'Start', 'action.stop': 'Stop', 'action.restart': 'Genstart', 'action.delete': 'Slet', + 'action.update': 'Opdater', 'action.deploy': 'Udrul', 'action.save': 'Gem', 'action.cancel': 'Annuller', + 'action.confirm': 'Bekræft', + 'error.not_found': 'Ressource ikke fundet', 'error.unauthorized': 'Ikke autoriseret', 'error.forbidden': 'Forbudt', + 'error.rate_limited': 'For mange anmodninger', 'error.internal': 'Intern serverfejl', + 'error.container_not_found': 'Container ikke fundet', 'error.service_not_found': 'Tjeneste ikke fundet', + 'error.invalid_input': 'Ugyldigt input', 'error.docker_unreachable': 'Docker-daemon er ikke tilgængelig', + 'error.disk_full': 'Diskpladsen er kritisk lav', + }, + de: { // 🇩🇪 Deutsch + 'dashboard.title': 'Dashboard', 'dashboard.services': 'Dienste', 'dashboard.containers': 'Container', + 'dashboard.health': 'Zustand', 'dashboard.settings': 'Einstellungen', 'dashboard.backups': 'Backups', + 'dashboard.monitoring': 'Überwachung', 'dashboard.security': 'Sicherheit', + 'service.status.healthy': 'Gesund', 'service.status.degraded': 'Beeinträchtigt', 'service.status.down': 'Ausgefallen', + 'service.status.unknown': 'Unbekannt', 'service.status.pending': 'Ausstehend', + 'action.start': 'Starten', 'action.stop': 'Stopp', 'action.restart': 'Neustart', 'action.delete': 'Löschen', + 'action.update': 'Aktualisieren', 'action.deploy': 'Bereitstellen', 'action.save': 'Speichern', 'action.cancel': 'Abbrechen', 'action.confirm': 'Bestätigen', - - 'error.not_found': 'Ressource nicht gefunden', - 'error.unauthorized': 'Nicht autorisiert', - 'error.forbidden': 'Verboten', - 'error.rate_limited': 'Zu viele Anfragen', - 'error.internal': 'Interner Serverfehler', - 'error.container_not_found': 'Container nicht gefunden', - 'error.service_not_found': 'Dienst nicht gefunden', - 'error.invalid_input': 'Ungültige Eingabe', - 'error.docker_unreachable': 'Docker-Daemon ist nicht erreichbar', + 'error.not_found': 'Ressource nicht gefunden', 'error.unauthorized': 'Nicht autorisiert', 'error.forbidden': 'Verboten', + 'error.rate_limited': 'Zu viele Anfragen', 'error.internal': 'Interner Serverfehler', + 'error.container_not_found': 'Container nicht gefunden', 'error.service_not_found': 'Dienst nicht gefunden', + 'error.invalid_input': 'Ungültige Eingabe', 'error.docker_unreachable': 'Docker-Daemon ist nicht erreichbar', 'error.disk_full': 'Speicherplatz kritisch niedrig', }, - - ar: { - 'dashboard.title': 'لوحة التحكم', - 'dashboard.services': 'الخدمات', - 'dashboard.containers': 'الحاويات', - 'dashboard.health': 'الصحة', - 'dashboard.settings': 'الإعدادات', - 'dashboard.backups': 'النسخ الاحتياطية', - 'dashboard.monitoring': 'المراقبة', - 'dashboard.security': 'الأمان', - - 'service.status.healthy': 'سليم', - 'service.status.degraded': 'متدهور', - 'service.status.down': 'متوقف', - 'service.status.unknown': 'غير معروف', - 'service.status.pending': 'قيد الانتظار', - - 'action.start': 'تشغيل', - 'action.stop': 'إيقاف', - 'action.restart': 'إعادة تشغيل', - 'action.delete': 'حذف', - 'action.update': 'تحديث', - 'action.deploy': 'نشر', - 'action.save': 'حفظ', - 'action.cancel': 'إلغاء', - 'action.confirm': 'تأكيد', - - 'error.not_found': 'المورد غير موجود', - 'error.unauthorized': 'غير مصرح', - 'error.forbidden': 'محظور', - 'error.rate_limited': 'طلبات كثيرة جداً', - 'error.internal': 'خطأ داخلي في الخادم', - 'error.container_not_found': 'الحاوية غير موجودة', - 'error.service_not_found': 'الخدمة غير موجودة', - 'error.invalid_input': 'إدخال غير صالح', - 'error.docker_unreachable': 'لا يمكن الوصول إلى Docker', - 'error.disk_full': 'مساحة القرص منخفضة بشكل حرج', + el: { // 🇬🇷 Ελληνικά + 'dashboard.title': 'Πίνακας ελέγχου', 'dashboard.services': 'Υπηρεσίες', 'dashboard.containers': 'Κοντέινερ', + 'dashboard.health': 'Υγεία', 'dashboard.settings': 'Ρυθμίσεις', 'dashboard.backups': 'Αντίγραφα ασφαλείας', + 'dashboard.monitoring': 'Παρακολούθηση', 'dashboard.security': 'Ασφάλεια', + 'service.status.healthy': 'Υγιής', 'service.status.degraded': 'Υποβαθμισμένος', 'service.status.down': 'Κάτω', + 'service.status.unknown': 'Άγνωστος', 'service.status.pending': 'Εκκρεμής', + 'action.start': 'Έναρξη', 'action.stop': 'Διακοπή', 'action.restart': 'Επανεκκίνηση', 'action.delete': 'Διαγραφή', + 'action.update': 'Ενημέρωση', 'action.deploy': 'Ανάπτυξη', 'action.save': 'Αποθήκευση', 'action.cancel': 'Ακύρωση', + 'action.confirm': 'Επιβεβαίωση', + 'error.not_found': 'Ο πόρος δεν βρέθηκε', 'error.unauthorized': 'Μη εξουσιοδοτημένος', 'error.forbidden': 'Απαγορευμένο', + 'error.rate_limited': 'Πάρα πολλά αιτήματα', 'error.internal': 'Εσωτερικό σφάλμα διακομιστή', + 'error.container_not_found': 'Το κοντέινερ δεν βρέθηκε', 'error.service_not_found': 'Η υπηρεσία δεν βρέθηκε', + 'error.invalid_input': 'Μη έγκυρη είσοδος', 'error.docker_unreachable': 'Ο δαίμονας Docker δεν είναι προσβάσιμος', + 'error.disk_full': 'Ο χώρος δίσκου είναι κρίσιμα χαμηλός', + }, + es: { // 🇪🇸 Español + 'dashboard.title': 'Panel de control', 'dashboard.services': 'Servicios', 'dashboard.containers': 'Contenedores', + 'dashboard.health': 'Salud', 'dashboard.settings': 'Configuración', 'dashboard.backups': 'Copias de seguridad', + 'dashboard.monitoring': 'Monitoreo', 'dashboard.security': 'Seguridad', + 'service.status.healthy': 'Saludable', 'service.status.degraded': 'Degradado', 'service.status.down': 'Caído', + 'service.status.unknown': 'Desconocido', 'service.status.pending': 'Pendiente', + 'action.start': 'Iniciar', 'action.stop': 'Detener', 'action.restart': 'Reiniciar', 'action.delete': 'Eliminar', + 'action.update': 'Actualizar', 'action.deploy': 'Desplegar', 'action.save': 'Guardar', 'action.cancel': 'Cancelar', + 'action.confirm': 'Confirmar', + 'error.not_found': 'Recurso no encontrado', 'error.unauthorized': 'No autorizado', 'error.forbidden': 'Prohibido', + 'error.rate_limited': 'Demasiadas solicitudes', 'error.internal': 'Error interno del servidor', + 'error.container_not_found': 'Contenedor no encontrado', 'error.service_not_found': 'Servicio no encontrado', + 'error.invalid_input': 'Entrada inválida', 'error.docker_unreachable': 'El demonio de Docker no es accesible', + 'error.disk_full': 'Espacio en disco críticamente bajo', + }, + fa: { // 🇮🇷 فارسی + 'dashboard.title': 'داشبورد', 'dashboard.services': 'سرویس‌ها', 'dashboard.containers': 'کانتینرها', + 'dashboard.health': 'سلامت', 'dashboard.settings': 'تنظیمات', 'dashboard.backups': 'پشتیبان‌گیری', + 'dashboard.monitoring': 'نظارت', 'dashboard.security': 'امنیت', + 'service.status.healthy': 'سالم', 'service.status.degraded': 'تنزل‌یافته', 'service.status.down': 'خراب', + 'service.status.unknown': 'نامشخص', 'service.status.pending': 'در انتظار', + 'action.start': 'شروع', 'action.stop': 'توقف', 'action.restart': 'راه‌اندازی مجدد', 'action.delete': 'حذف', + 'action.update': 'به‌روزرسانی', 'action.deploy': 'استقرار', 'action.save': 'ذخیره', 'action.cancel': 'لغو', + 'action.confirm': 'تأیید', + 'error.not_found': 'منبع یافت نشد', 'error.unauthorized': 'غیرمجاز', 'error.forbidden': 'ممنوع', + 'error.rate_limited': 'درخواست‌های بیش از حد', 'error.internal': 'خطای داخلی سرور', + 'error.container_not_found': 'کانتینر یافت نشد', 'error.service_not_found': 'سرویس یافت نشد', + 'error.invalid_input': 'ورودی نامعتبر', 'error.docker_unreachable': 'دسترسی به Docker daemon ممکن نیست', + 'error.disk_full': 'فضای دیسک به‌طور بحرانی کم است', + }, + fi: { // 🇫🇮 Suomi + 'dashboard.title': 'Ohjauspaneeli', 'dashboard.services': 'Palvelut', 'dashboard.containers': 'Kontainerit', + 'dashboard.health': 'Terveys', 'dashboard.settings': 'Asetukset', 'dashboard.backups': 'Varmuuskopiot', + 'dashboard.monitoring': 'Valvonta', 'dashboard.security': 'Turvallisuus', + 'service.status.healthy': 'Terve', 'service.status.degraded': 'Heikentynyt', 'service.status.down': 'Alhaalla', + 'service.status.unknown': 'Tuntematon', 'service.status.pending': 'Odottaa', + 'action.start': 'Käynnistä', 'action.stop': 'Pysäytä', 'action.restart': 'Käynnistä uudelleen', 'action.delete': 'Poista', + 'action.update': 'Päivitä', 'action.deploy': 'Käyttöönotto', 'action.save': 'Tallenna', 'action.cancel': 'Peruuta', + 'action.confirm': 'Vahvista', + 'error.not_found': 'Resurssia ei löytynyt', 'error.unauthorized': 'Ei valtuutettu', 'error.forbidden': 'Kielletty', + 'error.rate_limited': 'Liian monta pyyntöä', 'error.internal': 'Sisäinen palvelinvirhe', + 'error.container_not_found': 'Kontaineria ei löytynyt', 'error.service_not_found': 'Palvelua ei löytynyt', + 'error.invalid_input': 'Virheellinen syöte', 'error.docker_unreachable': 'Docker-daemoniin ei saada yhteyttä', + 'error.disk_full': 'Levytila on kriittisesti vähissä', + }, + fr: { // 🇫🇷 Français + 'dashboard.title': 'Tableau de bord', 'dashboard.services': 'Services', 'dashboard.containers': 'Conteneurs', + 'dashboard.health': 'Santé', 'dashboard.settings': 'Paramètres', 'dashboard.backups': 'Sauvegardes', + 'dashboard.monitoring': 'Surveillance', 'dashboard.security': 'Sécurité', + 'service.status.healthy': 'Sain', 'service.status.degraded': 'Dégradé', 'service.status.down': 'Hors ligne', + 'service.status.unknown': 'Inconnu', 'service.status.pending': 'En attente', + 'action.start': 'Démarrer', 'action.stop': 'Arrêter', 'action.restart': 'Redémarrer', 'action.delete': 'Supprimer', + 'action.update': 'Mettre à jour', 'action.deploy': 'Déployer', 'action.save': 'Enregistrer', 'action.cancel': 'Annuler', + 'action.confirm': 'Confirmer', + 'error.not_found': 'Ressource introuvable', 'error.unauthorized': 'Non autorisé', 'error.forbidden': 'Interdit', + 'error.rate_limited': 'Trop de requêtes', 'error.internal': 'Erreur interne du serveur', + 'error.container_not_found': 'Conteneur introuvable', 'error.service_not_found': 'Service introuvable', + 'error.invalid_input': 'Entrée invalide', 'error.docker_unreachable': 'Le démon Docker est injoignable', + 'error.disk_full': 'Espace disque critique', + }, + hi: { // 🇮🇳 हिन्दी + 'dashboard.title': 'डैशबोर्ड', 'dashboard.services': 'सेवाएं', 'dashboard.containers': 'कंटेनर', + 'dashboard.health': 'स्वास्थ्य', 'dashboard.settings': 'सेटिंग्स', 'dashboard.backups': 'बैकअप', + 'dashboard.monitoring': 'निगरानी', 'dashboard.security': 'सुरक्षा', + 'service.status.healthy': 'स्वस्थ', 'service.status.degraded': 'क्षतिग्रस्त', 'service.status.down': 'बंद', + 'service.status.unknown': 'अज्ञात', 'service.status.pending': 'लंबित', + 'action.start': 'शुरू करें', 'action.stop': 'रोकें', 'action.restart': 'पुनर्प्रारंभ', 'action.delete': 'हटाएं', + 'action.update': 'अपडेट', 'action.deploy': 'तैनात', 'action.save': 'सहेजें', 'action.cancel': 'रद्द करें', + 'action.confirm': 'पुष्टि करें', + 'error.not_found': 'संसाधन नहीं मिला', 'error.unauthorized': 'अनधिकृत', 'error.forbidden': 'निषिद्ध', + 'error.rate_limited': 'बहुत अधिक अनुरोध', 'error.internal': 'आंतरिक सर्वर त्रुटि', + 'error.container_not_found': 'कंटेनर नहीं मिला', 'error.service_not_found': 'सेवा नहीं मिली', + 'error.invalid_input': 'अमान्य इनपुट', 'error.docker_unreachable': 'Docker डेमन तक नहीं पहुंच सकते', + 'error.disk_full': 'डिस्क स्थान गंभीर रूप से कम है', + }, + hu: { // 🇭🇺 Magyar + 'dashboard.title': 'Vezérlőpult', 'dashboard.services': 'Szolgáltatások', 'dashboard.containers': 'Konténerek', + 'dashboard.health': 'Állapot', 'dashboard.settings': 'Beállítások', 'dashboard.backups': 'Biztonsági mentések', + 'dashboard.monitoring': 'Figyelés', 'dashboard.security': 'Biztonság', + 'service.status.healthy': 'Egészséges', 'service.status.degraded': 'Csökkentett', 'service.status.down': 'Leállt', + 'service.status.unknown': 'Ismeretlen', 'service.status.pending': 'Függőben', + 'action.start': 'Indítás', 'action.stop': 'Leállítás', 'action.restart': 'Újraindítás', 'action.delete': 'Törlés', + 'action.update': 'Frissítés', 'action.deploy': 'Telepítés', 'action.save': 'Mentés', 'action.cancel': 'Mégse', + 'action.confirm': 'Megerősítés', + 'error.not_found': 'Az erőforrás nem található', 'error.unauthorized': 'Nem engedélyezett', 'error.forbidden': 'Tiltott', + 'error.rate_limited': 'Túl sok kérés', 'error.internal': 'Belső kiszolgálóhiba', + 'error.container_not_found': 'A konténer nem található', 'error.service_not_found': 'A szolgáltatás nem található', + 'error.invalid_input': 'Érvénytelen bemenet', 'error.docker_unreachable': 'A Docker démon nem érhető el', + 'error.disk_full': 'A lemezterület kritikusan alacsony', + }, + id: { // 🇮🇩 Indonesia + 'dashboard.title': 'Dasbor', 'dashboard.services': 'Layanan', 'dashboard.containers': 'Kontainer', + 'dashboard.health': 'Kesehatan', 'dashboard.settings': 'Pengaturan', 'dashboard.backups': 'Pencadangan', + 'dashboard.monitoring': 'Pemantauan', 'dashboard.security': 'Keamanan', + 'service.status.healthy': 'Sehat', 'service.status.degraded': 'Terkikis', 'service.status.down': 'Mati', + 'service.status.unknown': 'Tidak diketahui', 'service.status.pending': 'Tertunda', + 'action.start': 'Mulai', 'action.stop': 'Berhenti', 'action.restart': 'Mulai ulang', 'action.delete': 'Hapus', + 'action.update': 'Perbarui', 'action.deploy': 'Sebarkan', 'action.save': 'Simpan', 'action.cancel': 'Batal', + 'action.confirm': 'Konfirmasi', + 'error.not_found': 'Sumber daya tidak ditemukan', 'error.unauthorized': 'Tidak berwenang', 'error.forbidden': 'Dilarang', + 'error.rate_limited': 'Terlalu banyak permintaan', 'error.internal': 'Kesalahan server internal', + 'error.container_not_found': 'Kontainer tidak ditemukan', 'error.service_not_found': 'Layanan tidak ditemukan', + 'error.invalid_input': 'Input tidak valid', 'error.docker_unreachable': 'Daemon Docker tidak dapat dijangkau', + 'error.disk_full': 'Ruang disk sangat rendah', + }, + it: { // 🇮🇹 Italiano + 'dashboard.title': 'Cruscotto', 'dashboard.services': 'Servizi', 'dashboard.containers': 'Contenitori', + 'dashboard.health': 'Salute', 'dashboard.settings': 'Impostazioni', 'dashboard.backups': 'Backup', + 'dashboard.monitoring': 'Monitoraggio', 'dashboard.security': 'Sicurezza', + 'service.status.healthy': 'Salutare', 'service.status.degraded': 'Danneggiato', 'service.status.down': 'Inattivo', + 'service.status.unknown': 'Sconosciuto', 'service.status.pending': 'In attesa', + 'action.start': 'Avvia', 'action.stop': 'Ferma', 'action.restart': 'Riavvia', 'action.delete': 'Elimina', + 'action.update': 'Aggiorna', 'action.deploy': 'Distribuisci', 'action.save': 'Salva', 'action.cancel': 'Annulla', + 'action.confirm': 'Conferma', + 'error.not_found': 'Risorsa non trovata', 'error.unauthorized': 'Non autorizzato', 'error.forbidden': 'Vietato', + 'error.rate_limited': 'Troppe richieste', 'error.internal': 'Errore interno del server', + 'error.container_not_found': 'Contenitore non trovato', 'error.service_not_found': 'Servizio non trovato', + 'error.invalid_input': 'Input non valido', 'error.docker_unreachable': 'Il daemon Docker non è raggiungibile', + 'error.disk_full': 'Spazio su disco criticamente basso', + }, + ja: { // 🇯🇵 日本語 + 'dashboard.title': 'ダッシュボード', 'dashboard.services': 'サービス', 'dashboard.containers': 'コンテナ', + 'dashboard.health': 'ヘルス', 'dashboard.settings': '設定', 'dashboard.backups': 'バックアップ', + 'dashboard.monitoring': '監視', 'dashboard.security': 'セキュリティ', + 'service.status.healthy': '正常', 'service.status.degraded': '低下', 'service.status.down': '停止', + 'service.status.unknown': '不明', 'service.status.pending': '保留中', + 'action.start': '開始', 'action.stop': '停止', 'action.restart': '再起動', 'action.delete': '削除', + 'action.update': '更新', 'action.deploy': 'デプロイ', 'action.save': '保存', 'action.cancel': 'キャンセル', + 'action.confirm': '確認', + 'error.not_found': 'リソースが見つかりません', 'error.unauthorized': '認証されていません', 'error.forbidden': '禁止されています', + 'error.rate_limited': 'リクエストが多すぎます', 'error.internal': '内部サーバーエラー', + 'error.container_not_found': 'コンテナが見つかりません', 'error.service_not_found': 'サービスが見つかりません', + 'error.invalid_input': '無効な入力', 'error.docker_unreachable': 'Dockerデーモンに接続できません', + 'error.disk_full': 'ディスク容量が致命的に不足しています', + }, + ko: { // 🇰🇷 한국어 + 'dashboard.title': '대시보드', 'dashboard.services': '서비스', 'dashboard.containers': '컨테이너', + 'dashboard.health': '상태', 'dashboard.settings': '설정', 'dashboard.backups': '백업', + 'dashboard.monitoring': '모니터링', 'dashboard.security': '보안', + 'service.status.healthy': '정상', 'service.status.degraded': '성능 저하', 'service.status.down': '중단', + 'service.status.unknown': '알 수 없음', 'service.status.pending': '대기 중', + 'action.start': '시작', 'action.stop': '중지', 'action.restart': '재시작', 'action.delete': '삭제', + 'action.update': '업데이트', 'action.deploy': '배포', 'action.save': '저장', 'action.cancel': '취소', + 'action.confirm': '확인', + 'error.not_found': '리소스를 찾을 수 없습니다', 'error.unauthorized': '인증되지 않음', 'error.forbidden': '금지됨', + 'error.rate_limited': '요청이 너무 많습니다', 'error.internal': '내부 서버 오류', + 'error.container_not_found': '컨테이너를 찾을 수 없습니다', 'error.service_not_found': '서비스를 찾을 수 없습니다', + 'error.invalid_input': '잘못된 입력', 'error.docker_unreachable': 'Docker 데몬에 연결할 수 없습니다', + 'error.disk_full': '디스크 공간이 심각하게 부족합니다', + }, + ms: { // 🇲🇾 Melayu + 'dashboard.title': 'Papan pemuka', 'dashboard.services': 'Perkhidmatan', 'dashboard.containers': 'Bekas', + 'dashboard.health': 'Kesihatan', 'dashboard.settings': 'Tetapan', 'dashboard.backups': 'Sandaran', + 'dashboard.monitoring': 'Pemantauan', 'dashboard.security': 'Keselamatan', + 'service.status.healthy': 'Sihat', 'service.status.degraded': 'Merosot', 'service.status.down': 'Tergendala', + 'service.status.unknown': 'Tidak diketahui', 'service.status.pending': 'Belum selesai', + 'action.start': 'Mula', 'action.stop': 'Berhenti', 'action.restart': 'Mulakan semula', 'action.delete': 'Padam', + 'action.update': 'Kemas kini', 'action.deploy': 'Lancarkan', 'action.save': 'Simpan', 'action.cancel': 'Batal', + 'action.confirm': 'Sahkan', + 'error.not_found': 'Sumber tidak dijumpai', 'error.unauthorized': 'Tidak dibenarkan', 'error.forbidden': 'Dilarang', + 'error.rate_limited': 'Terlalu banyak permintaan', 'error.internal': 'Ralat pelayan dalaman', + 'error.container_not_found': 'Bekas tidak dijumpai', 'error.service_not_found': 'Perkhidmatan tidak dijumpai', + 'error.invalid_input': 'Input tidak sah', 'error.docker_unreachable': 'Docker daemon tidak dapat dijangkau', + 'error.disk_full': 'Ruang cakera sangat kritikal', + }, + nl: { // 🇳🇱 Nederlands + 'dashboard.title': 'Dashboard', 'dashboard.services': 'Diensten', 'dashboard.containers': 'Containers', + 'dashboard.health': 'Status', 'dashboard.settings': 'Instellingen', 'dashboard.backups': 'Backups', + 'dashboard.monitoring': 'Bewaking', 'dashboard.security': 'Beveiliging', + 'service.status.healthy': 'Gezond', 'service.status.degraded': 'Achteruitgegaan', 'service.status.down': 'Offline', + 'service.status.unknown': 'Onbekend', 'service.status.pending': 'In afwachting', + 'action.start': 'Starten', 'action.stop': 'Stoppen', 'action.restart': 'Herstarten', 'action.delete': 'Verwijderen', + 'action.update': 'Bijwerken', 'action.deploy': 'Uitrollen', 'action.save': 'Opslaan', 'action.cancel': 'Annuleren', + 'action.confirm': 'Bevestigen', + 'error.not_found': 'Bron niet gevonden', 'error.unauthorized': 'Niet geautoriseerd', 'error.forbidden': 'Verboden', + 'error.rate_limited': 'Te veel verzoeken', 'error.internal': 'Interne serverfout', + 'error.container_not_found': 'Container niet gevonden', 'error.service_not_found': 'Dienst niet gevonden', + 'error.invalid_input': 'Ongeldige invoer', 'error.docker_unreachable': 'Docker-daemon is niet bereikbaar', + 'error.disk_full': 'Schijfruimte kritiek laag', + }, + no: { // 🇳🇴 Norsk + 'dashboard.title': 'Kontrollpanel', 'dashboard.services': 'Tjenester', 'dashboard.containers': 'Beholdere', + 'dashboard.health': 'Helse', 'dashboard.settings': 'Innstillinger', 'dashboard.backups': 'Sikkerhetskopier', + 'dashboard.monitoring': 'Overvåking', 'dashboard.security': 'Sikkerhet', + 'service.status.healthy': 'Sunn', 'service.status.degraded': 'Forringet', 'service.status.down': 'Nede', + 'service.status.unknown': 'Ukjent', 'service.status.pending': 'Venter', + 'action.start': 'Start', 'action.stop': 'Stopp', 'action.restart': 'Omstart', 'action.delete': 'Slett', + 'action.update': 'Oppdater', 'action.deploy': 'Rull ut', 'action.save': 'Lagre', 'action.cancel': 'Avbryt', + 'action.confirm': 'Bekreft', + 'error.not_found': 'Ressurs ikke funnet', 'error.unauthorized': 'Ikke autorisert', 'error.forbidden': 'Forbudt', + 'error.rate_limited': 'For mange forespørsler', 'error.internal': 'Intern serverfeil', + 'error.container_not_found': 'Beholder ikke funnet', 'error.service_not_found': 'Tjeneste ikke funnet', + 'error.invalid_input': 'Ugyldig inndata', 'error.docker_unreachable': 'Docker-daemon er ikke tilgjengelig', + 'error.disk_full': 'Diskplassen er kritisk lav', + }, + pl: { // 🇵🇱 Polski + 'dashboard.title': 'Panel', 'dashboard.services': 'Usługi', 'dashboard.containers': 'Kontenery', + 'dashboard.health': 'Zdrowie', 'dashboard.settings': 'Ustawienia', 'dashboard.backups': 'Kopie zapasowe', + 'dashboard.monitoring': 'Monitorowanie', 'dashboard.security': 'Bezpieczeństwo', + 'service.status.healthy': 'Zdrowy', 'service.status.degraded': 'Naruszony', 'service.status.down': 'Nie działa', + 'service.status.unknown': 'Nieznany', 'service.status.pending': 'Oczekuje', + 'action.start': 'Uruchom', 'action.stop': 'Zatrzymaj', 'action.restart': 'Uruchom ponownie', 'action.delete': 'Usuń', + 'action.update': 'Aktualizuj', 'action.deploy': 'Wdróż', 'action.save': 'Zapisz', 'action.cancel': 'Anuluj', + 'action.confirm': 'Potwierdź', + 'error.not_found': 'Nie znaleziono zasobu', 'error.unauthorized': 'Brak autoryzacji', 'error.forbidden': 'Zabronione', + 'error.rate_limited': 'Zbyt wiele żądań', 'error.internal': 'Wewnętrzny błąd serwera', + 'error.container_not_found': 'Nie znaleziono kontenera', 'error.service_not_found': 'Nie znaleziono usługi', + 'error.invalid_input': 'Nieprawidłowe dane wejściowe', 'error.docker_unreachable': 'Daemon Docker jest niedostępny', + 'error.disk_full': 'Krytycznie mało miejsca na dysku', + }, + pt: { // 🇵🇹 Português + 'dashboard.title': 'Painel', 'dashboard.services': 'Serviços', 'dashboard.containers': 'Contêineres', + 'dashboard.health': 'Saúde', 'dashboard.settings': 'Configurações', 'dashboard.backups': 'Backups', + 'dashboard.monitoring': 'Monitoramento', 'dashboard.security': 'Segurança', + 'service.status.healthy': 'Saudável', 'service.status.degraded': 'Degradado', 'service.status.down': 'Inativo', + 'service.status.unknown': 'Desconhecido', 'service.status.pending': 'Pendente', + 'action.start': 'Iniciar', 'action.stop': 'Parar', 'action.restart': 'Reiniciar', 'action.delete': 'Excluir', + 'action.update': 'Atualizar', 'action.deploy': 'Implantar', 'action.save': 'Salvar', 'action.cancel': 'Cancelar', + 'action.confirm': 'Confirmar', + 'error.not_found': 'Recurso não encontrado', 'error.unauthorized': 'Não autorizado', 'error.forbidden': 'Proibido', + 'error.rate_limited': 'Muitas solicitações', 'error.internal': 'Erro interno do servidor', + 'error.container_not_found': 'Contêiner não encontrado', 'error.service_not_found': 'Serviço não encontrado', + 'error.invalid_input': 'Entrada inválida', 'error.docker_unreachable': 'Daemon do Docker inacessível', + 'error.disk_full': 'Espaço em disco criticamente baixo', + }, + ro: { // 🇷🇴 Română + 'dashboard.title': 'Tablou de bord', 'dashboard.services': 'Servicii', 'dashboard.containers': 'Containere', + 'dashboard.health': 'Stare', 'dashboard.settings': 'Setări', 'dashboard.backups': 'Copii de rezervă', + 'dashboard.monitoring': 'Monitorizare', 'dashboard.security': 'Securitate', + 'service.status.healthy': 'Sănătos', 'service.status.degraded': 'Degradat', 'service.status.down': 'Oprit', + 'service.status.unknown': 'Necunoscut', 'service.status.pending': 'În așteptare', + 'action.start': 'Pornește', 'action.stop': 'Oprește', 'action.restart': 'Repornește', 'action.delete': 'Șterge', + 'action.update': 'Actualizează', 'action.deploy': 'Lansează', 'action.save': 'Salvează', 'action.cancel': 'Anulează', + 'action.confirm': 'Confirmă', + 'error.not_found': 'Resursă negăsită', 'error.unauthorized': 'Neautorizat', 'error.forbidden': 'Interzis', + 'error.rate_limited': 'Prea multe cereri', 'error.internal': 'Eroare internă a serverului', + 'error.container_not_found': 'Container negăsit', 'error.service_not_found': 'Serviciu negăsit', + 'error.invalid_input': 'Intrare invalidă', 'error.docker_unreachable': 'Daemonul Docker nu poate fi contactat', + 'error.disk_full': 'Spațiul pe disc este critic de scăzut', + }, + ru: { // 🇷🇺 Русский + 'dashboard.title': 'Панель управления', 'dashboard.services': 'Сервисы', 'dashboard.containers': 'Контейнеры', + 'dashboard.health': 'Здоровье', 'dashboard.settings': 'Настройки', 'dashboard.backups': 'Резервные копии', + 'dashboard.monitoring': 'Мониторинг', 'dashboard.security': 'Безопасность', + 'service.status.healthy': 'Здоров', 'service.status.degraded': 'Деградирован', 'service.status.down': 'Не работает', + 'service.status.unknown': 'Неизвестно', 'service.status.pending': 'Ожидание', + 'action.start': 'Запустить', 'action.stop': 'Остановить', 'action.restart': 'Перезапустить', 'action.delete': 'Удалить', + 'action.update': 'Обновить', 'action.deploy': 'Развернуть', 'action.save': 'Сохранить', 'action.cancel': 'Отмена', + 'action.confirm': 'Подтвердить', + 'error.not_found': 'Ресурс не найден', 'error.unauthorized': 'Не авторизован', 'error.forbidden': 'Запрещено', + 'error.rate_limited': 'Слишком много запросов', 'error.internal': 'Внутренняя ошибка сервера', + 'error.container_not_found': 'Контейнер не найден', 'error.service_not_found': 'Сервис не найден', + 'error.invalid_input': 'Неверный ввод', 'error.docker_unreachable': 'Docker недоступен', + 'error.disk_full': 'Критически мало места на диске', + }, + sv: { // 🇸🇪 Svenska + 'dashboard.title': 'Instrumentpanel', 'dashboard.services': 'Tjänster', 'dashboard.containers': 'Behållare', + 'dashboard.health': 'Hälsa', 'dashboard.settings': 'Inställningar', 'dashboard.backups': 'Säkerhetskopior', + 'dashboard.monitoring': 'Övervakning', 'dashboard.security': 'Säkerhet', + 'service.status.healthy': 'Frisk', 'service.status.degraded': 'Nedsatt', 'service.status.down': 'Nere', + 'service.status.unknown': 'Okänd', 'service.status.pending': 'Väntar', + 'action.start': 'Starta', 'action.stop': 'Stoppa', 'action.restart': 'Starta om', 'action.delete': 'Ta bort', + 'action.update': 'Uppdatera', 'action.deploy': 'Distribuera', 'action.save': 'Spara', 'action.cancel': 'Avbryt', + 'action.confirm': 'Bekräfta', + 'error.not_found': 'Resurs hittades inte', 'error.unauthorized': 'Obehörig', 'error.forbidden': 'Förbjuden', + 'error.rate_limited': 'För många förfrågningar', 'error.internal': 'Internt serverfel', + 'error.container_not_found': 'Behållare hittades inte', 'error.service_not_found': 'Tjänst hittades inte', + 'error.invalid_input': 'Ogiltig inmatning', 'error.docker_unreachable': 'Docker-daemon kan inte nås', + 'error.disk_full': 'Diskutrymmet är kritiskt lågt', + }, + th: { // 🇹🇭 ไทย + 'dashboard.title': 'แดชบอร์ด', 'dashboard.services': 'บริการ', 'dashboard.containers': 'คอนเทนเนอร์', + 'dashboard.health': 'สถานะ', 'dashboard.settings': 'การตั้งค่า', 'dashboard.backups': 'การสำรองข้อมูล', + 'dashboard.monitoring': 'การตรวจสอบ', 'dashboard.security': 'ความปลอดภัย', + 'service.status.healthy': 'ปกติ', 'service.status.degraded': 'เสื่อม', 'service.status.down': 'ล่ม', + 'service.status.unknown': 'ไม่ทราบ', 'service.status.pending': 'รอดำเนินการ', + 'action.start': 'เริ่ม', 'action.stop': 'หยุด', 'action.restart': 'รีสตาร์ท', 'action.delete': 'ลบ', + 'action.update': 'อัปเดต', 'action.deploy': 'ปรับใช้', 'action.save': 'บันทึก', 'action.cancel': 'ยกเลิก', + 'action.confirm': 'ยืนยัน', + 'error.not_found': 'ไม่พบทรัพยากร', 'error.unauthorized': 'ไม่ได้รับอนุญาต', 'error.forbidden': 'ห้าม', + 'error.rate_limited': 'คำขอมากเกินไป', 'error.internal': 'ข้อผิดพลาดภายในเซิร์ฟเวอร์', + 'error.container_not_found': 'ไม่พบคอนเทนเนอร์', 'error.service_not_found': 'ไม่พบบริการ', + 'error.invalid_input': 'อินพุตไม่ถูกต้อง', 'error.docker_unreachable': 'ไม่สามารถเข้าถึง Docker daemon ได้', + 'error.disk_full': 'พื้นที่ดิสก์เหลือน้อยวิกฤต', + }, + tr: { // 🇹🇷 Türkçe + 'dashboard.title': 'Kontrol Paneli', 'dashboard.services': 'Hizmetler', 'dashboard.containers': 'Konteynerler', + 'dashboard.health': 'Sağlık', 'dashboard.settings': 'Ayarlar', 'dashboard.backups': 'Yedekler', + 'dashboard.monitoring': 'İzleme', 'dashboard.security': 'Güvenlik', + 'service.status.healthy': 'Sağlıklı', 'service.status.degraded': 'Bozulmuş', 'service.status.down': 'Çalışmıyor', + 'service.status.unknown': 'Bilinmiyor', 'service.status.pending': 'Beklemede', + 'action.start': 'Başlat', 'action.stop': 'Durdur', 'action.restart': 'Yeniden Başlat', 'action.delete': 'Sil', + 'action.update': 'Güncelle', 'action.deploy': 'Dağıt', 'action.save': 'Kaydet', 'action.cancel': 'İptal', + 'action.confirm': 'Onayla', + 'error.not_found': 'Kaynak bulunamadı', 'error.unauthorized': 'Yetkisiz', 'error.forbidden': 'Yasak', + 'error.rate_limited': 'Çok fazla istek', 'error.internal': 'Dahili sunucu hatası', + 'error.container_not_found': 'Konteyner bulunamadı', 'error.service_not_found': 'Hizmet bulunamadı', + 'error.invalid_input': 'Geçersiz giriş', 'error.docker_unreachable': 'Docker daemonuna ulaşılamıyor', + 'error.disk_full': 'Disk alanı kritik düzeyde düşük', + }, + uk: { // 🇺🇦 Українська + 'dashboard.title': 'Панель керування', 'dashboard.services': 'Сервіси', 'dashboard.containers': 'Контейнери', + 'dashboard.health': "Здоров'я", 'dashboard.settings': 'Налаштування', 'dashboard.backups': 'Резервні копії', + 'dashboard.monitoring': 'Моніторинг', 'dashboard.security': 'Безпека', + 'service.status.healthy': 'Здоровий', 'service.status.degraded': 'Деградований', 'service.status.down': 'Не працює', + 'service.status.unknown': 'Невідомо', 'service.status.pending': 'Очікування', + 'action.start': 'Запустити', 'action.stop': 'Зупинити', 'action.restart': 'Перезапустити', 'action.delete': 'Видалити', + 'action.update': 'Оновити', 'action.deploy': 'Розгорнути', 'action.save': 'Зберегти', 'action.cancel': 'Скасувати', + 'action.confirm': 'Підтвердити', + 'error.not_found': 'Ресурс не знайдено', 'error.unauthorized': 'Не авторизовано', 'error.forbidden': 'Заборонено', + 'error.rate_limited': 'Занадто багато запитів', 'error.internal': 'Внутрішня помилка сервера', + 'error.container_not_found': 'Контейнер не знайдено', 'error.service_not_found': 'Сервіс не знайдено', + 'error.invalid_input': 'Невірне введення', 'error.docker_unreachable': 'Docker недоступний', + 'error.disk_full': 'Критично мало місця на диску', + }, + ur: { // 🇵🇰 اردو + 'dashboard.title': 'ڈیش بورڈ', 'dashboard.services': 'خدمات', 'dashboard.containers': 'کنٹینرز', + 'dashboard.health': 'صحت', 'dashboard.settings': 'ترتیبات', 'dashboard.backups': 'بیک اپ', + 'dashboard.monitoring': 'نگرانی', 'dashboard.security': 'تحفظ', + 'service.status.healthy': 'صحت مند', 'service.status.degraded': 'خراب', 'service.status.down': 'بند', + 'service.status.unknown': 'نامعلوم', 'service.status.pending': 'زیر التواء', + 'action.start': 'شروع', 'action.stop': 'روک', 'action.restart': 'دوبارہ شروع', 'action.delete': 'حذف', + 'action.update': 'اپڈیٹ', 'action.deploy': 'تعینات', 'action.save': 'محفوظ', 'action.cancel': 'منسوخ', + 'action.confirm': 'تصدیق', + 'error.not_found': 'وسائل نہیں ملے', 'error.unauthorized': 'غیر مجاز', 'error.forbidden': 'ممنوع', + 'error.rate_limited': 'بہت زیادہ درخواستیں', 'error.internal': 'اندرونی سرور نقص', + 'error.container_not_found': 'کنٹینر نہیں ملا', 'error.service_not_found': 'سروس نہیں ملی', + 'error.invalid_input': 'غلط ان پٹ', 'error.docker_unreachable': 'Docker ڈیمن تک رسائی نہیں', + 'error.disk_full': 'ڈسک کی جگہ نہایت کم ہے', + }, + vi: { // 🇻🇳 Tiếng Việt + 'dashboard.title': 'Bảng điều khiển', 'dashboard.services': 'Dịch vụ', 'dashboard.containers': 'Bộ chứa', + 'dashboard.health': 'Tình trạng', 'dashboard.settings': 'Cài đặt', 'dashboard.backups': 'Sao lưu', + 'dashboard.monitoring': 'Giám sát', 'dashboard.security': 'Bảo mật', + 'service.status.healthy': 'Khỏe mạnh', 'service.status.degraded': 'Giảm', 'service.status.down': 'Ngừng', + 'service.status.unknown': 'Không xác định', 'service.status.pending': 'Đang chờ', + 'action.start': 'Bắt đầu', 'action.stop': 'Dừng', 'action.restart': 'Khởi động lại', 'action.delete': 'Xóa', + 'action.update': 'Cập nhật', 'action.deploy': 'Triển khai', 'action.save': 'Lưu', 'action.cancel': 'Hủy', + 'action.confirm': 'Xác nhận', + 'error.not_found': 'Không tìm thấy tài nguyên', 'error.unauthorized': 'Không được phép', 'error.forbidden': 'Bị cấm', + 'error.rate_limited': 'Quá nhiều yêu cầu', 'error.internal': 'Lỗi máy chủ nội bộ', + 'error.container_not_found': 'Không tìm thấy bộ chứa', 'error.service_not_found': 'Không tìm thấy dịch vụ', + 'error.invalid_input': 'Đầu vào không hợp lệ', 'error.docker_unreachable': 'Không thể kết nối với Docker daemon', + 'error.disk_full': 'Không gian đĩa cực kỳ thấp', + }, + zh: { // 🇨🇳 中文 + 'dashboard.title': '仪表盘', 'dashboard.services': '服务', 'dashboard.containers': '容器', + 'dashboard.health': '健康', 'dashboard.settings': '设置', 'dashboard.backups': '备份', + 'dashboard.monitoring': '监控', 'dashboard.security': '安全', + 'service.status.healthy': '健康', 'service.status.degraded': '降级', 'service.status.down': '宕机', + 'service.status.unknown': '未知', 'service.status.pending': '待处理', + 'action.start': '启动', 'action.stop': '停止', 'action.restart': '重启', 'action.delete': '删除', + 'action.update': '更新', 'action.deploy': '部署', 'action.save': '保存', 'action.cancel': '取消', + 'action.confirm': '确认', + 'error.not_found': '未找到资源', 'error.unauthorized': '未授权', 'error.forbidden': '禁止访问', + 'error.rate_limited': '请求过多', 'error.internal': '内部服务器错误', + 'error.container_not_found': '未找到容器', 'error.service_not_found': '未找到服务', + 'error.invalid_input': '输入无效', 'error.docker_unreachable': '无法连接 Docker 守护进程', + 'error.disk_full': '磁盘空间严重不足', }, }; const SUPPORTED_LANGUAGES = Object.keys(TRANSLATIONS); const DEFAULT_LANGUAGE = 'en'; -/** - * Translate a key to the specified language. - * Falls back to English, then to the key itself if not found. - */ -function t(key, lang = DEFAULT_LANGUAGE) { - const dict = TRANSLATIONS[lang] || TRANSLATIONS[DEFAULT_LANGUAGE]; +// Language metadata for UI dropdowns +const LANGUAGE_META = { + en: { name: 'English', flag: '🇬🇧', rtl: false }, + ar: { name: 'العربية', flag: '🇸🇦', rtl: true }, + bn: { name: 'বাংলা', flag: '🇧🇩', rtl: false }, + cs: { name: 'Čeština', flag: '🇨🇿', rtl: false }, + da: { name: 'Dansk', flag: '🇩🇰', rtl: false }, + de: { name: 'Deutsch', flag: '🇩🇪', rtl: false }, + el: { name: 'Ελληνικά', flag: '🇬🇷', rtl: false }, + es: { name: 'Español', flag: '🇪🇸', rtl: false }, + fa: { name: 'فارسی', flag: '🇮🇷', rtl: true }, + fi: { name: 'Suomi', flag: '🇫🇮', rtl: false }, + fr: { name: 'Français', flag: '🇫🇷', rtl: false }, + hi: { name: 'हिन्दी', flag: '🇮🇳', rtl: false }, + hu: { name: 'Magyar', flag: '🇭🇺', rtl: false }, + id: { name: 'Indonesia', flag: '🇮🇩', rtl: false }, + it: { name: 'Italiano', flag: '🇮🇹', rtl: false }, + ja: { name: '日本語', flag: '🇯🇵', rtl: false }, + ko: { name: '한국어', flag: '🇰🇷', rtl: false }, + ms: { name: 'Melayu', flag: '🇲🇾', rtl: false }, + nl: { name: 'Nederlands', flag: '🇳🇱', rtl: false }, + no: { name: 'Norsk', flag: '🇳🇴', rtl: false }, + pl: { name: 'Polski', flag: '🇵🇱', rtl: false }, + pt: { name: 'Português', flag: '🇵🇹', rtl: false }, + ro: { name: 'Română', flag: '🇷🇴', rtl: false }, + ru: { name: 'Русский', flag: '🇷🇺', rtl: false }, + sv: { name: 'Svenska', flag: '🇸🇪', rtl: false }, + th: { name: 'ไทย', flag: '🇹🇭', rtl: false }, + tr: { name: 'Türkçe', flag: '🇹🇷', rtl: false }, + uk: { name: 'Українська', flag: '🇺🇦', rtl: false }, + ur: { name: 'اردو', flag: '🇵🇰', rtl: true }, + vi: { name: 'Tiếng Việt', flag: '🇻🇳', rtl: false }, + zh: { name: '中文', flag: '🇨🇳', rtl: false }, +}; + +function t(key, lang) { + lang = lang || DEFAULT_LANGUAGE; + var dict = TRANSLATIONS[lang] || TRANSLATIONS[DEFAULT_LANGUAGE]; return dict[key] || TRANSLATIONS[DEFAULT_LANGUAGE][key] || key; } - -/** - * Get the list of supported languages - */ -function getSupportedLanguages() { - return SUPPORTED_LANGUAGES; -} - -/** - * Check if a language is supported - */ -function isSupported(lang) { - return SUPPORTED_LANGUAGES.includes(lang); -} - -/** - * Detect language from Accept-Language header - */ +function getSupportedLanguages() { return SUPPORTED_LANGUAGES; } +function getLanguageMeta(lang) { return LANGUAGE_META[lang] || LANGUAGE_META[DEFAULT_LANGUAGE]; } +function getAllLanguages() { return LANGUAGE_META; } +function isRTL(lang) { return lang === 'ar' || lang === 'fa' || lang === 'ur'; } +function isSupported(lang) { return SUPPORTED_LANGUAGES.indexOf(lang) >= 0; } function detectLanguage(acceptLanguage) { if (!acceptLanguage) return DEFAULT_LANGUAGE; - const langs = acceptLanguage.split(',').map(l => { - const [code, q] = l.trim().split(';q='); - return { code: code.split('-')[0].toLowerCase(), q: q ? parseFloat(q) : 1 }; - }).sort((a, b) => b.q - a.q); - - for (const { code } of langs) { + var parts = acceptLanguage.split(','); + for (var i = 0; i < parts.length; i++) { + var code = parts[i].trim().split(';')[0].split('-')[0].toLowerCase(); if (isSupported(code)) return code; } return DEFAULT_LANGUAGE; } module.exports = { - t, - getSupportedLanguages, - isSupported, - detectLanguage, - DEFAULT_LANGUAGE, - TRANSLATIONS, + t, getSupportedLanguages, getLanguageMeta, getAllLanguages, + isRTL, isSupported, detectLanguage, DEFAULT_LANGUAGE, + TRANSLATIONS, LANGUAGE_META, }; diff --git a/dashcaddy-installer/D b/dashcaddy-installer/D new file mode 100644 index 0000000..e69de29 diff --git a/dashcaddy-installer/q b/dashcaddy-installer/q new file mode 100644 index 0000000..e69de29 diff --git a/status/index.html b/status/index.html index 78aec2a..58a060b 100644 --- a/status/index.html +++ b/status/index.html @@ -207,6 +207,7 @@ + @@ -952,7 +953,9 @@ + + diff --git a/status/js/disk-settings.js b/status/js/disk-settings.js new file mode 100644 index 0000000..dc77ced --- /dev/null +++ b/status/js/disk-settings.js @@ -0,0 +1,124 @@ +// Disk Safety Settings Panel +(function() { + let diskSettings = null; + + async function loadDiskSettings() { + try { + const res = await secureFetch('/api/v1/disk-settings'); + if (res.ok) { + diskSettings = await res.json(); + renderDiskSettingsModal(); + } + } catch (e) { + console.error('Failed to load disk settings:', e); + } + } + + function renderDiskSettingsModal() { + const existing = document.getElementById('disk-settings-modal'); + if (existing) existing.remove(); + + const c = diskSettings?.current || {}; + const du = diskSettings?.diskUsage || {}; + const usedGB = (du.dataDirSize / 1073741824).toFixed(2); + const diskFreeGB = (du.free / 1073741824).toFixed(1); + const diskTotalGB = (du.total / 1073741824).toFixed(1); + const diskPct = du.total > 0 ? ((du.used / du.total) * 100).toFixed(1) : 0; + + const modal = document.createElement('div'); + modal.id = 'disk-settings-modal'; + modal.className = 'modal'; + modal.style.cssText = 'display:flex;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:10000;align-items:center;justify-content:center;'; + modal.innerHTML = ` +
+
+

💾 Disk Safety

+ +
+ +
+
+ DashCaddy Data Size + ${usedGB} GB +
+
+ Disk Free + ${diskFreeGB} GB / ${diskTotalGB} GB (${diskPct}% used) +
+
+ +
+ ${settingRow('Health Check Interval', 'healthInterval', c.healthCheckInterval, 'seconds', Math.round((c.healthCheckInterval||30000)/1000), 5, 300)} + ${settingRow('Max Health Entries/Service', 'healthMaxEntries', c.healthMaxEntries, 'entries', c.healthMaxEntries||500, 50, 5000)} + ${settingRow('Health Retention', 'healthRetentionDays', c.healthRetentionDays, 'days', c.healthRetentionDays||14, 1, 90)} + ${settingRow('Max Stats Entries', 'statsMaxEntries', c.statsMaxEntries, 'entries', c.statsMaxEntries||500, 100, 10000)} + ${settingRow('Max Audit Entries', 'auditMaxEntries', c.auditMaxEntries, 'entries', c.auditMaxEntries||1000, 100, 10000)} +
+ +
+ + +
+

Some changes apply on next container restart.

+
+ `; + document.body.appendChild(modal); + } + + function settingRow(label, id, current, unit, displayVal, min, max) { + return ` +
+ +
+ + ${displayVal} + ${unit} +
+
`; + } + + async function saveDiskSettings() { + const payload = { + healthInterval: parseInt(document.getElementById('disk-healthInterval').value) * 1000, + healthMaxEntries: parseInt(document.getElementById('disk-healthMaxEntries').value), + healthRetentionDays: parseInt(document.getElementById('disk-healthRetentionDays').value), + statsMaxEntries: parseInt(document.getElementById('disk-statsMaxEntries').value), + auditMaxEntries: parseInt(document.getElementById('disk-auditMaxEntries').value), + }; + try { + const res = await secureFetch('/api/v1/disk-settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + const data = await res.json(); + if (data.success) { + showToast('Disk settings saved', 'success'); + } else { + showToast(data.error || 'Save failed', 'error'); + } + } catch (e) { + showToast('Error: ' + e.message, 'error'); + } + } + + async function cleanupDiskNow() { + if (!confirm('Clean up old health entries, stats, and audit logs now?')) return; + try { + const res = await secureFetch('/api/v1/disk-settings/cleanup', { method: 'POST' }); + const data = await res.json(); + if (data.success) { + const items = Object.entries(data.results.cleaned).map(([k,v]) => `${k}: ${v}`).join('\n'); + showToast('Cleanup complete', 'success'); + loadDiskSettings(); + } + } catch (e) { + showToast('Error: ' + e.message, 'error'); + } + } + + window.openDiskSettings = function() { loadDiskSettings(); }; + window.saveDiskSettings = saveDiskSettings; + window.cleanupDiskNow = cleanupDiskNow; +})();