Job search tracker: CRUD, AI parse/tailor/cover via OmniRoute, legacy import

This commit is contained in:
Jonathan Sykes
2026-08-16 19:05:19 +08:00
parent 8aebc279db
commit 9ba9ae675a
31 changed files with 2294 additions and 85 deletions

169
scripts/import-legacy.ts Normal file
View File

@@ -0,0 +1,169 @@
/**
* One-time import of the legacy markdown job files + resumes into the DB.
* Usage: npx tsx scripts/import-legacy.ts
*/
import { PrismaClient } from "@prisma/client";
import matter from "gray-matter";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
const prisma = new PrismaClient();
const LEGACY = path.join(os.homedir(), "development/personal/ai-job-search");
function section(body: string, name: string): string | undefined {
const re = new RegExp(`##\\s*${name}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "i");
return body.match(re)?.[1]?.trim();
}
function firstMatch(body: string, re: RegExp): string | undefined {
return body.match(re)?.[1]?.trim();
}
async function importJobs() {
const dir = path.join(LEGACY, "jobs");
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
for (const file of files) {
const raw = fs.readFileSync(path.join(dir, file), "utf8");
const { data, content } = matter(raw);
const title = (data.title as string) ?? file.replace(/\.md$/, "");
const company = (data.company as string) ?? "Unknown";
const existing = await prisma.job.findFirst({ where: { title, company } });
if (existing) {
console.log(`skip (exists): ${file}`);
continue;
}
const url = firstMatch(content, /\*\*Link:\*\*\s*(\S+)/);
const applyUrl = firstMatch(content, /\*\*Apply(?: link)?:\*\*\s*(\S+)/);
const details = section(content, "Details");
const payLine = details ? firstMatch(details, /\*\*Pay(?:\s*Range)?:\*\*\s*(.+)/) : undefined;
const location = details ? firstMatch(details, /\*\*Location:\*\*\s*(.+)/) : undefined;
// crude pay parse: "$15-20/hr", "₱150-170k/mo", "$65-120/hr"
let payMin: number | undefined, payMax: number | undefined, payCurrency: string | undefined, payPeriod: string | undefined;
if (payLine) {
const m = payLine.match(/([$₱P])\s*([\d,.]+)\s*[-–]\s*([\d,.]+)(k?)\s*\/?\s*(hr|hour|mo|month|yr|year)?/i);
if (m) {
const mult = m[4] === "k" ? 1000 : 1;
payMin = parseFloat(m[2].replace(/,/g, "")) * mult;
payMax = parseFloat(m[3].replace(/,/g, "")) * mult;
payCurrency = m[1] === "$" ? "USD" : "PHP";
payPeriod = /hr|hour/i.test(m[5] ?? "") ? "hour" : /mo/i.test(m[5] ?? "") ? "month" : /yr|year/i.test(m[5] ?? "") ? "year" : undefined;
}
}
const phEligibility = /philippines|ph eligible|170\+ countries|worldwide/i.test(content) ? "yes" : "unclear";
const status = (data.status as string)?.replace(/\s+/g, "-") === "not-applied" ? "prospect" : "prospect";
await prisma.job.create({
data: {
title,
company,
source: file.replace(/\.md$/, ""),
url,
applyUrl,
payMin,
payMax,
payCurrency,
payPeriod,
location,
remoteType: "remote",
phEligibility,
description: section(content, "Description"),
requirements: section(content, "Requirements"),
notes: section(content, "Notes") ?? section(content, "Tailoring notes"),
status,
},
});
console.log(`imported: ${file}`);
}
}
async function importResumes() {
const base = path.join(LEGACY, "resume/resume.md");
if (fs.existsSync(base)) {
const md = fs.readFileSync(base, "utf8");
const exists = await prisma.resume.findFirst({ where: { kind: "base" } });
if (!exists) {
await prisma.resume.create({ data: { name: "Base Resume", kind: "base", markdown: md } });
console.log("imported: base resume");
}
}
const tailoredDir = path.join(LEGACY, "resume/tailored");
if (fs.existsSync(tailoredDir)) {
for (const f of fs.readdirSync(tailoredDir).filter((f) => f.endsWith(".md"))) {
const name = `Tailored: ${f.replace(/\.md$/, "")}`;
const exists = await prisma.resume.findFirst({ where: { name } });
if (exists) continue;
await prisma.resume.create({
data: { name, kind: "tailored", markdown: fs.readFileSync(path.join(tailoredDir, f), "utf8") },
});
console.log(`imported: tailored ${f}`);
}
}
}
async function seedProfile() {
const exists = await prisma.profile.findUnique({ where: { id: "main" } });
if (exists) return;
await prisma.profile.create({
data: {
id: "main",
fullName: "Joshua C. Sarmiento",
email: "joshuacsarmiento@gmail.com",
location: "Philippines",
headline: "Software Developer / AI Engineer — Prompt Engineering, Conversational AI, Backend Systems",
summary:
"Software developer with 8+ years of experience spanning enterprise IT, full-stack web development, and applied AI: building LLM-powered APIs and RAG systems with FastAPI and Amazon Bedrock.",
skills: JSON.stringify([
"Prompt engineering", "Amazon Bedrock", "RAG", "Conversational AI", "Python", "FastAPI",
"REST API design", "Active Directory", "Full-stack web development", "Bash/PowerShell automation",
]),
workHistory: JSON.stringify([
{
position: "Project Technical Specialist III",
company: "Philippine Institute of Volcanology and Seismology (PHIVOLCS)",
unit: "Geomatics and Hazard Assessment Services Section",
start: "2025-03", end: null, supervisor: "Eric Santos",
bullets: [
"Contributed to CLUP and CDRA systems",
"Gates Handa: AI APIs with FastAPI + Amazon Bedrock, chatbot, RAG document/report analyzer",
"Design system diagrams and plans; support project management",
],
},
{
position: "Computer Programmer II",
company: "Presidential Commission for the Urban Poor (PCUP)",
unit: "IT Unit",
start: "2019-08", end: "2024-02", supervisor: "Jason Ronidel",
bullets: [
"Developed major web apps: Attendance Monitoring, Service Workflow Management, UPO Accreditation, Reservation, Directory",
"Managed commission network infrastructure and devices",
"MS Access + VBA database applications",
],
},
{
position: "IT Associate",
company: "Manulife Business Process Services",
unit: "Global Access Management (GAM) Division",
start: "2016-11", end: "2017-10", supervisor: "Emory Lopez",
bullets: [
"Automated Active Directory user management with scripts (.sh, .ps1, .bat)",
"Managed AD, mainframe, Oracle, FTP, and email accounts (Exchange, Lotus Notes, O365)",
"ServiceNow request handling; VPN token creation",
],
},
]),
},
});
console.log("seeded: profile");
}
async function main() {
await importJobs();
await importResumes();
await seedProfile();
console.log("done");
}
main().finally(() => prisma.$disconnect());