sweep the last typographic punctuation out of the components and data files
build-and-deploy / build (push) Failing after 14m21s

This commit is contained in:
2026-08-17 23:21:38 +10:00
parent 3cfd59a464
commit 43188589ab
35 changed files with 159 additions and 159 deletions
+1 -1
View File
@@ -59,7 +59,7 @@ const olderCount = experience.length - recent.length;
</ol> </ol>
{olderCount > 0 && ( {olderCount > 0 && (
<p class="about__more mono"> <p class="about__more mono">
+ {olderCount} earlier roles (ELGAS, Darktime){cvAvailable ? " see the CV" : ""}. + {olderCount} earlier roles (ELGAS, Darktime){cvAvailable ? " - see the CV" : ""}.
</p> </p>
)} )}
</div> </div>
+1 -1
View File
@@ -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. // (cyan, magenta). Pure CSS, from the design handoff.
--- ---
+8 -8
View File
@@ -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). // 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 // 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). // stroke-dasharray 9/127 sliding via stroke-dashoffset. Hub = cave (56px chip, radar ring).
--- ---
@@ -42,7 +42,7 @@
<path class="t-base t-mag" d="M380 310 H470 L540 380 H602 L640 420"></path> <path class="t-base t-mag" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-pkt t-pkt--mag pkt-rev" d="M380 310 H470 L540 380 H602 L640 420"></path> <path class="t-pkt t-pkt--mag pkt-rev" d="M380 310 H470 L540 380 H602 L640 420"></path>
</g> </g>
<!-- kate (standby dimmer) --> <!-- kate (standby - dimmer) -->
<g transform="translate(2,-3)" class="standby"> <g transform="translate(2,-3)" class="standby">
<path class="t-glow t-cyan" d="M380 310 H300 L240 250 V202 L188 150 H176"></path> <path class="t-glow t-cyan" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-base t-cyan" d="M380 310 H300 L240 250 V202 L188 150 H176"></path> <path class="t-base t-cyan" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
@@ -110,7 +110,7 @@
<rect class="plate plate--hub" x="338" y="352" width="84" height="24" rx="4"></rect> <rect class="plate plate--hub" x="338" y="352" width="84" height="24" rx="4"></rect>
<text class="lbl lbl--hub" x="380" y="368" text-anchor="middle">cave</text> <text class="lbl lbl--hub" x="380" y="368" text-anchor="middle">cave</text>
<!-- ── awake: alfred (Intel compute NUC iGPU/QuickSync) ── --> <!-- ── awake: alfred (Intel compute - NUC iGPU/QuickSync) ── -->
<circle cx="612" cy="128" r="24" fill="rgba(0,199,253,0.07)"></circle> <circle cx="612" cy="128" r="24" fill="rgba(0,199,253,0.07)"></circle>
<rect class="chip chip--intel" x="595" y="111" width="34" height="34" rx="5"></rect> <rect class="chip chip--intel" x="595" y="111" width="34" height="34" rx="5"></rect>
<circle class="core core--intel pulse" cx="612" cy="128" r="4"></circle> <circle class="core core--intel pulse" cx="612" cy="128" r="4"></circle>
@@ -136,13 +136,13 @@
<rect class="plate plate--dim" x="180" y="508" width="68" height="20" rx="3"></rect> <rect class="plate plate--dim" x="180" y="508" width="68" height="20" rx="3"></rect>
<text class="lbl lbl--dim" x="214" y="522" text-anchor="middle">wgirl</text> <text class="lbl lbl--dim" x="214" y="522" text-anchor="middle">wgirl</text>
<!-- ── standby: elfastc (Intel compute 2× Arc B580) ── --> <!-- ── standby: elfastc (Intel compute - 2× Arc B580) ── -->
<rect class="chip chip--intel" x="437" y="533" width="30" height="30" rx="5"></rect> <rect class="chip chip--intel" x="437" y="533" width="30" height="30" rx="5"></rect>
<circle class="core core--intel pulse-slow" cx="452" cy="548" r="3.5"></circle> <circle class="core core--intel pulse-slow" cx="452" cy="548" r="3.5"></circle>
<rect class="plate plate--dim" x="412" y="570" width="80" height="20" rx="3"></rect> <rect class="plate plate--dim" x="412" y="570" width="80" height="20" rx="3"></rect>
<text class="lbl lbl--dim" x="452" y="584" text-anchor="middle">elfastc</text> <text class="lbl lbl--dim" x="452" y="584" text-anchor="middle">elfastc</text>
<!-- ── key: compute colours (right side left edge clips on narrow heroes) ── --> <!-- ── key: compute colours (right side - left edge clips on narrow heroes) ── -->
<g aria-label="compute colour key"> <g aria-label="compute colour key">
<rect class="core--mag" x="620" y="572" width="8" height="8" rx="2"></rect> <rect class="core--mag" x="620" y="572" width="8" height="8" rx="2"></rect>
<text class="cap cap--key" x="634" y="580">nvidia compute</text> <text class="cap cap--key" x="634" y="580">nvidia compute</text>
@@ -230,7 +230,7 @@
} }
.cap--dim { fill: #5c6a82; font-size: 9px; } .cap--dim { fill: #5c6a82; font-size: 9px; }
.cap--key { fill: #7e8ba1; 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 { .key--bat {
fill: #10131c; fill: #10131c;
stroke: rgba(245, 197, 24, 0.55); stroke: rgba(245, 197, 24, 0.55);
@@ -245,7 +245,7 @@
animation: bzRing 3.6s ease-out infinite; 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. */ (2×136 = 272) or every packet dot visibly teleports at the loop seam. */
@keyframes bzDash { to { stroke-dashoffset: -272; } } @keyframes bzDash { to { stroke-dashoffset: -272; } }
@keyframes bzDashRev { to { stroke-dashoffset: 272; } } @keyframes bzDashRev { to { stroke-dashoffset: 272; } }
+1 -1
View File
@@ -7,7 +7,7 @@ import { socials } from "../data/socials";
<div class="contact__body"> <div class="contact__body">
<p class="contact__lead"> <p class="contact__lead">
Open to conversations about platform engineering, edge infrastructure, and 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. LinkedIn or email; everything else is best-effort delivery.
</p> </p>
<a class="btn btn--primary contact__mail mono" href={`mailto:${site.email}`}> <a class="btn btn--primary contact__mail mono" href={`mailto:${site.email}`}>
+2 -2
View File
@@ -2,7 +2,7 @@
// Architecture diagrams as static SVG (authored to match the site style), shown on a // Architecture diagrams as static SVG (authored to match the site style), shown on a
// constant light card so the fixed-colour art stays legible in BOTH light and dark themes. // constant light card so the fixed-colour art stays legible in BOTH light and dark themes.
// CSP-clean: a plain same-origin <img> (img-src 'self'); no inline script, no web fonts. // CSP-clean: a plain same-origin <img> (img-src 'self'); no inline script, no web fonts.
// SVG sources live in /public/diagrams/<name>.svg the same files the blog posts embed. // SVG sources live in /public/diagrams/<name>.svg - the same files the blog posts embed.
interface Props { interface Props {
name: string; name: string;
caption?: string; caption?: string;
@@ -14,7 +14,7 @@ const captions: Record<string, string> = {
"iac-fleet": "One source of truth → AWX/Ansible → identical edge nodes, even air-gapped", "iac-fleet": "One source of truth → AWX/Ansible → identical edge nodes, even air-gapped",
homelab: "Bare metal → GitOps clusters → services, exposed outbound-only via a tunnel", homelab: "Bare metal → GitOps clusters → services, exposed outbound-only via a tunnel",
"authentik-sso": "One identity provider, 2FA at the flow; apps validate over an internal back channel", "authentik-sso": "One identity provider, 2FA at the flow; apps validate over an internal back channel",
"outbound-exposure": "No open ports the origin dials out; the edge is the perimeter", "outbound-exposure": "No open ports - the origin dials out; the edge is the perimeter",
}; };
const cap = caption ?? captions[name]; const cap = caption ?? captions[name];
--- ---
+3 -3
View File
@@ -25,7 +25,7 @@ const { kind = "generic" } = Astro.props;
</> </>
) : kind === "homelab" ? ( ) : kind === "homelab" ? (
<> <>
{/* hub and spokes the fleet */} {/* hub and spokes - the fleet */}
<line x1="200" y1="90" x2="90" y2="45" class="ln ln--c" /> <line x1="200" y1="90" x2="90" y2="45" class="ln ln--c" />
<line x1="200" y1="90" x2="90" y2="135" class="ln ln--c" /> <line x1="200" y1="90" x2="90" y2="135" class="ln ln--c" />
<line x1="200" y1="90" x2="310" y2="45" class="ln ln--m" /> <line x1="200" y1="90" x2="310" y2="45" class="ln ln--m" />
@@ -56,7 +56,7 @@ const { kind = "generic" } = Astro.props;
</> </>
) : kind === "globe" ? ( ) : kind === "globe" ? (
<> <>
{/* globe of regions a modernised multi-region estate */} {/* globe of regions - a modernised multi-region estate */}
<circle cx="200" cy="90" r="52" fill="none" stroke="rgba(63,186,245,0.35)" stroke-width="1" /> <circle cx="200" cy="90" r="52" fill="none" stroke="rgba(63,186,245,0.35)" stroke-width="1" />
<ellipse cx="200" cy="90" rx="52" ry="20" fill="none" stroke="rgba(63,186,245,0.25)" stroke-width="1" /> <ellipse cx="200" cy="90" rx="52" ry="20" fill="none" stroke="rgba(63,186,245,0.25)" stroke-width="1" />
<ellipse cx="200" cy="90" rx="20" ry="52" fill="none" stroke="rgba(63,186,245,0.25)" stroke-width="1" /> <ellipse cx="200" cy="90" rx="20" ry="52" fill="none" stroke="rgba(63,186,245,0.25)" stroke-width="1" />
@@ -91,7 +91,7 @@ const { kind = "generic" } = Astro.props;
</> </>
) : ( ) : (
<> <>
{/* pipeline commits flowing through gates to a fleet */} {/* pipeline - commits flowing through gates to a fleet */}
<line x1="40" y1="90" x2="360" y2="90" class="ln ln--c" /> <line x1="40" y1="90" x2="360" y2="90" class="ln ln--c" />
<rect x="88" y="70" width="40" height="40" rx="5" class="nd nd--c" /> <rect x="88" y="70" width="40" height="40" rx="5" class="nd nd--c" />
<rect x="180" y="70" width="40" height="40" rx="5" class="nd nd--dim" /> <rect x="180" y="70" width="40" height="40" rx="5" class="nd nd--dim" />
+3 -3
View File
@@ -23,7 +23,7 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
<p class="term__line"><span class="term__prompt">$</span> whoami</p> <p class="term__line"><span class="term__prompt">$</span> whoami</p>
<p class="term__out"> <p class="term__out">
<span class="term__name">{site.name}</span> <span class="term__name">{site.name}</span>
<span class="term__sep"></span> {site.role} <span class="term__sep"> - </span> {site.role}
</p> </p>
<p class="term__line"><span class="term__prompt">$</span> traceroute www.bztmon.com</p> <p class="term__line"><span class="term__prompt">$</span> traceroute www.bztmon.com</p>
@@ -42,7 +42,7 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
<p class="term__out"> <p class="term__out">
ping me on{" "} ping me on{" "}
<a href={linkedinHref} rel="noopener noreferrer" target="_blank">LinkedIn</a>{" "} <a href={linkedinHref} rel="noopener noreferrer" target="_blank">LinkedIn</a>{" "}
or <a href={emailHref}>email</a> everything else is best-effort delivery. or <a href={emailHref}>email</a> - everything else is best-effort delivery.
</p> </p>
<p class="term__line" aria-hidden="true"> <p class="term__line" aria-hidden="true">
@@ -104,7 +104,7 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
letter-spacing: 0.03em; letter-spacing: 0.03em;
box-shadow: 0 0 24px -14px var(--accent-glow); box-shadow: 0 0 24px -14px var(--accent-glow);
} }
/* faint CRT scanlines across the whole session (static not motion) */ /* faint CRT scanlines across the whole session (static - not motion) */
.term::after { .term::after {
content: ""; content: "";
position: absolute; position: absolute;
+5 -5
View File
@@ -66,7 +66,7 @@ import Constellation from "./Constellation.astro";
<div class="hero__scroll mono" aria-hidden="true"> <div class="hero__scroll mono" aria-hidden="true">
<span class="hero__diamond"></span> <span class="hero__diamond"></span>
<span>SCROLL DESCENDING: EDGE / CLUSTER / WORKLOAD / SECURITY</span> <span>SCROLL - DESCENDING: EDGE / CLUSTER / WORKLOAD / SECURITY</span>
</div> </div>
</section> </section>
@@ -168,7 +168,7 @@ import Constellation from "./Constellation.astro";
line-height: 1.05; line-height: 1.05;
letter-spacing: -0.005em; letter-spacing: -0.005em;
padding-bottom: 0.14em; 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 */ of the constellation's left-most node labels */
max-width: min-content; max-width: min-content;
background: background:
@@ -219,7 +219,7 @@ import Constellation from "./Constellation.astro";
gap: var(--space-3); gap: var(--space-3);
} }
/* the typing bar glass panel */ /* the typing bar - glass panel */
.hero__fact { .hero__fact {
margin-top: var(--space-6); margin-top: var(--space-6);
display: flex; display: flex;
@@ -232,7 +232,7 @@ import Constellation from "./Constellation.astro";
backdrop-filter: var(--panel-blur); backdrop-filter: var(--panel-blur);
-webkit-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 /* 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; width: max-content;
max-width: min(52rem, calc(100vw - 2 * var(--space-5))); max-width: min(52rem, calc(100vw - 2 * var(--space-5)));
min-height: 20px; min-height: 20px;
@@ -304,7 +304,7 @@ import Constellation from "./Constellation.astro";
@media (max-width: 560px) { @media (max-width: 560px) {
.hero__viz { display: none; } .hero__viz { display: none; }
.hero__scroll { 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 */ the desktop one-liner (nowrap + max-content) busts a 390px viewport */
.hero__fact { .hero__fact {
font-size: 12px; font-size: 12px;
+4 -4
View File
@@ -5,7 +5,7 @@ import { site } from "../data/site";
// `id` ties a nav link to a homepage section so the Motion layer animates its // `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 // 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 // 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 = [ const links = [
{ label: "About", href: "/#about", id: "about" }, { label: "About", href: "/#about", id: "about" },
{ label: "Skills", href: "/#skills", id: "skills" }, { label: "Skills", href: "/#skills", id: "skills" },
@@ -17,7 +17,7 @@ const links = [
<header class="nav"> <header class="nav">
<div class="container nav__inner"> <div class="container nav__inner">
<a class="nav__brand mono" href="/" aria-label={`${site.name} home`}> <a class="nav__brand mono" href="/" aria-label={`${site.name} - home`}>
<span class="nav__prompt">~/</span><span>{site.handle}</span><span class="nav__caret" aria-hidden="true">▮</span> <span class="nav__prompt">~/</span><span>{site.handle}</span><span class="nav__caret" aria-hidden="true">▮</span>
</a> </a>
@@ -95,7 +95,7 @@ const links = [
color: var(--text); color: var(--text);
text-decoration: none; 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. */ (spring). transform-origin left so it grows from the start of the label. */
.nav__ul { .nav__ul {
position: absolute; position: absolute;
@@ -120,7 +120,7 @@ const links = [
display: flex; 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 */ the bar wraps: brand + toggle on row 1, links across row 2 */
@media (max-width: 719px) { @media (max-width: 719px) {
.nav__inner { .nav__inner {
+1 -1
View File
@@ -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. // tag chips, mono meta footer. Facelift shape from the design handoff.
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import DossierThumb from "./DossierThumb.astro"; import DossierThumb from "./DossierThumb.astro";
+1 -1
View File
@@ -25,7 +25,7 @@ const { id, eyebrow, title, index } = Astro.props;
.section__head { .section__head {
margin-bottom: var(--space-6); 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 { .section__head .eyebrow {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
+2 -2
View File
@@ -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). // (span: hero 2×2 flagship · wide 2×1 · full row · default 1×1).
import type { SkillGroup } from "../data/skills"; import type { SkillGroup } from "../data/skills";
@@ -181,7 +181,7 @@ const spanClass =
padding: 5px 10px; 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 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 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. */ (58px 58px 760px), squishing the first tile to ~130px in landscape phone. */
+3 -3
View File
@@ -7,7 +7,7 @@ const projects = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/projects" }), loader: glob({ pattern: "**/*.md", base: "./src/content/projects" }),
schema: z.object({ schema: z.object({
title: z.string(), 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(), outcome: z.string(),
summary: z.string(), summary: z.string(),
role: 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({ const blog = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }), loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
schema: z.object({ schema: z.object({
@@ -36,7 +36,7 @@ const blog = defineCollection({
summary: z.string(), summary: z.string(),
tags: z.array(z.string()).default([]), tags: z.array(z.string()).default([]),
draft: z.boolean().default(false), draft: z.boolean().default(false),
// Optional hero image a /blog/<slug>.webp in public/. Rendered on the post + as the card thumbnail. // Optional hero image - a /blog/<slug>.webp in public/. Rendered on the post + as the card thumbnail.
hero: z.string().optional(), hero: z.string().optional(),
heroAlt: z.string().optional(), heroAlt: z.string().optional(),
}), }),
+7 -7
View File
@@ -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." 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." 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" role: "Infrastructure / DevOps Engineer · Woolworths"
period: "2025 Present" period: "2025 - Present"
stack: ["Kubernetes", "Edge", "NVIDIA GPU", "CD pipelines", "Helm", "Python"] stack: ["Kubernetes", "Edge", "NVIDIA GPU", "CD pipelines", "Helm", "Python"]
featured: true featured: true
order: 10 order: 10
@@ -16,12 +16,12 @@ diagram: "edge-ai"
Edge AI at retail scale lives or dies on repeatability. A computer-vision workload that 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, 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 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." the gap between "it works" and "it ships."
## Constraints ## 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, - **GPU timing.** Inference pods must never schedule before the GPU device plugin is healthy,
or they crash-loop and poison the rollout. or they crash-loop and poison the rollout.
- **Heterogeneous stores.** Per-site variables (network, hardware, identity) without forking - **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 ## 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. GPU at boot.
- **Single source of truth** for config drift can't creep in store-to-store. - **Single source of truth** for config - drift can't creep in store-to-store.
- **Spec-driven, documented-as-code** the deployment *is* the documentation. - **Spec-driven, documented-as-code** - the deployment *is* the documentation.
## Outcome ## Outcome
A high-level idea becomes a real, repeatable deployment on a single press. New edge sites 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. by a pipeline anyone on the team can trigger.
## Future improvements ## Future improvements
@@ -1,9 +1,9 @@
--- ---
title: "Global Infrastructure Modernisation" 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." 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" 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"] stack: ["VMware / vSphere", "Azure (Blob, AVS)", "Microsoft 365", "SD-WAN", "Aruba ClearPass", "Palo Alto / FortiGate", "Veeam"]
featured: false featured: false
order: 40 order: 40
@@ -14,13 +14,13 @@ cover: "global-infra-modernisation"
Enterprise estates accrete. Flat networks, sprawling VM counts, aging firewalls, and 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, 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 ## Constraints
- **Keep the lights on** change a live, multi-region estate without downtime. - **Keep the lights on** - change a live, multi-region estate without downtime.
- **Security and compliance** segmentation, patching, and auditability throughout. - **Security and compliance** - segmentation, patching, and auditability throughout.
- **Cost-aware** modernise to cloud where it pays, justified through CapEx/OpEx cases. - **Cost-aware** - modernise to cloud where it pays, justified through CapEx/OpEx cases.
## Design ## 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 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 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 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. 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 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 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** 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 ## Security & reliability decisions
- **Flat → segmented** isolation by design, not by exception. - **Flat → segmented** - isolation by design, not by exception.
- **Authenticated access** (ClearPass, 802.1x) the network knows who's on it. - **Authenticated access** (ClearPass, 802.1x) - the network knows who's on it.
- **Patched, current firewalls** closing the easy doors first. - **Patched, current firewalls** - closing the easy doors first.
- **DR built in** recovery designed, not assumed. - **DR built in** - recovery designed, not assumed.
## Outcome ## 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. delivered against live-business constraints across multiple regions.
## Future improvements ## Future improvements
+8 -8
View File
@@ -1,9 +1,9 @@
--- ---
title: "GPU-as-Code on the Edge" 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." 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" role: "Infrastructure / DevOps Engineer"
period: "2025 Present" period: "2025 - Present"
stack: ["GPU passthrough", "ESXi", "DCGM Exporter", "Prometheus", "Bash", "Watchdogs"] stack: ["GPU passthrough", "ESXi", "DCGM Exporter", "Prometheus", "Bash", "Watchdogs"]
featured: false featured: false
order: 30 order: 30
@@ -18,9 +18,9 @@ to refuse to start until both are true. Doing that by hand, per site, doesn't sc
## Constraints ## Constraints
- **As-code, not click-ops** GPU passthrough defined in code, not the ESXi UI. - **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. - **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. - **Observable** - GPU health has to be visible alongside the rest of the platform.
## Design ## Design
@@ -33,9 +33,9 @@ platform signal does.
## Security & reliability decisions ## Security & reliability decisions
- **Readiness gating** pods wait for the hardware; no boot-time races. - **Readiness gating** - pods wait for the hardware; no boot-time races.
- **End-state manifests** the node's GPU config is declarative and reproducible. - **End-state manifests** - the node's GPU config is declarative and reproducible.
- **DCGM telemetry** GPU failures are detected, not discovered. - **DCGM telemetry** - GPU failures are detected, not discovered.
## Outcome ## Outcome
+11 -11
View File
@@ -1,9 +1,9 @@
--- ---
title: "IaC Fleet Automation" title: "IaC Fleet Automation"
outcome: "Stood up identical edge sites from code every store comes up the same way, every time." 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." 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" role: "Automation Engineer"
period: "2025 Present" period: "2025 - Present"
stack: ["Ansible", "AWX", "GitOps", "ACR / NVCR", "Image pre-pull", "Secrets mgmt"] stack: ["Ansible", "AWX", "GitOps", "ACR / NVCR", "Image pre-pull", "Secrets mgmt"]
featured: true featured: true
order: 20 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 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 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 ## Constraints
- **Repeatability over cleverness** the same playbook must produce the same node anywhere. - **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. - **Air-gapped reality** - disconnected edge sites still have to build from local images.
- **No secrets in code** credentials delivered at deploy time, never committed. - **No secrets in code** - credentials delivered at deploy time, never committed.
## Design ## Design
@@ -36,14 +36,14 @@ internet path at the moment it matters.
## Security & reliability decisions ## Security & reliability decisions
- **Secrets management at deploy time** nothing sensitive in git. - **Secrets management at deploy time** - nothing sensitive in git.
- **Pre-staged, mirrored images** supply chain stays available and pinned, even offline. - **Pre-staged, mirrored images** - supply chain stays available and pinned, even offline.
- **AWX job-level reporting** every run is visible and auditable. - **AWX job-level reporting** - every run is visible and auditable.
## Outcome ## Outcome
New edge sites are provisioned from code with consistent results, manual build steps are 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. instead of a runbook.
## Future improvements ## Future improvements
@@ -1,9 +1,9 @@
--- ---
title: "Network Automation at Fleet Scale" 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." 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." 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" 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"] stack: ["Unimus (NCM)", "NetBox (IPAM / SoT)", "Config backup & DR", "Bulk config push", "Credential vaulting + rotation", "Multi-vendor switching"]
featured: false featured: false
order: 45 order: 45
@@ -18,49 +18,49 @@ links:
## Problem ## 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 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 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. that production depends on, there was no real recovery path and no audit trail.
## Constraints ## Constraints
- **Live production network** change the fleet without taking sites down. - **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. - **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. - **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. - **Cost-justified** - a lean tool that earns its place through a real CapEx/OpEx case.
## Design ## 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, 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 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 place: **automated, scheduled config backups**, **change tracking with config diffs**, and
**mass / bulk config push** across many devices at once. **mass / bulk config push** across many devices at once.
Paired with it, **NetBox** served as the **IPAM and source-of-truth** for the device 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 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 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 ## 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. scoped, rotated place, never embedded in scripts or shared by hand.
- **Config backup = real network DR** every device restorable from a known-good config. - **Config backup = real network DR** - every device restorable from a known-good config.
- **Diffs + auditing** every configuration change is visible, attributable, and reversible. - **Diffs + auditing** - every configuration change is visible, attributable, and reversible.
- **Source-of-truth driven** automation acts on NetBox's model, not on guesswork. - **Source-of-truth driven** - automation acts on NetBox's model, not on guesswork.
## Outcome ## Outcome
The switch fleet across AU/NZ/PNG was maintained from a single pane: configs backed up 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 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. community toolset, productionised with enterprise rigour.
## Future improvements ## Future improvements
Drive the automation deeper from the source-of-truth intent in NetBox, pushed to the fleet 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 through Unimus - and template standard site configs so a new location onboards straight from
the inventory. the inventory.
+12 -12
View File
@@ -1,6 +1,6 @@
--- ---
title: "Self-Hosted AI & Homelab Platform" 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." 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" role: "Owner / Operator"
period: "Ongoing" period: "Ongoing"
@@ -13,38 +13,38 @@ diagram: "homelab"
## Problem ## 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 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. genuine proving ground for Kubernetes, GPUs, AI and security, not a pile of containers.
## Constraints ## Constraints
- **Run it like production** GitOps, backups, observability, no snowflake config. - **Run it like production** - GitOps, backups, observability, no snowflake config.
- **Secure by default** nothing exposed that doesn't need to be. - **Secure by default** - nothing exposed that doesn't need to be.
- **Reproducible** rebuild a node from code, not from memory. - **Reproducible** - rebuild a node from code, not from memory.
## Design ## Design
Proxmox provides the hypervisor layer with **PCIe passthrough** (GPU and storage) into 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 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 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 **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. which is exactly how this site is served.
## Security & reliability decisions ## Security & reliability decisions
- **GitOps as the source of truth** drift is reconciled, not chased. - **GitOps as the source of truth** - drift is reconciled, not chased.
- **2FA / SSO and segmented access** least privilege across the lab. - **2FA / SSO and segmented access** - least privilege across the lab.
- **Back up state, not just volumes** restores are drilled, not hoped for. - **Back up state, not just volumes** - restores are drilled, not hoped for.
- **Outbound-only public exposure** a tunnel, not an open port. - **Outbound-only public exposure** - a tunnel, not an open port.
## Outcome ## Outcome
A homelab that behaves like a platform: rebuildable from code, observable, backed up, and 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 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 ## Future improvements
+11 -11
View File
@@ -13,33 +13,33 @@ export const experience: Role[] = [
{ {
title: "Infrastructure / DevOps Engineer", title: "Infrastructure / DevOps Engineer",
org: "Woolworths", org: "Woolworths",
period: "Aug 2025 Present", period: "Aug 2025 - Present",
summary: 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: [ highlights: [
"Single-touch deployment of an edge AI solution running on Kubernetes at the store edge", "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", "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", title: "Infrastructure Systems Engineer",
org: "Virtus Health", org: "Virtus Health",
period: "Aug 2022 Aug 2025", period: "Aug 2022 - Aug 2025",
summary: summary:
"Global IT optimisation, implementation and projects across an enterprise estate.", "Global IT - optimisation, implementation and projects across an enterprise estate.",
highlights: [ highlights: [
"Managed ~1,000 VMs across a global VMware estate", "Managed ~1,000 VMs across a global VMware estate",
"Re-segmented flat sites into isolated VLAN ranges; SD-WAN and Aruba ClearPass onboarding", "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", "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", title: "Global IT Infrastructure Engineer",
org: "Linde Asia Pacific", org: "Linde Asia Pacific",
period: "Sep 2019 Aug 2022", period: "Sep 2019 - Aug 2022",
summary: summary:
"Maintained and modernised global infrastructure to enterprise standards across the region.", "Maintained and modernised global infrastructure to enterprise standards across the region.",
highlights: [ highlights: [
@@ -51,7 +51,7 @@ export const experience: Role[] = [
{ {
title: "Web Developer / Tech Admin", title: "Web Developer / Tech Admin",
org: "ELGAS", org: "ELGAS",
period: "Aug 2018 Sep 2019", period: "Aug 2018 - Sep 2019",
summary: summary:
"Development, optimisation and escalation for the IT service-desk team.", "Development, optimisation and escalation for the IT service-desk team.",
highlights: [ highlights: [
@@ -62,12 +62,12 @@ export const experience: Role[] = [
{ {
title: "IT Technician", title: "IT Technician",
org: "Darktime Computer Services", org: "Darktime Computer Services",
period: "Nov 2010 Jun 2013", period: "Nov 2010 - Jun 2013",
summary: summary:
"Field and workshop IT across legal firms, schools, real estate and small business.", "Field and workshop IT across legal firms, schools, real estate and small business.",
highlights: [ highlights: [
"SBS 2008 server installs, Exchange, backups and RAID storage for small business", "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",
], ],
}, },
]; ];
+5 -5
View File
@@ -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 // 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). // (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 = [ export const fleetFacts = [
"6 single-node Talos clusters · 0 SSH daemons · every machine is a document", "6 single-node Talos clusters · 0 SSH daemons · every machine is a document",
"139 ArgoCD Applications reconciled from one hub", "139 ArgoCD Applications reconciled from one hub",
"170+ CiliumNetworkPolicies fleet-wide every namespace starts at default-deny", "170+ CiliumNetworkPolicies fleet-wide - every namespace starts at default-deny",
"zero open inbound ports the origin dials out", "zero open inbound ports - the origin dials out",
"*.bztmon.org a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed", "*.bztmon.org - a Let's Encrypt wildcard per cluster · DNS-01 · auto-renewed",
"~140 pods running right now, reconciled by git", "~140 pods running right now, reconciled by git",
] as const; ] as const;
+2 -2
View File
@@ -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. // Edit here, not in components.
export const site = { export const site = {
@@ -20,7 +20,7 @@ export const site = {
"Edge AI", "Edge AI",
], ],
// One-paragraph elevator pitch for the About section. // 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). // Canonical URL (used for OG/sitemap/RSS).
url: "https://www.bztmon.com", url: "https://www.bztmon.com",
+12 -12
View File
@@ -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. // Driven entirely by this file; the Skills section renders whatever is here.
export type SkillGroup = { export type SkillGroup = {
title: string; title: string;
// Short framing line for the group. // Short framing line for the group.
blurb: string; 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. */ * tall tiles (hero / full) so the tile reads as substance, not whitespace. */
points?: string[]; points?: string[];
items: 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"; span?: "hero" | "wide" | "full";
/** Accent flavour "magenta" marks security-flavoured tiles. */ /** Accent flavour - "magenta" marks security-flavoured tiles. */
flavor?: "magenta"; flavor?: "magenta";
/** Eyebrow label rendered on the hero tile. */ /** Eyebrow label rendered on the hero tile. */
flag?: string; flag?: string;
@@ -24,10 +24,10 @@ export const skills: SkillGroup[] = [
flag: "FLAGSHIP", flag: "FLAGSHIP",
blurb: "Serving vision and language models on real hardware at the edge.", blurb: "Serving vision and language models on real hardware at the edge.",
points: [ points: [
"Fail-closed computer vision on single-node OpenShift probes that catch a silently dead model, not just a dead pod.", "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.", "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.", "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: [ items: [
"NVIDIA GPU Operator", "NVIDIA GPU Operator",
@@ -56,7 +56,7 @@ export const skills: SkillGroup[] = [
}, },
{ {
title: "Automation & IaC", 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: [ items: [
"Ansible / AWX", "Ansible / AWX",
"PowerShell (PowerCLI)", "PowerShell (PowerCLI)",
@@ -97,11 +97,11 @@ export const skills: SkillGroup[] = [
{ {
title: "Observability & Ops", title: "Observability & Ops",
span: "full", 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: [ points: [
"Fleet metrics by remote-write every cluster ships to one Prometheus, GPUs included (DCGM / Intel Xe).", "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.", "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.", "A backup is a rumour until a restore drill proves it - drills are scheduled, not aspirational.",
], ],
items: [ items: [
"Prometheus", "Prometheus",
+1 -1
View File
@@ -11,7 +11,7 @@ export type Social = {
export const socials: 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", label: "Source",
href: "https://git.bztmon.com/jwright", href: "https://git.bztmon.com/jwright",
external: true, external: true,
+2 -2
View File
@@ -7,7 +7,7 @@ import { site } from "../data/site";
interface Props { interface Props {
title?: string; title?: string;
description?: 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; path?: string;
} }
@@ -17,7 +17,7 @@ const {
path = "/", path = "/",
} = Astro.props; } = 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 canonical = new URL(path, site.url).href;
const ogImage = new URL(site.ogImage, site.url).href; const ogImage = new URL(site.ogImage, site.url).href;
--- ---
+1 -1
View File
@@ -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. // The footer renders these as the "receipt" line; values are baked into the static HTML.
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
+1 -1
View File
@@ -2,7 +2,7 @@
import Layout from "../layouts/Layout.astro"; import Layout from "../layouts/Layout.astro";
--- ---
<Layout title="404 Not found" path="/404"> <Layout title="404 - Not found" path="/404">
<section class="nf"> <section class="nf">
<div class="container nf__inner"> <div class="container nf__inner">
<p class="eyebrow">Error 404</p> <p class="eyebrow">Error 404</p>
+1 -1
View File
@@ -130,7 +130,7 @@ const { Content } = await render(entry);
margin-top: var(--space-7); margin-top: var(--space-7);
} }
/* Diagram images (Markdown `![](/diagrams/*.svg)`) sit on a constant light card so the /* Diagram images (Markdown `![](/diagrams/*.svg)`) 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/"]) { .post__body :global(img[src^="/diagrams/"]) {
display: block; display: block;
width: 100%; width: 100%;
+1 -1
View File
@@ -95,7 +95,7 @@ const featuredSeries = series[featured.id];
text-decoration: none; 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 { .featured {
position: relative; position: relative;
display: grid; display: grid;
+1 -1
View File
@@ -18,7 +18,7 @@ const { tag, posts } = Astro.props;
--- ---
<Layout title={`#${tag}`} path={`/blog/tags/${tag}/`} description={`Posts tagged ${tag}.`}> <Layout title={`#${tag}`} path={`/blog/tags/${tag}/`} description={`Posts tagged ${tag}.`}>
<Section id="tag" eyebrow="Tag" index="#" title={`Posts tagged ${tag}`}> <Section id="tag" eyebrow="Tag" index="#" title={`Posts tagged "${tag}"`}>
<p class="tag-page__back" data-reveal> <p class="tag-page__back" data-reveal>
<a class="btn" href="/blog/">← All posts</a> <a class="btn" href="/blog/">← All posts</a>
</p> </p>
+1 -1
View File
@@ -46,7 +46,7 @@ const postTags = allTags(allPosts);
<Section id="writing" eyebrow="Writing" index="04" title="Notes from the build."> <Section id="writing" eyebrow="Writing" index="04" title="Notes from the build.">
<p class="writing__intro" data-reveal> <p class="writing__intro" data-reveal>
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. written up as I go.
</p> </p>
{postTags.length > 0 && ( {postTags.length > 0 && (
+1 -1
View File
@@ -23,7 +23,7 @@ const { Content } = await render(entry);
<figure class="case__cover" data-reveal> <figure class="case__cover" data-reveal>
<img <img
src={`/covers/${p.cover}.webp`} src={`/covers/${p.cover}.webp`}
alt={`${p.title} cover art`} alt={`${p.title} - cover art`}
width="1600" width="1600"
height="1073" height="1073"
loading="eager" loading="eager"
+2 -2
View File
@@ -10,10 +10,10 @@ const projects = (await getCollection("projects")).sort(
--- ---
<Layout title="Projects" path="/projects" description="Selected platform & infrastructure projects by Jonathon Wright."> <Layout title="Projects" path="/projects" description="Selected platform & infrastructure projects by Jonathon Wright.">
<Section id="all-projects" eyebrow="Projects" index="*" title="Some bytes I'm proud of a starting line, not a trophy shelf."> <Section id="all-projects" eyebrow="Projects" index="*" title="Some bytes I'm proud of - a starting line, not a trophy shelf.">
<p class="lead projects__intro" data-reveal> <p class="lead projects__intro" data-reveal>
Edge Kubernetes, GPU inference, self-hosted AI, productionised network automation, and 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. the outcome. More always cooking.
</p> </p>
<div class="grid"> <div class="grid">
+1 -1
View File
@@ -6,7 +6,7 @@ import { site } from "../data/site";
export async function GET(context: APIContext) { export async function GET(context: APIContext) {
const posts = await getPosts(); // drafts excluded in prod const posts = await getPosts(); // drafts excluded in prod
return rss({ return rss({
title: `${site.name} Blog`, title: `${site.name} - Blog`,
description: description:
"Notes on platform engineering, edge Kubernetes, GPUs and homelab infrastructure.", "Notes on platform engineering, edge Kubernetes, GPUs and homelab infrastructure.",
site: context.site ?? site.url, site: context.site ?? site.url,
+12 -12
View File
@@ -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 <script> in Layout.astro, so Vite bundles it * Imported by an Astro-PROCESSED <script> in Layout.astro, so Vite bundles it
* into a self-hosted /_astro/[hash].js stays within CSP `script-src 'self'` * into a self-hosted /_astro/[hash].js stays within CSP `script-src 'self'`
* (no inline, no CDN). Motion sets styles via JS (WAAPI / element.style), which * (no inline, no CDN). Motion sets styles via JS (WAAPI / element.style), which
* is allowed under `style-src` so no `unsafe-inline` is introduced. * is allowed under `style-src` - so no `unsafe-inline` is introduced.
* *
* Progressive enhancement: every animated element is VISIBLE with JS off. The * Progressive enhancement: every animated element is VISIBLE with JS off. The
* pre-paint head script (site.js) adds `html.js`; only THEN does CSS hide the * pre-paint head script (site.js) adds `html.js`; only THEN does CSS hide the
@@ -13,7 +13,7 @@
import { animate, inView, scroll, stagger } from "motion"; import { animate, inView, scroll, stagger } from "motion";
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// easeOutExpo-ish confident, decelerating; good for entrances/reveals. // easeOutExpo-ish - confident, decelerating; good for entrances/reveals.
const EASE = [0.16, 1, 0.3, 1] as const; const EASE = [0.16, 1, 0.3, 1] as const;
if (!reduce) { if (!reduce) {
@@ -28,7 +28,7 @@ if (!reduce) {
} }
function run(): void { function run(): void {
/* 1. Hero staggered fade-up on load. */ /* 1. Hero - staggered fade-up on load. */
const heroEls = document.querySelectorAll<HTMLElement>(".hero__inner > *"); const heroEls = document.querySelectorAll<HTMLElement>(".hero__inner > *");
if (heroEls.length) { if (heroEls.length) {
animate( animate(
@@ -38,7 +38,7 @@ function run(): void {
); );
} }
/* 2. Scroll reveals section heads, skill cards (+ staggered chips), CTAs. /* 2. Scroll reveals - section heads, skill cards (+ staggered chips), CTAs.
* inView fires as each [data-reveal] enters; re-fires on re-entry. */ * inView fires as each [data-reveal] enters; re-fires on re-entry. */
inView( inView(
"[data-reveal]", "[data-reveal]",
@@ -56,7 +56,7 @@ function run(): void {
{ amount: 0.2 }, { amount: 0.2 },
); );
/* 3. Backdrop parallax drive --bg-shift from scroll progress (replaces the /* 3. Backdrop parallax - drive --bg-shift from scroll progress (replaces the
* hand-rolled rAF listener; Motion throttles via ScrollTimeline/rAF). */ * hand-rolled rAF listener; Motion throttles via ScrollTimeline/rAF). */
const bg = document.querySelectorAll<HTMLElement>(".site-bg__img"); const bg = document.querySelectorAll<HTMLElement>(".site-bg__img");
if (bg.length) { if (bg.length) {
@@ -66,7 +66,7 @@ function run(): void {
}); });
} }
/* 4. Top scroll-progress bar cross-browser (replaces the Chromium-only CSS /* 4. Top scroll-progress bar - cross-browser (replaces the Chromium-only CSS
* animation-timeline). Drive scaleX STRAIGHT from scroll progress via the * animation-timeline). Drive scaleX STRAIGHT from scroll progress via the
* callback form: scroll(animate(...)) snapped the bar back to 0 at the very * callback form: scroll(animate(...)) snapped the bar back to 0 at the very
* bottom (the progress=1 boundary); the callback maps 01 cleanly both ways. */ * bottom (the progress=1 boundary); the callback maps 01 cleanly both ways. */
@@ -83,10 +83,10 @@ function run(): void {
setBar(); setBar();
} }
/* 5. Nav animated active-section underline + tactile hover. */ /* 5. Nav - animated active-section underline + tactile hover. */
initNav(); initNav();
/* 6. Buttons spring lift on hover, press on tap. */ /* 6. Buttons - spring lift on hover, press on tap. */
initButtons(); initButtons();
} }
@@ -119,9 +119,9 @@ function initNav(): void {
for (const a of links) { for (const a of links) {
a.addEventListener("pointerenter", () => under(a, true)); a.addEventListener("pointerenter", () => under(a, true));
// On leave, keep the underline only if this link IS the active scroll-spy // On leave, keep the underline only if this link IS the active scroll-spy
// section. Guard on activeIdx >= 0 so that when nothing is active e.g. the // section. Guard on activeIdx >= 0 so that when nothing is active - e.g. the
// Projects/Blog pages, where none of these sections exist and activeIdx stays // Projects/Blog pages, where none of these sections exist and activeIdx stays
// -1 a hovered tab doesn't stick lit via the -1 === indexOf(non-member) -1. // -1 - a hovered tab doesn't stick lit via the -1 === indexOf(non-member) -1.
a.addEventListener("pointerleave", () => a.addEventListener("pointerleave", () =>
under(a, activeIdx >= 0 && sectioned.indexOf(a) === activeIdx), under(a, activeIdx >= 0 && sectioned.indexOf(a) === activeIdx),
); );
@@ -141,7 +141,7 @@ function initNav(): void {
if (sec && sec.getBoundingClientRect().top <= ref) idx = i; if (sec && sec.getBoundingClientRect().top <= ref) idx = i;
}); });
// At the very bottom the last (often short) section may never push its top // At the very bottom the last (often short) section may never push its top
// above the line force it so the final item lights up. // above the line - force it so the final item lights up.
if (window.scrollY + vh >= document.documentElement.scrollHeight - 4) { if (window.scrollY + vh >= document.documentElement.scrollHeight - 4) {
idx = sectioned.length - 1; idx = sectioned.length - 1;
} }