mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-16 01:01:58 +00:00
Add RLUtils class for managing RL/AI dashboard endpoints
- Implemented methods for fetching AI stats, training history, and recent experiences. - Added functionality to set operation mode (MANUAL, AUTO, AI) with appropriate handling. - Included helper methods for querying the database and sending JSON responses. - Integrated model metadata extraction for visualization purposes.
This commit is contained in:
459
web/js/pages/backup.js
Normal file
459
web/js/pages/backup.js
Normal file
@@ -0,0 +1,459 @@
|
||||
import { ResourceTracker } from '../core/resource-tracker.js';
|
||||
import { api } from '../core/api.js';
|
||||
import { el, $, empty, toast } from '../core/dom.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { initSharedSidebarLayout } from '../core/sidebar-layout.js';
|
||||
|
||||
const PAGE = 'backup';
|
||||
|
||||
let tracker = null;
|
||||
let disposeSidebarLayout = null;
|
||||
let backups = [];
|
||||
let currentSection = 'backup';
|
||||
let pendingModalAction = null;
|
||||
|
||||
export async function mount(container) {
|
||||
tracker = new ResourceTracker(PAGE);
|
||||
const shell = buildShell();
|
||||
container.appendChild(shell);
|
||||
tracker.trackEventListener(window, 'keydown', (e) => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
});
|
||||
disposeSidebarLayout = initSharedSidebarLayout(shell, {
|
||||
sidebarSelector: '.backup-sidebar',
|
||||
mainSelector: '.backup-main',
|
||||
storageKey: 'sidebar:backup',
|
||||
toggleLabel: t('common.menu'),
|
||||
});
|
||||
wireEvents();
|
||||
switchSection('backup');
|
||||
await loadBackups();
|
||||
}
|
||||
|
||||
export function unmount() {
|
||||
if (disposeSidebarLayout) {
|
||||
try { disposeSidebarLayout(); } catch { /* noop */ }
|
||||
disposeSidebarLayout = null;
|
||||
}
|
||||
if (tracker) {
|
||||
tracker.cleanupAll();
|
||||
tracker = null;
|
||||
}
|
||||
backups = [];
|
||||
currentSection = 'backup';
|
||||
pendingModalAction = null;
|
||||
}
|
||||
|
||||
function buildShell() {
|
||||
return el('div', { class: 'page-backup page-with-sidebar' }, [
|
||||
el('aside', { class: 'backup-sidebar page-sidebar' }, [
|
||||
el('div', { class: 'sidehead backup-sidehead' }, [
|
||||
el('h3', { class: 'backup-side-title' }, [t('backup.title')]),
|
||||
el('div', { class: 'spacer' }),
|
||||
el('button', { class: 'btn', id: 'hideSidebar', 'data-hide-sidebar': '1', type: 'button' }, [t('common.hide')]),
|
||||
]),
|
||||
navItem('backup', '/web/images/backuprestore.png', t('backup.backupRestore')),
|
||||
navItem('update', '/web/images/update.png', t('backup.update')),
|
||||
]),
|
||||
|
||||
el('div', { class: 'backup-main page-main' }, [
|
||||
buildBackupSection(),
|
||||
buildUpdateSection(),
|
||||
]),
|
||||
|
||||
buildOptionsModal(),
|
||||
el('div', { id: 'backup-loading', class: 'backup-loading-overlay', style: 'display:none' }, [
|
||||
el('div', { class: 'backup-spinner' }),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function navItem(key, icon, label) {
|
||||
return el('button', {
|
||||
type: 'button',
|
||||
class: 'backup-nav-item',
|
||||
'data-section': key,
|
||||
onclick: () => switchSection(key),
|
||||
}, [
|
||||
el('img', { src: icon, alt: '', class: 'backup-nav-icon' }),
|
||||
el('span', { class: 'backup-nav-label' }, [label]),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildBackupSection() {
|
||||
return el('section', { id: 'section-backup', class: 'backup-section' }, [
|
||||
el('h2', { class: 'backup-title' }, [t('backup.backupRestore')]),
|
||||
|
||||
el('form', { id: 'backup-form', class: 'backup-form' }, [
|
||||
el('label', { for: 'backup-desc-input', class: 'backup-label' }, [t('common.description')]),
|
||||
el('div', { class: 'backup-form-row' }, [
|
||||
el('input', {
|
||||
id: 'backup-desc-input',
|
||||
class: 'backup-input',
|
||||
type: 'text',
|
||||
placeholder: t('backup.descriptionPlaceholder'),
|
||||
required: 'required',
|
||||
}),
|
||||
el('button', { type: 'submit', class: 'btn btn-primary' }, [t('backup.createBackup')]),
|
||||
]),
|
||||
]),
|
||||
|
||||
el('h3', { class: 'backup-subtitle' }, [t('backup.lastBackup')]),
|
||||
el('div', { id: 'backup-table-wrap', class: 'backup-table-wrap' }, [
|
||||
el('div', { class: 'page-loading' }, [t('common.loading')]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildUpdateSection() {
|
||||
return el('section', { id: 'section-update', class: 'backup-section', style: 'display:none' }, [
|
||||
el('h2', { class: 'backup-title' }, [t('backup.update')]),
|
||||
el('div', { id: 'update-version-info', class: 'backup-update-message' }, [
|
||||
t('backup.checkUpdatesHint'),
|
||||
]),
|
||||
el('div', { class: 'backup-update-actions' }, [
|
||||
el('button', { class: 'btn', id: 'btn-check-update', onclick: onCheckUpdate }, [t('backup.checkUpdates')]),
|
||||
el('button', { class: 'btn btn-primary', id: 'btn-upgrade', onclick: onUpgrade }, [t('backup.installUpdate')]),
|
||||
el('button', { class: 'btn btn-danger', id: 'btn-fresh', onclick: onFreshStart }, [t('backup.freshStart')]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildOptionsModal() {
|
||||
return el('div', {
|
||||
id: 'backup-modal',
|
||||
class: 'backup-modal-overlay',
|
||||
'aria-hidden': 'true',
|
||||
style: 'display:none',
|
||||
onclick: (e) => {
|
||||
if (e.target.id === 'backup-modal') closeModal();
|
||||
},
|
||||
}, [
|
||||
el('div', { class: 'backup-modal' }, [
|
||||
el('div', { class: 'backup-modal-head' }, [
|
||||
el('h3', { id: 'modal-title', class: 'backup-modal-title' }, [t('common.options')]),
|
||||
el('button', { class: 'btn btn-sm', onclick: closeModal, type: 'button' }, ['X']),
|
||||
]),
|
||||
el('p', { class: 'backup-modal-help' }, [t('backup.selectKeepFolders')]),
|
||||
keepCheckbox('keep-data', t('backup.keepData')),
|
||||
keepCheckbox('keep-resources', t('backup.keepResources')),
|
||||
keepCheckbox('keep-actions', t('backup.keepActions')),
|
||||
keepCheckbox('keep-config', t('backup.keepConfig')),
|
||||
el('div', { class: 'backup-modal-actions' }, [
|
||||
el('button', { class: 'btn', type: 'button', onclick: closeModal }, [t('common.cancel')]),
|
||||
el('button', { class: 'btn btn-primary', type: 'button', onclick: onModalConfirm }, [t('common.confirm')]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
function keepCheckbox(id, label) {
|
||||
return el('label', { class: 'backup-keep' }, [
|
||||
el('input', { id, type: 'checkbox' }),
|
||||
el('span', {}, [label]),
|
||||
]);
|
||||
}
|
||||
|
||||
function wireEvents() {
|
||||
const form = $('#backup-form');
|
||||
if (form) {
|
||||
tracker?.trackEventListener(form, 'submit', onCreateBackup);
|
||||
}
|
||||
}
|
||||
|
||||
function switchSection(section) {
|
||||
currentSection = section;
|
||||
|
||||
const secBackup = $('#section-backup');
|
||||
const secUpdate = $('#section-update');
|
||||
if (secBackup) secBackup.style.display = section === 'backup' ? '' : 'none';
|
||||
if (secUpdate) secUpdate.style.display = section === 'update' ? '' : 'none';
|
||||
|
||||
document.querySelectorAll('.backup-nav-item').forEach((item) => {
|
||||
item.classList.toggle('active', item.getAttribute('data-section') === section);
|
||||
});
|
||||
|
||||
if (section === 'update') {
|
||||
onCheckUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function ensureOk(response, fallbackMessage) {
|
||||
if (!response || typeof response !== 'object') {
|
||||
throw new Error(fallbackMessage || t('common.error'));
|
||||
}
|
||||
if (response.status && response.status !== 'success') {
|
||||
throw new Error(response.message || fallbackMessage || t('common.error'));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function loadBackups() {
|
||||
const wrap = $('#backup-table-wrap');
|
||||
if (wrap) {
|
||||
empty(wrap);
|
||||
wrap.appendChild(el('div', { class: 'page-loading' }, [t('common.loading')]));
|
||||
}
|
||||
|
||||
try {
|
||||
const data = ensureOk(await api.post('/list_backups', {}), t('backup.failedLoadBackups'));
|
||||
backups = Array.isArray(data.backups) ? data.backups : [];
|
||||
renderBackupTable();
|
||||
} catch (err) {
|
||||
backups = [];
|
||||
renderBackupTable();
|
||||
toast(`${t('backup.failedLoadBackups')}: ${err.message}`, 3200, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderBackupTable() {
|
||||
const wrap = $('#backup-table-wrap');
|
||||
if (!wrap) return;
|
||||
empty(wrap);
|
||||
|
||||
if (!backups.length) {
|
||||
wrap.appendChild(el('div', { class: 'backup-empty' }, [t('backup.noBackupsCreateAbove')]));
|
||||
return;
|
||||
}
|
||||
|
||||
const table = el('table', { class: 'backup-table' }, [
|
||||
el('thead', {}, [
|
||||
el('tr', {}, [
|
||||
el('th', {}, [t('common.date')]),
|
||||
el('th', {}, [t('common.description')]),
|
||||
el('th', {}, [t('common.actions')]),
|
||||
]),
|
||||
]),
|
||||
el('tbody', {}, backups.map((b) => backupRow(b))),
|
||||
]);
|
||||
|
||||
wrap.appendChild(table);
|
||||
}
|
||||
|
||||
function backupRow(backup) {
|
||||
const actions = [
|
||||
el('button', { class: 'btn btn-sm', type: 'button', onclick: () => onRestoreBackup(backup.filename) }, [t('backup.restoreBackup')]),
|
||||
];
|
||||
|
||||
if (!backup.is_default) {
|
||||
actions.push(el('button', { class: 'btn btn-sm', type: 'button', onclick: () => onSetDefault(backup.filename) }, [t('backup.setDefault')]));
|
||||
}
|
||||
|
||||
actions.push(el('button', { class: 'btn btn-sm btn-danger', type: 'button', onclick: () => onDeleteBackup(backup.filename) }, [t('common.delete')]));
|
||||
|
||||
return el('tr', {}, [
|
||||
el('td', {}, [formatDate(backup.date)]),
|
||||
el('td', {}, [
|
||||
el('span', {}, [backup.description || backup.filename || t('backup.unnamedBackup')]),
|
||||
backup.is_default ? el('span', { class: 'pill backup-default-pill' }, [t('common.default')]) : null,
|
||||
backup.is_github ? el('span', { class: 'pill' }, [t('backup.github')]) : null,
|
||||
backup.is_restore ? el('span', { class: 'pill' }, [t('backup.restorePoint')]) : null,
|
||||
]),
|
||||
el('td', {}, [el('div', { class: 'backup-row-actions' }, actions)]),
|
||||
]);
|
||||
}
|
||||
|
||||
async function onCreateBackup(event) {
|
||||
event.preventDefault();
|
||||
const input = $('#backup-desc-input');
|
||||
const description = input ? input.value.trim() : '';
|
||||
|
||||
if (!description) {
|
||||
toast(t('backup.enterDescription'), 2200, 'warning');
|
||||
if (input) input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
const res = ensureOk(await api.post('/create_backup', { description }), t('backup.failedCreate'));
|
||||
toast(res.message || t('backup.createdSuccessfully'), 2600, 'success');
|
||||
if (input) input.value = '';
|
||||
await loadBackups();
|
||||
} catch (err) {
|
||||
toast(`${t('backup.failedCreate')}: ${err.message}`, 3200, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function onRestoreBackup(filename) {
|
||||
pendingModalAction = { type: 'restore', filename };
|
||||
openModal(t('backup.restoreOptions'));
|
||||
}
|
||||
|
||||
async function onSetDefault(filename) {
|
||||
showLoading();
|
||||
try {
|
||||
ensureOk(await api.post('/set_default_backup', { filename }), t('backup.failedSetDefault'));
|
||||
toast(t('backup.defaultUpdated'), 2200, 'success');
|
||||
await loadBackups();
|
||||
} catch (err) {
|
||||
toast(`${t('backup.failedSetDefault')}: ${err.message}`, 3200, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteBackup(filename) {
|
||||
if (!confirm(t('common.confirmQuestion'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
const res = ensureOk(await api.post('/delete_backup', { filename }), t('backup.failedDelete'));
|
||||
toast(res.message || t('backup.deleted'), 2200, 'success');
|
||||
await loadBackups();
|
||||
} catch (err) {
|
||||
toast(`${t('backup.failedDelete')}: ${err.message}`, 3200, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function onCheckUpdate() {
|
||||
const infoEl = $('#update-version-info');
|
||||
if (infoEl) infoEl.textContent = t('backup.checkingUpdates');
|
||||
|
||||
try {
|
||||
const data = await api.get('/check_update');
|
||||
if (!infoEl) return;
|
||||
|
||||
empty(infoEl);
|
||||
infoEl.appendChild(el('div', { class: 'backup-version-lines' }, [
|
||||
el('span', {}, [t('backup.currentVersion'), ': ', el('strong', {}, [String(data.current_version || t('common.unknown'))])]),
|
||||
el('span', {}, [t('backup.latestVersion'), ': ', el('strong', {}, [String(data.latest_version || t('common.unknown'))])]),
|
||||
data.update_available
|
||||
? el('span', { class: 'backup-update-available' }, [t('backup.updateAvailable')])
|
||||
: el('span', { class: 'backup-update-ok' }, [t('backup.upToDate')]),
|
||||
]));
|
||||
infoEl.classList.remove('fade-in');
|
||||
void infoEl.offsetWidth;
|
||||
infoEl.classList.add('fade-in');
|
||||
} catch (err) {
|
||||
if (infoEl) infoEl.textContent = `${t('backup.failedCheckUpdates')}: ${err.message}`;
|
||||
toast(`${t('backup.failedCheckUpdates')}: ${err.message}`, 3200, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function onUpgrade() {
|
||||
pendingModalAction = { type: 'update' };
|
||||
openModal(t('backup.updateOptions'));
|
||||
}
|
||||
|
||||
async function onFreshStart() {
|
||||
if (!confirm(t('backup.confirmFreshStart'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
try {
|
||||
const res = ensureOk(await api.post('/update_application', { mode: 'fresh_start', keeps: [] }), t('backup.freshStartFailed'));
|
||||
toast(res.message || t('backup.freshStartInitiated'), 3000, 'success');
|
||||
} catch (err) {
|
||||
toast(`${t('backup.freshStartFailed')}: ${err.message}`, 3200, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function openModal(title) {
|
||||
const modal = $('#backup-modal');
|
||||
const titleEl = $('#modal-title');
|
||||
if (titleEl) titleEl.textContent = title || t('common.options');
|
||||
|
||||
['keep-data', 'keep-resources', 'keep-actions', 'keep-config'].forEach((id) => {
|
||||
const cb = $(`#${id}`);
|
||||
if (cb) cb.checked = false;
|
||||
});
|
||||
|
||||
if (modal) modal.style.display = 'flex';
|
||||
if (modal) modal.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modal = $('#backup-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
if (modal) modal.setAttribute('aria-hidden', 'true');
|
||||
pendingModalAction = null;
|
||||
}
|
||||
|
||||
function selectedKeeps() {
|
||||
const map = {
|
||||
'keep-data': 'data',
|
||||
'keep-resources': 'resources',
|
||||
'keep-actions': 'actions',
|
||||
'keep-config': 'config',
|
||||
};
|
||||
const keeps = [];
|
||||
for (const [id, value] of Object.entries(map)) {
|
||||
const cb = $(`#${id}`);
|
||||
if (cb && cb.checked) keeps.push(value);
|
||||
}
|
||||
return keeps;
|
||||
}
|
||||
|
||||
async function onModalConfirm() {
|
||||
const action = pendingModalAction;
|
||||
if (!action) return;
|
||||
|
||||
const keeps = selectedKeeps();
|
||||
closeModal();
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
if (action.type === 'restore') {
|
||||
const mode = keeps.length ? 'selective_restore' : 'full_restore';
|
||||
const res = ensureOk(await api.post('/restore_backup', {
|
||||
filename: action.filename,
|
||||
mode,
|
||||
keeps,
|
||||
}), t('backup.restoreBackup'));
|
||||
toast(res.message || t('backup.restoreCompleted'), 3000, 'success');
|
||||
await loadBackups();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.type === 'update') {
|
||||
const res = ensureOk(await api.post('/update_application', {
|
||||
mode: 'upgrade',
|
||||
keeps,
|
||||
}), t('backup.update'));
|
||||
toast(res.message || t('backup.updateInitiated'), 3000, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
toast(`${t('common.failed')}: ${err.message}`, 3500, 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
const overlay = $('#backup-loading');
|
||||
if (overlay) overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
const overlay = $('#backup-loading');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return t('common.unknown');
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.replace(' ', 'T');
|
||||
const parsed = new Date(normalized);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return parsed.toLocaleString();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user