Hide logo below 100 columns width
This commit is contained in:
@@ -80,7 +80,7 @@ class MainMenuScreen(Screen):
|
|||||||
width = self.app.size.width
|
width = self.app.size.width
|
||||||
logo = self.query_one("#logo")
|
logo = self.query_one("#logo")
|
||||||
layout = self.query_one("#main-layout")
|
layout = self.query_one("#main-layout")
|
||||||
logo.display = width >= 48
|
logo.display = width >= 100
|
||||||
if width < 100:
|
if width < 100:
|
||||||
layout.styles.layout = "vertical"
|
layout.styles.layout = "vertical"
|
||||||
layout.styles.align = ("center", "top")
|
layout.styles.align = ("center", "top")
|
||||||
|
|||||||
30
web/backend.py
Normal file
30
web/backend.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def _gniza_bin():
|
||||||
|
env = os.environ.get("GNIZA_DIR")
|
||||||
|
if env:
|
||||||
|
p = Path(env) / "bin" / "gniza"
|
||||||
|
if p.exists():
|
||||||
|
return str(p)
|
||||||
|
rel = Path(__file__).resolve().parent.parent / "bin" / "gniza"
|
||||||
|
if rel.exists():
|
||||||
|
return str(rel)
|
||||||
|
return "gniza"
|
||||||
|
|
||||||
|
|
||||||
|
def run_cli_sync(*args, timeout=300):
|
||||||
|
cmd = [_gniza_bin(), "--cli"] + list(args)
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||||
|
return result.returncode, result.stdout, result.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def start_cli_background(*args, log_file):
|
||||||
|
cmd = [_gniza_bin(), "--cli"] + list(args)
|
||||||
|
fh = open(log_file, "w")
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd, stdout=fh, stderr=subprocess.STDOUT, start_new_session=True
|
||||||
|
)
|
||||||
|
return proc
|
||||||
0
web/blueprints/__init__.py
Normal file
0
web/blueprints/__init__.py
Normal file
26
web/blueprints/auth.py
Normal file
26
web/blueprints/auth.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import secrets
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint, render_template, request, redirect, url_for,
|
||||||
|
session, flash, current_app,
|
||||||
|
)
|
||||||
|
|
||||||
|
bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/login", methods=["GET", "POST"])
|
||||||
|
def login():
|
||||||
|
if request.method == "POST":
|
||||||
|
token = request.form.get("token", "")
|
||||||
|
stored_key = current_app.config["API_KEY"]
|
||||||
|
if token and secrets.compare_digest(token, stored_key):
|
||||||
|
session["logged_in"] = True
|
||||||
|
return redirect(url_for("dashboard.index"))
|
||||||
|
flash("Invalid API key.", "error")
|
||||||
|
return render_template("auth/login.html")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/logout")
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
return redirect(url_for("auth.login"))
|
||||||
77
web/blueprints/dashboard.py
Normal file
77
web/blueprints/dashboard.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
from tui.config import CONFIG_DIR, LOG_DIR, parse_conf, list_conf_dir
|
||||||
|
from tui.models import Target, Remote, Schedule
|
||||||
|
from web.app import login_required
|
||||||
|
|
||||||
|
bp = Blueprint("dashboard", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_targets():
|
||||||
|
targets = []
|
||||||
|
for name in list_conf_dir("targets.d"):
|
||||||
|
data = parse_conf(CONFIG_DIR / "targets.d" / f"{name}.conf")
|
||||||
|
targets.append(Target.from_conf(name, data))
|
||||||
|
return targets
|
||||||
|
|
||||||
|
|
||||||
|
def _load_remotes():
|
||||||
|
remotes = []
|
||||||
|
for name in list_conf_dir("remotes.d"):
|
||||||
|
data = parse_conf(CONFIG_DIR / "remotes.d" / f"{name}.conf")
|
||||||
|
remotes.append(Remote.from_conf(name, data))
|
||||||
|
return remotes
|
||||||
|
|
||||||
|
|
||||||
|
def _load_schedules():
|
||||||
|
schedules = []
|
||||||
|
for name in list_conf_dir("schedules.d"):
|
||||||
|
data = parse_conf(CONFIG_DIR / "schedules.d" / f"{name}.conf")
|
||||||
|
schedules.append(Schedule.from_conf(name, data))
|
||||||
|
return schedules
|
||||||
|
|
||||||
|
|
||||||
|
def _last_log_info():
|
||||||
|
if not LOG_DIR.is_dir():
|
||||||
|
return None
|
||||||
|
logs = sorted(
|
||||||
|
LOG_DIR.glob("gniza-*.log"),
|
||||||
|
key=lambda x: x.stat().st_mtime,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
if not logs:
|
||||||
|
return None
|
||||||
|
latest = logs[0]
|
||||||
|
lines = latest.read_text(errors="replace").splitlines()
|
||||||
|
last_lines = lines[-50:] if len(lines) > 50 else lines
|
||||||
|
status = "unknown"
|
||||||
|
for line in reversed(lines):
|
||||||
|
lower = line.lower()
|
||||||
|
if "completed successfully" in lower or "backup done" in lower:
|
||||||
|
status = "success"
|
||||||
|
break
|
||||||
|
if "error" in lower or "failed" in lower:
|
||||||
|
status = "error"
|
||||||
|
break
|
||||||
|
return {
|
||||||
|
"name": latest.name,
|
||||||
|
"mtime": latest.stat().st_mtime,
|
||||||
|
"status": status,
|
||||||
|
"tail": "\n".join(last_lines),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/")
|
||||||
|
@login_required
|
||||||
|
def index():
|
||||||
|
targets = _load_targets()
|
||||||
|
remotes = _load_remotes()
|
||||||
|
schedules = _load_schedules()
|
||||||
|
last_log = _last_log_info()
|
||||||
|
return render_template(
|
||||||
|
"dashboard.html",
|
||||||
|
targets=targets,
|
||||||
|
remotes=remotes,
|
||||||
|
schedules=schedules,
|
||||||
|
last_log=last_log,
|
||||||
|
)
|
||||||
30
web/templates/auth/login.html
Normal file
30
web/templates/auth/login.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}GNIZA - Login{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="min-h-screen flex items-center justify-center bg-base-100">
|
||||||
|
{% include "components/flash.html" %}
|
||||||
|
<div class="card w-full max-w-sm bg-base-200 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-center justify-center gap-3 mb-4">
|
||||||
|
<span class="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-primary text-primary-content font-bold text-lg">G</span>
|
||||||
|
<h2 class="card-title text-2xl font-bold">GNIZA</h2>
|
||||||
|
</div>
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-control">
|
||||||
|
<label class="label" for="token">
|
||||||
|
<span class="label-text">API Key</span>
|
||||||
|
</label>
|
||||||
|
<input type="password" name="token" id="token" placeholder="Enter your API key"
|
||||||
|
class="input input-bordered w-full" autofocus />
|
||||||
|
</div>
|
||||||
|
<div class="form-control mt-6">
|
||||||
|
<button type="submit" class="btn btn-primary w-full">Sign In</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="text-center text-sm text-base-content/50 mt-4">GNIZA Backup Manager</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
52
web/templates/base.html
Normal file
52
web/templates/base.html
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}GNIZA{% endblock %}</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.14/dist/full.min.css" rel="stylesheet" type="text/css" />
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-base-100">
|
||||||
|
|
||||||
|
{% include "components/flash.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="drawer lg:drawer-open">
|
||||||
|
<input id="sidebar-toggle" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
|
<!-- Main content area -->
|
||||||
|
<div class="drawer-content flex flex-col">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="navbar bg-base-200 border-b border-base-300 sticky top-0 z-30">
|
||||||
|
<div class="flex-none lg:hidden">
|
||||||
|
<label for="sidebar-toggle" class="btn btn-square btn-ghost">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-6 h-6 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-lg font-semibold px-2">{% block page_title %}Dashboard{% endblock %}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none">
|
||||||
|
<a href="{{ url_for('auth.logout') }}" class="btn btn-ghost btn-sm">Logout</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page content -->
|
||||||
|
<main class="p-4 md:p-6 max-w-7xl w-full mx-auto">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="drawer-side z-40">
|
||||||
|
<label for="sidebar-toggle" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
|
{% include "components/navbar.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
17
web/templates/components/flash.html
Normal file
17
web/templates/components/flash.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="toast toast-top toast-end z-50">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="alert {{ 'alert-error' if category == 'error' else 'alert-success' if category == 'success' else 'alert-info' }} shadow-lg">
|
||||||
|
<span>{{ message }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
setTimeout(function() {
|
||||||
|
var toast = document.querySelector('.toast');
|
||||||
|
if (toast) toast.style.display = 'none';
|
||||||
|
}, 5000);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
88
web/templates/components/navbar.html
Normal file
88
web/templates/components/navbar.html
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<ul class="menu p-4 w-64 min-h-full bg-base-200 text-base-content">
|
||||||
|
<li class="menu-title">
|
||||||
|
<div class="flex items-center gap-2 text-lg font-bold text-primary">
|
||||||
|
<span class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-primary text-primary-content font-bold text-sm">G</span>
|
||||||
|
GNIZA
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('dashboard.index') }}" class="{{ 'active' if active_page == 'dashboard' else '' }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title mt-2">Configuration</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
|
||||||
|
Sources
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
|
||||||
|
Destinations
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||||
|
Schedules
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title mt-2">Operations</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
|
||||||
|
Backup
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||||
|
Restore
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||||||
|
Running Tasks
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title mt-2">Data</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8"/></svg>
|
||||||
|
Snapshots
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
Retention
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="menu-title mt-2">System</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
||||||
|
Logs
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="disabled opacity-50 pointer-events-none">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||||
|
Settings
|
||||||
|
<span class="badge badge-xs badge-ghost">Soon</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
Reference in New Issue
Block a user