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:
enzo 2026-03-22 03:34:17 +01:00
parent 91cf788221
commit ec7d120ef6
15 changed files with 460 additions and 997 deletions

View file

@ -1,6 +1,3 @@
// Package modules — Loader de modules.
// Découvre les modules disponibles, vérifie leur état en DB, et les initialise si activés.
// Un module désactivé ne fait appel à aucune de ses méthodes Register().
package modules
import (
@ -8,25 +5,34 @@ import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/crypto"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/db"
sshpool "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh"
)
// Loader charge et gère les modules actifs.
type Loader struct {
db *sql.DB
db *db.DB
pool *sshpool.Pool
enc *crypto.Encryptor
registry *coreRegistry
modules []Module
}
// NewLoader crée un Loader avec le router et la DB fournis.
func NewLoader(db *sql.DB) *Loader {
// NewLoader crée un Loader avec les services du CORE nécessaires aux modules.
func NewLoader(database *db.DB, pool *sshpool.Pool, enc *crypto.Encryptor) *Loader {
return &Loader{
db: db,
registry: newCoreRegistry(db),
db: database,
pool: pool,
enc: enc,
registry: newCoreRegistry(database.DB, pool, enc),
}
}
// RegisterModule enregistre un module disponible (appelé à l'init, depuis main.go).
// Le module sera initialisé seulement s'il est activé en base.
func (l *Loader) RegisterModule(m Module) {
l.modules = append(l.modules, m)
}
@ -57,19 +63,18 @@ func (l *Loader) isEnabled(id string) (bool, error) {
var enabled int
err := l.db.QueryRow(`SELECT is_enabled FROM modules WHERE id = ?`, id).Scan(&enabled)
if err == sql.ErrNoRows {
return false, nil // Module inconnu = désactivé
return false, nil
}
return enabled == 1, err
}
// Registry retourne le registry partagé (pour accès par le serveur HTTP).
// Registry retourne le registry partagé.
func (l *Loader) Registry() *coreRegistry {
return l.registry
}
// ---- Implémentation interne du Registry ----
// RouteEntry décrit une route HTTP enregistrée par un module.
type RouteEntry struct {
Method string
Path string
@ -90,19 +95,25 @@ type translationEntry struct {
// coreRegistry implémente l'interface Registry.
type coreRegistry struct {
db *sql.DB
sqlDB *sql.DB
pool *sshpool.Pool
enc *crypto.Encryptor
routes []RouteEntry
wsChannels map[string]WSHandler
widgets []WidgetDef
settingsTabs []SettingsTabDef
migrations []migrationEntry
translations []translationEntry
navItems map[string]NavItemDef // nav items en mémoire (clé = module ID)
}
func newCoreRegistry(db *sql.DB) *coreRegistry {
func newCoreRegistry(sqlDB *sql.DB, pool *sshpool.Pool, enc *crypto.Encryptor) *coreRegistry {
return &coreRegistry{
db: db,
sqlDB: sqlDB,
pool: pool,
enc: enc,
wsChannels: make(map[string]WSHandler),
navItems: make(map[string]NavItemDef),
}
}
@ -130,8 +141,69 @@ func (r *coreRegistry) RegisterMigration(version int, sqlStr string, fn Migratio
r.migrations = append(r.migrations, migrationEntry{version, sqlStr, fn})
}
// RegisterNavItem enregistre l'entrée de navigation d'un module en mémoire et en DB.
func (r *coreRegistry) RegisterNavItem(item NavItemDef) {
r.navItems[item.ID] = item
// Persister en DB pour que le frontend puisse le récupérer via /api/modules
r.sqlDB.Exec(
`UPDATE modules SET nav_href=?, nav_icon=?, nav_color=?, nav_label_key=? WHERE id=?`,
item.Href, item.Icon, item.Color, item.LabelKey, item.ID,
)
}
// DB retourne la connexion SQLite brute.
func (r *coreRegistry) DB() *sql.DB {
return r.db
return r.sqlDB
}
// RunOnTarget exécute une commande SSH sur la cible (host ou lxc:VMID).
// La commande est wrappée via pct exec pour les cibles LXC.
func (r *coreRegistry) RunOnTarget(target, command string) (string, error) {
host, user, pass, err := r.sshCreds()
if err != nil {
return "", err
}
cmd := buildTargetCmd(target, command)
return r.pool.RunCommand(host, user, pass, cmd)
}
// StreamOnTarget exécute une commande SSH en streaming sur la cible.
func (r *coreRegistry) StreamOnTarget(target, command string, output chan<- string) error {
host, user, pass, err := r.sshCreds()
if err != nil {
return err
}
cmd := buildTargetCmd(target, command)
return r.pool.StreamCommand(host, user, pass, cmd, output)
}
// sshCreds récupère et déchiffre les credentials SSH depuis la configuration.
func (r *coreRegistry) sshCreds() (host, user, pass string, err error) {
var h, u, ep string
r.sqlDB.QueryRow(`SELECT value FROM settings WHERE key='ssh_host'`).Scan(&h)
r.sqlDB.QueryRow(`SELECT value FROM settings WHERE key='ssh_username'`).Scan(&u)
r.sqlDB.QueryRow(`SELECT value FROM settings WHERE key='ssh_password'`).Scan(&ep)
if ep != "" {
pass, err = r.enc.Decrypt(ep)
if err != nil {
return "", "", "", fmt.Errorf("impossible de déchiffrer le mot de passe SSH")
}
}
if h == "" || u == "" || pass == "" {
return "", "", "", fmt.Errorf("SSH non configuré")
}
return h, u, pass, nil
}
// buildTargetCmd construit la commande pour la cible (host ou lxc:VMID).
func buildTargetCmd(target, command string) string {
if strings.HasPrefix(target, "lxc:") {
vmid := strings.TrimPrefix(target, "lxc:")
if _, err := strconv.Atoi(vmid); err == nil {
return fmt.Sprintf("pct exec %s -- sh -c %q", vmid, command)
}
}
return command
}
// GetRoutes retourne les routes enregistrées par les modules.
@ -139,6 +211,11 @@ func (r *coreRegistry) GetRoutes() []RouteEntry {
return r.routes
}
// GetNavItems retourne les nav items enregistrés en mémoire.
func (r *coreRegistry) GetNavItems() map[string]NavItemDef {
return r.navItems
}
// GetWidgets retourne les types de widgets disponibles.
func (r *coreRegistry) GetWidgets() []WidgetDef {
return r.widgets