tududi/backend/migrations/20260320000001-add-notification-channel-sent-tracking.js
Chris 11cd77bedd
Fix Telegram notification spam with channel-level rate limiting (#951)
* Fix Telegram notification spam with channel-level rate limiting

Addresses issue #950 where Telegram notifications were sent excessively
(96-288 messages per day per task) due to the delete-and-recreate pattern
added in commit 105a913a to fix navbar notification pile-up.

Changes:
- Add channel_sent_at JSON field to notifications table to track when
  each channel (telegram, email, push) was last sent
- Add helper methods to notification model:
  - markChannelAsSent(channel): Records send timestamp
  - wasChannelRecentlySent(channel, threshold): Checks if sent within 24h
- Modify sendTelegramNotification() to check rate limit before sending
- Update service layer (dueTaskService, deferredTaskService,
  dueProjectService) to preserve channel_sent_at when recreating
  notifications
- Add comprehensive unit and integration tests (20 tests, all passing)

Impact:
- Reduces Telegram notifications from 96-288/day to 1/day per item
- Preserves in-app notification refresh behavior (every 5-15 min)
- Maintains navbar pile-up fix from original commit
- Rate limit configurable (default: 24 hours)

Fixes #950

* Fix linting and formatting issues

* Fix integration test that was trying to access private function

* Fix prettier formatting in integration test
2026-03-19 20:26:11 +02:00

36 lines
975 B
JavaScript

'use strict';
const {
safeAddColumns,
safeRemoveColumn,
} = require('../utils/migration-utils');
/**
* Migration to add channel_sent_at JSON column to notifications table.
* This tracks when each notification channel (telegram, email, push) was last sent,
* enabling rate limiting to prevent notification spam.
*
* Example value: {"telegram": "2026-03-19T10:30:00Z", "email": "2026-03-19T11:00:00Z"}
*/
module.exports = {
async up(queryInterface, Sequelize) {
await safeAddColumns(queryInterface, 'notifications', [
{
name: 'channel_sent_at',
definition: {
type: Sequelize.JSON,
allowNull: true,
defaultValue: null,
},
},
]);
},
async down(queryInterface) {
await safeRemoveColumn(
queryInterface,
'notifications',
'channel_sent_at'
);
},
};