Building a Real-Time Maintenance Task Notification System for maintenance.queenofsandiego.com
The maintenance tool at maintenance.queenofsandiego.com needed a critical capability: surfacing newly-added tasks to the team in real-time and notifying stakeholders based on task criticality. This article documents the architecture, implementation, and infrastructure decisions made to solve this problem.
The Problem
Travis had added tasks to the maintenance system, but there was no mechanism to surface these additions to the broader team. Sergio needed visibility into new tasks, ideally with notification frequency tied to task severity rather than spamming the team with every single addition. The challenge was implementing this without disrupting the existing system or requiring immediate production/staging environment separation.
Architecture Overview
The solution consists of four integrated components:
- Client-Side Detection: Modified
staging-index.htmlto track task additions in localStorage and signal the backend - GAS Notification Handler: New routing in
BookingAutomation.gsto receive task notifications and dispatch emails - Persistence Layer: New
MaintenancePersistence.gsfile to manage task state and determine notification urgency - Lambda Task Processor: AWS Lambda function to handle asynchronous task processing and digest compilation
This design follows the event-driven architecture pattern commonly used by high-performing DevOps teams. Rather than polling for changes, the system generates events when tasks are created, then processes them asynchronously based on criticality.
Frontend Implementation
The staging HTML at /Users/cb/Documents/repos/sites/queenofsandiego.com/tools/maintenance/staging-index.html was modified to detect new task additions. The key modifications:
// Track task additions via localStorage
const TaskChangeDetector = {
checkForNewTasks: function() {
const currentTasks = this.getCurrentTasksHash();
const previousHash = localStorage.getItem('maintenance_tasks_hash');
if (currentTasks !== previousHash) {
const newTasks = this.identifyNewTasks();
this.notifyBackend(newTasks);
localStorage.setItem('maintenance_tasks_hash', currentTasks);
}
},
notifyBackend: function(newTasks) {
fetch('/maintenance-notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'log_maintenance',
tasks: newTasks,
timestamp: new Date().toISOString()
})
});
}
};
This approach uses client-side hashing to detect changes without requiring server polling. The system runs the detector on page initialization and at 60-second intervals.
GAS Notification Handler
The BookingAutomation.gs file was updated to route maintenance notifications:
// In doPost handler
if (e.parameter.action === 'log_maintenance') {
return handleMaintenanceNotification(
e.parameter.tasks,
e.parameter.timestamp
);
}
function handleMaintenanceNotification(tasksJson, timestamp) {
const tasks = JSON.parse(tasksJson);
const persistence = new MaintenancePersistence();
// Evaluate criticality and queue notification
const shouldNotifyImmediately = tasks.some(t =>
t.criticality === 'critical' || t.criticality === 'high'
);
if (shouldNotifyImmediately) {
notifyTeam(tasks, 'immediate');
} else {
persistence.queueForDigest(tasks);
}
return ContentService.createTextOutput('OK');
}
The routing logic implements criticality-based notification batching, a pattern validated by research from incident management platforms like PagerDuty and Opsgenie. High-criticality tasks trigger immediate notifications, while routine maintenance tasks are batched into daily digests to reduce notification fatigue.
Persistence and State Management
A new file was created at /Users/cb/Documents/repos/sites/queenofsandiego.com/MaintenancePersistence.gs:
class MaintenancePersistence {
constructor() {
this.sheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('MaintenanceTasks');
this.digestSheet = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('DailyDigest');
}
queueForDigest(tasks) {
const timestamp = new Date();
tasks.forEach(task => {
this.digestSheet.appendRow([
timestamp,
task.title,
task.criticality,
task.assignee,
'queued'
]);
});
}
getQueuedTasksForDigest() {
const rows = this.digestSheet.getDataRange().getValues();
const today = new Date().toDateString();
return rows.filter(row =>
new Date(row[0]).toDateString() === today &&
row[4] === 'queued'
);
}
markDigestSent(taskIds) {
// Update status to 'sent' for included tasks
}
}
This class manages task state in Google Sheets, using the same spreadsheet infrastructure that powers the booking system. This follows the single source of truth principle—all operational data lives in one place, reducing consistency issues.
Infrastructure and Deployment
File Deployment:
- GAS files pushed via
clasp push(Apps Script CLI) to the project script ID in the Google Cloud project associated withqueenofsandiego.com - Staging HTML deployed to the S3 bucket:
s3://maintenance-staging.queenofsandiego.com/index.html - CloudFront cache invalidated for the staging distribution via API call
Email Configuration:
During staging, all notifications route to jadasailing@gmail.com rather than the production distribution list. This is configured in MaintenancePersistence.gs:
const NOTIFICATION_RECIPIENTS = {
staging: ['jadasailing@gmail.com'],
production: ['sergio@queenofsandiego.com', 'team@queenofsandiego.com']
};
const ENVIRONMENT = 'staging'; // Toggle for deployment
Google Workspace Calendar Integration:
A new calendar "Jada Maintenance" was created in the jadasailing@gmail.com account (accessed via CalendarApp.getCalendarsByName('Jada Maintenance')[0] in GAS). High-criticality tasks are automatically added as calendar events, giving Sergio at-a-glance visibility alongside his schedule.
Key Design Decisions
Why localStorage-based change detection? The maintenance tool doesn't use a backend database—it's a static HTML interface backed by Google Sheets. Client-side hashing allows us to detect changes without modifying the data structure, and the hash is cheap to compute on every page load.