feat: notification digests via SMTP
Adds a periodic inbox-summary email per opted-in user. SMTP is configured via appsettings (Smtp section); the digest is built from existing analytics (health score, top senders, recommendations) and sent by a new hourly DigestWorker, mirroring the GmailSyncWorker pattern. Frequency and send hour are configurable (Digest section). Backend: IEmailSender (SmtpEmailSender, MailKit), IDigestService, DigestWorker, new User.DigestEnabled/LastDigestSentUtc fields + migration, SettingsController for the per-user toggle and a send-now test endpoint, all scoped to the authenticated UserId. Frontend: SettingsApi + a bell/no-bell toggle button in the topbar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -148,4 +148,10 @@ export const ExportApi = {
|
||||
reportUrl: (format) => `/api/v1/export/report?format=${format}`
|
||||
};
|
||||
|
||||
export const SettingsApi = {
|
||||
getDigest: () => api.get('/settings/digest').then((r) => r.data),
|
||||
setDigest: (enabled) => api.put('/settings/digest', enabled).then((r) => r.data),
|
||||
sendDigestNow: () => api.post('/settings/digest/send-now'),
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SettingsApi } from '../api/client.js';
|
||||
|
||||
export default function DigestToggle() {
|
||||
const [enabled, setEnabled] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
SettingsApi.getDigest().then((d) => setEnabled(d.enabled)).catch(() => setEnabled(false));
|
||||
}, []);
|
||||
|
||||
const toggle = async () => {
|
||||
const next = !enabled;
|
||||
setEnabled(next);
|
||||
try { await SettingsApi.setDigest(next); } catch { setEnabled(!next); }
|
||||
};
|
||||
|
||||
if (enabled === null) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
className="ghost digest-toggle"
|
||||
title={enabled ? 'Weekly digest emails: on' : 'Weekly digest emails: off'}
|
||||
onClick={toggle}
|
||||
>
|
||||
{enabled ? '🔔' : '🔕'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { AuthApi, SyncApi, AnalyticsApi } from '../api/client.js';
|
||||
import Logo from './Logo.jsx';
|
||||
import DevBanner from './DevBanner.jsx';
|
||||
import SyncStatus from './SyncStatus.jsx';
|
||||
import DigestToggle from './DigestToggle.jsx';
|
||||
import useSavedSearches from '../hooks/useSavedSearches.js';
|
||||
|
||||
// ── Folder definitions ────────────────────────────────────────────────────────
|
||||
@@ -255,6 +256,7 @@ export default function Layout() {
|
||||
</form>
|
||||
<div className="spacer" />
|
||||
<SyncStatus />
|
||||
<DigestToggle />
|
||||
<button onClick={startSync}>Sync now</button>
|
||||
<span className="user">{user?.email}</span>
|
||||
<button className="ghost" onClick={logout}>Log out</button>
|
||||
|
||||
@@ -412,3 +412,5 @@ input, select { background: var(--panel-2); border: 1px solid #2c3550; color: va
|
||||
.saved-search-save-btn:hover { color: var(--text); border-color: var(--accent); }
|
||||
|
||||
.email-row--focused { outline: 1px solid var(--accent); outline-offset: -1px; }
|
||||
|
||||
.digest-toggle { font-size: 16px; padding: 4px 8px; }
|
||||
|
||||
Reference in New Issue
Block a user