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

+4 -1
View File
@@ -52,6 +52,8 @@ interface Props {
onMoved: (from: string, to: string) => void;
/** Un fichier ou dossier a été supprimé. */
onDeleted: (path: string) => void;
/** Affiche l'historique des versions d'un schéma. */
onHistory: (path: string) => void;
}
type View = "list" | "grid";
@@ -78,7 +80,7 @@ const initialView = (): View => {
const canMoveInto = (path: string, targetDir: string) =>
dirname(path) !== targetDir && targetDir !== path && !targetDir.startsWith(path + "/");
export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted }: Props) {
export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted, onHistory }: Props) {
const [dir, setDir] = useState(() => (current ? dirname(current) : ""));
const [entries, setEntries] = useState<Entry[]>([]);
const [query, setQuery] = useState("");
@@ -421,6 +423,7 @@ export function Explorer({ current, refreshKey, onOpen, onMoved, onDeleted }: Pr
<button onClick={() => rename(e)}>Renommer</button>
<button onClick={() => setMoving([e.path])}>Déplacer…</button>
{!e.isDir && <button onClick={() => duplicate(e)}>Dupliquer</button>}
{!e.isDir && <button onClick={() => onHistory(e.path)}>Historique…</button>}
{!e.isDir && (
<a href={api.downloadUrl(e.path)} download={e.name}>
Télécharger