feat: show token usage and cost tracker in gateway status popover (#284)

* feat: show token usage, cost tracker, and uptime in gateway status popover

The "Connected" hover popover in the web gateway now displays three
sections: connection info (SSE/WS counts, uptime), daily cost tracker
(spend + actions/hr), and per-model token usage (input/output counts
with cost per model). Also fixes the field name mismatch between the
backend response and JS rendering that prevented the popover from
showing correct data.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: address PR review — escape HTML in popover, add model_usage test

- Escape model name and cost strings with escapeHtml() before inserting
  into innerHTML to prevent XSS via crafted model names
- Add test_model_usage_per_model_tracking test covering multi-model
  token/cost accumulation in CostGuard

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-21 03:45:04 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 493e4578d0
commit b68d67bd35
9 changed files with 241 additions and 14 deletions
+9 -6
View File
@@ -1319,6 +1319,14 @@ async fn main() -> anyhow::Result<()> {
(None, None)
};
// Create cost guard early so gateway can reference it.
let cost_guard = Arc::new(ironclaw::agent::cost_guard::CostGuard::new(
ironclaw::agent::cost_guard::CostGuardConfig {
max_cost_per_day_cents: config.agent.max_cost_per_day_cents,
max_actions_per_hour: config.agent.max_actions_per_hour,
},
));
// Add web gateway channel if configured
let mut gateway_url: Option<String> = None;
if let Some(ref gw_config) = config.channels.gateway {
@@ -1345,6 +1353,7 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref sc) = skill_catalog {
gw = gw.with_skill_catalog(Arc::clone(sc));
}
gw = gw.with_cost_guard(Arc::clone(&cost_guard));
if config.sandbox.enabled {
gw = gw.with_prompt_queue(Arc::clone(&prompt_queue));
@@ -1379,12 +1388,6 @@ async fn main() -> anyhow::Result<()> {
let boot_cheap_model = cheap_llm.as_ref().map(|c| c.model_name().to_string());
// Create and run the agent
let cost_guard = Arc::new(ironclaw::agent::cost_guard::CostGuard::new(
ironclaw::agent::cost_guard::CostGuardConfig {
max_cost_per_day_cents: config.agent.max_cost_per_day_cents,
max_actions_per_hour: config.agent.max_actions_per_hour,
},
));
let deps = AgentDeps {
store: db,
llm,