Skip to content

UI dashboard - #6

Merged
PRAteek-singHWY merged 6 commits into
ScalerOpenSourceLabsOrg:mainfrom
bhumindeshpande8-spec:ui-dashboard
Sep 4, 2026
Merged

UI dashboard#6
PRAteek-singHWY merged 6 commits into
ScalerOpenSourceLabsOrg:mainfrom
bhumindeshpande8-spec:ui-dashboard

Conversation

@bhumindeshpande8-spec

Copy link
Copy Markdown
Contributor

Joining used to end on a read-only card in the right-hand column of /join: seven
fields, a "saved" message, and nothing after it. Everything the club wanted a member
to do next had nowhere to live. This branch builds that place, fixes two production
bugs found on the way that were silently breaking sign-in for real students, and
brings the whole page down to a size that fits a laptop screen.

Three routes instead of four states

/join is the door, /onboarding asks three questions, /dashboard is where a member
lives. Signed-in routes are a route group (app/(app)/) with their own layout, so no
app page can ship the marketing nav and strip it after hydration. Each route paints a
neutral card until auth resolves — treating "still checking" as "signed out" flashes a
sign-in prompt at every returning member.

Seven questions became three

year_branch, level, programs and programs_other are gone. The first is already
in the address: every member signs in as abhinav.23bcs10045@sst.scaler.com, so
lib/batch.ts parses batch, branch and roll on read and stores nothing — the rules
pin the address to request.auth.token.email, so a derived value cannot be forged and
cannot drift from the document beside it. The other three were a self-assessment nobody
acted on and a preference nobody read, replaced by enrolling in a programme, which is a
decision with a consequence.

Membership is not the same as signing in

isMember() used to mean "has a verified @sst.scaler.com address", so every student on
the domain read everything the organisers posted. That was a function name that had
stopped being true, not a decision anyone made. It is now isStudent(), with
isClubMember() a separate question answered by a membership field only an organiser
can write. Notices, forms and sessions carry an audience — members, students or
both — defaulting to both so everything written before the field existed stays
exactly as visible as the day it was posted.

What organisers get

  • Roster — one Firestore row per person holding both halves of "core team": the
    access grant (role + active, read by the rules on every request) and the public
    billing the /team page shows. They were two lists that drifted, silently in both
    directions. npm run team:sync projects only the public fields at build time, because
    rules are per-document and the document id is an email.
  • Roles — owners manage the roster; admins cannot appoint anyone, so a compromised
    organiser account cannot escalate. Both fields default permissively so rows seeded
    before they existed keep working.
  • Forms and polls — one collection; a poll is a form with show_tally on. Responses
    are keyed by uid, so one-per-member is structural rather than checked, and the tally is
    function-written and refused to every client.
  • Sessions, notices, and a composer with audience targeting and a reversible archive.
  • Mentorship — organisers publish mentors, members pick a first preference and then
    either a second or "just my first" (exactly one, enforced both ways: "I only want
    Priya" and "I have not decided" must not look alike to whoever pairs the cohort). No
    capacity counter — a slot counter turns choosing a mentor into a race. Nothing here
    allocates; the pairing stays a human decision.

The whole page, at three quarters

The design was drawn large enough that a 1080p laptop had to be zoomed to 75% before a
section fit on screen — which is the browser telling you the root is too big, not the
reader. html { font-size: 75% } does what that zoom did, at every reader's default
zoom, on the site and in the dashboard alike.

A percentage rather than font-size: 12px, and the difference matters: a percentage
resolves against whatever the reader set as their browser's default text size, so
somebody running 20px because they need to still gets their proportional increase. A
hard px value would overrule that preference outright — the accessibility failure this
looks like and is not.

What follows from that, and is the bulk of the file count:

  • Every text-[13px]-style utility was rewritten in rem (text-[0.8125rem]) so type
    actually comes down with the root. This is why ~40 components are touched for no
    behavioural change.
  • The vw terms in the fluid clamps came down by the same quarter (6.2vw4.65vw,
    and so on). Left in px, headings would hold their old size across the middle of the
    viewport range while everything around them shrank.
  • Px stays px where it is a physical constant — hairlines, borders, and the 44px
    minimum touch targets. A 0.75px rule is a blurry rule, and a thumb does not get
    smaller because the type did.
  • The nav plate therefore stopped scaling (h-14h-[56px], it contains a 44px
    target), so scroll-padding-top and .page-top moved to px too — a clearance in rem
    would have shrunk away from a bar that did not.

One nav, every reader

The "Sign in" link and the button beside it used to swap on user. The bar does no
access control and never did, so all that state bought was a control that read
differently depending on which browser you opened the site in — and a member who was
signed in lost the only door in the chrome that names the members' area. One label now,
every visit: /dashboard shows the sign-in card to a stranger and the dashboard to a
member, which is where that branch belongs.

Two production fixes carried on this branch

  • authDomain pointed at firebaseapp.com. That made the sign-in handler a
    third-party origin; Chrome partitions third-party storage, so the credential Google
    issued could not be written back and every sign-in silently failed. Two real students
    hit this — accounts exist in Auth for 26 and 30 Aug, and users was empty. Now
    same-origin via our own /__/auth/handler.
  • Every page was served with max-age=3600. The **/*.html cache rule matched the
    request path, and with trailingSlash a page is requested as /join/, never
    /join/index.html — so it read correctly and applied to nothing. A deploy readers
    cannot receive for an hour is not a deploy. ** is no-cache now, with the year-long
    cache bought back for /_next/static/**, where fingerprinted filenames make it correct.

Team

Yash Virulkar's shadow entry gets a batch, a photo and his own words, set as prose
rather than as labels. Emoji come out of one member's highlights so the column reads as
one voice. sarvika.jpeg (the 155KB original) is deleted now that the cropped
sarvika-sharma.jpg is the one being rendered.

Verification

Rules assertions against the emulator executing the rules as five different people
(npm run rules:emulator), two browser-driven e2e suites over auth and mentorship
(npm run e2e:auth, npm run e2e:mentorship), plus qa, smoke and browsers. Two
bugs came out of driving rather than 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 33×28 tap target on mobile.

Also fixes a CI assertion that had been red on every push to main since 23 August — it
asserted one of the two states /join legitimately has, and .env.local never exists on
a runner. A permanently red CI trains everyone to ignore the one signal that matters.

Notes for review

  • firestore.indexes.json adds the one composite index the app needs (announcements
    by audience + date) — the audience filter cannot be done in memory, because a query
    returning a refused document fails entirely rather than dropping the row. Deploy with
    firebase deploy --only firestore so it lands alongside the rules that need it.
  • The audience default lives in two places by necessity — lib/audience.ts and the
    .get('audience', 'both') calls in firestore.rules. Change neither alone.
  • mentors/ is the first collection a client may write that is not its own row. It is
    admin-only and validated anyway; a mentor entry is published, organiser-authored,
    non-personal copy. That argument deliberately does not extend to admins/.
  • The 75% root is one line with page-wide reach. Worth a look at the smallest type on
    the dashboard at a default-zoom 1080p screen before merging — that is where it bites
    first if it bites.

bhumideshpande and others added 6 commits September 2, 2026 23:33
Adds, on top of the sign-in that already existed:

  roles      admins/{email} gains role + active, splitting isAdmin() from
             isOwner(). Owners manage the roster from /admin; admins cannot
             appoint anyone, so a compromised organiser account cannot
             escalate. Both fields default permissively so rows seeded before
             they existed keep working.
  forms      one collection for forms AND polls -- a poll is a form with
             show_tally on. Responses are keyed by uid, so one-per-member is
             structural rather than checked. The tally is function-written and
             refused to every client.
  sessions   when the club meets. A real starts_at timestamp, which is the one
             date in these rules not pinned to request.time, because scheduling
             is the whole feature.
  notices    categories and a reversible archive.

The dashboards share one app shell (components/dashboard/Shell.tsx), which
replaces the marketing chrome on /dashboard and /admin -- see ChromeGate.tsx.
The profile form stopped being a gate in front of the member dashboard and
became a panel inside it; it was the reason organisers reported the site "has
no dashboard".

155 rules assertions against the emulator, plus a two-role browser pass over
both screens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both sides had independently built an app shell and a member dashboard, so
this is a synthesis rather than a fast-forward.

TAKEN FROM UPSTREAM
  route groups   app/(site)/ and app/(app)/. This is the textbook way to give
                 one set of routes a different layout, and it deletes
                 ChromeGate.tsx, which did the same job by checking the
                 pathname. A page in (app) can no longer forget the shell.
  firestore.rules, firebase.json, FIREBASE.md, lib/firebase.ts
  the check scripts: rules, rules-emulator, smoke, assert-site, e2e-auth
  the profile model, which lost year_branch, level, programs and
                 programs_other -- programmes moved into their mentorship model
  mentorship, onboarding, and their AdminDashboard

KEPT FROM OURS
  the app shell   components/dashboard/Shell.tsx is now (app)/layout.tsx,
                  replacing their AppHeader/AppFooter pair
  MemberDashboard and its panels: the summary strip, notice board, forms and
                  polls, sessions, GitHub activity, where-to-go-next, details
  the organisers' page composition

ADAPTED
  ProfileCard and NextUp drop the fields upstream removed -- a row for a field
  that no longer exists prints "undefined" at a member. NextUp loses its
  programmes clause, which was the better half of that panel; re-attaching it
  to the mentorship enrolment is one line when someone wants it.
  ApplyForm follows LEVELS -> LEVEL_LABEL.
  lib/firebase.ts gets our four collection constants back, additively.

KNOWN BROKEN, DELIBERATELY
  firestore.rules is upstream's wholesale, so forms, sessions and the roster
  fields have no rules and fall to the catch-all. Those panels will error.
  That is a loud break, not a hole -- nothing leaks. Re-apply those blocks
  before deploying.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@PRAteek-singHWY
PRAteek-singHWY merged commit 7641700 into ScalerOpenSourceLabsOrg:main Sep 4, 2026
1 of 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.

3 participants