refactor: architecture modules indépendants — nettoyage CORE, registry enrichi, page modules dynamique
- Supprimer les modules services et logs du CORE (déplacés dans viewServices et viewLogs) - Enrichir modules/module.go : interface Registry avec NavItemDef, RunOnTarget, StreamOnTarget - Réécrire modules/loader.go : NewLoader accepte *db.DB, *sshpool.Pool, *crypto.Encryptor - Ajouter migration 005 : colonnes nav_* sur la table modules + suppression services/logs DB - Mettre à jour db.go (repairSchema) pour ajout idempotent des colonnes nav_* - Mettre à jour settings.go : GetModules retourne les champs nav, ajout GetRegistryModules et InstallRegistryModule - Mettre à jour main.go : NewLoader avec les bons arguments, ajout routes /api/registry/modules - Mettre à jour modules.html : section Store avec liste des modules Forgejo - Mettre à jour app.js : sidebar dynamique (nav_href depuis DB), modulesPage avec store - Mettre à jour pages.css : styles pour store de modules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
91cf788221
commit
ec7d120ef6
15 changed files with 460 additions and 997 deletions
|
|
@ -2,6 +2,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
|
@ -123,11 +126,26 @@ func (h *SettingsHandler) UpdateSetting(w http.ResponseWriter, r *http.Request)
|
|||
JSONResponse(w, http.StatusOK, map[string]string{"message": "Paramètre mis à jour"})
|
||||
}
|
||||
|
||||
// moduleResp représente un module dans les réponses API, incluant les champs de navigation.
|
||||
type moduleResp struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
IsCore bool `json:"is_core"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
NavHref string `json:"nav_href"`
|
||||
NavIcon string `json:"nav_icon"`
|
||||
NavColor string `json:"nav_color"`
|
||||
NavLabelKey string `json:"nav_label_key"`
|
||||
}
|
||||
|
||||
// GetModules retourne la liste de tous les modules et leur état.
|
||||
// GET /api/modules
|
||||
func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.db.Query(`
|
||||
SELECT id, name, description, version, is_core, is_enabled, installed_at
|
||||
SELECT id, name, description, version, is_core, is_enabled,
|
||||
COALESCE(nav_href,''), COALESCE(nav_icon,''), COALESCE(nav_color,''), COALESCE(nav_label_key,'')
|
||||
FROM modules ORDER BY is_core DESC, name ASC
|
||||
`)
|
||||
if err != nil {
|
||||
|
|
@ -136,25 +154,14 @@ func (h *SettingsHandler) GetModules(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
type module struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
IsCore bool `json:"is_core"`
|
||||
IsEnabled bool `json:"is_enabled"`
|
||||
InstalledAt *string `json:"installed_at,omitempty"`
|
||||
}
|
||||
|
||||
var modules []module
|
||||
var modules []moduleResp
|
||||
for rows.Next() {
|
||||
var m module
|
||||
var m moduleResp
|
||||
var isCore, isEnabled int
|
||||
var installedAt *string
|
||||
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled, &installedAt)
|
||||
rows.Scan(&m.ID, &m.Name, &m.Description, &m.Version, &isCore, &isEnabled,
|
||||
&m.NavHref, &m.NavIcon, &m.NavColor, &m.NavLabelKey)
|
||||
m.IsCore = isCore == 1
|
||||
m.IsEnabled = isEnabled == 1
|
||||
m.InstalledAt = installedAt
|
||||
modules = append(modules, m)
|
||||
}
|
||||
|
||||
|
|
@ -256,3 +263,163 @@ func (h *SettingsHandler) GetAuditLog(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
JSONResponse(w, http.StatusOK, entries)
|
||||
}
|
||||
|
||||
// RegistryModule représente un module disponible dans le store Forgejo.
|
||||
type RegistryModule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
// GetRegistryModules liste les repos de l'organisation proxmoxPanel sur Forgejo.
|
||||
// GET /api/registry/modules
|
||||
func (h *SettingsHandler) GetRegistryModules(w http.ResponseWriter, r *http.Request) {
|
||||
// Récupérer les modules déjà installés en DB
|
||||
rows, err := h.db.Query(`SELECT id FROM modules`)
|
||||
if err != nil {
|
||||
JSONError(w, "Erreur lecture modules", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
installed := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
installed[id] = true
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Appel à l'API Forgejo
|
||||
resp, err := http.Get("https://git.geronzi.fr/api/v1/repos/search?q=&owner=proxmoxPanel&limit=50")
|
||||
if err != nil {
|
||||
JSONError(w, fmt.Sprintf("Erreur accès store : %v", err), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
JSONError(w, "Erreur lecture réponse store", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// Structure de réponse Gitea/Forgejo
|
||||
var forgejoResp struct {
|
||||
Data []struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Description string `json:"description"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &forgejoResp); err != nil {
|
||||
JSONError(w, "Erreur parsing réponse store", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
var modules []RegistryModule
|
||||
for _, repo := range forgejoResp.Data {
|
||||
// Exclure le repo "core"
|
||||
if repo.Name == "core" {
|
||||
continue
|
||||
}
|
||||
modules = append(modules, RegistryModule{
|
||||
ID: repo.Name,
|
||||
Name: repo.Name,
|
||||
Description: repo.Description,
|
||||
RepoURL: repo.HTMLURL,
|
||||
Installed: installed[repo.Name],
|
||||
})
|
||||
}
|
||||
|
||||
if modules == nil {
|
||||
modules = []RegistryModule{}
|
||||
}
|
||||
JSONResponse(w, http.StatusOK, modules)
|
||||
}
|
||||
|
||||
// moduleJSON représente le fichier module.json d'un module.
|
||||
type moduleJSON struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
NavHref string `json:"nav_href"`
|
||||
NavIcon string `json:"nav_icon"`
|
||||
NavColor string `json:"nav_color"`
|
||||
NavLabelKey string `json:"nav_label_key"`
|
||||
CoreMinVersion string `json:"core_min_version"`
|
||||
}
|
||||
|
||||
// InstallRegistryModule installe un module depuis le store Forgejo.
|
||||
// POST /api/registry/modules/{id}/install
|
||||
func (h *SettingsHandler) InstallRegistryModule(w http.ResponseWriter, r *http.Request) {
|
||||
claims := GetClaims(r)
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
// Récupérer module.json depuis Forgejo
|
||||
url := fmt.Sprintf("https://git.geronzi.fr/api/v1/repos/proxmoxPanel/%s/raw/module.json?ref=main", id)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
JSONError(w, fmt.Sprintf("Impossible d'accéder au module %s : %v", id, err), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
JSONError(w, fmt.Sprintf("module.json introuvable pour %s", id), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
JSONError(w, fmt.Sprintf("Erreur récupération module.json : HTTP %d", resp.StatusCode), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
JSONError(w, "Erreur lecture module.json", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
var mod moduleJSON
|
||||
if err := json.Unmarshal(body, &mod); err != nil {
|
||||
JSONError(w, "Erreur parsing module.json", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// Valider l'ID
|
||||
if mod.ID == "" {
|
||||
mod.ID = id
|
||||
}
|
||||
if mod.Version == "" {
|
||||
mod.Version = "1.0.0"
|
||||
}
|
||||
|
||||
// URL du repo
|
||||
repoURL := fmt.Sprintf("https://git.geronzi.fr/proxmoxPanel/%s", id)
|
||||
|
||||
// Insérer ou remplacer en DB
|
||||
_, err = h.db.Exec(`
|
||||
INSERT INTO modules (id, name, description, version, is_core, is_enabled,
|
||||
nav_href, nav_icon, nav_color, nav_label_key, repo_url, installed_at)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name=excluded.name, description=excluded.description, version=excluded.version,
|
||||
nav_href=excluded.nav_href, nav_icon=excluded.nav_icon, nav_color=excluded.nav_color,
|
||||
nav_label_key=excluded.nav_label_key, repo_url=excluded.repo_url,
|
||||
installed_at=CURRENT_TIMESTAMP
|
||||
`, mod.ID, mod.Name, mod.Description, mod.Version,
|
||||
mod.NavHref, mod.NavIcon, mod.NavColor, mod.NavLabelKey, repoURL)
|
||||
if err != nil {
|
||||
JSONError(w, fmt.Sprintf("Erreur installation module : %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLogger.Log(&claims.UserID, claims.Username, "module_install", mod.ID,
|
||||
map[string]string{"version": mod.Version}, clientIP(r))
|
||||
|
||||
JSONResponse(w, http.StatusOK, map[string]string{
|
||||
"message": fmt.Sprintf("Module %s installé — rebuild requis pour activation", mod.ID),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue