feat(notifications): add unified multi-channel notification system

Introduces a fully featured notification system with three delivery
channels (in-app, email, webhook), normalized per-user/per-event/
per-channel preferences, admin-scoped notifications, scheduled trip
reminders and version update alerts.

- New notificationService.send() as the single orchestration entry point
- In-app notifications with simple/boolean/navigate types and WebSocket push
- Per-user preference matrix with normalized notification_channel_preferences table
- Admin notification preferences stored globally in app_settings
- Migration 69 normalizes legacy notification_preferences table
- Scheduler hooks for daily trip reminders and version checks
- DevNotificationsPanel for testing in dev mode
- All new tests passing, covering dispatch, preferences, migration, boolean
  responses, resilience, and full API integration (NSVC, NPREF, INOTIF,
  MIGR, VNOTIF, NROUTE series)
 - Previous tests passing
This commit is contained in:
jubnl
2026-04-05 01:20:33 +02:00
parent 179938e904
commit fc29c5f7d0
46 changed files with 21923 additions and 18383 deletions
+8 -10
View File
@@ -12,22 +12,19 @@ import {
deleteAll,
respondToBoolean,
} from '../services/inAppNotifications';
import * as prefsService from '../services/notificationPreferencesService';
import { getPreferencesMatrix, setPreferences } from '../services/notificationPreferencesService';
const router = express.Router();
router.get('/preferences', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
res.json({ preferences: prefsService.getPreferences(authReq.user.id) });
res.json(getPreferencesMatrix(authReq.user.id, authReq.user.role, 'user'));
});
router.put('/preferences', authenticate, (req: Request, res: Response) => {
const authReq = req as AuthRequest;
const { notify_trip_invite, notify_booking_change, notify_trip_reminder, notify_webhook } = req.body;
const preferences = prefsService.updatePreferences(authReq.user.id, {
notify_trip_invite, notify_booking_change, notify_trip_reminder, notify_webhook
});
res.json({ preferences });
setPreferences(authReq.user.id, req.body);
res.json(getPreferencesMatrix(authReq.user.id, authReq.user.role, 'user'));
});
router.post('/test-smtp', authenticate, async (req: Request, res: Response) => {
@@ -38,9 +35,10 @@ router.post('/test-smtp', authenticate, async (req: Request, res: Response) => {
});
router.post('/test-webhook', authenticate, async (req: Request, res: Response) => {
const authReq = req as AuthRequest;
if (authReq.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' });
res.json(await testWebhook());
const { url } = req.body;
if (!url || typeof url !== 'string') return res.status(400).json({ error: 'url is required' });
try { new URL(url); } catch { return res.status(400).json({ error: 'Invalid URL' }); }
res.json(await testWebhook(url));
});
// ── In-app notifications ──────────────────────────────────────────────────────