feat: reset sélection batch au chargement + style toast warning

- initApp() appelle deselectAll() au démarrage (sécurité)
- Toast warning : fond jaune transparent + icône colorée
- Ajout classe toast-icon pour colorer l'icône selon le type
- Correction broadcast WebSocket batch delete (type mismatch)
- Correction compilation Linux : service install/uninstall gated
This commit is contained in:
2026-06-25 12:25:50 -04:00
parent 1f02bdc987
commit f51d70a646
9 changed files with 303 additions and 321 deletions
+90
View File
@@ -100,6 +100,7 @@ pub fn api_routes(state: AppState) -> Router {
.route("/api/images/{id}", get(get_image).delete(delete_image))
.route("/api/images/{id}/tags", put(update_tags))
.route("/api/images/{id}/thumbnail", get(get_thumbnail))
.route("/api/images/batch-delete", post(batch_delete))
.route("/api/stats", get(stats))
.route("/api/version", get(version))
.with_state(state)
@@ -497,6 +498,95 @@ pub struct TagsUpdate {
pub tags: Vec<String>,
}
/// Requête de suppression multiple
#[derive(Deserialize)]
pub struct BatchDeleteRequest {
pub ids: Vec<i64>,
}
/// Réponse de suppression multiple
#[derive(Serialize)]
pub struct BatchDeleteResponse {
pub deleted: Vec<i64>,
pub failed: Vec<serde_json::Value>,
pub total_requested: usize,
pub total_deleted: usize,
}
/// Suppression en lot : supprime plusieurs images en une seule requête
async fn batch_delete(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
Json(body): Json<BatchDeleteRequest>,
) -> impl IntoResponse {
if body.ids.is_empty() {
return error_response(400, "Aucun ID fourni");
}
if body.ids.len() > 100 {
return error_response(400, "Maximum 100 images par lot");
}
let total_requested = body.ids.len();
let mut deleted: Vec<i64> = Vec::new();
let mut failed: Vec<serde_json::Value> = Vec::new();
let client = match get_client(&state, &headers) {
Ok(c) => c,
Err(e) => return error_response(500, &format!("Erreur client : {:#}", e)),
};
tracing::info!("Suppression batch de {} images...", total_requested);
for &id in &body.ids {
match client.delete_image(id).await {
Ok(()) => {
deleted.push(id);
tracing::debug!(" ✅ Image {} supprimée", id);
}
Err(e) => {
failed.push(serde_json::json!({
"id": id,
"error": format!("{:#}", e)
}));
tracing::warn!(" ❌ Échec suppression image {}: {:#}", id, e);
}
}
}
tracing::info!(
"Batch delete terminé: {}/{} supprimées, {} échecs",
deleted.len(),
total_requested,
failed.len()
);
// Notifier les clients WebSocket
if !deleted.is_empty() {
let _ = state.ws_hub.broadcast(&serde_json::json!({
"type": "images_deleted",
"ids": &deleted
}).to_string());
}
let status = if failed.is_empty() {
axum::http::StatusCode::OK
} else if deleted.is_empty() {
axum::http::StatusCode::INTERNAL_SERVER_ERROR
} else {
axum::http::StatusCode::MULTI_STATUS
};
(
status,
Json(serde_json::json!({
"deleted": deleted,
"failed": failed,
"total_requested": total_requested,
"total_deleted": deleted.len()
})),
)
}
async fn update_tags(
State(state): State<Arc<AppState>>,
headers: HeaderMap,