Shaheer Sarfaraz 19266fe5eb
City search (#217)
* wave 1, jobspy only

* combine usa/ca to united states

* strict city location filter

* hide and show based on focus

* UI changes

* allow clicking cross!

* pill animate in

* animate out, uggo fix

* animate out

* framer motion

* animate component height

* adzuna

* hiring cafe implementation

* refactor: centralize shared search-city parsing and matching

* feat: migrate city setting to searchCities with legacy fallback

* docs: update pipeline and extractor city-search wording

* fix(orchestrator): normalize tokenized paste behavior

* fix(shared): tighten city matching semantics

* docs(extractors): document city-location knobs and geocoding note
2026-02-21 00:42:09 +00:00

338 lines
12 KiB
TypeScript

import * as settingsRepo from "@server/repositories/settings";
import type { AppSettings } from "@shared/types";
import { getEnvSettingsData } from "./envSettings";
import { getProfile } from "./profile";
import {
extractProjectsFromProfile,
resolveResumeProjectsSettings,
} from "./resumeProjects";
import { getResume, RxResumeCredentialsError } from "./rxresume-v4";
import { resolveSettingValue } from "./settings-conversion";
/**
* Get the effective app settings, combining environment variables and database overrides.
*/
export async function getEffectiveSettings(): Promise<AppSettings> {
const overrides = await settingsRepo.getAllSettings();
const rxresumeBaseResumeId = overrides.rxresumeBaseResumeId ?? null;
let profile: Record<string, unknown> = {};
if (rxresumeBaseResumeId) {
try {
const resume = await getResume(rxresumeBaseResumeId);
if (resume.data && typeof resume.data === "object") {
profile = resume.data as Record<string, unknown>;
}
} catch (error) {
if (error instanceof RxResumeCredentialsError) {
console.warn(
"RxResume credentials missing while loading base resume from settings.",
);
} else {
console.warn(
"Failed to load RxResume base resume for settings:",
error,
);
}
}
}
if (Object.keys(profile).length === 0) {
profile = await getProfile().catch((error) => {
console.warn("Failed to load base resume profile for settings:", error);
return {};
});
}
const envSettings = await getEnvSettingsData(overrides);
const defaultModel = process.env.MODEL || "google/gemini-3-flash-preview";
const overrideModel = overrides.model ?? null;
const model = overrideModel || defaultModel;
const overrideModelScorer = overrides.modelScorer ?? null;
const modelScorer = overrideModelScorer || model;
const overrideModelTailoring = overrides.modelTailoring ?? null;
const modelTailoring = overrideModelTailoring || model;
const overrideModelProjectSelection = overrides.modelProjectSelection ?? null;
const modelProjectSelection = overrideModelProjectSelection || model;
const defaultLlmProvider = process.env.LLM_PROVIDER || "openrouter";
const overrideLlmProvider = overrides.llmProvider ?? null;
const llmProvider = overrideLlmProvider || defaultLlmProvider;
const defaultLlmBaseUrl =
process.env.LLM_BASE_URL || resolveDefaultLlmBaseUrl(llmProvider);
const overrideLlmBaseUrl = overrides.llmBaseUrl ?? null;
const llmBaseUrl = overrideLlmBaseUrl || defaultLlmBaseUrl;
const defaultPipelineWebhookUrl =
process.env.PIPELINE_WEBHOOK_URL || process.env.WEBHOOK_URL || "";
const overridePipelineWebhookUrl = overrides.pipelineWebhookUrl ?? null;
const pipelineWebhookUrl =
overridePipelineWebhookUrl || defaultPipelineWebhookUrl;
const defaultJobCompleteWebhookUrl =
process.env.JOB_COMPLETE_WEBHOOK_URL || "";
const overrideJobCompleteWebhookUrl = overrides.jobCompleteWebhookUrl ?? null;
const jobCompleteWebhookUrl =
overrideJobCompleteWebhookUrl || defaultJobCompleteWebhookUrl;
const { catalog } = extractProjectsFromProfile(profile);
const overrideResumeProjectsRaw = overrides.resumeProjects ?? null;
const resumeProjectsData = resolveResumeProjectsSettings({
catalog,
overrideRaw: overrideResumeProjectsRaw,
});
const ukvisajobsMaxJobsSetting = resolveSettingValue(
"ukvisajobsMaxJobs",
overrides.ukvisajobsMaxJobs,
);
const defaultUkvisajobsMaxJobs = ukvisajobsMaxJobsSetting.defaultValue;
const overrideUkvisajobsMaxJobs = ukvisajobsMaxJobsSetting.overrideValue;
const ukvisajobsMaxJobs = ukvisajobsMaxJobsSetting.value;
const adzunaMaxJobsPerTermSetting = resolveSettingValue(
"adzunaMaxJobsPerTerm",
overrides.adzunaMaxJobsPerTerm,
);
const defaultAdzunaMaxJobsPerTerm = adzunaMaxJobsPerTermSetting.defaultValue;
const overrideAdzunaMaxJobsPerTerm =
adzunaMaxJobsPerTermSetting.overrideValue;
const adzunaMaxJobsPerTerm = adzunaMaxJobsPerTermSetting.value;
const gradcrackerMaxJobsPerTermSetting = resolveSettingValue(
"gradcrackerMaxJobsPerTerm",
overrides.gradcrackerMaxJobsPerTerm,
);
const defaultGradcrackerMaxJobsPerTerm =
gradcrackerMaxJobsPerTermSetting.defaultValue;
const overrideGradcrackerMaxJobsPerTerm =
gradcrackerMaxJobsPerTermSetting.overrideValue;
const gradcrackerMaxJobsPerTerm = gradcrackerMaxJobsPerTermSetting.value;
const searchTermsSetting = resolveSettingValue(
"searchTerms",
overrides.searchTerms,
);
const defaultSearchTerms = searchTermsSetting.defaultValue;
const overrideSearchTerms = searchTermsSetting.overrideValue;
const searchTerms = searchTermsSetting.value;
const searchCitiesSetting = resolveSettingValue(
"searchCities",
overrides.searchCities ?? overrides.jobspyLocation,
);
const defaultSearchCities = searchCitiesSetting.defaultValue;
const overrideSearchCities = searchCitiesSetting.overrideValue;
const searchCities = searchCitiesSetting.value;
const jobspyResultsWantedSetting = resolveSettingValue(
"jobspyResultsWanted",
overrides.jobspyResultsWanted,
);
const defaultJobspyResultsWanted = jobspyResultsWantedSetting.defaultValue;
const overrideJobspyResultsWanted = jobspyResultsWantedSetting.overrideValue;
const jobspyResultsWanted = jobspyResultsWantedSetting.value;
const jobspyCountryIndeedSetting = resolveSettingValue(
"jobspyCountryIndeed",
overrides.jobspyCountryIndeed,
);
const defaultJobspyCountryIndeed = jobspyCountryIndeedSetting.defaultValue;
const overrideJobspyCountryIndeed = jobspyCountryIndeedSetting.overrideValue;
const jobspyCountryIndeed = jobspyCountryIndeedSetting.value;
const showSponsorInfoSetting = resolveSettingValue(
"showSponsorInfo",
overrides.showSponsorInfo,
);
const defaultShowSponsorInfo = showSponsorInfoSetting.defaultValue;
const overrideShowSponsorInfo = showSponsorInfoSetting.overrideValue;
const showSponsorInfo = showSponsorInfoSetting.value;
const chatStyleToneSetting = resolveSettingValue(
"chatStyleTone",
overrides.chatStyleTone,
);
const defaultChatStyleTone = chatStyleToneSetting.defaultValue;
const overrideChatStyleTone = chatStyleToneSetting.overrideValue;
const chatStyleTone = chatStyleToneSetting.value;
const chatStyleFormalitySetting = resolveSettingValue(
"chatStyleFormality",
overrides.chatStyleFormality,
);
const defaultChatStyleFormality = chatStyleFormalitySetting.defaultValue;
const overrideChatStyleFormality = chatStyleFormalitySetting.overrideValue;
const chatStyleFormality = chatStyleFormalitySetting.value;
const chatStyleConstraintsSetting = resolveSettingValue(
"chatStyleConstraints",
overrides.chatStyleConstraints,
);
const defaultChatStyleConstraints = chatStyleConstraintsSetting.defaultValue;
const overrideChatStyleConstraints =
chatStyleConstraintsSetting.overrideValue;
const chatStyleConstraints = chatStyleConstraintsSetting.value;
const chatStyleDoNotUseSetting = resolveSettingValue(
"chatStyleDoNotUse",
overrides.chatStyleDoNotUse,
);
const defaultChatStyleDoNotUse = chatStyleDoNotUseSetting.defaultValue;
const overrideChatStyleDoNotUse = chatStyleDoNotUseSetting.overrideValue;
const chatStyleDoNotUse = chatStyleDoNotUseSetting.value;
const backupEnabledSetting = resolveSettingValue(
"backupEnabled",
overrides.backupEnabled,
);
const defaultBackupEnabled = backupEnabledSetting.defaultValue;
const overrideBackupEnabled = backupEnabledSetting.overrideValue;
const backupEnabled = backupEnabledSetting.value;
const backupHourSetting = resolveSettingValue(
"backupHour",
overrides.backupHour,
);
const defaultBackupHour = backupHourSetting.defaultValue;
const overrideBackupHour = backupHourSetting.overrideValue;
const backupHour = backupHourSetting.value;
const backupMaxCountSetting = resolveSettingValue(
"backupMaxCount",
overrides.backupMaxCount,
);
const defaultBackupMaxCount = backupMaxCountSetting.defaultValue;
const overrideBackupMaxCount = backupMaxCountSetting.overrideValue;
const backupMaxCount = backupMaxCountSetting.value;
const penalizeMissingSalarySetting = resolveSettingValue(
"penalizeMissingSalary",
overrides.penalizeMissingSalary,
);
const defaultPenalizeMissingSalary =
penalizeMissingSalarySetting.defaultValue;
const overridePenalizeMissingSalary =
penalizeMissingSalarySetting.overrideValue;
const penalizeMissingSalary = penalizeMissingSalarySetting.value;
const missingSalaryPenaltySetting = resolveSettingValue(
"missingSalaryPenalty",
overrides.missingSalaryPenalty,
);
const defaultMissingSalaryPenalty = missingSalaryPenaltySetting.defaultValue;
const overrideMissingSalaryPenalty =
missingSalaryPenaltySetting.overrideValue;
const missingSalaryPenalty = missingSalaryPenaltySetting.value;
const autoSkipScoreThresholdSetting = resolveSettingValue(
"autoSkipScoreThreshold",
overrides.autoSkipScoreThreshold,
);
const defaultAutoSkipScoreThreshold =
autoSkipScoreThresholdSetting.defaultValue;
const overrideAutoSkipScoreThreshold =
autoSkipScoreThresholdSetting.overrideValue;
const autoSkipScoreThreshold = autoSkipScoreThresholdSetting.value;
return {
...envSettings,
model,
defaultModel,
overrideModel,
modelScorer,
overrideModelScorer,
modelTailoring,
overrideModelTailoring,
modelProjectSelection,
overrideModelProjectSelection,
llmProvider,
defaultLlmProvider,
overrideLlmProvider,
llmBaseUrl,
defaultLlmBaseUrl,
overrideLlmBaseUrl,
pipelineWebhookUrl,
defaultPipelineWebhookUrl,
overridePipelineWebhookUrl,
jobCompleteWebhookUrl,
defaultJobCompleteWebhookUrl,
overrideJobCompleteWebhookUrl,
...resumeProjectsData,
rxresumeBaseResumeId,
ukvisajobsMaxJobs,
defaultUkvisajobsMaxJobs,
overrideUkvisajobsMaxJobs,
adzunaMaxJobsPerTerm,
defaultAdzunaMaxJobsPerTerm,
overrideAdzunaMaxJobsPerTerm,
gradcrackerMaxJobsPerTerm,
defaultGradcrackerMaxJobsPerTerm,
overrideGradcrackerMaxJobsPerTerm,
searchTerms,
defaultSearchTerms,
overrideSearchTerms,
searchCities,
defaultSearchCities,
overrideSearchCities,
jobspyResultsWanted,
defaultJobspyResultsWanted,
overrideJobspyResultsWanted,
jobspyCountryIndeed,
defaultJobspyCountryIndeed,
overrideJobspyCountryIndeed,
showSponsorInfo,
defaultShowSponsorInfo,
overrideShowSponsorInfo,
chatStyleTone,
defaultChatStyleTone,
overrideChatStyleTone,
chatStyleFormality,
defaultChatStyleFormality,
overrideChatStyleFormality,
chatStyleConstraints,
defaultChatStyleConstraints,
overrideChatStyleConstraints,
chatStyleDoNotUse,
defaultChatStyleDoNotUse,
overrideChatStyleDoNotUse,
backupEnabled,
defaultBackupEnabled,
overrideBackupEnabled,
backupHour,
defaultBackupHour,
overrideBackupHour,
backupMaxCount,
defaultBackupMaxCount,
overrideBackupMaxCount,
penalizeMissingSalary,
defaultPenalizeMissingSalary,
overridePenalizeMissingSalary,
missingSalaryPenalty,
defaultMissingSalaryPenalty,
overrideMissingSalaryPenalty,
autoSkipScoreThreshold,
defaultAutoSkipScoreThreshold,
overrideAutoSkipScoreThreshold,
} as AppSettings;
}
function resolveDefaultLlmBaseUrl(provider: string): string {
const normalized = provider.trim().toLowerCase();
if (normalized === "ollama") return "http://localhost:11434";
if (normalized === "lmstudio") return "http://localhost:1234";
if (normalized === "openai") {
return "https://api.openai.com";
}
if (normalized === "gemini") {
return "https://generativelanguage.googleapis.com";
}
return "https://openrouter.ai";
}