Add sort_by ordering for the log viewer, with a missing-param fallback/drop toggle (ticket 045)

/api/logs now accepts sort_by (an already-declared param name) and
sort_missing_fallback: lines carrying that param are ordered by its value,
while lines missing it are either appended in ottyl_seq order (fallback,
the default) or dropped from the response. Default ottyl_ts-tracking
order is unchanged when sort_by is omitted.

The log viewer (both the inline panel and its popout) exposes this as a
"Sort by" control next to the column picker, persisted to localStorage
under one shared key (ottyl.logPreview.sort) — the same global-viewer-
setting convention already used for visible param columns and the log
viewer's height, since there's no per-dashboard scope for this to divide
by.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-06 01:58:30 +01:00
co-authored by Claude Sonnet 5
parent 70d98562d1
commit 3d2652e95b
7 changed files with 381 additions and 20 deletions
+48
View File
@@ -1527,4 +1527,52 @@ mod tests {
let (_, body) = get_json(&app, "/api/logs").await;
assert_eq!(body.as_array().unwrap().len(), 3);
}
/// Ticket 045: seeded fixture mixing lines that carry the sort param with lines that don't,
/// exercised through the actual HTTP endpoint rather than just `handle_query` directly.
#[tokio::test]
async fn get_logs_sort_by_orders_by_param_and_toggles_missing_param_handling() {
let (app, engine, _settings, _dir) = test_app();
declare_test_param(&app, "priority", r"priority=(?P<priority>\d+)", "priority", "number").await;
ingest_lines(
&engine,
0,
&[
"priority=3 boom", // sequence 0, has param
"no param here", // sequence 1, missing
"priority=1 boom", // sequence 2, has param
"also missing", // sequence 3, missing
],
)
.await;
// Default order (no sort_by): unchanged, plain ingest/ottyl_seq order.
let (status, body) = get_json(&app, "/api/logs").await;
assert_eq!(status, StatusCode::OK);
let sequences: Vec<u64> = body.as_array().unwrap().iter().map(|e| e["sequence"].as_u64().unwrap()).collect();
assert_eq!(sequences, vec![0, 1, 2, 3]);
// sort_by=priority with the fallback toggle on (the default): entries carrying the param
// sorted ascending by its value, then the missing-param entries ordered by ottyl_seq.
let (status, body) = get_json(&app, "/api/logs?sort_by=priority").await;
assert_eq!(status, StatusCode::OK);
let sequences: Vec<u64> = body.as_array().unwrap().iter().map(|e| e["sequence"].as_u64().unwrap()).collect();
assert_eq!(sequences, vec![2, 0, 1, 3]);
// Same sort_by, but with the fallback toggle explicitly off: missing-param lines are dropped.
let (status, body) = get_json(&app, "/api/logs?sort_by=priority&sort_missing_fallback=false").await;
assert_eq!(status, StatusCode::OK);
let sequences: Vec<u64> = body.as_array().unwrap().iter().map(|e| e["sequence"].as_u64().unwrap()).collect();
assert_eq!(sequences, vec![2, 0]);
}
#[tokio::test]
async fn get_logs_sort_by_unknown_param_returns_400() {
let (app, _engine, _settings, _dir) = test_app();
let (status, body) = get_json(&app, "/api/logs?sort_by=does_not_exist").await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(body["error"].as_str().unwrap().contains("does_not_exist"));
}
}