From 9e1493eea762a39b9c585396545239b2f45363b8 Mon Sep 17 00:00:00 2001 From: Nguyen HP Laptop Date: Sun, 24 May 2026 22:36:36 -0400 Subject: [PATCH] 05/24 Fix bugs --- models.py | 28 +++++++++++++++++++++++++ routes/ai_summary.py | 32 +++++++++++++++++++++------- routes/auth.py | 5 +++++ templates/admin/shifts.html | 24 +++++++++++++++++++++ templates/ai_summary.html | 42 ++++++++++++++++++++++++++++++++++++- 5 files changed, 122 insertions(+), 9 deletions(-) diff --git a/models.py b/models.py index 3865bf1..5ba4d4c 100644 --- a/models.py +++ b/models.py @@ -1668,3 +1668,31 @@ def consume_password_reset_token(token: str, new_password: str) -> bool: finally: if conn: conn.close() + + +def purge_expired_reset_tokens(): + """Delete all expired password reset tokens.""" + conn = None + try: + conn = get_connection() + cur = conn.cursor() + cur.execute("DELETE FROM password_reset_tokens WHERE expires_at < NOW()") + conn.commit() + cur.close() + finally: + if conn: + conn.close() + + +def delete_ai_analysis(analysis_id: int): + """Permanently delete an AI analysis record.""" + conn = None + try: + conn = get_connection() + cur = conn.cursor() + cur.execute("DELETE FROM ai_analysis_log WHERE id=%s", (analysis_id,)) + conn.commit() + cur.close() + finally: + if conn: + conn.close() diff --git a/routes/ai_summary.py b/routes/ai_summary.py index 700a5fb..e2326ab 100644 --- a/routes/ai_summary.py +++ b/routes/ai_summary.py @@ -13,7 +13,7 @@ from flask import (Blueprint, render_template, request, redirect, url_for, from models import ( get_all_criteria, get_active_criteria, create_criterion, update_criterion, delete_criterion, save_ai_analysis, get_ai_analysis_history, - get_ai_analysis_detail, log_action, + get_ai_analysis_detail, delete_ai_analysis, log_action, ) from config import get_setting from utils.decorators import login_required, admin_required @@ -370,11 +370,27 @@ def analysis_detail(analysis_id): if user["role"] != "admin" and record["user_id"] != user["id"]: return jsonify({"error": "Access denied"}), 403 return jsonify({ - "id": record["id"], - "username": record.get("username"), - "file_names": record["file_names"], - "model": record["model"], - "verdict": record["verdict"], - "analyzed_at": str(record["analyzed_at"]), - "summary_text": record["summary_text"], + "id": record["id"], + "username": record.get("username"), + "file_names": record["file_names"], + "model": record["model"], + "verdict": record["verdict"], + "analyzed_at": str(record["analyzed_at"]), + "summary_text": record["summary_text"], + "criteria_snapshot": record.get("criteria_snapshot") or "", }) + + +@ai_summary_bp.route("/history//delete", methods=["POST"]) +@login_required +def delete_analysis(analysis_id): + record = get_ai_analysis_detail(analysis_id) + if not record: + return jsonify({"error": "Not found"}), 404 + user = session["user"] + if user["role"] != "admin" and record["user_id"] != user["id"]: + return jsonify({"error": "Access denied"}), 403 + delete_ai_analysis(analysis_id) + log_action(user["id"], "DELETE_AI_ANALYSIS", "ai_analysis_log", analysis_id, + f"Deleted AI analysis id={analysis_id}.") + return jsonify({"ok": True}) diff --git a/routes/auth.py b/routes/auth.py index bd6bedb..681a658 100644 --- a/routes/auth.py +++ b/routes/auth.py @@ -8,6 +8,7 @@ from models import ( authenticate, check_login_allowed, change_password, log_action, get_user_by_email, create_password_reset_token, get_password_reset_user, consume_password_reset_token, + purge_expired_reset_tokens, ) from utils.decorators import login_required from utils.email import send_email @@ -46,6 +47,10 @@ def login(): "role": user["role"], "email": user.get("email", ""), } + try: + purge_expired_reset_tokens() + except Exception: + pass # non-critical cleanup; never fail a login because of it logger.info(f"User '{username}' logged in from {ip}.") flash(f"You have been signed in. Welcome, {session['user']['full_name']}!", "success") if user["role"] == "admin": diff --git a/templates/admin/shifts.html b/templates/admin/shifts.html index df52d43..6ecf436 100644 --- a/templates/admin/shifts.html +++ b/templates/admin/shifts.html @@ -220,5 +220,29 @@ function buildShiftForm(s) { `; } function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/ + + {% endblock %} diff --git a/templates/ai_summary.html b/templates/ai_summary.html index dba5b13..8d74aba 100644 --- a/templates/ai_summary.html +++ b/templates/ai_summary.html @@ -233,9 +233,11 @@ @@ -407,13 +409,18 @@ document.querySelectorAll('.js-edit-criterion').forEach(function(btn) { }); /* ─── History rows ──────────────────────────────────────────── */ +var _currentAnalysisId = null; + document.querySelectorAll('.history-row').forEach(function(row) { row.addEventListener('click', function() { var id = row.dataset.id; + _currentAnalysisId = id; document.getElementById('hist-title').textContent = 'Loading…'; document.getElementById('hist-meta').textContent = ''; document.getElementById('hist-body').innerHTML = ''; + document.getElementById('hist-criteria').style.display = 'none'; document.getElementById('hist-verdict-banner').style.display = 'none'; + document.getElementById('btn-delete-analysis').style.display = 'none'; openModal('modal-history'); fetch('/ai-summary/history/' + id) @@ -435,8 +442,23 @@ document.querySelectorAll('.history-row').forEach(function(row) { vb.innerHTML = verdictBannerHTML(d.verdict); vb.style.display = ''; } + + // Criteria snapshot + var criteriaEl = document.getElementById('hist-criteria'); + if (d.criteria_snapshot) { + try { + var snap = JSON.parse(d.criteria_snapshot); + if (snap && snap.length) { + var labels = snap.map(function(c) { return c.title; }).join(' Β· '); + criteriaEl.innerHTML = 'Criteria used: ' + escHtml(labels); + criteriaEl.style.display = ''; + } + } catch(e) { /* ignore malformed snapshot */ } + } + document.getElementById('hist-body').innerHTML = renderAI(d.summary_text || ''); + document.getElementById('btn-delete-analysis').style.display = ''; }) .catch(function() { document.getElementById('hist-body').textContent = 'Failed to load analysis.'; @@ -444,6 +466,24 @@ document.querySelectorAll('.history-row').forEach(function(row) { }); }); +function deleteAnalysis() { + if (!_currentAnalysisId) return; + if (!confirm('Delete this analysis? This cannot be undone.')) return; + fetch('/ai-summary/history/' + _currentAnalysisId + '/delete', { + method: 'POST', + headers: { 'X-CSRFToken': getCsrfToken() }, + }).then(function(r) { return r.json(); }) + .then(function(d) { + if (d.error) { alert(d.error); return; } + closeModal('modal-history'); + // Remove the row from the table + var row = document.querySelector('.history-row[data-id="' + _currentAnalysisId + '"]'); + if (row) row.remove(); + _currentAnalysisId = null; + }) + .catch(function() { alert('Delete failed. Please try again.'); }); +} + /* ─── Utility ───────────────────────────────────────────────── */ function escHtml(str) { return String(str || '').replace(/[&<>"']/g, function(c) {