05/24 Fix bugs
This commit is contained in:
@@ -1668,3 +1668,31 @@ def consume_password_reset_token(token: str, new_password: str) -> bool:
|
|||||||
finally:
|
finally:
|
||||||
if conn:
|
if conn:
|
||||||
conn.close()
|
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()
|
||||||
|
|||||||
+24
-8
@@ -13,7 +13,7 @@ from flask import (Blueprint, render_template, request, redirect, url_for,
|
|||||||
from models import (
|
from models import (
|
||||||
get_all_criteria, get_active_criteria, create_criterion, update_criterion,
|
get_all_criteria, get_active_criteria, create_criterion, update_criterion,
|
||||||
delete_criterion, save_ai_analysis, get_ai_analysis_history,
|
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 config import get_setting
|
||||||
from utils.decorators import login_required, admin_required
|
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"]:
|
if user["role"] != "admin" and record["user_id"] != user["id"]:
|
||||||
return jsonify({"error": "Access denied"}), 403
|
return jsonify({"error": "Access denied"}), 403
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"id": record["id"],
|
"id": record["id"],
|
||||||
"username": record.get("username"),
|
"username": record.get("username"),
|
||||||
"file_names": record["file_names"],
|
"file_names": record["file_names"],
|
||||||
"model": record["model"],
|
"model": record["model"],
|
||||||
"verdict": record["verdict"],
|
"verdict": record["verdict"],
|
||||||
"analyzed_at": str(record["analyzed_at"]),
|
"analyzed_at": str(record["analyzed_at"]),
|
||||||
"summary_text": record["summary_text"],
|
"summary_text": record["summary_text"],
|
||||||
|
"criteria_snapshot": record.get("criteria_snapshot") or "",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ai_summary_bp.route("/history/<int:analysis_id>/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})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from models import (
|
|||||||
authenticate, check_login_allowed, change_password, log_action,
|
authenticate, check_login_allowed, change_password, log_action,
|
||||||
get_user_by_email, create_password_reset_token,
|
get_user_by_email, create_password_reset_token,
|
||||||
get_password_reset_user, consume_password_reset_token,
|
get_password_reset_user, consume_password_reset_token,
|
||||||
|
purge_expired_reset_tokens,
|
||||||
)
|
)
|
||||||
from utils.decorators import login_required
|
from utils.decorators import login_required
|
||||||
from utils.email import send_email
|
from utils.email import send_email
|
||||||
@@ -46,6 +47,10 @@ def login():
|
|||||||
"role": user["role"],
|
"role": user["role"],
|
||||||
"email": user.get("email", ""),
|
"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}.")
|
logger.info(f"User '{username}' logged in from {ip}.")
|
||||||
flash(f"You have been signed in. Welcome, {session['user']['full_name']}!", "success")
|
flash(f"You have been signed in. Welcome, {session['user']['full_name']}!", "success")
|
||||||
if user["role"] == "admin":
|
if user["role"] == "admin":
|
||||||
|
|||||||
@@ -220,5 +220,29 @@ function buildShiftForm(s) {
|
|||||||
<textarea class="form-control" name="note" rows="2">${esc(s.note)}</textarea></div>`;
|
<textarea class="form-control" name="note" rows="2">${esc(s.note)}</textarea></div>`;
|
||||||
}
|
}
|
||||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
||||||
|
|
||||||
|
// Highlight today's column in the calendar
|
||||||
|
(function() {
|
||||||
|
var jsDay = new Date().getDay(); // 0=Sun … 6=Sat
|
||||||
|
var mysqlDay = String(jsDay === 0 ? 1 : jsDay + 1); // MySQL DAYOFWEEK: 1=Sun, 2=Mon…
|
||||||
|
var colIdx = DAY_MAP.findIndex(function(d) { return d[1] === mysqlDay; });
|
||||||
|
if (colIdx === -1) return;
|
||||||
|
var calTable = document.querySelector('#tab-calendar table');
|
||||||
|
if (!calTable) return;
|
||||||
|
calTable.querySelectorAll('tr').forEach(function(tr) {
|
||||||
|
var cells = tr.querySelectorAll('th, td');
|
||||||
|
var cell = cells[colIdx + 1]; // +1 for the leading Shift name column
|
||||||
|
if (cell) cell.classList.add('cal-today');
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cal-today { background: #eff6ff !important; }
|
||||||
|
.cal-today th, thead .cal-today {
|
||||||
|
background: #dbeafe !important;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -233,9 +233,11 @@
|
|||||||
<div id="hist-verdict-banner" style="display:none"></div>
|
<div id="hist-verdict-banner" style="display:none"></div>
|
||||||
<div class="modal-body" style="padding:0">
|
<div class="modal-body" style="padding:0">
|
||||||
<div class="ai-meta-bar" id="hist-meta"></div>
|
<div class="ai-meta-bar" id="hist-meta"></div>
|
||||||
<div id="hist-body" class="ai-rendered-output" style="max-height:65vh;overflow-y:auto;padding:1.25rem 1.5rem"></div>
|
<div id="hist-criteria" style="display:none;padding:.6rem 1.5rem;font-size:.78rem;background:var(--bg-subtle);border-bottom:1px solid var(--border)"></div>
|
||||||
|
<div id="hist-body" class="ai-rendered-output" style="max-height:60vh;overflow-y:auto;padding:1.25rem 1.5rem"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-danger btn-sm" id="btn-delete-analysis" style="margin-right:auto;display:none" onclick="deleteAnalysis()">🗑 Delete</button>
|
||||||
<button class="btn btn-secondary" onclick="closeModal('modal-history')">Close</button>
|
<button class="btn btn-secondary" onclick="closeModal('modal-history')">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,13 +409,18 @@ document.querySelectorAll('.js-edit-criterion').forEach(function(btn) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* ─── History rows ──────────────────────────────────────────── */
|
/* ─── History rows ──────────────────────────────────────────── */
|
||||||
|
var _currentAnalysisId = null;
|
||||||
|
|
||||||
document.querySelectorAll('.history-row').forEach(function(row) {
|
document.querySelectorAll('.history-row').forEach(function(row) {
|
||||||
row.addEventListener('click', function() {
|
row.addEventListener('click', function() {
|
||||||
var id = row.dataset.id;
|
var id = row.dataset.id;
|
||||||
|
_currentAnalysisId = id;
|
||||||
document.getElementById('hist-title').textContent = 'Loading…';
|
document.getElementById('hist-title').textContent = 'Loading…';
|
||||||
document.getElementById('hist-meta').textContent = '';
|
document.getElementById('hist-meta').textContent = '';
|
||||||
document.getElementById('hist-body').innerHTML = '';
|
document.getElementById('hist-body').innerHTML = '';
|
||||||
|
document.getElementById('hist-criteria').style.display = 'none';
|
||||||
document.getElementById('hist-verdict-banner').style.display = 'none';
|
document.getElementById('hist-verdict-banner').style.display = 'none';
|
||||||
|
document.getElementById('btn-delete-analysis').style.display = 'none';
|
||||||
openModal('modal-history');
|
openModal('modal-history');
|
||||||
|
|
||||||
fetch('/ai-summary/history/' + id)
|
fetch('/ai-summary/history/' + id)
|
||||||
@@ -435,8 +442,23 @@ document.querySelectorAll('.history-row').forEach(function(row) {
|
|||||||
vb.innerHTML = verdictBannerHTML(d.verdict);
|
vb.innerHTML = verdictBannerHTML(d.verdict);
|
||||||
vb.style.display = '';
|
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 = '<strong style="color:var(--text)">Criteria used:</strong> ' + escHtml(labels);
|
||||||
|
criteriaEl.style.display = '';
|
||||||
|
}
|
||||||
|
} catch(e) { /* ignore malformed snapshot */ }
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('hist-body').innerHTML =
|
document.getElementById('hist-body').innerHTML =
|
||||||
renderAI(d.summary_text || '');
|
renderAI(d.summary_text || '');
|
||||||
|
document.getElementById('btn-delete-analysis').style.display = '';
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
document.getElementById('hist-body').textContent = 'Failed to load analysis.';
|
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 ───────────────────────────────────────────────── */
|
/* ─── Utility ───────────────────────────────────────────────── */
|
||||||
function escHtml(str) {
|
function escHtml(str) {
|
||||||
return String(str || '').replace(/[&<>"']/g, function(c) {
|
return String(str || '').replace(/[&<>"']/g, function(c) {
|
||||||
|
|||||||
Reference in New Issue
Block a user