refactor(clustermap): derive user grade server-side, shrink wire format#22
Open
CheapFuck wants to merge 1 commit into
Open
Conversation
Moves the pisciner / advanced / alumni classification from the frontend into src/routes/clustermap.ts so it lives in one place and the JSON payload only carries a single 'grade' string per user instead of the full cursus_users array. Server (src/routes/clustermap.ts) - Named constants for the cursus taxonomy (PISCINE_CURSUS_ID, CORE_CURSUS_ID, GRADE_TRANSCENDER, GRADE_ALUMNI) and a UserGrade union type, replacing the magic numbers / strings previously duplicated in base.js. - deriveUserGrade() encodes the rule once; precedence (ongoing piscine > Transcender > Alumni) is now explicit and documented. - enrichUser() / enrichLocation() strip cursus_users from the response and add the derived grade. Applied at all three query sites: the live SSE stream, /clustermap/locations/:at and /clustermap/locations/:from/:to. - ClustermapUser interface drops cursus_users in favour of grade: UserGrade. The Prisma select is unchanged (cursus_users is still needed to derive grade), so DB cost is identical; only the JSON payload shrinks (~80-150 bytes saved per active user per SSE tick). Frontend (static/js/clustermaps/base.js) - createLocation() now just reads location.user.grade. A defensive fallback re-derives locally from cursus_users when grade is absent, so a stale cached client served against a new server (or vice versa) during a rolling deploy keeps working. Also guards against missing user / non-array cursus_users. Frontend (static/js/clustermaps/playback.js) - Forwards user.grade instead of user.cursus_users when constructing the synthetic location objects passed to createLocation/removeLocation.
FreekBes
requested changes
Jun 4, 2026
Member
FreekBes
left a comment
There was a problem hiding this comment.
Good refactor (I should have coded it like this in the first place). One part of the logic can be removed, see the comment from the code review.
| } | ||
| else if (location.user.cursus_users.some(cu => cu.cursus_id === 21 && cu.grade === 'Alumni')) { | ||
| userGrade = 'alumni'; | ||
| // User grade is derived server-side and shipped as a single string. |
Member
There was a problem hiding this comment.
This logic can be skipped; CodamHero only has 1 server that is always on the latest version.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to a1e749a (#PR-ish reference to the pisciner/advanced/alumni feature).
Moves the classification rule out of the two frontend files and into
src/routes/clustermap.tsso it lives in one place, and shrinks the JSON wire format to a singlegradefield per user instead of the fullcursus_usersarray.Why
The classification logic was duplicated across
static/js/clustermaps/base.js(read) andstatic/js/clustermaps/playback.js(relay), with the cursus ids (9,21) and grade labels ('Transcender','Alumni') as magic values in the client. Every live SSE tick and every history fetch also shipped the rawcursus_usersarray for every user just so the client could compute one label from it.What changes
Server (
src/routes/clustermap.ts)PISCINE_CURSUS_ID,CORE_CURSUS_ID,GRADE_TRANSCENDER,GRADE_ALUMNI.UserGradeunion type ('pisciner' | 'advanced' | 'alumni' | null).deriveUserGrade()encodes the rule once. The precedence (ongoing piscine wins over Transcender, which wins over Alumni) is now explicit and documented in a comment.enrichUser()/enrichLocation()stripcursus_usersfrom the response and add the derivedgrade. Applied at all three query sites:/clustermap/locations/live)/clustermap/locations/:at/clustermap/locations/:from/:toClustermapUsernow exposesgrade: UserGradeinstead ofcursus_users.selectis unchanged —cursus_usersis still loaded because the server needs it to derivegrade. So DB cost is identical; only the outgoing JSON shrinks (~80–150 bytes saved per active user per SSE tick).Frontend (
static/js/clustermaps/base.js)createLocation()readslocation.user.gradedirectly.gradeis missing butcursus_usersis present, re-derive locally. This way a stale cached client hitting a new server, or vice versa during a rolling deploy, keeps working. Also guards against missinguserand non-arraycursus_usersso a malformed payload doesn't throw.Frontend (
static/js/clustermaps/playback.js)user.gradeinstead ofuser.cursus_userswhen constructing the synthetic location objects passed tocreateLocation/removeLocation.Non-goals / not touched
.pisciner/.advanced/.alumniCSS, tooltip height) is unchanged.gradeper SSE tick is negligible compared to the bandwidth saved.Verification
npx tsc --noEmit -p .is clean for everything undersrc/after the change.