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

@ -16,4 +16,5 @@ APTIVA_API_BASE=https://dev1.aptivaai.com/api
REACT_APP_API_URL=https://dev1.aptivaai.com/api 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
View File

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

View File

@ -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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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);
}
});
}); });
/* ------------------------------------------------------------------ /* ------------------------------------------------------------------

View File

@ -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 {

View File

@ -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
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: 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]

View File

@ -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
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: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -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 */}

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", "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",

View File

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

View File

@ -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'),
], ],
}; };