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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1 @@
|
|||||||
defaults.local.js
|
defaults.local.json
|
||||||
|
|||||||
23
README.md
23
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/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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
if (!shaResult?.success) return;
|
||||||
|
|
||||||
|
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();
|
const result = await syncManager.pullTemplates();
|
||||||
console.log('[Sync] Auto-pull Vorlagen abgeschlossen:', result);
|
console.log('[Sync] Vorlagen geladen:', result);
|
||||||
}
|
await updateSyncHashes();
|
||||||
|
|
||||||
console.log('[Sync] Auto-pull Signaturen gestartet...');
|
|
||||||
const sigResult = await syncManager.pullSignatures();
|
const sigResult = await syncManager.pullSignatures();
|
||||||
console.log('[Sync] Auto-pull Signaturen abgeschlossen:', sigResult);
|
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.
@@ -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>
|
||||||
|
|||||||
@@ -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,11 +1389,16 @@ 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 {
|
||||||
|
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 });
|
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
|
||||||
}
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
if (config) {
|
if (config) {
|
||||||
document.getElementById('sync-url').value = config.baseUrl || '';
|
document.getElementById('sync-url').value = config.baseUrl || '';
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user