feat: onglet Réparation dans paramètres — gestion modules fantômes
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
ab834600ba
commit
3bc55a4c6f
4 changed files with 132 additions and 1 deletions
|
|
@ -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"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@
|
|||
<button class="tab-btn" :class="{ active: tab === 'shortcuts' }" @click="tab = 'shortcuts'">
|
||||
<i class="lnid-link-1"></i> Raccourcis
|
||||
</button>
|
||||
<button class="tab-btn" :class="{ active: tab === 'repair' }" @click="tab = 'repair'; loadRepair()">
|
||||
<i class="lnid-wrench-1"></i> Réparation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Général -->
|
||||
|
|
@ -175,8 +178,45 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Réparation -->
|
||||
<div class="tab-panel" x-show="tab === 'repair'">
|
||||
<p class="form-hint" style="margin-bottom:1rem">
|
||||
Les <strong>modules fantômes</strong> 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.
|
||||
</p>
|
||||
<div class="loading-state" x-show="repairLoading">
|
||||
<div class="spinner-lg"></div><span>Chargement…</span>
|
||||
</div>
|
||||
<div x-show="!repairLoading">
|
||||
<p class="empty-state" x-show="repairModules.length === 0">Aucun module non-core en base de données.</p>
|
||||
<div class="modules-grid" x-show="repairModules.length > 0">
|
||||
<template x-for="mod in repairModules" :key="mod.id">
|
||||
<div class="neu-card module-card">
|
||||
<div class="module-header">
|
||||
<div class="module-icon"><i class="lnid-puzzle"></i></div>
|
||||
<div class="module-info">
|
||||
<span class="module-name" x-text="mod.name || mod.id"></span>
|
||||
<span class="module-version" x-text="mod.is_enabled ? 'Activé' : 'Désactivé'"></span>
|
||||
<span class="module-desc" x-text="mod.installed_at ? 'Installé le ' + mod.installed_at.slice(0,10) : 'Date inconnue'"></span>
|
||||
</div>
|
||||
<div class="module-toggle">
|
||||
<button class="neu-btn neu-btn--sm neu-btn--danger"
|
||||
@click="resetModule(mod)"
|
||||
:disabled="resetting[mod.id]">
|
||||
<span x-show="resetting[mod.id]" class="spinner-sm"></span>
|
||||
<i x-show="!resetting[mod.id]" class="lnid-trash-1"></i>
|
||||
Supprimer de la DB
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save actions -->
|
||||
<div class="save-bar" x-show="tab !== 'shortcuts'">
|
||||
<div class="save-bar" x-show="tab !== 'shortcuts' && tab !== 'repair'">
|
||||
<div class="save-feedback">
|
||||
<span class="save-success" x-show="saved">
|
||||
<i class="lnid-check-circle-1"></i> Paramètres sauvegardés
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue