Update über den Git-Server, Tests, Repo ohne node_modules und Sicherungen

- Admin → Update: neuere Versionen (Tags) vom Git-Server anzeigen und per
  Knopf einspielen; ausgeführt vom Dienst auf dem Rechner
  (Docker/update-einrichten.sh, Docker/update-ausfuehren.sh), nur vorwärts,
  Fehler bei ausbleibendem Neustart, abgebrochene Läufe sperren nicht
- Tests (npm test): Zusammenführen paralleler Änderungen, Rechteprüfung
  beim Speichern; SESSIONS_DIR lenkt den Datenordner für Testserver um
- node_modules, Sicherungen und Uploads aus dem Repo genommen
- cors und requireRole() entfernt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
erikandClaude Opus 5.5 committed 2026-11-10 11:10:56 +01:00
1 parent e12936444d
commit fd44db2213
2357 files changed
+588 -514948

No files matched your search

+125 -1
View File
@@ -41,7 +41,7 @@
//
// Dass der Bereich für Nicht-Administratoren gar nicht erst auftaucht, ist
// wie überall in dieser App ein reines UI-Gate; die eigentliche Sperre sitzt
// serverseitig in requireRole('admin') auf PUT /app-settings,
// serverseitig in requireRecht() (je Abschnitt ein Recht) auf PUT /app-settings,
// PUT /honorar-config, POST/PATCH/DELETE /lph-templates, POST/DELETE /holidays,
// GET /backup, POST /import und POST /delete-week.
@@ -65,6 +65,7 @@ const ADMIN_SECTIONS = [
{ key: 'planchecker', title: 'Plan-Checker-Merkmale', recht: 'a.checker' },
{ key: 'betrieb', title: 'Betrieb & Sicherheit', recht: 'a.betrieb' },
{ key: 'teams', title: 'Teams', recht: 'a.teams' },
{ key: 'update', title: 'Update', recht: 'a.update' },
{ key: 'papierkorb', title: 'Papierkorb', recht: 'a.papierkorb' },
{ key: 'datenbank', title: 'Datenbank & Sitzungen', recht: ['a.backup', 'a.import'] },
];
@@ -168,6 +169,7 @@ function showAdminSection(key){
// frisch holen.
if(key === 'papierkorb') loadAdminPapierkorb();
if(key === 'teams') loadAdminTeams();
if(key === 'update') loadAdminUpdate();
if(key === 'rollen' && typeof renderRollenRechte === 'function') renderRollenRechte();
try {
@@ -1113,6 +1115,128 @@ async function adminProjektEndgueltigLoeschen(id){
// ---------- Teams (Admin → Teams, Server: /admin/teams*) ----------
// Die Webhook-Adresse ist ein Geheimnis und kommt nie zurück in den Browser;
// angezeigt wird nur, ob eine eingetragen ist (und an welchen Host).
// ---------- Update über den Git-Server (Admin → Update) ----------
//
// Versionen sind Tags vX.Y.Z auf dem Git-Server; gesucht wird nur auf
// Knopfdruck (GET /admin/update?suchen=1). „Update installieren“ legt einen
// Auftrag ab, den der Dienst auf dem Rechner ausführt
// (Docker/update-ausfuehren.sh). Währenddessen fragt die Karte alle 5 s nach
// dem Stand — auch über den Neustart des Servers hinweg, in dem die Anfrage
// scheitert.
let updateTimer = null;
let updateZiel = ''; // Version, auf die gerade aktualisiert wird
let updateSuche = null; // letzte Antwort von „Nach Updates suchen“
let updateBestaetigen = ''; // Version, deren Knopf auf „Wirklich?“ steht
function updateDatum(iso){
const d = iso ? new Date(iso) : null;
return d && !isNaN(d) ? d.toLocaleDateString('de-DE') + ', ' + d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : '';
}
async function loadAdminUpdate(suchen){
const el = document.getElementById('adminUpdateInhalt');
if(!el) return;
let d;
try {
d = await fetch(baseUrl() + '/admin/update' + (suchen ? '?suchen=1' : '')).then(r => r.json());
} catch(e){
// Während des Neustarts ist der Server kurz weg — dann weiter warten.
if(updateZiel){ el.innerHTML = updateLaufHtml({ version: updateZiel, schritt: 'Neustart', laeuft: true }, true); updateWeiterFragen(); }
else el.innerHTML = '<span class="settings-warn">Konnte nicht geladen werden.</span>';
return;
}
if(!d || !d.ok){ el.innerHTML = '<span class="settings-warn">Keine Berechtigung.</span>'; return; }
if(suchen) updateSuche = d;
const inst = d.installiert;
const st = d.status;
const laeuft = d.auftragOffen || (st && st.laeuft);
if(laeuft) updateZiel = (st && st.laeuft && st.version) || updateZiel;
let html = `<div class="teams-status${inst && inst.version ? ' ist-an' : ''}">
<span class="teams-punkt" aria-hidden="true"></span>
<span class="teams-status-text">${inst && inst.version
? '<b>Installiert: <span class="update-version">' + esc(inst.version) + '</span></b><span>' + esc(inst.commit || '') + (inst.am ? ' · seit ' + esc(updateDatum(inst.am)) : '') + '</span>'
: '<b>Installierte Version unbekannt</b><span>Wird beim Einrichten des Update-Dienstes vermerkt</span>'}</span>
</div>`;
if(!d.eingerichtet){
html += '<p class="settings-warn">Der Git-Server ist nicht eingetragen (UPDATE_REPO_API in docker-compose.yml).</p>';
} else if(!d.dienst){
html += '<p class="settings-warn">Der Update-Dienst ist auf diesem Rechner nicht eingerichtet. Einmal im Repo ausführen: <code>sudo bash Docker/update-einrichten.sh</code></p>';
}
if(laeuft){
html += updateLaufHtml(st && st.laeuft ? st : { version: updateZiel, schritt: 'Auftrag übergeben', laeuft: true });
el.innerHTML = html;
updateWeiterFragen();
return;
}
if(updateZiel){
// Lauf beendet: fertig oder Fehler.
const ok = st && !st.fehler && inst && inst.version === updateZiel;
html += ok
? `<div class="update-meldung ist-ok"><b>Aktualisiert auf ${esc(updateZiel)}</b><span>Bitte die Seite neu laden, damit der Browser die neue Version verwendet.</span>
<button type="button" class="small" onclick="location.reload()">Seite neu laden</button></div>`
: '';
if(ok) updateZiel = '';
}
if(st && st.fehler){
html += `<div class="update-meldung ist-fehler"><b>Update auf ${esc(st.version || '')} fehlgeschlagen</b>
<span>${esc(st.schritt || '')}: ${esc(st.fehler)}${st.am ? ' (' + esc(updateDatum(st.am)) + ')' : ''}</span>
${d.log ? '<pre class="update-log">' + esc(d.log) + '</pre>' : ''}</div>`;
updateZiel = '';
}
const s = updateSuche;
if(s && s.fehler) html += '<p class="settings-warn">' + esc(s.fehler) + '</p>';
else if(s && s.neuer && !s.neuer.length) html += '<p class="hint">Keine neuere Version. Neueste auf dem Git-Server: <span class="update-version">' + esc(s.neueste || '–') + '</span></p>';
else if(s && s.neuer){
const neueste = s.neuer[0].version;
html += '<div class="update-liste">' + s.neuer.map(t => `<div class="update-eintrag">
<span class="update-version">${esc(t.version)}</span>
<span class="update-text">${esc(t.text.replace(/^v\d+\.\d+\.\d+:\s*/, '') || 'ohne Beschreibung')}</span>
<span class="update-datum">${esc(updateDatum(t.datum))}</span></div>`).join('') + '</div>';
const wirklich = updateBestaetigen === neueste;
html += `<div class="teams-aktionen">
<button type="button" class="small${wirklich ? ' danger' : ''}" ${d.dienst && inst && inst.version ? '' : 'disabled'} onclick="updateInstallieren('${esc(neueste)}')">
${wirklich ? 'Wirklich? App ist einige Minuten weg' : 'Update auf ' + esc(neueste) + ' installieren'}</button>
${wirklich ? '<button type="button" class="secondary small" onclick="updateBestaetigen=\'\';loadAdminUpdate()">Abbrechen</button>' : ''}
</div>`;
}
html += `<div class="teams-aktionen"><button type="button" class="secondary small" ${d.eingerichtet ? '' : 'disabled'} onclick="loadAdminUpdate(true)">Nach Updates suchen</button></div>`;
el.innerHTML = html;
}
// updateLaufHtml: Fortschritt eines laufenden Updates.
function updateLaufHtml(st, serverWeg){
const schritte = ['Version holen', 'Container bauen', 'Neustart'];
const i = Math.max(0, schritte.indexOf(st.schritt));
return `<div class="update-meldung ist-lauf"><b>Update auf ${esc(st.version || '')} läuft</b>
<ol class="update-schritte">${schritte.map((x, j) => `<li class="${j < i ? 'ist-fertig' : (j === i ? 'ist-jetzt' : '')}">${esc(x)}</li>`).join('')}</ol>
<span>${serverWeg ? 'Der Server startet gerade neu …' : 'Das Bauen dauert auf dem Pi einige Minuten. Diese Seite kann offen bleiben.'}</span></div>`;
}
function updateWeiterFragen(){
clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
if(currentTab === 'admin' && adminSection === 'update') loadAdminUpdate();
}, 5000);
}
async function updateInstallieren(version){
if(updateBestaetigen !== version){ updateBestaetigen = version; loadAdminUpdate(); return; }
updateBestaetigen = '';
try {
const r = await fetch(baseUrl() + '/admin/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version }) });
const d = await r.json().catch(() => ({}));
if(!r.ok || !d.ok){ alert(d.error || 'Update konnte nicht gestartet werden.'); return loadAdminUpdate(); }
updateZiel = version;
updateSuche = null;
} catch(e){ alert('Server nicht erreichbar.'); }
loadAdminUpdate();
}
async function loadAdminTeams(){
const el = document.getElementById('adminTeamsInhalt');
if(!el) return;