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