Reduce low risk duplication (#79)

* clean up helpers

* shared in it's own top level folder

* workspaces setup

* build fix

* disable workspaces?

* run ci

* rename job-flow to gradcracker

* optional dependencies

* formatting?

* more optional modules

* allow post install runs

* node bump

* remove post install

* add optionals

* add more

* formatting

* comments, but im unsure

* run typescript DIRECTLY

* better build

* camoufox simplification

* lint

* build process doesn't exist

* build fix

* lockfile

* type check everything, build only for client

* rename steps correctly

* import from package!

* fix formatting

* don't fetch twice

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

View File

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

View File

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

5
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,30 @@
{
"name": "job-flow",
"name": "gradcracker-extractor",
"version": "0.0.1",
"type": "module",
"description": "This is an example of a Crawlee project.",
"dependencies": {
"camoufox-js": "^0.8.0",
"crawlee": "^3.0.0",
"playwright": "*"
"playwright": "*",
"tsx": "^4.4.0"
},
"devDependencies": {
"@apify/tsconfig": "^0.1.0",
"@types/fs-extra": "^11",
"@types/node": "^24.0.0",
"fs-extra": "^11.3.0",
"tsx": "^4.4.0",
"typescript": "~5.9.0"
},
"optionalDependencies": {
"impit-linux-x64-gnu": "^0.1.0"
},
"scripts": {
"start": "npm run start:dev",
"start:prod": "node dist/main.js",
"start": "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",
"get-binaries": "camoufox-js fetch",
"postinstall": "npm run get-binaries"
"get-binaries": "camoufox-js fetch"
},
"author": "It's not you it's me",
"license": "ISC"

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,4 +1,4 @@
/**
/**
* UK Visa Jobs Extractor
*
* 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 { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
toNumberOrNull,
toStringOrNull,
} from "job-ops-shared/utils/type-conversion";
import type { Request } from "playwright";
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(
pageNo: number,
session: UkVisaJobsAuthSession,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
*/
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 React from "react";
import { Link, useLocation } from "react-router-dom";
@ -24,7 +25,6 @@ import {
SheetTrigger,
} from "@/components/ui/sheet";
import { cn, sourceLabel } from "@/lib/utils";
import type { JobSource } from "../../shared/types";
interface HeaderProps {
onRunPipeline: () => void;

View File

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

View File

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

View File

@ -1,4 +1,6 @@
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 { Controller, useForm } from "react-hook-form";
import * as z from "zod";
@ -22,8 +24,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import type { StageEvent } from "../../shared/types";
import { STAGE_LABELS } from "../../shared/types";
const logEventSchema = z.object({
stage: z.string(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@
* 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 type React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ApplicationStage, StageEvent } from "../../../shared/types";
import { ConversionAnalytics } from "./ConversionAnalytics";
// Mock UI components

View File

@ -3,6 +3,7 @@
* 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 { useMemo } from "react";
import {
@ -18,7 +19,6 @@ import {
XAxis,
YAxis,
} from "recharts";
import {
Card,
CardContent,
@ -27,7 +27,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import type { StageEvent } from "../../../shared/types";
type FunnelStage = {
name: string;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,11 @@
import {
type ApplicationStage,
type ApplicationTask,
type Job,
type JobOutcome,
STAGE_LABELS,
type StageEvent,
} from "@shared/types.js";
import confetti from "canvas-confetti";
import {
ArrowLeft,
@ -13,14 +21,6 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
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 { ConfirmDelete } from "../components/ConfirmDelete";
import { JobHeader } from "../components/JobHeader";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import {
ALL_JOB_STATUSES,
STATUS_DESCRIPTIONS,
} 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 type React from "react";
import {

View File

@ -1,5 +1,5 @@
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 { Controller, useFormContext } from "react-hook-form";
import {

View File

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

View File

@ -1,7 +1,7 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { EnvSettingsValues } from "@client/pages/settings/types";
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 { Controller, useFormContext } from "react-hook-form";
import {

View File

@ -1,6 +1,6 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
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 { Controller, useFormContext } from "react-hook-form";
import {

View File

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

View File

@ -1,6 +1,6 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
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 { Controller, useFormContext } from "react-hook-form";
import {

View File

@ -4,7 +4,7 @@ import {
formatSecretHint,
getLlmProviderConfig,
} 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 { useEffect } from "react";
import { Controller, useFormContext } from "react-hook-form";

View File

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

View File

@ -1,5 +1,5 @@
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 { Controller, useFormContext } from "react-hook-form";
import {

View File

@ -1,6 +1,6 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
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 { Controller, useFormContext } from "react-hook-form";
import {

View File

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

View File

@ -1,7 +1,7 @@
import { SettingsInput } from "@client/pages/settings/components/SettingsInput";
import type { WebhookValues } from "@client/pages/settings/types";
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 { useFormContext } from "react-hook-form";
import {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import fs from "node:fs";
import type { Server } from "node:http";
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", () => {
let server: Server;

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;
@ -17,7 +17,7 @@ describe.sequential("Database API routes", () => {
});
it("clears jobs and pipeline runs", async () => {
const { createJob } = await import("../../repositories/jobs.js");
const { createJob } = await import("../../repositories/jobs");
await createJob({
source: "manual",
title: "Cleanup Role",

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;
@ -17,7 +17,7 @@ describe.sequential("Jobs API routes", () => {
});
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({
source: "manual",
title: "Test Role",
@ -43,7 +43,7 @@ describe.sequential("Jobs API routes", () => {
});
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({
source: "manual",
title: "Test Role",
@ -73,13 +73,13 @@ describe.sequential("Jobs API routes", () => {
});
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({
success: true,
pageId: "page-123",
});
const { createJob } = await import("../../repositories/jobs.js");
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Test Role",
@ -106,9 +106,9 @@ describe.sequential("Jobs API routes", () => {
});
it("rescoring a job updates the suitability fields", async () => {
const { createJob } = await import("../../repositories/jobs.js");
const { scoreJobSuitability } = await import("../../services/scorer.js");
const { getProfile } = await import("../../services/profile.js");
const { createJob } = await import("../../repositories/jobs");
const { scoreJobSuitability } = await import("../../services/scorer");
const { getProfile } = await import("../../services/profile");
vi.mocked(getProfile).mockResolvedValue({});
vi.mocked(scoreJobSuitability).mockResolvedValue({
@ -124,7 +124,7 @@ describe.sequential("Jobs API routes", () => {
jobDescription: "Test description",
});
const { updateJob } = await import("../../repositories/jobs.js");
const { updateJob } = await import("../../repositories/jobs");
await updateJob(job.id, {
suitabilityScore: 55,
suitabilityReason: "Old fit",
@ -142,7 +142,7 @@ describe.sequential("Jobs API routes", () => {
it("checks visa sponsor status for a job", async () => {
const { searchSponsors } = await import(
"../../services/visa-sponsors/index.js"
"../../services/visa-sponsors/index"
);
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({
source: "manual",
title: "Sponsored Dev",
@ -174,7 +174,7 @@ describe.sequential("Jobs API routes", () => {
let jobId: string;
beforeEach(async () => {
const { createJob } = await import("../../repositories/jobs.js");
const { createJob } = await import("../../repositories/jobs");
const job = await createJob({
source: "manual",
title: "Tracking Test",
@ -245,7 +245,7 @@ describe.sequential("Jobs API routes", () => {
});
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 { tasks } = schema;

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;
@ -46,9 +46,7 @@ describe.sequential("Manual jobs API routes", () => {
});
expect(badRes.status).toBe(400);
const { inferManualJobDetails } = await import(
"../../services/manualJob.js"
);
const { inferManualJobDetails } = await import("../../services/manualJob");
vi.mocked(inferManualJobDetails).mockResolvedValue({
job: { title: "Backend Engineer", employer: "Acme" },
warning: null,
@ -65,7 +63,7 @@ describe.sequential("Manual jobs API routes", () => {
});
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({
score: 88,
reason: "Strong fit",

View File

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

View File

@ -1,7 +1,7 @@
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 { startServer, stopServer } from "./test-utils.js";
import { startServer, stopServer } from "./test-utils";
describe.sequential("Onboarding API routes", () => {
let server: Server;

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;
@ -32,7 +32,7 @@ describe.sequential("Pipeline API routes", () => {
});
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`, {
method: "POST",
headers: { "Content-Type": "application/json" },

View File

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

View File

@ -1,9 +1,9 @@
import type { Server } from "node:http";
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
vi.mock("../../services/rxresume-v4.js", () => ({
vi.mock("../../services/rxresume-v4", () => ({
getResume: vi.fn(),
listResumes: vi.fn(),
RxResumeCredentialsError: class RxResumeCredentialsError extends Error {
@ -15,13 +15,13 @@ vi.mock("../../services/rxresume-v4.js", () => ({
}));
// Mock the profile service
vi.mock("../../services/profile.js", () => ({
vi.mock("../../services/profile", () => ({
getProfile: vi.fn(),
clearProfileCache: vi.fn(),
}));
// 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>;
return {
...original,
@ -29,12 +29,12 @@ vi.mock("../../repositories/settings.js", async (importOriginal) => {
};
});
import { getSetting } from "../../repositories/settings.js";
import { getProfile } from "../../services/profile.js";
import { getSetting } from "../../repositories/settings";
import { getProfile } from "../../services/profile";
import {
getResume,
RxResumeCredentialsError,
} from "../../services/rxresume-v4.js";
} from "../../services/rxresume-v4";
describe.sequential("Profile API routes", () => {
let server: Server;

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;
@ -26,9 +26,7 @@ describe.sequential("UK Visa Jobs API routes", () => {
});
it("searches UK Visa Jobs with valid payloads", async () => {
const { fetchUkVisaJobsPage } = await import(
"../../services/ukvisajobs.js"
);
const { fetchUkVisaJobsPage } = await import("../../services/ukvisajobs");
vi.mocked(fetchUkVisaJobsPage).mockResolvedValue({
jobs: [
{
@ -58,7 +56,7 @@ describe.sequential("UK Visa Jobs API routes", () => {
});
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 });
const res = await fetch(`${baseUrl}/api/ukvisajobs/search`, {

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import type { Server } from "node:http";
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", () => {
let server: Server;

View File

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

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