Skip to content

Commit f69a5b4

Browse files
tombelieberclaude
andcommitted
feat(teams): add GET /api/teams/{name}/sidechains endpoint
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 5c33289 commit f69a5b4

1 file changed

Lines changed: 51 additions & 2 deletions

File tree

crates/server/src/routes/teams.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
//! - GET /teams/:name — Get team detail (config + members)
66
//! - GET /teams/:name/inbox — Get team inbox messages
77
//! - GET /teams/:name/cost — Get team cost breakdown (resolves member sessions)
8+
//! - GET /teams/:name/sidechains?session_id=xxx — Get team member sidechains
89
9-
use axum::extract::{Path, State};
10+
use axum::extract::{Path, Query, State};
1011
use axum::routing::get;
1112
use axum::{Json, Router};
13+
use serde::Deserialize;
1214
use std::sync::Arc;
1315

1416
use crate::error::{ApiError, ApiResult};
1517
use crate::routes::sessions::resolve_session_file_path;
1618
use crate::state::AppState;
17-
use crate::teams::{InboxMessage, TeamCostBreakdown, TeamDetail, TeamSummary};
19+
use crate::teams::{InboxMessage, TeamCostBreakdown, TeamDetail, TeamMemberSidechain, TeamSummary};
1820

1921
/// GET /api/teams — List all teams.
2022
#[utoipa::path(get, path = "/api/teams", tag = "teams",
@@ -132,10 +134,57 @@ pub async fn get_team_cost(
132134
Ok(Json(cost))
133135
}
134136

137+
#[derive(Debug, Deserialize)]
138+
pub struct TeamSidechainsQuery {
139+
pub session_id: String,
140+
}
141+
142+
/// GET /api/teams/:name/sidechains?session_id=xxx — Get team member sidechains.
143+
///
144+
/// Resolves sidechain `.meta.json` / `.jsonl` pairs inside the session directory
145+
/// to enumerate each member's spawned sub-conversations.
146+
#[utoipa::path(get, path = "/api/teams/{name}/sidechains", tag = "teams",
147+
params(
148+
("name" = String, Path, description = "Team name"),
149+
("session_id" = String, Query, description = "Lead session ID"),
150+
),
151+
responses(
152+
(status = 200, description = "Team member sidechains", body = serde_json::Value),
153+
(status = 404, description = "Team not found"),
154+
)
155+
)]
156+
pub async fn get_team_sidechains(
157+
State(state): State<Arc<AppState>>,
158+
Path(name): Path<String>,
159+
Query(query): Query<TeamSidechainsQuery>,
160+
) -> ApiResult<Json<Vec<TeamMemberSidechain>>> {
161+
// Verify team exists
162+
let _team = state
163+
.teams
164+
.get(&name)
165+
.ok_or_else(|| ApiError::NotFound(format!("Team '{}' not found", name)))?;
166+
167+
// Resolve session JSONL path, then get its parent directory
168+
let session_path = resolve_session_file_path(&state, &query.session_id).await?;
169+
let session_dir = session_path
170+
.parent()
171+
.ok_or_else(|| ApiError::Internal("Session path has no parent directory".into()))?
172+
.to_path_buf();
173+
174+
// Heavy I/O: read meta.json + count JSONL lines on blocking thread
175+
let sidechains =
176+
tokio::task::spawn_blocking(move || crate::teams::resolve_team_sidechains(&session_dir))
177+
.await
178+
.map_err(|e| ApiError::Internal(format!("Join error: {e}")))?;
179+
180+
Ok(Json(sidechains))
181+
}
182+
135183
pub fn router() -> Router<Arc<AppState>> {
136184
Router::new()
137185
.route("/teams", get(list_teams))
138186
.route("/teams/{name}", get(get_team))
139187
.route("/teams/{name}/inbox", get(get_team_inbox))
140188
.route("/teams/{name}/cost", get(get_team_cost))
189+
.route("/teams/{name}/sidechains", get(get_team_sidechains))
141190
}

0 commit comments

Comments
 (0)