diff --git a/models.py b/models.py index 413b052..dc704a1 100644 --- a/models.py +++ b/models.py @@ -1073,7 +1073,24 @@ def get_user_active_shifts(user_id: int): # ─── Admin Dashboard ────────────────────────────────────────────────────────── +_admin_stats_cache: dict | None = None +_admin_stats_cached_at: datetime.datetime | None = None +_ADMIN_STATS_TTL_S = 60 # seconds + + +def invalidate_admin_stats_cache(): + global _admin_stats_cache, _admin_stats_cached_at + _admin_stats_cache = None + _admin_stats_cached_at = None + + def get_admin_dashboard_stats(): + global _admin_stats_cache, _admin_stats_cached_at + now = datetime.datetime.now() + if (_admin_stats_cache is not None and _admin_stats_cached_at is not None + and (now - _admin_stats_cached_at).total_seconds() < _ADMIN_STATS_TTL_S): + return _admin_stats_cache + conn = None try: conn = get_connection() @@ -1123,7 +1140,7 @@ def get_admin_dashboard_stats(): ) ai_analyses_30d = cur.fetchone()["n"] cur.close() - return { + result = { "user_stats": user_stats, "total_sites": total_sites, "total_users": total_users, @@ -1132,6 +1149,9 @@ def get_admin_dashboard_stats(): "bids_due_soon": bids_due_soon, "ai_analyses_30d": ai_analyses_30d, } + _admin_stats_cache = result + _admin_stats_cached_at = now + return result finally: if conn: conn.close() diff --git a/routes/ai_summary.py b/routes/ai_summary.py index e2326ab..2051517 100644 --- a/routes/ai_summary.py +++ b/routes/ai_summary.py @@ -108,7 +108,7 @@ def analyze(): f"AI analysis on {len(file_names)} file(s). Verdict: {result.get('verdict')}.") return jsonify({"analysis_id": analysis_id, "model": model, - "file_count": len(file_names), **result}) + "file_count": len(file_names), "file_errors": errors, **result}) # Magic-byte signatures for each supported binary format. diff --git a/static/js/app.js b/static/js/app.js index 7ec0aa4..515a039 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -75,7 +75,7 @@ document.addEventListener('DOMContentLoaded', () => { alert.style.transition = 'opacity 0.5s'; alert.style.opacity = '0'; setTimeout(() => alert.remove(), 500); - }, 5000); + }, 7000); }); }); diff --git a/templates/ai_summary.html b/templates/ai_summary.html index cf1ad5d..2073920 100644 --- a/templates/ai_summary.html +++ b/templates/ai_summary.html @@ -358,7 +358,13 @@ async function runAnalysis() { var btn = document.getElementById('btn-analyze'); btn.disabled = true; - btn.textContent = '⏳ Analysing…'; + + var _elapsed = 0; + var _elapsedTimer = setInterval(function() { + _elapsed++; + btn.textContent = '⏳ Analysing… ' + _elapsed + 's'; + }, 1000); + btn.textContent = '⏳ Analysing… 0s'; document.getElementById('ai-output').innerHTML = '

Running AI analysis… this may take a moment.

'; @@ -384,11 +390,21 @@ async function runAnalysis() { data.model = model || '{{ groq_model }}'; data.criteria_count = {{ criteria | selectattr('is_active') | list | length }}; showResult(data); + if (data.file_errors && data.file_errors.length) { + var warn = document.createElement('div'); + warn.className = 'alert alert-warning'; + warn.style.cssText = 'margin:.75rem 0 0'; + warn.innerHTML = '⚠ ' + data.file_errors.length + ' file(s) skipped:'; + document.getElementById('ai-output').prepend(warn); + } } } catch(e) { document.getElementById('ai-output').innerHTML = '
Request failed: ' + escHtml(String(e)) + '
'; } finally { + clearInterval(_elapsedTimer); btn.disabled = false; btn.textContent = '🔍 Analyze with AI'; } diff --git a/templates/bid_tracker.html b/templates/bid_tracker.html index f2b4093..cd3801f 100644 --- a/templates/bid_tracker.html +++ b/templates/bid_tracker.html @@ -91,7 +91,7 @@
- +
@@ -481,6 +481,9 @@ function esc(str) { /* ── Init ───────────────────────────────────────────────── */ loadBids(); + +/* Set today as the minimum selectable due date for new bids */ +document.getElementById('add-bid-due-date').min = new Date().toISOString().slice(0, 10);