Release v2.5.0 — Client-Versions-Übersicht im Web-Editor
Jeder Client meldet beim Start seinen Status (Name, Abteilung, Plugin- und Thunderbird-Version) als _clients/<email>.json ins Gitea-Repo — aber nur, wenn sich tatsächlich etwas geändert hat (kein Commit-Spam pro Start). Web-Editor: neuer Admin-Unterpunkt "User & Versionen" (Route /api/clients) mit Tabelle je User und "veraltet"-Markierung für Clients, die nicht auf der neuesten gemeldeten Version laufen. _clients/ ist in den Ordner-Filtern ausgenommen, damit es nicht als Abteilung erscheint. Nebenbei: Web-Editor-Button-Label "Abteilung" -> "Vorlage" (Templates-Tab). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015PZHd9vRsBiH8cKYdQo1Yv
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
'link2': '<path d="M9 17H7A5 5 0 0 1 7 7h2"/><path d="M15 7h2a5 5 0 1 1 0 10h-2"/><line x1="8" x2="16" y1="12" y2="12"/>',
|
||||
'chevron': '<path d="m6 9 6 6 6-6"/>',
|
||||
'plug': '<path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/>',
|
||||
'users': '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
||||
};
|
||||
function icon(name, size) {
|
||||
const s = size || 18;
|
||||
@@ -213,7 +214,7 @@
|
||||
const isAdmin = cat === 'admin';
|
||||
el.btnListAdd.style.display = isAdmin ? 'none' : '';
|
||||
el.treeSearch.parentElement.style.display = isAdmin ? 'none' : '';
|
||||
if (cat === 'templates') el.btnListAddLabel.textContent = 'Abteilung';
|
||||
if (cat === 'templates') el.btnListAddLabel.textContent = 'Vorlage';
|
||||
else if (cat === 'footers') el.btnListAddLabel.textContent = 'Fußzeile';
|
||||
else if (cat === 'headers') el.btnListAddLabel.textContent = 'Signatur';
|
||||
renderList();
|
||||
@@ -309,6 +310,7 @@
|
||||
wrap.appendChild(adminNavItem('departments', 'Abteilungen', 'building'));
|
||||
wrap.appendChild(adminNavItem('mapping', 'E-Mail-Zuordnung', 'at-sign'));
|
||||
wrap.appendChild(adminNavItem('tags', 'Schlagwörter', 'tag'));
|
||||
wrap.appendChild(adminNavItem('clients', 'User & Versionen', 'users'));
|
||||
c.appendChild(wrap);
|
||||
return;
|
||||
}
|
||||
@@ -528,6 +530,21 @@
|
||||
const path = folder + '/' + slug + '.html'; if (existsInTree(path)) { toast('Eine Vorlage mit diesem Namen existiert bereits.', 'error'); return; }
|
||||
openNewFile(path, slug, 'template');
|
||||
}
|
||||
// Vorlage anlegen mit Ordner-Auswahl (Haupt-„+"-Button). Das kleine „+" am
|
||||
// Gruppenkopf nutzt weiterhin newTemplate(folder) direkt.
|
||||
async function newTemplatePrompt() {
|
||||
const t = state.tree || {};
|
||||
const options = [{ value: SHARED_FOLDER, label: 'Alle Abteilungen' }]
|
||||
.concat((t.departments || []).map((d) => ({ value: d, label: d })));
|
||||
const res = await promptModal('Neue Vorlage', [
|
||||
{ key: 'folder', label: 'Für welche Abteilung?', type: 'select', options, required: true },
|
||||
{ key: 'name', label: 'Vorlagenname', placeholder: 'z. B. Angebot Doppelzimmer', required: true, live: true },
|
||||
], (values, inputs, root) => { const slug = slugifyName(values.name || ''); const h = root.querySelector('[data-live="name"]'); if (h) h.innerHTML = slug ? 'Datei: <span class="preview-name">' + esc(slug) + '.html</span>' : 'Bitte einen Namen eingeben.'; });
|
||||
if (!res) return; const slug = slugifyName(res.name); if (!slug) { toast('Ungültiger Name.', 'error'); return; }
|
||||
const folder = res.folder || SHARED_FOLDER; const path = folder + '/' + slug + '.html';
|
||||
if (existsInTree(path)) { toast('Eine Vorlage mit diesem Namen existiert bereits.', 'error'); return; }
|
||||
openNewFile(path, slug, 'template');
|
||||
}
|
||||
async function newFooter() {
|
||||
const t = state.tree || {};
|
||||
const options = [{ value: '_default', label: 'Gemeinsam (alle Abteilungen)' }].concat((t.departments || []).map((d) => ({ value: d, label: d })));
|
||||
@@ -554,7 +571,7 @@
|
||||
try { const r = await api('/api/departments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }) }); toast('Abteilung „' + (r.name || name) + '“ angelegt.', 'success'); await loadTree(); if (state.category === 'admin') setAdminView('departments'); }
|
||||
catch (e) { toast('Abteilung anlegen fehlgeschlagen: ' + e.message, 'error'); }
|
||||
}
|
||||
function listAddAction() { if (state.category === 'templates') newDepartment(); else if (state.category === 'footers') newFooter(); else if (state.category === 'headers') newHeader(); }
|
||||
function listAddAction() { if (state.category === 'templates') newTemplatePrompt(); else if (state.category === 'footers') newFooter(); else if (state.category === 'headers') newHeader(); }
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Verwaltung (Admin)
|
||||
@@ -568,6 +585,56 @@
|
||||
else if (view === 'departments') renderDepartments();
|
||||
else if (view === 'mapping') renderMapping();
|
||||
else if (view === 'tags') renderTags();
|
||||
else if (view === 'clients') renderClients();
|
||||
}
|
||||
|
||||
// Semver-ähnlicher Vergleich: -1 / 0 / 1 (a<b / a==b / a>b).
|
||||
function cmpVersion(a, b) {
|
||||
const pa = String(a || '').split('.').map((n) => parseInt(n, 10) || 0);
|
||||
const pb = String(b || '').split('.').map((n) => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const d = (pa[i] || 0) - (pb[i] || 0);
|
||||
if (d) return d < 0 ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function renderClients() {
|
||||
const sub = 'Welche Plugin- und Thunderbird-Version läuft bei wem. Clients melden sich beim Start (Datei _clients/<email>.json).';
|
||||
el.adminPanel.innerHTML = adminHeader('User & Versionen', sub) + '<div class="tree-empty">Lädt…</div>';
|
||||
let clients = [];
|
||||
try { const r = await api('/api/clients'); clients = r.clients || []; }
|
||||
catch (e) { toast('Clients nicht ladbar: ' + e.message, 'error'); }
|
||||
|
||||
if (!clients.length) {
|
||||
el.adminPanel.innerHTML = adminHeader('User & Versionen', sub) +
|
||||
'<div class="tree-empty">Noch keine Client-Meldungen. Sobald Clients mit der neuen Plugin-Version starten, tauchen sie hier auf.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Neueste gemeldete Plugin-Version → alle anderen sind „veraltet".
|
||||
const latest = clients.map((c) => c.pluginVersion).filter(Boolean).sort(cmpVersion).slice(-1)[0] || '';
|
||||
const onLatest = clients.filter((c) => c.pluginVersion === latest).length;
|
||||
|
||||
const rows = clients.map((c) => {
|
||||
const outdated = latest && c.pluginVersion && c.pluginVersion !== latest;
|
||||
const ver = esc(c.pluginVersion || '—') + (outdated ? ' <span class="cl-badge cl-old">veraltet</span>' : '');
|
||||
return '<tr' + (outdated ? ' class="cl-row-old"' : '') + '>' +
|
||||
'<td>' + esc(c.name || '—') + '</td>' +
|
||||
'<td class="cl-mail">' + esc(c.email) + '</td>' +
|
||||
'<td>' + esc(c.department || '—') + '</td>' +
|
||||
'<td>' + ver + '</td>' +
|
||||
'<td>' + esc(c.tbVersion || '—') + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
|
||||
const summary = clients.length + ' Client' + (clients.length === 1 ? '' : 's') +
|
||||
' · ' + onLatest + ' auf neuester Version' + (latest ? ' (' + esc(latest) + ')' : '');
|
||||
|
||||
el.adminPanel.innerHTML = adminHeader('User & Versionen', summary) +
|
||||
'<div class="cl-tablewrap"><table class="cl-table">' +
|
||||
'<thead><tr><th>Name</th><th>E-Mail</th><th>Abteilung</th><th>Plugin</th><th>Thunderbird</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody></table></div>';
|
||||
}
|
||||
function adminHeader(title, subtitle) {
|
||||
return '<div class="admin-head"><h2>' + esc(title) + '</h2>' + (subtitle ? '<p>' + esc(subtitle) + '</p>' : '') + '</div>';
|
||||
|
||||
@@ -279,6 +279,19 @@ button { font-family: inherit; }
|
||||
.list-body::-webkit-scrollbar, .html-editor::-webkit-scrollbar, .editorpane::-webkit-scrollbar { width: 10px; }
|
||||
.list-body::-webkit-scrollbar-thumb, .html-editor::-webkit-scrollbar-thumb, .editorpane::-webkit-scrollbar-thumb { background: #cdd6c9; border-radius: 10px; border: 2px solid transparent; background-clip: content-box; }
|
||||
|
||||
/* ── User & Versionen (Clients) ── */
|
||||
.cl-tablewrap { margin-top: 6px; overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--panel); }
|
||||
.cl-table { width: 100%; border-collapse: collapse; font-size: 13.5px; }
|
||||
.cl-table th, .cl-table td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
||||
.cl-table thead th { font-size: 12px; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: .03em; background: var(--bg); }
|
||||
.cl-table tbody tr:last-child td { border-bottom: none; }
|
||||
.cl-table tbody tr:hover { background: var(--bg); }
|
||||
.cl-table .cl-mail { color: var(--muted); font-family: var(--mono); font-size: 12.5px; }
|
||||
.cl-row-old td { background: var(--danger-50); }
|
||||
.cl-row-old:hover td { background: #fbe3e1; }
|
||||
.cl-badge { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px; font-size: 11px; font-weight: 700; vertical-align: middle; }
|
||||
.cl-badge.cl-old { background: var(--danger-50); color: var(--danger); border: 1px solid #ecc4c2; }
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 980px) { .workspace { grid-template-columns: 250px 1fr; } .stat-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 760px) {
|
||||
|
||||
Reference in New Issue
Block a user