|
| 1 | +# Notifications |
| 2 | + |
| 3 | +This explains how notifications work in OnTrack after the unification. |
| 4 | + |
| 5 | +## The idea |
| 6 | + |
| 7 | +Before, each type of message did its own thing. Emails were sent from many |
| 8 | +places. There was no single system. |
| 9 | + |
| 10 | +Now there is one system. You send a notification once. It goes out on all the |
| 11 | +channels the user has turned on: in-app, email, and Web Push when the deployment |
| 12 | +has VAPID keys and the user has subscribed a browser. |
| 13 | + |
| 14 | +## The flow |
| 15 | + |
| 16 | + something happens in the app |
| 17 | + -> you call NotificationService.notify(...) |
| 18 | + -> saves an in-app notification (the bell) |
| 19 | + -> queues an ID-only email job |
| 20 | + -> queues an ID-only push job |
| 21 | + -> Sidekiq workers reload the notification and contact providers |
| 22 | + |
| 23 | +You only call one thing. The system handles the rest. |
| 24 | + |
| 25 | +## How to send one |
| 26 | + |
| 27 | +Call this from anywhere in the API code: |
| 28 | + |
| 29 | + NotificationService.notify( |
| 30 | + user: project.student, |
| 31 | + type: 'feedback', |
| 32 | + event: 'task_comment_created', |
| 33 | + message: "New feedback is ready for #{task_definition.name}.", |
| 34 | + link: "/#/projects/#{project.id}" |
| 35 | + ) |
| 36 | + |
| 37 | +- user: who gets it. |
| 38 | +- type: the category. One of task, feedback, portfolio, extension, general. |
| 39 | + This is what the user's on/off setting controls. |
| 40 | +- event: the specific thing that happened, as a lower_snake_case string. |
| 41 | + Required. Use one event name per ticket, and use the same name every time you |
| 42 | + raise that notification, so a notification can always be traced back to the |
| 43 | + code that sent it. |
| 44 | +- message: the text the user sees. Keep it short, 500 characters at most. |
| 45 | +- link: where clicking it should take them. Optional. |
| 46 | + |
| 47 | +type and event are different on purpose. type is the coarse category the user |
| 48 | +switches off in their profile. event is the fine-grained reason, and there will |
| 49 | +be many events inside one type. |
| 50 | + |
| 51 | +## Types and preferences |
| 52 | + |
| 53 | +Each user already has three on/off settings in their profile: |
| 54 | + |
| 55 | +- receive_task_notifications |
| 56 | +- receive_feedback_notifications |
| 57 | +- receive_portfolio_notifications |
| 58 | + |
| 59 | +The type you pass maps to one of these settings. |
| 60 | + |
| 61 | +- task uses receive_task_notifications |
| 62 | +- feedback uses receive_feedback_notifications |
| 63 | +- portfolio uses receive_portfolio_notifications |
| 64 | +- extension and general are always sent |
| 65 | + |
| 66 | +If the matching setting is off, nothing is sent. Not the bell, not the email, |
| 67 | +not the push. One switch controls all channels. This keeps it simple. We can add |
| 68 | +per-channel switches later if we want. |
| 69 | + |
| 70 | +## The pieces |
| 71 | + |
| 72 | +- app/models/notification.rb: the notification record. Has the type, message, |
| 73 | + link, and whether it has been read. |
| 74 | +- app/services/notification_service.rb: the one entry point. Checks the setting, |
| 75 | + saves the record, and queues the email and push channel jobs. |
| 76 | +- app/sidekiq/notification_email_job.rb: reloads a notification by id and sends |
| 77 | + its email on the `mailers` queue. |
| 78 | +- app/sidekiq/push_notification_delivery_job.rb: reloads a notification by id |
| 79 | + and hands it to the Web Push delivery channel on the `notifications` queue. |
| 80 | +- app/services/push_notification_service.rb: the Web Push delivery channel. It |
| 81 | + remains a safe no-op until both VAPID keys are configured. |
| 82 | +- app/mailers/notifications_mailer.rb: the email. New method single_notification |
| 83 | + with templates in app/views/notifications_mailer. |
| 84 | +- app/api/notifications_api.rb: the endpoints the web app calls. |
| 85 | +- app/api/entities/notification_entity.rb: the shape of the data sent back. |
| 86 | + |
| 87 | +## The endpoints |
| 88 | + |
| 89 | + GET /api/notifications list my notifications |
| 90 | + GET /api/notifications/unread_count how many I have not read |
| 91 | + PUT /api/notifications/:id/read mark one as read |
| 92 | + PUT /api/notifications/read_all mark all as read |
| 93 | + DELETE /api/notifications/:id delete one |
| 94 | + |
| 95 | + GET /api/push_subscriptions list my browser subscriptions |
| 96 | + POST /api/push_subscriptions register or update a browser |
| 97 | + DELETE /api/push_subscriptions remove the browser identified by endpoint |
| 98 | + |
| 99 | +Every endpoint only ever touches the current user's own notifications. |
| 100 | + |
| 101 | +## What is on now |
| 102 | + |
| 103 | +- In-app: working. The record is saved and the endpoints return it. |
| 104 | +- Email: working. Best effort. If email fails, the in-app notification is still |
| 105 | + saved. |
| 106 | +- Push: implemented and deliberately configuration-gated. It sends only when |
| 107 | + VAPID keys are set and that user has opted in from a supported browser. Keep |
| 108 | + the production keys blank until browser/device acceptance testing is complete. |
0 commit comments