Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ npm ci
npm run dev
```

`npm run dev` starts the starter project at http://localhost:5173.
`npm run dev` starts the starter project at http://localhost:5173/project2/.

Other commands:

Expand Down
10 changes: 8 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# Copy this file to .env and fill in your own values. .env is not committed.
#
# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
DATABASE_URL="postgresql://cop:your-password@localhost:5432/cop?schema=public"
# Runtime server connection (wag_app - restricted CRUD permissions)
DATABASE_URL="postgresql://wag_app:YOUR_APP_PASSWORD@localhost:5432/wag_catalog"

# Migration connection string (wag_migrator - schema owner for migrations & setup)
MIGRATION_DATABASE_URL="postgresql://wag_migrator:YOUR_MIGRATOR_PASSWORD@localhost:5432/wag_catalog"

PORT=3001
SESSION_SECRET="your_session_secret_here"
10 changes: 7 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,24 @@
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:seed": "prisma db seed",
"prisma:studio": "prisma studio"
"prisma:studio": "prisma studio",
"setup": "DATABASE_URL=${MIGRATION_DATABASE_URL:-$DATABASE_URL} prisma migrate deploy && DATABASE_URL=${MIGRATION_DATABASE_URL:-$DATABASE_URL} npm run prisma:seed",
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"engines": {
"node": ">=24"
},
"devDependencies": {
"@types/node": "^24.19.2",
"dotenv": "^17.4.2",
"prisma": "7.10.0",
"prisma": "^7.10.1",
"tsx": "^4.23.15",
"typescript": "~6.0.2"
},
"dependencies": {
"@prisma/adapter-pg": "7.10.0",
"@prisma/client": "7.10.0"
"@prisma/client": "^7.10.1"
}
}
108 changes: 81 additions & 27 deletions backend/src/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -32,29 +32,39 @@
# - Failures that the user can fix (NAME_TAKEN, INVALID_ITEMS) carry a
# readable message that the UI can show as it is.

"""One of the four apparatus: VT, UB, BB or FX."""
"""
One of the four apparatus: VT, UB, BB or FX.
"""
type Apparatus {
id: ID!
name: String!
groups: [ElementGroup!]!
}

"""A group of elements or vaults on one apparatus, e.g. "1.000 Mounts"."""
"""
A group of elements or vaults on one apparatus, e.g. "1.000 Mounts".
"""
type ElementGroup {
id: ID!
number: Int!
name: String!
apparatus: Apparatus!
}

"""Fields shared by everything in the catalog: elements and vaults."""
"""
Fields shared by everything in the catalog: elements and vaults.
"""
interface CatalogItem {
id: ID!
code: String!
description: String!
"""The gymnast the item is named after, if any."""
"""
The gymnast the item is named after, if any.
"""
name: String
"""Full URL of the drawing, ready to use as the src of an <img>."""
"""
Full URL of the drawing, ready to use as the src of an <img>.
"""
imageUrl: String!
elementGroup: ElementGroup!
"""
Expand All @@ -65,13 +75,17 @@ interface CatalogItem {
removed: Boolean!
}

"""One element on uneven bars, balance beam or floor."""
"""
One element on uneven bars, balance beam or floor.
"""
type Element implements CatalogItem {
id: ID!
code: String!
description: String!
name: String
"""Full URL of the drawing, ready to use as the src of an <img>."""
"""
Full URL of the drawing, ready to use as the src of an <img>.
"""
imageUrl: String!
elementGroup: ElementGroup!
"""
Expand All @@ -92,13 +106,17 @@ type Element implements CatalogItem {
variants: [Element!]!
}

"""One vault from the vault table."""
"""
One vault from the vault table.
"""
type Vault implements CatalogItem {
id: ID!
code: String!
description: String!
name: String
"""Full URL of the drawing, ready to use as the src of an <img>."""
"""
Full URL of the drawing, ready to use as the src of an <img>.
"""
imageUrl: String!
elementGroup: ElementGroup!
"""
Expand All @@ -107,13 +125,19 @@ type Vault implements CatalogItem {
found by the search and does not count in a D-value.
"""
removed: Boolean!
"""Fixed difficulty score, e.g. 2.6."""
"""
Fixed difficulty score, e.g. 2.6.
"""
dValue: Float!
"""Other ways of performing the same vault. Removed variants are left out."""
"""
Other ways of performing the same vault. Removed variants are left out.
"""
variants: [Vault!]!
}

"""What the user can sort the catalog by."""
"""
What the user can sort the catalog by.
"""
enum CatalogSortField {
CODE
"""
Expand Down Expand Up @@ -147,12 +171,16 @@ input CatalogFilter {
it throws BAD_USER_INPUT. Empty or missing means no difficulty filter.
"""
difficultyValues: [Float!]
"""Only items named after a gymnast."""
"""
Only items named after a gymnast.
"""
namedOnly: Boolean
}

type CatalogEdge {
"""Opaque position marker. Pass it as `after` to get the next page."""
"""
Opaque position marker. Pass it as `after` to get the next page.
"""
cursor: String!
node: CatalogItem!
}
Expand Down Expand Up @@ -180,11 +208,17 @@ type Query {
vault(id: ID!): Vault
catalogItem(id: ID!): CatalogItem

"""The logged-in coach, or null when nobody is logged in."""
"""
The logged-in coach, or null when nobody is logged in.
"""
me: Coach
"""Throws UNAUTHENTICATED or NOT_FOUND."""
"""
Throws UNAUTHENTICATED or NOT_FOUND.
"""
gymnast(id: ID!): Gymnast
"""Throws UNAUTHENTICATED or NOT_FOUND."""
"""
Throws UNAUTHENTICATED or NOT_FOUND.
"""
routine(id: ID!): Routine
"""
The D-score of a list of items, for the calculator. Open to everyone and
Expand All @@ -206,22 +240,30 @@ type Query {
search: String
filter: CatalogFilter
sort: CatalogSort
"""Page size, 1-100. A value outside the range gives BAD_USER_INPUT."""
"""
Page size, 1-100. A value outside the range gives BAD_USER_INPUT.
"""
first: Int! = 20
after: String
): CatalogConnection!
}

"""An ISO 8601 date and time, e.g. 2026-10-01T12:00:00Z."""
"""
An ISO 8601 date and time, e.g. 2026-10-01T12:00:00Z.
"""
scalar DateTime

"""A logged-in user. Google's own id is never exposed."""
"""
A logged-in user. Google's own id is never exposed.
"""
type Coach {
id: ID!
name: String!
email: String!
createdAt: DateTime!
"""All the coach's gymnasts. Limited to 200 per coach by the backend."""
"""
All the coach's gymnasts. Limited to 200 per coach by the backend.
"""
gymnasts: [Gymnast!]!
}

Expand Down Expand Up @@ -259,7 +301,9 @@ type Routine {
}

type AuthPayload {
"""Session token the client sends in the Authorization header."""
"""
Session token the client sends in the Authorization header.
"""
token: String!
coach: Coach!
}
Expand All @@ -271,18 +315,28 @@ input CreateRoutineInput {
}

type Mutation {
"""Verifies the Google token and creates the coach on the first login."""
"""
Verifies the Google token and creates the coach on the first login.
"""
login(googleToken: String!): AuthPayload!

"""Throws LIMIT_REACHED."""
"""
Throws LIMIT_REACHED.
"""
createGymnast(name: String!): Gymnast!
renameGymnast(id: ID!, name: String!): Gymnast!
"""Also deletes the gymnast's routines. Returns the id of the deleted gymnast."""
"""
Also deletes the gymnast's routines. Returns the id of the deleted gymnast.
"""
deleteGymnast(id: ID!): ID!

"""Throws NAME_TAKEN or LIMIT_REACHED."""
"""
Throws NAME_TAKEN or LIMIT_REACHED.
"""
createRoutine(input: CreateRoutineInput!): Routine!
"""Throws NAME_TAKEN."""
"""
Throws NAME_TAKEN.
"""
renameRoutine(id: ID!, name: String!): Routine!
deleteRoutine(id: ID!): ID!
"""
Expand Down
8 changes: 8 additions & 0 deletions docs/AI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@
its own issue. Claude made the changes, checked the query plans with
`EXPLAIN ANALYZE`, added tests for each point and updated the docs.

## Database Infrastructure & Setup Completion (`backend/package.json`, `backend/.env.example`, `docs/VM_SETUP.md`, `docs/AI_USAGE.md`, `src/App.tsx`)

- **Tool:** ChatGPT & Gemini
- **What the team did:** Defined the PostgreSQL 18 dual-user security model for the VM (separating `wag_migrator` schema management from `wag_app` runtime CRUD operations per `docs/API.md`), updated `backend/.env.example` and `package.json` to handle dynamic migration connection fallbacks, and resolved Vite `base: '/project2/'` asset resolution breaking SVG icons in `src/App.tsx`.
- **What the AI did:** Generated the complete VM setup guide (`docs/VM_SETUP.md`) detailing PostgreSQL 18 installation, SQL privilege configurations, and seeding verification steps. Drafted the fallback script for `backend/package.json`, updated environment templates, and refactored hardcoded SVG `/icons.svg#` links in `src/App.tsx` to dynamically import and resolve relative to Vite's base path.
- **After the review:** Verified that PostgreSQL 18, database creation, user roles, and catalog seeding were fully executed and tested on the VM. Confirmed that local dev server instructions in `README.md` correctly reflected the `/project2/` subpath.

## Tailwind design tokens (`index.html`, `src/index.css`)

- **Tool:** GitHub Copilot
Expand All @@ -195,3 +202,4 @@
Tailwind theme, and changed global heading text to the existing navy token.
- **Review:** The team reviewed the theme changes and confirmed the colors
should come from the existing CSS variables rather than new palette values.

91 changes: 91 additions & 0 deletions docs/VM_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# VM Database Setup (PostgreSQL 18)

This document describes how to set up PostgreSQL 18 on the virtual machine (VM) and configure the two database users as required by [`docs/API.md`](API.md).

---

## 1. Install PostgreSQL 18

Install PostgreSQL 18 on Ubuntu/Debian using the official PostgreSQL APT repository:

```bash
# Add official PostgreSQL APT repository
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

# Update packages and install PostgreSQL 18
sudo apt-get update
sudo apt-get -y install postgresql-18
```

Verify that PostgreSQL is active:

```bash
sudo systemctl status postgresql
```

---

## 2. Configure Database & Users

Log into PostgreSQL as the superuser:

```bash
sudo -u postgres psql
```

Run the following SQL statements to create the `wag_catalog` database and set up the dual user roles (`wag_migrator` and `wag_app`):

```sql
-- Create the project database
CREATE DATABASE wag_catalog;

-- 1. Migration User (Schema owner: can create/alter tables and run migrations)
CREATE USER wag_migrator WITH PASSWORD 'YOUR_SECURE_MIGRATOR_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE wag_catalog TO wag_migrator;

-- 2. Application User (Restricted runtime access: CRUD on tables only)
CREATE USER wag_app WITH PASSWORD 'YOUR_SECURE_APP_PASSWORD';
GRANT CONNECT ON DATABASE wag_catalog TO wag_app;

-- Connect to wag_catalog to configure schema permissions
\c wag_catalog

-- Grant schema and table permissions to wag_app
GRANT USAGE ON SCHEMA public TO wag_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO wag_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO wag_app;
```

Type `\q` to exit `psql`.

---

## 3. Configure Environment Variables

Create a `backend/.env` file on the server (copied from `backend/.env.example`):

```env
# Runtime backend server connection (wag_app)
DATABASE_URL="postgresql://wag_app:YOUR_SECURE_APP_PASSWORD@localhost:5432/wag_catalog"
# Migration script connection (wag_migrator)
MIGRATION_DATABASE_URL="postgresql://wag_migrator:YOUR_SECURE_MIGRATOR_PASSWORD@localhost:5432/wag_catalog"
PORT=3001
SESSION_SECRET="your_production_session_secret"
```

---

## 4. Run Migrations & Initial Seed

Run the database setup using the migration connection string:

```bash
DATABASE_URL="postgresql://wag_migrator:YOUR_SECURE_MIGRATOR_PASSWORD@localhost:5432/wag_catalog" npm run setup
```

This command executes Prisma migrations to create all database tables and populates the database with the initial gymnastics catalog data.

_Note: The initial setup and database seeding have been successfully executed on the production VM._
Loading