env variables for server3

This commit is contained in:
Josh 2025-06-23 11:45:04 +00:00
parent 24e5b4d0f9
commit 7ddbbe4227
6 changed files with 120 additions and 63 deletions

20
backend/config/env.js Normal file
View File

@ -0,0 +1,20 @@
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// repo root = two levels up from /backend/config
const repoRoot = path.resolve(__dirname, '..', '..');
const env = (process.env.NODE_ENV || 'development').trim();
// Prefer .env.development / .env.production — fall back to plain .env
const fileA = path.join(repoRoot, `.env.${env}`);
const fileB = path.join(repoRoot, '.env');
const chosen = fs.existsSync(fileA) ? fileA : fileB;
dotenv.config({ path: chosen });
console.log(`[env] loaded ${path.basename(chosen)} → DB_HOST=${process.env.DB_HOST}`);

View File

@ -1,4 +1,5 @@
// backend/config/mysqlPool.js // backend/config/mysqlPool.js
import './env.js';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
const pool = mysql.createPool({ const pool = mysql.createPool({
@ -12,4 +13,7 @@ const pool = mysql.createPool({
...(process.env.DB_SOCKET ? { socketPath: process.env.DB_SOCKET } : {}) ...(process.env.DB_SOCKET ? { socketPath: process.env.DB_SOCKET } : {})
}); });
console.log('[mysqlPool] Using config →',
{ host: process.env.DB_HOST, port: process.env.DB_PORT, socket: process.env.DB_SOCKET });
export default pool; export default pool;

View File

@ -1,15 +1,11 @@
// ─── server3.js ──────────────────────────────────────────────────────────── // ─── server3.js ────────────────────────────────────────────────────────────
import './config/env.js';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const env = (process.env.NODE_ENV || 'development').trim();
const envPath = path.resolve(__dirname, '..', `.env.${env}`);
dotenv.config({ path: envPath }); // ✅ envs are now ready
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import helmet from 'helmet'; import helmet from 'helmet';
@ -810,6 +806,8 @@ ${econText}
const { status: userStatus } = scenarioRow; const { status: userStatus } = scenarioRow;
const { career_situation: userSituation } = userProfile; const { career_situation: userSituation } = userProfile;
const careerName = scenarioRow?.career_name || "this career"; const careerName = scenarioRow?.career_name || "this career";
/* How many past exchanges to keep */
const MAX_CHAT_TURNS = 6;
const combinedStatusSituation = buildStatusSituationMessage( const combinedStatusSituation = buildStatusSituationMessage(
userStatus, userStatus,
@ -949,6 +947,17 @@ RESPOND ONLY with valid JSON in this shape:
Otherwise, answer normally. Otherwise, answer normally.
`.trim(); `.trim();
/* ─── date guard ─────────────────────────────────────────────── */
const todayISO = new Date().toISOString().slice(0, 10);
const systemPromptDateGuard = `
📅 DATE GUARD
Every milestone date must be **on or after** ${todayISO}.
If youre asked for short-term dates, they still must be ${todayISO}.
Reject or re-ask if the user insists on a past date.
`.trim();
const avoidBlock = existingTitles.length const avoidBlock = existingTitles.length
? "\nAVOID repeating any of these title|date combinations:\n" + ? "\nAVOID repeating any of these title|date combinations:\n" +
@ -969,8 +978,7 @@ ${systemPromptMilestoneFormat}
${systemPromptDateGuard} ${systemPromptDateGuard}
`.trim(); `.trim();
/* How many past exchanges to keep */
const MAX_CHAT_TURNS = 6;
// Build up the final messages array // Build up the final messages array
const messagesToSend = [ const messagesToSend = [

View File

@ -34,9 +34,9 @@ module.exports = {
DB_PASSWORD : 'ps<g+2DO-eTb2mb5', DB_PASSWORD : 'ps<g+2DO-eTb2mb5',
DB_NAME : 'user_profile_db', DB_NAME : 'user_profile_db',
TWILIO_ACCOUNT_SID : 'AC', TWILIO_ACCOUNT_SID : 'ACd700c6fb9f691ccd9ccab73f2dd4173d',
TWILIO_AUTH_TOKEN : 'fb89', TWILIO_AUTH_TOKEN : 'fb8979ccb172032a249014c9c30eba80',
TWILIO_MESSAGING_SERVICE_SID : 'MG' TWILIO_MESSAGING_SERVICE_SID : 'MGMGaa07992a9231c841b1bfb879649026d6'
} }
} }

View File

@ -360,6 +360,8 @@ export default function CareerRoadmap({ selectedCareer: initialCareer }) {
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
const [focusMid , setFocusMid ] = useState(null); const [focusMid , setFocusMid ] = useState(null);
const [drawerMilestone, setDrawerMilestone] = useState(null); const [drawerMilestone, setDrawerMilestone] = useState(null);
const [impactsById, setImpactsById] = useState({}); // id → [impacts]
// Config // Config
const [simulationYearsInput, setSimulationYearsInput] = useState('20'); const [simulationYearsInput, setSimulationYearsInput] = useState('20');
@ -928,6 +930,14 @@ useEffect(() => {
.map((m, i) => ({ ...m, impacts: impactsForEach[i] || [] })) .map((m, i) => ({ ...m, impacts: impactsForEach[i] || [] }))
.flatMap((m) => m.impacts); .flatMap((m) => m.impacts);
/* NEW build a quick lookup table and expose it */
const map = {};
allImpacts.forEach((imp) => {
(map[imp.milestone_id] = map[imp.milestone_id] || []).push(imp);
});
setImpactsById(map); // <-- saves for the modal
const f = financialProfile; const f = financialProfile;
const financialBase = { const financialBase = {
currentSalary: parseFloatOrZero(f.current_salary, 0), currentSalary: parseFloatOrZero(f.current_salary, 0),
@ -1196,10 +1206,12 @@ const DAILY_CLICK_LIMIT = 10; // example limit per day
const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?'; const buttonLabel = recommendations.length > 0 ? 'New Suggestions' : 'What Should I Do Next?';
const chartRef = useRef(null); const chartRef = useRef(null);
const onEditMilestone = useCallback((m) => { const onEditMilestone = useCallback((m) => {
setMilestoneForModal(m); // open modal setMilestoneForModal({
}, []); ...m,
impacts: impactsById[m.id] || [] // give the modal what it needs
});
}, [impactsById]);
const currentIdRef = useRef(null); const currentIdRef = useRef(null);
@ -1525,10 +1537,11 @@ const fetchMilestones = useCallback(async () => {
{milestoneForModal && ( {milestoneForModal && (
<MilestoneEditModal <MilestoneEditModal
careerProfileId={careerProfileId} // number careerProfileId={careerProfileId} // number
milestones={scenarioMilestones} // array milestones={scenarioMilestones}
milestone={milestoneForModal}
fetchMilestones={fetchMilestones} // helper to refresh list fetchMilestones={fetchMilestones} // helper to refresh list
onClose={(didSave) => { onClose={(didSave) => {
setMilestoneForModal(false); // or setShowMilestoneModal(false) setMilestoneForModal(null); // or setShowMilestoneModal(false)
if (didSave) { if (didSave) {
fetchMilestones(); fetchMilestones();
} }

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Button } from "./ui/button.js"; import { Button } from "./ui/button.js";
import authFetch from "../utils/authFetch.js"; import authFetch from "../utils/authFetch.js";
import parseFloatOrZero from "../utils/ParseFloatorZero.js";
import MilestoneCopyWizard from "./MilestoneCopyWizard.js"; import MilestoneCopyWizard from "./MilestoneCopyWizard.js";
/** /**
@ -18,6 +17,7 @@ import MilestoneCopyWizard from "./MilestoneCopyWizard.js";
export default function MilestoneEditModal({ export default function MilestoneEditModal({
careerProfileId, careerProfileId,
milestones: incomingMils = [], milestones: incomingMils = [],
milestone: selectedMilestone,
fetchMilestones, fetchMilestones,
onClose onClose
}) { }) {
@ -39,62 +39,74 @@ export default function MilestoneEditModal({
}); });
const [copyWizardMilestone, setCopyWizardMilestone] = useState(null); const [copyWizardMilestone, setCopyWizardMilestone] = useState(null);
function toSqlDate(str = '') { function toSqlDate(val) {
// Handles '', null, undefined gracefully if (!val) return ''; // null | undefined | '' | 0
return str.slice(0, 10); // "YYYY-MM-DD" return String(val).slice(0, 10);
} }
/* keep milestones in sync with prop */ /* keep milestones in sync with prop */
useEffect(() => { useEffect(() => {
setMilestones(incomingMils); setMilestones(incomingMils);
}, [incomingMils]); }, [incomingMils]);
/* /*
Inline-edit helpers Inline-edit helpers
*/ */
const [originalImpactIdsMap, setOriginalImpactIdsMap] = useState({}); // snapshot per milestone
/* 1⃣ fetch impacts + open editor */ // 1⃣ fetch impacts + open editor ── moved **up** so the next effect
const loadMilestoneImpacts = useCallback(async (m) => { // can safely reference it in its dependency array
try { const loadMilestoneImpacts = useCallback(async (m) => {
const res = await authFetch(`/api/premium/milestone-impacts?milestone_id=${m.id}`); try {
if (!res.ok) throw new Error('impact fetch failed'); const res = await authFetch(
const json = await res.json(); `/api/premium/milestone-impacts?milestone_id=${m.id}`
);
if (!res.ok) throw new Error('impact fetch failed');
const json = await res.json();
const impacts = (json.impacts || []).map(imp => ({ const impacts = (json.impacts || []).map(imp => ({
id : imp.id, id : imp.id,
impact_type : imp.impact_type || 'ONE_TIME', impact_type : imp.impact_type || 'ONE_TIME',
direction : imp.direction || 'subtract', direction : imp.direction || 'subtract',
amount : imp.amount || 0, amount : imp.amount || 0,
start_date : toSqlDate(imp.start_date) || '', start_date : toSqlDate(imp.start_date) || '',
end_date : toSqlDate(imp.end_date) || '' end_date : toSqlDate(imp.end_date) || ''
})); }));
/* editable copy for the form */ // editable copy for the form
setNewMilestoneMap(prev => ({ setNewMilestoneMap(prev => ({
...prev, ...prev,
[m.id]: { [m.id]: {
title : m.title || '', title : m.title || '',
description : m.description || '', description : m.description || '',
date : toSqlDate(m.date) || '', date : toSqlDate(m.date) || '',
progress : m.progress || 0, progress : m.progress || 0,
newSalary : m.new_salary || '', newSalary : m.new_salary || '',
impacts, impacts,
isUniversal : m.is_universal ? 1 : 0 isUniversal : m.is_universal ? 1 : 0
} }
})); }));
/* snapshot the IDs that existed when editing started */ // snapshot of original impact IDs
setOriginalImpactIdsMap(prev => ({ setOriginalImpactIdsMap(prev => ({
...prev, ...prev,
[m.id]: impacts.map(i => i.id) // array of strings [m.id]: impacts.map(i => i.id)
})); }));
setEditingMilestoneId(m.id); // open accordion
} catch (err) {
console.error('loadImpacts', err);
}
}, []); // ← useCallback deps (none)
// NOW the effect that calls it; declared **after** the callback
useEffect(() => {
if (selectedMilestone) {
loadMilestoneImpacts(selectedMilestone);
}
}, [selectedMilestone, loadMilestoneImpacts]);
const [originalImpactIdsMap, setOriginalImpactIdsMap] = useState({});
setEditingMilestoneId(m.id); // open the accordion
} catch (err) {
console.error('loadImpacts', err);
}
}, []);
/* 2⃣ toggle open / close */ /* 2⃣ toggle open / close */
const handleEditMilestoneInline = (milestone) => { const handleEditMilestoneInline = (milestone) => {
@ -360,7 +372,7 @@ const saveNewMilestone = async () => {
</div> </div>
<p>{m.description}</p> <p>{m.description}</p>
<p> <p>
<strong>Date:</strong> {m.date} <strong>Date:</strong> {toSqlDate(m.date)}
</p> </p>
<p>Progress: {m.progress}%</p> <p>Progress: {m.progress}%</p>