Repository navigation
Architecture
benjamindobbs edited this page Oct 1, 2026
·
3 revisions
| Layer | Choice |
|---|---|
| Runtime | Node ≥ 22.5 |
| Web framework | Express 4 (server/index.js), single process |
| Database | SQLite via Node's built-in node:sqlite (DatabaseSync). No ORM — raw SQL through db.prepare(...).run()/.get()/.all(). |
| Front-end | Static HTML/CSS/vanilla JS. No build step. Chart.js (CDN) on dashboard pages only. |
| Auth | Google Identity Services (OAuth 2.0 implicit token flow), verified server-side against Google's tokeninfo endpoint; persistent random session UUIDs after that. |
| Hosting | Fly.io, from the repo Dockerfile. fly.toml in repo. |
server/index.js is the whole server:
app.use(express.json());
// it.<host> rewrite: serve the /it bundle for the IT-tool subdomain
app.use('/api/auth', require('./routes/auth'));
app.use('/api/kenken', require('./routes/kenken'));
app.use('/api/sat', require('./routes/sat'));
app.use('/api/sat-math', require('./routes/sat-math'));
app.use('/api/measurement', require('./routes/measurement'));
app.use('/api/projection', require('./routes/projection'));
app.use('/api/teacher', require('./routes/teacher'));
app.use('/api/student', require('./routes/student'));
app.use('/api/questions',require('./routes/questions'));
app.use('/api/it', require('./routes/it'));
app.use('/api/wbl', require('./routes/wbl'));
app.use(express.static(path.join(__dirname, '..'))); // the repo root is the web root
app.get('*', /* SPA-ish fallback to index.html, or it/index.html on the it. host */);The repo root is the document root — every top-level folder (KenKen/, SAT-Math/,
SAT-Questions/, Measurement/, Projections/, Skill-Maps/, student/, teacher/, it/, assets/, js/) is served as-is.
There is no bundler; js/*.js files are loaded directly by <script> tags.
| Subsystem | Front-end | API router | Storage |
|---|---|---|---|
| KenKen |
KenKen/, js/kenken.js, js/submitscore.js
|
routes/kenken.js |
kenken_scores |
| SAT English |
SAT-Questions/, js/SATmain.js, js/SATsubmit.js
|
routes/sat.js |
sat_scores; question bank JSON is static |
| SAT Math |
SAT-Math/, js/SATmathmain.js, js/SATmathsubmit.js
|
routes/sat-math.js |
sat_math_scores |
| Ruler Game |
Measurement/, js/measurement.js, js/measurement-core.js (shared with the server) |
routes/measurement.js, measurement.js
|
measurement_scores, measurement_progress
|
| Projections |
Projections/, js/projection.js, js/projection-core.js (shared with the server) |
routes/projection.js, projection.js
|
projection_problems, projection_scores, projection_progress
|
| Skill Maps |
Skill-Maps/, generated data.js per course |
— (static) | — (CSV in Skill-Maps/data/) |
| Student dashboard | student/ |
routes/student.js |
reads the above |
| Teacher portal | teacher/ |
routes/teacher.js, routes/wbl.js
|
classes, class_students, gradebook_settings, WBL tables |
| WBL |
teacher/ WBL tab, student/ WBL section |
routes/wbl.js, wbl/logic.js
|
wbl_* tables |
| IT tool |
it/ (served on it. subdomain) |
routes/it.js |
it_* tables |
| Grade / attendance sync | the DobbsCore Chrome extension |
routes/teacher.js GET /grades, routes/wbl.js /sync/*, /classes/:id/attendance/import
|
writes PowerSchool assignments directly |
The Ruler Game and Projections share:
| File | What it does |
|---|---|
server/tieredGame.js |
createTieredGame({ core, scoresTable, progressTable, rescore }): current tier, rolling-window stats, moving up or down after each answer (including the optional streak rule), and re-scoring old rows when a game's SCORE_VERSION changes. The rules come from the game's *-core.js. |
js/tierprogress.js |
the Tier Progress bar under each game. |
js/howtoplay.js |
the collapsible How to play panel and its one-time "New" notes. |
js/tiercharts.js |
Chart.js configs for the teacher portal and student My Progress charts. |
- Students authenticate as a
user_key— the local part of their@students.hartfordschools.orgemail. - Teachers authenticate as a
teacher_key— the local part of their@hartfordschools.orgemail. Any staff account is a teacher; there is no allow-list. - All cross-subsystem records key on
student_id— the district student number — in canonical form (normalizeStudentId: trimmed, and for all-numeric IDs, leading zeros stripped;"0"for all-zeros). WBL tables store normalized IDs;class_studentsstores the raw PowerSchool number, so code that joins the two does a dual lookup. -
class_students.user_keyis the link between a roster row and an app account, set byPOST /api/teacher/classes/:id/auto-link(matchesstudent_id == users.user_key).
- Browser loads a static practice page + its JS.
-
js/auth.jsrestores a session fromlocalStorage['classtech_session'](or runs Google sign-in), then calls the page'sonSignedIn(). -
js/classpicker.jsfetchesGET /api/student/classes; the student picks the class they're working for. -
GET /api/sat/next?class_id=…returns{domainIdx, skill, difficulty}; the page pulls a matching question from the static bank. - On answer,
POST /api/sat/scorelogs the attempt (withclass_id). -
GET /api/student/daily-progress?class_id=…refreshes the progress pill.
- Teacher, in PowerSchool, clicks a DobbsCore-extension button.
- Extension calls
GET /api/teacher/grades?class_id=&start=&end=(through its background worker) as the teacher. - Server computes each student's grade (see Grading Algorithms) and returns JSON.
- Extension calls PowerSchool's internal
ws/xteendpoints (with the teacher's PS session cookies) to create the assignment and write scores. - For WBL, the extension also calls
POST /api/wbl/sync/idsto store the created PowerSchool assignment IDs so the next sync updates instead of duplicating.
-
Migrations: the base schema is one
db.execblock; every later change is a guardedtry { db.prepare('ALTER TABLE …').run(); } catch {}near the bottom ofserver/db.js. A backfill that must run once is gated on itsALTERsucceeding. -
Foreign keys:
PRAGMA foreign_keys = OFF.REFERENCESclauses are documentation only; every cascade is written explicitly in the route layer. Triggers do still fire (used by WBL to enforce the Do Now cap and Exit Slip immutability). -
Timestamps:
INTEGERepoch-ms (Date.now()). Calendar dates:TEXTYYYY-MM-DD. -
Upserts: prefer
INSERT … ON CONFLICT(...) DO UPDATE SET <named cols>overINSERT OR REPLACE, which deletes-then-reinserts and silently wipes unlisted columns. -
Dev bypass:
DEV_USER=<key>makes everyrequireAuthroute act as that student;DEV_TEACHER=<key>does the same forrequireTeacher.DB_PATHandPORTenv vars for local runs.