feat(applications): improve performance

This commit is contained in:
Amruth Pillai
2026-07-05 19:47:49 +02:00
parent 5a1cc6585c
commit 00a9721556
37 changed files with 11819 additions and 362 deletions
+8 -10
View File
@@ -22,10 +22,14 @@ const applicationSchema = createSelectSchema(schema.application, {
jobDescription: z.string().nullable(),
matchScore: z.number().int().min(0).max(100).nullable(),
aiMetadata: aiMetadataSchema.nullable(),
campaign: z.string().trim().nullable(),
notes: z.string().nullable(),
// Rendered as an <a href>; only same-origin storage URLs are ever stored. Constrain to
// http(s)/relative so a hand-crafted `update` can't smuggle a `javascript:` href.
resumeFileUrl: z
.string()
.refine((value) => /^(https?:\/\/|\/)/.test(value), "Resume file URL must be http(s) or a relative path.")
.nullable(),
resumeFileName: z.string().nullable(),
coverLetterUrl: z
.string()
.refine((value) => /^(https?:\/\/|\/)/.test(value), "Cover letter URL must be http(s) or a relative path.")
@@ -52,8 +56,9 @@ const editableSchema = applicationSchema.pick({
source: true,
sourceUrl: true,
jobDescription: true,
campaign: true,
notes: true,
resumeFileUrl: true,
resumeFileName: true,
coverLetterUrl: true,
coverLetterName: true,
followUpAt: true,
@@ -74,7 +79,6 @@ export const applicationDto = {
input: z
.object({
status: applicationStatusSchema.optional(),
campaign: z.string().optional(),
tags: z.array(z.string()).optional(),
includeArchived: z.boolean().optional().default(false),
})
@@ -135,7 +139,7 @@ export const applicationDto = {
// Aggregates for the Insights view. Everything else (funnel, sankey, tiles) is derived
// client-side from these raw counts via computeInsights().
stats: {
input: z.object({ campaign: z.string().optional() }).optional(),
input: z.void(),
output: z.object({
total: z.number(),
byStage: z.array(z.object({ status: applicationStatusSchema, count: z.number() })),
@@ -143,12 +147,6 @@ export const applicationDto = {
}),
},
// Distinct campaign names for the sidebar/filter (Phase 2 uses the same shape).
campaigns: {
input: z.void(),
output: z.array(z.object({ name: z.string(), count: z.number() })),
},
tags: {
input: z.void(),
output: z.array(z.string()),
+7 -1
View File
@@ -61,9 +61,15 @@ async function fetchJobPostingText(url: string): Promise<string> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
// Job boards 403 obvious bot user-agents, so present as a normal browser.
const response = await fetch(url, {
signal: controller.signal,
headers: { "user-agent": "ReactiveResumeBot/1.0" },
headers: {
"user-agent":
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"accept-language": "en-US,en;q=0.9",
},
});
if (!response.ok)
throw new ORPCError("BAD_REQUEST", { message: `Couldn't fetch the posting (HTTP ${response.status}).` });
+2 -22
View File
@@ -21,7 +21,6 @@ export const crudRouter = {
return applicationService.list({
userId: context.user.id,
...(input.status ? { status: input.status } : {}),
...(input.campaign ? { campaign: input.campaign } : {}),
...(input.tags ? { tags: input.tags } : {}),
includeArchived: input.includeArchived,
});
@@ -174,32 +173,13 @@ export const crudRouter = {
tags: ["Applications"],
operationId: "getApplicationStats",
summary: "Application pipeline stats",
description:
"Returns aggregate counts (per stage, per source) for the Insights view. Optionally scoped to a campaign. Requires authentication.",
description: "Returns aggregate counts (per stage, per source) for the Insights view. Requires authentication.",
successDescription: "Aggregate application counts.",
})
.input(applicationDto.stats.input)
.output(applicationDto.stats.output)
.handler(async ({ input, context }) => {
return applicationService.stats({
userId: context.user.id,
...(input?.campaign ? { campaign: input.campaign } : {}),
});
}),
campaigns: protectedProcedure
.route({
method: "GET",
path: "/applications/campaigns",
tags: ["Applications"],
operationId: "listApplicationCampaigns",
summary: "List campaigns",
description: "Returns distinct campaign names with their application counts. Requires authentication.",
successDescription: "Distinct campaigns with counts.",
})
.output(applicationDto.campaigns.output)
.handler(async ({ context }) => {
return applicationService.campaigns({ userId: context.user.id });
return applicationService.stats({ userId: context.user.id });
}),
tags: protectedProcedure
@@ -12,7 +12,6 @@ export const applicationsRouter = {
bulkUpdate: crudRouter.bulkUpdate,
bulkDelete: crudRouter.bulkDelete,
stats: crudRouter.stats,
campaigns: crudRouter.campaigns,
tags: crudRouter.tags,
ai: aiRouter,
};
@@ -24,8 +24,9 @@ type EditableFields = {
source?: string | null | undefined;
sourceUrl?: string | null | undefined;
jobDescription?: string | null | undefined;
campaign?: string | null | undefined;
notes?: string | null | undefined;
resumeFileUrl?: string | null | undefined;
resumeFileName?: string | null | undefined;
coverLetterUrl?: string | null | undefined;
coverLetterName?: string | null | undefined;
followUpAt?: Date | null | undefined;
@@ -51,13 +52,7 @@ const stripUserId = <T extends { userId: string }>(row: T) => {
};
export const applicationService = {
list: async (input: {
userId: string;
status?: ApplicationStatus;
campaign?: string;
tags?: string[];
includeArchived?: boolean;
}) => {
list: async (input: { userId: string; status?: ApplicationStatus; tags?: string[]; includeArchived?: boolean }) => {
const rows = await db
.select()
.from(schema.application)
@@ -65,7 +60,6 @@ export const applicationService = {
and(
eq(schema.application.userId, input.userId),
input.status ? eq(schema.application.status, input.status) : undefined,
input.campaign ? eq(schema.application.campaign, input.campaign) : undefined,
input.tags && input.tags.length > 0 ? arrayContains(schema.application.tags, input.tags) : undefined,
),
)
@@ -241,12 +235,8 @@ export const applicationService = {
},
// Raw counts for Insights; funnel/sankey/tiles are derived client-side from these.
stats: async (input: { userId: string; campaign?: string }) => {
const scope = and(
eq(schema.application.userId, input.userId),
eq(schema.application.archived, false),
input.campaign ? eq(schema.application.campaign, input.campaign) : undefined,
);
stats: async (input: { userId: string }) => {
const scope = and(eq(schema.application.userId, input.userId), eq(schema.application.archived, false));
const byStage = await db
.select({ status: schema.application.status, count: sql<number>`count(*)::int` })
@@ -271,16 +261,6 @@ export const applicationService = {
};
},
campaigns: async (input: { userId: string }) => {
const rows = await db
.select({ name: schema.application.campaign, count: sql<number>`count(*)::int` })
.from(schema.application)
.where(and(eq(schema.application.userId, input.userId), eq(schema.application.archived, false)))
.groupBy(schema.application.campaign);
return rows.filter((row): row is { name: string; count: number } => !!row.name).sort((a, b) => b.count - a.count);
},
listTags: async (input: { userId: string }) => {
const rows = await db
.select({ tag: sql<string>`distinct unnest(${schema.application.tags})` })
+4 -2
View File
@@ -35,9 +35,11 @@ export const application = pg.pgTable(
jobDescription: pg.text("job_description"),
matchScore: pg.integer("match_score"),
aiMetadata: pg.jsonb("ai_metadata").$type<AiMetadata>(),
// Reserves the deferred campaigns feature; no management UI this pass.
campaign: pg.text("campaign"),
notes: pg.text("notes"),
// An uploaded resume file (PDF) sent with this application, distinct from the live
// resumeId link. Stored as the storage URL + original filename for display.
resumeFileUrl: pg.text("resume_file_url"),
resumeFileName: pg.text("resume_file_name"),
// A cover letter sent with this application (PDF/doc uploaded to storage). Stored as the
// key returned by the storage upload plus the original filename for display.
coverLetterUrl: pg.text("cover_letter_url"),