Auto-Update über Gitea einrichten + Web-Editor + Sync-Verbesserungen

- Thunderbird Auto-Update: update_url im Manifest, updates.json, release.sh
- .xpi neu gebaut (mit update_url, ohne defaults.local.json/Token)
- README + CLAUDE.md: Auto-Update-Doku, Repo muss public bleiben
- web-editor/ (Node/Docker WYSIWYG-Editor) hinzugefügt
- gitea-sync.js + templates_options: bestehende Anpassungen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kendrick Bollens
2026-06-18 00:12:33 +02:00
parent edb979a1b2
commit eff90e9517
23 changed files with 3437 additions and 41 deletions

View File

@@ -1119,6 +1119,42 @@ function combineSignature(header, footer) {
return header + '\n' + FOOTER_SEPARATOR + '\n' + footer;
}
// Wendet den Footer lokal auf alle eingerichteten Identitäten an,
// indem der vorhandene Header (lokal) mit dem neuen Footer neu kombiniert wird.
// Funktioniert auch ohne hochgeladenen Server-Header.
async function applyFooterLocally(footer) {
const sourceMap = await getSigSourceMap();
let applied = 0;
for (const identity of allIdentities) {
const source = sourceMap[identity.email.toLowerCase()] || 'own';
let header;
if (source.startsWith('=')) {
const srcEmail = source.substring(1);
const srcIdentity = allIdentities.find(i => i.email.toLowerCase() === srcEmail);
if (!srcIdentity) continue;
header = extractHeader(srcIdentity.signature || '');
} else {
header = extractHeader(identity.signature || '');
}
// Unangetastete, leere Konten nicht mit einem reinen Footer versehen
const alreadyManaged = (identity.signature || '').includes(FOOTER_SEPARATOR);
if (!header.trim() && !alreadyManaged) continue;
const fullSig = combineSignature(header, footer);
if (fullSig === identity.signature) { applied++; continue; }
await browser.identities.update(identity.id, {
signature: fullSig,
signatureIsPlainText: false,
});
identity.signature = fullSig;
applied++;
}
return applied;
}
// ── "Vorlage laden" Button ──
document.getElementById('sig-load-template').addEventListener('click', async () => {
@@ -1328,45 +1364,94 @@ document.getElementById('sig-sync-refresh').addEventListener('click', async () =
// ── Footer Editor ──
const footerEditorArea = document.getElementById('footer-editor-area');
const footerScopeSelect = document.getElementById('footer-scope');
setupToolbarCommands('footer-toolbar', footerEditorArea);
setupImageInsert('footer-insert-image', 'footer-image-file', footerEditorArea);
// Keep the "Abteilung" option label in sync with the selected department
function updateFooterScopeLabel() {
const dept = document.getElementById('sync-department')?.value || '';
const deptOption = footerScopeSelect?.querySelector('option[value="department"]');
if (deptOption) {
deptOption.textContent = dept ? `Abteilung: ${dept}` : 'Abteilung';
deptOption.disabled = !dept;
}
}
function currentFooterScope() {
return footerScopeSelect?.value === 'department' ? 'department' : 'shared';
}
// Default-Auswahl: Abteilungs-Footer bevorzugen, wenn einer existiert.
// Sonst gemeinsam. Wird nur für die Vorbelegung genutzt — manuelles
// Umschalten bleibt erhalten.
async function pickDefaultFooterScope() {
const dept = document.getElementById('sync-department')?.value || '';
if (!dept) return 'shared';
try {
const res = await browser.runtime.sendMessage({ action: 'loadFooter', scope: 'department' });
if (res && res.success && res.html) return 'department';
} catch (_) {}
return 'shared';
}
// Race-Schutz: nur das Ergebnis des zuletzt gestarteten Loads anwenden
let footerLoadSeq = 0;
// Load footer for the currently selected scope into the editor
async function loadFooterForScope(showStatus) {
const scope = currentFooterScope();
// Gleiche Offline-Prüfung wie bei den anderen Netzwerk-Aktionen
if (showStatus && !checkOnline()) return;
const seq = ++footerLoadSeq;
const loadBtn = document.getElementById('footer-load-button');
if (showStatus && loadBtn) loadBtn.disabled = true;
if (showStatus) showFooterStatus('Lade...', '#777');
try {
const result = await browser.runtime.sendMessage({ action: 'loadFooter', scope });
// Veralteter Lauf: ein neuerer Load wurde gestartet → Ergebnis verwerfen
if (seq !== footerLoadSeq) return;
if (result && result.success) {
footerEditorArea.innerHTML = result.html || '';
if (showStatus) {
const where = scope === 'shared' ? 'gemeinsamer' : 'Abteilungs-';
showFooterStatus(result.html ? `${where}Fußbereich geladen.` : `Noch kein ${where}Fußbereich vorhanden — du kannst einen anlegen.`, result.html ? 'green' : '#e65100');
}
} else if (showStatus) {
showFooterStatus(result?.error || 'Fehler', 'red');
}
} catch (err) {
if (seq === footerLoadSeq && showStatus) showFooterStatus('Fehler: ' + err.message, 'red');
} finally {
if (seq === footerLoadSeq && showStatus && loadBtn) loadBtn.disabled = false;
}
}
document.getElementById('footer-toggle').addEventListener('click', async function() {
this.classList.toggle('open');
document.getElementById('footer-body').classList.toggle('open');
// Auto-load footer when opening and editor is empty
if (this.classList.contains('open') && (!footerEditorArea.innerHTML || footerEditorArea.innerHTML === '<br>')) {
try {
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
if (result && result.success && result.html) {
footerEditorArea.innerHTML = result.html;
}
} catch (_) {}
updateFooterScopeLabel();
// Standardmäßig den Abteilungs-Footer vorbelegen, falls vorhanden
if (footerScopeSelect) footerScopeSelect.value = await pickDefaultFooterScope();
await loadFooterForScope(false);
}
});
// Reload editor content when switching scope
footerScopeSelect?.addEventListener('change', () => loadFooterForScope(true));
function showFooterStatus(message, color) {
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
showToast(message, type, type === 'error' ? 6000 : 4000);
}
// Load footer from server
document.getElementById('footer-load-button').addEventListener('click', async () => {
showFooterStatus('Lade...', '#777');
try {
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
if (result && result.success) {
footerEditorArea.innerHTML = result.html || '';
showFooterStatus(result.html ? 'Fußbereich geladen.' : 'Kein Fußbereich für diese Abteilung gefunden.', result.html ? 'green' : '#e65100');
} else {
showFooterStatus(result?.error || 'Fehler', 'red');
}
} catch (err) {
showFooterStatus('Fehler: ' + err.message, 'red');
}
});
document.getElementById('footer-load-button').addEventListener('click', () => loadFooterForScope(true));
// Save & push footer
document.getElementById('footer-save-button').addEventListener('click', async () => {
@@ -1376,16 +1461,57 @@ document.getElementById('footer-save-button').addEventListener('click', async ()
return;
}
if (!checkOnline()) return;
const scope = currentFooterScope();
const saveBtn = document.getElementById('footer-save-button');
saveBtn.disabled = true;
showFooterStatus('Speichere...', '#777');
try {
const result = await browser.runtime.sendMessage({ action: 'pushFooter', html });
if (result && result.success) {
showFooterStatus('Fußbereich gespeichert & hochgeladen!', 'green');
} else {
const result = await browser.runtime.sendMessage({ action: 'pushFooter', html, scope });
if (!result || !result.success) {
showFooterStatus(result?.error || 'Fehler', 'red');
return;
}
// Neuen Footer direkt (lokal) auf die Thunderbird-Identitäten anwenden —
// funktioniert auch ohne hochgeladenen Server-Header.
const footer = await getFooter();
const applied = await applyFooterLocally(footer);
// Geänderte Header zusätzlich hochladen (best effort), damit andere
// Geräte beim nächsten Sync den kombinierten Stand erhalten.
try { await browser.runtime.sendMessage({ action: 'pushSignatures' }); } catch (_) {}
for (const id of allIdentities) {
sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
}
saveSyncHashes();
updateSigSyncIndicator();
// Aktuell geöffneten Signatur-Editor neu rendern
sigIdentitySelect.dispatchEvent(new Event('change'));
// Wenn "gemeinsam" gespeichert wurde, aber ein Abteilungs-Footer
// existiert, verdeckt dieser den gemeinsamen beim Anwenden.
let shadowed = false;
if (scope === 'shared') {
try {
const dept = await browser.runtime.sendMessage({ action: 'loadFooter', scope: 'department' });
shadowed = !!(dept && dept.success && dept.html);
} catch (_) {}
}
const where = scope === 'shared' ? 'Gemeinsamer' : 'Abteilungs-';
if (shadowed) {
showFooterStatus('Gemeinsamer Fußbereich gespeichert — aber deine Abteilung hat einen eigenen Footer, der ihn überschreibt. Zum Anwenden den Abteilungs-Footer bearbeiten/löschen.', '#e65100');
} else if (applied > 0) {
showFooterStatus(`${where} Fußbereich gespeichert & in ${applied} Signatur(en) übernommen!`, 'green');
} else {
showFooterStatus(`${where} Fußbereich gespeichert. Hinweis: keine Signatur aktualisiert — erst eine persönliche Signatur speichern/hochladen.`, '#e65100');
}
} catch (err) {
showFooterStatus('Fehler: ' + err.message, 'red');
} finally {
saveBtn.disabled = false;
}
});
@@ -1616,6 +1742,7 @@ async function loadDepartments() {
if (dept === savedDept) opt.selected = true;
select.appendChild(opt);
}
updateFooterScopeLabel();
}
} catch (_) {}
}
@@ -1652,6 +1779,8 @@ document.getElementById('sync-department').addEventListener('change', async () =
config.department = document.getElementById('sync-department').value;
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
updateFooterScopeLabel();
if (config.department) {
try {
// Pull templates for new department