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:
1 parent
e12936444d
commit
fd44db2213
2357 files changed
+588
-514948
No files matched your search
+11
-2
@@ -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 & 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 & 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 & 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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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; } }
|
||||
Reference in new issue
Block a user