mirror of
https://github.com/infinition/Bjorn.git
synced 2025-12-13 16:14:57 +00:00
377 lines
22 KiB
HTML
377 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>Bjorn Cyberviking - Update and Backup Management</title>
|
||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||
<link rel="stylesheet" href="web/css/global.css">
|
||
<link rel="manifest" href="manifest.json">
|
||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="theme-color" content="#333">
|
||
<script src="web/js/global.js"></script>
|
||
|
||
<style>
|
||
/* ======================================================================
|
||
Page-scoped CSS (kept minimal) — uses your global tokens
|
||
====================================================================== */
|
||
.main-container{display:flex;height:calc(100vh - 60px);width:100%;position:relative}
|
||
.section-list{list-style-type:none;padding:0;margin:0;flex-grow:1}
|
||
.list-item{display:flex;align-items:center;padding:12px;cursor:pointer;border-radius:var(--radius);margin-bottom:12px;transition:box-shadow .3s, background-color .3s, border-color .3s;background:var(--grad-card);border:1px solid var(--c-border);box-shadow:var(--shadow)}
|
||
.list-item:hover{box-shadow:var(--shadow-hover)}
|
||
.list-item.selected{border:1px solid #00e764}
|
||
.list-item img{margin-right:10px}
|
||
@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
|
||
.right-panel{flex:1;display:flex;flex-direction:column;padding:20px;overflow-y:auto;box-sizing:border-box;background-color:#1e1e1e}
|
||
.content-section{display:none}
|
||
.content-section.active{display:block}
|
||
form{margin-top:20px}
|
||
form label{display:block;margin-bottom:5px;color:white}
|
||
form input[type="text"]{width:100%;padding:8px;margin-bottom:10px;border:1px solid #555;border-radius:4px;background-color:#07422f40;color:#fff;cursor:text;pointer-events:auto}
|
||
form input[type="text"]:focus{outline:none;border-color:#007acc;background-color:#3d3d3d}
|
||
form input[type="text"]:hover{border-color:#666}
|
||
.default-badge{display:inline-block;padding:2px 8px;margin-left:8px;background-color:#007acc;color:white;border-radius:12px;font-size:.85em;font-weight:700}
|
||
/* ===== Bjorn-scoped Modal (avoid conflicts with global.js) ===== */
|
||
.bj-modal{display:none;position:fixed;z-index:1000;inset:0;overflow:auto;background-color:rgba(0,0,0,.5)}
|
||
.bj-modal__content{background-color:#2d2d2d;margin:10% auto;padding:20px;border:1px solid #888;width:80%;max-width:fit-content;border-radius:8px;z-index:1001;color:#fff}
|
||
.bj-modal__close{color:#aaa;float:right;font-size:28px;font-weight:700;cursor:pointer}
|
||
.bj-modal__close:hover,.bj-modal__close:focus{color:#fff;text-decoration:none}
|
||
/* ===== Bjorn-scoped Loading Overlay ===== */
|
||
.bj-loading-overlay{display:none;position:fixed;z-index:1100;inset:0;background-color:rgba(0,0,0,.7);justify-content:center;align-items:center}
|
||
.bj-rotating-arrow{width:50px;height:50px;border:5px solid transparent;border-top:5px solid #007acc;border-right:5px solid #007acc;border-radius:50%;animation:spin 1.5s linear infinite,bj-pulse 1.5s ease-in-out infinite}
|
||
@keyframes bj-pulse{0%{box-shadow:0 0 0 0 rgba(0,122,204,.7)}70%{box-shadow:0 0 0 20px rgba(0,122,204,0)}100%{box-shadow:0 0 0 0 rgba(0,122,204,0)}}
|
||
/* Update message bubble */
|
||
#bj-update-message{background-color:#28a745;color:#fff;padding:12px 20px;border-radius:25px;display:inline-block;margin-bottom:15px;box-shadow:0 4px 6px rgba(0,0,0,.1);font-size:16px;max-width:100%;word-wrap:break-word}
|
||
#bj-update-message.fade-in{animation:bjFadeIn .5s ease-in-out}
|
||
@keyframes bjFadeIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
|
||
/* Responsive */
|
||
@media (max-width:768px){.main-container{flex-direction:column}}
|
||
@media (min-width:769px){.menu-icon{display:none}.side-menu{transform:translateX(0);position:relative;height:98%;z-index:10000}}
|
||
.form-control{cursor:text;pointer-events:auto;background-color:#2d2d2d;color:#ffffff}
|
||
.backups-table button.loading{position:relative;pointer-events:none;opacity:.6;background-color:#2d2d2d;color:#fff;border:#007acc}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="sidehead">
|
||
<div class="spacer"></div>
|
||
<button class="btn" id="hideSidebar"><span class="icon">⟵</span><span class="label">Hide</span></button>
|
||
</div>
|
||
|
||
<li class="list-item" data-section="backup-section"><img src="/web/images/backuprestore.png" alt="Icon_backup" style="height:72px;"><span>Backup / Restore</span></li>
|
||
<li class="list-item" data-section="update-section"><img src="/web/images/update.png" alt="Icon_update" style="height:72px;"><span>Update</span></li>
|
||
|
||
<div class="sidecontent" id="sidecontent">
|
||
<div class="content-section" id="logs-section-content">
|
||
<h2>Clear Logs</h2>
|
||
<button class="btn danger" onclick="clear_logs()">Clear Logs</button>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div id="empty-list-hint" style="display:none;opacity:.8;margin-top:8px;font-size:.95em">No attacks found. Import a .py attack with “Add Attack”.</div>
|
||
</aside>
|
||
|
||
<div class="main" id="main">
|
||
<!-- Backup and Restore Section -->
|
||
<div class="content-section" id="backup-section-content">
|
||
<h2>Backup and Restore</h2>
|
||
<form id="backup-form">
|
||
<label for="backup-description">Backup Description:</label>
|
||
<input type="text" id="backup-description" class="form-control" name="description" required>
|
||
<button type="submit" class="btn">Create Backup</button>
|
||
</form>
|
||
|
||
<h3>Backup List</h3>
|
||
<table id="backups-table" class="backups-table">
|
||
<thead><tr><th>Date</th><th>Description</th><th>Actions</th></tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Update Application Section -->
|
||
<div class="content-section" id="update-section-content">
|
||
<div id="bj-update-message" style="margin-bottom:10px;"></div>
|
||
<h2>Update Application (From Github)</h2>
|
||
<button class="btn" onclick="bj_checkUpdate()">Check for Updates</button>
|
||
<button class="btn" onclick="bj_update_application('upgrade')">Upgrade (With options to keep your data)</button>
|
||
<button class="btn danger" onclick="bj_update_application('fresh_start')">Fresh Start (Replace everything)</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bjorn-scoped Loading Overlay -->
|
||
<div id="bj-loading-overlay" class="bj-loading-overlay"><div class="bj-rotating-arrow"></div></div>
|
||
|
||
<!-- Bjorn-scoped Restore/Update Modal -->
|
||
<div id="bj-restore-modal" class="bj-modal" aria-hidden="true" role="dialog">
|
||
<div class="bj-modal__content" role="document">
|
||
<span class="bj-modal__close" id="bj-modal-close" aria-label="Close">×</span>
|
||
<h2>Restore Options</h2>
|
||
<form id="bj-restore-form">
|
||
<p>Please select the folders to keep during restoration:</p>
|
||
<label><input type="checkbox" name="keep" value="data"> Keep the <strong>data</strong> folder (/home/bjorn/Bjorn/data)</label><br>
|
||
<label><input type="checkbox" name="keep" value="resources"> Keep the <strong>resources</strong> folder (/home/bjorn/Bjorn/resources)</label><br>
|
||
<label><input type="checkbox" name="keep" value="actions"> Keep the <strong>actions</strong> folder (/home/bjorn/Bjorn/actions)</label><br>
|
||
<label><input type="checkbox" name="keep" value="config"> Keep the <strong>config</strong> folder (/home/bjorn/Bjorn/config)</label><br><br>
|
||
<button type="submit" class="btn">Restore Backup</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ======================================================================
|
||
// Bjorn UI helpers — non-blocking toast + confirm over your global.js
|
||
// ======================================================================
|
||
const t = (msg, ms=2600) => (typeof window.toast === 'function' ? window.toast(msg, ms) : console.log(msg));
|
||
|
||
// Lightweight, non-blocking confirmation; auto-cleans DOM; returns Promise<boolean>
|
||
async function toastConfirm(message, { okText='Proceed', cancelText='Cancel', timeout=0 } = {}) {
|
||
return new Promise(resolve => {
|
||
const box = document.createElement('div');
|
||
Object.assign(box.style, { position:'fixed', right:'16px', bottom:'16px', zIndex:99999, maxWidth:'460px', background:'rgba(10,16,16,.96)', color:'#eafff6', border:'1px solid rgba(0,255,154,.35)', borderRadius:'12px', padding:'12px 14px', boxShadow:'0 10px 24px rgba(0,0,0,.35)', font:'14px/1.45 system-ui' });
|
||
box.innerHTML = `
|
||
<div style="margin-bottom:10px">${message}</div>
|
||
<div style="display:flex; gap:8px; justify-content:flex-end">
|
||
<button data-x style="background:#333;border:1px solid #555;color:#fff;padding:6px 10px;border-radius:8px;cursor:pointer">${cancelText}</button>
|
||
<button data-ok style="background:#00ff9a;border:1px solid #00ff9a33;color:#001b11;padding:6px 10px;border-radius:8px;cursor:pointer;font-weight:700">${okText}</button>
|
||
</div>`;
|
||
document.body.appendChild(box);
|
||
const done = v => { try { box.remove(); } catch {} resolve(v); };
|
||
box.querySelector('[data-ok]').addEventListener('click', () => done(true));
|
||
box.querySelector('[data-x]').addEventListener('click', () => done(false));
|
||
if (timeout > 0) setTimeout(() => done(false), timeout);
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
/* ===== Sections wiring ===== */
|
||
const sections = {
|
||
'logs-section': document.getElementById('logs-section-content'),
|
||
'backup-section': document.getElementById('backup-section-content'),
|
||
'update-section': document.getElementById('update-section-content'),
|
||
};
|
||
|
||
const defaultSection = 'backup-section';
|
||
const defaultSectionElement = document.querySelector(`[data-section="${defaultSection}"]`);
|
||
if (defaultSectionElement) {
|
||
defaultSectionElement.classList.add('selected');
|
||
if (sections[defaultSection]) {
|
||
sections[defaultSection].classList.add('active');
|
||
loadBackups();
|
||
}
|
||
}
|
||
|
||
/* ===== Scoped modal/overlay helpers ===== */
|
||
const bjModal = document.getElementById('bj-restore-modal');
|
||
const bjModalClose = document.getElementById('bj-modal-close');
|
||
|
||
function bj_showLoading(){const overlay=document.getElementById('bj-loading-overlay'); if(overlay) overlay.style.display='flex'}
|
||
function bj_hideLoading(){const overlay=document.getElementById('bj-loading-overlay'); if(overlay) overlay.style.display='none'}
|
||
|
||
function hideAllContents(){for (let key in sections) sections[key].classList.remove('active'); document.querySelectorAll('.list-item').forEach(item => item.classList.remove('selected'))}
|
||
|
||
function selectSection(event){
|
||
const clickedItem = event.currentTarget;
|
||
const section = clickedItem.getAttribute('data-section');
|
||
hideAllContents();
|
||
if (sections[section]) {
|
||
sections[section].classList.add('active');
|
||
clickedItem.classList.add('selected');
|
||
if (section === 'backup-section') { loadBackups(); }
|
||
else if (section === 'update-section') { bj_checkUpdate(); }
|
||
}
|
||
}
|
||
|
||
document.querySelectorAll('.list-item').forEach(item => item.addEventListener('click', selectSection));
|
||
|
||
/* ===== API: Update Check (shows status in green bubble + toast) ===== */
|
||
window.bj_checkUpdate = function(){
|
||
fetch('/check_update', { method:'GET', headers:{ 'Content-Type':'application/json' } })
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const messageDiv = document.getElementById('bj-update-message');
|
||
if (data.update_available) {
|
||
messageDiv.innerHTML = `New update available: <strong>${data.latest_version}</strong> (currently on: <strong>${data.current_version}</strong>)`;
|
||
t('⬆️ Update available');
|
||
} else {
|
||
messageDiv.innerHTML = `You are on the latest version: <strong>${data.current_version}</strong>`;
|
||
t('✅ Latest version already installed');
|
||
}
|
||
messageDiv.classList.remove('fade-in'); void messageDiv.offsetWidth; messageDiv.classList.add('fade-in');
|
||
})
|
||
.catch(err => {
|
||
console.error('Error checking updates:', err);
|
||
const messageDiv = document.getElementById('bj-update-message');
|
||
messageDiv.innerHTML = `Error checking updates.`;
|
||
messageDiv.classList.remove('fade-in'); void messageDiv.offsetWidth; messageDiv.classList.add('fade-in');
|
||
t('⛔ Error while checking updates — see console');
|
||
});
|
||
};
|
||
|
||
/* ===== Backups: list/load (toast feedback) ===== */
|
||
function loadBackups(){
|
||
fetch('/list_backups', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({}) })
|
||
.then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); })
|
||
.then(data => {
|
||
if (data.status === 'success') {
|
||
const tbody = document.querySelector('#backups-table tbody'); tbody.innerHTML = '';
|
||
data.backups.forEach(backup => {
|
||
const row = document.createElement('tr');
|
||
row.innerHTML = `
|
||
<td>${backup.date}</td>
|
||
<td>${backup.description}${backup.is_default ? ' <span class="default-badge">default</span>' : ''}</td>
|
||
<td style="display:flex;gap:5px;flex-wrap:wrap">
|
||
<button class="btn" onclick="bj_openRestoreModal('${backup.filename}')">Restore</button>
|
||
${!backup.is_default ? `
|
||
<button class="btn danger" onclick="bj_deleteBackup('${backup.filename}', event)">Delete</button>
|
||
<button class="btn" onclick="bj_setAsDefault('${backup.filename}', event)">Set as Default</button>
|
||
` : ''}
|
||
</td>`;
|
||
tbody.appendChild(row);
|
||
});
|
||
t('✅ Backups loaded');
|
||
} else {
|
||
t('⚠️ ' + (data.message || 'Error while loading backups'));
|
||
}
|
||
})
|
||
.catch(error => { console.error('Error loading backups:', error); t('⛔ Failed to load backups — see console'); });
|
||
}
|
||
|
||
/* ===== Create backup (toast feedback, no blocking alert) ===== */
|
||
const backupForm = document.getElementById('backup-form');
|
||
backupForm.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
const description = document.getElementById('backup-description').value;
|
||
const button = backupForm.querySelector('button');
|
||
bj_showLoading(); button.classList.add('loading');
|
||
|
||
fetch('/create_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({ description }) })
|
||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||
.then(data => {
|
||
button.classList.remove('loading'); bj_hideLoading();
|
||
if (data.status === 'success') { t('✅ ' + (data.message || 'Backup created')); loadBackups(); backupForm.reset(); }
|
||
else { t('⚠️ ' + (data.message || 'Backup failed')); }
|
||
})
|
||
.catch(error => { button.classList.remove('loading'); bj_hideLoading(); console.error('Error creating backup:', error); t('⛔ Error while creating the backup — see console'); });
|
||
});
|
||
|
||
/* ===== Set default backup ===== */
|
||
window.bj_setAsDefault = function(filename, ev){
|
||
const button = ev?.target; if (button) button.classList.add('loading');
|
||
fetch('/set_default_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({ filename }) })
|
||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||
.then(data => {
|
||
if (button) button.classList.remove('loading');
|
||
if (data.status === 'success') { t('✅ Default backup set'); loadBackups(); }
|
||
else { t('⚠️ ' + (data.message || 'Could not set default')); }
|
||
})
|
||
.catch(error => { if (button) button.classList.remove('loading'); console.error('Error setting default backup:', error); t('⛔ Failed to set default — see console'); });
|
||
};
|
||
|
||
/* ===== Open modal for restore ===== */
|
||
window.bj_openRestoreModal = function (filename){
|
||
window.bj_filenameToRestore = filename;
|
||
window.bj_updateMode = null;
|
||
document.querySelector('#bj-restore-modal .bj-modal__content h2').textContent = 'Restore Options';
|
||
document.querySelector('#bj-restore-modal .bj-modal__content button').textContent = 'Restore Backup';
|
||
bjModal.style.display = 'block';
|
||
bjModal.setAttribute('aria-hidden', 'false');
|
||
};
|
||
|
||
/* ===== Update (shared) ===== */
|
||
function proceedWithUpdate(mode, keeps, ev){
|
||
const button = ev?.target; if (button) button.classList.add('loading');
|
||
const bodyData = { mode, keeps };
|
||
fetch('/update_application', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify(bodyData) })
|
||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||
.then(data => {
|
||
if (button) button.classList.remove('loading');
|
||
if (data.status === 'success') { t('✅ ' + (data.message || 'Application updated')); bjModal.style.display='none'; bjModal.setAttribute('aria-hidden','true'); }
|
||
else { t('⚠️ ' + (data.message || 'Update failed')); }
|
||
})
|
||
.catch(error => { if (button) button.classList.remove('loading'); console.error('Error updating application:', error); t('⛔ Update failed — see console'); });
|
||
}
|
||
|
||
/* ===== Update application (toastConfirm instead of confirm) ===== */
|
||
window.bj_update_application = async function (mode){
|
||
const msg = mode === 'upgrade' ? 'Proceed with application upgrade?' : 'Fresh Start will delete all data. Continue?';
|
||
const ok = await toastConfirm(msg, { okText:'Proceed', cancelText:'Cancel' });
|
||
if (!ok) { t('ℹ️ Cancelled'); return; }
|
||
|
||
if (mode === 'upgrade') {
|
||
window.bj_updateMode = mode;
|
||
window.bj_filenameToRestore = null;
|
||
document.querySelector('#bj-restore-modal .bj-modal__content h2').textContent = 'Update Options';
|
||
document.querySelector('#bj-restore-modal .bj-modal__content button').textContent = 'Update Application';
|
||
document.getElementById('bj-restore-form').reset();
|
||
bjModal.style.display = 'block';
|
||
bjModal.setAttribute('aria-hidden', 'false');
|
||
} else {
|
||
bj_showLoading();
|
||
proceedWithUpdate(mode, []);
|
||
bj_hideLoading();
|
||
}
|
||
};
|
||
|
||
/* ===== Restore form submit (handles restore or upgrade) ===== */
|
||
document.getElementById('bj-restore-form').addEventListener('submit', function(e){
|
||
e.preventDefault();
|
||
const keeps = Array.from(this.querySelectorAll('input[name="keep"]:checked')).map(cb => cb.value);
|
||
|
||
if (window.bj_updateMode === 'upgrade') {
|
||
proceedWithUpdate('upgrade', keeps, e);
|
||
} else {
|
||
const filename = window.bj_filenameToRestore;
|
||
if (!filename) { t('⚠️ No backup file selected'); return; }
|
||
|
||
const mode = keeps.length > 0 ? 'selective_restore' : 'full_restore';
|
||
const bodyData = { filename, mode, keeps };
|
||
|
||
const button = this.querySelector('button');
|
||
button.classList.add('loading'); bj_showLoading();
|
||
|
||
fetch('/restore_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify(bodyData) })
|
||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||
.then(data => {
|
||
button.classList.remove('loading'); bj_hideLoading();
|
||
if (data.status === 'success') { t('✅ ' + (data.message || 'Backup restored')); loadBackups(); this.reset(); bjModal.style.display='none'; bjModal.setAttribute('aria-hidden','true'); }
|
||
else { t('⚠️ ' + (data.message || 'Restore failed')); }
|
||
})
|
||
.catch(error => { button.classList.remove('loading'); bj_hideLoading(); console.error('Error restoring backup:', error); t('⛔ Restore failed — see console'); });
|
||
}
|
||
});
|
||
|
||
/* ===== Delete backup (toastConfirm instead of confirm) ===== */
|
||
window.bj_deleteBackup = async function (filename, ev){
|
||
const ok = await toastConfirm('Delete this backup?', { okText:'Delete', cancelText:'Cancel' });
|
||
if (!ok) { t('ℹ️ Deletion cancelled'); return; }
|
||
|
||
const button = ev?.target; if (button) button.classList.add('loading');
|
||
bj_showLoading();
|
||
|
||
fetch('/delete_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({ filename }) })
|
||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||
.then(data => {
|
||
if (button) button.classList.remove('loading'); bj_hideLoading();
|
||
if (data.status === 'success') { t('✅ ' + (data.message || 'Backup deleted')); loadBackups(); }
|
||
else { t('⚠️ ' + (data.message || 'Delete failed')); }
|
||
})
|
||
.catch(error => { if (button) button.classList.remove('loading'); bj_hideLoading(); console.error('Error deleting backup:', error); t('⛔ Delete failed — see console'); });
|
||
};
|
||
|
||
/* ===== Modal close handlers (scoped) ===== */
|
||
bjModalClose.addEventListener('click', function(){
|
||
bjModal.style.display = 'none'; bjModal.setAttribute('aria-hidden', 'true'); window.bj_updateMode = null; window.bj_filenameToRestore = null;
|
||
});
|
||
window.addEventListener('click', function(event){
|
||
if (event.target === bjModal) {
|
||
bjModal.style.display = 'none'; bjModal.setAttribute('aria-hidden','true'); window.bj_updateMode = null; window.bj_filenameToRestore = null;
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|