This document explains how the MyPack Plus browser extension is structured, how data flows through it, and where to look when changing behavior. It is written for maintainers and contributors who need a mental model of the whole system, not just a single feature.
For UI surface inventory and redesign constraints, see UI_INVENTORY_AND_DESIGN_CRITERIA.md.
MyPack Plus is a Manifest V3 Chrome extension that augments NC State's MyPack Portal registration workflow. It runs inside supported MyPack pages and adds:
- Course, GEP, and major/minor plan search with section comparison
- Schedule preview against enrolled and carted classes
- Historical grade distributions and professor ratings
- Add-to-cart helpers for lecture + lab/recitation combinations
- Local caching so repeat searches stay fast
The extension is independent and is not affiliated with NC State University.
flowchart TB
subgraph Browser["Browser (MyPack Portal page)"]
CS["Content script<br/>src/extension/content.tsx"]
Hook["Page hook<br/>public/realFetchHook.js"]
HostDOM["MyPack DOM + iframes"]
Shadow["Shadow-root overlay<br/>#extension-overlay-root"]
end
subgraph SW["Extension service worker"]
BG["Background<br/>src/extension/background.ts"]
end
subgraph External["External services"]
Registrar["NC State registrar<br/>search.php, cart scripts"]
Supabase["Supabase Edge Functions<br/>grades + professor data"]
GA["Google Analytics 4<br/>Measurement Protocol"]
Status["Status worker<br/>extension health banner"]
end
subgraph Storage["Local persistence"]
ChromeStorage["chrome.storage.local"]
IDB["IndexedDB<br/>mypack-extension-cache"]
LS["localStorage<br/>planner session"]
end
HostDOM --> Hook
Hook -->|"postMessage CLASS_DATA"| CS
CS --> Shadow
CS -->|"DOM injection"| HostDOM
CS -->|"chrome.runtime.sendMessage"| BG
BG --> Registrar
CS --> Registrar
CS --> Supabase
BG --> GA
BG --> Status
CS --> ChromeStorage
CS --> IDB
CS --> LS
| Component | File(s) | Responsibility |
|---|---|---|
| Content script | src/extension/content.tsx |
Bootstraps UI, injects hooks, listens for MyPack XHR data, mounts React overlay |
| Background service worker | src/extension/background.ts |
Cross-origin POST proxy to registrar, analytics relay, status checks, cache clear on update |
| Page hook | public/realFetchHook.js |
Patches XMLHttpRequest in page context to capture MyPack schedule/cart API responses |
| Planner UI | src/ui-system/components/SlideOutDrawer.tsx |
Main “Pack Planner” dialog and launcher |
| Data layer | src/course-management/ |
Search, merge, cache, cart, calendar |
| In-page cards | src/degree-planning/ |
Grade/professor cards injected into MyPack planner rows |
| Staging app | src/staging/ |
Standalone Vite page for UI work without MyPack |
When a supported MyPack page loads, the content script runs at document_start (see public/manifest.json).
- Analytics bootstrap (top frame only):
initializeAnalytics()→ background worker. setupListener()fromsiteResponseStorage.ts: registers awindow.messagelistener for hook payloads.realFetchHook.jsinjection into the top document and into MyPack iframes (PAGECONTAINER, enrollment URLs). The hook must run in page context because content scripts cannot intercept page XHR directly.- Shadow-root overlay creation via
ensureOverlayContainer()insrc/utils/dom.ts:- Creates
#extension-overlay-rootwith an open shadow root - Inlines Tailwind/extension CSS rewritten for
chrome-extension://asset URLs - Attaches a style-order observer so Tailwind utilities win over host CSS bleed
- Creates
- React mount:
RootOverlayDialogsrendersWhatsNewDialog,FirstStartDialog, andSlideOutDrawer. - Schedule scrape: waits for
#scheduleTable, thenscrapeScheduleTable()to inject grade/prof cards into enrolled rows. - Planner iframe click listener: debounced
debouncedScrapePlanner()when the user opens class sections in the cart/planner iframe.
The content script guards against double initialization with window.__mypackEnhancerInitialized.
src/
├── extension/ # MV3 entry points (content + background)
├── ui-system/ # Shared planner UI, workbench, styles, themes
├── course-management/ # Search tabs, APIs, cache, cart, calendar
├── degree-planning/ # Plan types, injected audit cards, response storage
├── user-experience/ # Onboarding, status banner, release notes
├── generated/catalogs/ # Large static reference datasets (terms, GEP, majors)
├── components/ui/ # shadcn-style primitives (Button, Dialog, Tabs, …)
├── utils/ # DOM helpers, debounce, logger, course-search parsers
├── analytics/ # GA4 client + background relay
├── config/ # Supabase URL/key
├── types/ # Shared API response types
├── hooks/ # React hooks (e.g. useAutoSize)
├── lib/ # cn() and small shared utilities
└── staging/ # Planner staging app (no CRX plugin in Vite)
The repo is mid-refactor from PascalCase paths (SearchTabs/, CourseRetrieval.ts) to kebab-case (search-tabs/, courseRetrieval.ts). Treat both as the same modules until the migration finishes; imports should follow whichever path exists in your branch.
All three search tabs (CourseSearch, GepSearch, MajorPlanSearch) converge on the same merged section model.
User selects term + filters
│
▼
dataService.ts (planner-search)
│
├─► searchService.ts ──► background fetchData ──► registrar search.php
│ │
│ ▼
│ parseRegistrarUtil.ts (HTML → CourseData)
│
├─► Supabase batch/single endpoints (grades + RateMyProf matches)
│
▼
mergeDataUtil.ts
│
├─► groupSections.ts (lecture + lab/rec grouping by NC State section numbering)
│
▼
MergedCourseData / ModifiedSection
│
▼
SectionCompareCard + CourseSectionsCardList
Key types (src/course-management/types/section.ts):
ModifiedSection— registrar section row enriched with grades, ratings, cart fields, optionallinkedMeetingsGroupedSections—{ lecture, labs }for a enrollment groupMergedCourseData— course metadata +sections: Record<string, GroupedSections>
Section grouping logic (groupSections.ts) encodes NC State registrar conventions:
- Lectures: section numbers 000–199 (and standalone 3xx, 5xx, 6xx DE, 8xx)
- Labs (2xx) and problem sessions (4xx) attach to lectures sharing the last two digits
- Additional DE components (7xx) attach to 6xx lectures
MyPack loads schedule and cart data via internal XHR endpoints. The extension does not call these APIs directly; it observes them.
MyPack XHR (_getScheduleTableData, _getShopCartTableData, …)
│
▼
realFetchHook.js (page context)
│
postMessage { source: "realFetchHook", type: "CLASS_DATA", … }
│
▼
siteResponseStorage.ts (content script)
│
▼
courseRetrieval cache categories:
scheduleTableData, shopCartTableData, planTermTableData,
scheduleCalEventsData, shopCartCalEventsData
│
▼
CalendarView.tsx + useScheduleBackgroundEvents.ts
CalendarView reads cached table/calendar entries, parses meeting times via parseScheduleDayTime.ts, and renders a week grid. The preview rail overlays a candidate section on top of enrolled/cart blocks and marks conflicts.
When the user views planner/cart rows, scraper.ts:
- Walks MyPack table DOM (selectors tied to PeopleSoft class names)
- Extracts course abbreviation, class number, instructor
- Calls
courseDetailService.ts→ Supabase for grade + professor data - Renders
GradeCard/ProfRatingCardintotd.mypack-extension-cellor a dedicated shadow host
These cards use degreeAuditCards.css injected separately because they live outside the main planner shadow root.
addSectionToCart.ts POSTs to MyPack's IScript_addClassToShopCart PeopleSoft script URL (built by mypackScriptUrl.ts) with the user's session cookies (credentials: "include").
- Standalone section:
class_nbr= selected section - Linked lab/rec:
class_nbr= lecture,relate_class_nbr_1= lab/rec class number
The UI entry point is ToCartButtonCell.tsx on section comparison cards.
Implementation: src/course-management/cache/courseRetrieval.ts
| Layer | When used | TTL / notes |
|---|---|---|
| In-memory memoization | Hot paths inside a session | Process lifetime |
chrome.storage.local |
Small cache entries | Category-specific |
IndexedDB mypack-extension-cache |
Entries > 100 KB | Default 6 h; open-course availability uses 2 min override |
| Null-course cache | Courses known to return no sections | Avoids repeat failed lookups |
localStorage |
Planner tab state + search form state | plannerSessionPersistence.ts |
Cache categories include: courseList, openCourses, gradeProfData, nullCourses, schedule/cart table data, and calendar event blobs.
On extension update, the background worker calls clearAllExtensionCaches() so stale registrar snapshots are not reused across versions.
The main planner renders inside #extension-overlay-root's shadow root so MyPack's global CSS cannot break Tailwind/shadcn styling. Portal targets (dialogs, tooltips) resolve through useOverlayPortalContainer.ts so overlays stay inside the shadow tree.
Some dynamic styling remains inline by design (calendar event geometry, fractional star fills) — see README notes.
SlideOutDrawer.tsx is the primary surface:
- Floating Pack Planner launcher button
- Large
Dialogwith theme toggle, status banner, feedback link - Three tabs: Course Search, GEP Search, Major Search
- Shared preview rail (
PlannerPreviewRail) on the right
Tab state is lifted to the drawer and persisted via plannerSessionPersistence.ts so switching tabs does not lose in-progress searches.
Each search tab uses PlannerWorkbenchLayout:
| Column | Contents |
|---|---|
| Controls | Term/subject/course or GEP/major filters, schedule-fit toggle, density toggle, search button |
| Results | Paginated SectionCompareCard list or grouped course lists |
| Preview rail | Calendar + notes/prereqs for the selected section |
Shared workbench pieces live under src/ui-system/components/workbench/.
Large lookup tables live in src/generated/catalogs/:
termIds.ts,subjectSearchValues.tscourse-search/departmentCourses.typed.tsgep-search/gepCourses.typed.tsmajor-plan-search/majorPlans.ts,minorPlans.ts
These are generated or bulk-maintained datasets, not hand-written UI logic. Update them when NC State publishes new term or catalog data.
| Service | Config | Usage |
|---|---|---|
| NC State registrar | Host permissions in manifest | Open-course HTML search via background POST; cart script via content-script fetch with cookies |
| Supabase | src/config/supabase.ts |
Grade distributions and professor matches via Edge Functions (dataService.ts, gradeService.ts, ratingService.ts) |
| Google Analytics 4 | scripts/build-config.js → .env.* |
Content script events forwarded through background Measurement Protocol (ga4.ts, gaBackground.ts) |
| Status worker | user-experience/status/statusWorker.ts |
Optional operational banner; background can fetch status directly |
The service worker handles these message types:
| Message | Handler | Purpose |
|---|---|---|
fetchData |
POST proxy | Registrar search.php from content script (avoids page CORS/context limits) |
analytics_* |
gaBackground.ts |
Initialize, events, opt-out |
status_worker_fetch |
statusWorker.ts |
Health/status payload |
ping |
immediate pong |
Liveness check |
All fetches that need the extension origin use return true on the listener to keep the Chrome message channel open for async sendResponse.
Tooling: Vite 6, @crxjs/vite-plugin, TypeScript 5.8, Tailwind CSS v4.
| Script | Mode | Output |
|---|---|---|
npm run dev |
development + CRX | HMR extension build |
npm run dev:planner-staging |
staging, no CRX | Opens planner-staging.html |
npm run build |
default production | dist/ |
npm run build:prod |
production + clean + zip | dist/ + dist.zip |
npm run build:staging |
staging extension build | Debug-friendly staging config |
scripts/build-config.js writes .env.development, .env.staging, and .env.production with feature flags (VITE_ENABLE_ANALYTICS, VITE_ENABLE_DEBUG_LOGS, etc.).
Production builds:
- Terser minification with console stripping (unless
VITE_ENABLE_DEBUG_LOGS=true) - Manual chunks:
vendor(React),utils(cheerio) - Optional bundle analysis via
npm run build:analyze→dist/stats.html
Path alias: @/ → src/.
src/staging/PlannerStagingApp.tsx renders planner workbench components against fixture data (plannerDebugData.ts, stagingPreviewAdapter.ts) without MyPack or the CRX plugin.
Use staging when:
- Iterating on section card layout, preview rail, or calendar styling
- Testing schedule-fit filtering without live registrar data
- Running visual checks in a normal browser tab
Entry: npm run dev:planner-staging → /planner-staging.html.
- Unit tests: Vitest (
npm test/npm exec vitest) - Notable suites:
course-management/cache/courseRetrieval.test.ts— cache expiry and storage behaviorcourse-management/services/api/planner-search/dataService.test.ts— data merge/cache keyscourse-management/schedule/parseScheduleDayTime.test.ts— schedule parsingdegree-planning/services/siteResponseStorage.test.ts— hook message handling
There is no automated E2E against live MyPack in CI; manual verification on portalsp.acs.ncsu.edu / webappprd.acs.ncsu.edu remains required for integration changes.
- Keep imports at module top — no inline imports except documented circular-dependency cases.
- Exhaustive switches — use
neverin default branches for discriminated unions (TypeScript will fail when variants are added). - Logging — use
AppLoggerfromsrc/utils/logger.ts; production stripsconsole.*unless debug logs are enabled. - DOM selectors — MyPack uses PeopleSoft-generated IDs/classes; prefer stable patterns (
[id^="scheduleInner_"],[data-label="INSTRUCTOR"]) and guard for missing nodes. - Cache invalidation — schedule/cart changes call
invalidateScheduleCache()fromCalendarView.tsxwhen hook data arrives. - Shadow vs light DOM — planner UI in extension shadow root; row cards may use their own shadow host or plain DOM cells.
- Do not commit secrets — Supabase anon key is public-by-design; keep service role keys and GA secrets out of the repo (build-config values should be rotated if exposed).
| Goal | Start here |
|---|---|
| Add a planner filter | Tab component in search-tabs/, state in tab-state/tabState.ts, persistence in plannerSessionPersistence.ts |
| Change section card fields | SectionCompareCard.tsx, sectionCompareUtils.ts, types in types/section.ts |
| Fix lab/lecture pairing | groupSections.ts, then verify mergeDataUtil.ts linkedMeetings |
| Update schedule preview | CalendarView.tsx, modifiedSectionToScheduleEvents.ts, scheduleFitFilter.ts |
| New MyPack XHR endpoint | Add keyword to realFetchHook.js, handler in siteResponseStorage.ts, cache category in courseRetrieval.ts |
| Registrar HTML parsing change | parseRegistrarUtil.ts (cheerio-based) |
| New catalog term or major list | Regenerate files under generated/catalogs/ |
| Extension permissions | public/manifest.json + verify host URLs match NC State endpoints |
README.md— user-facing overview and dev commandsUI_INVENTORY_AND_DESIGN_CRITERIA.md— UI surfaces and redesign constraintssrc/generated/catalogs/README.md— catalog data ownershipWEBSITE_FEATURES.md— marketing/feature list for the project site
| Term | Meaning |
|---|---|
| MergedCourseData | Registrar course + grouped sections + grades/ratings |
| ModifiedSection | Single section row ready for UI and cart actions |
| GroupedSections | Lecture plus attached labs/recitations for one enrollment choice |
| Preview rail | Calendar column showing schedule fit for a highlighted section |
| Hook | realFetchHook.js XHR interceptor running in page context |
| Workbench | Three-column planner layout (controls / results / preview) |