Repository navigation
Extension Integration
Technical detail on how the DobbsCore Gradebook Sync extension
(benjamindobbs/DobbsCore) connects this platform to
PowerSchool. Teacher-facing usage is in The DobbsCore Extension.
| File | Role |
|---|---|
manifest.json |
MV3. permissions: storage, tabs, scripting. host_permissions for hartford.powerschool.com, classicaltech.org, the Fly.io host, and localhost. |
content.js |
Injected into PS gradebook/attendance pages and the DobbsCore portal. Renders every button and panel; contains all sync + attendance logic. |
background.js |
Service worker. Proxies DobbsCore API calls (CORS bypass) and reads PS page-scope variables via chrome.scripting.executeScript. |
popup.html / popup.js
|
Settings: serverUrl, teacherToken in chrome.storage.sync. |
https://hartford.powerschool.com/teachers/index.html*https://hartford.powerschool.com/teachers/saveAttendanceGrid.action*https://hartford.powerschool.com/teachers/attendance-grid.action*-
https://classicaltech.org/*, the Fly.io host,http://localhost:*/*
content.js never fetches DobbsCore directly (cross-origin from the PS tab). It posts a message to
the background worker:
chrome.runtime.sendMessage({
type: 'KENKEN_FETCH',
url: `${serverUrl}/api/teacher/grades?class_id=${id}&start=${s}&end=${e}`,
method: 'GET', // any method
token: teacherToken, // sent as Authorization: Bearer
body: {...} // JSON, for POST/PATCH
});background.js performs the fetch with Authorization: Bearer <token> and returns
{ ok, status, data }. The token is the teacher's DobbsCore session token (teacher_sessions.token),
copied from the portal — not a Google token (though the server also accepts a raw Google access token
from an @hartfordschools.org account).
Endpoints used: GET /api/teacher/classes, GET /api/teacher/gradebook-settings,
PATCH /api/teacher/classes/:id (domains/assessment write-back), POST /api/teacher/classes/import-roster,
POST /api/teacher/classes/:id/sync-roster, GET /api/teacher/grades,
GET /api/wbl/sync/progress, GET /api/wbl/sync/habits, POST /api/wbl/sync/ids,
POST /api/wbl/classes/:classId/attendance/import,
GET /api/how/sync and POST /api/how/sync/ids (non-WBL Habits of Work).
From a PS tab, content.js calls PS's internal PowerTeacher-Pro endpoints directly — session
cookies ride along automatically:
| Call | Purpose |
|---|---|
GET /ws/xte/student?section_ids=<id>&status=A,P |
roster; each row has studentnumber (district ID) and dcid (PS internal ID). The extension builds dcidMap[studentnumber] = dcid. |
GET /ws/xte/teacher_category/calculations |
marking-period term bins per section — storecode, startdate/enddate, category weights. Stashed on the category <select> dataset (storecode, termstart, termend). |
POST /ws/xte/section/assignment |
create an assignment (see payload below). Response headers Location (assignment id) and AssignmentSectionIds. |
GET /ws/xte/section/assignment/{id} |
read an assignment before reusing it (verify it still exists, read its student list). |
PUT /ws/xte/section/assignment/{id} |
update an assignment — used to reconcile a Work Event assignment's student list. |
PUT /ws/xte/score?push_assignment_scores=false&status=A,I,P |
write an array of score entries. |
When the panel is opened from the DobbsCore portal rather than a PS tab, background.js relays
PS fetches through an open PS tab via executeScript so cookies are still included.
POST /ws/xte/section/assignment body (abridged):
On a PUT (edit), each _assignmentstudentassociations entry additionally carries
assignmentsectionid.
{
"studentsdcid": <dcid>,
"actualscoreentered": "<score>",
"actualscorekind": "REAL_SCORE",
"islate": false, "isexempt": false, "ismissing": false, ...,
"assignmentsectionid": <id>, "sectionsdcid": <id>, "assignmentid": <id>,
"_assignmentsection": { "assignmentsectionid", "sectionsdcid", "_assignment": { "assignmentid" } }
}A student for whom the score function returns null gets no entry — that's how "leave blank" is
implemented (In Progress non-earners, unmatched students, non-participants).
On saveAttendanceGrid.action / attendance-grid.action, content.js calls
captureAttendanceFromPage(), which asks background.js to run executeScript in the page's MAIN
world and read the page-scope JS variable sec_cls_att_arr (plus companions). The result — a
{ sectionId, dates, students, attData } structure — is written to chrome.storage.local, keyed by
section. A toast reports counts.
PS's grid dates are bare M/D with no year; the extension resolves the year from the current date
(with a small look-back for a December/January boundary).
doPullAttendance() (from either portal anchor: #wbl-attendance-sync or
#classes-attendance-sync):
-
GET /api/teacher/classes→ the class list (all classes, or a program's linked classes). - For each class, find its cached PS section — by
ps_section_id, or by PS student-DCID overlap if the section IDs don't line up (gradebook vs. attendance section IDs differ in PS). -
POST /api/wbl/classes/:classId/attendance/importwith{ rows: [{ student_id, date, code }] }— the whole cached range, no per-date step.
Server side, that route only checks class ownership (no WBL-linkage requirement), so every class can
receive attendance. The rows land in wbl_attendance, which is both the WBL Holistic attendance
source and the meeting-day calendar for GET /api/teacher/grades.
After a WBL sync, the extension posts the PowerSchool assignment IDs it created/reused to
POST /api/wbl/sync/ids:
{ "class_id": <id>,
"skills": [{ "credential_id", "skill_id", "ps_assignment_id", "ps_assignmentsection_id" }],
"credentials": [{ "credential_id", "ps_assignment_id", "ps_assignmentsection_id" }],
"work_events": [{ "work_event_id", "ps_assignment_id", "ps_assignmentsection_id" }],
"habits": [{ "soft_skill_code", "ps_assignment_id", "ps_assignmentsection_id" }] }These are stored on wbl_credential_sync / wbl_credential_skill_sync / wbl_work_event_sync /
wbl_habits_sync (upsert that preserves the state column) so the next sync updates the same
assignments instead of creating duplicates.
For a registered section not linked to a WBL program, the extension shows a Sync Habits of
Work button instead of Sync Work-Based Learning. doHowSync:
-
GET /api/how/sync?class_id=&week=→ per-categoryweekly+standingaggregates ({ category, name, weekly:{ …, scores:[{student_id, score}] }, standing:{…} }). - For each of the 6 (category ×
{weekly, standing}): reuse the storedps_assignment_idif it still resolves, elsePOST /ws/xte/section/assignmentwithiscountedinfinalgrade: false. Names:HoW <Category> — Weekly/HoW <Category> — Standing. -
PUT /ws/xte/scorewithround2(score / 100 * maxPoints)per student (rows with no score omitted → blank). -
POST /api/how/sync/idswith{ class_id, assignments:[{ category, kind, ps_assignment_id, ps_assignmentsection_id }] }, stored onhow_sync(upsert on(class_id, category, kind)).
The payload shape is identical whether the class runs its Do Now daily or weekly.
Always by district Student Number (class_students.student_id ↔ PS studentnumber). DobbsCore
normalizes leading zeros; PS reports the number un-padded. dcid is used only inside PS payloads.
{ "standardcalcdirection": "NONE", "standardscoringmethod": "GradeScale", "yearid": <floor(termid/100)>, "_assignmentsections": [{ "name", "duedate", "dueDateObj", "pointspossible", "scoreentrypoints", "totalpointvalue", // = max points "scoretype": "POINTS", "iscountedinfinalgrade": true, // FALSE for Habits of Work "isscorespublish": true, "publishoption": "Immediately", "sectionsdcid": <PS section dcid>, "_assignmentcategoryassociations": [{ "teachercategoryid": <cat>, "isprimary": true }], // Work Events only — restrict to specific students: "_assignmentstudentassociations": [{ "studentsdcid": <dcid> }, ...] }] }