Files
gniza4linux/web/templates/dashboard.html
shuki cf00ecdd4b Add web dashboard with systemd service support
- Flask web dashboard with dark theme matching TUI
- Login with API key authentication
- Dashboard shows targets, remotes, schedules, last backup status
- Trigger backups from web UI per target
- View logs via /api/logs endpoint
- systemd service: gniza web install-service / remove-service / status
- CLI: gniza web start [--port=PORT] [--host=HOST]
- TUI settings: web enabled, port, host, API key fields
- Install script: optional web dashboard setup with auto-generated API key
- Uninstall script: removes systemd service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 05:34:02 +02:00

240 lines
6.2 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GNIZA Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
color: #e0e0e0;
font-family: 'Courier New', monospace;
padding: 1rem;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #00cc00;
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
}
header h1 {
color: #00cc00;
font-size: 1.4rem;
}
header a {
color: #aaa;
text-decoration: none;
font-size: 0.9rem;
}
header a:hover { color: #00cc00; }
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
@media (max-width: 800px) {
.grid { grid-template-columns: 1fr; }
}
.card {
background: #16213e;
border: 1px solid #333;
border-radius: 6px;
padding: 1rem;
}
.card h2 {
color: #00cc00;
font-size: 1rem;
margin-bottom: 0.75rem;
border-bottom: 1px solid #333;
padding-bottom: 0.4rem;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
th {
text-align: left;
color: #00cc00;
padding: 0.3rem 0.5rem;
border-bottom: 1px solid #333;
}
td {
padding: 0.3rem 0.5rem;
border-bottom: 1px solid #222;
}
.badge {
display: inline-block;
padding: 0.1rem 0.4rem;
border-radius: 3px;
font-size: 0.75rem;
font-weight: bold;
}
.badge-yes { background: #00cc00; color: #1a1a2e; }
.badge-no { background: #555; color: #ccc; }
.badge-success { background: #00cc00; color: #1a1a2e; }
.badge-error { background: #cc3333; color: #fff; }
.badge-unknown { background: #666; color: #ccc; }
.btn-backup {
background: #0f3460;
color: #00cc00;
border: 1px solid #00cc00;
padding: 0.2rem 0.6rem;
border-radius: 3px;
cursor: pointer;
font-family: inherit;
font-size: 0.75rem;
}
.btn-backup:hover { background: #00cc00; color: #1a1a2e; }
.btn-backup:disabled { opacity: 0.5; cursor: not-allowed; }
.log-tail {
background: #0a0a1a;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
color: #aaa;
}
.empty { color: #666; font-style: italic; }
.full-width { grid-column: 1 / -1; }
.status-msg {
margin-top: 0.3rem;
font-size: 0.8rem;
color: #00cc00;
}
</style>
</head>
<body>
<header>
<h1>GNIZA Backup Dashboard</h1>
<a href="/logout">Logout</a>
</header>
<div class="grid">
<div class="card">
<h2>Targets</h2>
{% if targets %}
<table>
<tr><th>Name</th><th>Remote</th><th>Status</th><th></th></tr>
{% for t in targets %}
<tr>
<td>{{ t.name }}</td>
<td>{{ t.remote or '-' }}</td>
<td>
{% if t.enabled == 'yes' %}
<span class="badge badge-yes">enabled</span>
{% else %}
<span class="badge badge-no">disabled</span>
{% endif %}
</td>
<td>
{% if t.enabled == 'yes' %}
<button class="btn-backup" onclick="triggerBackup(this, '{{ t.name }}')">Backup</button>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">No targets configured.</p>
{% endif %}
</div>
<div class="card">
<h2>Remotes</h2>
{% if remotes %}
<table>
<tr><th>Name</th><th>Type</th><th>Host</th></tr>
{% for r in remotes %}
<tr>
<td>{{ r.name }}</td>
<td>{{ r.type }}</td>
<td>{{ r.host or r.base }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">No remotes configured.</p>
{% endif %}
</div>
<div class="card">
<h2>Schedules</h2>
{% if schedules %}
<table>
<tr><th>Name</th><th>Schedule</th><th>Time</th><th>Status</th></tr>
{% for s in schedules %}
<tr>
<td>{{ s.name }}</td>
<td>{{ s.schedule }}</td>
<td>{{ s.time or '-' }}</td>
<td>
{% if s.active == 'yes' %}
<span class="badge badge-yes">active</span>
{% else %}
<span class="badge badge-no">inactive</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">No schedules configured.</p>
{% endif %}
</div>
<div class="card">
<h2>Last Backup</h2>
{% if last_log %}
<p>
<strong>{{ last_log.name }}</strong>
<span class="badge badge-{{ last_log.status }}">{{ last_log.status }}</span>
</p>
<div class="log-tail">{{ last_log.tail }}</div>
{% else %}
<p class="empty">No log files found.</p>
{% endif %}
</div>
</div>
<script>
function triggerBackup(btn, target) {
btn.disabled = true;
btn.textContent = '...';
var fd = new FormData();
fd.append('target', target);
fetch('/api/backup', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.error) {
btn.textContent = 'Error';
btn.style.borderColor = '#cc3333';
btn.style.color = '#cc3333';
} else {
btn.textContent = 'Started';
}
setTimeout(function() {
btn.disabled = false;
btn.textContent = 'Backup';
btn.style.borderColor = '';
btn.style.color = '';
}, 3000);
})
.catch(function() {
btn.textContent = 'Error';
setTimeout(function() {
btn.disabled = false;
btn.textContent = 'Backup';
}, 3000);
});
}
</script>
</body>
</html>