635 lines
26 KiB
HTML
635 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>BrainSteel Backup</title>
|
||
<link rel="icon" href="data:,">
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
:root{
|
||
--bg:#0d1117;--surface:#161b22;--border:#30363d;--text:#c9d1d9;
|
||
--muted:#8b949e;--accent:#58a6ff;--success:#3fb950;--danger:#f85149;
|
||
--warning:#d29922;--font:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif
|
||
}
|
||
body{background:var(--bg);color:var(--text);font-family:var(--font);min-height:100vh;font-size:14px}
|
||
input,button,select{font-family:inherit}
|
||
|
||
/* LOGIN */
|
||
#login-wrap{display:flex;align-items:center;justify-content:center;height:100vh}
|
||
#login-box{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:40px;width:340px;text-align:center}
|
||
#login-box h1{font-size:20px;margin-bottom:8px;color:var(--text)}
|
||
#login-box p{color:var(--muted);font-size:12px;margin-bottom:24px}
|
||
#login-box input{width:100%;padding:12px 16px;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:14px;outline:none;transition:border .2s}
|
||
#login-box input:focus{border-color:var(--accent)}
|
||
#login-box button{width:100%;margin-top:12px;padding:12px;background:var(--accent);border:none;border-radius:8px;color:#fff;font-weight:600;cursor:pointer;font-size:14px;transition:opacity .2s}
|
||
#login-box button:hover{opacity:.85}
|
||
#login-error{color:var(--danger);font-size:12px;margin-top:10px;display:none}
|
||
|
||
/* MAIN LAYOUT */
|
||
#app{display:none;min-height:100vh;padding:16px;max-width:800px;margin:0 auto}
|
||
|
||
/* HEADER */
|
||
.header{display:flex;align-items:center;justify-content:space-between;padding:12px 0 20px;border-bottom:1px solid var(--border);margin-bottom:20px}
|
||
.header h1{font-size:18px;font-weight:700}
|
||
.header-right{display:flex;gap:8px;align-items:center}
|
||
.header-right button{padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text);cursor:pointer;font-size:12px;transition:background .2s}
|
||
.header-right button:hover{background:var(--surface)}
|
||
.header-right button.danger{color:var(--danger);border-color:var(--danger)}
|
||
.header-right button.danger:hover{background:rgba(248,81,73,.1)}
|
||
|
||
/* SECTION */
|
||
.section{margin-bottom:28px}
|
||
.section-title{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
|
||
.section-title h2{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
|
||
.section-title .badge{padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
|
||
.badge.green{background:rgba(63,185,80,.15);color:var(--success)}
|
||
.badge.yellow{background:rgba(210,153,34,.15);color:var(--warning)}
|
||
.badge.red{background:rgba(248,81,73,.15);color:var(--danger)}
|
||
|
||
/* STATUS CARDS */
|
||
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}
|
||
.card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:14px;cursor:pointer;transition:border .2s,transform .1s}
|
||
.card:hover{border-color:var(--accent);transform:translateY(-1px)}
|
||
.card-icon{font-size:20px;margin-bottom:8px}
|
||
.card-name{font-size:13px;font-weight:600;margin-bottom:4px}
|
||
.card-meta{font-size:11px;color:var(--muted);line-height:1.5}
|
||
.card-meta span{display:block}
|
||
.card .run-btn{display:none;margin-top:10px;padding:6px 12px;background:var(--accent);border:none;border-radius:6px;color:#fff;font-size:11px;cursor:pointer;width:100%}
|
||
.card:hover .run-btn{display:block}
|
||
|
||
/* BACKUP LIST */
|
||
.backup-item{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface);border:1px solid var(--border);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:border .2s}
|
||
.backup-item:hover{border-color:var(--accent)}
|
||
.backup-item .bi-left{display:flex;align-items:center;gap:10px;min-width:0}
|
||
.backup-item .bi-name{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}
|
||
.backup-item .bi-date{font-size:11px;color:var(--muted)}
|
||
.backup-item .bi-right{display:flex;align-items:center;gap:10px}
|
||
.backup-item .bi-size{font-size:11px;color:var(--muted);white-space:nowrap}
|
||
.backup-item .bi-size .icon{color:var(--success);font-size:12px}
|
||
|
||
.restore-btn{padding:5px 12px;background:var(--surface);border:1px solid var(--border);border-radius:6px;color:var(--accent);font-size:11px;cursor:pointer;white-space:nowrap;transition:all .2s}
|
||
.restore-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
|
||
.restore-btn:disabled{opacity:.4;cursor:not-allowed}
|
||
|
||
/* REPO GRID */
|
||
.repo-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}
|
||
.repo-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px;cursor:pointer;transition:border .2s}
|
||
.repo-card:hover{border-color:var(--accent)}
|
||
.repo-card .owner{font-size:11px;color:var(--muted);margin-bottom:4px}
|
||
.repo-card .name{font-size:14px;font-weight:600;margin-bottom:6px;word-break:break-all}
|
||
.repo-card .meta{font-size:11px;color:var(--muted)}
|
||
.repo-card .versions{font-size:10px;color:var(--muted);margin-top:4px}
|
||
|
||
/* TABS */
|
||
.tabs{display:flex;gap:4px;margin-bottom:16px;overflow-x:auto;padding-bottom:4px}
|
||
.tab{padding:8px 16px;background:transparent;border:1px solid transparent;border-radius:8px;color:var(--muted);font-size:13px;cursor:pointer;white-space:nowrap;transition:all .2s}
|
||
.tab.active{background:var(--surface);border-color:var(--border);color:var(--text)}
|
||
.tab:hover:not(.active){color:var(--text)}
|
||
|
||
/* MODAL */
|
||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:100;align-items:center;justify-content:center;padding:16px}
|
||
.modal-overlay.show{display:flex}
|
||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:12px;width:100%;max-width:480px;max-height:80vh;overflow-y:auto}
|
||
.modal-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||
.modal-header h3{font-size:15px}
|
||
.modal-close{background:none;border:none;color:var(--muted);cursor:pointer;font-size:18px;padding:4px;transition:color .2s}
|
||
.modal-close:hover{color:var(--text)}
|
||
.modal-body{padding:20px}
|
||
.modal-footer{padding:16px 20px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
|
||
|
||
/* FORM */
|
||
.form-group{margin-bottom:14px}
|
||
.form-group label{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}
|
||
.form-group input,.form-group select,.form-group textarea{width:100%;padding:10px 12px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;outline:none;transition:border .2s}
|
||
.form-group input:focus,.form-group select:focus{border-color:var(--accent)}
|
||
.form-group textarea{resize:vertical;min-height:80px;font-family:monospace;font-size:12px}
|
||
|
||
/* BUTTONS */
|
||
.btn{padding:8px 16px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:13px;cursor:pointer;transition:all .2s}
|
||
.btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
|
||
.btn-primary:hover{opacity:.85}
|
||
.btn-danger{background:var(--danger);border-color:var(--danger);color:#fff}
|
||
.btn-danger:hover{opacity:.85}
|
||
.btn:disabled{opacity:.4;cursor:not-allowed}
|
||
|
||
/* LOG VIEWER */
|
||
#log-viewer{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;font-family:monospace;font-size:11px;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;color:var(--muted);display:none}
|
||
#log-viewer.show{display:block}
|
||
|
||
/* TOAST */
|
||
#toast{position:fixed;bottom:20px;right:20px;padding:12px 20px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:13px;z-index:200;display:none;animation:slideUp .3s ease}
|
||
#toast.show{display:block}
|
||
#toast.ok{border-color:var(--success)}
|
||
#toast.error{border-color:var(--danger)}
|
||
@keyframes slideUp{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||
|
||
/* LOADING */
|
||
.loading{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
|
||
@keyframes spin{to{transform:rotate(360deg)}
|
||
|
||
/* PROGRESS */
|
||
.progress-bar{width:100%;height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-top:8px;display:none}
|
||
.progress-bar.show{display:block}
|
||
.progress-bar .fill{height:100%;background:var(--accent);transition:width .3s}
|
||
|
||
/* EMPTY STATE */
|
||
.empty{text-align:center;padding:40px 20px;color:var(--muted);font-size:13px}
|
||
.empty-icon{font-size:32px;margin-bottom:8px;opacity:.5}
|
||
|
||
/* BACK BUTTON */
|
||
.back-btn{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:12px;cursor:pointer;margin-bottom:16px;padding:4px 0;transition:color .2s}
|
||
.back-btn:hover{color:var(--text)}
|
||
|
||
/* DETAIL VIEW */
|
||
.detail-header{margin-bottom:20px}
|
||
.detail-title{font-size:18px;font-weight:700;margin-bottom:4px}
|
||
.detail-meta{font-size:12px;color:var(--muted)}
|
||
|
||
/* RESPONSIVE */
|
||
@media(max-width:600px){
|
||
#app{padding:12px}
|
||
.cards{grid-template-columns:1fr 1fr}
|
||
.card{padding:10px}
|
||
.tabs{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||
.backup-item .bi-name{max-width:120px}
|
||
.repo-grid{grid-template-columns:1fr}
|
||
.header h1{font-size:16px}
|
||
.modal{max-width:100%;border-radius:12px 12px 0 0;position:fixed;bottom:0;max-height:90vh}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- LOGIN -->
|
||
<div id="login-wrap">
|
||
<div id="login-box">
|
||
<h1>🛡️ BrainSteel Backup</h1>
|
||
<p>Digite a senha para acessar</p>
|
||
<input type="password" id="password" placeholder="Senha" onkeydown="if(event.key==='Enter')doLogin()">
|
||
<button onclick="doLogin()">Entrar</button>
|
||
<div id="login-error">Senha incorreta</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MAIN APP -->
|
||
<div id="app">
|
||
|
||
<!-- HEADER -->
|
||
<div class="header">
|
||
<h1>🛡️ BrainSteel Backup</h1>
|
||
<div class="header-right">
|
||
<button onclick="showLog()">📋 Log</button>
|
||
<button class="danger" onclick="logout()">Sair</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LOG TOGGLE -->
|
||
<div id="log-viewer"></div>
|
||
|
||
<!-- STATUS SECTION -->
|
||
<div class="section">
|
||
<div class="section-title">
|
||
<h2>Status dos Backups</h2>
|
||
<span id="last-run" class="badge green">—</span>
|
||
</div>
|
||
<div class="cards" id="cards"></div>
|
||
</div>
|
||
|
||
<!-- BACKUPS LIST -->
|
||
<div class="section">
|
||
<div class="section-title">
|
||
<h2>Backups</h2>
|
||
</div>
|
||
<div class="tabs" id="tabs">
|
||
<button class="tab active" onclick="switchTab('apps')">📦 Apps</button>
|
||
<button class="tab" onclick="switchTab('bd')">🗄️ Bancos</button>
|
||
<button class="tab" onclick="switchTab('git')">🐙 Git Full</button>
|
||
<button class="tab" onclick="switchTab('repos')">📁 Repos</button>
|
||
</div>
|
||
<div id="backup-list"></div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- RESTORE MODAL -->
|
||
<div class="modal-overlay" id="restore-modal">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>🔄 Restaurar</h3>
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" id="modal-body"></div>
|
||
<div class="modal-footer" id="modal-footer">
|
||
<button class="btn" onclick="closeModal()">Cancelar</button>
|
||
<button class="btn btn-primary" id="modal-confirm" onclick="doRestore()">Restaurar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TOAST -->
|
||
<div id="toast"></div>
|
||
|
||
<script>
|
||
const API = '/api';
|
||
let token = '';
|
||
let currentTab = 'apps';
|
||
let currentBackups = [];
|
||
let pendingRestore = null;
|
||
|
||
function showToast(msg, type='ok') {
|
||
const t = document.getElementById('toast');
|
||
t.textContent = msg;
|
||
t.className = `show ${type}`;
|
||
setTimeout(() => t.className = '', 3000);
|
||
}
|
||
|
||
async function doLogin() {
|
||
const pw = document.getElementById('password').value;
|
||
const res = await fetch(`${API}/login`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({password: pw})
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
token = pw;
|
||
document.getElementById('login-wrap').style.display = 'none';
|
||
document.getElementById('app').style.display = 'block';
|
||
loadAll();
|
||
} else {
|
||
document.getElementById('login-error').style.display = 'block';
|
||
}
|
||
}
|
||
|
||
async function loadAll() {
|
||
await Promise.all([loadStatus(), loadBackups()]);
|
||
}
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await fetch(`${API}/status`);
|
||
const d = await res.json();
|
||
|
||
// Last run badge
|
||
const lr = d.log.last_run;
|
||
const badge = document.getElementById('last-run');
|
||
let isAnyRunning = false;
|
||
let runningScripts = [];
|
||
|
||
if (lr) {
|
||
const isRunning = lr.scripts && lr.scripts.some(s => s.status === 'running');
|
||
if (isRunning) {
|
||
isAnyRunning = true;
|
||
runningScripts = lr.scripts ? lr.scripts.filter(s => s.status === 'running').map(s => s.name) : [];
|
||
badge.innerHTML = '<span class="loading" style="width:10px;height:10px;margin-right:4px;border-width:2px"></span> Em andamento';
|
||
badge.className = 'badge yellow';
|
||
} else {
|
||
const ok = !lr.has_error && lr.scripts && lr.scripts.every(s => s.status === 'ok');
|
||
badge.textContent = ok ? '✓ Concluído' : '⚠ Erro';
|
||
badge.className = `badge ${ok ? 'green' : 'red'}`;
|
||
}
|
||
} else {
|
||
badge.textContent = '—';
|
||
badge.className = 'badge yellow';
|
||
}
|
||
|
||
// Cards
|
||
const s = d.stats;
|
||
const isAppsRunning = runningScripts.includes('Backup Apps');
|
||
const isBdRunning = runningScripts.includes('Backup BD');
|
||
const isGitRunning = runningScripts.includes('Backup Git');
|
||
const isReposRunning = runningScripts.includes('Backup Git repos');
|
||
|
||
const cards = [
|
||
{id:'apps', icon:'📦', name:'Apps', count: s.apps.count, meta: s.apps.latest ? `Último: ${formatDateTime(s.apps.latest)}` : 'Nenhum backup', size: formatSize(s.apps.size), running: isAppsRunning},
|
||
{id:'bd', icon:'🗄️', name:'Bancos', count: s.bd.count, meta: s.bd.latest ? `Último: ${formatDateTime(s.bd.latest)}` : 'Nenhum backup', size: formatSize(s.bd.size), running: isBdRunning},
|
||
{id:'git', icon:'🐙', name:'Git Full', count: s.git.count, meta: s.git.latest ? `Último: ${formatDateTime(s.git.latest)}` : 'Nenhum backup', size: formatSize(s.git.size), running: isGitRunning},
|
||
{id:'repos', icon:'📁', name:'Repos', count: s.repos.count, meta: s.repos.latest ? `Último: ${formatDateTime(s.repos.latest)}` : 'Nenhum backup', size: formatSize(s.repos.size), running: isReposRunning},
|
||
];
|
||
|
||
document.getElementById('cards').innerHTML = cards.map(c => `
|
||
<div class="card" onclick="switchTab('${c.id}')" style="${c.running ? 'border-color:var(--accent)' : ''}">
|
||
<div class="card-icon">${c.icon}</div>
|
||
<div class="card-name">${c.name}</div>
|
||
<div class="card-meta">
|
||
<span style="font-weight:600;color:var(--text);font-size:12px;margin:4px 0">${c.count} arquivo${c.count!==1?'s':''} de backup</span>
|
||
<span>${c.meta}</span>
|
||
<span>Tamanho: ${c.size}</span>
|
||
</div>
|
||
${c.running
|
||
? `<div style="margin-top:12px;text-align:center;font-size:11px;color:var(--accent);font-weight:bold"><span class="loading"></span> Sincronizando...</div>`
|
||
: `<button class="run-btn" onclick="event.stopPropagation();runBackup('${c.id}')">▶ Executar Backup</button>`
|
||
}
|
||
</div>
|
||
`).join('');
|
||
|
||
// Poll faster if something is running
|
||
if (isAnyRunning) {
|
||
if (!window.pollIntervalFast) {
|
||
window.pollIntervalFast = setInterval(loadStatus, 2000);
|
||
}
|
||
} else {
|
||
if (window.pollIntervalFast) {
|
||
clearInterval(window.pollIntervalFast);
|
||
window.pollIntervalFast = null;
|
||
// Reload backups to show the newly finished one
|
||
loadBackups();
|
||
}
|
||
}
|
||
} catch(e) { console.error(e); }
|
||
}
|
||
|
||
async function loadBackups() {
|
||
const res = await fetch(`${API}/backups/${currentTab}`);
|
||
const d = await res.json();
|
||
currentBackups = d.items || [];
|
||
|
||
if (currentTab === 'repos') {
|
||
renderRepos(currentBackups);
|
||
} else {
|
||
renderBackupList(currentBackups);
|
||
}
|
||
}
|
||
|
||
function renderBackupList(items) {
|
||
const list = document.getElementById('backup-list');
|
||
if (!items.length) {
|
||
list.innerHTML = '<div class="empty"><div class="empty-icon">📭</div>Nenhum backup encontrado</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = items.map(item => `
|
||
<div class="backup-item">
|
||
<div class="bi-left">
|
||
<span class="bi-name">${item.name}</span>
|
||
<span class="bi-date">${formatDateTime(item.modified)}</span>
|
||
</div>
|
||
<div class="bi-right">
|
||
<span class="bi-size">${formatSize(item.size)}</span>
|
||
<button class="restore-btn" onclick="openRestoreModal('${currentTab}', '${item.name}', '${item.size}')">Restaurar</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function renderRepos(grouped) {
|
||
const list = document.getElementById('backup-list');
|
||
const owners = Object.keys(grouped);
|
||
if (!owners.length) {
|
||
list.innerHTML = '<div class="empty"><div class="empty-icon">📭</div>Nenhum repositório encontrado</div>';
|
||
return;
|
||
}
|
||
list.innerHTML = `<div class="back-btn" onclick="showAllRepos()"><span>←</span> Mostrar todos</div>` +
|
||
owners.map(owner => `
|
||
<div style="margin-bottom:16px">
|
||
<div style="font-size:12px;color:var(--muted);margin-bottom:8px;font-weight:600">👤 ${owner}</div>
|
||
<div class="repo-grid">
|
||
${grouped[owner].map(repo => `
|
||
<div class="repo-card" onclick="openRestoreRepo('${owner}', '${repo.name}', ${repo.size})">
|
||
<div class="owner">${owner}</div>
|
||
<div class="name">${repo.name.replace('.git.tar.gz','')}</div>
|
||
<div class="meta">${formatSize(repo.size)}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function showAllRepos() {
|
||
// Flatten and show all repos
|
||
const res = fetch(`${API}/backups/repos`).then(r => r.json()).then(d => {
|
||
const flat = [];
|
||
for (const [owner, repos] of Object.entries(d.items || {})) {
|
||
for (const repo of repos) {
|
||
flat.push({owner, ...repo});
|
||
}
|
||
}
|
||
document.getElementById('backup-list').innerHTML = flat.map(r => `
|
||
<div class="backup-item">
|
||
<div class="bi-left">
|
||
<span class="bi-name">${r.owner}/${r.name.replace('.git.tar.gz','')}</span>
|
||
</div>
|
||
<div class="bi-right">
|
||
<span class="bi-size">${formatSize(r.size)}</span>
|
||
<button class="restore-btn" onclick="openRestoreRepo('${r.owner}', '${r.name}')">Restaurar</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
});
|
||
}
|
||
|
||
function switchTab(tab) {
|
||
currentTab = tab;
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelector(`.tab[onclick="switchTab('${tab}')"]`).classList.add('active');
|
||
loadBackups();
|
||
}
|
||
|
||
async function runBackup(type) {
|
||
const btn = event.target;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="loading"></span> Rodando...';
|
||
try {
|
||
const res = await fetch(`${API}/run-backup`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({script: type})
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
showToast(`${type} backup iniciado ✓`);
|
||
loadStatus();
|
||
} else {
|
||
showToast(d.error, 'error');
|
||
}
|
||
} catch(e) { showToast('Erro ao iniciar backup', 'error'); }
|
||
btn.disabled = false;
|
||
btn.innerHTML = '▶ Executar Backup';
|
||
}
|
||
|
||
let restoreTarget = null;
|
||
|
||
function openRestoreModal(type, filename, size) {
|
||
restoreTarget = {type, filename};
|
||
const body = document.getElementById('modal-body');
|
||
body.innerHTML = `
|
||
<div class="form-group">
|
||
<label>Tipo</label>
|
||
<input type="text" value={getTypeLabel(type)} disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Arquivo</label>
|
||
<input type="text" value="${filename}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Tamanho</label>
|
||
<input type="text" value="${formatSize(size)}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Instruções pós-restore</label>
|
||
<textarea id="restore-instructions" disabled>Baixando e preparando...</textarea>
|
||
</div>
|
||
<div class="progress-bar" id="dl-progress"><div class="fill" style="width:0%"></div></div>
|
||
`;
|
||
document.getElementById('modal-confirm').style.display = 'inline-block';
|
||
document.getElementById('restore-modal').classList.add('show');
|
||
|
||
// Start download
|
||
fetch(`${API}/restore/${type}`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({filename})
|
||
}).then(r => r.json()).then(d => {
|
||
if (d.ok) {
|
||
// Now get instructions based on type
|
||
let instructions = '';
|
||
if (type === 'apps') {
|
||
instructions = `📦 Apps restaurado em: ${d.extracted_to}\n\n` +
|
||
`Para aplicar:\n` +
|
||
` cp -r ${d.extracted_to}/Camila/* /root/Apps/Camila/\n` +
|
||
` cp -r ${d.extracted_to}/RAG/* /root/Apps/RAG/\n` +
|
||
` cp -r ${d.extracted_to}/VOXDO/* /root/Apps/VOXDO/\n\n` +
|
||
`Depois reinicie os serviços.`;
|
||
} else if (type === 'bd') {
|
||
instructions = `🗄️ Backup BD restaurado em: ${d.extracted_to}\n\n` +
|
||
`SQL: ${d.sql_files?.join(', ') || 'não encontrado'}\n\n` +
|
||
`Para restaurar:\n` +
|
||
` cat ${d.sql_files?.[0] || '<sql-file>'} | docker exec -i <container> psql -U postgres -d <db>`;
|
||
} else if (type === 'git') {
|
||
instructions = `🐙 Backup Git restaurado em: ${d.extracted_to}\n\n` +
|
||
`Arquivos: ${(d.files || []).slice(0,5).join(', ')}...\n\n` +
|
||
`Para aplicar:\n` +
|
||
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/`;
|
||
}
|
||
document.getElementById('restore-instructions').value = instructions || 'Pronto para uso.';
|
||
} else {
|
||
document.getElementById('restore-instructions').value = `Erro: ${d.error}`;
|
||
}
|
||
});
|
||
}
|
||
|
||
function openRestoreRepo(owner, repoName, size) {
|
||
restoreTarget = {type: 'repo', owner, repoName};
|
||
const body = document.getElementById('modal-body');
|
||
body.innerHTML = `
|
||
<div class="form-group">
|
||
<label>Owner</label>
|
||
<input type="text" value="${owner}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Repositório</label>
|
||
<input type="text" value="${repoName.replace('.git.tar.gz','')}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Tamanho</label>
|
||
<input type="text" value="${formatSize(size)}" disabled>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Instruções</label>
|
||
<textarea id="restore-instructions" disabled>Baixando e preparando...</textarea>
|
||
</div>
|
||
`;
|
||
document.getElementById('restore-modal').classList.add('show');
|
||
|
||
fetch(`${API}/restore/repo`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type':'application/json'},
|
||
body: JSON.stringify({owner, repo: repoName.replace('.git.tar.gz',''), filename: repoName})
|
||
}).then(r => r.json()).then(d => {
|
||
if (d.ok) {
|
||
const repo = repoName.replace('.git.tar.gz','');
|
||
document.getElementById('restore-instructions').value =
|
||
`📁 Repo restaurado em: ${d.extracted_to}\n\n` +
|
||
`Git dir: ${d.git_dir || 'não encontrado'}\n\n` +
|
||
`Para restaurar no Gitea:\n` +
|
||
` # Pare o Gitea\n` +
|
||
` docker stop <gitea-container>\n` +
|
||
` # Copie o repo (ajuste o path)\n` +
|
||
` cp -r ${d.extracted_to}/* /var/lib/docker/volumes/yccsckck4g004gosccwc4kg4_gitea-data/_data/git/repositories/${owner}/\n` +
|
||
` # Reinicie o Gitea\n` +
|
||
` docker start <gitea-container>`;
|
||
} else {
|
||
document.getElementById('restore-instructions').value = `Erro: ${d.error}`;
|
||
}
|
||
});
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('restore-modal').classList.remove('show');
|
||
restoreTarget = null;
|
||
}
|
||
|
||
async function doRestore() {
|
||
// For now, just close - instructions are shown
|
||
showToast('Arquivo baixado! Veja as instruções no textarea acima.');
|
||
closeModal();
|
||
}
|
||
|
||
async function showLog() {
|
||
const lv = document.getElementById('log-viewer');
|
||
if (lv.classList.contains('show')) {
|
||
lv.classList.remove('show');
|
||
return;
|
||
}
|
||
const res = await fetch(`${API}/log`);
|
||
const d = await res.json();
|
||
lv.textContent = d.lines.join('');
|
||
lv.classList.add('show');
|
||
lv.scrollTop = lv.scrollHeight;
|
||
}
|
||
|
||
function logout() {
|
||
document.getElementById('app').style.display = 'none';
|
||
document.getElementById('login-wrap').style.display = 'flex';
|
||
document.getElementById('password').value = '';
|
||
token = '';
|
||
}
|
||
|
||
// HELPERS
|
||
function formatSize(bytes) {
|
||
if (!bytes) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B','KB','MB','GB','TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function formatDateTime(dateStr) {
|
||
if (!dateStr) return '—';
|
||
try {
|
||
// try to match YYYYMMDD_HHMMSS if it's from filename
|
||
const m = dateStr.match(/(\d{4})(\d{2})(\d{2})_?(\d{2})?(\d{2})?(\d{2})?/);
|
||
let d;
|
||
if (m && m[1] && m[2] && m[3] && dateStr.indexOf('-') === -1) {
|
||
d = new Date(m[1], m[2]-1, m[3], m[4]||0, m[5]||0, m[6]||0);
|
||
} else {
|
||
d = new Date(dateStr);
|
||
}
|
||
|
||
if (isNaN(d.getTime())) return dateStr;
|
||
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const year = d.getFullYear();
|
||
const hr = String(d.getHours()).padStart(2, '0');
|
||
const min = String(d.getMinutes()).padStart(2, '0');
|
||
|
||
return `${day}/${month}/${year} às ${hr}:${min}`;
|
||
} catch { return dateStr; }
|
||
}
|
||
|
||
function getTypeLabel(type) {
|
||
return {apps:'📦 Apps', bd:'🗄️ Banco de Dados', git:'🐙 Git Full', repos:'📁 Repositórios'}[type] || type;
|
||
}
|
||
|
||
// Auto-refresh every 30s
|
||
setInterval(() => {
|
||
if (document.getElementById('app').style.display !== 'none') {
|
||
loadStatus();
|
||
}
|
||
}, 30000);
|
||
</script>
|
||
</body>
|
||
</html>
|