Building Real-Time Task Notification for Maintenance.queenofsandiego.com: A Multi-Layer Architecture for Operational Awareness
When Travis added new maintenance tasks via SMS and the system had no way to surface them to Sergio or the team, we faced a classic problem in operational tools: data exists, but visibility doesn't. This post documents how we built a notification system that bridges Google Apps Script, Lambda, S3, CloudFront, and email to create real-time awareness of maintenance updates.
The Problem: Silent Data Entry
The maintenance.queenofsandiego.com tool was accepting task submissions, but new entries weren't being surfaced to stakeholders. Without notification, even critical tasks could languish unnoticed. We needed a solution that:
- Notifies team members (initially Sergio and CB) when tasks are added
- Respects task criticality—urgent tasks notify immediately, routine items can digest
- Works in staging/production parity (currently unified, but architecture supports separation)
- Integrates with existing Google Workspace infrastructure (GAS, Gmail, Google Calendar)
- Provides audit trails via the electrical maintenance documentation already in place
Architecture Overview
We implemented a three-tier notification system:
Frontend (staging-index.html)
↓
GAS Handler (BookingAutomation.gs - log_maintenance route)
↓
Lambda Persistence Layer (async processing)
↓
Email Notification (Gmail via SendGrid equivalent or GAS MailApp)
↓
Calendar Event (Google Calendar - "Jada Maintenance")
↓
Audit Log (electrical/01_work_completed_tonight.md + others)
Technical Implementation
1. Frontend Modification (staging-index.html)
Path: /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html
We added task submission capture with criticality metadata. When a user adds a task, the form now includes:
- Task description (existing)
- Criticality level (new): Critical, High, Medium, Low
- Assigned to (new): Sergio, CB, Crew
- Submission timestamp (auto-captured)
The JavaScript modification hooks form submission to POST to the GAS endpoint with action log_maintenance, including the criticality level in the payload.
2. GAS Handler (BookingAutomation.gs)
Path: /Users/cb/Documents/repos/sites/queenofsandiego.com/BookingAutomation.gs
We added a maintenance routing handler to the existing doPost method. When action=log_maintenance:
if (params.action === 'log_maintenance') {
const criticality = params.criticality || 'medium';
const assignedTo = params.assigned_to || 'crew';
// Route based on criticality
if (criticality === 'critical') {
// Immediate notification
notifyMaintenance(params, 'immediate');
} else if (criticality === 'high') {
// Within 1 hour
notifyMaintenance(params, 'priority');
} else {
// End of day digest
notifyMaintenance(params, 'digest');
}
// Persist to Lambda
callMaintenanceLambda(params);
}
This design follows the principle of "criticality-driven notification pacing"—a pattern validated by high-performing operations teams that reduces notification fatigue while ensuring urgent issues get immediate attention.
3. Persistence Layer (MaintenancePersistence.gs)
Path: /Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenancePersistence.gs
New file that handles async calls to a Lambda function. Instead of blocking the GAS execution with direct database writes, we invoke Lambda asynchronously:
function callMaintenanceLambda(taskData) {
const lambdaUrl = PropertiesService.getScriptProperties()
.getProperty('MAINTENANCE_LAMBDA_URL');
const payload = {
task_id: Utilities.getUuid(),
description: taskData.description,
criticality: taskData.criticality,
assigned_to: taskData.assigned_to,
submitted_at: new Date().toISOString(),
submitted_by: Session.getActiveUser().getEmail()
};
UrlFetchApp.fetch(lambdaUrl, {
method: 'post',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
}
Why Lambda instead of direct GAS Sheets writes? Lambda provides:
- Non-blocking execution (GAS POST returns before Lambda finishes)
- Retry logic if persistence fails
- Ability to scale independently from GAS quota constraints
- Structured logging in CloudWatch for audit purposes
4. Notification Handler (MaintenanceCalendar.gs)
Path: /Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenanceCalendar.gs
New file that creates Google Calendar events and sends email notifications. Two notification patterns:
Immediate Notification (Critical tasks):
- Email sent to Sergio + CB immediately via
GmailApp.sendEmail() - Calendar event created on "Jada Maintenance" calendar with 15-minute reminder
- Subject line:
[CRITICAL] New Maintenance Task: {description}
Digest Notification (High/Medium/Low tasks):
- Tasks cached in ScriptProperties
- Time-based trigger (nightly at 6 PM) aggregates tasks added that day
- Single consolidated email sent to jadasailing@gmail.com with task list
- Calendar event created with all tasks in description
Infrastructure Changes
Google Apps Script Deployment
Scripts deployed via clasp to Project ID queenofsandiego-gcp:
clasp push
# Pushes: BookingAutomation.gs, MaintenancePersistence.gs, MaintenanceCalendar.gs
Google Calendar Setup
Created new calendar in jadasailing@gmail.com account:
- Calendar name: "Jada Maintenance"
- Sharing: Editor access to sergio@queenofsandiego.com and cb@queenofsandiego.com
- Notifications: Default 15 minutes for critical events, 1 hour for routine
S3 / CloudFront Updates
Path: s3://maintenance-queenofsandiego-staging/index.html
We deployed the modified staging