Problem #
As a remote dev, I always found it hard to manage multiple meetings across different accounts. Missing important meetings, getting late in joining, etc.
Friction:
- Account switching overhead - 2-3 minutes per meeting to ensure the right account is active
- Wrong account access - Joining meetings with personal account when work account is required
- Configuration portability - Setting up meeting preferences on each new device
For someone attending meetings daily, this adds up to wasted time and frustration.
Constraints #
- Manifest V3 compliance - Chrome's new extension standard with service workers
- No external servers - All data must stay local (privacy)
- Works without Google API access - Parse calendar DOM directly since OAuth adds friction
- Cross-platform support - Google Meet, Zoom, Microsoft Teams
- Shareable configs - Team members should be able to share meeting setups
Architecture #
Manager"] meetings["Meetings
List"] qr["QR Export
/Import"] accounts ~~~ meetings ~~~ qr end messaging["Chrome Messaging API"] subgraph worker["Service Worker (Background)"] direction LR alarms["Chrome Alarms
(Scheduling)"] tabs["Tab Management
(Auto-join + URL rewrite)"] alarms ~~~ tabs end subgraph scripts["Content Scripts"] direction LR calendar["calendar.google.com
(Event extraction)"] meet["meet.google.com
(Join button detection)"] calendar ~~~ meet end end subgraph storage["Chrome Storage API"] data["Accounts | Meetings | User Preferences"] end popup --> messaging messaging --> worker worker --> scripts scripts --> storage
Key components:
- Popup UI - Vanilla JavaScript UI for managing accounts and meetings
- Service Worker - Handles alarms, tab opening, and URL manipulation
- Content Scripts - DOM parsing on Google Calendar and Meet
- Chrome Storage - Local-only data persistence
Decisions & Tradeoffs #
Why content scripts over Google Calendar API?
The official Calendar API requires OAuth consent flow:
- User must authorize the extension
- Token refresh complexity
- Privacy concerns about calendar access
Content scripts let us extract meeting data directly from the DOM:
- No OAuth required
- Works immediately after install
- Only extracts data from events the user explicitly views
Tradeoff: Fragile if Google changes their DOM structure, but avoids the OAuth friction. Works well for personal use.
Why MutationObserver + URL watching for dialog detection?
Google Calendar is a single-page application-event dialogs appear without page navigation:
class MeetAssistOverlay {
initDOMObserver() {
const observer = new MutationObserver((mutations) => {
this.checkForEventDialog();
});
observer.observe(document.body, { childList: true, subtree: true });
}
initUrlWatcher() {
// Detect /eventedit/ URLs for new events
setInterval(() => {
if (location.href !== this.lastUrl) {
this.lastUrl = location.href;
this.checkForEventDialog();
}
}, 500);
}
}Three detection strategies provide reliable dialog detection.
Why authuser URL parameter for account switching?
Google uses authuser=N to specify which logged-in account to use:
meet.google.com/abc-xyz?authuser=0→ First accountmeet.google.com/abc-xyz?authuser=1→ Second account
The extension rewrites meeting URLs before opening tabs:
function rewriteUrlWithAccount(url, accountIndex) {
const urlObj = new URL(url);
urlObj.searchParams.set('authuser', accountIndex.toString());
return urlObj.toString();
}This is simpler than cookie manipulation and works across all Google services.
Why QR codes for configuration sharing?
For working with same setup on different systems, we need some sort of import export functionality. Options considered:
- Cloud sync - Requires server infrastructure and accounts
- JSON file export - Clunky UX for non-technical users
- QR codes - Visual, easy to scan, works on mobile
QR with gzip compression fits ~99 meetings in a single code:
import pako from 'pako';
function exportToQR(config) {
const json = JSON.stringify(config);
const compressed = pako.gzip(json);
const base64 = btoa(String.fromCharCode(...compressed));
return generateQRCode(base64);
}Implementation Details #
Google Calendar Event Extraction
The content script parses various DOM patterns to extract meeting data:
extractMeetingData() {
// Strategy 1: URL-based extraction (for /eventedit URLs)
const urlMatch = location.href.match(/eventedit\/(\w+)/);
// Strategy 2: Dialog DOM parsing
const dialog = document.querySelector('[role="dialog"]');
// Extract title, time, and meeting URL
const title = dialog?.querySelector('[data-eventid]')?.textContent;
const meetUrl = this.findMeetingUrl(dialog);
const datetime = this.parseEventDateTime(dialog);
return { title, meetUrl, datetime };
}
findMeetingUrl(container) {
// Check for Google Meet, Zoom, Teams links
const links = container.querySelectorAll('a[href]');
for (const link of links) {
if (this.detectPlatform(link.href)) {
return link.href;
}
}
}Service Worker Alarm System
Chrome Alarms API schedules meeting auto-joins:
// Schedule meeting 1 minute before start
chrome.alarms.create(`meeting-${meeting.id}`, {
when: meeting.datetime - 60000
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name.startsWith('meeting-')) {
const meetingId = alarm.name.split('-')[1];
autoJoinMeeting(meetingId);
}
});Recurring Meeting Pattern Support
Supports daily, weekly, and monthly recurrence:
function getNextOccurrence(meeting) {
const { recurrence, datetime } = meeting;
const now = Date.now();
let next = new Date(datetime);
while (next.getTime() < now) {
switch (recurrence.type) {
case 'daily':
next.setDate(next.getDate() + recurrence.interval);
break;
case 'weekly':
next.setDate(next.getDate() + 7 * recurrence.interval);
break;
}
}
return next;
}Failure Modes & Mitigations #
DOM Structure Changes
- Problem: Google Calendar UI updates can break selectors
- Mitigation: Multiple fallback selectors, defensive parsing
- Monitoring: If user(me) faces issues, can fix it in the source code.
Service Worker Termination
- Problem: Manifest V3 service workers are ephemeral (30s timeout)
- Mitigation: Chrome Alarms persist across worker restarts
- Design: Stateless worker design, all state in Chrome Storage
Account Index Mismatch
- Problem: User logs out of account, shifting authuser indices
- Mitigation: Store account email as identifier, re-detect index on use
- UX: Prompt user to verify accounts periodically
Results & Metrics #
- Load Time: < 100ms for popup UI
- Detection Accuracy: Works on ~90% of calendar events tested
- Memory Usage: < 10MB RAM footprint
- Supported Platforms: Google Meet, Zoom, Microsoft Teams
Lessons Learned #
What I'd do differently
- Add DOM selector versioning - Track which CSS selectors work, rotate on failure
- Implement account email verification - Currently trusts authuser index, should verify
- Use TypeScript from start - Vanilla JS worked but types would catch DOM parsing bugs
What worked well
- MutationObserver + polling fallback - Reliable dialog detection despite SPA navigation
- URL rewriting over cookies - Simple, cross-platform account switching
- Gzip + QR for sharing - Config portability without a backend
- Manifest V3 migration - Service worker architecture keeps the extension lightweight