core/backend/modules/loader.go
enzo ec7d120ef6 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>
2026-03-22 03:34:17 +01:00

227 lines
6.5 KiB
Go

package modules
import (
"database/sql"
"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 *db.DB
pool *sshpool.Pool
enc *crypto.Encryptor
registry *coreRegistry
modules []Module
}
// 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: database,
pool: pool,
enc: enc,
registry: newCoreRegistry(database.DB, pool, enc),
}
}
// RegisterModule enregistre un module disponible (appelé à l'init, depuis main.go).
func (l *Loader) RegisterModule(m Module) {
l.modules = append(l.modules, m)
}
// LoadActive charge et initialise tous les modules activés en base de données.
func (l *Loader) LoadActive() error {
for _, m := range l.modules {
enabled, err := l.isEnabled(m.ID())
if err != nil {
return fmt.Errorf("vérification module %s : %w", m.ID(), err)
}
if !enabled {
log.Printf("Module %s : désactivé, ignoré", m.ID())
continue
}
log.Printf("Module %s : chargement...", m.ID())
if err := m.Register(l.registry); err != nil {
return fmt.Errorf("initialisation module %s : %w", m.ID(), err)
}
log.Printf("Module %s : chargé avec succès", m.ID())
}
return nil
}
// isEnabled vérifie en base de données si un module est activé.
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
}
return enabled == 1, err
}
// Registry retourne le registry partagé.
func (l *Loader) Registry() *coreRegistry {
return l.registry
}
// ---- Implémentation interne du Registry ----
type RouteEntry struct {
Method string
Path string
Handler http.HandlerFunc
RequireAdmin bool
}
type migrationEntry struct {
version int
sql string
fn MigrationFn
}
type translationEntry struct {
lang string
keys map[string]string
}
// coreRegistry implémente l'interface Registry.
type coreRegistry struct {
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(sqlDB *sql.DB, pool *sshpool.Pool, enc *crypto.Encryptor) *coreRegistry {
return &coreRegistry{
sqlDB: sqlDB,
pool: pool,
enc: enc,
wsChannels: make(map[string]WSHandler),
navItems: make(map[string]NavItemDef),
}
}
func (r *coreRegistry) RegisterRoute(method, path string, handler http.HandlerFunc, requireAdmin bool) {
r.routes = append(r.routes, RouteEntry{method, path, handler, requireAdmin})
}
func (r *coreRegistry) RegisterWSChannel(channel string, handler WSHandler) {
r.wsChannels[channel] = handler
}
func (r *coreRegistry) RegisterWidget(widget WidgetDef) {
r.widgets = append(r.widgets, widget)
}
func (r *coreRegistry) RegisterSettingsTab(tab SettingsTabDef) {
r.settingsTabs = append(r.settingsTabs, tab)
}
func (r *coreRegistry) RegisterTranslations(lang string, keys map[string]string) {
r.translations = append(r.translations, translationEntry{lang, keys})
}
func (r *coreRegistry) RegisterMigration(version int, sqlStr string, fn MigrationFn) {
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.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.
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
}
// GetSettingsTabs retourne les onglets de paramètres des modules.
func (r *coreRegistry) GetSettingsTabs() []SettingsTabDef {
return r.settingsTabs
}