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="3d">last 3d</button>
|
||||||
<button type="button" data-tab="7d">last 7d</button>
|
<button type="button" data-tab="7d">last 7d</button>
|
||||||
<button type="button" data-tab="30d">last 30d</button>
|
<button type="button" data-tab="30d">last 30d</button>
|
||||||
|
<button type="button" data-tab="all">all</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary" id="summary"></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="3d">3d</button>
|
||||||
<button type="button" class="btn" data-tab="7d">7d</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="30d">30d</button>
|
||||||
|
<button type="button" class="btn" data-tab="all">all</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="hive-stats-chips" id="hive-stats-summary"></div>
|
<div class="hive-stats-chips" id="hive-stats-summary"></div>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ pub enum Window {
|
||||||
ThreeDay,
|
ThreeDay,
|
||||||
Week,
|
Week,
|
||||||
Month,
|
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 {
|
impl Window {
|
||||||
|
|
@ -37,6 +41,7 @@ impl Window {
|
||||||
"3d" => Self::ThreeDay,
|
"3d" => Self::ThreeDay,
|
||||||
"7d" => Self::Week,
|
"7d" => Self::Week,
|
||||||
"30d" => Self::Month,
|
"30d" => Self::Month,
|
||||||
|
"all" => Self::All,
|
||||||
_ => Self::Day,
|
_ => Self::Day,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -49,6 +54,7 @@ impl Window {
|
||||||
Self::ThreeDay => "3d",
|
Self::ThreeDay => "3d",
|
||||||
Self::Week => "7d",
|
Self::Week => "7d",
|
||||||
Self::Month => "30d",
|
Self::Month => "30d",
|
||||||
|
Self::All => "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,21 +67,42 @@ impl Window {
|
||||||
Self::ThreeDay => 3 * 24 * 3600,
|
Self::ThreeDay => 3 * 24 * 3600,
|
||||||
Self::Week => 7 * 24 * 3600,
|
Self::Week => 7 * 24 * 3600,
|
||||||
Self::Month => 30 * 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 {
|
fn bucket_secs(self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
// 5-min buckets for 1h (12 buckets), 15-min for 4h (16 buckets),
|
// 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::Hour => 300,
|
||||||
Self::FourHour => 900,
|
Self::FourHour => 900,
|
||||||
Self::Day | Self::ThreeDay => 3600,
|
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)]
|
#[derive(Debug, Serialize)]
|
||||||
pub struct Snapshot {
|
pub struct Snapshot {
|
||||||
pub window: &'static str,
|
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)
|
let conn = Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY)
|
||||||
.with_context(|| format!("open {} read-only", path.display()))?;
|
.with_context(|| format!("open {} read-only", path.display()))?;
|
||||||
let now = now_secs();
|
let now = now_secs();
|
||||||
let from = now - window.span_secs();
|
// Fixed windows look back a constant span; `all` starts at the earliest
|
||||||
let bucket_secs = window.bucket_secs();
|
// 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(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT started_at, duration_ms,
|
"SELECT started_at, duration_ms,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ pub enum Window {
|
||||||
ThreeDay,
|
ThreeDay,
|
||||||
Week,
|
Week,
|
||||||
Month,
|
Month,
|
||||||
|
/// All available data: aggregate over every recorded turn since the
|
||||||
|
/// earliest (`from == 0`), with no fixed lookback.
|
||||||
|
All,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
|
|
@ -46,6 +49,7 @@ impl Window {
|
||||||
"3d" => Self::ThreeDay,
|
"3d" => Self::ThreeDay,
|
||||||
"7d" => Self::Week,
|
"7d" => Self::Week,
|
||||||
"30d" => Self::Month,
|
"30d" => Self::Month,
|
||||||
|
"all" => Self::All,
|
||||||
_ => Self::Day,
|
_ => Self::Day,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +62,7 @@ impl Window {
|
||||||
Self::ThreeDay => "3d",
|
Self::ThreeDay => "3d",
|
||||||
Self::Week => "7d",
|
Self::Week => "7d",
|
||||||
Self::Month => "30d",
|
Self::Month => "30d",
|
||||||
|
Self::All => "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,6 +74,9 @@ impl Window {
|
||||||
Self::ThreeDay => 3 * 24 * 3600,
|
Self::ThreeDay => 3 * 24 * 3600,
|
||||||
Self::Week => 7 * 24 * 3600,
|
Self::Week => 7 * 24 * 3600,
|
||||||
Self::Month => 30 * 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]
|
#[must_use]
|
||||||
pub fn hive_snapshot(window: Window, prices: &PriceTable) -> HiveStats {
|
pub fn hive_snapshot(window: Window, prices: &PriceTable) -> HiveStats {
|
||||||
let now = now_secs();
|
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 agents: Vec<AgentRollup> = Vec::new();
|
||||||
let mut model_mix: HashMap<String, u64> = HashMap::new();
|
let mut model_mix: HashMap<String, u64> = HashMap::new();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue