Skip to content
Merged

Dev #606

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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
56 changes: 50 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,57 @@ PASSWORD_RESET_MAX_REQUESTS_PER_HOUR=5
# ---- Frontend ----
FRONTEND_PORT=8000

# ---- Seed dataset friendliness ----
# These three are flipped to `false` so the docker-compose `seed` profile
# (and any high-volume local testing) can run unimpeded. `true` is the
# right value for production.
#
# APP_RATELIMIT_ENABLED — blocks per-IP/per-user login bursts. The
# demo dataset has 78 accounts; you'll want them all available
# immediately.
# APP_EMAIL_ENABLED — when on, the backend tries to deliver
# verification + password-reset mail through Resend. With the
# placeholder API key it returns 401 and registration fails.
# APP_SPAM_ENABLED — form-token + honeypot defences on register.
# Off in dev makes E2E tooling and Playwright less fragile.
APP_RATELIMIT_ENABLED=false
APP_EMAIL_ENABLED=false
APP_SPAM_ENABLED=false

# ---- OpenAI / Spring AI (advanced mentor recommendation, #436) ----
# API key used by Spring AI for both embeddings (text-embedding-3-small) and
# chat completions (gpt-4o-mini) in the advanced mentor ranker + LLM prose
# explanation layer. Leave blank to run the backend with only the legacy
# rule-based ranker — the advanced path gracefully degrades when this key is
# absent.
#
# To get the full demo experience (LLM "why recommended" explanations,
# semantic-affinity signals on the follow ranker), set this to a real
# OpenAI API key. Without it the advanced ranker still produces deterministic
# factor scores; only the prose explanation falls back to null.
OPENAI_API_KEY=
# Enable the advanced ranker (otherwise the legacy bag-of-words ranker is used).
MENTOR_ADVANCED_RANKER=false
# Chat / embedding model overrides — leave at the defaults unless you
# specifically need a different model.
OPENAI_CHAT_MODEL=gpt-4o-mini
OPENAI_EMBEDDING_MODEL=text-embedding-3-small

# ---- Advanced ranker toggles ----
# The three "advanced" rankers ship default-off in docker-compose so a
# bare boot is prod-shaped. For the hand-crafted demo dataset you'll
# want them all on so the UI surfaces the richer match scores, the
# LLM-authored explanations, MMR diversity, etc.
MENTOR_ADVANCED_RANKER=true
MENTOR_EXPLANATION_ENABLED=true
# gpt-4o-mini occasionally exceeds the default 3000ms timeout under load;
# bumping to 15s eliminates flaky explanation drops without measurably
# delaying the response (the call still cancels at 15s).
MENTOR_EXPLANATION_TIMEOUT_MS=15000
# Swaps the legacy InterestOverlapFeedRanker for AdvancedForYouFeedRanker
# (candidate generation + scoring + MMR + diversity floor).
FEED_ADVANCED_RANKER=true
# Contextual bandit on top of the For-You ranker. Stays off until the
# impression-tracking follow-up PR lands; flipping to true now is a no-op.
FEED_BANDIT_ENABLED=false

# ---- Neo4j (Follow-graph mirror, #437) ----
# The follow-graph mirror writes every Postgres follow/unfollow into Neo4j
Expand Down Expand Up @@ -84,7 +126,9 @@ NEO4J_BOLT_PORT=7687
# legacy follow ranker still serves recommendations.
FOLLOW_GRAPH_SYNC_ENABLED=true
# Selects between the legacy rule-based follow ranker and the multi-signal
# AdvancedFollowRanker (which includes PersonalizedPageRank — needs Neo4j).
# Leave `legacy` until Neo4j is reachable AND OPENAI_API_KEY is set if you
# want the SemanticAffinity signal active.
FOLLOW_RANKER=legacy
# AdvancedFollowRanker (PPR + second-hop + recent-engagement + direct-
# interaction + cold-start popularity + interest-overlap + optional
# semantic-affinity). The advanced path needs Neo4j reachable (the
# default in the compose stack); the semantic-affinity signal needs
# OPENAI_API_KEY set (it gracefully degrades when the key is empty).
FOLLOW_RANKER=advanced
16 changes: 15 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,27 @@ jobs:
host: ${{ secrets.DEPLOY_HOST }}
username: root
key: ${{ secrets.DEPLOY_SSH_KEY }}
# 30m: cold Maven rebuild (~7m) + Neo4j boot (~3m) + backend
# health wait. The new seed service adds ~10s on top, well
# within budget.
# Default is 10m; cold Maven rebuild (~7m) + Neo4j + backend
# health wait (~3m) exceeds it. Cf. run 25790084160 which hit
# the cap and left the frontend container in `Created` state.
command_timeout: 30m
script: |
set -e
cd /root/bounswe2026group7
git fetch origin
git checkout main
git pull origin main
docker compose up --build -d
# --profile demo enables the one-shot `seed` service that
# loads scripts/seed_snapshot_data/ into Postgres and into
# the shared uploads volume after the backend reports
# healthy. The seed is idempotent (DELETE-then-COPY of
# %@seed.test rows only — real users registered through
# the UI and the bootstrap admin are never touched), so
# every deploy re-syncs the demo dataset without affecting
# production accounts. Pair this with --profile demo on
# any future `docker compose down` invocation so the
# one-shot seed container is cleaned up too.
docker compose --profile demo up --build -d
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ scripts/seed_local_credentials.txt
__pycache__/
*.pyc

# Local Python venv for seed/snapshot scripts (bootstrapped by scripts/seed.sh)
.venv/

# Seed-pipeline runtime artifacts (token cache, intermediate image
# downloads, exported personas.json — all regenerated on each run).
scripts/.seed_*.json
scripts/.seed_*_tmp/
scripts/personas.json

# Backend - Spring Boot
backend/target/
backend/*.class
Expand Down Expand Up @@ -41,5 +50,8 @@ npm-debug.*
yarn-debug.*
yarn-error.*
.env
.env.*
!.env.example
!.env.production.example
# Local issue/PR scaffolding (claude-generated drafts)
claude_files/
207 changes: 176 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,22 @@ Compose installed.

```bash
cp .env.example .env
docker compose up --build
docker compose --profile demo up --build -d
```

This starts four services:
That single command does three things:

1. Builds the backend, frontend, and custom Neo4j-GDS images on first run.
2. Starts PostgreSQL, Neo4j, the backend, and the frontend.
3. Once the backend reports healthy, runs the one-shot **`seed`** service —
it loads the hand-crafted demo dataset (78 users, 288 posts, ~50 mentor
ratings, 6 active + 48 past mentorships, profile photos, post images,
task-attached PDFs) into Postgres and into the shared uploads volume.

The seed step is opt-in: omit `--profile demo` if you want a clean DB.

Once everything settles you have four long-running services plus a one-shot
seed container that exits cleanly:

| Service | URL | Description |
|----------|---------------------------------------|--------------------------------------------|
Expand All @@ -29,43 +41,143 @@ This starts four services:
| Database | `localhost:5433` | PostgreSQL (managed via Flyway migrations) |

Defaults in `.env.example` are good enough for local development — no editing
required.
required. The seed-friendly safety flags (`APP_RATELIMIT_ENABLED=false`,
`APP_EMAIL_ENABLED=false`, `APP_SPAM_ENABLED=false`) and the four advanced
ranker toggles (`MENTOR_ADVANCED_RANKER`, `MENTOR_EXPLANATION_ENABLED`,
`FEED_ADVANCED_RANKER`, `FOLLOW_RANKER=advanced`) are all pre-set so the
demo dataset surfaces the full UI. The only value worth editing for a
real local-with-LLM run is `OPENAI_API_KEY` — see the [env reference](#environment-variable-reference)
below.

Stop the stack:

```bash
docker compose --profile demo down
```

Include `--profile demo` here too — without it the seed container is left
orphaned with a stale network reference and the next `up` will fail. Add
`-v` to also drop the Postgres and Neo4j volumes for a fully clean reset.

Re-seed after manual mutation (stack already running):

```bash
docker compose --profile demo run --rm seed
```

`run --rm` runs the loader as a one-shot and removes the container on exit,
so it never goes stale.

### Seeding options

Stop the stack with `docker compose down`. Add `-v` to also drop the Postgres
and Neo4j volumes if you want a fully clean reset on the next boot.
Two seeders are available depending on what you need.

### Seed local data
#### Demo dataset (recommended — automatic, hand-crafted content)

Right after a fresh `docker compose up`, the app is empty. The seed script
populates Postgres directly (bypassing email verification, Resend, and the
spam-bot defences) with **1 admin + 40 mentors + 60 mentees**, each with
realistic interests, bios, and goals.
The hand-crafted demo dataset (78 users, 288 posts with images,
6 active + 48 past mentorships with tasks/meetings/messages, 45 ratings,
100 mentor-mentee + mentor-pair messages, AI-generated profile photos,
Unsplash-fetched post images, task-attached PDFs) ships pre-built as a
snapshot under `scripts/seed_snapshot_data/` and loads automatically
through the `demo` compose profile shown in the [Quick start](#quick-start-development-docker)
above:

```bash
docker compose --profile demo up --build -d # first run on a fresh checkout
docker compose --profile demo down # tear down (don't drop the profile flag)
docker compose --profile demo run --rm seed # re-seed against a running stack
```

Each load is idempotent (DELETE-then-COPY against seed-owned rows; the
`admin@group7.com` bootstrap is never touched).

The same snapshot is reachable from the host without involving the
compose service, which is useful when iterating on the snapshot itself:

```bash
./scripts/seed.sh load # uses scripts/seed_snapshot_data/ by default
./scripts/seed.sh dump # re-captures current DB state into the same dir
./scripts/seed.sh status # prints seed-owned counts
./scripts/seed.sh help
```

The first call bootstraps `.venv/` and installs `scripts/requirements-seed.txt`;
subsequent calls reuse it.

#### Lightweight roster (`scripts/seed_local.py`)

If you only need a clean list of accounts (1 admin + 40 mentors + 60
mentees, no posts/mentorships/photos), use the older direct-Postgres
seeder. It's destructive-but-scoped — it wipes every prior
`@seed.local` user first and re-inserts the roster.

```bash
pip install -r scripts/requirements-seed.txt
python3 scripts/seed_local.py
```

Every run is destructive-but-scoped: it deletes all prior `@seed.local` users
first, then re-inserts the roster. Real users you registered through the web
UI are never touched. Run it again whenever you want a clean demo dataset.

The script prints a credentials block to stdout (sample users per role) and
writes the full list — including each mentor's field and each mentee's major —
to `scripts/seed_local_credentials.txt`.
The script prints a credentials block to stdout and writes the full
roster to `scripts/seed_local_credentials.txt`.

### Default credentials (after seeding)

All seeded passwords are deterministic. Sample accounts:
Sample accounts from the demo dataset (`--profile demo`, `@seed.test`,
all passwords `Seed1234!`):

| Role | Email | Notes |
|------------------|--------------------------------|---------------------------------------------------------|
| Demo mentee | `defne.korkmaz.74@seed.test` | CS senior, 3 past mentorships, no active mentor — ready for AI-match flow |
| Demo mentor | `yildiz.demir.78@seed.test` | Active mentorship with Rıza Yavuz; full task + meeting timeline |
| Senior mentor | `melis.sezen.2@seed.test` | Cloud Architecture, active mentee Tolga |
| Research mentor | `dilara.oz.5@seed.test` | Completed engagement with Defne, 5-star review attached |

The full 78-user roster (20 mentors + 57 mentees + 1 admin) is in
`scripts/seed_snapshot_data/data/users.csv`. Every user has the same
password `Seed1234!`.

If you ran `scripts/seed_local.py` instead, accounts use the
`@seed.local` domain with role-specific passwords (`Admin1234!`,
`Mentor1234!`, `Mentee1234!`); the full list is written to
`scripts/seed_local_credentials.txt`.

### Environment variable reference

All knobs below live in `.env.example` with full inline comments. The
table here is a quick map of what each one controls and what state the
demo expects.

#### Seed-friendliness (`.env.example` defaults to demo-safe values)

| Variable | Demo (`.env.example`) | Prod | What it does |
|---------------------------|-----------------------|---------------|-----------------------------------------------------------------------------|
| `APP_RATELIMIT_ENABLED` | `false` | `true` | Per-IP/per-user login throttling. 78-account demo needs it off for E2E. |
| `APP_EMAIL_ENABLED` | `false` | `true` | If on, registration sends through Resend; placeholder API key returns 401. |
| `APP_SPAM_ENABLED` | `false` | `true` | Form-token + honeypot defences on the register form. |

| Role | Email | Password |
|--------|--------------------------------------------------|---------------|
| Admin | `admin@seed.local` | `Admin1234!` |
| Mentor | `mentor.<first>.<last>.<n>@seed.local` (40 total) | `Mentor1234!` |
| Mentee | `mentee.<first>.<last>.<n>@seed.local` (60 total) | `Mentee1234!` |
#### Advanced ranker stack (`.env.example` defaults all on)

The first 5 mentors and 5 mentees are echoed when the script finishes; the
remaining accounts live in `scripts/seed_local_credentials.txt`.
| Variable | Demo | Default in compose | What it controls |
|--------------------------------|-------|--------------------|---------------------------------------------------------------------------|
| `MENTOR_ADVANCED_RANKER` | `true`| `false` | Swaps the rule-based mentor matcher for the weighted-signal pipeline. |
| `MENTOR_EXPLANATION_ENABLED` | `true`| `false` | LLM-authored "why recommended" sentence on each mentor card. |
| `MENTOR_EXPLANATION_TIMEOUT_MS`| `15000`| `3000` | Upper bound on the gpt-4o-mini call. 3000 is too tight under load. |
| `FEED_ADVANCED_RANKER` | `true`| `false` | Advanced For-You pipeline (candidate generation + scoring + MMR). |
| `FEED_BANDIT_ENABLED` | `false`| `false` | Contextual bandit — gated behind impression tracking; stays off. |
| `FOLLOW_RANKER` | `advanced`| `legacy` | Multi-signal follow ranker (PPR + second-hop + …). Needs Neo4j. |
| `FOLLOW_GRAPH_SYNC_ENABLED` | `true`| `false` | Mirrors every PG follow/unfollow into Neo4j after-commit. |

#### Keys you (might) need to set manually

| Variable | When to set |
|-------------------|------------------------------------------------------------------------------------------------------------|
| `OPENAI_API_KEY` | If you want LLM mentor explanations and the semantic-affinity follow signal. Empty = graceful fallback. |
| `RESEND_API_KEY` | Only when `APP_EMAIL_ENABLED=true`. The demo stack doesn't need it. |
| `JWT_SECRET` | Already set to a base64 dev secret; rotate in `.env.production` for any deploy. |
| `NEO4J_PASSWORD` | `group7pass` locally; **must** be rotated in `.env.production`. |

The OpenAI key flows into both embeddings (`text-embedding-3-small`) and
chat (`gpt-4o-mini`) by default. Override with `OPENAI_CHAT_MODEL` /
`OPENAI_EMBEDDING_MODEL` if needed.

### Quick start (development, without Docker)

Expand Down Expand Up @@ -102,8 +214,40 @@ used by Playwright, are documented in [frontend/README.md](frontend/README.md).

### Production deployment

Production uses a separate compose file (`docker-compose.prod.yml`) that
expects a managed Postgres and reads secrets from `.env.production`.
The live demo at [mymentornet.org](https://mymentornet.org) is deployed by
[`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) on every
push to `main`. The workflow SSHes into the DigitalOcean droplet and runs:

```bash
git pull origin main
docker compose --profile demo up --build -d
```

The droplet uses the same `docker-compose.yml` as local dev (bundled
Postgres + Neo4j) with its own `.env` carrying real secrets (`JWT_SECRET`,
`OPENAI_API_KEY`, `RESEND_API_KEY`, `NEO4J_PASSWORD`, …). The `--profile demo`
flag activates the one-shot seed service so every deploy re-syncs the
hand-crafted demo dataset; the seed is scoped to `%@seed.test` users only
and never touches real accounts registered through the UI.

For the demo profile to surface the full UI, the droplet's `.env` should
include the same advanced flags shipped in [`.env.example`](.env.example):

```sh
MENTOR_ADVANCED_RANKER=true
MENTOR_EXPLANATION_ENABLED=true
MENTOR_EXPLANATION_TIMEOUT_MS=15000
FEED_ADVANCED_RANKER=true
FOLLOW_RANKER=advanced
FOLLOW_GRAPH_SYNC_ENABLED=true
```

`APP_RATELIMIT_ENABLED`, `APP_EMAIL_ENABLED`, and `APP_SPAM_ENABLED` can
stay at their prod defaults (`true`) — the seed itself uses direct
Postgres connections, so rate limits and spam defences don't affect it.

For a hardened install that doesn't bundle Postgres in-stack (e.g. against
managed PG), use the separate `docker-compose.prod.yml`:

```bash
cp .env.production.example .env.production
Expand All @@ -113,10 +257,11 @@ cp .env.production.example .env.production
docker compose --env-file .env.production -f docker-compose.prod.yml up --build -d
```

The prod compose runs the backend, frontend, and Neo4j in-stack and binds the
frontend to host port 80 by default (override with `FRONTEND_PORT`). It does
not bring up a Postgres service — point `SPRING_DATASOURCE_URL` at your
managed database.
That compose targets a managed PostgreSQL via `SPRING_DATASOURCE_URL`,
runs Neo4j and the backend in-stack, and binds the frontend to host
port 80 by default (override with `FRONTEND_PORT`). The `seed` service
isn't wired into the prod compose by design — production datasets
shouldn't be reset on every boot.

See [PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md) for the broader
deployment checklist (TLS, backups, secret rotation).
Expand Down
Loading
Loading