Merge remote-tracking branch 'origin/main' into change-68b704f7

# Conflicts:
#	backend/ee-repo-ref.txt
#	backend/windmill-api-workspaces/src/workspaces.rs
This commit is contained in:
Diego Imbert
2026-06-29 16:55:41 +02:00
528 changed files with 40725 additions and 8064 deletions
@@ -244,6 +244,7 @@ pub fn global_service() -> Router {
.route("/list_as_superadmin", get(list_workspaces_as_super_admin))
.route("/list", get(list_workspaces))
.route("/users", get(user_workspaces))
.route("/session_workspace_status", post(session_workspace_status))
.route("/create", post(create_workspace))
.route("/create_fork", post(deprecated_create_workspace_fork))
.route("/exists", post(exists_workspace))
@@ -2395,7 +2396,7 @@ async fn edit_ducklake_config(
"#,
&w_id
)
.fetch_one(&db)
.fetch_one(&mut *tx)
.await?
.unwrap_or(serde_json::Value::Null);
let old_ducklakes: HashMap<String, Ducklake> =
@@ -4919,6 +4920,47 @@ async fn user_workspaces(
Ok(Json(WorkspaceList { email, workspaces }))
}
#[derive(Deserialize)]
struct SessionWorkspaceStatusRequest {
workspace_ids: Vec<String>,
}
/// Reconciliation support for client-side AI sessions, which the backend cannot touch
/// directly. The client posts the workspace ids its sessions reference and uses the
/// per-id status to keep sessions in sync with workspace lifecycle: `deleted` (no row /
/// no access → unresolvable) drops the sessions, `archived` (soft-deleted, still a
/// member) archives them, `active` restores ones previously archived-by-workspace.
/// Archived and hard-deleted workspaces are absent from `user_workspaces`, so this is the
/// only way the client learns about a change made while it was away or on another device.
async fn session_workspace_status(
Extension(db): Extension<DB>,
ApiAuthed { email, .. }: ApiAuthed,
Json(req): Json<SessionWorkspaceStatusRequest>,
) -> JsonResult<HashMap<String, String>> {
if req.workspace_ids.len() > 1000 {
return Err(Error::BadRequest(
"Too many workspace ids (max 1000)".to_string(),
));
}
let rows = sqlx::query!(
"SELECT req.id AS \"id!\",
(CASE
WHEN usr.email IS NULL THEN 'deleted'
WHEN workspace.deleted THEN 'archived'
ELSE 'active'
END) AS \"status!\"
FROM unnest($1::text[]) AS req(id)
LEFT JOIN workspace ON workspace.id = req.id
LEFT JOIN usr ON usr.workspace_id = workspace.id AND usr.email = $2",
&req.workspace_ids[..],
email,
)
.fetch_all(&db)
.await?;
let statuses = rows.into_iter().map(|r| (r.id, r.status)).collect();
Ok(Json(statuses))
}
pub async fn check_w_id_conflict<'c>(tx: &mut Transaction<'c, Postgres>, w_id: &str) -> Result<()> {
if w_id == "global" {
return Err(windmill_common::error::Error::BadRequest(