Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 239d3dec0e | |||
| bfa8cfd357 | |||
| a10418ab93 | |||
| b4b293bdb4 | |||
| 18c556bd54 |
+3
-2
@@ -4,8 +4,9 @@
|
|||||||
{
|
{
|
||||||
"name": "site",
|
"name": "site",
|
||||||
"runtimeExecutable": "pnpm",
|
"runtimeExecutable": "pnpm",
|
||||||
"runtimeArgs": ["-C", "site", "dev", "--host", "--port", "4321"],
|
"runtimeArgs": ["-C", "site", "dev", "--host"],
|
||||||
"port": 4321
|
"port": 4321,
|
||||||
|
"autoPort": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|
||||||
# Binary assets — never touch.
|
# Binary assets — never touch.
|
||||||
|
*.docx binary
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
*.jpeg binary
|
*.jpeg binary
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ relay/appsettings.*.local.json
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
# Mission prompt scratch files (kept locally, not part of the product)
|
# Mission prompt scratch files (kept locally, not part of the product)
|
||||||
step1.txt
|
step1.txt
|
||||||
|
|||||||
+19
-11
@@ -59,19 +59,27 @@
|
|||||||
6. **Theme-init is an external script** (`/theme-init.js`) so the CSP can use `script-src
|
6. **Theme-init is an external script** (`/theme-init.js`) so the CSP can use `script-src
|
||||||
'self'` with no inline hash.
|
'self'` with no inline hash.
|
||||||
|
|
||||||
## Pre-launch checklist (carried from Phase 1/2 — owner action)
|
## Pre-launch checklist (owner action)
|
||||||
|
|
||||||
- [ ] **Replace the `/Linkedin` redirect target** in `site/nginx.conf` with the real LinkedIn
|
- [x] ~~`/Linkedin` redirect target~~ — set to `https://www.linkedin.com/in/connor-babbington`.
|
||||||
profile URL before cutover (currently a placeholder slug).
|
- [x] ~~JobTrack seed-data screenshots~~ — real captures integrated (`/assets/projects/jobtrack/`).
|
||||||
- [ ] Screenshot capture pass on **seeded demo data** for JobTrack + InboxIntel; drop real
|
- [x] ~~Real headshot~~ — integrated (`/assets/portrait.webp`, hero + about).
|
||||||
captures into the `placeholder:` media refs (`src/data/projects/*`).
|
- [x] ~~ATS-safe CV rework (EN/NO)~~ — done; generator in `tools/cv/`.
|
||||||
- [ ] Drop the real **outdoor headshot** into the portrait ref (replaces `placeholder:portrait`).
|
- [ ] **InboxIntel screenshots** — still placeholders (no captures provided; it is "in development").
|
||||||
- [ ] **ATS-safe CV rework** (EN/NO) — replace the PDFs at `site/public/cv/` (same URLs).
|
- [ ] **Native-Norwegian review** of all NO content, incl. the NO CV (launch gate).
|
||||||
- [ ] **Native-Norwegian review** of all NO content (launch gate).
|
|
||||||
- [ ] **Legacy WordPress URL inventory** → confirm the 301/410 map in `nginx.conf`.
|
- [ ] **Legacy WordPress URL inventory** → confirm the 301/410 map in `nginx.conf`.
|
||||||
- [ ] Configure CI secrets (`REGISTRY_USER`, `REGISTRY_TOKEN`) + the relay `.env` on the server.
|
- [ ] Configure CI secrets (`REGISTRY_USER`, `REGISTRY_TOKEN`) + the relay `.env` on the server.
|
||||||
|
- [ ] Confirm the Traefik entrypoint / cert-resolver names in `deploy/.env.example` match the host.
|
||||||
|
|
||||||
## Open questions / blocked-on-user
|
## Infrastructure (verified live, July 2026)
|
||||||
|
|
||||||
- Are the JobTrack / InboxIntel repos public? If so, add `repo` links in `src/data/projects/*`
|
Deploy target is the `mediaserver` host (Ubuntu 24.04): ~30 Docker services behind **Traefik**
|
||||||
(currently omitted to avoid dead links).
|
(Cloudflare-fronted, TLS, HTTP/3), with **Authentik** SSO, **CrowdSec**, **Pi-hole**, and
|
||||||
|
self-hosted **Gitea + Actions runner** (the CI/CD target for this site). The homelab case study
|
||||||
|
and `deploy/` now reflect this (Traefik labels, `traefik_proxy` network). WordPress still runs
|
||||||
|
but is being decommissioned as this site replaces it — not documented as future architecture.
|
||||||
|
|
||||||
|
## Resolved
|
||||||
|
|
||||||
|
- JobTrack / InboxIntel repos are **private** → repo links intentionally omitted; case studies
|
||||||
|
describe the architecture without linking code.
|
||||||
|
|||||||
+4
-2
@@ -14,5 +14,7 @@ RELAY_WINDOW_SECONDS=600
|
|||||||
# --- Site build ---
|
# --- Site build ---
|
||||||
PUBLIC_SITE_URL=https://cesnimda.co.uk
|
PUBLIC_SITE_URL=https://cesnimda.co.uk
|
||||||
|
|
||||||
# --- Infra ---
|
# --- Infra (Traefik host reverse proxy) ---
|
||||||
PROXY_NETWORK=web # name of the existing external reverse-proxy docker network
|
PROXY_NETWORK=traefik_proxy # existing external Traefik docker network
|
||||||
|
SITE_HOST=cesnimda.co.uk # Host rule for the Traefik routers
|
||||||
|
TRAEFIK_ENTRYPOINT=websecure-external # Traefik entrypoint name on the host
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Production stack (DOCKER_SPEC §2). Publishes no host ports — the existing host
|
# Production stack (DOCKER_SPEC §2). Publishes no host ports — Traefik (the host's
|
||||||
# reverse proxy routes cesnimda.co.uk -> site:8080 and /api/contact -> relay:8081
|
# existing reverse proxy) discovers these containers on the shared traefik_proxy
|
||||||
# over the shared external proxy network.
|
# network via the labels below and routes cesnimda.co.uk to the site, and
|
||||||
|
# cesnimda.co.uk/api/contact to the relay. Entrypoint / cert-resolver names match
|
||||||
|
# the host Traefik convention; override via the .env values if they differ.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
site:
|
site:
|
||||||
@@ -14,7 +16,14 @@ services:
|
|||||||
- /tmp
|
- /tmp
|
||||||
- /var/cache/nginx
|
- /var/cache/nginx
|
||||||
- /var/run
|
- /var/run
|
||||||
networks: [web]
|
networks: [proxy]
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=${PROXY_NETWORK:-traefik_proxy}
|
||||||
|
- traefik.http.routers.resumesite.rule=Host(`${SITE_HOST:-cesnimda.co.uk}`)
|
||||||
|
- traefik.http.routers.resumesite.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure-external}
|
||||||
|
- traefik.http.routers.resumesite.tls=true
|
||||||
|
- traefik.http.services.resumesite.loadbalancer.server.port=8080
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options: { max-size: '10m', max-file: '3' }
|
options: { max-size: '10m', max-file: '3' }
|
||||||
@@ -36,12 +45,20 @@ services:
|
|||||||
- Relay__AllowedOrigin=${RELAY_ALLOWED_ORIGIN:-https://cesnimda.co.uk}
|
- Relay__AllowedOrigin=${RELAY_ALLOWED_ORIGIN:-https://cesnimda.co.uk}
|
||||||
- Relay__RateLimitPerWindow=${RELAY_RATE_LIMIT:-5}
|
- Relay__RateLimitPerWindow=${RELAY_RATE_LIMIT:-5}
|
||||||
- Relay__WindowSeconds=${RELAY_WINDOW_SECONDS:-600}
|
- Relay__WindowSeconds=${RELAY_WINDOW_SECONDS:-600}
|
||||||
networks: [web]
|
networks: [proxy]
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.docker.network=${PROXY_NETWORK:-traefik_proxy}
|
||||||
|
# More specific rule than the site router, so /api/contact wins.
|
||||||
|
- traefik.http.routers.resumerelay.rule=Host(`${SITE_HOST:-cesnimda.co.uk}`) && PathPrefix(`/api/contact`)
|
||||||
|
- traefik.http.routers.resumerelay.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure-external}
|
||||||
|
- traefik.http.routers.resumerelay.tls=true
|
||||||
|
- traefik.http.services.resumerelay.loadbalancer.server.port=8081
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options: { max-size: '10m', max-file: '3' }
|
options: { max-size: '10m', max-file: '3' }
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
proxy:
|
||||||
external: true
|
external: true
|
||||||
name: ${PROXY_NETWORK:-web}
|
name: ${PROXY_NETWORK:-traefik_proxy}
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
# Master Career Profile — Connor Babbington
|
||||||
|
|
||||||
|
**Single source of truth** for generating CVs, cover letters, LinkedIn content, bios and
|
||||||
|
other career materials. Everything here is factual and verified against the CV, the
|
||||||
|
portfolio site content, the project repositories, and a live inspection of the
|
||||||
|
infrastructure (July 2026). When generating any artifact, pull from this document and
|
||||||
|
follow the **Voice & usage guide** at the end.
|
||||||
|
|
||||||
|
Related sources: [`design/02-ux/CANONICAL_CONTENT.md`](design/02-ux/CANONICAL_CONTENT.md)
|
||||||
|
(original CV facts) · site content in `site/src/data/` · CV generator in `tools/cv/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Identity & contact
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Name | Connor Babbington |
|
||||||
|
| Title (self-label) | **Systems Developer** (NO: *Systemutvikler*) |
|
||||||
|
| Location | Tønsberg, Norway |
|
||||||
|
| Work eligibility | Valid Norwegian residence permit |
|
||||||
|
| E-mail | connor.babbington@cesnimda.co.uk |
|
||||||
|
| Phone | +47 41 33 44 70 |
|
||||||
|
| Website | https://cesnimda.co.uk |
|
||||||
|
| LinkedIn | https://www.linkedin.com/in/connor-babbington |
|
||||||
|
| Git (self-hosted) | https://git.cesnimda.uk/cesnimda |
|
||||||
|
| Languages | English (native) · Norwegian (B1, actively developing) |
|
||||||
|
| Availability | Open to remote, hybrid or on-site developer roles |
|
||||||
|
|
||||||
|
## 2. Positioning
|
||||||
|
|
||||||
|
**Core positioning:** a systems developer with **eight years' experience** building and
|
||||||
|
maintaining production software for UK local government, now building and self-hosting
|
||||||
|
full-stack products in Norway. Backend-leaning full-stack, with genuine DevOps and
|
||||||
|
operations depth.
|
||||||
|
|
||||||
|
- **Never** self-label "senior" or "mid-level". Lead with "eight years' experience" and
|
||||||
|
let the evidence (production systems, security decisions, operations) carry seniority.
|
||||||
|
- Emphasis differs by audience: for Norwegian public/SME employers, foreground the
|
||||||
|
**public-sector systems background** and **operations/security**; for product companies,
|
||||||
|
foreground **full-stack delivery** and the **case studies**.
|
||||||
|
|
||||||
|
**Headline options (LinkedIn / CV subtitle):**
|
||||||
|
- `Systems Developer · .NET, full-stack & infrastructure · Tønsberg, Norway`
|
||||||
|
- `Systems Developer — 8 years building & running production software`
|
||||||
|
- `Backend-leaning full-stack developer (.NET, React, Docker) · self-hosting enthusiast`
|
||||||
|
- NO: `Systemutvikler · .NET, fullstack og infrastruktur · Tønsberg`
|
||||||
|
|
||||||
|
## 3. Professional summary (three lengths)
|
||||||
|
|
||||||
|
**One line:**
|
||||||
|
> Systems developer with eight years' experience building, shipping and running
|
||||||
|
> production software.
|
||||||
|
|
||||||
|
**Short (2–3 sentences):**
|
||||||
|
> Systems developer with eight years' experience delivering internal software for UK local
|
||||||
|
> government. Backend-leaning full-stack across C#, .NET, Python, JavaScript/TypeScript and
|
||||||
|
> SQL, with hands-on DevOps in Docker, Linux and CI/CD. Now building and self-hosting
|
||||||
|
> full-stack products in Norway.
|
||||||
|
|
||||||
|
**Paragraph:**
|
||||||
|
> 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 — and I care about the parts users never see: tests,
|
||||||
|
> deployments and keeping things running. Alongside professional work I design, build and
|
||||||
|
> self-host my own products and a ~30-service home infrastructure lab, which keeps my skills
|
||||||
|
> current and hands-on. Based in Tønsberg with a valid residence permit; native English
|
||||||
|
> speaker with Norwegian at B1 and actively developing.
|
||||||
|
|
||||||
|
**Norwegian (paragraph):**
|
||||||
|
> 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. Ved siden av jobb bygger og drifter jeg egne
|
||||||
|
> produkter og en hjemmelab med rundt tretti tjenester. Bosatt i Tønsberg med gyldig
|
||||||
|
> oppholdstillatelse; engelsk morsmål, norsk på B1-nivå og i aktiv utvikling.
|
||||||
|
|
||||||
|
## 4. Skills taxonomy
|
||||||
|
|
||||||
|
Grouped for CV/LinkedIn. Every item is defensible in an interview — don't pad.
|
||||||
|
|
||||||
|
**Languages:** C#, Python, JavaScript, TypeScript, SQL, Ruby (Ruby on Rails, from the
|
||||||
|
council role). *(HTML/CSS assumed.)*
|
||||||
|
|
||||||
|
**Frameworks & platforms:** .NET / ASP.NET Core, React, FastAPI, Ruby on Rails, Astro,
|
||||||
|
Entity Framework Core, Node.js.
|
||||||
|
|
||||||
|
**Data:** SQL databases, PostgreSQL, SQLite, MariaDB, Redis, EF Core.
|
||||||
|
|
||||||
|
**DevOps & infrastructure:** Docker & Docker Compose, Linux (Ubuntu), CI/CD, Azure DevOps,
|
||||||
|
GitHub, Gitea + Actions runners, Traefik, nginx, reverse proxies, Cloudflare, self-hosting,
|
||||||
|
container orchestration.
|
||||||
|
|
||||||
|
**Security & identity:** OAuth2 integrations, encrypted secrets at rest (Data Protection
|
||||||
|
API), Authentik (SSO / forward-auth), CrowdSec (intrusion prevention), Pi-hole (DNS),
|
||||||
|
security hardening, preview-then-confirm destructive operations.
|
||||||
|
|
||||||
|
**AI / integrations:** local LLMs via Ollama, AI-assisted summarisation, Gmail API (OAuth2).
|
||||||
|
|
||||||
|
**Architecture & practices:** Clean Architecture, full-stack design, REST APIs, background
|
||||||
|
workers, testing (unit + integration), production support & troubleshooting, stakeholder
|
||||||
|
communication, cross-functional collaboration, WCAG accessibility, SEO.
|
||||||
|
|
||||||
|
**Tools:** Git, Portainer, Serilog, Polly, QuestPDF, Chart.js, Playwright, Vitest.
|
||||||
|
|
||||||
|
## 5. Experience
|
||||||
|
|
||||||
|
### System Developer — Warwickshire County Council, UK · 2015–2023
|
||||||
|
*(First two years completed as an apprenticeship; progressed to full developer.)*
|
||||||
|
- 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 across several projects.
|
||||||
|
- Delivered reliable, well-tested software; resolved stability and performance issues on
|
||||||
|
live systems.
|
||||||
|
- Owned **deployments and production troubleshooting**; strengthened permissions and
|
||||||
|
reliability.
|
||||||
|
- Produced clean, maintainable, documented code; **guided colleagues** on best practices for
|
||||||
|
code and process.
|
||||||
|
- Context for framing: UK **local government / public sector** — maps directly to Norwegian
|
||||||
|
*kommune/fylkeskommune* systems work.
|
||||||
|
|
||||||
|
### Independent Development & Norwegian Study — Tønsberg, Norway · 2023–Present
|
||||||
|
- Building and self-hosting full-stack products (JobTrack, InboxIntel) and operating a
|
||||||
|
~30-service home infrastructure lab.
|
||||||
|
- Developing Norwegian language skills (norskkurs) toward professional fluency.
|
||||||
|
|
||||||
|
### Earlier roles (part-time, alongside the above; customer-facing)
|
||||||
|
- Sales Representative — Royal Vapes, UK · 2017–2021
|
||||||
|
- Bartender — The Hodcarrier, UK · 2016–2018
|
||||||
|
- Receptionist — Nuffield Health, UK · 2014–2015
|
||||||
|
|
||||||
|
*Framing:* held alongside the council role; evidence of communication skills and work ethic.
|
||||||
|
De-emphasise on technical CVs; a single line usually suffices.
|
||||||
|
|
||||||
|
## 6. Projects (portfolio)
|
||||||
|
|
||||||
|
### JobTrack — full-stack job-application tracker · *Active*
|
||||||
|
- **What:** a workspace for the whole job-application journey — pipeline, correspondence,
|
||||||
|
attachments, AI summaries, reminders and analytics.
|
||||||
|
- **Stack:** React + TypeScript SPA · ASP.NET Core (.NET 9) API · SQLite (EF Core) · FastAPI +
|
||||||
|
Ollama local AI service · Docker Compose · installable PWA with a mobile share-target.
|
||||||
|
- **Key decisions:** local AI (Ollama) over a cloud API for privacy and zero per-call cost;
|
||||||
|
**no offline PWA cache by design** (frequent deploys → stale builds are worse than offline
|
||||||
|
gaps); SQLite over Postgres for a single-user app.
|
||||||
|
- **Security/production:** optional Google sign-in (ID tokens), per-owner record scoping,
|
||||||
|
validated file uploads with ownership checks, advisory-only AI, scheduled JSON/CSV exports.
|
||||||
|
- **Talking points:** product thinking end-to-end; a deliberate anti-feature with reasoning;
|
||||||
|
integration depth (Gmail OAuth2).
|
||||||
|
|
||||||
|
### InboxIntel — Gmail analytics & safe bulk cleanup · *In development*
|
||||||
|
- **What:** Gmail analytics, cleanup and unsubscribe management with a draggable dashboard.
|
||||||
|
- **Stack:** .NET 8 · Clean Architecture (Domain → Application → Infrastructure → Api) ·
|
||||||
|
PostgreSQL (EF Core + Npgsql) · React 18 + Vite + Chart.js · hosted background sync worker ·
|
||||||
|
Serilog · FluentValidation · Polly · Gmail API.
|
||||||
|
- **Key decisions:** four-project Clean Architecture with an enforced dependency rule;
|
||||||
|
**OAuth refresh tokens encrypted at rest** (ASP.NET Data Protection API / AES, never
|
||||||
|
logged); **every destructive action is preview-then-confirm**; advisory-only AI.
|
||||||
|
- **Security/production:** Gmail read/modify scopes only (no send); Polly retry/backoff;
|
||||||
|
unit tests (query parser, unsubscribe extraction) + integration tests asserting authz.
|
||||||
|
- **Talking points:** architecture discipline; safe-by-design destructive operations.
|
||||||
|
|
||||||
|
### Self-hosted infrastructure lab · *Active*
|
||||||
|
- **What:** an Ubuntu 24.04 server running **~30 Docker services** behind a single reverse
|
||||||
|
proxy — the deploy target for the portfolio site itself.
|
||||||
|
- **Platform:** **Traefik** (TLS, HTTP/3, Cloudflare-fronted, separate internal/external
|
||||||
|
entrypoints) · **Authentik** SSO as forward-auth in front of internal tools · **CrowdSec**
|
||||||
|
intrusion prevention · **Pi-hole** DNS filtering.
|
||||||
|
- **CI/CD & data:** self-hosted **Gitea + Actions runner** (git.cesnimda.uk) building and
|
||||||
|
deploying this site; PostgreSQL, MariaDB, Redis; Portainer + Dozzle for management/logs.
|
||||||
|
- **Security posture:** Docker socket exposed only via a hardened socket-proxy; each app on
|
||||||
|
its own isolated Docker network.
|
||||||
|
- **Also runs:** application containers (JobTrack) and a media stack (Jellyfin + *arr).
|
||||||
|
- **Talking points:** real operations and security experience, not just deployment; network
|
||||||
|
segmentation; SSO/IPS/DNS in a home setting; runs its own CI/CD.
|
||||||
|
|
||||||
|
### The portfolio site itself (cesnimda.co.uk)
|
||||||
|
- Astro 5 static, bilingual (EN/NO), Tailwind 4 over design tokens, CSS-first motion, a
|
||||||
|
slug-map i18n contract, a small .NET 9 contact relay, WCAG AA accessibility, Lighthouse
|
||||||
|
budgets, and full CI — self-hosted in Docker behind Traefik. A work sample in its own right.
|
||||||
|
|
||||||
|
## 7. Education
|
||||||
|
|
||||||
|
- **Extended Diploma — NVQ Level 3 in ICT**, Warwickshire College, UK · 2012–2015.
|
||||||
|
Professional education in IT: programming, systems administration and IT support.
|
||||||
|
|
||||||
|
## 8. Achievement / evidence library (bullet bank)
|
||||||
|
|
||||||
|
Reusable, outcome-oriented bullets for CVs and cover letters:
|
||||||
|
- Delivered and maintained internal software used across a UK county council for eight years.
|
||||||
|
- Contributed to a **county-wide highways/streetlight fault-reporting system**.
|
||||||
|
- Replaced spreadsheet-heavy and manual workflows with reliable internal systems.
|
||||||
|
- Built full-stack applications in five languages (C#, Python, Ruby on Rails, SQL, JS),
|
||||||
|
adapting across frameworks as projects changed.
|
||||||
|
- Owned deployments and production troubleshooting on live systems.
|
||||||
|
- Designed and shipped two full-stack products end-to-end (product, backend, frontend, ops,
|
||||||
|
security).
|
||||||
|
- Made and documented real engineering trade-offs (local vs cloud AI; deliberate anti-feature;
|
||||||
|
Clean Architecture; encrypted secrets at rest).
|
||||||
|
- Operate a ~30-service self-hosted infrastructure with SSO, intrusion prevention and CI/CD.
|
||||||
|
- Built an accessible (WCAG AA), performant, bilingual site with automated testing and CI.
|
||||||
|
|
||||||
|
## 9. Working style & soft skills
|
||||||
|
|
||||||
|
Problem solving · troubleshooting · system improvement · stakeholder communication ·
|
||||||
|
production support · cross-functional collaboration · mentoring/guiding colleagues ·
|
||||||
|
comfortable with both structured and open-ended tasks · effective independently and in a
|
||||||
|
team · takes initiative · dependable and detail-oriented.
|
||||||
|
|
||||||
|
## 10. Interests
|
||||||
|
|
||||||
|
PC and board games (strategic thinking, problem solving), cooking, and continually learning
|
||||||
|
new skills. Keep to one line on formal documents.
|
||||||
|
|
||||||
|
## 11. Ready-made assets
|
||||||
|
|
||||||
|
**Elevator pitch (spoken, ~20s):**
|
||||||
|
> I'm a systems developer with eight years building and running production software for UK
|
||||||
|
> local government — backend-leaning full-stack in .NET, plus real DevOps. I moved to Norway
|
||||||
|
> and I've been shipping my own full-stack products and running a fairly serious home
|
||||||
|
> infrastructure lab. I'm looking for a developer role where I can build things and help keep
|
||||||
|
> them running.
|
||||||
|
|
||||||
|
**LinkedIn headline:**
|
||||||
|
> Systems Developer · .NET, full-stack & infrastructure · 8 years · Tønsberg, Norway
|
||||||
|
|
||||||
|
**LinkedIn "About":**
|
||||||
|
> Systems developer with eight years' experience building and maintaining production software
|
||||||
|
> for UK local government. Backend-leaning full-stack across C#, .NET, Python, JavaScript/
|
||||||
|
> TypeScript and SQL, with hands-on DevOps in Docker, Linux and CI/CD. I like turning real
|
||||||
|
> requirements into reliable, well-tested systems — and I care about the parts users never
|
||||||
|
> see: tests, deployments and keeping things running.
|
||||||
|
>
|
||||||
|
> Outside work I design, build and self-host my own products (a full-stack job-application
|
||||||
|
> tracker and a Gmail analytics tool) and run a ~30-service home infrastructure lab with
|
||||||
|
> Traefik, single sign-on, intrusion prevention and self-hosted CI/CD. Based in Tønsberg with
|
||||||
|
> a valid residence permit; native English, Norwegian at B1 and improving. Open to remote,
|
||||||
|
> hybrid or on-site developer roles.
|
||||||
|
|
||||||
|
**Short bio (third person, ~40 words):**
|
||||||
|
> Connor Babbington is a systems developer with eight years' experience building production
|
||||||
|
> software for UK local government. Backend-leaning full-stack in .NET with real DevOps
|
||||||
|
> depth, he now builds and self-hosts his own products from Tønsberg, Norway.
|
||||||
|
|
||||||
|
**Cover-letter opener (template):**
|
||||||
|
> I'm a systems developer with eight years' experience delivering and running production
|
||||||
|
> software, and I'm writing about the [ROLE] position at [COMPANY]. [ONE SENTENCE connecting a
|
||||||
|
> specific project/skill to their need.] I turn requirements into reliable, well-tested
|
||||||
|
> systems and I'm comfortable owning them in production.
|
||||||
|
|
||||||
|
## 12. Voice & usage guide
|
||||||
|
|
||||||
|
**Tone:** plain-spoken, evidence-led, understated. Calibrated for the Norwegian market —
|
||||||
|
avoid grandiosity (no "rockstar/ninja/guru", no "senior" unless a specific role warrants it).
|
||||||
|
Let concrete facts carry weight.
|
||||||
|
|
||||||
|
**When generating an artifact:**
|
||||||
|
- **CV:** use §3 short summary + §4 skills + §5 experience + §6 projects (2 lines each) + §7
|
||||||
|
education. Keep to one page; ATS-safe single column; the generator in `tools/cv/` already
|
||||||
|
encodes this. Positioning = "eight years", never "mid-level".
|
||||||
|
- **Cover letter:** open with §11 template; pick 2–3 bullets from §8 that match the job;
|
||||||
|
close on availability + permit + location fit.
|
||||||
|
- **LinkedIn:** headline + About from §11; Experience from §5; Skills from §4.
|
||||||
|
- **Bio:** pick the length from §11 (or §3).
|
||||||
|
- **Norwegian materials:** use the native NO summary in §3; have a fluent speaker review
|
||||||
|
before sending (the NO here is solid but not yet native-reviewed).
|
||||||
|
|
||||||
|
**Facts to never overstate:** don't claim "senior"; don't invent user numbers (say
|
||||||
|
"county-wide"); InboxIntel is "in development"; project repos are **private** (describe, don't
|
||||||
|
link); Norwegian is **B1**.
|
||||||
|
|
||||||
|
**Maintenance:** update this document when facts change (new role, project, certification,
|
||||||
|
language level), then regenerate downstream materials from it. Keep it the single source of
|
||||||
|
truth — edit here first, propagate outward.
|
||||||
@@ -7,7 +7,7 @@ System architecture for the bilingual portfolio. Companion detail specs: ROUTING
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────── Connor's server ───────────────────────────┐
|
┌──────────────────────────── Connor's server ───────────────────────────┐
|
||||||
Browser ── HTTPS ──► │ nginx (existing reverse proxy, TLS) │
|
Browser ── HTTPS ──► │ Traefik (existing host reverse proxy, TLS; Cloudflare-fronted) │
|
||||||
│ ├── cesnimda.co.uk/ → [site] nginx container (static) │
|
│ ├── cesnimda.co.uk/ → [site] nginx container (static) │
|
||||||
│ ├── cesnimda.co.uk/api/contact → [contact-relay] .NET 9 container │
|
│ ├── cesnimda.co.uk/api/contact → [contact-relay] .NET 9 container │
|
||||||
│ └── git.cesnimda.uk → Gitea (existing) │
|
│ └── git.cesnimda.uk → Gitea (existing) │
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ push / PR to main
|
|||||||
|
|
||||||
1. Pre-launch: staging fully verified (both locales, CV downloads, form → real mailbox, redirects incl. `/Linkedin`, 410 map, OG previews via LinkedIn/Slack debuggers, Search Console fetch).
|
1. Pre-launch: staging fully verified (both locales, CV downloads, form → real mailbox, redirects incl. `/Linkedin`, 410 map, OG previews via LinkedIn/Slack debuggers, Search Console fetch).
|
||||||
2. URL inventory + redirect/410 map finalised (SEO_SPEC §6).
|
2. URL inventory + redirect/410 map finalised (SEO_SPEC §6).
|
||||||
3. Cutover = repoint host nginx server-block from WP to the `site` container (single reload; WP left running dark for 2 weeks as instant rollback).
|
3. Cutover = move the `cesnimda.co.uk` Traefik Host router from the WordPress container to the `site` container (a label change + `compose up -d`; WP left running dark for 2 weeks as instant rollback).
|
||||||
4. Post-launch checklist: submit sitemaps, watch Search Console coverage + relay logs for form spam patterns, then decommission WP + its PHP/MySQL surface (attack-surface win worth noting on the colophon).
|
4. Post-launch checklist: submit sitemaps, watch Search Console coverage + relay logs for form spam patterns, then decommission WP + its PHP/MySQL surface (attack-surface win worth noting on the colophon).
|
||||||
|
|
||||||
## 4. Operational guardrails
|
## 4. Operational guardrails
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ deploy/
|
|||||||
.env.example # documented variables, no real values
|
.env.example # documented variables, no real values
|
||||||
```
|
```
|
||||||
|
|
||||||
- Prod compose joins the server's existing reverse-proxy network; the host nginx (existing, TLS-terminating) routes `cesnimda.co.uk` → `site:8080` and `cesnimda.co.uk/api/contact` → `contact-relay:8081`. The stack publishes **no host ports** itself.
|
- Prod compose joins the server's existing Traefik network (`traefik_proxy`); the host Traefik (existing, TLS-terminating, Cloudflare-fronted) discovers the containers via labels and routes `cesnimda.co.uk` → `site:8080` and `cesnimda.co.uk/api/contact` → `relay:8081` (the relay's `PathPrefix` rule outranks the site's `Host` rule). The stack publishes **no host ports** itself.
|
||||||
- `restart: unless-stopped`, log rotation via compose logging options, images referenced by registry tag + digest.
|
- `restart: unless-stopped`, log rotation via compose logging options, images referenced by registry tag + digest.
|
||||||
- Optional third service slot (`analytics`, self-hosted Umami/Plausible) reserved in the compose file as a commented profile — decision deferred.
|
- Optional third service slot (`analytics`, self-hosted Umami/Plausible) reserved in the compose file as a commented profile — decision deferred.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
dist/
|
dist/
|
||||||
.astro/
|
.astro/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.pnpm-store/
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
public/
|
public/
|
||||||
*.pdf
|
*.pdf
|
||||||
|
|||||||
+2
-2
@@ -27,9 +27,9 @@ server {
|
|||||||
gzip_proxied any;
|
gzip_proxied any;
|
||||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml application/xml+rss;
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml application/xml application/xml+rss;
|
||||||
|
|
||||||
# --- Preserved LinkedIn redirect (printed on the CVs). Replace slug before cutover. ---
|
# --- Preserved LinkedIn redirect (printed on the CVs). ---
|
||||||
location = /Linkedin {
|
location = /Linkedin {
|
||||||
return 301 https://www.linkedin.com/in/REPLACE-WITH-REAL-SLUG/;
|
return 301 https://www.linkedin.com/in/connor-babbington;
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Legacy WordPress URLs -> 410 Gone (crawler cleanup) ---
|
# --- Legacy WordPress URLs -> 410 Gone (crawler cleanup) ---
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Binary file not shown.
@@ -34,7 +34,7 @@ const copy = {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Self-hosted',
|
'Self-hosted',
|
||||||
'Built and deployed from my own Gitea instance to Docker behind nginx. The contact form is a small stateless .NET service — even that is part of my own stack.',
|
'Built and deployed from my own Gitea instance to Docker behind Traefik. The contact form is a small stateless .NET service — even that is part of my own stack.',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'No tracking',
|
'No tracking',
|
||||||
@@ -59,7 +59,7 @@ const copy = {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Egendriftet',
|
'Egendriftet',
|
||||||
'Bygget og rullet ut fra min egen Gitea-instans til Docker bak nginx. Kontaktskjemaet er en liten tilstandsløs .NET-tjeneste — også den er en del av min egen stack.',
|
'Bygget og rullet ut fra min egen Gitea-instans til Docker bak Traefik. Kontaktskjemaet er en liten tilstandsløs .NET-tjeneste — også den er en del av min egen stack.',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'Ingen sporing',
|
'Ingen sporing',
|
||||||
|
|||||||
@@ -30,9 +30,9 @@ const currentLabel = locale === 'en' ? d.cv.english : d.cv.norsk;
|
|||||||
{d.cv.download}
|
{d.cv.download}
|
||||||
{!compact && <span class="text-mono-label ml-2 font-mono opacity-70">EN/NO</span>}
|
{!compact && <span class="text-mono-label ml-2 font-mono opacity-70">EN/NO</span>}
|
||||||
</a>
|
</a>
|
||||||
<details class="cv-menu">
|
<details class="cv-menu flex">
|
||||||
<summary
|
<summary
|
||||||
class="border-accent-ink/20 bg-accent text-accent-ink flex cursor-pointer list-none items-center rounded-r-sm border-l px-2 transition-[filter] duration-[--dur-quick] hover:brightness-105"
|
class="border-accent-ink/20 bg-accent text-accent-ink flex h-full cursor-pointer list-none items-center rounded-r-sm border-l px-2 transition-[filter] duration-[--dur-quick] hover:brightness-105"
|
||||||
aria-label={d.cv.download}
|
aria-label={d.cv.download}
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ export const profile: Profile = {
|
|||||||
gitea: 'https://git.cesnimda.uk/cesnimda',
|
gitea: 'https://git.cesnimda.uk/cesnimda',
|
||||||
},
|
},
|
||||||
cv: {
|
cv: {
|
||||||
en: { path: '/cv/connor-babbington-cv-en.pdf', sizeKb: 132, 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: 172, updated: '2026-07' },
|
no: { path: '/cv/connor-babbington-cv-no.pdf', sizeKb: 8, updated: '2026-07' },
|
||||||
},
|
},
|
||||||
photo: {
|
photo: {
|
||||||
src: 'placeholder:portrait',
|
src: '/assets/portrait.webp',
|
||||||
alt: {
|
alt: {
|
||||||
en: 'Connor Babbington, smiling, outdoors',
|
en: 'Connor Babbington, smiling, outdoors',
|
||||||
no: 'Connor Babbington, smilende, utendørs',
|
no: 'Connor Babbington, smilende, utendørs',
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { Project } from '@lib/schema';
|
import type { Project } from '@lib/schema';
|
||||||
|
|
||||||
/* Homelab — capability page (lighter template): no TL;DR/decisions/gallery rendered. */
|
/*
|
||||||
|
Homelab — capability page (lighter template): no TL;DR/decisions/gallery rendered.
|
||||||
|
Content reflects the real setup inspected on the host (Ubuntu 24.04, ~30 Docker
|
||||||
|
services behind Traefik with Authentik SSO, CrowdSec, Pi-hole, Gitea CI).
|
||||||
|
*/
|
||||||
export const homelab: Project = {
|
export const homelab: Project = {
|
||||||
id: 'homelab',
|
id: 'homelab',
|
||||||
name: 'Self-hosted infrastructure lab',
|
name: 'Self-hosted infrastructure lab',
|
||||||
@@ -10,88 +14,100 @@ export const homelab: Project = {
|
|||||||
stack: [
|
stack: [
|
||||||
{ name: 'Ubuntu' },
|
{ name: 'Ubuntu' },
|
||||||
{ name: 'Docker' },
|
{ name: 'Docker' },
|
||||||
{ name: 'nginx' },
|
{ name: 'Traefik' },
|
||||||
{ name: 'Gitea' },
|
{ name: 'Authentik' },
|
||||||
{ name: 'Monitoring' },
|
{ name: 'CrowdSec' },
|
||||||
{ name: 'Backups' },
|
{ name: 'Gitea CI' },
|
||||||
],
|
],
|
||||||
links: [],
|
links: [],
|
||||||
diagram: {
|
diagram: {
|
||||||
viewBox: '0 0 1120 300',
|
viewBox: '0 0 1120 310',
|
||||||
title: { en: 'Homelab topology', no: 'Hjemmelab-topologi' },
|
title: { en: 'Homelab topology', no: 'Hjemmelab-topologi' },
|
||||||
desc: {
|
desc: {
|
||||||
en: 'Inbound traffic reaches an nginx reverse proxy terminating TLS, which routes to self-hosted services: Gitea, application containers and a monitoring stack.',
|
en: 'Traffic arrives through Cloudflare to a Traefik reverse proxy that terminates TLS and applies Authentik single sign-on, then routes to self-hosted services: Gitea and its CI runner, application containers, and a media stack.',
|
||||||
no: 'Innkommende trafikk når en nginx reverse proxy som terminerer TLS, og rutes videre til egendriftede tjenester: Gitea, applikasjonscontainere og en overvåkingsstack.',
|
no: 'Trafikk kommer via Cloudflare til en Traefik reverse proxy som terminerer TLS og bruker Authentik single sign-on, og rutes videre til egendriftede tjenester: Gitea med CI-runner, applikasjonscontainere og en mediastack.',
|
||||||
},
|
},
|
||||||
nodes: [
|
nodes: [
|
||||||
{
|
{
|
||||||
id: 'internet',
|
id: 'cloudflare',
|
||||||
x: 40,
|
x: 40,
|
||||||
y: 116,
|
y: 124,
|
||||||
w: 160,
|
w: 170,
|
||||||
h: 72,
|
h: 68,
|
||||||
kind: 'external',
|
kind: 'external',
|
||||||
label: 'Internet',
|
label: 'Cloudflare',
|
||||||
sub: { en: 'HTTPS', no: 'HTTPS' },
|
sub: { en: 'DNS · edge · WAF', no: 'DNS · edge · WAF' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'proxy',
|
id: 'traefik',
|
||||||
x: 280,
|
x: 290,
|
||||||
y: 116,
|
y: 124,
|
||||||
w: 190,
|
w: 180,
|
||||||
h: 72,
|
h: 68,
|
||||||
kind: 'primary',
|
kind: 'primary',
|
||||||
label: 'nginx',
|
label: 'Traefik',
|
||||||
sub: { en: 'reverse proxy · TLS', no: 'reverse proxy · TLS' },
|
sub: { en: 'reverse proxy · TLS', no: 'reverse proxy · TLS' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'authentik',
|
||||||
|
x: 290,
|
||||||
|
y: 232,
|
||||||
|
w: 180,
|
||||||
|
h: 56,
|
||||||
|
kind: 'internal',
|
||||||
|
label: 'Authentik',
|
||||||
|
sub: { en: 'SSO · forward-auth', no: 'SSO · forward-auth' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'gitea',
|
id: 'gitea',
|
||||||
x: 560,
|
x: 560,
|
||||||
y: 40,
|
y: 44,
|
||||||
w: 190,
|
w: 210,
|
||||||
h: 60,
|
h: 60,
|
||||||
kind: 'internal',
|
kind: 'internal',
|
||||||
label: 'Gitea',
|
label: 'Gitea + runner',
|
||||||
sub: { en: 'git.cesnimda.uk', no: 'git.cesnimda.uk' },
|
sub: { en: 'git.cesnimda.uk · CI/CD', no: 'git.cesnimda.uk · CI/CD' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'apps',
|
id: 'apps',
|
||||||
x: 560,
|
x: 560,
|
||||||
y: 120,
|
y: 128,
|
||||||
w: 190,
|
w: 210,
|
||||||
h: 60,
|
h: 60,
|
||||||
kind: 'internal',
|
kind: 'internal',
|
||||||
label: 'App services',
|
label: 'App containers',
|
||||||
sub: { en: 'Docker containers', no: 'Docker-containere' },
|
sub: { en: 'JobTrack · this site', no: 'JobTrack · denne siden' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'monitor',
|
id: 'media',
|
||||||
x: 560,
|
x: 560,
|
||||||
y: 200,
|
y: 212,
|
||||||
w: 190,
|
w: 210,
|
||||||
h: 60,
|
h: 60,
|
||||||
kind: 'internal',
|
kind: 'internal',
|
||||||
label: 'Monitoring',
|
label: 'Media stack',
|
||||||
sub: { en: 'uptime · logs', no: 'oppetid · logger' },
|
sub: { en: 'Jellyfin · *arr', no: 'Jellyfin · *arr' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
{ d: 'M200 152 H280', kind: 'flow' },
|
{ d: 'M210 158 H290', kind: 'flow' },
|
||||||
{ d: 'M470 140 L560 70', kind: 'flow' },
|
{ d: 'M380 192 V232', kind: 'flow', label: 'auth', labelX: 400, labelY: 218 },
|
||||||
{ d: 'M470 152 H560', kind: 'flow' },
|
{ d: 'M470 150 L560 74', kind: 'flow' },
|
||||||
{ d: 'M470 164 L560 230', kind: 'flow' },
|
{ d: 'M470 158 H560', kind: 'flow' },
|
||||||
|
{ d: 'M470 166 L560 242', kind: 'flow' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
media: [],
|
media: [],
|
||||||
content: {
|
content: {
|
||||||
en: {
|
en: {
|
||||||
valueProp: 'Where I run, break and fix real infrastructure — not just deploy it.',
|
valueProp:
|
||||||
|
'Where I run, break and fix real infrastructure — around thirty containerised services behind a single reverse proxy.',
|
||||||
cardTeaser:
|
cardTeaser:
|
||||||
'Ubuntu · Docker services · reverse proxy · auth · monitoring · Gitea (this site’s own git remote).',
|
'Ubuntu + Docker · Traefik · Authentik SSO · CrowdSec · Gitea CI — ~30 self-hosted services, and the deploy target for this site.',
|
||||||
tldr: {
|
tldr: {
|
||||||
what: 'A self-hosted home lab running production-style services.',
|
what: 'A self-hosted home server running ~30 production-style services.',
|
||||||
why: 'To keep operations skills hands-on.',
|
why: 'To keep operations and security skills hands-on.',
|
||||||
stack: 'Ubuntu · Docker · nginx · Gitea · monitoring.',
|
stack: 'Ubuntu · Docker · Traefik · Authentik · CrowdSec · Gitea.',
|
||||||
role: 'Operator.',
|
role: 'Operator.',
|
||||||
},
|
},
|
||||||
sections: [
|
sections: [
|
||||||
@@ -102,7 +118,7 @@ export const homelab: Project = {
|
|||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'p',
|
||||||
text: 'A home lab running Ubuntu and Docker-based services behind an nginx reverse proxy, with authentication, monitoring and backups. It’s where I get ongoing, hands-on practice with deployment, troubleshooting and real-world operations — including the self-hosted Gitea instance that this very site is built and deployed from.',
|
text: 'An Ubuntu 24.04 server running around thirty Docker services behind a Traefik reverse proxy, with single sign-on, an intrusion-prevention layer, DNS filtering and a full CI/CD setup. It’s where I get ongoing, hands-on practice with deployment, security and real-world operations — including the self-hosted Gitea instance and runner that build and deploy this very portfolio.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -114,10 +130,11 @@ export const homelab: Project = {
|
|||||||
{
|
{
|
||||||
type: 'ul',
|
type: 'ul',
|
||||||
items: [
|
items: [
|
||||||
'nginx reverse proxy terminating TLS in front of every service.',
|
'Traefik terminates TLS and routes every service, fronted by Cloudflare, with separate internal and external entrypoints and HTTP/3.',
|
||||||
'Docker Compose stacks for applications, each isolated on its own network.',
|
'Authentik provides single sign-on as a Traefik forward-auth middleware in front of internal tools; CrowdSec adds intrusion prevention and Pi-hole handles DNS filtering.',
|
||||||
'Authentication in front of internal tools; monitoring for uptime and logs.',
|
'Gitea and a Gitea Actions runner give me self-hosted git and CI/CD — the build-and-deploy target for this site.',
|
||||||
'Self-hosted Gitea with CI runners — the deployment target for this portfolio.',
|
'The Docker socket is exposed to Traefik only through a hardened socket-proxy, and each application runs on its own isolated Docker network.',
|
||||||
|
'Portainer and Dozzle handle container management and logs; alongside these run application, media and utility stacks.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -129,7 +146,7 @@ export const homelab: Project = {
|
|||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'p',
|
||||||
text: 'Tightening backup verification and adding more automated health checks and alerting across the stack.',
|
text: 'Retiring the legacy WordPress site now that this portfolio replaces it, then tightening backup verification and expanding automated health checks and alerting across the stack.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -137,13 +154,13 @@ export const homelab: Project = {
|
|||||||
},
|
},
|
||||||
no: {
|
no: {
|
||||||
valueProp:
|
valueProp:
|
||||||
'Der jeg drifter, ødelegger og fikser ekte infrastruktur — ikke bare ruller den ut.',
|
'Der jeg drifter, ødelegger og fikser ekte infrastruktur — rundt tretti containeriserte tjenester bak én reverse proxy.',
|
||||||
cardTeaser:
|
cardTeaser:
|
||||||
'Ubuntu · Docker-tjenester · reverse proxy · auth · overvåking · Gitea (git-remoten til denne siden).',
|
'Ubuntu + Docker · Traefik · Authentik SSO · CrowdSec · Gitea CI — ~30 egendriftede tjenester, og utrullingsmålet for denne siden.',
|
||||||
tldr: {
|
tldr: {
|
||||||
what: 'En egendriftet hjemmelab som kjører produksjonslignende tjenester.',
|
what: 'En egendriftet hjemmeserver med ~30 produksjonslignende tjenester.',
|
||||||
why: 'For å holde driftsferdighetene praktiske.',
|
why: 'For å holde drifts- og sikkerhetsferdighetene praktiske.',
|
||||||
stack: 'Ubuntu · Docker · nginx · Gitea · overvåking.',
|
stack: 'Ubuntu · Docker · Traefik · Authentik · CrowdSec · Gitea.',
|
||||||
role: 'Operatør.',
|
role: 'Operatør.',
|
||||||
},
|
},
|
||||||
sections: [
|
sections: [
|
||||||
@@ -154,7 +171,7 @@ export const homelab: Project = {
|
|||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'p',
|
||||||
text: 'En hjemmelab som kjører Ubuntu og Docker-baserte tjenester bak en nginx reverse proxy, med autentisering, overvåking og sikkerhetskopier. Det er her jeg får løpende, praktisk øving på utrulling, feilsøking og ekte drift — inkludert den egendriftede Gitea-instansen som denne siden bygges og rulles ut fra.',
|
text: 'En Ubuntu 24.04-server som kjører rundt tretti Docker-tjenester bak en Traefik reverse proxy, med single sign-on, et lag for inntrengingsvern, DNS-filtrering og full CI/CD. Det er her jeg får løpende, praktisk øving på utrulling, sikkerhet og ekte drift — inkludert den egendriftede Gitea-instansen og runneren som bygger og ruller ut denne porteføljen.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -166,10 +183,11 @@ export const homelab: Project = {
|
|||||||
{
|
{
|
||||||
type: 'ul',
|
type: 'ul',
|
||||||
items: [
|
items: [
|
||||||
'nginx reverse proxy som terminerer TLS foran hver tjeneste.',
|
'Traefik terminerer TLS og ruter alle tjenester, med Cloudflare foran, egne interne og eksterne entrypoints og HTTP/3.',
|
||||||
'Docker Compose-stacker for applikasjoner, hver isolert på eget nettverk.',
|
'Authentik gir single sign-on som en Traefik forward-auth-middleware foran interne verktøy; CrowdSec legger til inntrengingsvern og Pi-hole håndterer DNS-filtrering.',
|
||||||
'Autentisering foran interne verktøy; overvåking av oppetid og logger.',
|
'Gitea og en Gitea Actions-runner gir egendriftet git og CI/CD — bygge- og utrullingsmålet for denne siden.',
|
||||||
'Egendriftet Gitea med CI-runnere — utrullingsmålet for denne porteføljen.',
|
'Docker-socketen eksponeres kun til Traefik gjennom en herdet socket-proxy, og hver applikasjon kjører på sitt eget isolerte Docker-nettverk.',
|
||||||
|
'Portainer og Dozzle håndterer containere og logger; ved siden av kjører applikasjons-, media- og verktøystacker.',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -181,7 +199,7 @@ export const homelab: Project = {
|
|||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
type: 'p',
|
type: 'p',
|
||||||
text: 'Strammere verifisering av sikkerhetskopier og flere automatiske helsesjekker og varsling på tvers av stacken.',
|
text: 'Legge ned den gamle WordPress-siden nå som denne porteføljen erstatter den, og deretter stramme inn verifisering av sikkerhetskopier og utvide automatiske helsesjekker og varsling på tvers av stacken.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -85,42 +85,42 @@ export const jobtrack: Project = {
|
|||||||
},
|
},
|
||||||
media: [
|
media: [
|
||||||
{
|
{
|
||||||
src: 'placeholder:jobtrack-table',
|
src: '/assets/projects/jobtrack/applications.webp',
|
||||||
width: 1600,
|
width: 1600,
|
||||||
height: 1000,
|
height: 768,
|
||||||
alt: {
|
alt: {
|
||||||
en: 'JobTrack applications table with status filters and search',
|
en: 'JobTrack applications table showing companies, roles, statuses and follow-up flags',
|
||||||
no: 'JobTrack-søknadstabell med statusfiltre og søk',
|
no: 'JobTrack-søknadstabell med selskaper, roller, statuser og oppfølgingsmarkører',
|
||||||
},
|
},
|
||||||
caption: {
|
caption: {
|
||||||
en: 'Applications table — pipeline at a glance',
|
en: 'Applications table — the whole pipeline at a glance, with per-row next actions',
|
||||||
no: 'Søknadstabell — hele pipelinen samlet',
|
no: 'Søknadstabell — hele pipelinen samlet, med neste handling per rad',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'placeholder:jobtrack-details',
|
src: '/assets/projects/jobtrack/dashboard.webp',
|
||||||
width: 1600,
|
width: 1600,
|
||||||
height: 1000,
|
height: 759,
|
||||||
alt: {
|
alt: {
|
||||||
en: 'Job details dialog combining summary, notes, correspondence and attachments',
|
en: 'JobTrack dashboard with application trends, response rates and skill tags',
|
||||||
no: 'Jobbdetaljer med sammendrag, notater, korrespondanse og vedlegg',
|
no: 'JobTrack-dashbord med søknadstrender, svarrater og ferdighetstagger',
|
||||||
},
|
},
|
||||||
caption: {
|
caption: {
|
||||||
en: 'Job details — everything about one application in one place',
|
en: 'Dashboard — trends, response rates and pipeline health',
|
||||||
no: 'Jobbdetaljer — alt om én søknad på ett sted',
|
no: 'Dashbord — trender, svarrater og pipeline-helse',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: 'placeholder:jobtrack-gmail',
|
src: '/assets/projects/jobtrack/reminders.webp',
|
||||||
width: 1600,
|
width: 1600,
|
||||||
height: 1000,
|
height: 758,
|
||||||
alt: {
|
alt: {
|
||||||
en: 'Gmail import view with smart query filters and thread import',
|
en: 'JobTrack reminders view surfacing applications that need follow-up',
|
||||||
no: 'Gmail-import med smarte søkefiltre og trådimport',
|
no: 'JobTrack-påminnelser som løfter fram søknader som trenger oppfølging',
|
||||||
},
|
},
|
||||||
caption: {
|
caption: {
|
||||||
en: 'Gmail import — pull correspondence straight onto the application',
|
en: 'Reminders — applications that need attention, driven by follow-up rules',
|
||||||
no: 'Gmail-import — hent korrespondanse rett inn på søknaden',
|
no: 'Påminnelser — søknader som trenger oppfølging, styrt av regler',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ export default defineConfig({
|
|||||||
retries: process.env.CI ? 1 : 0,
|
retries: process.env.CI ? 1 : 0,
|
||||||
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
|
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : 'list',
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:4321',
|
baseURL: 'http://localhost:4399',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
},
|
},
|
||||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'pnpm -C ../site build && pnpm -C ../site preview --port 4321 --host',
|
command: 'pnpm -C ../site build && pnpm -C ../site preview --port 4399 --host',
|
||||||
url: 'http://localhost:4321/',
|
url: 'http://localhost:4399/',
|
||||||
timeout: 180_000,
|
timeout: 180_000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||||
@@ -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();
|
||||||
@@ -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');
|
||||||
|
})();
|
||||||
@@ -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 };
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user