Historique des versions : snapshots horodatés, différences, restauration

- Avant d'écraser un schéma, le contenu précédent est copié dans
  DATA_DIR/.history/<chemin>/<date>.excalidraw, au plus une version par
  HISTORY_INTERVAL (10 min) ; rétention HISTORY_KEEP (50) et HISTORY_MAX_AGE.
  Pas de git : l'image reste distroless.
- API : GET /api/history, GET /api/history/version, POST /api/history/snapshot,
  POST /api/history/restore (le contenu remplacé devient une version).
- L'historique suit renommages et déplacements, part à la corbeille avec le
  schéma supprimé.
- Dialogue Historique (barre du haut, menu de l'explorateur) : aperçu,
  différences colorées avec l'état actuel, restaurer, restaurer en copie,
  créer une version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6J7vKwXx7uopxKhWfFJpm
This commit is contained in:
claude BotandClaude Opus 5.5 committed 2026-10-07 18:03:08 +02:00
1 parent 88f7750577
commit 9f9ec3b3a4
14 files changed
+1005 -7

No files matched your search

+25 -1
View File
@@ -50,6 +50,13 @@ func main() {
os.Exit(1)
}
// Historique des versions : HISTORY_KEEP versions par schéma (0 : désactivé),
// au plus une toutes les HISTORY_INTERVAL, purgées après HISTORY_MAX_AGE (0 : jamais).
if store.History, err = historyPolicy(); err != nil {
slog.Error("historique", "err", err)
os.Exit(1)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
@@ -91,7 +98,8 @@ func main() {
}
go func() {
slog.Info("démarrage", "addr", addr, "data", dataDir)
slog.Info("démarrage", "addr", addr, "data", dataDir,
"history_keep", store.History.Keep, "history_interval", store.History.Interval.String())
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("serveur", "err", err)
os.Exit(1)
@@ -104,6 +112,22 @@ func main() {
slog.Info("arrêt")
}
func historyPolicy() (storage.HistoryPolicy, error) {
p := storage.DefaultHistory
keep, err := strconv.Atoi(env("HISTORY_KEEP", strconv.Itoa(p.Keep)))
if err != nil || keep < 0 {
return p, errors.New("HISTORY_KEEP invalide (entier >= 0)")
}
p.Keep = keep
if p.Interval, err = time.ParseDuration(env("HISTORY_INTERVAL", p.Interval.String())); err != nil || p.Interval < 0 {
return p, errors.New("HISTORY_INTERVAL invalide (ex. 10m, 0 : à chaque enregistrement)")
}
if p.MaxAge, err = time.ParseDuration(env("HISTORY_MAX_AGE", "0")); err != nil || p.MaxAge < 0 {
return p, errors.New("HISTORY_MAX_AGE invalide (ex. 2160h, 0 : illimité)")
}
return p, nil
}
func healthcheck(addr string) int {
host, port, err := net.SplitHostPort(addr)
if err != nil {