Hide logo below 100 columns width

This commit is contained in:
shuki
2026-03-07 17:13:24 +02:00
parent 51f5abe21f
commit cd39f22226
9 changed files with 321 additions and 1 deletions

View File

@@ -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")

30
web/backend.py Normal file
View 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

View File

26
web/blueprints/auth.py Normal file
View 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"))

View 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,
)

View 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
View 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>

View 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 %}

View 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>