use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing::info;
use trusty_common::{init_tracing, shutdown_signal, write_daemon_addr};
pub mod bind;
pub mod connector;
pub mod detect;
pub mod mcp_handle;
pub mod metrics_poller;
pub mod poller;
pub mod proxy;
pub mod server;
#[derive(Debug, Parser)]
#[command(
name = "trusty-console",
version,
about = "Web dashboard for trusty services"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Serve(ServeArgs),
}
#[derive(Debug, Parser)]
pub struct ServeArgs {
#[arg(long, default_value = "127.0.0.1:7788")]
pub http: String,
#[arg(long, default_value_t = false)]
pub tailscale: bool,
#[arg(long, default_value_t = false)]
pub open: bool,
#[arg(long, default_value_t = 15u64)]
pub poll_interval: u64,
}
pub async fn run() -> Result<()> {
init_tracing(1);
let cli = Cli::parse();
match cli.command {
Commands::Serve(args) => run_serve(args).await,
}
}
pub async fn run_serve(args: ServeArgs) -> Result<()> {
const DEFAULT_HTTP: &str = "127.0.0.1:7788";
let mode = bind::BindMode::from_env_and_flags(&args.http, DEFAULT_HTTP, args.tailscale);
let port = bind::port_from_addr(&args.http, 7788);
let addrs = bind::resolve_bind_addrs(&mode, port, bind::detect_tailscale_ipv4);
let connectors = detect::all_connectors();
let state = server::AppState::new(connectors);
{
let cache = state.poller_cache().clone();
let c = state.connectors();
cache.poll_once(c).await;
}
poller::start(
state.poller_cache().clone(),
state.connectors(),
Duration::from_secs(args.poll_interval),
);
metrics_poller::start(
state.analyze_handle(),
state.metrics_cache().clone(),
Duration::from_secs(args.poll_interval),
);
{
let handles = state.mcp_handles();
if let Some(h) = handles.get("trusty-memory") {
metrics_poller::start(
Arc::clone(h),
state.memory_metrics_cache().clone(),
Duration::from_secs(args.poll_interval),
);
} else {
tracing::warn!(
service = "trusty-memory",
"run_serve: no MCP handle registered for trusty-memory — \
metrics poller will not start for this service"
);
}
}
{
let handles = state.mcp_handles();
if let Some(h) = handles.get("trusty-search") {
metrics_poller::start(
Arc::clone(h),
state.search_metrics_cache().clone(),
Duration::from_secs(args.poll_interval),
);
} else {
tracing::warn!(
service = "trusty-search",
"run_serve: no MCP handle registered for trusty-search — \
metrics poller will not start for this service"
);
}
}
{
let handles = state.mcp_handles();
if let Some(h) = handles.get("trusty-review") {
metrics_poller::start(
Arc::clone(h),
state.review_metrics_cache().clone(),
Duration::from_secs(args.poll_interval),
);
} else {
tracing::warn!(
service = "trusty-review",
"run_serve: no MCP handle registered for trusty-review — \
metrics poller will not start for this service"
);
}
}
let router = server::build_router(state.clone());
let primary_addr = *addrs.first().context("bind address list is empty")?;
let primary_listener = bind::bind_listener(primary_addr).await?;
let primary_local = primary_listener.local_addr().context("get local addr")?;
let addr_string = primary_local.to_string();
info!("trusty-console listening on http://{primary_local}");
for &extra_addr in addrs.get(1..).unwrap_or(&[]) {
let extra_listener = bind::bind_listener(extra_addr).await?;
let extra_local = extra_listener
.local_addr()
.context("get extra local addr")?;
info!("trusty-console also listening on http://{extra_local}");
eprintln!("trusty-console (tailnet): http://{extra_local}");
let r = router.clone();
tokio::spawn(async move {
if let Err(e) = axum::serve(extra_listener, r)
.with_graceful_shutdown(trusty_common::shutdown_signal())
.await
{
tracing::warn!("extra listener {extra_local} exited: {e}");
}
});
}
if let Err(e) = write_daemon_addr("trusty-console", &addr_string) {
tracing::warn!("could not write trusty-console discovery file: {e}");
}
let console_url = format!("http://{primary_local}");
eprintln!("trusty-console: {console_url}");
if args.open {
let _ = open::that(&console_url);
}
axum::serve(primary_listener, router)
.with_graceful_shutdown(shutdown_signal())
.await
.context("server error")?;
if let Ok(Some(recorded)) = trusty_common::read_daemon_addr("trusty-console")
&& recorded == addr_string
&& let Ok(dir) = trusty_common::resolve_data_dir("trusty-console")
{
let _ = std::fs::remove_file(dir.join("http_addr"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serve_args_defaults() {
let cli = Cli::parse_from(["trusty-console", "serve"]);
match cli.command {
Commands::Serve(args) => {
assert_eq!(args.http, "127.0.0.1:7788");
assert!(!args.open);
assert!(!args.tailscale);
assert_eq!(args.poll_interval, 15);
}
}
}
#[test]
fn test_serve_args_tailscale_flag() {
let cli = Cli::parse_from(["trusty-console", "serve", "--tailscale"]);
match cli.command {
Commands::Serve(args) => {
assert!(args.tailscale);
assert_eq!(args.http, "127.0.0.1:7788");
}
}
}
#[test]
fn test_serve_args_custom_http() {
let cli = Cli::parse_from(["trusty-console", "serve", "--http", "0.0.0.0:9000"]);
match cli.command {
Commands::Serve(args) => {
assert_eq!(args.http, "0.0.0.0:9000");
}
}
}
#[test]
fn test_serve_args_custom_poll_interval() {
let cli = Cli::parse_from(["trusty-console", "serve", "--poll-interval", "30"]);
match cli.command {
Commands::Serve(args) => {
assert_eq!(args.poll_interval, 30);
}
}
}
}