Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
7387b91
Select only needed jobPool columns on dashboard to cut payload 80-90%
mojoro Mar 25, 2026
9eab75a
Replace optimistic application ID with real server ID after save
mojoro Mar 25, 2026
eb9a027
Update audit findings with completion status and parallelization guide
mojoro Mar 25, 2026
63ba345
Add pg_trgm indexes on job_pool title and location
mojoro Mar 25, 2026
4780f90
Replace NOT IN subqueries with NOT EXISTS in match-sql
mojoro Mar 25, 2026
5e1bda2
Standardize on max_completion_tokens for OpenAI API calls
mojoro Mar 25, 2026
8a9e30d
Mirror feedStatus ARCHIVED in Zustand when app status becomes APPLIED
mojoro Mar 25, 2026
06ca775
Revalidate dashboard and pipeline after resume export
mojoro Mar 25, 2026
ca52b64
Sync Zustand store after job import so feed updates immediately
mojoro Mar 25, 2026
8b12655
Parallelize Adzuna and USAJobs search configs with allSettled
mojoro Mar 25, 2026
db3db15
Unify TERMINAL_STATUSES into shared pipeline-constants
mojoro Mar 25, 2026
026f04a
Replace NextResponse.json with Response.json where possible
mojoro Mar 25, 2026
2c5c673
Wrap usage limit checks in transactions to prevent race conditions
mojoro Mar 25, 2026
16e0dd5
Restrict CORS to specific Chrome extension ID from env var
mojoro Mar 25, 2026
2241c55
Add shared incrementUsage and checkUsageLimit helpers
mojoro Mar 25, 2026
19a47be
Set shortlist-onboarded cookie from server with HttpOnly flag
mojoro Mar 25, 2026
3fe3d25
Use validated env.ADMIN_USER_ID in dashboard layout
mojoro Mar 25, 2026
e1584eb
Reduce shortlist-active cookie TTL to 60s and add HttpOnly
mojoro Mar 25, 2026
c38b04d
Document in-memory rate limiter cold-start limitation
mojoro Mar 25, 2026
77c4d7f
Use findUnique instead of findFirst for primary key lookups
mojoro Mar 25, 2026
42facd4
Add missing statusUpdatedAt to createApplication upsert
mojoro Mar 25, 2026
dbe2896
Verify profile ownership in analyze and match API routes
mojoro Mar 25, 2026
3a13acd
Switch admin actions from Zod parse to safeParse
mojoro Mar 25, 2026
15006b0
Replace 5 usage upsert copies and 4 limit checks with shared helpers
mojoro Mar 25, 2026
891b765
Add revalidatePath to match and analyze API routes
mojoro Mar 25, 2026
a8eac49
Use capped length for pendingMatchCount to exclude dupes
mojoro Mar 25, 2026
a7c0f51
Change updateCustomJob to throw instead of returning errors
mojoro Mar 25, 2026
434f7f3
Validate analyzeSchema profileId with cuid instead of min
mojoro Mar 25, 2026
56cad37
Select only needed jobPool columns on dashboard to cut payload
mojoro Mar 25, 2026
2c457a3
Create Application records in batchSaveJobs to match toggleSaveJob
mojoro Mar 25, 2026
d0843fa
Replace O(n^2) indexOf with Map lookup in virtualizer loop
mojoro Mar 25, 2026
a8cf3c4
Add revalidatePath to updateJobNotes for dashboard and detail
mojoro Mar 25, 2026
db6ab26
Parallelize pool insert chunks in scrape route
mojoro Mar 25, 2026
a934184
Recalculate followUpCount in store after followUpAt changes
mojoro Mar 25, 2026
0617272
Extract requireProfile helper to deduplicate auth checks
mojoro Mar 25, 2026
f0adea4
Add sync generation counter to discard stale mutation callbacks
mojoro Mar 25, 2026
e882b03
Generalize parseAiJsonResponse into a shared generic helper
mojoro Mar 25, 2026
56bfcb3
Extract usePipelineEditing hook from PipelineTable and KanbanBoard
mojoro Mar 25, 2026
588e7fc
Remove unused rematchProfileSql export from match-sql
mojoro Mar 25, 2026
8b0db0f
Deduplicate URL_RE and TurndownService into shared modules
mojoro Mar 25, 2026
a290de9
Extract rematchAndRevalidate helper to deduplicate settings actions
mojoro Mar 25, 2026
f5c4df0
Extract shared logAiContext helper with strict host check
mojoro Mar 25, 2026
3e7c494
Extract score label helpers to reduce threshold duplication
mojoro Mar 25, 2026
7d08f33
Add missing Metadata types and exports across pages
mojoro Mar 25, 2026
1a9dc48
Log webhook deletion errors and fix dev route comment
mojoro Mar 25, 2026
15c5da1
Merge branch 'fix/audit-high' into audit/integration
mojoro Mar 26, 2026
604601d
Merge perf/query-optimization keeping audit/base jobPool select
mojoro Mar 26, 2026
7e0ae42
Merge branch 'fix/security-hardening' into audit/integration
mojoro Mar 26, 2026
d822011
Merge chore/opportunistic-cleanup preserving auth import for ownershi…
mojoro Mar 26, 2026
856a8fe
Merge fix/sync-gaps with generation checks and correct rollback
mojoro Mar 26, 2026
1a3b18e
Merge branch 'fix/consistency-cleanup' into audit/integration
mojoro Mar 26, 2026
a87bbd0
Merge refactor/extract-shared-helpers with usage and auth helpers
mojoro Mar 26, 2026
b35d30d
Remove accidentally committed untracked files
mojoro Mar 26, 2026
3d877bd
Create Usage row on user.created webhook so AI features work immediately
mojoro Apr 1, 2026
ead2d3a
Increase tailor output token limit from 8000 to 12000
mojoro Apr 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 38 additions & 47 deletions docs/audit-findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ files, line references, root cause, and fix instructions. A cleared session can
work through these top-to-bottom.

> **CRITICAL items (1–4) were fixed in PR `fix/critical-security-fixes`.**
> Start from HIGH (#5) onward.
> **HIGH items #5 and #6 also fixed** on the same branch.
> Start from HIGH (#7) onward.

---

Expand All @@ -20,6 +21,31 @@ work through these top-to-bottom.
4. Run `pnpm tsc --noEmit` after each change.
5. Group related findings into one branch/PR where noted.

### Parallelization Guide

The remaining findings can be split across independent agents/branches:

| Agent | Branch | Findings | Files touched |
|-------|--------|----------|---------------|
| **A: Remaining HIGH** | `fix/critical-security-fixes` (continue) | #7–#11 | `store.ts`, `tailor/save/route.ts`, `ImportJobModal.tsx`, `analyze/route.ts`, `tailor/route.ts`, `middleware.ts` |
| **B: Redundancies** | `refactor/extract-shared-helpers` | #12–#18 | New: `src/lib/usage.ts`, `src/lib/auth-helpers.ts`, `src/components/pipeline/use-pipeline-editing.ts`, `src/lib/pipeline-constants.ts`. Modified: all API routes, server actions, pipeline components |
| **C: Inconsistencies** | `fix/consistency-cleanup` | #19–#26 | `tailor/route.ts`, `extract/route.ts`, `dashboard/actions.ts`, `pipeline/actions.ts`, `validations.ts`, admin actions, `(dashboard)/layout.tsx` |
| **D: Performance** | `perf/query-optimization` | #27–#33 | New migration, `match-sql.ts`, `adzuna.ts`, `usajobs.ts`, `actions-sync.ts`, `JobFeed.tsx`, `scrape/route.ts` |
| **E: Data Syncing** | `fix/sync-gaps` | #34–#39 | `dashboard/actions.ts`, `scrape/route.ts`, `analyze/route.ts`, `match-pipeline.ts`, `store.ts`, `ProfileSwitcher.tsx` |
| **F: Security** | `fix/security-hardening` | #40–#43 | `OnboardingWizard.tsx`, `middleware.ts`, `rate-limit.ts`, `analyze/route.ts`, `match/route.ts` |
| **G: Low cleanup** | `chore/opportunistic-cleanup` | #44–#60 | Scattered small changes across many files |

**Conflict zones:** Agents A, B, C, and E all touch `dashboard/actions.ts` and `store.ts`. Run A first, then B+C+D+E+F+G can run in parallel if they each create their own branch. Merge A first, then rebase others.

**Safest parallel split:** D (performance), F (security), and G (cleanup) have minimal overlap with each other and the rest. B (redundancies) and C (inconsistencies) touch many of the same files but different lines.

### Important context for agents

- **`retryServerAction<T>` now returns `T | null`** (not `boolean`). Callers must check `result === null` for failure (not `!result`), because `void` actions return `undefined` on success which is falsy.
- **`JobWithApplication` type changed.** `jobPool` is now a select subset (no `description`, no `rawData`). The `jobPoolSummarySelect` constant in `src/types/index.ts` defines the selected fields. Any code needing `description` must fetch it separately (see job detail page pattern: server component passes `description` prop).
- **`ApplicationWithJob` type also changed** — its nested `job.jobPool` uses the same summary select.
- **Store `partialize`** no longer strips `rawData` manually since the data never arrives from the server.

---

## CRITICAL — Fixed in `fix/critical-security-fixes`
Expand Down Expand Up @@ -69,57 +95,22 @@ now fail if any client component imports this module.

### 5. Over-fetching `jobPool.description` + `rawData` on every dashboard load

**Files:** `src/app/(dashboard)/actions-sync.ts:39-48`

**Problem:** `include: { jobPool: true }` fetches ALL columns including
`description` (5-20KB) and `rawData` (10-50KB) per job. For 200 jobs = 2-10MB
per page load. This data is never displayed on the dashboard.

**Fix:**
1. In `fetchDashboardData()`, replace `include: { jobPool: true }` with:
```ts
include: {
jobPool: {
select: {
id: true, title: true, company: true, location: true,
locationType: true, url: true, source: true, postedAt: true,
skills: true, salaryMin: true, salaryMax: true, currency: true,
jobType: true, country: true,
},
},
application: { select: { status: true } },
}
```
2. Do the same for the applications query's nested `job.jobPool`.
3. In `src/lib/store.ts` `partialize` function (~line 594), also strip
`description` from persisted jobs (currently only strips `rawData`).
4. Update the `JobWithApplication` type in `src/types/index.ts` if needed
(may need a `JobPoolSummary` type without `description`/`rawData`).
**Status: FIXED**

**Impact:** Biggest single performance win — reduces dashboard data transfer
by 80-90%.
Created `jobPoolSummarySelect` constant in `src/types/index.ts`. Updated
`fetchDashboardData()`, `getMoreJobs()`, and both type definitions to use
selective `jobPool` queries excluding `description` and `rawData`. Job detail
page now receives `description` as a server-component prop. Store `partialize`
simplified since heavy fields are never fetched.

### 6. Optimistic application ID never replaced after `toggleSaveJob`

**Files:** `src/lib/store.ts:230,273-274`

**Problem:** Saving a job creates an optimistic application with
`id: "optimistic-{jobId}"`. The server returns `{ applicationId }` with the
real DB ID, but the store never swaps it in. If the user immediately opens
Pipeline and edits that application, server actions fail.
**Status: FIXED**

**Fix:** In the `toggleSaveJob` store action's `.then()` callback (around line
273), after the server action succeeds, update the application's ID:
```ts
// After successful save, replace optimistic ID with real one
if (result.applicationId) {
set((state) => ({
applications: state.applications.map((app) =>
app.id === `optimistic-${jobId}` ? { ...app, id: result.applicationId! } : app
),
}));
}
```
`retryServerAction` is now generic (`<T>`) returning `T | null` instead of
`boolean`. The `toggleSaveJob` callback captures the server response and swaps
`optimistic-{jobId}` with the real `applicationId`. All other store callers
updated to check `result === null` for failure.

### 7. `updateAppStatus` doesn't mirror server-side `feedStatus: ARCHIVED`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Enable pg_trgm extension for trigram-based indexing
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Trigram GIN index on job_pool.title for LIKE ANY(...) in match-sql.ts
CREATE INDEX idx_job_pool_title_trgm ON job_pool USING gin (title gin_trgm_ops);

-- Trigram GIN index on job_pool.location for LIKE ANY(...) in match-sql.ts
CREATE INDEX idx_job_pool_location_trgm ON job_pool USING gin (location gin_trgm_ops);
26 changes: 16 additions & 10 deletions src/app/(admin)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,43 @@ async function requireAdmin() {

export async function adminAdjustUsageLimit(data: unknown) {
await requireAdmin();
const parsed = adminAdjustUsageLimitSchema.parse(data);
const parsed = adminAdjustUsageLimitSchema.safeParse(data);
if (!parsed.success) throw new Error("Invalid input");
await prisma.usage.update({
where: { userId: parsed.userId },
data: { monthlyLimitInputTokens: parsed.monthlyLimitInputTokens },
where: { userId: parsed.data.userId },
data: { monthlyLimitInputTokens: parsed.data.monthlyLimitInputTokens },
});
revalidatePath("/admin/users");
}

export async function adminDisableUser(data: unknown) {
await requireAdmin();
const { userId } = adminUserIdSchema.parse(data);
const parsed = adminUserIdSchema.safeParse(data);
if (!parsed.success) throw new Error("Invalid input");
await prisma.user.update({
where: { id: userId },
where: { id: parsed.data.userId },
data: { disabledAt: new Date() },
});
revalidatePath("/admin/users");
}

export async function adminEnableUser(data: unknown) {
await requireAdmin();
const { userId } = adminUserIdSchema.parse(data);
const parsed = adminUserIdSchema.safeParse(data);
if (!parsed.success) throw new Error("Invalid input");
await prisma.user.update({
where: { id: userId },
where: { id: parsed.data.userId },
data: { disabledAt: null },
});
revalidatePath("/admin/users");
}

export async function adminResetMonthlyUsage(data: unknown) {
await requireAdmin();
const { userId } = adminUserIdSchema.parse(data);
const parsed = adminUserIdSchema.safeParse(data);
if (!parsed.success) throw new Error("Invalid input");
await prisma.usage.update({
where: { userId },
where: { userId: parsed.data.userId },
data: { currentMonthInputTokens: 0, currentMonthOutputTokens: 0 },
});
revalidatePath("/admin/users");
Expand Down Expand Up @@ -82,7 +86,9 @@ export async function adminCopyProfileToAdmin(
data: unknown,
): Promise<{ profileId: string; jobsCopied: number; applicationsCopied: number }> {
const adminUserId = await requireAdmin();
const { profileId, mode } = adminCopyProfileSchema.parse(data);
const parsed = adminCopyProfileSchema.safeParse(data);
if (!parsed.success) throw new Error("Invalid input");
const { profileId, mode } = parsed.data;

const sourceProfile = await prisma.profile.findUnique({
where: { id: profileId },
Expand Down
3 changes: 3 additions & 0 deletions src/app/(admin)/admin/feedback/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Link from "next/link";
import { formatDistanceToNow } from "date-fns";
import type { Metadata } from "next";

import { getAdminFeedbackList } from "@/lib/admin-queries";

export const metadata: Metadata = { title: "Feedback" };

type FeedbackMetadata = {
pathname?: string;
profileName?: string;
Expand Down
3 changes: 3 additions & 0 deletions src/app/(admin)/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { formatDistanceToNow } from "date-fns";
import type { Metadata } from "next";

import { AdminStatCard } from "@/components/admin/AdminStatCard";

export const metadata: Metadata = { title: "Admin Overview" };
import {
getAdminOverviewStats,
getRecentScrapeRuns,
Expand Down
3 changes: 3 additions & 0 deletions src/app/(admin)/admin/users/[userId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { format, formatDistanceToNow } from "date-fns";
import type { Metadata } from "next";

import { env } from "@/env";

export const metadata: Metadata = { title: "User Detail" };
import { getAdminUserDetail } from "@/lib/admin-queries";
import { AdminStatCard } from "@/components/admin/AdminStatCard";
import { CopyProfileButton } from "@/components/admin/CopyProfileButton";
Expand Down
3 changes: 2 additions & 1 deletion src/app/(admin)/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Link from "next/link";
import { formatDistanceToNow } from "date-fns";
import type { Metadata } from "next";

import { getAdminUserList } from "@/lib/admin-queries";
import { UserSearchBar } from "@/components/admin/UserSearchBar";

export const metadata = { title: "Users" };
export const metadata: Metadata = { title: "Users" };

export default async function AdminUsersPage({
searchParams,
Expand Down
10 changes: 8 additions & 2 deletions src/app/(dashboard)/actions-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { auth } from "@clerk/nextjs/server";
import { prisma } from "@/lib/prisma";
import { getActiveProfile } from "@/lib/get-active-profile";
import { getFollowUpCount } from "@/app/(dashboard)/pipeline/actions";
import { jobPoolSummarySelect } from "@/types";

/**
* Fetches all dashboard data for the authenticated user.
Expand Down Expand Up @@ -38,12 +39,17 @@ export async function fetchDashboardData() {
}),
prisma.job.findMany({
where: { profileId: activeProfile.id },
include: { jobPool: true, application: { select: { status: true } } },
include: {
jobPool: { select: jobPoolSummarySelect },
application: { select: { status: true } },
},
orderBy: { createdAt: "desc" },
}),
prisma.application.findMany({
where: { profileId: activeProfile.id },
include: { job: { include: { jobPool: true } } },
include: {
job: { include: { jobPool: { select: jobPoolSummarySelect } } },
},
orderBy: { updatedAt: "desc" },
}),
getFollowUpCount(userId),
Expand Down
Loading
Loading