From cb98c7e222390a3392b69ac057d7a2c336b2a7c2 Mon Sep 17 00:00:00 2001 From: Nguyen HP Laptop Date: Sun, 24 May 2026 22:51:27 -0400 Subject: [PATCH] 05/24 Enhance functionalities --- routes/admin_settings.py | 52 +++++++++++++++++++++++++++++++++-- routes/admin_users.py | 37 +++++++++++++++++++++++-- routes/bid_tracker.py | 30 +++++++++++++++++++- templates/admin/settings.html | 45 ++++++++++++++++++++++++++++++ templates/admin/users.html | 34 +++++++++++++++++++++++ templates/ai_summary.html | 34 ++++++++++++++++++++++- templates/bid_tracker.html | 7 +++++ 7 files changed, 233 insertions(+), 6 deletions(-) diff --git a/routes/admin_settings.py b/routes/admin_settings.py index 3618194..b222151 100644 --- a/routes/admin_settings.py +++ b/routes/admin_settings.py @@ -3,8 +3,10 @@ routes/admin_settings.py — Application settings (email, Groq API, etc.) """ import logging -from flask import Blueprint, render_template, request, redirect, url_for, flash, session -from config import get_settings_dict, set_setting +import os +import requests as http_requests +from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify +from config import get_settings_dict, set_setting, get_setting from models import log_action from utils.decorators import admin_required @@ -54,3 +56,49 @@ def save_groq(): flash("Groq settings saved successfully.", "success") logger.info(f"Groq settings updated by admin_id={admin['id']}.") return redirect(url_for("admin_settings.settings")) + + +@admin_settings_bp.route("/test-email", methods=["POST"]) +@admin_required +def test_email(): + from utils.email import send_email + admin = session["user"] + to = (admin.get("email") or "").strip() + if not to: + return jsonify({"error": "Your admin account has no email address. Add one in User Management."}), 400 + try: + send_email( + to, + "Website Checker — SMTP Test", + "This is a test email from Website Checker. Your SMTP configuration is working correctly.", + ) + log_action(admin["id"], "TEST_EMAIL", "app_settings", None, + f"SMTP test email sent to {to}.") + return jsonify({"ok": True, "to": to}) + except Exception as e: + logger.error(f"SMTP test failed: {e}") + return jsonify({"error": str(e)}), 500 + + +@admin_settings_bp.route("/test-groq", methods=["POST"]) +@admin_required +def test_groq(): + admin = session["user"] + api_key = (os.environ.get("GROQ_API_KEY") or "").strip() or get_setting("groq.api_key", "").strip() + if not api_key: + return jsonify({"error": "No Groq API key is configured. Save one first."}), 400 + model = get_setting("groq.model", "llama-3.3-70b-versatile") + try: + resp = http_requests.post( + "https://api.groq.com/openai/v1/chat/completions", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json={"model": model, "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 5}, + timeout=15, + ) + if resp.ok: + log_action(admin["id"], "TEST_GROQ", "app_settings", None, "Groq API test successful.") + return jsonify({"ok": True, "model": model}) + return jsonify({"error": f"Groq API returned {resp.status_code}: {resp.text[:200]}"}), 400 + except Exception as e: + logger.error(f"Groq test failed: {e}") + return jsonify({"error": str(e)}), 500 diff --git a/routes/admin_users.py b/routes/admin_users.py index 241a4a2..3c0620d 100644 --- a/routes/admin_users.py +++ b/routes/admin_users.py @@ -3,9 +3,13 @@ routes/admin_users.py — User management CRUD routes. """ import logging -from flask import Blueprint, render_template, request, redirect, url_for, flash, session -from models import get_all_users, create_user, update_user, delete_user +from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify +from models import ( + get_all_users, create_user, update_user, delete_user, + get_user_by_id, create_password_reset_token, log_action, +) from utils.decorators import admin_required +from utils.email import send_email logger = logging.getLogger("routes.admin_users") admin_users_bp = Blueprint("admin_users", __name__, url_prefix="/admin/users") @@ -82,3 +86,32 @@ def delete(user_id): flash(f"Error deleting user: {e}", "danger") return redirect(url_for("admin_users.users_list")) + + +@admin_users_bp.route("//send-reset", methods=["POST"]) +@admin_required +def send_reset(user_id): + admin = session["user"] + user = get_user_by_id(user_id) + if not user: + return jsonify({"error": "User not found."}), 404 + if not user.get("email"): + return jsonify({"error": f"User '{user['username']}' has no email address set."}), 400 + try: + token = create_password_reset_token(user_id) + reset_url = url_for("auth.reset_password", token=token, _external=True) + body = ( + f"Hi {user.get('full_name') or user['username']},\n\n" + f"An administrator has sent you a password reset link for your Website Checker account.\n" + f"Click the link below to set a new password (valid for 1 hour):\n\n" + f"{reset_url}\n\n" + f"If you did not expect this, please contact your administrator.\n" + ) + send_email(user["email"], "Website Checker — Password Reset", body) + log_action(admin["id"], "SEND_RESET_LINK", "users", user_id, + f"Password reset link sent to '{user['username']}' ({user['email']}).") + logger.info(f"Reset email sent to user_id={user_id} by admin_id={admin['id']}.") + return jsonify({"ok": True, "to": user["email"]}) + except Exception as e: + logger.error(f"send_reset error for user_id={user_id}: {e}") + return jsonify({"error": str(e)}), 500 diff --git a/routes/bid_tracker.py b/routes/bid_tracker.py index 007f2a5..6f30a14 100644 --- a/routes/bid_tracker.py +++ b/routes/bid_tracker.py @@ -2,9 +2,11 @@ routes/bid_tracker.py — Bid / Opportunity tracker routes. """ +import csv +import io import logging from flask import (Blueprint, render_template, request, redirect, url_for, - flash, session, jsonify) + flash, session, jsonify, Response) from models import ( get_all_bids, get_bid, create_bid, update_bid, delete_bid, get_bid_updates, add_bid_update, delete_bid_update, BID_STATUSES, @@ -276,3 +278,29 @@ def send_reminders(): flash(f"Failed to send email: {e}", "danger") return redirect(url_for("bid_tracker.bids_list")) + + +@bid_tracker_bp.route("/export.csv") +@login_required +def export_csv(): + status = request.args.get("status", "") + bids = get_all_bids(status_filter=status, limit=10000, offset=0) + buf = io.StringIO() + writer = csv.writer(buf) + writer.writerow([ + "ID", "Title", "Source", "Solicitation #", "Status", + "Due Date", "URL", "Notes", "Added By", "Created At", + ]) + for b in bids: + writer.writerow([ + b["id"], b.get("title", ""), b.get("source", ""), + b.get("solicitation_number", ""), b.get("status", ""), + str(b.get("due_date") or ""), b.get("url", ""), + b.get("notes", ""), b.get("added_by_username", ""), + str(b.get("created_at") or ""), + ]) + return Response( + buf.getvalue(), + mimetype="text/csv", + headers={"Content-Disposition": "attachment; filename=bids_export.csv"}, + ) diff --git a/templates/admin/settings.html b/templates/admin/settings.html index eb4bbba..3849575 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -52,6 +52,8 @@ @@ -77,9 +79,52 @@ + + + + {% endblock %} diff --git a/templates/admin/users.html b/templates/admin/users.html index e21f064..b9626b4 100644 --- a/templates/admin/users.html +++ b/templates/admin/users.html @@ -27,6 +27,11 @@ + {% if u.email %} + + {% endif %}
@@ -141,5 +146,34 @@ function openEditUser(u) { '/admin/users/' + u.id + '/edit'; openModal('modal-edit-user'); } + +document.querySelectorAll('.js-send-reset').forEach(function(btn) { + btn.addEventListener('click', function() { + var username = btn.dataset.username; + var email = btn.dataset.email; + if (!confirm('Send a password reset link to ' + username + ' (' + email + ')?')) return; + btn.disabled = true; + btn.textContent = '…'; + fetch('/admin/users/' + btn.dataset.id + '/send-reset', { + method: 'POST', + headers: { 'X-CSRFToken': getCsrfToken() }, + }).then(function(r) { return r.json(); }) + .then(function(d) { + if (d.ok) { + btn.textContent = '✓ Sent'; + btn.style.color = 'var(--success, #16a34a)'; + } else { + alert('Error: ' + d.error); + btn.disabled = false; + btn.textContent = '✉ Reset'; + } + }) + .catch(function() { + alert('Request failed. Please try again.'); + btn.disabled = false; + btn.textContent = '✉ Reset'; + }); + }); +}); {% endblock %} diff --git a/templates/ai_summary.html b/templates/ai_summary.html index 8d74aba..cf1ad5d 100644 --- a/templates/ai_summary.html +++ b/templates/ai_summary.html @@ -113,7 +113,16 @@
-
Analysis History
+
+ Analysis History +
+ + + + + +
+
@@ -484,6 +493,22 @@ function deleteAnalysis() { .catch(function() { alert('Delete failed. Please try again.'); }); } +/* ─── History verdict filter ────────────────────────────────── */ +document.querySelectorAll('.ai-hist-filter').forEach(function(btn) { + btn.addEventListener('click', function() { + document.querySelectorAll('.ai-hist-filter').forEach(function(b) { b.classList.remove('active'); }); + btn.classList.add('active'); + var verdict = btn.dataset.verdict; + document.querySelectorAll('.history-row').forEach(function(row) { + if (!verdict) { row.style.display = ''; return; } + var badge = row.querySelector('.badge'); + var rowVerdict = badge ? badge.textContent.trim() : ''; + var show = verdict === 'none' ? !badge : rowVerdict === verdict; + row.style.display = show ? '' : 'none'; + }); + }); +}); + /* ─── Utility ───────────────────────────────────────────────── */ function escHtml(str) { return String(str || '').replace(/[&<>"']/g, function(c) { @@ -577,6 +602,13 @@ function escHtml(str) { /* ── History ───────────────────────────────────────────────── */ .history-row:hover td{background:var(--bg-subtle)} +.ai-hist-filter{ + background:none;border:1px solid var(--border); + padding:.2rem .55rem;font-size:.72rem;border-radius:var(--r-sm); + cursor:pointer;color:var(--text-secondary);font-family:inherit; + transition:background var(--t),color var(--t); +} +.ai-hist-filter.active{background:var(--accent);border-color:var(--accent);color:#fff} @media(max-width:960px){ .ai-layout{grid-template-columns:1fr} diff --git a/templates/bid_tracker.html b/templates/bid_tracker.html index 7611db6..cddbe35 100644 --- a/templates/bid_tracker.html +++ b/templates/bid_tracker.html @@ -22,6 +22,7 @@ + {% if session['user']['role'] == 'admin' %} @@ -172,6 +173,12 @@ var _allBids = []; // cached from last load var _bidOffset = 0; var _hasMore = false; var _PAGE_SIZE = 50; + +function exportCsv() { + var url = '/bids/export.csv'; + if (_activeStatus) url += '?status=' + encodeURIComponent(_activeStatus); + window.location.href = url; +} var _currentUserId = {{ session['user']['id'] }}; var _isAdmin = {{ 'true' if session['user']['role'] == 'admin' else 'false' }};