Jobber/shared/src/types.ts
Shaheer Sarfaraz 687fd5e91f
feat(post-application): automatically pull from email (#145)
* feat(post-application): add schema and shared types for provider ingestion (#136)

* test(orchestrator): ensure full localStorage shape in vitest setup

* feat(post-application): add provider registry and dispatcher framework (#137) (#146)

* Implement Gmail provider credential persistence (#147)

* Add unified post-application provider action API (#148)

* Implement Gmail ingestion sync with 95/60 relevance policy

* Implement Gmail ingestion sync with 95/60 relevance policy (#149)

* feat(post-application): add job mapping engine with llm rerank fallback

* feat(post-application): add inbox review APIs with transactional approve/deny (#151)

* feat(post-application): add tracking inbox UI with provider controls (#152)

* oauth implementation

* UI changes

* see past runs in more detail

* occurred at comes from email

* state mismatch

* better UI representation

* comments

* comments

* comments

* comments

* documentation

* explainer

* set things manually

* scrolling

* any found email can be pending

* searchable download

* Email-to-Job Matching Decision Tree

* email viewer list improvement

* simplification initial commit

* exclude discovered jobs

* show only resady

* dropdown

* mermaid

* syntax

* targets is the same as logging that is done manually

* event label

* duplicate avoidance

* clean up html

* token saving

* print

* send idx not uuid

* remove logging

* formatting

* better documentation

* documentation

* comments

* process all

* comments
2026-02-12 19:48:25 +00:00

855 lines
20 KiB
TypeScript

/**
* Shared types for the job-ops orchestrator.
*/
export type JobStatus =
| "discovered" // Crawled but not processed
| "processing" // Currently generating resume
| "ready" // PDF generated, waiting for user to apply
| "applied" // User marked as applied
| "skipped" // User skipped this job
| "expired"; // Deadline passed
export const APPLICATION_STAGES = [
"applied",
"recruiter_screen",
"assessment",
"hiring_manager_screen",
"technical_interview",
"onsite",
"offer",
"closed",
] as const;
export type ApplicationStage = (typeof APPLICATION_STAGES)[number];
export const STAGE_LABELS: Record<ApplicationStage, string> = {
applied: "Applied",
recruiter_screen: "Recruiter Screen",
assessment: "Assessment",
hiring_manager_screen: "Hiring Manager Screen",
technical_interview: "Technical Interview",
onsite: "Final Round",
offer: "Offer",
closed: "Closed",
};
export type StageTransitionTarget = ApplicationStage | "no_change";
export const APPLICATION_OUTCOMES = [
"offer_accepted",
"offer_declined",
"rejected",
"withdrawn",
"no_response",
"ghosted",
] as const;
export type JobOutcome = (typeof APPLICATION_OUTCOMES)[number];
export const APPLICATION_TASK_TYPES = [
"prep",
"todo",
"follow_up",
"check_status",
] as const;
export type ApplicationTaskType = (typeof APPLICATION_TASK_TYPES)[number];
export const INTERVIEW_TYPES = [
"recruiter_screen",
"technical",
"onsite",
"panel",
"behavioral",
"final",
] as const;
export type InterviewType = (typeof INTERVIEW_TYPES)[number];
export const INTERVIEW_OUTCOMES = [
"pass",
"fail",
"pending",
"cancelled",
] as const;
export type InterviewOutcome = (typeof INTERVIEW_OUTCOMES)[number];
export interface StageEventMetadata {
note?: string | null;
actor?: "system" | "user";
groupId?: string | null;
groupLabel?: string | null;
eventLabel?: string | null;
externalUrl?: string | null;
reasonCode?: string | null;
eventType?: "interview_log" | "status_update" | "note" | null;
}
export interface StageEvent {
id: string;
applicationId: string;
title: string;
groupId: string | null;
fromStage: ApplicationStage | null;
toStage: ApplicationStage;
occurredAt: number;
metadata: StageEventMetadata | null;
outcome: JobOutcome | null;
}
export interface ApplicationTask {
id: string;
applicationId: string;
type: ApplicationTaskType;
title: string;
dueDate: number | null;
isCompleted: boolean;
notes: string | null;
}
export interface Interview {
id: string;
applicationId: string;
scheduledAt: number;
durationMins: number | null;
type: InterviewType;
outcome: InterviewOutcome | null;
}
export type JobSource =
| "gradcracker"
| "indeed"
| "linkedin"
| "glassdoor"
| "ukvisajobs"
| "manual";
export interface Job {
id: string;
// Source / provenance
source: JobSource;
sourceJobId: string | null; // External ID (if provided)
jobUrlDirect: string | null; // Source-provided direct URL (if provided)
datePosted: string | null; // Source-provided posting date (if provided)
// From crawler (normalized)
title: string;
employer: string;
employerUrl: string | null;
jobUrl: string; // Gradcracker listing URL
applicationLink: string | null; // Actual application URL
disciplines: string | null;
deadline: string | null;
salary: string | null;
location: string | null;
degreeRequired: string | null;
starting: string | null;
jobDescription: string | null;
// Orchestrator enrichments
status: JobStatus;
outcome: JobOutcome | null;
closedAt: number | null;
suitabilityScore: number | null; // 0-100 AI-generated score
suitabilityReason: string | null; // AI explanation
tailoredSummary: string | null; // Generated resume summary
tailoredHeadline: string | null; // Generated resume headline
tailoredSkills: string | null; // Generated resume skills (JSON)
selectedProjectIds: string | null; // Comma-separated IDs of selected projects
pdfPath: string | null; // Path to generated PDF
sponsorMatchScore: number | null; // 0-100 fuzzy match score with visa sponsors
sponsorMatchNames: string | null; // JSON array of matched sponsor names (when 100% matches or top match)
// JobSpy fields (nullable for non-JobSpy sources)
jobType: string | null;
salarySource: string | null;
salaryInterval: string | null;
salaryMinAmount: number | null;
salaryMaxAmount: number | null;
salaryCurrency: string | null;
isRemote: boolean | null;
jobLevel: string | null;
jobFunction: string | null;
listingType: string | null;
emails: string | null;
companyIndustry: string | null;
companyLogo: string | null;
companyUrlDirect: string | null;
companyAddresses: string | null;
companyNumEmployees: string | null;
companyRevenue: string | null;
companyDescription: string | null;
skills: string | null;
experienceRange: string | null;
companyRating: number | null;
companyReviewsCount: number | null;
vacancyCount: number | null;
workFromHomeType: string | null;
// Timestamps
discoveredAt: string;
processedAt: string | null;
appliedAt: string | null;
createdAt: string;
updatedAt: string;
}
export type JobListItem = Pick<
Job,
| "id"
| "source"
| "title"
| "employer"
| "jobUrl"
| "applicationLink"
| "datePosted"
| "deadline"
| "salary"
| "location"
| "status"
| "suitabilityScore"
| "sponsorMatchScore"
| "jobType"
| "jobFunction"
| "salaryMinAmount"
| "salaryMaxAmount"
| "salaryCurrency"
| "discoveredAt"
| "appliedAt"
| "updatedAt"
>;
export interface CreateJobInput {
source: JobSource;
title: string;
employer: string;
employerUrl?: string;
jobUrl: string;
applicationLink?: string;
disciplines?: string;
deadline?: string;
salary?: string;
location?: string;
degreeRequired?: string;
starting?: string;
jobDescription?: string;
// JobSpy fields (optional)
sourceJobId?: string;
jobUrlDirect?: string;
datePosted?: string;
jobType?: string;
salarySource?: string;
salaryInterval?: string;
salaryMinAmount?: number;
salaryMaxAmount?: number;
salaryCurrency?: string;
isRemote?: boolean;
jobLevel?: string;
jobFunction?: string;
listingType?: string;
emails?: string;
companyIndustry?: string;
companyLogo?: string;
companyUrlDirect?: string;
companyAddresses?: string;
companyNumEmployees?: string;
companyRevenue?: string;
companyDescription?: string;
skills?: string;
experienceRange?: string;
companyRating?: number;
companyReviewsCount?: number;
vacancyCount?: number;
workFromHomeType?: string;
}
export interface ManualJobDraft {
title?: string;
employer?: string;
jobUrl?: string;
applicationLink?: string;
location?: string;
salary?: string;
deadline?: string;
jobDescription?: string;
jobType?: string;
jobLevel?: string;
jobFunction?: string;
disciplines?: string;
degreeRequired?: string;
starting?: string;
}
export interface ManualJobInferenceResponse {
job: ManualJobDraft;
warning?: string | null;
}
export interface ManualJobFetchResponse {
content: string;
url: string;
}
export interface UpdateJobInput {
title?: string;
employer?: string;
jobUrl?: string;
applicationLink?: string | null;
location?: string | null;
salary?: string | null;
deadline?: string | null;
status?: JobStatus;
outcome?: JobOutcome | null;
closedAt?: number | null;
jobDescription?: string | null;
suitabilityScore?: number;
suitabilityReason?: string;
tailoredSummary?: string;
tailoredHeadline?: string;
tailoredSkills?: string;
selectedProjectIds?: string;
pdfPath?: string;
appliedAt?: string;
sponsorMatchScore?: number;
sponsorMatchNames?: string;
}
export interface PipelineConfig {
topN: number; // Number of top jobs to process
minSuitabilityScore: number; // Minimum score to auto-process
sources: JobSource[]; // Job sources to crawl
outputDir: string; // Directory for generated PDFs
enableCrawling?: boolean;
enableScoring?: boolean;
enableImporting?: boolean;
enableAutoTailoring?: boolean;
}
export interface PipelineRun {
id: string;
startedAt: string;
completedAt: string | null;
status: "running" | "completed" | "failed" | "cancelled";
jobsDiscovered: number;
jobsProcessed: number;
errorMessage: string | null;
}
// API Response types
export interface ApiMeta {
requestId: string;
simulated?: boolean;
blockedReason?: string;
}
export interface ApiErrorPayload {
code: string;
message: string;
details?: unknown;
}
export type ApiResponse<T> =
| {
ok: true;
data: T;
meta?: ApiMeta;
}
| {
ok: false;
error: ApiErrorPayload;
meta: ApiMeta;
};
export const POST_APPLICATION_PROVIDERS = ["gmail", "imap"] as const;
export type PostApplicationProvider =
(typeof POST_APPLICATION_PROVIDERS)[number];
export const POST_APPLICATION_PROVIDER_ACTIONS = [
"connect",
"status",
"sync",
"disconnect",
] as const;
export type PostApplicationProviderAction =
(typeof POST_APPLICATION_PROVIDER_ACTIONS)[number];
export const POST_APPLICATION_INTEGRATION_STATUSES = [
"disconnected",
"connected",
"error",
] as const;
export type PostApplicationIntegrationStatus =
(typeof POST_APPLICATION_INTEGRATION_STATUSES)[number];
export const POST_APPLICATION_SYNC_RUN_STATUSES = [
"running",
"completed",
"failed",
"cancelled",
] as const;
export type PostApplicationSyncRunStatus =
(typeof POST_APPLICATION_SYNC_RUN_STATUSES)[number];
export const POST_APPLICATION_RELEVANCE_DECISIONS = [
"relevant",
"not_relevant",
"needs_llm",
] as const;
export type PostApplicationRelevanceDecision =
(typeof POST_APPLICATION_RELEVANCE_DECISIONS)[number];
export const POST_APPLICATION_MESSAGE_TYPES = [
"interview",
"rejection",
"offer",
"update",
"other",
] as const;
export type PostApplicationMessageType =
(typeof POST_APPLICATION_MESSAGE_TYPES)[number];
export const POST_APPLICATION_ROUTER_STAGE_TARGETS = [
"no_change",
"applied",
"recruiter_screen",
"assessment",
"hiring_manager_screen",
"technical_interview",
"onsite",
"offer",
"rejected",
"withdrawn",
"closed",
] as const;
export type PostApplicationRouterStageTarget =
(typeof POST_APPLICATION_ROUTER_STAGE_TARGETS)[number];
export const POST_APPLICATION_PROCESSING_STATUSES = [
"auto_linked",
"pending_user",
"manual_linked",
"ignored",
] as const;
export type PostApplicationProcessingStatus =
(typeof POST_APPLICATION_PROCESSING_STATUSES)[number];
export interface PostApplicationIntegration {
id: string;
provider: PostApplicationProvider;
accountKey: string;
displayName: string | null;
status: PostApplicationIntegrationStatus;
credentials: Record<string, unknown> | null;
lastConnectedAt: number | null;
lastSyncedAt: number | null;
lastError: string | null;
createdAt: string;
updatedAt: string;
}
export interface PostApplicationSyncRun {
id: string;
provider: PostApplicationProvider;
accountKey: string;
integrationId: string | null;
status: PostApplicationSyncRunStatus;
startedAt: number;
completedAt: number | null;
messagesDiscovered: number;
messagesRelevant: number;
messagesClassified: number;
messagesMatched: number;
messagesApproved: number;
messagesDenied: number;
messagesErrored: number;
errorCode: string | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
}
export interface PostApplicationMessage {
id: string;
provider: PostApplicationProvider;
accountKey: string;
integrationId: string | null;
syncRunId: string | null;
externalMessageId: string;
externalThreadId: string | null;
fromAddress: string;
fromDomain: string | null;
senderName: string | null;
subject: string;
receivedAt: number;
snippet: string;
classificationLabel: string | null;
classificationConfidence: number | null;
classificationPayload: Record<string, unknown> | null;
relevanceLlmScore: number | null;
relevanceDecision: PostApplicationRelevanceDecision;
matchedJobId: string | null;
matchConfidence: number | null;
stageTarget: PostApplicationRouterStageTarget | null;
messageType: PostApplicationMessageType;
stageEventPayload: Record<string, unknown> | null;
processingStatus: PostApplicationProcessingStatus;
decidedAt: number | null;
decidedBy: string | null;
errorCode: string | null;
errorMessage: string | null;
createdAt: string;
updatedAt: string;
}
export interface PostApplicationProviderActionConnectRequest {
accountKey?: string;
payload?: Record<string, unknown>;
}
export interface PostApplicationProviderActionSyncRequest {
accountKey?: string;
maxMessages?: number;
searchDays?: number;
}
export interface PostApplicationProviderStatus {
provider: PostApplicationProvider;
accountKey: string;
connected: boolean;
integration: PostApplicationIntegration | null;
}
export interface PostApplicationProviderActionResponse {
provider: PostApplicationProvider;
action: PostApplicationProviderAction;
accountKey: string;
status: PostApplicationProviderStatus;
message?: string;
}
export interface PostApplicationInboxItem {
message: PostApplicationMessage;
matchedJob?: {
id: string;
title: string;
employer: string;
} | null;
}
export type BulkPostApplicationAction = "approve" | "deny";
export interface BulkPostApplicationActionRequest {
action: BulkPostApplicationAction;
provider: PostApplicationProvider;
accountKey: string;
}
export type BulkPostApplicationActionResult =
| {
messageId: string;
ok: true;
message: PostApplicationMessage;
stageEventId?: string | null;
}
| {
messageId: string;
ok: false;
error: {
code: string;
message: string;
};
};
export interface BulkPostApplicationActionResponse {
action: BulkPostApplicationAction;
requested: number;
succeeded: number;
failed: number;
skipped: number;
results: BulkPostApplicationActionResult[];
}
export interface JobsListResponse<TJob = Job> {
jobs: TJob[];
total: number;
byStatus: Record<JobStatus, number>;
revision: string;
}
export interface JobsRevisionResponse {
revision: string;
latestUpdatedAt: string | null;
total: number;
statusFilter: string | null;
}
export type BulkJobAction = "skip" | "move_to_ready" | "rescore";
export interface BulkJobActionRequest {
action: BulkJobAction;
jobIds: string[];
}
export type BulkJobActionResult =
| {
jobId: string;
ok: true;
job: Job;
}
| {
jobId: string;
ok: false;
error: {
code: string;
message: string;
};
};
export interface BulkJobActionResponse {
action: BulkJobAction;
requested: number;
succeeded: number;
failed: number;
results: BulkJobActionResult[];
}
// Visa Sponsors types
export interface VisaSponsor {
organisationName: string;
townCity: string;
county: string;
typeRating: string;
route: string;
}
export interface VisaSponsorSearchResult {
sponsor: VisaSponsor;
score: number;
matchedName: string;
}
export interface VisaSponsorSearchResponse {
results: VisaSponsorSearchResult[];
query: string;
total: number;
}
export interface VisaSponsorStatusResponse {
lastUpdated: string | null;
csvPath: string | null;
totalSponsors: number;
isUpdating: boolean;
nextScheduledUpdate: string | null;
error: string | null;
}
export interface PipelineStatusResponse {
isRunning: boolean;
lastRun: PipelineRun | null;
nextScheduledRun: string | null;
}
export interface ResumeProjectCatalogItem {
id: string;
name: string;
description: string;
date: string;
isVisibleInBase: boolean;
}
export interface ResumeProjectsSettings {
maxProjects: number;
lockedProjectIds: string[];
aiSelectableProjectIds: string[];
}
export interface ResumeProfile {
basics?: {
name?: string;
label?: string;
image?: string;
email?: string;
phone?: string;
url?: string;
summary?: string;
headline?: string;
location?: {
address?: string;
postalCode?: string;
city?: string;
countryCode?: string;
region?: string;
};
profiles?: Array<{
network?: string;
username?: string;
url?: string;
}>;
};
sections?: {
summary?: {
id?: string;
visible?: boolean;
name?: string;
content?: string;
};
skills?: {
id?: string;
visible?: boolean;
name?: string;
items?: Array<{
id: string;
name: string;
description: string;
level: number;
keywords: string[];
visible: boolean;
}>;
};
projects?: {
id?: string;
visible?: boolean;
name?: string;
items?: Array<{
id: string;
name: string;
description: string;
date: string;
summary: string;
visible: boolean;
keywords?: string[];
url?: string;
}>;
};
experience?: {
id?: string;
visible?: boolean;
name?: string;
items?: Array<{
id: string;
company: string;
position: string;
location: string;
date: string;
summary: string;
visible: boolean;
}>;
};
[key: string]: unknown;
};
[key: string]: unknown;
}
export interface ProfileStatusResponse {
exists: boolean;
error: string | null;
}
export interface ValidationResult {
valid: boolean;
message: string | null;
}
export interface DemoInfoResponse {
demoMode: boolean;
resetCadenceHours: number;
lastResetAt: string | null;
nextResetAt: string | null;
baselineVersion: string | null;
baselineName: string | null;
}
export interface AppSettings {
model: string;
defaultModel: string;
overrideModel: string | null;
// Specific model overrides
modelScorer: string; // resolved
overrideModelScorer: string | null;
modelTailoring: string; // resolved
overrideModelTailoring: string | null;
modelProjectSelection: string; // resolved
overrideModelProjectSelection: string | null;
llmProvider: string;
defaultLlmProvider: string;
overrideLlmProvider: string | null;
llmBaseUrl: string;
defaultLlmBaseUrl: string;
overrideLlmBaseUrl: string | null;
pipelineWebhookUrl: string;
defaultPipelineWebhookUrl: string;
overridePipelineWebhookUrl: string | null;
jobCompleteWebhookUrl: string;
defaultJobCompleteWebhookUrl: string;
overrideJobCompleteWebhookUrl: string | null;
profileProjects: ResumeProjectCatalogItem[];
resumeProjects: ResumeProjectsSettings;
defaultResumeProjects: ResumeProjectsSettings;
overrideResumeProjects: ResumeProjectsSettings | null;
rxresumeBaseResumeId: string | null;
ukvisajobsMaxJobs: number;
defaultUkvisajobsMaxJobs: number;
overrideUkvisajobsMaxJobs: number | null;
gradcrackerMaxJobsPerTerm: number;
defaultGradcrackerMaxJobsPerTerm: number;
overrideGradcrackerMaxJobsPerTerm: number | null;
searchTerms: string[];
defaultSearchTerms: string[];
overrideSearchTerms: string[] | null;
jobspyLocation: string;
defaultJobspyLocation: string;
overrideJobspyLocation: string | null;
jobspyResultsWanted: number;
defaultJobspyResultsWanted: number;
overrideJobspyResultsWanted: number | null;
jobspyCountryIndeed: string;
defaultJobspyCountryIndeed: string;
overrideJobspyCountryIndeed: string | null;
showSponsorInfo: boolean;
defaultShowSponsorInfo: boolean;
overrideShowSponsorInfo: boolean | null;
llmApiKeyHint: string | null;
rxresumeEmail: string | null;
rxresumePasswordHint: string | null;
basicAuthUser: string | null;
basicAuthPasswordHint: string | null;
ukvisajobsEmail: string | null;
ukvisajobsPasswordHint: string | null;
webhookSecretHint: string | null;
basicAuthActive: boolean;
// Backup settings
backupEnabled: boolean;
defaultBackupEnabled: boolean;
overrideBackupEnabled: boolean | null;
backupHour: number;
defaultBackupHour: number;
overrideBackupHour: number | null;
backupMaxCount: number;
defaultBackupMaxCount: number;
overrideBackupMaxCount: number | null;
// Scoring settings
penalizeMissingSalary: boolean;
defaultPenalizeMissingSalary: boolean;
overridePenalizeMissingSalary: boolean | null;
missingSalaryPenalty: number;
defaultMissingSalaryPenalty: number;
overrideMissingSalaryPenalty: number | null;
// Auto-skip settings
autoSkipScoreThreshold: number | null;
defaultAutoSkipScoreThreshold: number | null;
overrideAutoSkipScoreThreshold: number | null;
}
export interface BackupInfo {
filename: string;
type: "auto" | "manual";
size: number;
createdAt: string;
}