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 #!/usr/bin/env python3
from __future__ import annotations from __future__ import annotations
import csv
import os import os
import shutil
from datetime import datetime, timezone
from pathlib import Path 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 ( from stats import (
apply_rank_changes, apply_rank_changes,
@@ -236,6 +248,87 @@ def admin_session_state() -> str:
return redirect(url_for("admin_dashboard")) 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"]) @app.route("/admin", methods=["GET", "POST"])
def admin_dashboard() -> str: def admin_dashboard() -> str:
if not is_admin(): if not is_admin():
@@ -286,7 +379,7 @@ def admin_dashboard() -> str:
sessions = build_session_summaries(events) sessions = build_session_summaries(events)
recent_sessions = sessions[:6] recent_sessions = sessions[:6]
open_sessions = [session for session in sessions if session.status == "open"] 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( return render_template(
"admin_dashboard.html", "admin_dashboard.html",
+32
View File
@@ -1000,6 +1000,38 @@ td a:hover {
border-color: rgba(255, 166, 77, 0.35); 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) { @media (max-width: 980px) {
.site-shell { .site-shell {
width: min(100% - 24px, 100%); width: min(100% - 24px, 100%);
+42 -6
View File
@@ -52,6 +52,7 @@
<button class="primary-button" type="submit">Add event</button> <button class="primary-button" type="submit">Add event</button>
</form> </form>
<section class="grid">
<form class="panel form-card" method="post" action="{{ url_for('admin_session_state') }}"> <form class="panel form-card" method="post" action="{{ url_for('admin_session_state') }}">
<p class="eyebrow">Session state</p> <p class="eyebrow">Session state</p>
<h2>Open or close a session</h2> <h2>Open or close a session</h2>
@@ -67,8 +68,45 @@
</div> </div>
</form> </form>
<div class="stack-gap"> <section class="panel form-card">
<section class="panel"> <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 class="panel-header">
<div> <div>
<p class="eyebrow">Recent sessions</p> <p class="eyebrow">Recent sessions</p>
@@ -122,9 +160,9 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</section> </section>
<section class="panel"> <section class="panel">
<div class="panel-header"> <div class="panel-header">
<div> <div>
<p class="eyebrow">Recent raw events</p> <p class="eyebrow">Recent raw events</p>
@@ -146,7 +184,5 @@
<p class="muted-text">No recent events.</p> <p class="muted-text">No recent events.</p>
{% endfor %} {% endfor %}
</div> </div>
</section>
</div>
</section> </section>
{% endblock %} {% endblock %}