From cd39f222268c6818ef18b5e45dec5079d51a1cdf Mon Sep 17 00:00:00 2001 From: shuki Date: Sat, 7 Mar 2026 17:13:24 +0200 Subject: [PATCH] Hide logo below 100 columns width --- tui/screens/main_menu.py | 2 +- web/backend.py | 30 ++++++++++ web/blueprints/__init__.py | 0 web/blueprints/auth.py | 26 ++++++++ web/blueprints/dashboard.py | 77 ++++++++++++++++++++++++ web/templates/auth/login.html | 30 ++++++++++ web/templates/base.html | 52 ++++++++++++++++ web/templates/components/flash.html | 17 ++++++ web/templates/components/navbar.html | 88 ++++++++++++++++++++++++++++ 9 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 web/backend.py create mode 100644 web/blueprints/__init__.py create mode 100644 web/blueprints/auth.py create mode 100644 web/blueprints/dashboard.py create mode 100644 web/templates/auth/login.html create mode 100644 web/templates/base.html create mode 100644 web/templates/components/flash.html create mode 100644 web/templates/components/navbar.html 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 %} +
+ {% include "components/flash.html" %} +
+
+
+ G +

GNIZA

+
+
+
+ + +
+
+ +
+
+

GNIZA Backup Manager

+
+
+
+{% endblock %} diff --git a/web/templates/base.html b/web/templates/base.html new file mode 100644 index 0000000..1aac501 --- /dev/null +++ b/web/templates/base.html @@ -0,0 +1,52 @@ + + + + + + {% block title %}GNIZA{% endblock %} + + + + + + + +{% include "components/flash.html" %} + +{% block body %} +
+ + + +
+ + + + +
+ {% block content %}{% endblock %} +
+
+ + +
+ + {% include "components/navbar.html" %} +
+
+{% endblock %} + + + diff --git a/web/templates/components/flash.html b/web/templates/components/flash.html new file mode 100644 index 0000000..0a2caae --- /dev/null +++ b/web/templates/components/flash.html @@ -0,0 +1,17 @@ +{% with messages = get_flashed_messages(with_categories=true) %} +{% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ +{% endif %} +{% endwith %} diff --git a/web/templates/components/navbar.html b/web/templates/components/navbar.html new file mode 100644 index 0000000..6213ac9 --- /dev/null +++ b/web/templates/components/navbar.html @@ -0,0 +1,88 @@ +