diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index c7679af..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/client/.env.production b/client/.env.production
new file mode 100644
index 0000000..8eb4c9e
--- /dev/null
+++ b/client/.env.production
@@ -0,0 +1 @@
+VITE_GRAPHQL_URL=http://it2810-01.idi.ntnu.no:3001/
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index e580398..cb9fa52 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -12,6 +12,7 @@
"graphql": "^16.14.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
+ "react-router": "^8.4.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
@@ -617,6 +618,12 @@
"url": "https://github.com/sponsors/oxc-project"
}
},
+ "node_modules/@remix-run/route-pattern": {
+ "version": "0.22.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz",
+ "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==",
+ "license": "MIT"
+ },
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz",
@@ -1403,6 +1410,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie-es": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
+ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2531,6 +2544,28 @@
"react": "^19.3.0"
}
},
+ "node_modules/react-router": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.4.0.tgz",
+ "integrity": "sha512-JtydAkBvU9UTEe5qNC+POhu+ZBNM7rlKY2IegwXc7Idj7xfRG16x5RZrw3mju+XlqBxfvb2ky9P3jUp9WyWC1g==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/route-pattern": "^0.22.1",
+ "cookie-es": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=22.22.0"
+ },
+ "peerDependencies": {
+ "react": ">=19.2.7",
+ "react-dom": ">=19.2.7"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/rolldown": {
"version": "1.2.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz",
diff --git a/client/package.json b/client/package.json
index 55cb7d5..a82364f 100644
--- a/client/package.json
+++ b/client/package.json
@@ -14,6 +14,7 @@
"graphql": "^16.14.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
+ "react-router": "^8.4.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
diff --git a/client/src/apollo/client.ts b/client/src/apollo/client.ts
new file mode 100644
index 0000000..e0a5bff
--- /dev/null
+++ b/client/src/apollo/client.ts
@@ -0,0 +1,17 @@
+import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
+
+/**
+ * Apollo Client handles communication with our GraphQL backend and is also
+ * our state management: InMemoryCache stores query results, so the same
+ * query isn't sent to the server again (fewer requests, more sustainable).
+ *
+ * The backend URL is read from the environment variable VITE_GRAPHQL_URL,
+ * so the same code works locally and on the VM. Without it, we use the
+ * local backend on port 3001.
+ */
+export const client = new ApolloClient({
+ link: new HttpLink({
+ uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/',
+ }),
+ cache: new InMemoryCache(),
+});
\ No newline at end of file
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 3db6379..e1bbd9e 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -1,30 +1,17 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
-import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
+import { BrowserRouter } from 'react-router';
import { ApolloProvider } from '@apollo/client/react';
+import { client } from './apollo/client';
+import App from './App';
import './index.css';
-import App from './App.tsx';
-
-/**
- * Apollo Client handles communication with our GraphQL backend and is also
- * our state management: InMemoryCache stores query results, so the same
- * query isn't sent to the server again (fewer requests, more sustainable).
- *
- * The backend URL is read from the environment variable VITE_GRAPHQL_URL,
- * so the same code works locally and on the VM. Without it, we use the
- * local backend on port 3001.
- */
-const client = new ApolloClient({
- link: new HttpLink({
- uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/',
- }),
- cache: new InMemoryCache(),
-});
createRoot(document.getElementById('root')!).render(
-
-
-
+
+
+
+
+
,
);
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 9982072..2a1e3b1 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -3,5 +3,7 @@ import { defineConfig } from 'vite'
// https://vite.dev/config/
export default defineConfig({
+ // The app is served from http://it2810-xx.idi.ntnu.no/project2/
+ base: '/project2/',
plugins: [react()],
})
diff --git a/deploy/apache-project2.conf b/deploy/apache-project2.conf
new file mode 100644
index 0000000..d7632c8
--- /dev/null
+++ b/deploy/apache-project2.conf
@@ -0,0 +1,20 @@
+# Apache config for the Wine not? client, served at /project2.
+# Installed to /etc/apache2/conf-available/project2.conf on the VM by deploy/deploy.sh on every deploy.
+
+
+ # Unknown paths (e.g. /project2/wine/42) get index.html,
+ # so React Router can handle them and a page refresh doesn't give 404.
+ FallbackResource /project2/index.html
+
+ # index.html and version.txt are always re-checked,
+ # so a new deploy shows up immediately.
+
+ Header set Cache-Control "no-cache"
+
+
+
+# Vite gives built files hashed names, so browsers can cache them for a year.
+# Returning visitors don't download them again (less data transferred).
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
diff --git a/deploy/backend.env.example b/deploy/backend.env.example
new file mode 100644
index 0000000..1f02da8
--- /dev/null
+++ b/deploy/backend.env.example
@@ -0,0 +1,9 @@
+# Template for /etc/wine-not/backend.env on the VM. The real file, with the
+# password, only exists on the VM and can be read with sudo.
+# Same variable names as backend/.env on a laptop.
+PORT=3001
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=winenot
+DB_USER=winenot_app
+DB_PASSWORD=
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100755
index 0000000..12538e1
--- /dev/null
+++ b/deploy/deploy.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Builds the backend and the client on the VM and puts them live. Runs as root.
+#
+# Normally started by deploy/push-to-vm.sh on a laptop, which sends a new
+# copy of main:
+# bash deploy.sh --install
+#
+# To rebuild the code that's already on the VM (e.g. after a migration),
+# on the VM:
+# sudo bash /opt/wine-not/deploy/deploy.sh
+#
+# Both parts are built before anything goes live. If a build fails, the
+# script stops and the version that's already live keeps running.
+set -euo pipefail
+
+main() {
+ local app_dir=/opt/wine-not
+ local web_dir=/var/www/html/project2
+
+ # Only one deploy at a time; a second one waits for the first to finish.
+ exec 9>/run/wine-not-deploy.lock
+ flock 9
+
+ if [ "${1:-}" = "--install" ]; then
+ local src="$2" commit="$3"
+ echo "==> Copying commit $commit to $app_dir"
+ mkdir -p "$app_dir"
+ chmod 755 "$src" # the backend runs as user winenot and must read the code
+ # Replace the code, but keep installed packages, builds and the CSV data.
+ rsync -a --delete \
+ --exclude node_modules --exclude dist --exclude /backend/data \
+ "$src/" "$app_dir/"
+ echo "$commit" > "$app_dir/COMMIT"
+ fi
+
+ echo "==> Backend: install and build"
+ cd "$app_dir/backend"
+ npm ci
+ npm run build
+
+ echo "==> Client: install and build"
+ cd "$app_dir/client"
+ npm ci
+ npm run build
+
+ echo "==> Server setup: backend service and Apache config"
+ install -m 644 "$app_dir/deploy/wine-not-api.service" /etc/systemd/system/wine-not-api.service
+ systemctl daemon-reload
+ systemctl enable --quiet wine-not-api
+ install -m 644 "$app_dir/deploy/apache-project2.conf" /etc/apache2/conf-available/project2.conf
+ a2enmod --quiet headers
+ a2enconf --quiet project2
+ apache2ctl configtest
+
+ echo "==> Going live"
+ systemctl restart wine-not-api
+ mkdir -p "$web_dir"
+ rsync -a --delete "$app_dir/client/dist/" "$web_dir/"
+ systemctl restart apache2
+
+ # Shows which commit is live at /project2/version.txt
+ local commit_live
+ commit_live="$(cat "$app_dir/COMMIT" 2>/dev/null || echo unknown)"
+ echo "$commit_live deployed $(date '+%Y-%m-%d %H:%M')" > "$web_dir/version.txt"
+
+ sleep 2
+ echo "==> Done. Commit $commit_live. Backend status: $(systemctl is-active wine-not-api)"
+}
+
+# Everything is inside a function so bash reads the whole script before
+# running it, even if the file is replaced while it runs.
+main "$@"
diff --git a/deploy/push-to-vm.sh b/deploy/push-to-vm.sh
new file mode 100755
index 0000000..cfe210f
--- /dev/null
+++ b/deploy/push-to-vm.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+# Deploys the newest version of main to the VM.
+#
+# Run it on your LAPTOP, from the repo, on the NTNU network or VPN:
+# bash deploy/push-to-vm.sh
+#
+# The VM has no access to the Git server, so your laptop packs up what's on
+# main, copies it to the VM and runs deploy/deploy.sh there.
+# You may be asked for your NTNU password up to three times:
+# once to copy, once to log in and once for sudo.
+set -euo pipefail
+
+VM_HOST="it2810-01.idi.ntnu.no"
+
+if [ "$#" -ne 1 ]; then
+ echo "Usage: bash deploy/push-to-vm.sh " >&2
+ exit 1
+fi
+vm_user="$1"
+
+cd "$(git rev-parse --show-toplevel)"
+
+echo "==> Getting the newest main"
+git fetch --quiet origin main
+commit="$(git rev-parse --short origin/main)"
+echo " Commit $commit: $(git log -1 --format=%s origin/main)"
+
+# Only committed files on main are packed: no node_modules, .env or CSV data.
+tmp_dir="$(mktemp -d)"
+trap 'rm -rf "$tmp_dir"' EXIT
+git archive --format=tar.gz -o "$tmp_dir/wine-not.tar.gz" origin/main
+
+echo "==> Copying to $VM_HOST"
+remote_archive="/tmp/wine-not-$commit.tar.gz"
+scp -q "$tmp_dir/wine-not.tar.gz" "$vm_user@$VM_HOST:$remote_archive"
+
+echo "==> Building and starting on the VM"
+# Unpacks into a temporary folder and runs the deploy.sh from this commit,
+# which copies the code to /opt/wine-not, builds it and puts it live.
+ssh -t "$vm_user@$VM_HOST" \
+ "sudo bash -c 'set -e; d=\$(mktemp -d); tar -xzf $remote_archive -C \"\$d\"; rm -f $remote_archive; bash \"\$d/deploy/deploy.sh\" --install \"\$d\" $commit; rm -rf \"\$d\"'"
+
+echo "==> Live at http://$VM_HOST/project2/"
diff --git a/deploy/wine-not-api.service b/deploy/wine-not-api.service
new file mode 100644
index 0000000..c469722
--- /dev/null
+++ b/deploy/wine-not-api.service
@@ -0,0 +1,19 @@
+# systemd service that runs the Wine not? GraphQL backend on port 3001.
+# Installed to /etc/systemd/system/ on the VM by deploy/deploy.sh on every deploy.
+[Unit]
+Description=Wine not? GraphQL backend
+After=network.target postgresql.service
+
+[Service]
+User=winenot
+Group=winenot
+WorkingDirectory=/opt/wine-not/backend
+# Database settings and port. This file exists only on the VM, never in Git.
+EnvironmentFile=/etc/wine-not/backend.env
+Environment=NODE_ENV=production
+ExecStart=/usr/bin/node dist/index.js
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
diff --git a/deploy/with-env.sh b/deploy/with-env.sh
new file mode 100755
index 0000000..d4d8dd8
--- /dev/null
+++ b/deploy/with-env.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+# Runs one command with the backend's database settings loaded from
+# /etc/wine-not/backend.env (the secret file that only exists on the VM).
+# The DB_ settings are also given to psql (as PGHOST, PGUSER, ...),
+# so psql connects to the same database as the backend.
+#
+# Usage on the VM, from /opt/wine-not/backend:
+# sudo bash ../deploy/with-env.sh npm run import
+# sudo bash ../deploy/with-env.sh psql -f db/migrations/.sql
+set -euo pipefail
+
+if [ "$#" -eq 0 ]; then
+ echo "Usage: sudo bash ../deploy/with-env.sh [args...]" >&2
+ exit 1
+fi
+
+set -a # export every variable defined in the file
+# shellcheck source=/dev/null
+. /etc/wine-not/backend.env
+set +a
+
+export PGHOST="${DB_HOST:-localhost}"
+export PGPORT="${DB_PORT:-5432}"
+export PGDATABASE="${DB_NAME:-}"
+export PGUSER="${DB_USER:-}"
+export PGPASSWORD="${DB_PASSWORD:-}"
+
+exec "$@"