Compare commits
7 changed files with 13 additions and 138 deletions
|
|
@ -80,7 +80,6 @@
|
||||||
<div class="card wide"><h3>turn duration (ms) — p50 / p95 / avg</h3><div class="chart-wrap"><canvas id="chart-duration"></canvas></div></div>
|
<div class="card wide"><h3>turn duration (ms) — p50 / p95 / avg</h3><div class="chart-wrap"><canvas id="chart-duration"></canvas></div></div>
|
||||||
<div class="card wide"><h3>context tokens (last inference per turn) — avg / max</h3><div class="chart-wrap"><canvas id="chart-ctx"></canvas></div></div>
|
<div class="card wide"><h3>context tokens (last inference per turn) — avg / max</h3><div class="chart-wrap"><canvas id="chart-ctx"></canvas></div></div>
|
||||||
<div class="card wide"><h3>token cost per bucket (sum across inferences)</h3><div class="chart-wrap"><canvas id="chart-cost"></canvas></div></div>
|
<div class="card wide"><h3>token cost per bucket (sum across inferences)</h3><div class="chart-wrap"><canvas id="chart-cost"></canvas></div></div>
|
||||||
<div class="card wide"><h3>turns by model per bucket — model drives token cost</h3><div class="chart-wrap"><canvas id="chart-model"></canvas></div></div>
|
|
||||||
<div class="card"><h3>top tools</h3><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
|
<div class="card"><h3>top tools</h3><div class="chart-wrap"><canvas id="chart-tools"></canvas></div></div>
|
||||||
<div class="card"><h3>wake source mix</h3><div class="chart-wrap"><canvas id="chart-wake"></canvas></div></div>
|
<div class="card"><h3>wake source mix</h3><div class="chart-wrap"><canvas id="chart-wake"></canvas></div></div>
|
||||||
<div class="card"><h3>result mix</h3><div class="chart-wrap"><canvas id="chart-result"></canvas></div></div>
|
<div class="card"><h3>result mix</h3><div class="chart-wrap"><canvas id="chart-result"></canvas></div></div>
|
||||||
|
|
|
||||||
|
|
@ -226,34 +226,6 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderModelChart(s) {
|
|
||||||
const id = 'chart-model';
|
|
||||||
destroy(id);
|
|
||||||
const models = s.models || [];
|
|
||||||
if (!models.length) { paintEmpty(id, 'no turns in window'); return; }
|
|
||||||
const labels = s.buckets.map((b) => bucketLabel(b.ts, s.bucket_seconds));
|
|
||||||
// One stacked series per model. Model choice drives token cost,
|
|
||||||
// so this lines up against the cost chart above it.
|
|
||||||
const datasets = models.map((m, i) => ({
|
|
||||||
label: m,
|
|
||||||
data: s.buckets.map((b) => (b.model_counts && b.model_counts[m]) || 0),
|
|
||||||
backgroundColor: wheel[i % wheel.length],
|
|
||||||
}));
|
|
||||||
charts[id] = new Chart(document.getElementById(id), {
|
|
||||||
type: 'bar',
|
|
||||||
data: { labels, datasets },
|
|
||||||
options: {
|
|
||||||
responsive: true, maintainAspectRatio: false,
|
|
||||||
plugins: { legend: { position: 'top', labels: { boxWidth: 12 } } },
|
|
||||||
scales: {
|
|
||||||
x: { stacked: true, grid: { color: palette.border } },
|
|
||||||
y: { stacked: true, beginAtZero: true,
|
|
||||||
grid: { color: palette.border }, ticks: { precision: 0 } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderKeyCount(canvasId, items, emptyMsg) {
|
function renderKeyCount(canvasId, items, emptyMsg) {
|
||||||
destroy(canvasId);
|
destroy(canvasId);
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
|
|
@ -280,7 +252,6 @@
|
||||||
paintEmpty('chart-duration', 'no turns in window');
|
paintEmpty('chart-duration', 'no turns in window');
|
||||||
paintEmpty('chart-ctx', 'no turns in window');
|
paintEmpty('chart-ctx', 'no turns in window');
|
||||||
paintEmpty('chart-cost', 'no turns in window');
|
paintEmpty('chart-cost', 'no turns in window');
|
||||||
paintEmpty('chart-model', 'no turns in window');
|
|
||||||
paintEmpty('chart-tools', 'no tool calls');
|
paintEmpty('chart-tools', 'no tool calls');
|
||||||
paintEmpty('chart-wake', 'no wakes');
|
paintEmpty('chart-wake', 'no wakes');
|
||||||
paintEmpty('chart-result', 'no results');
|
paintEmpty('chart-result', 'no results');
|
||||||
|
|
@ -290,7 +261,6 @@
|
||||||
renderDurationChart(s);
|
renderDurationChart(s);
|
||||||
renderCtxChart(s);
|
renderCtxChart(s);
|
||||||
renderCostChart(s);
|
renderCostChart(s);
|
||||||
renderModelChart(s);
|
|
||||||
renderKeyCount('chart-tools', s.tool_breakdown, 'no tool calls');
|
renderKeyCount('chart-tools', s.tool_breakdown, 'no tool calls');
|
||||||
renderKeyCount('chart-wake', s.wake_mix, 'no wakes');
|
renderKeyCount('chart-wake', s.wake_mix, 'no wakes');
|
||||||
renderKeyCount('chart-result', s.result_mix, 'no results');
|
renderKeyCount('chart-result', s.result_mix, 'no results');
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//! propagating — the stats page is decorative, not authoritative, and
|
//! propagating — the stats page is decorative, not authoritative, and
|
||||||
//! a missing db on a brand-new agent shouldn't 500 the route.
|
//! a missing db on a brand-new agent shouldn't 500 the route.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
@ -73,10 +73,6 @@ pub struct Snapshot {
|
||||||
pub tool_breakdown: Vec<KeyCount>,
|
pub tool_breakdown: Vec<KeyCount>,
|
||||||
pub wake_mix: Vec<KeyCount>,
|
pub wake_mix: Vec<KeyCount>,
|
||||||
pub result_mix: Vec<KeyCount>,
|
pub result_mix: Vec<KeyCount>,
|
||||||
/// Distinct models seen in the window, sorted. Each bucket's
|
|
||||||
/// `model_counts` keys into this set; the stats page uses it as
|
|
||||||
/// the stacked-bar series list (stable order + colours).
|
|
||||||
pub models: Vec<String>,
|
|
||||||
/// Across-window p50 / p95 / avg of `duration_ms`. Same numbers
|
/// Across-window p50 / p95 / avg of `duration_ms`. Same numbers
|
||||||
/// as the per-bucket fields but aggregated over the whole window
|
/// as the per-bucket fields but aggregated over the whole window
|
||||||
/// for the headline summary chips.
|
/// for the headline summary chips.
|
||||||
|
|
@ -101,10 +97,6 @@ pub struct Bucket {
|
||||||
/// size at turn-end — useful for spotting drift toward compaction).
|
/// size at turn-end — useful for spotting drift toward compaction).
|
||||||
pub avg_ctx_tokens: f64,
|
pub avg_ctx_tokens: f64,
|
||||||
pub max_ctx_tokens: u64,
|
pub max_ctx_tokens: u64,
|
||||||
/// Turn count per model in this bucket. Model choice greatly
|
|
||||||
/// affects token cost, so this lets the operator line model usage
|
|
||||||
/// up against the cost series over time.
|
|
||||||
pub model_counts: HashMap<String, u64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
|
@ -150,7 +142,6 @@ fn empty_snapshot(window: Window) -> Snapshot {
|
||||||
tool_breakdown: Vec::new(),
|
tool_breakdown: Vec::new(),
|
||||||
wake_mix: Vec::new(),
|
wake_mix: Vec::new(),
|
||||||
result_mix: Vec::new(),
|
result_mix: Vec::new(),
|
||||||
models: Vec::new(),
|
|
||||||
duration_summary: DurationSummary::default(),
|
duration_summary: DurationSummary::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +162,7 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
||||||
cache_read_input_tokens, cache_creation_input_tokens,
|
cache_read_input_tokens, cache_creation_input_tokens,
|
||||||
last_input_tokens,
|
last_input_tokens,
|
||||||
tool_call_breakdown_json,
|
tool_call_breakdown_json,
|
||||||
wake_from, result_kind, model
|
wake_from, result_kind
|
||||||
FROM turn_stats
|
FROM turn_stats
|
||||||
WHERE started_at >= ?1
|
WHERE started_at >= ?1
|
||||||
ORDER BY started_at ASC",
|
ORDER BY started_at ASC",
|
||||||
|
|
@ -188,7 +179,6 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
||||||
tool_breakdown_json: row.get::<_, Option<String>>(7)?,
|
tool_breakdown_json: row.get::<_, Option<String>>(7)?,
|
||||||
wake_from: row.get::<_, String>(8)?,
|
wake_from: row.get::<_, String>(8)?,
|
||||||
result_kind: row.get::<_, String>(9)?,
|
result_kind: row.get::<_, String>(9)?,
|
||||||
model: row.get::<_, String>(10)?,
|
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
@ -196,7 +186,6 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
||||||
let mut tool_totals: HashMap<String, u64> = HashMap::new();
|
let mut tool_totals: HashMap<String, u64> = HashMap::new();
|
||||||
let mut wake_totals: HashMap<String, u64> = HashMap::new();
|
let mut wake_totals: HashMap<String, u64> = HashMap::new();
|
||||||
let mut result_totals: HashMap<String, u64> = HashMap::new();
|
let mut result_totals: HashMap<String, u64> = HashMap::new();
|
||||||
let mut model_set: HashSet<String> = HashSet::new();
|
|
||||||
let mut all_durations: Vec<i64> = Vec::new();
|
let mut all_durations: Vec<i64> = Vec::new();
|
||||||
let mut turn_count: u64 = 0;
|
let mut turn_count: u64 = 0;
|
||||||
|
|
||||||
|
|
@ -217,12 +206,10 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
||||||
.saturating_add(r.cache_creation_input_tokens);
|
.saturating_add(r.cache_creation_input_tokens);
|
||||||
acc.ctx_sum = acc.ctx_sum.saturating_add(r.last_input_tokens);
|
acc.ctx_sum = acc.ctx_sum.saturating_add(r.last_input_tokens);
|
||||||
acc.ctx_max = acc.ctx_max.max(r.last_input_tokens);
|
acc.ctx_max = acc.ctx_max.max(r.last_input_tokens);
|
||||||
*acc.model_counts.entry(r.model.clone()).or_insert(0) += 1;
|
|
||||||
|
|
||||||
all_durations.push(r.duration_ms.max(0));
|
all_durations.push(r.duration_ms.max(0));
|
||||||
*wake_totals.entry(r.wake_from).or_insert(0) += 1;
|
*wake_totals.entry(r.wake_from).or_insert(0) += 1;
|
||||||
*result_totals.entry(r.result_kind).or_insert(0) += 1;
|
*result_totals.entry(r.result_kind).or_insert(0) += 1;
|
||||||
model_set.insert(r.model);
|
|
||||||
|
|
||||||
if let Some(json) = r.tool_breakdown_json
|
if let Some(json) = r.tool_breakdown_json
|
||||||
&& let Ok(map) = serde_json::from_str::<HashMap<String, u64>>(&json)
|
&& let Ok(map) = serde_json::from_str::<HashMap<String, u64>>(&json)
|
||||||
|
|
@ -235,8 +222,6 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
||||||
|
|
||||||
let buckets = fill_buckets(from, now, bucket_secs, &by_bucket);
|
let buckets = fill_buckets(from, now, bucket_secs, &by_bucket);
|
||||||
let duration_summary = summarize_durations(&mut all_durations);
|
let duration_summary = summarize_durations(&mut all_durations);
|
||||||
let mut models: Vec<String> = model_set.into_iter().collect();
|
|
||||||
models.sort_unstable();
|
|
||||||
|
|
||||||
Ok(Snapshot {
|
Ok(Snapshot {
|
||||||
window: window.label(),
|
window: window.label(),
|
||||||
|
|
@ -248,7 +233,6 @@ fn snapshot(path: &Path, window: Window) -> Result<Snapshot> {
|
||||||
tool_breakdown: top_n(tool_totals, 10),
|
tool_breakdown: top_n(tool_totals, 10),
|
||||||
wake_mix: top_n(wake_totals, 20),
|
wake_mix: top_n(wake_totals, 20),
|
||||||
result_mix: top_n(result_totals, 20),
|
result_mix: top_n(result_totals, 20),
|
||||||
models,
|
|
||||||
duration_summary,
|
duration_summary,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +248,6 @@ struct Row {
|
||||||
tool_breakdown_json: Option<String>,
|
tool_breakdown_json: Option<String>,
|
||||||
wake_from: String,
|
wake_from: String,
|
||||||
result_kind: String,
|
result_kind: String,
|
||||||
model: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -277,7 +260,6 @@ struct BucketAcc {
|
||||||
cache_creation_input_tokens: u64,
|
cache_creation_input_tokens: u64,
|
||||||
ctx_sum: u64,
|
ctx_sum: u64,
|
||||||
ctx_max: u64,
|
ctx_max: u64,
|
||||||
model_counts: HashMap<String, u64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_buckets(
|
fn fill_buckets(
|
||||||
|
|
@ -325,7 +307,6 @@ fn fill_buckets(
|
||||||
cache_creation_input_tokens: acc.cache_creation_input_tokens,
|
cache_creation_input_tokens: acc.cache_creation_input_tokens,
|
||||||
avg_ctx_tokens: avg_ctx,
|
avg_ctx_tokens: avg_ctx,
|
||||||
max_ctx_tokens: acc.ctx_max,
|
max_ctx_tokens: acc.ctx_max,
|
||||||
model_counts: acc.model_counts.clone(),
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Bucket {
|
Bucket {
|
||||||
|
|
@ -340,7 +321,6 @@ fn fill_buckets(
|
||||||
cache_creation_input_tokens: 0,
|
cache_creation_input_tokens: 0,
|
||||||
avg_ctx_tokens: 0.0,
|
avg_ctx_tokens: 0.0,
|
||||||
max_ctx_tokens: 0,
|
max_ctx_tokens: 0,
|
||||||
model_counts: HashMap::new(),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
out.push(bucket);
|
out.push(bucket);
|
||||||
|
|
@ -412,7 +392,7 @@ mod tests {
|
||||||
std::env::temp_dir().join(format!("hyperhive-stats-test-{pid}-{n}.sqlite"))
|
std::env::temp_dir().join(format!("hyperhive-stats-test-{pid}-{n}.sqlite"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seed_db(path: &Path, rows: &[(i64, i64, &str, &str, &str, &str)]) {
|
fn seed_db(path: &Path, rows: &[(i64, i64, &str, &str, &str)]) {
|
||||||
let conn = Connection::open(path).unwrap();
|
let conn = Connection::open(path).unwrap();
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"CREATE TABLE turn_stats (
|
"CREATE TABLE turn_stats (
|
||||||
|
|
@ -439,13 +419,13 @@ mod tests {
|
||||||
);",
|
);",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for (started, dur, model, wake, result, tools_json) in rows {
|
for (started, dur, wake, result, tools_json) in rows {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO turn_stats
|
"INSERT INTO turn_stats
|
||||||
(started_at, ended_at, duration_ms, model, wake_from,
|
(started_at, ended_at, duration_ms, model, wake_from,
|
||||||
last_input_tokens, tool_call_breakdown_json, result_kind)
|
last_input_tokens, tool_call_breakdown_json, result_kind)
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, 1000, ?6, ?7)",
|
VALUES (?1, ?2, ?3, 'm', ?4, 1000, ?5, ?6)",
|
||||||
params![started, started + dur / 1000, dur, model, wake, tools_json, result],
|
params![started, started + dur / 1000, dur, wake, tools_json, result],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -459,9 +439,9 @@ mod tests {
|
||||||
seed_db(
|
seed_db(
|
||||||
&db,
|
&db,
|
||||||
&[
|
&[
|
||||||
(now - 600, 5_000, "opus", "recv", "ok", r#"{"Read":2,"Bash":1}"#),
|
(now - 600, 5_000, "recv", "ok", r#"{"Read":2,"Bash":1}"#),
|
||||||
(now - 300, 10_000, "opus", "recv", "ok", r#"{"Read":3}"#),
|
(now - 300, 10_000, "recv", "ok", r#"{"Read":3}"#),
|
||||||
(now - 100, 20_000, "sonnet", "operator", "failed", "{}"),
|
(now - 100, 20_000, "operator", "failed", "{}"),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
let s = snapshot(&db, Window::Day).unwrap();
|
let s = snapshot(&db, Window::Day).unwrap();
|
||||||
|
|
@ -489,17 +469,6 @@ mod tests {
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(result_map.get("ok").copied(), Some(2));
|
assert_eq!(result_map.get("ok").copied(), Some(2));
|
||||||
assert_eq!(result_map.get("failed").copied(), Some(1));
|
assert_eq!(result_map.get("failed").copied(), Some(1));
|
||||||
// Model breakdown: 2 opus + 1 sonnet, all in the same hour
|
|
||||||
// bucket given the 24h window.
|
|
||||||
assert_eq!(s.models, vec!["opus".to_string(), "sonnet".to_string()]);
|
|
||||||
let mut model_totals: HashMap<String, u64> = HashMap::new();
|
|
||||||
for b in &s.buckets {
|
|
||||||
for (k, v) in &b.model_counts {
|
|
||||||
*model_totals.entry(k.clone()).or_insert(0) += v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(model_totals.get("opus").copied(), Some(2));
|
|
||||||
assert_eq!(model_totals.get("sonnet").copied(), Some(1));
|
|
||||||
// Durations: [5000, 10000, 20000] → avg ≈ 11666.67, p50 = 10000, p95 ~ 20000
|
// Durations: [5000, 10000, 20000] → avg ≈ 11666.67, p50 = 10000, p95 ~ 20000
|
||||||
assert!((s.duration_summary.avg_ms - 11_666.666_666_666_666).abs() < 1.0);
|
assert!((s.duration_summary.avg_ms - 11_666.666_666_666_666).abs() < 1.0);
|
||||||
assert!((s.duration_summary.p50_ms - 10_000.0).abs() < 1.0);
|
assert!((s.duration_summary.p50_ms - 10_000.0).abs() < 1.0);
|
||||||
|
|
|
||||||
|
|
@ -85,25 +85,13 @@
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
// Lazy-load `path` from /api/state-file into the side panel.
|
// Lazy-load `path` from /api/state-file into the side panel.
|
||||||
// Markdown files render through `marked` into a `.md` block; every
|
|
||||||
// other file stays raw text in a <pre>.
|
|
||||||
async function openFilePanel(path) {
|
async function openFilePanel(path) {
|
||||||
const isMd = /\.(md|markdown)$/i.test(path);
|
const pre = el('pre', { class: 'path-preview-body' }, '(fetching…)');
|
||||||
const view = isMd
|
Panel.open('↳ ' + path, pre);
|
||||||
? el('div', { class: 'md' })
|
|
||||||
: el('pre', { class: 'path-preview-body' });
|
|
||||||
view.textContent = '(fetching…)';
|
|
||||||
Panel.open('↳ ' + path, view);
|
|
||||||
try {
|
try {
|
||||||
const text = await fetchStateFile(path);
|
pre.textContent = await fetchStateFile(path);
|
||||||
if (isMd && window.marked && typeof window.marked.parse === 'function') {
|
|
||||||
marked.setOptions({ breaks: true, gfm: true });
|
|
||||||
view.innerHTML = marked.parse(text);
|
|
||||||
} else {
|
|
||||||
view.textContent = text;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
view.textContent = 'error: ' + (e.message || e);
|
pre.textContent = 'error: ' + (e.message || e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function makePathLink(path) {
|
function makePathLink(path) {
|
||||||
|
|
|
||||||
|
|
@ -805,43 +805,3 @@ footer a { color: var(--purple); }
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
/* Markdown file previews rendered by `marked`. TERMINAL_CSS scopes
|
|
||||||
its own `.md` rules to `.live .row`, so the panel needs its own. */
|
|
||||||
.side-panel-body .md { color: var(--fg); line-height: 1.5; }
|
|
||||||
.side-panel-body .md > :first-child { margin-top: 0; }
|
|
||||||
.side-panel-body .md > :last-child { margin-bottom: 0; }
|
|
||||||
.side-panel-body .md p { margin: 0.5em 0; }
|
|
||||||
.side-panel-body .md h1,
|
|
||||||
.side-panel-body .md h2,
|
|
||||||
.side-panel-body .md h3,
|
|
||||||
.side-panel-body .md h4 { color: var(--purple); margin: 0.9em 0 0.4em; }
|
|
||||||
.side-panel-body .md code {
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0.05em 0.3em;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
.side-panel-body .md pre {
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 0.6em 0.8em;
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
.side-panel-body .md pre code { background: none; border: none; padding: 0; }
|
|
||||||
.side-panel-body .md a { color: var(--cyan); }
|
|
||||||
.side-panel-body .md ul,
|
|
||||||
.side-panel-body .md ol { margin: 0.4em 0; padding-left: 1.5em; }
|
|
||||||
.side-panel-body .md blockquote {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding-left: 0.8em;
|
|
||||||
border-left: 2px solid var(--border);
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
.side-panel-body .md table { border-collapse: collapse; margin: 0.5em 0; }
|
|
||||||
.side-panel-body .md th,
|
|
||||||
.side-panel-body .md td {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 0.2em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/hive-fr0nt.js" defer></script>
|
<script src="/static/hive-fr0nt.js" defer></script>
|
||||||
<script src="/static/marked.js" defer></script>
|
|
||||||
<script src="/static/app.js" defer></script>
|
<script src="/static/app.js" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
||||||
.route("/dashboard/stream", get(dashboard_stream))
|
.route("/dashboard/stream", get(dashboard_stream))
|
||||||
.route("/dashboard/history", get(dashboard_history))
|
.route("/dashboard/history", get(dashboard_history))
|
||||||
.route("/static/hive-fr0nt.js", get(serve_shared_js))
|
.route("/static/hive-fr0nt.js", get(serve_shared_js))
|
||||||
.route("/static/marked.js", get(serve_marked_js))
|
|
||||||
.with_state(AppState { coord });
|
.with_state(AppState { coord });
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||||
let listener = bind_with_retry(addr).await?;
|
let listener = bind_with_retry(addr).await?;
|
||||||
|
|
@ -147,15 +146,6 @@ async fn serve_shared_js() -> impl IntoResponse {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vendored `marked` bundle — the side panel renders markdown file
|
|
||||||
/// previews with it.
|
|
||||||
async fn serve_marked_js() -> impl IntoResponse {
|
|
||||||
(
|
|
||||||
[("content-type", "application/javascript")],
|
|
||||||
hive_fr0nt::MARKED_JS,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct StateSnapshot {
|
struct StateSnapshot {
|
||||||
/// Broker seq at the moment this snapshot was assembled. Clients
|
/// Broker seq at the moment this snapshot was assembled. Clients
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue