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>
9.6 KiB
9.6 KiB
Architecture Go + htmx (sogoms-admin)
Guide pour comprendre et modifier l'interface d'administration SOGOMS.
Vue d'ensemble
Browser (htmx) ←→ Go Server ←→ Services (sockets)
│ │
│ HTTP GET/POST │
▼ ▼
Templates Handlers
(HTML) (Go)
Stack :
- Backend : Go net/http (pas de framework)
- Frontend : Go templates + htmx + Pico.css
- Principe : Le serveur renvoie des fragments HTML, htmx les injecte dans le DOM
Structure des fichiers
cmd/sogoms/admin/
├── main.go # Point d'entrée, routes, config
├── handlers.go # Handlers HTTP (logique métier)
├── middleware.go # Auth, CSRF, logging, rate limiting
├── session.go # Gestion des sessions
├── services.go # Appels vers microservices (sockets)
└── templates/
├── layout.html # Layout commun (head, nav, footer)
├── login.html # Page de connexion
├── dashboard.html # Dashboard principal
└── partials/ # Fragments htmx
├── apps_list.html
└── services_status.html
Flux de données
1. Requête initiale (page complète)
GET /admin/
│
▼
main.go:103 ─────────────────────────────────────────────┐
│ mux.HandleFunc("GET /admin/", AuthMiddleware(...)) │
└───────────────────────────────────────────────────────┘
│
▼
handlers.go:114-138 ─────────────────────────────────────┐
│ func HandleDashboard(w, r) { │
│ data := map[string]any{ │
│ "Title": "Dashboard", │
│ "Apps": accessibleApps, │
│ ... │
│ } │
│ s.render(w, "dashboard.html", data) │
│ } │
└───────────────────────────────────────────────────────┘
│
▼
templates/dashboard.html ────────────────────────────────┐
│ {{template "layout" .}} │
│ <h1>Dashboard</h1> │
│ <div hx-get="/admin/api/services/health" │
│ hx-trigger="load"> │
│ Chargement... │
│ </div> │
└───────────────────────────────────────────────────────┘
2. Requête htmx (fragment)
hx-get="/admin/api/services/health"
│
▼
main.go:109 ─────────────────────────────────────────────┐
│ mux.HandleFunc("GET /admin/api/services/health", ...) │
└───────────────────────────────────────────────────────┘
│
▼
handlers.go:172-193 ─────────────────────────────────────┐
│ func HandleAPIServicesHealth(w, r) { │
│ statuses := s.services.HealthCheck() │
│ data := map[string]any{"Services": statuses} │
│ s.render(w, "partials/services_status.html", data)│
│ } │
└───────────────────────────────────────────────────────┘
│
▼
templates/partials/services_status.html ─────────────────┐
│ <ul> │
│ {{range .Services}} │
│ <li>{{.Name}} - {{if .Available}}OK{{end}}</li> │
│ {{end}} │
│ </ul> │
└───────────────────────────────────────────────────────┘
│
▼
htmx remplace le contenu du <div> avec ce fragment
Attributs htmx essentiels
| Attribut | Description | Exemple |
|---|---|---|
hx-get |
GET AJAX vers URL | hx-get="/admin/api/apps" |
hx-post |
POST AJAX vers URL | hx-post="/admin/api/apps/create" |
hx-trigger |
Quand déclencher | load, click, every 30s, submit |
hx-target |
Où injecter la réponse | hx-target="#result" (défaut: élément courant) |
hx-swap |
Comment injecter | innerHTML (défaut), outerHTML, beforeend |
hx-indicator |
Spinner pendant chargement | hx-indicator=".loading" |
hx-confirm |
Confirmation avant action | hx-confirm="Supprimer ?" |
Exemples
<!-- Charger au chargement de la page -->
<div hx-get="/admin/api/apps" hx-trigger="load">
Chargement...
</div>
<!-- Rafraîchir toutes les 30 secondes -->
<div hx-get="/admin/api/services/health"
hx-trigger="load, every 30s">
</div>
<!-- Soumettre un formulaire en AJAX -->
<form hx-post="/admin/api/apps/create"
hx-target="#apps-list"
hx-swap="beforeend">
<input name="name" required>
<button type="submit">Créer</button>
</form>
<!-- Supprimer avec confirmation -->
<button hx-delete="/admin/api/apps/123"
hx-confirm="Supprimer cette app ?"
hx-target="closest li"
hx-swap="outerHTML">
Supprimer
</button>
Templates Go
Syntaxe de base
<!-- Variable -->
{{.Title}}
<!-- Condition -->
{{if .IsSuperAdmin}}
Super Admin
{{else}}
App Admin
{{end}}
<!-- Boucle -->
{{range .Apps}}
<li>{{.Name}}</li>
{{end}}
<!-- Inclusion de template -->
{{template "layout" .}}
<!-- Bloc définissable -->
{{define "content"}}
Contenu ici
{{end}}
Structure layout.html
{{define "layout"}}
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}} - SOGOMS Admin</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@1/css/pico.min.css">
</head>
<body>
<nav>...</nav>
<main class="container">
{{template "content" .}}
</main>
</body>
</html>
{{end}}
Structure page (dashboard.html)
{{define "dashboard.html"}}
{{template "layout" .}}
{{end}}
{{define "content"}}
<h1>Dashboard</h1>
<!-- contenu de la page -->
{{end}}
Ajouter une nouvelle fonctionnalité
Exemple : Liste des jobs cron
1. Créer le partial templates/partials/cron_jobs.html
{{define "partials/cron_jobs.html"}}
<table>
<thead>
<tr>
<th>Job</th>
<th>Schedule</th>
<th>Prochain run</th>
</tr>
</thead>
<tbody>
{{range .Jobs}}
<tr>
<td>{{.Name}}</td>
<td><code>{{.Schedule}}</code></td>
<td>{{.NextRun}}</td>
</tr>
{{end}}
</tbody>
</table>
{{end}}
2. Ajouter le handler dans handlers.go
func (s *AdminServer) HandleAPICronJobs(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
jobs, err := s.services.GetCronJobs()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := map[string]any{
"Jobs": jobs,
}
s.render(w, "partials/cron_jobs.html", data)
}
3. Ajouter la route dans main.go
mux.HandleFunc("GET /admin/api/cron/jobs",
AuthMiddleware(sessions, adminCfg, server.HandleAPICronJobs))
4. Utiliser dans le dashboard templates/dashboard.html
<article>
<header><strong>Jobs Cron</strong></header>
<div hx-get="/admin/api/cron/jobs" hx-trigger="load">
Chargement...
</div>
</article>
Mode développement
Le flag -dev recharge les templates à chaque requête :
# config/sogoctl.yaml
sogoms-admin:
args:
- "-templates"
- "/config/admin/templates"
- "-dev" # ← rechargement auto
Workflow dev :
# 1. Modifier un template localement
vim cmd/sogoms/admin/templates/dashboard.html
# 2. Déployer les templates (sans recompilation)
./deploy-admin.sh
# 3. Rafraîchir le navigateur → changements visibles
Pour la prod, retirer -dev (templates chargés une fois au démarrage).
Fichiers de configuration
| Fichier | Rôle |
|---|---|
/secrets/admin_users.yaml |
Users, passwords, rôles |
/secrets/admin_session_secret |
Clé HMAC pour cookies |
/config/sogoctl.yaml |
Args de sogoms-admin |
/config/admin/templates/ |
Templates HTML (si -templates) |
Ressources
- htmx : https://htmx.org/docs/
- Pico.css : https://picocss.com/docs/
- Go templates : https://pkg.go.dev/html/template