pdf generation fix
This commit is contained in:
parent
9905353a49
commit
302fadb494
@ -21,6 +21,7 @@ export const App: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isPipelineRunning, setIsPipelineRunning] = useState(false);
|
||||
const [processingJobId, setProcessingJobId] = useState<string | null>(null);
|
||||
const [isProcessingAll, setIsProcessingAll] = useState(false);
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
// Toast helpers
|
||||
@ -147,6 +148,31 @@ export const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Process all discovered jobs
|
||||
const handleProcessAll = async () => {
|
||||
try {
|
||||
setIsProcessingAll(true);
|
||||
const result = await api.processAllDiscovered();
|
||||
addToast(`Processing ${result.count} jobs in background...`, 'info');
|
||||
|
||||
// Poll for completion
|
||||
const pollInterval = setInterval(async () => {
|
||||
await loadJobs();
|
||||
const currentStats = await api.getJobs();
|
||||
const stillDiscovered = currentStats.byStatus.discovered + currentStats.byStatus.processing;
|
||||
if (stillDiscovered === 0) {
|
||||
clearInterval(pollInterval);
|
||||
setIsProcessingAll(false);
|
||||
addToast('All jobs processed!', 'success');
|
||||
}
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
setIsProcessingAll(false);
|
||||
const message = error instanceof Error ? error.message : 'Failed to process jobs';
|
||||
addToast(message, 'error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@ -167,7 +193,9 @@ export const App: React.FC = () => {
|
||||
onApply={handleApply}
|
||||
onReject={handleReject}
|
||||
onProcess={handleProcess}
|
||||
onProcessAll={handleProcessAll}
|
||||
processingJobId={processingJobId}
|
||||
isProcessingAll={isProcessingAll}
|
||||
/>
|
||||
</main>
|
||||
|
||||
|
||||
@ -104,3 +104,16 @@ export async function clearDatabase(): Promise<{
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk operations
|
||||
export async function processAllDiscovered(): Promise<{
|
||||
message: string;
|
||||
count: number;
|
||||
}> {
|
||||
return fetchApi<{
|
||||
message: string;
|
||||
count: number;
|
||||
}>('/jobs/process-discovered', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
@ -114,15 +114,28 @@ export const JobCard: React.FC<JobCardProps> = ({
|
||||
View Job
|
||||
</a>
|
||||
|
||||
{/* View PDF in browser */}
|
||||
{hasPdf && (
|
||||
<a
|
||||
href={`/pdfs/resume_${job.id}.pdf`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-ghost"
|
||||
>
|
||||
<ExternalLinkIcon size={16} />
|
||||
View PDF
|
||||
</a>
|
||||
)}
|
||||
|
||||
{/* Download PDF */}
|
||||
{hasPdf && (
|
||||
<a
|
||||
href={`/pdfs/resume_${job.id}.pdf`}
|
||||
download
|
||||
download={`resume_${job.employer.replace(/[^a-z0-9]/gi, '_')}_${job.title.replace(/[^a-z0-9]/gi, '_')}.pdf`}
|
||||
className="btn btn-ghost"
|
||||
>
|
||||
<DownloadIcon size={16} />
|
||||
Download PDF
|
||||
Download
|
||||
</a>
|
||||
)}
|
||||
|
||||
|
||||
@ -5,13 +5,16 @@
|
||||
import React, { useState } from 'react';
|
||||
import type { Job, JobStatus } from '../../shared/types';
|
||||
import { JobCard } from './JobCard';
|
||||
import { RefreshIcon } from './Icons';
|
||||
|
||||
interface JobListProps {
|
||||
jobs: Job[];
|
||||
onApply: (id: string) => void;
|
||||
onReject: (id: string) => void;
|
||||
onProcess: (id: string) => void;
|
||||
onProcessAll: () => void;
|
||||
processingJobId: string | null;
|
||||
isProcessingAll: boolean;
|
||||
}
|
||||
|
||||
type FilterTab = 'ready' | 'discovered' | 'applied' | 'all';
|
||||
@ -28,7 +31,9 @@ export const JobList: React.FC<JobListProps> = ({
|
||||
onApply,
|
||||
onReject,
|
||||
onProcess,
|
||||
onProcessAll,
|
||||
processingJobId,
|
||||
isProcessingAll,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState<FilterTab>('ready');
|
||||
|
||||
@ -40,24 +45,49 @@ export const JobList: React.FC<JobListProps> = ({
|
||||
return jobs.filter(job => tab.statuses.includes(job.status));
|
||||
}, [jobs, activeTab]);
|
||||
|
||||
const discoveredCount = jobs.filter(j => j.status === 'discovered').length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="tabs">
|
||||
{tabs.map(tab => {
|
||||
const count = tab.statuses.length === 0
|
||||
? jobs.length
|
||||
: jobs.filter(j => tab.statuses.includes(j.status)).length;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label} ({count})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="tabs" style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-4)' }}>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-2)', flex: 1 }}>
|
||||
{tabs.map(tab => {
|
||||
const count = tab.statuses.length === 0
|
||||
? jobs.length
|
||||
: jobs.filter(j => tab.statuses.includes(j.status)).length;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
{tab.label} ({count})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{activeTab === 'discovered' && discoveredCount > 0 && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={onProcessAll}
|
||||
disabled={isProcessingAll}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
{isProcessingAll ? (
|
||||
<>
|
||||
<div className="spinner" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshIcon size={16} />
|
||||
Process All ({discoveredCount})
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filteredJobs.length === 0 ? (
|
||||
|
||||
@ -170,6 +170,39 @@ apiRouter.post('/jobs/:id/reject', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/jobs/process-discovered - Process all discovered jobs (generate PDFs)
|
||||
*/
|
||||
apiRouter.post('/jobs/process-discovered', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const discoveredJobs = await jobsRepo.getAllJobs(['discovered']);
|
||||
|
||||
// Process each job in background
|
||||
const processInBackground = async () => {
|
||||
for (const job of discoveredJobs.filter(j => j.status === 'discovered')) {
|
||||
try {
|
||||
await processJob(job.id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to process job ${job.id}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
processInBackground().catch(console.error);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
message: `Processing ${discoveredJobs.length} jobs`,
|
||||
count: discoveredJobs.length,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Pipeline API
|
||||
// ============================================================================
|
||||
|
||||
@ -97,11 +97,10 @@ async function runPythonPdfGenerator(
|
||||
outputFilename: string
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Note: This calls the Python script with the JSON path
|
||||
// The Python script needs to be modified to accept these args
|
||||
// For now, we'll use environment variables
|
||||
// Use the virtual environment's Python
|
||||
const pythonPath = join(RESUME_GEN_DIR, '.venv', 'bin', 'python');
|
||||
|
||||
const child = spawn('python3', ['rxresume_automation.py'], {
|
||||
const child = spawn(pythonPath, ['rxresume_automation.py'], {
|
||||
cwd: RESUME_GEN_DIR,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@ -18,6 +18,10 @@ export default defineConfig({
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/pdfs': {
|
||||
target: 'http://localhost:3001',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
||||
8
resume-generator/.gitignore
vendored
8
resume-generator/.gitignore
vendored
@ -1,2 +1,8 @@
|
||||
# any json files that start with "temp_"
|
||||
# Temp JSON files (used by orchestrator)
|
||||
temp_*.json
|
||||
|
||||
# Python virtual environment
|
||||
.venv/
|
||||
|
||||
# Generated resumes
|
||||
resumes/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user