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