Skip to content

Give members a dashboard, and the club a way to run GSoC mentorship - #5

Merged
abhinavjha0239 merged 2 commits into
ScalerOpenSourceLabsOrg:mainfrom
abhinavjha0239:member-dashboard-and-mentorship
Aug 31, 2026
Merged

Give members a dashboard, and the club a way to run GSoC mentorship#5
abhinavjha0239 merged 2 commits into
ScalerOpenSourceLabsOrg:mainfrom
abhinavjha0239:member-dashboard-and-mentorship

Conversation

@abhinavjha0239

Copy link
Copy Markdown
Collaborator

What this is

Joining used to end on a read-only card in the right-hand column of /join — seven fields, a "details saved" message, and nothing else. This gives members somewhere to land, and gives the club the machinery to actually run its GSoC cohort.

Sign in on /join → answer three questions on /onboarding → land on /dashboard, where you can enrol in GSoC mentorship by ranking mentors an organiser has published.

The four pieces

The batch comes out of the address. Every member signs in as abhinav.23bcs10045@sst.scaler.com — 2023–27, branch BCS, roll 10045 — so web/lib/batch.ts parses it and the form stops asking. Nothing derived is stored: the rules pin the address to request.auth.token.email, so a value read from it cannot be forged and cannot drift out of step with the document beside it. That deleted yearOf(), branchOf() and the "Unparsed" bucket the organisers' dashboard used to guess with. The cost is that batch is not queryable — noted in FIREBASE.md and in the file.

BRANCH_LABELS ships empty on purpose: nobody has confirmed what bcs expands to, so it renders as BCS rather than a guess. One line to fill in when the club confirms the codes.

Three questions, not seven. year_branch, level, programs and programs_other are gone. The first is in the address. The second was a self-assessment made before somebody had met the club, that nothing acted on and that stopped being true a fortnight later. The last two were a preference nobody read — replaced by enrolling in a programme, which is a decision with a consequence.

Mentorship. Organisers publish mentors from /admin (add, edit, hide, delete) and members pick a first preference, then either a second or "just my first preference". Exactly one of those two, enforced in the rules in both directions — because "I only want Priya" and "I have not decided" must not look identical to whoever pairs the cohort. No capacity: a slot counter turns choosing a mentor into a race, which is a bad first experience of a club whose whole argument is that it is not a competition. Nothing allocates — preferences are listed and counted, and the pairing stays a human decision.

The signed-in routes have their own shell. They were carrying the marketing nav: six links arguing the case for joining, ending in a filled button pointing at the page you were already on. They are a route group now. Route groups rather than a usePathname() check, so the decision is made at build time — a client-side check ships the marketing nav to every app page and strips it after hydration, flashing somebody else's chrome on the one surface that should feel built for the person signed into it.

Two things worth reviewing carefully

mentors/ is the first collection a client may write that is not its own row. Admin-only, and validated anyway. It is acceptable because a mentor entry is published, organiser-authored, non-personal copy — the worst a stolen admin session can do there is deface a list. The same argument deliberately does not extend to admins/, where every client write stays denied; appointing an admin is the one privilege escalation this model would otherwise allow. The reasoning is in the firestore.rules header.

Deleting a mentor is guarded in the client, not the rules. Firestore rules cannot express "no document in another collection references this one" — that needs a query. So the guard is in AdminMentors.tsx, which already has every enrolment in memory, and the button is replaced by "3 students picked this mentor — hide instead" rather than disabled. Getting it wrong is cosmetic (an id where a name should be) rather than a disclosure, which is why it is enforced one level up.

Verification

Check Result
npm run typecheck / build pass, 14 routes prerendered
npm run rules 43 pass — the drift checker, extended
npm run rules:emulator 80 pass, rules executed as five different people
npm run e2e:auth 101 pass, real browser against both emulators
npm run smoke 101 pass, 0 fail
npm run qa 0 issues across 80 combinations
npm run browsers 0 failures, three engines
functions: npm test pass

Both emulator suites grew rather than being loosened. smoke now asserts the shell split both ways — marketing routes must have the nav and the Join button, app routes must not — so a signed-in route that grows a Join button fails.

Two bugs came out of driving it rather than reading it. /join was navigating away while signInWithPopup was still finalising, orphaning the Google popup about one run in three; and the app header's wordmark was a 33×28 tap target on mobile, caught by the QA sweep. A third was a latent test bug: the e2e's Firestore clear was fire-and-forget where only Auth was verified, so leftovers from a previous run failed six unrelated assertions at once.

Known gaps, stated rather than hidden

  • qa and smoke run signed out, so on /onboarding and /dashboard they only examine the "sign in first" card. The signed-in UI has been checked by hand in both themes but is not covered by the automated contrast/tap sweep. The fix is a small a11y pass inside e2e-auth.mjs, which already has an authenticated browser open.
  • enrollments/{uid} holds one enrolment forever. When the next GSoC cycle opens, this year's preferences are still there with nothing to distinguish them. Cheapest fix while the collection is empty: a cycle field and a {uid}__{cycle} key.
  • No allocation, and no mentor notification. Deliberate stopping points, both noted in lib/mentorship.ts.
  • path is kept on the profile but never asked for — captured from the ?path= links every CTA already uses and shown back on the dashboard. Say the word and I will drop it and the preselect assertions with it.

On CONTRIBUTING's "one change per PR"

This is four things, and by that rule it should be four PRs. They are interdependent enough that three of them would not be mergeable alone — the dashboard exists to hold the mentorship card, and the app shell exists because the routes do. It is one commit for the same reason: splitting heavily-overlapping files (firestore.rules, e2e-auth.mjs) into logical commits would produce commits that do not build individually. Happy to break it up if you would rather review it in pieces.

Before merging

firebase deploy --only firestore:rules — until that runs, mentors and enrollments do not exist in production and every write is denied by the catch-all. Everything above was verified against the emulator.

🤖 Generated with Claude Code

Joining used to end on a read-only card in the right-hand column of /join:
you filled in seven fields, it told you they were saved, and there was
nothing else. Everything the club wanted a member to do next had nowhere
to live.

READ THE BATCH OUT OF THE ADDRESS. Every member signs in as
abhinav.23bcs10045@sst.scaler.com — 2023-27, branch BCS, roll 10045 — so
the form no longer asks for what it already knows. lib/batch.ts parses it
on read and NOTHING is stored: the rules pin the address to
request.auth.token.email, so a derived value cannot be forged and cannot
drift out of step with the document beside it. That deleted the two
regexes and the "Unparsed" bucket the organisers' dashboard used to guess
with. It also means batch is not queryable, which is stated where it
matters.

THREE QUESTIONS, NOT SEVEN. year_branch, level, programs and
programs_other are gone. The first is in the address; the second was a
self-assessment made before somebody had met the club that nothing acted
on; the last two were a preference nobody read, now replaced by enrolling
in a programme, which is a decision with a consequence.

THREE ROUTES INSTEAD OF FOUR STATES. /join is the door, /onboarding asks
the three questions, /dashboard is where a member lives. Each ships as
static HTML before auth resolves, so every one of them paints a neutral
card until it knows who you are — treating "still checking" as "signed
out" flashes a sign-in prompt at every returning member.

MENTORSHIP. Organisers publish mentors from /admin; members pick a first
preference and then either a second or "just my first". Exactly one of
those two, enforced in both directions, because "I only want Priya" and
"I have not decided" must not look the same to whoever pairs the cohort.
No capacity: a slot counter turns choosing a mentor into a race. Nothing
here allocates — preferences are counted and listed, and the pairing
stays a human decision.

mentors/ is the first collection a client may write that is not its own
row. It is admin-only and validated anyway; the widening is acceptable
because a mentor entry is published, organiser-authored, non-personal
copy, and the same argument deliberately does not extend to admins/.

A SHELL OF ITS OWN. The signed-in routes carried the marketing nav —
six links arguing the case for joining, ending in a filled button
pointing at the page you were already on. They are a route group now,
with a header that says who you are signed in as. Route groups rather
than a pathname check: the decision is made at build time, so no app page
ships somebody else's chrome and strips it after hydration.

Verified with 101 assertions in scripts/e2e-auth.mjs driving a real
browser against both emulators, 80 in scripts/rules-emulator.mjs
executing the rules as five different people, and the QA sweep clean
across 80 combinations. Two bugs came out of the driving rather than the
reading: /join navigated away while signInWithPopup was still finalising,
orphaning the Google window about one run in three, and the app header's
wordmark was a 33x28 tap target on mobile.
`join shows the sign-in step to a signed-out reader` asserted one of the
two states that page legitimately has. With a Firebase config a
signed-out reader gets the sign-in card; without one the gate says
plainly there is nothing to sign in to. `.env.local` is gitignored and
never exists on a runner, so CI only ever saw the second — and this line
had been red on every push to main since at least 23 August, reporting a
broken join page that was not broken.

A permanently red CI is worse than no CI: it trains everybody to ignore
the one signal that would have caught something real. Four of the last
five runs on main are failures, all of them this.

The assertion now accepts either settled state and still refuses the one
that matters — a page stuck on "checking your sign-in", which is what a
dead bundle or a botched redirect looks like. The two new routes were
written this way from the start; this brings the original into line.

Verified both ways: 101 pass with .env.local present, and 101 pass with
it moved aside, which is how the runner sees it. lint, qa and browsers
are clean unconfigured too.
@abhinavjha0239
abhinavjha0239 merged commit b996d6a into ScalerOpenSourceLabsOrg:main Aug 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant