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:
- Frontend Capture Layer: Modified
/Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.htmlto intercept task creation events - Backend Persistence Layer: New Google Apps Script file
MaintenancePersistence.gsto store tasks in Google Sheets - 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:
- Deploy staging changes to the same CloudFront distribution with invalidation of only the staging path cache
- Route staging notifications to
jadasailing@gmail.com(shared test inbox) instead of production recipients - Use a feature flag in GAS to check hostname and route appropriately:
// 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';
}