Add settings-configurable cap on logs kept in memory (ticket 026)

Settings.max_logs (default None = unlimited) is pushed to the engine
via a new EngineMessage::SetMaxLogs, enforced on every ingest and
whenever the setting changes at runtime (no restart required).
Eviction drops the oldest (lowest-sequence) logs; sequence numbering
isn't renumbered and buckets already derived from an evicted log are
left in place. Exposed via PATCH /api/settings (max_logs: number |
null) and a NavMenu numeric field (blank = unlimited).

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-05 15:12:12 +01:00
co-authored by Claude Sonnet 5
parent aaa5596030
commit 1d0f324c43
8 changed files with 261 additions and 16 deletions
+22 -3
View File
@@ -194,6 +194,8 @@ impl From<&CssEntry> for CssEntryDto {
pub struct SettingsDto {
pub color_scheme: ColorScheme,
pub active_profile: Option<String>,
/// `None` means unlimited (ticket 026).
pub max_logs: Option<usize>,
}
impl From<&Settings> for SettingsDto {
@@ -201,16 +203,33 @@ impl From<&Settings> for SettingsDto {
SettingsDto {
color_scheme: settings.color_scheme,
active_profile: settings.profiles.active_profile.clone(),
max_logs: settings.max_logs,
}
}
}
/// Request body for `PATCH /api/settings`. Only `color_scheme` is
/// updatable for now; `None` leaves the current value untouched (so a
/// partial body doesn't reset other fields).
/// Request body for `PATCH /api/settings`. `color_scheme`: `None` leaves the
/// current value untouched. `max_logs`: distinguishes "field omitted" (leave
/// unchanged, outer `None`) from "field present" (inner `Option<usize>`
/// applied verbatim — `null` clears the cap back to unlimited, a number sets
/// it), via `deserialize_double_option`.
#[derive(Debug, Deserialize)]
pub struct UpdateSettingsRequest {
pub color_scheme: Option<ColorScheme>,
#[serde(default, deserialize_with = "deserialize_double_option")]
pub max_logs: Option<Option<usize>>,
}
/// Distinguishes a missing JSON field (outer `None`, via `#[serde(default)]`
/// short-circuiting before this runs) from a present `null` (`Some(None)`)
/// or present value (`Some(Some(value))`) — plain `Option<Option<T>>` can't
/// tell "absent" and "explicit null" apart on its own.
fn deserialize_double_option<'de, D, T>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(deserializer).map(Some)
}
/// Summary of one profile for `GET /api/settings/profiles` (ticket 011):