fix(intra): refresh ongoing regular-cursus levels to catch silent Intra recomputes#21
Open
CheapFuck wants to merge 1 commit into
Conversation
…ra recomputes
Intra recomputes `cursus_users.level` server-side as a side-effect of
project_user / scale_team events, but the recompute does NOT reliably bump
`cursus_users.updated_at`. The incremental sync uses `filter[updated_at]`,
so those level changes are silently dropped.
The author already worked around this for piscines via a force-refetch loop,
but the same workaround was never extended to common-core (cursus 1 and 21).
Result: a common-core student's level can freeze at the value from the last
event that *did* happen to touch the timestamp, even though Intra's own UI
shows the correct value.
Real-world reproduction (Codam 2023 cohort student):
- Resume bumped level to 11.16 (synced OK).
- ft_transcendence was validated, taking the level to 12.69 on Intra.
- Three more project validations followed.
- Hero stayed stuck at 11.16 the entire time.
Fix:
- Extract the piscine force-refetch loop into a reusable
`refreshOngoingCursusLevels()` helper.
- Tighten the 'ongoing' filter to `begin_at <= syncDate AND (end_at IS NULL
OR end_at >= syncDate)` so it handles both piscines (end_at always set)
and common-core (end_at typically NULL).
- Make the 'delete missing rows' behaviour opt-in via `deleteMissing`.
Kept `true` for piscines (a pisciner can unregister via Apply, where
that semantics make sense). Set to `false` for REGULAR_CURSUS_IDS, since
silently dropping a common-core row because the API briefly omitted it is
too risky.
- Call the helper for REGULAR_CURSUS_IDS in addition to the existing
piscines pass.
FreekBes
requested changes
Jun 4, 2026
Member
FreekBes
left a comment
There was a problem hiding this comment.
LGTM! Only one small change can be made, where cursus_users for the regular cursus that are no longer found in the API can also be deleted from the CodamHero database (see review comment).
| // We do NOT delete missing rows here: a common-core student dropping out of | ||
| // the curriculum is rare, and Intra may temporarily omit a row for unrelated | ||
| // reasons; better to leave the existing record in place than to delete it. | ||
| await refreshOngoingCursusLevels(api, syncDate, REGULAR_CURSUS_IDS, { deleteMissing: false }); |
Member
There was a problem hiding this comment.
deleteMissing can be safely set to true here too. When applicants sign up for the Common Core through Apply but then change their mind and unregister, nowadays the cursus_user gets deleted and thus CodamHero should delete it too.
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.
What
Extends the existing "force-refetch ongoing cursus_users to catch silent server-side level recomputes" workaround — currently applied only to piscines — to regular cursuses (
cursus_id1 and 21) as well.Why
Intra recomputes
cursus_users.levelserver-side as a side-effect ofproject_user/scale_teamevents, but the recompute does not reliably bumpcursus_users.updated_at. Because the incremental sync filters onupdated_at, those level changes are silently dropped — the local DB freezes at the value from whichever event last happened to also touch the timestamp.The author already knew this and worked around it for piscines via the loop at the old
src/intra/cursus.ts:L201-L255, with the explicit comment:…but the same workaround was never extended to common-core. As a result, common-core students can sit at a stale level indefinitely once they hit a "silent" event.
Real-world reproduction
A Codam 2023 cohort student (cursus 21):
updated_at).ft_containerswas later removed from the curriculum — theproject_userdeletion bumpedupdated_at, synced OK.ft_transcendencewas validated, taking the level to 12.69 on Intra —updated_atnot bumped, sync missed it.How
refreshOngoingCursusLevels(api, syncDate, cursusIds, opts)helper.begin_at <= syncDate AND (end_at IS NULL OR end_at >= syncDate)so it handles both piscines (end_atalways set) and common-core (end_attypicallyNULLuntil graduation/alumnization). The old filterend_at: { gte: syncDate }would exclude every common-core student.opts.deleteMissing:truefor piscines (preserves existing behaviour — a pisciner can unregister via Apply, and dropping the row is appropriate there).falseforREGULAR_CURSUS_IDS. Silently dropping a common-core row because the API briefly omitted one is too risky.REGULAR_CURSUS_IDSafter the existing piscine pass.Cost
/cursus_users?filter[id]=…round per sync, chunked at 100 ids. For Codam, the ongoing common-core population is roughly the size of one cohort × a few years — a handful of API pages, negligible.end_at IS NULLbranch, which can't match piscines anyway).Notes
cursus_id: 0in thein:clause; that ID corresponds to no real cursus and has been dropped in the refactor (it never matched anything).Tested locally
Verified end-to-end with a focused script that seeds one row, writes a fake-stale
level, runs the newrefreshOngoingCursusLevels(REGULAR_CURSUS_IDS), and diffs before/after. Refresh correctly pulls the current Intra value.