fix(layout): auto-réparer l'onglet actif

stale

Le backend renvoie l'id actif autoritaire et le
    frontend l'adopte pour éviter de rejouer un layout
    disparu après overwrite externe.

Co-Authored-By: Claude Opus 4.8
    <noreply@anthropic.com>
This commit is contained in:
2026-06-25 16:23:40 +02:00
parent 137620daa3
commit e916ecd95e
14 changed files with 372 additions and 51 deletions

View File

@ -297,6 +297,15 @@ pub struct SetActiveLayoutInput {
pub layout_id: LayoutId,
}
/// Output of [`SetActiveLayout::execute`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetActiveLayoutOutput {
/// The id of the layout that was **actually** made active. Equals the
/// requested id when it exists; otherwise the unchanged current active id
/// (self-healing fallback). Authoritative for the frontend (invariant I4).
pub active_id: LayoutId,
}
/// Switches the active layout of a project.
pub struct SetActiveLayout {
store: Arc<dyn ProjectStore>,
@ -317,20 +326,30 @@ impl SetActiveLayout {
/// Sets the active layout.
///
/// A stale requested id (e.g. an `activeId` left over after git overwrote
/// `layouts.json`) must **not** freeze the workspace: instead of hard-
/// erroring, it degrades silently to the current active layout (invariant
/// I3). The returned [`SetActiveLayoutOutput::active_id`] is the id that was
/// actually activated and is authoritative for the frontend (invariant I4).
///
/// # Errors
/// [`AppError::NotFound`] if the project or layout is unknown.
pub async fn execute(&self, input: SetActiveLayoutInput) -> Result<(), AppError> {
/// - [`AppError::FileSystem`] on persistence failure,
/// - [`AppError::Store`] on registry I/O failure.
pub async fn execute(
&self,
input: SetActiveLayoutInput,
) -> Result<SetActiveLayoutOutput, AppError> {
let project = self.store.load_project(input.project_id).await?;
let mut doc = resolve_doc(self.fs.as_ref(), &project).await?;
if doc.find(input.layout_id).is_none() {
return Err(AppError::NotFound(format!("layout {}", input.layout_id)));
}
doc.active_id = input.layout_id;
// Self-heal: keep the requested id when valid, else fall back to the
// (always-valid, I2) current active id rather than erroring.
let active_id = doc.resolve_existing_id(Some(input.layout_id));
doc.active_id = active_id;
persist_doc(self.fs.as_ref(), &project, &doc).await?;
self.events.publish(DomainEvent::LayoutChanged {
project_id: input.project_id,
});
Ok(())
Ok(SetActiveLayoutOutput { active_id })
}
}