robustness
This commit is contained in:
parent
cae54c39fb
commit
4b65f39650
@ -215,8 +215,8 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
||||
setIsRegenerating(true);
|
||||
await api.generateJobPdf(job.id);
|
||||
toast.success("PDF regenerated");
|
||||
setMode("ready");
|
||||
await onJobUpdated();
|
||||
setMode("ready");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Failed to regenerate PDF";
|
||||
toast.error(message);
|
||||
@ -282,7 +282,7 @@ export const ReadyPanel: React.FC<ReadyPanelProps> = ({
|
||||
<Button asChild variant="outline" className="h-9 w-full gap-1 px-2 text-xs">
|
||||
<a
|
||||
href={pdfHref}
|
||||
download={`${personName.replace(/\s+/g, '_')}_${safeFilenamePart(job.employer)}.pdf`}
|
||||
download={`${safeFilenamePart(personName)}_${safeFilenamePart(job.employer)}.pdf`}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">Download PDF</span>
|
||||
|
||||
@ -4,7 +4,7 @@ import { z } from 'zod';
|
||||
import * as jobsRepo from '../../repositories/jobs.js';
|
||||
import { inferManualJobDetails } from '../../services/manualJob.js';
|
||||
import { scoreJobSuitability } from '../../services/scorer.js';
|
||||
import { loadResumeProfile } from '../../services/resumeProjects.js';
|
||||
import { getProfile } from '../../services/profile.js';
|
||||
import type { ApiResponse, ManualJobInferenceResponse } from '../../../shared/types.js';
|
||||
|
||||
export const manualJobsRouter = Router();
|
||||
@ -98,7 +98,7 @@ manualJobsRouter.post('/import', async (req: Request, res: Response) => {
|
||||
// Score asynchronously so the import returns immediately.
|
||||
(async () => {
|
||||
try {
|
||||
const rawProfile = await loadResumeProfile();
|
||||
const rawProfile = await getProfile();
|
||||
if (!rawProfile || typeof rawProfile !== 'object' || Array.isArray(rawProfile)) {
|
||||
throw new Error('Invalid resume profile format');
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { extractProjectsFromProfile, loadResumeProfile } from '../../services/resumeProjects.js';
|
||||
import { extractProjectsFromProfile } from '../../services/resumeProjects.js';
|
||||
import { getProfile } from '../../services/profile.js';
|
||||
|
||||
export const profileRouter = Router();
|
||||
|
||||
@ -8,7 +9,7 @@ export const profileRouter = Router();
|
||||
*/
|
||||
profileRouter.get('/projects', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const profile = await loadResumeProfile();
|
||||
const profile = await getProfile();
|
||||
const { catalog } = extractProjectsFromProfile(profile);
|
||||
res.json({ success: true, data: catalog });
|
||||
} catch (error) {
|
||||
@ -22,7 +23,7 @@ profileRouter.get('/projects', async (req: Request, res: Response) => {
|
||||
*/
|
||||
profileRouter.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const profile = await loadResumeProfile();
|
||||
const profile = await getProfile();
|
||||
res.json({ success: true, data: profile });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
@ -3,10 +3,10 @@ import { z } from 'zod';
|
||||
import * as settingsRepo from '../../repositories/settings.js';
|
||||
import {
|
||||
extractProjectsFromProfile,
|
||||
loadResumeProfile,
|
||||
normalizeResumeProjectsSettings,
|
||||
resolveResumeProjectsSettings,
|
||||
} from '../../services/resumeProjects.js';
|
||||
import { getProfile } from '../../services/profile.js';
|
||||
|
||||
export const settingsRouter = Router();
|
||||
|
||||
@ -37,7 +37,7 @@ settingsRouter.get('/', async (_req: Request, res: Response) => {
|
||||
const defaultJobCompleteWebhookUrl = process.env.JOB_COMPLETE_WEBHOOK_URL || '';
|
||||
const jobCompleteWebhookUrl = overrideJobCompleteWebhookUrl || defaultJobCompleteWebhookUrl;
|
||||
|
||||
const profile = await loadResumeProfile();
|
||||
const profile = await getProfile();
|
||||
const { catalog } = extractProjectsFromProfile(profile);
|
||||
const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects');
|
||||
const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw });
|
||||
@ -216,7 +216,7 @@ settingsRouter.patch('/', async (req: Request, res: Response) => {
|
||||
if (resumeProjects === null) {
|
||||
await settingsRepo.setSetting('resumeProjects', null);
|
||||
} else {
|
||||
const rawProfile = await loadResumeProfile();
|
||||
const rawProfile = await getProfile();
|
||||
|
||||
if (rawProfile === null || typeof rawProfile !== 'object' || Array.isArray(rawProfile)) {
|
||||
throw new Error('Invalid resume profile format: expected a non-null object');
|
||||
@ -301,7 +301,7 @@ settingsRouter.patch('/', async (req: Request, res: Response) => {
|
||||
const defaultJobCompleteWebhookUrl = process.env.JOB_COMPLETE_WEBHOOK_URL || '';
|
||||
const jobCompleteWebhookUrl = overrideJobCompleteWebhookUrl || defaultJobCompleteWebhookUrl;
|
||||
|
||||
const profile = await loadResumeProfile();
|
||||
const profile = await getProfile();
|
||||
const { catalog } = extractProjectsFromProfile(profile);
|
||||
const overrideResumeProjectsRaw = await settingsRepo.getSetting('resumeProjects');
|
||||
const resumeProjectsData = resolveResumeProjectsSettings({ catalog, overrideRaw: overrideResumeProjectsRaw });
|
||||
|
||||
@ -87,7 +87,12 @@ export async function callOpenRouter<T>(
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`OpenRouter API error: ${response.status}`);
|
||||
// Throw error with status to allow specific retries
|
||||
const errorBody = await response.text().catch(() => 'No error body');
|
||||
const err = new Error(`OpenRouter API error: ${response.status}`);
|
||||
(err as any).status = response.status;
|
||||
(err as any).body = errorBody;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@ -104,10 +109,22 @@ export async function callOpenRouter<T>(
|
||||
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const status = (error as any).status;
|
||||
|
||||
// Only retry on parsing errors
|
||||
if (attempt < maxRetries && message.includes('parse')) {
|
||||
console.warn(`⚠️ [${jobId ?? 'unknown'}] Attempt ${attempt + 1} failed: ${message}`);
|
||||
// Retry on:
|
||||
// 1. Parsing errors (AI returned malformed JSON)
|
||||
// 2. Rate limits (429)
|
||||
// 3. Server errors (5xx)
|
||||
// 4. Timeouts/Network issues
|
||||
const shouldRetry =
|
||||
message.includes('parse') ||
|
||||
status === 429 ||
|
||||
(status >= 500 && status <= 599) ||
|
||||
message.toLowerCase().includes('timeout') ||
|
||||
message.toLowerCase().includes('fetch failed');
|
||||
|
||||
if (attempt < maxRetries && shouldRetry) {
|
||||
console.warn(`⚠️ [${jobId ?? 'unknown'}] Attempt ${attempt + 1} failed (${status ?? 'no-status'}): ${message}. Retrying...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -129,9 +146,12 @@ export function parseJsonContent<T>(content: string, jobId?: string): T {
|
||||
candidate = candidate.replace(/```(?:json|JSON)?\s*/g, '').replace(/```/g, '').trim();
|
||||
|
||||
// Try to extract JSON object if there's surrounding text
|
||||
const jsonMatch = candidate.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
candidate = jsonMatch[0];
|
||||
// Use non-greedy match and find the outermost braces
|
||||
const firstBrace = candidate.indexOf('{');
|
||||
const lastBrace = candidate.lastIndexOf('}');
|
||||
|
||||
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
||||
candidate = candidate.substring(firstBrace, lastBrace + 1);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@ -69,7 +69,7 @@ export async function generatePdf(
|
||||
if (baseResume.sections?.skills?.items && Array.isArray(baseResume.sections.skills.items)) {
|
||||
baseResume.sections.skills.items = baseResume.sections.skills.items.map((skill: any, index: number) => ({
|
||||
...skill,
|
||||
id: skill.id || `skill-${Date.now()}-${index}`,
|
||||
id: skill.id || `skill-${index}`,
|
||||
visible: skill.visible ?? true,
|
||||
// Zod schema requires string, default to empty string if missing
|
||||
description: skill.description ?? '',
|
||||
@ -112,7 +112,7 @@ export async function generatePdf(
|
||||
const existing = existingSkills.find((s: any) => s.name === newSkill.name);
|
||||
|
||||
return {
|
||||
id: newSkill.id || existing?.id || `skill-${Date.now()}-${index}`,
|
||||
id: newSkill.id || existing?.id || `skill-${index}`,
|
||||
visible: newSkill.visible !== undefined ? newSkill.visible : (existing?.visible ?? true),
|
||||
name: newSkill.name || existing?.name || '',
|
||||
description: newSkill.description !== undefined ? newSkill.description : (existing?.description || ''),
|
||||
|
||||
@ -3,7 +3,7 @@ import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_PROFILE_PATH = process.env.RESUME_PROFILE_PATH || join(__dirname, '../../../../resume-generator/base.json');
|
||||
export const DEFAULT_PROFILE_PATH = process.env.RESUME_PROFILE_PATH || join(__dirname, '../../../../resume-generator/base.json');
|
||||
|
||||
let cachedProfile: any = null;
|
||||
let cachedProfilePath: string | null = null;
|
||||
|
||||
@ -4,23 +4,12 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import type { ResumeProjectCatalogItem, ResumeProjectsSettings } from '../../shared/types.js';
|
||||
|
||||
import { getProfile } from './profile.js';
|
||||
import { getProfile, DEFAULT_PROFILE_PATH } from './profile.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const DEFAULT_RESUME_PROFILE_PATH =
|
||||
process.env.RESUME_PROFILE_PATH || join(__dirname, '../../../../resume-generator/base.json');
|
||||
|
||||
type ResumeProjectSelectionItem = ResumeProjectCatalogItem & { summaryText: string };
|
||||
|
||||
export async function loadResumeProfile(profilePath: string = DEFAULT_RESUME_PROFILE_PATH): Promise<unknown> {
|
||||
if (profilePath === DEFAULT_RESUME_PROFILE_PATH) {
|
||||
return getProfile(profilePath);
|
||||
}
|
||||
const content = await readFile(profilePath, 'utf-8');
|
||||
return JSON.parse(content) as unknown;
|
||||
}
|
||||
|
||||
export function extractProjectsFromProfile(profile: unknown): {
|
||||
catalog: ResumeProjectCatalogItem[];
|
||||
selectionItems: ResumeProjectSelectionItem[];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user