Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e8defb2
initial implementation
DaKheera47 Jan 23, 2026
f28d6d3
onboarding doesn't pop until invalid values are present
DaKheera47 Jan 23, 2026
dc501b9
link to job page
DaKheera47 Jan 23, 2026
a2c2b99
proactive inputs working slightly
DaKheera47 Jan 24, 2026
c625a4e
onboarding gate reinstated
DaKheera47 Jan 24, 2026
88ee5de
better proactive buttons
DaKheera47 Jan 24, 2026
4e24199
fully manual tracking for now.
DaKheera47 Jan 24, 2026
f3e02e6
edit and delete timeline events
DaKheera47 Jan 24, 2026
f7e8415
status showing correctly
DaKheera47 Jan 24, 2026
9e4c93f
tests update
DaKheera47 Jan 24, 2026
fd4b1d4
tests
DaKheera47 Jan 24, 2026
3d51ac7
Update orchestrator/src/server/services/applicationTracking.ts
DaKheera47 Jan 24, 2026
31be173
Update orchestrator/src/server/services/applicationTracking.test.ts
DaKheera47 Jan 24, 2026
33b44f6
Update orchestrator/src/server/services/applicationTracking.test.ts
DaKheera47 Jan 24, 2026
d261dc0
Update orchestrator/src/client/pages/job/Timeline.tsx
DaKheera47 Jan 24, 2026
7fe9eeb
Update orchestrator/src/client/pages/JobPage.tsx
DaKheera47 Jan 24, 2026
fc6369d
add tests for application tracking routes and remove unused actionId …
DaKheera47 Jan 24, 2026
d3d6c2d
remove unnecessary await from synchronous transitionStage calls and i…
DaKheera47 Jan 24, 2026
621d10a
relax externalUrl validation to allow non-URL metadata
DaKheera47 Jan 24, 2026
bcca4f0
add toast notifications for data loading and event logging in JobPage
DaKheera47 Jan 24, 2026
2bbc4ec
comments
DaKheera47 Jan 24, 2026
204c234
Merge branch 'timeline-implementation' of https://github.com/DaKheera…
DaKheera47 Jan 24, 2026
eb020aa
chore: resolve merge conflicts in timeline-implementation
DaKheera47 Jan 26, 2026
5b139b5
fix: resolve type error in sponsor-matching.test.ts
DaKheera47 Jan 26, 2026
a8716f4
fix ci
DaKheera47 Jan 26, 2026
2939e36
tests fix for github
DaKheera47 Jan 26, 2026
2ae5169
lint
DaKheera47 Jan 26, 2026
0bd39c6
github comments
DaKheera47 Jan 26, 2026
83c0baa
build fix
DaKheera47 Jan 26, 2026
b721401
Merge branch 'main' into timeline-implementation
DaKheera47 Jan 27, 2026
7b190b0
dedupe
DaKheera47 Jan 27, 2026
63dd1fa
format
DaKheera47 Jan 27, 2026
86a6d7b
types fix
DaKheera47 Jan 27, 2026
f575f7e
Apply suggestion from @Copilot
DaKheera47 Jan 27, 2026
20a0c0b
formatting
DaKheera47 Jan 27, 2026
62dc5f6
Merge branch 'timeline-implementation' of https://github.com/DaKheera…
DaKheera47 Jan 27, 2026
0c4f43b
title and group id are discrete fields
DaKheera47 Jan 27, 2026
2171e52
backfill
DaKheera47 Jan 27, 2026
bbc643f
hide view button on page
DaKheera47 Jan 27, 2026
c765118
show relevant dropdown options
DaKheera47 Jan 27, 2026
51e4f4f
confetti!
DaKheera47 Jan 27, 2026
2dd6be0
remove redundant
DaKheera47 Jan 27, 2026
660a992
confirm delete is a custom element now
DaKheera47 Jan 27, 2026
22e6d16
formatting
DaKheera47 Jan 27, 2026
1e41ea0
fix styling
DaKheera47 Jan 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions orchestrator/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions orchestrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.8",
Expand All @@ -42,7 +43,9 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.18",
"@types/canvas-confetti": "^1.9.0",
"better-sqlite3": "^11.6.0",
"canvas-confetti": "^1.9.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cors": "^2.8.5",
Expand Down
4 changes: 3 additions & 1 deletion orchestrator/src/client/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
/**
* Main App component.
*/

Expand All @@ -8,6 +8,7 @@ import { CSSTransition, SwitchTransition } from "react-transition-group";

import { Toaster } from "@/components/ui/sonner";
import { OnboardingGate } from "./components/OnboardingGate";
import { JobPage } from "./pages/JobPage";
import { OrchestratorPage } from "./pages/OrchestratorPage";
import { SettingsPage } from "./pages/SettingsPage";
import { UkVisaJobsPage } from "./pages/UkVisaJobsPage";
Expand Down Expand Up @@ -40,6 +41,7 @@ export const App: React.FC = () => {
<div ref={nodeRef}>
<Routes location={location}>
<Route path="/" element={<Navigate to="/ready" replace />} />
<Route path="/job/:id" element={<JobPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/ukvisajobs" element={<UkVisaJobsPage />} />
<Route path="/visa-sponsors" element={<VisaSponsorsPage />} />
Expand Down
73 changes: 72 additions & 1 deletion orchestrator/src/client/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import { trackEvent } from "@/lib/analytics";
import type {
ApiResponse,
ApplicationStage,
ApplicationTask,
AppSettings,
CreateJobInput,
Job,
JobOutcome,
JobSource,
JobsListResponse,
ManualJobDraft,
Expand All @@ -18,6 +21,9 @@ import type {
ResumeProfile,
ResumeProjectCatalogItem,
ResumeProjectsSettings,
StageEvent,
StageEventMetadata,
StageTransitionTarget,
UkVisaJobsImportResponse,
UkVisaJobsSearchResponse,
ValidationResult,
Expand Down Expand Up @@ -67,7 +73,7 @@ export async function getJobs(statuses?: string[]): Promise<JobsListResponse> {
}

export async function getJob(id: string): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}`);
return fetchApi<Job>(`/jobs/${id}?t=${Date.now()}`);
}

export async function updateJob(
Expand Down Expand Up @@ -130,6 +136,71 @@ export async function skipJob(id: string): Promise<Job> {
});
}

export async function getJobStageEvents(id: string): Promise<StageEvent[]> {
return fetchApi<StageEvent[]>(`/jobs/${id}/events?t=${Date.now()}`);
}

export async function getJobTasks(
id: string,
options?: { includeCompleted?: boolean },
): Promise<ApplicationTask[]> {
const params = new URLSearchParams();
if (options?.includeCompleted) params.set("includeCompleted", "1");
params.set("t", Date.now().toString());
const query = params.toString();
return fetchApi<ApplicationTask[]>(`/jobs/${id}/tasks?${query}`);
}

export async function transitionJobStage(
id: string,
input: {
toStage: StageTransitionTarget;
occurredAt?: number | null;
metadata?: StageEventMetadata | null;
outcome?: JobOutcome | null;
},
): Promise<StageEvent> {
Comment thread
DaKheera47 marked this conversation as resolved.
return fetchApi<StageEvent>(`/jobs/${id}/stages`, {
method: "POST",
body: JSON.stringify(input),
});
}

export async function updateJobStageEvent(
id: string,
eventId: string,
input: {
toStage?: ApplicationStage;
occurredAt?: number | null;
metadata?: StageEventMetadata | null;
outcome?: JobOutcome | null;
},
): Promise<void> {
return fetchApi<void>(`/jobs/${id}/events/${eventId}`, {
method: "PATCH",
body: JSON.stringify(input),
});
}

export async function deleteJobStageEvent(
id: string,
eventId: string,
): Promise<void> {
return fetchApi<void>(`/jobs/${id}/events/${eventId}`, {
method: "DELETE",
});
}

export async function updateJobOutcome(
id: string,
input: { outcome: JobOutcome | null; closedAt?: number | null },
): Promise<Job> {
return fetchApi<Job>(`/jobs/${id}/outcome`, {
method: "PATCH",
body: JSON.stringify(input),
});
}

// Pipeline API
export async function getPipelineStatus(): Promise<PipelineStatusResponse> {
return fetchApi<PipelineStatusResponse>("/pipeline/status");
Expand Down
50 changes: 50 additions & 0 deletions orchestrator/src/client/components/ConfirmDelete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type React from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";

interface ConfirmDeleteProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title?: string;
description?: string;
}

export const ConfirmDelete: React.FC<ConfirmDeleteProps> = ({
isOpen,
onClose,
onConfirm,
title = "Are you sure?",
description = "This action cannot be undone. This will permanently delete this event from the timeline.",
}) => {
return (
<AlertDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onClose}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
onConfirm();
onClose();
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
50 changes: 42 additions & 8 deletions orchestrator/src/client/components/JobHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import type React from "react";
import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Job } from "../../shared/types";
import { useSettings } from "../hooks/useSettings";
Expand Down Expand Up @@ -37,6 +38,8 @@ const mockJob: Job = {
salary: "£60,000",
deadline: "2025-12-31",
status: "discovered",
outcome: null,
closedAt: null,
source: "linkedin",
suitabilityScore: 85,
suitabilityReason: "Strong match",
Expand All @@ -46,6 +49,9 @@ const mockJob: Job = {
} as Job;

describe("JobHeader", () => {
const renderWithRouter = (ui: React.ReactElement) =>
render(<MemoryRouter>{ui}</MemoryRouter>);

beforeEach(() => {
vi.clearAllMocks();
(useSettings as any).mockReturnValue({
Expand All @@ -54,21 +60,37 @@ describe("JobHeader", () => {
});

it("renders basic job information", () => {
render(<JobHeader job={mockJob} />);
renderWithRouter(<JobHeader job={mockJob} />);
expect(screen.getByText("Software Engineer")).toBeInTheDocument();
expect(screen.getByText("Tech Corp")).toBeInTheDocument();
expect(screen.getByText("London")).toBeInTheDocument();
expect(screen.getByText("£60,000")).toBeInTheDocument();
});

it("links the title and view button to the job page", () => {
renderWithRouter(<JobHeader job={mockJob} />);

expect(
screen.getByRole("link", { name: "Software Engineer" }),
).toHaveAttribute("href", "/job/job-1");
expect(screen.getByRole("link", { name: /view/i })).toHaveAttribute(
"href",
"/job/job-1",
);
});

it("shows 'Check Sponsorship Status' button when sponsorMatchScore is null", async () => {
const onCheckSponsor = vi.fn().mockResolvedValue(undefined);
render(<JobHeader job={mockJob} onCheckSponsor={onCheckSponsor} />);
renderWithRouter(
<JobHeader job={mockJob} onCheckSponsor={onCheckSponsor} />,
);

const button = screen.getByText("Check Sponsorship Status");
expect(button).toBeInTheDocument();

fireEvent.click(button);
await act(async () => {
fireEvent.click(button);
});

expect(onCheckSponsor).toHaveBeenCalled();
});
Expand All @@ -79,7 +101,7 @@ describe("JobHeader", () => {
sponsorMatchScore: 98,
sponsorMatchNames: '["Tech Corp Ltd"]',
};
render(<JobHeader job={jobWithSponsor} />);
renderWithRouter(<JobHeader job={jobWithSponsor} />);

expect(screen.getByText("Confirmed Sponsor")).toBeInTheDocument();
});
Expand All @@ -90,7 +112,7 @@ describe("JobHeader", () => {
sponsorMatchScore: 85,
sponsorMatchNames: '["Techy Corp"]',
};
render(<JobHeader job={jobWithPotential} />);
renderWithRouter(<JobHeader job={jobWithPotential} />);

expect(screen.getByText("Potential Sponsor")).toBeInTheDocument();
});
Expand All @@ -101,7 +123,7 @@ describe("JobHeader", () => {
sponsorMatchScore: 40,
sponsorMatchNames: '["Other Corp"]',
};
render(<JobHeader job={jobNoSponsor} />);
renderWithRouter(<JobHeader job={jobNoSponsor} />);

expect(screen.getByText("Sponsor Not Found")).toBeInTheDocument();
});
Expand All @@ -112,11 +134,23 @@ describe("JobHeader", () => {
});

const jobWithSponsor = { ...mockJob, sponsorMatchScore: 98 };
render(<JobHeader job={jobWithSponsor} />);
renderWithRouter(<JobHeader job={jobWithSponsor} />);

expect(screen.queryByText("Confirmed Sponsor")).not.toBeInTheDocument();
expect(
screen.queryByText("Check Sponsorship Status"),
).not.toBeInTheDocument();
});

it("hides the view button when already on a job page", () => {
render(
<MemoryRouter initialEntries={["/job/job-1"]}>
<JobHeader job={mockJob} />
</MemoryRouter>,
);

expect(
screen.queryByRole("link", { name: /view/i }),
).not.toBeInTheDocument();
});
});
Loading