Compare commits
10 Commits
586282d7fa
...
4edf11a62d
Author | SHA1 | Date | |
---|---|---|---|
4edf11a62d | |||
dba497c854 | |||
afd8c1df21 | |||
3a5b0f86fb | |||
5b33d7fa5f | |||
5750577eaf | |||
6845ed7c62 | |||
d69ef95a41 | |||
a5f16494fc | |||
30d7035946 |
@ -231,7 +231,7 @@ RUN { \
|
|||||||
echo ' while true; do'; \
|
echo ' while true; do'; \
|
||||||
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
|
echo ' if [[ ! -e /opt/backstory/block-server ]]; then'; \
|
||||||
echo ' echo "Launching Backstory server..."'; \
|
echo ' echo "Launching Backstory server..."'; \
|
||||||
echo ' python src/server.py "${@}" || echo "Backstory server died."'; \
|
echo ' python src/backend/main.py "${@}" || echo "Backstory server died."'; \
|
||||||
echo ' echo "Sleeping for 3 seconds."'; \
|
echo ' echo "Sleeping for 3 seconds."'; \
|
||||||
echo ' else'; \
|
echo ' else'; \
|
||||||
echo ' if [[ ${once} -eq 0 ]]; then' ; \
|
echo ' if [[ ${once} -eq 0 ]]; then' ; \
|
||||||
|
@ -11,6 +11,7 @@ services:
|
|||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- PRODUCTION=0
|
- PRODUCTION=0
|
||||||
|
- FRONTEND_URL=https://backstory-beta.ketrenos.com
|
||||||
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
- REDIS_DB=0
|
- REDIS_DB=0
|
||||||
@ -50,6 +51,7 @@ services:
|
|||||||
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
- MODEL_NAME=${MODEL_NAME:-qwen2.5:7b}
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
- REDIS_DB=1
|
- REDIS_DB=1
|
||||||
|
- SSL_ENABLED=false
|
||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
depends_on:
|
depends_on:
|
||||||
|
9
frontend/package-lock.json
generated
9
frontend/package-lock.json
generated
@ -45,6 +45,7 @@
|
|||||||
"react-router-dom": "^7.6.0",
|
"react-router-dom": "^7.6.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-spinners": "^0.15.0",
|
"react-spinners": "^0.15.0",
|
||||||
|
"react-to-print": "^3.1.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
@ -20206,6 +20207,14 @@
|
|||||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-to-print": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-hiJZVmJtaRm9EHoUTG2bordyeRxVSGy9oFVV7fSvzOWwctPp6jbz2R6NFkaokaTYBxC7wTM/fMV5eCXsNpEwsA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"react-router-dom": "^7.6.0",
|
"react-router-dom": "^7.6.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-spinners": "^0.15.0",
|
"react-spinners": "^0.15.0",
|
||||||
|
"react-to-print": "^3.1.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
@ -82,9 +82,12 @@ const BackstoryTextField = React.forwardRef<BackstoryTextFieldRef, BackstoryText
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (!onEnter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === 'Enter' && !event.shiftKey) {
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
event.preventDefault(); // Prevent newline
|
event.preventDefault(); // Prevent newline
|
||||||
onEnter && onEnter(editValue);
|
onEnter(editValue);
|
||||||
setEditValue(''); // Clear textarea
|
setEditValue(''); // Clear textarea
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -502,7 +502,6 @@ const JobCreator = (props: JobCreatorProps) => {
|
|||||||
flexShrink: 0, /* Prevent shrinking */
|
flexShrink: 0, /* Prevent shrinking */
|
||||||
},
|
},
|
||||||
position: "relative",
|
position: "relative",
|
||||||
border: "3px solid purple",
|
|
||||||
}}>
|
}}>
|
||||||
<Scrollable sx={{ display: "flex", flexGrow: 1, position: "relative", maxHeight: "30rem" }}><JobInfo job={job} /></Scrollable>
|
<Scrollable sx={{ display: "flex", flexGrow: 1, position: "relative", maxHeight: "30rem" }}><JobInfo job={job} /></Scrollable>
|
||||||
<Scrollable sx={{ display: "flex", flexGrow: 1, position: "relative", maxHeight: "30rem" }}><StyledMarkdown content={job.description} /></Scrollable>
|
<Scrollable sx={{ display: "flex", flexGrow: 1, position: "relative", maxHeight: "30rem" }}><StyledMarkdown content={job.description} /></Scrollable>
|
||||||
|
@ -314,11 +314,10 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
|||||||
}
|
}
|
||||||
<Typography variant="caption">Job ID: {job.id}</Typography>
|
<Typography variant="caption">Job ID: {job.id}</Typography>
|
||||||
</>}
|
</>}
|
||||||
{variant === 'all' && <StyledMarkdown content={activeJob.description} />}
|
{variant === 'all' && <StyledMarkdown sx={{ display: "flex" }} content={activeJob.description} />}
|
||||||
|
|
||||||
{(variant !== 'small' && variant !== 'minimal') && <><Divider />{renderJobRequirements()}</>}
|
{(variant !== 'small' && variant !== 'minimal') && <Box><Divider />{renderJobRequirements()}</Box>}
|
||||||
|
|
||||||
</Box >
|
|
||||||
{isAdmin &&
|
{isAdmin &&
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", p: 1 }}>
|
||||||
<Box sx={{ display: "flex", flexDirection: "row", pl: 1, pr: 1, gap: 1, alignContent: "center", height: "32px" }}>
|
<Box sx={{ display: "flex", flexDirection: "row", pl: 1, pr: 1, gap: 1, alignContent: "center", height: "32px" }}>
|
||||||
@ -371,6 +370,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
</Box >
|
</Box >
|
||||||
|
</Box >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -83,6 +83,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
|
|||||||
if (job) {
|
if (job) {
|
||||||
setSelectedJob(job);
|
setSelectedJob(job);
|
||||||
onSelect?.(job);
|
onSelect?.(job);
|
||||||
|
setMobileDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -147,11 +148,8 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
|
|||||||
const handleJobSelect = (job: Job) => {
|
const handleJobSelect = (job: Job) => {
|
||||||
setSelectedJob(job);
|
setSelectedJob(job);
|
||||||
onSelect?.(job);
|
onSelect?.(job);
|
||||||
if (isMobile) {
|
setMobileDialogOpen(true);
|
||||||
setMobileDialogOpen(true);
|
navigate(`/candidate/jobs/${job.id}`);
|
||||||
} else {
|
|
||||||
navigate(`/candidate/jobs/${job.id}`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMobileDialogClose = () => {
|
const handleMobileDialogClose = () => {
|
||||||
@ -181,11 +179,9 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
...(isMobile ? {
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent'
|
||||||
} : { width: '50%' })
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
@ -452,87 +448,54 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<Box sx={{
|
|
||||||
height: '100%',
|
|
||||||
p: 0.5,
|
|
||||||
backgroundColor: 'background.default'
|
|
||||||
}}>
|
|
||||||
<JobList />
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
fullScreen
|
|
||||||
open={mobileDialogOpen}
|
|
||||||
onClose={handleMobileDialogClose}
|
|
||||||
TransitionComponent={Transition}
|
|
||||||
TransitionProps={{ timeout: 300 }}
|
|
||||||
>
|
|
||||||
<AppBar sx={{ position: 'relative', elevation: 1 }}>
|
|
||||||
<Toolbar variant="dense" sx={{ minHeight: 48 }}>
|
|
||||||
<IconButton
|
|
||||||
edge="start"
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleMobileDialogClose}
|
|
||||||
aria-label="back"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<ArrowBackIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Box sx={{ ml: 1, flex: 1, minWidth: 0 }}>
|
|
||||||
<Typography
|
|
||||||
variant="h6"
|
|
||||||
component="div"
|
|
||||||
noWrap
|
|
||||||
sx={{ fontSize: '1rem' }}
|
|
||||||
>
|
|
||||||
{selectedJob?.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
component="div"
|
|
||||||
sx={{ color: 'rgba(255, 255, 255, 0.7)' }}
|
|
||||||
noWrap
|
|
||||||
>
|
|
||||||
{selectedJob?.company}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
<JobDetails inDialog />
|
|
||||||
</Dialog>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
|
||||||
height: '100%',
|
height: '100%',
|
||||||
gap: 0.75,
|
p: 0.5,
|
||||||
p: 0.75,
|
|
||||||
backgroundColor: 'background.default'
|
backgroundColor: 'background.default'
|
||||||
}}>
|
}}>
|
||||||
<JobList />
|
<JobList />
|
||||||
|
|
||||||
<Paper sx={{
|
<Dialog
|
||||||
width: '50%',
|
fullScreen
|
||||||
display: 'flex',
|
open={mobileDialogOpen}
|
||||||
flexDirection: 'column',
|
onClose={handleMobileDialogClose}
|
||||||
elevation: 1
|
TransitionComponent={Transition}
|
||||||
}}>
|
TransitionProps={{ timeout: 300 }}
|
||||||
<Box sx={{
|
>
|
||||||
p: 0.75,
|
<AppBar sx={{ position: 'relative', elevation: 1 }}>
|
||||||
borderBottom: 1,
|
<Toolbar variant="dense" sx={{ minHeight: 48 }}>
|
||||||
borderColor: 'divider',
|
<IconButton
|
||||||
backgroundColor: 'background.paper'
|
edge="start"
|
||||||
}}>
|
color="inherit"
|
||||||
<Typography variant="h6" sx={{ fontSize: '1.1rem', fontWeight: 600 }}>
|
onClick={handleMobileDialogClose}
|
||||||
Job Details
|
aria-label="back"
|
||||||
</Typography>
|
size="small"
|
||||||
</Box>
|
>
|
||||||
<JobDetails />
|
<ArrowBackIcon />
|
||||||
</Paper>
|
</IconButton>
|
||||||
|
<Box sx={{ ml: 1, flex: 1, minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
component="div"
|
||||||
|
noWrap
|
||||||
|
sx={{ fontSize: '1rem' }}
|
||||||
|
>
|
||||||
|
{selectedJob?.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
component="div"
|
||||||
|
sx={{ color: 'rgba(255, 255, 255, 0.7)' }}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{selectedJob?.company}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
<JobDetails inDialog />
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Tab
|
Tab
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import PrintIcon from '@mui/icons-material/Print';
|
||||||
import {
|
import {
|
||||||
Delete as DeleteIcon,
|
Delete as DeleteIcon,
|
||||||
Restore as RestoreIcon,
|
Restore as RestoreIcon,
|
||||||
@ -41,11 +42,15 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import PreviewIcon from '@mui/icons-material/Preview';
|
import PreviewIcon from '@mui/icons-material/Preview';
|
||||||
import EditDocumentIcon from '@mui/icons-material/EditDocument';
|
import EditDocumentIcon from '@mui/icons-material/EditDocument';
|
||||||
|
|
||||||
|
import { useReactToPrint } from "react-to-print";
|
||||||
|
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useAppState } from 'hooks/GlobalContext';
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
import { StyledMarkdown } from 'components/StyledMarkdown';
|
import { StyledMarkdown } from 'components/StyledMarkdown';
|
||||||
import { Resume } from 'types/types';
|
import { Resume } from 'types/types';
|
||||||
import { BackstoryTextField } from 'components/BackstoryTextField';
|
import { BackstoryTextField } from 'components/BackstoryTextField';
|
||||||
|
import { JobInfo } from './JobInfo';
|
||||||
|
|
||||||
interface ResumeInfoProps {
|
interface ResumeInfoProps {
|
||||||
resume: Resume;
|
resume: Resume;
|
||||||
@ -73,10 +78,13 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
|
const [shouldShowMoreButton, setShouldShowMoreButton] = useState(false);
|
||||||
const [deleted, setDeleted] = useState<boolean>(false);
|
const [deleted, setDeleted] = useState<boolean>(false);
|
||||||
const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
|
const [editDialogOpen, setEditDialogOpen] = useState<boolean>(false);
|
||||||
|
const [printDialogOpen, setPrintDialogOpen] = useState<boolean>(false);
|
||||||
const [editContent, setEditContent] = useState<string>('');
|
const [editContent, setEditContent] = useState<string>('');
|
||||||
const [saving, setSaving] = useState<boolean>(false);
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const [tabValue, setTabValue] = useState("markdown");
|
const [tabValue, setTabValue] = useState("markdown");
|
||||||
|
const printContentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const reactToPrintFn = useReactToPrint({ contentRef: printContentRef, pageStyle: '@page { margin: 10px; }' });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resume && resume.id !== activeResume?.id) {
|
if (resume && resume.id !== activeResume?.id) {
|
||||||
@ -92,10 +100,10 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
}
|
}
|
||||||
}, [resume.resume]);
|
}, [resume.resume]);
|
||||||
|
|
||||||
const deleteResume = async (resumeId: string | undefined) => {
|
const deleteResume = async (id: string | undefined) => {
|
||||||
if (resumeId) {
|
if (id) {
|
||||||
try {
|
try {
|
||||||
await apiClient.deleteResume(resumeId);
|
await apiClient.deleteResume(id);
|
||||||
setDeleted(true);
|
setDeleted(true);
|
||||||
setSnack('Resume deleted successfully.');
|
setSnack('Resume deleted successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -113,8 +121,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
try {
|
try {
|
||||||
const result = await apiClient.updateResume(activeResume.id || '', editContent);
|
const result = await apiClient.updateResume(activeResume.id || '', editContent);
|
||||||
const updatedResume = { ...activeResume, resume: editContent, updatedAt: new Date() };
|
const updatedResume = { ...activeResume, resume: editContent, updatedAt: new Date() };
|
||||||
setActiveResume(updatedResume);
|
setActiveResume(updatedResume);
|
||||||
setEditDialogOpen(false);
|
|
||||||
setSnack('Resume updated successfully.');
|
setSnack('Resume updated successfully.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSnack('Failed to update resume.');
|
setSnack('Failed to update resume.');
|
||||||
@ -144,6 +151,10 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
|
if (newValue === "print") {
|
||||||
|
reactToPrintFn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
setTabValue(newValue);
|
setTabValue(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -218,7 +229,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
Updated: {formatDate(activeResume.updatedAt)}
|
Updated: {formatDate(activeResume.updatedAt)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Resume ID: {activeResume.resumeId}
|
Resume ID: {activeResume.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -338,26 +349,57 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Print Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={printDialogOpen}
|
||||||
|
onClose={() => { }}//setPrintDialogOpen(false)}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
fullScreen={true}
|
||||||
|
>
|
||||||
|
<StyledMarkdown
|
||||||
|
content={activeResume.resume}
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
position: "relative",
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
flex: 1, /* Take remaining space in some-container */
|
||||||
|
overflowY: "auto", /* Scroll if content overflows */
|
||||||
|
}} />
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Edit Dialog */}
|
{/* Edit Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
open={editDialogOpen}
|
open={editDialogOpen}
|
||||||
onClose={() => setEditDialogOpen(false)}
|
onClose={() => setEditDialogOpen(false)}
|
||||||
maxWidth="lg"
|
maxWidth="lg"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
disableEscapeKeyDown={true}
|
||||||
fullScreen={true}
|
fullScreen={true}
|
||||||
>
|
>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Edit Resume Content
|
Edit Resume Content
|
||||||
<Typography variant="caption" display="block" color="text.secondary">
|
<Typography variant="caption" display="block" color="text.secondary">
|
||||||
Resume for {activeResume.candidate?.fullName || activeResume.candidateId}
|
Resume for {activeResume.candidate?.fullName || activeResume.candidateId}, {activeResume.job?.title || 'No Job Title Assigned'}, {activeResume.job?.company || 'No Company Assigned'}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography variant="caption" display="block" color="text.secondary">
|
||||||
|
Resume ID: # {activeResume.id}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" display="block" color="text.secondary">
|
||||||
|
Last saved: {activeResume.updatedAt ? new Date(activeResume.updatedAt).toLocaleString() : 'N/A'}
|
||||||
|
</Typography>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent sx={{ position: "relative", display: "flex", flexDirection: "column", height: "100%" }}>
|
<DialogContent sx={{ position: "relative", display: "flex", flexDirection: "column", height: "100%" }}>
|
||||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||||
<Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
|
<Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
|
||||||
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
|
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
|
||||||
|
<Tab value="job" icon={<WorkIcon />} label="Job" />
|
||||||
|
<Tab value="print" icon={<PrintIcon />} label="Print" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Box sx={{
|
<Box ref={printContentRef} sx={{
|
||||||
display: "flex", flexDirection: "column",
|
display: "flex", flexDirection: "column",
|
||||||
height: "100%", /* Restrict to main-container's height */
|
height: "100%", /* Restrict to main-container's height */
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -367,7 +409,6 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
flexShrink: 0, /* Prevent shrinking */
|
flexShrink: 0, /* Prevent shrinking */
|
||||||
},
|
},
|
||||||
position: "relative",
|
position: "relative",
|
||||||
border: "2px solid purple",
|
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
{tabValue === "markdown" &&
|
{tabValue === "markdown" &&
|
||||||
@ -405,6 +446,20 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
content={editContent} />
|
content={editContent} />
|
||||||
<Box sx={{ pb: 2 }}></Box></>
|
<Box sx={{ pb: 2 }}></Box></>
|
||||||
}
|
}
|
||||||
|
{tabValue === "job" && activeResume.job && <JobInfo
|
||||||
|
variant="all"
|
||||||
|
job={activeResume.job}
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
position: "relative",
|
||||||
|
maxHeight: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexGrow: 1,
|
||||||
|
flex: 1, /* Take remaining space in some-container */
|
||||||
|
overflowY: "auto", /* Scroll if content overflows */
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
@ -133,7 +133,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
|
|||||||
resume.job?.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
resume.job?.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
resume.job?.company?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
resume.job?.company?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
resume.resume?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
resume.resume?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
resume.resumeId?.toLowerCase().includes(searchQuery.toLowerCase())
|
resume.id?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
setFilteredResumes(filtered);
|
setFilteredResumes(filtered);
|
||||||
}
|
}
|
||||||
@ -464,7 +464,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
|
|||||||
noWrap
|
noWrap
|
||||||
sx={{ fontSize: isMobile ? '0.65rem' : '0.7rem' }}
|
sx={{ fontSize: isMobile ? '0.65rem' : '0.7rem' }}
|
||||||
>
|
>
|
||||||
{resume.resumeId}
|
{resume.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
BubbleChart,
|
BubbleChart,
|
||||||
AutoFixHigh,
|
AutoFixHigh,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import EditDocumentIcon from '@mui/icons-material/EditDocument';
|
||||||
|
|
||||||
import { BackstoryLogo } from "components/ui/BackstoryLogo";
|
import { BackstoryLogo } from "components/ui/BackstoryLogo";
|
||||||
import { HomePage } from "pages/HomePage";
|
import { HomePage } from "pages/HomePage";
|
||||||
@ -131,12 +132,12 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
component: <CandidateChatPage />,
|
component: <CandidateChatPage />,
|
||||||
userTypes: ["guest", "candidate", "employer"],
|
userTypes: ["guest", "candidate", "employer"],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: "explore",
|
// id: "explore",
|
||||||
label: "Explore",
|
// label: "Explore",
|
||||||
icon: <SearchIcon />,
|
// icon: <SearchIcon />,
|
||||||
userTypes: ["candidate", "guest", "employer"],
|
// userTypes: ["candidate", "guest", "employer"],
|
||||||
children: [
|
// children: [
|
||||||
// {
|
// {
|
||||||
// id: "explore-candidates",
|
// id: "explore-candidates",
|
||||||
// label: "Candidates",
|
// label: "Candidates",
|
||||||
@ -147,29 +148,9 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
// ),
|
// ),
|
||||||
// userTypes: ["candidate", "guest", "employer"],
|
// userTypes: ["candidate", "guest", "employer"],
|
||||||
// },
|
// },
|
||||||
{
|
// ],
|
||||||
id: "explore-jobs",
|
// showInNavigation: true,
|
||||||
label: "Jobs",
|
// },
|
||||||
path: "/candidate/jobs/:jobId?",
|
|
||||||
icon: <SearchIcon />,
|
|
||||||
component: (
|
|
||||||
<JobViewer />
|
|
||||||
),
|
|
||||||
userTypes: ["candidate", "guest", "employer"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "explore-resumes",
|
|
||||||
label: "Resumes",
|
|
||||||
path: "/candidate/resumes/:resumeId?",
|
|
||||||
icon: <SearchIcon />,
|
|
||||||
component: (
|
|
||||||
<ResumeViewer />
|
|
||||||
),
|
|
||||||
userTypes: ["candidate", "guest", "employer"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
showInNavigation: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "generate-candidate",
|
id: "generate-candidate",
|
||||||
@ -205,6 +186,32 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
showInUserMenu: true,
|
showInUserMenu: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "explore-jobs",
|
||||||
|
label: "Jobs",
|
||||||
|
path: "/candidate/jobs/:jobId?",
|
||||||
|
icon: <WorkIcon />,
|
||||||
|
component: (
|
||||||
|
<JobViewer />
|
||||||
|
),
|
||||||
|
userTypes: ["candidate", "guest", "employer"],
|
||||||
|
showInNavigation: false,
|
||||||
|
showInUserMenu: true,
|
||||||
|
userMenuGroup: "profile",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "explore-resumes",
|
||||||
|
label: "Resumes",
|
||||||
|
path: "/candidate/resumes/:resumeId?",
|
||||||
|
icon: <EditDocumentIcon />,
|
||||||
|
component: (
|
||||||
|
<ResumeViewer />
|
||||||
|
),
|
||||||
|
userTypes: ["candidate", "guest", "employer"],
|
||||||
|
showInNavigation: false,
|
||||||
|
showInUserMenu: true,
|
||||||
|
userMenuGroup: "profile",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "candidate-docs",
|
id: "candidate-docs",
|
||||||
label: "Content",
|
label: "Content",
|
||||||
@ -291,33 +298,21 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "register",
|
id: "verify-email",
|
||||||
label: "Register",
|
label: "Verify Email",
|
||||||
path: "/login/register",
|
path: "/login/verify-email",
|
||||||
component: (
|
component: <EmailVerificationPage />,
|
||||||
<BetaPage>
|
userTypes: ["guest", "candidate", "employer"],
|
||||||
<CreateProfilePage />
|
|
||||||
</BetaPage>
|
|
||||||
),
|
|
||||||
userTypes: ["guest"],
|
|
||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "login",
|
id: "login",
|
||||||
label: "Login",
|
label: "Login",
|
||||||
path: "/login/*",
|
path: "/login/:tab?",
|
||||||
component: <LoginPage />,
|
component: <LoginPage />,
|
||||||
userTypes: ["guest", "candidate", "employer"],
|
userTypes: ["guest", "candidate", "employer"],
|
||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "verify-email",
|
|
||||||
label: "Verify Email",
|
|
||||||
path: "/login/verify-email",
|
|
||||||
component: <EmailVerificationPage />,
|
|
||||||
userTypes: ["guest", "candidate", "employer"],
|
|
||||||
showInNavigation: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "logout-page",
|
id: "logout-page",
|
||||||
label: "Logout",
|
label: "Logout",
|
||||||
|
@ -15,6 +15,8 @@ import {
|
|||||||
Stepper,
|
Stepper,
|
||||||
Stack,
|
Stack,
|
||||||
ButtonProps,
|
ButtonProps,
|
||||||
|
useMediaQuery,
|
||||||
|
useTheme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
@ -215,6 +217,8 @@ const HeroButton = (props: HeroButtonProps) => {
|
|||||||
|
|
||||||
const HowItWorks: React.FC = () => {
|
const HowItWorks: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const handleGetStarted = () => {
|
const handleGetStarted = () => {
|
||||||
navigate('/job-analysis');
|
navigate('/job-analysis');
|
||||||
@ -289,7 +293,7 @@ const HowItWorks: React.FC = () => {
|
|||||||
</Container>
|
</Container>
|
||||||
</HeroSection>
|
</HeroSection>
|
||||||
<HeroSection sx={{ display: "flex", position: "relative", overflow: "hidden", border: "2px solid orange" }}>
|
<HeroSection sx={{ display: "flex", position: "relative", overflow: "hidden", border: "2px solid orange" }}>
|
||||||
<Beta sx={{ left: "-90px" }} />
|
<Beta adaptive={false} sx={{ left: "-90px" }} />
|
||||||
<Container sx={{ display: "flex", position: "relative" }}>
|
<Container sx={{ display: "flex", position: "relative" }}>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", textAlign: 'center', maxWidth: 800, mx: 'auto', position: "relative" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", textAlign: 'center', maxWidth: 800, mx: 'auto', position: "relative" }}>
|
||||||
<Typography
|
<Typography
|
||||||
|
@ -26,14 +26,14 @@ import { BackstoryPageProps } from 'components/BackstoryTab';
|
|||||||
|
|
||||||
import { LoginForm } from "components/EmailVerificationComponents";
|
import { LoginForm } from "components/EmailVerificationComponents";
|
||||||
import { CandidateRegistrationForm } from "pages/candidate/RegistrationForms";
|
import { CandidateRegistrationForm } from "pages/candidate/RegistrationForms";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useAppState } from 'hooks/GlobalContext';
|
import { useAppState } from 'hooks/GlobalContext';
|
||||||
import * as Types from 'types/types';
|
import * as Types from 'types/types';
|
||||||
|
|
||||||
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
const [tabValue, setTabValue] = useState(0);
|
const [tabValue, setTabValue] = useState<string>('login');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const { guest, user, login, isLoading, error } = useAuth();
|
const { guest, user, login, isLoading, error } = useAuth();
|
||||||
@ -42,6 +42,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
const showGuest: boolean = false;
|
const showGuest: boolean = false;
|
||||||
|
const { tab } = useParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading || !error) {
|
if (!loading || !error) {
|
||||||
@ -60,7 +61,13 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
}
|
}
|
||||||
}, [error, loading]);
|
}, [error, loading]);
|
||||||
|
|
||||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
useEffect(() => {
|
||||||
|
if (tab === 'register') {
|
||||||
|
setTabValue(tab);
|
||||||
|
}
|
||||||
|
}, [tab, setTabValue]);
|
||||||
|
|
||||||
|
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||||||
setTabValue(newValue);
|
setTabValue(newValue);
|
||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
};
|
};
|
||||||
@ -93,8 +100,8 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
|
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
||||||
<Tab icon={<Person />} label="Login" />
|
<Tab value="login" icon={<Person />} label="Login" />
|
||||||
<Tab icon={<PersonAdd />} label="Register" />
|
<Tab value="register" icon={<PersonAdd />} label="Register" />
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@ -110,11 +117,11 @@ const LoginPage: React.FC<BackstoryPageProps> = (props: BackstoryPageProps) => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tabValue === 0 && (
|
{tabValue === "login" && (
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{tabValue === 1 && (
|
{tabValue === "register" && (
|
||||||
<CandidateRegistrationForm />
|
<CandidateRegistrationForm />
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -184,13 +184,32 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (props: BackstoryPageProp
|
|||||||
|
|
||||||
// Handle profile image upload
|
// Handle profile image upload
|
||||||
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (!e.target.files || !e.target.files[0]) {
|
||||||
if (await apiClient.uploadCandidateProfile(e.target.files[0])) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Uploading profile image:', e.target.files[0]);
|
||||||
|
const success = await apiClient.uploadCandidateProfile(e.target.files[0]);
|
||||||
|
if (success) {
|
||||||
setProfileImage(URL.createObjectURL(e.target.files[0]));
|
setProfileImage(URL.createObjectURL(e.target.files[0]));
|
||||||
candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, '');
|
candidate.profileImage = 'profile.' + e.target.files[0].name.replace(/^.*\./, '');
|
||||||
console.log(`Set profile image to: ${candidate.profileImage}`);
|
console.log(`Set profile image to: ${candidate.profileImage}`);
|
||||||
updateUserData(candidate);
|
updateUserData(candidate);
|
||||||
|
} else {
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: 'Failed to upload profile image. Please try again.',
|
||||||
|
severity: 'error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading profile image:', error);
|
||||||
|
setSnackbar({
|
||||||
|
open: true,
|
||||||
|
message: 'Failed to upload profile image. Please try again.',
|
||||||
|
severity: 'error'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -738,14 +738,14 @@ class ApiClient {
|
|||||||
return handleApiResponse<{ success: boolean; statistics: any }>(response);
|
return handleApiResponse<{ success: boolean; statistics: any }>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateResume(resumeId: string, content: string): Promise<{ success: boolean; message: string; resume: Types.Resume }> {
|
async updateResume(resumeId: string, content: string): Promise<Types.Resume> {
|
||||||
const response = await fetch(`${this.baseUrl}/resumes/${resumeId}`, {
|
const response = await fetch(`${this.baseUrl}/resumes/${resumeId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: this.defaultHeaders,
|
headers: this.defaultHeaders,
|
||||||
body: JSON.stringify(content)
|
body: JSON.stringify(content)
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleApiResponse<{ success: boolean; message: string; resume: Types.Resume }>(response);
|
return this.handleApiResponseWithConversion<Types.Resume>(response, 'Resume');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJob(id: string): Promise<Types.Job> {
|
async getJob(id: string): Promise<Types.Job> {
|
||||||
|
@ -50,25 +50,19 @@ emptyUser = {
|
|||||||
"questions": [],
|
"questions": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_persona_system_prompt = """\
|
def generate_persona_system_prompt(persona: Dict[str, Any]) -> str:
|
||||||
|
return f"""\
|
||||||
You are a casting director for a movie. Your job is to provide information on ficticious personas for use in a screen play.
|
You are a casting director for a movie. Your job is to provide information on ficticious personas for use in a screen play.
|
||||||
|
|
||||||
All response field MUST BE IN ENGLISH, regardless of ethnicity.
|
All response field MUST BE IN ENGLISH, regardless of ethnicity.
|
||||||
|
|
||||||
You will be provided with defaults to use if not specified by the user:
|
Use the following defaults, unless the user indicates they should be something else:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{json.dumps(persona, indent=4)}
|
||||||
"age": number,
|
|
||||||
"gender": "male" | "female",
|
|
||||||
"ethnicity": string,
|
|
||||||
"full_name": string,
|
|
||||||
"first_name": string,
|
|
||||||
"last_name": string,
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Additional information provided in the user message can override those defaults.
|
Additional information provided in the user message can override those defaults. For example, if the user provides a name, you should use that name instead of the defaults.
|
||||||
|
|
||||||
You need to randomly assign an English username (can include numbers), a first name, last name, and a two English sentence description of
|
You need to randomly assign an English username (can include numbers), a first name, last name, and a two English sentence description of
|
||||||
that individual's work given the demographics provided.
|
that individual's work given the demographics provided.
|
||||||
@ -78,14 +72,17 @@ Provide only the JSON response, and match the field names EXACTLY.
|
|||||||
Provide all information in English ONLY, with no other commentary:
|
Provide all information in English ONLY, with no other commentary:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{{
|
||||||
|
"first_name": string, # First name of the person. If the user , use the first name provided or generated
|
||||||
|
"full_name": string, # Full name of the person, use the first and last name provided or generated
|
||||||
|
"last_name": string, # Last name of the person, use the last name provided or generated
|
||||||
"username": string, # A likely-to-be unique username, no more than 15 characters (can include numbers and letters but no special characters)
|
"username": string, # A likely-to-be unique username, no more than 15 characters (can include numbers and letters but no special characters)
|
||||||
"description": string, # One to two sentence description of their job
|
"description": string, # One to two sentence description of their job
|
||||||
"location": string, # In the location, provide ALL of: City, State/Region, and Country
|
"location": string, # In the location, provide ALL of: City, State/Region, and Country
|
||||||
"phone": string, # Location appropriate phone number with area code
|
"phone": string, # Location appropriate phone number with area code
|
||||||
"email": string, # primary email address
|
"email": string, # primary email address
|
||||||
"title": string, # Job title of their current job
|
"title": string, # Job title of their current job
|
||||||
}
|
}}
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure to provide a username and that the field name for the job description is "description".
|
Make sure to provide a username and that the field name for the job description is "description".
|
||||||
@ -296,7 +293,6 @@ class GeneratePersona(Agent):
|
|||||||
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
_agent_type: ClassVar[str] = agent_type # Add this for registration
|
||||||
agent_persist: bool = False
|
agent_persist: bool = False
|
||||||
|
|
||||||
system_prompt: str = generate_persona_system_prompt
|
|
||||||
age: int = 0
|
age: int = 0
|
||||||
gender: str = ""
|
gender: str = ""
|
||||||
username: str = ""
|
username: str = ""
|
||||||
@ -325,16 +321,18 @@ class GeneratePersona(Agent):
|
|||||||
|
|
||||||
original_prompt = prompt.strip()
|
original_prompt = prompt.strip()
|
||||||
|
|
||||||
prompt = f"""\
|
persona = {
|
||||||
```json
|
|
||||||
{json.dumps({
|
|
||||||
"age": self.age,
|
"age": self.age,
|
||||||
"gender": self.gender,
|
"gender": self.gender,
|
||||||
"ethnicity": self.ethnicity,
|
"ethnicity": self.ethnicity,
|
||||||
"full_name": self.full_name,
|
"full_name": self.full_name,
|
||||||
"first_name": self.first_name,
|
"first_name": self.first_name,
|
||||||
"last_name": self.last_name,
|
"last_name": self.last_name,
|
||||||
})}
|
}
|
||||||
|
|
||||||
|
prompt = f"""\
|
||||||
|
```json
|
||||||
|
{json.dumps(persona)}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -346,13 +344,13 @@ Incorporate the following into the job description: {original_prompt}
|
|||||||
#
|
#
|
||||||
# Generate the persona
|
# Generate the persona
|
||||||
#
|
#
|
||||||
logger.info(f"🤖 Generating persona for {self.full_name}")
|
logger.info(f"🤖 Generating persona...")
|
||||||
generating_message = None
|
generating_message = None
|
||||||
async for generating_message in self.llm_one_shot(
|
async for generating_message in self.llm_one_shot(
|
||||||
llm=llm, model=model,
|
llm=llm, model=model,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
system_prompt=generate_persona_system_prompt,
|
system_prompt=generate_persona_system_prompt(persona=persona),
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
):
|
):
|
||||||
if generating_message.status == ApiStatusType.ERROR:
|
if generating_message.status == ApiStatusType.ERROR:
|
||||||
|
@ -138,7 +138,7 @@ When sections lack data, output "Information not provided" or use placeholder te
|
|||||||
2. Format the resume in a clean, concise, and modern style that will pass ATS systems.
|
2. Format the resume in a clean, concise, and modern style that will pass ATS systems.
|
||||||
3. Include these sections:
|
3. Include these sections:
|
||||||
- Professional Summary (highlight strongest skills and experience level)
|
- Professional Summary (highlight strongest skills and experience level)
|
||||||
- Skills (organized by strength)
|
- Skills (organized by strength, under a single section). When listing skills, rephrase them so they are not identical to the original assessment.
|
||||||
- Professional Experience (focus on achievements and evidence of the skill)
|
- Professional Experience (focus on achievements and evidence of the skill)
|
||||||
4. Optional sections, to include only if evidence is present:
|
4. Optional sections, to include only if evidence is present:
|
||||||
- Education section
|
- Education section
|
||||||
@ -165,7 +165,11 @@ ELSE:
|
|||||||
Provide the resume in clean markdown format, ready for the candidate to use.
|
Provide the resume in clean markdown format, ready for the candidate to use.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
prompt = "Create a tailored professional resume that highlights candidate's skills and experience most relevant to the job requirements. Format it in clean, ATS-friendly markdown. Provide ONLY the resume with no commentary before or after."
|
prompt = """\
|
||||||
|
Create a tailored professional resume that highlights candidate's skills and experience most relevant to the job requirements.
|
||||||
|
Format it in clean, ATS-friendly markdown. Provide ONLY the resume with no commentary before or after.
|
||||||
|
"""
|
||||||
|
|
||||||
return system_prompt, prompt
|
return system_prompt, prompt
|
||||||
|
|
||||||
async def generate_resume(
|
async def generate_resume(
|
||||||
|
@ -1,28 +1,30 @@
|
|||||||
"""
|
"""
|
||||||
Background tasks for guest cleanup and system maintenance
|
Background tasks for guest cleanup and system maintenance
|
||||||
|
Fixed for event loop safety
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import schedule # type: ignore
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from datetime import datetime, timedelta, UTC
|
from datetime import datetime, timedelta, UTC
|
||||||
from typing import Optional
|
from typing import Optional, List, Dict, Any, Callable
|
||||||
from logger import logger
|
from logger import logger
|
||||||
from database import DatabaseManager
|
from database import DatabaseManager
|
||||||
|
|
||||||
class BackgroundTaskManager:
|
class BackgroundTaskManager:
|
||||||
"""Manages background tasks for the application"""
|
"""Manages background tasks for the application using asyncio instead of threading"""
|
||||||
|
|
||||||
def __init__(self, database_manager: DatabaseManager):
|
def __init__(self, database_manager: DatabaseManager):
|
||||||
self.database_manager = database_manager
|
self.database_manager = database_manager
|
||||||
self.running = False
|
self.running = False
|
||||||
self.tasks = []
|
self.tasks: List[asyncio.Task] = []
|
||||||
self.scheduler_thread: Optional[threading.Thread] = None
|
self.main_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||||
|
|
||||||
async def cleanup_inactive_guests(self, inactive_hours: int = 24):
|
async def cleanup_inactive_guests(self, inactive_hours: int = 24):
|
||||||
"""Clean up inactive guest sessions"""
|
"""Clean up inactive guest sessions"""
|
||||||
try:
|
try:
|
||||||
|
if self.database_manager.is_shutting_down:
|
||||||
|
logger.info("Skipping guest cleanup - application shutting down")
|
||||||
|
return 0
|
||||||
|
|
||||||
database = self.database_manager.get_database()
|
database = self.database_manager.get_database()
|
||||||
cleaned_count = await database.cleanup_inactive_guests(inactive_hours)
|
cleaned_count = await database.cleanup_inactive_guests(inactive_hours)
|
||||||
|
|
||||||
@ -37,6 +39,10 @@ class BackgroundTaskManager:
|
|||||||
async def cleanup_expired_verification_tokens(self):
|
async def cleanup_expired_verification_tokens(self):
|
||||||
"""Clean up expired email verification tokens"""
|
"""Clean up expired email verification tokens"""
|
||||||
try:
|
try:
|
||||||
|
if self.database_manager.is_shutting_down:
|
||||||
|
logger.info("Skipping token cleanup - application shutting down")
|
||||||
|
return 0
|
||||||
|
|
||||||
database = self.database_manager.get_database()
|
database = self.database_manager.get_database()
|
||||||
cleaned_count = await database.cleanup_expired_verification_tokens()
|
cleaned_count = await database.cleanup_expired_verification_tokens()
|
||||||
|
|
||||||
@ -51,6 +57,10 @@ class BackgroundTaskManager:
|
|||||||
async def update_guest_statistics(self):
|
async def update_guest_statistics(self):
|
||||||
"""Update guest usage statistics"""
|
"""Update guest usage statistics"""
|
||||||
try:
|
try:
|
||||||
|
if self.database_manager.is_shutting_down:
|
||||||
|
logger.info("Skipping stats update - application shutting down")
|
||||||
|
return {}
|
||||||
|
|
||||||
database = self.database_manager.get_database()
|
database = self.database_manager.get_database()
|
||||||
stats = await database.get_guest_statistics()
|
stats = await database.get_guest_statistics()
|
||||||
|
|
||||||
@ -68,8 +78,15 @@ class BackgroundTaskManager:
|
|||||||
async def cleanup_old_rate_limit_data(self, days_old: int = 7):
|
async def cleanup_old_rate_limit_data(self, days_old: int = 7):
|
||||||
"""Clean up old rate limiting data"""
|
"""Clean up old rate limiting data"""
|
||||||
try:
|
try:
|
||||||
|
if self.database_manager.is_shutting_down:
|
||||||
|
logger.info("Skipping rate limit cleanup - application shutting down")
|
||||||
|
return 0
|
||||||
|
|
||||||
database = self.database_manager.get_database()
|
database = self.database_manager.get_database()
|
||||||
redis = database.redis
|
|
||||||
|
# Get Redis client safely (using the event loop safe method)
|
||||||
|
from database import redis_manager
|
||||||
|
redis = await redis_manager.get_client()
|
||||||
|
|
||||||
# Clean up rate limit keys older than specified days
|
# Clean up rate limit keys older than specified days
|
||||||
cutoff_time = datetime.now(UTC) - timedelta(days=days_old)
|
cutoff_time = datetime.now(UTC) - timedelta(days=days_old)
|
||||||
@ -103,73 +120,206 @@ class BackgroundTaskManager:
|
|||||||
logger.error(f"❌ Error cleaning up rate limit data: {e}")
|
logger.error(f"❌ Error cleaning up rate limit data: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def schedule_periodic_tasks(self):
|
async def cleanup_orphaned_data(self):
|
||||||
"""Schedule periodic background tasks with safer intervals"""
|
"""Clean up orphaned database records"""
|
||||||
|
|
||||||
# Guest cleanup - every 6 hours instead of every hour (less aggressive)
|
|
||||||
schedule.every(6).hours.do(self._run_async_task, self.cleanup_inactive_guests, 48) # 48 hours instead of 24
|
|
||||||
|
|
||||||
# Verification token cleanup - every 12 hours
|
|
||||||
schedule.every(12).hours.do(self._run_async_task, self.cleanup_expired_verification_tokens)
|
|
||||||
|
|
||||||
# Guest statistics update - every hour
|
|
||||||
schedule.every().hour.do(self._run_async_task, self.update_guest_statistics)
|
|
||||||
|
|
||||||
# Rate limit data cleanup - daily at 3 AM
|
|
||||||
schedule.every().day.at("03:00").do(self._run_async_task, self.cleanup_old_rate_limit_data, 7)
|
|
||||||
|
|
||||||
logger.info("📅 Background tasks scheduled with safer intervals")
|
|
||||||
|
|
||||||
def _run_async_task(self, coro_func, *args, **kwargs):
|
|
||||||
"""Run an async task in the background"""
|
|
||||||
try:
|
try:
|
||||||
# Create new event loop for this thread if needed
|
if self.database_manager.is_shutting_down:
|
||||||
try:
|
return 0
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
except RuntimeError:
|
database = self.database_manager.get_database()
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
|
|
||||||
# Run the coroutine
|
# Clean up orphaned job requirements
|
||||||
loop.run_until_complete(coro_func(*args, **kwargs))
|
orphaned_count = await database.cleanup_orphaned_job_requirements()
|
||||||
|
|
||||||
|
if orphaned_count > 0:
|
||||||
|
logger.info(f"🧹 Cleaned up {orphaned_count} orphaned job requirements")
|
||||||
|
|
||||||
|
return orphaned_count
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error running background task {coro_func.__name__}: {e}")
|
logger.error(f"❌ Error cleaning up orphaned data: {e}")
|
||||||
|
return 0
|
||||||
|
|
||||||
def _scheduler_worker(self):
|
async def _run_periodic_task(self, name: str, task_func: Callable, interval_seconds: int, *args, **kwargs):
|
||||||
"""Worker thread for running scheduled tasks"""
|
"""Run a periodic task safely in the same event loop"""
|
||||||
|
logger.info(f"🔄 Starting periodic task: {name} (every {interval_seconds}s)")
|
||||||
|
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
schedule.run_pending()
|
# Verify we're still in the correct event loop
|
||||||
time.sleep(60) # Check every minute
|
current_loop = asyncio.get_running_loop()
|
||||||
|
if current_loop != self.main_loop:
|
||||||
|
logger.error(f"Task {name} detected event loop change! Stopping.")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Run the task
|
||||||
|
await task_func(*args, **kwargs)
|
||||||
|
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info(f"Periodic task {name} was cancelled")
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error in scheduler worker: {e}")
|
logger.error(f"❌ Error in periodic task {name}: {e}")
|
||||||
time.sleep(60)
|
# Continue running despite errors
|
||||||
|
|
||||||
|
# Sleep with cancellation support
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(interval_seconds)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.info(f"Periodic task {name} cancelled during sleep")
|
||||||
|
break
|
||||||
|
|
||||||
def start(self):
|
async def start(self):
|
||||||
"""Start the background task manager"""
|
"""Start all background tasks in the current event loop"""
|
||||||
if self.running:
|
if self.running:
|
||||||
logger.warning("⚠️ Background task manager already running")
|
logger.warning("⚠️ Background task manager already running")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Store the current event loop
|
||||||
|
self.main_loop = asyncio.get_running_loop()
|
||||||
self.running = True
|
self.running = True
|
||||||
self.schedule_periodic_tasks()
|
|
||||||
|
|
||||||
# Start scheduler thread
|
# Define periodic tasks with their intervals (in seconds)
|
||||||
self.scheduler_thread = threading.Thread(target=self._scheduler_worker, daemon=True)
|
periodic_tasks = [
|
||||||
self.scheduler_thread.start()
|
# (name, function, interval_seconds, *args)
|
||||||
|
("guest_cleanup", self.cleanup_inactive_guests, 6 * 3600, 48), # Every 6 hours, cleanup 48h old
|
||||||
|
("token_cleanup", self.cleanup_expired_verification_tokens, 12 * 3600), # Every 12 hours
|
||||||
|
("guest_stats", self.update_guest_statistics, 3600), # Every hour
|
||||||
|
("rate_limit_cleanup", self.cleanup_old_rate_limit_data, 24 * 3600, 7), # Daily, cleanup 7 days old
|
||||||
|
("orphaned_cleanup", self.cleanup_orphaned_data, 6 * 3600), # Every 6 hours
|
||||||
|
]
|
||||||
|
|
||||||
logger.info("🚀 Background task manager started")
|
# Create asyncio tasks for each periodic task
|
||||||
|
for name, func, interval, *args in periodic_tasks:
|
||||||
|
task = asyncio.create_task(
|
||||||
|
self._run_periodic_task(name, func, interval, *args),
|
||||||
|
name=f"background_{name}"
|
||||||
|
)
|
||||||
|
self.tasks.append(task)
|
||||||
|
logger.info(f"📅 Scheduled background task: {name}")
|
||||||
|
|
||||||
|
# Run initial cleanup tasks immediately (but don't wait for them)
|
||||||
|
asyncio.create_task(self._run_initial_cleanup(), name="initial_cleanup")
|
||||||
|
|
||||||
|
logger.info("🚀 Background task manager started with asyncio tasks")
|
||||||
|
|
||||||
def stop(self):
|
async def _run_initial_cleanup(self):
|
||||||
"""Stop the background task manager"""
|
"""Run some cleanup tasks immediately on startup"""
|
||||||
|
try:
|
||||||
|
logger.info("🧹 Running initial cleanup tasks...")
|
||||||
|
|
||||||
|
# Clean up expired tokens immediately
|
||||||
|
await asyncio.sleep(5) # Give the app time to fully start
|
||||||
|
await self.cleanup_expired_verification_tokens()
|
||||||
|
|
||||||
|
# Clean up very old inactive guests (7 days old)
|
||||||
|
await self.cleanup_inactive_guests(inactive_hours=7 * 24)
|
||||||
|
|
||||||
|
# Update statistics
|
||||||
|
await self.update_guest_statistics()
|
||||||
|
|
||||||
|
logger.info("✅ Initial cleanup tasks completed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error in initial cleanup: {e}")
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
"""Stop all background tasks gracefully"""
|
||||||
|
logger.info("🛑 Stopping background task manager...")
|
||||||
|
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
if self.scheduler_thread and self.scheduler_thread.is_alive():
|
# Cancel all running tasks
|
||||||
self.scheduler_thread.join(timeout=5)
|
for task in self.tasks:
|
||||||
|
if not task.done():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
# Clear scheduled tasks
|
# Wait for all tasks to complete with timeout
|
||||||
schedule.clear()
|
if self.tasks:
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(
|
||||||
|
asyncio.gather(*self.tasks, return_exceptions=True),
|
||||||
|
timeout=30.0
|
||||||
|
)
|
||||||
|
logger.info("✅ All background tasks stopped gracefully")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.warning("⚠️ Some background tasks did not stop within timeout")
|
||||||
|
|
||||||
|
self.tasks.clear()
|
||||||
|
self.main_loop = None
|
||||||
|
|
||||||
logger.info("🛑 Background task manager stopped")
|
logger.info("🛑 Background task manager stopped")
|
||||||
|
|
||||||
|
async def get_task_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get status of all background tasks"""
|
||||||
|
status = {
|
||||||
|
"running": self.running,
|
||||||
|
"main_loop_id": id(self.main_loop) if self.main_loop else None,
|
||||||
|
"current_loop_id": None,
|
||||||
|
"task_count": len(self.tasks),
|
||||||
|
"tasks": []
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_loop = asyncio.get_running_loop()
|
||||||
|
status["current_loop_id"] = id(current_loop)
|
||||||
|
status["loop_matches"] = (id(current_loop) == id(self.main_loop)) if self.main_loop else False
|
||||||
|
except RuntimeError:
|
||||||
|
status["current_loop_id"] = "no_running_loop"
|
||||||
|
|
||||||
|
for task in self.tasks:
|
||||||
|
task_info = {
|
||||||
|
"name": task.get_name(),
|
||||||
|
"done": task.done(),
|
||||||
|
"cancelled": task.cancelled(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.done() and not task.cancelled():
|
||||||
|
try:
|
||||||
|
task.result() # This will raise an exception if the task failed
|
||||||
|
task_info["status"] = "completed"
|
||||||
|
except Exception as e:
|
||||||
|
task_info["status"] = "failed"
|
||||||
|
task_info["error"] = str(e)
|
||||||
|
elif task.cancelled():
|
||||||
|
task_info["status"] = "cancelled"
|
||||||
|
else:
|
||||||
|
task_info["status"] = "running"
|
||||||
|
|
||||||
|
status["tasks"].append(task_info)
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
async def force_run_task(self, task_name: str) -> Any:
|
||||||
|
"""Manually trigger a specific background task"""
|
||||||
|
task_map = {
|
||||||
|
"guest_cleanup": self.cleanup_inactive_guests,
|
||||||
|
"token_cleanup": self.cleanup_expired_verification_tokens,
|
||||||
|
"guest_stats": self.update_guest_statistics,
|
||||||
|
"rate_limit_cleanup": self.cleanup_old_rate_limit_data,
|
||||||
|
"orphaned_cleanup": self.cleanup_orphaned_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
if task_name not in task_map:
|
||||||
|
raise ValueError(f"Unknown task: {task_name}. Available: {list(task_map.keys())}")
|
||||||
|
|
||||||
|
logger.info(f"🔧 Manually running task: {task_name}")
|
||||||
|
result = await task_map[task_name]()
|
||||||
|
logger.info(f"✅ Manual task {task_name} completed")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Usage in your main application
|
||||||
|
async def setup_background_tasks(database_manager: DatabaseManager) -> BackgroundTaskManager:
|
||||||
|
"""Setup and start background tasks"""
|
||||||
|
task_manager = BackgroundTaskManager(database_manager)
|
||||||
|
await task_manager.start()
|
||||||
|
return task_manager
|
||||||
|
|
||||||
|
# For integration with your existing app startup
|
||||||
|
async def initialize_with_background_tasks(database_manager: DatabaseManager):
|
||||||
|
"""Initialize database and background tasks together"""
|
||||||
|
# Start background tasks
|
||||||
|
background_tasks = await setup_background_tasks(database_manager)
|
||||||
|
|
||||||
|
# Return both for your app to manage
|
||||||
|
return database_manager, background_tasks
|
@ -17,6 +17,12 @@ class _RedisManager:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.redis: Optional[redis.Redis] = None
|
self.redis: Optional[redis.Redis] = None
|
||||||
self.redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
|
self.redis_url = os.getenv("REDIS_URL", "redis://redis:6379")
|
||||||
|
self.redis_db = int(os.getenv("REDIS_DB", "0"))
|
||||||
|
|
||||||
|
# Append database to URL if not already present
|
||||||
|
if not self.redis_url.endswith(f"/{self.redis_db}"):
|
||||||
|
self.redis_url = f"{self.redis_url}/{self.redis_db}"
|
||||||
|
|
||||||
self._connection_pool: Optional[redis.ConnectionPool] = None
|
self._connection_pool: Optional[redis.ConnectionPool] = None
|
||||||
self._is_connected = False
|
self._is_connected = False
|
||||||
|
|
||||||
@ -210,10 +216,10 @@ class RedisDatabase:
|
|||||||
"""Save a resume for a user"""
|
"""Save a resume for a user"""
|
||||||
try:
|
try:
|
||||||
# Generate resume_id if not present
|
# Generate resume_id if not present
|
||||||
if 'resume_id' not in resume_data:
|
if 'id' not in resume_data:
|
||||||
resume_data['resume_id'] = f"resume_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}_{user_id[:8]}"
|
raise ValueError("Resume data must include an 'id' field")
|
||||||
|
|
||||||
resume_id = resume_data['resume_id']
|
resume_id = resume_data['id']
|
||||||
|
|
||||||
# Store the resume data
|
# Store the resume data
|
||||||
key = f"{self.KEY_PREFIXES['resumes']}{user_id}:{resume_id}"
|
key = f"{self.KEY_PREFIXES['resumes']}{user_id}:{resume_id}"
|
||||||
@ -236,9 +242,9 @@ class RedisDatabase:
|
|||||||
data = await self.redis.get(key)
|
data = await self.redis.get(key)
|
||||||
if data:
|
if data:
|
||||||
resume_data = self._deserialize(data)
|
resume_data = self._deserialize(data)
|
||||||
logger.debug(f"📄 Retrieved resume {resume_id} for user {user_id}")
|
logger.info(f"📄 Retrieved resume {resume_id} for user {user_id}")
|
||||||
return resume_data
|
return resume_data
|
||||||
logger.debug(f"📄 Resume {resume_id} not found for user {user_id}")
|
logger.info(f"📄 Resume {resume_id} not found for user {user_id}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error retrieving resume {resume_id} for user {user_id}: {e}")
|
logger.error(f"❌ Error retrieving resume {resume_id} for user {user_id}: {e}")
|
||||||
@ -252,7 +258,7 @@ class RedisDatabase:
|
|||||||
resume_ids = await self.redis.lrange(user_resumes_key, 0, -1)
|
resume_ids = await self.redis.lrange(user_resumes_key, 0, -1)
|
||||||
|
|
||||||
if not resume_ids:
|
if not resume_ids:
|
||||||
logger.debug(f"📄 No resumes found for user {user_id}")
|
logger.info(f"📄 No resumes found for user {user_id}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get all resume data
|
# Get all resume data
|
||||||
@ -275,7 +281,7 @@ class RedisDatabase:
|
|||||||
# Sort by created_at timestamp (most recent first)
|
# Sort by created_at timestamp (most recent first)
|
||||||
resumes.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
resumes.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
||||||
|
|
||||||
logger.debug(f"📄 Retrieved {len(resumes)} resumes for user {user_id}")
|
logger.info(f"📄 Retrieved {len(resumes)} resumes for user {user_id}")
|
||||||
return resumes
|
return resumes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error retrieving resumes for user {user_id}: {e}")
|
logger.error(f"❌ Error retrieving resumes for user {user_id}: {e}")
|
||||||
@ -392,7 +398,7 @@ class RedisDatabase:
|
|||||||
if query_lower in searchable_text:
|
if query_lower in searchable_text:
|
||||||
matching_resumes.append(resume)
|
matching_resumes.append(resume)
|
||||||
|
|
||||||
logger.debug(f"📄 Found {len(matching_resumes)} matching resumes for user {user_id}")
|
logger.info(f"📄 Found {len(matching_resumes)} matching resumes for user {user_id}")
|
||||||
return matching_resumes
|
return matching_resumes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error searching resumes for user {user_id}: {e}")
|
logger.error(f"❌ Error searching resumes for user {user_id}: {e}")
|
||||||
@ -407,7 +413,7 @@ class RedisDatabase:
|
|||||||
if resume.get("candidate_id") == candidate_id
|
if resume.get("candidate_id") == candidate_id
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.debug(f"📄 Found {len(candidate_resumes)} resumes for candidate {candidate_id} by user {user_id}")
|
logger.info(f"📄 Found {len(candidate_resumes)} resumes for candidate {candidate_id} by user {user_id}")
|
||||||
return candidate_resumes
|
return candidate_resumes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error retrieving resumes for candidate {candidate_id} by user {user_id}: {e}")
|
logger.error(f"❌ Error retrieving resumes for candidate {candidate_id} by user {user_id}: {e}")
|
||||||
@ -422,7 +428,7 @@ class RedisDatabase:
|
|||||||
if resume.get("job_id") == job_id
|
if resume.get("job_id") == job_id
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.debug(f"📄 Found {len(job_resumes)} resumes for job {job_id} by user {user_id}")
|
logger.info(f"📄 Found {len(job_resumes)} resumes for job {job_id} by user {user_id}")
|
||||||
return job_resumes
|
return job_resumes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error retrieving resumes for job {job_id} by user {user_id}: {e}")
|
logger.error(f"❌ Error retrieving resumes for job {job_id} by user {user_id}: {e}")
|
||||||
@ -558,7 +564,7 @@ class RedisDatabase:
|
|||||||
cache_key,
|
cache_key,
|
||||||
json.dumps(assessment.model_dump(mode='json', by_alias=True), default=str) # Serialize with datetime handling
|
json.dumps(assessment.model_dump(mode='json', by_alias=True), default=str) # Serialize with datetime handling
|
||||||
)
|
)
|
||||||
logger.debug(f"💾 Skill match cached: {cache_key}")
|
logger.info(f"💾 Skill match cached: {cache_key}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error caching skill match: {e}")
|
logger.error(f"❌ Error caching skill match: {e}")
|
||||||
|
|
||||||
@ -734,9 +740,9 @@ class RedisDatabase:
|
|||||||
data = await self.redis.get(key)
|
data = await self.redis.get(key)
|
||||||
if data:
|
if data:
|
||||||
requirements_data = self._deserialize(data)
|
requirements_data = self._deserialize(data)
|
||||||
logger.debug(f"📋 Retrieved cached job requirements for document {document_id}")
|
logger.info(f"📋 Retrieved cached job requirements for document {document_id}")
|
||||||
return requirements_data
|
return requirements_data
|
||||||
logger.debug(f"📋 No cached job requirements found for document {document_id}")
|
logger.info(f"📋 No cached job requirements found for document {document_id}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error retrieving job requirements for document {document_id}: {e}")
|
logger.error(f"❌ Error retrieving job requirements for document {document_id}: {e}")
|
||||||
@ -759,7 +765,7 @@ class RedisDatabase:
|
|||||||
# Optional: Set expiration (e.g., 30 days) to prevent indefinite storage
|
# Optional: Set expiration (e.g., 30 days) to prevent indefinite storage
|
||||||
# await self.redis.expire(key, 30 * 24 * 60 * 60) # 30 days
|
# await self.redis.expire(key, 30 * 24 * 60 * 60) # 30 days
|
||||||
|
|
||||||
logger.debug(f"📋 Saved job requirements for document {document_id}")
|
logger.info(f"📋 Saved job requirements for document {document_id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error saving job requirements for document {document_id}: {e}")
|
logger.error(f"❌ Error saving job requirements for document {document_id}: {e}")
|
||||||
@ -771,7 +777,7 @@ class RedisDatabase:
|
|||||||
key = f"{self.KEY_PREFIXES['job_requirements']}{document_id}"
|
key = f"{self.KEY_PREFIXES['job_requirements']}{document_id}"
|
||||||
result = await self.redis.delete(key)
|
result = await self.redis.delete(key)
|
||||||
if result > 0:
|
if result > 0:
|
||||||
logger.debug(f"📋 Deleted job requirements for document {document_id}")
|
logger.info(f"📋 Deleted job requirements for document {document_id}")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1010,7 +1016,7 @@ class RedisDatabase:
|
|||||||
if candidate_email and await self.user_exists_by_email(candidate_email):
|
if candidate_email and await self.user_exists_by_email(candidate_email):
|
||||||
await self.delete_user(candidate_email)
|
await self.delete_user(candidate_email)
|
||||||
user_records_deleted += 1
|
user_records_deleted += 1
|
||||||
logger.debug(f"🗑️ Deleted user record by email: {candidate_email}")
|
logger.info(f"🗑️ Deleted user record by email: {candidate_email}")
|
||||||
|
|
||||||
# Delete by username (if different from email)
|
# Delete by username (if different from email)
|
||||||
if (candidate_username and
|
if (candidate_username and
|
||||||
@ -1018,7 +1024,7 @@ class RedisDatabase:
|
|||||||
await self.user_exists_by_username(candidate_username)):
|
await self.user_exists_by_username(candidate_username)):
|
||||||
await self.delete_user(candidate_username)
|
await self.delete_user(candidate_username)
|
||||||
user_records_deleted += 1
|
user_records_deleted += 1
|
||||||
logger.debug(f"🗑️ Deleted user record by username: {candidate_username}")
|
logger.info(f"🗑️ Deleted user record by username: {candidate_username}")
|
||||||
|
|
||||||
# Delete user by ID if exists
|
# Delete user by ID if exists
|
||||||
user_by_id = await self.get_user_by_id(candidate_id)
|
user_by_id = await self.get_user_by_id(candidate_id)
|
||||||
@ -1026,7 +1032,7 @@ class RedisDatabase:
|
|||||||
key = f"user_by_id:{candidate_id}"
|
key = f"user_by_id:{candidate_id}"
|
||||||
await self.redis.delete(key)
|
await self.redis.delete(key)
|
||||||
user_records_deleted += 1
|
user_records_deleted += 1
|
||||||
logger.debug(f"🗑️ Deleted user record by ID: {candidate_id}")
|
logger.info(f"🗑️ Deleted user record by ID: {candidate_id}")
|
||||||
|
|
||||||
deletion_stats["user_records"] = user_records_deleted
|
deletion_stats["user_records"] = user_records_deleted
|
||||||
logger.info(f"🗑️ Deleted {user_records_deleted} user records for candidate {candidate_id}")
|
logger.info(f"🗑️ Deleted {user_records_deleted} user records for candidate {candidate_id}")
|
||||||
@ -1038,14 +1044,14 @@ class RedisDatabase:
|
|||||||
auth_deleted = await self.delete_authentication(candidate_id)
|
auth_deleted = await self.delete_authentication(candidate_id)
|
||||||
if auth_deleted:
|
if auth_deleted:
|
||||||
deletion_stats["auth_records"] = 1
|
deletion_stats["auth_records"] = 1
|
||||||
logger.debug(f"🗑️ Deleted authentication record for candidate {candidate_id}")
|
logger.info(f"🗑️ Deleted authentication record for candidate {candidate_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error deleting authentication records: {e}")
|
logger.error(f"❌ Error deleting authentication records: {e}")
|
||||||
|
|
||||||
# 7. Revoke all refresh tokens for this user
|
# 7. Revoke all refresh tokens for this user
|
||||||
try:
|
try:
|
||||||
await self.revoke_all_user_tokens(candidate_id)
|
await self.revoke_all_user_tokens(candidate_id)
|
||||||
logger.debug(f"🗑️ Revoked all refresh tokens for candidate {candidate_id}")
|
logger.info(f"🗑️ Revoked all refresh tokens for candidate {candidate_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error revoking refresh tokens: {e}")
|
logger.error(f"❌ Error revoking refresh tokens: {e}")
|
||||||
|
|
||||||
@ -1068,7 +1074,7 @@ class RedisDatabase:
|
|||||||
|
|
||||||
deletion_stats["security_logs"] = security_logs_deleted
|
deletion_stats["security_logs"] = security_logs_deleted
|
||||||
if security_logs_deleted > 0:
|
if security_logs_deleted > 0:
|
||||||
logger.debug(f"🗑️ Deleted {security_logs_deleted} security log entries for candidate {candidate_id}")
|
logger.info(f"🗑️ Deleted {security_logs_deleted} security log entries for candidate {candidate_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error deleting security logs: {e}")
|
logger.error(f"❌ Error deleting security logs: {e}")
|
||||||
|
|
||||||
@ -1115,7 +1121,7 @@ class RedisDatabase:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if tokens_deleted > 0:
|
if tokens_deleted > 0:
|
||||||
logger.debug(f"🗑️ Deleted {tokens_deleted} email verification tokens for candidate {candidate_id}")
|
logger.info(f"🗑️ Deleted {tokens_deleted} email verification tokens for candidate {candidate_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error deleting email verification tokens: {e}")
|
logger.error(f"❌ Error deleting email verification tokens: {e}")
|
||||||
|
|
||||||
@ -1141,7 +1147,7 @@ class RedisDatabase:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if tokens_deleted > 0:
|
if tokens_deleted > 0:
|
||||||
logger.debug(f"🗑️ Deleted {tokens_deleted} password reset tokens for candidate {candidate_id}")
|
logger.info(f"🗑️ Deleted {tokens_deleted} password reset tokens for candidate {candidate_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error deleting password reset tokens: {e}")
|
logger.error(f"❌ Error deleting password reset tokens: {e}")
|
||||||
|
|
||||||
@ -1163,7 +1169,7 @@ class RedisDatabase:
|
|||||||
break
|
break
|
||||||
|
|
||||||
if mfa_codes_deleted > 0:
|
if mfa_codes_deleted > 0:
|
||||||
logger.debug(f"🗑️ Deleted {mfa_codes_deleted} MFA codes for candidate {candidate_id}")
|
logger.info(f"🗑️ Deleted {mfa_codes_deleted} MFA codes for candidate {candidate_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error deleting MFA codes: {e}")
|
logger.error(f"❌ Error deleting MFA codes: {e}")
|
||||||
|
|
||||||
@ -1417,7 +1423,7 @@ class RedisDatabase:
|
|||||||
if current_time > expires_at:
|
if current_time > expires_at:
|
||||||
await self.redis.delete(key)
|
await self.redis.delete(key)
|
||||||
cleaned_count += 1
|
cleaned_count += 1
|
||||||
logger.debug(f"🧹 Cleaned expired verification token for {verification_info.get('email')}")
|
logger.info(f"🧹 Cleaned expired verification token for {verification_info.get('email')}")
|
||||||
|
|
||||||
if cursor == 0:
|
if cursor == 0:
|
||||||
break
|
break
|
||||||
@ -1509,7 +1515,7 @@ class RedisDatabase:
|
|||||||
json.dumps(verification_data, default=str)
|
json.dumps(verification_data, default=str)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"📧 Stored email verification token for {email}")
|
logger.info(f"📧 Stored email verification token for {email}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error storing email verification token: {e}")
|
logger.error(f"❌ Error storing email verification token: {e}")
|
||||||
@ -1568,7 +1574,7 @@ class RedisDatabase:
|
|||||||
json.dumps(mfa_data, default=str)
|
json.dumps(mfa_data, default=str)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"🔐 Stored MFA code for {email}")
|
logger.info(f"🔐 Stored MFA code for {email}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error storing MFA code: {e}")
|
logger.error(f"❌ Error storing MFA code: {e}")
|
||||||
@ -2093,7 +2099,7 @@ class RedisDatabase:
|
|||||||
key = f"{self.KEY_PREFIXES['job_requirements']}{document_id}"
|
key = f"{self.KEY_PREFIXES['job_requirements']}{document_id}"
|
||||||
pipe.delete(key)
|
pipe.delete(key)
|
||||||
orphaned_count += 1
|
orphaned_count += 1
|
||||||
logger.debug(f"📋 Queued orphaned job requirements for deletion: {document_id}")
|
logger.info(f"📋 Queued orphaned job requirements for deletion: {document_id}")
|
||||||
|
|
||||||
if orphaned_count > 0:
|
if orphaned_count > 0:
|
||||||
await pipe.execute()
|
await pipe.execute()
|
||||||
@ -2128,7 +2134,7 @@ class RedisDatabase:
|
|||||||
try:
|
try:
|
||||||
key = f"auth:{user_id}"
|
key = f"auth:{user_id}"
|
||||||
await self.redis.set(key, json.dumps(auth_data, default=str))
|
await self.redis.set(key, json.dumps(auth_data, default=str))
|
||||||
logger.debug(f"🔐 Stored authentication record for user {user_id}")
|
logger.info(f"🔐 Stored authentication record for user {user_id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error storing authentication record for {user_id}: {e}")
|
logger.error(f"❌ Error storing authentication record for {user_id}: {e}")
|
||||||
@ -2151,7 +2157,7 @@ class RedisDatabase:
|
|||||||
try:
|
try:
|
||||||
key = f"auth:{user_id}"
|
key = f"auth:{user_id}"
|
||||||
result = await self.redis.delete(key)
|
result = await self.redis.delete(key)
|
||||||
logger.debug(f"🔐 Deleted authentication record for user {user_id}")
|
logger.info(f"🔐 Deleted authentication record for user {user_id}")
|
||||||
return result > 0
|
return result > 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error deleting authentication record for {user_id}: {e}")
|
logger.error(f"❌ Error deleting authentication record for {user_id}: {e}")
|
||||||
@ -2163,7 +2169,7 @@ class RedisDatabase:
|
|||||||
try:
|
try:
|
||||||
key = f"user_by_id:{user_id}"
|
key = f"user_by_id:{user_id}"
|
||||||
await self.redis.set(key, json.dumps(user_data, default=str))
|
await self.redis.set(key, json.dumps(user_data, default=str))
|
||||||
logger.debug(f"👤 Stored user data by ID for {user_id}")
|
logger.info(f"👤 Stored user data by ID for {user_id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error storing user by ID {user_id}: {e}")
|
logger.error(f"❌ Error storing user by ID {user_id}: {e}")
|
||||||
@ -2213,10 +2219,10 @@ class RedisDatabase:
|
|||||||
data = await self.redis.get(key)
|
data = await self.redis.get(key)
|
||||||
if data:
|
if data:
|
||||||
user_data = json.loads(data)
|
user_data = json.loads(data)
|
||||||
logger.debug(f"👤 Retrieved user data for {login}")
|
logger.info(f"👤 Retrieved user data for {login}")
|
||||||
return user_data
|
return user_data
|
||||||
|
|
||||||
logger.debug(f"👤 No user found for {login}")
|
logger.info(f"👤 No user found for {login}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error retrieving user {login}: {e}")
|
logger.error(f"❌ Error retrieving user {login}: {e}")
|
||||||
@ -2233,7 +2239,7 @@ class RedisDatabase:
|
|||||||
key = f"users:{login}"
|
key = f"users:{login}"
|
||||||
|
|
||||||
await self.redis.set(key, json.dumps(user_data, default=str))
|
await self.redis.set(key, json.dumps(user_data, default=str))
|
||||||
logger.debug(f"👤 Stored user data for {login}")
|
logger.info(f"👤 Stored user data for {login}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error storing user {login}: {e}")
|
logger.error(f"❌ Error storing user {login}: {e}")
|
||||||
@ -2257,7 +2263,7 @@ class RedisDatabase:
|
|||||||
ttl_seconds = int((expires_at - datetime.now(timezone.utc)).total_seconds())
|
ttl_seconds = int((expires_at - datetime.now(timezone.utc)).total_seconds())
|
||||||
if ttl_seconds > 0:
|
if ttl_seconds > 0:
|
||||||
await self.redis.setex(key, ttl_seconds, json.dumps(token_data, default=str))
|
await self.redis.setex(key, ttl_seconds, json.dumps(token_data, default=str))
|
||||||
logger.debug(f"🔐 Stored refresh token for user {user_id}")
|
logger.info(f"🔐 Stored refresh token for user {user_id}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ Attempted to store expired refresh token for user {user_id}")
|
logger.warning(f"⚠️ Attempted to store expired refresh token for user {user_id}")
|
||||||
@ -2287,7 +2293,7 @@ class RedisDatabase:
|
|||||||
token_data["is_revoked"] = True
|
token_data["is_revoked"] = True
|
||||||
token_data["revoked_at"] = datetime.now(timezone.utc).isoformat()
|
token_data["revoked_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
await self.redis.set(key, json.dumps(token_data, default=str))
|
await self.redis.set(key, json.dumps(token_data, default=str))
|
||||||
logger.debug(f"🔐 Revoked refresh token")
|
logger.info(f"🔐 Revoked refresh token")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -2340,7 +2346,7 @@ class RedisDatabase:
|
|||||||
ttl_seconds = int((expires_at - datetime.now(timezone.utc)).total_seconds())
|
ttl_seconds = int((expires_at - datetime.now(timezone.utc)).total_seconds())
|
||||||
if ttl_seconds > 0:
|
if ttl_seconds > 0:
|
||||||
await self.redis.setex(key, ttl_seconds, json.dumps(token_data, default=str))
|
await self.redis.setex(key, ttl_seconds, json.dumps(token_data, default=str))
|
||||||
logger.debug(f"🔐 Stored password reset token for {email}")
|
logger.info(f"🔐 Stored password reset token for {email}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ Attempted to store expired password reset token for {email}")
|
logger.warning(f"⚠️ Attempted to store expired password reset token for {email}")
|
||||||
@ -2370,7 +2376,7 @@ class RedisDatabase:
|
|||||||
token_data["used"] = True
|
token_data["used"] = True
|
||||||
token_data["used_at"] = datetime.now(timezone.utc).isoformat()
|
token_data["used_at"] = datetime.now(timezone.utc).isoformat()
|
||||||
await self.redis.set(key, json.dumps(token_data, default=str))
|
await self.redis.set(key, json.dumps(token_data, default=str))
|
||||||
logger.debug(f"🔐 Marked password reset token as used")
|
logger.info(f"🔐 Marked password reset token as used")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -2398,7 +2404,7 @@ class RedisDatabase:
|
|||||||
# Set expiration for 30 days
|
# Set expiration for 30 days
|
||||||
await self.redis.expire(key, 30 * 24 * 60 * 60)
|
await self.redis.expire(key, 30 * 24 * 60 * 60)
|
||||||
|
|
||||||
logger.debug(f"🔒 Logged security event {event_type} for user {user_id}")
|
logger.info(f"🔒 Logged security event {event_type} for user {user_id}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error logging security event for {user_id}: {e}")
|
logger.error(f"❌ Error logging security event for {user_id}: {e}")
|
||||||
@ -2443,7 +2449,7 @@ class RedisDatabase:
|
|||||||
json.dumps(guest_data)
|
json.dumps(guest_data)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(f"💾 Guest stored with backup: {guest_id}")
|
logger.info(f"💾 Guest stored with backup: {guest_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error storing guest {guest_id}: {e}")
|
logger.error(f"❌ Error storing guest {guest_id}: {e}")
|
||||||
raise
|
raise
|
||||||
@ -2458,7 +2464,7 @@ class RedisDatabase:
|
|||||||
# Update last activity when accessed
|
# Update last activity when accessed
|
||||||
guest_data["last_activity"] = datetime.now(UTC).isoformat()
|
guest_data["last_activity"] = datetime.now(UTC).isoformat()
|
||||||
await self.set_guest(guest_id, guest_data)
|
await self.set_guest(guest_id, guest_data)
|
||||||
logger.debug(f"🔍 Guest found in primary storage: {guest_id}")
|
logger.info(f"🔍 Guest found in primary storage: {guest_id}")
|
||||||
return guest_data
|
return guest_data
|
||||||
|
|
||||||
# Fallback to backup storage
|
# Fallback to backup storage
|
||||||
@ -2534,7 +2540,7 @@ class RedisDatabase:
|
|||||||
created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
|
created_at = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
|
||||||
if current_time - created_at < timedelta(hours=1):
|
if current_time - created_at < timedelta(hours=1):
|
||||||
preserved_count += 1
|
preserved_count += 1
|
||||||
logger.debug(f"🛡️ Preserving new guest: {guest_id}")
|
logger.info(f"🛡️ Preserving new guest: {guest_id}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check last activity
|
# Check last activity
|
||||||
@ -2760,4 +2766,5 @@ class DatabaseManager:
|
|||||||
if self._shutdown_initiated:
|
if self._shutdown_initiated:
|
||||||
raise RuntimeError("Application is shutting down")
|
raise RuntimeError("Application is shutting down")
|
||||||
return self.db
|
return self.db
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ class EmailService:
|
|||||||
self.email_password = os.getenv("EMAIL_PASSWORD")
|
self.email_password = os.getenv("EMAIL_PASSWORD")
|
||||||
self.from_name = os.getenv("FROM_NAME", "Backstory")
|
self.from_name = os.getenv("FROM_NAME", "Backstory")
|
||||||
self.app_name = os.getenv("APP_NAME", "Backstory")
|
self.app_name = os.getenv("APP_NAME", "Backstory")
|
||||||
self.frontend_url = os.getenv("FRONTEND_URL", "https://backstory-beta.ketrenos.com")
|
self.frontend_url = os.getenv("FRONTEND_URL", "https://backstory.ketrenos.com")
|
||||||
if not self.smtp_server or self.smtp_port == 0 or self.email_user is None or self.email_password is None:
|
if not self.smtp_server or self.smtp_port == 0 or self.email_user is None or self.email_password is None:
|
||||||
raise ValueError("SMTP configuration is not set in the environment variables")
|
raise ValueError("SMTP configuration is not set in the environment variables")
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user