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:
@@ -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/<email>.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);
|
||||
|
||||
Reference in New Issue
Block a user