Building Real-Time Task Notifications for the Maintenance Management System

The maintenance.queenofsandiego.com tool needed a critical feature: surface newly added tasks and notify stakeholders in real-time. Previously, when Travis or other team members added maintenance tasks, there was no mechanism to alert Sergio or other crew members. This post details the infrastructure and code changes required to implement task notification with criticality-aware delivery patterns.

Problem Statement

The maintenance tool lacked visibility into task additions. Without explicit notifications, new tasks could be missed, creating operational gaps. The challenge was implementing notifications that:

  • Surface new tasks to all relevant stakeholders
  • Use task criticality to determine notification frequency (immediate for critical tasks, digest for routine items)
  • Support staging/production separation for the web-based tool
  • Integrate with existing Google Workspace infrastructure
  • Follow industry best practices for notification systems in high-performing teams

Architecture Overview

The solution spans multiple infrastructure components:

  • Frontend: Modified staging-index.html with task submission interceptors
  • Routing: New maintenance action handlers in BookingAutomation.gs
  • Persistence: New MaintenancePersistence.gs for task storage and lookup
  • Notifications: Lambda function for email delivery with criticality routing
  • Calendar Integration: New MaintenanceCalendar.gs for Google Calendar sync
  • Distribution: CloudFront caching layer for maintenance.queenofsandiego.com

Technical Implementation Details

Frontend Task Submission

Modified /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html to intercept task creation events. When a task form is submitted, JavaScript now captures:

  • Task title and description
  • Assigned crew member
  • Criticality level (Critical / High / Medium / Low)
  • Timestamp of task creation

The form submission triggers a POST to the GAS endpoint with action log_maintenance, passing task metadata for processing.

GAS Routing and Persistence

Created MaintenancePersistence.gs in the Apps Script project to handle task storage:

function storeMaintenance(taskData) {
  // Store in Google Sheets as single source of truth
  // Format: timestamp, title, assignee, criticality, status
  const sheet = getMaintenanceSheet();
  sheet.appendRow([
    new Date(),
    taskData.title,
    taskData.assignee,
    taskData.criticality,
    'pending'
  ]);
  
  return {success: true, taskId: generateTaskId()};
}

Updated BookingAutomation.gs doPost handler to route maintenance actions:

if (params.action === 'log_maintenance') {
  const result = MaintenancePersistence.storeMaintenance(params.taskData);
  triggerNotificationWorkflow(result.taskId, params.taskData);
}

Created MaintenanceCalendar.gs to sync tasks with Google Calendar for visibility across the Jada Sailing workspace. The calendar "Jada Maintenance" stores all maintenance events linked to their criticality.

Criticality-Aware Notification Logic

Research from high-performing operations teams (specifically studies on incident response and on-call rotations) shows that notification fatigue degrades team responsiveness. The solution implements tiered delivery:

  • Critical: Immediate email to Sergio + calendar alert. These are safety or revenue-blocking issues.
  • High: Immediate email notification. Same-day action required.
  • Medium: Daily digest email (accumulated tasks sent once at end-of-business). Batching reduces context switching.
  • Low: Weekly digest. Routine maintenance items reviewed in planning context.

This approach is backed by Atlassian's research on notification interruption costs and Google's SRE practices around alert fatigue.

Email Notification System

Implemented a Lambda function (deployed in AWS) that receives task notifications from the GAS workflow:

// Lambda receives event with taskData and criticality
// Routes to appropriate email queue and delivery schedule

exports.handler = async (event) => {
  const criticality = event.taskData.criticality;
  const recipient = process.env.MAINTENANCE_EMAIL; // jadasailing@gmail.com for staging
  
  if (['Critical', 'High'].includes(criticality)) {
    await sendImmediate(recipient, event.taskData);
  } else {
    await queueForDigest(recipient, event.taskData, criticality);
  }
  
  return {statusCode: 200};
};

The Lambda function uses SNS for immediate notifications and DynamoDB for digest queue storage. Digest emails are triggered by CloudWatch Events (daily at 5 PM Pacific for medium priority, weekly Monday 9 AM for low priority).

Staging/Production Separation

For now, staging and production use the same maintenance.queenofsandiego.com domain served by CloudFront distribution E2XXXXXXXXXXX. Separation achieved through:

  • Environment variables in Lambda: MAINTENANCE_EMAIL set to jadasailing@gmail.com for staging testing
  • HTML form includes hidden field: <input type="hidden" name="environment" value="staging">
  • GAS routing checks environment flag and logs to separate sheets before production deployment
  • CloudFront cache invalidation used after each staging deployment to bypass caching

A future enhancement will use separate S3 origins or a Lambda@Edge function to cleanly separate staging and production at the infrastructure level.

Infrastructure Changes

S3 Buckets:

  • maintenance.queenofsandiego.com S3 bucket: stores live index.html and assets
  • Staging version: uploaded to same bucket with path prefix or separate bucket (TBD)

CloudFront Distribution:

  • Distribution ID: E2XXXXXXXXXXX
  • Origin: S3 bucket for maintenance tool
  • Cache invalidation command: aws cloudfront create-invalidation --distribution-id E2XXXXXXXXXXX --paths "/*"

Google Workspace:

  • Calendar: Created "Jada Maintenance" calendar in jadasailing@gmail.com account
  • Shared with crew members for visibility into scheduled maintenance windows

AWS Lambda:

  • IAM Role: lambda-maintenance-notifications-role with permissions for SNS Publish and DynamoDB PutItem
  • Runtime: Node.js 18