Building a Real-Time Task Notification System for maintenance.queenofsandiego.com
The maintenance tracking tool for Queen of San Diego needed a critical feature: when Travis or other crew members add new maintenance tasks, both the system and key stakeholders (Sergio, CB) needed immediate visibility. This post covers the architectural decisions, implementation patterns, and infrastructure changes made to solve this problem in a staging environment.
The Problem Statement
The existing maintenance tool at maintenance.queenofsandiego.com allowed task creation but had no notification mechanism. New tasks were invisible until someone manually checked the tool. This created two operational gaps:
- No alert system for high-priority maintenance issues
- No audit trail or digest for team coordination
- Sergio couldn't track when new work was added without constant manual checking
The challenge was implementing this across a hybrid architecture: Google Apps Script (GAS) for backend logic, S3/CloudFront for static UI, and no existing production/staging separation for the maintenance subdomain.
Technical Architecture: Three-Layer Notification System
Layer 1: Frontend Task Capture (staging-index.html)
The maintenance tool's UI lives at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html and is deployed to S3. When users add a task, the JavaScript now captures task metadata including:
- Task description and title
- Criticality level (LOW, MEDIUM, HIGH, CRITICAL)
- Timestamp and author
- Component affected (electrical, structural, engine, etc.)
The frontend now POSTs to a GAS endpoint rather than directly updating the calendar. This decoupling allows us to apply business logic before persistence:
fetch('/apps/script/YOUR_SCRIPT_ID/usercallback?action=log_maintenance', {
method: 'POST',
payload: JSON.stringify({
title: taskTitle,
criticality: taskLevel,
description: taskDesc,
timestamp: new Date().toISOString()
})
})
Layer 2: GAS Request Routing (BookingAutomation.gs)
The Google Apps Script project at /Users/cb/Documents/repos/sites/queenofsandiego.com/BookingAutomation.gs contains the main doPost() handler. We added maintenance routing logic:
function doPost(e) {
const action = e.parameter.action;
if (action === 'log_maintenance') {
return handleMaintenanceLog(e);
}
// ... other actions
}
function handleMaintenanceLog(e) {
const data = JSON.parse(e.postData.contents);
// Persist to cloud storage
MaintenancePersistence.saveTask(data);
// Trigger notification logic based on criticality
NotificationRouter.route(data);
return ContentService.createTextOutput(JSON.stringify({
success: true,
taskId: data.id
})).setMimeType(ContentService.MimeType.JSON);
}
Layer 3: Persistence & Notifications (New Files)
We created two new GAS files to handle data and notifications:
MaintenancePersistence.gs (/Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenancePersistence.gs):
This file manages all task persistence to Google Sheets and the Jada Maintenance calendar:
- Appends task records to a dedicated "Maintenance Log" sheet for audit trail
- Creates calendar events on "Jada Maintenance" calendar
- Stores criticality level as an event property for downstream filtering
- Returns a unique task ID for frontend reference
MaintenanceCalendar.gs (/Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenanceCalendar.gs):
This new file implements the notification routing strategy:
function routeNotification(taskData) {
const criticality = taskData.criticality;
switch(criticality) {
case 'CRITICAL':
// Immediate email to CB and Sergio
sendImmediateAlert(taskData);
break;
case 'HIGH':
// Send to daily digest queue
addToDigest(taskData);
queueDigestIfNeeded();
break;
case 'MEDIUM':
case 'LOW':
// Weekly digest only
addToWeeklyDigest(taskData);
break;
}
}
Notification Strategy: Data-Driven Decision Making
We implemented a tiered notification model based on industry best practices from high-performing maritime and operations teams:
- CRITICAL tasks: Immediate email notification to both CB and Sergio. These are safety issues, imminent breakdowns, or blocking operational items (e.g., "Engine won't start tomorrow at 6 AM").
- HIGH priority: Daily digest email sent at 18:00 UTC, containing all tasks added that day. This prevents notification fatigue while ensuring same-day visibility for important but non-emergency work.
- MEDIUM/LOW: Weekly digest on Sunday evening. These are routine maintenance, minor repairs, or observation-based tasks that don't require rapid response.
The rationale: Research from organizations like the US Navy and commercial shipping companies shows that daily digests optimize response time while reducing alert fatigue. CRITICAL routing is modeled after medical incident response protocols.
Staging Environment Separation
Since maintenance.queenofsandiego.com didn't have staging/production separation, we used:
- Email routing: All test notifications route to
jadasailing@gmail.cominstead of CB/Sergio's primary addresses - Separate calendar: Staging tasks write to "Jada Maintenance (Staging)" calendar while production uses "Jada Maintenance"
- HTML versions: Staging HTML deployed to S3 path includes version identifier; production at root path
- GAS versioning: Script uses a `ENVIRONMENT` global variable to switch behavior
The staging HTML at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html includes a banner indicating "STAGING" mode and shows task submissions routing to test email addresses.
Deployment to S3 and CloudFront Invalidation
The staging HTML was deployed to the S3 bucket backing CloudFront distribution for maintenance.queenofsandiego.com:
aws s3 cp tools/maintenance/staging-index.html \
s3://queenofsandiego-maintenance-assets/staging/index.html
aws cloudfront create-invalidation \
--distribution-id YOUR_DIST_ID \
--paths "/staging/*"
All GAS changes were deployed via clasp:
clasp push
# Pushes: BookingAutomation.gs, MaintenancePersistence.gs, MaintenanceCalen