Building a Real-Time Task Notification System for the JADA Maintenance Tool
The JADA maintenance tool at maintenance.queenofsandiego.com needed a critical capability: surface newly added tasks to the team and notify stakeholders in real-time. This post details the architecture, implementation, and infrastructure changes required to accomplish this across staging and production environments.
The Problem
The maintenance tool was functioning as a task dashboard, but lacked visibility into newly added tasks. When Travis added tasks via SMS integration, there was no mechanism to alert Sergio or other team members. Additionally, the team needed a scalable notification strategy that considered task criticality to determine notification frequency—alerting immediately for critical issues while batching lower-priority tasks into daily digests.
Architecture Overview
The solution involved four major components:
- Frontend Enhancement: Modified staging HTML at
/tools/maintenance/staging-index.htmlto detect new task additions - GAS Handler: New route in
BookingAutomation.gsto handlelog_maintenancePOST requests - Persistence Layer: New file
MaintenancePersistence.gsto manage task state and change detection - Notification Lambda: Serverless function to evaluate task criticality and dispatch emails via SES
Technical Implementation Details
Frontend Changes to staging-index.html
The live maintenance tool HTML at /tools/maintenance/index.html was cloned to staging. Key modifications included:
- Added task change detection logic in the
init()function - Implemented a task diff algorithm comparing current DOM state against a persisted baseline
- On task additions, trigger a POST to GAS with payload:
{action: 'log_maintenance', task: {...}, timestamp: Date.now()} - Maintained backward compatibility with existing access code authentication
The detection mechanism watches for new rows in the task table and serializes them before transmission:
// Pseudo-implementation detail
function detectNewTasks() {
const currentTasks = Array.from(document.querySelectorAll('table.tasks tr'))
.map(row => ({id: row.dataset.taskId, title: row.textContent}));
const newTasks = currentTasks.filter(t => !previousState[t.id]);
if (newTasks.length > 0) {
notifyBackend(newTasks);
}
previousState = currentTasks;
}
GAS Router in BookingAutomation.gs
Extended the existing doPost handler to route maintenance requests:
// In BookingAutomation.gs doPost handler
if (params.action === 'log_maintenance') {
const task = params.task;
const timestamp = params.timestamp;
MaintenancePersistence.recordTask(task, timestamp);
MaintenancePersistence.evaluateAndNotify(task);
return ContentService.createTextOutput(JSON.stringify({status: 'recorded'}));
}
This routing keeps all webhook handling in a single entry point, making debugging and monitoring straightforward.
MaintenancePersistence.gs - New Persistence Layer
Created a dedicated file to handle task state management and criticality evaluation. Key responsibilities:
- Persist task metadata to Google Sheets (a dedicated sheet named "Maintenance Tasks")
- Implement criticality classification logic based on task keywords and tags
- Determine notification strategy: immediate vs. daily digest
- Invoke Lambda function for email dispatch
Critical tasks (containing keywords like "electrical hazard," "safety," "emergency") trigger immediate SNS notifications. Standard tasks are batched for a daily 5 PM digest.
Lambda Notification Function
Deployed a Node.js Lambda function (deployed via CloudFormation) that:
- Receives SNS events from GAS via HTTPS endpoint
- Evaluates task criticality using configurable rules
- Formats email with task details, assigned owner, and maintenance context
- Dispatches via SES to stakeholders (initially
jadasailing@gmail.comfor staging) - Logs all notifications to CloudWatch for audit trails
The Lambda role requires permissions: ses:SendEmail, logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents.
Infrastructure and Deployment Strategy
Environment Separation
Since maintenance.queenofsandiego.com serves both staging and production, we implemented separation at multiple layers:
- S3: Staging HTML deployed to
s3://jada-website-assets/maintenance/staging-index.html - CloudFront: Query parameter routing:
?env=stagingserves staging HTML; otherwise production - GAS: Script detects origin domain and routes accordingly
- Email: Staging sends to
jadasailing@gmail.com; production sends to ops team distribution list
CloudFront Configuration
The maintenance distribution (ID: E...) was updated with:
- New origin behavior for
/maintenance/staging-index.html - Cache invalidation pattern:
/maintenance/staging-index.html*after each deployment - Query string forwarding enabled for environment detection
Invalidate cache post-deployment using:
aws cloudfront create-invalidation \
--distribution-id E... \
--paths "/maintenance/staging-index.html" "/maintenance/*"
Google Workspace Integration
Created a new calendar in the JADA Workspace: "Jada Maintenance" (shared with jadasailing@gmail.com). The MaintenanceCalendar.gs file syncs critical maintenance tasks to this calendar for visibility alongside charter schedules.
Key Architectural Decisions
Why Google Sheets for task persistence? The team already uses Google Workspace and authenticated with GAS scripts. No additional infrastructure required. Easy for non-technical team members to audit task history.
Why Lambda for notifications? Decouples email dispatch from GAS execution, which has stricter rate limits. Lambda scales automatically with notification volume and provides CloudWatch logging for operational visibility.
Why criticality-based notification strategy? Industry best practice (supported by research from high-performing ops teams like Netflix and Spotify) shows that "alert fatigue" reduces team responsiveness. Immediate alerts for critical issues, daily digests for standard tasks, balances urgency with cognitive load.
Why query parameter for staging vs. production? Avoids DNS duplication and allows testing production infrastructure behavior while isolating data. The single CloudFront distribution is easier to manage than parallel staging/prod CDNs.