- + {olderCount} earlier roles (ELGAS, Darktime){cvAvailable ? " — see the CV" : ""}.
+ + {olderCount} earlier roles (ELGAS, Darktime){cvAvailable ? " - see the CV" : ""}.
)}
diff --git a/src/components/CircuitDivider.astro b/src/components/CircuitDivider.astro
index e7a6038..d35beae 100644
--- a/src/components/CircuitDivider.astro
+++ b/src/components/CircuitDivider.astro
@@ -1,5 +1,5 @@
---
-// Circuit divider — gradient hairlines meeting two rotated-square "diamonds"
+// Circuit divider - gradient hairlines meeting two rotated-square "diamonds"
// (cyan, magenta). Pure CSS, from the design handoff.
---
diff --git a/src/components/Constellation.astro b/src/components/Constellation.astro
index 5fd49fa..9715869 100644
--- a/src/components/Constellation.astro
+++ b/src/components/Constellation.astro
@@ -1,7 +1,7 @@
---
-// The fleet constellation — 6 real nodes as IC chips on PCB traces.
+// The fleet constellation - 6 real nodes as IC chips on PCB traces.
// Pure SVG + scoped CSS keyframes (zero JS; reduced-motion turns every animation off).
-// Anatomy per the design handoff: each route is TWO parallel traces (offset ±(2,-3)) —
+// Anatomy per the design handoff: each route is TWO parallel traces (offset ±(2,-3)) -
// one carries a cyan packet outbound, the other a magenta packet inbound. Packets are
// stroke-dasharray 9/127 sliding via stroke-dashoffset. Hub = cave (56px chip, radar ring).
---
@@ -42,7 +42,7 @@
-
+
@@ -110,7 +110,7 @@
cave
-
+
@@ -136,13 +136,13 @@
wgirl
-
+
elfastc
-
+
nvidia compute
@@ -230,7 +230,7 @@
}
.cap--dim { fill: #5c6a82; font-size: 9px; }
.cap--key { fill: #7e8ba1; font-size: 9px; }
- /* gloss bat-black with a faint gold ring — the batcave swatch for core/services */
+ /* gloss bat-black with a faint gold ring - the batcave swatch for core/services */
.key--bat {
fill: #10131c;
stroke: rgba(245, 197, 24, 0.55);
@@ -245,7 +245,7 @@
animation: bzRing 3.6s ease-out infinite;
}
- /* dash pattern is 9+127 = a 136px period — the loop offset MUST be an exact multiple of it
+ /* dash pattern is 9+127 = a 136px period - the loop offset MUST be an exact multiple of it
(2×136 = 272) or every packet dot visibly teleports at the loop seam. */
@keyframes bzDash { to { stroke-dashoffset: -272; } }
@keyframes bzDashRev { to { stroke-dashoffset: 272; } }
diff --git a/src/components/Contact.astro b/src/components/Contact.astro
index 6749a27..73688ff 100644
--- a/src/components/Contact.astro
+++ b/src/components/Contact.astro
@@ -7,7 +7,7 @@ import { socials } from "../data/socials";
Open to conversations about platform engineering, edge infrastructure, and
- GPU/AI systems. Based in {site.location} — for a low-ms reply, ping me on
+ GPU/AI systems. Based in {site.location} - for a low-ms reply, ping me on
LinkedIn or email; everything else is best-effort delivery.
@@ -168,7 +168,7 @@ import Constellation from "./Constellation.astro";
line-height: 1.05;
letter-spacing: -0.005em;
padding-bottom: 0.14em;
- /* two-line nameplate (mock parity) — wrap at the space, keep the column clear
+ /* two-line nameplate (mock parity) - wrap at the space, keep the column clear
of the constellation's left-most node labels */
max-width: min-content;
background:
@@ -219,7 +219,7 @@ import Constellation from "./Constellation.astro";
gap: var(--space-3);
}
- /* the typing bar — glass panel */
+ /* the typing bar - glass panel */
.hero__fact {
margin-top: var(--space-6);
display: flex;
@@ -232,7 +232,7 @@ import Constellation from "./Constellation.astro";
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
/* one-liner: the box sizes to its fact (smaller type) and may run a little
- past the copy column — there's clear board under it until the viz */
+ past the copy column - there's clear board under it until the viz */
width: max-content;
max-width: min(52rem, calc(100vw - 2 * var(--space-5)));
min-height: 20px;
@@ -304,7 +304,7 @@ import Constellation from "./Constellation.astro";
@media (max-width: 560px) {
.hero__viz { display: none; }
.hero__scroll { display: none; }
- /* phones: the fact bar fills the column and WRAPS like a real terminal —
+ /* phones: the fact bar fills the column and WRAPS like a real terminal -
the desktop one-liner (nowrap + max-content) busts a 390px viewport */
.hero__fact {
font-size: 12px;
diff --git a/src/components/Nav.astro b/src/components/Nav.astro
index 6c858e0..6523886 100644
--- a/src/components/Nav.astro
+++ b/src/components/Nav.astro
@@ -5,7 +5,7 @@ import { site } from "../data/site";
// `id` ties a nav link to a homepage section so the Motion layer animates its
// underline as that section scrolls through (scroll-spy). Projects/Blog navigate
// to their own pages on click, but on the homepage they track the #projects and
-// #writing sections so every nav item gets the underline — in both directions.
+// #writing sections so every nav item gets the underline - in both directions.
const links = [
{ label: "About", href: "/#about", id: "about" },
{ label: "Skills", href: "/#skills", id: "skills" },
@@ -17,7 +17,7 @@ const links = [
-
+ ~/{site.handle}▮
@@ -95,7 +95,7 @@ const links = [
color: var(--text);
text-decoration: none;
}
- /* Active-section / hover underline — scaleX scrubbed by the Motion layer
+ /* Active-section / hover underline - scaleX scrubbed by the Motion layer
(spring). transform-origin left so it grows from the start of the label. */
.nav__ul {
position: absolute;
@@ -120,7 +120,7 @@ const links = [
display: flex;
}
}
- /* phones: the tabs live on a compact second row (zero-JS, no hamburger) —
+ /* phones: the tabs live on a compact second row (zero-JS, no hamburger) -
the bar wraps: brand + toggle on row 1, links across row 2 */
@media (max-width: 719px) {
.nav__inner {
diff --git a/src/components/ProjectCard.astro b/src/components/ProjectCard.astro
index 3977c6d..5b98fca 100644
--- a/src/components/ProjectCard.astro
+++ b/src/components/ProjectCard.astro
@@ -1,5 +1,5 @@
---
-// Dossier card — thumbnail diagram, DOSSIER/00N corner tag, title, "the argument",
+// Dossier card - thumbnail diagram, DOSSIER/00N corner tag, title, "the argument",
// tag chips, mono meta footer. Facelift shape from the design handoff.
import type { CollectionEntry } from "astro:content";
import DossierThumb from "./DossierThumb.astro";
diff --git a/src/components/Section.astro b/src/components/Section.astro
index 4326295..8dba319 100644
--- a/src/components/Section.astro
+++ b/src/components/Section.astro
@@ -25,7 +25,7 @@ const { id, eyebrow, title, index } = Astro.props;
.section__head {
margin-bottom: var(--space-6);
}
- /* A short cyan→magenta tick leads each section eyebrow — board palette. */
+ /* A short cyan→magenta tick leads each section eyebrow - board palette. */
.section__head .eyebrow {
display: inline-flex;
align-items: center;
diff --git a/src/components/SkillGroup.astro b/src/components/SkillGroup.astro
index 55dca05..6bcfefc 100644
--- a/src/components/SkillGroup.astro
+++ b/src/components/SkillGroup.astro
@@ -1,5 +1,5 @@
---
-// One bento tile in the capabilities grid — size/flavour driven by the data
+// One bento tile in the capabilities grid - size/flavour driven by the data
// (span: hero 2×2 flagship · wide 2×1 · full row · default 1×1).
import type { SkillGroup } from "../data/skills";
@@ -181,7 +181,7 @@ const spanClass =
padding: 5px 10px;
}
- /* Below 1000px the parent grid is only 2 columns — so the multi-column spans
+ /* Below 1000px the parent grid is only 2 columns - so the multi-column spans
must collapse to full-width HERE, not at 720px. A `span 3` tile left active
in a 2-col grid forces a phantom 3rd column and collapses the 1fr tracks
(58px 58px 760px), squishing the first tile to ~130px in landscape phone. */
diff --git a/src/content.config.ts b/src/content.config.ts
index 20d7089..78c4720 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -7,7 +7,7 @@ const projects = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/projects" }),
schema: z.object({
title: z.string(),
- // The one-line "so what" — shown on cards and the case-study header.
+ // The one-line "so what" - shown on cards and the case-study header.
outcome: z.string(),
summary: z.string(),
role: z.string(),
@@ -27,7 +27,7 @@ const projects = defineCollection({
}),
});
-// Blog — write-only, schema-validated, pipeline-publishable.
+// Blog - write-only, schema-validated, pipeline-publishable.
const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({
@@ -36,7 +36,7 @@ const blog = defineCollection({
summary: z.string(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
- // Optional hero image — a /blog/.webp in public/. Rendered on the post + as the card thumbnail.
+ // Optional hero image - a /blog/.webp in public/. Rendered on the post + as the card thumbnail.
hero: z.string().optional(),
heroAlt: z.string().optional(),
}),
diff --git a/src/content/projects/edge-ai-platform.md b/src/content/projects/edge-ai-platform.md
index dd52f76..aa25790 100644
--- a/src/content/projects/edge-ai-platform.md
+++ b/src/content/projects/edge-ai-platform.md
@@ -3,7 +3,7 @@ title: "Single-Touch Edge AI Platform"
outcome: "Turned a high-level edge-AI design into a single-press deployment running on Kubernetes at the store edge."
summary: "Store-edge Kubernetes running GPU-backed AI workloads, deployed from one command, with readiness-gated GPUs so inference never starts before the hardware is ready."
role: "Infrastructure / DevOps Engineer · Woolworths"
-period: "2025 – Present"
+period: "2025 - Present"
stack: ["Kubernetes", "Edge", "NVIDIA GPU", "CD pipelines", "Helm", "Python"]
featured: true
order: 10
@@ -16,12 +16,12 @@ diagram: "edge-ai"
Edge AI at retail scale lives or dies on repeatability. A computer-vision workload that
runs perfectly in a lab has to come up the same way in a store with no on-site engineer,
flaky connectivity, and a GPU that may not be ready the instant Kubernetes wants to schedule
-against it. The starting point was a high-level design and a pile of manual steps — exactly
+against it. The starting point was a high-level design and a pile of manual steps - exactly
the gap between "it works" and "it ships."
## Constraints
-- **No hands at the edge.** Deployment has to be hands-off and idempotent — a single press.
+- **No hands at the edge.** Deployment has to be hands-off and idempotent - a single press.
- **GPU timing.** Inference pods must never schedule before the GPU device plugin is healthy,
or they crash-loop and poison the rollout.
- **Heterogeneous stores.** Per-site variables (network, hardware, identity) without forking
@@ -41,15 +41,15 @@ lifecycle management keeps the workload honest from there.
## Security & reliability decisions
-- **Init-gated GPU readiness** — the single biggest reliability win; no more pods racing the
+- **Init-gated GPU readiness** - the single biggest reliability win; no more pods racing the
GPU at boot.
-- **Single source of truth** for config — drift can't creep in store-to-store.
-- **Spec-driven, documented-as-code** — the deployment *is* the documentation.
+- **Single source of truth** for config - drift can't creep in store-to-store.
+- **Spec-driven, documented-as-code** - the deployment *is* the documentation.
## Outcome
A high-level idea becomes a real, repeatable deployment on a single press. New edge sites
-come up consistently, GPUs come online reliably, and the manual runbook is gone — replaced
+come up consistently, GPUs come online reliably, and the manual runbook is gone - replaced
by a pipeline anyone on the team can trigger.
## Future improvements
diff --git a/src/content/projects/global-infra-modernisation.md b/src/content/projects/global-infra-modernisation.md
index b0693c1..bcf3ba0 100644
--- a/src/content/projects/global-infra-modernisation.md
+++ b/src/content/projects/global-infra-modernisation.md
@@ -1,9 +1,9 @@
---
title: "Global Infrastructure Modernisation"
-outcome: "Modernised a global, multi-region estate at scale — ~1,000 VMs, flat-to-segmented networks, and a migration to Azure and Microsoft 365 — on a live 24/7 business."
+outcome: "Modernised a global, multi-region estate at scale - ~1,000 VMs, flat-to-segmented networks, and a migration to Azure and Microsoft 365 - on a live 24/7 business."
summary: "Across global IT roles at Virtus Health and Linde Asia Pacific: a ~1,000-VM VMware estate managed centrally, a flat-to-segmented network redesign with SD-WAN and Aruba ClearPass, Palo Alto / FortiGate firewall redesigns, and migration to Azure (Blob, AVS) and Microsoft 365."
role: "Infrastructure Engineer · Virtus Health & Linde Asia Pacific"
-period: "2019 – 2025"
+period: "2019 - 2025"
stack: ["VMware / vSphere", "Azure (Blob, AVS)", "Microsoft 365", "SD-WAN", "Aruba ClearPass", "Palo Alto / FortiGate", "Veeam"]
featured: false
order: 40
@@ -14,13 +14,13 @@ cover: "global-infra-modernisation"
Enterprise estates accrete. Flat networks, sprawling VM counts, aging firewalls, and
on-prem-only services become a security and operations drag. The work: modernise a global,
-multi-region business that runs 24/7 — without breaking it.
+multi-region business that runs 24/7 - without breaking it.
## Constraints
-- **Keep the lights on** — change a live, multi-region estate without downtime.
-- **Security and compliance** — segmentation, patching, and auditability throughout.
-- **Cost-aware** — modernise to cloud where it pays, justified through CapEx/OpEx cases.
+- **Keep the lights on** - change a live, multi-region estate without downtime.
+- **Security and compliance** - segmentation, patching, and auditability throughout.
+- **Cost-aware** - modernise to cloud where it pays, justified through CapEx/OpEx cases.
## Design
@@ -28,10 +28,10 @@ Across global roles I ran and improved a **~1,000-VM VMware estate**, managed ce
the IT team and operated across regions including the UK. I re-segmented **flat sites into
isolated VLAN ranges** with ACLs, layering in **SD-WAN** and **Aruba ClearPass** with 802.1x
onboarding for a tiered, authenticated network. **Palo Alto / FortiGate** firewalls were
-upgraded and redesigned around the new segmentation — RCA, staging through FortiManager, and
+upgraded and redesigned around the new segmentation - RCA, staging through FortiManager, and
a flat-to-segmented redesign.
-On the platform side: workloads and identity moved to **Azure** (Blob storage, AVS — lifting
+On the platform side: workloads and identity moved to **Azure** (Blob storage, AVS - lifting
existing vSphere environments) and **Microsoft 365**, with a **hybrid AD sync** I architected
to bridge on-prem and cloud identity. The estate work also covered an **ERP hardware refresh
with a new DR / mainframe solution**, file shares to Azure Blob over Kerberos auth, **Veeam**
@@ -39,14 +39,14 @@ backups, and a region-wide **PBX-to-VoIP** migration (RingCentral).
## Security & reliability decisions
-- **Flat → segmented** — isolation by design, not by exception.
-- **Authenticated access** (ClearPass, 802.1x) — the network knows who's on it.
-- **Patched, current firewalls** — closing the easy doors first.
-- **DR built in** — recovery designed, not assumed.
+- **Flat → segmented** - isolation by design, not by exception.
+- **Authenticated access** (ClearPass, 802.1x) - the network knows who's on it.
+- **Patched, current firewalls** - closing the easy doors first.
+- **DR built in** - recovery designed, not assumed.
## Outcome
-A more secure, segmented, cloud-leaning estate that's cheaper to run and easier to operate —
+A more secure, segmented, cloud-leaning estate that's cheaper to run and easier to operate -
delivered against live-business constraints across multiple regions.
## Future improvements
diff --git a/src/content/projects/gpu-as-code.md b/src/content/projects/gpu-as-code.md
index c5a994d..d23d090 100644
--- a/src/content/projects/gpu-as-code.md
+++ b/src/content/projects/gpu-as-code.md
@@ -1,9 +1,9 @@
---
title: "GPU-as-Code on the Edge"
-outcome: "Brought GPUs online as code — passthrough, readiness-gated, and reproducible across the fleet."
+outcome: "Brought GPUs online as code - passthrough, readiness-gated, and reproducible across the fleet."
summary: "GPU passthrough configured through ESXi via code with end-state manifests and Helm, paired with readiness probes, watchdogs, and DCGM-based health reporting."
role: "Infrastructure / DevOps Engineer"
-period: "2025 – Present"
+period: "2025 - Present"
stack: ["GPU passthrough", "ESXi", "DCGM Exporter", "Prometheus", "Bash", "Watchdogs"]
featured: false
order: 30
@@ -18,9 +18,9 @@ to refuse to start until both are true. Doing that by hand, per site, doesn't sc
## Constraints
-- **As-code, not click-ops** — GPU passthrough defined in code, not the ESXi UI.
-- **Fail safe** — a not-ready GPU must block the workload, not crash it.
-- **Observable** — GPU health has to be visible alongside the rest of the platform.
+- **As-code, not click-ops** - GPU passthrough defined in code, not the ESXi UI.
+- **Fail safe** - a not-ready GPU must block the workload, not crash it.
+- **Observable** - GPU health has to be visible alongside the rest of the platform.
## Design
@@ -33,9 +33,9 @@ platform signal does.
## Security & reliability decisions
-- **Readiness gating** — pods wait for the hardware; no boot-time races.
-- **End-state manifests** — the node's GPU config is declarative and reproducible.
-- **DCGM telemetry** — GPU failures are detected, not discovered.
+- **Readiness gating** - pods wait for the hardware; no boot-time races.
+- **End-state manifests** - the node's GPU config is declarative and reproducible.
+- **DCGM telemetry** - GPU failures are detected, not discovered.
## Outcome
diff --git a/src/content/projects/iac-fleet-automation.md b/src/content/projects/iac-fleet-automation.md
index 3e3deec..7c3eb76 100644
--- a/src/content/projects/iac-fleet-automation.md
+++ b/src/content/projects/iac-fleet-automation.md
@@ -1,9 +1,9 @@
---
title: "IaC Fleet Automation"
-outcome: "Stood up identical edge sites from code — every store comes up the same way, every time."
-summary: "Ansible/AWX playbooks wired through a single source-of-truth pipeline: GPU operator, templated networking, image pre-pull and secrets — with air-gapped registry mirroring for disconnected sites."
+outcome: "Stood up identical edge sites from code - every store comes up the same way, every time."
+summary: "Ansible/AWX playbooks wired through a single source-of-truth pipeline: GPU operator, templated networking, image pre-pull and secrets - with air-gapped registry mirroring for disconnected sites."
role: "Automation Engineer"
-period: "2025 – Present"
+period: "2025 - Present"
stack: ["Ansible", "AWX", "GitOps", "ACR / NVCR", "Image pre-pull", "Secrets mgmt"]
featured: true
order: 20
@@ -15,13 +15,13 @@ diagram: "iac-fleet"
A fleet only behaves like a fleet if every node is built the same way. Hand-configuring GPU
drivers, CNI, image caches and secrets per site is slow, error-prone, and impossible to
-audit — and at the edge, half the sites can't reach the internet when you need them to.
+audit - and at the edge, half the sites can't reach the internet when you need them to.
## Constraints
-- **Repeatability over cleverness** — the same playbook must produce the same node anywhere.
-- **Air-gapped reality** — disconnected edge sites still have to build from local images.
-- **No secrets in code** — credentials delivered at deploy time, never committed.
+- **Repeatability over cleverness** - the same playbook must produce the same node anywhere.
+- **Air-gapped reality** - disconnected edge sites still have to build from local images.
+- **No secrets in code** - credentials delivered at deploy time, never committed.
## Design
@@ -36,14 +36,14 @@ internet path at the moment it matters.
## Security & reliability decisions
-- **Secrets management at deploy time** — nothing sensitive in git.
-- **Pre-staged, mirrored images** — supply chain stays available and pinned, even offline.
-- **AWX job-level reporting** — every run is visible and auditable.
+- **Secrets management at deploy time** - nothing sensitive in git.
+- **Pre-staged, mirrored images** - supply chain stays available and pinned, even offline.
+- **AWX job-level reporting** - every run is visible and auditable.
## Outcome
New edge sites are provisioned from code with consistent results, manual build steps are
-removed wherever logic allows, and the whole fleet is reproducible — an IaC-first build
+removed wherever logic allows, and the whole fleet is reproducible - an IaC-first build
instead of a runbook.
## Future improvements
diff --git a/src/content/projects/network-automation-fleet.md b/src/content/projects/network-automation-fleet.md
index a546ce7..469e353 100644
--- a/src/content/projects/network-automation-fleet.md
+++ b/src/content/projects/network-automation-fleet.md
@@ -1,9 +1,9 @@
---
title: "Network Automation at Fleet Scale"
-outcome: "Took a multi-region switch fleet from hand-managed to single-pane — automated config backup, bulk change in minutes, and credentials secured and rotated."
-summary: "Productionised a vendor-agnostic network config-management platform (Unimus) with NetBox as the source-of-truth across an AU/NZ/PNG switch fleet: scheduled config backups, mass config push, and a centrally vaulted, rotated credential model — community tooling stood up ground-up under a corporate-supported licence."
+outcome: "Took a multi-region switch fleet from hand-managed to single-pane - automated config backup, bulk change in minutes, and credentials secured and rotated."
+summary: "Productionised a vendor-agnostic network config-management platform (Unimus) with NetBox as the source-of-truth across an AU/NZ/PNG switch fleet: scheduled config backups, mass config push, and a centrally vaulted, rotated credential model - community tooling stood up ground-up under a corporate-supported licence."
role: "Global IT Infrastructure Engineer · Linde Asia Pacific"
-period: "2019 – 2022"
+period: "2019 - 2022"
stack: ["Unimus (NCM)", "NetBox (IPAM / SoT)", "Config backup & DR", "Bulk config push", "Credential vaulting + rotation", "Multi-vendor switching"]
featured: false
order: 45
@@ -18,49 +18,49 @@ links:
## Problem
-A multi-region switch fleet — Australia, New Zealand and PNG — was maintained by hand.
+A multi-region switch fleet - Australia, New Zealand and PNG - was maintained by hand.
Configs drifted, backups were "hope someone saved the last one," bulk changes were pushed
box-by-box, and device credentials were scattered across people and scripts. For a network
that production depends on, there was no real recovery path and no audit trail.
## Constraints
-- **Live production network** — change the fleet without taking sites down.
-- **Mixed-vendor estate** — the tooling had to be vendor-agnostic, not tied to one OS.
-- **Credentials are the crown jewels** — secured, rotated, and never sitting in a script.
-- **Cost-justified** — a lean tool that earns its place through a real CapEx/OpEx case.
+- **Live production network** - change the fleet without taking sites down.
+- **Mixed-vendor estate** - the tooling had to be vendor-agnostic, not tied to one OS.
+- **Credentials are the crown jewels** - secured, rotated, and never sitting in a script.
+- **Cost-justified** - a lean tool that earns its place through a real CapEx/OpEx case.
## Design
-I productionised **Unimus** — a vendor-agnostic Network Configuration Management platform —
+I productionised **Unimus** - a vendor-agnostic Network Configuration Management platform -
as the automation backbone, and stood it up **ground-up**: install, device onboarding,
backup scheduling, and change workflows. It gave the fleet three things it never had from one
place: **automated, scheduled config backups**, **change tracking with config diffs**, and
**mass / bulk config push** across many devices at once.
Paired with it, **NetBox** served as the **IPAM and source-of-truth** for the device
-inventory the automation runs against — so the fleet is driven from a single, accurate model
+inventory the automation runs against - so the fleet is driven from a single, accurate model
of what exists rather than tribal knowledge. Both are lean, community-rooted tools adopted
under a **corporate-supported licence**, which made them viable inside an enterprise change
-process — the open-source-into-corporate pattern, done properly.
+process - the open-source-into-corporate pattern, done properly.
## Security & reliability decisions
-- **Centralised, vaulted credentials with rotation** — the network's secrets live in one
+- **Centralised, vaulted credentials with rotation** - the network's secrets live in one
scoped, rotated place, never embedded in scripts or shared by hand.
-- **Config backup = real network DR** — every device restorable from a known-good config.
-- **Diffs + auditing** — every configuration change is visible, attributable, and reversible.
-- **Source-of-truth driven** — automation acts on NetBox's model, not on guesswork.
+- **Config backup = real network DR** - every device restorable from a known-good config.
+- **Diffs + auditing** - every configuration change is visible, attributable, and reversible.
+- **Source-of-truth driven** - automation acts on NetBox's model, not on guesswork.
## Outcome
The switch fleet across AU/NZ/PNG was maintained from a single pane: configs backed up
automatically, bulk changes pushed in minutes instead of days, credentials secured and
-rotated, and — for the first time — a genuine recovery path for network configuration. A lean
+rotated, and - for the first time - a genuine recovery path for network configuration. A lean
community toolset, productionised with enterprise rigour.
## Future improvements
-Drive the automation deeper from the source-of-truth — intent in NetBox, pushed to the fleet
-through Unimus — and template standard site configs so a new location onboards straight from
+Drive the automation deeper from the source-of-truth - intent in NetBox, pushed to the fleet
+through Unimus - and template standard site configs so a new location onboards straight from
the inventory.
diff --git a/src/content/projects/self-hosted-ai-homelab.md b/src/content/projects/self-hosted-ai-homelab.md
index 1d0d371..e9a83e3 100644
--- a/src/content/projects/self-hosted-ai-homelab.md
+++ b/src/content/projects/self-hosted-ai-homelab.md
@@ -1,6 +1,6 @@
---
title: "Self-Hosted AI & Homelab Platform"
-outcome: "A production-grade homelab — GitOps from bare metal to local AI, and the platform that serves this very site."
+outcome: "A production-grade homelab - GitOps from bare metal to local AI, and the platform that serves this very site."
summary: "Proxmox with PCIe passthrough under Talos and OpenShift clusters, all driven by ArgoCD GitOps: local LLM inference, split-horizon DNS, 2FA/SSO VPN, full observability and NAS-backed backups."
role: "Owner / Operator"
period: "Ongoing"
@@ -13,38 +13,38 @@ diagram: "homelab"
## Problem
-The best way to stay sharp on platform engineering is to run a real platform — one with the
+The best way to stay sharp on platform engineering is to run a real platform - one with the
same rigour as production, where the only person on call is you. The goal: a homelab that's a
genuine proving ground for Kubernetes, GPUs, AI and security, not a pile of containers.
## Constraints
-- **Run it like production** — GitOps, backups, observability, no snowflake config.
-- **Secure by default** — nothing exposed that doesn't need to be.
-- **Reproducible** — rebuild a node from code, not from memory.
+- **Run it like production** - GitOps, backups, observability, no snowflake config.
+- **Secure by default** - nothing exposed that doesn't need to be.
+- **Reproducible** - rebuild a node from code, not from memory.
## Design
Proxmox provides the hypervisor layer with **PCIe passthrough** (GPU and storage) into
-single-node **Talos** and **OpenShift** clusters. Everything is **ArgoCD GitOps** — the
+single-node **Talos** and **OpenShift** clusters. Everything is **ArgoCD GitOps** - the
cluster state lives in git and reconciles itself. On top: **local LLM inference** on a
Blackwell-class GPU, **split-horizon DNS** via Pi-hole, a VPN with **2FA/SSO**, and a
**Prometheus / Grafana** observability stack. ZFS handles storage tiering; restic ships
-**NAS-backed backups**. Public services reach the internet through a **Cloudflare Tunnel** —
+**NAS-backed backups**. Public services reach the internet through a **Cloudflare Tunnel** -
which is exactly how this site is served.
## Security & reliability decisions
-- **GitOps as the source of truth** — drift is reconciled, not chased.
-- **2FA / SSO and segmented access** — least privilege across the lab.
-- **Back up state, not just volumes** — restores are drilled, not hoped for.
-- **Outbound-only public exposure** — a tunnel, not an open port.
+- **GitOps as the source of truth** - drift is reconciled, not chased.
+- **2FA / SSO and segmented access** - least privilege across the lab.
+- **Back up state, not just volumes** - restores are drilled, not hoped for.
+- **Outbound-only public exposure** - a tunnel, not an open port.
## Outcome
A homelab that behaves like a platform: rebuildable from code, observable, backed up, and
secure enough to host a public site on. It's where new patterns get proven before they go
-anywhere near real infrastructure — and it's running right now, under this page.
+anywhere near real infrastructure - and it's running right now, under this page.
## Future improvements
diff --git a/src/data/experience.ts b/src/data/experience.ts
index 964add0..b63ec82 100644
--- a/src/data/experience.ts
+++ b/src/data/experience.ts
@@ -13,33 +13,33 @@ export const experience: Role[] = [
{
title: "Infrastructure / DevOps Engineer",
org: "Woolworths",
- period: "Aug 2025 – Present",
+ period: "Aug 2025 - Present",
summary:
- "Core DevOps infrastructure engineer for store-edge solutions — turning high-level designs into single-press deployments and CD pipelines.",
+ "Core DevOps infrastructure engineer for store-edge solutions - turning high-level designs into single-press deployments and CD pipelines.",
highlights: [
"Single-touch deployment of an edge AI solution running on Kubernetes at the store edge",
- "GPU brought online as code — passthrough via ESXi, end-state manifests / Helm charts",
+ "GPU brought online as code - passthrough via ESXi, end-state manifests / Helm charts",
"Ansible playbook builds wired through a single source-of-truth pipeline with per-store vars",
- "Removed manual practices across infra and ops — IaC throughout, spec-driven and documented as code",
+ "Removed manual practices across infra and ops - IaC throughout, spec-driven and documented as code",
],
},
{
title: "Infrastructure Systems Engineer",
org: "Virtus Health",
- period: "Aug 2022 – Aug 2025",
+ period: "Aug 2022 - Aug 2025",
summary:
- "Global IT — optimisation, implementation and projects across an enterprise estate.",
+ "Global IT - optimisation, implementation and projects across an enterprise estate.",
highlights: [
"Managed ~1,000 VMs across a global VMware estate",
"Re-segmented flat sites into isolated VLAN ranges; SD-WAN and Aruba ClearPass onboarding",
"Migrated workloads to Azure (Blob, AVS); upgraded Palo Alto / FortiGate firewalls",
- "Led a new major-site build — end-to-end services implementation",
+ "Led a new major-site build - end-to-end services implementation",
],
},
{
title: "Global IT Infrastructure Engineer",
org: "Linde Asia Pacific",
- period: "Sep 2019 – Aug 2022",
+ period: "Sep 2019 - Aug 2022",
summary:
"Maintained and modernised global infrastructure to enterprise standards across the region.",
highlights: [
@@ -51,7 +51,7 @@ export const experience: Role[] = [
{
title: "Web Developer / Tech Admin",
org: "ELGAS",
- period: "Aug 2018 – Sep 2019",
+ period: "Aug 2018 - Sep 2019",
summary:
"Development, optimisation and escalation for the IT service-desk team.",
highlights: [
@@ -62,12 +62,12 @@ export const experience: Role[] = [
{
title: "IT Technician",
org: "Darktime Computer Services",
- period: "Nov 2010 – Jun 2013",
+ period: "Nov 2010 - Jun 2013",
summary:
"Field and workshop IT across legal firms, schools, real estate and small business.",
highlights: [
"SBS 2008 server installs, Exchange, backups and RAID storage for small business",
- "Solo on-site support — diagnose, fix, move on",
+ "Solo on-site support - diagnose, fix, move on",
],
},
];
diff --git a/src/data/fleet.ts b/src/data/fleet.ts
index 2ee2c81..afe5291 100644
--- a/src/data/fleet.ts
+++ b/src/data/fleet.ts
@@ -1,14 +1,14 @@
-// The fleet, as data — drives the hero constellation + typing facts.
+// The fleet, as data - drives the hero constellation + typing facts.
// REAL numbers, captured 2026-07-03; refresh from the fleet at rebuild
// (kubectl get applications -n argocd | wc -l · kubectl get cnp -A | wc -l per cluster).
-// Facts the hero typing bar cycles through. Real, verbatim — no invented numbers.
+// Facts the hero typing bar cycles through. Real, verbatim - no invented numbers.
export const fleetFacts = [
"6 single-node Talos clusters · 0 SSH daemons · every machine is a document",
"139 ArgoCD Applications reconciled from one hub",
- "170+ CiliumNetworkPolicies fleet-wide — every namespace starts at default-deny",
- "zero open inbound ports — the origin dials out",
- "*.bztmon.org — a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed",
+ "170+ CiliumNetworkPolicies fleet-wide - every namespace starts at default-deny",
+ "zero open inbound ports - the origin dials out",
+ "*.bztmon.org - a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed",
"~140 pods running right now, reconciled by git",
] as const;
diff --git a/src/data/site.ts b/src/data/site.ts
index 39c3c81..b91e256 100644
--- a/src/data/site.ts
+++ b/src/data/site.ts
@@ -1,4 +1,4 @@
-// Central site config — single source of truth for identity + metadata.
+// Central site config - single source of truth for identity + metadata.
// Edit here, not in components.
export const site = {
@@ -20,7 +20,7 @@ export const site = {
"Edge AI",
],
// One-paragraph elevator pitch for the About section.
- bio: "I'm an infrastructure and DevOps engineer who turns high-level designs into single-press deployments. My day job is store-edge Kubernetes running GPU-backed AI; my craft is the automation, GitOps and network policy that make a fleet behave the same way every time. I've run global infrastructure at enterprise scale — a thousand VMs, multi-region migrations, segmented networks — and I bring that same rigour to the smallest edge node. I'm an IaC enthusiast: if it's a manual step, I want it gone.",
+ bio: "I'm an infrastructure and DevOps engineer who turns high-level designs into single-press deployments. My day job is store-edge Kubernetes running GPU-backed AI; my craft is the automation, GitOps and network policy that make a fleet behave the same way every time. I've run global infrastructure at enterprise scale - a thousand VMs, multi-region migrations, segmented networks - and I bring that same rigour to the smallest edge node. I'm an IaC enthusiast: if it's a manual step, I want it gone.",
// Canonical URL (used for OG/sitemap/RSS).
url: "https://www.bztmon.com",
diff --git a/src/data/skills.ts b/src/data/skills.ts
index b988d77..78affb1 100644
--- a/src/data/skills.ts
+++ b/src/data/skills.ts
@@ -1,17 +1,17 @@
-// Grouped capability matrix — NO percentage bars / ratings / logo walls.
+// Grouped capability matrix - NO percentage bars / ratings / logo walls.
// Driven entirely by this file; the Skills section renders whatever is here.
export type SkillGroup = {
title: string;
// Short framing line for the group.
blurb: string;
- /** Proof-point lines rendered between the blurb and the chips — fills the
+ /** Proof-point lines rendered between the blurb and the chips - fills the
* tall tiles (hero / full) so the tile reads as substance, not whitespace. */
points?: string[];
items: string[];
- /** Bento tile size — "hero" = 2×2 flagship · "wide" = 2×1 · "full" = whole row · default 1×1. */
+ /** Bento tile size - "hero" = 2×2 flagship · "wide" = 2×1 · "full" = whole row · default 1×1. */
span?: "hero" | "wide" | "full";
- /** Accent flavour — "magenta" marks security-flavoured tiles. */
+ /** Accent flavour - "magenta" marks security-flavoured tiles. */
flavor?: "magenta";
/** Eyebrow label rendered on the hero tile. */
flag?: string;
@@ -24,10 +24,10 @@ export const skills: SkillGroup[] = [
flag: "FLAGSHIP",
blurb: "Serving vision and language models on real hardware at the edge.",
points: [
- "Fail-closed computer vision on single-node OpenShift — probes that catch a silently dead model, not just a dead pod.",
- "One GPU, two jobs — a single card time-sliced between LLM serving and video transcode, with neither noticing.",
+ "Fail-closed computer vision on single-node OpenShift - probes that catch a silently dead model, not just a dead pod.",
+ "One GPU, two jobs - a single card time-sliced between LLM serving and video transcode, with neither noticing.",
"Vendor video-analytics stacks re-platformed as first-class Kubernetes workloads, not appliances.",
- "Local models behind an OpenAI-compatible gateway — swap the backend, keep every client.",
+ "Local models behind an OpenAI-compatible gateway - swap the backend, keep every client.",
],
items: [
"NVIDIA GPU Operator",
@@ -56,7 +56,7 @@ export const skills: SkillGroup[] = [
},
{
title: "Automation & IaC",
- blurb: "Single-touch deployments — removing the manual step, not documenting it.",
+ blurb: "Single-touch deployments - removing the manual step, not documenting it.",
items: [
"Ansible / AWX",
"PowerShell (PowerCLI)",
@@ -97,11 +97,11 @@ export const skills: SkillGroup[] = [
{
title: "Observability & Ops",
span: "full",
- blurb: "Knowing the system is healthy — and the GPUs with it.",
+ blurb: "Knowing the system is healthy - and the GPUs with it.",
points: [
- "Fleet metrics by remote-write — every cluster ships to one Prometheus, GPUs included (DCGM / Intel Xe).",
- "Alerts that reach a phone and mean it — tuned until silence means healthy, not unmonitored.",
- "A backup is a rumour until a restore drill proves it — drills are scheduled, not aspirational.",
+ "Fleet metrics by remote-write - every cluster ships to one Prometheus, GPUs included (DCGM / Intel Xe).",
+ "Alerts that reach a phone and mean it - tuned until silence means healthy, not unmonitored.",
+ "A backup is a rumour until a restore drill proves it - drills are scheduled, not aspirational.",
],
items: [
"Prometheus",
diff --git a/src/data/socials.ts b/src/data/socials.ts
index 0b1aad9..1681da0 100644
--- a/src/data/socials.ts
+++ b/src/data/socials.ts
@@ -11,7 +11,7 @@ export type Social = {
export const socials: Social[] = [
{
- // Self-hosted public git — his code, on his own infrastructure (goes live with git.bztmon.com).
+ // Self-hosted public git - his code, on his own infrastructure (goes live with git.bztmon.com).
label: "Source",
href: "https://git.bztmon.com/jwright",
external: true,
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index 29a05e0..0d9d83f 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -7,7 +7,7 @@ import { site } from "../data/site";
interface Props {
title?: string;
description?: string;
- /** Path to the page, e.g. "/projects" — used for canonical + OG url. */
+ /** Path to the page, e.g. "/projects" - used for canonical + OG url. */
path?: string;
}
@@ -17,7 +17,7 @@ const {
path = "/",
} = Astro.props;
-const pageTitle = title ? `${title} — ${site.name}` : `${site.name} · ${site.role}`;
+const pageTitle = title ? `${title} - ${site.name}` : `${site.name} · ${site.role}`;
const canonical = new URL(path, site.url).href;
const ogImage = new URL(site.ogImage, site.url).href;
---
diff --git a/src/lib/build-info.ts b/src/lib/build-info.ts
index fa2435e..7f2993d 100644
--- a/src/lib/build-info.ts
+++ b/src/lib/build-info.ts
@@ -1,4 +1,4 @@
-// Build-time GitOps receipt data — captured ONCE at build (SSG), zero runtime calls.
+// Build-time GitOps receipt data - captured ONCE at build (SSG), zero runtime calls.
// The footer renders these as the "receipt" line; values are baked into the static HTML.
import { execSync } from "node:child_process";
diff --git a/src/pages/404.astro b/src/pages/404.astro
index c9a6a79..702e6fd 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -2,7 +2,7 @@
import Layout from "../layouts/Layout.astro";
---
-
+
Error 404
diff --git a/src/pages/blog/[slug].astro b/src/pages/blog/[slug].astro
index 7627b76..6aa251d 100644
--- a/src/pages/blog/[slug].astro
+++ b/src/pages/blog/[slug].astro
@@ -130,7 +130,7 @@ const { Content } = await render(entry);
margin-top: var(--space-7);
}
/* Diagram images (Markdown ``) sit on a constant light card so the
- fixed-colour SVGs stay legible in dark mode — mirrors the Diagram.astro component. */
+ fixed-colour SVGs stay legible in dark mode - mirrors the Diagram.astro component. */
.post__body :global(img[src^="/diagrams/"]) {
display: block;
width: 100%;
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro
index 743a781..9242c94 100644
--- a/src/pages/blog/index.astro
+++ b/src/pages/blog/index.astro
@@ -95,7 +95,7 @@ const featuredSeries = series[featured.id];
text-decoration: none;
}
- /* featured card — text left, hero art right, scrimmed into the panel */
+ /* featured card - text left, hero art right, scrimmed into the panel */
.featured {
position: relative;
display: grid;
diff --git a/src/pages/blog/tags/[tag].astro b/src/pages/blog/tags/[tag].astro
index 98e59c3..1b1157f 100644
--- a/src/pages/blog/tags/[tag].astro
+++ b/src/pages/blog/tags/[tag].astro
@@ -18,7 +18,7 @@ const { tag, posts } = Astro.props;
---
-
+
- Lessons from edge Kubernetes, GPUs, and running infrastructure like it matters —
+ Lessons from edge Kubernetes, GPUs, and running infrastructure like it matters -
written up as I go.
Edge Kubernetes, GPU inference, self-hosted AI, productionised network automation, and
- the infrastructure rigour that ties it together — each with the problem, the design, and
+ the infrastructure rigour that ties it together - each with the problem, the design, and
the outcome. More always cooking.
diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts
index cefdcf3..24033f4 100644
--- a/src/pages/rss.xml.ts
+++ b/src/pages/rss.xml.ts
@@ -6,7 +6,7 @@ import { site } from "../data/site";
export async function GET(context: APIContext) {
const posts = await getPosts(); // drafts excluded in prod
return rss({
- title: `${site.name} — Blog`,
+ title: `${site.name} - Blog`,
description:
"Notes on platform engineering, edge Kubernetes, GPUs and homelab infrastructure.",
site: context.site ?? site.url,
diff --git a/src/scripts/anim.ts b/src/scripts/anim.ts
index ea96503..7b5f503 100644
--- a/src/scripts/anim.ts
+++ b/src/scripts/anim.ts
@@ -1,9 +1,9 @@
-/* Motion (motion.dev) — the site's unified animation layer.
+/* Motion (motion.dev) - the site's unified animation layer.
*
* Imported by an Astro-PROCESSED