Hide logo below 100 columns width
This commit is contained in:
@@ -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
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