sweep the last typographic punctuation out of the components and data files
build-and-deploy / build (push) Failing after 14m21s
build-and-deploy / build (push) Failing after 14m21s
This commit is contained in:
@@ -59,7 +59,7 @@ const olderCount = experience.length - recent.length;
|
||||
</ol>
|
||||
{olderCount > 0 && (
|
||||
<p class="about__more mono">
|
||||
+ {olderCount} earlier roles (ELGAS, Darktime){cvAvailable ? " — see the CV" : ""}.
|
||||
+ {olderCount} earlier roles (ELGAS, Darktime){cvAvailable ? " - see the CV" : ""}.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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.
|
||||
---
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
<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>
|
||||
</g>
|
||||
<!-- kate (standby — dimmer) -->
|
||||
<!-- kate (standby - dimmer) -->
|
||||
<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-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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -136,13 +136,13 @@
|
||||
<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>
|
||||
|
||||
<!-- ── 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>
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -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; } }
|
||||
|
||||
@@ -7,7 +7,7 @@ import { socials } from "../data/socials";
|
||||
<div class="contact__body">
|
||||
<p class="contact__lead">
|
||||
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.
|
||||
</p>
|
||||
<a class="btn btn--primary contact__mail mono" href={`mailto:${site.email}`}>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 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.
|
||||
// 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 {
|
||||
name: 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",
|
||||
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",
|
||||
"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];
|
||||
---
|
||||
|
||||
@@ -25,7 +25,7 @@ const { kind = "generic" } = Astro.props;
|
||||
</>
|
||||
) : 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="135" class="ln ln--c" />
|
||||
<line x1="200" y1="90" x2="310" y2="45" class="ln ln--m" />
|
||||
@@ -56,7 +56,7 @@ const { kind = "generic" } = Astro.props;
|
||||
</>
|
||||
) : 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" />
|
||||
<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" />
|
||||
@@ -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" />
|
||||
<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" />
|
||||
|
||||
@@ -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__out">
|
||||
<span class="term__name">{site.name}</span>
|
||||
<span class="term__sep">—</span> {site.role}
|
||||
<span class="term__sep"> - </span> {site.role}
|
||||
</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">
|
||||
ping me on{" "}
|
||||
<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 class="term__line" aria-hidden="true">
|
||||
@@ -104,7 +104,7 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
|
||||
letter-spacing: 0.03em;
|
||||
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 {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
@@ -66,7 +66,7 @@ import Constellation from "./Constellation.astro";
|
||||
|
||||
<div class="hero__scroll mono" aria-hidden="true">
|
||||
<span class="hero__diamond"></span>
|
||||
<span>SCROLL — DESCENDING: EDGE / CLUSTER / WORKLOAD / SECURITY</span>
|
||||
<span>SCROLL - DESCENDING: EDGE / CLUSTER / WORKLOAD / SECURITY</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
<header class="nav">
|
||||
<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>
|
||||
</a>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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/<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(),
|
||||
heroAlt: z.string().optional(),
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+11
-11
@@ -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",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
+12
-12
@@ -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",
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
---
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<Layout title="404 — Not found" path="/404">
|
||||
<Layout title="404 - Not found" path="/404">
|
||||
<section class="nf">
|
||||
<div class="container nf__inner">
|
||||
<p class="eyebrow">Error 404</p>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ const { tag, posts } = Astro.props;
|
||||
---
|
||||
|
||||
<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>
|
||||
<a class="btn" href="/blog/">← All posts</a>
|
||||
</p>
|
||||
|
||||
@@ -46,7 +46,7 @@ const postTags = allTags(allPosts);
|
||||
|
||||
<Section id="writing" eyebrow="Writing" index="04" title="Notes from the build.">
|
||||
<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.
|
||||
</p>
|
||||
{postTags.length > 0 && (
|
||||
|
||||
@@ -23,7 +23,7 @@ const { Content } = await render(entry);
|
||||
<figure class="case__cover" data-reveal>
|
||||
<img
|
||||
src={`/covers/${p.cover}.webp`}
|
||||
alt={`${p.title} — cover art`}
|
||||
alt={`${p.title} - cover art`}
|
||||
width="1600"
|
||||
height="1073"
|
||||
loading="eager"
|
||||
|
||||
@@ -10,10 +10,10 @@ const projects = (await getCollection("projects")).sort(
|
||||
---
|
||||
|
||||
<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>
|
||||
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.
|
||||
</p>
|
||||
<div class="grid">
|
||||
|
||||
@@ -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,
|
||||
|
||||
+12
-12
@@ -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
|
||||
* 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
|
||||
* 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
|
||||
* 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";
|
||||
|
||||
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;
|
||||
|
||||
if (!reduce) {
|
||||
@@ -28,7 +28,7 @@ if (!reduce) {
|
||||
}
|
||||
|
||||
function run(): void {
|
||||
/* 1. Hero — staggered fade-up on load. */
|
||||
/* 1. Hero - staggered fade-up on load. */
|
||||
const heroEls = document.querySelectorAll<HTMLElement>(".hero__inner > *");
|
||||
if (heroEls.length) {
|
||||
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(
|
||||
"[data-reveal]",
|
||||
@@ -56,7 +56,7 @@ function run(): void {
|
||||
{ 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). */
|
||||
const bg = document.querySelectorAll<HTMLElement>(".site-bg__img");
|
||||
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
|
||||
* callback form: scroll(animate(...)) snapped the bar back to 0 at the very
|
||||
* bottom (the progress=1 boundary); the callback maps 0→1 cleanly both ways. */
|
||||
@@ -83,10 +83,10 @@ function run(): void {
|
||||
setBar();
|
||||
}
|
||||
|
||||
/* 5. Nav — animated active-section underline + tactile hover. */
|
||||
/* 5. Nav - animated active-section underline + tactile hover. */
|
||||
initNav();
|
||||
|
||||
/* 6. Buttons — spring lift on hover, press on tap. */
|
||||
/* 6. Buttons - spring lift on hover, press on tap. */
|
||||
initButtons();
|
||||
}
|
||||
|
||||
@@ -119,9 +119,9 @@ function initNav(): void {
|
||||
for (const a of links) {
|
||||
a.addEventListener("pointerenter", () => under(a, true));
|
||||
// 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
|
||||
// -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", () =>
|
||||
under(a, activeIdx >= 0 && sectioned.indexOf(a) === activeIdx),
|
||||
);
|
||||
@@ -141,7 +141,7 @@ function initNav(): void {
|
||||
if (sec && sec.getBoundingClientRect().top <= ref) idx = i;
|
||||
});
|
||||
// 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) {
|
||||
idx = sectioned.length - 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user