Skip to content

Latest commit

 

History

History
675 lines (524 loc) · 16.3 KB

File metadata and controls

675 lines (524 loc) · 16.3 KB

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

{
  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:

// 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:

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:

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

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:

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:

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

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

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

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

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

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:

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

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

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

// 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)
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:

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

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