feat(#1187): dynamic terminal scrollback with paginated history

This commit is contained in:
iris 2026-06-03 20:49:30 +02:00 committed by mara
commit 62841a549d
4 changed files with 227 additions and 26 deletions

View file

@ -717,15 +717,41 @@ async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) ->
}
}
async fn events_history(State(state): State<AppState>) -> axum::Json<serde_json::Value> {
// Capture seq *before* the read so dedupe is "drop buffered events
// you've already seen in history", never "lose an event that fired
// between the read and the timestamp." Historical rows have no
// per-row seq; only the high-water mark matters for the dedupe
// window.
let seq = state.bus.current_seq();
let events = state.bus.history();
axum::Json(serde_json::json!({ "seq": seq, "events": events }))
/// Query params for the paginated history endpoint.
#[derive(Debug, Deserialize)]
struct HistoryParams {
/// Cursor: only return events with sqlite row id < `before`.
/// Omit for the initial (most-recent) page.
before: Option<i64>,
/// Page size (default 100, capped at HISTORY_CAPACITY).
limit: Option<usize>,
}
async fn events_history(
State(state): State<AppState>,
axum::extract::Query(params): axum::extract::Query<HistoryParams>,
) -> axum::Json<serde_json::Value> {
use crate::events::HISTORY_CAPACITY;
let limit = params.limit.unwrap_or(100).min(HISTORY_CAPACITY);
let before = params.before;
let is_initial = before.is_none();
// Capture seq *before* the read on initial loads so the SSE dedupe
// window is "drop buffered events you've already seen in history",
// never "lose an event that fired between the read and the seq."
// On paginated loads (`before` is set) seq is not needed.
let seq = if is_initial { Some(state.bus.current_seq()) } else { None };
let (events, min_id, has_more) = state.bus.history_page(before, limit);
let mut resp = serde_json::json!({
"events": events,
"min_id": min_id,
"has_more": has_more,
});
if let Some(s) = seq {
resp["seq"] = serde_json::json!(s);
}
axum::Json(resp)
}
async fn events_stream(