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:
Shaheer Sarfaraz
2026-02-21 03:07:51 +00:00
committed by GitHub
parent 19266fe5eb
commit b18c2eccbb
47 changed files with 3437 additions and 3810 deletions
+105
View File
@@ -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();
});
});
});
+423
View File
@@ -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
View File
@@ -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>;
+53 -84
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+122
View File
@@ -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;
}
+95
View File
@@ -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;
};
+322
View File
@@ -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;
}
+124
View File
@@ -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;
}
+208
View File
@@ -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[];
}
+164
View File
@@ -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[];
}
+28
View File
@@ -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;
}