Skip to content

Architecture

benjamindobbs edited this page Oct 1, 2026 · 3 revisions

Architecture

Stack

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.

Process shape

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.

Subsystems

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

Shared pieces for tiered games

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.

Identity model

  • Students authenticate as a user_key — the local part of their @students.hartfordschools.org email.
  • Teachers authenticate as a teacher_key — the local part of their @hartfordschools.org email. 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_students stores the raw PowerSchool number, so code that joins the two does a dual lookup.
  • class_students.user_key is the link between a roster row and an app account, set by POST /api/teacher/classes/:id/auto-link (matches student_id == users.user_key).

Request lifecycle (student practice)

  1. Browser loads a static practice page + its JS.
  2. js/auth.js restores a session from localStorage['classtech_session'] (or runs Google sign-in), then calls the page's onSignedIn().
  3. js/classpicker.js fetches GET /api/student/classes; the student picks the class they're working for.
  4. GET /api/sat/next?class_id=… returns {domainIdx, skill, difficulty}; the page pulls a matching question from the static bank.
  5. On answer, POST /api/sat/score logs the attempt (with class_id).
  6. GET /api/student/daily-progress?class_id=… refreshes the progress pill.

Request lifecycle (grade push)

  1. Teacher, in PowerSchool, clicks a DobbsCore-extension button.
  2. Extension calls GET /api/teacher/grades?class_id=&start=&end= (through its background worker) as the teacher.
  3. Server computes each student's grade (see Grading Algorithms) and returns JSON.
  4. Extension calls PowerSchool's internal ws/xte endpoints (with the teacher's PS session cookies) to create the assignment and write scores.
  5. For WBL, the extension also calls POST /api/wbl/sync/ids to store the created PowerSchool assignment IDs so the next sync updates instead of duplicating.

Conventions

  • Migrations: the base schema is one db.exec block; every later change is a guarded try { db.prepare('ALTER TABLE …').run(); } catch {} near the bottom of server/db.js. A backfill that must run once is gated on its ALTER succeeding.
  • Foreign keys: PRAGMA foreign_keys = OFF. REFERENCES clauses 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: INTEGER epoch-ms (Date.now()). Calendar dates: TEXT YYYY-MM-DD.
  • Upserts: prefer INSERT … ON CONFLICT(...) DO UPDATE SET <named cols> over INSERT OR REPLACE, which deletes-then-reinserts and silently wipes unlisted columns.
  • Dev bypass: DEV_USER=<key> makes every requireAuth route act as that student; DEV_TEACHER=<key> does the same for requireTeacher. DB_PATH and PORT env vars for local runs.

Clone this wiki locally