Reduce low risk duplication (#79)

* clean up helpers

* shared in it's own top level folder

* workspaces setup

* build fix

* disable workspaces?

* run ci

* rename job-flow to gradcracker

* optional dependencies

* formatting?

* more optional modules

* allow post install runs

* node bump

* remove post install

* add optionals

* add more

* formatting

* comments, but im unsure

* run typescript DIRECTLY

* better build

* camoufox simplification

* lint

* build process doesn't exist

* build fix

* lockfile

* type check everything, build only for client

* rename steps correctly

* import from package!

* fix formatting

* don't fetch twice

* fix concern
This commit is contained in:
Shaheer Sarfaraz 2026-02-02 21:30:14 +00:00 committed by GitHub
parent 179deffe13
commit b94f85b149
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
165 changed files with 8909 additions and 13638 deletions

View File

@ -43,3 +43,21 @@ npm-debug.log*
# Documentation # Documentation
*.md *.md
!README.md !README.md
# CI/CD
.github
.gitlab-ci.yml
.travis.yml
# Docker
Dockerfile*
docker-compose*
# Temporary files
tmp
temp
*.tmp
# Coverage
coverage
.nyc_output

View File

@ -31,45 +31,47 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: "npm" cache: "npm"
cache-dependency-path: orchestrator/package-lock.json cache-dependency-path: package-lock.json
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci --workspaces --include-workspace-root
working-directory: orchestrator working-directory: .
- name: Build better-sqlite3
run: npm --workspace orchestrator rebuild better-sqlite3
working-directory: .
- name: Run Vitest - name: Run Vitest
run: npm run test:run run: npm --workspace orchestrator run test:run
working-directory: orchestrator working-directory: .
build: typecheck:
name: Build Verification name: Type Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
project: [orchestrator, extractors/gradcracker, extractors/ukvisajobs] project: [orchestrator, gradcracker-extractor, ukvisajobs-extractor]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 22
cache: "npm" cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json cache-dependency-path: package-lock.json
- name: Install dependencies - name: Install dependencies
run: | run: npm ci --workspaces --include-workspace-root
if [[ "${{ matrix.project }}" == extractors/* ]]; then working-directory: .
npm ci --ignore-scripts
else
npm ci
fi
working-directory: ${{ matrix.project }}
- name: Type Check (orchestrator only) - name: Check shared package types
run: npm run check:types:shared
working-directory: .
- name: Type Check ${{ matrix.project }}
run: npm --workspace ${{ matrix.project }} run check:types
working-directory: .
- name: Build ${{ matrix.project }} (client)
if: matrix.project == 'orchestrator' if: matrix.project == 'orchestrator'
run: npm run check:types run: npm --workspace orchestrator run build:client
working-directory: ${{ matrix.project }} working-directory: .
- name: Build ${{ matrix.project }}
run: npm run build
working-directory: ${{ matrix.project }}

5
.gitignore vendored
View File

@ -2,6 +2,11 @@
.env .env
*.env.local *.env.local
# Dependencies
node_modules/
**/node_modules/
**/.package-lock.json
# Data directory (bind mount in Docker) # Data directory (bind mount in Docker)
data/ data/

View File

@ -0,0 +1,81 @@
---
description: >-
Use this agent when you need a requirements-driven feature implementer that
asks clarifying questions, completes the implementation, and explains all
changes made. This is ideal for feature requests that need validation of scope
or acceptance criteria, and for end-to-end tasks where requirements must be
gathered before coding.
<example>
Context: The user wants a new filtering feature but has not specified edge
cases or UX details.
user: "Add a filter for archived items."
assistant: "I'll use the Agent tool to launch the
requirements-to-implementation agent to gather details and implement the
feature."
<commentary>
Since the requirements are underspecified, use the Agent tool to ask
clarifying questions, then implement and explain all changes.
</commentary>
assistant: "Now I'll ask a few clarifying questions before implementing."
</example>
<example>
Context: The user provides requirements for an API endpoint but is unsure
about error handling and pagination.
user: "Create a GET /reports endpoint for monthly summaries."
assistant: "I'll use the Agent tool to launch the
requirements-to-implementation agent for clarification and implementation."
<commentary>
Use the Agent tool to confirm pagination, auth, and error codes, then
implement and explain the modifications.
</commentary>
</example>
mode: all
---
You are a requirements-to-implementation specialist. Your job is to take in user requirements, ask clarifying questions when necessary, implement the feature end-to-end, and then explain all changes made in detail.
Behavior and workflow:
- Start by extracting the core intent, acceptance criteria, and any implied constraints from the user request.
- If requirements are ambiguous, incomplete, or risky to assume, ask concise, targeted clarifying questions. Ask only what is necessary to proceed.
- When you can reasonably infer defaults, proceed without asking and state the assumptions in your final explanation.
- After clarification (or reasonable inference), implement the feature completely, following the projects conventions and standards.
- Ensure the implementation is correct, tests or validations are updated if they exist, and edge cases are handled.
Quality and verification:
- Before writing code, identify impacted components and data flows.
- After implementing, perform a self-check: verify logic, error handling, and integration points.
- If tests are available, run relevant ones or state which should be run.
- Avoid over-engineering; match the complexity to the requirement.
Explanation requirements:
- Provide a comprehensive explanation of what was changed and why, describing all modifications.
- Reference relevant files and major logic points.
- Call out assumptions, tradeoffs, and any areas left for follow-up.
Decision framework:
- Prefer minimal, safe changes that meet the requirement.
- Escalate only when a decision could materially change behavior, security, or data integrity.
- If you must choose a default, pick the most conservative and documented option.
Output format:
- Provide clear, structured responses with: clarifying questions (if needed), implementation summary, detailed change explanation, and verification steps.
You will be proactive, precise, and reliable, ensuring the feature is implemented and fully explained.

View File

@ -1,60 +1,68 @@
# syntax=docker/dockerfile:1.6 # syntax=docker/dockerfile:1.6
FROM node:20-slim AS builder # ============================================================================
# BUILD STAGE
# ============================================================================
FROM node:22-slim AS builder
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# Put Playwright browsers in a known cacheable location ENV NODE_ENV=production
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip curl ca-certificates git \ ca-certificates \
python3 python3-minimal libpython3.11-minimal \
python3-pip \
build-essential pkg-config \ build-essential pkg-config \
&& rm -rf /var/lib/apt/lists/* libgtk-3-0 libgtk-3-common \
libdbus-glib-1-2 libxt6 libx11-xcb1 libasound2 \
curl && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
WORKDIR /app WORKDIR /app
# ---- Python deps (cached) ---- # Install Python dependencies with pip cache
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install --no-cache-dir --break-system-packages playwright python-jobspy pip3 install --no-cache-dir --break-system-packages playwright python-jobspy
# Install Firefox for Python Playwright (cached via PLAYWRIGHT_BROWSERS_PATH layer + mount) # Install Firefox for Python Playwright with cache
RUN python3 -m playwright install firefox RUN --mount=type=cache,target=/root/.cache/pip \
python3 -m playwright install firefox
# ---- Node deps (copy lockfiles; cached) ---- # Copy package files for dependency installation
COPY package*.json ./
COPY shared/package*.json ./shared/
COPY orchestrator/package*.json ./orchestrator/ COPY orchestrator/package*.json ./orchestrator/
COPY extractors/gradcracker/package*.json ./extractors/gradcracker/ COPY extractors/gradcracker/package*.json ./extractors/gradcracker/
COPY extractors/ukvisajobs/package*.json ./extractors/ukvisajobs/ COPY extractors/ukvisajobs/package*.json ./extractors/ukvisajobs/
WORKDIR /app/orchestrator # Install Node dependencies with npm cache (dev deps needed for build)
RUN --mount=type=cache,target=/root/.npm \ RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund --progress=false npm install --workspaces --include-workspace-root --include=dev \
--no-audit --no-fund --progress=false
WORKDIR /app/extractors/gradcracker # Fetch Camoufox binaries - do this before copying source code to cache the download
RUN --mount=type=cache,target=/root/.npm \ # Even if source changes, this layer remains cached.
npm ci --no-audit --no-fund --progress=false RUN npx camoufox-js fetch
# Camoufox fetch (cache npm + whatever it downloads to; if it uses HOME, this helps) # Copy source code
WORKDIR /app/extractors/gradcracker
RUN --mount=type=cache,target=/root/.npm \
npx camoufox fetch
WORKDIR /app/extractors/ukvisajobs
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund --progress=false
# ---- Copy sources late (preserves dependency cache) ----
WORKDIR /app WORKDIR /app
COPY shared ./shared
COPY orchestrator ./orchestrator COPY orchestrator ./orchestrator
COPY extractors/gradcracker ./extractors/gradcracker COPY extractors/gradcracker ./extractors/gradcracker
COPY extractors/jobspy ./extractors/jobspy COPY extractors/jobspy ./extractors/jobspy
COPY extractors/ukvisajobs ./extractors/ukvisajobs COPY extractors/ukvisajobs ./extractors/ukvisajobs
# Build orchestrator # Build client bundle
WORKDIR /app/orchestrator WORKDIR /app/orchestrator
RUN npm run build RUN npm run build:client
# ============================================================================
# PRODUCTION STAGE
# ============================================================================
FROM node:22-slim AS production
FROM node:20-slim AS runtime
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PORT=3001 ENV PORT=3001
@ -62,30 +70,53 @@ ENV PYTHON_PATH=/usr/bin/python3
ENV DATA_DIR=/app/data ENV DATA_DIR=/app/data
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
# Install only runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip curl ca-certificates \ ca-certificates \
libgtk-3-0 libdbus-glib-1-2 libxt6 libx11-xcb1 libasound2 \ python3 python3-minimal libpython3.11-minimal \
&& rm -rf /var/lib/apt/lists/* python3-pip \
libgtk-3-0 libgtk-3-common \
libdbus-glib-1-2 libxt6 libx11-xcb1 libasound2 \
curl && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
WORKDIR /app WORKDIR /app
# Python runtime deps # Copy Python dependencies from builder
RUN --mount=type=cache,target=/root/.cache/pip \ COPY --from=builder /usr/local/lib/python3.11/dist-packages /usr/local/lib/python3.11/dist-packages
pip3 install --no-cache-dir --break-system-packages playwright python-jobspy
# Copy cached browsers from builder (fast; no redownload)
COPY --from=builder /ms-playwright /ms-playwright COPY --from=builder /ms-playwright /ms-playwright
# Copy package files
COPY package*.json ./
COPY shared/package*.json ./shared/
COPY orchestrator/package*.json ./orchestrator/
COPY extractors/gradcracker/package*.json ./extractors/gradcracker/
COPY extractors/ukvisajobs/package*.json ./extractors/ukvisajobs/
# Install production Node dependencies only
RUN --mount=type=cache,target=/root/.npm \
npm install --workspaces --include-workspace-root --omit=dev \
--no-audit --no-fund --progress=false
# Copy built assets and source code from builder
COPY --from=builder /app/orchestrator/dist ./orchestrator/dist
COPY shared ./shared
COPY orchestrator ./orchestrator
COPY extractors/gradcracker ./extractors/gradcracker
COPY extractors/jobspy ./extractors/jobspy
COPY extractors/ukvisajobs ./extractors/ukvisajobs
# Reuse Camoufox binaries from builder instead of fetching again
COPY --from=builder /root/.cache/camoufox /root/.cache/camoufox COPY --from=builder /root/.cache/camoufox /root/.cache/camoufox
# Copy built app + node_modules from builder (fast path) WORKDIR /app
COPY --from=builder /app/orchestrator /app/orchestrator # Create data directory
COPY --from=builder /app/extractors /app/extractors
RUN mkdir -p /app/data/pdfs RUN mkdir -p /app/data/pdfs
EXPOSE 3001 EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1 CMD curl -f http://localhost:3001/health || exit 1
WORKDIR /app/orchestrator WORKDIR /app/orchestrator
CMD ["sh", "-c", "npm run db:migrate && npm run start"] CMD ["sh", "-c", "npx tsx src/server/db/migrate.ts && npm run start"]

View File

@ -7,8 +7,9 @@ FROM apify/actor-node-playwright-chrome:20-1.50.1 AS builder
# to speed up the build using Docker layer cache. # to speed up the build using Docker layer cache.
COPY --chown=myuser package*.json ./ COPY --chown=myuser package*.json ./
# Install all dependencies. Don't audit to speed up the installation. # Install all dependencies with cache mount for faster rebuilds
RUN npm install --include=dev --audit=false RUN --mount=type=cache,target=/root/.npm \
npm install --include=dev --audit=false --progress=false
# Next, copy the source files using the user set # Next, copy the source files using the user set
# in the base image. # in the base image.
@ -33,14 +34,16 @@ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=0
# Install NPM packages, skip optional and development dependencies to # Install NPM packages, skip optional and development dependencies to
# keep the image small. Avoid logging too much and print the dependency # keep the image small. Avoid logging too much and print the dependency
# tree for debugging # tree for debugging
RUN npm --quiet set progress=false \ RUN --mount=type=cache,target=/root/.npm \
&& npm install --omit=dev \ npm --quiet set progress=false \
&& npm install --omit=dev --progress=false \
&& echo "Installed NPM packages:" \ && echo "Installed NPM packages:" \
&& (npm list --omit=dev --all || true) \ && (npm list --omit=dev --all || true) \
&& echo "Node.js version:" \ && echo "Node.js version:" \
&& node --version \ && node --version \
&& echo "NPM version:" \ && echo "NPM version:" \
&& npm --version && npm --version \
&& npm run get-binaries
# Next, copy the remaining files and directories with the source code. # Next, copy the remaining files and directories with the source code.
# Since we do this after NPM install, quick build will be really fast # Since we do this after NPM install, quick build will be really fast

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,30 @@
{ {
"name": "job-flow", "name": "gradcracker-extractor",
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
"description": "This is an example of a Crawlee project.", "description": "This is an example of a Crawlee project.",
"dependencies": { "dependencies": {
"camoufox-js": "^0.8.0", "camoufox-js": "^0.8.0",
"crawlee": "^3.0.0", "crawlee": "^3.0.0",
"playwright": "*" "playwright": "*",
"tsx": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@apify/tsconfig": "^0.1.0", "@apify/tsconfig": "^0.1.0",
"@types/fs-extra": "^11", "@types/fs-extra": "^11",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"tsx": "^4.4.0",
"typescript": "~5.9.0" "typescript": "~5.9.0"
}, },
"optionalDependencies": {
"impit-linux-x64-gnu": "^0.1.0"
},
"scripts": { "scripts": {
"start": "npm run start:dev", "start": "tsx src/main.ts",
"start:prod": "node dist/main.js",
"start:dev": "tsx src/main.ts", "start:dev": "tsx src/main.ts",
"build": "tsc", "check:types": "tsc --noEmit",
"test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1", "test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1",
"get-binaries": "camoufox-js fetch", "get-binaries": "camoufox-js fetch"
"postinstall": "npm run get-binaries"
}, },
"author": "It's not you it's me", "author": "It's not you it's me",
"license": "ISC" "license": "ISC"

File diff suppressed because it is too large Load Diff

View File

@ -3,24 +3,26 @@
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
"description": "UK Visa Jobs extractor - fetches job listings that may sponsor work visas", "description": "UK Visa Jobs extractor - fetches job listings that may sponsor work visas",
"main": "dist/main.js", "main": "src/main.ts",
"dependencies": { "dependencies": {
"camoufox-js": "^0.8.0", "camoufox-js": "^0.8.0",
"playwright": "^1.57.0" "playwright": "^1.57.0",
"tsx": "^4.4.0",
"job-ops-shared": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"@apify/tsconfig": "^0.1.0", "@apify/tsconfig": "^0.1.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"tsx": "^4.4.0",
"typescript": "~5.9.0" "typescript": "~5.9.0"
}, },
"optionalDependencies": {
"impit-linux-x64-gnu": "^0.1.0"
},
"scripts": { "scripts": {
"start": "npm run start:dev", "start": "tsx src/main.ts",
"start:prod": "node dist/main.js",
"start:dev": "tsx src/main.ts", "start:dev": "tsx src/main.ts",
"build": "tsc", "check:types": "tsc --noEmit",
"get-binaries": "camoufox-js fetch", "get-binaries": "camoufox-js fetch"
"postinstall": "npm run get-binaries"
}, },
"author": "", "author": "",
"license": "ISC" "license": "ISC"

View File

@ -1,4 +1,4 @@
/** /**
* UK Visa Jobs Extractor * UK Visa Jobs Extractor
* *
* Fetches job listings from my.ukvisajobs.com that may sponsor work visas. * Fetches job listings from my.ukvisajobs.com that may sponsor work visas.
@ -16,6 +16,10 @@
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import {
toNumberOrNull,
toStringOrNull,
} from "job-ops-shared/utils/type-conversion";
import type { Request } from "playwright"; import type { Request } from "playwright";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@ -103,29 +107,6 @@ class UkVisaJobsAuthError extends Error {
} }
} }
function toStringOrNull(value: unknown): string | null {
if (value === null || value === undefined) return null;
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
if (typeof value === "number" || typeof value === "boolean")
return String(value);
return null;
}
function toNumberOrNull(value: unknown): number | null {
if (value === null || value === undefined) return null;
if (typeof value === "number") return Number.isFinite(value) ? value : null;
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) return null;
const parsed = Number(trimmed);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
async function fetchPage( async function fetchPage(
pageNo: number, pageNo: number,
session: UkVisaJobsAuthSession, session: UkVisaJobsAuthSession,

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"paths": {
"@shared/*": ["../../shared/dist/*"]
}
}
}

View File

@ -6,7 +6,11 @@
"target": "ES2022", "target": "ES2022",
"outDir": "dist", "outDir": "dist",
"noUnusedLocals": false, "noUnusedLocals": false,
"lib": ["DOM"] "lib": ["DOM"],
"baseUrl": ".",
"paths": {
"@shared/*": ["../../shared/src/*"]
}
}, },
"include": ["./src/**/*"] "include": ["./src/**/*"]
} }

View File

@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"description": "Unified orchestrator for job application pipeline", "description": "Unified orchestrator for job application pipeline",
"main": "dist/server/index.js", "main": "src/server/index.ts",
"scripts": { "scripts": {
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"dev:server": "tsx watch src/server/index.ts", "dev:server": "tsx watch src/server/index.ts",
@ -15,11 +15,10 @@
"check:types": "tsc --noEmit", "check:types": "tsc --noEmit",
"format": "biome format", "format": "biome format",
"format:fix": "biome format --write", "format:fix": "biome format --write",
"build": "npm run build:client && npm run build:server",
"build:server": "tsc -p tsconfig.server.json && tsc-alias -p tsconfig.server.json",
"build:client": "vite build", "build:client": "vite build",
"start": "node dist/server/index.js", "start": "tsx src/server/index.ts",
"db:migrate": "tsx src/server/db/migrate.ts", "db:migrate": "tsx src/server/db/migrate.ts",
"db:migrate:prod": "tsx src/server/db/migrate.ts",
"db:clear": "tsx src/server/db/clear.ts", "db:clear": "tsx src/server/db/clear.ts",
"db:drop": "tsx src/server/db/clear.ts --drop", "db:drop": "tsx src/server/db/clear.ts --drop",
"pipeline:run": "tsx src/server/pipeline/run.ts", "pipeline:run": "tsx src/server/pipeline/run.ts",
@ -52,6 +51,9 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-orm": "^0.38.2", "drizzle-orm": "^0.38.2",
"get-tsconfig": "^4.10.0",
"jsdom": "^25.0.1",
"tsx": "^4.19.2",
"express": "^4.18.2", "express": "^4.18.2",
"lucide-react": "^0.561.0", "lucide-react": "^0.561.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
@ -83,17 +85,22 @@
"autoprefixer": "^10.4.22", "autoprefixer": "^10.4.22",
"concurrently": "^9.1.0", "concurrently": "^9.1.0",
"drizzle-kit": "^0.30.1", "drizzle-kit": "^0.30.1",
"jsdom": "^25.0.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.0.2", "react-router-dom": "^7.0.2",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tsc-alias": "^1.8.16",
"tsx": "^4.19.2",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^6.0.3", "vite": "^6.0.3",
"vitest": "^4.0.16" "vitest": "^4.0.16"
},
"optionalDependencies": {
"lightningcss-linux-x64-gnu": "^1.29.3",
"lightningcss-linux-arm64-gnu": "^1.29.3",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@rollup/rollup-linux-arm64-gnu": "^4.30.0",
"@rollup/rollup-linux-x64-gnu": "^4.30.0"
} }
} }

View File

@ -2,7 +2,6 @@
* API client for the orchestrator backend. * API client for the orchestrator backend.
*/ */
import { trackEvent } from "@/lib/analytics";
import type { import type {
ApiResponse, ApiResponse,
ApplicationStage, ApplicationStage,
@ -31,7 +30,8 @@ import type {
VisaSponsor, VisaSponsor,
VisaSponsorSearchResponse, VisaSponsorSearchResponse,
VisaSponsorStatusResponse, VisaSponsorStatusResponse,
} from "../../shared/types"; } from "@shared/types";
import { trackEvent } from "@/lib/analytics";
const API_BASE = "/api"; const API_BASE = "/api";

View File

@ -1,7 +1,7 @@
import type { Job } from "@shared/types.js";
import { Sparkles } from "lucide-react"; import { Sparkles } from "lucide-react";
import type React from "react"; import type React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Job } from "../../shared/types";
interface FitAssessmentProps { interface FitAssessmentProps {
job: Job; job: Job;

View File

@ -3,6 +3,7 @@
*/ */
import { isNavActive, NAV_LINKS } from "@client/components/navigation"; import { isNavActive, NAV_LINKS } from "@client/components/navigation";
import type { JobSource } from "@shared/types.js";
import { ChevronDown, Loader2, Menu, Play, RefreshCcw } from "lucide-react"; import { ChevronDown, Loader2, Menu, Play, RefreshCcw } from "lucide-react";
import React from "react"; import React from "react";
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation } from "react-router-dom";
@ -24,7 +25,6 @@ import {
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { cn, sourceLabel } from "@/lib/utils"; import { cn, sourceLabel } from "@/lib/utils";
import type { JobSource } from "../../shared/types";
interface HeaderProps { interface HeaderProps {
onRunPipeline: () => void; onRunPipeline: () => void;

View File

@ -1,8 +1,8 @@
import type { Job } from "@shared/types.js";
import { act, fireEvent, render, screen } from "@testing-library/react"; import { act, fireEvent, render, screen } from "@testing-library/react";
import type React from "react"; import type React from "react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Job } from "../../shared/types";
import { useSettings } from "../hooks/useSettings"; import { useSettings } from "../hooks/useSettings";
import { JobHeader } from "./JobHeader"; import { JobHeader } from "./JobHeader";

View File

@ -1,3 +1,4 @@
import type { Job, JobStatus } from "@shared/types.js";
import { import {
ArrowUpRight, ArrowUpRight,
Calendar, Calendar,
@ -18,7 +19,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn, formatDate, sourceLabel } from "@/lib/utils"; import { cn, formatDate, sourceLabel } from "@/lib/utils";
import type { Job, JobStatus } from "../../shared/types";
import { useSettings } from "../hooks/useSettings"; import { useSettings } from "../hooks/useSettings";
import { import {
defaultStatusToken, defaultStatusToken,

View File

@ -1,4 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { StageEvent } from "@shared/types.js";
import { STAGE_LABELS } from "@shared/types.js";
import React from "react"; import React from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import * as z from "zod"; import * as z from "zod";
@ -22,8 +24,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import type { StageEvent } from "../../shared/types";
import { STAGE_LABELS } from "../../shared/types";
const logEventSchema = z.object({ const logEventSchema = z.object({
stage: z.string(), stage: z.string(),

View File

@ -2,6 +2,7 @@
* Manual job import flow (paste JD -> infer -> review -> import). * Manual job import flow (paste JD -> infer -> review -> import).
*/ */
import type { ManualJobDraft } from "@shared/types.js";
import { import {
ArrowLeft, ArrowLeft,
ClipboardPaste, ClipboardPaste,
@ -13,7 +14,6 @@ import {
import type React from "react"; import type React from "react";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
@ -26,7 +26,6 @@ import {
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ManualJobDraft } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
type ManualImportStep = "paste" | "loading" | "review"; type ManualImportStep = "paste" | "loading" | "review";

View File

@ -9,7 +9,7 @@ import {
LLM_PROVIDERS, LLM_PROVIDERS,
normalizeLlmProvider, normalizeLlmProvider,
} from "@client/pages/settings/utils"; } from "@client/pages/settings/utils";
import type { ValidationResult } from "@shared/types"; import type { ValidationResult } from "@shared/types.js";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";

View File

@ -1,9 +1,9 @@
import type { Job } from "@shared/types.js";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type React from "react"; import type React from "react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Job } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
import { ReadyPanel } from "./ReadyPanel"; import { ReadyPanel } from "./ReadyPanel";

View File

@ -7,6 +7,7 @@
* Now includes inline tailoring mode for editing and regenerating PDFs without switching tabs. * Now includes inline tailoring mode for editing and regenerating PDFs without switching tabs.
*/ */
import type { Job, ResumeProjectCatalogItem } from "@shared/types.js";
import { import {
CheckCircle2, CheckCircle2,
ChevronUp, ChevronUp,
@ -39,7 +40,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn, copyTextToClipboard, formatJobForWebhook } from "@/lib/utils"; import { cn, copyTextToClipboard, formatJobForWebhook } from "@/lib/utils";
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
import { useProfile } from "../hooks/useProfile"; import { useProfile } from "../hooks/useProfile";
import { useRescoreJob } from "../hooks/useRescoreJob"; import { useRescoreJob } from "../hooks/useRescoreJob";

View File

@ -2,6 +2,7 @@
* Stats dashboard showing job counts by status. * Stats dashboard showing job counts by status.
*/ */
import type { JobStatus } from "@shared/types.js";
import { import {
CheckCircle2, CheckCircle2,
Clock, Clock,
@ -11,9 +12,7 @@ import {
XCircle, XCircle,
} from "lucide-react"; } from "lucide-react";
import type React from "react"; import type React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { JobStatus } from "../../shared/types";
interface StatsProps { interface StatsProps {
stats: Record<JobStatus, number>; stats: Record<JobStatus, number>;

View File

@ -2,12 +2,11 @@
* Status badge component. * Status badge component.
*/ */
import type { JobStatus } from "@shared/types.js";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import type React from "react"; import type React from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { JobStatus } from "../../shared/types";
interface StatusBadgeProps { interface StatusBadgeProps {
status: JobStatus; status: JobStatus;

View File

@ -1,6 +1,6 @@
import type { Job } from "@shared/types.js";
import type React from "react"; import type React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Job } from "../../shared/types";
interface TailoredSummaryProps { interface TailoredSummaryProps {
job: Job; job: Job;

View File

@ -1,3 +1,4 @@
import type { Job, ResumeProjectCatalogItem } from "@shared/types.js";
import { import {
AlertTriangle, AlertTriangle,
Check, Check,
@ -8,11 +9,9 @@ import {
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import type { Job, ResumeProjectCatalogItem } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
interface TailoringEditorProps { interface TailoringEditorProps {

View File

@ -3,10 +3,10 @@
* Tests real-world edge cases for conversion funnel and analytics * Tests real-world edge cases for conversion funnel and analytics
*/ */
import type { ApplicationStage, StageEvent } from "@shared/types.js";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import type React from "react"; import type React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ApplicationStage, StageEvent } from "../../../shared/types";
import { ConversionAnalytics } from "./ConversionAnalytics"; import { ConversionAnalytics } from "./ConversionAnalytics";
// Mock UI components // Mock UI components

View File

@ -3,6 +3,7 @@
* Shows Application Response conversion metrics including funnel, time-series, and insights. * Shows Application Response conversion metrics including funnel, time-series, and insights.
*/ */
import type { StageEvent } from "@shared/types.js";
import { TrendingDown, TrendingUp } from "lucide-react"; import { TrendingDown, TrendingUp } from "lucide-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { import {
@ -18,7 +19,6 @@ import {
XAxis, XAxis,
YAxis, YAxis,
} from "recharts"; } from "recharts";
import { import {
Card, Card,
CardContent, CardContent,
@ -27,7 +27,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart"; import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import type { StageEvent } from "../../../shared/types";
type FunnelStage = { type FunnelStage = {
name: string; name: string;

View File

@ -1,3 +1,4 @@
import type { Job } from "@shared/types.js";
import { import {
ChevronUp, ChevronUp,
ExternalLink, ExternalLink,
@ -8,7 +9,6 @@ import {
} from "lucide-react"; } from "lucide-react";
import type React from "react"; import type React from "react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -17,7 +17,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import type { Job } from "../../../shared/types";
import { FitAssessment, JobHeader, TailoredSummary } from ".."; import { FitAssessment, JobHeader, TailoredSummary } from "..";
import { CollapsibleSection } from "./CollapsibleSection"; import { CollapsibleSection } from "./CollapsibleSection";
import { getPlainDescription } from "./helpers"; import { getPlainDescription } from "./helpers";

View File

@ -1,9 +1,9 @@
import type { Job } from "@shared/types.js";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type React from "react"; import type React from "react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Job } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
import { DiscoveredPanel } from "./DiscoveredPanel"; import { DiscoveredPanel } from "./DiscoveredPanel";

View File

@ -1,7 +1,7 @@
import type { Job } from "@shared/types.js";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Job } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
import { useRescoreJob } from "../../hooks/useRescoreJob"; import { useRescoreJob } from "../../hooks/useRescoreJob";
import { DecideMode } from "./DecideMode"; import { DecideMode } from "./DecideMode";

View File

@ -1,9 +1,8 @@
import type { ResumeProjectCatalogItem } from "@shared/types.js";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import type React from "react"; import type React from "react";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { ResumeProjectCatalogItem } from "../../../shared/types";
interface ProjectSelectorProps { interface ProjectSelectorProps {
catalog: ResumeProjectCatalogItem[]; catalog: ResumeProjectCatalogItem[];

View File

@ -1,11 +1,10 @@
import type { Job, ResumeProjectCatalogItem } from "@shared/types.js";
import { ArrowLeft, Check, Loader2, Sparkles } from "lucide-react"; import { ArrowLeft, Check, Loader2, Sparkles } from "lucide-react";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import type { Job, ResumeProjectCatalogItem } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
import { CollapsibleSection } from "./CollapsibleSection"; import { CollapsibleSection } from "./CollapsibleSection";
import { ProjectSelector } from "./ProjectSelector"; import { ProjectSelector } from "./ProjectSelector";

View File

@ -1,5 +1,5 @@
import type { ResumeProfile } from "@shared/types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { ResumeProfile } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
let profileCache: ResumeProfile | null = null; let profileCache: ResumeProfile | null = null;

View File

@ -1,5 +1,5 @@
import type { AppSettings } from "@shared/types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { AppSettings } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
let settingsCache: AppSettings | null = null; let settingsCache: AppSettings | null = null;

View File

@ -6,6 +6,7 @@ import {
type DurationValue, type DurationValue,
} from "@client/components/charts"; } from "@client/components/charts";
import { PageMain } from "@client/components/layout"; import { PageMain } from "@client/components/layout";
import type { StageEvent } from "@shared/types.js";
import { Home, Menu } from "lucide-react"; import { Home, Menu } from "lucide-react";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
@ -19,7 +20,6 @@ import {
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { StageEvent } from "../../shared/types";
import { isNavActive, NAV_LINKS } from "../components/navigation"; import { isNavActive, NAV_LINKS } from "../components/navigation";
type JobWithEvents = { type JobWithEvents = {

View File

@ -1,3 +1,11 @@
import {
type ApplicationStage,
type ApplicationTask,
type Job,
type JobOutcome,
STAGE_LABELS,
type StageEvent,
} from "@shared/types.js";
import confetti from "canvas-confetti"; import confetti from "canvas-confetti";
import { import {
ArrowLeft, ArrowLeft,
@ -13,14 +21,6 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatTimestamp } from "@/lib/utils"; import { formatTimestamp } from "@/lib/utils";
import {
type ApplicationStage,
type ApplicationTask,
type Job,
type JobOutcome,
STAGE_LABELS,
type StageEvent,
} from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
import { ConfirmDelete } from "../components/ConfirmDelete"; import { ConfirmDelete } from "../components/ConfirmDelete";
import { JobHeader } from "../components/JobHeader"; import { JobHeader } from "../components/JobHeader";

View File

@ -1,7 +1,7 @@
import type { Job } from "@shared/types.js";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Job } from "../../shared/types";
import { OrchestratorPage } from "./OrchestratorPage"; import { OrchestratorPage } from "./OrchestratorPage";
import type { FilterTab } from "./orchestrator/constants"; import type { FilterTab } from "./orchestrator/constants";

View File

@ -3,13 +3,13 @@
*/ */
import { useSettings } from "@client/hooks/useSettings"; import { useSettings } from "@client/hooks/useSettings";
import type { JobSource } from "@shared/types.js";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"; import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
import type { JobSource } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
import { ManualImportSheet } from "../components"; import { ManualImportSheet } from "../components";
import type { FilterTab, JobSort } from "./orchestrator/constants"; import type { FilterTab, JobSort } from "./orchestrator/constants";

View File

@ -1,4 +1,4 @@
import type { AppSettings } from "@shared/types"; import type { AppSettings } from "@shared/types.js";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";

View File

@ -20,14 +20,14 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { import {
type UpdateSettingsInput, type UpdateSettingsInput,
updateSettingsSchema, updateSettingsSchema,
} from "@shared/settings-schema"; } from "@shared/settings-schema.js";
import type { import type {
AppSettings, AppSettings,
BackupInfo, BackupInfo,
JobStatus, JobStatus,
ResumeProjectCatalogItem, ResumeProjectCatalogItem,
ResumeProjectsSettings, ResumeProjectsSettings,
} from "@shared/types"; } from "@shared/types.js";
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";

View File

@ -3,6 +3,7 @@
*/ */
import { isNavActive, NAV_LINKS } from "@client/components/navigation"; import { isNavActive, NAV_LINKS } from "@client/components/navigation";
import type { CreateJobInput } from "@shared/types.js";
import { import {
Briefcase, Briefcase,
Calendar, Calendar,
@ -36,7 +37,6 @@ import {
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { cn, formatDate, formatDateTime, stripHtml } from "@/lib/utils"; import { cn, formatDate, formatDateTime, stripHtml } from "@/lib/utils";
import type { CreateJobInput } from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
const clampText = (value: string, max = 160) => const clampText = (value: string, max = 160) =>

View File

@ -3,6 +3,11 @@
* Allows searching the government's list of licensed visa sponsors. * Allows searching the government's list of licensed visa sponsors.
*/ */
import type {
VisaSponsor,
VisaSponsorSearchResult,
VisaSponsorStatusResponse,
} from "@shared/types.js";
import { import {
AlertCircle, AlertCircle,
Building2, Building2,
@ -20,17 +25,11 @@ import {
import type React from "react"; import type React from "react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer"; import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn, formatDateTime } from "@/lib/utils"; import { cn, formatDateTime } from "@/lib/utils";
import type {
VisaSponsor,
VisaSponsorSearchResult,
VisaSponsorStatusResponse,
} from "../../shared/types";
import * as api from "../api"; import * as api from "../api";
import { import {
DetailPanel, DetailPanel,

View File

@ -1,6 +1,6 @@
import type { StageEvent } from "@shared/types.js";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { StageEvent } from "../../../shared/types";
import { JobTimeline } from "./Timeline"; import { JobTimeline } from "./Timeline";
const baseEvent: StageEvent = { const baseEvent: StageEvent = {

View File

@ -1,3 +1,8 @@
import {
type ApplicationStage,
STAGE_LABELS,
type StageEvent,
} from "@shared/types.js";
import { import {
CheckCircle2, CheckCircle2,
ClipboardList, ClipboardList,
@ -11,14 +16,8 @@ import {
Video, Video,
} from "lucide-react"; } from "lucide-react";
import React from "react"; import React from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { cn, formatTimestamp, formatTimestampWithTime } from "@/lib/utils"; import { cn, formatTimestamp, formatTimestampWithTime } from "@/lib/utils";
import {
type ApplicationStage,
STAGE_LABELS,
type StageEvent,
} from "../../../shared/types";
import { CollapsibleSection } from "../../components/discovered-panel/CollapsibleSection"; import { CollapsibleSection } from "../../components/discovered-panel/CollapsibleSection";
const stageIcons: Record<ApplicationStage, React.ReactNode> = { const stageIcons: Record<ApplicationStage, React.ReactNode> = {

View File

@ -1,7 +1,7 @@
import type { Job } from "@shared/types.js";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import type React from "react"; import type React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Job } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
import { JobDetailPanel } from "./JobDetailPanel"; import { JobDetailPanel } from "./JobDetailPanel";

View File

@ -1,3 +1,4 @@
import type { Job } from "@shared/types.js";
import { import {
CheckCircle2, CheckCircle2,
Copy, Copy,
@ -15,7 +16,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -32,7 +32,6 @@ import {
safeFilenamePart, safeFilenamePart,
stripHtml, stripHtml,
} from "@/lib/utils"; } from "@/lib/utils";
import type { Job } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
import { import {
DiscoveredPanel, DiscoveredPanel,

View File

@ -1,6 +1,6 @@
import type { Job } from "@shared/types.js";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { Job } from "../../../shared/types";
import { JobListPanel } from "./JobListPanel"; import { JobListPanel } from "./JobListPanel";
const createJob = (overrides: Partial<Job> = {}): Job => ({ const createJob = (overrides: Partial<Job> = {}): Job => ({

View File

@ -1,9 +1,7 @@
import type { Job } from "@shared/types.js";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import type React from "react"; import type React from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Job } from "../../../shared/types";
import type { FilterTab } from "./constants"; import type { FilterTab } from "./constants";
import { defaultStatusToken, emptyStateCopy, statusTokens } from "./constants"; import { defaultStatusToken, emptyStateCopy, statusTokens } from "./constants";

View File

@ -1,7 +1,7 @@
import type { JobSource } from "@shared/types.js";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import type { ComponentProps } from "react"; import type { ComponentProps } from "react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import type { JobSource } from "../../../shared/types";
import type { FilterTab, JobSort } from "./constants"; import type { FilterTab, JobSort } from "./constants";
import { OrchestratorFilters } from "./OrchestratorFilters"; import { OrchestratorFilters } from "./OrchestratorFilters";

View File

@ -1,6 +1,6 @@
import type { JobSource } from "@shared/types.js";
import { ArrowUpDown, Filter, Search } from "lucide-react"; import { ArrowUpDown, Filter, Search } from "lucide-react";
import type React from "react"; import type React from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -14,9 +14,7 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { sourceLabel } from "@/lib/utils"; import { sourceLabel } from "@/lib/utils";
import type { JobSource } from "../../../shared/types";
import type { FilterTab, JobSort } from "./constants"; import type { FilterTab, JobSort } from "./constants";
import { import {
defaultSortDirection, defaultSortDirection,

View File

@ -1,4 +1,5 @@
import { isNavActive, NAV_LINKS } from "@client/components/navigation"; import { isNavActive, NAV_LINKS } from "@client/components/navigation";
import type { JobSource } from "@shared/types.js";
import { import {
ChevronDown, ChevronDown,
FileText, FileText,
@ -26,7 +27,6 @@ import {
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet"; } from "@/components/ui/sheet";
import { cn, sourceLabel } from "@/lib/utils"; import { cn, sourceLabel } from "@/lib/utils";
import type { JobSource } from "../../../shared/types";
import { orderedSources } from "./constants"; import { orderedSources } from "./constants";
interface OrchestratorHeaderProps { interface OrchestratorHeaderProps {

View File

@ -1,5 +1,5 @@
import type { JobStatus } from "@shared/types.js";
import type React from "react"; import type React from "react";
import type { JobStatus } from "../../../shared/types";
import { PipelineProgress } from "../../components"; import { PipelineProgress } from "../../components";
interface OrchestratorSummaryProps { interface OrchestratorSummaryProps {

View File

@ -1,4 +1,4 @@
import type { JobSource, JobStatus } from "../../../shared/types"; import type { JobSource, JobStatus } from "@shared/types";
export const DEFAULT_PIPELINE_SOURCES: JobSource[] = [ export const DEFAULT_PIPELINE_SOURCES: JobSource[] = [
"gradcracker", "gradcracker",

View File

@ -1,6 +1,5 @@
import type { Job, JobSource } from "@shared/types";
import { useMemo } from "react"; import { useMemo } from "react";
import type { Job, JobSource } from "../../../shared/types";
import type { FilterTab, JobSort } from "./constants"; import type { FilterTab, JobSort } from "./constants";
import { compareJobs, jobMatchesQuery } from "./utils"; import { compareJobs, jobMatchesQuery } from "./utils";

View File

@ -1,7 +1,6 @@
import type { Job, JobStatus } from "@shared/types";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Job, JobStatus } from "../../../shared/types";
import * as api from "../../api"; import * as api from "../../api";
const initialStats: Record<JobStatus, number> = { const initialStats: Record<JobStatus, number> = {

View File

@ -1,6 +1,5 @@
import type { JobSource } from "@shared/types";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import type { JobSource } from "../../../shared/types";
import { import {
DEFAULT_PIPELINE_SOURCES, DEFAULT_PIPELINE_SOURCES,
orderedSources, orderedSources,

View File

@ -1,4 +1,4 @@
import type { AppSettings, Job, JobSource } from "../../../shared/types"; import type { AppSettings, Job, JobSource } from "@shared/types";
import type { FilterTab, JobSort } from "./constants"; import type { FilterTab, JobSort } from "./constants";
import { orderedFilterSources, orderedSources } from "./constants"; import { orderedFilterSources, orderedSources } from "./constants";

View File

@ -1,8 +1,8 @@
import { EmptyState, ListItem, ListPanel } from "@client/components/layout"; import { EmptyState, ListItem, ListPanel } from "@client/components/layout";
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { BackupValues } from "@client/pages/settings/types"; import type { BackupValues } from "@client/pages/settings/types";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type { BackupInfo } from "@shared/types"; import type { BackupInfo } from "@shared/types.js";
import { Archive, Clock, Trash2 } from "lucide-react"; import { Archive, Clock, Trash2 } from "lucide-react";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";

View File

@ -1,4 +1,4 @@
import type { JobStatus } from "@shared/types"; import type { JobStatus } from "@shared/types.js";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react"; import { useState } from "react";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";

View File

@ -2,7 +2,7 @@ import {
ALL_JOB_STATUSES, ALL_JOB_STATUSES,
STATUS_DESCRIPTIONS, STATUS_DESCRIPTIONS,
} from "@client/pages/settings/constants"; } from "@client/pages/settings/constants";
import type { JobStatus } from "@shared/types"; import type { JobStatus } from "@shared/types.js";
import { AlertTriangle, Trash2 } from "lucide-react"; import { AlertTriangle, Trash2 } from "lucide-react";
import type React from "react"; import type React from "react";
import { import {

View File

@ -1,5 +1,5 @@
import type { DisplayValues } from "@client/pages/settings/types"; import type { DisplayValues } from "@client/pages/settings/types";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { import {

View File

@ -1,4 +1,4 @@
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { Accordion } from "@/components/ui/accordion"; import { Accordion } from "@/components/ui/accordion";

View File

@ -1,7 +1,7 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { EnvSettingsValues } from "@client/pages/settings/types"; import type { EnvSettingsValues } from "@client/pages/settings/types";
import { formatSecretHint } from "@client/pages/settings/utils"; import { formatSecretHint } from "@client/pages/settings/utils";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { import {

View File

@ -1,6 +1,6 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { NumericSettingValues } from "@client/pages/settings/types"; import type { NumericSettingValues } from "@client/pages/settings/types";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { import {

View File

@ -1,4 +1,4 @@
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen } from "@testing-library/react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";

View File

@ -1,6 +1,6 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { JobspyValues } from "@client/pages/settings/types"; import type { JobspyValues } from "@client/pages/settings/types";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { import {

View File

@ -4,7 +4,7 @@ import {
formatSecretHint, formatSecretHint,
getLlmProviderConfig, getLlmProviderConfig,
} from "@client/pages/settings/utils"; } from "@client/pages/settings/utils";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";

View File

@ -1,5 +1,5 @@
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type { ResumeProjectCatalogItem } from "@shared/types"; import type { ResumeProjectCatalogItem } from "@shared/types.js";
import { AlertCircle, CheckCircle2 } from "lucide-react"; import { AlertCircle, CheckCircle2 } from "lucide-react";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";

View File

@ -1,5 +1,5 @@
import type { SearchTermsValues } from "@client/pages/settings/types"; import type { SearchTermsValues } from "@client/pages/settings/types";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { import {

View File

@ -1,6 +1,6 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { NumericSettingValues } from "@client/pages/settings/types"; import type { NumericSettingValues } from "@client/pages/settings/types";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import { import {

View File

@ -1,4 +1,4 @@
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { Accordion } from "@/components/ui/accordion"; import { Accordion } from "@/components/ui/accordion";

View File

@ -1,7 +1,7 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput"; import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { WebhookValues } from "@client/pages/settings/types"; import type { WebhookValues } from "@client/pages/settings/types";
import { formatSecretHint } from "@client/pages/settings/utils"; import { formatSecretHint } from "@client/pages/settings/utils";
import type { UpdateSettingsInput } from "@shared/settings-schema"; import type { UpdateSettingsInput } from "@shared/settings-schema.js";
import type React from "react"; import type React from "react";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import { import {

View File

@ -1 +1 @@
export { apiRouter } from "./routes.js"; export { apiRouter } from "./routes";

View File

@ -3,17 +3,17 @@
*/ */
import { Router } from "express"; import { Router } from "express";
import { backupRouter } from "./routes/backup.js"; import { backupRouter } from "./routes/backup";
import { databaseRouter } from "./routes/database.js"; import { databaseRouter } from "./routes/database";
import { jobsRouter } from "./routes/jobs.js"; import { jobsRouter } from "./routes/jobs";
import { manualJobsRouter } from "./routes/manual-jobs.js"; import { manualJobsRouter } from "./routes/manual-jobs";
import { onboardingRouter } from "./routes/onboarding.js"; import { onboardingRouter } from "./routes/onboarding";
import { pipelineRouter } from "./routes/pipeline.js"; import { pipelineRouter } from "./routes/pipeline";
import { profileRouter } from "./routes/profile.js"; import { profileRouter } from "./routes/profile";
import { settingsRouter } from "./routes/settings.js"; import { settingsRouter } from "./routes/settings";
import { ukVisaJobsRouter } from "./routes/ukvisajobs.js"; import { ukVisaJobsRouter } from "./routes/ukvisajobs";
import { visaSponsorsRouter } from "./routes/visa-sponsors.js"; import { visaSponsorsRouter } from "./routes/visa-sponsors";
import { webhookRouter } from "./routes/webhook.js"; import { webhookRouter } from "./routes/webhook";
export const apiRouter = Router(); export const apiRouter = Router();

View File

@ -1,7 +1,7 @@
import fs from "node:fs"; import fs from "node:fs";
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Backup API routes", () => { describe.sequential("Backup API routes", () => {
let server: Server; let server: Server;

View File

@ -3,7 +3,7 @@ import {
deleteBackup, deleteBackup,
getNextBackupTime, getNextBackupTime,
listBackups, listBackups,
} from "@server/services/backup/index.js"; } from "@server/services/backup/index";
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
export const backupRouter = Router(); export const backupRouter = Router();

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Database API routes", () => { describe.sequential("Database API routes", () => {
let server: Server; let server: Server;
@ -17,7 +17,7 @@ describe.sequential("Database API routes", () => {
}); });
it("clears jobs and pipeline runs", async () => { it("clears jobs and pipeline runs", async () => {
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
await createJob({ await createJob({
source: "manual", source: "manual",
title: "Cleanup Role", title: "Cleanup Role",

View File

@ -1,5 +1,5 @@
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
import { clearDatabase } from "../../db/clear.js"; import { clearDatabase } from "../../db/clear";
export const databaseRouter = Router(); export const databaseRouter = Router();

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Jobs API routes", () => { describe.sequential("Jobs API routes", () => {
let server: Server; let server: Server;
@ -17,7 +17,7 @@ describe.sequential("Jobs API routes", () => {
}); });
it("lists jobs and supports status filtering", async () => { it("lists jobs and supports status filtering", async () => {
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
const job = await createJob({ const job = await createJob({
source: "manual", source: "manual",
title: "Test Role", title: "Test Role",
@ -43,7 +43,7 @@ describe.sequential("Jobs API routes", () => {
}); });
it("validates job updates and supports skip/delete flow", async () => { it("validates job updates and supports skip/delete flow", async () => {
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
const job = await createJob({ const job = await createJob({
source: "manual", source: "manual",
title: "Test Role", title: "Test Role",
@ -73,13 +73,13 @@ describe.sequential("Jobs API routes", () => {
}); });
it("applies a job and syncs to Notion", async () => { it("applies a job and syncs to Notion", async () => {
const { createNotionEntry } = await import("../../services/notion.js"); const { createNotionEntry } = await import("../../services/notion");
vi.mocked(createNotionEntry).mockResolvedValue({ vi.mocked(createNotionEntry).mockResolvedValue({
success: true, success: true,
pageId: "page-123", pageId: "page-123",
}); });
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
const job = await createJob({ const job = await createJob({
source: "manual", source: "manual",
title: "Test Role", title: "Test Role",
@ -106,9 +106,9 @@ describe.sequential("Jobs API routes", () => {
}); });
it("rescoring a job updates the suitability fields", async () => { it("rescoring a job updates the suitability fields", async () => {
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
const { scoreJobSuitability } = await import("../../services/scorer.js"); const { scoreJobSuitability } = await import("../../services/scorer");
const { getProfile } = await import("../../services/profile.js"); const { getProfile } = await import("../../services/profile");
vi.mocked(getProfile).mockResolvedValue({}); vi.mocked(getProfile).mockResolvedValue({});
vi.mocked(scoreJobSuitability).mockResolvedValue({ vi.mocked(scoreJobSuitability).mockResolvedValue({
@ -124,7 +124,7 @@ describe.sequential("Jobs API routes", () => {
jobDescription: "Test description", jobDescription: "Test description",
}); });
const { updateJob } = await import("../../repositories/jobs.js"); const { updateJob } = await import("../../repositories/jobs");
await updateJob(job.id, { await updateJob(job.id, {
suitabilityScore: 55, suitabilityScore: 55,
suitabilityReason: "Old fit", suitabilityReason: "Old fit",
@ -142,7 +142,7 @@ describe.sequential("Jobs API routes", () => {
it("checks visa sponsor status for a job", async () => { it("checks visa sponsor status for a job", async () => {
const { searchSponsors } = await import( const { searchSponsors } = await import(
"../../services/visa-sponsors/index.js" "../../services/visa-sponsors/index"
); );
vi.mocked(searchSponsors).mockReturnValue([ vi.mocked(searchSponsors).mockReturnValue([
{ {
@ -152,7 +152,7 @@ describe.sequential("Jobs API routes", () => {
}, },
]); ]);
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
const job = await createJob({ const job = await createJob({
source: "manual", source: "manual",
title: "Sponsored Dev", title: "Sponsored Dev",
@ -174,7 +174,7 @@ describe.sequential("Jobs API routes", () => {
let jobId: string; let jobId: string;
beforeEach(async () => { beforeEach(async () => {
const { createJob } = await import("../../repositories/jobs.js"); const { createJob } = await import("../../repositories/jobs");
const job = await createJob({ const job = await createJob({
source: "manual", source: "manual",
title: "Tracking Test", title: "Tracking Test",
@ -245,7 +245,7 @@ describe.sequential("Jobs API routes", () => {
}); });
it("manages application tasks", async () => { it("manages application tasks", async () => {
const { db, schema } = await import("../../db/index.js"); const { db, schema } = await import("../../db/index");
const { eq } = await import("drizzle-orm"); const { eq } = await import("drizzle-orm");
const { tasks } = schema; const { tasks } = schema;

View File

@ -1,5 +1,3 @@
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import { import {
APPLICATION_OUTCOMES, APPLICATION_OUTCOMES,
APPLICATION_STAGES, APPLICATION_STAGES,
@ -7,14 +5,17 @@ import {
type Job, type Job,
type JobStatus, type JobStatus,
type JobsListResponse, type JobsListResponse,
} from "../../../shared/types.js"; } from "@shared/types";
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import { import {
generateFinalPdf, generateFinalPdf,
processJob, processJob,
summarizeJob, summarizeJob,
} from "../../pipeline/index.js"; } from "../../pipeline/index";
import * as jobsRepo from "../../repositories/jobs.js"; import * as jobsRepo from "../../repositories/jobs";
import * as settingsRepo from "../../repositories/settings.js"; import * as settingsRepo from "../../repositories/settings";
import { import {
deleteStageEvent, deleteStageEvent,
getStageEvents, getStageEvents,
@ -22,11 +23,11 @@ import {
stageEventMetadataSchema, stageEventMetadataSchema,
transitionStage, transitionStage,
updateStageEvent, updateStageEvent,
} from "../../services/applicationTracking.js"; } from "../../services/applicationTracking";
import { createNotionEntry } from "../../services/notion.js"; import { createNotionEntry } from "../../services/notion";
import { getProfile } from "../../services/profile.js"; import { getProfile } from "../../services/profile";
import { scoreJobSuitability } from "../../services/scorer.js"; import { scoreJobSuitability } from "../../services/scorer";
import * as visaSponsors from "../../services/visa-sponsors/index.js"; import * as visaSponsors from "../../services/visa-sponsors/index";
export const jobsRouter = Router(); export const jobsRouter = Router();

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Manual jobs API routes", () => { describe.sequential("Manual jobs API routes", () => {
let server: Server; let server: Server;
@ -46,9 +46,7 @@ describe.sequential("Manual jobs API routes", () => {
}); });
expect(badRes.status).toBe(400); expect(badRes.status).toBe(400);
const { inferManualJobDetails } = await import( const { inferManualJobDetails } = await import("../../services/manualJob");
"../../services/manualJob.js"
);
vi.mocked(inferManualJobDetails).mockResolvedValue({ vi.mocked(inferManualJobDetails).mockResolvedValue({
job: { title: "Backend Engineer", employer: "Acme" }, job: { title: "Backend Engineer", employer: "Acme" },
warning: null, warning: null,
@ -65,7 +63,7 @@ describe.sequential("Manual jobs API routes", () => {
}); });
it("imports manual jobs and generates a fallback URL", async () => { it("imports manual jobs and generates a fallback URL", async () => {
const { scoreJobSuitability } = await import("../../services/scorer.js"); const { scoreJobSuitability } = await import("../../services/scorer");
vi.mocked(scoreJobSuitability).mockResolvedValue({ vi.mocked(scoreJobSuitability).mockResolvedValue({
score: 88, score: 88,
reason: "Strong fit", reason: "Strong fit",

View File

@ -1,16 +1,16 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { type Request, type Response, Router } from "express";
import { JSDOM } from "jsdom";
import { z } from "zod";
import type { import type {
ApiResponse, ApiResponse,
ManualJobFetchResponse, ManualJobFetchResponse,
ManualJobInferenceResponse, ManualJobInferenceResponse,
} from "../../../shared/types.js"; } from "@shared/types";
import * as jobsRepo from "../../repositories/jobs.js"; import { type Request, type Response, Router } from "express";
import { inferManualJobDetails } from "../../services/manualJob.js"; import { JSDOM } from "jsdom";
import { getProfile } from "../../services/profile.js"; import { z } from "zod";
import { scoreJobSuitability } from "../../services/scorer.js"; import * as jobsRepo from "../../repositories/jobs";
import { inferManualJobDetails } from "../../services/manualJob";
import { getProfile } from "../../services/profile";
import { scoreJobSuitability } from "../../services/scorer";
export const manualJobsRouter = Router(); export const manualJobsRouter = Router();

View File

@ -1,7 +1,7 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { RxResumeClient } from "@server/services/rxresume-client.js"; import { RxResumeClient } from "@server/services/rxresume-client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Onboarding API routes", () => { describe.sequential("Onboarding API routes", () => {
let server: Server; let server: Server;

View File

@ -1,11 +1,11 @@
import { getSetting } from "@server/repositories/settings.js"; import { getSetting } from "@server/repositories/settings";
import { LlmService } from "@server/services/llm-service.js"; import { LlmService } from "@server/services/llm-service";
import { RxResumeClient } from "@server/services/rxresume-client.js"; import { RxResumeClient } from "@server/services/rxresume-client";
import { import {
getResume, getResume,
RxResumeCredentialsError, RxResumeCredentialsError,
} from "@server/services/rxresume-v4.js"; } from "@server/services/rxresume-v4";
import { resumeDataSchema } from "@shared/rxresume-schema.js"; import { resumeDataSchema } from "@shared/rxresume-schema";
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
export const onboardingRouter = Router(); export const onboardingRouter = Router();

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Pipeline API routes", () => { describe.sequential("Pipeline API routes", () => {
let server: Server; let server: Server;
@ -32,7 +32,7 @@ describe.sequential("Pipeline API routes", () => {
}); });
expect(badRun.status).toBe(400); expect(badRun.status).toBe(400);
const { runPipeline } = await import("../../pipeline/index.js"); const { runPipeline } = await import("../../pipeline/index");
const runRes = await fetch(`${baseUrl}/api/pipeline/run`, { const runRes = await fetch(`${baseUrl}/api/pipeline/run`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@ -1,15 +1,12 @@
import type { ApiResponse, PipelineStatusResponse } from "@shared/types";
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
import { z } from "zod"; import { z } from "zod";
import type {
ApiResponse,
PipelineStatusResponse,
} from "../../../shared/types.js";
import { import {
getPipelineStatus, getPipelineStatus,
runPipeline, runPipeline,
subscribeToProgress, subscribeToProgress,
} from "../../pipeline/index.js"; } from "../../pipeline/index";
import * as pipelineRepo from "../../repositories/pipeline.js"; import * as pipelineRepo from "../../repositories/pipeline";
export const pipelineRouter = Router(); export const pipelineRouter = Router();

View File

@ -1,9 +1,9 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
// Mock the rxresume-v4 service // Mock the rxresume-v4 service
vi.mock("../../services/rxresume-v4.js", () => ({ vi.mock("../../services/rxresume-v4", () => ({
getResume: vi.fn(), getResume: vi.fn(),
listResumes: vi.fn(), listResumes: vi.fn(),
RxResumeCredentialsError: class RxResumeCredentialsError extends Error { RxResumeCredentialsError: class RxResumeCredentialsError extends Error {
@ -15,13 +15,13 @@ vi.mock("../../services/rxresume-v4.js", () => ({
})); }));
// Mock the profile service // Mock the profile service
vi.mock("../../services/profile.js", () => ({ vi.mock("../../services/profile", () => ({
getProfile: vi.fn(), getProfile: vi.fn(),
clearProfileCache: vi.fn(), clearProfileCache: vi.fn(),
})); }));
// Mock the settings repository // Mock the settings repository
vi.mock("../../repositories/settings.js", async (importOriginal) => { vi.mock("../../repositories/settings", async (importOriginal) => {
const original = (await importOriginal()) as Record<string, unknown>; const original = (await importOriginal()) as Record<string, unknown>;
return { return {
...original, ...original,
@ -29,12 +29,12 @@ vi.mock("../../repositories/settings.js", async (importOriginal) => {
}; };
}); });
import { getSetting } from "../../repositories/settings.js"; import { getSetting } from "../../repositories/settings";
import { getProfile } from "../../services/profile.js"; import { getProfile } from "../../services/profile";
import { import {
getResume, getResume,
RxResumeCredentialsError, RxResumeCredentialsError,
} from "../../services/rxresume-v4.js"; } from "../../services/rxresume-v4";
describe.sequential("Profile API routes", () => { describe.sequential("Profile API routes", () => {
let server: Server; let server: Server;

View File

@ -1,11 +1,11 @@
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
import { getSetting } from "../../repositories/settings.js"; import { getSetting } from "../../repositories/settings";
import { clearProfileCache, getProfile } from "../../services/profile.js"; import { clearProfileCache, getProfile } from "../../services/profile";
import { extractProjectsFromProfile } from "../../services/resumeProjects.js"; import { extractProjectsFromProfile } from "../../services/resumeProjects";
import { import {
getResume, getResume,
RxResumeCredentialsError, RxResumeCredentialsError,
} from "../../services/rxresume-v4.js"; } from "../../services/rxresume-v4";
export const profileRouter = Router(); export const profileRouter = Router();

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Settings API routes", () => { describe.sequential("Settings API routes", () => {
let server: Server; let server: Server;

View File

@ -1,21 +1,18 @@
import * as settingsRepo from "@server/repositories/settings.js"; import * as settingsRepo from "@server/repositories/settings";
import { setBackupSettings } from "@server/services/backup/index.js"; import { setBackupSettings } from "@server/services/backup/index";
import { import { applyEnvValue, normalizeEnvInput } from "@server/services/envSettings";
applyEnvValue, import { getProfile } from "@server/services/profile";
normalizeEnvInput,
} from "@server/services/envSettings.js";
import { getProfile } from "@server/services/profile.js";
import { import {
extractProjectsFromProfile, extractProjectsFromProfile,
normalizeResumeProjectsSettings, normalizeResumeProjectsSettings,
} from "@server/services/resumeProjects.js"; } from "@server/services/resumeProjects";
import { import {
getResume, getResume,
listResumes, listResumes,
RxResumeCredentialsError, RxResumeCredentialsError,
} from "@server/services/rxresume-v4.js"; } from "@server/services/rxresume-v4";
import { getEffectiveSettings } from "@server/services/settings.js"; import { getEffectiveSettings } from "@server/services/settings";
import { updateSettingsSchema } from "@shared/settings-schema.js"; import { updateSettingsSchema } from "@shared/settings-schema";
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
export const settingsRouter = Router(); export const settingsRouter = Router();

View File

@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { vi } from "vitest"; import { vi } from "vitest";
vi.mock("../../pipeline/index.js", () => { vi.mock("../../pipeline/index", () => {
const progress = { const progress = {
step: "idle", step: "idle",
message: "Ready", message: "Ready",
@ -37,27 +37,27 @@ vi.mock("../../pipeline/index.js", () => {
}; };
}); });
vi.mock("../../services/notion.js", () => ({ vi.mock("../../services/notion", () => ({
createNotionEntry: vi.fn(), createNotionEntry: vi.fn(),
})); }));
vi.mock("../../services/manualJob.js", () => ({ vi.mock("../../services/manualJob", () => ({
inferManualJobDetails: vi.fn(), inferManualJobDetails: vi.fn(),
})); }));
vi.mock("../../services/scorer.js", () => ({ vi.mock("../../services/scorer", () => ({
scoreJobSuitability: vi.fn(), scoreJobSuitability: vi.fn(),
})); }));
vi.mock("../../services/profile.js", () => ({ vi.mock("../../services/profile", () => ({
getProfile: vi.fn().mockResolvedValue({}), getProfile: vi.fn().mockResolvedValue({}),
})); }));
vi.mock("../../services/ukvisajobs.js", () => ({ vi.mock("../../services/ukvisajobs", () => ({
fetchUkVisaJobsPage: vi.fn(), fetchUkVisaJobsPage: vi.fn(),
})); }));
vi.mock("../../services/visa-sponsors/index.js", () => ({ vi.mock("../../services/visa-sponsors/index", () => ({
getStatus: vi.fn(), getStatus: vi.fn(),
searchSponsors: vi.fn(), searchSponsors: vi.fn(),
getOrganizationDetails: vi.fn(), getOrganizationDetails: vi.fn(),
@ -96,13 +96,13 @@ export async function startServer(options?: {
...envOverrides, ...envOverrides,
}; };
await import("../../db/migrate.js"); await import("../../db/migrate");
const { applyStoredEnvOverrides } = await import( const { applyStoredEnvOverrides } = await import(
"../../services/envSettings.js" "../../services/envSettings"
); );
const { createApp } = await import("../../app.js"); const { createApp } = await import("../../app");
const { closeDb } = await import("../../db/index.js"); const { closeDb } = await import("../../db/index");
const { getPipelineStatus } = await import("../../pipeline/index.js"); const { getPipelineStatus } = await import("../../pipeline/index");
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false }); vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: false });
await applyStoredEnvOverrides(); await applyStoredEnvOverrides();
@ -127,7 +127,7 @@ export async function startServer(options?: {
export async function stopServer(args: { export async function stopServer(args: {
server: Server; server: Server;
closeDb: () => void; closeDb: () => void;
tempDir: string; tempDir?: string;
}) { }) {
// Defensive: if startServer throws, callers may still run cleanup. // Defensive: if startServer throws, callers may still run cleanup.
if (args.server) { if (args.server) {
@ -136,7 +136,9 @@ export async function stopServer(args: {
if (args.closeDb) { if (args.closeDb) {
args.closeDb(); args.closeDb();
} }
await rm(args.tempDir, { recursive: true, force: true }); if (args.tempDir) {
await rm(args.tempDir, { recursive: true, force: true });
}
process.env = { ...originalEnv }; process.env = { ...originalEnv };
vi.clearAllMocks(); vi.clearAllMocks();
} }

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("UK Visa Jobs API routes", () => { describe.sequential("UK Visa Jobs API routes", () => {
let server: Server; let server: Server;
@ -26,9 +26,7 @@ describe.sequential("UK Visa Jobs API routes", () => {
}); });
it("searches UK Visa Jobs with valid payloads", async () => { it("searches UK Visa Jobs with valid payloads", async () => {
const { fetchUkVisaJobsPage } = await import( const { fetchUkVisaJobsPage } = await import("../../services/ukvisajobs");
"../../services/ukvisajobs.js"
);
vi.mocked(fetchUkVisaJobsPage).mockResolvedValue({ vi.mocked(fetchUkVisaJobsPage).mockResolvedValue({
jobs: [ jobs: [
{ {
@ -58,7 +56,7 @@ describe.sequential("UK Visa Jobs API routes", () => {
}); });
it("blocks search when pipeline is running", async () => { it("blocks search when pipeline is running", async () => {
const { getPipelineStatus } = await import("../../pipeline/index.js"); const { getPipelineStatus } = await import("../../pipeline/index");
vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: true }); vi.mocked(getPipelineStatus).mockReturnValue({ isRunning: true });
const res = await fetch(`${baseUrl}/api/ukvisajobs/search`, { const res = await fetch(`${baseUrl}/api/ukvisajobs/search`, {

View File

@ -1,13 +1,14 @@
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import type { import type {
ApiResponse, ApiResponse,
UkVisaJobsImportResponse, UkVisaJobsImportResponse,
UkVisaJobsSearchResponse, UkVisaJobsSearchResponse,
} from "../../../shared/types.js"; } from "@shared/types";
import { getPipelineStatus } from "../../pipeline/index.js"; import { type Request, type Response, Router } from "express";
import * as jobsRepo from "../../repositories/jobs.js"; import { z } from "zod";
import { fetchUkVisaJobsPage } from "../../services/ukvisajobs.js";
import { getPipelineStatus } from "../../pipeline/index";
import * as jobsRepo from "../../repositories/jobs";
import { fetchUkVisaJobsPage } from "../../services/ukvisajobs";
export const ukVisaJobsRouter = Router(); export const ukVisaJobsRouter = Router();
let isUkVisaJobsSearchRunning = false; let isUkVisaJobsSearchRunning = false;

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Visa sponsors API routes", () => { describe.sequential("Visa sponsors API routes", () => {
let server: Server; let server: Server;
@ -18,7 +18,7 @@ describe.sequential("Visa sponsors API routes", () => {
it("returns status and surfaces update errors", async () => { it("returns status and surfaces update errors", async () => {
const { getStatus, downloadLatestCsv } = await import( const { getStatus, downloadLatestCsv } = await import(
"../../services/visa-sponsors/index.js" "../../services/visa-sponsors/index"
); );
vi.mocked(getStatus).mockReturnValue({ vi.mocked(getStatus).mockReturnValue({
lastUpdated: null, lastUpdated: null,
@ -46,7 +46,7 @@ describe.sequential("Visa sponsors API routes", () => {
it("validates search payloads and handles missing organizations", async () => { it("validates search payloads and handles missing organizations", async () => {
const { searchSponsors, getOrganizationDetails } = await import( const { searchSponsors, getOrganizationDetails } = await import(
"../../services/visa-sponsors/index.js" "../../services/visa-sponsors/index"
); );
vi.mocked(searchSponsors).mockReturnValue([ vi.mocked(searchSponsors).mockReturnValue([
{ {

View File

@ -1,11 +1,12 @@
import { type Request, type Response, Router } from "express";
import { z } from "zod";
import type { import type {
ApiResponse, ApiResponse,
VisaSponsorSearchResponse, VisaSponsorSearchResponse,
VisaSponsorStatusResponse, VisaSponsorStatusResponse,
} from "../../../shared/types.js"; } from "@shared/types";
import * as visaSponsors from "../../services/visa-sponsors/index.js"; import { type Request, type Response, Router } from "express";
import { z } from "zod";
import * as visaSponsors from "../../services/visa-sponsors/index";
export const visaSponsorsRouter = Router(); export const visaSponsorsRouter = Router();

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http"; import type { Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { startServer, stopServer } from "./test-utils.js"; import { startServer, stopServer } from "./test-utils";
describe.sequential("Webhook API routes", () => { describe.sequential("Webhook API routes", () => {
let server: Server; let server: Server;

View File

@ -1,5 +1,5 @@
import { type Request, type Response, Router } from "express"; import { type Request, type Response, Router } from "express";
import { runPipeline } from "../../pipeline/index.js"; import { runPipeline } from "../../pipeline/index";
export const webhookRouter = Router(); export const webhookRouter = Router();

Some files were not shown because too many files have changed in this diff Show More