Files
clawmates/crates/cm-db/src/repo/teams.rs
T
Omar SobhandClaude Opus 4.8 3554a3aaf2
ci / gates (push) Successful in 5s
ci / frontend (push) Successful in 23s
ci / rust (push) Failing after 27s
ci / e2e (push) Has been skipped
CI: remove k8s stages, fix the Docker-level pipeline green
Survey + fixes so the pipeline passes at the Docker level (no k8s).

- Remove k8s: drop the `sandbox-k8s` job (kind/Calico/--features k8s-tests) and the
  "Helm chart lints" gate step. release.yml was already k8s-clean.
- Rust job:
  - `cargo fmt --all` — fix pre-existing formatting drift (fmt --check was failing).
  - clippy -D warnings: fix 3 lib warnings (cm-brain sort_by_key→Reverse, cm-api
    fleet.rs doc list indentation, node_rules map_or→is_none_or).
  - Regenerate the .sqlx offline cache (was missing the cm-runtime run_loop test
    query → offline compile failed). DB-backed tests use testcontainers at runtime.
  - Set SQLX_OFFLINE=true on the rust + e2e jobs so query! macros compile against
    the committed cache deterministically (no DB needed at compile time).
- Frontend job:
  - Fix the 1 ESLint error (useAgentTelemetry: no setState-synchronously-in-effect;
    tag the slice with agentId + derive null on mismatch).
  - Fix 2 stale panel-params tests (`terminal` is a valid app id now; assert the
    current APP_IDS + use a genuinely-unknown id for the reject case).

Verified locally: fmt clean, clippy --all-targets -D warnings clean (offline),
frontend lint 0 errors, tsc clean, 86/86 frontend tests pass, build OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 18:15:31 -07:00

192 lines
4.7 KiB
Rust

//! Persistence for deployed teams — a baseline topology staffed with real
//! workspace claws. `teams` holds the topology graph; `team_members` is the
//! durable node→claw binding.
use cm_domain::WorkspaceId;
use serde_json::Value;
use sqlx::PgPool;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::DbError;
/// A team row summary (list view).
pub struct TeamSummary {
pub id: Uuid,
pub name: String,
pub kind: String,
pub status: String,
pub created_at: OffsetDateTime,
}
/// A full team (graph + metadata).
pub struct Team {
pub id: Uuid,
pub name: String,
pub kind: String,
pub graph: Value,
pub status: String,
pub created_at: OffsetDateTime,
}
/// A node→claw binding within a team.
pub struct TeamMember {
pub node_id: String,
pub claw_id: Uuid,
pub role: String,
}
/// Insert a team (the topology graph). Members are added separately.
pub async fn insert_team(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
name: &str,
kind: &str,
graph: &Value,
) -> Result<(), DbError> {
sqlx::query!(
"INSERT INTO teams (id, workspace_id, name, kind, graph)
VALUES ($1, $2, $3, $4, $5)",
id,
workspace_id.as_uuid(),
name,
kind,
graph,
)
.execute(pool)
.await?;
Ok(())
}
/// Rebuild a team's topology (kind + graph) in place; node→claw bindings (which
/// key off stable node ids `n0..`) are left untouched. Non-macro query so it
/// needs no offline sqlx cache entry.
pub async fn set_topology(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
kind: &str,
graph: &Value,
) -> Result<(), DbError> {
let res =
sqlx::query("UPDATE teams SET kind = $3, graph = $4 WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(workspace_id.as_uuid())
.bind(kind)
.bind(graph)
.execute(pool)
.await?;
if res.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
/// Delete a team and its node→claw bindings.
pub async fn delete_team(
pool: &PgPool,
id: Uuid,
workspace_id: WorkspaceId,
) -> Result<(), DbError> {
sqlx::query("DELETE FROM team_members WHERE team_id = $1")
.bind(id)
.execute(pool)
.await?;
let res = sqlx::query("DELETE FROM teams WHERE id = $1 AND workspace_id = $2")
.bind(id)
.bind(workspace_id.as_uuid())
.execute(pool)
.await?;
if res.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
/// Bind a claw to a topology node within a team.
pub async fn add_member(
pool: &PgPool,
team_id: Uuid,
node_id: &str,
claw_id: Uuid,
role: &str,
) -> Result<(), DbError> {
sqlx::query!(
"INSERT INTO team_members (team_id, node_id, claw_id, role)
VALUES ($1, $2, $3, $4)",
team_id,
node_id,
claw_id,
role,
)
.execute(pool)
.await?;
Ok(())
}
/// The most recent teams for a workspace, newest first.
pub async fn list_for_workspace(
pool: &PgPool,
workspace_id: WorkspaceId,
limit: i64,
) -> Result<Vec<TeamSummary>, DbError> {
let rows = sqlx::query!(
"SELECT id, name, kind, status, created_at FROM teams
WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT $2",
workspace_id.as_uuid(),
limit,
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| TeamSummary {
id: r.id,
name: r.name,
kind: r.kind,
status: r.status,
created_at: r.created_at,
})
.collect())
}
/// A single team, workspace-scoped.
pub async fn get_team(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result<Team, DbError> {
let row = sqlx::query!(
"SELECT id, name, kind, graph, status, created_at FROM teams
WHERE id = $1 AND workspace_id = $2",
id,
workspace_id.as_uuid(),
)
.fetch_optional(pool)
.await?
.ok_or(DbError::NotFound)?;
Ok(Team {
id: row.id,
name: row.name,
kind: row.kind,
graph: row.graph,
status: row.status,
created_at: row.created_at,
})
}
/// The node→claw bindings for a team.
pub async fn members_for_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<TeamMember>, DbError> {
let rows = sqlx::query!(
"SELECT node_id, claw_id, role FROM team_members WHERE team_id = $1 ORDER BY node_id",
team_id,
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|r| TeamMember {
node_id: r.node_id,
claw_id: r.claw_id,
role: r.role,
})
.collect())
}