Docker fixes and data wiring for prod. Created Staging.

This commit is contained in:
Josh 2025-07-10 11:28:42 +00:00
parent 88e6000755
commit 54c07122f5
19 changed files with 353 additions and 119 deletions

View File

@ -17,3 +17,4 @@ REACT_APP_API_URL=https://dev1.aptivaai.com/api
REACT_APP_ENV=production
REACT_APP_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
View File

@ -20,3 +20,4 @@ yarn-error.log*
.bash_history
.bashrc
_logout
env/*.env

View File

@ -6,4 +6,4 @@ RUN npm ci --omit=dev --ignore-scripts
COPY . .
ENV PORT=5000
EXPOSE 5000
CMD ["npm","run","server"]
CMD ["node","backend/server.js"]

View File

@ -3,7 +3,8 @@ FROM --platform=$TARGETPLATFORM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev --ignore-scripts
RUN apt-get update && apt-get install -y sqlite3 && rm -rf /var/lib/apt/lists/*
COPY . .
ENV PORT=5001
EXPOSE 5001
CMD ["npm","run","server2"]
CMD ["node","backend/server2.js"]

View File

@ -10,4 +10,4 @@ RUN npm ci --omit=dev --ignore-scripts
COPY . .
ENV PORT=5002
EXPOSE 5002
CMD ["npm","run","server3"]
CMD ["node","backend/server3.js"]

View File

@ -22,13 +22,19 @@ const envPath = path.resolve(rootPath, `.env.${env}`);
dotenv.config({ path: envPath }); // Load .env file
// 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_PORT = process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306;
const DB_USER = process.env.DB_USER || 'sqluser';
const DB_PASSWORD = process.env.DB_PASSWORD || '';
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
const pool = mysql.createPool({
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
app.use(
cors({
@ -553,42 +561,46 @@ app.get('/api/user-profile', (req, res) => {
/* ------------------------------------------------------------------
SALARY_INFO REMAINS IN SQLITE
------------------------------------------------------------------ */
------------------------------------------------------------------ */
app.get('/api/areas', (req, res) => {
const { state } = req.query;
if (!state) {
return res.status(400).json({ error: 'State parameter is required' });
}
const salaryDbPath = path.resolve(
'/home/jcoakley/aptiva-dev1-app/salary_info.db'
);
// Use env when present (Docker), fall back for local dev
const salaryDbPath =
process.env.SALARY_DB || '/app/data/salary_info.db';
const salaryDb = new sqlite3.Database(
salaryDbPath,
sqlite3.OPEN_READONLY,
(err) => {
if (err) {
console.error('Error connecting to database:', err.message);
return res.status(500).json({ error: 'Failed to connect to database' });
console.error('DB connect error:', err.message);
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) => {
if (err) {
console.error('Error executing query:', err.message);
return res.status(500).json({ error: 'Failed to fetch areas' });
console.error('Query error:', err.message);
return res
.status(500)
.json({ error: 'Failed to fetch areas' });
}
const areas = rows.map((row) => row.AREA_TITLE);
res.json({ areas });
res.json({ areas: rows.map(r => r.AREA_TITLE) });
});
salaryDb.close((err) => {
if (err) {
console.error('Error closing the salary_info.db:', err.message);
}
});
salaryDb.close();
});
/* ------------------------------------------------------------------

View File

@ -55,9 +55,14 @@ const institutionFilePath = path.resolve(rootPath, 'public', 'Institution_data.j
const app = express();
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
**************************************************/
let db;
const initDB = async () => {
try {

View File

@ -58,7 +58,7 @@ const authenticatePremiumUser = (req, res, next) => {
}
try {
const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey';
const SECRET_KEY = process.env.SECRET_KEY;
const { id } = jwt.verify(token, SECRET_KEY);
req.id = id; // store user ID in request
next();
@ -69,6 +69,9 @@ const authenticatePremiumUser = (req, res, next) => {
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
* and returns an array of confirmation strings

41
docker-compose.prod.yml Normal file
View 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

View 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

View File

@ -1,33 +1,42 @@
version: "3.9"
services:
server:
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server:latest
server1:
image: us-central1-docker.pkg.dev/aptivaai-dev/aptiva-repo/server1:${TAG:-latest}
restart: unless-stopped
ports:
- "5000:5000"
expose: ["5000"]
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:5000/healthz || exit 1"]
interval: 30s
timeout: 5s
retries: 3
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
ports:
- "5001:5001"
expose: ["5001"]
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:5001/healthz || exit 1"]
interval: 30s
timeout: 5s
retries: 3
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
ports:
- "5002:5002"
expose: ["5002"]
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:5002/healthz || exit 1"]
interval: 30s
timeout: 5s
retries: 3
nginx:
image: nginx:1.27
restart: unless-stopped
depends_on:
- server
- server2
- server3
image: nginx:1.25-alpine
command: ["nginx", "-g", "daemon off;"]
ports:
- "80:80"
- "443:443"
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]

View File

@ -1,13 +1,72 @@
events {}
http {
upstream backend5000 { server server:5000; }
include /etc/nginx/mime.types;
default_type application/octet-stream;
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/; }
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
View 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/; }
}
}

View File

@ -1,8 +1,6 @@
export default {
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@ -221,9 +221,17 @@ const uiToolHandlers = useMemo(() => {
<ChatCtx.Provider value={{ setChatSnapshot,
openSupport: () => { setDrawerPane('support'); setDrawerOpen(true); },
openRetire : (props) => {
setRetireProps(props); // { scenario, financialProfile, onScenarioPatch }
setRetireProps(props);
setDrawerPane('retire');
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">
{/* Header */}

View File

@ -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",
"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",
"description": "Return in-state / out-state tuition rows for schools matching CIP prefixes in a given state",

View File

@ -9,10 +9,7 @@
"getTuitionForCIPs"
],
"CareerExplorer": [
"resolveCareerTitle",
"getEconomicProjections",
"getSalaryData",
"addCareerToComparison",
"openCareerModal"
"getSalaryData"
]
}

View File

@ -1,39 +1,46 @@
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
content: [
'./src/**/*.{js,jsx,ts,tsx}',
'./src/**/*.css', // let Tailwind parse the @apply lines
'./public/index.html',
],
theme: {
extend: {
keyframes: {
"slide-in": { from: { transform: "translateX(100%)" }, to: { transform: "translateX(0)" } },
},
animation: { "slide-in": "slide-in 0.25s ease-out forwards" },
},
},
theme: {
extend: {
/* brand colours */
colors: {
aptiva: {
DEFAULT : '#0A84FF', // primary blue
DEFAULT: '#0A84FF',
dark : '#005FCC',
light : '#3AA0FF',
accent : '#FF7C00', // accent orange
gray : '#F7FAFC', // page bg
accent : '#FF7C00',
gray : '#F7FAFC',
},
},
/* fonts */
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
},
/* radii */
borderRadius: {
xl: '1rem', // 16 px extra-round corners everywhere
borderRadius: { xl: '1rem' },
keyframes: {
'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: [
require('@tailwindcss/forms'), // prettier inputs
require('@tailwindcss/typography'), // prose-class for AI answers
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
};