<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>A Platform of Whims</title><link>http://InfraHeaven.io/</link><description>Recent content on A Platform of Whims</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 21 Aug 2026 21:44:00 +1000</lastBuildDate><atom:link href="http://InfraHeaven.io/index.xml" rel="self" type="application/rss+xml"/><item><title>My AI Setup — Ollama Cloud, the Pi Agent, and a Multi-Model Subagent Stack</title><link>http://InfraHeaven.io/blog/ai-setup-ollama-pi-agent/</link><pubDate>Fri, 21 Aug 2026 21:44:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/ai-setup-ollama-pi-agent/</guid><description>&lt;p&gt;I have spent the last few months tuning how I actually run AI day-to-day. The result is a setup that mixes a cloud subscription for the heavy lifting with a local model as a safety net, all wired through a single coding agent that can route different sub-tasks to different models. In this post I will walk through the pieces: the Ollama Cloud subscription, the Pi agent, and the multi-model subagent configuration I use for project development and local hosting tasks — with the local model fallbacks for outages, subscription exhaustion, or remote work.&lt;/p&gt;</description></item><item><title>Managed Kubernetes: EKS Auto Mode &amp; GKE Autopilot vs. Standard</title><link>http://InfraHeaven.io/blog/managed-kubernetes-eks-gke/</link><pubDate>Mon, 13 Apr 2026 10:00:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/managed-kubernetes-eks-gke/</guid><description>&lt;p&gt;Running Kubernetes well is deceptively hard. The control plane is just the beginning — you still have to care about node pool sizing, OS patching, add-on upgrades, autoscaler tuning, CNI compatibility, observability, and a hundred other things that have nothing to do with your actual workloads. Both AWS and Google Cloud have responded to this with &amp;ldquo;managed mode&amp;rdquo; cluster variants: &lt;strong&gt;EKS Auto Mode&lt;/strong&gt; and &lt;strong&gt;GKE Autopilot&lt;/strong&gt;. The pitch is the same: hand us the node layer and we&amp;rsquo;ll keep it running, just the way &lt;del&gt;you&lt;/del&gt; they like.&lt;/p&gt;</description></item><item><title>Gemma 4 — Local AI for Development</title><link>http://InfraHeaven.io/blog/gemm4-local-agentic/</link><pubDate>Sun, 12 Apr 2026 10:00:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/gemm4-local-agentic/</guid><description>&lt;p&gt;Google&amp;rsquo;s Gemma 4 is a surprisingly powerful local agentic AI model that can run on local hardware, making it a very welcome addition to my programming suite of tools. In this Blog I will cover my experience with Gemma 4, how to set it up, and some of the things I have been using it for. I will also cover some of the limitations and challenges I have faced with using a local agentic AI model.&lt;/p&gt;</description></item><item><title>Dockerfile Scorer</title><link>http://InfraHeaven.io/tools/dockerfile-scorer/</link><pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/tools/dockerfile-scorer/</guid><description>&lt;div class="dfs-wrap"&gt;&#10;&#10; &lt;div class="dfs-controls"&gt;&#10; &lt;textarea&#10; id="dfsInput"&#10; class="dfs-textarea"&#10; spellcheck="false"&#10; placeholder="Paste your Dockerfile here…"&#10; &gt;&lt;/textarea&gt;&#10; &lt;div class="dfs-btn-row"&gt;&#10; &lt;button class="dfs-btn dfs-btn--primary" onclick="dfsRun()"&gt;&#10; &lt;i class="fas fa-star"&gt;&lt;/i&gt; Score Dockerfile&#10; &lt;/button&gt;&#10; &lt;button class="dfs-btn" onclick="dfsLoadExample('good')"&gt;Good Example&lt;/button&gt;&#10; &lt;button class="dfs-btn" onclick="dfsLoadExample('poor')"&gt;Poor Example&lt;/button&gt;&#10; &lt;button class="dfs-btn dfs-btn--ghost" onclick="document.getElementById('dfsInput').value=''; document.getElementById('dfsResults').style.display='none'"&gt;Clear&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div id="dfsResults" style="display:none"&gt;&#10;&#10; &lt;div class="dfs-hero"&gt;&#10; &lt;div class="dfs-ring-wrap"&gt;&#10; &lt;svg class="dfs-ring-svg" viewBox="0 0 120 120" aria-hidden="true"&gt;&#10; &lt;circle class="dfs-ring-track" cx="60" cy="60" r="50"/&gt;&#10; &lt;circle id="dfsRingFill" class="dfs-ring-fill" cx="60" cy="60" r="50"&#10; transform="rotate(-90 60 60)"/&gt;&#10; &lt;/svg&gt;&#10; &lt;div class="dfs-ring-inner"&gt;&#10; &lt;span id="dfsScoreNum" class="dfs-score-num"&gt;0&lt;/span&gt;&#10; &lt;span class="dfs-score-denom"&gt;/10&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div id="dfsGrade" class="dfs-grade"&gt;&lt;/div&gt;&#10; &lt;div id="dfsSummary" class="dfs-summary"&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div id="dfsChecklist" class="dfs-checklist"&gt;&lt;/div&gt;&#10;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;style&gt;&#10;.dfs-wrap {&#10; font-family: var(--bs-font-sans-serif);&#10;}&#10;&#10; &#10;.dfs-controls {}&#10;&#10;.dfs-textarea {&#10; width: 100%;&#10; height: 220px;&#10; padding: 0.85rem 1rem;&#10; border: 1px solid var(--bs-border-color);&#10; border-radius: 10px;&#10; background: var(--bs-body-bg);&#10; color: var(--bs-body-color);&#10; font-family: 'Courier New', Courier, monospace;&#10; font-size: 0.82rem;&#10; line-height: 1.6;&#10; resize: vertical;&#10; outline: none;&#10; transition: border-color 0.15s;&#10; box-sizing: border-box;&#10;}&#10;.dfs-textarea:focus { border-color: var(--bs-primary-text-emphasis); }&#10;&#10;.dfs-btn-row {&#10; display: flex;&#10; flex-wrap: wrap;&#10; gap: 0.5rem;&#10; margin-top: 0.65rem;&#10;}&#10;&#10;.dfs-btn {&#10; padding: 0.5rem 1.1rem;&#10; border-radius: 8px;&#10; border: 1px solid var(--bs-border-color);&#10; background: var(--bs-secondary-bg);&#10; color: var(--bs-body-color);&#10; font-size: 0.85rem;&#10; font-weight: 600;&#10; cursor: pointer;&#10; transition: all 0.15s;&#10;}&#10;.dfs-btn:hover { background: var(--bs-body-color); color: var(--bs-body-bg); border-color: var(--bs-body-color); }&#10;&#10;.dfs-btn--primary {&#10; background: var(--bs-body-color);&#10; color: var(--bs-body-bg);&#10; border-color: var(--bs-body-color);&#10;}&#10;.dfs-btn--primary:hover { opacity: 0.8; }&#10;&#10;.dfs-btn--ghost {&#10; opacity: 0.55;&#10; background: transparent;&#10;}&#10;.dfs-btn--ghost:hover { opacity: 1; }&#10;&#10; &#10;.dfs-hero {&#10; display: flex;&#10; flex-direction: column;&#10; align-items: center;&#10; padding: 2rem 1rem 1.5rem;&#10;}&#10;&#10;.dfs-ring-wrap {&#10; position: relative;&#10; width: 130px;&#10; height: 130px;&#10;}&#10;&#10;.dfs-ring-svg {&#10; width: 100%;&#10; height: 100%;&#10;}&#10;&#10;.dfs-ring-track {&#10; fill: none;&#10; stroke: var(--bs-secondary-bg);&#10; stroke-width: 10;&#10;}&#10;&#10;.dfs-ring-fill {&#10; fill: none;&#10; stroke: #2ecc71;&#10; stroke-width: 10;&#10; stroke-linecap: round;&#10; stroke-dasharray: 314.16;&#10; stroke-dashoffset: 314.16;&#10; transition: stroke-dashoffset 0.8s cubic-bezier(.4,0,.2,1), stroke 0.5s;&#10;}&#10;&#10;.dfs-ring-inner {&#10; position: absolute;&#10; inset: 0;&#10; display: flex;&#10; align-items: center;&#10; justify-content: center;&#10; flex-direction: column;&#10; gap: 0;&#10;}&#10;&#10;.dfs-score-num {&#10; font-size: 2.2rem;&#10; font-weight: 800;&#10; line-height: 1;&#10; color: var(--bs-body-color);&#10; font-variant-numeric: tabular-nums;&#10;}&#10;&#10;.dfs-score-denom {&#10; font-size: 0.85rem;&#10; font-weight: 600;&#10; color: var(--bs-secondary-color);&#10; line-height: 1;&#10; margin-top: 0.1rem;&#10;}&#10;&#10;.dfs-grade {&#10; margin-top: 0.75rem;&#10; font-size: 1.15rem;&#10; font-weight: 700;&#10; letter-spacing: 0.02em;&#10;}&#10;&#10;.dfs-summary {&#10; margin-top: 0.35rem;&#10; font-size: 0.85rem;&#10; color: var(--bs-secondary-color);&#10;}&#10;&#10; &#10;.dfs-checklist {&#10; display: flex;&#10; flex-direction: column;&#10; gap: 0.5rem;&#10; margin-top: 0.5rem;&#10; padding-top: 0.5rem;&#10; border-top: 1px solid var(--bs-border-color);&#10;}&#10;&#10;.dfs-check {&#10; border: 1px solid var(--bs-border-color);&#10; border-radius: 10px;&#10; overflow: hidden;&#10;}&#10;&#10;.dfs-check--pass { border-left: 4px solid #2ecc71; }&#10;.dfs-check--fail { border-left: 4px solid #e74c3c; }&#10;.dfs-check--partial { border-left: 4px solid #f4a261; }&#10;&#10;.dfs-check-header {&#10; display: flex;&#10; align-items: center;&#10; gap: 0.75rem;&#10; padding: 0.7rem 1rem;&#10;}&#10;&#10;.dfs-check-icon {&#10; font-size: 1rem;&#10; width: 1.25rem;&#10; text-align: center;&#10; flex-shrink: 0;&#10;}&#10;&#10;.dfs-check--pass .dfs-check-icon { color: #2ecc71; }&#10;.dfs-check--fail .dfs-check-icon { color: #e74c3c; }&#10;.dfs-check--partial .dfs-check-icon { color: #f4a261; }&#10;&#10;.dfs-check-name {&#10; flex: 1;&#10; font-weight: 600;&#10; font-size: 0.9rem;&#10; color: var(--bs-body-color);&#10;}&#10;&#10;.dfs-check-pts {&#10; font-family: 'Courier New', Courier, monospace;&#10; font-size: 0.82rem;&#10; font-weight: 700;&#10; padding: 0.2rem 0.6rem;&#10; border-radius: 20px;&#10; background: var(--bs-secondary-bg);&#10; color: var(--bs-secondary-color);&#10; white-space: nowrap;&#10;}&#10;&#10;.dfs-check--pass .dfs-check-pts { background: rgba(46,204,113,0.15); color: #2ecc71; }&#10;.dfs-check--fail .dfs-check-pts { background: rgba(231,76,60,0.15); color: #e74c3c; }&#10;.dfs-check--partial .dfs-check-pts { background: rgba(244,162,97,0.15); color: #f4a261; }&#10;&#10;.dfs-check-body {&#10; padding: 0 1rem 0.75rem 2.95rem;&#10;}&#10;&#10;.dfs-check-detail {&#10; font-size: 0.82rem;&#10; color: var(--bs-secondary-color);&#10; margin: 0 0 0;&#10;}&#10;&#10;.dfs-check-tip {&#10; margin-top: 0.5rem;&#10; padding: 0.6rem 0.75rem;&#10; background: var(--bs-secondary-bg);&#10; border-radius: 6px;&#10; font-size: 0.8rem;&#10; color: var(--bs-secondary-color);&#10; font-family: 'Courier New', Courier, monospace;&#10; white-space: pre-wrap;&#10; line-height: 1.6;&#10;}&#10;&#10;.dfs-check-tip-label {&#10; font-family: var(--bs-font-sans-serif);&#10; font-weight: 700;&#10; font-size: 0.75rem;&#10; text-transform: uppercase;&#10; letter-spacing: 0.06em;&#10; color: var(--bs-secondary-color);&#10; margin-bottom: 0.25rem;&#10; opacity: 0.7;&#10;}&#10;&#10;@media (max-width: 600px) {&#10; .dfs-score-num { font-size: 1.8rem; }&#10; .dfs-ring-wrap { width: 110px; height: 110px; }&#10;}&#10;&lt;/style&gt;&#10;&#10;&lt;script&gt;&#10;&#10;&#10;&#10;&#10;const DFS_CIRCUMFERENCE = 2 * Math.PI * 50; &#10;&#10;function dfsParse(content) {&#10; const lines = content.split('\n');&#10; const instructions = [];&#10; let current = null;&#10;&#10; for (const line of lines) {&#10; const trimmed = line.trim();&#10; &#10; if (current !== null &amp;&amp; current.endsWith('\\')) {&#10; const stripped = current.slice(0, -1);&#10; if (!trimmed || trimmed.startsWith('#')) {&#10; &#10; current = stripped + ' ';&#10; continue;&#10; }&#10; current = stripped + ' ' + trimmed;&#10; continue;&#10; }&#10; &#10; if (!trimmed || trimmed.startsWith('#')) continue;&#10; if (current !== null) instructions.push(current.trim());&#10; current = trimmed;&#10; }&#10; if (current !== null &amp;&amp; current.trim()) instructions.push(current.trim());&#10;&#10; return instructions.map(ins =&gt; {&#10; const m = ins.match(/^(\S+)\s*([\s\S]*)/);&#10; return m ? { keyword: m[1].toUpperCase(), rest: m[2] } : { keyword: ins.toUpperCase(), rest: '' };&#10; });&#10;}&#10;&#10;function dfsScore(content) {&#10; const parsed = dfsParse(content);&#10; const results = [];&#10;&#10; const froms = parsed.filter(p =&gt; p.keyword === 'FROM');&#10; const users = parsed.filter(p =&gt; p.keyword === 'USER');&#10; const healthchecks = parsed.filter(p =&gt; p.keyword === 'HEALTHCHECK');&#10; const workdirs = parsed.filter(p =&gt; p.keyword === 'WORKDIR');&#10; const labels = parsed.filter(p =&gt; p.keyword === 'LABEL');&#10; const args = parsed.filter(p =&gt; p.keyword === 'ARG');&#10; const runs = parsed.filter(p =&gt; p.keyword === 'RUN');&#10; const envs = parsed.filter(p =&gt; p.keyword === 'ENV');&#10; const adds = parsed.filter(p =&gt; p.keyword === 'ADD');&#10;&#10; &#10; const fromImage = (rest) =&gt; rest&#10; .replace(/^--platform=\S+\s+/, '')&#10; .replace(/\s+AS\s+\S+$/i, '')&#10; .trim();&#10;&#10; &#10; {&#10; const stages = froms.length;&#10; const passed = stages &gt; 1;&#10; results.push({&#10; id: 'multistage', name: 'Multi-stage build',&#10; points: passed ? 2 : 0, max: 2, passed,&#10; detail: passed&#10; ? `${stages} build stages — separating build and runtime reduces final image size`&#10; : 'Single FROM statement — no multi-stage build detected',&#10; tip: 'Use two or more FROM statements:\n FROM golang:1.22-alpine AS builder\n RUN go build …\n\n FROM gcr.io/distroless/static AS final\n COPY --from=builder /app/server .',&#10; });&#10; }&#10;&#10; &#10; {&#10; const nonRoot = users.find(u =&gt; {&#10; const v = u.rest.trim().toLowerCase();&#10; return v &amp;&amp; v !== 'root' &amp;&amp; v !== '0' &amp;&amp; !v.startsWith('root:') &amp;&amp; !v.startsWith('0:');&#10; });&#10; const passed = !!nonRoot;&#10; results.push({&#10; id: 'nonroot', name: 'Non-root USER',&#10; points: passed ? 2 : 0, max: 2, passed,&#10; detail: passed&#10; ? `USER ${nonRoot.rest} — container does not run as root`&#10; : users.length === 0&#10; ? 'No USER instruction — container runs as root by default'&#10; : 'USER is explicitly set to root (0)',&#10; tip: 'Create a dedicated user and switch to it at the end of your final stage:\n RUN addgroup -S app &amp;&amp; adduser -S app -G app # Alpine\n USER app:app',&#10; });&#10; }&#10;&#10; &#10; {&#10; const images = froms.map(f =&gt; fromImage(f.rest));&#10; const unpinned = images.filter(img =&gt; {&#10; if (img.toLowerCase() === 'scratch') return false; &#10; if (img.includes('@')) return false; &#10; if (!img.includes(':')) return true; &#10; return img.split(':').pop() === 'latest';&#10; });&#10; const allPinned = unpinned.length === 0;&#10; const partial = !allPinned &amp;&amp; unpinned.length &lt; images.length;&#10; const pts = allPinned ? 1.5 : partial ? 0.75 : 0;&#10; results.push({&#10; id: 'versions', name: 'Version-pinned base images',&#10; points: pts, max: 1.5,&#10; passed: allPinned, partial,&#10; detail: allPinned&#10; ? 'All base images use specific version tags'&#10; : `Unpinned: ${unpinned.join(', ')}`,&#10; tip: 'Replace :latest with an explicit tag:\n FROM node:22.3-alpine3.20\nFor the strongest guarantee, pin to a digest:\n FROM node@sha256:abc123…',&#10; });&#10; }&#10;&#10; &#10; {&#10; const argNames = args.map(a =&gt; a.rest.split(/[=\s]/)[0].trim().toUpperCase());&#10; const multiArchArgs = ['TARGETARCH', 'TARGETPLATFORM', 'BUILDPLATFORM', 'BUILDARCH', 'TARGETOS'];&#10; const hasArgDecl = multiArchArgs.some(n =&gt; argNames.includes(n));&#10; const hasPlatformFrom = froms.some(f =&gt; f.rest.includes('--platform'));&#10; const passed = hasArgDecl || hasPlatformFrom;&#10; results.push({&#10; id: 'multiarch', name: 'Multi-arch support',&#10; points: passed ? 1 : 0, max: 1, passed,&#10; detail: passed&#10; ? `Multi-arch build ARGs declared (${multiArchArgs.filter(n =&gt; argNames.includes(n)).join(', ') || '--platform'})`&#10; : 'No TARGETARCH / TARGETPLATFORM ARGs found',&#10; tip: 'Declare these ARGs (docker buildx injects them automatically):\n ARG TARGETPLATFORM\n ARG TARGETARCH\nThen use in your build:\n RUN GOARCH=$TARGETARCH go build …\nBuild with: docker buildx build --platform linux/amd64,linux/arm64 .',&#10; });&#10; }&#10;&#10; &#10; {&#10; const valid = healthchecks.find(h =&gt; !/^NONE$/i.test(h.rest.trim()));&#10; const passed = !!valid;&#10; results.push({&#10; id: 'healthcheck', name: 'HEALTHCHECK instruction',&#10; points: passed ? 0.5 : 0, max: 0.5, passed,&#10; detail: passed&#10; ? 'HEALTHCHECK is configured'&#10; : healthchecks.some(h =&gt; /^NONE$/i.test(h.rest.trim()))&#10; ? 'HEALTHCHECK NONE disables health checking'&#10; : 'No HEALTHCHECK instruction',&#10; tip: 'Add a health check so orchestrators can detect unhealthy containers:\n HEALTHCHECK --interval=30s --timeout=5s --retries=3 \\\n CMD curl -f http://localhost:8080/health || exit 1',&#10; });&#10; }&#10;&#10; &#10; {&#10; const sensible = workdirs.find(w =&gt; {&#10; const v = w.rest.trim();&#10; return v &amp;&amp; v !== '/';&#10; });&#10; results.push({&#10; id: 'workdir', name: 'WORKDIR set',&#10; points: sensible ? 0.5 : 0, max: 0.5, passed: !!sensible,&#10; detail: sensible&#10; ? `WORKDIR ${sensible.rest.trim()} — relative paths are predictable`&#10; : workdirs.length === 0&#10; ? 'No WORKDIR — files land in /'&#10; : 'WORKDIR is set to / (root)',&#10; tip: 'Set a working directory early in each stage:\n WORKDIR /app',&#10; });&#10; }&#10;&#10; &#10; {&#10; const finalFrom = froms[froms.length - 1];&#10; const finalImg = finalFrom ? fromImage(finalFrom.rest).toLowerCase() : '';&#10; const lean = ['alpine', 'distroless', 'slim', 'scratch', 'busybox', 'chainguard', 'wolfi', 'minimal', 'nonroot'];&#10; const passed = lean.some(kw =&gt; finalImg.includes(kw));&#10; results.push({&#10; id: 'lean', name: 'Lean final image',&#10; points: passed ? 0.5 : 0, max: 0.5, passed,&#10; detail: passed&#10; ? `Final stage uses a minimal base (${finalImg})`&#10; : `Final stage is ${finalImg || 'unknown'} — not a known slim base`,&#10; tip: 'Choose a minimal final image:\n gcr.io/distroless/static:nonroot (static binaries)\n alpine:3.21 (general purpose)\n scratch (fully static Go/C)',&#10; });&#10; }&#10;&#10; &#10; {&#10; const aptRuns = runs.filter(r =&gt; /apt-get install|apt install/.test(r.rest));&#10; const apkRuns = runs.filter(r =&gt; /apk add/.test(r.rest));&#10; const yumRuns = runs.filter(r =&gt; /yum install|dnf install/.test(r.rest));&#10;&#10; let passed = true; &#10; let detail = 'No package manager installs detected';&#10;&#10; if (aptRuns.length &gt; 0) {&#10; const clean = aptRuns.every(r =&gt;&#10; r.rest.includes('rm -rf /var/lib/apt') ||&#10; r.rest.includes('apt-get clean') ||&#10; r.rest.includes('--no-install-recommends')&#10; );&#10; passed = clean;&#10; detail = clean&#10; ? 'apt-get used with cache cleanup / --no-install-recommends'&#10; : 'apt-get install found without cache cleanup';&#10; } else if (apkRuns.length &gt; 0) {&#10; const clean = apkRuns.every(r =&gt; r.rest.includes('--no-cache'));&#10; passed = clean;&#10; detail = clean&#10; ? 'apk add uses --no-cache'&#10; : 'apk add found without --no-cache';&#10; } else if (yumRuns.length &gt; 0) {&#10; const clean = yumRuns.every(r =&gt; /yum clean|dnf clean/.test(r.rest));&#10; passed = clean;&#10; detail = clean&#10; ? 'yum/dnf used with cache cleanup'&#10; : 'yum/dnf install found without cache cleanup';&#10; }&#10;&#10; results.push({&#10; id: 'cache', name: 'Package cache cleanup',&#10; points: passed ? 0.5 : 0, max: 0.5, passed,&#10; detail,&#10; tip: 'Chain cleanup in the same RUN layer to avoid bloat:\n # Debian/Ubuntu\n RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \\\n curl ca-certificates \\\n &amp;&amp; rm -rf /var/lib/apt/lists/*\n # Alpine\n RUN apk add --no-cache curl',&#10; });&#10; }&#10;&#10; &#10; {&#10; const passed = labels.length &gt; 0;&#10; results.push({&#10; id: 'labels', name: 'Image labels / metadata',&#10; points: passed ? 0.5 : 0, max: 0.5, passed,&#10; detail: passed&#10; ? `${labels.length} LABEL instruction${labels.length &gt; 1 ? 's' : ''}`&#10; : 'No LABEL instructions',&#10; tip: 'Add OCI standard labels:\n LABEL org.opencontainers.image.title="my-app"\n LABEL org.opencontainers.image.version="1.2.3"\n LABEL org.opencontainers.image.source="https://github.com/you/repo"',&#10; });&#10; }&#10;&#10; &#10; {&#10; const secretRe = /\b(password|passwd|secret|token|api[_-]?key|private[_-]?key|access[_-]?key|auth[_-]?key|credentials?)\s*=/i;&#10; const suspects = [...envs, ...args]&#10; .filter(p =&gt; secretRe.test(p.rest))&#10; .map(p =&gt; p.keyword + ' ' + p.rest.split('=')[0].trim());&#10; const passed = suspects.length === 0;&#10; results.push({&#10; id: 'secrets', name: 'No hardcoded secrets',&#10; points: passed ? 0.5 : 0, max: 0.5, passed,&#10; detail: passed&#10; ? 'No suspicious ENV / ARG values detected'&#10; : `Potential secret in: ${suspects.slice(0, 3).join(', ')}`,&#10; tip: 'Never put secrets in ENV or ARG — they appear in the image history.\nUse BuildKit secret mounts instead:\n RUN --mount=type=secret,id=mysecret \\\n MY_SECRET=$(cat /run/secrets/mysecret) &amp;&amp; …\nOr inject at runtime via your orchestrator.',&#10; });&#10; }&#10;&#10; const total = Math.round(results.reduce((s, r) =&gt; s + r.points, 0) * 10) / 10;&#10; return { results, total };&#10;}&#10;&#10;function dfsRender(total, results) {&#10; const resultsEl = document.getElementById('dfsResults');&#10; const ringFill = document.getElementById('dfsRingFill');&#10; const scoreNum = document.getElementById('dfsScoreNum');&#10; const gradeEl = document.getElementById('dfsGrade');&#10; const summaryEl = document.getElementById('dfsSummary');&#10;&#10; &#10; const fraction = total / 10;&#10; const offset = DFS_CIRCUMFERENCE * (1 - fraction);&#10; const color = total &gt;= 9 ? '#2ecc71' : total &gt;= 7 ? '#74b9ff' : total &gt;= 5 ? '#f4a261' : '#e74c3c';&#10;&#10; ringFill.style.strokeDashoffset = DFS_CIRCUMFERENCE; &#10; ringFill.style.stroke = color;&#10; gradeEl.style.color = color;&#10; scoreNum.style.color = 'var(--bs-body-color)';&#10;&#10; &#10; requestAnimationFrame(() =&gt; requestAnimationFrame(() =&gt; {&#10; ringFill.style.strokeDashoffset = offset;&#10; }));&#10;&#10; scoreNum.textContent = total % 1 === 0 ? total : total.toFixed(1);&#10;&#10; const grades = [&#10; { min: 9, label: 'Excellent', icon: 'fas fa-trophy' },&#10; { min: 7, label: 'Good', icon: 'fas fa-thumbs-up' },&#10; { min: 5, label: 'Fair', icon: 'fas fa-circle-half-stroke' },&#10; { min: 3, label: 'Needs Work', icon: 'fas fa-screwdriver-wrench' },&#10; { min: 0, label: 'Critical Issues',icon: 'fas fa-triangle-exclamation' },&#10; ];&#10; const grade = grades.find(g =&gt; total &gt;= g.min);&#10; gradeEl.innerHTML = `&lt;i class="${grade.icon}"&gt;&lt;/i&gt; ${grade.label}`;&#10;&#10; const passed = results.filter(r =&gt; r.passed).length;&#10; const partial = results.filter(r =&gt; r.partial &amp;&amp; !r.passed).length;&#10; const failed = results.filter(r =&gt; !r.passed &amp;&amp; !r.partial).length;&#10; summaryEl.textContent = `${passed} passed · ${partial &gt; 0 ? partial + ' partial · ' : ''}${failed} failed out of ${results.length} checks`;&#10;&#10; &#10; const checklistEl = document.getElementById('dfsChecklist');&#10; checklistEl.innerHTML = results.map(r =&gt; {&#10; const cls = r.passed ? 'pass' : (r.partial ? 'partial' : 'fail');&#10; const icon = r.passed&#10; ? '&lt;i class="fas fa-circle-check"&gt;&lt;/i&gt;'&#10; : r.partial&#10; ? '&lt;i class="fas fa-circle-half-stroke"&gt;&lt;/i&gt;'&#10; : '&lt;i class="fas fa-circle-xmark"&gt;&lt;/i&gt;';&#10; const ptsLabel = r.points % 1 === 0 ? r.points : r.points.toFixed(2).replace(/\.?0+$/, '');&#10; const maxLabel = r.max % 1 === 0 ? r.max : r.max.toFixed(2).replace(/\.?0+$/, '');&#10;&#10; const tipHtml = (!r.passed &amp;&amp; r.tip) ? `&#10; &lt;div class="dfs-check-tip-label"&gt;&lt;i class="fas fa-lightbulb"&gt;&lt;/i&gt; Fix&lt;/div&gt;&#10; &lt;div class="dfs-check-tip"&gt;${dfsEsc(r.tip)}&lt;/div&gt;` : '';&#10;&#10; return `&#10; &lt;div class="dfs-check dfs-check--${cls}"&gt;&#10; &lt;div class="dfs-check-header"&gt;&#10; &lt;span class="dfs-check-icon"&gt;${icon}&lt;/span&gt;&#10; &lt;span class="dfs-check-name"&gt;${r.name}&lt;/span&gt;&#10; &lt;span class="dfs-check-pts"&gt;${ptsLabel} / ${maxLabel}&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class="dfs-check-body"&gt;&#10; &lt;div class="dfs-check-detail"&gt;${dfsEsc(r.detail)}&lt;/div&gt;&#10; ${tipHtml}&#10; &lt;/div&gt;&#10; &lt;/div&gt;`;&#10; }).join('');&#10;&#10; resultsEl.style.display = 'block';&#10;}&#10;&#10;function dfsEsc(str) {&#10; return String(str)&#10; .replace(/&amp;/g, '&amp;amp;')&#10; .replace(/&lt;/g, '&amp;lt;')&#10; .replace(/&gt;/g, '&amp;gt;');&#10;}&#10;&#10;function dfsRun() {&#10; const content = document.getElementById('dfsInput').value.trim();&#10; if (!content) return;&#10;&#10; const hasFROM = /^\s*FROM\s/mi.test(content);&#10; if (!hasFROM) {&#10; document.getElementById('dfsResults').style.display = 'none';&#10; document.getElementById('dfsInput').style.borderColor = '#e74c3c';&#10; setTimeout(() =&gt; document.getElementById('dfsInput').style.borderColor = '', 1500);&#10; return;&#10; }&#10;&#10; const { results, total } = dfsScore(content);&#10; dfsRender(total, results);&#10; document.getElementById('dfsResults').scrollIntoView({ behavior: 'smooth', block: 'start' });&#10;}&#10;&#10;function dfsLoadExample(type) {&#10; const good = `# syntax=docker/dockerfile:1&#10;# Well-crafted multi-stage Go service Dockerfile&#10;&#10;FROM golang:1.22-alpine3.20 AS builder&#10;&#10;ARG TARGETARCH&#10;ARG TARGETPLATFORM&#10;&#10;WORKDIR /build&#10;&#10;COPY go.mod go.sum ./&#10;RUN go mod download&#10;&#10;COPY . .&#10;RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH \\&#10; go build -ldflags="-s -w" -trimpath -o server ./cmd/server&#10;&#10;# ── Final stage ──────────────────────────────────────&#10;FROM gcr.io/distroless/static-debian12:nonroot AS final&#10;&#10;ARG TARGETARCH&#10;ARG TARGETPLATFORM&#10;&#10;LABEL org.opencontainers.image.title="my-service"&#10;LABEL org.opencontainers.image.version="1.0.0"&#10;LABEL org.opencontainers.image.source="https://github.com/example/my-service"&#10;&#10;WORKDIR /app&#10;&#10;COPY --from=builder /build/server .&#10;&#10;USER nonroot:nonroot&#10;&#10;HEALTHCHECK --interval=30s --timeout=5s --retries=3 \\&#10; CMD ["/app/server", "--healthcheck"]&#10;&#10;EXPOSE 8080&#10;ENTRYPOINT ["/app/server"]`;&#10;&#10; const poor = `FROM node:latest&#10;&#10;WORKDIR /app&#10;&#10;COPY . .&#10;&#10;RUN npm install&#10;RUN apt-get install -y curl&#10;&#10;ENV DB_PASSWORD=supersecret123&#10;ENV API_KEY=abc123&#10;&#10;EXPOSE 3000&#10;&#10;CMD ["node", "server.js"]`;&#10;&#10; document.getElementById('dfsInput').value = type === 'good' ? good : poor;&#10; dfsRun();&#10;}&#10;&lt;/script&gt;</description></item><item><title>Time Traveling Containers</title><link>http://InfraHeaven.io/blog/time-traveling-containers/</link><pubDate>Wed, 08 Apr 2026 22:00:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/time-traveling-containers/</guid><description>&lt;p&gt;All my projects start in the same manner, a cool little script, an idea or technology I want to work with or just something curious to me. The literal second I have something compiling I (or the AI Model of the week) would make a Dockerfile for it.&lt;/p&gt;&#10;&lt;p&gt;However, a while ago I was working with &lt;a href="https://docs.cloud.google.com/docs/buildpacks/builders"&gt;Google&amp;rsquo;s BuildPacks&lt;/a&gt;, a way to containerize applications for Google Services supporting Multiple languages. I pulled the latest version and &amp;hellip;..&lt;/p&gt;</description></item><item><title>GCP Professional Architect Exam 2026</title><link>http://InfraHeaven.io/blog/gcp-pro-arch-guide/</link><pubDate>Tue, 10 Mar 2026 22:00:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/gcp-pro-arch-guide/</guid><description>&lt;p&gt;The following are my notes / thoughts and guide to passing the Google Cloud Architecture exam in 2026.&lt;/p&gt;&#10;&lt;p&gt;I am unable to post direct questions / examples / answers I can only talk through tips, advice, tricks and topics I found useful.&lt;/p&gt;&#10;&lt;p&gt;ANY Listed examples/answers are made by myself based on how I interpret Google&amp;rsquo;s documentation and how I would answer such a situation!&lt;/p&gt;&#10;&lt;h3 id="helpful-links"&gt;Helpful links:&lt;/h3&gt;&#10;&lt;p&gt;As always please refer to the exam certification page for details and guidelines:&#10;&lt;a href="https://cloud.google.com/learn/certification/cloud-architect"&gt;https://cloud.google.com/learn/certification/cloud-architect&lt;/a&gt;&lt;/p&gt;</description></item><item><title>SRE Calculators</title><link>http://InfraHeaven.io/blog/the-sre-calculators/</link><pubDate>Sat, 08 Nov 2025 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/blog/the-sre-calculators/</guid><description>&lt;p&gt;In this blog I will cover the three calculators I relied on during my time in SRE and that I continue to use as a Cloud Engineer.&lt;/p&gt;&#10;&lt;h2 id="what-is-sre"&gt;What is SRE?&lt;/h2&gt;&#10;&lt;p&gt;Site Reliability Engineering - &amp;ldquo;&lt;em&gt;SRE is what you get when you treat operations as if it’s a software problem.&lt;/em&gt;&amp;rdquo; - &lt;a href="https://sre.google/"&gt;SRE Google&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Unlike the traditional DevOps philosophy of development and operations being under the same team handling all application operations, the goal of Site Reliability is the practical management of operations while teams are implementing features or new developments.&lt;/p&gt;</description></item><item><title>Developers and the Bill</title><link>http://InfraHeaven.io/blog/developers-and-the-bill/</link><pubDate>Sat, 18 Oct 2025 22:00:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/developers-and-the-bill/</guid><description>&lt;p&gt;I have always worked with or in Cloud/Platform teams aspiring to charge back developers for the cost of the cloud resources they consume, but I have never seen it effectively implemented. And while I do not encourage a &lt;em&gt;blameful&lt;/em&gt; FinOps team hunting down teams for deploying a first cut application that reached high customer engagement. It is nice when others are mindful to help correctly size an application, opt for scheduling their instances and checking queries/APIs before rushing ahead.&lt;/p&gt;</description></item><item><title>Starting Point</title><link>http://InfraHeaven.io/blog/starting-point/</link><pubDate>Thu, 08 Aug 2024 22:00:00 +1000</pubDate><guid>http://InfraHeaven.io/blog/starting-point/</guid><description>&lt;h1 id="starting-point"&gt;Starting Point&lt;/h1&gt;&#10;&lt;p&gt;This is the first post while building out the website, a lot will change over the coming weeks as I add:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Better CI/CD&lt;/li&gt;&#10;&lt;li&gt;AWS Hosting not just GCP&lt;/li&gt;&#10;&lt;li&gt;SRE Monitoring and Clear Objectives (I love bringing chaos to systems I build)&lt;/li&gt;&#10;&lt;li&gt;Open up the Github Repository&lt;/li&gt;&#10;&lt;li&gt;Add interesting things I have noted over my last 5 years while consulting DevOps/Cloud/Platform Engineering&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="where-are-we-now"&gt;Where are we now?&lt;/h2&gt;&#10;&lt;p&gt;Right now this website was built by a backend engineer (Who has some growing respect to front-end devs), built in Hugo, using Bootstrap 5.3 in the base template written again by the very same backend engineer, currently hosting out of Google Cloud Platform (GCP).&lt;/p&gt;</description></item><item><title>About</title><link>http://InfraHeaven.io/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/about/</guid><description>&lt;style&gt;&#10; /* ── Hero ── */&#10; .resume-hero {&#10; display: flex;&#10; align-items: center;&#10; gap: 2.5rem;&#10; padding: 2.5rem;&#10; flex-wrap: wrap;&#10; position: relative;&#10; overflow: hidden;&#10; }&#10; .resume-photo {&#10; width: 145px;&#10; height: 145px;&#10; border-radius: 50%;&#10; object-fit: cover;&#10; object-position: center 15%;&#10; border: 3px solid var(--bs-border-color);&#10; flex-shrink: 0;&#10; position: relative;&#10; z-index: 1;&#10; }&#10; .resume-hero-text {&#10; flex: 1;&#10; min-width: 220px;&#10; position: relative;&#10; z-index: 1;&#10; }&#10; .resume-name {&#10; font-size: 2rem;&#10; font-weight: 800;&#10; margin: 0 0 0.2rem;&#10; line-height: 1.1;&#10; }&#10; .resume-headline {&#10; font-size: 0.95rem;&#10; color: var(--bs-secondary-color);&#10; margin-bottom: 0.75rem;&#10; font-weight: 500;&#10; }&#10; .resume-tagline {&#10; font-size: 0.87rem;&#10; color: var(--bs-secondary-color);&#10; max-width: 560px;&#10; line-height: 1.7;&#10; margin-bottom: 1rem;&#10; }&#10; .resume-links {&#10; display: flex;&#10; gap: 0.5rem;&#10; flex-wrap: wrap;&#10; }&#10; .resume-link-btn {&#10; display: inline-flex;&#10; align-items: center;&#10; gap: 0.4rem;&#10; padding: 0.45rem 1rem;&#10; border-radius: 8px;&#10; border: 1px solid var(--bs-border-color);&#10; background: var(--bs-secondary-bg);&#10; color: var(--bs-body-color);&#10; font-size: 0.82rem;&#10; font-weight: 600;&#10; text-decoration: none;&#10; transition: all 0.15s;&#10; }&#10; .resume-link-btn:hover {&#10; background: var(--bs-body-color);&#10; color: var(--bs-body-bg);&#10; border-color: var(--bs-body-color);&#10; text-decoration: none;&#10; }&#10;&#10; /* ── Section header ── */&#10; .resume-section-title {&#10; font-size: 0.7rem;&#10; font-weight: 700;&#10; text-transform: uppercase;&#10; letter-spacing: 0.1em;&#10; color: var(--bs-secondary-color);&#10; margin: 0 0 1rem;&#10; padding-bottom: 0.45rem;&#10; border-bottom: 1px solid var(--bs-border-color);&#10; }&#10;&#10; /* ── Tech stack ── */&#10; .tech-category-label {&#10; font-size: 0.68rem;&#10; font-weight: 700;&#10; text-transform: uppercase;&#10; letter-spacing: 0.08em;&#10; color: var(--bs-secondary-color);&#10; opacity: 0.55;&#10; margin: 0 0 0.5rem;&#10; }&#10; .prof-item {&#10; margin-bottom: 1.25rem;&#10; }&#10; .prof-item:last-child {&#10; margin-bottom: 0;&#10; }&#10; .prof-header {&#10; display: flex;&#10; justify-content: space-between;&#10; align-items: baseline;&#10; margin-bottom: 0.3rem;&#10; }&#10; .prof-label {&#10; font-size: 0.82rem;&#10; font-weight: 600;&#10; color: var(--bs-body-color);&#10; }&#10; .prof-score {&#10; font-size: 0.72rem;&#10; color: var(--bs-secondary-color);&#10; opacity: 0.7;&#10; }&#10; .prof-track {&#10; height: 6px;&#10; border-radius: 99px;&#10; background: var(--bs-border-color);&#10; overflow: hidden;&#10; }&#10; .prof-fill {&#10; height: 100%;&#10; border-radius: 99px;&#10; background: linear-gradient(90deg, var(--bs-primary, #0d6efd), #6ea8fe);&#10; }&#10; .tech-grid {&#10; display: flex;&#10; flex-wrap: wrap;&#10; gap: 0.55rem;&#10; }&#10; .tech-pill {&#10; display: flex;&#10; align-items: center;&#10; gap: 0.45rem;&#10; padding: 0.35rem 0.7rem;&#10; border-radius: 7px;&#10; border: 1px solid var(--bs-border-color);&#10; background: var(--bs-secondary-bg);&#10; font-size: 0.77rem;&#10; font-weight: 600;&#10; color: var(--bs-secondary-color);&#10; transition: all 0.2s;&#10; cursor: default;&#10; user-select: none;&#10; }&#10; .tech-pill:hover {&#10; border-color: var(--bs-body-color);&#10; color: var(--bs-body-color);&#10; background: var(--bs-body-bg);&#10; }&#10; .tech-pill svg {&#10; width: 18px;&#10; height: 18px;&#10; flex-shrink: 0;&#10; filter: grayscale(0.35) opacity(0.75);&#10; transition: filter 0.2s;&#10; }&#10; .tech-pill:hover svg {&#10; filter: grayscale(0) opacity(1);&#10; }&#10;&#10; /* ── Side projects &amp; homelab ── */&#10; .side-projects {&#10; list-style: none;&#10; padding: 0;&#10; margin: 0;&#10; }&#10; .side-projects li {&#10; display: flex;&#10; align-items: flex-start;&#10; gap: 0.75rem;&#10; padding: 0.6rem 0;&#10; }&#10; .side-projects li + li {&#10; border-top: 1px solid var(--bs-border-color);&#10; }&#10; .side-project-icon {&#10; flex-shrink: 0;&#10; width: 34px;&#10; height: 34px;&#10; border-radius: 9px;&#10; display: flex;&#10; align-items: center;&#10; justify-content: center;&#10; background: var(--bs-secondary-bg);&#10; border: 1px solid var(--bs-border-color);&#10; color: var(--bs-primary, #0d6efd);&#10; margin-top: 1px;&#10; }&#10; .side-project-icon svg {&#10; width: 18px;&#10; height: 18px;&#10; }&#10; .side-project-title {&#10; font-weight: 700;&#10; font-size: 0.85rem;&#10; color: var(--bs-body-color);&#10; }&#10; .side-project-desc {&#10; font-size: 0.78rem;&#10; color: var(--bs-secondary-color);&#10; line-height: 1.5;&#10; margin-top: 0.1rem;&#10; }&#10;&#10; /* ── Experience timeline ── */&#10; .exp-timeline {&#10; display: flex;&#10; flex-direction: column;&#10; position: relative;&#10; }&#10; .exp-timeline::before {&#10; content: "";&#10; position: absolute;&#10; left: 7px;&#10; top: 10px;&#10; bottom: 10px;&#10; width: 2px;&#10; background: var(--ih-card-border, var(--bs-border-color));&#10; }&#10; .exp-item {&#10; display: flex;&#10; gap: 1.2rem;&#10; padding-bottom: 1.5rem;&#10; position: relative;&#10; }&#10; .exp-dot {&#10; width: 16px;&#10; height: 16px;&#10; border-radius: 50%;&#10; border: 2px solid var(--ih-card-border, var(--bs-border-color));&#10; background: var(--bs-body-bg);&#10; flex-shrink: 0;&#10; margin-top: 3px;&#10; z-index: 1;&#10; }&#10; .exp-item--active .exp-dot {&#10; border-color: #2ecc71;&#10; background: #2ecc71;&#10; box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.15);&#10; }&#10; .exp-body {&#10; flex: 1;&#10; }&#10; .exp-role {&#10; font-weight: 700;&#10; font-size: 0.9rem;&#10; color: var(--bs-body-color);&#10; line-height: 1.3;&#10; margin-bottom: 0.1rem;&#10; }&#10; .exp-company {&#10; font-size: 0.8rem;&#10; font-weight: 500;&#10; color: var(--bs-secondary-color);&#10; margin-bottom: 0.35rem;&#10; }&#10; .exp-year {&#10; display: inline-block;&#10; font-size: 0.7rem;&#10; font-weight: 700;&#10; color: var(--bs-secondary-color);&#10; background: var(--bs-secondary-bg);&#10; border: 1px solid var(--bs-border-color);&#10; border-radius: 4px;&#10; padding: 0.1rem 0.45rem;&#10; margin-bottom: 0.4rem;&#10; }&#10; .exp-bullets {&#10; list-style: none;&#10; padding: 0;&#10; margin: 0;&#10; }&#10; .exp-bullets li {&#10; font-size: 0.8rem;&#10; color: var(--bs-secondary-color);&#10; padding: 0.08rem 0 0.08rem 1rem;&#10; position: relative;&#10; }&#10; .exp-bullets li::before {&#10; content: "›";&#10; position: absolute;&#10; left: 0;&#10; opacity: 0.45;&#10; }&#10;&#10; @media (max-width: 600px) {&#10; .resume-hero {&#10; flex-direction: column;&#10; align-items: flex-start;&#10; padding: 1.5rem;&#10; }&#10; .resume-name {&#10; font-size: 1.6rem;&#10; }&#10; .resume-photo {&#10; width: 110px;&#10; height: 110px;&#10; }&#10; .hero-clouds {&#10; opacity: 0.2;&#10; }&#10; }&#10;&#10; /* ── Scroll-in animations ── */&#10; [data-animate] {&#10; opacity: 0;&#10; transform: translateY(28px);&#10; transition:&#10; opacity 0.55s cubic-bezier(0.4, 0, 0.2, 1),&#10; transform 0.55s cubic-bezier(0.4, 0, 0.2, 1);&#10; transition-delay: var(--anim-delay, 0ms);&#10; }&#10; [data-animate].is-visible {&#10; opacity: 1;&#10; transform: translateY(0);&#10; }&#10; /* ── Clouds ── */&#10; .hero-clouds {&#10; position: absolute;&#10; inset: 0;&#10; z-index: 0;&#10; pointer-events: none;&#10; overflow: hidden;&#10; opacity: 0.45;&#10; }&#10; .cloud {&#10; position: absolute;&#10; background: var(--bs-secondary-bg);&#10; border-radius: 50px;&#10; filter: blur(1px);&#10; }&#10; .cloud::before,&#10; .cloud::after {&#10; content: "";&#10; position: absolute;&#10; background: inherit;&#10; border-radius: 50%;&#10; }&#10; /* cloud sizes */&#10; .c1 {&#10; width: 120px;&#10; height: 36px;&#10; top: 18px;&#10; animation: cloud-drift 20s linear infinite;&#10; }&#10; .c1::before {&#10; width: 56px;&#10; height: 56px;&#10; top: -30px;&#10; left: 16px;&#10; }&#10; .c1::after {&#10; width: 40px;&#10; height: 40px;&#10; top: -20px;&#10; left: 60px;&#10; }&#10; .c2 {&#10; width: 90px;&#10; height: 28px;&#10; top: 100px;&#10; animation: cloud-drift 28s linear infinite;&#10; animation-delay: -10s;&#10; }&#10; .c2::before {&#10; width: 42px;&#10; height: 42px;&#10; top: -24px;&#10; left: 12px;&#10; }&#10; .c2::after {&#10; width: 30px;&#10; height: 30px;&#10; top: -16px;&#10; left: 46px;&#10; }&#10; .c3 {&#10; width: 150px;&#10; height: 42px;&#10; top: 190px;&#10; animation: cloud-drift 24s linear infinite;&#10; animation-delay: -16s;&#10; }&#10; .c3::before {&#10; width: 68px;&#10; height: 68px;&#10; top: -36px;&#10; left: 18px;&#10; }&#10; .c3::after {&#10; width: 48px;&#10; height: 48px;&#10; top: -24px;&#10; left: 74px;&#10; }&#10; .c4 {&#10; width: 80px;&#10; height: 24px;&#10; top: 60px;&#10; animation: cloud-drift 34s linear infinite;&#10; animation-delay: -5s;&#10; }&#10; .c4::before {&#10; width: 38px;&#10; height: 38px;&#10; top: -20px;&#10; left: 10px;&#10; }&#10; .c4::after {&#10; width: 26px;&#10; height: 26px;&#10; top: -14px;&#10; left: 40px;&#10; }&#10; @keyframes cloud-drift {&#10; from {&#10; transform: translateX(-220px);&#10; }&#10; to {&#10; transform: translateX(1300px);&#10; }&#10; }&#10;&lt;/style&gt;&#10;&#10;&lt;div class="container my-5"&gt;&#10; &lt;!-- ── Hero ── --&gt;&#10; &lt;div class="main-card mb-4" data-animate&gt;&#10; &lt;div class="resume-hero"&gt;&#10; &lt;img&#10; class="resume-photo"&#10; src="./static/personal-headshot-02.png"&#10; alt="Matthew Callinan"&#10; /&gt;&#10; &lt;div class="resume-hero-text"&gt;&#10; &lt;h1 class="resume-name"&gt;Matthew Callinan&lt;/h1&gt;&#10; &lt;div class="resume-headline"&gt;&#10; Staff Cloud &amp;amp; Platform Engineer &amp;nbsp;·&amp;nbsp; SRE &amp;nbsp;·&amp;nbsp;&#10; DevOps&#10; &lt;/div&gt;&#10; &lt;p class="resume-tagline"&gt;&#10; 7+ years building and scaling cloud infrastructure across GCP and AWS&#10; using Kubernetes, Terraform, and automation-first practices.&#10; Passionate about site reliability engineering, platform tooling, and&#10; helping teams ship faster with confidence. Based in Melbourne,&#10; Australia.&#10; &lt;/p&gt;</description></item><item><title>About</title><link>http://InfraHeaven.io/about_old/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/about_old/</guid><description>&lt;div class="container"&gt;&#10; &lt;div class="my-5"&gt;&#10; &lt;div class="p-5 text-center bg-body-tertiary"&gt;&#10; &lt;div class="container py-5"&gt;&#10; &lt;h1 class="text-body-emphasis"&gt;InfraHeaven, Building a Starting Point.&lt;/h1&gt;&#10; &lt;br&gt;&#10; &lt;p class="col-lg-8 mx-auto lead"&gt;&#10; This website is a place to share my thoughts, ideas, and experiences while working in DevOps, Platform Engineering, Site-Reliability Engineering and Cloud.&#10; &lt;/p&gt;&#10; &lt;p class="col-lg-8 mx-auto lead"&gt;&#10; I hope you enjoy it!&#10; &lt;/p&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class="row align-items-md-stretch"&gt;&#10; &lt;div class="col-md-6"&gt;&#10; &lt;div class="h-100 p-5 rounded-3"&gt;&#10; &lt;h2&gt;Matthew Callinan&lt;/h2&gt;&#10; &lt;p&gt;Author&lt;/p&gt;&#10; &lt;img class=blocks src="./static/skull-charactor.png" alt="A 2d picture of a skeleton with a macbook and coffee"&gt;&#10; &lt;div class="d-lg-flex p-2 col-md-6"&gt;&#10; &lt;a href="https://www.linkedin.com/in/mcallinanitdev/" target="https://www.linkedin.com/in/mcallinanitdev/" rel="noopener noreferrer"&gt;&#10; &lt;button type="button" class="btn btn-secondary"&gt;&#10; &lt;svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16"&gt;&#10; &lt;path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"&gt;&lt;/path&gt;&#10; &lt;/svg&gt; LinkedIn&#10; &lt;/button&gt;&#10; &lt;/a&gt;&#10; &lt;/div&gt;&#10; &lt;div class="d-lg-flex p-2 col-md-6"&gt;&#10; &lt;a href="https://github.com/Mattible" target="https://github.com/Mattible" rel="noopener noreferrer"&gt;&#10; &lt;button type="button" class="btn btn-secondary"&gt;&#10; &lt;svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-github" viewBox="0 0 16 16"&gt;&#10; &lt;path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"&gt;&lt;/path&gt;&#10; &lt;/svg&gt; Github - Mattible&#10; &lt;/button&gt;&#10; &lt;/a&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;div class="h-100 p-1 rounded-3"&gt;&#10; &lt;h2&gt;Contact&lt;/h2&gt;&#10; &lt;br&#10; &lt;p&gt;Please feel free to reach out to me on socials, LinkedIn or look at whatever I am most recently working on in Github&lt;/p&gt;</description></item><item><title>Meetups</title><link>http://InfraHeaven.io/meetups/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/meetups/</guid><description>&lt;style&gt;&#10;/* ── Calendar Header ── */&#10;.meetup-month-header {&#10; display: flex;&#10; align-items: center;&#10; justify-content: space-between;&#10; margin-bottom: 1.5rem;&#10;}&#10;.meetup-month-title {&#10; font-size: 1.4rem;&#10; font-weight: 800;&#10; margin: 0;&#10;}&#10;.meetup-month-sub {&#10; font-size: 0.82rem;&#10; color: var(--bs-secondary-color);&#10; margin: 0;&#10;}&#10;&#10;/* ── Calendar Grid ── */&#10;.cal-grid {&#10; display: grid;&#10; grid-template-columns: repeat(7, 1fr);&#10; gap: 4px;&#10; margin-bottom: 2rem;&#10;}&#10;.cal-day-name {&#10; text-align: center;&#10; font-size: 0.65rem;&#10; font-weight: 700;&#10; text-transform: uppercase;&#10; letter-spacing: 0.08em;&#10; color: var(--bs-secondary-color);&#10; padding: 0.4rem 0;&#10;}&#10;.cal-day {&#10; aspect-ratio: 1;&#10; display: flex;&#10; flex-direction: column;&#10; align-items: center;&#10; justify-content: center;&#10; border-radius: 8px;&#10; font-size: 0.8rem;&#10; font-weight: 500;&#10; color: var(--bs-secondary-color);&#10; border: 1px solid transparent;&#10; cursor: default;&#10; position: relative;&#10; transition: all 0.15s;&#10;}&#10;.cal-day--empty {&#10; background: transparent;&#10; border-color: transparent;&#10;}&#10;.cal-day--normal {&#10; background: var(--bs-secondary-bg);&#10; border-color: var(--bs-border-color);&#10;}&#10;.cal-day--today {&#10; background: var(--bs-primary, #0d6efd);&#10; color: #fff;&#10; border-color: var(--bs-primary, #0d6efd);&#10; font-weight: 700;&#10;}&#10;.cal-day--event {&#10; background: var(--bs-secondary-bg);&#10; border-color: #2ecc71;&#10; color: var(--bs-body-color);&#10; font-weight: 700;&#10; cursor: pointer;&#10;}&#10;.cal-day--event:hover {&#10; background: rgba(46, 204, 113, 0.12);&#10;}&#10;.cal-day--past {&#10; opacity: 0.4;&#10;}&#10;.cal-event-dot {&#10; width: 5px;&#10; height: 5px;&#10; border-radius: 50%;&#10; background: #2ecc71;&#10; position: absolute;&#10; bottom: 4px;&#10;}&#10;&#10;/* ── Meetup Cards ── */&#10;.meetup-list {&#10; display: flex;&#10; flex-direction: column;&#10; gap: 1rem;&#10;}&#10;.meetup-card {&#10; display: flex;&#10; gap: 1.2rem;&#10; align-items: flex-start;&#10; padding: 1.2rem;&#10; border-radius: 10px;&#10; border: 1px solid var(--bs-border-color);&#10; background: var(--bs-secondary-bg);&#10; transition: all 0.2s;&#10; text-decoration: none;&#10; color: inherit;&#10;}&#10;.meetup-card:hover {&#10; border-color: #2ecc71;&#10; background: var(--bs-body-bg);&#10; text-decoration: none;&#10; color: inherit;&#10;}&#10;.meetup-date-badge {&#10; display: flex;&#10; flex-direction: column;&#10; align-items: center;&#10; justify-content: center;&#10; min-width: 52px;&#10; height: 56px;&#10; border-radius: 10px;&#10; background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);&#10; color: #fff;&#10; flex-shrink: 0;&#10;}&#10;.meetup-date-badge--past {&#10; background: var(--bs-border-color);&#10; color: var(--bs-secondary-color);&#10;}&#10;.meetup-badge-day {&#10; font-size: 1.3rem;&#10; font-weight: 800;&#10; line-height: 1;&#10;}&#10;.meetup-badge-mon {&#10; font-size: 0.6rem;&#10; font-weight: 700;&#10; text-transform: uppercase;&#10; letter-spacing: 0.06em;&#10; opacity: 0.9;&#10;}&#10;.meetup-info { flex: 1; min-width: 0; }&#10;.meetup-name {&#10; font-size: 0.92rem;&#10; font-weight: 700;&#10; color: var(--bs-body-color);&#10; margin-bottom: 0.15rem;&#10; line-height: 1.3;&#10;}&#10;.meetup-group {&#10; font-size: 0.78rem;&#10; color: var(--bs-secondary-color);&#10; font-weight: 500;&#10; margin-bottom: 0.35rem;&#10;}&#10;.meetup-meta {&#10; display: flex;&#10; flex-wrap: wrap;&#10; gap: 0.5rem;&#10;}&#10;.meetup-meta-pill {&#10; display: inline-flex;&#10; align-items: center;&#10; gap: 0.3rem;&#10; font-size: 0.7rem;&#10; color: var(--bs-secondary-color);&#10; background: var(--bs-body-bg);&#10; border: 1px solid var(--bs-border-color);&#10; border-radius: 5px;&#10; padding: 0.15rem 0.5rem;&#10; font-weight: 500;&#10;}&#10;.meetup-status-badge {&#10; display: inline-flex;&#10; align-items: center;&#10; gap: 0.3rem;&#10; font-size: 0.68rem;&#10; font-weight: 700;&#10; padding: 0.2rem 0.6rem;&#10; border-radius: 20px;&#10; white-space: nowrap;&#10; align-self: flex-start;&#10; flex-shrink: 0;&#10;}&#10;.meetup-status-badge--upcoming {&#10; background: rgba(46, 204, 113, 0.12);&#10; color: #27ae60;&#10; border: 1px solid rgba(46, 204, 113, 0.3);&#10;}&#10;.meetup-status-badge--today {&#10; background: rgba(13, 110, 253, 0.12);&#10; color: var(--bs-primary, #0d6efd);&#10; border: 1px solid rgba(13, 110, 253, 0.3);&#10;}&#10;.meetup-status-badge--past {&#10; background: var(--bs-secondary-bg);&#10; color: var(--bs-secondary-color);&#10; border: 1px solid var(--bs-border-color);&#10;}&#10;.meetup-empty {&#10; text-align: center;&#10; padding: 3rem 1rem;&#10; color: var(--bs-secondary-color);&#10; font-size: 0.88rem;&#10;}&#10;.meetup-empty svg {&#10; width: 40px;&#10; height: 40px;&#10; margin-bottom: 0.75rem;&#10; opacity: 0.35;&#10;}&#10;&#10;/* ── Section header (reuse from about) ── */&#10;.resume-section-title {&#10; font-size: 0.7rem;&#10; font-weight: 700;&#10; text-transform: uppercase;&#10; letter-spacing: 0.1em;&#10; color: var(--bs-secondary-color);&#10; margin: 0 0 1rem;&#10; padding-bottom: 0.45rem;&#10; border-bottom: 1px solid var(--bs-border-color);&#10;}&#10;&#10;@media (max-width: 600px) {&#10; .cal-day-name, .cal-day { font-size: 0.7rem; }&#10; .meetup-card { flex-wrap: wrap; }&#10;}&#10;&lt;/style&gt;&#10;&#10;&lt;div class="container my-5"&gt;&#10;&#10; &lt;div class="row g-4"&gt;&#10;&#10; &lt;!-- ── Left: Calendar ── --&gt;&#10; &lt;div class="col-lg-4"&gt;&#10; &lt;div class="main-card p-4"&gt;&#10; &lt;div class="meetup-month-header"&gt;&#10; &lt;div&gt;&#10; &lt;h2 class="meetup-month-title" id="cal-month-title"&gt;&lt;/h2&gt;&#10; &lt;p class="meetup-month-sub"&gt;Meetups I'm attending&lt;/p&gt;</description></item><item><title>Recovery Calculator</title><link>http://InfraHeaven.io/tools/recovery-calculator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/tools/recovery-calculator/</guid><description>&lt;div class="container mt-4"&gt;&#10; &lt;div class="row justify-content-center"&gt;&#10; &lt;div class="col-md-6"&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="eventName"&gt;Event Name:&lt;/label&gt;&#10; &lt;input type="text" class="form-control" id="eventName" placeholder="e.g., Earthquake"&gt;&#10; &lt;/div&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="likelihood"&gt;Likelihood (per year, e.g., 0.01 for once every 100 years):&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="likelihood" min="0" step="0.01" value="0.01"&gt;&#10; &lt;/div&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="mttd"&gt;MTTD (hours):&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="mttd" min="0" value="1"&gt;&#10; &lt;/div&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="mttr"&gt;MTTR (hours):&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="mttr" min="0" value="4"&gt;&#10; &lt;/div&gt;&#10; &lt;button class="btn btn-primary" onclick="addRecoveryCalculation()"&gt;Add Event&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;div class="container mt-4"&gt;&#10; &lt;div class="row justify-content-center"&gt;&#10; &lt;div class="col-md-10"&gt;&#10; &lt;button class="btn btn-secondary mb-3" onclick="exportToCSV('recoveryTable', 'recovery-calculations.csv')"&gt;Export to CSV&lt;/button&gt;&#10; &lt;table class="table table-striped table-bordered" id="recoveryTable"&gt;&#10; &lt;thead class="thead-dark"&gt;&#10; &lt;tr&gt;&#10; &lt;th&gt;Event&lt;/th&gt;&#10; &lt;th&gt;Likelihood (per year)&lt;/th&gt;&#10; &lt;th&gt;MTTD (hours)&lt;/th&gt;&#10; &lt;th&gt;MTTR (hours)&lt;/th&gt;&#10; &lt;th&gt;Expected Annual Downtime (hours)&lt;/th&gt;&#10; &lt;th&gt;Action&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody id="recoveryTableBody"&gt;&#10; &lt;/tbody&gt;&#10; &lt;/table&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;script&gt;&#10;function addRecoveryCalculation() {&#10; const eventName = document.getElementById('eventName').value.trim();&#10; const likelihood = parseFloat(document.getElementById('likelihood').value);&#10; const mttd = parseFloat(document.getElementById('mttd').value);&#10; const mttr = parseFloat(document.getElementById('mttr').value);&#10;&#10; if (!eventName || isNaN(likelihood) || isNaN(mttd) || isNaN(mttr) || likelihood &lt; 0 || mttd &lt; 0 || mttr &lt; 0) {&#10; alert('Please enter valid inputs.');&#10; return;&#10; }&#10;&#10; const expectedAnnualDowntime = likelihood * (mttd + mttr);&#10;&#10; const tableBody = document.getElementById('recoveryTableBody');&#10; const row = tableBody.insertRow();&#10; row.insertCell(0).textContent = eventName;&#10; row.insertCell(1).textContent = likelihood.toFixed(3);&#10; row.insertCell(2).textContent = mttd.toFixed(2);&#10; row.insertCell(3).textContent = mttr.toFixed(2);&#10; row.insertCell(4).textContent = expectedAnnualDowntime.toFixed(2);&#10;&#10; const deleteCell = row.insertCell(5);&#10; const deleteButton = document.createElement('button');&#10; deleteButton.textContent = 'Delete';&#10; deleteButton.className = 'btn btn-danger btn-sm';&#10; deleteButton.onclick = function() {&#10; tableBody.removeChild(row);&#10; };&#10; deleteCell.appendChild(deleteButton);&#10;&#10; &#10; document.getElementById('eventName').value = '';&#10; document.getElementById('likelihood').value = '0.01';&#10; document.getElementById('mttd').value = '1';&#10; document.getElementById('mttr').value = '4';&#10;}&#10;&#10;function exportToCSV(tableId, filename) {&#10; const table = document.getElementById(tableId);&#10; let csv = [];&#10; for (let row of table.rows) {&#10; let rowData = [];&#10; for (let cell of row.cells) {&#10; if (cell.cellIndex &lt; row.cells.length - 1) { &#10; rowData.push('"' + cell.textContent.replace(/"/g, '""') + '"');&#10; }&#10; }&#10; csv.push(rowData.join(','));&#10; }&#10; const csvContent = csv.join('\n');&#10; const blob = new Blob([csvContent], { type: 'text/csv' });&#10; const url = URL.createObjectURL(blob);&#10; const a = document.createElement('a');&#10; a.href = url;&#10; a.download = filename;&#10; a.click();&#10;}&#10;&lt;/script&gt;</description></item><item><title>SLO Calculator</title><link>http://InfraHeaven.io/tools/slo-calculator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/tools/slo-calculator/</guid><description>&lt;div class="container mt-4"&gt;&#10; &lt;div class="row justify-content-center"&gt;&#10; &lt;div class="col-md-6"&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="slo"&gt;SLO Target (%):&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="slo" step="0.1" min="0" max="100" value="99.9"&gt;&#10; &lt;/div&gt;&#10; &lt;button class="btn btn-primary" onclick="calculate()"&gt;Calculate&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;div class="container mt-4"&gt;&#10; &lt;div class="row justify-content-center"&gt;&#10; &lt;div class="col-md-8"&gt;&#10; &lt;div id="results" class="alert alert-info" style="display: none;"&gt;&lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;script&gt;&#10;function calculate() {&#10; const slo = parseFloat(document.getElementById('slo').value);&#10; if (isNaN(slo) || slo &lt; 0 || slo &gt; 100) {&#10; document.getElementById('results').innerHTML = '&lt;p&gt;Please enter a valid SLO between 0 and 100.&lt;/p&gt;';&#10; return;&#10; }&#10;&#10; const allowedDowntimePercent = (100 - slo) / 100;&#10;&#10; const periods = {&#10; Weekly: 7 * 24,&#10; Monthly: 30.44 * 24,&#10; Yearly: 365.25 * 24&#10; };&#10;&#10; let results = '&lt;h4&gt;Allowed Downtime&lt;/h4&gt;&lt;ul class="list-group list-group-flush"&gt;';&#10; for (const [period, hours] of Object.entries(periods)) {&#10; const downtimeHours = allowedDowntimePercent * hours;&#10; const hoursPart = Math.floor(downtimeHours);&#10; const minutesPart = Math.floor((downtimeHours - hoursPart) * 60);&#10; const secondsPart = Math.floor(((downtimeHours - hoursPart) * 60 - minutesPart) * 60);&#10; results += `&lt;li class="list-group-item"&gt;${period}: ${hoursPart} hours, ${minutesPart} minutes, and ${secondsPart} seconds&lt;/li&gt;`;&#10; }&#10; results += '&lt;/ul&gt;';&#10;&#10; const resultsDiv = document.getElementById('results');&#10; resultsDiv.innerHTML = results;&#10; resultsDiv.style.display = 'block';&#10;}&#10;&lt;/script&gt;</description></item><item><title>Toil Calculator</title><link>http://InfraHeaven.io/tools/toil-calculator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/tools/toil-calculator/</guid><description>&lt;div class="container mt-4"&gt;&#10; &lt;div class="row justify-content-center"&gt;&#10; &lt;div class="col-md-6"&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="taskName"&gt;Task Name:&lt;/label&gt;&#10; &lt;input type="text" class="form-control" id="taskName" placeholder="e.g., Clean up Non-Prod"&gt;&#10; &lt;/div&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="timePerTask"&gt;Time per manual task (minutes):&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="timePerTask" min="0" value="30"&gt;&#10; &lt;/div&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="frequency"&gt;Frequency per week:&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="frequency" min="0" value="5"&gt;&#10; &lt;/div&gt;&#10; &lt;div class="form-group"&gt;&#10; &lt;label for="automationTime"&gt;Time to automate (hours):&lt;/label&gt;&#10; &lt;input type="number" class="form-control" id="automationTime" min="0" value="4"&gt;&#10; &lt;/div&gt;&#10; &lt;button class="btn btn-primary" onclick="addToilCalculation()"&gt;Add Task&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;div class="container mt-4"&gt;&#10; &lt;div class="row justify-content-center"&gt;&#10; &lt;div class="col-md-10"&gt;&#10; &lt;button class="btn btn-secondary mb-3" onclick="exportToCSV('toilTable', 'toil-calculations.csv')"&gt;Export to CSV&lt;/button&gt;&#10; &lt;table class="table table-striped table-bordered" id="toilTable"&gt;&#10; &lt;thead class="thead-dark"&gt;&#10; &lt;tr&gt;&#10; &lt;th&gt;Task&lt;/th&gt;&#10; &lt;th&gt;Weekly Time (hours)&lt;/th&gt;&#10; &lt;th&gt;Monthly Time (hours)&lt;/th&gt;&#10; &lt;th&gt;Yearly Time (hours)&lt;/th&gt;&#10; &lt;th&gt;Break-even (weeks)&lt;/th&gt;&#10; &lt;th&gt;Action&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody id="toilTableBody"&gt;&#10; &lt;/tbody&gt;&#10; &lt;/table&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;script&gt;&#10;function addToilCalculation() {&#10; const taskName = document.getElementById('taskName').value.trim();&#10; const timePerTask = parseFloat(document.getElementById('timePerTask').value) / 60; &#10; const frequency = parseFloat(document.getElementById('frequency').value);&#10; const automationTime = parseFloat(document.getElementById('automationTime').value);&#10;&#10; if (!taskName || isNaN(timePerTask) || isNaN(frequency) || isNaN(automationTime) || timePerTask &lt; 0 || frequency &lt; 0 || automationTime &lt; 0) {&#10; alert('Please enter valid inputs.');&#10; return;&#10; }&#10;&#10; const weeklyManualTime = timePerTask * frequency;&#10; const monthlyManualTime = weeklyManualTime * 4.33; &#10; const yearlyManualTime = weeklyManualTime * 52;&#10;&#10; const breakEvenExecutions = automationTime / timePerTask;&#10; const breakEvenWeeks = breakEvenExecutions / frequency;&#10;&#10; const tableBody = document.getElementById('toilTableBody');&#10; const row = tableBody.insertRow();&#10; row.insertCell(0).textContent = taskName;&#10; row.insertCell(1).textContent = weeklyManualTime.toFixed(2);&#10; row.insertCell(2).textContent = monthlyManualTime.toFixed(2);&#10; row.insertCell(3).textContent = yearlyManualTime.toFixed(2);&#10; row.insertCell(4).textContent = breakEvenWeeks.toFixed(1);&#10;&#10; const deleteCell = row.insertCell(5);&#10; const deleteButton = document.createElement('button');&#10; deleteButton.textContent = 'Delete';&#10; deleteButton.className = 'btn btn-danger btn-sm';&#10; deleteButton.onclick = function() {&#10; tableBody.removeChild(row);&#10; };&#10; deleteCell.appendChild(deleteButton);&#10;&#10; &#10; document.getElementById('taskName').value = '';&#10; document.getElementById('timePerTask').value = '30';&#10; document.getElementById('frequency').value = '5';&#10; document.getElementById('automationTime').value = '4';&#10;}&#10;&#10;function exportToCSV(tableId, filename) {&#10; const table = document.getElementById(tableId);&#10; let csv = [];&#10; for (let row of table.rows) {&#10; let rowData = [];&#10; for (let cell of row.cells) {&#10; if (cell.cellIndex &lt; row.cells.length - 1) { &#10; rowData.push('"' + cell.textContent.replace(/"/g, '""') + '"');&#10; }&#10; }&#10; csv.push(rowData.join(','));&#10; }&#10; const csvContent = csv.join('\n');&#10; const blob = new Blob([csvContent], { type: 'text/csv' });&#10; const url = URL.createObjectURL(blob);&#10; const a = document.createElement('a');&#10; a.href = url;&#10; a.download = filename;&#10; a.click();&#10;}&#10;&lt;/script&gt;</description></item><item><title>YAML Validator</title><link>http://InfraHeaven.io/tools/yaml-validator/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://InfraHeaven.io/tools/yaml-validator/</guid><description>&lt;script src="https://cdn.jsdelivr.net/npm/js-yaml@4/dist/js-yaml.min.js"&gt;&lt;/script&gt;&#10;&#10;&lt;div class="yaml-validator-wrap"&gt;&#10; &lt;div class="yaml-mode-toggle"&gt;&#10; &lt;button id="modeYamlToJson" class="yaml-mode-btn yaml-mode-btn--active" onclick="yamlSetMode('yaml2json')"&gt;YAML → JSON&lt;/button&gt;&#10; &lt;button id="modeJsonToYaml" class="yaml-mode-btn" onclick="yamlSetMode('json2yaml')"&gt;JSON → YAML&lt;/button&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class="yaml-validator-editor-row"&gt;&#10; &lt;div class="yaml-validator-pane"&gt;&#10; &lt;div class="yaml-validator-pane-header"&gt;&#10; &lt;span id="yamlInputLabel"&gt;YAML Input&lt;/span&gt;&#10; &lt;div class="yaml-validator-header-actions"&gt;&#10; &lt;button class="yaml-btn yaml-btn-ghost" onclick="yamlClearInput()"&gt;Clear&lt;/button&gt;&#10; &lt;button class="yaml-btn yaml-btn-ghost" onclick="yamlLoadSample()"&gt;Sample&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10; &lt;textarea&#10; id="yamlInput"&#10; class="yaml-textarea"&#10; spellcheck="false"&#10; placeholder="Paste your YAML here..."&#10; oninput="yamlValidate()"&#10; &gt;&lt;/textarea&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div class="yaml-validator-pane"&gt;&#10; &lt;div class="yaml-validator-pane-header"&gt;&#10; &lt;span id="yamlOutputLabel"&gt;JSON Output&lt;/span&gt;&#10; &lt;button class="yaml-btn yaml-btn-ghost" id="yamlCopyBtn" onclick="yamlCopyOutput()"&gt;Copy JSON&lt;/button&gt;&#10; &lt;/div&gt;&#10; &lt;pre id="yamlOutput" class="yaml-output yaml-output--empty"&gt;Output will appear here...&lt;/pre&gt;&#10; &lt;/div&gt;&#10; &lt;/div&gt;&#10;&#10; &lt;div id="yamlStatus" class="yaml-status yaml-status--idle"&gt;&#10; &lt;i class="fas fa-circle-info"&gt;&lt;/i&gt; Waiting for input…&#10; &lt;/div&gt;&#10;&#10; &lt;div id="yamlStats" class="yaml-stats" style="display:none;"&gt;&#10; &lt;span id="yamlStatKeys"&gt;&lt;/span&gt;&#10; &lt;span id="yamlStatLines"&gt;&lt;/span&gt;&#10; &lt;span id="yamlStatSize"&gt;&lt;/span&gt;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;style&gt;&#10;.yaml-validator-wrap {&#10; font-family: var(--bs-font-sans-serif);&#10;}&#10;&#10;.yaml-validator-editor-row {&#10; display: grid;&#10; grid-template-columns: 1fr 1fr;&#10; gap: 1rem;&#10;}&#10;&#10;@media (max-width: 700px) {&#10; .yaml-validator-editor-row {&#10; grid-template-columns: 1fr;&#10; }&#10;}&#10;&#10;.yaml-validator-pane {&#10; display: flex;&#10; flex-direction: column;&#10; border: 1px solid var(--bs-border-color);&#10; border-radius: 10px;&#10; overflow: hidden;&#10;}&#10;&#10;.yaml-validator-pane-header {&#10; display: flex;&#10; align-items: center;&#10; justify-content: space-between;&#10; padding: 0.5rem 0.875rem;&#10; background: var(--bs-secondary-bg);&#10; border-bottom: 1px solid var(--bs-border-color);&#10; font-size: 0.8rem;&#10; font-weight: 600;&#10; text-transform: uppercase;&#10; letter-spacing: 0.05em;&#10; color: var(--bs-secondary-color);&#10;}&#10;&#10;.yaml-validator-header-actions {&#10; display: flex;&#10; gap: 0.4rem;&#10;}&#10;&#10;.yaml-btn {&#10; font-size: 0.75rem;&#10; font-weight: 600;&#10; padding: 0.2rem 0.6rem;&#10; border-radius: 6px;&#10; border: 1px solid var(--bs-border-color);&#10; cursor: pointer;&#10; transition: all 0.15s;&#10;}&#10;&#10;.yaml-btn-ghost {&#10; background: transparent;&#10; color: var(--bs-secondary-color);&#10;}&#10;&#10;.yaml-btn-ghost:hover {&#10; background: var(--bs-body-color);&#10; color: var(--bs-body-bg);&#10; border-color: var(--bs-body-color);&#10;}&#10;&#10;.yaml-textarea {&#10; flex: 1;&#10; min-height: 320px;&#10; padding: 1rem;&#10; font-family: 'Courier New', Courier, monospace;&#10; font-size: 0.85rem;&#10; line-height: 1.6;&#10; background: var(--bs-body-bg);&#10; color: var(--bs-body-color);&#10; border: none;&#10; resize: vertical;&#10; outline: none;&#10;}&#10;&#10;.yaml-output {&#10; flex: 1;&#10; min-height: 320px;&#10; padding: 1rem;&#10; font-family: 'Courier New', Courier, monospace;&#10; font-size: 0.85rem;&#10; line-height: 1.6;&#10; background: var(--bs-body-bg);&#10; color: var(--bs-body-color);&#10; margin: 0;&#10; overflow: auto;&#10; white-space: pre-wrap;&#10; word-break: break-all;&#10;}&#10;&#10;.yaml-output--empty {&#10; color: var(--bs-secondary-color);&#10;}&#10;&#10;.yaml-output--valid {&#10; color: #55efc4;&#10;}&#10;&#10;.yaml-output--error {&#10; color: #ff7675;&#10;}&#10;&#10;.yaml-status {&#10; margin-top: 0.875rem;&#10; padding: 0.6rem 1rem;&#10; border-radius: 8px;&#10; font-size: 0.9rem;&#10; font-weight: 500;&#10;}&#10;&#10;.yaml-status--idle {&#10; background: var(--bs-secondary-bg);&#10; color: var(--bs-secondary-color);&#10;}&#10;&#10;.yaml-status--valid {&#10; background: #1a3d2b;&#10; color: #55efc4;&#10;}&#10;&#10;.yaml-status--error {&#10; background: #3d1a1a;&#10; color: #ff7675;&#10;}&#10;&#10;.yaml-status--warning {&#10; background: #3d2b1f;&#10; color: #f4a261;&#10;}&#10;&#10;.yaml-stats {&#10; display: flex;&#10; gap: 1.25rem;&#10; margin-top: 0.6rem;&#10; font-size: 0.8rem;&#10; color: var(--bs-secondary-color);&#10;}&#10;&#10;.yaml-stats span::before {&#10; margin-right: 0.25rem;&#10;}&#10;&#10;.yaml-mode-toggle {&#10; display: flex;&#10; gap: 0.5rem;&#10; margin-bottom: 1rem;&#10;}&#10;&#10;.yaml-mode-btn {&#10; font-size: 0.85rem;&#10; font-weight: 600;&#10; padding: 0.4rem 1.1rem;&#10; border-radius: 8px;&#10; border: 1px solid var(--bs-border-color);&#10; background: transparent;&#10; color: var(--bs-secondary-color);&#10; cursor: pointer;&#10; transition: all 0.15s;&#10;}&#10;&#10;.yaml-mode-btn--active {&#10; background: var(--bs-body-color);&#10; color: var(--bs-body-bg);&#10; border-color: var(--bs-body-color);&#10;}&#10;&lt;/style&gt;&#10;&#10;&lt;script&gt;&#10;let currentMode = 'yaml2json'; &#10;&#10;const JSON_SAMPLE = JSON.stringify({&#10; apiVersion: 'apps/v1',&#10; kind: 'Deployment',&#10; metadata: { name: 'my-app', namespace: 'default', labels: { app: 'my-app', version: '1.0.0' } },&#10; spec: {&#10; replicas: 3,&#10; selector: { matchLabels: { app: 'my-app' } },&#10; template: {&#10; metadata: { labels: { app: 'my-app' } },&#10; spec: {&#10; containers: [{&#10; name: 'my-app', image: 'my-app:1.0.0',&#10; ports: [{ containerPort: 8080 }],&#10; resources: { requests: { cpu: '100m', memory: '128Mi' }, limits: { cpu: '500m', memory: '256Mi' } },&#10; env: [{ name: 'ENV', value: 'production' }]&#10; }]&#10; }&#10; }&#10; }&#10;}, null, 2);&#10;&#10;function yamlSetMode(mode) {&#10; currentMode = mode;&#10; const textarea = document.getElementById('yamlInput');&#10; document.getElementById('modeYamlToJson').className = mode === 'yaml2json' ? 'yaml-mode-btn yaml-mode-btn--active' : 'yaml-mode-btn';&#10; document.getElementById('modeJsonToYaml').className = mode === 'json2yaml' ? 'yaml-mode-btn yaml-mode-btn--active' : 'yaml-mode-btn';&#10; document.getElementById('yamlInputLabel').textContent = mode === 'yaml2json' ? 'YAML Input' : 'JSON Input';&#10; document.getElementById('yamlOutputLabel').textContent = mode === 'yaml2json' ? 'JSON Output' : 'YAML Output';&#10; document.getElementById('yamlCopyBtn').textContent = mode === 'yaml2json' ? 'Copy JSON' : 'Copy YAML';&#10; textarea.placeholder = mode === 'yaml2json' ? 'Paste your YAML here...' : 'Paste your JSON here...';&#10; textarea.value = '';&#10; yamlValidate();&#10;}&#10;&#10;const YAML_SAMPLE = `# Sample Kubernetes Deployment&#10;apiVersion: apps/v1&#10;kind: Deployment&#10;metadata:&#10; name: my-app&#10; namespace: default&#10; labels:&#10; app: my-app&#10; version: "1.0.0"&#10;spec:&#10; replicas: 3&#10; selector:&#10; matchLabels:&#10; app: my-app&#10; template:&#10; metadata:&#10; labels:&#10; app: my-app&#10; spec:&#10; containers:&#10; - name: my-app&#10; image: my-app:1.0.0&#10; ports:&#10; - containerPort: 8080&#10; resources:&#10; requests:&#10; cpu: "100m"&#10; memory: "128Mi"&#10; limits:&#10; cpu: "500m"&#10; memory: "256Mi"&#10; env:&#10; - name: ENV&#10; value: production&#10;`;&#10;&#10;function yamlValidate() {&#10; const input = document.getElementById('yamlInput').value;&#10; const outputEl = document.getElementById('yamlOutput');&#10; const statusEl = document.getElementById('yamlStatus');&#10; const statsEl = document.getElementById('yamlStats');&#10;&#10; if (!input.trim()) {&#10; outputEl.textContent = 'Output will appear here...';&#10; outputEl.className = 'yaml-output yaml-output--empty';&#10; statusEl.className = 'yaml-status yaml-status--idle';&#10; statusEl.innerHTML = '&lt;i class="fas fa-circle-info"&gt;&lt;/i&gt; Waiting for input…';&#10; statsEl.style.display = 'none';&#10; return;&#10; }&#10;&#10; try {&#10; let outputText, parsed;&#10; if (currentMode === 'yaml2json') {&#10; parsed = jsyaml.load(input);&#10; if (parsed === null || parsed === undefined) {&#10; outputEl.textContent = '(empty document)';&#10; outputEl.className = 'yaml-output yaml-output--warning';&#10; statusEl.className = 'yaml-status yaml-status--warning';&#10; statusEl.innerHTML = '&lt;i class="fas fa-triangle-exclamation"&gt;&lt;/i&gt; Valid YAML but document is empty.';&#10; statsEl.style.display = 'none';&#10; return;&#10; }&#10; outputText = JSON.stringify(parsed, null, 2);&#10; statusEl.innerHTML = '&lt;i class="fas fa-circle-check"&gt;&lt;/i&gt; Valid YAML!';&#10; } else {&#10; parsed = JSON.parse(input);&#10; outputText = jsyaml.dump(parsed, { indent: 2, lineWidth: -1 });&#10; statusEl.innerHTML = '&lt;i class="fas fa-circle-check"&gt;&lt;/i&gt; Valid JSON!';&#10; }&#10;&#10; outputEl.textContent = outputText;&#10; outputEl.className = 'yaml-output yaml-output--valid';&#10; statusEl.className = 'yaml-status yaml-status--valid';&#10;&#10; const lines = input.split('\n').length;&#10; const size = new Blob([input]).size;&#10; const keyCount = countKeys(parsed);&#10; document.getElementById('yamlStatKeys').textContent = `Keys: ${keyCount}`;&#10; document.getElementById('yamlStatLines').textContent = `Lines: ${lines}`;&#10; document.getElementById('yamlStatSize').textContent = `Size: ${size} bytes`;&#10; statsEl.style.display = 'flex';&#10;&#10; } catch (e) {&#10; outputEl.textContent = e.message;&#10; outputEl.className = 'yaml-output yaml-output--error';&#10; statusEl.className = 'yaml-status yaml-status--error';&#10; const lineMatch = e.message.match(/line (\d+)/i);&#10; const lineHint = lineMatch ? ` (line ${lineMatch[1]})` : '';&#10; const label = currentMode === 'yaml2json' ? 'YAML' : 'JSON';&#10; statusEl.innerHTML = `&lt;i class="fas fa-circle-xmark"&gt;&lt;/i&gt; Invalid ${label}${lineHint}: ${e.reason || e.message}`;&#10; statsEl.style.display = 'none';&#10; }&#10;}&#10;&#10;function countKeys(obj) {&#10; if (typeof obj !== 'object' || obj === null) return 0;&#10; let count = 0;&#10; for (const key in obj) {&#10; count++;&#10; count += countKeys(obj[key]);&#10; }&#10; return count;&#10;}&#10;&#10;function yamlClearInput() {&#10; document.getElementById('yamlInput').value = '';&#10; yamlValidate();&#10;}&#10;&#10;function yamlLoadSample() {&#10; document.getElementById('yamlInput').value = currentMode === 'yaml2json' ? YAML_SAMPLE : JSON_SAMPLE;&#10; yamlValidate();&#10;}&#10;&#10;function yamlCopyOutput() {&#10; const text = document.getElementById('yamlOutput').textContent;&#10; if (!text || text === 'Output will appear here...') return;&#10; navigator.clipboard.writeText(text).then(() =&gt; {&#10; const btn = event.target;&#10; const original = btn.textContent;&#10; btn.textContent = 'Copied!';&#10; setTimeout(() =&gt; { btn.textContent = original; }, 1500);&#10; });&#10;}&#10;&lt;/script&gt;</description></item></channel></rss>