College Deadline Tracker
A single-pane-of-glass application workflow for rising high school seniors: college lists, essay prompts, deadlines, calendars, free-time reminders, and a support chatbot—all living inside the browser.
01 Overview
The College Deadline Tracker is a Manifest V3 browser extension targeting Chrome and Edge, with Firefox compatibility as a stretch goal. It gives rising high school seniors a single-pane-of-glass for managing their college application workflow.
- College list building
- Supplement essay prompts
- Application deadlines
- Google Calendar synchronization
- Smart free-time reminders
- In-extension AI support
Student data lives in chrome.storage.local.
There is no proprietary backend server.
External Network Calls
- Third-party supplement prompt source
- Google Calendar API v3
- OpenAI Chat Completions API
Technology Stack
| Layer | Technology |
|---|---|
| Language | TypeScript |
| Build | Vite + @crxjs/vite-plugin |
| UI | React 18 + CSS Modules |
| State | Zustand |
| Testing | Vitest + fast-check |
| Formatting | ESLint + Prettier |
02 Architecture
The extension is divided into four execution contexts that communicate through the Chrome extension messaging system.
React SPA
background.ts
REST / OAuth
Browser Contexts
- Popup: React interface and user interaction.
- Service Worker: networking, persistence, authentication, scheduling.
- Content Script: optional page scraping and DOM extraction.
Storage
All persistent state is stored in:
chrome.storage.local
The service worker does not rely on window,
localStorage, or persistent DOM state.
Service workers may be terminated between events. Every operation therefore assumes that the worker may disappear and restart.
03 Communication
The popup never calls external APIs directly. It sends typed messages to the service worker, which performs network I/O and storage operations.
type ExtensionMessage =
```
| { type: 'ADD_COLLEGE'; payload: { name: string } }
| { type: 'REMOVE_COLLEGE'; payload: { id: string } }
| { type: 'ADD_DEADLINE'; payload: {
collegeId: string;
label: string;
date: string;
} }
| { type: 'REMOVE_DEADLINE'; payload: {
deadlineId: string;
} }
| { type: 'EDIT_DEADLINE'; payload: {
deadlineId: string;
label: string;
date: string;
} }
| { type: 'CONNECT_CALENDAR' }
| { type: 'DISCONNECT_CALENDAR' }
| { type: 'ENABLE_FREE_TIME_REMINDERS'; payload: {
deadlineId: string;
} }
| { type: 'DISABLE_FREE_TIME_REMINDERS'; payload: {
deadlineId: string;
} }
| { type: 'RETRY_PROMPT_FETCH'; payload: {
collegeId: string;
} }
| { type: 'SEND_CHAT_MESSAGE'; payload: {
message: string;
sessionHistory: ChatMessage[];
} }
| { type: 'GET_STATE' };
```
04 Module Boundaries
| Module | Location | Responsibility |
|---|---|---|
| StorageService | src/services/storage.ts |
Typed chrome.storage wrappers |
| CollegeListService | src/services/collegeList.ts |
College CRUD + validation |
| DeadlineService | src/services/deadline.ts |
Deadline CRUD + validation |
| PromptFetcherService | src/services/promptFetcher.ts |
Prompt fetching + caching |
| CalendarService | src/services/calendar.ts |
Google Calendar integration |
| AuthService | src/services/auth.ts |
OAuth lifecycle |
| ChatbotService | src/services/chatbot.ts |
AI calls + session isolation |
| MessageRouter | src/background/router.ts |
Message dispatch |
| Popup | src/popup/ |
React UI |
05 Components & Interfaces
Service Worker
chrome.runtime.onInstalled.addListener(initializeStorage);
```
chrome.runtime.onMessage.addListener(
MessageRouter.handle
);
chrome.alarms.onAlarm.addListener(
handleAlarm
);
chrome.alarms.create(
'refreshCommonAppPrompts',
{ periodInMinutes: 1440 }
);
```
StorageService
interface StorageSchema {
```
collegeList: CollegeEntry[];
commonAppPrompts: CommonAppPrompt[];
commonAppPromptsLastUpdated: number;
promptCache: Record;
authState: AuthState | null;
chatOnboardingShown: boolean;
onboardingDismissed: boolean;
}
class StorageService { static async get(
key: K
): Promise;
static async set(
key: K,
value: StorageSchema[K]
): Promise;
static async remove(
key: K
): Promise;
static async clear(): Promise;
}
```
College List
interface AddCollegeResult {
```
success: true;
entry: CollegeEntry;
} | {
success: false;
error:
| 'DUPLICATE'
| 'MAX_REACHED'
| 'EMPTY_NAME'
| 'STORAGE_FAILURE';
}
```
Deadline Service
interface DeadlineValidationError {
```
field: 'label' | 'date';
reason:
| 'EMPTY'
| 'TOO_LONG'
| 'INVALID_DATE';
}
```
Calendar
Calendar synchronization uses exponential backoff:
- Initial delay: 2 seconds
- Each retry doubles the delay
- Maximum delay: 30 seconds
- Maximum retries: 3
Chatbot
Only message text is sent to the AI API. No PII or authentication tokens are included.
Popup Structure
src/popup/
```
├── App.tsx
├── pages/
│ ├── CollegeListPage.tsx
│ ├── DeadlinesPage.tsx
│ ├── EssayPromptsPage.tsx
│ ├── CalendarSettingsPage.tsx
│ └── SupportChatPage.tsx
│
├── components/
│ ├── CollegeSearchBar.tsx
│ ├── CollegeEntryRow.tsx
│ ├── DeadlineItem.tsx
│ ├── DeadlineForm.tsx
│ ├── PromptCard.tsx
│ ├── ChatMessageBubble.tsx
│ ├── LoadingIndicator.tsx
│ ├── ErrorMessage.tsx
│ ├── SyncStatusIcon.tsx
│ ├── NavBar.tsx
│ └── OnboardingPrompt.tsx
│
└── hooks/
├── useStorage.ts
├── useTheme.ts
└── useMessageDispatch.ts
```
06 Data Models
Persistent application state lives entirely within
chrome.storage.local.
CollegeEntry
interface CollegeEntry {
```
id: string;
name: string;
addedAt: number;
deadlines: Deadline[];
calendarSyncStatus:
| 'synced'
| 'unsynced'
| 'partial';
}
```
Deadline
interface Deadline {
```
id: string;
collegeId: string;
label: string;
date: string;
calendarEventId: string | null;
freeTimeReminderIds: string[];
syncStatus:
| 'synced'
| 'unsynced'
| 'failed';
createdAt: number;
updatedAt: number;
}
```
Prompt Cache
interface PromptCacheEntry {
```
collegeId: string;
collegeName: string;
prompts: SupplementPrompt[];
fetchedAt: number;
status:
| 'fresh'
| 'stale'
| 'error';
}
interface SupplementPrompt {
id: string;
title: string;
body: string;
wordLimit: number | null;
}
```
ChatMessage
interface ChatMessage {
```
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
```
Chat messages are session-scoped and are never persisted
to chrome.storage.local.
Storage Layout
chrome.storage.local = {
```
"collegeList": CollegeEntry[],
"commonAppPrompts": CommonAppPrompt[],
"commonAppPromptsLastUpdated": number,
"promptCache": {
[collegeId: string]: PromptCacheEntry
},
"authState": AuthState | null,
"onboardingDismissed": boolean
}
```
Deadline Classification
type DeadlineStatus =
```
| 'passed'
| 'upcoming'
| 'normal';
function classifyDeadline(
dateStr: string,
now: Date = new Date()
): DeadlineStatus {
const deadline = new Date(dateStr);
if (deadline < now)
return 'passed';
const daysUntil =
(deadline.getTime() - now.getTime())
/ (1000 * 60 * 60 * 24);
if (daysUntil <= 14)
return 'upcoming';
return 'normal';
}
```
07 Correctness Properties
These properties define behaviors that should remain true across valid executions of the system and form the foundation of the property-based testing strategy.
Adding a valid, non-duplicate college to a list under the 30-entry limit results in exactly one entry containing that college.
Adding the same college name again, case-insensitively and after trimming, must fail with DUPLICATE.
Empty or whitespace-only college names must be rejected.
A list containing exactly 30 entries must reject additional colleges.
Empty, whitespace-only, or over-50-character labels cannot be persisted.
Invalid dates such as 2024-02-30 or non-date strings must be rejected.
Dates before now are passed; dates within 14 days are upcoming; all others are normal.
Sorting preserves all entries, orders them by locale-sensitive college name, and is idempotent.
A valid deadline written to storage must return with identical label and date values.
Deadlines returned for a college are ordered chronologically.
Free slots must be at least 60 minutes, occur between 07:00 and 22:00, and never overlap an existing event.
No more than three free-time reminders may be scheduled, distributed across distinct days where possible.
Chatbot requests contain only role/content message data and never include auth tokens, emails, or other identifiers.
08 Error Handling
Storage Failures
Storage writes occur only after validation. In-memory state is never considered committed until the storage operation succeeds.
Prompt Fetch Timeout
- 5-second timeout
- Previous data → stale
- No previous data → error
- UI provides retry functionality
Calendar Retry Logic
async function fetchWithRetry(
```
url: string,
opts: RequestInit,
attempt = 0
): Promise {
const response = await fetch(url, opts);
if (response.ok)
return response;
if (attempt >= 3)
throw new CalendarSyncError(
'MAX_RETRIES_EXCEEDED'
);
const delay = Math.min(
2000 * 2 ** attempt,
30000
);
await sleep(delay);
return fetchWithRetry(
url,
opts,
attempt + 1
);
}
```
OAuth Expiration
Tokens approaching expiration are refreshed silently. Failed refreshes trigger a re-authentication prompt and abort pending Calendar calls.
Chatbot Failure
Network failures and non-2xx responses produce a typed error response. The UI provides external help resources rather than leaving the user without support.
Widget Timeout
Async popup operations have a 10-second timeout. Previously loaded information remains visible when a new request fails.
09 Testing Strategy
Unit Tests
Vitest tests each service in isolation with Chrome APIs mocked.
| Service | Focus |
|---|---|
| CollegeListService | Add / remove / duplicate / cap / validation |
| DeadlineService | Validation / classification / sorting |
| PromptFetcherService | Timeout / stale fallback / caching |
| AuthService | Expiration / disconnect |
| CalendarService | Retries / event payloads / free slots |
| ChatbotService | Privacy / prompts / disclaimers |
Property-Based Testing
fast-check runs every correctness property with
a minimum of 100 generated cases.
// Feature: college-deadline-tracker
```
// Property 7: Deadline classification correctness
fc.assert(
fc.property(
fc.date(),
fc.date(),
(deadline, now) => {
// invariant checks...
}
),
{ numRuns: 100 }
);
```
Integration Tests
- Add college → fetch prompts → create deadline → calendar sync
- OAuth happy path
- Storage cleanup on uninstall
End-to-End Tests
- Extension opens from toolbar
- College add/remove is visible
- 14-day deadline visual state
- Dark/light theme switching
10 Accessibility
Interactive elements must meet a minimum 4.5:1 text contrast ratio and 3:1 contrast for UI components.
Automated accessibility checks run through
axe-core during Playwright tests.
Full compliance also requires manual testing with screen readers including NVDA and VoiceOver.