feat(diagnostic): Auto-Versionierung fuer /shared/uploads/ + Versions-UI

Stefan-Wunsch: ARIA-Aenderungen an Dateien sollen vom System (nicht
von ARIA selbst) automatisch versioniert werden. Im Datei-Manager:
Versionen auflisten, einzelne downloaden, oder als neue aktive Version
setzen (Restore = non-destructive neuer Commit).

Implementierung (alles im diagnostic-Container, da der eh schon
File-Handling kann):

1. Dockerfile: apk add git

2. server.js — Auto-Commit-Loop:
   - Beim Start: /shared/uploads als git-Repo initialisieren (idempotent;
     bestehendes .git wird uebernommen)
   - setInterval(30s): git status --porcelain → wenn dirty, add+commit
     mit "auto: <ISO-Timestamp>"-Message
   - Re-Entrancy-Guard fuer langsame git-Ops

3. server.js — drei neue HTTP-Routen:
   GET  /api/files-versions?path=X
     → [{hash, ts, subject, isCurrent}] aus git log --follow
   GET  /api/files-version-content?path=X&hash=Y
     → Binary-Stream der Datei aus diesem Commit (Content-Disposition
       attachment mit "name@<short-hash>.ext" als Default-Dateiname)
   POST /api/files-version-restore body={path, hash}
     → non-destructive: schreibt alten Inhalt als NEUE Version, neuer
       Commit "restore: <path> <- <short>". Aktive Version damit
       weiterhin rollback-bar.

4. index.html — Datei-Manager:
   - Pro Datei zusaetzlich 🕒-Button neben ⬇/🗑
   - Klick zeigt Modal mit Version-Liste (timestamp, short-hash,
     'AKTIV'-Marker fuer den jeweils letzten)
   - Pro Version: ⬇ Download + ⟲ Restore (mit Confirm)
   - Restore broadcasted file_version_restored damit Browser refreshen

Path-Safety: alle Pfade muessen relative-to-uploads sein, kein '..',
kein '/', kein '.git/'. Hash muss [0-9a-f]{7,40}.

.gitignore zunaechst keine — uploads/ ist eh nur User-/ARIA-Dateien,
kein Log-Noise erwartet. Falls Disk explodiert: spaeter ergaenzen.

Step-2 (App-Side via RVS-Messages) folgt im naechsten Commit, sobald
das hier in Diagnostic funktioniert.
This commit is contained in:
2026-06-02 09:25:06 +02:00
parent c38e1b197b
commit 6464dbe28c
3 changed files with 85 additions and 1 deletions
+2 -1
View File
@@ -1,7 +1,8 @@
FROM node:22-alpine
WORKDIR /app
# zip fuer Multi-Datei-Downloads (Brain-Export nutzt tar.gz, Datei-Manager zip)
RUN apk add --no-cache zip
# git fuer Auto-Versionierung von /shared/uploads/ (siehe server.js)
RUN apk add --no-cache zip git
COPY package.json ./
RUN npm install --production
COPY . .
+83
View File
@@ -4039,11 +4039,83 @@
<div style="color:#555570;font-size:10px;">${fmtSize(f.size)} · ${fmtDate(f.mtime)}</div>
</div>
<button class="btn secondary" onclick="downloadFile('${encodeURIComponent(f.path)}')" style="padding:2px 8px;font-size:10px;" title="Herunterladen"></button>
<button class="btn secondary" onclick="showVersions('${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;" title="Versionen">🕒</button>
<button class="btn secondary" onclick="deleteFile('${pathEsc}','${escapeHtml(f.name)}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;border-color:#FF6B6B;" title="Loeschen">🗑</button>
</div>`;
}).join('');
}
// ── Versions-Modal ──────────────────────────────────────
async function showVersions(fileName) {
// path-relative-to-/shared/uploads ist hier == fileName, weil unser
// file-Manager-Verzeichnis flach ist
const rel = fileName;
const modal = document.getElementById('versions-modal');
const title = document.getElementById('versions-title');
const body = document.getElementById('versions-body');
title.textContent = `Versionen — ${fileName}`;
body.innerHTML = '<div style="color:#8888AA;text-align:center;padding:20px;">Lade...</div>';
modal.style.display = 'flex';
modal.dataset.path = rel;
try {
const r = await fetch('/api/files-versions?path=' + encodeURIComponent(rel));
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Fehler');
if (!d.versions.length) {
body.innerHTML = '<div style="color:#8888AA;text-align:center;padding:20px;">Noch keine Versions-Historie (Datei kommt erst nach naechstem Auto-Commit in den Index).</div>';
return;
}
const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
body.innerHTML = d.versions.map(v => {
const isCur = v.isCurrent
? '<span style="background:#34C75922;color:#34C759;padding:1px 6px;border-radius:3px;font-size:10px;margin-right:6px;">AKTIV</span>'
: '';
const subjShort = (v.subject || '').slice(0, 60);
return `<div style="padding:10px;border-bottom:1px solid #1E1E2E;display:flex;gap:8px;align-items:center;">
<div style="flex:1;min-width:0;">
<div style="color:#E0E0F0;font-size:12px;">${isCur}<code style="color:#0096FF;">${v.hash.slice(0,7)}</code> · ${escapeHtml(subjShort)}</div>
<div style="color:#555570;font-size:10px;">${fmtDate(v.ts)}</div>
</div>
<button class="btn secondary" onclick="downloadVersion('${escapeHtml(rel)}','${v.hash}')" style="padding:3px 10px;font-size:11px;">⬇ Download</button>
${v.isCurrent ? '' : `<button class="btn" onclick="restoreVersion('${escapeHtml(rel)}','${v.hash}')" style="padding:3px 10px;font-size:11px;background:#0096FF;color:#fff;">⟲ Restore</button>`}
</div>`;
}).join('');
} catch (e) {
body.innerHTML = `<div style="color:#FF6B6B;padding:20px;">${escapeHtml(e.message)}</div>`;
}
}
function closeVersionsModal() {
document.getElementById('versions-modal').style.display = 'none';
}
function downloadVersion(rel, hash) {
const url = '/api/files-version-content?path=' + encodeURIComponent(rel) + '&hash=' + encodeURIComponent(hash);
const a = document.createElement('a');
a.href = url;
a.download = '';
document.body.appendChild(a); a.click();
setTimeout(() => a.remove(), 100);
}
async function restoreVersion(rel, hash) {
if (!confirm(`Diese Version (${hash.slice(0,7)}) als aktive Version setzen?\n\nDie aktuelle Version bleibt rollback-bar in der Historie.`)) return;
try {
const r = await fetch('/api/files-version-restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: rel, hash }),
});
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'Fehler');
// Modal neu laden mit aktualisierter Liste
showVersions(rel);
loadFiles();
} catch (e) {
alert('Restore fehlgeschlagen: ' + e.message);
}
}
async function downloadSelected() {
const paths = [...filesSelected];
if (!paths.length) return;
@@ -5612,5 +5684,16 @@
// History gleich nach Seitenstart laden damit Browser-Reload nichts verliert.
loadAriaStreamHistory();
</script>
<!-- Versions-Modal fuer Datei-Manager -->
<div id="versions-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.75);z-index:1000;align-items:center;justify-content:center;" onclick="if(event.target===this)closeVersionsModal()">
<div style="background:#0D0D1A;border:1px solid #1E1E2E;border-radius:8px;width:90%;max-width:600px;max-height:80vh;display:flex;flex-direction:column;">
<div style="padding:12px 16px;border-bottom:1px solid #1E1E2E;display:flex;align-items:center;gap:8px;">
<strong id="versions-title" style="color:#E0E0F0;flex:1;font-size:13px;">Versionen</strong>
<button class="btn secondary" onclick="closeVersionsModal()" style="padding:4px 10px;font-size:11px;">✕ Schliessen</button>
</div>
<div id="versions-body" style="overflow-y:auto;padding:4px 12px;"></div>
</div>
</div>
</body>
</html>
Binary file not shown.