feat(settings): add database backup management
This commit is contained in:
+12
-2
@@ -31,16 +31,17 @@ async function apiFetch(path, options = {}, _retried = false) {
|
||||
|
||||
const method = options.method ?? 'GET';
|
||||
const stateChanging = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
||||
const { headers: optionHeaders = {}, ...fetchOptions } = options;
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(stateChanging ? { 'X-CSRF-Token': getCsrfToken() } : {}),
|
||||
...options.headers,
|
||||
...optionHeaders,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
@@ -115,6 +116,15 @@ const api = {
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
rawPost: (path, body, headers = {}) => apiFetch(path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
...headers,
|
||||
},
|
||||
body,
|
||||
}),
|
||||
|
||||
put: (path, body) => apiFetch(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(body),
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "تتم مزامنة هذا العضو تلقائيًا مع جهات الاتصال وأعياد الميلاد.",
|
||||
"memberBirthDateInvalid": "استخدم تاريخ ميلاد صحيحًا بالتنسيق المحدد.",
|
||||
"memberPhoneMeta": "الهاتف: {{value}}",
|
||||
"memberBirthdayMeta": "عيد الميلاد: {{date}}"
|
||||
"memberBirthdayMeta": "عيد الميلاد: {{date}}",
|
||||
"tabBackup": "إدارة النسخ الاحتياطي",
|
||||
"sectionBackup": "إدارة النسخ الاحتياطي",
|
||||
"backupDownloadTitle": "تنزيل نسخة احتياطية من قاعدة البيانات",
|
||||
"backupDownloadHint": "ينشئ نسخة SQLite متسقة لكل بيانات التطبيق.",
|
||||
"backupDownloadButton": "تنزيل النسخة الاحتياطية",
|
||||
"backupRestoreTitle": "استعادة نسخة قاعدة البيانات",
|
||||
"backupRestoreHint": "الاستعادة تستبدل قاعدة البيانات الحالية. نزّل نسخة حديثة قبل المتابعة.",
|
||||
"backupDropzoneTitle": "أسقط ملف النسخة هنا أو انقر للاختيار",
|
||||
"backupDropzoneHint": "ملفات SQLite: .db أو .sqlite أو .sqlite3",
|
||||
"backupRestoreButton": "استعادة النسخة",
|
||||
"backupRestoreConfirm": "ستستبدل هذه الاستعادة قاعدة البيانات الحالية للجميع. هل تريد المتابعة؟",
|
||||
"backupRestoring": "جارٍ الاستعادة...",
|
||||
"backupRestoredToast": "تمت استعادة قاعدة البيانات. جارٍ إعادة التحميل...",
|
||||
"backupCliTitle": "استعادة CLI / Docker Compose",
|
||||
"backupCliHint": "للاستعادة التشغيلية، اربط النسخة داخل الحاوية وشغّل مساعد الاستعادة.",
|
||||
"backupCliBackupHint": "يمكنك أيضًا إنشاء نسخة مباشرة عبر Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "تخطيط عائلي. آمن. يحترم الخصوصية. مفتوح المصدر.",
|
||||
|
||||
+17
-1
@@ -777,7 +777,23 @@
|
||||
"memberContactBirthdayHint": "Dieses Mitglied wird automatisch mit Kontakten und Geburtstagen synchronisiert.",
|
||||
"memberBirthDateInvalid": "Bitte ein gültiges Geburtstagsdatum im ausgewählten Format verwenden.",
|
||||
"memberPhoneMeta": "Telefon: {{value}}",
|
||||
"memberBirthdayMeta": "Geburtstag: {{date}}"
|
||||
"memberBirthdayMeta": "Geburtstag: {{date}}",
|
||||
"tabBackup": "Backup-Verwaltung",
|
||||
"sectionBackup": "Backup-Verwaltung",
|
||||
"backupDownloadTitle": "Datenbank-Backup herunterladen",
|
||||
"backupDownloadHint": "Erstellt ein konsistentes SQLite-Backup aller Anwendungsdaten.",
|
||||
"backupDownloadButton": "Backup herunterladen",
|
||||
"backupRestoreTitle": "Datenbank-Backup wiederherstellen",
|
||||
"backupRestoreHint": "Die Wiederherstellung ersetzt die aktuelle Datenbank. Lade vorher ein frisches Backup herunter.",
|
||||
"backupDropzoneTitle": "Backup-Datei hier ablegen oder zum Auswählen klicken",
|
||||
"backupDropzoneHint": "SQLite-Backup-Dateien: .db, .sqlite oder .sqlite3",
|
||||
"backupRestoreButton": "Backup wiederherstellen",
|
||||
"backupRestoreConfirm": "Dieses Backup ersetzt die aktuelle Datenbank für alle Benutzer. Fortfahren?",
|
||||
"backupRestoring": "Wird wiederhergestellt...",
|
||||
"backupRestoredToast": "Datenbank wiederhergestellt. Seite wird neu geladen...",
|
||||
"backupCliTitle": "CLI / Docker-Compose-Wiederherstellung",
|
||||
"backupCliHint": "Kopiere für operative Wiederherstellungen das Backup in den Container und starte den Restore-Helfer.",
|
||||
"backupCliBackupHint": "Du kannst auch direkt über Docker Compose ein Backup erstellen:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Αυτό το μέλος συγχρονίζεται αυτόματα με τις Επαφές και τα Γενέθλια.",
|
||||
"memberBirthDateInvalid": "Χρησιμοποιήστε μια έγκυρη ημερομηνία γέννησης στην επιλεγμένη μορφή.",
|
||||
"memberPhoneMeta": "Τηλέφωνο: {{value}}",
|
||||
"memberBirthdayMeta": "Γενέθλια: {{date}}"
|
||||
"memberBirthdayMeta": "Γενέθλια: {{date}}",
|
||||
"tabBackup": "Διαχείριση αντιγράφων",
|
||||
"sectionBackup": "Διαχείριση αντιγράφων",
|
||||
"backupDownloadTitle": "Λήψη αντιγράφου βάσης δεδομένων",
|
||||
"backupDownloadHint": "Δημιουργεί συνεπές αντίγραφο SQLite όλων των δεδομένων.",
|
||||
"backupDownloadButton": "Λήψη αντιγράφου",
|
||||
"backupRestoreTitle": "Επαναφορά αντιγράφου βάσης δεδομένων",
|
||||
"backupRestoreHint": "Η επαναφορά αντικαθιστά την τρέχουσα βάση. Κατεβάστε νέο αντίγραφο πριν συνεχίσετε.",
|
||||
"backupDropzoneTitle": "Αφήστε εδώ ένα αρχείο αντιγράφου ή κάντε κλικ για επιλογή",
|
||||
"backupDropzoneHint": "Αρχεία SQLite: .db, .sqlite ή .sqlite3",
|
||||
"backupRestoreButton": "Επαναφορά αντιγράφου",
|
||||
"backupRestoreConfirm": "Αυτό το αντίγραφο θα αντικαταστήσει την τρέχουσα βάση για όλους. Συνέχεια;",
|
||||
"backupRestoring": "Γίνεται επαναφορά...",
|
||||
"backupRestoredToast": "Η βάση επαναφέρθηκε. Επαναφόρτωση...",
|
||||
"backupCliTitle": "Επαναφορά CLI / Docker Compose",
|
||||
"backupCliHint": "Για λειτουργική επαναφορά, προσαρτήστε το αντίγραφο στο container και εκτελέστε τον βοηθό.",
|
||||
"backupCliBackupHint": "Μπορείτε επίσης να δημιουργήσετε αντίγραφο απευθείας με Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Οικογενειακός προγραμματισμός. Ασφαλής. Φιλικός προς την ιδιωτικότητα. Ανοιχτός κώδικας.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "This member is automatically synchronized with Contacts and Birthdays.",
|
||||
"memberBirthDateInvalid": "Use a valid birthday date in the selected date format.",
|
||||
"memberPhoneMeta": "Phone: {{value}}",
|
||||
"memberBirthdayMeta": "Birthday: {{date}}"
|
||||
"memberBirthdayMeta": "Birthday: {{date}}",
|
||||
"tabBackup": "Backup Management",
|
||||
"sectionBackup": "Backup Management",
|
||||
"backupDownloadTitle": "Download database backup",
|
||||
"backupDownloadHint": "Create a consistent SQLite backup of all application data.",
|
||||
"backupDownloadButton": "Download backup",
|
||||
"backupRestoreTitle": "Restore database backup",
|
||||
"backupRestoreHint": "Restore replaces the current database. Download a fresh backup before continuing.",
|
||||
"backupDropzoneTitle": "Drop a backup file here or click to select",
|
||||
"backupDropzoneHint": "SQLite backup files: .db, .sqlite or .sqlite3",
|
||||
"backupRestoreButton": "Restore backup",
|
||||
"backupRestoreConfirm": "Restoring this backup will replace the current database for everyone. Continue?",
|
||||
"backupRestoring": "Restoring...",
|
||||
"backupRestoredToast": "Database restored. Reloading...",
|
||||
"backupCliTitle": "CLI / Docker Compose restore",
|
||||
"backupCliHint": "For operational restores, copy the backup into the container and run the restore helper.",
|
||||
"backupCliBackupHint": "You can also create a backup directly from Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Family planning. Secure. Privacy-friendly. Open source.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Este miembro se sincroniza automáticamente con Contactos y Cumpleaños.",
|
||||
"memberBirthDateInvalid": "Usa una fecha de nacimiento válida en el formato seleccionado.",
|
||||
"memberPhoneMeta": "Teléfono: {{value}}",
|
||||
"memberBirthdayMeta": "Cumpleaños: {{date}}"
|
||||
"memberBirthdayMeta": "Cumpleaños: {{date}}",
|
||||
"tabBackup": "Gestión de copias",
|
||||
"sectionBackup": "Gestión de copias",
|
||||
"backupDownloadTitle": "Descargar copia de la base de datos",
|
||||
"backupDownloadHint": "Crea una copia SQLite consistente de todos los datos de la aplicación.",
|
||||
"backupDownloadButton": "Descargar copia",
|
||||
"backupRestoreTitle": "Restaurar copia de la base de datos",
|
||||
"backupRestoreHint": "La restauración reemplaza la base de datos actual. Descarga una copia reciente antes de continuar.",
|
||||
"backupDropzoneTitle": "Suelta un archivo de copia aquí o haz clic para seleccionarlo",
|
||||
"backupDropzoneHint": "Archivos SQLite: .db, .sqlite o .sqlite3",
|
||||
"backupRestoreButton": "Restaurar copia",
|
||||
"backupRestoreConfirm": "Restaurar esta copia reemplazará la base de datos actual para todos. ¿Continuar?",
|
||||
"backupRestoring": "Restaurando...",
|
||||
"backupRestoredToast": "Base de datos restaurada. Recargando...",
|
||||
"backupCliTitle": "Restauración por CLI / Docker Compose",
|
||||
"backupCliHint": "Para restauraciones operativas, monta la copia en el contenedor y ejecuta el asistente de restauración.",
|
||||
"backupCliBackupHint": "También puedes crear una copia directamente con Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Planificación familiar. Segura. Privada. Código abierto.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Ce membre est automatiquement synchronisé avec les Contacts et les Anniversaires.",
|
||||
"memberBirthDateInvalid": "Utilisez une date de naissance valide dans le format sélectionné.",
|
||||
"memberPhoneMeta": "Téléphone : {{value}}",
|
||||
"memberBirthdayMeta": "Anniversaire : {{date}}"
|
||||
"memberBirthdayMeta": "Anniversaire : {{date}}",
|
||||
"tabBackup": "Gestion des sauvegardes",
|
||||
"sectionBackup": "Gestion des sauvegardes",
|
||||
"backupDownloadTitle": "Télécharger la sauvegarde de la base",
|
||||
"backupDownloadHint": "Crée une sauvegarde SQLite cohérente de toutes les données.",
|
||||
"backupDownloadButton": "Télécharger la sauvegarde",
|
||||
"backupRestoreTitle": "Restaurer une sauvegarde de la base",
|
||||
"backupRestoreHint": "La restauration remplace la base actuelle. Télécharge une sauvegarde récente avant de continuer.",
|
||||
"backupDropzoneTitle": "Dépose un fichier de sauvegarde ici ou clique pour le choisir",
|
||||
"backupDropzoneHint": "Fichiers SQLite : .db, .sqlite ou .sqlite3",
|
||||
"backupRestoreButton": "Restaurer la sauvegarde",
|
||||
"backupRestoreConfirm": "Cette restauration remplacera la base actuelle pour tout le monde. Continuer ?",
|
||||
"backupRestoring": "Restauration...",
|
||||
"backupRestoredToast": "Base restaurée. Rechargement...",
|
||||
"backupCliTitle": "Restauration CLI / Docker Compose",
|
||||
"backupCliHint": "Pour une restauration opérationnelle, monte la sauvegarde dans le conteneur et lance l’assistant.",
|
||||
"backupCliBackupHint": "Tu peux aussi créer une sauvegarde directement avec Docker Compose :"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Planification familiale. Sécurisée. Respectueuse de la vie privée. Open source.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "यह सदस्य स्वचालित रूप से संपर्क और जन्मदिन के साथ समन्वयित होता है।",
|
||||
"memberBirthDateInvalid": "चयनित दिनांक प्रारूप में एक मान्य जन्म तिथि का उपयोग करें।",
|
||||
"memberPhoneMeta": "फ़ोन: {{value}}",
|
||||
"memberBirthdayMeta": "जन्मदिन: {{date}}"
|
||||
"memberBirthdayMeta": "जन्मदिन: {{date}}",
|
||||
"tabBackup": "बैकअप प्रबंधन",
|
||||
"sectionBackup": "बैकअप प्रबंधन",
|
||||
"backupDownloadTitle": "डेटाबेस बैकअप डाउनलोड करें",
|
||||
"backupDownloadHint": "सभी ऐप डेटा का एक संगत SQLite बैकअप बनाता है।",
|
||||
"backupDownloadButton": "बैकअप डाउनलोड करें",
|
||||
"backupRestoreTitle": "डेटाबेस बैकअप पुनर्स्थापित करें",
|
||||
"backupRestoreHint": "पुनर्स्थापना मौजूदा डेटाबेस को बदल देगी। जारी रखने से पहले नया बैकअप डाउनलोड करें।",
|
||||
"backupDropzoneTitle": "बैकअप फ़ाइल यहाँ छोड़ें या चुनने के लिए क्लिक करें",
|
||||
"backupDropzoneHint": "SQLite बैकअप फ़ाइलें: .db, .sqlite या .sqlite3",
|
||||
"backupRestoreButton": "बैकअप पुनर्स्थापित करें",
|
||||
"backupRestoreConfirm": "यह बैकअप सभी के लिए मौजूदा डेटाबेस को बदल देगा। जारी रखें?",
|
||||
"backupRestoring": "पुनर्स्थापित हो रहा है...",
|
||||
"backupRestoredToast": "डेटाबेस पुनर्स्थापित हुआ। फिर से लोड हो रहा है...",
|
||||
"backupCliTitle": "CLI / Docker Compose पुनर्स्थापना",
|
||||
"backupCliHint": "ऑपरेशनल पुनर्स्थापना के लिए, बैकअप को कंटेनर में माउंट करें और restore helper चलाएँ।",
|
||||
"backupCliBackupHint": "आप Docker Compose से सीधे बैकअप भी बना सकते हैं:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "पारिवारिक योजना। सुरक्षित। गोपनीयता-अनुकूल। ओपन सोर्स।",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Questo membro viene sincronizzato automaticamente con i Contatti e i Compleanni.",
|
||||
"memberBirthDateInvalid": "Usa una data di nascita valida nel formato selezionato.",
|
||||
"memberPhoneMeta": "Telefono: {{value}}",
|
||||
"memberBirthdayMeta": "Compleanno: {{date}}"
|
||||
"memberBirthdayMeta": "Compleanno: {{date}}",
|
||||
"tabBackup": "Gestione backup",
|
||||
"sectionBackup": "Gestione backup",
|
||||
"backupDownloadTitle": "Scarica backup del database",
|
||||
"backupDownloadHint": "Crea un backup SQLite coerente di tutti i dati dell’applicazione.",
|
||||
"backupDownloadButton": "Scarica backup",
|
||||
"backupRestoreTitle": "Ripristina backup del database",
|
||||
"backupRestoreHint": "Il ripristino sostituisce il database corrente. Scarica un backup recente prima di continuare.",
|
||||
"backupDropzoneTitle": "Trascina qui un file di backup o fai clic per selezionarlo",
|
||||
"backupDropzoneHint": "File SQLite: .db, .sqlite o .sqlite3",
|
||||
"backupRestoreButton": "Ripristina backup",
|
||||
"backupRestoreConfirm": "Questo backup sostituirà il database corrente per tutti. Continuare?",
|
||||
"backupRestoring": "Ripristino...",
|
||||
"backupRestoredToast": "Database ripristinato. Ricaricamento...",
|
||||
"backupCliTitle": "Ripristino CLI / Docker Compose",
|
||||
"backupCliHint": "Per ripristini operativi, monta il backup nel container ed esegui l’helper.",
|
||||
"backupCliBackupHint": "Puoi anche creare un backup direttamente con Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Pianificazione familiare. Sicura. Rispettosa della privacy. Open source.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "このメンバーは連絡先と誕生日と自動的に同期されます。",
|
||||
"memberBirthDateInvalid": "選択した日付形式で有効な生年月日を入力してください。",
|
||||
"memberPhoneMeta": "電話: {{value}}",
|
||||
"memberBirthdayMeta": "誕生日: {{date}}"
|
||||
"memberBirthdayMeta": "誕生日: {{date}}",
|
||||
"tabBackup": "バックアップ管理",
|
||||
"sectionBackup": "バックアップ管理",
|
||||
"backupDownloadTitle": "データベースバックアップをダウンロード",
|
||||
"backupDownloadHint": "すべてのアプリデータを含む一貫した SQLite バックアップを作成します。",
|
||||
"backupDownloadButton": "バックアップをダウンロード",
|
||||
"backupRestoreTitle": "データベースバックアップを復元",
|
||||
"backupRestoreHint": "復元すると現在のデータベースが置き換えられます。続行前に新しいバックアップをダウンロードしてください。",
|
||||
"backupDropzoneTitle": "バックアップファイルをここにドロップするかクリックして選択",
|
||||
"backupDropzoneHint": "SQLite バックアップファイル: .db、.sqlite、.sqlite3",
|
||||
"backupRestoreButton": "バックアップを復元",
|
||||
"backupRestoreConfirm": "このバックアップを復元すると全員の現在のデータベースが置き換えられます。続行しますか?",
|
||||
"backupRestoring": "復元中...",
|
||||
"backupRestoredToast": "データベースを復元しました。再読み込み中...",
|
||||
"backupCliTitle": "CLI / Docker Compose 復元",
|
||||
"backupCliHint": "運用復元では、バックアップをコンテナにマウントして復元ヘルパーを実行します。",
|
||||
"backupCliBackupHint": "Docker Compose から直接バックアップを作成することもできます:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "家族計画。安全。プライバシー重視。オープンソース。",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Este membro é sincronizado automaticamente com Contatos e Aniversários.",
|
||||
"memberBirthDateInvalid": "Use uma data de aniversário válida no formato selecionado.",
|
||||
"memberPhoneMeta": "Telefone: {{value}}",
|
||||
"memberBirthdayMeta": "Aniversário: {{date}}"
|
||||
"memberBirthdayMeta": "Aniversário: {{date}}",
|
||||
"tabBackup": "Gestão de backups",
|
||||
"sectionBackup": "Gestão de backups",
|
||||
"backupDownloadTitle": "Baixar backup do banco de dados",
|
||||
"backupDownloadHint": "Cria um backup SQLite consistente com todos os dados da aplicação.",
|
||||
"backupDownloadButton": "Baixar backup",
|
||||
"backupRestoreTitle": "Restaurar backup do banco de dados",
|
||||
"backupRestoreHint": "A restauração substitui o banco atual. Baixe um backup novo antes de continuar.",
|
||||
"backupDropzoneTitle": "Arraste um arquivo de backup aqui ou clique para selecionar",
|
||||
"backupDropzoneHint": "Arquivos de backup SQLite: .db, .sqlite ou .sqlite3",
|
||||
"backupRestoreButton": "Restaurar backup",
|
||||
"backupRestoreConfirm": "Restaurar este backup substituirá o banco de dados atual para todos. Continuar?",
|
||||
"backupRestoring": "Restaurando...",
|
||||
"backupRestoredToast": "Banco de dados restaurado. Recarregando...",
|
||||
"backupCliTitle": "Restauração via CLI / Docker Compose",
|
||||
"backupCliHint": "Para restaurações operacionais, copie o backup para o container e execute o helper de restauração.",
|
||||
"backupCliBackupHint": "Você também pode criar um backup diretamente pelo Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Planejamento familiar. Seguro. Privado. Código aberto.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Этот участник автоматически синхронизируется с контактами и днями рождения.",
|
||||
"memberBirthDateInvalid": "Используйте действительную дату рождения в выбранном формате.",
|
||||
"memberPhoneMeta": "Телефон: {{value}}",
|
||||
"memberBirthdayMeta": "День рождения: {{date}}"
|
||||
"memberBirthdayMeta": "День рождения: {{date}}",
|
||||
"tabBackup": "Управление резервными копиями",
|
||||
"sectionBackup": "Управление резервными копиями",
|
||||
"backupDownloadTitle": "Скачать резервную копию базы данных",
|
||||
"backupDownloadHint": "Создает согласованную SQLite-копию всех данных приложения.",
|
||||
"backupDownloadButton": "Скачать копию",
|
||||
"backupRestoreTitle": "Восстановить резервную копию базы данных",
|
||||
"backupRestoreHint": "Восстановление заменит текущую базу данных. Перед продолжением скачайте свежую копию.",
|
||||
"backupDropzoneTitle": "Перетащите файл копии сюда или нажмите для выбора",
|
||||
"backupDropzoneHint": "Файлы SQLite: .db, .sqlite или .sqlite3",
|
||||
"backupRestoreButton": "Восстановить копию",
|
||||
"backupRestoreConfirm": "Эта копия заменит текущую базу данных для всех. Продолжить?",
|
||||
"backupRestoring": "Восстановление...",
|
||||
"backupRestoredToast": "База данных восстановлена. Перезагрузка...",
|
||||
"backupCliTitle": "Восстановление CLI / Docker Compose",
|
||||
"backupCliHint": "Для операционного восстановления подключите копию к контейнеру и запустите помощник восстановления.",
|
||||
"backupCliBackupHint": "Также можно создать копию напрямую через Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Семейное планирование. Безопасно. С уважением к приватности. Открытый исходный код.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Den här medlemmen synkroniseras automatiskt med Kontakter och Födelsedagar.",
|
||||
"memberBirthDateInvalid": "Använd ett giltigt födelsedatum i det valda datumformatet.",
|
||||
"memberPhoneMeta": "Telefon: {{value}}",
|
||||
"memberBirthdayMeta": "Födelsedag: {{date}}"
|
||||
"memberBirthdayMeta": "Födelsedag: {{date}}",
|
||||
"tabBackup": "Backuphantering",
|
||||
"sectionBackup": "Backuphantering",
|
||||
"backupDownloadTitle": "Ladda ner databasbackup",
|
||||
"backupDownloadHint": "Skapar en konsekvent SQLite-backup av alla appdata.",
|
||||
"backupDownloadButton": "Ladda ner backup",
|
||||
"backupRestoreTitle": "Återställ databasbackup",
|
||||
"backupRestoreHint": "Återställning ersätter den aktuella databasen. Ladda ner en ny backup innan du fortsätter.",
|
||||
"backupDropzoneTitle": "Släpp en backupfil här eller klicka för att välja",
|
||||
"backupDropzoneHint": "SQLite-backuper: .db, .sqlite eller .sqlite3",
|
||||
"backupRestoreButton": "Återställ backup",
|
||||
"backupRestoreConfirm": "Den här backupen ersätter aktuell databas för alla. Fortsätta?",
|
||||
"backupRestoring": "Återställer...",
|
||||
"backupRestoredToast": "Databasen återställd. Laddar om...",
|
||||
"backupCliTitle": "CLI / Docker Compose-återställning",
|
||||
"backupCliHint": "För driftåterställning, montera backupen i containern och kör återställningshjälpen.",
|
||||
"backupCliBackupHint": "Du kan också skapa en backup direkt med Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Familjeplanering. Säker. Sekretessvänlig. Öppen källkod.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Bu üye otomatik olarak Kişiler ve Doğum Günleri ile senkronize edilir.",
|
||||
"memberBirthDateInvalid": "Seçilen tarih formatında geçerli bir doğum tarihi kullanın.",
|
||||
"memberPhoneMeta": "Telefon: {{value}}",
|
||||
"memberBirthdayMeta": "Doğum günü: {{date}}"
|
||||
"memberBirthdayMeta": "Doğum günü: {{date}}",
|
||||
"tabBackup": "Yedek yönetimi",
|
||||
"sectionBackup": "Yedek yönetimi",
|
||||
"backupDownloadTitle": "Veritabanı yedeğini indir",
|
||||
"backupDownloadHint": "Tüm uygulama verilerinin tutarlı bir SQLite yedeğini oluşturur.",
|
||||
"backupDownloadButton": "Yedeği indir",
|
||||
"backupRestoreTitle": "Veritabanı yedeğini geri yükle",
|
||||
"backupRestoreHint": "Geri yükleme mevcut veritabanını değiştirir. Devam etmeden önce yeni bir yedek indirin.",
|
||||
"backupDropzoneTitle": "Yedek dosyasını buraya bırakın veya seçmek için tıklayın",
|
||||
"backupDropzoneHint": "SQLite yedek dosyaları: .db, .sqlite veya .sqlite3",
|
||||
"backupRestoreButton": "Yedeği geri yükle",
|
||||
"backupRestoreConfirm": "Bu yedek herkes için mevcut veritabanını değiştirecek. Devam edilsin mi?",
|
||||
"backupRestoring": "Geri yükleniyor...",
|
||||
"backupRestoredToast": "Veritabanı geri yüklendi. Yeniden yükleniyor...",
|
||||
"backupCliTitle": "CLI / Docker Compose geri yükleme",
|
||||
"backupCliHint": "Operasyonel geri yüklemeler için yedeği konteynere bağlayın ve geri yükleme yardımcısını çalıştırın.",
|
||||
"backupCliBackupHint": "Docker Compose ile doğrudan yedek de oluşturabilirsiniz:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Aile planlaması. Güvenli. Gizlilik dostu. Açık kaynak.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "Цей учасник автоматично синхронізується з контактами та днями народження.",
|
||||
"memberBirthDateInvalid": "Використовуйте дійсну дату народження у вибраному форматі.",
|
||||
"memberPhoneMeta": "Телефон: {{value}}",
|
||||
"memberBirthdayMeta": "День народження: {{date}}"
|
||||
"memberBirthdayMeta": "День народження: {{date}}",
|
||||
"tabBackup": "Керування резервними копіями",
|
||||
"sectionBackup": "Керування резервними копіями",
|
||||
"backupDownloadTitle": "Завантажити резервну копію бази даних",
|
||||
"backupDownloadHint": "Створює узгоджену SQLite-копію всіх даних застосунку.",
|
||||
"backupDownloadButton": "Завантажити копію",
|
||||
"backupRestoreTitle": "Відновити резервну копію бази даних",
|
||||
"backupRestoreHint": "Відновлення замінить поточну базу даних. Перед продовженням завантажте свіжу копію.",
|
||||
"backupDropzoneTitle": "Перетягніть файл копії сюди або натисніть для вибору",
|
||||
"backupDropzoneHint": "Файли SQLite: .db, .sqlite або .sqlite3",
|
||||
"backupRestoreButton": "Відновити копію",
|
||||
"backupRestoreConfirm": "Ця копія замінить поточну базу даних для всіх. Продовжити?",
|
||||
"backupRestoring": "Відновлення...",
|
||||
"backupRestoredToast": "Базу даних відновлено. Перезавантаження...",
|
||||
"backupCliTitle": "Відновлення CLI / Docker Compose",
|
||||
"backupCliHint": "Для операційного відновлення підключіть копію до контейнера та запустіть помічник.",
|
||||
"backupCliBackupHint": "Також можна створити копію безпосередньо через Docker Compose:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "Планування для родини. Безпечно. Конфіденційно. Відкритий код.",
|
||||
|
||||
+17
-1
@@ -752,7 +752,23 @@
|
||||
"memberContactBirthdayHint": "此成员自动与联系人和生日同步。",
|
||||
"memberBirthDateInvalid": "请使用所选日期格式的有效出生日期。",
|
||||
"memberPhoneMeta": "电话:{{value}}",
|
||||
"memberBirthdayMeta": "生日:{{date}}"
|
||||
"memberBirthdayMeta": "生日:{{date}}",
|
||||
"tabBackup": "备份管理",
|
||||
"sectionBackup": "备份管理",
|
||||
"backupDownloadTitle": "下载数据库备份",
|
||||
"backupDownloadHint": "创建包含所有应用数据的一致 SQLite 备份。",
|
||||
"backupDownloadButton": "下载备份",
|
||||
"backupRestoreTitle": "恢复数据库备份",
|
||||
"backupRestoreHint": "恢复会替换当前数据库。继续前请先下载新的备份。",
|
||||
"backupDropzoneTitle": "将备份文件拖到这里,或点击选择",
|
||||
"backupDropzoneHint": "SQLite 备份文件:.db、.sqlite 或 .sqlite3",
|
||||
"backupRestoreButton": "恢复备份",
|
||||
"backupRestoreConfirm": "恢复此备份将为所有用户替换当前数据库。继续吗?",
|
||||
"backupRestoring": "正在恢复...",
|
||||
"backupRestoredToast": "数据库已恢复。正在重新加载...",
|
||||
"backupCliTitle": "CLI / Docker Compose 恢复",
|
||||
"backupCliHint": "对于运维恢复,请将备份挂载到容器并运行恢复助手。",
|
||||
"backupCliBackupHint": "也可以直接通过 Docker Compose 创建备份:"
|
||||
},
|
||||
"login": {
|
||||
"tagline": "家庭规划。安全。注重隐私。开源。",
|
||||
|
||||
@@ -244,6 +244,7 @@ export async function render(container, { user }) {
|
||||
'general', 'meals', 'budget', 'shopping', 'calendar',
|
||||
...(user?.role === 'admin' ? ['family', 'api-tokens'] : []),
|
||||
'account',
|
||||
...(user?.role === 'admin' ? ['backup'] : []),
|
||||
];
|
||||
const storedTab = sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general';
|
||||
const activeTab = (syncOk || syncErr)
|
||||
@@ -272,6 +273,7 @@ export async function render(container, { user }) {
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('family')}" role="tab" data-tab="family" aria-selected="${btnAria('family')}">${t('settings.tabFamily')}</button>` : ''}
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('api-tokens')}" role="tab" data-tab="api-tokens" aria-selected="${btnAria('api-tokens')}">${t('settings.tabApiTokens')}</button>` : ''}
|
||||
<button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}">${t('settings.tabAccount')}</button>
|
||||
${user?.role === 'admin' ? `<button class="${btnClass('backup')}" role="tab" data-tab="backup" aria-selected="${btnAria('backup')}">${t('settings.tabBackup')}</button>` : ''}
|
||||
</nav>
|
||||
|
||||
<!-- Panel: Allgemein (Design + Sprache) -->
|
||||
@@ -712,6 +714,62 @@ export async function render(container, { user }) {
|
||||
<button class="btn btn--danger-outline settings-logout-btn" id="logout-btn">${t('settings.logout')}</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
${user?.role === 'admin' ? `
|
||||
<!-- Panel: Backup Management -->
|
||||
<div class="settings-tab-panel" data-panel="backup" role="tabpanel"${panelHidden('backup')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionBackup')}</h2>
|
||||
|
||||
<div class="settings-card settings-backup-card">
|
||||
<div class="settings-backup-card__icon">
|
||||
<i data-lucide="database-backup" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="settings-backup-card__body">
|
||||
<h3 class="settings-card__title">${t('settings.backupDownloadTitle')}</h3>
|
||||
<p class="form-hint">${t('settings.backupDownloadHint')}</p>
|
||||
<div class="settings-form-actions">
|
||||
<a class="btn btn--primary" href="/api/v1/backup/database" download>${t('settings.backupDownloadButton')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card settings-backup-card settings-backup-card--danger">
|
||||
<div class="settings-backup-card__icon">
|
||||
<i data-lucide="rotate-ccw" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="settings-backup-card__body">
|
||||
<h3 class="settings-card__title">${t('settings.backupRestoreTitle')}</h3>
|
||||
<p class="form-hint">${t('settings.backupRestoreHint')}</p>
|
||||
<form id="backup-restore-form" class="settings-form settings-form--compact">
|
||||
<label class="settings-backup-dropzone" id="backup-dropzone" for="backup-restore-file">
|
||||
<i data-lucide="upload-cloud" aria-hidden="true"></i>
|
||||
<span>${t('settings.backupDropzoneTitle')}</span>
|
||||
<small>${t('settings.backupDropzoneHint')}</small>
|
||||
</label>
|
||||
<input class="sr-only" type="file" id="backup-restore-file" accept=".db,.sqlite,.sqlite3,application/octet-stream" />
|
||||
<div class="settings-backup-file" id="backup-selected-file" hidden></div>
|
||||
<div id="backup-restore-error" class="form-error" hidden></div>
|
||||
<div class="settings-form-actions">
|
||||
<button type="submit" class="btn btn--danger-outline" id="backup-restore-btn" disabled>${t('settings.backupRestoreButton')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.backupCliTitle')}</h3>
|
||||
<p class="form-hint">${t('settings.backupCliHint')}</p>
|
||||
<pre class="settings-code-block"><code>docker compose stop oikos
|
||||
docker compose run --rm -v "$PWD/oikos-backup.db:/tmp/oikos-restore.db:ro" --entrypoint node oikos scripts/restore-backup.js /tmp/oikos-restore.db
|
||||
docker compose up -d</code></pre>
|
||||
<p class="form-hint">${t('settings.backupCliBackupHint')}</p>
|
||||
<pre class="settings-code-block"><code>docker compose exec oikos node -e "import('./server/db.js').then(async db => { await db.backupToFile('/data/oikos-backup.db'); process.exit(0); })"
|
||||
docker cp oikos:/data/oikos-backup.db ./oikos-backup.db</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -737,6 +795,7 @@ function bindEvents(container, user, users, categories, icsSubscriptions, apiTok
|
||||
bindCategoryEvents(container);
|
||||
bindIcsEvents(container, user, icsSubscriptions);
|
||||
bindApiTokenEvents(container, apiTokens);
|
||||
bindBackupEvents(container);
|
||||
// Theme-Toggle
|
||||
const themeToggle = container.querySelector('#theme-toggle');
|
||||
if (themeToggle) {
|
||||
@@ -1403,6 +1462,75 @@ function bindApiTokenEvents(container, initialTokens) {
|
||||
});
|
||||
}
|
||||
|
||||
function bindBackupEvents(container) {
|
||||
const form = container.querySelector('#backup-restore-form');
|
||||
const fileInput = container.querySelector('#backup-restore-file');
|
||||
const selectedFile = container.querySelector('#backup-selected-file');
|
||||
const restoreBtn = container.querySelector('#backup-restore-btn');
|
||||
const errorEl = container.querySelector('#backup-restore-error');
|
||||
const dropzone = container.querySelector('#backup-dropzone');
|
||||
|
||||
if (!form || !fileInput || !selectedFile || !restoreBtn || !errorEl) return;
|
||||
|
||||
function setFile(file) {
|
||||
if (!file) {
|
||||
selectedFile.hidden = true;
|
||||
selectedFile.textContent = '';
|
||||
restoreBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
selectedFile.textContent = `${file.name} · ${Math.round(file.size / 1024)} KB`;
|
||||
selectedFile.hidden = false;
|
||||
restoreBtn.disabled = false;
|
||||
}
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
errorEl.hidden = true;
|
||||
setFile(fileInput.files?.[0]);
|
||||
});
|
||||
|
||||
dropzone?.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('settings-backup-dropzone--active');
|
||||
});
|
||||
|
||||
dropzone?.addEventListener('dragleave', () => {
|
||||
dropzone.classList.remove('settings-backup-dropzone--active');
|
||||
});
|
||||
|
||||
dropzone?.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('settings-backup-dropzone--active');
|
||||
const file = e.dataTransfer?.files?.[0];
|
||||
if (!file) return;
|
||||
const transfer = new DataTransfer();
|
||||
transfer.items.add(file);
|
||||
fileInput.files = transfer.files;
|
||||
errorEl.hidden = true;
|
||||
setFile(file);
|
||||
});
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const file = fileInput.files?.[0];
|
||||
if (!file) return;
|
||||
if (!await confirmModal(t('settings.backupRestoreConfirm'), { danger: true, confirmLabel: t('settings.backupRestoreButton') })) return;
|
||||
|
||||
errorEl.hidden = true;
|
||||
restoreBtn.disabled = true;
|
||||
restoreBtn.textContent = t('settings.backupRestoring');
|
||||
try {
|
||||
await api.rawPost('/backup/restore', file);
|
||||
window.oikos?.showToast(t('settings.backupRestoredToast'), 'success');
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
showError(errorEl, err.message ?? t('common.errorGeneric'));
|
||||
restoreBtn.disabled = false;
|
||||
restoreBtn.textContent = t('settings.backupRestoreButton');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Kategorie-Verwaltung
|
||||
|
||||
@@ -466,6 +466,108 @@
|
||||
background: var(--color-surface-2);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Backup Management
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-backup-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: var(--space-4);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.settings-backup-card__icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-backup-card--danger .settings-backup-card__icon {
|
||||
background: var(--color-warning-light, var(--color-surface-2));
|
||||
color: var(--color-warning, var(--color-text-primary));
|
||||
}
|
||||
|
||||
.settings-backup-card__icon svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.settings-backup-card__body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-backup-dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 148px;
|
||||
padding: var(--space-5);
|
||||
border: 1.5px dashed var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast), background-color var(--transition-fast), color var(--transition-fast);
|
||||
}
|
||||
|
||||
.settings-backup-dropzone:hover,
|
||||
.settings-backup-dropzone--active {
|
||||
border-color: var(--color-accent);
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.settings-backup-dropzone svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.settings-backup-dropzone span {
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.settings-backup-dropzone small {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.settings-backup-file {
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-2);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.settings-code-block {
|
||||
margin: var(--space-3) 0;
|
||||
padding: var(--space-3);
|
||||
overflow-x: auto;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.settings-backup-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Theme-Toggle
|
||||
-------------------------------------------------------- */
|
||||
|
||||
+4
-4
@@ -13,10 +13,10 @@
|
||||
* → bypassCacheUntil (in-memory + Cache API für SW-Restart-Robustheit)
|
||||
*/
|
||||
|
||||
const SHELL_CACHE = 'oikos-shell-v68';
|
||||
const PAGES_CACHE = 'oikos-pages-v63';
|
||||
const LOCALES_CACHE = 'oikos-locales-v12';
|
||||
const ASSETS_CACHE = 'oikos-assets-v63';
|
||||
const SHELL_CACHE = 'oikos-shell-v69';
|
||||
const PAGES_CACHE = 'oikos-pages-v64';
|
||||
const LOCALES_CACHE = 'oikos-locales-v13';
|
||||
const ASSETS_CACHE = 'oikos-assets-v64';
|
||||
const BYPASS_CACHE = 'oikos-bypass-flag';
|
||||
const ALL_CACHES = [SHELL_CACHE, PAGES_CACHE, LOCALES_CACHE, ASSETS_CACHE];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user