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,
|
"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.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
|
// WebSocket — les routes WS extraient le token via query param
|
||||||
r.Get("/ws/proxmox", proxmoxHandler.WebSocket)
|
r.Get("/ws/proxmox", proxmoxHandler.WebSocket)
|
||||||
r.Get("/ws/updates/{jobId}", updatesHandler.WebSocketUpdate)
|
r.Get("/ws/updates/{jobId}", updatesHandler.WebSocketUpdate)
|
||||||
|
|
|
||||||
|
|
@ -980,6 +980,9 @@ document.addEventListener('alpine:init', () => {
|
||||||
saved: false,
|
saved: false,
|
||||||
error: '',
|
error: '',
|
||||||
shortcuts: [],
|
shortcuts: [],
|
||||||
|
repairModules: [],
|
||||||
|
repairLoading: false,
|
||||||
|
resetting: {},
|
||||||
settings: {
|
settings: {
|
||||||
instance_name: '',
|
instance_name: '',
|
||||||
public_url: '',
|
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) },
|
t(key) { return Alpine.store('i18n').t(key) },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@
|
||||||
<button class="tab-btn" :class="{ active: tab === 'shortcuts' }" @click="tab = 'shortcuts'">
|
<button class="tab-btn" :class="{ active: tab === 'shortcuts' }" @click="tab = 'shortcuts'">
|
||||||
<i class="lnid-link-1"></i> Raccourcis
|
<i class="lnid-link-1"></i> Raccourcis
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tab-btn" :class="{ active: tab === 'repair' }" @click="tab = 'repair'; loadRepair()">
|
||||||
|
<i class="lnid-wrench-1"></i> Réparation
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Général -->
|
<!-- Général -->
|
||||||
|
|
@ -175,8 +178,45 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Save actions -->
|
||||||
<div class="save-bar" x-show="tab !== 'shortcuts'">
|
<div class="save-bar" x-show="tab !== 'shortcuts' && tab !== 'repair'">
|
||||||
<div class="save-feedback">
|
<div class="save-feedback">
|
||||||
<span class="save-success" x-show="saved">
|
<span class="save-success" x-show="saved">
|
||||||
<i class="lnid-check-circle-1"></i> Paramètres sauvegardés
|
<i class="lnid-check-circle-1"></i> Paramètres sauvegardés
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue