refactor, security updates, cv extraction upgrades

This commit is contained in:
2026-04-11 01:34:32 +02:00
parent 806b200ac5
commit 27fd70a2d7
59 changed files with 6817 additions and 1561 deletions
+4 -4
View File
@@ -5,7 +5,7 @@ import { Box, Button, Checkbox, FormControlLabel, Paper, Tab, Tabs, TextField, T
import { useLocation, useNavigate } from "react-router-dom";
import { api, getApiErrorMessage } from "../api";
import { getRememberMePref, setAuthToken, setRememberMePref } from "../auth";
import { getRememberMePref, setAuthPersistencePreference } from "../auth";
import GoogleAuthCard from "../components/GoogleAuthCard";
import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
@@ -44,9 +44,9 @@ export default function LoginPage() {
setLoading(true);
try {
const url = mode === "register" ? "/auth/register" : "/auth/login";
const res = await api.post<{ accessToken: string; tokenType: string }>(url, { email, password });
setRememberMePref(rememberMe);
setAuthToken(res.data.accessToken, rememberMe ? "local" : "session");
await api.post(url, { email, password, rememberMe });
setAuthPersistencePreference(rememberMe ? "local" : "session");
await api.get("/auth/me");
toast(t("signedIn"), "success");
navigate(nextPath, { replace: true });
} catch (e: any) {
+53 -10
View File
@@ -42,6 +42,12 @@ type ExtractionRun = {
errorMessage?: string;
};
type QueuedCvRunResponse = {
queued: boolean;
extractionRunId: number;
status: string;
};
type JobListResponse = {
items: JobApplication[];
total: number;
@@ -199,6 +205,7 @@ export default function ProfilePage() {
const avatarInputRef = useRef<HTMLInputElement | null>(null);
const [me, setMe] = useState<MeResponse | null>(null);
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [uploadingCv, setUploadingCv] = useState(false);
const [improvingCv, setImprovingCv] = useState(false);
const [rebuildingCv, setRebuildingCv] = useState(false);
@@ -225,10 +232,12 @@ export default function ProfilePage() {
const [reprocessingCv, setReprocessingCv] = useState(false);
const [structuredCv, setStructuredCv] = useState<StructuredCvProfile>(emptyStructuredCv());
const [extractionRuns, setExtractionRuns] = useState<ExtractionRun[]>([]);
const runStatusRef = useRef<Record<number, string>>({});
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const loadProfile = useCallback(async () => {
setLoading(true);
try {
const [profileResponse, runsResponse, jobsResponse] = await Promise.all([
api.get<MeResponse>("/auth/me"),
@@ -247,10 +256,14 @@ export default function ProfilePage() {
setExtractionRuns(runsResponse.data ?? []);
setSavedJobs(jobsResponse.data?.items ?? []);
setHeadline(window.localStorage.getItem("profileHeadline") ?? "");
} catch {
setLoadError(null);
} catch (error: any) {
setMe(null);
setExtractionRuns([]);
setSavedJobs([]);
setLoadError(String(error?.response?.data || error?.message || "Unable to load profile right now."));
} finally {
setLoading(false);
}
}, []);
@@ -258,6 +271,31 @@ export default function ProfilePage() {
void loadProfile();
}, [loadProfile]);
useEffect(() => {
const activeRuns = extractionRuns.filter((run) => run.status === "queued" || run.status === "running");
if (activeRuns.length === 0) return;
const timer = window.setInterval(() => {
void loadProfile();
}, 4000);
return () => window.clearInterval(timer);
}, [extractionRuns, loadProfile]);
useEffect(() => {
const previous = runStatusRef.current;
for (const run of extractionRuns) {
const prior = previous[run.id];
if ((prior === "queued" || prior === "running") && run.status === "applied") {
toast(`CV ${run.trigger} completed.`, "success");
}
if ((prior === "queued" || prior === "running") && run.status === "failed") {
toast(run.errorMessage || `CV ${run.trigger} failed.`, "error");
}
previous[run.id] = run.status;
}
}, [extractionRuns, toast]);
const initials = useMemo(() => initialsFrom([me?.displayName, me?.firstName, me?.lastName, me?.userName, me?.email]), [me]);
const isLocal = me?.provider === "local";
const fullName = [me?.firstName, me?.lastName].filter(Boolean).join(" ");
@@ -305,6 +343,13 @@ export default function ProfilePage() {
}}
/>
{loadError ? (
<Alert severity="error" sx={{ mb: 2, borderRadius: 2.5 }} action={<Button color="inherit" size="small" onClick={() => void loadProfile()}>Retry</Button>}>
Unable to load profile.
<Typography variant="body2" sx={{ mt: 0.5 }}>{loadError}</Typography>
</Alert>
) : null}
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap" }}>
<Box sx={{ display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }}>
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 1 }}>
@@ -413,7 +458,7 @@ export default function ProfilePage() {
formData.append("file", file);
setUploadingCv(true);
try {
await api.post("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
await api.post<QueuedCvRunResponse>("/profile-cv/upload", formData, { headers: { "Content-Type": "multipart/form-data" } });
await loadProfile();
toast(t("profileCvUploaded"), "success");
} catch (e: any) {
@@ -432,10 +477,9 @@ export default function ProfilePage() {
onClick={async () => {
setRebuildingCv(true);
try {
const res = await api.post<{ text?: string }>("/profile-cv/rebuild");
if (res.data?.text) setProfileCvText(res.data.text);
const res = await api.post<QueuedCvRunResponse>("/profile-cv/rebuild");
await loadProfile();
toast(t("profileCvRebuilt"), "success");
toast(`Queued CV rebuild (run ${res.data.extractionRunId}).`, "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvRebuildFailed")), "error");
} finally {
@@ -451,10 +495,9 @@ export default function ProfilePage() {
onClick={async () => {
setImprovingCv(true);
try {
const res = await api.post<{ text?: string }>("/profile-cv/improve");
if (res.data?.text) setProfileCvText(res.data.text);
const res = await api.post<QueuedCvRunResponse>("/profile-cv/improve");
await loadProfile();
toast(t("profileCvImproved"), "success");
toast(`Queued CV improve run (run ${res.data.extractionRunId}).`, "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvImproveFailed")), "error");
} finally {
@@ -470,9 +513,9 @@ export default function ProfilePage() {
onClick={async () => {
setReprocessingCv(true);
try {
await api.post("/profile-cv/reprocess");
const res = await api.post<QueuedCvRunResponse>("/profile-cv/reprocess");
await loadProfile();
toast(t("profileCvReprocessed"), "success");
toast(`Queued CV reprocess run (run ${res.data.extractionRunId}).`, "info");
} catch (e: any) {
toast(String(e?.response?.data || e?.message || t("profileCvReprocessFailed")), "error");
} finally {