From 1fd6a4b4c2d9946fef29272384e27837eb304a3d Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Tue, 20 Jan 2026 22:11:09 +0000 Subject: [PATCH] initial commit --- orchestrator/package-lock.json | 76 +++++++++++ orchestrator/package.json | 1 + orchestrator/src/client/api/client.ts | 7 + .../src/client/components/JobHeader.tsx | 126 +++++++++++++++++- .../src/client/components/ReadyPanel.tsx | 11 +- .../discovered-panel/DecideMode.tsx | 4 +- .../discovered-panel/DiscoveredPanel.tsx | 4 + .../client/pages/OrchestratorPage.test.tsx | 4 +- .../src/client/pages/SettingsPage.test.tsx | 3 + .../src/client/pages/SettingsPage.tsx | 24 +++- .../orchestrator/JobDetailPanel.test.tsx | 2 + .../pages/orchestrator/JobDetailPanel.tsx | 8 +- .../pages/orchestrator/JobListPanel.test.tsx | 2 + .../components/DisplaySettingsSection.tsx | 77 +++++++++++ orchestrator/src/components/ui/tooltip.tsx | 32 +++++ .../src/server/api/routes/jobs.test.ts | 2 +- orchestrator/src/server/api/routes/jobs.ts | 59 ++++++++ .../src/server/api/routes/settings.ts | 38 +++++- orchestrator/src/server/db/migrate.ts | 4 + orchestrator/src/server/db/schema.ts | 2 + .../src/server/pipeline/orchestrator.ts | 28 +++- orchestrator/src/server/repositories/jobs.ts | 28 ++-- .../src/server/repositories/settings.ts | 1 + .../src/server/services/ai-resilience.test.ts | 30 +++-- orchestrator/src/shared/types.ts | 9 +- 25 files changed, 537 insertions(+), 45 deletions(-) create mode 100644 orchestrator/src/client/pages/settings/components/DisplaySettingsSection.tsx create mode 100644 orchestrator/src/components/ui/tooltip.tsx diff --git a/orchestrator/package-lock.json b/orchestrator/package-lock.json index 5322da3..2af6a58 100644 --- a/orchestrator/package-lock.json +++ b/orchestrator/package-lock.json @@ -17,6 +17,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "better-sqlite3": "^11.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2231,6 +2232,58 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", @@ -2359,6 +2412,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", diff --git a/orchestrator/package.json b/orchestrator/package.json index 1130faf..d810575 100644 --- a/orchestrator/package.json +++ b/orchestrator/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "better-sqlite3": "^11.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index 9b3cae5..6558b78 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -86,6 +86,12 @@ export async function generateJobPdf(id: string): Promise { }); } +export async function checkSponsor(id: string): Promise { + return fetchApi(`/jobs/${id}/check-sponsor`, { + method: 'POST', + }); +} + export async function markAsApplied(id: string): Promise { return fetchApi(`/jobs/${id}/apply`, { method: 'POST', @@ -186,6 +192,7 @@ export async function updateSettings(update: { jobspyCountryIndeed?: string | null jobspySites?: string[] | null jobspyLinkedinFetchDescription?: boolean | null + showSponsorInfo?: boolean | null }): Promise { return fetchApi('/settings', { method: 'PATCH', diff --git a/orchestrator/src/client/components/JobHeader.tsx b/orchestrator/src/client/components/JobHeader.tsx index 3d1f9be..6437604 100644 --- a/orchestrator/src/client/components/JobHeader.tsx +++ b/orchestrator/src/client/components/JobHeader.tsx @@ -1,6 +1,8 @@ -import React from "react"; -import { Calendar, DollarSign, MapPin } from "lucide-react"; +import React, { useMemo, useState } from "react"; +import { Calendar, DollarSign, Loader2, MapPin, Search, Shield } from "lucide-react"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { cn, formatDate, sourceLabel } from "@/lib/utils"; import type { Job, JobStatus } from "../../shared/types"; import { defaultStatusToken, statusTokens } from "../pages/orchestrator/constants"; @@ -8,6 +10,8 @@ import { defaultStatusToken, statusTokens } from "../pages/orchestrator/constant interface JobHeaderProps { job: Job; className?: string; + showSponsorInfo?: boolean; + onCheckSponsor?: () => Promise; } const StatusPill: React.FC<{ status: JobStatus }> = ({ status }) => { @@ -42,7 +46,112 @@ const ScoreMeter: React.FC<{ score: number | null }> = ({ score }) => { ); }; -export const JobHeader: React.FC = ({ job, className }) => { +interface SponsorBadgeProps { + score: number | null; + names: string | null; + onCheck?: () => Promise; +} + +const SponsorBadge: React.FC = ({ score, names, onCheck }) => { + const [isChecking, setIsChecking] = useState(false); + + const parsedNames = useMemo(() => { + if (!names) return []; + try { + return JSON.parse(names) as string[]; + } catch { + return []; + } + }, [names]); + + const handleCheck = async () => { + if (!onCheck) return; + setIsChecking(true); + try { + await onCheck(); + } finally { + setIsChecking(false); + } + }; + + // Show "Check" button if no score and callback provided + if (score == null && onCheck) { + return ( + + + + + + +

Check if employer is a visa sponsor

+
+
+
+ ); + } + + // If no score (and no callback), show nothing + if (score == null || score < 50) { + return null; + } + + // Color tokens based on score + const getScoreTokens = (s: number) => { + if (s >= 90) return { + badge: "border-emerald-500/40 bg-emerald-500/15 text-emerald-300", + label: "Visa Sponsor" + }; + if (s >= 70) return { + badge: "border-amber-500/40 bg-amber-500/15 text-amber-300", + label: "Likely Sponsor" + }; + return { + badge: "border-orange-500/40 bg-orange-500/15 text-orange-300", + label: "Possible Sponsor" + }; + }; + + const tokens = getScoreTokens(score); + const tooltipContent = parsedNames.length > 0 + ? `${score}% match: ${parsedNames.join(", ")}` + : `${score}% match with visa sponsor list`; + + return ( + + + + + + {score}% + + + +

{tooltipContent}

+
+
+
+ ); +}; + +export const JobHeader: React.FC = ({ job, className, showSponsorInfo = true, onCheckSponsor }) => { const deadline = formatDate(job.deadline); return ( @@ -51,7 +160,16 @@ export const JobHeader: React.FC = ({ job, className }) => {
{job.title}
-
{job.employer}
+
+ {job.employer} + {showSponsorInfo && ( + + )} +
{sourceLabel[job.source]} diff --git a/orchestrator/src/client/components/ReadyPanel.tsx b/orchestrator/src/client/components/ReadyPanel.tsx index c3f0de6..acc2ade 100644 --- a/orchestrator/src/client/components/ReadyPanel.tsx +++ b/orchestrator/src/client/components/ReadyPanel.tsx @@ -141,7 +141,7 @@ export const ReadyPanel: React.FC = ({ // Revert to ready status await api.updateJob(jobId, { status: "ready" }); toast.success("Reverted to Ready"); - + if (recentlyApplied?.timeoutId) { clearTimeout(recentlyApplied.timeoutId); } @@ -215,7 +215,14 @@ export const ReadyPanel: React.FC = ({ return (
- + { + await api.checkSponsor(job.id); + await onJobUpdated(); + }} + /> {/* ───────────────────────────────────────────────────────────────────── PRIMARY ACTION CLUSTER diff --git a/orchestrator/src/client/components/discovered-panel/DecideMode.tsx b/orchestrator/src/client/components/discovered-panel/DecideMode.tsx index df9aced..4308e4e 100644 --- a/orchestrator/src/client/components/discovered-panel/DecideMode.tsx +++ b/orchestrator/src/client/components/discovered-panel/DecideMode.tsx @@ -14,6 +14,7 @@ interface DecideModeProps { onTailor: () => void; onSkip: () => void; isSkipping: boolean; + onCheckSponsor?: () => Promise; } export const DecideMode: React.FC = ({ @@ -21,6 +22,7 @@ export const DecideMode: React.FC = ({ onTailor, onSkip, isSkipping, + onCheckSponsor, }) => { const [showDescription, setShowDescription] = useState(false); const jobLink = job.applicationLink || job.jobUrl; @@ -33,7 +35,7 @@ export const DecideMode: React.FC = ({ return (
- +