diff --git a/.gitattributes b/.gitattributes index d57e87a..bf6320e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ * text=auto eol=lf # Binary assets — never touch. +*.docx binary *.png binary *.jpg binary *.jpeg binary diff --git a/site/public/cv/connor-babbington-cv-en.pdf b/site/public/cv/connor-babbington-cv-en.pdf index 3de417b..e09d29b 100644 Binary files a/site/public/cv/connor-babbington-cv-en.pdf and b/site/public/cv/connor-babbington-cv-en.pdf differ diff --git a/site/public/cv/connor-babbington-cv-no.pdf b/site/public/cv/connor-babbington-cv-no.pdf index 41acfca..6b999cb 100644 Binary files a/site/public/cv/connor-babbington-cv-no.pdf and b/site/public/cv/connor-babbington-cv-no.pdf differ diff --git a/site/src/data/profile.ts b/site/src/data/profile.ts index 0e9c47f..7f2a69f 100644 --- a/site/src/data/profile.ts +++ b/site/src/data/profile.ts @@ -15,8 +15,8 @@ export const profile: Profile = { gitea: 'https://git.cesnimda.uk/cesnimda', }, cv: { - en: { path: '/cv/connor-babbington-cv-en.pdf', sizeKb: 132, updated: '2026-07' }, - no: { path: '/cv/connor-babbington-cv-no.pdf', sizeKb: 172, updated: '2026-07' }, + en: { path: '/cv/connor-babbington-cv-en.pdf', sizeKb: 8, updated: '2026-07' }, + no: { path: '/cv/connor-babbington-cv-no.pdf', sizeKb: 8, updated: '2026-07' }, }, photo: { src: 'placeholder:portrait', diff --git a/tools/cv/Connor-Babbington-CV-EN.docx b/tools/cv/Connor-Babbington-CV-EN.docx new file mode 100644 index 0000000..9c26d89 Binary files /dev/null and b/tools/cv/Connor-Babbington-CV-EN.docx differ diff --git a/tools/cv/Connor-Babbington-CV-NO.docx b/tools/cv/Connor-Babbington-CV-NO.docx new file mode 100644 index 0000000..6054c2b Binary files /dev/null and b/tools/cv/Connor-Babbington-CV-NO.docx differ diff --git a/tools/cv/README.md b/tools/cv/README.md new file mode 100644 index 0000000..ad3b9a3 --- /dev/null +++ b/tools/cv/README.md @@ -0,0 +1,28 @@ +# CV generator + +Produces Connor Babbington's CV in both languages as **ATS-safe PDFs** (single column, +real text, standard headings, no letter-spacing tricks) and as editable **.docx**. + +Content lives in [`content.js`](content.js) — the single source, mirroring +`docs/design/02-ux/CANONICAL_CONTENT.md`. Positioning is "Systems Developer, eight +years' experience" (not "mid-level"); the council role shows the apprentice→developer +progression; a Projects section is included; Norwegian is written natively. + +## Regenerate + +```bash +cd tools/cv +npm install +npm run pdf # -> connor-babbington-cv-en.pdf / -no.pdf (deploy these to site/public/cv/) +npm run docx # -> Connor-Babbington-CV-EN.docx / -NO.docx (editable source) +``` + +Then copy the PDFs into `site/public/cv/` (same filenames) — the site links to those +stable URLs, so sharing links always fetch the newest CV. + +## Notes + +- The download default on the site is the **ATS-safe PDF** (these files). +- The current PDFs replaced the original two-column, letter-spaced CVs, which parsed + as scrambled text in applicant-tracking systems. +- `.docx` files are kept for easy manual editing; re-export or regenerate after edits. diff --git a/tools/cv/build-cv.js b/tools/cv/build-cv.js new file mode 100644 index 0000000..4c6b54e --- /dev/null +++ b/tools/cv/build-cv.js @@ -0,0 +1,216 @@ +const fs = require('fs'); +const { + Document, Packer, Paragraph, TextRun, ExternalHyperlink, + AlignmentType, LevelFormat, TabStopType, BorderStyle, HeadingLevel, +} = require('docx'); + +// A4, 1 inch margins. Content width = 11906 - 2*1440 = 9026 DXA. +const CONTENT_W = 9026; + +function styles() { + return { + default: { document: { run: { font: 'Arial', size: 21 } } }, // 10.5pt + paragraphStyles: [ + { id: 'Heading2', name: 'Heading 2', basedOn: 'Normal', next: 'Normal', quickFormat: true, + run: { size: 24, bold: true, font: 'Arial', color: '1A1A1A' }, + paragraph: { + spacing: { before: 280, after: 120 }, outlineLevel: 1, + border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'B0B0B0', space: 4 } }, + } }, + ], + }; +} + +const numbering = { + config: [{ + reference: 'b', + levels: [{ level: 0, format: LevelFormat.BULLET, text: '•', alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 360, hanging: 220 } } } }], + }], +}; + +const bullet = (text) => + new Paragraph({ numbering: { reference: 'b', level: 0 }, spacing: { after: 40 }, + children: [new TextRun(text)] }); + +const heading = (text) => new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun(text)] }); + +// Role line: "Role — Org" left, dates right (tab stop). +const roleLine = (left, dates) => + new Paragraph({ + spacing: { before: 120, after: 0 }, + tabStops: [{ type: TabStopType.RIGHT, position: CONTENT_W }], + children: [new TextRun({ text: left, bold: true }), new TextRun({ text: `\t${dates}`, color: '555555' })], + }); + +const note = (text) => + new Paragraph({ spacing: { after: 40 }, children: [new TextRun({ text, italics: true, color: '555555' })] }); + +const skillLine = (group, items) => + new Paragraph({ spacing: { after: 40 }, + children: [new TextRun({ text: `${group}: `, bold: true }), new TextRun(items)] }); + +const projectLine = (name, desc) => + new Paragraph({ numbering: { reference: 'b', level: 0 }, spacing: { after: 40 }, + children: [new TextRun({ text: `${name} — `, bold: true }), new TextRun(desc)] }); + +function buildCv(c) { + const children = []; + + // Name + title + children.push(new Paragraph({ spacing: { after: 0 }, + children: [new TextRun({ text: c.name, bold: true, size: 40 })] })); + children.push(new Paragraph({ spacing: { after: 60 }, + children: [new TextRun({ text: c.title, size: 24, color: '333333' })] })); + + // Contact (with hyperlinks), rule beneath + children.push(new Paragraph({ + spacing: { after: 20 }, + children: [ + new TextRun(`${c.location} · `), + new ExternalHyperlink({ link: `mailto:${c.email}`, children: [new TextRun({ text: c.email, style: 'Hyperlink' })] }), + new TextRun(` · ${c.phone}`), + ], + })); + children.push(new Paragraph({ + spacing: { after: 40 }, + border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'B0B0B0', space: 6 } }, + children: [ + new ExternalHyperlink({ link: 'https://cesnimda.co.uk', children: [new TextRun({ text: 'cesnimda.co.uk', style: 'Hyperlink' })] }), + new TextRun(' · '), + new ExternalHyperlink({ link: 'https://cesnimda.co.uk/Linkedin', children: [new TextRun({ text: 'LinkedIn', style: 'Hyperlink' })] }), + ], + })); + + // Summary + children.push(heading(c.t.summary)); + children.push(new Paragraph({ spacing: { after: 40 }, children: [new TextRun(c.summary)] })); + + // Skills + children.push(heading(c.t.skills)); + for (const s of c.skills) children.push(skillLine(s.group, s.items)); + + // Experience + children.push(heading(c.t.experience)); + for (const e of c.experience) { + children.push(roleLine(`${e.role} — ${e.org}`, e.dates)); + if (e.note) children.push(note(e.note)); + for (const b of e.bullets) children.push(bullet(b)); + } + + // Earlier roles + children.push(new Paragraph({ spacing: { before: 100, after: 20 }, + children: [new TextRun({ text: c.t.earlier, bold: true })] })); + for (const r of c.earlier) children.push(bullet(r)); + + // Projects + children.push(heading(c.t.projects)); + for (const p of c.projects) children.push(projectLine(p.name, p.desc)); + + // Education + children.push(heading(c.t.education)); + children.push(roleLine(c.education.qual, c.education.dates)); + children.push(new Paragraph({ children: [new TextRun(c.education.org)] })); + + return new Document({ + styles: styles(), + numbering, + sections: [{ + properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } }, + children, + }], + }); +} + +const EN = { + name: 'Connor Babbington', + title: 'Systems Developer', + location: 'Tønsberg, Norway', + email: 'connor.babbington@cesnimda.co.uk', + phone: '+47 41 33 44 70', + t: { summary: 'Professional Summary', skills: 'Core Skills', experience: 'Experience', + earlier: 'Earlier roles (part-time, alongside the above)', projects: 'Projects', education: 'Education' }, + summary: + "Systems developer with eight years' experience building and maintaining production software for UK local government. A backend-leaning full-stack developer across C#, .NET, Python, JavaScript/TypeScript and SQL, with hands-on DevOps in Docker, Linux, CI/CD, Azure DevOps and GitHub. I turn stakeholder requirements into reliable, well-tested systems and support them in production. Based in Tønsberg with a valid residence permit. Native English speaker; Norwegian at B1 and actively developing. Open to remote, hybrid or on-site roles.", + skills: [ + { group: 'Development', items: 'C#, .NET, Python, JavaScript, TypeScript, React, SQL' }, + { group: 'DevOps & Infrastructure', items: 'Docker, Linux, CI/CD, Azure DevOps, GitHub, nginx / reverse proxies, monitoring, self-hosting' }, + { group: 'Practices', items: 'Testing, security hardening, OAuth2 integrations, production support, stakeholder communication' }, + ], + experience: [ + { role: 'System Developer', org: 'Warwickshire County Council, UK', dates: '2015–2023', + note: 'The first two years were completed as an apprenticeship.', + bullets: [ + 'Worked on the county-wide highways and streetlight fault-reporting system used across Warwickshire.', + 'Designed, built and maintained full-stack applications in C#, Python, Ruby on Rails, SQL and JavaScript.', + 'Delivered reliable, well-tested software and resolved stability and performance issues on live systems.', + 'Owned deployments and production troubleshooting, and strengthened permissions and reliability.', + 'Guided colleagues on best practices for code and process.', + ] }, + { role: 'Independent Development & Norwegian Study', org: 'Tønsberg, Norway', dates: '2023–Present', + bullets: [ + 'Building and self-hosting full-stack products (see Projects) while developing Norwegian language skills.', + ] }, + ], + earlier: [ + 'Sales Representative — Royal Vapes, UK (2017–2021)', + 'Bartender — The Hodcarrier, UK (2016–2018)', + 'Receptionist — Nuffield Health, UK (2014–2015)', + ], + projects: [ + { name: 'JobTrack', desc: 'Full-stack job-application tracker (React, ASP.NET Core, SQLite, Docker) with a local AI summariser and Gmail integration.' }, + { name: 'InboxIntel', desc: 'Gmail analytics and safe bulk-cleanup tool built as Clean Architecture in .NET 8 with PostgreSQL and encrypted OAuth tokens.' }, + { name: 'Self-hosted infrastructure lab', desc: 'Ubuntu, Docker, nginx reverse proxy, authentication, monitoring and a self-hosted Gitea instance.' }, + ], + education: { qual: 'Extended Diploma — NVQ Level 3 in ICT', org: 'Warwickshire College, UK', dates: '2012–2015' }, +}; + +const NO = { + name: 'Connor Babbington', + title: 'Systemutvikler', + location: 'Tønsberg', + email: 'connor.babbington@cesnimda.co.uk', + phone: '+47 41 33 44 70', + t: { summary: 'Sammendrag', skills: 'Kjernekompetanse', experience: 'Erfaring', + earlier: 'Tidligere roller (deltid, ved siden av)', projects: 'Prosjekter', education: 'Utdanning' }, + summary: + 'Systemutvikler med åtte års erfaring med å bygge og vedlikeholde produksjonssystemer i britisk offentlig sektor. Fullstack-utvikler med tyngde på backend i C#, .NET, Python, JavaScript/TypeScript og SQL, med praktisk DevOps i Docker, Linux, CI/CD, Azure DevOps og GitHub. Jeg omsetter behov fra brukere og interessenter til pålitelige, godt testede systemer og drifter dem i produksjon. Bosatt i Tønsberg med gyldig oppholdstillatelse. Engelsk morsmål; norsk på B1-nivå og i aktiv utvikling. Åpen for remote, hybrid eller stedbaserte roller.', + skills: [ + { group: 'Utvikling', items: 'C#, .NET, Python, JavaScript, TypeScript, React, SQL' }, + { group: 'DevOps og infrastruktur', items: 'Docker, Linux, CI/CD, Azure DevOps, GitHub, nginx / reverse proxy, overvåking, egendrift' }, + { group: 'Arbeidsmåte', items: 'Testing, sikkerhet, OAuth2-integrasjoner, driftsstøtte, kommunikasjon med interessenter' }, + ], + experience: [ + { role: 'Systemutvikler', org: 'Warwickshire County Council, UK', dates: '2015–2023', + note: 'De to første årene var en lærlingperiode.', + bullets: [ + 'Jobbet på det fylkesdekkende systemet for feilmelding av veilys og veier, brukt i hele Warwickshire.', + 'Designet, bygde og vedlikeholdt fullstack-applikasjoner i C#, Python, Ruby on Rails, SQL og JavaScript.', + 'Leverte pålitelig, godt testet programvare og løste stabilitets- og ytelsesproblemer i produksjon.', + 'Hadde ansvar for utrulling og feilsøking i drift, og styrket tilganger og pålitelighet.', + 'Veiledet kollegaer i beste praksis for kode og prosess.', + ] }, + { role: 'Egen produktutvikling og norskkurs', org: 'Tønsberg', dates: '2023–nå', + bullets: [ + 'Bygger og drifter egne fullstack-produkter (se Prosjekter) samtidig som jeg utvikler norskkunnskapene.', + ] }, + ], + earlier: [ + 'Salgsrepresentant — Royal Vapes, UK (2017–2021)', + 'Bartender — The Hodcarrier, UK (2016–2018)', + 'Resepsjonist — Nuffield Health, UK (2014–2015)', + ], + projects: [ + { name: 'JobTrack', desc: 'Fullstack jobbsøknads-tracker (React, ASP.NET Core, SQLite, Docker) med lokal AI-oppsummering og Gmail-integrasjon.' }, + { name: 'InboxIntel', desc: 'Verktøy for Gmail-analyse og trygg masseopprydding, bygget som Clean Architecture i .NET 8 med PostgreSQL og krypterte OAuth-tokens.' }, + { name: 'Egendriftet hjemmelab', desc: 'Ubuntu, Docker, nginx reverse proxy, autentisering, overvåking og egen Gitea-instans.' }, + ], + education: { qual: 'Extended Diploma — NVQ nivå 3 i IKT', org: 'Warwickshire College, UK', dates: '2012–2015' }, +}; + +async function main() { + await Packer.toBuffer(buildCv(EN)).then((b) => fs.writeFileSync('Connor-Babbington-CV-EN.docx', b)); + await Packer.toBuffer(buildCv(NO)).then((b) => fs.writeFileSync('Connor-Babbington-CV-NO.docx', b)); + console.log('wrote EN + NO docx'); +} +main(); diff --git a/tools/cv/build-pdf.js b/tools/cv/build-pdf.js new file mode 100644 index 0000000..dae3bd2 --- /dev/null +++ b/tools/cv/build-pdf.js @@ -0,0 +1,107 @@ +const fs = require('fs'); +const PDFDocument = require('pdfkit'); + +// Shared content (mirrors build-cv.js). +const { EN, NO } = require('./content.js'); + +const PAGE_W = 595.28; +const MARGIN = 54; +const CW = PAGE_W - MARGIN * 2; // content width +const INK = '#1a1a1a'; +const GRAY = '#555555'; +const RULE = '#b0b0b0'; + +function build(c, outfile) { + const doc = new PDFDocument({ size: 'A4', margins: { top: MARGIN, bottom: MARGIN, left: MARGIN, right: MARGIN } }); + doc.pipe(fs.createWriteStream(outfile)); + + const rule = (gap = 4) => { + const y = doc.y + gap; + doc.moveTo(MARGIN, y).lineTo(MARGIN + CW, y).lineWidth(0.75).strokeColor(RULE).stroke(); + doc.y = y + gap + 2; + }; + + const heading = (text) => { + doc.moveDown(0.5); + doc.fillColor(INK).font('Helvetica-Bold').fontSize(12).text(text, MARGIN, doc.y); + rule(2); + }; + + const bullet = (text, prefix) => { + const y = doc.y; + doc.fillColor(INK).font('Helvetica').fontSize(10).text('•', MARGIN + 2, y, { width: 10, lineBreak: false }); + const tx = MARGIN + 16; + const tw = CW - 16; + if (prefix) { + doc.font('Helvetica-Bold').text(prefix, tx, y, { width: tw, continued: true }); + doc.font('Helvetica').text(text, { width: tw }); + } else { + doc.font('Helvetica').text(text, tx, y, { width: tw }); + } + doc.moveDown(0.15); + }; + + const roleLine = (left, dates) => { + doc.moveDown(0.3); + const y = doc.y; + doc.font('Helvetica').fontSize(9.5).fillColor(GRAY).text(dates, MARGIN, y, { width: CW, align: 'right', lineBreak: false }); + doc.font('Helvetica-Bold').fontSize(10.5).fillColor(INK).text(left, MARGIN, y, { width: CW - 90, lineBreak: false }); + doc.y = y + 14; + }; + + // Header + doc.fillColor(INK).font('Helvetica-Bold').fontSize(20).text(c.name, MARGIN, MARGIN); + doc.font('Helvetica').fontSize(12).fillColor('#333').text(c.title, { paragraphGap: 2 }); + doc.font('Helvetica').fontSize(9.5).fillColor(GRAY) + .text(`${c.location} · ${c.email} · ${c.phone}`); + doc.text('cesnimda.co.uk · cesnimda.co.uk/Linkedin'); + rule(4); + + // Summary + heading(c.t.summary); + doc.font('Helvetica').fontSize(10).fillColor(INK).text(c.summary, MARGIN, doc.y, { width: CW, align: 'justify' }); + + // Skills + heading(c.t.skills); + for (const s of c.skills) { + const y = doc.y; + doc.font('Helvetica-Bold').fontSize(10).fillColor(INK).text(`${s.group}: `, MARGIN, y, { width: CW, continued: true }); + doc.font('Helvetica').text(s.items, { width: CW }); + doc.moveDown(0.15); + } + + // Experience + heading(c.t.experience); + for (const e of c.experience) { + roleLine(`${e.role} — ${e.org}`, e.dates); + if (e.note) { + doc.font('Helvetica-Oblique').fontSize(9.5).fillColor(GRAY).text(e.note, MARGIN, doc.y, { width: CW }); + doc.moveDown(0.1); + } + for (const b of e.bullets) bullet(b); + } + + // Earlier roles + doc.moveDown(0.4); + doc.font('Helvetica-Bold').fontSize(10).fillColor(INK).text(c.t.earlier, MARGIN, doc.y, { width: CW }); + doc.moveDown(0.1); + for (const r of c.earlier) bullet(r); + + // Projects + heading(c.t.projects); + for (const p of c.projects) bullet(p.desc, `${p.name} — `); + + // Education + heading(c.t.education); + roleLine(c.education.qual, c.education.dates); + doc.font('Helvetica').fontSize(10).fillColor(INK).text(c.education.org, MARGIN, doc.y, { width: CW }); + + doc.end(); + return new Promise((res) => doc.on('end', res)); +} + +(async () => { + await build(EN, 'connor-babbington-cv-en.pdf'); + await build(NO, 'connor-babbington-cv-no.pdf'); + console.log('wrote EN + NO pdf'); +})(); diff --git a/tools/cv/content.js b/tools/cv/content.js new file mode 100644 index 0000000..efeb357 --- /dev/null +++ b/tools/cv/content.js @@ -0,0 +1,87 @@ +const EN = { + name: 'Connor Babbington', + title: 'Systems Developer', + location: 'Tønsberg, Norway', + email: 'connor.babbington@cesnimda.co.uk', + phone: '+47 41 33 44 70', + t: { summary: 'Professional Summary', skills: 'Core Skills', experience: 'Experience', + earlier: 'Earlier roles (part-time, alongside the above)', projects: 'Projects', education: 'Education' }, + summary: + "Systems developer with eight years’ experience building and maintaining production software for UK local government. A backend-leaning full-stack developer across C#, .NET, Python, JavaScript/TypeScript and SQL, with hands-on DevOps in Docker, Linux, CI/CD, Azure DevOps and GitHub. I turn stakeholder requirements into reliable, well-tested systems and support them in production. Based in Tønsberg with a valid residence permit. Native English speaker; Norwegian at B1 and actively developing. Open to remote, hybrid or on-site roles.", + skills: [ + { group: 'Development', items: 'C#, .NET, Python, JavaScript, TypeScript, React, SQL' }, + { group: 'DevOps & Infrastructure', items: 'Docker, Linux, CI/CD, Azure DevOps, GitHub, nginx / reverse proxies, monitoring, self-hosting' }, + { group: 'Practices', items: 'Testing, security hardening, OAuth2 integrations, production support, stakeholder communication' }, + ], + experience: [ + { role: 'System Developer', org: 'Warwickshire County Council, UK', dates: '2015–2023', + note: 'The first two years were completed as an apprenticeship.', + bullets: [ + 'Worked on the county-wide highways and streetlight fault-reporting system used across Warwickshire.', + 'Designed, built and maintained full-stack applications in C#, Python, Ruby on Rails, SQL and JavaScript.', + 'Delivered reliable, well-tested software and resolved stability and performance issues on live systems.', + 'Owned deployments and production troubleshooting, and strengthened permissions and reliability.', + 'Guided colleagues on best practices for code and process.', + ] }, + { role: 'Independent Development & Norwegian Study', org: 'Tønsberg, Norway', dates: '2023–Present', + bullets: [ + 'Building and self-hosting full-stack products (see Projects) while developing Norwegian language skills.', + ] }, + ], + earlier: [ + 'Sales Representative — Royal Vapes, UK (2017–2021)', + 'Bartender — The Hodcarrier, UK (2016–2018)', + 'Receptionist — Nuffield Health, UK (2014–2015)', + ], + projects: [ + { name: 'JobTrack', desc: 'Full-stack job-application tracker (React, ASP.NET Core, SQLite, Docker) with a local AI summariser and Gmail integration.' }, + { name: 'InboxIntel', desc: 'Gmail analytics and safe bulk-cleanup tool built as Clean Architecture in .NET 8 with PostgreSQL and encrypted OAuth tokens.' }, + { name: 'Self-hosted infrastructure lab', desc: 'Ubuntu, Docker, nginx reverse proxy, authentication, monitoring and a self-hosted Gitea instance.' }, + ], + education: { qual: 'Extended Diploma — NVQ Level 3 in ICT', org: 'Warwickshire College, UK', dates: '2012–2015' }, +}; + +const NO = { + name: 'Connor Babbington', + title: 'Systemutvikler', + location: 'Tønsberg', + email: 'connor.babbington@cesnimda.co.uk', + phone: '+47 41 33 44 70', + t: { summary: 'Sammendrag', skills: 'Kjernekompetanse', experience: 'Erfaring', + earlier: 'Tidligere roller (deltid, ved siden av)', projects: 'Prosjekter', education: 'Utdanning' }, + summary: + 'Systemutvikler med åtte års erfaring med å bygge og vedlikeholde produksjonssystemer i britisk offentlig sektor. Fullstack-utvikler med tyngde på backend i C#, .NET, Python, JavaScript/TypeScript og SQL, med praktisk DevOps i Docker, Linux, CI/CD, Azure DevOps og GitHub. Jeg omsetter behov fra brukere og interessenter til pålitelige, godt testede systemer og drifter dem i produksjon. Bosatt i Tønsberg med gyldig oppholdstillatelse. Engelsk morsmål; norsk på B1-nivå og i aktiv utvikling. Åpen for remote, hybrid eller stedbaserte roller.', + skills: [ + { group: 'Utvikling', items: 'C#, .NET, Python, JavaScript, TypeScript, React, SQL' }, + { group: 'DevOps og infrastruktur', items: 'Docker, Linux, CI/CD, Azure DevOps, GitHub, nginx / reverse proxy, overvåking, egendrift' }, + { group: 'Arbeidsmåte', items: 'Testing, sikkerhet, OAuth2-integrasjoner, driftsstøtte, kommunikasjon med interessenter' }, + ], + experience: [ + { role: 'Systemutvikler', org: 'Warwickshire County Council, UK', dates: '2015–2023', + note: 'De to første årene var en lærlingperiode.', + bullets: [ + 'Jobbet på det fylkesdekkende systemet for feilmelding av veilys og veier, brukt i hele Warwickshire.', + 'Designet, bygde og vedlikeholdt fullstack-applikasjoner i C#, Python, Ruby on Rails, SQL og JavaScript.', + 'Leverte pålitelig, godt testet programvare og løste stabilitets- og ytelsesproblemer i produksjon.', + 'Hadde ansvar for utrulling og feilsøking i drift, og styrket tilganger og pålitelighet.', + 'Veiledet kollegaer i beste praksis for kode og prosess.', + ] }, + { role: 'Egen produktutvikling og norskkurs', org: 'Tønsberg', dates: '2023–nå', + bullets: [ + 'Bygger og drifter egne fullstack-produkter (se Prosjekter) samtidig som jeg utvikler norskkunnskapene.', + ] }, + ], + earlier: [ + 'Salgsrepresentant — Royal Vapes, UK (2017–2021)', + 'Bartender — The Hodcarrier, UK (2016–2018)', + 'Resepsjonist — Nuffield Health, UK (2014–2015)', + ], + projects: [ + { name: 'JobTrack', desc: 'Fullstack jobbsøknads-tracker (React, ASP.NET Core, SQLite, Docker) med lokal AI-oppsummering og Gmail-integrasjon.' }, + { name: 'InboxIntel', desc: 'Verktøy for Gmail-analyse og trygg masseopprydding, bygget som Clean Architecture i .NET 8 med PostgreSQL og krypterte OAuth-tokens.' }, + { name: 'Egendriftet hjemmelab', desc: 'Ubuntu, Docker, nginx reverse proxy, autentisering, overvåking og egen Gitea-instans.' }, + ], + education: { qual: 'Extended Diploma — NVQ nivå 3 i IKT', org: 'Warwickshire College, UK', dates: '2012–2015' }, +}; + +module.exports = { EN, NO }; diff --git a/tools/cv/package.json b/tools/cv/package.json new file mode 100644 index 0000000..2b5d64c --- /dev/null +++ b/tools/cv/package.json @@ -0,0 +1,13 @@ +{ + "name": "resumesite-cv", + "private": true, + "description": "Generates the ATS-safe CV PDFs (and editable .docx) from canonical content", + "scripts": { + "pdf": "node build-pdf.js", + "docx": "node build-cv.js" + }, + "dependencies": { + "docx": "^9.0.0", + "pdfkit": "^0.15.0" + } +}