Smart-Sync, Scope-Fix, Auto-Refresh, defaults.local.json

- Background-Sync: SHA-Check alle 5s, voller Pull nur bei Änderung
- Sync-Hashes werden nach Pull im Storage geschrieben → grüne Dots
- UI refreshed automatisch bei Background-Sync (storage.onChanged)
- Scope-Badge-Bug gefixt (folderToScope normalisiert den Vergleich)
- defaults.local.json: optionale vorkonfigurierte Verbindungsdaten
- README: Doku für defaults.local.json und XPI-Build mit/ohne Defaults
This commit is contained in:
Kendrick Bollens
2026-05-07 10:50:36 +02:00
parent 533d5a34f2
commit bc82e33bf2
6 changed files with 118 additions and 38 deletions

2
.gitignore vendored
View File

@@ -1 +1 @@
defaults.local.js defaults.local.json

View File

@@ -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/gitea-sync.js` | Gitea-API-Client + Sync-Manager |
| `lib/mdi/` | Material Design Icons (Subset) | | `lib/mdi/` | Material Design Icons (Subset) |
| `templates_options/` | Einstellungsseite (Vorlagen, Signaturen, Verbindung) | | `templates_options/` | Einstellungsseite (Vorlagen, Signaturen, Verbindung) |
| `defaults.local.json` | Optionale vorkonfigurierte Verbindungsdaten (gitignored) |
## Installation ## Installation
@@ -71,12 +72,32 @@ Mapping von Abteilungs-E-Mail-Adressen zu Ordnernamen. Wird vom Plugin gelesen,
### XPI bauen ### XPI bauen
```bash ```bash
# Ohne vorkonfigurierte Verbindungsdaten:
7z a templates-reply-hotel.xpi manifest.json background.js popup.html popup.js lib/ templates_options/ icons/ 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 ## 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`) 2. **Abteilung wählen** (oder automatisch erkannt via `abteilungen.json`)
3. **Vorlagen erstellen**: Vorlagen-Tab → Neue Vorlage → Sichtbarkeit wählen → Speichern 3. **Vorlagen erstellen**: Vorlagen-Tab → Neue Vorlage → Sichtbarkeit wählen → Speichern
4. **Signaturen einrichten**: Signaturen-Tab → Identität wählen → Kopfbereich bearbeiten → Speichern 4. **Signaturen einrichten**: Signaturen-Tab → Identität wählen → Kopfbereich bearbeiten → Speichern

View File

@@ -3,7 +3,8 @@
const SYNC_CONFIG_KEY = 'gitea_config'; const SYNC_CONFIG_KEY = 'gitea_config';
const SYNC_STATE_KEY = 'sync_state'; const SYNC_STATE_KEY = 'sync_state';
const TEMPLATE_STORAGE_KEY = 'message_templates'; 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 SHARED_FOLDER = '_gemeinsam';
const USER_FOLDER = '_benutzer'; const USER_FOLDER = '_benutzer';
const CONFIG_FOLDER = '_config'; const CONFIG_FOLDER = '_config';
@@ -762,24 +763,66 @@ browser.runtime.onMessage.addListener(async (msg, sender) => {
// ── Auto-sync on startup (pull only) ── // ── 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 { try {
const initialized = await syncManager.init(); const initialized = await syncManager.init();
if (!initialized) return; if (!initialized) return;
if (syncManager.department) { // Lightweight SHA check
console.log('[Sync] Auto-pull Vorlagen gestartet...'); const shaResult = await syncManager.checkRemoteShas();
const result = await syncManager.pullTemplates(); if (!shaResult?.success) return;
console.log('[Sync] Auto-pull Vorlagen abgeschlossen:', result);
}
console.log('[Sync] Auto-pull Signaturen gestartet...'); const currentShas = JSON.stringify(shaResult.remoteShas);
const sigResult = await syncManager.pullSignatures();
console.log('[Sync] Auto-pull Signaturen abgeschlossen:', sigResult); // 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) { } catch (err) {
console.error('[Sync] Auto-pull fehlgeschlagen:', err); console.error('[Sync] Check fehlgeschlagen:', err);
syncInProgress = false;
} }
} }
autoSync(); smartSync();
setInterval(autoSync, SYNC_INTERVAL_MS); setInterval(smartSync, SHA_CHECK_INTERVAL_MS);

Binary file not shown.

View File

@@ -1062,7 +1062,6 @@
</div> </div>
</div> </div>
<script src="../defaults.local.js" onerror=""></script>
<script src="templates_options.js"></script> <script src="templates_options.js"></script>
</body> </body>
</html> </html>

View File

@@ -786,6 +786,12 @@ async function handlePushSingle(e) {
// ── Inline Scope Change ── // ── Inline Scope Change ──
function folderToScope(folder) {
if (folder === '_gemeinsam') return 'shared';
if (folder?.startsWith('_benutzer/')) return 'private';
return 'department';
}
async function handleScopeChange(e) { async function handleScopeChange(e) {
const id = e.target.dataset.id; const id = e.target.dataset.id;
const newScope = e.target.value; const newScope = e.target.value;
@@ -793,36 +799,26 @@ async function handleScopeChange(e) {
const template = templates.find(t => t.id === id); const template = templates.find(t => t.id === id);
if (!template) return; if (!template) return;
const oldScope = folderToScope(template.folder);
if (oldScope === newScope) return;
const authorEmail = await getAuthorEmail(); 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) { if (newScope === 'private' && !authorEmail) {
showToast('Bitte E-Mail in den Einstellungen eintragen.', 'error'); showToast('Bitte E-Mail in den Einstellungen eintragen.', 'error');
// Reset e.target.value = oldScope;
e.target.value = oldFolder === '_gemeinsam' ? 'shared'
: oldFolder?.startsWith('_benutzer/') ? 'private' : 'department';
return; return;
} }
// Warn on downgrade // Warn on downgrade
const isDowngrade = (oldFolder === '_gemeinsam' && newFolder !== '_gemeinsam') || const scopeRank = { shared: 2, department: 1, private: 0 };
(!oldFolder?.startsWith('_benutzer/') && newFolder?.startsWith('_benutzer/')); if (scopeRank[newScope] < scopeRank[oldScope] && template.remotePath) {
if (isDowngrade && template.remotePath) {
const confirmed = await showConfirmDialog( const confirmed = await showConfirmDialog(
'Sichtbarkeit verringern?', 'Sichtbarkeit verringern?',
'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.', 'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.',
'Trotzdem ändern' 'Trotzdem ändern'
); );
if (!confirmed) { if (!confirmed) {
e.target.value = oldFolder === '_gemeinsam' ? 'shared' e.target.value = oldScope;
: oldFolder?.startsWith('_benutzer/') ? 'private' : 'department';
return; return;
} }
} }
@@ -838,13 +834,18 @@ async function handleScopeChange(e) {
template.remotePath = undefined; 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); await saveTemplates(templates);
// Auto-push to new location // Auto-push to new location
try { try {
await browser.runtime.sendMessage({ action: 'pushTemplates' }); await browser.runtime.sendMessage({ action: 'pushTemplates' });
templates = await getTemplates(); templates = await getTemplates();
storeTplHashes(templates);
} catch (_) {} } catch (_) {}
renderTemplates(templates); renderTemplates(templates);
@@ -1388,10 +1389,15 @@ async function loadSyncConfig() {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY); const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
let config = result[SYNC_CONFIG_KEY]; let config = result[SYNC_CONFIG_KEY];
// First launch: apply defaults from defaults.local.js if available // First launch: load defaults from defaults.local.json if available
if (!config && typeof DEFAULT_SYNC_CONFIG !== 'undefined') { if (!config) {
config = { ...DEFAULT_SYNC_CONFIG }; try {
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config }); 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) { if (config) {
@@ -1670,4 +1676,15 @@ window.addEventListener('load', async () => {
checkForServerUpdates(); checkForServerUpdates();
setInterval(checkForServerUpdates, 30000); 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();
}
});
}); });