Skip to content

refactor(clustermap): derive user grade server-side, shrink wire format#22

Open
CheapFuck wants to merge 1 commit into
codam-coding-college:masterfrom
CheapFuck:refactor/clustermap-grade-server-derived
Open

refactor(clustermap): derive user grade server-side, shrink wire format#22
CheapFuck wants to merge 1 commit into
codam-coding-college:masterfrom
CheapFuck:refactor/clustermap-grade-server-derived

Conversation

@CheapFuck
Copy link
Copy Markdown

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.ts so it lives in one place, and shrinks the JSON wire format to a single grade field per user instead of the full cursus_users array.

Why

The classification logic was duplicated across static/js/clustermaps/base.js (read) and static/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 raw cursus_users array for every user just so the client could compute one label from it.

What changes

Server (src/routes/clustermap.ts)

  • Named constants for the Codam cursus taxonomy: PISCINE_CURSUS_ID, CORE_CURSUS_ID, GRADE_TRANSCENDER, GRADE_ALUMNI.
  • New UserGrade union 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() strip cursus_users from the response and add the derived grade. Applied at all three query sites:
    • the live SSE stream (/clustermap/locations/live)
    • /clustermap/locations/:at
    • /clustermap/locations/:from/:to
  • ClustermapUser now exposes grade: UserGrade instead of cursus_users.
  • The Prisma select is unchangedcursus_users is still loaded because the server needs it to derive grade. 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() reads location.user.grade directly.
  • Defensive fallback: if grade is missing but cursus_users is 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 missing user and non-array cursus_users so a malformed payload doesn't throw.

Frontend (static/js/clustermaps/playback.js)

  • Forwards user.grade instead of user.cursus_users when constructing the synthetic location objects passed to createLocation/removeLocation.

Non-goals / not touched

  • The visual treatment (.pisciner / .advanced / .alumni CSS, tooltip height) is unchanged.
  • The Prisma schema is unchanged.
  • No caching layer was added — at ~200 active sessions, recomputing grade per SSE tick is negligible compared to the bandwidth saved.

Verification

  • npx tsc --noEmit -p . is clean for everything under src/ after the change.
  • Local smoke: rendered clustermap and confirmed rings/tooltips still show pisciner/advanced/alumni correctly.

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.
Copy link
Copy Markdown
Member

@FreekBes FreekBes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic can be skipped; CodamHero only has 1 server that is always on the latest version.

@FreekBes FreekBes added the refactor Refactor of existing code label Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Refactor of existing code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants