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

+122
View File
@@ -1005,3 +1005,125 @@ li.active > .row {
background: var(--ex-primary);
color: var(--ex-surface-lowest);
}
/* --- Historique des versions ----------------------------------------------- */
.modal.history {
width: min(920px, 100%);
height: min(640px, 92dvh);
max-height: none;
}
.history-head {
display: flex;
align-items: center;
gap: 8px;
}
.history-head h3 {
flex: 1;
min-width: 0;
}
.history-body {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 240px 1fr;
gap: 10px;
}
.versions-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--ex-radius-lg);
background: var(--ex-surface-lowest);
}
.versions-list button {
width: 100%;
height: auto;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 8px 10px;
border-radius: 0;
background: transparent;
box-shadow: none;
font-size: 13px;
letter-spacing: normal;
}
.versions-list button.on {
background: var(--accent-soft);
color: var(--ex-on-primary-container);
}
.versions-list small {
font-size: 11px;
}
.versions-list .empty {
padding: 10px;
font-size: 13px;
}
.history-preview {
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.history-preview .segmented {
align-self: flex-start;
}
.history-preview .segmented button {
height: calc(var(--ex-btn-lg) - 6px);
background: transparent;
box-shadow: none;
}
.history-preview .segmented button.on {
background: var(--ex-primary-container);
color: var(--ex-on-primary-container);
}
.svg-box {
flex: 1;
min-height: 0;
display: grid;
place-items: center;
padding: 8px;
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--ex-radius-lg);
background: var(--ex-surface-lowest);
color: var(--muted);
}
.svg-box svg {
max-width: 100%;
max-height: 100%;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 4px 14px;
margin: 0;
font-size: 12px;
}
.legend i {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 5px;
border-radius: 2px;
}
.modal-actions a.link {
display: inline-flex;
padding: 6px;
}
@media (max-width: 640px) {
.modal.history {
height: 92dvh;
}
.history-body {
grid-template-columns: 1fr;
grid-template-rows: minmax(90px, 30%) 1fr;
}
.history-head button span {
display: none;
}
}