diff --git a/.gitignore b/.gitignore index 683b324..5e2c2d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -defaults.local.js +defaults.local.json diff --git a/README.md b/README.md index e079821..17620d5 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Mapping von Abteilungs-E-Mail-Adressen zu Ordnernamen. Wird vom Plugin gelesen, | `lib/gitea-sync.js` | Gitea-API-Client + Sync-Manager | | `lib/mdi/` | Material Design Icons (Subset) | | `templates_options/` | Einstellungsseite (Vorlagen, Signaturen, Verbindung) | +| `defaults.local.json` | Optionale vorkonfigurierte Verbindungsdaten (gitignored) | ## Installation @@ -71,12 +72,32 @@ Mapping von Abteilungs-E-Mail-Adressen zu Ordnernamen. Wird vom Plugin gelesen, ### XPI bauen ```bash +# Ohne vorkonfigurierte Verbindungsdaten: 7z a templates-reply-hotel.xpi manifest.json background.js popup.html popup.js lib/ templates_options/ icons/ + +# Mit vorkonfigurierten Verbindungsdaten (für Deployment): +7z a templates-reply-hotel.xpi manifest.json background.js popup.html popup.js lib/ templates_options/ icons/ defaults.local.json ``` +### Vorkonfigurierte Verbindungsdaten (`defaults.local.json`) + +Wenn eine `defaults.local.json` im Plugin-Root existiert und in die XPI eingebaut wird, werden die Verbindungsdaten beim ersten Start automatisch gesetzt. Der User muss dann nur noch "Verbindung speichern" klicken. + +```json +{ + "baseUrl": "https://git.example.com", + "owner": "organisation", + "repo": "email-vorlagen", + "branch": "main", + "token": "dein-api-token" +} +``` + +Die Datei ist in `.gitignore` — Tokens landen nicht im Repository. + ## Einrichtung -1. **Verbindung konfigurieren**: Einstellungen-Tab (⚙) → Server-URL, Repository, Token eingeben → Verbindung speichern +1. **Verbindung konfigurieren**: Einstellungen-Tab (⚙) → Server-URL, Repository, Token eingeben → Verbindung speichern (entfällt bei vorkonfigurierter XPI) 2. **Abteilung wählen** (oder automatisch erkannt via `abteilungen.json`) 3. **Vorlagen erstellen**: Vorlagen-Tab → Neue Vorlage → Sichtbarkeit wählen → Speichern 4. **Signaturen einrichten**: Signaturen-Tab → Identität wählen → Kopfbereich bearbeiten → Speichern diff --git a/lib/gitea-sync.js b/lib/gitea-sync.js index daa7064..53c5288 100644 --- a/lib/gitea-sync.js +++ b/lib/gitea-sync.js @@ -3,7 +3,8 @@ const SYNC_CONFIG_KEY = 'gitea_config'; const SYNC_STATE_KEY = 'sync_state'; const TEMPLATE_STORAGE_KEY = 'message_templates'; -const SYNC_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes +const SHA_CHECK_INTERVAL_MS = 5 * 1000; // 5 seconds — lightweight SHA check +const FULL_SYNC_COOLDOWN_MS = 10 * 1000; // min 10s between full pulls const SHARED_FOLDER = '_gemeinsam'; const USER_FOLDER = '_benutzer'; const CONFIG_FOLDER = '_config'; @@ -762,24 +763,66 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { // ── Auto-sync on startup (pull only) ── -async function autoSync() { +let lastKnownShas = null; +let lastFullSync = 0; +let syncInProgress = false; +const HASH_STORAGE_KEY_BG = 'sync_hashes'; + +function simpleHashBg(str) { + let h = 0; + for (let i = 0; i < str.length; i++) { + h = ((h << 5) - h + str.charCodeAt(i)) | 0; + } + return h; +} + +async function updateSyncHashes() { + const templates = await syncManager.getLocalTemplates(); + const result = await browser.storage.local.get(HASH_STORAGE_KEY_BG); + const data = result[HASH_STORAGE_KEY_BG] || {}; + data.tpl = {}; + for (const t of templates) { + data.tpl[t.id] = simpleHashBg(t.content || ''); + } + await browser.storage.local.set({ [HASH_STORAGE_KEY_BG]: data }); +} + +async function smartSync() { + if (syncInProgress) return; try { const initialized = await syncManager.init(); if (!initialized) return; - if (syncManager.department) { - console.log('[Sync] Auto-pull Vorlagen gestartet...'); - const result = await syncManager.pullTemplates(); - console.log('[Sync] Auto-pull Vorlagen abgeschlossen:', result); - } + // Lightweight SHA check + const shaResult = await syncManager.checkRemoteShas(); + if (!shaResult?.success) return; - console.log('[Sync] Auto-pull Signaturen gestartet...'); - const sigResult = await syncManager.pullSignatures(); - console.log('[Sync] Auto-pull Signaturen abgeschlossen:', sigResult); + const currentShas = JSON.stringify(shaResult.remoteShas); + + // First run or SHAs changed → full pull + if (lastKnownShas === null || currentShas !== lastKnownShas) { + const now = Date.now(); + if (now - lastFullSync < FULL_SYNC_COOLDOWN_MS) return; + + syncInProgress = true; + lastFullSync = now; + console.log('[Sync] Änderung erkannt, lade Vorlagen...'); + + const result = await syncManager.pullTemplates(); + console.log('[Sync] Vorlagen geladen:', result); + await updateSyncHashes(); + + const sigResult = await syncManager.pullSignatures(); + console.log('[Sync] Signaturen geladen:', sigResult); + + lastKnownShas = JSON.stringify((await syncManager.checkRemoteShas()).remoteShas || {}); + syncInProgress = false; + } } catch (err) { - console.error('[Sync] Auto-pull fehlgeschlagen:', err); + console.error('[Sync] Check fehlgeschlagen:', err); + syncInProgress = false; } } -autoSync(); -setInterval(autoSync, SYNC_INTERVAL_MS); +smartSync(); +setInterval(smartSync, SHA_CHECK_INTERVAL_MS); diff --git a/templates-reply-hotel.xpi b/templates-reply-hotel.xpi index 25021d6..67c21da 100644 Binary files a/templates-reply-hotel.xpi and b/templates-reply-hotel.xpi differ diff --git a/templates_options/templates_options.html b/templates_options/templates_options.html index 7404451..ad92766 100644 --- a/templates_options/templates_options.html +++ b/templates_options/templates_options.html @@ -1062,7 +1062,6 @@ - diff --git a/templates_options/templates_options.js b/templates_options/templates_options.js index 568ae26..6c4399e 100644 --- a/templates_options/templates_options.js +++ b/templates_options/templates_options.js @@ -786,6 +786,12 @@ async function handlePushSingle(e) { // ── Inline Scope Change ── +function folderToScope(folder) { + if (folder === '_gemeinsam') return 'shared'; + if (folder?.startsWith('_benutzer/')) return 'private'; + return 'department'; +} + async function handleScopeChange(e) { const id = e.target.dataset.id; const newScope = e.target.value; @@ -793,36 +799,26 @@ async function handleScopeChange(e) { const template = templates.find(t => t.id === id); if (!template) return; + const oldScope = folderToScope(template.folder); + if (oldScope === newScope) return; + const authorEmail = await getAuthorEmail(); - const oldFolder = template.folder; - let newFolder; - if (newScope === 'shared') newFolder = '_gemeinsam'; - else if (newScope === 'private') newFolder = `_benutzer/${authorEmail}`; - else newFolder = undefined; - - if (oldFolder === newFolder) return; - if (newScope === 'private' && !authorEmail) { showToast('Bitte E-Mail in den Einstellungen eintragen.', 'error'); - // Reset - e.target.value = oldFolder === '_gemeinsam' ? 'shared' - : oldFolder?.startsWith('_benutzer/') ? 'private' : 'department'; + e.target.value = oldScope; return; } // Warn on downgrade - const isDowngrade = (oldFolder === '_gemeinsam' && newFolder !== '_gemeinsam') || - (!oldFolder?.startsWith('_benutzer/') && newFolder?.startsWith('_benutzer/')); - - if (isDowngrade && template.remotePath) { + const scopeRank = { shared: 2, department: 1, private: 0 }; + if (scopeRank[newScope] < scopeRank[oldScope] && template.remotePath) { const confirmed = await showConfirmDialog( 'Sichtbarkeit verringern?', 'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.', 'Trotzdem ändern' ); if (!confirmed) { - e.target.value = oldFolder === '_gemeinsam' ? 'shared' - : oldFolder?.startsWith('_benutzer/') ? 'private' : 'department'; + e.target.value = oldScope; return; } } @@ -838,13 +834,18 @@ async function handleScopeChange(e) { template.remotePath = undefined; } - template.folder = newFolder; + // Set new folder + if (newScope === 'shared') template.folder = '_gemeinsam'; + else if (newScope === 'private') template.folder = `_benutzer/${authorEmail}`; + else template.folder = undefined; + await saveTemplates(templates); // Auto-push to new location try { await browser.runtime.sendMessage({ action: 'pushTemplates' }); templates = await getTemplates(); + storeTplHashes(templates); } catch (_) {} renderTemplates(templates); @@ -1388,10 +1389,15 @@ async function loadSyncConfig() { const result = await browser.storage.local.get(SYNC_CONFIG_KEY); let config = result[SYNC_CONFIG_KEY]; - // First launch: apply defaults from defaults.local.js if available - if (!config && typeof DEFAULT_SYNC_CONFIG !== 'undefined') { - config = { ...DEFAULT_SYNC_CONFIG }; - await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config }); + // First launch: load defaults from defaults.local.json if available + if (!config) { + try { + const res = await fetch(browser.runtime.getURL('defaults.local.json')); + if (res.ok) { + config = await res.json(); + await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config }); + } + } catch (_) {} } if (config) { @@ -1670,4 +1676,15 @@ window.addEventListener('load', async () => { checkForServerUpdates(); setInterval(checkForServerUpdates, 30000); + + // Auto-refresh UI when background sync updates hashes (fires after templates + hashes are both written) + browser.storage.onChanged.addListener(async (changes, area) => { + if (area !== 'local') return; + if (changes[HASH_STORAGE_KEY]) { + await loadSyncHashes(); + const templates = await getTemplates(); + renderTemplates(templates); + updateTplSyncIndicator(); + } + }); });