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

+11 -2
View File
@@ -38,7 +38,7 @@
<link rel="icon" type="image/png" href="/logo.png">
<link rel="alternate icon" href="/logo.ico">
<link rel="apple-touch-icon" href="/logo.png">
<link rel="stylesheet" href="/styles.css?v=124">
<link rel="stylesheet" href="/styles.css?v=125">
<link rel="stylesheet" href="/plan-styles.css?v=57">
<!-- Glas-Darstellung (public/glas.css, als letztes Stylesheet): greift nur mit
der Klasse html.glas. Gesetzt wird sie hier, vor dem ersten Zeichnen,
@@ -205,6 +205,7 @@
<div class="sidebar-subgruppe">System</div>
<button class="menu-item sidebar-subitem" data-admin-section="betrieb" onclick="showAdminSection('betrieb')">Betrieb &amp; Sicherheit</button>
<button class="menu-item sidebar-subitem" data-admin-section="teams" onclick="showAdminSection('teams')">Teams</button>
<button class="menu-item sidebar-subitem" data-admin-section="update" onclick="showAdminSection('update')">Update</button>
<button class="menu-item sidebar-subitem" data-admin-section="papierkorb" onclick="showAdminSection('papierkorb')">Papierkorb</button>
<button class="menu-item sidebar-subitem" data-admin-section="datenbank" onclick="showAdminSection('datenbank')">Datenbank &amp; Sitzungen</button>
</div>
@@ -680,6 +681,14 @@
leicht abweichen.</p>
</details>
</section>
<!-- Update über den Git-Server (loadAdminUpdate(), js/admin/admin.js; Server: /admin/update) -->
<section class="settings-card" id="adminUpdateCard" data-admin-section="update" style="display:none">
<h3 class="settings-card-title">Update <span class="settings-scope">nur Administratoren</span></h3>
<p class="hint">Neue Versionen entstehen als Tag (z. B. v1.1.8) auf dem Main-Branch des Git-Servers.
„Update installieren“ holt die Version auf diesen Rechner und startet die App neu. Dabei ist sie
einige Minuten nicht erreichbar; vor dem Neustart sichert sie die Datenbank wie bei jedem Start.</p>
<div id="adminUpdateInhalt"></div>
</section>
<section class="settings-card" id="adminBetriebCard" data-admin-section="betrieb" style="display:none">
<h3 class="settings-card-title">Betrieb &amp; Sicherheit <span class="settings-scope">firmenweit · nur Administratoren</span></h3>
<div class="settings-field-grid">
@@ -2024,7 +2033,7 @@
Tab-Ordner, keine neue Abhängigkeit nach vorn. -->
<script src="/js/wand/wand.js?v=22"></script>
<script src="/js/settings/settings.js?v=14"></script>
<script src="/js/admin/admin.js?v=24"></script>
<script src="/js/admin/admin.js?v=26"></script>
<script src="/js/admin-users.js?v=10"></script>
<script src="/js/admin/rollen.js?v=1"></script>
<script src="/js/einfuehrung.js?v=3"></script>
+1 -1
View File
@@ -16,7 +16,7 @@
// Seitenleiste bleibt stehen. Eine eigene Anmeldepruefung braucht es hier
// nicht mehr — der Bereich ist ohnehin nur fuer Administratoren erreichbar
// (showTab() in js/tabs-auth.js), und die eigentliche Sicherheitsgrenze sind
// die requireRole('admin')-Endpunkte auf dem Server.
// die Endpunkte mit requireRecht('a.benutzer') auf dem Server.
(function(){
// usersCache: zuletzt vom Server geladene Benutzerliste (für die Modale,
+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;
+1 -1
View File
@@ -5,7 +5,7 @@
// erzeugte .db-Datei aus, lädt sie als rohen Binär-Body an POST /import und
// lädt nach erfolgreicher Wiederherstellung die Seite neu. Da der Import den
// kompletten Datenbestand ersetzt, ist die Route serverseitig admin-only
// abgesichert (requireRole('admin'), server.js); der Button ist im UI
// abgesichert (requireRecht('a.import'), server.js); der Button ist im UI
// zusätzlich nur für Admins sichtbar (js/auth.js: renderUserBar()). Zusätzlich
// wird der Import im Browser per Bestätigungsdialog abgesichert.
+27
View File
@@ -6523,3 +6523,30 @@ button.ba-zeile:focus-visible { outline: 2px solid var(--accent); outline-offset
.ba-raute { width: 9px; height: 9px; margin-left: 2px; background: var(--meilenstein); transform: rotate(45deg); }
.ba-leer, .ba-hinweis { font-size: 12px; color: var(--text-subtle); padding: 4px 8px; }
.ba-hinweis { padding: 8px 0 0; }
/* ===========================================================================
ADMIN → UPDATE (loadAdminUpdate() in js/admin/admin.js): Statuszeile wie
bei Teams, Liste der neueren Versionen, Fortschritt eines laufenden Updates.
=========================================================================== */
#adminUpdateInhalt { display: grid; gap: 12px; }
.update-version { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.update-liste { display: grid; border: 1px solid var(--line-soft); border-radius: 8px; overflow: hidden; }
.update-eintrag { display: grid; grid-template-columns: 72px minmax(0, 1fr) auto; gap: 12px; align-items: baseline; padding: 8px 12px; font-size: 13px; }
.update-eintrag + .update-eintrag { border-top: 1px solid var(--line-soft); }
.update-eintrag .update-text { color: var(--text-muted); }
.update-eintrag .update-datum { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); white-space: nowrap; }
.update-meldung { display: grid; gap: 4px; padding: 12px 16px; border-radius: 8px; background: var(--surface-sunk); font-size: 13px; }
.update-meldung > span { color: var(--text-muted); }
.update-meldung > button { justify-self: start; margin-top: 4px; }
.update-meldung.ist-ok { box-shadow: inset 3px 0 0 var(--green); }
.update-meldung.ist-fehler { box-shadow: inset 3px 0 0 var(--red); }
.update-meldung.ist-lauf { box-shadow: inset 3px 0 0 var(--accent); }
.update-schritte { list-style: none; margin: 4px 0; padding: 0; display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; color: var(--text-subtle); }
.update-schritte li { display: inline-flex; align-items: center; gap: 6px; }
.update-schritte li::before { content: ""; width: 8px; height: 8px; border-radius: 50%; border: 1px solid var(--line); }
.update-schritte li.ist-fertig::before { background: var(--green); border-color: var(--green); }
.update-schritte li.ist-jetzt { color: var(--text); }
.update-schritte li.ist-jetzt::before { background: var(--accent); border-color: var(--accent); }
.update-log { margin: 4px 0 0; max-height: 200px; overflow: auto; padding: 8px; border-radius: 6px; background: var(--surface);
font-family: var(--font-mono); font-size: 11px; line-height: 16px; white-space: pre-wrap; color: var(--text-muted); }
@media (max-width: 560px) { .update-eintrag { grid-template-columns: 72px minmax(0, 1fr); } .update-eintrag .update-datum { grid-column: 2; } }