Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
# Keeps generated attribution out of commit messages: those lines put a bot in
# this repo's GitHub contributor list.

if grep -qiE '^(Co-Authored-By: Claude|Claude-Session:)|Generated with \[Claude Code\]' "$1"; then
echo "commit-msg: refusing an attribution line in the commit message" >&2
echo " Drop the Co-Authored-By: Claude / Claude-Session / Generated with lines." >&2
echo " They show up in GitHub Contributors for this repo." >&2
exit 1
fi

exit 0
38 changes: 38 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/sh
# Blocks credentials and local agent files from entering a commit.
# Enable once per clone: git config core.hooksPath .githooks
# Bypass a false positive: git commit --no-verify

fail() {
echo "pre-commit: refusing to commit $1" >&2
echo " $2" >&2
echo " Bypass with --no-verify only if you are certain." >&2
exit 1
}

staged=$(git diff --cached --name-only --diff-filter=ACMR)
[ -z "$staged" ] && exit 0

for file in $staged; do
case "$file" in
.env|.env.*|*/.env|*/.env.*)
[ "${file##*/}" = ".env.example" ] || fail "$file" "environment files carry live credentials" ;;
*.pem|*.key|*.p12|*.pfx|*.jks|*id_rsa|*id_ed25519)
fail "$file" "private key material" ;;
*-key.json|*service-account*.json|*credentials*.json|*application_default_credentials.json)
fail "$file" "a service-account or ADC key" ;;
.claude/*|*/.claude/*|.agents/*|*/.agents/*)
fail "$file" "local agent tooling, which is not shared" ;;
esac
done

# Content too: a key pasted into a config has an innocent path. Only markers
# that cannot appear by accident — the public Firebase web key is not one.
if git diff --cached -U0 | grep -qE '^\+.*(-----BEGIN [A-Z ]*PRIVATE KEY-----|sk_live_[A-Za-z0-9]|rk_live_[A-Za-z0-9]|whsec_[A-Za-z0-9]{16})'; then
echo "pre-commit: refusing to commit a live secret found in the diff" >&2
echo " A private key block or live Stripe key is being added." >&2
echo " Bypass with --no-verify only if you are certain." >&2
exit 1
fi

exit 0
7 changes: 4 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ jobs:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
# No version pin: the one in packageManager is the one that reads
# overrides and allowBuilds from pnpm-workspace.yaml. Pinning here
# silently installed an older pnpm that ignores both.
uses: pnpm/action-setup@v5

- name: Setup Node
uses: actions/setup-node@v6
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ jobs:
node-version: "20"

- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
# No version pin: the one in packageManager is the one that reads
# overrides and allowBuilds from pnpm-workspace.yaml. Pinning here
# silently installed an older pnpm that ignores both.
uses: pnpm/action-setup@v5

- name: Install dependencies
run: pnpm install
Expand Down
25 changes: 25 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,28 @@ bash.exe.stackdump
# failure on a fresh clone.
monitoring/secrets/*
!monitoring/secrets/.gitkeep

# ── Never commit ────────────────────────────────────────────────────────────
# Credentials. `*.pem` and the env files above cover some of this; these are the
# shapes that slipped past them — a downloaded service-account key, an ADC file
# copied out of ~/.config, an env file for an environment nobody listed.
*.key
*.p12
*.pfx
*.jks
*-key.json
service-account*.json
*credentials*.json
application_default_credentials.json
gha-creds-*.json
id_rsa
id_ed25519

# Every .env variant, not only the four spelled out above. `.env.example` is
# the one that belongs in the repo, so it is exempted.
.env.*
!.env.example

# Firebase local state, which carries project ids and cached tokens.
.firebase/
.firebaserc.local
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Two Next.js sites share one Postgres database and four internal packages. Club m

**Documentation:** start at [`docs/README.md`](./docs/README.md).

## Club project

The public website and member portal for Data Science at Georgia Tech, live at [datasciencegt.org](https://datasciencegt.org). This is production club infrastructure (not a greenfield student app).

Member-facing overview — what it is, what members use, current status, and how to help: [`docs/club-project.md`](./docs/club-project.md). Local setup and PR workflow stay in [`docs/getting-started.md`](./docs/getting-started.md) and [`docs/contributing.md`](./docs/contributing.md).

## Workspace layout

| Path | Workspace | Role |
Expand Down
45 changes: 45 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# TODO

Open items as of 2026-09-06. Delete a line when it is done.

## Deploy

- [ ] **Roll out.** The last successful rollout predates the pnpm 10 revert, the
resume error handling, and the p99 work below. Everything here is
committed but not live.
- [ ] **Point Prometheus at production.** `METRICS_TOKEN` now exists in Secret
Manager and is wired into `apphosting.yaml`, so `/api/metrics` answers
after the next rollout. Read the value with
`gcloud secrets versions access latest --secret=METRICS_TOKEN --project=dsgt-website`
and scrape with `Authorization: Bearer <token>`. A 404 means the token did
not match — that is the endpoint's designed answer, not an outage.

## p99

- [ ] **Decide on `minInstances`.** Held at 0 deliberately: one always-warm
instance at `cpu: 2` / 1 GiB is roughly $30-50/month. The boot-time pool
warmup in `sites/mainweb/instrumentation.ts` shortens a cold start; only
min-instances removes it. Revisit if the scrape shows cold starts
dominating the tail.
- [ ] **Read the histogram before optimising further.** `dsgt_trpc_duration_seconds`
is bucketed for the tail. Everything below is a guess until it is scraped.
- [ ] **Stripe reconcile N+1** (`routers/stripe.ts`, `reconcileMyPayments`).
One `stripePayments` lookup per intent, bounded at 20, on a user-triggered
backstop — low value, and the surrounding grant logic is delicate.
- [ ] **Metrics are per-instance.** In-memory registry, `maxInstances: 10`,
scale to zero: a scrape samples one instance and counters reset when it
dies. Fleet-wide numbers need aggregation.

## Housekeeping

- [ ] **5 pre-existing lint errors**, all `import/consistent-type-specifier-style`:
`app/HomePageClient.tsx`, `app/projects/ProjectsPageClient.tsx`,
`components/admin/hackathons/RegistrationControls.tsx`,
`lib/club-projects.test.ts`. `eslint --fix` clears them; kept out of the
p99 commits to keep those diffs readable.
- [ ] **Confirm the `/events` staleness call.** The page was `force-dynamic` and
is now `revalidate = 300`, matching `/projects`. `proxy.ts` already serves
that path `max-age=3600`, so nobody could observe the old freshness — but
it was an explicit choice, so it is worth a second opinion.
- [ ] **`nul`** — a 0-byte file at the repo root from a stray `> nul` redirect.
Gitignored, inert, deletable.
18 changes: 18 additions & 0 deletions apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ env:
secret: projects/672446353769/secrets/STRIPE_WEBHOOK_SECRET
- variable: NODE_ENV
value: production
# Cloud Storage bucket holding resume PDFs. Postgres keeps only the metadata
# and the object key — 5000 resumes is 1.5 GB and this database is 0.5 GB.
# The runtime service account needs objectAdmin on it:
# gcloud storage buckets create gs://dsgt-resumes --location=us-central1 \
# --uniform-bucket-level-access --public-access-prevention
# gcloud storage buckets add-iam-policy-binding gs://dsgt-resumes \
# --member=serviceAccount:<app-hosting-runtime-sa> \
# --role=roles/storage.objectAdmin
- variable: RESUME_BUCKET
value: dsgt-resumes
# Consumer Gmail, which caps around 500 recipients a day — shared between
# sign-in codes and every acceptance or announcement send. Acceptance waves
# are capped at 500 for that reason. Moving to a real provider is these three
Expand Down Expand Up @@ -98,3 +108,11 @@ env:
# ("[Security] x-forwarded-for has N entries"); expect hops = entries - 1.
- variable: TRUSTED_PROXY_HOPS
value: "1"
# Gates /api/metrics, the Prometheus scrape target that carries the tRPC
# duration histogram. Without it the route answers 404 in production — by
# design, so it does not announce itself — and the p99 exists but is
# unreadable. RUNTIME only: nothing in the build scrapes.
- variable: METRICS_TOKEN
secret: METRICS_TOKEN
availability:
- RUNTIME
4 changes: 3 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

This folder is the reference for **query**, the Data Science at Georgia Tech (DSGT) monorepo for club operations and digital infrastructure.

Start here, then jump to the page that matches the work you are doing.
Members looking for a club-language overview should start at [Club project](./club-project.md). For local setup and review rules, jump to the page that matches the work you are doing.

| Document | What it covers |
| --- | --- |
| [Club project](./club-project.md) | What the live site is, who uses it, current status, how to help |
| [Getting started](./getting-started.md) | Prerequisites, local Postgres, env vars, first `pnpm dev` |
| [Architecture](./architecture.md) | How the two sites and four packages fit together |
| [Contributing](./contributing.md) | Branches, scripts, tests, and review expectations |
Expand All @@ -14,6 +15,7 @@ Start here, then jump to the page that matches the work you are doing.
| [CI/CD](./operations/ci-cd.md) | GitHub Actions, Dependabot, branch automation |
| [Security](./operations/security.md) | Auth gates, rate limits, CSP, input scrubbing |
| [Testing](./operations/testing.md) | Vitest, Playwright, and what each suite protects |
| [Resume book](./resume-book.md) | Member uploads, the two staff views, limits, and why files skip tRPC |
| [Glossary](./glossary.md) | Club vs hackathon vocabulary |

## Packages
Expand Down
135 changes: 135 additions & 0 deletions docs/club-project.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Club project: DS@GT website

This is the member-facing overview of **query**, the live public website and member portal for [Data Science at Georgia Tech](https://datasciencegt.org) (DS@GT / DSGT). <!-- pragma: allowlist secret -->

It is not a setup manual. Local install is [Getting started](./getting-started.md). Pull requests and review rules are [Contributing](./contributing.md). Architecture, packages, and operations stay in the rest of [`docs/`](./README.md).

Aamogh Sawant ([@aamoghS](https://github.com/aamoghS)), club President, owns and ships this repo. There is no separate website lead.

## What this is

The public website visitors see, and the signed-in portal members use to join the club, pay dues, check in at events, follow bootcamp, apply to club projects, and handle Hacklytics interest and registration.

The public pages and the portal are one Next.js app (`sites/mainweb`). Signing in does not take you to a different hostname.

## Live URLs

| Surface | URL |
| -------------------- | ------------------------------------------------------------------------------------------------------------ |
| Public site + portal | [https://datasciencegt.org](https://datasciencegt.org) <!-- pragma: allowlist secret --> |
| Sign in | [https://datasciencegt.org/login](https://datasciencegt.org/login) <!-- pragma: allowlist secret --> |
| Member home | [https://datasciencegt.org/dashboard](https://datasciencegt.org/dashboard) <!-- pragma: allowlist secret --> |

`member.datasciencegt.org` does **not** resolve. Do not send people there, and do not put it in copy or onboarding.

Locally, the same app is [http://localhost:3001](http://localhost:3001). See [Getting started](./getting-started.md).

## What members use it for

After sign-in (Google, GitHub if configured, or email code):

| Need | Where |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Join / pay dues | Portal membership — Stripe **$25** annual membership, **$10** bootcamp add-on (on top of membership, not instead of it) |
| Club events and check-in | Portal events / club pass |
| Bootcamp (term-gated add-on) | `/club/bootcamp` and the public `/bootcamp` page |
| Club projects (pitch + optional resume) | `/initiatives` |
| Staff tools | `/admin` (appointed roles only; there is no public admin signup) |
| Hacklytics interest and registration | Portal `/hacklytics` (the marketing site links here after login) |

Hacklytics participation is open to non-members. A paid membership is not required to register for the hackathon.

Public pages (no login):

| Path | Page |
| ----------- | ------------------ |
| `/` | Home |
| `/team` | Executive board |
| `/events` | Public events |
| `/projects` | Projects |
| `/history` | Club history |
| `/bootcamp` | Bootcamp marketing |

Route-level detail: [Main website](./sites/mainweb.md). Club vs hackathon vocabulary: [Glossary](./glossary.md).

## Club projects (the roster on the site)

`/` and `/projects` read the roster from the `club_project` table, not from a hardcoded array. Editing a card is a row edit, which is why the old list sat five years stale.

| Column | What it does |
| --------------- | ----------------------------------------------------------------------------------------- |
| `status` | `active`, `revived`, `needs_lead`, or `past`. Only `past` drops out of the current roster |
| `lead_name` | Free text, so a lead can be named before they ever sign in |
| `initiative_id` | The portal initiative members apply to, when there is one |
| `join_url` | External destination for projects that recruit elsewhere (ARC) |
| `is_published` | Pull a card off the site without deleting it |

Applying happens in the portal. A card with an `initiative_id` links to `/initiatives`, where a signed-in member says why they want to join and may attach a PDF resume; the leader reads both and accepts or declines from `/lead`. A card with no initiative falls back to `join_url`, then to the shared interest form.

To reset the roster to the checked-in Fall 2026 list:

```bash
pnpm --filter @query/db db:seed:club-projects
```

The seed upserts on `slug` and never deletes, so re-running it republishes the roster without duplicating cards. Removing a project from the site is `is_published = false`, not a deleted row.

## Current status (Fall 2026)

This is **live production infrastructure**, not a greenfield student app and not a class project waiting for a first deploy.

- Serving real members at [datasciencegt.org](https://datasciencegt.org) <!-- pragma: allowlist secret -->
- Hosted on Firebase App Hosting / Cloud Run
- GCP project: `dsgt-website`
- Database: Neon (Postgres)
- Last `main` activity: late August 2026

Treat production as production. A broken PR can take down dues, login, or event check-in.

## How the repo is laid out

High level only. Details live in the linked docs.

| Path | What it is |
| ---------------------- | ---------------------------------------------------- |
| `sites/mainweb` | Public club site **and** the authenticated portal |
| `sites/hacklytics2027` | Hacklytics 2027 marketing site (static; no database) |
| `packages/api` | tRPC, pricing, server logic |
| `packages/auth` | Sign-in (NextAuth) |
| `packages/db` | Schema and membership rules |
| `packages/ui` | Shared React components |

Club operations (membership, club events, bootcamp, club projects) and hackathon editions share one database but are modeled as separate domains. Do not hang club tables off a hackathon row.

Setup, env, and first-admin bootstrap: [Getting started](./getting-started.md). How the pieces connect: [Architecture](./architecture.md). Index of the rest: [Documentation](./README.md).

## Older repos (do not revive)

These are predecessors. The live product is **this** repo (`DataScience-GT/query`). Do not open feature work there, do not migrate traffic back, and do not treat them as the current stack.

| Repo | What it was |
| ----------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
| [DataScience-GT/datascience-gt.github.io](https://github.com/DataScience-GT/datascience-gt.github.io) | Earlier website / portal repo |
| [DataScience-GT/dsgt-member-portal](https://github.com/DataScience-GT/dsgt-member-portal) | Earlier member portal (membership, Stripe, events) |

## How to help

Safe first work — useful, visible, and hard to take production down with:

1. **Public content accuracy** — `/team`, `/projects`, `/events` (and related copy) matching the current board and programs
2. **Onboarding and docs** — this folder, especially anything that helps a new contributor run the app without guessing
3. **Small UI bugs** — layout, dead links, copy, accessibility on pages you can exercise locally
4. **Tests** — fill gaps in existing Vitest suites; see [Testing](./operations/testing.md)

Label anything that touches **payments**, **auth**, or **production deploy** as **needs-exec-review**. Do not merge that class of change on a student PR alone. That includes Stripe amounts and webhooks, NextAuth / OAuth / email-code login, secrets, `apphosting.yaml`, Firebase Hosting, and anything that writes production schema.

Club events and working time are after **6:30 PM ET**. Questions: [hello@datasciencegt.org](mailto:hello@datasciencegt.org) or Aamogh.

## How to join / contribute

1. Read [Contributing](./contributing.md) and [Getting started](./getting-started.md).
2. Branch from `dev` (that is the integration branch). `main` is production.
3. Open the pull request against **this** repo (`DataScience-GT/query`). Feature branches are reviewed into `dev`; `dev` is what ships to `main`.
4. Never commit secrets (`.env`, Stripe keys, OAuth client secrets, SMTP passwords, production `DATABASE_URL`). If a secret was pasted into a PR, say so immediately — do not “fix” it by committing a deletion and moving on.

Code owners: `@aamoghS`.
Loading
Loading