feat(stats): add an 'all' time range to agent + hive stats
Adds a Window::All option to the per-agent stats page and the hive-wide rollup, selectable as a new 'all' tab on both. - hive-ag3nt (per-agent, time-bucketed): All ranges from MIN(started_at) to now (fallback to now on an empty table) with an adaptive bucket width laddered by span — hourly <=2d, daily <=90d, weekly <=2y, 30-day beyond — so the trend series stays bounded (~<=104 buckets) at any age. - hive-c0re (swarm rollup, not time-bucketed): All sets from=0 so the aggregate covers every recorded turn across all agents. - frontend: an 'all' button on both the agent stats and dashboard hive stats window selectors (createTabStrip + fetch already pass the window string through, so no JS change needed). cargo check passes on both crates; FE builds clean. Fixes #1919.
This commit is contained in:
parent
d4dd5aad3e
commit
684c85686e
4 changed files with 60 additions and 5 deletions
|
|
@ -23,6 +23,7 @@
|
|||
<button type="button" data-tab="3d">last 3d</button>
|
||||
<button type="button" data-tab="7d">last 7d</button>
|
||||
<button type="button" data-tab="30d">last 30d</button>
|
||||
<button type="button" data-tab="all">all</button>
|
||||
</div>
|
||||
|
||||
<div class="summary" id="summary"></div>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@
|
|||
<button type="button" class="btn" data-tab="3d">3d</button>
|
||||
<button type="button" class="btn" data-tab="7d">7d</button>
|
||||
<button type="button" class="btn" data-tab="30d">30d</button>
|
||||
<button type="button" class="btn" data-tab="all">all</button>
|
||||
</nav>
|
||||
|
||||
<div class="hive-stats-chips" id="hive-stats-summary"></div>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ pub enum Window {
|
|||
ThreeDay,
|
||||
Week,
|
||||
Month,
|
||||
/// All available data: the range starts at the earliest recorded turn
|
||||
/// (`MIN(started_at)`) rather than a fixed lookback, with an adaptive
|
||||
/// bucket width so the trend series stays bounded at any span.
|
||||
All,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -37,6 +41,7 @@ impl Window {
|
|||
"3d" => Self::ThreeDay,
|
||||
"7d" => Self::Week,
|
||||
"30d" => Self::Month,
|
||||
"all" => Self::All,
|
||||
_ => Self::Day,
|
||||
}
|
||||
}
|
||||
|
|
@ -49,6 +54,7 @@ impl Window {
|
|||
Self::ThreeDay => "3d",
|
||||
Self::Week => "7d",
|
||||
Self::Month => "30d",
|
||||
Self::All => "all",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,21 +67,42 @@ impl Window {
|
|||
Self::ThreeDay => 3 * 24 * 3600,
|
||||
Self::Week => 7 * 24 * 3600,
|
||||
Self::Month => 30 * 24 * 3600,
|
||||
// `All` has no fixed lookback — its range is computed from
|
||||
// `MIN(started_at)` in `snapshot()`. 0 is only a safe fallback
|
||||
// (→ `from == now` → empty range) if ever reached generically.
|
||||
Self::All => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn bucket_secs(self) -> i64 {
|
||||
match self {
|
||||
// 5-min buckets for 1h (12 buckets), 15-min for 4h (16 buckets),
|
||||
// hourly for 24h + 3d, daily for 7d + 30d.
|
||||
// hourly for 24h + 3d, daily for 7d + 30d. `All` shares the daily
|
||||
// fallback, but its real bucket width is chosen adaptively in
|
||||
// `snapshot()` via `adaptive_bucket_secs` — this arm is only
|
||||
// reached defensively.
|
||||
Self::Hour => 300,
|
||||
Self::FourHour => 900,
|
||||
Self::Day | Self::ThreeDay => 3600,
|
||||
Self::Week | Self::Month => 24 * 3600,
|
||||
Self::Week | Self::Month | Self::All => 24 * 3600,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bucket width for the unbounded `all` window, laddered by the actual
|
||||
/// span so the trend series stays bounded (≈ ≤100 buckets) at any range:
|
||||
/// hourly ≤ 2d, daily ≤ 90d, weekly ≤ 2y, ~monthly (30d) beyond.
|
||||
fn adaptive_bucket_secs(span_secs: i64) -> i64 {
|
||||
const HOUR: i64 = 3600;
|
||||
const DAY: i64 = 24 * HOUR;
|
||||
match span_secs {
|
||||
s if s <= 2 * DAY => HOUR,
|
||||
s if s <= 90 * DAY => DAY,
|
||||
s if s <= 730 * DAY => 7 * DAY,
|
||||
_ => 30 * DAY,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Snapshot {
|
||||
pub window: &'static str,
|
||||
|
|
@ -208,8 +235,20 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
|||
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)
|
||||
.with_context(|| format!("open {} read-only", path.display()))?;
|
||||
let now = now_secs();
|
||||
let from = now - window.span_secs();
|
||||
let bucket_secs = window.bucket_secs();
|
||||
// Fixed windows look back a constant span; `all` starts at the earliest
|
||||
// recorded turn (`MIN(started_at)`, falling back to `now` on an empty
|
||||
// table) and sizes its buckets adaptively from that span.
|
||||
let (from, bucket_secs) = match window {
|
||||
Window::All => {
|
||||
let min_ts: Option<i64> =
|
||||
conn.query_row("SELECT MIN(started_at) FROM turn_stats", [], |row| {
|
||||
row.get(0)
|
||||
})?;
|
||||
let from = min_ts.unwrap_or(now);
|
||||
(from, adaptive_bucket_secs(now - from))
|
||||
}
|
||||
_ => (now - window.span_secs(), window.bucket_secs()),
|
||||
};
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT started_at, duration_ms,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ pub enum Window {
|
|||
ThreeDay,
|
||||
Week,
|
||||
Month,
|
||||
/// All available data: aggregate over every recorded turn since the
|
||||
/// earliest (`from == 0`), with no fixed lookback.
|
||||
All,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -46,6 +49,7 @@ impl Window {
|
|||
"3d" => Self::ThreeDay,
|
||||
"7d" => Self::Week,
|
||||
"30d" => Self::Month,
|
||||
"all" => Self::All,
|
||||
_ => Self::Day,
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +62,7 @@ impl Window {
|
|||
Self::ThreeDay => "3d",
|
||||
Self::Week => "7d",
|
||||
Self::Month => "30d",
|
||||
Self::All => "all",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +74,9 @@ impl Window {
|
|||
Self::ThreeDay => 3 * 24 * 3600,
|
||||
Self::Week => 7 * 24 * 3600,
|
||||
Self::Month => 30 * 24 * 3600,
|
||||
// `All` has no fixed lookback; `hive_snapshot` uses `from == 0`
|
||||
// (every recorded turn). 0 here is a consistent safe fallback.
|
||||
Self::All => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -314,7 +322,13 @@ fn read_bash_heads(conn: &Connection, from: i64) -> HashMap<String, u64> {
|
|||
#[must_use]
|
||||
pub fn hive_snapshot(window: Window, prices: &PriceTable) -> HiveStats {
|
||||
let now = now_secs();
|
||||
let from = now - window.span_secs();
|
||||
// Fixed windows look back a constant span; `all` aggregates every
|
||||
// recorded turn (`from == 0`). The hive rollup isn't time-bucketed, so
|
||||
// unlike the per-agent snapshot it needs no adaptive bucket sizing.
|
||||
let from = match window {
|
||||
Window::All => 0,
|
||||
_ => now - window.span_secs(),
|
||||
};
|
||||
|
||||
let mut agents: Vec<AgentRollup> = Vec::new();
|
||||
let mut model_mix: HashMap<String, u64> = HashMap::new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue