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:
parent
179deffe13
commit
b94f85b149
@ -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
|
||||||
|
|||||||
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@ -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
5
.gitignore
vendored
@ -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/
|
||||||
|
|
||||||
|
|||||||
81
.opencode/agent/requirements-to-implementation.md
Normal file
81
.opencode/agent/requirements-to-implementation.md
Normal 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 project’s 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.
|
||||||
109
Dockerfile
109
Dockerfile
@ -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"]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
4941
extractors/gradcracker/package-lock.json
generated
4941
extractors/gradcracker/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
|||||||
1709
extractors/ukvisajobs/package-lock.json
generated
1709
extractors/ukvisajobs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
8
extractors/ukvisajobs/tsconfig.build.json
Normal file
8
extractors/ukvisajobs/tsconfig.build.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@shared/*": ["../../shared/dist/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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[];
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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) =>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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> = {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 => ({
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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> = {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { apiRouter } from "./routes.js";
|
export { apiRouter } from "./routes";
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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" },
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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`, {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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();
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
Loading…
x
Reference in New Issue
Block a user