mirror of
https://github.com/bckelley/cgm-remote-monitor.git
synced 2026-08-24 03:14:12 -05:00
Adjusted internal documentation links and updated references within various Markdown files to reflect a new directory structure for audit and meta documents. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 4dee684a-a4b9-4c32-a818-3a27dae1b027 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 971b8c75-c322-4ede-98ea-eb5ae31d4e5c Replit-Helium-Checkpoint-Created: true
676 lines
16 KiB
Markdown
676 lines
16 KiB
Markdown
# Messaging Subsystem Audit
|
|
|
|
**Document Version:** 1.0
|
|
**Last Updated:** January 2026
|
|
**Scope:** Pushover, IFTTT Maker, notification flows, deduplication, acknowledgment flows
|
|
|
|
---
|
|
|
|
## 1. Executive Summary
|
|
|
|
The Nightscout messaging subsystem enables critical alerts to reach caregivers through multiple channels. This audit examines notification generation, delivery mechanisms, and reliability considerations.
|
|
|
|
### Messaging Overview
|
|
|
|
| Component | Purpose | Status |
|
|
|-----------|---------|--------|
|
|
| Internal Notifications | Alarm management | Core |
|
|
| Pushover | Push notifications | Integration |
|
|
| IFTTT Maker | Webhook automation | Integration |
|
|
| Apple Push (APN) | iOS notifications | Optional |
|
|
| WebSocket Alerts | Browser notifications | Core |
|
|
|
|
---
|
|
|
|
## 2. Notification Architecture
|
|
|
|
### 2.1 Notification Flow
|
|
|
|
```
|
|
Plugin checks data
|
|
↓
|
|
requestNotify() or requestSnooze()
|
|
↓
|
|
Notification Manager (lib/notifications.js)
|
|
↓
|
|
Process notifications
|
|
↓
|
|
emit('notification', notify)
|
|
↓
|
|
┌───────────────────────────────────────────┐
|
|
│ Event Bus │
|
|
└───────┬─────────────┬─────────────────────┘
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
│ Pushover │ │ Maker │ │ WebSocket │
|
|
│ Plugin │ │ Plugin │ │ Broadcast │
|
|
└─────────────┘ └─────────────┘ └─────────────┘
|
|
```
|
|
|
|
### 2.2 Notification Object
|
|
|
|
```javascript
|
|
{
|
|
level: 1, // 0=INFO, 1=WARN, 2=URGENT
|
|
title: 'Low Glucose', // Short title
|
|
message: 'BG is 65 mg/dL', // Detailed message
|
|
plugin: plugin, // Source plugin reference
|
|
group: 'default', // Notification group
|
|
isAnnouncement: false, // Is user announcement
|
|
|
|
// Optional fields
|
|
clear: false, // Is clear notification
|
|
debug: {}, // Debug information
|
|
pushoverSound: 'climb', // Custom sound
|
|
|
|
// Computed
|
|
notifyhash: 'abc123' // Deduplication hash
|
|
}
|
|
```
|
|
|
|
### 2.3 Notification Levels
|
|
|
|
| Level | Name | Constant | Use Case |
|
|
|-------|------|----------|----------|
|
|
| -2 | None | `NONE` | Internal only |
|
|
| -1 | Low | `LOW` | Debug/trace |
|
|
| 0 | Info | `INFO` | Informational |
|
|
| 1 | Warning | `WARN` | Attention needed |
|
|
| 2 | Urgent | `URGENT` | Immediate action |
|
|
|
|
---
|
|
|
|
## 3. Notification Manager
|
|
|
|
### 3.1 Core Implementation
|
|
|
|
**Location:** `lib/notifications.js`
|
|
|
|
**Key Functions:**
|
|
|
|
```javascript
|
|
// Request a notification
|
|
notifications.requestNotify = function(notify) {
|
|
if (!notify.level || !notify.title || !notify.message || !notify.plugin) {
|
|
console.error('Incomplete notification');
|
|
return;
|
|
}
|
|
notify.group = notify.group || 'default';
|
|
requests.notifies.push(notify);
|
|
};
|
|
|
|
// Request a snooze
|
|
notifications.requestSnooze = function(snooze) {
|
|
snooze.group = snooze.group || 'default';
|
|
requests.snoozes.push(snooze);
|
|
};
|
|
|
|
// Process all pending notifications
|
|
notifications.process = function() {
|
|
// Find highest alarm per group
|
|
// Check for snoozing
|
|
// Emit or suppress
|
|
};
|
|
```
|
|
|
|
### 3.2 Alarm Management
|
|
|
|
**Alarm Object:**
|
|
```javascript
|
|
var Alarm = function(level, group, label) {
|
|
this.level = level;
|
|
this.group = group;
|
|
this.label = label;
|
|
this.silenceTime = 30 * 60 * 1000; // 30 minutes default
|
|
this.lastAckTime = 0;
|
|
this.lastEmitTime = null;
|
|
};
|
|
```
|
|
|
|
**Alarm Processing:**
|
|
1. Collect all requested notifications
|
|
2. Group by notification group
|
|
3. Find highest priority per group
|
|
4. Check if snoozed by any snooze request
|
|
5. Check if silenced from previous ack
|
|
6. Emit if not suppressed
|
|
|
|
### 3.3 Auto-Acknowledgment
|
|
|
|
When conditions return to normal:
|
|
```javascript
|
|
function autoAckAlarms(group) {
|
|
for (var level = 1; level <= 2; level++) {
|
|
var alarm = getAlarm(level, group);
|
|
if (alarm.lastEmitTime) {
|
|
notifications.ack(alarm.level, group, 1); // 1ms silence
|
|
sendClear = true;
|
|
}
|
|
}
|
|
|
|
if (sendClear) {
|
|
ctx.bus.emit('notification', {
|
|
clear: true,
|
|
title: 'All Clear',
|
|
message: 'Auto ack\'d alarm(s)',
|
|
group: group
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Push Notification Orchestrator
|
|
|
|
### 4.1 Implementation
|
|
|
|
**Location:** `lib/server/pushnotify.js`
|
|
|
|
```javascript
|
|
function init(env, ctx) {
|
|
var receipts = new NodeCache({ stdTTL: 3600 });
|
|
var recentlySent = new NodeCache({ stdTTL: 900 });
|
|
|
|
pushnotify.emitNotification = function(notify) {
|
|
if (notify.clear) {
|
|
cancelPushoverNotifications();
|
|
sendMakerAllClear(notify);
|
|
return;
|
|
}
|
|
|
|
// Check deduplication
|
|
var key = notify.notifyhash || generateHash(notify);
|
|
if (recentlySent.get(key)) {
|
|
console.log('Skipping duplicate notification');
|
|
return;
|
|
}
|
|
|
|
// Send to providers
|
|
ctx.pushover.send(notify, callback);
|
|
ctx.maker.sendEvent(notify, callback);
|
|
};
|
|
}
|
|
```
|
|
|
|
### 4.2 Deduplication
|
|
|
|
**Strategy:**
|
|
- Generate hash from notification content
|
|
- Cache recently sent hashes (15 minute TTL)
|
|
- Skip if hash exists in cache
|
|
|
|
**Hash Generation:**
|
|
```javascript
|
|
function generateHash(notify) {
|
|
const crypto = require('crypto');
|
|
const hash = crypto.createHash('sha1');
|
|
hash.update(notify.title + notify.message);
|
|
return hash.digest('hex').substring(0, 16);
|
|
}
|
|
```
|
|
|
|
### 4.3 Receipt Tracking (Pushover)
|
|
|
|
For emergency priority notifications:
|
|
```javascript
|
|
var receipts = new NodeCache({ stdTTL: 3600 });
|
|
|
|
// Store receipt from Pushover
|
|
receipts.set(receipt, notify);
|
|
|
|
// Periodic check
|
|
pushnotify.checkReceipts = function() {
|
|
receipts.keys().forEach(function(receipt) {
|
|
ctx.pushover.checkReceipt(receipt, function(err, result) {
|
|
if (result.acknowledged) {
|
|
// User acknowledged, remove from cache
|
|
receipts.del(receipt);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Pushover Integration
|
|
|
|
### 5.1 Configuration
|
|
|
|
**Location:** `lib/plugins/pushover.js`
|
|
|
|
**Environment Variables:**
|
|
```
|
|
PUSHOVER_API_TOKEN=your-app-token
|
|
PUSHOVER_USER_KEY=user-or-group-key
|
|
PUSHOVER_ALARM_KEY=key-for-alarms
|
|
PUSHOVER_ANNOUNCEMENT_KEY=key-for-announcements
|
|
BASE_URL=https://nightscout.example.com
|
|
```
|
|
|
|
### 5.2 Key Management
|
|
|
|
```javascript
|
|
var pushoverAPI = {
|
|
userKeys: env.extendedSettings.pushover.userKey.split(' '),
|
|
alarmKeys: (env.extendedSettings.pushover.alarmKey || userKey).split(' '),
|
|
announcementKeys: (env.extendedSettings.pushover.announcementKey || userKey).split(' '),
|
|
apiToken: env.extendedSettings.pushover.apiToken
|
|
};
|
|
|
|
function selectKeys(notify) {
|
|
if (notify.isAnnouncement) {
|
|
return pushoverAPI.announcementKeys;
|
|
} else if (ctx.levels.isAlarm(notify.level)) {
|
|
return pushoverAPI.alarmKeys;
|
|
}
|
|
return pushoverAPI.userKeys;
|
|
}
|
|
```
|
|
|
|
### 5.3 Priority Mapping
|
|
|
|
| Nightscout Level | Pushover Priority | Behavior |
|
|
|------------------|-------------------|----------|
|
|
| INFO | 0 (Normal) | Normal push |
|
|
| WARN | 1 (High) | Bypasses quiet hours |
|
|
| URGENT | 2 (Emergency) | Repeats until ack'd |
|
|
|
|
### 5.4 Message Sending
|
|
|
|
```javascript
|
|
pushover.send = function(notify, callback) {
|
|
var selectedKeys = selectKeys(notify);
|
|
|
|
selectedKeys.forEach(function(userKey) {
|
|
var msg = {
|
|
message: notify.message,
|
|
title: notify.title,
|
|
priority: mapPriority(notify.level),
|
|
sound: notify.pushoverSound || 'gamelan',
|
|
callback: env.base_url + '/api/v1/notifications/pushovercallback',
|
|
timestamp: Math.round(Date.now() / 1000)
|
|
};
|
|
|
|
if (msg.priority === 2) {
|
|
msg.retry = 120; // Retry every 2 minutes
|
|
msg.expire = 3600; // Expire after 1 hour
|
|
}
|
|
|
|
pushoverClient.send(msg, userKey, callback);
|
|
});
|
|
};
|
|
```
|
|
|
|
### 5.5 Callback Handling
|
|
|
|
**Endpoint:** `POST /api/v1/notifications/pushovercallback`
|
|
|
|
```javascript
|
|
api.post('/notifications/pushovercallback', function(req, res) {
|
|
if (ctx.pushnotify.pushoverAck(req.body)) {
|
|
res.sendStatus(200);
|
|
} else {
|
|
res.sendStatus(500);
|
|
}
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 6. IFTTT Maker Integration
|
|
|
|
### 6.1 Configuration
|
|
|
|
**Location:** `lib/plugins/maker.js`
|
|
|
|
**Environment Variables:**
|
|
```
|
|
MAKER_KEY=your-ifttt-webhooks-key
|
|
MAKER_ANNOUNCEMENT_KEY=optional-separate-key
|
|
```
|
|
|
|
### 6.2 Event Types
|
|
|
|
| Event Name | Trigger | Value1 | Value2 | Value3 |
|
|
|------------|---------|--------|--------|--------|
|
|
| `ns-event` | Any event | Title | Message | Timestamp |
|
|
| `ns-allclear` | Alarm cleared | Title | Message | - |
|
|
| `ns-info` | INFO level | Title | Message | - |
|
|
| `ns-warning` | WARN level | Title | Message | - |
|
|
| `ns-urgent` | URGENT level | Title | Message | - |
|
|
| `ns-{plugin}` | Plugin event | Title | Message | - |
|
|
| `ns-{level}-{eventName}` | Specific event | Title | Message | - |
|
|
|
|
### 6.3 Event Sending
|
|
|
|
```javascript
|
|
maker.sendEvent = function(notify, callback) {
|
|
if (!keys || keys.length === 0) return callback();
|
|
|
|
var events = [
|
|
'ns-event',
|
|
'ns-' + levelName(notify.level),
|
|
'ns-' + notify.plugin.name
|
|
];
|
|
|
|
if (notify.eventName) {
|
|
events.push('ns-' + levelName(notify.level) + '-' + notify.eventName);
|
|
}
|
|
|
|
events.forEach(function(event) {
|
|
keys.forEach(function(key) {
|
|
var url = 'https://maker.ifttt.com/trigger/' + event + '/with/key/' + key;
|
|
|
|
request.post({
|
|
url: url,
|
|
json: {
|
|
value1: notify.title,
|
|
value2: notify.message,
|
|
value3: Date.now()
|
|
}
|
|
}, callback);
|
|
});
|
|
});
|
|
};
|
|
```
|
|
|
|
### 6.4 All Clear Event
|
|
|
|
```javascript
|
|
maker.sendAllClear = function(notify, callback) {
|
|
if (Date.now() - lastAllClear > 30 * 60 * 1000) {
|
|
lastAllClear = Date.now();
|
|
|
|
var key = keys[0];
|
|
var url = 'https://maker.ifttt.com/trigger/ns-allclear/with/key/' + key;
|
|
|
|
request.post({
|
|
url: url,
|
|
json: {
|
|
value1: notify.title,
|
|
value2: notify.message
|
|
}
|
|
}, callback);
|
|
}
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 7. WebSocket Notification Delivery
|
|
|
|
### 7.1 Browser Notifications
|
|
|
|
**Location:** `lib/server/websocket.js`, `lib/api3/alarmSocket.js`
|
|
|
|
**Broadcast Flow:**
|
|
```javascript
|
|
ctx.bus.on('notification', function(notify) {
|
|
var event = mapLevelToEvent(notify.level);
|
|
|
|
if (notify.isAnnouncement) {
|
|
io.emit('announcement', notify);
|
|
} else if (notify.clear) {
|
|
io.emit('clear_alarm', {});
|
|
} else {
|
|
io.emit(event, notify); // 'alarm' or 'urgent_alarm'
|
|
}
|
|
});
|
|
```
|
|
|
|
### 7.2 Client-Side Handling
|
|
|
|
```javascript
|
|
socket.on('alarm', function(alarm) {
|
|
// Show notification
|
|
showDesktopNotification(alarm);
|
|
|
|
// Play sound
|
|
playAlarmSound(alarm.level);
|
|
|
|
// Update UI
|
|
showAlarmModal(alarm);
|
|
});
|
|
|
|
socket.on('urgent_alarm', function(alarm) {
|
|
// More aggressive notification
|
|
showUrgentNotification(alarm);
|
|
playUrgentSound();
|
|
});
|
|
|
|
socket.on('clear_alarm', function() {
|
|
// Dismiss notifications
|
|
hideAlarmModal();
|
|
stopAlarmSound();
|
|
});
|
|
```
|
|
|
|
### 7.3 Desktop Notifications
|
|
|
|
```javascript
|
|
function showDesktopNotification(alarm) {
|
|
if (Notification.permission === 'granted') {
|
|
new Notification(alarm.title, {
|
|
body: alarm.message,
|
|
icon: '/images/logo.png',
|
|
tag: 'nightscout-alarm-' + alarm.level,
|
|
requireInteraction: true
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Acknowledgment Flow
|
|
|
|
### 8.1 Acknowledgment Sources
|
|
|
|
| Source | Method | Scope |
|
|
|--------|--------|-------|
|
|
| Web UI | WebSocket `ack` | Local + server |
|
|
| Pushover | Callback POST | Server + cancel loop |
|
|
| API | GET /notifications/ack | Server |
|
|
|
|
### 8.2 Web Acknowledgment
|
|
|
|
```javascript
|
|
// Client sends ack
|
|
socket.emit('ack', level, group, silenceTime);
|
|
|
|
// Server handles
|
|
socket.on('ack', function(level, group, silenceTime) {
|
|
ctx.notifications.ack(level, group, silenceTime);
|
|
|
|
// Broadcast clear to all clients
|
|
ctx.bus.emit('notification', {
|
|
clear: true,
|
|
title: 'All Clear',
|
|
message: 'Alarm acknowledged',
|
|
group: group
|
|
});
|
|
});
|
|
```
|
|
|
|
### 8.3 API Acknowledgment
|
|
|
|
**Endpoint:** `GET /api/v1/notifications/ack`
|
|
|
|
**Parameters:**
|
|
- `level` - Alarm level (1 or 2)
|
|
- `group` - Notification group
|
|
- `time` - Silence duration (ms)
|
|
|
|
```javascript
|
|
api.get('/notifications/ack',
|
|
ctx.authorization.isPermitted('notifications:*:ack'),
|
|
function(req, res) {
|
|
var level = Number(req.query.level);
|
|
var group = req.query.group || 'default';
|
|
var time = Number(req.query.time) || 1800000; // 30 min default
|
|
|
|
ctx.notifications.ack(level, group, time);
|
|
res.sendStatus(200);
|
|
}
|
|
);
|
|
```
|
|
|
|
### 8.4 Silence Duration
|
|
|
|
| Method | Default Duration | Configurable |
|
|
|--------|-----------------|--------------|
|
|
| Web UI | 30 minutes | Yes (button presets) |
|
|
| Pushover | Until expired | Implicit |
|
|
| API | 30 minutes | Yes (query param) |
|
|
|
|
---
|
|
|
|
## 9. Reliability Considerations
|
|
|
|
### 9.1 Failure Modes
|
|
|
|
| Failure | Impact | Mitigation |
|
|
|---------|--------|------------|
|
|
| Pushover API down | No push notifications | Retry logic, alternative channel |
|
|
| IFTTT unavailable | No webhook events | Silent failure (acceptable) |
|
|
| Network partition | Delayed notifications | Queue locally, retry |
|
|
| Server crash | Lost in-memory state | Events reconstructed on reload |
|
|
|
|
### 9.2 Retry Logic
|
|
|
|
**Current State:** Limited retry for Pushover, none for Maker
|
|
|
|
**Recommendation:**
|
|
```javascript
|
|
async function sendWithRetry(fn, maxRetries = 3, delay = 1000) {
|
|
for (let i = 0; i < maxRetries; i++) {
|
|
try {
|
|
return await fn();
|
|
} catch (err) {
|
|
if (i === maxRetries - 1) throw err;
|
|
await sleep(delay * Math.pow(2, i));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 9.3 Queue Persistence
|
|
|
|
**Current State:** In-memory only
|
|
|
|
**Recommendation:**
|
|
- Add Redis queue for pending notifications
|
|
- Survive server restarts
|
|
- Enable horizontal scaling
|
|
|
|
---
|
|
|
|
## 10. Monitoring and Logging
|
|
|
|
### 10.1 Current Logging
|
|
|
|
```javascript
|
|
console.info('EMITTING ALARM:', JSON.stringify(notify));
|
|
console.log('Skipping duplicate notification');
|
|
console.error('Pushover send failed:', err);
|
|
```
|
|
|
|
### 10.2 Recommended Metrics
|
|
|
|
| Metric | Type | Purpose |
|
|
|--------|------|---------|
|
|
| `notifications_emitted_total` | Counter | Total by level |
|
|
| `notifications_acknowledged_total` | Counter | Ack rate |
|
|
| `pushover_send_duration_ms` | Histogram | Latency |
|
|
| `pushover_failures_total` | Counter | Error rate |
|
|
| `maker_events_sent_total` | Counter | Volume |
|
|
|
|
### 10.3 Alerting Recommendations
|
|
|
|
| Condition | Threshold | Action |
|
|
|-----------|-----------|--------|
|
|
| Pushover failure rate | >10% in 5min | Alert ops |
|
|
| Notification latency | >30s p99 | Warn |
|
|
| Queue depth | >100 | Scale |
|
|
|
|
---
|
|
|
|
## 11. Issues and Recommendations
|
|
|
|
### 11.1 Critical Issues
|
|
|
|
| Issue | Impact | Recommendation |
|
|
|-------|--------|----------------|
|
|
| No message queue | Lost notifications on crash | Add Redis queue |
|
|
| Deprecated `request` library | Security risk | Migrate to axios |
|
|
| No retry logic for IFTTT | Silent failures | Add retry with backoff |
|
|
|
|
### 11.2 Improvements
|
|
|
|
| Area | Current | Recommended |
|
|
|------|---------|-------------|
|
|
| Dedup window | 15 minutes | Configurable |
|
|
| Retry strategy | None | Exponential backoff |
|
|
| Failure logging | Basic | Structured logging |
|
|
| Rate limiting | None | Per-channel limits |
|
|
|
|
### 11.3 Additional Channels
|
|
|
|
Consider adding support for:
|
|
|
|
1. **Twilio SMS:**
|
|
- Critical for non-smartphone users
|
|
- Reliable delivery
|
|
|
|
2. **Email:**
|
|
- Summary/digest notifications
|
|
- Non-critical alerts
|
|
|
|
3. **Slack/Discord:**
|
|
- Team notifications
|
|
- Care team coordination
|
|
|
|
4. **Apple Push (APN):**
|
|
- Native iOS app support
|
|
- Already has dependency (`@parse/node-apn`)
|
|
|
|
---
|
|
|
|
## 12. Security Considerations
|
|
|
|
### 12.1 Sensitive Data
|
|
|
|
| Data | Risk | Mitigation |
|
|
|------|------|------------|
|
|
| API keys | Exposure | Environment variables only |
|
|
| User keys | Exposure | Never log full keys |
|
|
| Health data in messages | Privacy | Minimal message content |
|
|
|
|
### 12.2 Callback Security
|
|
|
|
**Pushover Callback:**
|
|
- No signature verification
|
|
- Relies on obscure URL
|
|
- Consider adding HMAC signature
|
|
|
|
### 12.3 Rate Limiting
|
|
|
|
**Current State:** Deduplication only (15 min window)
|
|
|
|
**Recommendation:**
|
|
- Add per-minute rate limits per channel
|
|
- Prevent notification storms
|
|
- Log rate limit events
|
|
|
|
---
|
|
|
|
## 13. Related Documents
|
|
|
|
- [Architecture Overview](../meta/architecture-overview.md)
|
|
- [Plugin Architecture Audit](./plugin-architecture-audit.md)
|
|
- [Real-Time Systems Audit](./realtime-systems-audit.md)
|
|
- [Modernization Roadmap](../meta/modernization-roadmap.md)
|