// ============================================================ // SITE BACKUP // ============================================================ (function() { var btn = document.getElementById('createSiteBackupBtn'); var progress = document.getElementById('backupProgress'); var progressText = document.getElementById('backupProgressText'); btn.addEventListener('click', async function() { btn.disabled = true; btn.style.display = 'none'; progress.style.display = 'flex'; try { var formData = new FormData(); formData.append('action', 'create-site-backup'); formData.append('csrf_token', CSRF_TOKEN); var response = await fetch('api.php', { method: 'POST', body: formData }); var result = await response.json(); if (result.success) { progressText.textContent = t('settings.backup_downloading'); var downloadUrl = 'api.php?action=download-site-backup' + '&token=' + encodeURIComponent(result.data.token) + '&csrf_token=' + encodeURIComponent(CSRF_TOKEN) + '&filename=' + encodeURIComponent(result.data.filename); var a = document.createElement('a'); a.href = downloadUrl; a.download = result.data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); showToast(t('toast.backup_site_created'), 'success'); } else { showToast(t('toast.backup_site_failed', {message: result.message}), 'error'); } } catch (error) { showToast(t('toast.backup_site_failed', {message: error.message}), 'error'); } setTimeout(function() { btn.disabled = false; btn.style.display = ''; progress.style.display = 'none'; progressText.textContent = t('settings.backup_creating'); }, 2000); }); })(); // ============================================================ // RESTORE FROM BACKUP // ============================================================ (function() { var fileInput = document.getElementById('restoreFileInput'); var selectBtn = document.getElementById('restoreSelectBtn'); var filenameEl = document.getElementById('restoreFilename'); var modeSelector = document.getElementById('restoreModeSelector'); var actionsEl = document.getElementById('restoreActions'); var restoreBtn = document.getElementById('restoreBtn'); var progress = document.getElementById('restoreProgress'); var progressText = document.getElementById('restoreProgressText'); selectBtn.addEventListener('click', function() { fileInput.click(); }); fileInput.addEventListener('change', function() { var file = fileInput.files[0]; if (file) { filenameEl.textContent = file.name + ' (' + (file.size / 1024 / 1024).toFixed(1) + ' MB)'; filenameEl.style.display = ''; selectBtn.querySelector('span').textContent = t('settings.restore_change_file'); modeSelector.style.display = ''; actionsEl.style.display = ''; } else { filenameEl.style.display = 'none'; selectBtn.querySelector('span').textContent = t('settings.restore_select_file'); modeSelector.style.display = 'none'; actionsEl.style.display = 'none'; } }); restoreBtn.addEventListener('click', function() { var file = fileInput.files[0]; if (!file) return; var mode = document.querySelector('input[name="restore_mode"]:checked').value; var warningKey = mode === 'full' ? 'settings.restore_warning_full' : 'settings.restore_warning_content'; showModal(t('settings.restore_title'), t(warningKey), async function() { closeModal(); restoreBtn.disabled = true; restoreBtn.style.display = 'none'; progress.style.display = 'flex'; try { var formData = new FormData(); formData.append('action', 'restore-site-backup'); formData.append('csrf_token', CSRF_TOKEN); formData.append('restore_mode', mode); formData.append('backup_zip', file); var response = await fetch('api.php', { method: 'POST', body: formData }); var result = await response.json(); if (result.success) { var toastKey = mode === 'full' ? 'toast.restore_success_full' : 'toast.restore_success_content'; showToast(t(toastKey, {extracted: result.data.extracted}), 'success'); setTimeout(function() { location.reload(); }, 2000); } else { showToast(t('toast.restore_failed', {message: result.message}), 'error'); restoreBtn.disabled = false; restoreBtn.style.display = ''; progress.style.display = 'none'; } } catch (error) { showToast(t('toast.restore_failed', {message: error.message}), 'error'); restoreBtn.disabled = false; restoreBtn.style.display = ''; progress.style.display = 'none'; } }); }); })(); // ============================================================ // SCHEDULED / AUTOMATED BACKUPS // ============================================================ (function() { var statusEl = document.getElementById('scheduledStatus'); var lastRunEl = document.getElementById('scheduledLastRun'); var statusMsgEl = document.getElementById('scheduledStatusMessage'); var storageCountEl = document.getElementById('scheduledStorageCount'); var storageDatesEl = document.getElementById('scheduledStorageDates'); var form = document.getElementById('scheduledBackupForm'); var enabledEl = document.getElementById('scheduledEnabled'); var cronModeEls = Array.from(document.querySelectorAll('input[name="scheduledCronMode"]')); var webCronUrlEl = document.getElementById('webCronUrl'); var dailyEl = document.getElementById('retentionDaily'); var weeklyEl = document.getElementById('retentionWeekly'); var monthlyEl = document.getElementById('retentionMonthly'); var yearlyEl = document.getElementById('retentionYearly'); var limitEl = document.getElementById('storageLimitMb'); var saveBtn = document.getElementById('scheduledSaveBtn'); var listBody = document.getElementById('scheduledListBody'); var cronCopyBtns = Array.from(document.querySelectorAll('[data-copy-code]')); var remoteListEl = document.getElementById('remoteTargetList'); var remoteAddBtn = document.getElementById('remoteAddBtn'); var remoteSelectEl = document.getElementById('remoteProviderSelect'); var remoteTargets = []; var remoteProviders = {}; var remoteFileCache = {}; if (!statusEl) return; // Tab not on this dashboard var remoteFieldLabels = { app_key: t('settings.remote_field_app_key'), access_token: t('settings.remote_field_access_token'), refresh_token: t('settings.remote_field_refresh_token'), client_id: t('settings.remote_field_client_id'), client_secret: t('settings.remote_field_client_secret'), path: t('settings.remote_field_path'), folder_id: t('settings.remote_field_folder_id'), folder_path: t('settings.remote_field_folder_path'), host: t('settings.remote_field_host'), port: t('settings.remote_field_port'), username: t('settings.remote_field_username'), password: t('settings.remote_field_password'), remote_path: t('settings.remote_field_remote_path'), ssl: t('settings.remote_field_ssl'), passive: t('settings.remote_field_passive'), endpoint: t('settings.remote_field_endpoint'), region: t('settings.remote_field_region'), bucket: t('settings.remote_field_bucket'), prefix: t('settings.remote_field_prefix'), access_key: t('settings.remote_field_access_key'), secret_key: t('settings.remote_field_secret_key'), path_style: t('settings.remote_field_path_style'), url: t('settings.remote_field_url'), bearer_token: t('settings.remote_field_bearer_token') }; var remotePlaceholders = { dropbox: { app_key: 'Dropbox app key', path: '/raiju Backups' }, google_drive: { client_id: 'Google OAuth client ID', client_secret: t('settings.remote_placeholder_optional'), folder_id: t('settings.remote_placeholder_optional') }, onedrive: { client_id: 'Microsoft app client ID', client_secret: t('settings.remote_placeholder_optional'), folder_path: '/raiju Backups' }, sftp: { port: '22', remote_path: '/home/user/backups' }, ftp: { port: '21', remote_path: 'backups', ssl: '0', passive: '1' }, s3: { endpoint: 'https://s3.example.com', region: 'eu-central-1', prefix: 'raiju', path_style: '0' }, webdav: { url: 'https://cloud.example.com/remote.php/dav/files/user/backups' } }; var remoteProviderHints = { sftp: t('settings.remote_hint_sftp'), ftp: t('settings.remote_hint_ftp') }; var remoteDefaultSettings = { sftp: { port: '22' }, ftp: { port: '21', passive: true } }; function fmtSize(bytes) { if (!bytes) return '0 B'; if (bytes >= 1073741824) return (bytes / 1073741824).toFixed(2) + ' GB'; if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB'; if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'; return bytes + ' B'; } function fmtDate(unixOrIso) { if (!unixOrIso) return '—'; var d = (typeof unixOrIso === 'number') ? new Date(unixOrIso * 1000) : new Date(unixOrIso); if (isNaN(d.getTime())) return '—'; return rjFormatDate(d, 'datetime'); } function selectedCronMode() { var checked = cronModeEls.find(function(input) { return input.checked; }); return checked ? checked.value : 'server'; } function setCronMode(mode) { cronModeEls.forEach(function(input) { input.checked = input.value === (mode === 'web' ? 'web' : 'server'); }); } function webCronUrl(token) { if (!token) return '—'; var url = new URL('../api/backup-cron.php', window.location.href); url.searchParams.set('token', token); return url.toString(); } async function refresh() { try { var res = await fetch('api.php?action=backup-status'); var json = await res.json(); if (!json.success) throw new Error(json.message); var s = json.data; // Form fields enabledEl.checked = !!s.enabled; setCronMode(s.cron_mode || 'server'); if (webCronUrlEl) webCronUrlEl.textContent = webCronUrl(s.web_cron_token || ''); dailyEl.value = s.retention.daily; weeklyEl.value = s.retention.weekly; monthlyEl.value = s.retention.monthly; yearlyEl.value = s.retention.yearly; limitEl.value = s.storage_limit_mb; remoteTargets = s.remote_targets || []; remoteProviders = s.remote_providers || {}; renderRemoteTargets(); // Last run + cron health if (s.last_run) { lastRunEl.textContent = fmtDate(s.last_run); var ageMs = Date.now() - new Date(s.last_run).getTime(); var ageDays = Math.floor(ageMs / 86400000); if (s.last_status === 'error') { statusEl.dataset.state = 'error'; statusMsgEl.textContent = t('settings.last_run_error', { message: s.last_message || '' }); } else if (s.enabled && ageDays >= 2) { statusEl.dataset.state = 'warning'; statusMsgEl.textContent = t('settings.last_run_warning', { days: ageDays }); } else { statusEl.dataset.state = 'ok'; statusMsgEl.textContent = t('settings.last_run_ok'); } } else { lastRunEl.textContent = t('settings.last_run_never'); if (s.enabled) { statusEl.dataset.state = 'warning'; statusMsgEl.textContent = t('settings.last_run_warning', { days: '∞' }); } else { statusEl.dataset.state = 'idle'; statusMsgEl.textContent = ''; } } // Storage summary storageCountEl.textContent = t('settings.storage_count', { count: s.count, size: fmtSize(s.total_bytes) }); if (s.oldest && s.newest && s.count > 0) { storageDatesEl.textContent = ' · ' + t('settings.storage_oldest', { date: fmtDate(s.oldest) }) + ' · ' + t('settings.storage_newest', { date: fmtDate(s.newest) }); } else { storageDatesEl.textContent = ''; } // Backup list var listRes = await fetch('api.php?action=backup-list'); var listJson = await listRes.json(); if (!listJson.success) throw new Error(listJson.message); var backups = listJson.data.backups || []; renderList(backups); } catch (err) { statusEl.dataset.state = 'error'; statusMsgEl.textContent = err.message || String(err); } } function renderList(backups) { if (backups.length === 0) { listBody.innerHTML = '' + t('settings.backup_list_empty') + ''; return; } listBody.innerHTML = ''; backups.forEach(function(b) { var tr = document.createElement('tr'); var tdDate = document.createElement('td'); tdDate.textContent = fmtDate(b.mtime); tr.appendChild(tdDate); var tdTier = document.createElement('td'); var tierBadge = document.createElement('span'); tierBadge.className = 'scheduled-tier scheduled-tier--' + b.tier; tierBadge.textContent = b.tier; tdTier.appendChild(tierBadge); tr.appendChild(tdTier); var tdSize = document.createElement('td'); tdSize.textContent = fmtSize(b.size); tr.appendChild(tdSize); var tdActions = document.createElement('td'); tdActions.className = 'scheduled-list__actions'; var dlBtn = document.createElement('button'); dlBtn.className = 'btn btn-secondary btn-sm'; dlBtn.textContent = t('settings.backup_download'); dlBtn.onclick = function() { downloadBackup(b.file); }; tdActions.appendChild(dlBtn); var rsBtn = document.createElement('button'); rsBtn.className = 'btn btn-secondary btn-sm'; rsBtn.textContent = t('settings.backup_restore_from_pool'); rsBtn.onclick = function() { restoreBackup(b.file); }; tdActions.appendChild(rsBtn); var delBtn = document.createElement('button'); delBtn.className = 'btn btn-danger btn-sm'; delBtn.textContent = t('settings.backup_delete'); delBtn.onclick = function() { deleteBackup(b.file); }; tdActions.appendChild(delBtn); if (remoteTargets.length > 0) { var upBtn = document.createElement('button'); upBtn.className = 'btn btn-secondary btn-sm'; upBtn.textContent = t('settings.remote_upload'); upBtn.onclick = function() { uploadBackupRemote(b.file, upBtn); }; tdActions.appendChild(upBtn); } tr.appendChild(tdActions); listBody.appendChild(tr); }); } function targetLabel(type) { return (remoteProviders[type] && remoteProviders[type].label) || type; } function localizeRemoteMessage(message) { var text = message || ''; var map = { 'Dropbox access token is missing.': t('settings.remote_error_dropbox_access_token_missing'), 'Google Drive access token is missing.': t('settings.remote_error_google_access_token_missing'), 'OneDrive access token is missing.': t('settings.remote_error_onedrive_access_token_missing'), 'PHP ssh2 extension is required for SFTP/SCP uploads.': t('settings.remote_error_ssh2_required'), 'PHP FTP extension is required for FTP/FTPS uploads.': t('settings.remote_error_ftp_required') }; return map[text] || text; } function makeTarget(type) { return { id: type + '-' + Date.now().toString(36), type: type, name: targetLabel(type), enabled: true, settings: Object.assign({}, remoteDefaultSettings[type] || {}), last_upload: null, last_status: null, last_message: null, last_file: null }; } function collectRemoteTargets() { if (!remoteListEl) return []; return Array.from(remoteListEl.querySelectorAll('.backup-remote-target')).map(function(card) { var target = { id: card.dataset.id, type: card.dataset.type, name: card.querySelector('[data-field="name"]').value.trim(), enabled: card.querySelector('[data-field="enabled"]').checked, settings: {}, last_upload: card.dataset.lastUpload || null, last_status: card.dataset.lastStatus || null, last_message: card.dataset.lastMessage || null, last_file: card.dataset.lastFile || null }; card.querySelectorAll('[data-setting]').forEach(function(input) { target.settings[input.dataset.setting] = input.type === 'checkbox' ? input.checked : input.value; }); return target; }); } function renderRemoteTargets() { if (!remoteListEl) return; if (!remoteTargets.length) { remoteListEl.innerHTML = '
' + t('settings.remote_empty') + '
'; return; } remoteListEl.innerHTML = ''; remoteTargets.forEach(function(target, idx) { var provider = remoteProviders[target.type] || {}; var oauthProvider = target.type === 'dropbox' || target.type === 'google_drive' || target.type === 'onedrive'; var card = document.createElement('div'); card.className = 'backup-remote-target'; card.dataset.id = target.id; card.dataset.type = target.type; card.dataset.lastUpload = target.last_upload || ''; card.dataset.lastStatus = target.last_status || ''; card.dataset.lastMessage = target.last_message || ''; card.dataset.lastFile = target.last_file || ''; var header = document.createElement('div'); header.className = 'backup-remote-target__header'; var title = document.createElement('div'); title.className = 'backup-remote-target__title'; var enabled = document.createElement('input'); enabled.type = 'checkbox'; enabled.checked = !!target.enabled; enabled.dataset.field = 'enabled'; title.appendChild(enabled); var name = document.createElement('input'); name.type = 'text'; name.value = target.name || targetLabel(target.type); name.dataset.field = 'name'; title.appendChild(name); var badge = document.createElement('span'); badge.textContent = targetLabel(target.type); title.appendChild(badge); header.appendChild(title); var actions = document.createElement('div'); actions.className = 'backup-remote-target__actions'; if (target.type === 'dropbox' || target.type === 'google_drive' || target.type === 'onedrive') { var connectBtn = document.createElement('button'); connectBtn.type = 'button'; connectBtn.className = 'btn btn-secondary btn-sm'; connectBtn.textContent = target.type === 'dropbox' ? t('settings.remote_connect_dropbox') : (target.type === 'google_drive' ? t('settings.remote_connect_google') : t('settings.remote_connect_onedrive')); connectBtn.onclick = function() { connectOAuthTarget(target.id, target.type); }; actions.appendChild(connectBtn); } var testBtn = document.createElement('button'); testBtn.type = 'button'; testBtn.className = 'btn btn-secondary btn-sm'; testBtn.textContent = t('settings.remote_test'); testBtn.onclick = function() { testRemoteTarget(target.id); }; actions.appendChild(testBtn); var filesBtn = document.createElement('button'); filesBtn.type = 'button'; filesBtn.className = 'btn btn-secondary btn-sm'; filesBtn.textContent = t('settings.remote_files'); filesBtn.onclick = function() { loadRemoteFiles(target.id); }; actions.appendChild(filesBtn); var removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'btn btn-danger btn-sm'; removeBtn.textContent = t('btn.delete'); removeBtn.onclick = function() { remoteTargets.splice(idx, 1); renderRemoteTargets(); }; actions.appendChild(removeBtn); header.appendChild(actions); card.appendChild(header); var grid = document.createElement('div'); grid.className = 'backup-remote-target__grid'; (provider.fields || []).forEach(function(field) { if (oauthProvider && (field === 'access_token' || field === 'refresh_token')) return; if (target.type === 'dropbox' && provider.has_global_oauth && field === 'app_key') return; if ((target.type === 'google_drive' || target.type === 'onedrive') && provider.has_global_oauth && (field === 'client_id' || field === 'client_secret')) return; var label = document.createElement('label'); if (field === 'path_style' || field === 'ssl' || field === 'passive') label.className = 'backup-remote-target__check'; var span = document.createElement('span'); span.textContent = remoteFieldLabels[field] || field; label.appendChild(span); var input = document.createElement('input'); input.dataset.setting = field; if (field === 'path_style' || field === 'ssl' || field === 'passive') { input.type = 'checkbox'; input.checked = !!(target.settings && target.settings[field]); } else { input.type = 'text'; input.value = (target.settings && target.settings[field]) || ''; input.placeholder = (remotePlaceholders[target.type] && remotePlaceholders[target.type][field]) || ''; input.name = 'raiju_remote_' + target.type + '_' + field + '_' + target.id; input.id = 'raiju_remote_' + target.id + '_' + field; input.autocomplete = 'off'; input.autocapitalize = 'off'; input.spellcheck = false; input.setAttribute('data-lpignore', 'true'); input.setAttribute('data-1p-ignore', 'true'); input.setAttribute('data-form-type', 'other'); if ((provider.secret_fields || []).indexOf(field) !== -1) { input.className = 'backup-remote-target__secret'; } } label.appendChild(input); grid.appendChild(label); }); card.appendChild(grid); if (remoteProviderHints[target.type]) { var hint = document.createElement('p'); hint.className = 'form-hint backup-remote-target__hint'; hint.textContent = remoteProviderHints[target.type]; card.appendChild(hint); } var status = document.createElement('div'); status.className = 'backup-remote-target__status'; status.dataset.state = target.last_status || 'idle'; status.textContent = target.last_upload ? t('settings.remote_last_upload', { date: fmtDate(target.last_upload), message: localizeRemoteMessage(target.last_message || '') }) : t('settings.remote_not_tested'); card.appendChild(status); if (remoteFileCache[target.id]) { card.appendChild(renderRemoteFileList(target.id, remoteFileCache[target.id])); } remoteListEl.appendChild(card); }); } function renderRemoteFileList(targetId, files) { var box = document.createElement('div'); box.className = 'backup-remote-files'; if (!files.length) { box.textContent = t('settings.remote_files_empty'); return box; } files.forEach(function(file) { var row = document.createElement('div'); row.className = 'backup-remote-file'; var meta = document.createElement('div'); meta.className = 'backup-remote-file__meta'; var name = document.createElement('strong'); name.textContent = file.file; var details = document.createElement('span'); details.textContent = file.tier + ' · ' + fmtSize(file.size) + ' · ' + fmtDate(file.mtime); meta.appendChild(name); meta.appendChild(details); row.appendChild(meta); var actions = document.createElement('div'); actions.className = 'backup-remote-file__actions'; var importBtn = document.createElement('button'); importBtn.type = 'button'; importBtn.className = 'btn btn-secondary btn-sm'; importBtn.textContent = t('settings.remote_import'); importBtn.onclick = function() { importRemoteBackup(targetId, file.file); }; actions.appendChild(importBtn); var deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'btn btn-danger btn-sm'; deleteBtn.textContent = t('btn.delete'); deleteBtn.onclick = function() { deleteRemoteBackup(targetId, file.file); }; actions.appendChild(deleteBtn); row.appendChild(actions); box.appendChild(row); }); return box; } async function saveRemoteSettingsOnly() { remoteTargets = collectRemoteTargets(); var fd = new FormData(); fd.append('action', 'backup-update-settings'); fd.append('csrf_token', CSRF_TOKEN); fd.append('remote_targets', JSON.stringify(remoteTargets)); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (!json.success) throw new Error(json.message); remoteTargets = json.data.remote_targets || remoteTargets; remoteProviders = json.data.remote_providers || remoteProviders; renderRemoteTargets(); } async function testRemoteTarget(targetId) { try { await saveRemoteSettingsOnly(); var fd = new FormData(); fd.append('action', 'backup-test-remote'); fd.append('csrf_token', CSRF_TOKEN); fd.append('target_id', targetId); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (!json.success) throw new Error(json.message); showToast(t('toast.remote_test_success'), 'success'); refresh(); } catch (err) { showToast(t('toast.remote_test_failed', { message: localizeRemoteMessage(err.message || String(err)) }), 'error'); refresh(); } } async function connectOAuthTarget(targetId, type) { var popup = window.open('about:blank', '_blank'); try { await saveRemoteSettingsOnly(); var target = remoteTargets.find(function(tg) { return tg.id === targetId; }); var requiredField = type === 'dropbox' ? 'app_key' : 'client_id'; var provider = remoteProviders[type] || {}; if (!target || !target.settings || (!target.settings[requiredField] && !provider.has_global_oauth)) { if (popup) popup.close(); showToast(t(type === 'dropbox' ? 'toast.remote_dropbox_app_key_missing' : 'toast.remote_client_id_missing'), 'error'); return; } var action = type === 'dropbox' ? 'backup-dropbox-oauth-start' : (type === 'google_drive' ? 'backup-google-oauth-start' : 'backup-onedrive-oauth-start'); var url = 'api.php?action=' + action + '&csrf_token=' + encodeURIComponent(CSRF_TOKEN) + '&target_id=' + encodeURIComponent(targetId); if (popup) { popup.location = url; } else { window.location.href = url; } } catch (err) { if (popup) popup.close(); showToast(t('toast.remote_oauth_connect_failed', { message: localizeRemoteMessage(err.message || String(err)) }), 'error'); } } async function uploadBackupRemote(file, button) { var originalHtml = button ? button.innerHTML : ''; var actionButtons = []; function setBusy(isBusy) { if (!button) return; var row = button.closest('tr'); actionButtons = row ? Array.from(row.querySelectorAll('button')) : [button]; actionButtons.forEach(function(btn) { btn.disabled = isBusy; }); button.classList.toggle('is-loading', isBusy); button.setAttribute('aria-busy', isBusy ? 'true' : 'false'); button.innerHTML = isBusy ? '' + t('settings.remote_uploading') + '' : originalHtml; } try { setBusy(true); await saveRemoteSettingsOnly(); var enabledTargets = remoteTargets.filter(function(tg) { return tg.enabled; }); if (!enabledTargets.length) { showToast(t('toast.remote_no_enabled_targets'), 'error'); return; } var fd = new FormData(); fd.append('action', 'backup-upload-remote'); fd.append('csrf_token', CSRF_TOKEN); fd.append('file', file); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (!json.success) throw new Error(json.message); showToast(t('toast.remote_upload_success'), 'success'); refresh(); } catch (err) { showToast(t('toast.remote_upload_failed', { message: localizeRemoteMessage(err.message || String(err)) }), 'error'); refresh(); } finally { setBusy(false); } } async function loadRemoteFiles(targetId) { try { var res = await fetch('api.php?action=backup-remote-list&target_id=' + encodeURIComponent(targetId)); var json = await res.json(); if (!json.success) throw new Error(json.message); remoteFileCache[targetId] = json.data.files || []; renderRemoteTargets(); } catch (err) { showToast(t('toast.remote_list_failed', { message: localizeRemoteMessage(err.message || String(err)) }), 'error'); } } async function importRemoteBackup(targetId, file) { try { var fd = new FormData(); fd.append('action', 'backup-remote-import'); fd.append('csrf_token', CSRF_TOKEN); fd.append('target_id', targetId); fd.append('file', file); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (!json.success) throw new Error(json.message); showToast(t('toast.remote_import_success'), 'success'); refresh(); } catch (err) { showToast(t('toast.remote_import_failed', { message: localizeRemoteMessage(err.message || String(err)) }), 'error'); } } function deleteRemoteBackup(targetId, file) { showModal(t('settings.remote_delete'), t('settings.remote_delete_confirm'), async function() { closeModal(); try { var fd = new FormData(); fd.append('action', 'backup-remote-delete'); fd.append('csrf_token', CSRF_TOKEN); fd.append('target_id', targetId); fd.append('file', file); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (!json.success) throw new Error(json.message); showToast(t('toast.remote_delete_success'), 'success'); await loadRemoteFiles(targetId); } catch (err) { showToast(t('toast.remote_delete_failed', { message: localizeRemoteMessage(err.message || String(err)) }), 'error'); } }); } async function downloadBackup(file) { try { var fd = new FormData(); fd.append('action', 'backup-prepare-download'); fd.append('csrf_token', CSRF_TOKEN); fd.append('file', file); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (!json.success) throw new Error(json.message); var url = 'api.php?action=download-site-backup' + '&token=' + encodeURIComponent(json.data.token) + '&csrf_token=' + encodeURIComponent(CSRF_TOKEN) + '&filename=' + encodeURIComponent(json.data.filename); var a = document.createElement('a'); a.href = url; a.download = json.data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); } catch (err) { showToast(t('toast.backup_site_failed', { message: err.message || String(err) }), 'error'); } } function restoreBackup(file) { showModal( t('settings.restore_title'), '', function() { var selected = document.querySelector('input[name="restoreMode"]:checked'); var mode = selected ? selected.value : 'content'; closeModal(); var warningKey = mode === 'full' ? 'settings.backup_restore_pool_warning_full' : 'settings.backup_restore_pool_warning_content'; showModal(t('settings.restore_title'), t(warningKey), async function() { closeModal(); try { var fd = new FormData(); fd.append('action', 'restore-site-backup'); fd.append('csrf_token', CSRF_TOKEN); fd.append('restore_mode', mode); fd.append('pool_file', file); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (json.success) { var toastKey = mode === 'full' ? 'toast.restore_success_full' : 'toast.restore_success_content'; showToast(t(toastKey, { extracted: json.data.extracted }), 'success'); setTimeout(function() { location.reload(); }, 2000); } else { showToast(t('toast.restore_failed', { message: json.message }), 'error'); } } catch (err) { showToast(t('toast.restore_failed', { message: err.message || String(err) }), 'error'); } }); }, { html: true, confirmText: t('settings.restore_title'), confirmClass: 'btn btn-primary' } ); } function deleteBackup(file) { showModal(t('settings.backup_delete'), t('settings.backup_delete_confirm'), async function() { closeModal(); try { var fd = new FormData(); fd.append('action', 'backup-delete'); fd.append('csrf_token', CSRF_TOKEN); fd.append('file', file); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (json.success) { showToast(t('toast.backup_deleted'), 'success'); refresh(); } else { showToast(t('toast.backup_delete_failed', { message: json.message }), 'error'); } } catch (err) { showToast(t('toast.backup_delete_failed', { message: err.message || String(err) }), 'error'); } }); } form.addEventListener('submit', async function(e) { e.preventDefault(); saveBtn.disabled = true; try { var fd = new FormData(); fd.append('action', 'backup-update-settings'); fd.append('csrf_token', CSRF_TOKEN); fd.append('enabled', enabledEl.checked ? 'true' : 'false'); fd.append('cron_mode', selectedCronMode()); fd.append('retention_daily', dailyEl.value); fd.append('retention_weekly', weeklyEl.value); fd.append('retention_monthly', monthlyEl.value); fd.append('retention_yearly', yearlyEl.value); fd.append('storage_limit_mb', limitEl.value); remoteTargets = collectRemoteTargets(); fd.append('remote_targets', JSON.stringify(remoteTargets)); var res = await fetch('api.php', { method: 'POST', body: fd }); var json = await res.json(); if (json.success) { showToast(t('toast.scheduled_backup_settings_saved'), 'success'); refresh(); } else { showToast(t('toast.scheduled_backup_settings_failed', { message: json.message }), 'error'); } } catch (err) { showToast(t('toast.scheduled_backup_settings_failed', { message: err.message || String(err) }), 'error'); } finally { saveBtn.disabled = false; } }); if (remoteAddBtn) { remoteAddBtn.addEventListener('click', function() { remoteTargets = collectRemoteTargets(); remoteTargets.push(makeTarget(remoteSelectEl.value)); renderRemoteTargets(); }); } window.addEventListener('message', function(event) { if (event.origin === window.location.origin && event.data && event.data.type === 'raiju:backup-oauth') { refresh(); showToast(t('toast.remote_oauth_connected'), 'success'); } }); cronCopyBtns.forEach(function(cronCopyBtn) { cronCopyBtn.addEventListener('click', function() { var codeEl = document.getElementById(cronCopyBtn.dataset.copyCode); if (!codeEl) return; var text = codeEl.textContent; var done = function() { var orig = cronCopyBtn.textContent; cronCopyBtn.textContent = t('settings.cron_copied'); setTimeout(function() { cronCopyBtn.textContent = orig; }, 1500); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done, done); } else { // Fallback: select + execCommand var range = document.createRange(); range.selectNode(codeEl); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); try { document.execCommand('copy'); done(); } catch (e) { done(); } window.getSelection().removeAllRanges(); } }); }); // Refresh whenever the backup tab is shown (lazy: only fetch when visible). var backupTab = document.getElementById('backupTab'); var observer = new MutationObserver(function() { if (backupTab.style.display !== 'none') refresh(); }); observer.observe(backupTab, { attributes: true, attributeFilter: ['style'] }); if (backupTab.style.display !== 'none') refresh(); // Refresh after a manual create-site-backup so the new entry shows up. var createBtn = document.getElementById('createSiteBackupBtn'); if (createBtn) { createBtn.addEventListener('click', function() { setTimeout(refresh, 3000); }); } })();