From 6464dbe28c141204f3b39962e47c9a24410bf8b7 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Tue, 2 Jun 2026 09:25:06 +0200 Subject: [PATCH] feat(diagnostic): Auto-Versionierung fuer /shared/uploads/ + Versions-UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: "-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@.ext" als Default-Dateiname) POST /api/files-version-restore body={path, hash} → non-destructive: schreibt alten Inhalt als NEUE Version, neuer Commit "restore: <- ". 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. --- diagnostic/Dockerfile | 3 +- diagnostic/index.html | 83 ++++++++++++++++++++++++++++++++++++++++++ diagnostic/server.js | Bin 107121 -> 115929 bytes 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/diagnostic/Dockerfile b/diagnostic/Dockerfile index 5f039bc..a6d5948 100644 --- a/diagnostic/Dockerfile +++ b/diagnostic/Dockerfile @@ -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 . . diff --git a/diagnostic/index.html b/diagnostic/index.html index fce8737..4f7f1d1 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -4039,11 +4039,83 @@
${fmtSize(f.size)} · ${fmtDate(f.mtime)}
+ `; }).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 = '
Lade...
'; + 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 = '
Noch keine Versions-Historie (Datei kommt erst nach naechstem Auto-Commit in den Index).
'; + 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 + ? 'AKTIV' + : ''; + const subjShort = (v.subject || '').slice(0, 60); + return `
+
+
${isCur}${v.hash.slice(0,7)} · ${escapeHtml(subjShort)}
+
${fmtDate(v.ts)}
+
+ + ${v.isCurrent ? '' : ``} +
`; + }).join(''); + } catch (e) { + body.innerHTML = `
${escapeHtml(e.message)}
`; + } + } + + 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(); + + + diff --git a/diagnostic/server.js b/diagnostic/server.js index b51d2cdc37a6f587f6dda4c5123decac9cb4c2a3..edb65829912e8cd0c77945a54a028360dfa04fca 100644 GIT binary patch delta 6187 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 delta 30 mcmccF$^P*O+Xgv-&F#t)pH4UMW#pc|PMVQ-yS@bDsx<(}$qTsv