wip: notifications

This commit is contained in:
Nikolai Giman
2026-03-22 20:01:05 +01:00
parent 5c4859743e
commit c403673f4d
108 changed files with 5662 additions and 582 deletions
+99
View File
@@ -0,0 +1,99 @@
---
title: Notifications
description: Real-time and push notifications in TaskView - deadline alerts, assignment notifications, per-user preferences, and multi-channel delivery via WebSocket and Firebase Cloud Messaging.
navigation:
icon: i-lucide-bell
---
TaskView notifies you when things happen in your projects. Notifications are delivered through multiple channels and can be customized per user.
## Notification types
| Type | When it fires | Delivery | Status |
|---|---|---|---|
| **Deadline** | When a task deadline is reached | Scheduled via background job (pgboss). If the deadline is already past when set, fires immediately. | Available |
| **Assignment** | When you are assigned to a task | Immediate | Available |
| **Mention** | When someone mentions you | Immediate | Planned |
| **Comment** | When someone comments on your task | Immediate | Planned |
| **Status change** | When a task status changes | Immediate | Planned |
## Delivery channels
Notifications can be sent through two channels (with email planned for the future):
| Channel | Description | Required configuration |
|---|---|---|
| **Push** | Native push notifications on iOS/Android via Firebase Cloud Messaging | `FIREBASE_CREDENTIALS_PATH` |
| **In-app (WebSocket)** | Real-time delivery to the browser via Centrifugo | `CENTRIFUGO_API_URL`, `CENTRIFUGO_API_KEY`, `CENTRIFUGO_TOKEN_SECRET`, `CENTRIFUGO_PUBLIC_PORT` |
Both channels are optional. If Firebase is not configured, push notifications are silently skipped. If Centrifugo is not configured, in-app real-time delivery is skipped. Notifications are always saved to the database regardless of channel availability.
## User preferences
Each user can control which notifications they receive and through which channels. Settings are available in **Account Settings > Notification Settings**.
Preferences follow an **opt-out model**: everything is enabled by default. Users explicitly disable what they do not want.
### Global and per-project settings
Preferences support two levels:
- **Global** applies to all projects
- **Project overrides** apply to a specific project and are merged on top of global settings
For example, a user can enable push for all deadline notifications globally, but disable push for deadlines in a specific project.
### Deadline intervals (planned)
::callout{icon="i-lucide-construction" color="warning"}
Deadline intervals are defined in the preferences structure but not yet active. Currently, deadline notifications fire once at the moment of the deadline. Multiple interval support is planned for a future release.
::
The preferences structure supports the following intervals (minutes before the deadline):
| Interval | Description |
|---|---|
| `0` | At the moment of the deadline |
| `15` | 15 minutes before |
| `30` | 30 minutes before |
| `60` | 1 hour before |
| `1440` | 1 day before |
Each interval can be independently enabled or disabled.
## How it works
1. An event occurs (task created, deadline changed, assignees changed, etc.)
2. **NotificationDispatcher** listens to the event bus and determines the notification type, recipients, and whether to send immediately or schedule a background job
3. For deadlines, **DeadlineScheduler** creates a pgboss job that fires at the right time
4. When it is time to deliver, **NotificationService** checks the user's preferences, saves the notification to the database, and sends it through enabled channels only
5. **Providers** (FCMProvider, CentrifugoProvider) handle the actual delivery
## Viewing notifications
Click the bell icon in the sidebar to open the notification panel. From there you can:
- See all your notifications with type icons and timestamps
- Click a notification to navigate to the related task
- Mark individual notifications as read
- Mark all notifications as read
- Load older notifications via pagination
Notifications older than 1 day are automatically cleaned up by a daily background job.
## Configuration
See [Environment Variables](/docs/configuration/environment-variables#notifications) for the full list of notification-related variables.
## API endpoints
| Method | Path | Description |
|---|---|---|
| `GET` | `/module/notifications` | Fetch notifications (cursor pagination) |
| `PATCH` | `/module/notifications/read` | Mark a notification as read |
| `PATCH` | `/module/notifications/read-all` | Mark all notifications as read |
| `GET` | `/module/notifications/preferences` | Get user notification preferences |
| `PUT` | `/module/notifications/preferences` | Save user notification preferences |
| `GET` | `/module/notifications/connection-token` | Get WebSocket connection token |
| `POST` | `/module/notifications/device/register` | Register a device for push notifications |
| `POST` | `/module/notifications/device/unregister` | Unregister a device |
+170
View File
@@ -0,0 +1,170 @@
---
title: Webhooks
description: Configure webhooks in TaskView to receive real-time HTTP notifications when tasks are created, updated, deleted, or reassigned. Includes HMAC-SHA256 signature verification, automatic retries, and delivery history.
navigation:
icon: i-lucide-webhook
---
Webhooks let you receive HTTP POST requests when events happen in your projects. Use them to integrate TaskView with external systems - CI/CD pipelines, Slack bots, custom dashboards, or any service that can accept HTTP requests.
## Supported events
| Event | When it fires |
|---|---|
| `task.created` | A new task is created in the project |
| `task.updated` | A task is updated (description, status, priority, deadline, etc.) |
| `task.deleted` | A task is deleted |
| `task.assigneesChanged` | Task assignees are added or removed |
## Setup
1. Open a project in TaskView
2. Right-click the project in the sidebar → **"Webhooks"**
3. Click **"Add Webhook"**
4. Enter the URL where you want to receive events
5. Select which events to subscribe to
6. Click **"Add"**
7. Copy the secret and store it securely - it will not be shown again
## Payload format
Every webhook delivery is an HTTP POST with `Content-Type: application/json`:
```json
{
"event": "task.updated",
"timestamp": "2026-03-22T12:00:00.000Z",
"task": {
"id": 123,
"goalId": 774,
"description": "Fix login bug",
"complete": false,
"statusId": 5,
"priorityId": 2,
"tags": [1, 3],
"assignedUsers": [10, 22],
"subtasks": []
},
"changes": {
"statusId": 5
},
"initiatorId": 1
}
```
The `changes` field is only present on `task.updated` events and contains only the fields that changed.
## Signature verification
Every request includes an `X-Webhook-Signature` header with an HMAC-SHA256 signature of the request body:
```
X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592...
```
Always verify the signature before processing the payload. Example in Node.js:
```javascript
const crypto = require('crypto')
function verifySignature(body, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
return signature === expected
}
// In your HTTP handler:
const body = req.body // raw string, not parsed JSON
const signature = req.headers['x-webhook-signature']
const isValid = verifySignature(body, signature, YOUR_SECRET)
```
::callout{icon="i-lucide-shield-alert" color="warning"}
Never process webhook payloads without verifying the signature. Without verification, anyone who knows your webhook URL can send fake events.
::
## Retries
If your server responds with a non-2xx status code or doesn't respond within 10 seconds, TaskView retries the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | ~10 seconds |
| 2nd retry | ~20 seconds |
After 3 total attempts (1 original + 2 retries), the delivery is marked as **failed**.
## Auto-deactivation
If a webhook accumulates **10 consecutive failed deliveries** (after all retries are exhausted), it is automatically deactivated. A single successful delivery resets the failure counter.
To reactivate a webhook, toggle it back on from the webhooks page. The failure counter is not reset automatically - the next successful delivery will reset it.
## Delivery history
The webhooks page shows delivery history for each webhook:
- **Event** - which event was delivered
- **Status** - success, failed, or pending
- **HTTP code** - response status code from your server
- **Attempts** - how many attempts were made
- **Payload** - click to view the full JSON payload
Failed deliveries can be retried manually from the delivery history.
## Managing webhooks
From the webhooks page you can:
- **Toggle** webhooks on/off
- **Edit** the URL and subscribed events
- **Test** - sends a test payload to verify connectivity
- **Rotate secret** - generates a new secret (the old one stops working immediately)
- **Delete** - removes the webhook and all delivery history
- **View deliveries** - see delivery history with status filter
## Secret rotation
If your secret is compromised, rotate it:
1. Click the key icon on the webhook
2. Confirm that you want to rotate
3. Copy the new secret
4. Update the secret in your receiving application
The old secret stops working immediately. Any in-flight deliveries signed with the old secret will fail signature verification on your end.
## Testing locally
You can use a simple Node.js script to test webhook deliveries:
```javascript
const http = require('http')
const crypto = require('crypto')
const PORT = 4545
const SECRET = 'your-secret-here'
const server = http.createServer((req, res) => {
const chunks = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
const body = Buffer.concat(chunks).toString()
const signature = req.headers['x-webhook-signature'] || ''
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(body)
.digest('hex')
console.log(signature === expected ? 'Valid' : 'INVALID')
console.log(JSON.stringify(JSON.parse(body), null, 2))
res.writeHead(200)
res.end('OK')
})
})
server.listen(PORT, () => console.log(`Listening on :${PORT}`))
```
Run with `node webhook-receiver.js` and set the webhook URL to `http://localhost:4545`.