Skip to main content

kaizen/web/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Local daemon web app: embedded UI plus WebSocket tool calls.
3
4mod assets;
5pub mod features;
6mod server;
7pub mod tools;
8
9use crate::ipc::WebEndpoint;
10use anyhow::{Context, Result};
11use std::net::{IpAddr, Ipv4Addr, SocketAddr};
12use tokio::net::TcpListener;
13use tokio::task::JoinHandle;
14use uuid::Uuid;
15
16const DEFAULT_LISTEN: &str = "127.0.0.1:7878";
17
18pub async fn start() -> Result<(WebEndpoint, JoinHandle<()>)> {
19    let listener = bind_loopback().await?;
20    start_with_listener(listener).await
21}
22
23pub async fn start_with_listener(listener: TcpListener) -> Result<(WebEndpoint, JoinHandle<()>)> {
24    let token = Uuid::now_v7().simple().to_string();
25    start_with_token(listener, token).await
26}
27
28pub async fn start_with_token(
29    listener: TcpListener,
30    token: String,
31) -> Result<(WebEndpoint, JoinHandle<()>)> {
32    let addr = listener.local_addr()?;
33    let endpoint = endpoint(addr, token);
34    let app = server::router(endpoint.token.clone());
35    let task = tokio::spawn(async move {
36        if let Err(err) = axum::serve(listener, app).await {
37            tracing::warn!(%err, "daemon web app stopped");
38        }
39    });
40    Ok((endpoint, task))
41}
42
43async fn bind_loopback() -> Result<TcpListener> {
44    match TcpListener::bind(DEFAULT_LISTEN).await {
45        Ok(listener) => Ok(listener),
46        Err(err) => bind_fallback()
47            .await
48            .with_context(|| format!("bind daemon web app at {DEFAULT_LISTEN}: {err}")),
49    }
50}
51
52async fn bind_fallback() -> Result<TcpListener> {
53    TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
54        .await
55        .map_err(Into::into)
56}
57
58fn endpoint(addr: SocketAddr, token: String) -> WebEndpoint {
59    let public = public_addr(addr);
60    WebEndpoint {
61        listen: addr.to_string(),
62        url: format!("http://{public}/?token={token}"),
63        token,
64    }
65}
66
67fn public_addr(addr: SocketAddr) -> SocketAddr {
68    if addr.ip().is_unspecified() {
69        return SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), addr.port());
70    }
71    addr
72}