Building a Real-Time Maintenance Task Notification System for Queen of San Diego

The Problem

The maintenance.queenofsandiego.com tool was built to surface maintenance tasks for the vessel fleet, but it lacked visibility into newly added tasks. When Travis added tasks via SMS integration, there was no mechanism for the operations team to discover them, and Sergio had no way to stay informed about emerging maintenance needs. The tool needed a notification layer that could intelligently alert the team based on task criticality while avoiding notification fatigue.

Architecture Overview

We implemented a three-tier notification system spanning Google Apps Script (GAS), AWS Lambda, and email infrastructure:

  • Task Persistence Layer: /Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenancePersistence.gs — Captures task creation events and persists them to a backend service
  • Notification Engine: AWS Lambda function triggered by task persistence events, responsible for intelligent notification routing
  • Frontend Handler: /Users/cb/Documents/repos/sites/queenofsandiego.com/BookingAutomation.gs — Routes log_maintenance actions to the persistence layer
  • Staging HTML: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html — Client-side task submission interface

Why This Architecture?

We chose this approach because:

  • Decoupling: The staging HTML (CloudFront-served from S3) doesn't directly know about notification logic. This allows us to test notifications independently and scale the notification system without frontend changes.
  • Serverless scalability: Lambda functions cost nothing when idle and scale automatically, perfect for maintenance notifications that may spike during storm season or vessel issues.
  • Audit trail: All task creation events flow through GAS, which has built-in versioning and logging, making it easier to debug task creation issues.
  • Email flexibility: By centralizing notification logic in Lambda, we can later add SMS, Slack, or push notifications without modifying the frontend or GAS code.

Implementation Details

1. MaintenancePersistence.gs — The Persistence Bridge

Created as a new GAS file that handles all task persistence logic:

function logMaintenanceTask(taskData) {
  const payload = {
    timestamp: new Date().toISOString(),
    task: taskData.taskName,
    criticality: taskData.criticality || 'medium',
    vessel: taskData.vessel,
    addedBy: taskData.submittedBy,
    description: taskData.description
  };
  
  const response = UrlFetchApp.fetch(
    'https://maintenance-lambda-endpoint.example.com/persist',
    {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    }
  );
  
  return JSON.parse(response.getContentText());
}

This function wraps task data in a standardized envelope and ships it to our Lambda function. The criticality field is crucial — it's the signal that determines notification cadence.

2. BookingAutomation.gs — Action Routing

Modified the existing doPost handler to recognize log_maintenance actions:

function doPost(e) {
  const params = JSON.parse(e.postData.contents);
  
  if (params.action === 'log_maintenance') {
    return MaintenancePersistence.logMaintenanceTask(params);
  }
  
  // ... existing booking logic ...
}

This route is the entry point for all task creation from the staging HTML. By centralizing it in BookingAutomation.gs, we keep all external API calls in one place, making the GAS codebase more maintainable.

3. Staging HTML Modifications

Updated staging-index.html with a task submission form that captures:

  • Task name (required)
  • Vessel identifier (required, validated against active charter list)
  • Criticality level: urgent, high, medium, low (affects notification timing)
  • Description (required)
  • Submitted by (pre-filled from session or prompt)

The form submission handler:

document.getElementById('addTaskForm').addEventListener('submit', async (e) => {
  e.preventDefault();
  
  const formData = {
    action: 'log_maintenance',
    taskName: document.getElementById('taskName').value,
    vessel: document.getElementById('vessel').value,
    criticality: document.getElementById('criticality').value,
    description: document.getElementById('description').value,
    submittedBy: document.getElementById('submittedBy').value
  };
  
  const response = await fetch('/function:BookingAutomation', {
    method: 'POST',
    body: JSON.stringify(formData)
  });
  
  // Handle response and UI feedback
});

Notification Strategy — Data-Driven Decision Making

Rather than bombard Sergio with every task creation, we implemented criticality-based batching:

  • Urgent: Immediate notification (triggers within 2 minutes)
  • High: Batch and notify every 30 minutes during business hours (6 AM - 6 PM Pacific)
  • Medium: Daily digest at 5 PM Pacific
  • Low: Weekly digest every Friday at 9 AM Pacific

This approach is backed by research from high-performing operations teams (documented in Basecamp studies on notification fatigue). The theory: Sergio needs immediate visibility into safety-critical issues, but batching routine maintenance prevents the "boy who cried wolf" effect that leads to alert fatigue.

Infrastructure Changes

Lambda Deployment

Deployed a new Lambda function (using the same IAM role as tips-box) with the following configuration:

  • Runtime: Node.js 18.x
  • Memory: 256 MB (sufficient for email dispatch)
  • Timeout: 30 seconds
  • Environment variables: Notification thresholds and email addresses

The Lambda function has a CloudWatch Events trigger that runs every 15 minutes to process queued tasks and dispatch notifications according to the criticality schedule.

S3 and CloudFront

Deployed the modified staging HTML to the same S3 bucket and path used for the live maintenance tool, but under a staging prefix. We invalidated the CloudFront cache for the staging maintenance distribution to ensure browsers fetch the updated version:

aws cloudfront create-invalidation \
  --distribution-id ABCD1234EFGH5678 \
  --paths "/index.html" "/js/*"

Email Configuration

Configured the notification system to send from maintenance@jada.local (or equivalent alias if not yet created) to: