From 0994e7ec6eb365a94eb94374c9f98c4bfd341d23 Mon Sep 17 00:00:00 2001 From: PathNotFound404 Date: Wed, 29 Apr 2026 16:25:16 -0500 Subject: [PATCH 1/3] Deployment Branch, Basic Setup for Deployment --- .github/workflows/ci.yml | 46 ++ .github/workflows/deploy.yml | 69 +++ backend/Dockerfile | 12 + backend/pom.xml | 5 + .../tcu/projectpulse/config/CorsConfig.java | 6 +- .../src/main/resources/application-prod.yaml | 21 + docs/azure-deployment.md | 418 ++++++++++++++++++ frontend/.env.example | 4 + frontend/Dockerfile | 13 + frontend/nginx.conf | 14 + frontend/src/services/api.js | 2 +- 11 files changed, 608 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 backend/Dockerfile create mode 100644 backend/src/main/resources/application-prod.yaml create mode 100644 docs/azure-deployment.md create mode 100644 frontend/.env.example create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0ac0515 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + pull_request: + branches: [main] + +jobs: + backend-test: + name: Backend — build & test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: temurin + cache: maven + + - name: Run tests + working-directory: backend + run: mvn test + + frontend-build: + name: Frontend — build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: frontend + run: npm ci + + - name: Build + working-directory: frontend + run: npm run build + env: + VITE_API_BASE_URL: https://placeholder-for-ci-build-check diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f758d5f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,69 @@ +name: Deploy + +on: + push: + branches: [main] + +env: + BACKEND_IMAGE: projectpulse-backend + FRONTEND_IMAGE: projectpulse-frontend + +jobs: + deploy-backend: + name: Backend — build, push & deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Log in to Azure Container Registry + uses: azure/docker-login@v2 + with: + login-server: ${{ secrets.ACR_LOGIN_SERVER }} + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and push backend image + working-directory: backend + run: | + IMAGE=${{ secrets.ACR_LOGIN_SERVER }}/${{ env.BACKEND_IMAGE }} + docker build -t $IMAGE:${{ github.sha }} -t $IMAGE:latest . + docker push $IMAGE:${{ github.sha }} + docker push $IMAGE:latest + + - name: Deploy to Azure App Service + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ secrets.BACKEND_APP_NAME }} + publish-profile: ${{ secrets.BACKEND_PUBLISH_PROFILE }} + images: ${{ secrets.ACR_LOGIN_SERVER }}/${{ env.BACKEND_IMAGE }}:${{ github.sha }} + + deploy-frontend: + name: Frontend — build, push & deploy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Log in to Azure Container Registry + uses: azure/docker-login@v2 + with: + login-server: ${{ secrets.ACR_LOGIN_SERVER }} + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + + - name: Build and push frontend image + working-directory: frontend + run: | + IMAGE=${{ secrets.ACR_LOGIN_SERVER }}/${{ env.FRONTEND_IMAGE }} + docker build \ + --build-arg VITE_API_BASE_URL=${{ secrets.BACKEND_URL }} \ + -t $IMAGE:${{ github.sha }} \ + -t $IMAGE:latest . + docker push $IMAGE:${{ github.sha }} + docker push $IMAGE:latest + + - name: Deploy to Azure App Service + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ secrets.FRONTEND_APP_NAME }} + publish-profile: ${{ secrets.FRONTEND_PUBLISH_PROFILE }} + images: ${{ secrets.ACR_LOGIN_SERVER }}/${{ env.FRONTEND_IMAGE }}:${{ github.sha }} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..cceaa7e --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM maven:3.9-eclipse-temurin-17 AS builder +WORKDIR /app +COPY pom.xml . +RUN mvn dependency:go-offline -q +COPY src ./src +RUN mvn package -DskipTests -q + +FROM eclipse-temurin:17-jre-alpine +WORKDIR /app +COPY --from=builder /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"] diff --git a/backend/pom.xml b/backend/pom.xml index f8123b8..66e4457 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -53,6 +53,11 @@ h2 runtime + + com.mysql + mysql-connector-j + runtime + org.springframework.security spring-security-crypto diff --git a/backend/src/main/java/com/tcu/projectpulse/config/CorsConfig.java b/backend/src/main/java/com/tcu/projectpulse/config/CorsConfig.java index fc5f86c..eb6ce47 100644 --- a/backend/src/main/java/com/tcu/projectpulse/config/CorsConfig.java +++ b/backend/src/main/java/com/tcu/projectpulse/config/CorsConfig.java @@ -1,5 +1,6 @@ package com.tcu.projectpulse.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -7,10 +8,13 @@ @Configuration public class CorsConfig implements WebMvcConfigurer { + @Value("${app.cors-allowed-origin:http://localhost:5173}") + private String allowedOrigin; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") - .allowedOrigins("http://localhost:5173") + .allowedOrigins(allowedOrigin) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); diff --git a/backend/src/main/resources/application-prod.yaml b/backend/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..3ba1828 --- /dev/null +++ b/backend/src/main/resources/application-prod.yaml @@ -0,0 +1,21 @@ +spring: + datasource: + url: ${SPRING_DATASOURCE_URL} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: update + database-platform: org.hibernate.dialect.MySQLDialect + show-sql: false + h2: + console: + enabled: false + +server: + port: 8080 + +app: + frontend-base-url: ${APP_FRONTEND_BASE_URL} + cors-allowed-origin: ${APP_CORS_ALLOWED_ORIGIN} diff --git a/docs/azure-deployment.md b/docs/azure-deployment.md new file mode 100644 index 0000000..13ce1a2 --- /dev/null +++ b/docs/azure-deployment.md @@ -0,0 +1,418 @@ +/# Azure Deployment Guide — Project Pulse + +This guide covers every manual step required to provision Azure infrastructure, configure secrets, and trigger the CI/CD pipeline for Project Pulse. + +--- + +## Prerequisites + +Install and authenticate the following tools before starting: + +- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) (`az login`) +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) +- GitHub repository admin access (to add Actions secrets) + +Log in to Azure: + +```bash +az login +az account set --subscription "" +``` + +--- + +## 1. Create a Resource Group + +All Project Pulse resources live in one group for easy management and teardown. + +**Azure Portal:** +1. Go to **Resource groups** → **Create** +2. Subscription: your subscription +3. Resource group name: `project-pulse-rg` +4. Region: `East US` (or nearest to your users) +5. Click **Review + create** → **Create** + +**Azure CLI equivalent:** +```bash +az group create --name project-pulse-rg --location eastus +``` + +--- + +## 2. Create Azure Container Registry (ACR) + +ACR stores the Docker images that GitHub Actions builds and Azure App Service pulls. + +**Azure Portal:** +1. Search **Container registries** → **Create** +2. Resource group: `project-pulse-rg` +3. Registry name: `projectpulseacr` *(must be globally unique — add initials if taken)* +4. Location: same as resource group +5. Pricing plan: **Basic** +6. Click **Review + create** → **Create** + +**Enable Admin user** (needed for GitHub Actions login): +1. Open the registry → **Settings → Access keys** +2. Toggle **Admin user** to **Enabled** +3. Note the **Login server**, **Username**, and **Password** — you'll need these for GitHub Secrets + +**Azure CLI equivalent:** +```bash +az acr create \ + --resource-group project-pulse-rg \ + --name projectpulseacr \ + --sku Basic \ + --admin-enabled true +``` + +--- + +## 3. Create Azure Database for MySQL Flexible Server + +**Azure Portal:** +1. Search **Azure Database for MySQL flexible servers** → **Create** +2. Resource group: `project-pulse-rg` +3. Server name: `projectpulse-db` *(globally unique)* +4. Region: same as resource group +5. MySQL version: **8.0** +6. Workload type: **Development** (selects Burstable B1ms — cheapest tier) +7. Admin username: choose one (e.g., `pulseadmin`) — **save this** +8. Password: choose a strong password — **save this** +9. **Networking tab**: set Connectivity method to **Public access** + - Check **Allow public access from any Azure service** *(needed for App Service connection)* + - Optionally add your own IP for local access +10. Click **Review + create** → **Create** (takes ~3 minutes) + +**Create the database:** +After the server is provisioned, go to the server → **Databases** → **Add**: +- Database name: `projectpulse` +- Click **Save** + +**Note the JDBC URL** for Key Vault — it will be: +``` +jdbc:mysql://projectpulse-db.mysql.database.azure.com:3306/projectpulse?useSSL=true&requireSSL=false&serverTimezone=UTC +``` + +**Azure CLI equivalent:** +```bash +az mysql flexible-server create \ + --resource-group project-pulse-rg \ + --name projectpulse-db \ + --admin-user pulseadmin \ + --admin-password "" \ + --sku-name Standard_B1ms \ + --tier Burstable \ + --version 8.0 \ + --public-access 0.0.0.0 + +az mysql flexible-server db create \ + --resource-group project-pulse-rg \ + --server-name projectpulse-db \ + --database-name projectpulse +``` + +--- + +## 4. Create Azure Key Vault + +Key Vault stores database credentials that the backend App Service reads at runtime via Managed Identity — no secrets in code or environment variables. + +**Azure Portal:** +1. Search **Key vaults** → **Create** +2. Resource group: `project-pulse-rg` +3. Key vault name: `projectpulse-kv` *(globally unique)* +4. Region: same as resource group +5. Pricing tier: **Standard** +6. **Access configuration tab**: Permission model → **Vault access policy** *(simpler for App Service)* +7. Click **Review + create** → **Create** + +**Add secrets** (Key Vault → **Objects → Secrets → Generate/Import**): + +| Secret name | Value | +|---|---| +| `db-url` | `jdbc:mysql://projectpulse-db.mysql.database.azure.com:3306/projectpulse?useSSL=true&requireSSL=false&serverTimezone=UTC` | +| `db-username` | your MySQL admin username (e.g., `pulseadmin`) | +| `db-password` | your MySQL admin password | +| `cors-allowed-origin` | `https://projectpulse-frontend.azurewebsites.net` *(fill in after frontend app is created)* | + +For each secret: +1. **Upload options**: Manual +2. **Name**: exact name from table above +3. **Value**: the value +4. Click **Create** + +After creating each secret, click it → click the current version → copy the **Secret Identifier URI** (format: `https://projectpulse-kv.vault.azure.net/secrets//`). You'll need these for App Service config references. + +**Azure CLI equivalent:** +```bash +az keyvault create \ + --resource-group project-pulse-rg \ + --name projectpulse-kv \ + --location eastus + +az keyvault secret set --vault-name projectpulse-kv --name db-url \ + --value "jdbc:mysql://projectpulse-db.mysql.database.azure.com:3306/projectpulse?useSSL=true&requireSSL=false&serverTimezone=UTC" + +az keyvault secret set --vault-name projectpulse-kv --name db-username --value "pulseadmin" +az keyvault secret set --vault-name projectpulse-kv --name db-password --value "" +az keyvault secret set --vault-name projectpulse-kv --name cors-allowed-origin \ + --value "https://projectpulse-frontend.azurewebsites.net" +``` + +--- + +## 5. Create an App Service Plan + +Both web apps share one Linux plan. + +**Azure Portal:** +1. Search **App Service plans** → **Create** +2. Resource group: `project-pulse-rg` +3. Name: `projectpulse-plan` +4. Operating system: **Linux** +5. Region: same as resource group +6. Pricing plan: **Basic B1** *(cheapest plan that supports custom domains and always-on)* +7. Click **Review + create** → **Create** + +**Azure CLI equivalent:** +```bash +az appservice plan create \ + --resource-group project-pulse-rg \ + --name projectpulse-plan \ + --is-linux \ + --sku B1 +``` + +--- + +## 6. Create the Backend Web App + +**Azure Portal:** +1. Search **App Services** → **Create** → **Web App** +2. Resource group: `project-pulse-rg` +3. Name: `projectpulse-backend` *(becomes the subdomain — must be globally unique)* +4. Publish: **Container** +5. Operating system: **Linux** +6. Region: same as resource group +7. App Service Plan: `projectpulse-plan` +8. **Container tab**: + - Image source: **Azure Container Registry** + - Registry: `projectpulseacr` + - Image: `projectpulse-backend` + - Tag: `latest` +9. Click **Review + create** → **Create** + +### 6a. Enable System-Assigned Managed Identity + +The backend uses Managed Identity to authenticate to Key Vault without storing credentials. + +1. Open `projectpulse-backend` App Service +2. **Settings → Identity** +3. **System assigned** tab → toggle Status to **On** → **Save** → confirm +4. Copy the **Object (principal) ID** that appears + +### 6b. Grant Key Vault Access to the Backend + +1. Open `projectpulse-kv` Key Vault +2. **Access policies** → **Create** +3. **Permissions tab**: under Secret permissions, check **Get** and **List** +4. **Principal tab**: search for `projectpulse-backend` (the App Service identity) +5. Click **Next** → **Next** → **Create** + +**Azure CLI equivalent:** +```bash +# Get the identity principal ID +PRINCIPAL_ID=$(az webapp identity show \ + --resource-group project-pulse-rg \ + --name projectpulse-backend \ + --query principalId -o tsv) + +az keyvault set-policy \ + --name projectpulse-kv \ + --object-id $PRINCIPAL_ID \ + --secret-permissions get list +``` + +### 6c. Configure App Settings (Key Vault References) + +Key Vault references let App Service inject secrets as environment variables at runtime. + +1. Open `projectpulse-backend` App Service → **Settings → Environment variables** +2. Add the following **Application settings** (click **Add** for each): + +| Name | Value | +|---|---| +| `SPRING_PROFILES_ACTIVE` | `prod` | +| `SPRING_DATASOURCE_URL` | `@Microsoft.KeyVault(SecretUri=https://projectpulse-kv.vault.azure.net/secrets/db-url//)` | +| `SPRING_DATASOURCE_USERNAME` | `@Microsoft.KeyVault(SecretUri=https://projectpulse-kv.vault.azure.net/secrets/db-username//)` | +| `SPRING_DATASOURCE_PASSWORD` | `@Microsoft.KeyVault(SecretUri=https://projectpulse-kv.vault.azure.net/secrets/db-password//)` | +| `APP_CORS_ALLOWED_ORIGIN` | `@Microsoft.KeyVault(SecretUri=https://projectpulse-kv.vault.azure.net/secrets/cors-allowed-origin//)` | +| `APP_FRONTEND_BASE_URL` | `https://projectpulse-frontend.azurewebsites.net` | +| `WEBSITES_PORT` | `8080` | + +> **Important**: Replace `` in each URI with the actual version ID copied from Key Vault, or use the versionless URI format: `https://projectpulse-kv.vault.azure.net/secrets/db-url/` (omit the version ID entirely — Azure always resolves to the latest version). + +3. Click **Apply** → **Confirm** + +### 6d. Get the Backend Publish Profile + +GitHub Actions uses this to deploy. + +1. Open `projectpulse-backend` App Service +2. Click **Download publish profile** (top menu bar) +3. Open the downloaded `.PublishSettings` file in a text editor — copy the entire XML content + +You'll add this to GitHub Secrets in step 8. + +--- + +## 7. Create the Frontend Web App + +**Azure Portal:** +1. Search **App Services** → **Create** → **Web App** +2. Resource group: `project-pulse-rg` +3. Name: `projectpulse-frontend` *(globally unique)* +4. Publish: **Container** +5. Operating system: **Linux** +6. Region: same as resource group +7. App Service Plan: `projectpulse-plan` +8. **Container tab**: + - Image source: **Azure Container Registry** + - Registry: `projectpulseacr` + - Image: `projectpulse-frontend` + - Tag: `latest` +9. Click **Review + create** → **Create** + +The frontend has no Key Vault needs — `VITE_API_BASE_URL` is baked into the image at Docker build time via `--build-arg`. + +### 7a. Get the Frontend Publish Profile + +Same as backend: +1. Open `projectpulse-frontend` App Service +2. Click **Download publish profile** +3. Copy the entire XML content of the downloaded file + +--- + +## 8. Configure GitHub Actions Secrets + +Go to your GitHub repository → **Settings → Secrets and variables → Actions → New repository secret**. Add each of the following: + +| Secret name | Value | +|---|---| +| `ACR_LOGIN_SERVER` | `projectpulseacr.azurecr.io` | +| `ACR_USERNAME` | ACR admin username (from ACR → Access keys) | +| `ACR_PASSWORD` | ACR admin password (from ACR → Access keys) | +| `BACKEND_APP_NAME` | `projectpulse-backend` | +| `FRONTEND_APP_NAME` | `projectpulse-frontend` | +| `BACKEND_PUBLISH_PROFILE` | Full XML content from backend publish profile download | +| `FRONTEND_PUBLISH_PROFILE` | Full XML content from frontend publish profile download | +| `BACKEND_URL` | `https://projectpulse-backend.azurewebsites.net` | + +--- + +## 9. How the CI/CD Pipeline Works + +### CI (`ci.yml`) — runs on every pull request to `main` + +| Job | What it does | +|---|---| +| `backend-test` | Checks out code → sets up Java 17 → runs `mvn test` (uses H2 in-memory DB, no Azure needed) | +| `frontend-build` | Checks out code → sets up Node 20 → `npm ci` → `npm run build` with a placeholder backend URL | + +A PR cannot be merged if either job fails. + +### Deploy (`deploy.yml`) — runs on every push to `main` + +| Job | Steps | +|---|---| +| `deploy-backend` | ACR login → `docker build` in `./backend` → push `:sha` and `:latest` tags → `azure/webapps-deploy` pulls the image | +| `deploy-frontend` | ACR login → `docker build --build-arg VITE_API_BASE_URL=...` in `./frontend` → push tags → `azure/webapps-deploy` pulls the image | + +The two jobs run in parallel. Each App Service automatically restarts with the new container after deployment completes (~2–4 minutes total from push to live). + +--- + +## 10. Triggering the First Deployment + +Merge any branch to `main` (or push directly) to trigger the deploy workflow: + +```bash +git checkout main +git merge feature/your-branch +git push origin main +``` + +Monitor progress at: **GitHub repo → Actions → Deploy** + +--- + +## 11. Verification Checklist + +After the deploy workflow goes green, verify each layer: + +- [ ] **Backend health**: `GET https://projectpulse-backend.azurewebsites.net/api/sections` returns a JSON array (proves: app started, DB connected, JPA schema created) +- [ ] **Key Vault resolution**: In App Service → Environment variables, each Key Vault reference shows a green checkmark (not a red warning icon) +- [ ] **Frontend loads**: Open `https://projectpulse-frontend.azurewebsites.net` — login page renders correctly +- [ ] **Login works**: Log in as an instructor or admin — data loads from backend +- [ ] **CORS working**: No CORS errors in browser DevTools when frontend calls backend APIs +- [ ] **Vue Router**: Navigate directly to a deep URL (e.g., `/instructor/reports`) — page loads (not 404), confirming Nginx SPA fallback works + +--- + +## 12. Troubleshooting + +### App Service shows "Application Error" or blank page + +1. Open App Service → **Monitoring → Log stream** to see live container output +2. Or go to **Diagnose and solve problems → Application Logs** + +### Key Vault reference shows red icon in App Settings + +The Managed Identity doesn't have access. Verify: +1. System-assigned identity is enabled on the App Service +2. The Key Vault access policy includes the App Service's principal ID with **Get** and **List** secret permissions +3. The Secret URI in the app setting is correctly formatted + +### CORS errors in browser console + +The `APP_CORS_ALLOWED_ORIGIN` setting must exactly match the frontend origin (no trailing slash): +- Correct: `https://projectpulse-frontend.azurewebsites.net` +- Wrong: `https://projectpulse-frontend.azurewebsites.net/` + +Update the `cors-allowed-origin` secret in Key Vault, then restart the backend App Service. + +### Backend can't connect to MySQL + +1. Check the MySQL server firewall: **MySQL server → Networking** → ensure **Allow public access from any Azure service** is checked +2. Verify the JDBC URL uses the full hostname: `projectpulse-db.mysql.database.azure.com` +3. Check `SPRING_DATASOURCE_USERNAME` format — MySQL Flexible Server requires just the username (e.g., `pulseadmin`), not `pulseadmin@projectpulse-db` +4. Ensure the database `projectpulse` exists on the server + +### GitHub Actions fails at `docker build` + +- Verify `ACR_LOGIN_SERVER`, `ACR_USERNAME`, `ACR_PASSWORD` secrets are set correctly +- Confirm Admin user is enabled on the ACR (ACR → Access keys) + +### Frontend shows old data / stale build + +The Nginx container caches static assets aggressively (1-year cache headers). Hard-refresh (`Ctrl+Shift+R`) or clear browser cache. Vite bundles include content hashes in filenames, so a fresh deployment always serves the new files on first load. + +### Cold start delays + +Free and Basic tier App Services may take 30–60 seconds on the first request after a period of inactivity. This is normal — the container needs to start. Upgrade to Standard tier and enable **Always On** if cold starts are unacceptable. + +--- + +## Resource Summary + +| Resource | Name | Purpose | +|---|---|---| +| Resource Group | `project-pulse-rg` | Container for all resources | +| Container Registry | `projectpulseacr` | Docker image storage | +| MySQL Flexible Server | `projectpulse-db` | Production database | +| Database | `projectpulse` | Application schema | +| Key Vault | `projectpulse-kv` | DB credentials, CORS origin | +| App Service Plan | `projectpulse-plan` | Shared Linux hosting plan | +| App Service (Backend) | `projectpulse-backend` | Spring Boot API container | +| App Service (Frontend) | `projectpulse-frontend` | Nginx + Vue 3 SPA container | diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..da74a57 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,4 @@ +# Copy this file to .env.local and fill in the values for local development. +# In production, VITE_API_BASE_URL is injected at Docker build time via --build-arg. + +VITE_API_BASE_URL=http://localhost:8080 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..4308432 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +ARG VITE_API_BASE_URL +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +RUN npm run build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..ce5f7cd --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 8ed560c..5725ae2 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,7 +1,7 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'http://localhost:8080', + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080', headers: { 'Content-Type': 'application/json' }, withCredentials: true, }) From f3a4b309953d6346b9e4bf75dc78c93f44168628 Mon Sep 17 00:00:00 2001 From: PathNotFound404 Date: Wed, 29 Apr 2026 16:49:49 -0500 Subject: [PATCH 2/3] Fix InstructorService Test Error --- .../projectpulse/instructor/InstructorServiceTest.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/test/java/com/tcu/projectpulse/instructor/InstructorServiceTest.java b/backend/src/test/java/com/tcu/projectpulse/instructor/InstructorServiceTest.java index acf16af..43374c4 100644 --- a/backend/src/test/java/com/tcu/projectpulse/instructor/InstructorServiceTest.java +++ b/backend/src/test/java/com/tcu/projectpulse/instructor/InstructorServiceTest.java @@ -54,8 +54,9 @@ class InstructorServiceTest { private Student student; // Shared date constants — "02-12-2024" is the week_start used across WAR tests - private static final LocalDate WEEK_START = LocalDate.of(2024, 2, 12); - private static final LocalDate WEEK_END = LocalDate.of(2024, 2, 18); + private static final LocalDate WEEK_START = LocalDate.of(2024, 2, 12); + private static final LocalDate WEEK_END = LocalDate.of(2024, 2, 18); + private static final LocalDate RANGE_END = LocalDate.of(2024, 4, 28); private static final String WEEK_START_STR = "02-12-2024"; private static final String WEEK_END_STR = "04-28-2024"; @@ -364,7 +365,7 @@ void generateStudentPeerEvalReport_hasData_returnsRowsGroupedByWeek() { pe.setEngagementInMeetings(9); pe.setPublicComments("Excellent!"); - given(peerEvaluationRepository.findByStudentIdAndWeekRange(1L, WEEK_START_STR, WEEK_END_STR)) + given(peerEvaluationRepository.findByStudentIdAndWeekRange(1L, WEEK_START, RANGE_END)) .willReturn(List.of(pe)); List rows = instructorService @@ -377,7 +378,7 @@ void generateStudentPeerEvalReport_hasData_returnsRowsGroupedByWeek() { @Test void generateStudentPeerEvalReport_noData_returnsEmptyList() { - given(peerEvaluationRepository.findByStudentIdAndWeekRange(1L, WEEK_START_STR, WEEK_END_STR)) + given(peerEvaluationRepository.findByStudentIdAndWeekRange(1L, WEEK_START, RANGE_END)) .willReturn(List.of()); List rows = instructorService From b881afbc4d70fc501a7942a906a7bc1b48fc8d68 Mon Sep 17 00:00:00 2001 From: PathNotFound404 Date: Wed, 29 Apr 2026 17:12:49 -0500 Subject: [PATCH 3/3] Fix case-sensitive import in InstructorRegister.vue Linux CI fails because the import used './instructorService' but the file is 'InstructorService.js' (capital S). Windows is case-insensitive so this worked locally but broke on GitHub Actions. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/features/instructor/InstructorRegister.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/instructor/InstructorRegister.vue b/frontend/src/features/instructor/InstructorRegister.vue index 9899b2a..b68a829 100644 --- a/frontend/src/features/instructor/InstructorRegister.vue +++ b/frontend/src/features/instructor/InstructorRegister.vue @@ -47,7 +47,7 @@