Skip to content

Extension Integration

benjamindobbs edited this page Sep 9, 2026 · 2 revisions

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.


Files

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.

Content-script match targets

  • 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:*/*

Talking to DobbsCore

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).

Talking to PowerSchool

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.

Assignment creation payload

POST /ws/xte/section/assignment body (abridged):

{
  "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> }, ...]
  }]
}

On a PUT (edit), each _assignmentstudentassociations entry additionally carries assignmentsectionid.

Score entry

{
  "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).

Attendance capture

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).

Attendance import

doPullAttendance() (from either portal anchor: #wbl-attendance-sync or #classes-attendance-sync):

  1. GET /api/teacher/classes → the class list (all classes, or a program's linked classes).
  2. 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).
  3. POST /api/wbl/classes/:classId/attendance/import with { 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.

Sync-id round-trip

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.

Non-WBL Habits of Work

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:

  1. GET /api/how/sync?class_id=&week= → per-category weekly + standing aggregates ({ category, name, weekly:{ …, scores:[{student_id, score}] }, standing:{…} }).
  2. For each of the 6 (category × {weekly, standing}): reuse the stored ps_assignment_id if it still resolves, else POST /ws/xte/section/assignment with iscountedinfinalgrade: false. Names: HoW <Category> — Weekly / HoW <Category> — Standing.
  3. PUT /ws/xte/score with round2(score / 100 * maxPoints) per student (rows with no score omitted → blank).
  4. POST /api/how/sync/ids with { class_id, assignments:[{ category, kind, ps_assignment_id, ps_assignmentsection_id }] }, stored on how_sync (upsert on (class_id, category, kind)).

The payload shape is identical whether the class runs its Do Now daily or weekly.

Student matching

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.

Clone this wiki locally