Docker fixes and data wiring for prod. Created Staging.
This commit is contained in:
parent
88e6000755
commit
54c07122f5
@ -17,3 +17,4 @@ REACT_APP_API_URL=https://dev1.aptivaai.com/api
|
|||||||
REACT_APP_ENV=production
|
REACT_APP_ENV=production
|
||||||
REACT_APP_OPENAI_API_KEY=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
|
REACT_APP_OPENAI_API_KEY=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
|
||||||
OPENAI_API_KEY=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
|
OPENAI_API_KEY=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
|
||||||
|
GCP_CLOUD_SQL_PASSWORD=q2O}1PU-R:|l57S0
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -20,3 +20,4 @@ yarn-error.log*
|
|||||||
.bash_history
|
.bash_history
|
||||||
.bashrc
|
.bashrc
|
||||||
_logout
|
_logout
|
||||||
|
env/*.env
|
||||||
|
@ -6,4 +6,4 @@ RUN npm ci --omit=dev --ignore-scripts
|
|||||||
COPY . .
|
COPY . .
|
||||||
ENV PORT=5000
|
ENV PORT=5000
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
CMD ["npm","run","server"]
|
CMD ["node","backend/server.js"]
|
||||||
|
@ -3,7 +3,8 @@ FROM --platform=$TARGETPLATFORM node:20-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --omit=dev --ignore-scripts
|
RUN npm ci --omit=dev --ignore-scripts
|
||||||
|
RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV PORT=5001
|
ENV PORT=5001
|
||||||
EXPOSE 5001
|
EXPOSE 5001
|
||||||
CMD ["npm","run","server2"]
|
CMD ["node","backend/server2.js"]
|
||||||
|
@ -10,4 +10,4 @@ RUN npm ci --omit=dev --ignore-scripts
|
|||||||
COPY . .
|
COPY . .
|
||||||
ENV PORT=5002
|
ENV PORT=5002
|
||||||
EXPOSE 5002
|
EXPOSE 5002
|
||||||
CMD ["npm","run","server3"]
|
CMD ["node","backend/server3.js"]
|
||||||
|
@ -22,13 +22,19 @@ const envPath = path.resolve(rootPath, `.env.${env}`);
|
|||||||
dotenv.config({ path: envPath }); // Load .env file
|
dotenv.config({ path: envPath }); // Load .env file
|
||||||
|
|
||||||
// Grab secrets and config from ENV
|
// Grab secrets and config from ENV
|
||||||
const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey';
|
const JWT_SECRET = process.env.JWT_SECRET;
|
||||||
const DB_HOST = process.env.DB_HOST || '127.0.0.1';
|
const DB_HOST = process.env.DB_HOST || '127.0.0.1';
|
||||||
const DB_PORT = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306;
|
const DB_PORT = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306;
|
||||||
const DB_USER = process.env.DB_USER || 'sqluser';
|
const DB_USER = process.env.DB_USER || 'sqluser';
|
||||||
const DB_PASSWORD = process.env.DB_PASSWORD || '';
|
const DB_PASSWORD = process.env.DB_PASSWORD || '';
|
||||||
const DB_NAME = process.env.DB_NAME || 'user_profile_db';
|
const DB_NAME = process.env.DB_NAME || 'user_profile_db';
|
||||||
|
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
console.error('FATAL: JWT_SECRET missing – aborting startup');
|
||||||
|
process.exit(1); // container exits, Docker marks it unhealthy
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Create a MySQL pool for user_profile data
|
// Create a MySQL pool for user_profile data
|
||||||
const pool = mysql.createPool({
|
const pool = mysql.createPool({
|
||||||
host: DB_HOST,
|
host: DB_HOST,
|
||||||
@ -68,6 +74,8 @@ app.use(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content
|
||||||
|
|
||||||
// Enable CORS with dynamic origin checking
|
// Enable CORS with dynamic origin checking
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
@ -553,42 +561,46 @@ app.get('/api/user-profile', (req, res) => {
|
|||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
SALARY_INFO REMAINS IN SQLITE
|
SALARY_INFO REMAINS IN SQLITE
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
app.get('/api/areas', (req, res) => {
|
app.get('/api/areas', (req, res) => {
|
||||||
const { state } = req.query;
|
const { state } = req.query;
|
||||||
if (!state) {
|
if (!state) {
|
||||||
return res.status(400).json({ error: 'State parameter is required' });
|
return res.status(400).json({ error: 'State parameter is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const salaryDbPath = path.resolve(
|
// Use env when present (Docker), fall back for local dev
|
||||||
'/home/jcoakley/aptiva-dev1-app/salary_info.db'
|
const salaryDbPath =
|
||||||
);
|
process.env.SALARY_DB || '/app/data/salary_info.db';
|
||||||
|
|
||||||
const salaryDb = new sqlite3.Database(
|
const salaryDb = new sqlite3.Database(
|
||||||
salaryDbPath,
|
salaryDbPath,
|
||||||
sqlite3.OPEN_READONLY,
|
sqlite3.OPEN_READONLY,
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error connecting to database:', err.message);
|
console.error('DB connect error:', err.message);
|
||||||
return res.status(500).json({ error: 'Failed to connect to database' });
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: 'Failed to connect to database' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = `SELECT DISTINCT AREA_TITLE FROM salary_data WHERE PRIM_STATE = ?`;
|
const query =
|
||||||
|
`SELECT DISTINCT AREA_TITLE
|
||||||
|
FROM salary_data
|
||||||
|
WHERE PRIM_STATE = ?`;
|
||||||
|
|
||||||
salaryDb.all(query, [state], (err, rows) => {
|
salaryDb.all(query, [state], (err, rows) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Error executing query:', err.message);
|
console.error('Query error:', err.message);
|
||||||
return res.status(500).json({ error: 'Failed to fetch areas' });
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: 'Failed to fetch areas' });
|
||||||
}
|
}
|
||||||
const areas = rows.map((row) => row.AREA_TITLE);
|
res.json({ areas: rows.map(r => r.AREA_TITLE) });
|
||||||
res.json({ areas });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
salaryDb.close((err) => {
|
salaryDb.close();
|
||||||
if (err) {
|
|
||||||
console.error('Error closing the salary_info.db:', err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
|
@ -55,9 +55,14 @@ const institutionFilePath = path.resolve(rootPath, 'public', 'Institution_data.j
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 5001;
|
const PORT = process.env.PORT || 5001;
|
||||||
|
|
||||||
|
// at top of backend/server.js (do once per server codebase)
|
||||||
|
app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content
|
||||||
|
|
||||||
/**************************************************
|
/**************************************************
|
||||||
* DB connections
|
* DB connections
|
||||||
**************************************************/
|
**************************************************/
|
||||||
|
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
const initDB = async () => {
|
const initDB = async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -58,7 +58,7 @@ const authenticatePremiumUser = (req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey';
|
const SECRET_KEY = process.env.SECRET_KEY;
|
||||||
const { id } = jwt.verify(token, SECRET_KEY);
|
const { id } = jwt.verify(token, SECRET_KEY);
|
||||||
req.id = id; // store user ID in request
|
req.id = id; // store user ID in request
|
||||||
next();
|
next();
|
||||||
@ -69,6 +69,9 @@ const authenticatePremiumUser = (req, res, next) => {
|
|||||||
|
|
||||||
const pool = db;
|
const pool = db;
|
||||||
|
|
||||||
|
// at top of backend/server.js (do once per server codebase)
|
||||||
|
app.get('/healthz', (req, res) => res.sendStatus(204)); // 204 No Content
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
* applyOps – executes the “milestones” array inside a fenced ```ops block
|
* applyOps – executes the “milestones” array inside a fenced ```ops block
|
||||||
* and returns an array of confirmation strings
|
* and returns an array of confirmation strings
|
||||||
|
41
docker-compose.prod.yml
Normal file
41
docker-compose.prod.yml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
# ─── server1 ───
|
||||||
|
server1:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yml
|
||||||
|
service: server1
|
||||||
|
env_file: [ ./env/prod.env ]
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SALARY_DB=/app/data/salary_info.db # optional, if code checks env
|
||||||
|
volumes:
|
||||||
|
# SQLite wage DB needed by /api/areas
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/salary_info.db:/app/data/salary_info.db:ro
|
||||||
|
# existing React build/public mount
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro
|
||||||
|
|
||||||
|
# ─── server2 ───
|
||||||
|
server2:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yml
|
||||||
|
service: server2
|
||||||
|
env_file: [ ./env/prod.env ]
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SALARY_DB=/home/jcoakley/aptiva-dev1-app/salary_info.db
|
||||||
|
volumes:
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/salary_info.db:/home/jcoakley/aptiva-dev1-app/salary_info.db:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro
|
||||||
|
|
||||||
|
# ─── server3 ───
|
||||||
|
server3:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yml
|
||||||
|
service: server3
|
||||||
|
env_file: [ ./env/prod.env ]
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
volumes:
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db:ro # keep one copy only
|
40
docker-compose.staging.yml
Normal file
40
docker-compose.staging.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
services:
|
||||||
|
# ─── server1 ───
|
||||||
|
server1:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yml
|
||||||
|
service: server1
|
||||||
|
env_file: [ ./env/staging.env ]
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/salary_info.db:/home/jcoakley/aptiva-dev1-app/salary_info.db:ro
|
||||||
|
|
||||||
|
|
||||||
|
# ─── server2 ─── (needs DB + public data)
|
||||||
|
server2:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yml
|
||||||
|
service: server2
|
||||||
|
env_file: [ ./env/staging.env ]
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- SALARY_DB=/home/jcoakley/aptiva-dev1-app/salary_info.db
|
||||||
|
volumes:
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/salary_info.db:/home/jcoakley/aptiva-dev1-app/salary_info.db:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db:ro
|
||||||
|
|
||||||
|
# ─── server3 ─── (needs public data only)
|
||||||
|
server3:
|
||||||
|
extends:
|
||||||
|
file: docker-compose.yml
|
||||||
|
service: server3
|
||||||
|
env_file: [ ./env/staging.env ]
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
volumes:
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/public:/home/jcoakley/aptiva-dev1-app/public:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db:ro
|
||||||
|
- /home/jcoakley/aptiva-dev1-app/user_profile.db:/home/jcoakley/aptiva-dev1-app/user_profile.db:ro
|
@ -1,33 +1,42 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
server:
|
server1:
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server:latest
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
expose: ["5000"]
|
||||||
- "5000:5000"
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:5000/healthz || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
server2:
|
server2:
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:latest
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server2:${TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
expose: ["5001"]
|
||||||
- "5001:5001"
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:5001/healthz || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
server3:
|
server3:
|
||||||
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:latest
|
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server3:${TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
expose: ["5002"]
|
||||||
- "5002:5002"
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:5002/healthz || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.27
|
image: nginx:1.25-alpine
|
||||||
restart: unless-stopped
|
command: ["nginx", "-g", "daemon off;"]
|
||||||
depends_on:
|
|
||||||
- server
|
|
||||||
- server2
|
|
||||||
- server3
|
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./build:/usr/share/nginx/html:ro # React build
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro # overwrite default
|
||||||
|
- /etc/letsencrypt:/etc/letsencrypt:ro # certs
|
||||||
|
- ./empty:/etc/nginx/conf.d # hide default.conf
|
||||||
|
depends_on: [server1, server2, server3]
|
||||||
|
79
nginx.conf
79
nginx.conf
@ -1,13 +1,72 @@
|
|||||||
events {}
|
events {}
|
||||||
http {
|
|
||||||
upstream backend5000 { server server:5000; }
|
|
||||||
upstream backend5001 { server server2:5001; }
|
|
||||||
upstream backend5002 { server server3:5002; }
|
|
||||||
|
|
||||||
server {
|
http {
|
||||||
listen 80;
|
include /etc/nginx/mime.types;
|
||||||
location /api1/ { proxy_pass http://backend5000/; }
|
default_type application/octet-stream;
|
||||||
location /api2/ { proxy_pass http://backend5001/; }
|
upstream backend5000 { server server1:5000; }
|
||||||
location /api3/ { proxy_pass http://backend5002/; }
|
upstream backend5001 { server server2:5001; }
|
||||||
}
|
upstream backend5002 { server server3:5002; }
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name dev1.aptivaai.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name dev1.aptivaai.com;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/dev1.aptivaai.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/dev1.aptivaai.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
|
# ---- server1 (port 5000) ----
|
||||||
|
location /api/register { proxy_pass http://server1:5000/api/register; }
|
||||||
|
location /api/check-username { proxy_pass http://server1:5000/api/check-username; }
|
||||||
|
location /api/signin { proxy_pass http://server1:5000/api/signin; }
|
||||||
|
location /api/login { proxy_pass http://server1:5000/api/login; }
|
||||||
|
location /api/user-profile { proxy_pass http://server1:5000/api/user-profile; }
|
||||||
|
location /api/areas { proxy_pass http://server1:5000/api/areas; }
|
||||||
|
location /api/activate-premium { proxy_pass http://server1:5000/api/activate-premium; }
|
||||||
|
|
||||||
|
# ---- server2 (port 5001) ----
|
||||||
|
location /api/onet/ { proxy_pass http://server2:5001; }
|
||||||
|
location /api/onet/career-description/ { proxy_pass http://server2:5001; }
|
||||||
|
location /api/job-zones { proxy_pass http://server2:5001/api/job-zones; }
|
||||||
|
location /api/salary { proxy_pass http://server2:5001/api/salary; }
|
||||||
|
location /api/cip/ { proxy_pass http://server2:5001/api/cip/; }
|
||||||
|
location /api/tuition/ { proxy_pass http://server2:5001/api/tuition/; }
|
||||||
|
location /api/projections/ { proxy_pass http://server2:5001/api/projections/; }
|
||||||
|
location /api/skills/ { proxy_pass http://server2:5001/api/skills/; }
|
||||||
|
location = /api/ai-risk { proxy_pass http://server2:5001/api/ai-risk; }
|
||||||
|
location /api/ai-risk/ { proxy_pass http://server2:5001/api/ai-risk/; }
|
||||||
|
location /api/chat/ {
|
||||||
|
proxy_pass http://server2:5001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
location ^~ /api/maps/distance { proxy_pass http://server2:5001; }
|
||||||
|
location /api/schools { proxy_pass http://server2:5001/api/schools; }
|
||||||
|
|
||||||
|
# ---- server3 (port 5002) ----
|
||||||
|
location ^~ /api/premium/ { proxy_pass http://server3:5002; }
|
||||||
|
location /api/public/ { proxy_pass http://server3:5002/api/public/; }
|
||||||
|
|
||||||
|
# ---- static React build ----
|
||||||
|
location / {
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg)$ {
|
||||||
|
expires 6M;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 502 503 504 /50x.html;
|
||||||
|
location = /50x.html { root /usr/share/nginx/html; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
89
nginx.conf.bak
Normal file
89
nginx.conf.bak
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
upstream backend5000 { server server1:5000; }
|
||||||
|
upstream backend5001 { server server2:5001; }
|
||||||
|
upstream backend5002 { server server3:5002; }
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name dev1.aptivaai.com;
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name dev1.aptivaai.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/dev1.aptivaai.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/dev1.aptivaai.com/privkey.pem;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256';
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 1h;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log debug;
|
||||||
|
access_log /var/log/nginx/access.log;
|
||||||
|
|
||||||
|
# ---------- server1 (5000) ----------
|
||||||
|
location /api/register { proxy_pass http://server1:5000/api/register; }
|
||||||
|
location /api/check-username { proxy_pass http://server1:5000/api/check-username; }
|
||||||
|
location /api/signin { proxy_pass http://server1:5000/api/signin; }
|
||||||
|
location /api/login { proxy_pass http://server1:5000/api/login; }
|
||||||
|
location /api/user-profile { proxy_pass http://server1:5000/api/user-profile; }
|
||||||
|
location /api/areas { proxy_pass http://server1:5000/api/areas; }
|
||||||
|
location /api/activate-premium{ proxy_pass http://server1:5000/api/activate-premium; }
|
||||||
|
|
||||||
|
# ---------- server2 (5001) ----------
|
||||||
|
location /api/onet/ { proxy_pass http://server2:5001; }
|
||||||
|
location /api/onet/career-description/ { proxy_pass http://server2:5001; }
|
||||||
|
location /api/job-zones { proxy_pass http://server2:5001/api/job-zones; }
|
||||||
|
location /api/salary { proxy_pass http://server2:5001/api/salary; }
|
||||||
|
location /api/cip/ { proxy_pass http://server2:5001/api/cip/; }
|
||||||
|
location /api/tuition/ { proxy_pass http://server2:5001/api/tuition/; }
|
||||||
|
location /api/projections/ { proxy_pass http://server2:5001/api/projections/; }
|
||||||
|
location /api/skills/ { proxy_pass http://server2:5001/api/skills/; }
|
||||||
|
location = /api/ai-risk { proxy_pass http://server2:5001/api/ai-risk; }
|
||||||
|
location /api/ai-risk/ { proxy_pass http://server2:5001/api/ai-risk/; }
|
||||||
|
|
||||||
|
location /api/chat/ {
|
||||||
|
proxy_pass http://server2:5001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /api/maps/distance { proxy_pass http://server2:5001; }
|
||||||
|
location /api/schools { proxy_pass http://server2:5001/api/schools; }
|
||||||
|
|
||||||
|
# ---------- server3 (5002) ----------
|
||||||
|
location ^~ /api/premium/ { proxy_pass http://server3:5002; }
|
||||||
|
location /api/public/ { proxy_pass http://server3:5002/api/public/; }
|
||||||
|
|
||||||
|
# ---------- static React build ----------
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / { try_files $uri /index.html; }
|
||||||
|
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg)$ {
|
||||||
|
expires 6M;
|
||||||
|
access_log off;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 502 503 504 /50x.html;
|
||||||
|
location = /50x.html { root /usr/share/nginx/html; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http {
|
||||||
|
upstream backend5000 { server server1:5000; }
|
||||||
|
upstream backend5001 { server server2:5001; }
|
||||||
|
upstream backend5002 { server server3:5002; }
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
location /api1/ { proxy_pass http://backend5000/; }
|
||||||
|
location /api2/ { proxy_pass http://backend5001/; }
|
||||||
|
location /api3/ { proxy_pass http://backend5002/; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
export default {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
10
src/App.js
10
src/App.js
@ -221,9 +221,17 @@ const uiToolHandlers = useMemo(() => {
|
|||||||
<ChatCtx.Provider value={{ setChatSnapshot,
|
<ChatCtx.Provider value={{ setChatSnapshot,
|
||||||
openSupport: () => { setDrawerPane('support'); setDrawerOpen(true); },
|
openSupport: () => { setDrawerPane('support'); setDrawerOpen(true); },
|
||||||
openRetire : (props) => {
|
openRetire : (props) => {
|
||||||
setRetireProps(props); // { scenario, financialProfile, onScenarioPatch }
|
setRetireProps(props);
|
||||||
setDrawerPane('retire');
|
setDrawerPane('retire');
|
||||||
setDrawerOpen(true);
|
setDrawerOpen(true);
|
||||||
|
|
||||||
|
if (pageContext === 'RetirementPlanner' || pageContext === 'RetirementLanding') {
|
||||||
|
setRetireProps(props);
|
||||||
|
setDrawerPane('retire');
|
||||||
|
setDrawerOpen(true);
|
||||||
|
} else {
|
||||||
|
console.warn('Retirement bot disabled on this page');
|
||||||
|
}
|
||||||
}}}>
|
}}}>
|
||||||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
@ -19,15 +19,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "resolveCareerTitle",
|
|
||||||
"description": "Convert a free-text career label to its canonical SOC record",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": { "title": { "type": "string" } },
|
|
||||||
"required": ["title"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getSchoolsForCIPs",
|
"name": "getSchoolsForCIPs",
|
||||||
"description": "Return a list of schools whose CIP codes match the supplied prefixes in the given state",
|
"description": "Return a list of schools whose CIP codes match the supplied prefixes in the given state",
|
||||||
@ -49,34 +40,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "addCareerToComparison",
|
|
||||||
"description": "Add the career with the given SOC code to the comparison table in Career Explorer",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"socCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full O*NET SOC code, e.g. \"15-2051\""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["socCode"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "openCareerModal",
|
|
||||||
"description": "Open the career-details modal for the given SOC code in Career Explorer",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"socCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Full O*NET SOC code, e.g. \"15-2051\""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["socCode"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getTuitionForCIPs",
|
"name": "getTuitionForCIPs",
|
||||||
"description": "Return in-state / out-state tuition rows for schools matching CIP prefixes in a given state",
|
"description": "Return in-state / out-state tuition rows for schools matching CIP prefixes in a given state",
|
||||||
|
@ -9,10 +9,7 @@
|
|||||||
"getTuitionForCIPs"
|
"getTuitionForCIPs"
|
||||||
],
|
],
|
||||||
"CareerExplorer": [
|
"CareerExplorer": [
|
||||||
"resolveCareerTitle",
|
|
||||||
"getEconomicProjections",
|
"getEconomicProjections",
|
||||||
"getSalaryData",
|
"getSalaryData"
|
||||||
"addCareerToComparison",
|
|
||||||
"openCareerModal"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1,39 +1,46 @@
|
|||||||
// tailwind.config.js
|
// tailwind.config.js
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: [
|
||||||
theme: {
|
'./src/**/*.{js,jsx,ts,tsx}',
|
||||||
extend: {
|
'./src/**/*.css', // let Tailwind parse the @apply lines
|
||||||
keyframes: {
|
'./public/index.html',
|
||||||
"slide-in": { from: { transform: "translateX(100%)" }, to: { transform: "translateX(0)" } },
|
],
|
||||||
},
|
|
||||||
animation: { "slide-in": "slide-in 0.25s ease-out forwards" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
/* brand colours */
|
|
||||||
colors: {
|
colors: {
|
||||||
aptiva: {
|
aptiva: {
|
||||||
DEFAULT : '#0A84FF', // primary blue
|
DEFAULT: '#0A84FF',
|
||||||
dark : '#005FCC',
|
dark : '#005FCC',
|
||||||
light : '#3AA0FF',
|
light : '#3AA0FF',
|
||||||
accent : '#FF7C00', // accent orange
|
accent : '#FF7C00',
|
||||||
gray : '#F7FAFC', // page bg
|
gray : '#F7FAFC',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/* fonts */
|
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||||
mono: ['Fira Code', 'monospace'],
|
mono: ['Fira Code', 'monospace'],
|
||||||
},
|
},
|
||||||
/* radii */
|
borderRadius: { xl: '1rem' },
|
||||||
borderRadius: {
|
keyframes: {
|
||||||
xl: '1rem', // 16 px extra-round corners everywhere
|
'slide-in': {
|
||||||
|
from: { transform: 'translateX(100%)' },
|
||||||
|
to : { transform: 'translateX(0)' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
animation: { 'slide-in': 'slide-in 0.25s ease-out forwards' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// OPTIONAL – only keep if you really call these classes in JSX/HTML
|
||||||
|
safelist: [
|
||||||
|
'bg-aptiva', 'bg-aptiva-dark', 'bg-aptiva-light',
|
||||||
|
'text-aptiva', 'border-aptiva',
|
||||||
|
'hover:bg-aptiva', // delete this line if unused
|
||||||
|
],
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/forms'), // prettier inputs
|
require('@tailwindcss/forms'),
|
||||||
require('@tailwindcss/typography'), // prose-class for AI answers
|
require('@tailwindcss/typography'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user