Files
sogoms/GO-HTMX.md
Pierre 65da4efdad 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>
2025-12-19 20:30:56 +01:00

339 lines
9.6 KiB
Markdown

# 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
```html
<!-- 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
```html
<!-- 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
```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)
```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`
```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`
```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`
```go
mux.HandleFunc("GET /admin/api/cron/jobs",
AuthMiddleware(sessions, adminCfg, server.HandleAPICronJobs))
```
**4. Utiliser dans le dashboard** `templates/dashboard.html`
```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 :
```yaml
# config/sogoctl.yaml
sogoms-admin:
args:
- "-templates"
- "/config/admin/templates"
- "-dev" # ← rechargement auto
```
**Workflow dev :**
```bash
# 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