feat: workplace filter, job dedup, company skip docs, deploy notes

- Add remote/orchestrator filter by workplace (remote, not remote, unknown) with URL param
- Expose isRemote on job list API; canonicalize URLs and source_job_id dedup on import
- Onboarding: optional VITE_SKIP_RXRESUME_ONBOARDING for RxResume-free onboarding
- Scoring UI + docs for company skip list; pipeline-run dedup note
- Vitest: TZ=UTC for stable time-based tests
- DEPLOY_GITEA_VM_CRON_TELEGRAM.md for VM/cron/Telegram ops

Made-with: Cursor
This commit is contained in:
ilia
2026-04-04 14:44:52 -04:00
parent 0c31377ac6
commit 9576c3d7a1
20 changed files with 705 additions and 61 deletions
+1
View File
@@ -1,4 +1,5 @@
export * from "./extractors";
export * from "./job-url-canonical";
export * from "./location-support";
export * from "./types";
export * from "./utils/type-conversion";
+27
View File
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { canonicalizeJobUrl } from "./job-url-canonical";
describe("canonicalizeJobUrl", () => {
it("strips tracking query params and normalizes host", () => {
const a =
"https://www.example.com/jobs/123?utm_source=linkedin&role=eng&utm_medium=social";
const b = "http://example.com/jobs/123?role=eng";
expect(canonicalizeJobUrl(a)).toBe(canonicalizeJobUrl(b));
});
it("removes trailing slash on path", () => {
expect(canonicalizeJobUrl("https://example.com/path/")).toBe(
"https://example.com/path",
);
});
it("sorts query params for stable comparison", () => {
const a = "https://example.com/x?b=2&a=1";
const b = "https://example.com/x?a=1&b=2";
expect(canonicalizeJobUrl(a)).toBe(canonicalizeJobUrl(b));
});
it("returns trimmed non-URL strings unchanged", () => {
expect(canonicalizeJobUrl(" not a url ")).toBe("not a url");
});
});
+61
View File
@@ -0,0 +1,61 @@
/**
* Normalize job listing URLs so the same role is not stored twice when only
* tracking params, scheme, or trivial path differences differ.
*/
const TRACKING_QUERY_PREFIXES = ["utm_", "stm_"] as const;
const DROP_QUERY_KEYS = new Set([
"ref",
"src",
"fbclid",
"gclid",
"mc_eid",
"icid",
]);
export function canonicalizeJobUrl(raw: string): string {
const trimmed = raw.trim();
if (!trimmed) return trimmed;
try {
const u = new URL(trimmed);
u.hash = "";
let host = u.hostname.toLowerCase();
if (host.startsWith("www.")) host = host.slice(4);
u.hostname = host;
u.protocol = "https:";
for (const key of [...u.searchParams.keys()]) {
const lower = key.toLowerCase();
if (
DROP_QUERY_KEYS.has(lower) ||
TRACKING_QUERY_PREFIXES.some((prefix) => lower.startsWith(prefix))
) {
u.searchParams.delete(key);
}
}
const sortedKeys = [...u.searchParams.keys()].sort((a, b) =>
a.localeCompare(b),
);
const next = new URLSearchParams();
for (const k of sortedKeys) {
for (const v of u.searchParams.getAll(k)) {
next.append(k, v);
}
}
u.search = next.toString() ? `?${next.toString()}` : "";
let path = u.pathname;
if (path.length > 1 && path.endsWith("/")) {
path = path.slice(0, -1);
}
u.pathname = path || "/";
return u.toString();
} catch {
return trimmed;
}
}
+1
View File
@@ -213,6 +213,7 @@ export type JobListItem = Pick<
| "salaryMinAmount"
| "salaryMaxAmount"
| "salaryCurrency"
| "isRemote"
| "discoveredAt"
| "appliedAt"
| "updatedAt"