04/24 fix bugs

This commit is contained in:
2026-04-24 08:08:35 -04:00
parent 6ec439bafe
commit 07bae2a724
5 changed files with 1124 additions and 58 deletions
+326 -28
View File
@@ -1,10 +1,30 @@
"""
views/admin_log_view.py — Admin panel: Activity Log tab.
views/admin_log_view.py — Admin panel: Logs
Two tabs:
Activity Log — user-action audit trail (existing, from activity_log table)
App Log — application-level logging (new, from app_log table)
"""
import tkinter as tk
from tkinter import ttk
from utils.ui_helpers import COLOURS, FONT_HEADING, show_error
from tkinter import ttk, messagebox
import logging
from utils.ui_helpers import (
COLOURS, FONT, FONT_BOLD, FONT_SMALL, FONT_HEADING,
show_error, show_info,
)
logger = logging.getLogger("admin_log_view")
# Colour map for log level badges
_LEVEL_COLOURS = {
"DEBUG": "text_dim",
"INFO": "text",
"WARNING": "warning",
"ERROR": "danger",
"CRITICAL": "danger",
}
class AdminLogView(ttk.Frame):
@@ -12,39 +32,317 @@ class AdminLogView(ttk.Frame):
super().__init__(parent)
self.current_user = current_user
self._build_ui()
self._load()
def _build_ui(self):
toolbar = ttk.Frame(self)
toolbar.pack(fill="x", pady=(0, 10))
ttk.Label(toolbar, text="Activity Log", style="Heading.TLabel").pack(side="left")
ttk.Button(toolbar, text="↻ Refresh", style="Ghost.TButton",
command=self._load).pack(side="right")
# Page header
hdr = ttk.Frame(self)
hdr.pack(fill="x", pady=(0, 8))
ttk.Label(hdr, text="Logs", style="Heading.TLabel").pack(side="left")
# Notebook with two tabs
nb = ttk.Notebook(self)
nb.pack(fill="both", expand=True)
# ── Tab 1 — Activity Log ──────────────────────────────────────────────
act_tab = ttk.Frame(nb)
nb.add(act_tab, text="📋 Activity Log")
self._build_activity_tab(act_tab)
# ── Tab 2 — App Log ───────────────────────────────────────────────────
app_tab = ttk.Frame(nb)
nb.add(app_tab, text="🖥 App Log")
self._build_app_log_tab(app_tab)
nb.bind("<<NotebookTabChanged>>", self._on_tab_changed)
self._nb = nb
# Load the default tab
self._load_activity()
# ── Activity Log tab ──────────────────────────────────────────────────────
def _build_activity_tab(self, parent):
C = COLOURS
toolbar = tk.Frame(parent, bg=C["surface"], pady=8, padx=10)
toolbar.pack(fill="x")
ttk.Button(toolbar, text="↻ Refresh", style="Ghost.TButton",
command=self._load_activity).pack(side="right")
# Search
tk.Label(toolbar, text="Search:",
bg=C["surface"], fg=C["text_dim"],
font=FONT_SMALL).pack(side="left", padx=(0, 4))
self._act_search_var = tk.StringVar()
self._act_search_var.trace_add("write", lambda *_: self._load_activity())
tk.Entry(toolbar, textvariable=self._act_search_var,
bg=C["surface2"], fg=C["text"],
insertbackground=C["text"],
relief="flat", font=FONT, width=24).pack(
side="left", ipady=4)
cols = ("Time", "User", "Action", "Entity", "Entity ID", "Detail")
self.tree = ttk.Treeview(self, columns=cols, show="headings", selectmode="browse")
widths = [140, 100, 130, 100, 70, 320]
widths = [140, 100, 140, 100, 70, 320]
self._act_tree = ttk.Treeview(
parent, columns=cols, show="headings", selectmode="browse")
for col, w in zip(cols, widths):
self.tree.heading(col, text=col)
self.tree.column(col, width=w, anchor="w")
self.tree.pack(fill="both", expand=True)
self._act_tree.heading(col, text=col)
self._act_tree.column(col, width=w, anchor="w")
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
self.tree.configure(yscrollcommand=vsb.set)
vsb.place(relx=1, rely=0, relheight=1, anchor="ne")
vsb = ttk.Scrollbar(parent, orient="vertical",
command=self._act_tree.yview)
self._act_tree.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self._act_tree.pack(fill="both", expand=True)
def _load(self):
# Status bar
self._act_status = tk.Label(
parent, text="", bg=C["surface2"], fg=C["text_dim"],
font=FONT_SMALL, anchor="w")
self._act_status.pack(fill="x", side="bottom")
def _load_activity(self, *_):
from models import get_activity_log
self.tree.delete(*self.tree.get_children())
search = self._act_search_var.get().strip() \
if hasattr(self, "_act_search_var") else ""
self._act_tree.delete(*self._act_tree.get_children())
try:
for entry in get_activity_log():
self.tree.insert("", "end", values=(
str(entry["logged_at"])[:16],
entry.get("username") or "",
entry["action"],
entry.get("entity") or "",
entry.get("entity_id") or "",
entry.get("detail") or "",
))
# Pass search to the DB query so filtering happens server-side.
entries = get_activity_log(limit=500, search=search)
except Exception as e:
show_error(f"Failed to load activity log:\n{e}")
return
for entry in entries:
self._act_tree.insert("", "end", values=(
str(entry.get("logged_at", ""))[:16],
entry.get("username") or "",
entry.get("action") or "",
entry.get("entity") or "",
entry.get("entity_id") or "",
entry.get("detail") or "",
))
self._act_status.config(text=f" {len(entries)} records")
# ── App Log tab ───────────────────────────────────────────────────────────
def _build_app_log_tab(self, parent):
C = COLOURS
toolbar = tk.Frame(parent, bg=C["surface"], pady=8, padx=10)
toolbar.pack(fill="x")
ttk.Button(toolbar, text="↻ Refresh", style="Ghost.TButton",
command=self._load_app_log).pack(side="right", padx=(4, 0))
# Purge button
tk.Button(
toolbar, text="🗑 Purge Old Entries",
command=self._purge_app_log,
bg=C["surface2"], fg=C["danger"],
activebackground=C["danger"], activeforeground=C["white"],
relief="flat", font=FONT_SMALL, cursor="hand2",
padx=8, pady=4,
).pack(side="right", padx=(4, 0))
# Level filter
tk.Label(toolbar, text="Level:",
bg=C["surface"], fg=C["text_dim"],
font=FONT_SMALL).pack(side="left", padx=(0, 4))
self._level_var = tk.StringVar(value="All")
level_cb = ttk.Combobox(
toolbar, textvariable=self._level_var,
values=["All", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
state="readonly", width=10,
)
level_cb.pack(side="left")
level_cb.bind("<<ComboboxSelected>>", lambda _: self._load_app_log())
# Search
tk.Label(toolbar, text="Search:",
bg=C["surface"], fg=C["text_dim"],
font=FONT_SMALL).pack(side="left", padx=(12, 4))
self._app_search_var = tk.StringVar()
self._app_search_var.trace_add("write", lambda *_: self._load_app_log())
tk.Entry(toolbar, textvariable=self._app_search_var,
bg=C["surface2"], fg=C["text"],
insertbackground=C["text"],
relief="flat", font=FONT, width=24).pack(
side="left", ipady=4)
# Treeview
cols = ("Time", "Level", "Logger", "Message")
widths = [140, 75, 130, 480]
self._app_tree = ttk.Treeview(
parent, columns=cols, show="headings", selectmode="browse")
for col, w in zip(cols, widths):
self._app_tree.heading(col, text=col)
self._app_tree.column(
col, width=w,
anchor="center" if col == "Level" else "w",
)
# Level colour tags
for level, colour_key in _LEVEL_COLOURS.items():
self._app_tree.tag_configure(level, foreground=C[colour_key])
# Bold for WARNING and above
self._app_tree.tag_configure("WARNING", foreground=C["warning"],
font=FONT_BOLD)
self._app_tree.tag_configure("ERROR", foreground=C["danger"],
font=FONT_BOLD)
self._app_tree.tag_configure("CRITICAL", foreground=C["danger"],
font=FONT_BOLD)
vsb = ttk.Scrollbar(parent, orient="vertical",
command=self._app_tree.yview)
self._app_tree.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
self._app_tree.pack(fill="both", expand=True)
# Detail strip — shows full message when a row is selected
detail_frame = tk.Frame(parent, bg=C["surface2"], pady=6, padx=10)
detail_frame.pack(fill="x", side="bottom")
self._app_detail_var = tk.StringVar(
value="Select a row to see the full message.")
tk.Label(
detail_frame, textvariable=self._app_detail_var,
bg=C["surface2"], fg=C["text"],
font=FONT_SMALL, anchor="w",
justify="left", wraplength=900,
).pack(fill="x")
self._app_tree.bind("<<TreeviewSelect>>", self._on_app_row_select)
# Status bar
self._app_status = tk.Label(
parent, text="", bg=C["surface2"], fg=C["text_dim"],
font=FONT_SMALL, anchor="w")
self._app_status.pack(fill="x", side="bottom")
# Store full messages keyed by tree iid for the detail strip
self._app_full_messages: dict[str, str] = {}
def _load_app_log(self, *_):
from models import get_app_log
level = self._level_var.get()
level_filter = "" if level == "All" else level
search = self._app_search_var.get().strip()
self._app_tree.delete(*self._app_tree.get_children())
self._app_full_messages.clear()
self._app_detail_var.set("Select a row to see the full message.")
try:
entries = get_app_log(
limit=500,
level_filter=level_filter,
search=search,
)
except Exception as e:
show_error(f"Failed to load app log:\n{e}")
return
for entry in entries:
lvl = entry.get("level", "INFO")
msg_full = entry.get("message", "")
# Truncate long messages in the tree; full text in detail strip
msg_short = msg_full[:120] + ("" if len(msg_full) > 120 else "")
iid = str(entry["id"])
self._app_tree.insert(
"", "end", iid=iid, tags=(lvl,),
values=(
str(entry.get("logged_at", ""))[:19],
lvl,
entry.get("logger_name", ""),
msg_short,
),
)
self._app_full_messages[iid] = msg_full
count = len(entries)
self._app_status.config(text=f" {count} records")
def _on_app_row_select(self, event=None):
sel = self._app_tree.selection()
if not sel:
return
iid = sel[0]
full = self._app_full_messages.get(iid, "")
self._app_detail_var.set(full)
def _purge_app_log(self):
"""Ask for confirmation, then delete log entries older than N days."""
days_var = tk.StringVar(value="30")
dlg = tk.Toplevel(self)
dlg.title("Purge App Log")
dlg.configure(bg=COLOURS["bg"])
dlg.resizable(False, False)
dlg.grab_set()
w, h = 340, 170
x = (dlg.winfo_screenwidth() - w) // 2
y = (dlg.winfo_screenheight() - h) // 2
dlg.geometry(f"{w}x{h}+{x}+{y}")
tk.Label(dlg,
text="Delete app log entries older than:",
bg=COLOURS["bg"], fg=COLOURS["text"],
font=FONT_BOLD).pack(pady=(20, 8))
row = tk.Frame(dlg, bg=COLOURS["bg"])
row.pack()
ttk.Spinbox(row, from_=1, to=365,
textvariable=days_var, width=6).pack(side="left")
tk.Label(row, text=" days",
bg=COLOURS["bg"], fg=COLOURS["text"],
font=FONT).pack(side="left")
def _do_purge():
try:
days = int(days_var.get())
if days < 1:
raise ValueError
except ValueError:
show_error("Please enter a valid number of days (1365).")
return
if not messagebox.askyesno(
"Confirm Purge",
f"Delete all app log entries older than {days} days?\n"
"This cannot be undone.",
parent=dlg,
):
return
try:
from models import purge_app_log
deleted = purge_app_log(days)
dlg.destroy()
show_info(f"{deleted} old log record(s) deleted.")
self._load_app_log()
except Exception as e:
show_error(f"Purge failed:\n{e}")
btn_frame = tk.Frame(dlg, bg=COLOURS["bg"])
btn_frame.pack(pady=12)
tk.Button(btn_frame, text="Purge",
command=_do_purge,
bg=COLOURS["danger"], fg=COLOURS["white"],
activebackground="#c94444", activeforeground=COLOURS["white"],
relief="flat", font=FONT_SMALL, cursor="hand2",
padx=12, pady=6).pack(side="left", padx=(0, 8))
tk.Button(btn_frame, text="Cancel",
command=dlg.destroy,
bg=COLOURS["surface2"], fg=COLOURS["text"],
relief="flat", font=FONT_SMALL, cursor="hand2",
padx=12, pady=6).pack(side="left")
# ── Tab switch ────────────────────────────────────────────────────────────
def _on_tab_changed(self, event=None):
try:
idx = self._nb.index(self._nb.select())
if idx == 0:
self._load_activity()
else:
self._load_app_log()
except Exception:
pass