use std::path::PathBuf;
use std::sync::Arc;
use axum::{
http::StatusCode,
routing::{delete, get, patch, post, put},
Json, Router,
};
use wsb::persist::global::models::PlaceFilter;
use wsb::persist::global::SessionRepo;
use super::handlers;
use super::handlers::sessions::ErrorResponse;
use super::sync::SyncHandle;
#[derive(Clone)]
pub struct MachineWeb {
pub db: Arc<SessionRepo>,
pub sync: SyncHandle,
pub volume_root: PathBuf,
}
impl MachineWeb {
pub fn open(machine_dir: &std::path::Path) -> anyhow::Result<Self> {
let prj = match wsb_prj::Prj::from_env() {
Ok(prj) => Some(prj),
Err(wsb_prj::PrjError::NotSet) => None,
Err(error) => return Err(anyhow::anyhow!(error)),
};
let db = Arc::new(SessionRepo::open_machine(machine_dir)?);
let volume_root = prj
.as_ref()
.map(|prj| prj.root().to_path_buf())
.unwrap_or_else(|| machine_dir.to_path_buf());
let sync = super::sync::spawn_sync_actor(db.clone(), prj);
Ok(Self {
db,
sync,
volume_root,
})
}
pub fn spawn_services(&self) {
let sync = self.sync.clone();
tokio::spawn(async move {
if let Err(error) = sync.sync_incremental().await {
wsb::errln!("[wsb] initial session sync failed: {error:#}");
}
if let Err(error) = super::sync::start_watcher(sync).await {
wsb::errln!("[wsb] the transcript watcher stopped: {error:#}");
}
});
}
}
pub fn place_filter(
project: Option<String>,
worktree: Option<String>,
placed: Option<String>,
) -> Result<PlaceFilter, (StatusCode, Json<ErrorResponse>)> {
let placed = match placed.as_deref() {
None | Some("false") => false,
Some("true") => true,
Some(other) => {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("placed is true or false, not {other:?}"),
}),
))
}
};
PlaceFilter::new(project, worktree, placed)
.map_err(|error| (StatusCode::BAD_REQUEST, Json(ErrorResponse { error })))
}
pub fn routes<S: Clone + Send + Sync + 'static>(state: MachineWeb) -> Router<S> {
let api = Router::new()
.route("/sessions", get(handlers::sessions::list_sessions))
.route("/sessions/places", get(handlers::sessions::list_places))
.route("/sessions/:id", get(handlers::sessions::get_session))
.route("/sessions/:id/messages", get(handlers::sessions::get_messages))
.route("/sessions/:id/children", get(handlers::sessions::get_children))
.route("/sessions/:id/minimap", get(handlers::sessions::get_minimap))
.route("/sessions/:id/search", get(handlers::sessions::search_session))
.route("/sessions/:id/compact", get(handlers::sessions::get_compact_text))
.route("/sessions/:id/export", get(handlers::sessions::export_session))
.route("/sessions/:id/watch", get(handlers::sessions::watch_session))
.route("/sessions/:id/rename", patch(handlers::sessions::rename_session))
.route("/sessions/:id", delete(handlers::sessions::delete_session))
.route("/sessions/:id/restore", post(handlers::sessions::restore_session))
.route("/sessions/:id/permanent", delete(handlers::sessions::permanent_delete))
.route("/machines", get(handlers::sessions::list_machines))
.route("/agents", get(handlers::sessions::list_agents))
.route("/stats", get(handlers::sessions::get_stats))
.route("/analytics/summary", get(handlers::analytics::summary))
.route("/analytics/activity", get(handlers::analytics::activity))
.route("/analytics/heatmap", get(handlers::analytics::heatmap))
.route("/analytics/projects", get(handlers::analytics::projects))
.route("/analytics/hour-of-week", get(handlers::analytics::hour_of_week))
.route("/analytics/top-sessions", get(handlers::analytics::top_sessions))
.route("/usage/summary", get(handlers::analytics::usage_summary))
.route("/usage/daily", get(handlers::analytics::usage_daily))
.route("/usage/by-model", get(handlers::analytics::usage_by_model))
.route("/usage/by-project", get(handlers::analytics::usage_by_project))
.route("/usage/sync-pricing", post(handlers::analytics::sync_pricing))
.route("/search", get(handlers::search::session_search))
.route("/sync", post(handlers::sync_handler::trigger_sync))
.route("/resync", post(handlers::sync_handler::trigger_resync))
.route("/sync/status", get(handlers::sync_handler::sync_status))
.route("/starred", get(handlers::starred::list_starred))
.route("/sessions/:id/star", put(handlers::starred::star_session))
.route("/sessions/:id/star", delete(handlers::starred::unstar_session))
.route("/starred/bulk", post(handlers::starred::bulk_star))
.route("/pins", get(handlers::pins::list_all_pins))
.route("/sessions/:id/pins", get(handlers::pins::list_session_pins))
.route(
"/sessions/:id/messages/:message_id/pin",
post(handlers::pins::pin_message),
)
.route(
"/sessions/:id/messages/:message_id/pin",
delete(handlers::pins::unpin_message),
)
.route("/trash", get(handlers::trash::list_trash))
.route("/trash", delete(handlers::trash::empty_trash))
.route("/plans/list", get(handlers::plans::list_plans))
.route("/plans/content", get(handlers::plans::get_plan_content))
.route("/host", get(handlers::host::host_snapshot))
.route("/host/stream", get(handlers::host::host_stream));
Router::new().nest("/api/v1", api).with_state(state)
}