* fix(tailoring): remove auto-sync effect causing race conditions Remove the problematic useEffect that was syncing incoming job data automatically. The effect caused race conditions where user edits were overwritten after auto-save completed. Now, state only resets when the job ID changes (user switches to a different job). User edits persist until explicitly saved. Fixes #133 * fix(tailoring): remove auto-save from tailor mode Remove the 1500ms auto-save timeout that was causing race conditions with the state sync. Users must now explicitly save changes via the Save Selection button or finalize to persist changes. * refactor(tailoring): remove draft status state and UI Remove the draftStatus state and related UI elements that showed saving/saved/unsaved status. With auto-save removed, this status indicator is no longer needed. Users now explicitly save via buttons. * test(tailoring): remove auto-save test Remove the test that verified auto-save behavior since auto-save has been removed from the tailor mode. Users now explicitly save via the Finalize button. * refactor(tailoring): remove dead focus tracking code Remove the activeField state and all related focus/blur tracking that was orphaned after removing auto-sync. The focus tracking was only used to prevent the auto-sync effect from running while editing. Changes: - Remove TailoringActiveField type export - Remove activeField state and setActiveField from useTailoringDraft - Remove handleFieldBlur callback from TailoringWorkspace - Remove onFieldFocus/onFieldBlur props and handlers from TailoringSections 39 lines of dead code removed. * docs(tailoring): clarify save behavior comment Update comment to distinguish between editor mode (Save Selection button) and tailor mode (only persists on finalize). Addresses review feedback. * docs(tailoring): clarify useEffect dependencies Add note explaining why 'job' is included in dependencies despite the effect being guarded by job.id check. Addresses review feedback about dependency array clarity. * fix(tailoring): sync server-normalized values after save Update persistCurrent and saveChanges to use the returned job from api.updateJob and call applyIncomingDraft. This ensures local state stays in sync with server-normalized values (e.g., trimmed fields). Also removes unused markCurrentAsSaved dependency. * refactor(tailoring): simplify draft sync effect Remove unused save snapshot helpers and stop exposing them from the hook. Track the latest job in a ref and only sync drafts when the job id changes to avoid unnecessary effect runs while keeping data correctness. Addresses review feedback on dependency churn and dead API surface.
Job Ops Orchestrator
A unified orchestrator for the job application pipeline. Discovers jobs, scores them for suitability, generates tailored resumes, and provides a UI to manage applications.
Architecture
orchestrator/
├── src/
│ ├── server/ # Express backend
│ │ ├── api/ # REST API routes
│ │ ├── db/ # SQLite + Drizzle ORM
│ │ ├── pipeline/ # Orchestration logic
│ │ ├── repositories/ # Data access layer
│ │ └── services/ # Integrations (crawler, AI, PDF)
│ ├── client/ # React frontend
│ │ ├── api/ # API client
│ │ ├── components/ # UI components
│ │ └── styles/ # CSS design system
│ └── shared/ # Shared types
├── data/ # SQLite DB + generated PDFs (gitignored)
└── public/ # Static assets
Setup
-
Install dependencies:
cd orchestrator npm install -
Set up environment:
cp .env.example .env # The app is self-configuring. You can add keys via the UI Onboarding.After the server starts, use the onboarding modal to connect OpenRouter, link your v4.rxresu.me account, and select a template resume.
OpenRouter is the default LLM provider, but LM Studio, Ollama, OpenAI, and Gemini are also supported.
Use
LLM_API_KEY/llmApiKeyto configure providers that require an API key. -
Initialize database:
npm run db:migrate -
Start development server:
npm run devThis starts:
- Backend API at
http://localhost:3001 - Frontend at
http://localhost:5173
- Backend API at
API Endpoints
Jobs
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/jobs |
List all jobs (filter with ?status=ready,discovered) |
| GET | /api/jobs/:id |
Get single job |
| PATCH | /api/jobs/:id |
Update job |
| POST | /api/jobs/:id/process |
Generate resume for job |
| POST | /api/jobs/:id/apply |
Mark as applied |
| POST | /api/jobs/:id/skip |
Mark as skipped |
Pipeline
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/pipeline/status |
Get pipeline status |
| GET | /api/pipeline/runs |
Get recent pipeline runs |
| POST | /api/pipeline/run |
Trigger pipeline manually |
| POST | /api/webhook/trigger |
Webhook for n8n (use WEBHOOK_SECRET) |
Daily Flow
-
17:00 - n8n triggers pipeline:
- Calls
POST /api/webhook/trigger - Pipeline crawls Gradcracker
- Scores jobs with AI
- Generates tailored resumes for top 10
- Calls
-
You review in the UI:
- See jobs at
http://localhost:5173 - "Ready" tab shows jobs with generated PDFs
- Use command bar search (
Cmd/Ctrl+K) to quickly find and open jobs - Click "View Job" to open application
- Download PDF and apply manually
- Click "Mark Applied" to mark application status
- See jobs at
n8n Setup
Create a workflow with:
- Schedule Trigger - Every day at 17:00
- HTTP Request:
- Method: POST
- URL:
http://localhost:3001/api/webhook/trigger - Headers:
Authorization: Bearer YOUR_WEBHOOK_SECRET
Development
# Run just the server
npm run dev:server
# Run just the client
npm run dev:client
# Run the pipeline manually
npm run pipeline:run
# Build for production
npm run build
npm start
Tech Stack
- Backend: Express, TypeScript, Drizzle ORM, SQLite
- Frontend: React, Vite, CSS (custom design system)
- AI: Configurable LLM provider (OpenRouter default; also supports OpenAI/Gemini/LM Studio/Ollama)
- PDF Generation: RxResume v4 API export (configured via Settings)
- Job Crawling: Wraps existing TypeScript Crawlee crawler