diff --git a/tui/screens/main_menu.py b/tui/screens/main_menu.py index d8733eb..30bd493 100644 --- a/tui/screens/main_menu.py +++ b/tui/screens/main_menu.py @@ -80,7 +80,7 @@ class MainMenuScreen(Screen): width = self.app.size.width logo = self.query_one("#logo") layout = self.query_one("#main-layout") - logo.display = width >= 48 + logo.display = width >= 100 if width < 100: layout.styles.layout = "vertical" layout.styles.align = ("center", "top") diff --git a/web/backend.py b/web/backend.py new file mode 100644 index 0000000..774f4fb --- /dev/null +++ b/web/backend.py @@ -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 diff --git a/web/blueprints/__init__.py b/web/blueprints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/blueprints/auth.py b/web/blueprints/auth.py new file mode 100644 index 0000000..6d25178 --- /dev/null +++ b/web/blueprints/auth.py @@ -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")) diff --git a/web/blueprints/dashboard.py b/web/blueprints/dashboard.py new file mode 100644 index 0000000..7549b09 --- /dev/null +++ b/web/blueprints/dashboard.py @@ -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, + ) diff --git a/web/templates/auth/login.html b/web/templates/auth/login.html new file mode 100644 index 0000000..218cdf9 --- /dev/null +++ b/web/templates/auth/login.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block title %}GNIZA - Login{% endblock %} + +{% block body %} +
GNIZA Backup Manager
+