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_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
|
||||
.bashrc
|
||||
_logout
|
||||
env/*.env
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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"]
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
|
@ -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 {
|
||||
|
@ -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
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:
|
||||
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]
|
||||
|
67
nginx.conf
67
nginx.conf
@ -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
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: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
};
|
10
src/App.js
10
src/App.js
@ -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 */}
|
||||
|
@ -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",
|
||||
|
@ -9,10 +9,7 @@
|
||||
"getTuitionForCIPs"
|
||||
],
|
||||
"CareerExplorer": [
|
||||
"resolveCareerTitle",
|
||||
"getEconomicProjections",
|
||||
"getSalaryData",
|
||||
"addCareerToComparison",
|
||||
"openCareerModal"
|
||||
"getSalaryData"
|
||||
]
|
||||
}
|
@ -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'),
|
||||
],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user