feat(domain,application,infrastructure,backend,app-tauri,web-server,frontend): restaurer les projets ouverts au redémarrage dans l'ordre mémorisé — #303 (état versionné OpenProjectRestoreState persisté sous <app_data_dir>/open-projects.json via port dédié OpenProjectRestoreStore, distinct du registre projects.json et sans recycler workspace.json; sept use cases cadrés: read/record opened/record closed/save order/restore/retry/remove, mutations sérialisées par mutex applicatif contre les lost updates, tombstones conservés à leur position avec lastFailure et codes stables notFound/inaccessible/store/invalid; prévalidation root par metadata+listing sans interrompre les autres restaurations; activation centralisée BackendCore::activate_project réutilisée par ouverture Tauri/HTTP, création, restore et retry: reconciliation layouts/live-state/background tasks, watcher orchestrateur + serveur MCP, réparation run dirs Claude; cinq commandes exposées Tauri et HTTP avec contrats DTO identiques: restore_open_projects, retry_open_project_restore, remove_project_from_restore, save_open_project_restore_order, read_open_project_restore_state; frontend: DTO de reprise + ProjectGateway étendu, useProjects initialise par listProjects+restoreOpenProjects et reconstruit openTabs dans l'ordre opened sans notification quand tout réussit, notification unique singulier/pluriel avec Voir les détails/Réessayer, détail par projet avec cause française et Retirer de la reprise, retry réussi réinséré à la position mémorisée, onglet conservé si closeProject échoué; saveOpenProjectRestoreOrder exposé non appelé car l'UI ne permet pas encore de réordonnancer; QA verte reconfirmée par Git: rustup run stable cargo fmt --all -- --check, git diff --check, cargo test -p backend open_project_restore_tests 1/1 couvrant absence initiale/ordre/fermeture/inaccessible/retry/notFound/remove/watcher+MCP, cargo test -p web-server authorized_open_project_restore_commands_share_the_tauri_contract 1/1, cargo test -p domain 311+ suites vertes, cargo check -p application -p infrastructure -p backend -p app-tauri -p web-server OK, npm run typecheck OK, npm test ciblé 3 fichiers/37 tests OK, npm run build OK; réserves hors périmètre documentées au carnet: application tests/layout_usecases.rs 2 échecs fixtures plugin package NotFound, infrastructure 1 timing test cancellation process, npm test large 32 échecs web shell/LS7/tickets web — préexistants sans lien avec #303; restant: rebuild AppImage demandé après validation puis validation live fermeture/relance; fichiers tickets/296/*, tickets/299-303/*, counter/index, attachments/ et scripts/ hors périmètre; aucun push)
This commit is contained in:
@ -17,8 +17,8 @@ use application::{
|
||||
CreateProjectInput, CreateProjectOutput, ExecuteSlashCommandOutput, GitGraphOutput,
|
||||
HealthInput, HealthReport, LayoutKind, ListProjectsOutput, ListSlashCommandsOutput,
|
||||
LiveRuntimeState, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
|
||||
SlashCommandEffect, StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage,
|
||||
TurnSource, TurnView,
|
||||
RestoreOpenProjectFailure, RestoreOpenProjectsOutput, SlashCommandEffect, StopLiveAgentOutput,
|
||||
TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
|
||||
};
|
||||
use domain::ports::{ReplyProgress, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage};
|
||||
use domain::{
|
||||
@ -1148,6 +1148,110 @@ impl From<OpenProjectOutput> for ProjectDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// One persisted entry in the ordered open-project restore state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenProjectRestoreEntryDto {
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub root: String,
|
||||
pub position: u32,
|
||||
pub last_failure: Option<domain::OpenProjectRestoreFailure>,
|
||||
}
|
||||
|
||||
/// Machine-local ordered open-project restore state.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenProjectRestoreStateDto {
|
||||
pub version: u32,
|
||||
pub entries: Vec<OpenProjectRestoreEntryDto>,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
impl From<domain::OpenProjectRestoreState> for OpenProjectRestoreStateDto {
|
||||
fn from(state: domain::OpenProjectRestoreState) -> Self {
|
||||
Self {
|
||||
version: state.version,
|
||||
entries: state
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| OpenProjectRestoreEntryDto {
|
||||
project_id: entry.project_id.to_string(),
|
||||
name: entry.name,
|
||||
root: entry.root,
|
||||
position: entry.position,
|
||||
last_failure: entry.last_failure,
|
||||
})
|
||||
.collect(),
|
||||
updated_at: state.updated_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One non-blocking project restore failure.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RestoreOpenProjectFailureDto {
|
||||
pub project_id: String,
|
||||
pub name: String,
|
||||
pub root: String,
|
||||
pub position: u32,
|
||||
pub code: domain::OpenProjectRestoreFailureCode,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl From<RestoreOpenProjectFailure> for RestoreOpenProjectFailureDto {
|
||||
fn from(failure: RestoreOpenProjectFailure) -> Self {
|
||||
Self {
|
||||
project_id: failure.project_id.to_string(),
|
||||
name: failure.name,
|
||||
root: failure.root,
|
||||
position: failure.position,
|
||||
code: failure.code,
|
||||
message: failure.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Projects restored successfully plus retained per-entry failures.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RestoreOpenProjectsOutputDto {
|
||||
pub opened: Vec<ProjectDto>,
|
||||
pub failures: Vec<RestoreOpenProjectFailureDto>,
|
||||
}
|
||||
|
||||
impl From<RestoreOpenProjectsOutput> for RestoreOpenProjectsOutputDto {
|
||||
fn from(output: RestoreOpenProjectsOutput) -> Self {
|
||||
Self {
|
||||
opened: output.opened.into_iter().map(ProjectDto::from).collect(),
|
||||
failures: output.failures.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional targeted retry request. Omit `projectId` to retry every failure.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RetryOpenProjectRestoreRequestDto {
|
||||
#[serde(default)]
|
||||
pub project_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Removes one retained entry from future restoration.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveOpenProjectRestoreEntryRequestDto {
|
||||
pub project_id: String,
|
||||
}
|
||||
|
||||
/// Persists the visible open-project order.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveOpenProjectRestoreOrderRequestDto {
|
||||
pub ordered_project_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Parses a project-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
Reference in New Issue
Block a user