mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 06:11:45 +00:00
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:
@@ -550,6 +550,58 @@ function runMigrations(db: Database.Database): void {
|
||||
);
|
||||
`);
|
||||
},
|
||||
// Migration 69: Normalized per-user per-channel notification preferences
|
||||
() => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS notification_channel_preferences (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (user_id, event_type, channel)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ncp_user ON notification_channel_preferences(user_id);
|
||||
`);
|
||||
|
||||
// Migrate data from old notification_preferences table
|
||||
const oldPrefs = db.prepare('SELECT * FROM notification_preferences').all() as Array<Record<string, number>>;
|
||||
const eventCols: Record<string, string> = {
|
||||
trip_invite: 'notify_trip_invite',
|
||||
booking_change: 'notify_booking_change',
|
||||
trip_reminder: 'notify_trip_reminder',
|
||||
vacay_invite: 'notify_vacay_invite',
|
||||
photos_shared: 'notify_photos_shared',
|
||||
collab_message: 'notify_collab_message',
|
||||
packing_tagged: 'notify_packing_tagged',
|
||||
};
|
||||
const insert = db.prepare(
|
||||
'INSERT OR IGNORE INTO notification_channel_preferences (user_id, event_type, channel, enabled) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
const insertMany = db.transaction((rows: Array<[number, string, string, number]>) => {
|
||||
for (const [userId, eventType, channel, enabled] of rows) {
|
||||
insert.run(userId, eventType, channel, enabled);
|
||||
}
|
||||
});
|
||||
|
||||
for (const row of oldPrefs) {
|
||||
const userId = row.user_id as number;
|
||||
const webhookEnabled = (row.notify_webhook as number) ?? 0;
|
||||
const rows: Array<[number, string, string, number]> = [];
|
||||
for (const [eventType, col] of Object.entries(eventCols)) {
|
||||
const emailEnabled = (row[col] as number) ?? 1;
|
||||
// Only insert if disabled (no row = enabled is our default)
|
||||
if (!emailEnabled) rows.push([userId, eventType, 'email', 0]);
|
||||
if (!webhookEnabled) rows.push([userId, eventType, 'webhook', 0]);
|
||||
}
|
||||
if (rows.length > 0) insertMany(rows);
|
||||
}
|
||||
|
||||
// Copy existing single-channel setting to new plural key
|
||||
db.exec(`
|
||||
INSERT OR IGNORE INTO app_settings (key, value)
|
||||
SELECT 'notification_channels', value FROM app_settings WHERE key = 'notification_channel';
|
||||
`);
|
||||
},
|
||||
];
|
||||
|
||||
if (currentVersion < migrations.length) {
|
||||
|
||||
@@ -418,6 +418,15 @@ function createTables(db: Database.Database): void {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_id, is_read, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_recipient_created ON notifications(recipient_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notification_channel_preferences (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
event_type TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (user_id, event_type, channel)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ncp_user ON notification_channel_preferences(user_id);
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user