structure polish: post-reify nav + ensure-chain + TeamWizard auto-parent
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 25s
ci / rust (push) Successful in 3m54s
ci / e2e (push) Has been skipped
ci / publish (push) Successful in 3m4s

Two small quality-of-life fixes on top of the reify commit:

Post-reify navigation
  OrphanMigrationDialog already returned team_id in its result;
  Dashboard now pushes /?team=<team_id> before router.refresh() so
  the user lands on the freshly-materialized team and sees exactly
  where their agents just moved. Previously they had to hunt for it
  in the newly-rebuilt sidebar.

Wizard auto-materialize (POST /api/structure/ensure-chain)
  cm-db: ensure_chain(pool, ws, fallback_org, fallback_company) —
    fast path returns coordinates of the first org+company already
    bound in this workspace (workspace's oldest org, oldest company
    under it). Slow path inserts a new org+company with the
    fallback names ("My Workspace" / "General") + binds them via
    org_companies. Returns { org_id, company_id, created }. Small
    txn — leaves the workspace consistent whether it was already
    wired or not.
  cm-api: POST /api/structure/ensure-chain accepts optional
    fallback_org_name and fallback_company_name in the body (trimmed,
    else default). Returns the ids.
  CreateTeamRequest gains an optional attach_to_company_id. When
    set, after build_team() completes, we look up the company
    (workspace ownership check enforced by companies::get), count
    its existing teams for a stable n_i node id, and insert a
    company_teams binding — so the team lands under the parent
    atomically instead of a follow-up round-trip.
  TeamWizard now calls ensure-chain before POST /api/teams and
    passes the returned company_id in attach_to_company_id. Both
    calls are best-effort — if ensure-chain fails (network etc.)
    we still try to create the team, and the migration dialog stays
    available as the fallback UX. Wizard flow now: fresh workspace's
    first team is fully wired from the moment it appears in the
    tree — no synthetic "My Workspace" scaffolding ever gets
    rendered around it.

The Team/Company create paths not touched here (create_team_from_claws,
company create, org create, MasterPlannerModal scaffold) still
work as before — they just won't auto-parent yet. Later commits
can wire them the same way.
This commit is contained in:
Omar Sobh
2026-07-09 11:41:27 -07:00
parent 8b789beec0
commit acd2a0f287
7 changed files with 221 additions and 2 deletions
+18
View File
@@ -34,6 +34,12 @@ pub struct CreateTeamRequest {
/// TopologyKind (snake_case), e.g. "hierarchical", "pipeline".
pub kind: String,
pub members: Vec<TeamMemberInput>,
/// Optional parent company — when provided, the freshly-created team
/// gets bound to it via `company_teams` so it never lands orphaned.
/// Wizards typically fetch this from `POST /api/structure/ensure-chain`
/// so the workspace always has a valid parent before team creation.
#[serde(default)]
pub attach_to_company_id: Option<Uuid>,
}
#[derive(Serialize)]
@@ -159,6 +165,18 @@ pub async fn create_team(
&body.members,
)
.await?;
// Auto-parent the new team when the wizard fetched a company via
// `POST /api/structure/ensure-chain`. Ownership check + node_id
// allocation happen inline so the team never lands orphaned mid-turn.
if let Some(company_id) = body.attach_to_company_id {
let company = cm_db::repo::companies::get(&state.pool, company_id, user.workspace_id)
.await
.map_err(|_| ApiError::NotFound)?;
let existing = cm_db::repo::companies::teams_for_company(&state.pool, company.id).await?;
let node_id = format!("n{}", existing.len());
cm_db::repo::companies::add_team(&state.pool, company.id, &node_id, team_id, "team")
.await?;
}
Ok((
StatusCode::CREATED,
Json(TeamCreated {