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
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
3 changes: 2 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 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`.
6 changes: 3 additions & 3 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
| Term | Meaning in this repo |
| --- | --- |
| **query** | This monorepo (`package.json` name). Not a search engine. |
| **Club** | Year-round DSGT operations: membership, club events, bootcamp, initiatives. Not keyed by hackathon. |
| **Club** | Year-round DSGT operations: membership, club events, bootcamp, club projects. Not keyed by hackathon. |
| **Hackathon / edition** | One `hackathon` row (e.g. Hacklytics 2027) and everything that cascades from it. |
| **Hacklytics** | DSGT’s annual data-science hackathon. Marketing site is `sites/hacklytics2027`; operations are the portal. |
| **Portal** | Authenticated product UI inside `sites/mainweb` route group `(portal)`. |
| **Member** | A `member` row with a **paid, unexpired** year. A lapsed row still exists but `isMember` is false. |
| **Pass** | `member.pass_code` — rotatable QR for club check-in. Independent of membership dates. |
| **Volunteer** | Weakest `admin.role`. Can scan badges (`isScanner`). Cannot pass `isAdmin`. |
| **Staff** | Active admin whose role is not `volunteer`. |
| **Project leader** | `project_leader` row. Runs club **initiatives**. Not a staff role. |
| **Initiative** | Club project members apply to join. Never judged. Distinct from a hackathon **project**. |
| **Project leader** | `project_leader` row. Runs **club projects**. Not a staff role. |
| **Club project** | `initiative` row. Members apply to join with a pitch and an optional resume. Never judged. Distinct from a hackathon **project**, which is a judged submission. The UI says "club project"; the table is still `initiative`. |
| **Hackathon project** | Team/solo submission (`hackathon_project`). Promoted into `judging_project` for scoring. |
| **Interest** | “Tell me when registration opens” (`hackathon_interest`). Requires a signed-in user. |
| **Current edition** | In-progress hackathon if one exists; otherwise the newest edition that is not `draft` or `announced`. |
Expand Down
2 changes: 1 addition & 1 deletion docs/sites/mainweb.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This is the club’s public site **and** the authenticated portal. There is no s
| `/` | Home (`HomePageClient`) |
| `/team` | Team |
| `/events` | Public events |
| `/projects` | Projects |
| `/projects` | Club projects — current roster and past archive, read from `club_project` |
| `/history` | Club history |
| `/bootcamp` | Bootcamp marketing |
| `/docs` | In-app docs UI |
Expand Down
11 changes: 8 additions & 3 deletions monitoring/grafana/dashboards/dsgt-portal.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@
},
{
"type": "timeseries",
"title": "Latency p50 / p95",
"title": "Latency p50 / p95 / p99",
"datasource": { "type": "prometheus", "uid": "dsgt-prometheus" },
"gridPos": { "h": 7, "w": 8, "x": 8, "y": 15 },
"fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] },
Expand All @@ -241,19 +241,24 @@
"refId": "B",
"expr": "histogram_quantile(0.95, sum by (le) (rate(dsgt_trpc_duration_seconds_bucket{env=~\"$env\"}[5m])))",
"legendFormat": "p95"
},
{
"refId": "C",
"expr": "histogram_quantile(0.99, sum by (le) (rate(dsgt_trpc_duration_seconds_bucket{env=~\"$env\"}[5m])))",
"legendFormat": "p99"
}
]
},
{
"type": "timeseries",
"title": "Slowest procedures (p95)",
"title": "Slowest procedures (p99)",
"datasource": { "type": "prometheus", "uid": "dsgt-prometheus" },
"gridPos": { "h": 7, "w": 8, "x": 16, "y": 15 },
"fieldConfig": { "defaults": { "unit": "s" }, "overrides": [] },
"targets": [
{
"refId": "A",
"expr": "topk(5, histogram_quantile(0.95, sum by (le, procedure) (rate(dsgt_trpc_duration_seconds_bucket{env=~\"$env\"}[5m]))))",
"expr": "topk(5, histogram_quantile(0.99, sum by (le, procedure) (rate(dsgt_trpc_duration_seconds_bucket{env=~\"$env\"}[5m]))))",
"legendFormat": "{{procedure}}"
}
]
Expand Down
37 changes: 37 additions & 0 deletions monitoring/rules/payments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,40 @@ groups:
for: 10m
annotations:
summary: "p95 portal API latency above 2s"

# Tail latency. The p95 alert above says the median experience held; this is
# the one that catches a slow path only a fraction of calls take — a cold
# pool, a cache key expiring under load, one procedure scanning a table.
#
# Recorded, not only alerted on: histogram_quantile over a 5m rate is the
# most expensive expression on the dashboard, and both the alert and the
# per-procedure panel want it. Read it against the bucket edges — the top
# ones are 1s, 1.5s, 2.5s, 5s, 10s, so a p99 quoted between them is an
# interpolation, not a measurement.
- name: latency
rules:
- record: job:dsgt_trpc_duration_seconds:p99
expr: >-
histogram_quantile(
0.99,
sum by (le) (rate(dsgt_trpc_duration_seconds_bucket[5m]))
)

# Which procedure to open first. Same expression, kept by name.
- record: procedure:dsgt_trpc_duration_seconds:p99
expr: >-
histogram_quantile(
0.99,
sum by (le, procedure) (rate(dsgt_trpc_duration_seconds_bucket[5m]))
)

- alert: PortalApiTailSlow
expr: job:dsgt_trpc_duration_seconds:p99 > 5
for: 10m
annotations:
summary: "p99 portal API latency above 5s"
description: >-
One call in a hundred is taking over five seconds. Sort
procedure:dsgt_trpc_duration_seconds:p99 to see which, and check
dsgt_nodejs_eventloop_lag_p99_seconds alongside it — a stall in the
instance moves every procedure at once, a slow query moves one.
13 changes: 7 additions & 6 deletions packages/api/src/.internal-tests/initiative-edge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ describe("Club initiatives", () => {
});

await expect(
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE }),
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE, pitch: "Why me" }),
).rejects.toMatchObject({ code: "FORBIDDEN" });
});

Expand All @@ -401,7 +401,7 @@ describe("Club initiatives", () => {
});

await expect(
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE }),
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE, pitch: "Why me" }),
).rejects.toMatchObject({ code: "NOT_FOUND" });
},
);
Expand All @@ -413,15 +413,15 @@ describe("Club initiatives", () => {
});

await expect(
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE }),
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE, pitch: "Why me" }),
).rejects.toMatchObject({ code: "NOT_FOUND" });
});

it("refuses the leader applying to their own initiative", async () => {
lookups({ initiative: openInitiative(), member: activeMember });

await expect(
callerFor(LEADER).initiative.requestToJoin({ initiativeId: INITIATIVE }),
callerFor(LEADER).initiative.requestToJoin({ initiativeId: INITIATIVE, pitch: "Why me" }),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});

Expand All @@ -433,7 +433,7 @@ describe("Club initiatives", () => {
onSelect = (t) => (t === initiativeApplications ? [{ taken: 3 }] : []);

await expect(
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE }),
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE, pitch: "Why me" }),
).rejects.toMatchObject({ code: "BAD_REQUEST" });
});

Expand All @@ -445,7 +445,7 @@ describe("Club initiatives", () => {
});

await expect(
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE }),
callerFor(MEMBER).initiative.requestToJoin({ initiativeId: INITIATIVE, pitch: "Why me" }),
).rejects.toMatchObject({ code: "CONFLICT" });
});

Expand All @@ -460,6 +460,7 @@ describe("Club initiatives", () => {

const res = await callerFor(MEMBER).initiative.requestToJoin({
initiativeId: INITIATIVE,
pitch: "Why me",
});

expect(res.status).toBe("pending");
Expand Down
26 changes: 0 additions & 26 deletions packages/api/src/.internal-tests/routers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1789,31 +1789,5 @@ describe("Router Integration and Access Control Verification Suite", () => {
});
});

describe("15. Audit Logs System", () => {
it("should allow admin to retrieve audit logs with filters", async () => {
const ctx = createMockCtx("admin_user_id");

mockFindFirst.mockImplementation((table) => {
if (table === "admins") {
return { id: "admin_1", userId: "admin_user_id", role: "admin", isActive: true };
}
return null;
});

mockFindMany.mockReturnValue([
{ id: "log_1", severity: "critical", userId: "target_user", createdAt: new Date() },
]);

const caller = appRouter.createCaller(ctx);
const res = await caller.audit.list({
limit: 10,
offset: 0,
severity: "critical",
});

expect(res.logs.length).toBe(1);
expect(res.pagination.limit).toBe(10);
});
});
});

Loading
Loading