From 3bc55a4c6f39a8031e433903a83e475611973cd2 Mon Sep 17 00:00:00 2001 From: enzo Date: Sun, 22 Mar 2026 18:27:37 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20onglet=20R=C3=A9paration=20dans=20param?= =?UTF-8?q?=C3=A8tres=20=E2=80=94=20gestion=20modules=20fant=C3=B4mes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/repair/modules : liste les modules non-core en DB - DELETE /api/repair/modules/{id} : supprime un module de la DB - settings.html : onglet Réparation avec liste + bouton Supprimer - app.js : loadRepair() + resetModule() dans settingsPage Co-Authored-By: Claude Sonnet 4.6 --- backend/internal/api/settings.go | 49 ++++++++++++++++++++++++++++++++ backend/main.go | 7 +++++ frontend/js/app.js | 35 +++++++++++++++++++++++ frontend/settings.html | 42 ++++++++++++++++++++++++++- 4 files changed, 132 insertions(+), 1 deletion(-) diff --git a/backend/internal/api/settings.go b/backend/internal/api/settings.go index 2dab5d8..a5d09dc 100644 --- a/backend/internal/api/settings.go +++ b/backend/internal/api/settings.go @@ -533,3 +533,52 @@ func (h *SettingsHandler) InstallRegistryModule(w http.ResponseWriter, r *http.R "rebuilding": rebuilding, }) } + +// ── Réparation ──────────────────────────────────────────────────────────── + +// repairModule est une entrée retournée par GetRepairStatus. +type repairModule struct { + ID string `json:"id"` + Name string `json:"name"` + IsEnabled bool `json:"is_enabled"` + HasBackend bool `json:"has_backend"` + InstalledAt string `json:"installed_at"` +} + +// GetRepairStatus retourne les modules non-core présents en DB (potentiellement fantômes). +// GET /api/repair/modules +func (h *SettingsHandler) GetRepairStatus(w http.ResponseWriter, r *http.Request) { + rows, err := h.db.Query( + `SELECT id, name, is_enabled, has_backend, COALESCE(installed_at,'') FROM modules WHERE is_core = 0`) + if err != nil { + JSONResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + defer rows.Close() + + modules := []repairModule{} + for rows.Next() { + var m repairModule + rows.Scan(&m.ID, &m.Name, &m.IsEnabled, &m.HasBackend, &m.InstalledAt) + modules = append(modules, m) + } + JSONResponse(w, http.StatusOK, map[string]interface{}{"modules": modules}) +} + +// ResetModule supprime un module non-core de la DB (permet de le réinstaller proprement). +// DELETE /api/repair/modules/{id} +func (h *SettingsHandler) ResetModule(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + res, err := h.db.Exec(`DELETE FROM modules WHERE id = ? AND is_core = 0`, id) + if err != nil { + JSONResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + n, _ := res.RowsAffected() + if n == 0 { + JSONResponse(w, http.StatusNotFound, map[string]string{"error": "module introuvable ou module core"}) + return + } + h.auditLogger.Log(r.Context(), "repair.reset_module", id, "") + JSONResponse(w, http.StatusOK, map[string]string{"message": "Module supprimé de la DB"}) +} diff --git a/backend/main.go b/backend/main.go index 7ccf814..0a01aa0 100644 --- a/backend/main.go +++ b/backend/main.go @@ -185,6 +185,13 @@ func main() { r.Post("/api/registry/modules/{id}/install", settingsHandler.InstallRegistryModule) }) + // Réparation DB — admin uniquement + r.Group(func(r chi.Router) { + r.Use(api.RequireAdmin) + r.Get("/api/repair/modules", settingsHandler.GetRepairStatus) + r.Delete("/api/repair/modules/{id}", settingsHandler.ResetModule) + }) + // WebSocket — les routes WS extraient le token via query param r.Get("/ws/proxmox", proxmoxHandler.WebSocket) r.Get("/ws/updates/{jobId}", updatesHandler.WebSocketUpdate) diff --git a/frontend/js/app.js b/frontend/js/app.js index e8c0219..eef9b9c 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -980,6 +980,9 @@ document.addEventListener('alpine:init', () => { saved: false, error: '', shortcuts: [], + repairModules: [], + repairLoading: false, + resetting: {}, settings: { instance_name: '', public_url: '', @@ -1070,6 +1073,38 @@ document.addEventListener('alpine:init', () => { } }, + async loadRepair() { + this.repairLoading = true + try { + const res = await apiFetch('/api/repair/modules') + if (res.ok) { + const data = await res.json() + this.repairModules = data.modules || [] + } + } finally { + this.repairLoading = false + } + }, + + async resetModule(mod) { + if (!confirm(`Supprimer "${mod.name || mod.id}" de la base de données ?\nLe module pourra être réinstallé depuis le Store.`)) return + this.resetting[mod.id] = true + try { + const res = await apiFetch(`/api/repair/modules/${mod.id}`, { method: 'DELETE' }) + if (res.ok) { + this.repairModules = this.repairModules.filter(m => m.id !== mod.id) + Alpine.store('toasts').success(`Module "${mod.name || mod.id}" supprimé de la DB`) + } else { + const d = await res.json().catch(() => ({})) + Alpine.store('toasts').error(d.error || 'Erreur suppression') + } + } catch(e) { + Alpine.store('toasts').error(e.message) + } finally { + this.resetting[mod.id] = false + } + }, + t(key) { return Alpine.store('i18n').t(key) }, })) diff --git a/frontend/settings.html b/frontend/settings.html index b0701e4..a37f218 100644 --- a/frontend/settings.html +++ b/frontend/settings.html @@ -71,6 +71,9 @@ + @@ -175,8 +178,45 @@ + +
+

+ Les modules fantômes sont des modules présents en base de données + mais dont le code n'est pas chargé. Les supprimer permet de les réinstaller proprement. +

+
+
Chargement… +
+
+

Aucun module non-core en base de données.

+
+ +
+
+
+ -
+
Paramètres sauvegardés