v0.0.5: gedeelde mailboxen (info@, facturen@, nieuwsbrief@, persoonlijk) met eigen IMAP/SMTP, afzender, rol en toegang per gebruiker; inbox per mailbox en versturen vanuit een gekozen mailbox; routines (jobs.py: types met @jobs.register, interval, instellingen, logboek, slot tegen dubbel draaien; app.py scheduler/tick) voor e-mail ophalen, nieuwsbrieven versturen en opschonen; meldingen (bel, bureaubladmelding); nieuwsbrieven met segmenten, testmail, inplannen en versturen per batch over één verbinding, afmeldlink met List-Unsubscribe en bounceverwerking; oude mailinstellingen worden de eerste mailbox

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-16 18:25:44 +02:00
co-authored by Claude Opus 5.5
parent 8f6e1ab443
commit cad838f1f4
24 changed files with 1710 additions and 185 deletions
+32
View File
@@ -0,0 +1,32 @@
// Bel in de bovenbalk: elke minuut het aantal ongelezen meldingen ophalen (/notifications/poll). Met toestemming
// van de browser verschijnen nieuwe meldingen ook als bureaubladmelding (zolang de CRM ergens open staat).
(() => {
const bell = document.querySelector('.bell[data-poll]');
if (!bell) return;
const count = bell.querySelector('.bell-count');
const desktop = bell.querySelector('.bell-desktop');
let last = Number(bell.dataset.last) || 0;
const canNotify = 'Notification' in window;
if (canNotify && Notification.permission === 'default') {
desktop.hidden = false;
desktop.addEventListener('click', () => Notification.requestPermission().then(() => { desktop.hidden = true; }));
}
async function poll() {
try {
const r = await fetch(`${bell.dataset.poll}?after=${last}`, { headers: { Accept: 'application/json' } });
if (!r.ok) return;
const d = await r.json();
count.textContent = d.count;
count.hidden = !d.count;
d.items.slice().reverse().forEach((n) => {
last = Math.max(last, n.id);
if (canNotify && Notification.permission === 'granted') {
const note = new Notification(n.title, { body: n.body || '', tag: `crm-${n.id}` });
note.onclick = () => { window.focus(); location = n.url; };
}
});
} catch (e) { /* offline of uitgelogd: volgende keer opnieuw */ }
}
setInterval(() => { if (!document.hidden) poll(); }, 60000);
document.addEventListener('visibilitychange', () => { if (!document.hidden) poll(); });
})();