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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue