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));
+}