diff --git a/orchestrator/src/client/api/client.ts b/orchestrator/src/client/api/client.ts index f3f1af3..5c8c427 100644 --- a/orchestrator/src/client/api/client.ts +++ b/orchestrator/src/client/api/client.ts @@ -205,10 +205,7 @@ export async function updateSettings(update: { basicAuthPassword?: string | null ukvisajobsEmail?: string | null ukvisajobsPassword?: string | null - ukvisajobsHeadless?: boolean | null webhookSecret?: string | null - notionApiKey?: string | null - notionDatabaseId?: string | null }): Promise { return fetchApi('/settings', { method: 'PATCH', diff --git a/orchestrator/src/client/pages/SettingsPage.test.tsx b/orchestrator/src/client/pages/SettingsPage.test.tsx index 6b5de51..3189857 100644 --- a/orchestrator/src/client/pages/SettingsPage.test.tsx +++ b/orchestrator/src/client/pages/SettingsPage.test.tsx @@ -102,10 +102,8 @@ const baseSettings: AppSettings = { basicAuthPasswordHint: null, ukvisajobsEmail: "", ukvisajobsPasswordHint: null, - ukvisajobsHeadless: true, webhookSecretHint: null, - notionApiKeyHint: null, - notionDatabaseId: "", + basicAuthActive: false, } const renderPage = () => { diff --git a/orchestrator/src/client/pages/settings/components/EnvironmentSettingsSection.tsx b/orchestrator/src/client/pages/settings/components/EnvironmentSettingsSection.tsx index 683191d..583c3a8 100644 --- a/orchestrator/src/client/pages/settings/components/EnvironmentSettingsSection.tsx +++ b/orchestrator/src/client/pages/settings/components/EnvironmentSettingsSection.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect } from "react" -import { useFormContext, Controller } from "react-hook-form" +import { useFormContext } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" import { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { EnvSettingsValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type EnvironmentSettingsSectionProps = { values: EnvSettingsValues @@ -21,8 +21,8 @@ export const EnvironmentSettingsSection: React.FC { - const { register, control, formState: { errors } } = useFormContext() - const { readable, private: privateValues, basicAuthActive } = values + const { register, formState: { errors } } = useFormContext() + const { private: privateValues, basicAuthActive } = values const [isBasicAuthEnabled, setIsBasicAuthEnabled] = useState(basicAuthActive) @@ -41,33 +41,25 @@ export const EnvironmentSettingsSection: React.FC
External Services
-
-
OpenRouter API key
- - {errors.openrouterApiKey &&

{errors.openrouterApiKey.message}

} -
- Current: {formatSecretHint(privateValues.openrouterApiKeyHint)} -
-
+ -
-
Webhook secret
- - {errors.webhookSecret &&

{errors.webhookSecret.message}

} -
- Current: {formatSecretHint(privateValues.webhookSecretHint)} -
-
+
@@ -80,56 +72,44 @@ export const EnvironmentSettingsSection: React.FC
RxResume
-
-
Email
- - {errors.rxresumeEmail &&

{errors.rxresumeEmail.message}

} -
-
-
Password
- - {errors.rxresumePassword &&

{errors.rxresumePassword.message}

} -
- Current: {formatSecretHint(privateValues.rxresumePasswordHint)} -
-
+ +
UKVisaJobs
-
-
Email
- - {errors.ukvisajobsEmail &&

{errors.ukvisajobsEmail.message}

} -
-
-
Password
- - {errors.ukvisajobsPassword &&

{errors.ukvisajobsPassword.message}

} -
- Current: {formatSecretHint(privateValues.ukvisajobsPasswordHint)} -
-
+ +
@@ -161,29 +141,23 @@ export const EnvironmentSettingsSection: React.FC -
-
Username
- - {errors.basicAuthUser &&

{errors.basicAuthUser.message}

} -
+ -
-
Password
- - {errors.basicAuthPassword &&

{errors.basicAuthPassword.message}

} -
- Current: {formatSecretHint(privateValues.basicAuthPasswordHint)} -
-
+ )} @@ -192,4 +166,3 @@ export const EnvironmentSettingsSection: React.FC ) } - diff --git a/orchestrator/src/client/pages/settings/components/GradcrackerSection.tsx b/orchestrator/src/client/pages/settings/components/GradcrackerSection.tsx index a8cdda3..aa540fc 100644 --- a/orchestrator/src/client/pages/settings/components/GradcrackerSection.tsx +++ b/orchestrator/src/client/pages/settings/components/GradcrackerSection.tsx @@ -2,10 +2,9 @@ import React from "react" import { useFormContext, Controller } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { NumericSettingValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type GradcrackerSectionProps = { values: NumericSettingValues @@ -28,48 +27,35 @@ export const GradcrackerSection: React.FC = ({
-
-
Max jobs per search term
- ( - { + ( + { const value = parseInt(event.target.value, 10) if (Number.isNaN(value)) { field.onChange(null) } else { field.onChange(Math.min(1000, Math.max(1, value))) } - }} - disabled={isLoading || isSaving} - /> - )} - /> - {errors.gradcrackerMaxJobsPerTerm &&

{errors.gradcrackerMaxJobsPerTerm.message}

} -
- Maximum number of jobs to fetch for EACH search term from Gradcracker. Range: 1-1000. -
-
- - - -
-
-
Effective
-
{effectiveGradcrackerMaxJobsPerTerm}
-
-
-
Default
-
{defaultGradcrackerMaxJobsPerTerm}
-
-
+ }, + }} + disabled={isLoading || isSaving} + error={errors.gradcrackerMaxJobsPerTerm?.message as string | undefined} + helper={`Maximum number of jobs to fetch for EACH search term from Gradcracker. Default: ${defaultGradcrackerMaxJobsPerTerm}. Range: 1-1000.`} + current={String(effectiveGradcrackerMaxJobsPerTerm)} + /> + )} + />
diff --git a/orchestrator/src/client/pages/settings/components/JobCompleteWebhookSection.tsx b/orchestrator/src/client/pages/settings/components/JobCompleteWebhookSection.tsx index 1d4b775..fa0c2c3 100644 --- a/orchestrator/src/client/pages/settings/components/JobCompleteWebhookSection.tsx +++ b/orchestrator/src/client/pages/settings/components/JobCompleteWebhookSection.tsx @@ -2,10 +2,9 @@ import React from "react" import { useFormContext } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { WebhookValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type JobCompleteWebhookSectionProps = { values: WebhookValues @@ -28,31 +27,15 @@ export const JobCompleteWebhookSection: React.FC
-
-
Job completion webhook URL
- - {errors.jobCompleteWebhookUrl &&

{errors.jobCompleteWebhookUrl.message}

} -
- When set, the server sends a POST when you mark a job as applied (includes the job description). -
-
- - - -
-
-
Effective
-
{effectiveJobCompleteWebhookUrl || "—"}
-
-
-
Default (env)
-
{defaultJobCompleteWebhookUrl || "—"}
-
-
+
diff --git a/orchestrator/src/client/pages/settings/components/JobspySection.tsx b/orchestrator/src/client/pages/settings/components/JobspySection.tsx index f004786..fa25071 100644 --- a/orchestrator/src/client/pages/settings/components/JobspySection.tsx +++ b/orchestrator/src/client/pages/settings/components/JobspySection.tsx @@ -3,10 +3,10 @@ import { useFormContext, Controller } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" import { Checkbox } from "@/components/ui/checkbox" -import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { JobspyValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type JobspySectionProps = { values: JobspyValues @@ -99,107 +99,85 @@ export const JobspySection: React.FC = ({
-
-
Location
- - {errors.jobspyLocation &&

{errors.jobspyLocation.message}

} -
- Location to search for jobs (e.g. "UK", "London", "Remote"). -
-
- Effective: {location.effective || "—"} - Default: {location.default || "—"} -
-
+ -
-
Results Wanted
- ( - { + ( + { const value = parseInt(event.target.value, 10) if (Number.isNaN(value)) { field.onChange(null) } else { field.onChange(Math.min(1000, Math.max(1, value))) } - }} - disabled={isLoading || isSaving} - /> - )} - /> - {errors.jobspyResultsWanted &&

{errors.jobspyResultsWanted.message}

} -
- Number of results to fetch per term per site. Max 1000. -
-
- Effective: {resultsWanted.effective} - Default: {resultsWanted.default} -
-
+ }, + }} + disabled={isLoading || isSaving} + error={errors.jobspyResultsWanted?.message as string | undefined} + helper={`Number of results to fetch per term per site. Default: ${resultsWanted.default}. Max 1000.`} + current={`Effective: ${resultsWanted.effective} | Default: ${resultsWanted.default}`} + /> + )} + /> -
-
Hours Old
- ( - { + ( + { const value = parseInt(event.target.value, 10) if (Number.isNaN(value)) { field.onChange(null) } else { field.onChange(Math.min(720, Math.max(1, value))) } - }} - disabled={isLoading || isSaving} - /> - )} - /> - {errors.jobspyHoursOld &&

{errors.jobspyHoursOld.message}

} -
- Max age of jobs in hours (e.g. 72 for 3 days). Max 720 (30 days). -
-
- Effective: {hoursOld.effective}h - Default: {hoursOld.default}h -
-
+ }, + }} + disabled={isLoading || isSaving} + error={errors.jobspyHoursOld?.message as string | undefined} + helper={`Max age of jobs in hours (e.g. 72 for 3 days). Default: ${hoursOld.default}. Max 720.`} + current={`Effective: ${hoursOld.effective}h | Default: ${hoursOld.default}h`} + /> + )} + /> -
-
Indeed Country
- - {errors.jobspyCountryIndeed &&

{errors.jobspyCountryIndeed.message}

} -
- Country domain for Indeed (e.g. "UK" for indeed.co.uk). -
-
- Effective: {countryIndeed.effective || "—"} - Default: {countryIndeed.default || "—"} -
-
+
diff --git a/orchestrator/src/client/pages/settings/components/ModelSettingsSection.tsx b/orchestrator/src/client/pages/settings/components/ModelSettingsSection.tsx index b08e414..7632ee2 100644 --- a/orchestrator/src/client/pages/settings/components/ModelSettingsSection.tsx +++ b/orchestrator/src/client/pages/settings/components/ModelSettingsSection.tsx @@ -2,10 +2,10 @@ import React from "react" import { useFormContext } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { ModelValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type ModelSettingsSectionProps = { values: ModelValues @@ -28,18 +28,15 @@ export const ModelSettingsSection: React.FC = ({
-
-
Override model
- - {errors.model &&

{errors.model.message}

} -
- Leave blank to use the default from server env (`MODEL`). -
-
+ @@ -47,44 +44,32 @@ export const ModelSettingsSection: React.FC = ({
Task-Specific Overrides
-
-
Scoring Model
- - {errors.modelScorer &&

{errors.modelScorer.message}

} -
- Effective: {scorer || effective} -
-
+ -
-
Tailoring Model
- - {errors.modelTailoring &&

{errors.modelTailoring.message}

} -
- Effective: {tailoring || effective} -
-
+ -
-
Project Selection Model
- - {errors.modelProjectSelection &&

{errors.modelProjectSelection.message}

} -
- Effective: {projectSelection || effective} -
-
+
diff --git a/orchestrator/src/client/pages/settings/components/PipelineWebhookSection.tsx b/orchestrator/src/client/pages/settings/components/PipelineWebhookSection.tsx index 8d92275..7339866 100644 --- a/orchestrator/src/client/pages/settings/components/PipelineWebhookSection.tsx +++ b/orchestrator/src/client/pages/settings/components/PipelineWebhookSection.tsx @@ -2,10 +2,9 @@ import React from "react" import { useFormContext } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { WebhookValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type PipelineWebhookSectionProps = { values: WebhookValues @@ -28,31 +27,15 @@ export const PipelineWebhookSection: React.FC = ({
-
-
Pipeline status webhook URL
- - {errors.pipelineWebhookUrl &&

{errors.pipelineWebhookUrl.message}

} -
- When set, the server sends a POST on pipeline completion/failure. Leave blank to disable. -
-
- - - -
-
-
Effective
-
{effectivePipelineWebhookUrl || "—"}
-
-
-
Default (env)
-
{defaultPipelineWebhookUrl || "—"}
-
-
+
diff --git a/orchestrator/src/client/pages/settings/components/SettingsInput.tsx b/orchestrator/src/client/pages/settings/components/SettingsInput.tsx new file mode 100644 index 0000000..bc1071b --- /dev/null +++ b/orchestrator/src/client/pages/settings/components/SettingsInput.tsx @@ -0,0 +1,39 @@ +import React from "react" + +import { Input } from "@/components/ui/input" + +type SettingsInputProps = { + label: string + inputProps: React.InputHTMLAttributes + placeholder?: string + type?: React.HTMLInputTypeAttribute + disabled?: boolean + error?: string + helper?: string + current?: string | null +} + +export const SettingsInput: React.FC = ({ + label, + inputProps, + placeholder, + type = "text", + disabled, + error, + helper, + current, +}) => { + return ( +
+
{label}
+ + {error &&

{error}

} + {helper &&
{helper}
} + {current !== undefined && ( +
+ Current: {current} +
+ )} +
+ ) +} diff --git a/orchestrator/src/client/pages/settings/components/UkvisajobsSection.tsx b/orchestrator/src/client/pages/settings/components/UkvisajobsSection.tsx index 05fce6c..fd85efe 100644 --- a/orchestrator/src/client/pages/settings/components/UkvisajobsSection.tsx +++ b/orchestrator/src/client/pages/settings/components/UkvisajobsSection.tsx @@ -2,10 +2,9 @@ import React from "react" import { useFormContext, Controller } from "react-hook-form" import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { Input } from "@/components/ui/input" -import { Separator } from "@/components/ui/separator" import { UpdateSettingsInput } from "@shared/settings-schema" import type { NumericSettingValues } from "@client/pages/settings/types" +import { SettingsInput } from "@client/pages/settings/components/SettingsInput" type UkvisajobsSectionProps = { values: NumericSettingValues @@ -28,48 +27,35 @@ export const UkvisajobsSection: React.FC = ({
-
-
Max jobs to fetch
- ( - { + ( + { const value = parseInt(event.target.value, 10) if (Number.isNaN(value)) { field.onChange(null) } else { field.onChange(Math.min(1000, Math.max(1, value))) } - }} - disabled={isLoading || isSaving} - /> - )} - /> - {errors.ukvisajobsMaxJobs &&

{errors.ukvisajobsMaxJobs.message}

} -
- Maximum number of jobs to fetch from UKVisaJobs per pipeline run. Range: 1-1000. -
-
- - - -
-
-
Effective
-
{effectiveUkvisajobsMaxJobs}
-
-
-
Default
-
{defaultUkvisajobsMaxJobs}
-
-
+ }, + }} + disabled={isLoading || isSaving} + error={errors.ukvisajobsMaxJobs?.message as string | undefined} + helper={`Maximum number of jobs to fetch from UKVisaJobs per pipeline run. Default: ${defaultUkvisajobsMaxJobs}. Range: 1-1000.`} + current={String(effectiveUkvisajobsMaxJobs)} + /> + )} + />
diff --git a/orchestrator/src/client/pages/settings/types.ts b/orchestrator/src/client/pages/settings/types.ts index a0b2555..c75cf80 100644 --- a/orchestrator/src/client/pages/settings/types.ts +++ b/orchestrator/src/client/pages/settings/types.ts @@ -28,8 +28,6 @@ export type EnvSettingsValues = { rxresumeEmail: string ukvisajobsEmail: string basicAuthUser: string - notionDatabaseId: string - ukvisajobsHeadless: boolean } private: { openrouterApiKeyHint: string | null diff --git a/orchestrator/src/server/api/routes/settings.ts b/orchestrator/src/server/api/routes/settings.ts index 0f5d687..039cca9 100644 --- a/orchestrator/src/server/api/routes/settings.ts +++ b/orchestrator/src/server/api/routes/settings.ts @@ -5,7 +5,6 @@ import { applyEnvValue, getEnvSettingsData, normalizeEnvInput, - serializeEnvBoolean, } from '@server/services/envSettings.js'; import { extractProjectsFromProfile,