Network Refresh — Technical Reference
This document describes the internal mechanics of the Network Refresh feature: how requests are routed, which IC REST API endpoints are called, how the JSON response is parsed into the extension's internal data format, and what each layer of code is responsible for.
For the user-facing feature overview (pre-flight dialog, progress dialog, fallback behavior), see features/network-refresh.md.
Contents
- High-Level Architecture
- Request Flow Diagram
- IC REST API Endpoints
- Session Validation
- The Three-Step Grade Fetch
- API Base URL Construction
- Why Requests Route Through the Service Worker
- Background Service Worker Handler
- ICApiClient Module
- Response Parsing (
parseApiResponse) - Assignment Mapping (
_mapAssignments) - Category Average Calculation (
_calcCategoryAvg) - Overall Grade Derivation (
_calcOverall) - RefreshController Integration
- Storage Write After Refresh
- Error Handling and Fallback
- Security Constraints
High-Level Architecture
Network Refresh is implemented across three files:
| File | Responsibility |
|---|---|
ic-api-client.js |
IC REST API session check, 3-step grade fetch, JSON → internal format parsing |
background.js |
Service worker message handler - executes the actual fetch() with credentials |
refresh-controller.js |
Pre-flight dialog, progress dialog, orchestrates per-course refresh loop |
The extension page (dashboard.html) cannot make credentialed cross-origin requests directly. All IC API calls are therefore routed through the background service worker using chrome.runtime.sendMessage.
Request Flow Diagram
dashboard.html (refresh-controller.js)
│
├─ calls: ICApiClient.validateSession(apiBase)
│ └─ ICApiClient._fetchViaServiceWorker(url)
│ └─ chrome.runtime.sendMessage({ type: 'ICI_IC_FETCH', url })
│ └─ background.js ICI_IC_FETCH handler
│ └─ fetch(url, { credentials: 'include', ... })
│ └─ IC server → JSON (or error)
│
└─ calls: ICApiClient.fetchCourseGrades(apiBase, sectionID)
├─ Step 1: fetch section data (term IDs)
├─ Step 2: fetch grading task list (task ID)
└─ Step 3: fetch grade detail (all quarters)
└─ ICApiClient.parseApiResponse(data, ...)
└─ writes to chrome.storage.local
IC REST API Endpoints
All endpoints are relative to the API base (see API Base URL Construction).
Session Check
GET {base}/resources/my/userAccount
Returns:
{ "personID": 12345, "firstName": "Jane", "lastName": "Smith" }
Returns HTTP 404 (not 401/403) when the session cookie is absent. Returns HTML (a login page redirect) instead of JSON when the district enforces a redirect-based login.
Step 1 - Section Metadata
GET {base}/resources/section/{sectionID}
?_expand=terms
&_expand=periods-periodSchedule
&_expand=room
&_expand=teachers
The terms array is the key output. Each term object includes:
| Field | Type | Meaning |
|---|---|---|
termID |
number | Primary key used in Step 3 |
termName |
string | Human-readable label, e.g. "Q1" |
termSeq |
number | Ordering index (ascending = chronological) |
startDate |
string (ISO) | First day of the term |
endDate |
string (ISO) | Last day of the term |
isPrimary |
boolean | false for non-graded scheduling terms |
Term selection logic: Terms are filtered to isPrimary !== false, then sorted by termSeq. The latest term whose startDate is on or before today is selected as targetTerm. This matches the term that IC's own portal selects when you open a course page. If all terms are future-dated (e.g. the school year hasn't started), the first sorted term is used as a fallback.
Step 2 - Grading Task List
GET {base}/resources/section/teacherSections/curriculumGradingTask
?_sectionID={sectionID}
Returns an array of grading task objects. The extension picks the task where portal: true and whose name does not match /standard|comment/i. In practice this is always the "Quarter Grade" task with _id: "4".
This step is non-fatal - if the request fails or no matching task is found, taskID is omitted from Step 3's URL and IC returns data using its default task.
Step 3 - Grade Detail
GET {base}/resources/portal/grades/detail/{sectionID}
?classroomSectionID={sectionID}
&selectedTermID={targetTerm.termID}
&selectedTaskID={taskID}
This is the primary data endpoint. When selectedTermID is the current (active) term, IC returns grades for all terms up to and including the selected one - not just the selected term. This means a single Step 3 call gives the full year's data.
The response shape:
{
"courseName": "HONORS ALGEBRA 2 & TRIG 434",
"terms": [
{ "termID": 4461, "termName": "Q1", "termSeq": 1, "startDate": "2024-09-05", "endDate": "2024-11-08" },
{ "termID": 4463, "termName": "Q2", "termSeq": 2, "startDate": "2024-11-12", "endDate": "2025-01-17" }
],
"details": [
{
"task": {
"termID": 4461,
"termName": "Q1",
"portal": true,
"hasAssignments": true,
"hasDetail": true
},
"categories": [
{
"groupID": "c0",
"name": "Tests/Quizzes",
"weight": 60,
"isWeighted": true,
"assignments": [ ... ]
}
]
}
]
}
Session Validation
ICApiClient.validateSession(apiBase) calls the /resources/my/userAccount endpoint and returns:
// Success
{ valid: true, personID: 12345, firstName: "Jane", lastName: "Smith" }
// Not signed in (HTTP 404 or not_json redirect)
{ valid: false, reason: "unauthenticated" }
// Login redirect (HTML response from _fetchDirect)
{ valid: false, reason: "redirected_to_login" }
// Network error or unknown failure
{ valid: false, reason: "<error message>" }
The pre-flight dialog uses this result to show a session badge. If valid is false with reason unauthenticated or redirected_to_login, both refresh buttons are visually disabled (opacity 0.5, pointer-events none). An "I know what I'm doing" override checkbox re-enables them for developers or power users who want to test refresh behavior without an active session.
The Three-Step Grade Fetch
ICApiClient.fetchCourseGrades(apiBase, sectionID) executes the three steps sequentially:
// Step 1
const sectionData = await doFetch(`${apiBase}/resources/section/${sectionID}?_expand=terms...`);
// Step 2 (non-fatal)
let taskID = null;
try {
const tasks = await doFetch(`${apiBase}/resources/section/teacherSections/curriculumGradingTask?_sectionID=${sectionID}`);
const primary = tasks.find(t => t.portal && !/standard|comment/i.test(t.name));
if (primary) taskID = primary._id;
} catch (_) { /* non-fatal */ }
// Step 3
let detailUrl = `${apiBase}/resources/portal/grades/detail/${sectionID}?classroomSectionID=${sectionID}`;
if (targetTerm) detailUrl += `&selectedTermID=${targetTerm.termID}`;
if (taskID) detailUrl += `&selectedTaskID=${taskID}`;
const gradeData = await doFetch(detailUrl);
The function returns the raw Step 3 response (enriched with courseName from Step 1 if absent).
API Base URL Construction
ICApiClient.buildApiBase(sourceUrl) extracts the IC district origin and servlet context path from a cached sourceUrl:
Input: https://scarsdaleny.infinitecampus.org/campus/nav-wrapper/student/…
Output: https://scarsdaleny.infinitecampus.org/campus
Implementation:
function buildApiBase(sourceUrl) {
const url = new URL(sourceUrl);
const firstSegment = url.pathname.split('/').filter(Boolean)[0];
const appContext = firstSegment ? `/${firstSegment}` : '/campus';
return `${url.origin}${appContext}`;
}
The first non-empty path segment is the IC application context (almost always /campus). This is reliable because the sourceUrl stored in the cache always comes from an actual IC course page URL, which always starts with the servlet context path.
Why Requests Route Through the Service Worker
Extension pages (chrome-extension:// origin) cannot make credentialed fetch() calls to external origins directly without CORS cooperation. However, Chrome's Manifest V3 service workers with host_permissions are granted a special privilege:
"A script executing in an extension service worker can talk to remote servers outside of its origin, as long as the extension requests host_permissions."
Additionally, SameSite=Lax cookies are bypassed for host_permissions URLs when the request originates from the service worker, so the IC session cookie (which uses SameSite=Lax) is included automatically. This is not the case for extension page fetch() calls, which are treated like cross-site requests by browsers.
The result: service worker fetch(url, { credentials: 'include' }) with host_permissions = full authenticated API access, no tab needed.
Background Service Worker Handler
background.js listens for ICI_IC_FETCH messages:
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type !== 'ICI_IC_FETCH') return;
// Security: reject any URL that is not on the IC domain
const h = new URL(msg.url).hostname;
if (!h.endsWith('.infinitecampus.org') && h !== 'infinitecampus.org') {
sendResponse({ err: 'blocked_url' });
return true;
}
fetch(msg.url, {
credentials: 'include',
headers: {
'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(resp => {
if (!resp.ok) { sendResponse({ err: `HTTP_${resp.status}` }); return; }
const ct = resp.headers.get('content-type') || '';
if (!ct.includes('application/json')) {
sendResponse({ err: `not_json:${ct.slice(0, 80)}` }); return;
}
return resp.json().then(data => sendResponse({ ok: data }));
})
.catch(e => sendResponse({ err: e.message || 'fetch_failed' }));
return true; // keep message channel open for async response
});
Key points:
- Domain guard: Only
*.infinitecampus.organdinfinitecampus.orghostnames are allowed. Any other URL is rejected witherr: 'blocked_url'. X-Requested-Withheader: IC checks this header to distinguish AJAX requests from browser page navigations. Without it, some endpoints return a full HTML page rather than JSON.return true: Required in Chrome MV3 message handlers to keep the response channel open for async callbacks.- Non-JSON detection: If IC returns HTML (e.g. a session expiry redirect page), the handler detects the non-
application/jsoncontent type and returns a descriptive error string rather than failing onJSON.parse.
ICApiClient Module
ic-api-client.js exposes four public functions:
| Function | Signature | Description |
|---|---|---|
buildApiBase |
(sourceUrl: string) → string|null |
Extracts {origin}/{appContext} from a course URL |
validateSession |
(apiBase: string) → Promise<SessionInfo> |
Checks if the user is signed in via /resources/my/userAccount |
fetchCourseGrades |
(apiBase: string, sectionID: string) → Promise<RawApiData> |
Executes the 3-step fetch and returns the raw Step 3 response |
parseApiResponse |
(apiData, courseName, sectionID, sourceUrl) → CacheEntry |
Converts the raw API response into the internal cache entry format |
The module detects whether it is running in an extension page via:
function _isExtensionPage() {
return typeof window !== 'undefined' &&
!!window.location &&
window.location.protocol === 'chrome-extension:';
}
- Extension page (
chrome-extension://): uses_fetchViaServiceWorker- routes throughICI_IC_FETCH. - Content script or Node (tests): uses
_fetchDirect- callsfetch()directly.
Response Parsing (parseApiResponse)
parseApiResponse(apiData, courseName, sectionID, sourceUrl) converts the Step 3 JSON into the CacheEntry structure expected by dashboard.js and details.js.
Input: Raw Step 3 response object (with terms[] and details[]).
Output:
{
snapshotTs: <epoch ms>,
snapshot: {
courseName: "HONORS ALGEBRA 2 & TRIG 434",
overall: 94.32, // derived from the most recent non-zero quarter
quarters: [
{
id: "Q1",
name: "Q1",
startDate: "2024-09-05",
endDate: "2024-11-08",
categories: [
{
id: "c0",
name: "Tests/Quizzes",
weight: 60,
avg: 91.5,
assignments: [ ... ]
}
]
}
]
},
raw: { courseId: "course:897283", courseName: "...", quarters: [] },
sourceUrl: "https://...",
source: "network"
}
Parsing logic:
- Build a
termMapfromapiData.terms[]:{ [termID]: { termName, startDate, endDate, termSeq } }. - Iterate
apiData.details[]. Skip entries wheretask.portalis falsy or bothtask.hasAssignmentsandtask.hasDetailare falsy. - Group by
termIDinto aquarterMap. For each detail entry, call_mapAssignmentson each category's assignment list and_calcCategoryAvgto compute the category average. - Sort quarters by
_termSeqascending, then strip the_termSeqfield. - Call
_calcOverall(quarters)to derive the overall grade.
Assignment Mapping (_mapAssignments)
Each raw IC assignment is mapped to the extension's normalized assignment shape:
| IC API field | Extension field | Notes |
|---|---|---|
assignmentName |
name |
- |
scorePoints |
earned |
null when ungraded; parsed as float |
totalPoints |
possible |
parsed as float |
score |
(derived) | Used to detect letter grades (`/^([A-D][+-]? |
notGraded |
ungraded: true |
Also set when score === null and not a letter grade and not dropped |
dropped: true |
dropped: true |
- |
multiplier |
multiplier |
Only set when !== 1 |
letter-grade score |
letterGrade: "B+" |
Set when score is a letter and scorePoints is null |
Ungraded detection rule:
const ungraded = !!(
a.notGraded ||
(!isLetterGrade && !a.dropped && rawScore === null)
);
IC uses scorePoints: "0" for some truly ungraded assignments (where no teacher input has been given yet) while setting score: null. The extension must check score (not scorePoints) to distinguish "the score is 0 points" from "not yet graded". Assignments with ungraded: true are excluded from all grade calculations and shown as – in the UI.
Category Average Calculation (_calcCategoryAvg)
function _calcCategoryAvg(assignments) {
const graded = assignments.filter(a => !a.ungraded && !a.dropped);
if (graded.length === 0) return 0;
let totalEarned = 0, totalPossible = 0;
for (const a of graded) {
const m = typeof a.multiplier === 'number' ? a.multiplier : 1;
totalEarned += Math.max(0, Number(a.earned)) * m;
totalPossible += Math.max(0, Number(a.possible)) * m;
}
return totalPossible > 0 ? (totalEarned / totalPossible) * 100 : 0;
}
- Dropped assignments are excluded.
- Ungraded assignments are excluded.
- Multipliers scale both numerator and denominator.
- Returns 0 (not NaN) when no eligible assignments exist.
Note: this function is used only during the network-refresh parse. The grade display on the dashboard and details pages re-calculates category averages using their own helpers (recomputeCategoryWithSebi in dashboard.js, catStats in details.js), which additionally handle letter grade resolution and SEBI mode.
Overall Grade Derivation (_calcOverall)
function _calcOverall(quarters) {
for (let i = quarters.length - 1; i >= 0; i--) {
const q = quarters[i];
const weighted = q.categories.filter(c => c.weight > 0 && c.avg > 0);
const unweighted = q.categories.filter(c => (!c.weight || c.weight === 0) && c.avg > 0);
let overall = 0;
if (weighted.length > 0) {
const weightSum = weighted.reduce((s, c) => s + c.weight, 0);
overall = weightSum > 0 ? weighted.reduce((s, c) => s + c.avg * c.weight, 0) / weightSum : 0;
} else if (unweighted.length > 0) {
// Points-based: aggregate all assignments across categories
let totalEarned = 0, totalPossible = 0;
for (const c of unweighted)
for (const a of c.assignments.filter(a2 => !a2.ungraded && !a2.dropped)) {
totalEarned += Math.max(0, Number(a.earned));
totalPossible += Math.max(0, Number(a.possible));
}
overall = totalPossible > 0 ? (totalEarned / totalPossible) * 100 : 0;
}
if (overall > 0) return overall;
}
return 0;
}
Iterates quarters in reverse order (most recent first) and returns the first non-zero overall grade. This mimics the "current quarter wins" behavior of the IC portal's grade summary page.
RefreshController Integration
refresh-controller.js calls network refresh inside networkRefreshCourse(courseKey, courseName, entry):
1. Extract sectionID from courseKey ("course:897283" → "897283")
2. buildApiBase(entry.sourceUrl) → apiBase
3. ICApiClient.fetchCourseGrades(apiBase, sectionID) → apiData
4. ICApiClient.parseApiResponse(apiData, ...) → newEntry
5. Guard: if newEntry.snapshot.overall === 0 and existing overall > 0, skip write (preserve existing)
6. Backup: chrome.storage.local.set({ [`${courseKey}_BACKUP`]: entry })
7. Write: chrome.storage.local.set({ [courseKey]: newEntry, ICI_CACHE_INDEX: index })
The zero-grade guard (step 5) prevents a network refresh that returns empty data from wiping out a valid cached grade. This can happen during school hours when some districts temporarily disable the gradebook API.
Storage Write After Refresh
After a successful network refresh, two keys are updated:
| Key | Value |
|---|---|
course:{sectionID} |
Full CacheEntry (see parseApiResponse output above) |
ICI_CACHE_INDEX |
Updated index entry: { courseId, courseName, lastUpdated, sourceUrl } |
A backup of the previous entry is also written to course:{sectionID}_BACKUP. This backup is not currently exposed in the UI but is available for manual recovery via the import/export mechanism.
Permanent score overrides (PE_course:{sectionID}) and permanent assignment additions (PA_course:{sectionID}) are not modified by network refresh. They continue to be applied on top of the refreshed grade data the next time the course is viewed.
Error Handling and Fallback
All errors from fetchCourseGrades (non-session errors) cause the refresh loop in RefreshController to fall back to the Classic scrape-based strategy for that individual course. The fallback is logged as scrape-fallback in the progress dialog.
Session errors (those whose Error.message starts with session:) are not retried or fallen back - if the session is invalid, Classic refresh would also fail, so the error is surfaced directly.
Error message prefix │ Behavior
───────────────────────┼─────────────────────────────────────
"session:*" │ Propagate; do not fall back to scrape
"Canceled by user" │ Abort immediately; show canceled state
HTTP_4xx / HTTP_5xx │ Fall back to Classic scrape
not_json:* │ Fall back to Classic scrape
sw_error:* / sw_no_* │ Fall back to Classic scrape
Security Constraints
- Domain restriction:
background.jsrejects any URL whose hostname is notinfinitecampus.organd does not end in.infinitecampus.org. This prevents theICI_IC_FETCHmessage handler from being misused as a general-purpose fetch proxy by any page that can send messages to the extension. - No user credentials stored: The extension never stores or transmits IC passwords or session tokens. It relies entirely on the browser's existing session cookie, which Chrome provides automatically when the service worker fetches a
host_permissionsURL withcredentials: 'include'. - Host permissions scope:
host_permissionsis limited tohttps://*.infinitecampus.org/*andhttps://infinitecampus.org/*. The extension cannot make credentialed requests to any other domain through the service worker. - Read-only API access: All IC API calls are
GETrequests. The extension never writes to the IC gradebook.