use boom_core::provider::KeyAliasLookup;
use boom_core::DebugErrorStore;
use boom_flowcontrol::FlowController;
use boom_limiter::{PlanStore, SlidingWindowLimiter};
use boom_promptlog::PromptLogConfig;
use boom_promptlog::PromptLogQueryApi;
use boom_routing::{AliasStore, DeploymentStore, InFlightTracker, RebalanceMoveTracker, RequestRateTracker};
use boom_ctxaware::AgentStatsTracker;
use dashmap::DashMap;
use serde_json::Value;
use sqlx::PgPool;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{mpsc, oneshot};
use uuid::Uuid;
use crate::handlers_admin::CreateDeploymentRequest;
/// Tracks login failure state per IP for brute-force protection.
#[derive(Debug)]
pub struct LoginAttempt {
pub fail_count: u32,
pub locked_until: Option<Instant>,
}
// ═══════════════════════════════════════════════════════════
// Admin Command channel (write operations → boom-main)
// ═══════════════════════════════════════════════════════════
/// Commands sent from dashboard to boom-main for state-mutating operations.
/// Model CRUD requires boom-provider + boom-config, which dashboard must not depend on.
pub enum AdminCommand {
CreateModel {
req: CreateDeploymentRequest,
reply: oneshot::Sender<Result<Value, String>>,
},
UpdateModel {
id: Uuid,
req: CreateDeploymentRequest,
reply: oneshot::Sender<Result<Value, String>>,
},
DeleteModel {
id: Uuid,
reply: oneshot::Sender<Result<Value, String>>,
},
/// Fire-and-forget: config changed, persist to YAML in place.
ConfigChanged,
/// Hot-reload config.yaml. Reply contains summary message.
ReloadConfig {
reply: oneshot::Sender<Result<String, String>>,
},
/// Read the live in-memory config as JSON (secrets masked).
GetConfig {
reply: oneshot::Sender<Result<Value, String>>,
},
/// Read the field manifest — declarative list of which config fields
/// are editable from the dashboard UI. See boom-config `manifest` module
/// and CLAUDE.md §9. Used by `GET /admin/config/schema` and (future)
/// auto-rendering frontend code.
GetConfigSchema {
reply: oneshot::Sender<Result<Value, String>>,
},
/// Update a singleton config section (e.g., `server`, `router_settings`)
/// by replacing it wholesale in the live `config.yaml`. Path is dotted
/// (`router_settings.kvc_aware`); value is the new JSON-serializable content.
/// Triggers a reload after writing.
UpdateConfigSection {
path: String,
value: Value,
reply: oneshot::Sender<Result<String, String>>,
},
/// Hot-swap the prompt-log config at runtime (toggle on/off, change
/// exclusion lists, flip the otlp sub-config). Boom-main owns the
/// `PromptLogWriter`; dashboard must not touch the writer handle directly
/// (see CLAUDE.md §5 AdminCommand pattern).
UpdatePromptLogConfig {
config: PromptLogConfig,
reply: oneshot::Sender<Result<(), String>>,
},
/// Probe a remote OTLP/HTTP collector. The dashboard passes the live
/// endpoint/headers/timeout (read from the prompt-log card form — not the
/// committed YAML, so the operator can type a new endpoint and test it
/// before saving). Reply carries the round-trip latency in ms on success
/// or a one-line error string on failure.
PingOtlpEndpoint {
endpoint: String,
headers: std::collections::HashMap<String, String>,
timeout_secs: u64,
reply: oneshot::Sender<Result<u64, String>>,
},
/// Read the live OTLP exporter's state machine snapshot — Online/Offline
/// status, endpoint, last_failure_ts, last_recovery_ts, episode counters,
/// dropped counts. The dashboard polls this every 5s to drive the
/// connectivity indicator (green=online, red=offline, gray=disabled).
/// Returns `None` when OTLP is not configured (no exporter in the
/// ArcSwap); the dashboard treats `None` as "disabled".
GetOtlpStatus {
reply: oneshot::Sender<Option<boom_promptlog::ExporterStatusSnapshot>>,
},
/// Manually trigger a probe on the live exporter. On success transitions
/// Offline → Online; on failure records a probe failure (but does NOT
/// drive Online → Offline — only repeated flush failures do that). Used
/// by the dashboard's "Probe now" action when the operator wants to
/// attempt recovery before the next periodic tick. Returns `None` when
/// OTLP is not configured.
ProbeOtlp {
reply: oneshot::Sender<Option<boom_promptlog::ProbeResult>>,
},
}
pub type AdminTx = mpsc::Sender<AdminCommand>;
// ═══════════════════════════════════════════════════════════
// Dashboard state
// ═══════════════════════════════════════════════════════════
/// Dashboard-specific state, injected via Extension layer.
/// Independent from boom-gateway's AppState to avoid type coupling.
#[derive(Clone)]
pub struct DashboardState {
/// Dashboard-only DB pool (max=3), isolated from the forwarding path's
/// pool (max=30). Heavy stats aggregations cannot starve request forwarding.
pub db_pool: Option<PgPool>,
pub plan_store: Arc<PlanStore>,
pub limiter: Arc<SlidingWindowLimiter>,
/// Deployment store for model reads.
pub deployment_store: Arc<DeploymentStore>,
/// Alias store for alias reads.
pub alias_store: Arc<AliasStore>,
/// In-flight request tracker for real-time stats.
pub inflight: Arc<InFlightTracker>,
/// Per-deployment flow controller for real-time stats.
pub flow_controller: Arc<FlowController>,
/// Channel for model write operations (handled by boom-main).
pub admin_tx: AdminTx,
/// JWT signing key (derived from master_key at startup).
pub jwt_secret: String,
/// Master key for admin login (constant-time comparison).
pub master_key: Option<String>,
/// Login rate-limit state per client IP.
pub login_attempts: Arc<DashMap<String, LoginAttempt>>,
/// Debug error store — shared with boom-main for recording upstream errors.
pub debug_store: Arc<DebugErrorStore>,
/// Read-only prompt-log query API — entry lookup + config snapshot.
/// Boom-main owns the writer; writes go through `AdminCommand::UpdatePromptLogConfig`.
pub prompt_log_query: Arc<dyn PromptLogQueryApi>,
/// Per-deployment rebalance move tracker (in/out) for dashboard debug page.
pub rebalance_move_tracker: Arc<RebalanceMoveTracker>,
/// Per-deployment request rate tracker for dashboard stats.
pub request_rate: Arc<RequestRateTracker>,
/// Agent (client-type) statistics tracker for dashboard stats.
pub agent_stats: Arc<AgentStatsTracker>,
/// Authenticator — used for key alias lookups (reads boom_verification_token).
pub auth: Arc<dyn KeyAliasLookup>,
/// Audit-log drop counter (channel full or batch INSERT failures).
/// None when DB not configured (no LogWriter). Surfaced on the debug page.
pub log_dropped: Option<Arc<dyn boom_core::LogDroppedCounter>>,
/// Real-time pressure metrics (CPU, RSS, tokio worker queue depth,
/// blocking pool queue, inflight). Polled every 1.5s by the admin
/// stats page's top sparkline chart.
pub stressmon: Arc<dyn boom_core::StressmonApi>,
}
impl DashboardState {
pub fn new(
db_pool: Option<PgPool>,
plan_store: Arc<PlanStore>,
limiter: Arc<SlidingWindowLimiter>,
deployment_store: Arc<DeploymentStore>,
alias_store: Arc<AliasStore>,
inflight: Arc<InFlightTracker>,
flow_controller: Arc<FlowController>,
admin_tx: AdminTx,
master_key: Option<String>,
debug_store: Arc<DebugErrorStore>,
prompt_log_query: Arc<dyn PromptLogQueryApi>,
rebalance_move_tracker: Arc<RebalanceMoveTracker>,
request_rate: Arc<RequestRateTracker>,
agent_stats: Arc<AgentStatsTracker>,
auth: Arc<dyn KeyAliasLookup>,
log_dropped: Option<Arc<dyn boom_core::LogDroppedCounter>>,
stressmon: Arc<dyn boom_core::StressmonApi>,
) -> Self {
// Derive JWT secret from master_key, or use a random fallback.
let jwt_secret = master_key
.as_deref()
.unwrap_or("boom-dashboard-default-secret")
.to_string();
Self {
db_pool,
plan_store,
limiter,
deployment_store,
alias_store,
inflight,
flow_controller,
admin_tx,
jwt_secret,
master_key,
login_attempts: Arc::new(DashMap::new()),
debug_store,
prompt_log_query,
rebalance_move_tracker,
request_rate,
agent_stats,
auth,
log_dropped,
stressmon,
}
}
}