diff --git a/crates/proxy/admin-ui/index.html b/crates/proxy/admin-ui/index.html index f75da81..4d1eb5f 100644 --- a/crates/proxy/admin-ui/index.html +++ b/crates/proxy/admin-ui/index.html @@ -1078,6 +1078,7 @@ input:focus,select:focus{outline:none;border-color:#4a9eff} requests.forEach(function(r) { feed.appendChild(makeRequestRow(r)); }); } document.getElementById('page-info').textContent = 'Page ' + (currentPage + 1); + document.getElementById('next-page').disabled = !data.has_more; }).catch(function(e) { console.error('Requests load failed:', e); }); } @@ -1682,6 +1683,7 @@ input:focus,select:focus{outline:none;border-color:#4a9eff} clearChildren(container, false); var entries = data.entries || []; document.getElementById('audit-page-info').textContent = 'Page ' + (auditPage + 1); + document.getElementById('audit-next-page').disabled = !data.has_more; if (entries.length === 0) { var auditMsg = auditPage === 0 ? 'No audit entries yet.' : 'No results on this page'; container.appendChild(el('div', {className: 'empty', textContent: auditMsg})); diff --git a/crates/proxy/src/admin/routes.rs b/crates/proxy/src/admin/routes.rs index 6498d25..042c820 100644 --- a/crates/proxy/src/admin/routes.rs +++ b/crates/proxy/src/admin/routes.rs @@ -1046,7 +1046,7 @@ async fn get_requests( match crate::admin::state::with_db(&shared.db, move |conn| { crate::admin::db::query_request_log( conn, - limit, + limit + 1, offset, backend.as_deref(), since.as_deref(), @@ -1057,11 +1057,18 @@ async fn get_requests( }) .await { - Some(Ok(entries)) => Json(serde_json::json!({ - "requests": entries, - "limit": limit, - "offset": offset, - })), + Some(Ok(mut entries)) => { + let has_more = entries.len() > limit as usize; + if has_more { + entries.truncate(limit as usize); + } + Json(serde_json::json!({ + "requests": entries, + "limit": limit, + "offset": offset, + "has_more": has_more, + })) + } Some(Err(e)) => { tracing::error!(error = %e, "query_request_log failed"); Json(serde_json::json!({ @@ -1696,7 +1703,7 @@ async fn get_audit_log( match crate::admin::state::with_db(&shared.db, move |conn| { crate::admin::db::query_audit_log( conn, - limit, + limit + 1, offset, action.as_deref(), target_type.as_deref(), @@ -1706,11 +1713,18 @@ async fn get_audit_log( }) .await { - Some(Ok(entries)) => Json(serde_json::json!({ - "entries": entries, - "limit": limit, - "offset": offset, - })), + Some(Ok(mut entries)) => { + let has_more = entries.len() > limit as usize; + if has_more { + entries.truncate(limit as usize); + } + Json(serde_json::json!({ + "entries": entries, + "limit": limit, + "offset": offset, + "has_more": has_more, + })) + } Some(Err(e)) => { tracing::error!(error = %e, "query_audit_log failed"); Json(serde_json::json!({ diff --git a/crates/proxy/tests/virtual_keys.rs b/crates/proxy/tests/virtual_keys.rs index 8f549b5..a29f6af 100644 --- a/crates/proxy/tests/virtual_keys.rs +++ b/crates/proxy/tests/virtual_keys.rs @@ -1095,3 +1095,55 @@ async fn add_model_rejects_unknown_backend() { body ); } + +// --------------------------------------------------------------------------- +// Pagination has_more field tests +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn get_requests_response_has_has_more_field() { + let (app, _state) = test_admin_router(); + let req = Request::get("/admin/api/requests?limit=10&offset=0") + .header("host", "localhost:9090") + .header("authorization", "Bearer test-admin-token") + .body(Body::empty()) + .unwrap(); + + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), 200); + let body: serde_json::Value = serde_json::from_slice( + &axum::body::to_bytes(resp.into_body(), 1 << 20) + .await + .unwrap(), + ) + .unwrap(); + assert!( + body.get("has_more").is_some(), + "response should include has_more field" + ); + assert_eq!(body["has_more"], serde_json::Value::Bool(false)); +} + +#[tokio::test] +async fn get_audit_response_has_has_more_field() { + let (app, _state) = test_admin_router(); + let req = Request::get("/admin/api/audit?limit=10&offset=0") + .header("host", "localhost:9090") + .header("authorization", "Bearer test-admin-token") + .body(Body::empty()) + .unwrap(); + + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), 200); + let body: serde_json::Value = serde_json::from_slice( + &axum::body::to_bytes(resp.into_body(), 1 << 20) + .await + .unwrap(), + ) + .unwrap(); + assert!( + body.get("has_more").is_some(), + "response should include has_more field" + ); + assert_eq!(body["has_more"], serde_json::Value::Bool(false)); +}