From 02776311c37f02a56ed5e02047ec6bc39bb22de0 Mon Sep 17 00:00:00 2001 From: Kendrick Bollens Date: Wed, 22 Jul 2026 14:42:23 +0200 Subject: [PATCH] =?UTF-8?q?Release=20v2.5.0=20=E2=80=94=20Client-Versions-?= =?UTF-8?q?=C3=9Cbersicht=20im=20Web-Editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jeder Client meldet beim Start seinen Status (Name, Abteilung, Plugin- und Thunderbird-Version) als _clients/.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) Claude-Session: https://claude.ai/code/session_015PZHd9vRsBiH8cKYdQo1Yv --- lib/gitea-sync.js | 66 ++++++++++++++++++++++++++++++++- manifest.json | 2 +- templates-reply-hotel.xpi | Bin 681621 -> 682351 bytes web-editor/public/app.js | 71 +++++++++++++++++++++++++++++++++++- web-editor/public/style.css | 13 +++++++ web-editor/server.js | 32 ++++++++++++++-- 6 files changed, 177 insertions(+), 7 deletions(-) diff --git a/lib/gitea-sync.js b/lib/gitea-sync.js index 7bd5a55..6344bf3 100644 --- a/lib/gitea-sync.js +++ b/lib/gitea-sync.js @@ -8,6 +8,7 @@ const FULL_SYNC_COOLDOWN_MS = 10 * 1000; // min 10s between full pulls const SHARED_FOLDER = '_gemeinsam'; const USER_FOLDER = '_benutzer'; const CONFIG_FOLDER = '_config'; +const CLIENTS_FOLDER = '_clients'; const SCHLAGWOERTER_CACHE_KEY = 'schlagwoerter_cache'; // ── Gitea API Client ── @@ -190,6 +191,59 @@ class SyncManager { return this.config?.authorEmail || ''; } + get authorName() { + return this.config?.authorName || ''; + } + + /** + * Meldet den eigenen Client-Status (Name, Abteilung, Plugin- + TB-Version) + * als _clients/.json ins Repo — Basis für die Admin-Übersicht im + * Web-Editor. Schreibt NUR, wenn sich tatsächlich etwas geändert hat + * (Plugin-Version, TB-Version, Name oder Abteilung) — sonst kein Commit. + */ + async reportClientStatus() { + if (!this.isConfigured) return { success: false, skipped: 'not-configured' }; + const email = this.authorEmail.trim().toLowerCase(); + if (!email) return { success: false, skipped: 'no-email' }; + + let tbVersion = ''; + try { + const info = await browser.runtime.getBrowserInfo(); + tbVersion = [info.name, info.version].filter(Boolean).join(' '); + } catch (_) {} + + const status = { + name: this.authorName, + email, + department: this.department, + pluginVersion: browser.runtime.getManifest().version, + tbVersion, + lastSeen: new Date().toISOString() + }; + + const filepath = `${CLIENTS_FOLDER}/${email}.json`; + const body = JSON.stringify(status, null, 2); + const commitMsg = `Client-Status: ${email} (v${status.pluginVersion})`; + + const existing = await this.client.getFile(filepath); + if (existing && existing.content) { + try { + const prev = JSON.parse(GiteaClient.fromBase64(existing.content)); + const unchanged = prev.name === status.name + && prev.department === status.department + && prev.pluginVersion === status.pluginVersion + && prev.tbVersion === status.tbVersion; + if (unchanged) { + return { success: true, skipped: 'unchanged' }; + } + } catch (_) { /* kaputter Vorgänger → einfach überschreiben */ } + await this.client.updateFile(filepath, body, existing.sha, commitMsg); + } else { + await this.client.createFile(filepath, body, commitMsg); + } + return { success: true, status }; + } + async autoDetect() { if (!this.isConfigured) return null; return await this.client.getConfig(); @@ -233,7 +287,7 @@ class SyncManager { const entries = await this.client.listDir(''); const departments = []; for (const entry of entries) { - if (entry.type === 'dir' && entry.name !== SHARED_FOLDER && entry.name !== USER_FOLDER && entry.name !== CONFIG_FOLDER && entry.name !== 'signatures' && !entry.name.startsWith('.')) { + if (entry.type === 'dir' && entry.name !== SHARED_FOLDER && entry.name !== USER_FOLDER && entry.name !== CONFIG_FOLDER && entry.name !== CLIENTS_FOLDER && entry.name !== 'signatures' && !entry.name.startsWith('.')) { departments.push(entry.name); } } @@ -968,6 +1022,7 @@ let lastKnownShas = null; let lastFullSync = 0; let lastTagSync = 0; let syncInProgress = false; +let clientStatusReported = false; const TAG_SYNC_INTERVAL_MS = 60 * 1000; const HASH_STORAGE_KEY_BG = 'sync_hashes'; @@ -1009,6 +1064,15 @@ async function smartSync() { const initialized = await syncManager.init(); if (!initialized) return; + // Einmal pro Session den eigenen Client-Status melden (Admin-Übersicht). + // Fire-and-forget, blockiert den Sync nicht. + if (!clientStatusReported) { + clientStatusReported = true; + syncManager.reportClientStatus() + .then((r) => { if (r?.status) console.log('[Sync] Client-Status gemeldet:', r.status.email, 'v' + r.status.pluginVersion); }) + .catch((e) => console.error('[Sync] Client-Status fehlgeschlagen:', e)); + } + syncInProgress = true; // Tag sync every 60s (schlagwoerter.json is not in SHA-checked folders) diff --git a/manifest.json b/manifest.json index 833100f..76d078e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "HPS Vorlagen & Signaturen", - "version": "2.4.0", + "version": "2.5.0", "description": "Vorlagen- und Signaturverwaltung für Hotel Park Soltau mit Git-Sync", "browser_specific_settings": { "gecko": { diff --git a/templates-reply-hotel.xpi b/templates-reply-hotel.xpi index 227a645c8efe142ca7920d42e71750297da9e6c3..614f2a4765178bfccc19d4bc2aba27ccdc0ee92b 100644 GIT binary patch delta 10026 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 delta 9358 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', 'chevron': '', 'plug': '', + 'users': '', }; 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: ' + esc(slug) + '.html' : '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 (ab). + 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/.json).'; + el.adminPanel.innerHTML = adminHeader('User & Versionen', sub) + '
Lädt…
'; + 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) + + '
Noch keine Client-Meldungen. Sobald Clients mit der neuen Plugin-Version starten, tauchen sie hier auf.
'; + 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 ? ' veraltet' : ''); + return '' + + '' + esc(c.name || '—') + '' + + '' + esc(c.email) + '' + + '' + esc(c.department || '—') + '' + + '' + ver + '' + + '' + esc(c.tbVersion || '—') + '' + + ''; + }).join(''); + + const summary = clients.length + ' Client' + (clients.length === 1 ? '' : 's') + + ' · ' + onLatest + ' auf neuester Version' + (latest ? ' (' + esc(latest) + ')' : ''); + + el.adminPanel.innerHTML = adminHeader('User & Versionen', summary) + + '
' + + '' + + '' + rows + '
NameE-MailAbteilungPluginThunderbird
'; } function adminHeader(title, subtitle) { return '

' + esc(title) + '

' + (subtitle ? '

' + esc(subtitle) + '

' : '') + '
'; diff --git a/web-editor/public/style.css b/web-editor/public/style.css index ee50426..f2c5406 100644 --- a/web-editor/public/style.css +++ b/web-editor/public/style.css @@ -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) { diff --git a/web-editor/server.js b/web-editor/server.js index 1a83d9c..5dcb073 100644 --- a/web-editor/server.js +++ b/web-editor/server.js @@ -23,6 +23,7 @@ const { const SHARED_FOLDER = '_gemeinsam'; const USER_FOLDER = '_benutzer'; const CONFIG_FOLDER = '_config'; +const CLIENTS_FOLDER = '_clients'; const SIG_FOOTERS = 'signatures/footers'; const SIG_HEADERS = 'signatures/headers'; @@ -421,7 +422,7 @@ app.get('/api/departments', wrap(async (_req, res) => { const entries = await listDir(''); const departments = entries .filter(e => e.type === 'dir' - && ![SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, 'signatures'].includes(e.name) + && ![SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, CLIENTS_FOLDER, 'signatures'].includes(e.name) && !e.name.startsWith('.')) .map(e => e.name) .sort((a, b) => a.localeCompare(b, 'de')); @@ -450,7 +451,7 @@ app.get('/api/files', wrap(async (req, res) => { // Full inventory in ONE request (recursive tree), strukturiert aus den Pfaden. app.get('/api/tree', wrap(async (_req, res) => { const files = await listAllFiles(); - const special = [SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, 'signatures']; + const special = [SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, CLIENTS_FOLDER, 'signatures']; const dirOf = (p) => { const i = p.lastIndexOf('/'); return i < 0 ? '' : p.slice(0, i); }; const baseOf = (p) => p.slice(p.lastIndexOf('/') + 1); @@ -511,6 +512,31 @@ app.delete('/api/file', wrap(async (req, res) => { res.json({ success: true }); })); +// Client-Status-Übersicht: liest alle _clients/.json und liefert die +// gemeldeten Plugin-/TB-Versionen je User (Basis für den Admin-Tab). +app.get('/api/clients', wrap(async (_req, res) => { + const entries = await listDir(CLIENTS_FOLDER); + const clients = []; + for (const e of entries) { + if (e.type !== 'file' || !e.name.endsWith('.json')) continue; + const data = await getFile(e.path); + if (!data) continue; + try { + const c = JSON.parse(fromBase64(data.content)); + clients.push({ + name: c.name || '', + email: c.email || e.name.replace(/\.json$/, ''), + department: c.department || '', + pluginVersion: c.pluginVersion || '', + tbVersion: c.tbVersion || '', + lastSeen: c.lastSeen || '', + }); + } catch (_) { /* kaputte Statusdatei überspringen */ } + } + clients.sort((a, b) => (a.name || a.email).localeCompare(b.name || b.email, 'de')); + res.json({ clients }); +})); + // Read/write the email→department mapping. app.get('/api/abteilungen', wrap(async (_req, res) => { const data = await getFile(`${CONFIG_FOLDER}/abteilungen.json`); @@ -548,7 +574,7 @@ app.put('/api/schlagwoerter', wrap(async (req, res) => { app.delete('/api/departments', wrap(async (req, res) => { const name = (req.body?.name || '').trim(); if (!name) return res.status(400).json({ error: 'Kein Name angegeben' }); - if ([SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, 'signatures'].includes(name) || name.includes('/') || name.includes('..')) { + if ([SHARED_FOLDER, USER_FOLDER, CONFIG_FOLDER, CLIENTS_FOLDER, 'signatures'].includes(name) || name.includes('/') || name.includes('..')) { return res.status(400).json({ error: 'Geschützter oder ungültiger Ordner' }); } const entries = await listDir(name);