Update src/api/admin.py
Browse files- src/api/admin.py +43 -38
src/api/admin.py
CHANGED
|
@@ -21,16 +21,38 @@ from src.config import get_settings
|
|
| 21 |
from src.services.runtime_store import JsonRuntimeStore
|
| 22 |
|
| 23 |
router = APIRouter()
|
| 24 |
-
admin_sessions: Dict[str, float] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
-
# ββ Persistent session store (plain JSON β no external dependencies) ββββββββββ
|
| 28 |
|
| 29 |
import json as _json
|
| 30 |
import os as _os
|
| 31 |
|
| 32 |
def _sessions_file() -> str:
|
| 33 |
-
"""Return path for persistent sessions file."""
|
| 34 |
cfg_path = _os.environ.get("CONFIG_STORE_PATH", "")
|
| 35 |
if cfg_path and "/data" in cfg_path:
|
| 36 |
return _os.path.join(_os.path.dirname(cfg_path), "admin_sessions.json")
|
|
@@ -38,7 +60,6 @@ def _sessions_file() -> str:
|
|
| 38 |
|
| 39 |
|
| 40 |
def _load_sessions() -> None:
|
| 41 |
-
"""Load persisted sessions into in-memory cache."""
|
| 42 |
global admin_sessions
|
| 43 |
try:
|
| 44 |
with open(_sessions_file(), "r", encoding="utf-8") as _f:
|
|
@@ -48,11 +69,10 @@ def _load_sessions() -> None:
|
|
| 48 |
if isinstance(exp, (int, float)) and exp > now}
|
| 49 |
admin_sessions.update(valid)
|
| 50 |
except Exception:
|
| 51 |
-
pass
|
| 52 |
|
| 53 |
|
| 54 |
def _save_sessions() -> None:
|
| 55 |
-
"""Persist valid sessions to disk."""
|
| 56 |
try:
|
| 57 |
now = time.time()
|
| 58 |
valid = {t: exp for t, exp in admin_sessions.items() if exp > now}
|
|
@@ -62,19 +82,6 @@ def _save_sessions() -> None:
|
|
| 62 |
pass
|
| 63 |
|
| 64 |
|
| 65 |
-
# ββ Session helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 66 |
-
|
| 67 |
-
def _cleanup_sessions() -> None:
|
| 68 |
-
now = time.time()
|
| 69 |
-
expired = [t for t, exp in list(admin_sessions.items()) if exp <= now]
|
| 70 |
-
for t in expired:
|
| 71 |
-
admin_sessions.pop(t, None)
|
| 72 |
-
try:
|
| 73 |
-
_load_sessions()
|
| 74 |
-
except Exception:
|
| 75 |
-
pass
|
| 76 |
-
|
| 77 |
-
|
| 78 |
def _require_session(request: Request) -> None:
|
| 79 |
_cleanup_sessions()
|
| 80 |
token = request.cookies.get("admin_session")
|
|
@@ -294,10 +301,11 @@ tr:last-child td{{border-bottom:none}}
|
|
| 294 |
<div id="metricsTable" style="overflow-x:auto"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
| 295 |
</div>
|
| 296 |
|
|
|
|
| 297 |
<div class="card">
|
| 298 |
<h2>π Cloudflare Analytics (Persistent)</h2>
|
| 299 |
-
<div class="stat-grid" id="cfStatsGrid"><div style="color:#6b7280;font-size:.84rem">Loading
|
| 300 |
-
<div style="margin-top:14px" id="cfToolsTable"><
|
| 301 |
</div>
|
| 302 |
|
| 303 |
<div class="card">
|
|
@@ -305,7 +313,6 @@ tr:last-child td{{border-bottom:none}}
|
|
| 305 |
<div id="recentTable" style="overflow-x:auto"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
| 306 |
</div>
|
| 307 |
|
| 308 |
-
|
| 309 |
<div class="card">
|
| 310 |
<h2>π System Status</h2>
|
| 311 |
<div id="sysStatus"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
|
@@ -325,7 +332,7 @@ async function loadStats(){{
|
|
| 325 |
<div class="stat"><div class="val">${{d.follow_up_hits||0}}</div><div class="lbl">Follow-ups (session)</div></div>
|
| 326 |
<div class="stat"><div class="val">${{d.feedback_total||0}}</div><div class="lbl">Feedback (session)</div></div>
|
| 327 |
`;
|
| 328 |
-
}}catch(e){{document.getElementById('statsGrid').innerHTML='<span style="color:#dc2626">
|
| 329 |
}}
|
| 330 |
|
| 331 |
async function loadCFAnalytics(){{
|
|
@@ -343,10 +350,9 @@ async function loadCFAnalytics(){{
|
|
| 343 |
const tools=(d.tools||[]).slice(0,15);
|
| 344 |
let rows=tools.map(t=>`<tr><td>${{t.tool_used||"unknown"}}</td><td>${{t.c}}</td></tr>`).join('');
|
| 345 |
document.getElementById('cfToolsTable').innerHTML=rows
|
| 346 |
-
?`<table><thead><tr><th>Intent:Tool</th><th>Count</th></tr></thead><tbody>${{rows}}</tbody></table>`
|
| 347 |
-
:'<span style="color:#6b7280;font-size:.84rem">No data yet.</span>';
|
| 348 |
}}catch(e){{
|
| 349 |
-
document.getElementById('cfStatsGrid').innerHTML='<span style="color:#dc2626">
|
| 350 |
}}
|
| 351 |
}}
|
| 352 |
|
|
@@ -357,17 +363,17 @@ async function loadRecentQueries(){{
|
|
| 357 |
if(d.error) throw new Error(d.error);
|
| 358 |
const rows=(d.results||[]).map(q=>`<tr>
|
| 359 |
<td style="white-space:nowrap;font-size:.74rem">${{q.timestamp||""}}</td>
|
| 360 |
-
<td style="max-width:
|
| 361 |
<td>${{q.tool_used||""}}</td>
|
| 362 |
<td>${{q.model||""}}</td>
|
| 363 |
-
<td
|
| 364 |
<td>${{q.result_count||0}}</td>
|
| 365 |
</tr>`).join('');
|
| 366 |
document.getElementById('recentTable').innerHTML=rows
|
| 367 |
-
?`<table><thead><tr><th>Time</th><th>Question</th><th>Tool</th><th>Model</th><th>Time</th><th>
|
| 368 |
-
:'<span style="color:#6b7280;font-size:.84rem">No queries
|
| 369 |
}}catch(e){{
|
| 370 |
-
document.getElementById('recentTable').innerHTML='<span style="color:#dc2626">
|
| 371 |
}}
|
| 372 |
}}
|
| 373 |
|
|
@@ -383,8 +389,8 @@ async function loadMetrics(){{
|
|
| 383 |
.map(([k,v])=>`<tr><td style="color:#dc2626">${{k}}</td><td>${{v}}</td><td style="color:#6b7280">error</td></tr>`).join('');
|
| 384 |
document.getElementById('metricsTable').innerHTML=rows
|
| 385 |
?`<table><thead><tr><th>Key</th><th>Count</th><th>Type</th></tr></thead><tbody>${{rows}}</tbody></table>`
|
| 386 |
-
:'<span style="color:#6b7280;font-size:.84rem">No session data yet
|
| 387 |
-
}}catch(e){{document.getElementById('metricsTable').innerHTML='<span style="color:#dc2626">Could not load
|
| 388 |
}}
|
| 389 |
|
| 390 |
async function loadRAGInfo(){{
|
|
@@ -444,9 +450,9 @@ loadRecentQueries();
|
|
| 444 |
loadMetrics();
|
| 445 |
loadRAGInfo();
|
| 446 |
loadStatus();
|
| 447 |
-
setInterval(loadStatus,
|
| 448 |
-
setInterval(loadCFAnalytics,
|
| 449 |
-
setInterval(loadRecentQueries,
|
| 450 |
</script>
|
| 451 |
</body></html>""")
|
| 452 |
|
|
@@ -455,7 +461,7 @@ setInterval(loadRecentQueries, 60000);
|
|
| 455 |
|
| 456 |
@router.get("/public-config")
|
| 457 |
async def public_config():
|
| 458 |
-
"""
|
| 459 |
try:
|
| 460 |
cfg = _load_config()
|
| 461 |
except Exception:
|
|
@@ -468,7 +474,6 @@ async def public_config():
|
|
| 468 |
}
|
| 469 |
|
| 470 |
|
| 471 |
-
|
| 472 |
@router.get("/config")
|
| 473 |
async def get_config(request: Request):
|
| 474 |
_require_session(request)
|
|
|
|
| 21 |
from src.services.runtime_store import JsonRuntimeStore
|
| 22 |
|
| 23 |
router = APIRouter()
|
| 24 |
+
admin_sessions: Dict[str, float] = {}
|
| 25 |
+
|
| 26 |
+
_START_TIME = time.time()
|
| 27 |
+
|
| 28 |
+
_CONFIG_DEFAULTS: Dict[str, Any] = {
|
| 29 |
+
"max_results": 5,
|
| 30 |
+
"maintenance_mode": False,
|
| 31 |
+
"welcome_message": "Hi! I'm LibBee, the Khalifa University Library AI Assistant.",
|
| 32 |
+
"custom_instructions": "",
|
| 33 |
+
"announcement": "",
|
| 34 |
+
"maintenance_message": "LibBee is currently under maintenance. Please try again shortly.",
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ββ Session helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
|
| 40 |
+
def _cleanup_sessions() -> None:
|
| 41 |
+
now = time.time()
|
| 42 |
+
expired = [t for t, exp in admin_sessions.items() if exp <= now]
|
| 43 |
+
for t in expired:
|
| 44 |
+
admin_sessions.pop(t, None)
|
| 45 |
+
try:
|
| 46 |
+
_load_sessions()
|
| 47 |
+
except Exception:
|
| 48 |
+
pass
|
| 49 |
|
| 50 |
|
|
|
|
| 51 |
|
| 52 |
import json as _json
|
| 53 |
import os as _os
|
| 54 |
|
| 55 |
def _sessions_file() -> str:
|
|
|
|
| 56 |
cfg_path = _os.environ.get("CONFIG_STORE_PATH", "")
|
| 57 |
if cfg_path and "/data" in cfg_path:
|
| 58 |
return _os.path.join(_os.path.dirname(cfg_path), "admin_sessions.json")
|
|
|
|
| 60 |
|
| 61 |
|
| 62 |
def _load_sessions() -> None:
|
|
|
|
| 63 |
global admin_sessions
|
| 64 |
try:
|
| 65 |
with open(_sessions_file(), "r", encoding="utf-8") as _f:
|
|
|
|
| 69 |
if isinstance(exp, (int, float)) and exp > now}
|
| 70 |
admin_sessions.update(valid)
|
| 71 |
except Exception:
|
| 72 |
+
pass
|
| 73 |
|
| 74 |
|
| 75 |
def _save_sessions() -> None:
|
|
|
|
| 76 |
try:
|
| 77 |
now = time.time()
|
| 78 |
valid = {t: exp for t, exp in admin_sessions.items() if exp > now}
|
|
|
|
| 82 |
pass
|
| 83 |
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def _require_session(request: Request) -> None:
|
| 86 |
_cleanup_sessions()
|
| 87 |
token = request.cookies.get("admin_session")
|
|
|
|
| 301 |
<div id="metricsTable" style="overflow-x:auto"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
| 302 |
</div>
|
| 303 |
|
| 304 |
+
|
| 305 |
<div class="card">
|
| 306 |
<h2>π Cloudflare Analytics (Persistent)</h2>
|
| 307 |
+
<div class="stat-grid" id="cfStatsGrid"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
| 308 |
+
<div style="margin-top:14px" id="cfToolsTable"></div>
|
| 309 |
</div>
|
| 310 |
|
| 311 |
<div class="card">
|
|
|
|
| 313 |
<div id="recentTable" style="overflow-x:auto"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
| 314 |
</div>
|
| 315 |
|
|
|
|
| 316 |
<div class="card">
|
| 317 |
<h2>π System Status</h2>
|
| 318 |
<div id="sysStatus"><div style="color:#6b7280;font-size:.84rem">Loadingβ¦</div></div>
|
|
|
|
| 332 |
<div class="stat"><div class="val">${{d.follow_up_hits||0}}</div><div class="lbl">Follow-ups (session)</div></div>
|
| 333 |
<div class="stat"><div class="val">${{d.feedback_total||0}}</div><div class="lbl">Feedback (session)</div></div>
|
| 334 |
`;
|
| 335 |
+
}}catch(e){{document.getElementById('statsGrid').innerHTML='<span style="color:#dc2626">Unavailable</span>';}}
|
| 336 |
}}
|
| 337 |
|
| 338 |
async function loadCFAnalytics(){{
|
|
|
|
| 350 |
const tools=(d.tools||[]).slice(0,15);
|
| 351 |
let rows=tools.map(t=>`<tr><td>${{t.tool_used||"unknown"}}</td><td>${{t.c}}</td></tr>`).join('');
|
| 352 |
document.getElementById('cfToolsTable').innerHTML=rows
|
| 353 |
+
?`<table><thead><tr><th>Intent:Tool</th><th>Count</th></tr></thead><tbody>${{rows}}</tbody></table>`:'';
|
|
|
|
| 354 |
}}catch(e){{
|
| 355 |
+
document.getElementById('cfStatsGrid').innerHTML='<span style="color:#dc2626">CF D1 unavailable: '+e.message+'</span>';
|
| 356 |
}}
|
| 357 |
}}
|
| 358 |
|
|
|
|
| 363 |
if(d.error) throw new Error(d.error);
|
| 364 |
const rows=(d.results||[]).map(q=>`<tr>
|
| 365 |
<td style="white-space:nowrap;font-size:.74rem">${{q.timestamp||""}}</td>
|
| 366 |
+
<td style="max-width:260px;word-break:break-word">${{(q.question||"").substring(0,100)}}</td>
|
| 367 |
<td>${{q.tool_used||""}}</td>
|
| 368 |
<td>${{q.model||""}}</td>
|
| 369 |
+
<td>${{(q.response_time||0).toFixed(2)}}s</td>
|
| 370 |
<td>${{q.result_count||0}}</td>
|
| 371 |
</tr>`).join('');
|
| 372 |
document.getElementById('recentTable').innerHTML=rows
|
| 373 |
+
?`<table><thead><tr><th>Time</th><th>Question</th><th>Tool</th><th>Model</th><th>Time</th><th>#</th></tr></thead><tbody>${{rows}}</tbody></table>`
|
| 374 |
+
:'<span style="color:#6b7280;font-size:.84rem">No queries yet.</span>';
|
| 375 |
}}catch(e){{
|
| 376 |
+
document.getElementById('recentTable').innerHTML='<span style="color:#dc2626">'+e.message+'</span>';
|
| 377 |
}}
|
| 378 |
}}
|
| 379 |
|
|
|
|
| 389 |
.map(([k,v])=>`<tr><td style="color:#dc2626">${{k}}</td><td>${{v}}</td><td style="color:#6b7280">error</td></tr>`).join('');
|
| 390 |
document.getElementById('metricsTable').innerHTML=rows
|
| 391 |
?`<table><thead><tr><th>Key</th><th>Count</th><th>Type</th></tr></thead><tbody>${{rows}}</tbody></table>`
|
| 392 |
+
:'<span style="color:#6b7280;font-size:.84rem">No session data yet.</span>';
|
| 393 |
+
}}catch(e){{document.getElementById('metricsTable').innerHTML='<span style="color:#dc2626">Could not load</span>';}}
|
| 394 |
}}
|
| 395 |
|
| 396 |
async function loadRAGInfo(){{
|
|
|
|
| 450 |
loadMetrics();
|
| 451 |
loadRAGInfo();
|
| 452 |
loadStatus();
|
| 453 |
+
setInterval(loadStatus,30000);
|
| 454 |
+
setInterval(loadCFAnalytics,60000);
|
| 455 |
+
setInterval(loadRecentQueries,60000);
|
| 456 |
</script>
|
| 457 |
</body></html>""")
|
| 458 |
|
|
|
|
| 461 |
|
| 462 |
@router.get("/public-config")
|
| 463 |
async def public_config():
|
| 464 |
+
"""No auth β returns public fields for the chat frontend."""
|
| 465 |
try:
|
| 466 |
cfg = _load_config()
|
| 467 |
except Exception:
|
|
|
|
| 474 |
}
|
| 475 |
|
| 476 |
|
|
|
|
| 477 |
@router.get("/config")
|
| 478 |
async def get_config(request: Request):
|
| 479 |
_require_session(request)
|