Code cleanup (#218)
* chore: move @types/canvas-confetti to devDependencies, remove unused get-tsconfig direct dep * chore: configure knip with workspace entry points for all packages * refactor(shared): split 1119-line types.ts into domain modules under types/ * refactor: remove llm-service.ts shim, migrate all import sites to llm/service directly * refactor(settings): migrate 4 manually-resolved settings into conversion registry * refactor: split gmail-sync.ts into gmail-api, email-router, and thin orchestrator * refactor(orchestrator): extract useKeyboardShortcuts and usePipelineControls from OrchestratorPage Splits the 840-line OrchestratorPage into a thin orchestration shell (~480 lines) by extracting keyboard shortcut handling into useKeyboardShortcuts.ts and pipeline control logic into usePipelineControls.ts. Net negative line count across all files. * feat: create settings registry (Step 1) Introduces a single source of truth for all settings, combining schema definitions, default logic, parsing, and serialization into a single configuration object. * feat: derive schema, keys, and types from settings registry (Step 2) Derives AppSettings nested shape, SettingKey DB union, and updateSettingsSchema Zod shape automatically from the settings registry. * refactor: gut envSettings and remove settings-conversion (Step 3) Replaces manual env arrays with registry-driven maps in envSettings.ts. Deletes settings-conversion.ts since all parsing/defaults now live in the registry. * refactor: simplify getEffectiveSettings with generic loop (Step 4) Replaces ~334 lines of manual key-by-key unpacking with a generic registry-driven iteration loop (~40 lines). Models, typed, string, and virtual kinds are automatically derived. * refactor: simplify settingsUpdateRegistry (Step 5) Replaces ~350 lines of explicit per-key update handlers with a dynamic generic loop over the settings registry, properly routing persistence and side effects. * refactor(settings): implement nested settings registry and clean up tests - Migrate settings system to use a centralized nested registry (`settings-schema.ts`, `registry.ts`) - Remove obsolete flat-to-nested conversion logic (`settings-conversion.ts`) - Address Biome warnings by explicitly ignoring intentional `any` usage in generic runtime schema builder and registry logic - Clean up unused variables in test files (`SettingsPage.test.tsx`) to achieve a 100% green CI pipeline * refactor(settings): address PR comments on env data and registry parsing - Narrow `getEnvSettingsData` return type to `Partial<AppSettings>` to satisfy strict typing and omit 'typed' registry entries - Introduce `parseNonEmptyStringOrNull` for typed string settings so empty-string overrides cleanly fall back to defaults (matching original `||` logic) - Add missing unit tests for registry parse/serialize helpers (JSON, bools, numeric clamping)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { settingsRegistry } from "./settings-registry";
|
||||
|
||||
describe("settingsRegistry helpers", () => {
|
||||
describe("string parsing (parseNonEmptyStringOrNull)", () => {
|
||||
it("returns null for undefined", () => {
|
||||
expect(settingsRegistry.model.parse(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for empty string", () => {
|
||||
expect(settingsRegistry.searchCities.parse("")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the string for non-empty string", () => {
|
||||
expect(settingsRegistry.searchCities.parse("London")).toBe("London");
|
||||
});
|
||||
});
|
||||
|
||||
describe("number parsing and clamping", () => {
|
||||
it("returns null for empty/invalid values", () => {
|
||||
expect(settingsRegistry.ukvisajobsMaxJobs.parse("")).toBeNull();
|
||||
expect(settingsRegistry.ukvisajobsMaxJobs.parse("abc")).toBeNull();
|
||||
expect(settingsRegistry.ukvisajobsMaxJobs.parse(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("parses valid numbers", () => {
|
||||
expect(settingsRegistry.ukvisajobsMaxJobs.parse("42")).toBe(42);
|
||||
});
|
||||
|
||||
it("clamps backupHour to 0-23", () => {
|
||||
expect(settingsRegistry.backupHour.parse("25")).toBe(23);
|
||||
expect(settingsRegistry.backupHour.parse("-1")).toBe(0);
|
||||
expect(settingsRegistry.backupHour.parse("12")).toBe(12);
|
||||
});
|
||||
|
||||
it("clamps backupMaxCount to 1-5", () => {
|
||||
expect(settingsRegistry.backupMaxCount.parse("10")).toBe(5);
|
||||
expect(settingsRegistry.backupMaxCount.parse("0")).toBe(1);
|
||||
expect(settingsRegistry.backupMaxCount.parse("3")).toBe(3);
|
||||
});
|
||||
|
||||
it("clamps missingSalaryPenalty to 0-100", () => {
|
||||
expect(settingsRegistry.missingSalaryPenalty.parse("150")).toBe(100);
|
||||
expect(settingsRegistry.missingSalaryPenalty.parse("-10")).toBe(0);
|
||||
expect(settingsRegistry.missingSalaryPenalty.parse("50")).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("boolean (bit-bool) parsing and serialization", () => {
|
||||
it("parses bit bools correctly", () => {
|
||||
expect(settingsRegistry.showSponsorInfo.parse("1")).toBe(true);
|
||||
expect(settingsRegistry.showSponsorInfo.parse("true")).toBe(true);
|
||||
expect(settingsRegistry.showSponsorInfo.parse("0")).toBe(false);
|
||||
expect(settingsRegistry.showSponsorInfo.parse("false")).toBe(false);
|
||||
expect(settingsRegistry.showSponsorInfo.parse("")).toBeNull();
|
||||
expect(settingsRegistry.showSponsorInfo.parse(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("serializes bit bools correctly", () => {
|
||||
expect(settingsRegistry.showSponsorInfo.serialize(true)).toBe("1");
|
||||
expect(settingsRegistry.showSponsorInfo.serialize(false)).toBe("0");
|
||||
expect(settingsRegistry.showSponsorInfo.serialize(null)).toBeNull();
|
||||
expect(settingsRegistry.showSponsorInfo.serialize(undefined)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON array parsing", () => {
|
||||
it("parses valid JSON arrays", () => {
|
||||
expect(settingsRegistry.searchTerms.parse('["dev", "engineer"]')).toEqual(
|
||||
["dev", "engineer"],
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null for invalid JSON or non-arrays", () => {
|
||||
expect(settingsRegistry.searchTerms.parse('{"not": "array"}')).toBeNull();
|
||||
expect(settingsRegistry.searchTerms.parse("invalid json")).toBeNull();
|
||||
expect(settingsRegistry.searchTerms.parse("")).toBeNull();
|
||||
expect(settingsRegistry.searchTerms.parse(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("serializes arrays back to JSON", () => {
|
||||
expect(settingsRegistry.searchTerms.serialize(["dev", "engineer"])).toBe(
|
||||
'["dev","engineer"]',
|
||||
);
|
||||
expect(settingsRegistry.searchTerms.serialize(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resume projects settings", () => {
|
||||
it("parses and serializes resume projects", () => {
|
||||
const obj = {
|
||||
maxProjects: 10,
|
||||
lockedProjectIds: ["1", "2"],
|
||||
aiSelectableProjectIds: ["3"],
|
||||
};
|
||||
const json = JSON.stringify(obj);
|
||||
|
||||
expect(settingsRegistry.resumeProjects.parse(json)).toEqual(obj);
|
||||
expect(settingsRegistry.resumeProjects.parse("invalid")).toBeNull();
|
||||
|
||||
expect(settingsRegistry.resumeProjects.serialize(obj)).toBe(json);
|
||||
expect(settingsRegistry.resumeProjects.serialize(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,423 @@
|
||||
import { z } from "zod";
|
||||
import type { ResumeProjectsSettings } from "./types/settings";
|
||||
|
||||
function parseNonEmptyStringOrNull(raw: string | undefined): string | null {
|
||||
return raw === undefined || raw === "" ? null : raw;
|
||||
}
|
||||
|
||||
function parseIntOrNull(raw: string | undefined): number | null {
|
||||
if (!raw) return null;
|
||||
const parsed = parseInt(raw, 10);
|
||||
return Number.isNaN(parsed) ? null : parsed;
|
||||
}
|
||||
|
||||
function parseJsonArrayOrNull(raw: string | undefined): string[] | null {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? (parsed as string[]) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseBitBoolOrNull(raw: string | undefined): boolean | null {
|
||||
if (!raw) return null;
|
||||
return raw === "true" || raw === "1";
|
||||
}
|
||||
|
||||
function serializeNullableNumber(
|
||||
value: number | null | undefined,
|
||||
): string | null {
|
||||
return value !== null && value !== undefined ? String(value) : null;
|
||||
}
|
||||
|
||||
function serializeNullableJsonArray(
|
||||
value: string[] | null | undefined,
|
||||
): string | null {
|
||||
return value !== null && value !== undefined ? JSON.stringify(value) : null;
|
||||
}
|
||||
|
||||
function serializeBitBool(value: boolean | null | undefined): string | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
return value ? "1" : "0";
|
||||
}
|
||||
|
||||
export const resumeProjectsSchema = z.object({
|
||||
maxProjects: z.number().int().min(0).max(100),
|
||||
lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
|
||||
aiSelectableProjectIds: z.array(z.string().trim().min(1)).max(200),
|
||||
});
|
||||
|
||||
export const settingsRegistry = {
|
||||
// --- Typed Settings ---
|
||||
model: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(200),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.MODEL || "google/gemini-3-flash-preview"
|
||||
: "google/gemini-3-flash-preview",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
llmProvider: {
|
||||
kind: "typed" as const,
|
||||
envKey: "LLM_PROVIDER",
|
||||
schema: z.preprocess(
|
||||
(v) => (v === "" ? null : v),
|
||||
z
|
||||
.enum(["openrouter", "lmstudio", "ollama", "openai", "gemini"])
|
||||
.nullable(),
|
||||
),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.LLM_PROVIDER || "openrouter"
|
||||
: "openrouter",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
llmBaseUrl: {
|
||||
kind: "typed" as const,
|
||||
envKey: "LLM_BASE_URL",
|
||||
schema: z.preprocess(
|
||||
(v) => (v === "" ? null : v),
|
||||
z.string().trim().url().max(2000).nullable(),
|
||||
),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined" ? process.env.LLM_BASE_URL || "" : "",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
pipelineWebhookUrl: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(2000),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || ""
|
||||
: "",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
jobCompleteWebhookUrl: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(2000),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.JOB_COMPLETE_WEBHOOK_URL || ""
|
||||
: "",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
resumeProjects: {
|
||||
kind: "typed" as const,
|
||||
schema: resumeProjectsSchema,
|
||||
default: (): ResumeProjectsSettings => ({
|
||||
maxProjects: 20,
|
||||
lockedProjectIds: [],
|
||||
aiSelectableProjectIds: [],
|
||||
}),
|
||||
parse: (raw: string | undefined): ResumeProjectsSettings | null => {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
serialize: (
|
||||
value: ResumeProjectsSettings | null | undefined,
|
||||
): string | null => {
|
||||
return value ? JSON.stringify(value) : null;
|
||||
},
|
||||
},
|
||||
ukvisajobsMaxJobs: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(1).max(1000),
|
||||
default: (): number => 50,
|
||||
parse: parseIntOrNull,
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
adzunaMaxJobsPerTerm: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(1).max(1000),
|
||||
default: (): number =>
|
||||
parseInt(
|
||||
typeof process !== "undefined"
|
||||
? process.env.ADZUNA_MAX_JOBS_PER_TERM || "50"
|
||||
: "50",
|
||||
10,
|
||||
),
|
||||
parse: parseIntOrNull,
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
gradcrackerMaxJobsPerTerm: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(1).max(1000),
|
||||
default: (): number => 50,
|
||||
parse: parseIntOrNull,
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
searchTerms: {
|
||||
kind: "typed" as const,
|
||||
schema: z.array(z.string().trim().min(1).max(200)).max(100),
|
||||
default: (): string[] =>
|
||||
(typeof process !== "undefined"
|
||||
? process.env.JOBSPY_SEARCH_TERMS || "web developer"
|
||||
: "web developer"
|
||||
)
|
||||
.split("|")
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean),
|
||||
parse: parseJsonArrayOrNull,
|
||||
serialize: serializeNullableJsonArray,
|
||||
},
|
||||
searchCities: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(100),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.SEARCH_CITIES || process.env.JOBSPY_LOCATION || "UK"
|
||||
: "UK",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
jobspyResultsWanted: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(1).max(1000),
|
||||
default: (): number =>
|
||||
parseInt(
|
||||
typeof process !== "undefined"
|
||||
? process.env.JOBSPY_RESULTS_WANTED || "200"
|
||||
: "200",
|
||||
10,
|
||||
),
|
||||
parse: parseIntOrNull,
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
jobspyCountryIndeed: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(100),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.JOBSPY_COUNTRY_INDEED || "UK"
|
||||
: "UK",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
showSponsorInfo: {
|
||||
kind: "typed" as const,
|
||||
schema: z.boolean(),
|
||||
default: (): boolean => true,
|
||||
parse: parseBitBoolOrNull,
|
||||
serialize: serializeBitBool,
|
||||
},
|
||||
chatStyleTone: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(100),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.CHAT_STYLE_TONE || "professional"
|
||||
: "professional",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
chatStyleFormality: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(100),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.CHAT_STYLE_FORMALITY || "medium"
|
||||
: "medium",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
chatStyleConstraints: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(4000),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.CHAT_STYLE_CONSTRAINTS || ""
|
||||
: "",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
chatStyleDoNotUse: {
|
||||
kind: "typed" as const,
|
||||
schema: z.string().trim().max(1000),
|
||||
default: (): string =>
|
||||
typeof process !== "undefined"
|
||||
? process.env.CHAT_STYLE_DO_NOT_USE || ""
|
||||
: "",
|
||||
parse: parseNonEmptyStringOrNull,
|
||||
serialize: (value: string | null | undefined): string | null =>
|
||||
value ?? null,
|
||||
},
|
||||
backupEnabled: {
|
||||
kind: "typed" as const,
|
||||
schema: z.boolean(),
|
||||
default: (): boolean => false,
|
||||
parse: parseBitBoolOrNull,
|
||||
serialize: serializeBitBool,
|
||||
},
|
||||
backupHour: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(0).max(23),
|
||||
default: (): number => 2,
|
||||
parse: (raw: string | undefined): number | null => {
|
||||
const parsed = raw ? parseInt(raw, 10) : NaN;
|
||||
if (Number.isNaN(parsed)) return null;
|
||||
return Math.min(23, Math.max(0, parsed));
|
||||
},
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
backupMaxCount: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(1).max(5),
|
||||
default: (): number => 5,
|
||||
parse: (raw: string | undefined): number | null => {
|
||||
const parsed = raw ? parseInt(raw, 10) : NaN;
|
||||
if (Number.isNaN(parsed)) return null;
|
||||
return Math.min(5, Math.max(1, parsed));
|
||||
},
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
penalizeMissingSalary: {
|
||||
kind: "typed" as const,
|
||||
schema: z.boolean(),
|
||||
default: (): boolean => {
|
||||
if (typeof process === "undefined") return false;
|
||||
const v = process.env.PENALIZE_MISSING_SALARY || "0";
|
||||
return v === "1" || v.toLowerCase() === "true";
|
||||
},
|
||||
parse: parseBitBoolOrNull,
|
||||
serialize: serializeBitBool,
|
||||
},
|
||||
missingSalaryPenalty: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(0).max(100),
|
||||
default: (): number => {
|
||||
if (typeof process === "undefined") return 10;
|
||||
const raw = process.env.MISSING_SALARY_PENALTY;
|
||||
if (!raw) return 10;
|
||||
const parsed = parseInt(raw, 10);
|
||||
return Number.isNaN(parsed) ? 10 : Math.min(100, Math.max(0, parsed));
|
||||
},
|
||||
parse: (raw: string | undefined): number | null => {
|
||||
const parsed = raw ? parseInt(raw, 10) : NaN;
|
||||
return Number.isNaN(parsed) ? null : Math.min(100, Math.max(0, parsed));
|
||||
},
|
||||
serialize: serializeNullableNumber,
|
||||
},
|
||||
autoSkipScoreThreshold: {
|
||||
kind: "typed" as const,
|
||||
schema: z.number().int().min(0).max(100),
|
||||
default: (): number | null => null,
|
||||
parse: (raw: string | undefined): number | null => {
|
||||
if (!raw || raw === "null" || raw === "") return null;
|
||||
const parsed = parseInt(raw, 10);
|
||||
return Number.isNaN(parsed) ? null : Math.min(100, Math.max(0, parsed));
|
||||
},
|
||||
serialize: (value: number | null | undefined): string | null => {
|
||||
return value === null || value === undefined ? null : String(value);
|
||||
},
|
||||
},
|
||||
|
||||
// --- Model Variants ---
|
||||
modelScorer: {
|
||||
kind: "model" as const,
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
modelTailoring: {
|
||||
kind: "model" as const,
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
modelProjectSelection: {
|
||||
kind: "model" as const,
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
|
||||
// --- Simple Strings ---
|
||||
rxresumeBaseResumeId: {
|
||||
kind: "string" as const,
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
rxresumeEmail: {
|
||||
kind: "string" as const,
|
||||
envKey: "RXRESUME_EMAIL",
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
ukvisajobsEmail: {
|
||||
kind: "string" as const,
|
||||
envKey: "UKVISAJOBS_EMAIL",
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
adzunaAppId: {
|
||||
kind: "string" as const,
|
||||
envKey: "ADZUNA_APP_ID",
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
basicAuthUser: {
|
||||
kind: "string" as const,
|
||||
envKey: "BASIC_AUTH_USER",
|
||||
schema: z.string().trim().max(200),
|
||||
},
|
||||
|
||||
// --- Secrets ---
|
||||
llmApiKey: {
|
||||
kind: "secret" as const,
|
||||
envKey: "LLM_API_KEY",
|
||||
schema: z.string().trim().max(2000),
|
||||
},
|
||||
rxresumePassword: {
|
||||
kind: "secret" as const,
|
||||
envKey: "RXRESUME_PASSWORD",
|
||||
schema: z.string().trim().max(2000),
|
||||
},
|
||||
ukvisajobsPassword: {
|
||||
kind: "secret" as const,
|
||||
envKey: "UKVISAJOBS_PASSWORD",
|
||||
schema: z.string().trim().max(2000),
|
||||
},
|
||||
adzunaAppKey: {
|
||||
kind: "secret" as const,
|
||||
envKey: "ADZUNA_APP_KEY",
|
||||
schema: z.string().trim().max(2000),
|
||||
},
|
||||
basicAuthPassword: {
|
||||
kind: "secret" as const,
|
||||
envKey: "BASIC_AUTH_PASSWORD",
|
||||
schema: z.string().trim().max(2000),
|
||||
},
|
||||
webhookSecret: {
|
||||
kind: "secret" as const,
|
||||
envKey: "WEBHOOK_SECRET",
|
||||
schema: z.string().trim().max(2000),
|
||||
},
|
||||
|
||||
// --- Aliases ---
|
||||
jobspyLocation: {
|
||||
kind: "alias" as const,
|
||||
schema: z.string().trim().max(100),
|
||||
target: "searchCities" as const,
|
||||
},
|
||||
|
||||
// --- Virtual ---
|
||||
enableBasicAuth: {
|
||||
kind: "virtual" as const,
|
||||
schema: z.boolean(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type SettingsRegistry = typeof settingsRegistry;
|
||||
export type SettingsRegistryKey = keyof SettingsRegistry;
|
||||
+40
-105
@@ -1,112 +1,47 @@
|
||||
import { z } from "zod";
|
||||
import { resumeProjectsSchema, settingsRegistry } from "./settings-registry";
|
||||
|
||||
export const resumeProjectsSchema = z.object({
|
||||
maxProjects: z.number().int().min(0).max(100),
|
||||
lockedProjectIds: z.array(z.string().trim().min(1)).max(200),
|
||||
aiSelectableProjectIds: z.array(z.string().trim().min(1)).max(200),
|
||||
});
|
||||
export { resumeProjectsSchema };
|
||||
|
||||
export const updateSettingsSchema = z
|
||||
.object({
|
||||
model: z.string().trim().max(200).nullable().optional(),
|
||||
modelScorer: z.string().trim().max(200).nullable().optional(),
|
||||
modelTailoring: z.string().trim().max(200).nullable().optional(),
|
||||
modelProjectSelection: z.string().trim().max(200).nullable().optional(),
|
||||
llmProvider: z
|
||||
.preprocess(
|
||||
(value) => (value === "" ? null : value),
|
||||
z
|
||||
.enum(["openrouter", "lmstudio", "ollama", "openai", "gemini"])
|
||||
.nullable(),
|
||||
)
|
||||
.optional(),
|
||||
llmBaseUrl: z
|
||||
.preprocess(
|
||||
(value) => (value === "" ? null : value),
|
||||
z.string().trim().url().max(2000).nullable(),
|
||||
)
|
||||
.optional(),
|
||||
llmApiKey: z.string().trim().max(2000).nullable().optional(),
|
||||
pipelineWebhookUrl: z.string().trim().max(2000).nullable().optional(),
|
||||
jobCompleteWebhookUrl: z.string().trim().max(2000).nullable().optional(),
|
||||
resumeProjects: resumeProjectsSchema.nullable().optional(),
|
||||
rxresumeBaseResumeId: z.string().trim().max(200).nullable().optional(),
|
||||
ukvisajobsMaxJobs: z.number().int().min(1).max(1000).nullable().optional(),
|
||||
adzunaMaxJobsPerTerm: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(1000)
|
||||
.nullable()
|
||||
.optional(),
|
||||
gradcrackerMaxJobsPerTerm: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(1000)
|
||||
.nullable()
|
||||
.optional(),
|
||||
searchTerms: z
|
||||
.array(z.string().trim().min(1).max(200))
|
||||
.max(100)
|
||||
.nullable()
|
||||
.optional(),
|
||||
searchCities: z.string().trim().max(100).nullable().optional(),
|
||||
// Deprecated legacy key; accepted for backward compatibility.
|
||||
jobspyLocation: z.string().trim().max(100).nullable().optional(),
|
||||
jobspyResultsWanted: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(1000)
|
||||
.nullable()
|
||||
.optional(),
|
||||
jobspyCountryIndeed: z.string().trim().max(100).nullable().optional(),
|
||||
showSponsorInfo: z.boolean().nullable().optional(),
|
||||
chatStyleTone: z.string().trim().max(100).nullable().optional(),
|
||||
chatStyleFormality: z.string().trim().max(100).nullable().optional(),
|
||||
chatStyleConstraints: z.string().trim().max(4000).nullable().optional(),
|
||||
chatStyleDoNotUse: z.string().trim().max(1000).nullable().optional(),
|
||||
rxresumeEmail: z.string().trim().max(200).nullable().optional(),
|
||||
rxresumePassword: z.string().trim().max(2000).nullable().optional(),
|
||||
basicAuthUser: z.string().trim().max(200).nullable().optional(),
|
||||
basicAuthPassword: z.string().trim().max(2000).nullable().optional(),
|
||||
ukvisajobsEmail: z.string().trim().max(200).nullable().optional(),
|
||||
ukvisajobsPassword: z.string().trim().max(2000).nullable().optional(),
|
||||
adzunaAppId: z.string().trim().max(200).nullable().optional(),
|
||||
adzunaAppKey: z.string().trim().max(2000).nullable().optional(),
|
||||
webhookSecret: z.string().trim().max(2000).nullable().optional(),
|
||||
enableBasicAuth: z.boolean().optional(),
|
||||
backupEnabled: z.boolean().nullable().optional(),
|
||||
backupHour: z.number().int().min(0).max(23).nullable().optional(),
|
||||
backupMaxCount: z.number().int().min(1).max(5).nullable().optional(),
|
||||
penalizeMissingSalary: z.boolean().nullable().optional(),
|
||||
missingSalaryPenalty: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.nullable()
|
||||
.optional(),
|
||||
autoSkipScoreThreshold: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.enableBasicAuth) {
|
||||
if (!data.basicAuthUser || data.basicAuthUser.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Username is required when basic auth is enabled",
|
||||
path: ["basicAuthUser"],
|
||||
});
|
||||
}
|
||||
type RegistryKeys = keyof typeof settingsRegistry;
|
||||
|
||||
type UpdateSchemaShape = {
|
||||
[K in RegistryKeys]: (typeof settingsRegistry)[K] extends {
|
||||
schema: z.ZodType<infer U, infer D, infer I>;
|
||||
}
|
||||
? K extends "enableBasicAuth"
|
||||
? z.ZodOptional<z.ZodType<U, D, I>>
|
||||
: z.ZodOptional<z.ZodNullable<z.ZodType<U, D, I>>>
|
||||
: z.ZodTypeAny;
|
||||
};
|
||||
|
||||
const shape = Object.fromEntries(
|
||||
Object.entries(settingsRegistry).map(([key, def]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: def is dynamic
|
||||
const fieldSchema = (def as any).schema as z.ZodTypeAny;
|
||||
if (key === "enableBasicAuth") {
|
||||
return [key, fieldSchema.optional()];
|
||||
}
|
||||
});
|
||||
return [key, fieldSchema.nullable().optional()];
|
||||
}),
|
||||
) as unknown as UpdateSchemaShape;
|
||||
|
||||
export const updateSettingsSchema = z.object(shape).superRefine((data, ctx) => {
|
||||
if (data.enableBasicAuth) {
|
||||
if (
|
||||
!data.basicAuthUser ||
|
||||
typeof data.basicAuthUser !== "string" ||
|
||||
data.basicAuthUser.trim() === ""
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Username is required when basic auth is enabled",
|
||||
path: ["basicAuthUser"],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type UpdateSettingsInput = z.infer<typeof updateSettingsSchema>;
|
||||
export type ResumeProjectsSettingsInput = z.infer<typeof resumeProjectsSchema>;
|
||||
|
||||
@@ -125,76 +125,57 @@ export const createResumeProjectCatalogItem = (
|
||||
export const createAppSettings = (
|
||||
overrides: Partial<AppSettings> = {},
|
||||
): AppSettings => ({
|
||||
model: "gpt-4o",
|
||||
defaultModel: "gpt-4o",
|
||||
overrideModel: null,
|
||||
modelScorer: "gpt-4o",
|
||||
overrideModelScorer: null,
|
||||
modelTailoring: "gpt-4o",
|
||||
overrideModelTailoring: null,
|
||||
modelProjectSelection: "gpt-4o",
|
||||
overrideModelProjectSelection: null,
|
||||
llmProvider: "openai",
|
||||
defaultLlmProvider: "openai",
|
||||
overrideLlmProvider: null,
|
||||
llmBaseUrl: "https://api.openai.com/v1",
|
||||
defaultLlmBaseUrl: "https://api.openai.com/v1",
|
||||
overrideLlmBaseUrl: null,
|
||||
pipelineWebhookUrl: "",
|
||||
defaultPipelineWebhookUrl: "",
|
||||
overridePipelineWebhookUrl: null,
|
||||
jobCompleteWebhookUrl: "",
|
||||
defaultJobCompleteWebhookUrl: "",
|
||||
overrideJobCompleteWebhookUrl: null,
|
||||
model: { value: "gpt-4o", default: "gpt-4o", override: null },
|
||||
modelScorer: { value: "gpt-4o", override: null },
|
||||
modelTailoring: { value: "gpt-4o", override: null },
|
||||
modelProjectSelection: { value: "gpt-4o", override: null },
|
||||
llmProvider: { value: "openai", default: "openai", override: null },
|
||||
llmBaseUrl: {
|
||||
value: "https://api.openai.com/v1",
|
||||
default: "https://api.openai.com/v1",
|
||||
override: null,
|
||||
},
|
||||
pipelineWebhookUrl: { value: "", default: "", override: null },
|
||||
jobCompleteWebhookUrl: { value: "", default: "", override: null },
|
||||
profileProjects: [],
|
||||
resumeProjects: {
|
||||
maxProjects: 3,
|
||||
lockedProjectIds: [],
|
||||
aiSelectableProjectIds: [],
|
||||
value: { maxProjects: 3, lockedProjectIds: [], aiSelectableProjectIds: [] },
|
||||
default: {
|
||||
maxProjects: 3,
|
||||
lockedProjectIds: [],
|
||||
aiSelectableProjectIds: [],
|
||||
},
|
||||
override: null,
|
||||
},
|
||||
defaultResumeProjects: {
|
||||
maxProjects: 3,
|
||||
lockedProjectIds: [],
|
||||
aiSelectableProjectIds: [],
|
||||
},
|
||||
overrideResumeProjects: null,
|
||||
rxresumeBaseResumeId: null,
|
||||
ukvisajobsMaxJobs: 50,
|
||||
defaultUkvisajobsMaxJobs: 50,
|
||||
overrideUkvisajobsMaxJobs: null,
|
||||
adzunaMaxJobsPerTerm: 50,
|
||||
defaultAdzunaMaxJobsPerTerm: 50,
|
||||
overrideAdzunaMaxJobsPerTerm: null,
|
||||
gradcrackerMaxJobsPerTerm: 50,
|
||||
defaultGradcrackerMaxJobsPerTerm: 50,
|
||||
overrideGradcrackerMaxJobsPerTerm: null,
|
||||
searchTerms: ["Software Engineer"],
|
||||
defaultSearchTerms: ["Software Engineer"],
|
||||
overrideSearchTerms: null,
|
||||
searchCities: "United Kingdom",
|
||||
defaultSearchCities: "United Kingdom",
|
||||
overrideSearchCities: null,
|
||||
jobspyResultsWanted: 20,
|
||||
defaultJobspyResultsWanted: 20,
|
||||
overrideJobspyResultsWanted: null,
|
||||
jobspyCountryIndeed: "united kingdom",
|
||||
defaultJobspyCountryIndeed: "united kingdom",
|
||||
overrideJobspyCountryIndeed: null,
|
||||
showSponsorInfo: true,
|
||||
defaultShowSponsorInfo: true,
|
||||
overrideShowSponsorInfo: null,
|
||||
chatStyleTone: "professional",
|
||||
defaultChatStyleTone: "professional",
|
||||
overrideChatStyleTone: null,
|
||||
chatStyleFormality: "medium",
|
||||
defaultChatStyleFormality: "medium",
|
||||
overrideChatStyleFormality: null,
|
||||
chatStyleConstraints: "",
|
||||
defaultChatStyleConstraints: "",
|
||||
overrideChatStyleConstraints: null,
|
||||
chatStyleDoNotUse: "",
|
||||
defaultChatStyleDoNotUse: "",
|
||||
overrideChatStyleDoNotUse: null,
|
||||
ukvisajobsMaxJobs: { value: 50, default: 50, override: null },
|
||||
adzunaMaxJobsPerTerm: { value: 50, default: 50, override: null },
|
||||
gradcrackerMaxJobsPerTerm: { value: 50, default: 50, override: null },
|
||||
searchTerms: {
|
||||
value: ["Software Engineer"],
|
||||
default: ["Software Engineer"],
|
||||
override: null,
|
||||
},
|
||||
searchCities: {
|
||||
value: "United Kingdom",
|
||||
default: "United Kingdom",
|
||||
override: null,
|
||||
},
|
||||
jobspyResultsWanted: { value: 20, default: 20, override: null },
|
||||
jobspyCountryIndeed: {
|
||||
value: "united kingdom",
|
||||
default: "united kingdom",
|
||||
override: null,
|
||||
},
|
||||
showSponsorInfo: { value: true, default: true, override: null },
|
||||
chatStyleTone: {
|
||||
value: "professional",
|
||||
default: "professional",
|
||||
override: null,
|
||||
},
|
||||
chatStyleFormality: { value: "medium", default: "medium", override: null },
|
||||
chatStyleConstraints: { value: "", default: "", override: null },
|
||||
chatStyleDoNotUse: { value: "", default: "", override: null },
|
||||
llmApiKeyHint: null,
|
||||
rxresumeEmail: null,
|
||||
rxresumePasswordHint: null,
|
||||
@@ -206,23 +187,11 @@ export const createAppSettings = (
|
||||
adzunaAppKeyHint: null,
|
||||
webhookSecretHint: null,
|
||||
basicAuthActive: false,
|
||||
backupEnabled: false,
|
||||
defaultBackupEnabled: false,
|
||||
overrideBackupEnabled: null,
|
||||
backupHour: 3,
|
||||
defaultBackupHour: 3,
|
||||
overrideBackupHour: null,
|
||||
backupMaxCount: 7,
|
||||
defaultBackupMaxCount: 7,
|
||||
overrideBackupMaxCount: null,
|
||||
penalizeMissingSalary: false,
|
||||
defaultPenalizeMissingSalary: false,
|
||||
overridePenalizeMissingSalary: null,
|
||||
missingSalaryPenalty: 10,
|
||||
defaultMissingSalaryPenalty: 10,
|
||||
overrideMissingSalaryPenalty: null,
|
||||
autoSkipScoreThreshold: null,
|
||||
defaultAutoSkipScoreThreshold: null,
|
||||
overrideAutoSkipScoreThreshold: null,
|
||||
backupEnabled: { value: false, default: false, override: null },
|
||||
backupHour: { value: 3, default: 3, override: null },
|
||||
backupMaxCount: { value: 7, default: 7, override: null },
|
||||
penalizeMissingSalary: { value: false, default: false, override: null },
|
||||
missingSalaryPenalty: { value: 10, default: 10, override: null },
|
||||
autoSkipScoreThreshold: { value: null, default: null, override: null },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
+10
-1114
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,122 @@
|
||||
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 interface TracerAnalyticsTimeseriesPoint {
|
||||
day: string; // YYYY-MM-DD
|
||||
clicks: number;
|
||||
uniqueOpens: number;
|
||||
botClicks: number;
|
||||
humanClicks: number;
|
||||
}
|
||||
|
||||
export interface TracerAnalyticsTopJob {
|
||||
jobId: string;
|
||||
title: string;
|
||||
employer: string;
|
||||
clicks: number;
|
||||
uniqueOpens: number;
|
||||
botClicks: number;
|
||||
humanClicks: number;
|
||||
lastClickedAt: number | null;
|
||||
}
|
||||
|
||||
export interface TracerAnalyticsTopLink {
|
||||
tracerLinkId: string;
|
||||
token: string;
|
||||
jobId: string;
|
||||
title: string;
|
||||
employer: string;
|
||||
sourcePath: string;
|
||||
sourceLabel: string;
|
||||
destinationUrl: string;
|
||||
clicks: number;
|
||||
uniqueOpens: number;
|
||||
botClicks: number;
|
||||
humanClicks: number;
|
||||
lastClickedAt: number | null;
|
||||
}
|
||||
|
||||
export interface TracerAnalyticsResponse {
|
||||
filters: {
|
||||
jobId: string | null;
|
||||
from: number | null;
|
||||
to: number | null;
|
||||
includeBots: boolean;
|
||||
limit: number;
|
||||
};
|
||||
totals: {
|
||||
clicks: number;
|
||||
uniqueOpens: number;
|
||||
botClicks: number;
|
||||
humanClicks: number;
|
||||
};
|
||||
timeSeries: TracerAnalyticsTimeseriesPoint[];
|
||||
topJobs: TracerAnalyticsTopJob[];
|
||||
topLinks: TracerAnalyticsTopLink[];
|
||||
}
|
||||
|
||||
export interface JobTracerLinkAnalyticsItem {
|
||||
tracerLinkId: string;
|
||||
token: string;
|
||||
sourcePath: string;
|
||||
sourceLabel: string;
|
||||
destinationUrl: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
clicks: number;
|
||||
uniqueOpens: number;
|
||||
botClicks: number;
|
||||
humanClicks: number;
|
||||
lastClickedAt: number | null;
|
||||
}
|
||||
|
||||
export interface JobTracerLinksResponse {
|
||||
job: {
|
||||
id: string;
|
||||
title: string;
|
||||
employer: string;
|
||||
tracerLinksEnabled: boolean;
|
||||
};
|
||||
totals: {
|
||||
links: number;
|
||||
clicks: number;
|
||||
uniqueOpens: number;
|
||||
botClicks: number;
|
||||
humanClicks: number;
|
||||
};
|
||||
links: JobTracerLinkAnalyticsItem[];
|
||||
}
|
||||
|
||||
export type TracerReadinessStatus = "ready" | "unconfigured" | "unavailable";
|
||||
|
||||
export interface TracerReadinessResponse {
|
||||
status: TracerReadinessStatus;
|
||||
canEnable: boolean;
|
||||
publicBaseUrl: string | null;
|
||||
healthUrl: string | null;
|
||||
checkedAt: number;
|
||||
lastSuccessAt: number | null;
|
||||
reason: string | null;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
export const JOB_CHAT_MESSAGE_ROLES = [
|
||||
"system",
|
||||
"user",
|
||||
"assistant",
|
||||
"tool",
|
||||
] as const;
|
||||
export type JobChatMessageRole = (typeof JOB_CHAT_MESSAGE_ROLES)[number];
|
||||
|
||||
export const JOB_CHAT_MESSAGE_STATUSES = [
|
||||
"complete",
|
||||
"partial",
|
||||
"cancelled",
|
||||
"failed",
|
||||
] as const;
|
||||
export type JobChatMessageStatus = (typeof JOB_CHAT_MESSAGE_STATUSES)[number];
|
||||
|
||||
export const JOB_CHAT_RUN_STATUSES = [
|
||||
"running",
|
||||
"completed",
|
||||
"cancelled",
|
||||
"failed",
|
||||
] as const;
|
||||
export type JobChatRunStatus = (typeof JOB_CHAT_RUN_STATUSES)[number];
|
||||
|
||||
export interface JobChatThread {
|
||||
id: string;
|
||||
jobId: string;
|
||||
title: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastMessageAt: string | null;
|
||||
}
|
||||
|
||||
export interface JobChatMessage {
|
||||
id: string;
|
||||
threadId: string;
|
||||
jobId: string;
|
||||
role: JobChatMessageRole;
|
||||
content: string;
|
||||
status: JobChatMessageStatus;
|
||||
tokensIn: number | null;
|
||||
tokensOut: number | null;
|
||||
version: number;
|
||||
replacesMessageId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface JobChatRun {
|
||||
id: string;
|
||||
threadId: string;
|
||||
jobId: string;
|
||||
status: JobChatRunStatus;
|
||||
model: string | null;
|
||||
provider: string | null;
|
||||
errorCode: string | null;
|
||||
errorMessage: string | null;
|
||||
startedAt: number;
|
||||
completedAt: number | null;
|
||||
requestId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type JobChatStreamEvent =
|
||||
| {
|
||||
type: "ready";
|
||||
runId: string;
|
||||
threadId: string;
|
||||
messageId: string;
|
||||
requestId: string;
|
||||
}
|
||||
| {
|
||||
type: "delta";
|
||||
runId: string;
|
||||
messageId: string;
|
||||
delta: string;
|
||||
}
|
||||
| {
|
||||
type: "completed";
|
||||
runId: string;
|
||||
message: JobChatMessage;
|
||||
}
|
||||
| {
|
||||
type: "cancelled";
|
||||
runId: string;
|
||||
message: JobChatMessage;
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
runId: string;
|
||||
code: string;
|
||||
message: string;
|
||||
requestId: string;
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
export type JobStatus =
|
||||
| "discovered" // Crawled but not processed
|
||||
| "processing" // Currently generating resume
|
||||
| "ready" // PDF generated, waiting for user to apply
|
||||
| "applied" // Application sent
|
||||
| "in_progress" // In process beyond initial application
|
||||
| "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: "Team Match",
|
||||
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"
|
||||
| "adzuna"
|
||||
| "hiringcafe"
|
||||
| "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
|
||||
tracerLinksEnabled: boolean; // Rewrite outbound resume links to tracer links on next PDF generation
|
||||
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"
|
||||
| "outcome"
|
||||
| "closedAt"
|
||||
| "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;
|
||||
tracerLinksEnabled?: boolean;
|
||||
appliedAt?: string;
|
||||
sponsorMatchScore?: number;
|
||||
sponsorMatchNames?: string;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { Job, JobSource, JobStatus } from "./jobs";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface PipelineStatusResponse {
|
||||
isRunning: boolean;
|
||||
lastRun: PipelineRun | null;
|
||||
nextScheduledRun: string | null;
|
||||
}
|
||||
|
||||
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 JobAction = "skip" | "move_to_ready" | "rescore";
|
||||
|
||||
export type JobActionRequest =
|
||||
| {
|
||||
action: "skip" | "rescore";
|
||||
jobIds: string[];
|
||||
}
|
||||
| {
|
||||
action: "move_to_ready";
|
||||
jobIds: string[];
|
||||
options?: {
|
||||
force?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type JobActionResult =
|
||||
| {
|
||||
jobId: string;
|
||||
ok: true;
|
||||
job: Job;
|
||||
}
|
||||
| {
|
||||
jobId: string;
|
||||
ok: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface JobActionResponse {
|
||||
action: JobAction;
|
||||
requested: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
results: JobActionResult[];
|
||||
}
|
||||
|
||||
export type JobActionStreamEvent =
|
||||
| {
|
||||
type: "started";
|
||||
action: JobAction;
|
||||
requested: number;
|
||||
completed: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
requestId: string;
|
||||
}
|
||||
| {
|
||||
type: "progress";
|
||||
action: JobAction;
|
||||
requested: number;
|
||||
completed: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
result: JobActionResult;
|
||||
requestId: string;
|
||||
}
|
||||
| {
|
||||
type: "completed";
|
||||
action: JobAction;
|
||||
requested: number;
|
||||
completed: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
results: JobActionResult[];
|
||||
requestId: string;
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
code: string;
|
||||
message: string;
|
||||
requestId: string;
|
||||
};
|
||||
|
||||
export interface BackupInfo {
|
||||
filename: string;
|
||||
type: "auto" | "manual";
|
||||
size: number;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
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 PostApplicationAction = "approve" | "deny";
|
||||
|
||||
export interface PostApplicationActionRequest {
|
||||
action: PostApplicationAction;
|
||||
provider: PostApplicationProvider;
|
||||
accountKey: string;
|
||||
}
|
||||
|
||||
export type PostApplicationActionResult =
|
||||
| {
|
||||
messageId: string;
|
||||
ok: true;
|
||||
message: PostApplicationMessage;
|
||||
stageEventId?: string | null;
|
||||
}
|
||||
| {
|
||||
messageId: string;
|
||||
ok: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface PostApplicationActionResponse {
|
||||
action: PostApplicationAction;
|
||||
requested: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
results: PostApplicationActionResult[];
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
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 type Resolved<T> = { value: T; default: T; override: T | null };
|
||||
export type ModelResolved = { value: string; override: string | null };
|
||||
|
||||
export interface AppSettings {
|
||||
// Typed settings (Resolved):
|
||||
model: Resolved<string>;
|
||||
llmProvider: Resolved<string>;
|
||||
llmBaseUrl: Resolved<string>;
|
||||
pipelineWebhookUrl: Resolved<string>;
|
||||
jobCompleteWebhookUrl: Resolved<string>;
|
||||
resumeProjects: Resolved<ResumeProjectsSettings>;
|
||||
ukvisajobsMaxJobs: Resolved<number>;
|
||||
adzunaMaxJobsPerTerm: Resolved<number>;
|
||||
gradcrackerMaxJobsPerTerm: Resolved<number>;
|
||||
searchTerms: Resolved<string[]>;
|
||||
searchCities: Resolved<string>;
|
||||
jobspyResultsWanted: Resolved<number>;
|
||||
jobspyCountryIndeed: Resolved<string>;
|
||||
showSponsorInfo: Resolved<boolean>;
|
||||
chatStyleTone: Resolved<string>;
|
||||
chatStyleFormality: Resolved<string>;
|
||||
chatStyleConstraints: Resolved<string>;
|
||||
chatStyleDoNotUse: Resolved<string>;
|
||||
backupEnabled: Resolved<boolean>;
|
||||
backupHour: Resolved<number>;
|
||||
backupMaxCount: Resolved<number>;
|
||||
penalizeMissingSalary: Resolved<boolean>;
|
||||
missingSalaryPenalty: Resolved<number>;
|
||||
autoSkipScoreThreshold: Resolved<number | null>;
|
||||
|
||||
// Model variants (no own default, fallback to model.value):
|
||||
modelScorer: ModelResolved;
|
||||
modelTailoring: ModelResolved;
|
||||
modelProjectSelection: ModelResolved;
|
||||
|
||||
// Simple strings:
|
||||
rxresumeBaseResumeId: string | null;
|
||||
rxresumeEmail: string | null;
|
||||
ukvisajobsEmail: string | null;
|
||||
adzunaAppId: string | null;
|
||||
basicAuthUser: string | null;
|
||||
|
||||
// Secret hints:
|
||||
llmApiKeyHint: string | null;
|
||||
rxresumePasswordHint: string | null;
|
||||
ukvisajobsPasswordHint: string | null;
|
||||
adzunaAppKeyHint: string | null;
|
||||
basicAuthPasswordHint: string | null;
|
||||
webhookSecretHint: string | null;
|
||||
|
||||
// Computed:
|
||||
basicAuthActive: boolean;
|
||||
profileProjects: ResumeProjectCatalogItem[];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user