diff --git a/docs/installation.md b/docs/installation.md index 8cdaf88..39f1e07 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -457,23 +457,33 @@ The SQLite database lives in a Docker named volume called `oikos_data`, mounted ### Backup -Copy the database from the running container to your host: +Use the built-in backup helper to create a consistent SQLite backup from the running container, then copy it to your host: ```bash -docker compose exec oikos cp /data/oikos.db /data/oikos-backup.db +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-$(date +%Y%m%d).db -docker compose exec oikos rm /data/oikos-backup.db ``` +Admins can also download a backup from **Settings → Backup Management**. + ### Restore -Copy a backup file back into the container and restart: +Admins can restore a backup from **Settings → Backup Management**. For operational restores via Docker Compose, stop the running app, mount the backup into a temporary container that uses the same Docker volume, and run the restore helper: ```bash -docker cp ./oikos-backup-20260401.db oikos:/data/oikos.db -docker compose restart +docker compose stop oikos +docker compose run --rm -v "$PWD/oikos-backup-20260401.db:/tmp/oikos-restore.db:ro" --entrypoint node oikos scripts/restore-backup.js /tmp/oikos-restore.db +docker compose up -d ``` +For a local CLI restore outside Docker, set the same environment variables used by the app and run: + +```bash +DB_PATH=/path/to/oikos.db node --import dotenv/config scripts/restore-backup.js ./oikos-backup-20260401.db +``` + +The restore helper validates that the file is an Oikos database before replacing the active database. It also keeps a pre-restore copy next to the database file for emergency rollback. + ### Automated Backups Add a cron job to back up daily (adjust the path to your preference): @@ -485,7 +495,7 @@ crontab -e Add this line: ``` -0 3 * * * docker cp oikos:/data/oikos.db /path/to/backups/oikos-$(date +\%Y\%m\%d).db +0 3 * * * docker compose exec -T oikos node -e "import('./server/db.js').then(async db => { await db.backupToFile('/data/oikos-cron-backup.db'); process.exit(0); })" && docker cp oikos:/data/oikos-cron-backup.db /path/to/backups/oikos-$(date +\%Y\%m\%d).db ``` This creates a backup at 3:00 AM every day. diff --git a/public/api.js b/public/api.js index b41817d..f05c83d 100644 --- a/public/api.js +++ b/public/api.js @@ -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), diff --git a/public/locales/ar.json b/public/locales/ar.json index f342dae..419f4bd 100644 --- a/public/locales/ar.json +++ b/public/locales/ar.json @@ -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": "تخطيط عائلي. آمن. يحترم الخصوصية. مفتوح المصدر.", diff --git a/public/locales/de.json b/public/locales/de.json index bc269bf..8200f3c 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -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.", diff --git a/public/locales/el.json b/public/locales/el.json index b1c39e6..f56659a 100644 --- a/public/locales/el.json +++ b/public/locales/el.json @@ -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": "Οικογενειακός προγραμματισμός. Ασφαλής. Φιλικός προς την ιδιωτικότητα. Ανοιχτός κώδικας.", diff --git a/public/locales/en.json b/public/locales/en.json index c9c5d11..8eeb9d9 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -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.", diff --git a/public/locales/es.json b/public/locales/es.json index 4e6716a..465d8a5 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -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.", diff --git a/public/locales/fr.json b/public/locales/fr.json index 82cf675..dc2b111 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -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.", diff --git a/public/locales/hi.json b/public/locales/hi.json index 0f2c858..382af74 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -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": "पारिवारिक योजना। सुरक्षित। गोपनीयता-अनुकूल। ओपन सोर्स।", diff --git a/public/locales/it.json b/public/locales/it.json index 9e085c7..c7e2287 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -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.", diff --git a/public/locales/ja.json b/public/locales/ja.json index 7995b14..9fc43a0 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -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": "家族計画。安全。プライバシー重視。オープンソース。", diff --git a/public/locales/pt.json b/public/locales/pt.json index e02332e..1fbd7d8 100644 --- a/public/locales/pt.json +++ b/public/locales/pt.json @@ -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.", diff --git a/public/locales/ru.json b/public/locales/ru.json index 4ad7293..c370d92 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -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": "Семейное планирование. Безопасно. С уважением к приватности. Открытый исходный код.", diff --git a/public/locales/sv.json b/public/locales/sv.json index fb700de..340ac4f 100644 --- a/public/locales/sv.json +++ b/public/locales/sv.json @@ -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.", diff --git a/public/locales/tr.json b/public/locales/tr.json index bfa2345..0a26d13 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -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.", diff --git a/public/locales/uk.json b/public/locales/uk.json index f27b19e..79f11d5 100644 --- a/public/locales/uk.json +++ b/public/locales/uk.json @@ -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": "Планування для родини. Безпечно. Конфіденційно. Відкритий код.", diff --git a/public/locales/zh.json b/public/locales/zh.json index fdbbafb..a5a417a 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -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": "家庭规划。安全。注重隐私。开源。", diff --git a/public/pages/settings.js b/public/pages/settings.js index 976ab2a..f7954c3 100644 --- a/public/pages/settings.js +++ b/public/pages/settings.js @@ -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' ? `` : ''} ${user?.role === 'admin' ? `` : ''} + ${user?.role === 'admin' ? `` : ''} @@ -712,6 +714,62 @@ export async function render(container, { user }) { + + ${user?.role === 'admin' ? ` + +
${t('settings.backupDownloadHint')}
+ +${t('settings.backupRestoreHint')}
+ +${t('settings.backupCliHint')}
+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
+ ${t('settings.backupCliBackupHint')}
+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
+