Update src/api/admin.py
Browse files- src/api/admin.py +35 -12
src/api/admin.py
CHANGED
|
@@ -266,6 +266,15 @@ async def admin_dashboard(request: Request):
|
|
| 266 |
mm_on = "selected" if cfg.get("maintenance_mode") else ""
|
| 267 |
mm_off = "" if cfg.get("maintenance_mode") else "selected"
|
| 268 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
return HTMLResponse(f"""<!DOCTYPE html>
|
| 270 |
<html><head><meta charset="utf-8"><title>KU Library AI — Admin</title>
|
| 271 |
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -357,8 +366,26 @@ tr:last-child td{{border-bottom:none}}
|
|
| 357 |
|
| 358 |
</div>
|
| 359 |
<script>
|
| 360 |
-
|
| 361 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
async function loadStats(){{
|
| 364 |
try{{
|
|
@@ -375,10 +402,8 @@ async function loadStats(){{
|
|
| 375 |
|
| 376 |
async function loadCFAnalytics(){{
|
| 377 |
try{{
|
| 378 |
-
const
|
| 379 |
-
const d=
|
| 380 |
-
if(!r.ok||d.detail) throw new Error('HTTP '+r.status+' '+(d.detail||''));
|
| 381 |
-
if(d.error) throw new Error(d.error.message||JSON.stringify(d.error));
|
| 382 |
document.getElementById('cfStatsGrid').innerHTML=`
|
| 383 |
<div class="stat"><div class="val">${{d.total||0}}</div><div class="lbl">Total Queries</div></div>
|
| 384 |
<div class="stat"><div class="val">${{d.today||0}}</div><div class="lbl">Today</div></div>
|
|
@@ -397,10 +422,8 @@ async function loadCFAnalytics(){{
|
|
| 397 |
|
| 398 |
async function loadRecentQueries(){{
|
| 399 |
try{{
|
| 400 |
-
const
|
| 401 |
-
const d=
|
| 402 |
-
if(!r.ok||d.detail) throw new Error('HTTP '+r.status+' '+(d.detail||''));
|
| 403 |
-
if(d.error) throw new Error(d.error.message||JSON.stringify(d.error));
|
| 404 |
if(d.error) throw new Error(d.error);
|
| 405 |
const rows=(d.results||[]).map(q=>`<tr>
|
| 406 |
<td style="white-space:nowrap;font-size:.74rem">${{q.timestamp||""}}</td>
|
|
@@ -698,7 +721,7 @@ async def _worker_get(path: str, params: Optional[dict] = None):
|
|
| 698 |
if settings.cloudflare_worker_token:
|
| 699 |
headers["Authorization"] = f"Bearer {settings.cloudflare_worker_token}"
|
| 700 |
try:
|
| 701 |
-
async with _mk(timeout=
|
| 702 |
r = await client.get(f"{worker}{path}", params=params or {}, headers=headers)
|
| 703 |
return r.json()
|
| 704 |
except HTTPException:
|
|
@@ -756,7 +779,7 @@ async def admin_worker_health(request: Request):
|
|
| 756 |
"""One probe, its own short timeout, never raises."""
|
| 757 |
entry = {"path": path}
|
| 758 |
try:
|
| 759 |
-
async with _mk(timeout=
|
| 760 |
r = await client.get(f"{worker}{path}",
|
| 761 |
headers=headers if use_auth else {})
|
| 762 |
entry["status"] = r.status_code
|
|
|
|
| 266 |
mm_on = "selected" if cfg.get("maintenance_mode") else ""
|
| 267 |
mm_off = "" if cfg.get("maintenance_mode") else "selected"
|
| 268 |
|
| 269 |
+
# Worker credentials injected into this page only. The dashboard tries the
|
| 270 |
+
# server-side proxy first and falls back to a direct browser -> Worker call,
|
| 271 |
+
# because some hosting environments (HF Spaces) cannot reach workers.dev
|
| 272 |
+
# while the admin's own browser can. This page is served exclusively to an
|
| 273 |
+
# authenticated admin session, so the token is not publicly exposed.
|
| 274 |
+
_s = get_settings()
|
| 275 |
+
cf_url = (_s.cloudflare_worker_url or "").strip().strip('"').strip("'").rstrip("/")
|
| 276 |
+
cf_token = (_s.cloudflare_worker_token or "").strip()
|
| 277 |
+
|
| 278 |
return HTMLResponse(f"""<!DOCTYPE html>
|
| 279 |
<html><head><meta charset="utf-8"><title>KU Library AI — Admin</title>
|
| 280 |
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
|
|
| 366 |
|
| 367 |
</div>
|
| 368 |
<script>
|
| 369 |
+
const CF_URL = "{cf_url}";
|
| 370 |
+
const CF_TOKEN = "{cf_token}";
|
| 371 |
+
|
| 372 |
+
// Two-path analytics fetch. The backend proxy keeps credentials server-side and
|
| 373 |
+
// is tried first; if the backend cannot reach the Worker (a known limitation on
|
| 374 |
+
// some hosts) we retry directly from this browser, which can.
|
| 375 |
+
async function cfFetch(path){{
|
| 376 |
+
try{{
|
| 377 |
+
const r = await fetch('/admin'+path);
|
| 378 |
+
const d = await r.json();
|
| 379 |
+
if(r.ok && !d.detail && !d.error) return {{data:d, via:'backend proxy'}};
|
| 380 |
+
}}catch(e){{ /* fall through to direct */ }}
|
| 381 |
+
if(!CF_URL) throw new Error('CLOUDFLARE_WORKER_URL not configured');
|
| 382 |
+
const h = CF_TOKEN ? {{'Authorization':'Bearer '+CF_TOKEN}} : {{}};
|
| 383 |
+
const r2 = await fetch(CF_URL+path, {{headers:h}});
|
| 384 |
+
const d2 = await r2.json();
|
| 385 |
+
if(!r2.ok) throw new Error('Worker HTTP '+r2.status+' '+(d2.error&&d2.error.message||''));
|
| 386 |
+
if(d2.error) throw new Error(d2.error.message||'Worker error');
|
| 387 |
+
return {{data:d2, via:'direct'}};
|
| 388 |
+
}}
|
| 389 |
|
| 390 |
async function loadStats(){{
|
| 391 |
try{{
|
|
|
|
| 402 |
|
| 403 |
async function loadCFAnalytics(){{
|
| 404 |
try{{
|
| 405 |
+
const res=await cfFetch('/analytics');
|
| 406 |
+
const d=res.data;
|
|
|
|
|
|
|
| 407 |
document.getElementById('cfStatsGrid').innerHTML=`
|
| 408 |
<div class="stat"><div class="val">${{d.total||0}}</div><div class="lbl">Total Queries</div></div>
|
| 409 |
<div class="stat"><div class="val">${{d.today||0}}</div><div class="lbl">Today</div></div>
|
|
|
|
| 422 |
|
| 423 |
async function loadRecentQueries(){{
|
| 424 |
try{{
|
| 425 |
+
const res=await cfFetch('/analytics/recent');
|
| 426 |
+
const d=res.data;
|
|
|
|
|
|
|
| 427 |
if(d.error) throw new Error(d.error);
|
| 428 |
const rows=(d.results||[]).map(q=>`<tr>
|
| 429 |
<td style="white-space:nowrap;font-size:.74rem">${{q.timestamp||""}}</td>
|
|
|
|
| 721 |
if settings.cloudflare_worker_token:
|
| 722 |
headers["Authorization"] = f"Bearer {settings.cloudflare_worker_token}"
|
| 723 |
try:
|
| 724 |
+
async with _mk(timeout=25) as client:
|
| 725 |
r = await client.get(f"{worker}{path}", params=params or {}, headers=headers)
|
| 726 |
return r.json()
|
| 727 |
except HTTPException:
|
|
|
|
| 779 |
"""One probe, its own short timeout, never raises."""
|
| 780 |
entry = {"path": path}
|
| 781 |
try:
|
| 782 |
+
async with _mk(timeout=20) as client:
|
| 783 |
r = await client.get(f"{worker}{path}",
|
| 784 |
headers=headers if use_auth else {})
|
| 785 |
entry["status"] = r.status_code
|