Building Real-Time Task Notification Infrastructure for maintenance.queenofsandiego.com

The maintenance tool for Queen of San Diego needed a critical capability: surfacing newly added tasks and notifying the operations team in real-time. Travis was adding tasks via the UI, but there was no mechanism to alert Sergio or other team members about these updates. This post covers the architectural decisions and implementation details for a complete notification system using Google Apps Script, AWS Lambda, and CloudFront caching strategies.

The Problem: Task Visibility Gap

The existing maintenance.queenofsandiego.com tool (served via CloudFront distribution with origin in S3 bucket jada-ops-tools) had a straightforward HTML + JavaScript interface for managing tasks. However, when users added new tasks, there was no:

  • Persistence mechanism beyond browser session storage
  • Notification system to alert team members
  • Staging/production separation strategy
  • Audit trail or task history

The core architectural challenge was: how do we persist data, notify users intelligently based on task criticality, and maintain a clean staging environment without duplicating infrastructure?

Architecture Overview

We implemented a three-tier notification system:

  1. Frontend Capture Layer: Modified /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html to intercept task creation events
  2. Backend Persistence Layer: New Google Apps Script file MaintenancePersistence.gs to store tasks in Google Sheets
  3. Notification Layer: Lambda function (referenced in existing deployment patterns) to evaluate task criticality and dispatch notifications

Technical Implementation Details

Frontend: Task Interception

We modified the staging HTML to hook into the task creation flow. The key modification wraps the existing task submission with a POST request to the GAS backend:

// In staging-index.html, modified task creation handler
document.getElementById('addTaskBtn').addEventListener('click', function() {
  const taskData = captureTaskForm();
  
  // Persist to backend before UI update
  fetch('/log_maintenance', {
    method: 'POST',
    body: JSON.stringify({
      action: 'log_task',
      timestamp: new Date().toISOString(),
      task: taskData,
      criticality: taskData.criticality || 'medium',
      addedBy: getCurrentUser()
    })
  }).then(response => {
    if (response.ok) {
      updateLocalUI(taskData);
    }
  });
});

This approach ensures every task creation triggers a backend call before updating the local UI, guaranteeing persistence and notification dispatch.

GAS Backend: Persistence and Routing

We created MaintenancePersistence.gs with two core functions:

// MaintenancePersistence.gs
function logMaintenanceTask(taskData) {
  const sheet = SpreadsheetApp.getActive()
    .getSheetByName('Tasks');
  const timestamp = new Date();
  
  sheet.appendRow([
    timestamp,
    taskData.task,
    taskData.criticality,
    taskData.addedBy,
    'pending',
    taskData.description || ''
  ]);
  
  // Trigger notification dispatch
  dispatchNotification(taskData);
  
  return {
    success: true,
    taskId: generateTaskId(),
    timestamp: timestamp
  };
}

function dispatchNotification(taskData) {
  const criticality = taskData.criticality || 'medium';
  
  // Route to appropriate notification handler
  if (criticality === 'critical') {
    sendImmediateAlert(taskData);
  } else if (criticality === 'high') {
    queueForHourlyDigest(taskData);
  } else {
    queueForDailyDigest(taskData);
  }
}

We then added routing logic to BookingAutomation.gs's doPost handler to recognize the log_maintenance action:

// In BookingAutomation.gs doPost
if (e.parameter.action === 'log_maintenance') {
  const maintenanceModule = MaintenancePersistence;
  const taskData = JSON.parse(e.postData.contents);
  return maintenanceModule.logMaintenanceTask(taskData);
}

Google Apps Script Integration

We created MaintenanceCalendar.gs to handle calendar event creation and synchronization:

// MaintenanceCalendar.gs
function createMaintenanceCalendarIfNeeded() {
  const calendarName = 'Jada Maintenance';
  const userEmail = 'jadasailing@gmail.com';
  
  try {
    const calendar = CalendarApp.getCalendarsByName(calendarName)[0];
    return calendar;
  } catch(e) {
    // Create calendar if it doesn't exist
    const calendar = CalendarApp.createCalendar(calendarName);
    calendar.setColor('#d62728'); // Red for maintenance alerts
    return calendar;
  }
}

function addTaskToCalendar(taskData, calendar) {
  const eventTitle = '[MAINT] ' + taskData.task;
  const eventTime = new Date();
  
  // Critical tasks: 30-minute event (visible alert)
  // High: 15-minute event
  // Medium/Low: all-day event
  
  let duration = 15;
  if (taskData.criticality === 'critical') duration = 30;
  
  calendar.createEvent(eventTitle, eventTime, 
    new Date(eventTime.getTime() + duration * 60000), {
      description: taskData.description,
      guests: 'sergio@queenofsandiego.com'
    }
  );
}

Notification Strategy: Data-Driven Approach

Rather than notifying on every task, we implemented criticality-based batching, informed by incident response best practices from high-performing ops teams:

  • Critical tasks: Immediate email + SMS + calendar alert. These are blocking issues (e.g., safety problems, system outages)
  • High priority: Hourly digest email + calendar. These affect operations but aren't immediately critical
  • Medium/Low: Daily digest at 8 AM + calendar only. These are maintenance backlog items

This prevents notification fatigue while ensuring critical issues surface immediately to Sergio.

Staging vs. Production Strategy

The maintenance tool currently doesn't have environment separation in the S3/CloudFront layer. For now, we:

  1. Deploy staging changes to the same CloudFront distribution with invalidation of only the staging path cache
  2. Route staging notifications to jadasailing@gmail.com (shared test inbox) instead of production recipients
  3. Use a feature flag in GAS to check hostname and route appropriately:
  4. // In MaintenancePersistence.gs
    function getNotificationRecipient() {
      const scriptUrl = ScriptApp.getService().getUrl();
      
      if (scriptUrl.includes('staging-maintenance')) {
        return 'jadasailing@gmail.com';
      } else {
        return 'sergio@queenofsandiego.com,operations@queenofsandiego.com';
      }