SOGOMS v1.0.3 - Admin UI, Cron, Config reload

Phase 13 : sogoms-cron
- Jobs planifiés avec schedule cron standard
- Types: query_email, http, service
- Actions: list, trigger, status

Phase 16 : Réorganisation config/apps/{app}/
- Tous les fichiers d'une app dans un seul dossier
- Migration prokov vers nouvelle structure

Phase 17 : sogoms-admin
- Interface web d'administration (Go templates + htmx)
- Auth sessions cookies signées HMAC-SHA256
- Rôles super_admin / app_admin avec permissions

Phase 19 : Création d'app via Admin UI
- Formulaire création app avec config DB/auth
- Bouton "Scanner la base" : introspection + schema.yaml
- Rechargement automatique sogoway via SIGHUP

Infrastructure :
- sogoctl : socket de contrôle /run/sogoctl.sock
- sogoway : reload config sur SIGHUP sans restart

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-19 20:30:56 +01:00
parent a4694a10d1
commit 65da4efdad
76 changed files with 5305 additions and 80 deletions

View File

@@ -20,7 +20,8 @@ type AppConfig struct {
Database Database `yaml:"database"`
Auth Auth `yaml:"auth"`
Routes []Route `yaml:"routes"`
Queries *Queries // Chargé depuis config/queries/{app}/
Queries *Queries // Chargé depuis config/apps/{app}/queries/
Schema *Schema // Chargé depuis config/apps/{app}/schema.yaml
}
// Queries stocke les requêtes SQL par domaine.
@@ -48,6 +49,14 @@ func (q *Queries) Get(domain, key string) string {
return ""
}
// FileCount retourne le nombre de fichiers de queries chargés.
func (q *Queries) FileCount() int {
if q == nil || q.files == nil {
return 0
}
return len(q.files)
}
// GetMap retourne une map de requêtes (ex: login_data).
func (q *Queries) GetMap(domain, key string) map[string]string {
if q == nil || q.files == nil {
@@ -301,26 +310,33 @@ func NewRegistry(configDir string) *Registry {
}
}
// Load charge toutes les configurations depuis le répertoire routes.
// Load charge toutes les configurations depuis le répertoire apps.
func (r *Registry) Load() error {
r.mu.Lock()
defer r.mu.Unlock()
routesDir := filepath.Join(r.configDir, "routes")
entries, err := os.ReadDir(routesDir)
appsDir := filepath.Join(r.configDir, "apps")
entries, err := os.ReadDir(appsDir)
if err != nil {
return fmt.Errorf("read routes dir: %w", err)
return fmt.Errorf("read apps dir: %w", err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
if !entry.IsDir() {
continue
}
path := filepath.Join(routesDir, entry.Name())
cfg, err := r.loadAppConfig(path)
appID := entry.Name()
appConfigPath := filepath.Join(appsDir, appID, "app.yaml")
// Vérifier que app.yaml existe
if _, err := os.Stat(appConfigPath); os.IsNotExist(err) {
continue
}
cfg, err := r.loadAppConfig(appConfigPath, appID)
if err != nil {
return fmt.Errorf("load %s: %w", entry.Name(), err)
return fmt.Errorf("load %s: %w", appID, err)
}
r.apps[cfg.App] = cfg
@@ -333,7 +349,7 @@ func (r *Registry) Load() error {
}
// loadAppConfig charge une configuration d'application depuis un fichier YAML.
func (r *Registry) loadAppConfig(path string) (*AppConfig, error) {
func (r *Registry) loadAppConfig(path string, appID string) (*AppConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
@@ -372,15 +388,18 @@ func (r *Registry) loadAppConfig(path string) (*AppConfig, error) {
cfg.Auth.JWTExpiry = "24h"
}
// Charger les requêtes depuis config/queries/{app}/
cfg.Queries = r.loadQueries(cfg.App)
// Charger les requêtes depuis config/apps/{app}/queries/
cfg.Queries = r.loadQueries(appID)
// Charger le schema depuis config/apps/{app}/schema.yaml
cfg.Schema = loadSchema(r.configDir, appID)
return &cfg, nil
}
// loadQueries charge les fichiers de requêtes pour une application.
func (r *Registry) loadQueries(appID string) *Queries {
queriesDir := filepath.Join(r.configDir, "queries", appID)
queriesDir := filepath.Join(r.configDir, "apps", appID, "queries")
entries, err := os.ReadDir(queriesDir)
if err != nil {
return nil // Pas de répertoire queries, c'est OK