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>
339 lines
9.6 KiB
Markdown
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
|