update admin layout

This commit is contained in:
SowinskiBraeden committed 2026-03-21 01:30:42 -07:00
1 parent 613ffb394c
commit 6c508f4ef3
3 files changed
+169 -8

No files matched your search

+95 -2
View File
@@ -1,10 +1,22 @@
#!/usr/bin/env python3
from __future__ import annotations
import csv
import os
import shutil
from datetime import datetime, timezone
from pathlib import Path
from flask import Flask, flash, redirect, render_template, request, session, url_for
from flask import (
Flask,
flash,
redirect,
render_template,
request,
send_file,
session,
url_for,
)
from stats import (
apply_rank_changes,
@@ -236,6 +248,87 @@ def admin_session_state() -> str:
return redirect(url_for("admin_dashboard"))
@app.get("/admin/export")
def admin_export_csv():
if not is_admin():
flash("Admin login required.", "error")
return redirect(url_for("admin_login"))
export_name = (
f"entries_export_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.csv"
)
return send_file(
DATA_PATH,
as_attachment=True,
download_name=export_name,
mimetype="text/csv",
)
@app.post("/admin/import")
def admin_import_csv():
if not is_admin():
flash("Admin login required.", "error")
return redirect(url_for("admin_login"))
uploaded_file = request.files.get("csv_file")
if uploaded_file is None or uploaded_file.filename == "":
flash("Choose a CSV file to import.", "error")
return redirect(url_for("admin_dashboard"))
if not uploaded_file.filename.lower().endswith(".csv"):
flash("Only CSV files are allowed.", "error")
return redirect(url_for("admin_dashboard"))
try:
uploaded_text = uploaded_file.stream.read().decode("utf-8-sig")
except UnicodeDecodeError:
flash("CSV file must be valid UTF-8 text.", "error")
return redirect(url_for("admin_dashboard"))
uploaded_lines = uploaded_text.splitlines()
if not uploaded_lines:
flash("CSV file is empty.", "error")
return redirect(url_for("admin_dashboard"))
try:
current_header = next(
csv.reader(open(DATA_PATH, "r", encoding="utf-8", newline=""))
)
except Exception:
flash("Could not read the current audit CSV header.", "error")
return redirect(url_for("admin_dashboard"))
try:
uploaded_header = next(csv.reader(uploaded_lines))
except Exception:
flash("Could not read the uploaded CSV header.", "error")
return redirect(url_for("admin_dashboard"))
if uploaded_header != current_header:
flash("CSV header does not match the current audit format.", "error")
return redirect(url_for("admin_dashboard"))
data_dir = os.path.dirname(DATA_PATH)
backup_name = (
f"entries_backup_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.csv"
)
backup_path = os.path.join(data_dir, backup_name)
try:
shutil.copy2(DATA_PATH, backup_path)
with open(DATA_PATH, "w", encoding="utf-8", newline="") as handle:
handle.write(uploaded_text)
flash(f"CSV imported successfully. Backup created: {backup_name}", "success")
except Exception:
flash("Import failed. Existing CSV was not updated.", "error")
return redirect(url_for("admin_dashboard"))
@app.route("/admin", methods=["GET", "POST"])
def admin_dashboard() -> str:
if not is_admin():
@@ -286,7 +379,7 @@ def admin_dashboard() -> str:
sessions = build_session_summaries(events)
recent_sessions = sessions[:6]
open_sessions = [session for session in sessions if session.status == "open"]
recent_events = list(reversed(events[-20:]))
recent_events = list(reversed(events[-8:]))
return render_template(
"admin_dashboard.html",
+32
View File
@@ -1000,6 +1000,38 @@ td a:hover {
border-color: rgba(255, 166, 77, 0.35);
}
.admin-tools-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.import-form input[type="file"] {
width: 100%;
margin-top: 0.5rem;
margin-bottom: 1rem;
color: var(--text-muted);
}
.import-form input[type="file"]::file-selector-button {
appearance: none;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
color: var(--text-strong);
border-radius: 10px;
padding: 0.55rem 0.8rem;
font: inherit;
font-size: 0.9rem;
font-weight: 600;
margin-right: 0.75rem;
cursor: pointer;
}
.import-form input[type="file"]::file-selector-button:hover {
background: rgba(255, 166, 77, 0.12);
border-color: rgba(255, 166, 77, 0.28);
}
@media (max-width: 980px) {
.site-shell {
width: min(100% - 24px, 100%);
+42 -6
View File
@@ -52,6 +52,7 @@
<button class="primary-button" type="submit">Add event</button>
</form>
<section class="grid">
<form class="panel form-card" method="post" action="{{ url_for('admin_session_state') }}">
<p class="eyebrow">Session state</p>
<h2>Open or close a session</h2>
@@ -67,8 +68,45 @@
</div>
</form>
<div class="stack-gap">
<section class="panel">
<section class="panel form-card">
<div class="panel-header">
<div>
<p class="eyebrow">Tools</p>
<h2>Audit CSV</h2>
</div>
</div>
<div class="admin-tools-grid">
<div class="helper-box">
<p class="muted-text">
Export the current audit trail or import a replacement CSV. Import creates a timestamped backup first.
</p>
<div class="button-row">
<a class="primary-button" href="{{ url_for('admin_export_csv') }}">Export CSV</a>
</div>
</div>
<form method="post" action="{{ url_for('admin_import_csv') }}" enctype="multipart/form-data" class="helper-box import-form">
<label for="csv_file">
<span>Import replacement CSV</span>
</label>
<input id="csv_file" name="csv_file" type="file" accept=".csv,text/csv" required>
<div class="button-row">
<button
class="secondary-button"
type="submit"
onclick="return confirm('Replace the current audit CSV? A backup will be created first.');"
>
Import CSV
</button>
</div>
</form>
</div>
</section>
</section>
</section>
<section class="panel">
<div class="panel-header">
<div>
<p class="eyebrow">Recent sessions</p>
@@ -122,9 +160,9 @@
</tbody>
</table>
</div>
</section>
</section>
<section class="panel">
<section class="panel">
<div class="panel-header">
<div>
<p class="eyebrow">Recent raw events</p>
@@ -146,7 +184,5 @@
<p class="muted-text">No recent events.</p>
{% endfor %}
</div>
</section>
</div>
</section>
{% endblock %}