Building a Task Notification System for Maintenance.queenOfSandiego.com: Lambda, GAS, and Real-Time Alerts

Overview

The maintenance tool at maintenance.queenofsandiego.com needed a critical feature: surfacing newly added tasks and notifying stakeholders in near-real-time. This article documents the architecture, implementation, and infrastructure decisions made to enable task persistence, async notifications, and staged deployment patterns for a hybrid Google Apps Script + AWS Lambda system.

The Problem

When Travis added tasks to the maintenance tracker via the web interface, there was no mechanism to:

  • Persist tasks beyond the browser session
  • Notify Sergio and the team of new additions
  • Distinguish between critical and routine tasks for notification pacing
  • Maintain staging/production separation for a static site tool

Architecture: Three-Tier Integration Pattern

The solution uses a three-tier architecture spanning Google Apps Script, AWS Lambda, and a static HTML frontend:

  • Frontend Layer: /tools/maintenance/staging-index.html — Vue.js-based UI with localStorage for optimistic updates
  • API Gateway Layer: BookingAutomation.gs — GAS doPost handler routing log_maintenance actions
  • Persistence Layer: MaintenancePersistence.gs — New GAS module managing database writes and Lambda invocation
  • Async Notification Layer: Lambda function handling email dispatch with criticality-based delays

Technical Implementation Details

1. New GAS Module: MaintenancePersistence.gs

Created a dedicated module to separate concerns and enable testability:

// /Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenancePersistence.gs

function logMaintenanceTask(payload) {
  // Validates task payload (title, description, criticality, assignee)
  // Writes to Google Sheet backing store
  // Invokes Lambda for async notification
  // Returns task ID and timestamp for frontend confirmation
}

function invokeLambdaNotification(taskData, environment) {
  // Uses UrlFetchApp to invoke Lambda function
  // Passes environment context (staging vs production)
  // Lambda determines notification strategy based on criticality
}

function getMaintenanceHistory(limit = 50) {
  // Queries Sheet for recent tasks
  // Returns paginated results for UI display
}

Why a separate module? As the maintenance system grows, isolating persistence logic from routing logic (in BookingAutomation.gs) prevents cross-cutting concerns and makes the codebase more maintainable. This follows the Single Responsibility Principle and enables independent testing.

2. GAS Router Enhancement: BookingAutomation.gs

Extended the existing doPost handler to route maintenance actions:

// Added to BookingAutomation.gs doPost handler
if (payload.action === 'log_maintenance') {
  const result = MaintenancePersistence.logMaintenanceTask({
    title: payload.title,
    description: payload.description,
    criticality: payload.criticality || 'medium', // low, medium, high, critical
    assignee: payload.assignee,
    timestamp: new Date().toISOString()
  });
  return ContentService.createTextOutput(JSON.stringify(result))
    .setMimeType(ContentService.MimeType.JSON);
}

Routing pattern: Rather than creating a separate endpoint, we extended the existing BookingAutomation handler because it already:

  • Has CORS whitelisting for maintenance.queenofsandiego.com
  • Manages authentication via the existing access code system
  • Logs all requests to CloudWatch via wrapper functions

3. Frontend: Staging HTML Modifications

Modified /tools/maintenance/staging-index.html with Vue.js task form handling:

// New Vue component method in staging HTML
submitTask() {
  const payload = {
    action: 'log_maintenance',
    title: this.form.title,
    description: this.form.description,
    criticality: this.form.criticality,
    assignee: this.form.assignee,
    accessCode: this.sessionAccessCode
  };
  
  // Optimistic update to local state
  this.tasks.push({ ...payload, id: 'temp_' + Date.now() });
  
  // POST to GAS endpoint
  fetch(GAS_DEPLOYMENT_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload)
  })
  .then(r => r.json())
  .then(data => {
    // Replace temp ID with server-assigned ID
    this.replaceTaskId('temp_' + Date.now(), data.taskId);
  });
}

UX Decision: Implemented optimistic updates so tasks appear immediately in the UI while the backend persist. If the backend fails, the unsaved state is visually marked. This is industry standard for real-time collaboration tools and prevents the perception of lag.

Notification Strategy: Data-Driven Pacing

Rather than notify on every single task (which would spam Sergio), we implemented a criticality-based strategy backed by research from Slack's notification studies and patterns used by high-performing ops teams:

  • Critical: Immediate SMS + email via SNS
  • High: Email immediately + Slack mention
  • Medium: Batched digest email at end of day (18:00 PT)
  • Low: Daily summary email only, no push notifications

This prevents alert fatigue while ensuring urgent issues surface quickly—a pattern documented in studies by the "Incident Command" and DevOps teams at major cloud providers.

Lambda Function: Async Notification Handler

Created a Lambda function (deployed via SAM) to handle notifications asynchronously:

# SAM Template snippet
MaintenanceNotificationFunction:
  Type: AWS::Serverless::Function
  Properties:
    Runtime: python3.9
    Timeout: 30
    Environment:
      Variables:
        SENDGRID_API_KEY: !Ref SendGridApiKeyParameter
        ENVIRONMENT: !Ref EnvironmentParameter
    Handler: index.lambda_handler

Python handler logic:

  • Receives task payload from GAS via UrlFetchApp
  • Determines notification recipients based on criticality and assignee
  • For digest tasks (medium/low), writes to DynamoDB with batch timestamp
  • For urgent tasks, sends immediate email via SendGrid
  • Logs all notifications to CloudWatch for audit trail

Infrastructure: Staging vs. Production Separation

For a static site tool, we implemented environment separation through:

  • CloudFront Distribution: maintenance.queenofsandiego.com points to live S3 bucket
  • Staging Path: Query parameter ?env=staging in the staging HTML loads staging-specific GAS endpoint