Goal: get the web app live on the VM so Brennan can test it over the campus network by June 9. Architecture: plain HTTP, single origin via nginx on :80 (no TLS — decided with IT; see doc/it-install-requirements.md).
Brennan's browser ──HTTP :80──► nginx ──┬─► / → Next.js 127.0.0.1:3000 http://demo.ece.mcmaster.ca ├─► /api/* → FastAPI 127.0.0.1:8000 └─► /healthz → FastAPI 127.0.0.1:8000Backend + frontend listen on localhost only; nginx is the one public door. Auth is the MacID shim (no SSO yet) — fine for a trial; the only "secret" is a non-secret username.
export REPO_DIR="$HOME/ta-course-match" # where the code lives on the VM
export HOSTNAME_PUBLIC="demo.ece.mcmaster.ca" # the URL Brennan visits
export SERVICE_USER="$(whoami)" # the account Ron gave you (has sudo)
export ADMIN_MACIDS="brennan,$(whoami)" # comma-separated; Brennan MUST be here to be admin
export DEMO_CYCLE="mock-2025-26" # solver-ready demo cycle shipped in the repoReplace
brennanwith his real MacID and add any co-admins. Anyone not in this list (and not in the imported cycle's instructor/student tables) gets a 401.
java -version # expect 17.x (Timefold's JVM solver core)
node -v # expect v22.x or v24.x (NOT v20 — EOL)
npm -v
uv --version
git --versionIf any are missing, stop and go back to IT — the rest won't work without them.
git clone https://github.com/NooriDan/ta-course-match.git "$REPO_DIR"
cd "$REPO_DIR"(If IT pre-placed it, just cd "$REPO_DIR" and git pull.)
cd "$REPO_DIR"
uv sync --extra web # creates .venv/ with tcm, tcm-web, alembic console scriptsThis pins Python 3.12 automatically (via .python-version) and installs
Timefold + FastAPI. No system Python needed.
cd "$REPO_DIR"
.venv/bin/tcm validate "$DEMO_CYCLE"
.venv/bin/tcm solve "$DEMO_CYCLE" --time-limit-seconds 20validate should pass; solve should run the JVM and write a result under
results/$DEMO_CYCLE/. If this fails with a JVM/Java error, fix that now —
nothing downstream will solve. (This is the HANDOFF canary, run early.)
cd "$REPO_DIR"
.venv/bin/alembic upgrade head # creates data/tcm.sqlite3 at schema head (migrations 0001–0007)We leave TCM_DB_URL unset → bundled SQLite. Good enough for ~120 trial
users; switch to Postgres only when this goes past the demo.
Create /etc/tcm/tcm.env — read by the backend service:
sudo mkdir -p /etc/tcm
sudo tee /etc/tcm/tcm.env >/dev/null <<EOF
# --- Auth shim (no SSO yet). ENVIRONMENT MUST be dev/test/ci or the boot
# guard refuses to start while TCM_AUTH_MODE=shim. "dev" is what unlocks
# the shim; it does NOT mean hot-reload (TCM_RELOAD stays 0). ---
ENVIRONMENT=dev
TCM_AUTH_MODE=shim
TCM_ADMIN_MACIDS=${ADMIN_MACIDS}
# --- Bind localhost only; nginx is the public door ---
TCM_HOST=127.0.0.1
TCM_PORT=8000
TCM_RELOAD=0
TCM_CORS_ORIGINS=http://${HOSTNAME_PUBLIC}
# --- JVM heap for Timefold (2 GiB suits ECE scale) ---
JAVA_OPTS=-Xmx2g
# systemd has a minimal PATH; pin JAVA_HOME so the service can find the JVM.
JAVA_HOME=$(dirname "$(dirname "$(readlink -f "$(which java)")")")
EOF
cat /etc/tcm/tcm.env # sanity-check the expanded values (esp. JAVA_HOME)NEXT_PUBLIC_API_BASE_URL is baked in at build time — the browser uses it to
reach the API. With single-origin nginx it's just the public hostname:
cd "$REPO_DIR/frontend"
npm ci
NEXT_PUBLIC_API_BASE_URL="http://${HOSTNAME_PUBLIC}" npm run build
⚠️ If you ever change the hostname, you must rebuild — the URL is compiled in.⚠️ Small VM?next buildcan spike to ~1–2 GiB RAM. If it OOM-kills, add swap (sudo fallocate -l 2G /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile) or build locally andrsyncthefrontend/.nextdir up.
Backend:
sudo tee /etc/systemd/system/tcm-web.service >/dev/null <<EOF
[Unit]
Description=tcm-web (FastAPI backend)
After=network.target
[Service]
User=${SERVICE_USER}
WorkingDirectory=${REPO_DIR}
EnvironmentFile=/etc/tcm/tcm.env
ExecStart=${REPO_DIR}/.venv/bin/tcm-web
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
EOFFrontend:
sudo tee /etc/systemd/system/tcm-frontend.service >/dev/null <<EOF
[Unit]
Description=tcm-frontend (Next.js)
After=network.target tcm-web.service
[Service]
User=${SERVICE_USER}
WorkingDirectory=${REPO_DIR}/frontend
Environment=NODE_ENV=production
ExecStart=/usr/bin/npm run start -- -H 127.0.0.1 -p 3000
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
EOFEnable + start:
sudo systemctl daemon-reload
sudo systemctl enable --now tcm-web tcm-frontend
sudo systemctl status tcm-web tcm-frontend --no-pagerBoth should be active (running). Tail logs with
journalctl -u tcm-web -f (or tcm-frontend).
sudo tee /etc/nginx/sites-available/tcm >/dev/null <<EOF
server {
listen 80;
server_name ${HOSTNAME_PUBLIC};
# CV uploads (10 MiB) + cycle-import payloads (50 MiB) — nginx default is 1 MiB
client_max_body_size 50m;
# SSE: live solve-progress stream. Must NOT buffer; long-lived.
location ~ ^/api/cycles/.+/runs/.+/events\$ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host \$host;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
# API + health -> backend
location /api/ {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
location = /healthz {
proxy_pass http://127.0.0.1:8000;
}
# Everything else -> Next.js frontend
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
}
EOF
sudo ln -sf /etc/nginx/sites-available/tcm /etc/nginx/sites-enabled/tcm
sudo rm -f /etc/nginx/sites-enabled/default # drop the "Welcome to nginx" default
sudo nginx -t # config syntax check
sudo systemctl reload nginxsudo ufw allow 80/tcp # Ubuntu/Debian
# RHEL/firewalld: sudo firewall-cmd --permanent --add-service=http && sudo firewall-cmd --reloadConfirm with IT that the campus firewall also lets port 80 reach the VM — the host firewall is only half the path.
curl -s http://localhost/healthz # -> {"status":"ok"}
curl -sI http://localhost/ # -> 200 (Next.js homepage)
curl -si http://localhost/api/cycles # -> 401 (shim rejects: no MacID)
curl -s http://localhost/api/cycles -H "X-MacID: $(whoami)" # -> [] (admin, empty list)Then from your laptop (not the VM), confirm it's reachable on the network:
curl -sI http://demo.ece.mcmaster.ca/ # -> 200Import mock-2025-26 (119 students, 26 courses, 294 rankings — pre-populated, ready to solve):
curl -X POST http://localhost/api/cycles/import \
-H "X-MacID: $(whoami)" \
-H "Content-Type: application/json" \
-d "{\"cycle\":\"${DEMO_CYCLE}\",\"input_dir\":\"${REPO_DIR}/data/${DEMO_CYCLE}\"}"The import endpoint only accepts
input_dirpaths under<repo>/data/— the shipped cycle already lives there, so this just works. A 201 means success; re-checkGET /api/cyclesand you'll see the cycle listed.
Web-path solve canary: sign in as admin (next step), open the cycle, and run
the solver from the dashboard. A run that reaches completed with a score
confirms the full browser → API → JVM → DB round-trip.
- Send him http://demo.ece.mcmaster.ca.
- He lands on the dev sign-in screen → enters his MacID (the one you put
in
ADMIN_MACIDS) → he's in as admin. - He can browse the imported
mock-2025-26cycle, run/inspect a solve, view rankings, etc. - Instructors/students can only sign in after their cycle is imported
(their MacID has to exist in the cycle's tables) — for the demo, the
mock-2025-26roster is already loaded, so any of those MacIDs work too.
| Task | Command |
|---|---|
| Tail backend logs | journalctl -u tcm-web -f |
| Tail frontend logs | journalctl -u tcm-frontend -f |
| Restart everything | sudo systemctl restart tcm-web tcm-frontend |
| Pull a code update | cd $REPO_DIR && git pull && uv sync --extra web && .venv/bin/alembic upgrade head |
| Rebuild FE after update | cd $REPO_DIR/frontend && npm ci && NEXT_PUBLIC_API_BASE_URL="http://$HOSTNAME_PUBLIC" npm run build && sudo systemctl restart tcm-frontend |
| Re-import / add a cycle | drop CSVs under $REPO_DIR/data/<cycle>/, re-run the step 12 curl with that cycle name |
| Back up the demo | copy data/tcm.sqlite3 + data/<cycle>/cvs/ (CVs live on disk, not in the DB) |
| Symptom | Cause → Fix |
|---|---|
| Backend won't start, log says "Refusing to boot: TCM_AUTH_MODE=shim only allowed when ENVIRONMENT is dev/test/ci" | ENVIRONMENT isn't dev. Fix /etc/tcm/tcm.env, systemctl restart tcm-web. |
| Solve fails only as a service (works from CLI in step 4) with a JVM-not-found error | JAVA_HOME wrong/empty in the env file. Recompute: dirname $(dirname $(readlink -f $(which java))), put it in /etc/tcm/tcm.env, restart. |
| Browser loads the page but all data calls fail / CORS error | NEXT_PUBLIC_API_BASE_URL was wrong at build time, or TCM_CORS_ORIGINS ≠ the public origin. Rebuild FE (step 7) and check the env file. |
| Live solve progress bar stalls / SSE not updating | nginx is buffering the stream. Confirm the …/events regex location block is present and nginx -t && systemctl reload nginx. |
| CV upload or cycle import returns 413 | client_max_body_size 50m; missing from the nginx server block. |
next build killed / OOM |
small VM — add swap or build locally and rsync .next (see step 7 note). |
| Brennan gets 401 on sign-in | his MacID isn't in TCM_ADMIN_MACIDS (and the cycle isn't imported). Fix the env file, restart, re-import. |
Owner: Danial (dnoorizadeh@gmail.com). Surface-area reference: HANDOFF.md. Install list for IT: doc/it-install-requirements.md.