Compare commits

72 Commits

Author SHA1 Message Date
jwright cf900b3409 images: bust CF cache for the de-watermarked art; add the dewatermark tool
build-and-deploy / build (push) Failing after 10m31s
public/ assets are edge-cached under non-hashed URLs, so the scrubbed covers
and heroes were still being served stale. Project covers carried no cache-bust
at all - added ?v=3 to both cover references and bumped the blog heroes from
v2 to v3. Adds scripts/dewatermark.mjs implementing the documented approach
(hard-coded mark centre, feathered disc, border-mean seed, Jacobi diffusion)
rather than the ffmpeg delogo used in the previous pass.
2026-08-25 18:20:29 +10:00
jwright 4a8552c408 images: remove generator watermark from all published art
build-and-deploy / build (push) Failing after 10m22s
The pale four-point star bottom-right in seven project covers and four blog
heroes is the image generator's watermark, not house style. Scrubbed via
delogo and verified numerically (peak luminance in the mark's box drops from
~100-255 to under 30 in every file); thumbnails regenerated from the cleaned
masters. The cover prompt file wrongly described the mark as part of the house
look and asked new generations to include one - corrected, with a reject-list
entry naming the mark and its usual position.
2026-08-25 18:00:38 +10:00
jwright b5e0b25667 site update: cover art for the learn-site case studies; publish untrusted-in-a-vm (SECURITY #7)
build-and-deploy / build (push) Failing after 11m8s
2026-08-25 17:24:51 +10:00
jwright c5b1044d7a projects: add The Mirror pull-through registry case study; wire cover art for both learn-site projects
build-and-deploy / build (push) Failing after 12m49s
Adds mirror-registry as a featured project (order 26, beside The Exploded
Cluster) covering the zot deployment, the bootstrap and auth circular
dependencies, and the retention/revalidation behaviours that corrected the
first published write-up. Adds the cover: field to exploded-cluster (it had
none, so the card rendered imageless) and stages public/covers/image.md with
the generation prompts and processing steps for both covers.
2026-08-25 09:18:11 +10:00
jwright 8d394fe2c7 nav hover: glow instead of the underline bar, and give keyboard focus the same glow
build-and-deploy / build (push) Failing after 11m32s
2026-08-18 11:04:27 +10:00
jwright 21616762e6 nav: add a Learn tab pointing at the teaching site, and drop the scroll-spy underline - tracking the active section as you scrolled read as clunky, so the underline is hover-only now; also refresh the public CV
build-and-deploy / build (push) Failing after 15m10s
2026-08-18 10:05:49 +10:00
jwright f6b683713e publish the public CV and fix the availability check to resolve from the project root - Vite rewrites module URLs at build so the relative walk never found public/
build-and-deploy / build (push) Failing after 14m0s
2026-08-18 09:56:59 +10:00
jwright b6d3d4487a offer the CV download in the contact section too, so a recruiter who scrolls straight there can grab it
build-and-deploy / build (push) Failing after 10m40s
2026-08-18 09:50:21 +10:00
jwright dd2207bfa6 add the exploded cluster as a project with a live link to learn.bztmon.com
build-and-deploy / build (push) Failing after 15m2s
2026-08-18 09:40:58 +10:00
jwright 51d1ccb149 README: describe the JS that actually ships instead of claiming zero, and note why the site still works with JS disabled 2026-08-18 02:03:57 +10:00
jwright 36444c3424 corrections from Jonathon: Unimus and NetBox were different jobs on different infrastructure, so NetBox comes out of the network project entirely; the Google migration was off GCP and Google Workspace onto Windows AD, Microsoft 365 and Azure; the ERP refresh included daily COBOL green-screen work; contact line kept in one place
build-and-deploy / build (push) Failing after 11m10s
2026-08-18 01:54:49 +10:00
jwright 7cbc3629a6 second pass: name the real GPU mechanism (resource request schedules, init container gates startup, probes track runtime health), scope the network DR and reversibility claims to what config backup actually gives you, drop production-grade, and colour the education hash instead of the first letter
build-and-deploy / build (push) Failing after 10m52s
2026-08-18 01:50:07 +10:00
jwright c07f8433dc revert the homepage JSON-LD: application/ld+json is still an inline script under script-src 'self', and the no-inline-script posture is worth more than the structured data
build-and-deploy / build (push) Successful in 15s
2026-08-18 01:36:47 +10:00
jwright 0f85bd9870 add Person and WebSite structured data on the homepage so a search for the name resolves to the right engineer
build-and-deploy / build (push) Failing after 10m35s
2026-08-18 01:15:24 +10:00
jwright 6155aca055 bastion post: drop the host address and the list of credentials it holds - the architecture makes the point without the inventory
build-and-deploy / build (push) Failing after 11m58s
2026-08-18 01:13:57 +10:00
jwright 573e7a6002 blog: give each post its own closing heading instead of sixteen sections called 'The principle'
build-and-deploy / build (push) Failing after 13m5s
2026-08-18 01:12:54 +10:00
jwright 25425d6f6a portfolio pass: correct the CSP claim to match what nginx sends, rename the project outcome label, plain-english the projects intro and bio, standardise on Argo CD and single-touch, drop the education filler line
build-and-deploy / build (push) Failing after 14m34s
2026-08-18 01:11:25 +10:00
jwright 7542ae2665 turn off smartypants so the rendered pages stay ascii
build-and-deploy / build (push) Failing after 12m30s
2026-08-17 23:23:31 +10:00
jwright 43188589ab sweep the last typographic punctuation out of the components and data files
build-and-deploy / build (push) Failing after 14m21s
2026-08-17 23:21:38 +10:00
jwright 3cfd59a464 copy pass: straighten quotes and arrows, thin the dash asides, drop the app-count caption from the constellation
build-and-deploy / build (push) Failing after 13m18s
2026-08-17 23:12:42 +10:00
jwright 78e8740408 deps: clear the nanoid and ip-address high advisories flagged by the audit gate
build-and-deploy / build (push) Failing after 12m55s
2026-08-17 02:08:03 +10:00
jwright e90bf177a5 blog: open the EDGE AI, AUTOMATION and OBSERVABILITY arcs
build-and-deploy / build (push) Failing after 10m35s
Three arc-opening posts, each with hero and diagram:
- probation-for-models (EDGE AI 01)
- one-value-many-enforcers (AUTOMATION 01)
- six-days-of-silence (OBSERVABILITY 01)
2026-08-17 01:50:25 +10:00
jwright 6c0d7643a6 gitignore python bytecode 2026-08-05 02:02:11 +10:00
jwright 3a13cff1fa gitignore local blog-workshop tooling 2026-08-04 12:31:44 +10:00
jwright df72a22952 docs: refresh README for the current layout; ASCII sweep on SECURITY.md 2026-08-04 03:45:58 +10:00
jwright f89957370e gitignore local staging
Draft posts are private staging. They reach the public only by being moved
into src/content/blog at publish time, deliberately, one at a time.
2026-08-04 03:28:31 +10:00
jwright d26e981d7a blog: expand the series to six pillars; add the catalogue generator
Drafted from the last month of real work - OpenShift fleet hardening, edge AI
and GPU, network policy enforcement, supply-chain security, automation and
observability - weighted toward the work stack the site advertises. Each has
a photo prompt in the house palette and a diagram brief.

Adds BLOG-LEDGER.md, generated from disk so it cannot drift, listing every
live and drafted idea in one place. Search it before drafting; claim the idea
when the folder is created rather than at publish, so two sessions cannot
collide. Expands the series from three arcs to six now the catalogue is deep
enough to split edge AI, automation and observability out of PLATFORM.
2026-08-04 03:21:49 +10:00
jwright cbfc4007cf projects: cover art on the dossier cards, replacing the sketch thumbnails
The inline SVG thumbs were placeholder-grade next to real art. Cards now use
a 640px mini of the same cover (9-22KB), object-fit cover with a slow hover
zoom, and a corner scrim so the DOSSIER tag stays legible over the brighter
frames. Falls back to the sketch for any project without a cover.
2026-08-03 17:57:44 +10:00
jwright cd08cd04c1 projects: cover art on every case study
Six generated covers wired in via an optional cover key - 2528x1696 sources
optimised to 1600px webp, 49-144KB each. They sit on the page background
rather than the diagrams' light card, since the art is already generated in
the site palette on near-black. Cover carries the story at a glance, the
diagram still carries the architecture below it.
2026-08-03 17:53:25 +10:00
jwright ba81705a39 projects: drop the two diagram refs that never had files
gpu-as-code and global-infra-modernisation pointed at diagrams that do not
exist, rendering a dead image box on a light card. Removing the key is
better than a broken frame until real art lands. Prompt pack for all six
covers ships alongside, in the blog-hero visual language.
2026-08-03 17:34:07 +10:00
jwright 99fd652569 projects: author the missing network-fleet diagram (was a 404 on the case study) 2026-08-03 17:32:52 +10:00
jwright afba75dd3a capabilities: keep the video-analytics line vendor-neutral 2026-08-03 16:58:14 +10:00
jwright f7e64cebe5 capabilities: proof-points fill the tall tiles; headers rebalanced to the flagship weight
The hero and full-row tiles stretched to their grid height with nothing
between blurb and chips - reading as missing body. Adds a points field
rendered as accent-dashed proof lines (flagship gets four, observability
three), each grounded in the running estate. Category titles step up to
23px/700 with the flagship at 30px so the hierarchy holds.
2026-08-03 16:54:53 +10:00
jwright 6c7e577633 blog: three curated series across the whole catalogue + badge on post pages
Extends the lone security thread into three arcs that read as skill pillars -
SECURITY (6), NETWORKING (5), PLATFORM (7) - so every post carries a numbered
badge and the catalogue advertises three areas of depth rather than a loose
list. Adds the badge to the post detail header too, styled to match the cards.
2026-08-03 16:47:07 +10:00
jwright 6ea2582a66 blog: heroes belong in frontmatter - fix the five new posts
The five carried the hero only as an inline markdown image, but the index
cards and the post hero figure render from hero/heroAlt frontmatter - so
the cards showed no image and the post pages lost the hero treatment.
Moved each hero into frontmatter and dropped the inline line (the body
keeps only the diagram), matching every earlier post.
2026-08-03 08:24:22 +10:00
jwright 317e5230b6 deps: clear the audit gate (astro 7.0.10 + transitive fixes); reword one-doorman past the secret scan
npm audit fix - 13 advisories incl. astro high and tar critical, all
non-breaking, gates re-run green locally. The dist secret scan greps
password-adjacent patterns, so the post now says htpasswd file (which the
registry auth actually is), credential vault, and lock-of-its-own.
2026-08-02 23:34:17 +10:00
jwright 521cc2f066 blog: publish five posts - landing-on-cilium, sleep-half-the-fleet, clone-not-keys, stage-the-locks, one-doorman-many-doors
Heroes optimised to 1600px webp from the prepped batch; diagrams authored
to the house palette; dates set to publish day.
2026-08-02 23:17:31 +10:00
jwright 91b3cff67a blog: publish three posts — observe-first-deny-second, snat-ate-my-source, no-ssh
Heroes de-watermarked via the calibrated Jacobi diffuse (dewatermark.mjs now
parameterised src/out), diagrams authored in the house light-card style.
2026-07-14 18:43:32 +10:00
jwright a0d71388c4 constellation: seamless animation loops — dash offset aligned to the 136px pattern period (dots no longer jump at the seam), radar ring fades in instead of popping 2026-07-13 15:32:09 +10:00
jwright 3217d779b3 constellation: key to the right edge (left clips on narrow heroes), bat-gloss swatch for core/services 2026-07-13 15:16:24 +10:00
jwright 94698c6315 constellation: intel accent for alfred + elfastc, compute colour key, drop the stale kate caption 2026-07-13 15:00:11 +10:00
jwright 87cfe78ffc blog: publish three posts — backup restore drills, the bastion pattern, split-horizon DNS-01 (heroes + diagrams) 2026-07-09 15:02:17 +10:00
jwright 2896f89af3 footer: unify the receipt + identity into one terminal session panel; fix HEAD sha in container builds (GIT_SHA build-arg) 2026-07-09 14:04:54 +10:00
jwright 908d311e5c fix(skills): collapse bento spans at 2-col breakpoint, not 720px
Landscape phone (720-1000px band) put the parent grid at 2 columns while the
tile spans (hero span-2, full span-3) only reset at 720px. A span-3 tile in a
2-col grid forced a phantom 3rd column and collapsed the 1fr tracks to
58px 58px 760px, squishing the flagship card to ~132px off the visible area.
Move the span reset to the same 1000px breakpoint the grid drops to 2 columns:
now 446px 446px, hero full-width. Desktop 4-col + portrait 1-col unchanged.
2026-07-03 23:56:07 +10:00
jwright 0d05aae721 blog: republish 2 edited posts, dossier thumbnails 4-6, new post broker-pattern
Content: shipping-this-site + init-gating-gpu-readiness get their edited-workshop bodies (from the
NAS Published/ pass) - site frontmatter (hero/heroAlt) kept, only prose replaced.

Dossiers 004-006 (gpu-as-code, global-infra-modernisation, network-automation-fleet) were all
rendering the same generic pipeline thumbnail - added 3 tailored DossierThumb motifs (gpu die +
passthrough lanes, region globe, switch-fleet grid) via a new diagram: frontmatter field. Also
closed the dead-space gap above the tag row on short-argument cards with a faint on-theme grid
fill (::before, masked fade) instead of a flat void.

New post: broker-pattern ('An agent should never hold the key it's using') - SECURITY SERIES 02,
between secret-zero and workload-least-privilege. Hero de-watermarked from the NAS source (resized
to the calibrated 1600x1073 coordinate space, Jacobi-diffused out the Nano Banana sparkle at its
known center) via scripts/dewatermark-broker-pattern.mjs (reusable for the next 3 drafts). New
diagram/broker-pattern.svg in the house light-card palette. Inline top-of-body hero stripped per
the astro-static-site skill guard (frontmatter hero: is now the only render path).
2026-07-03 21:12:52 +10:00
jwright c162f2e4e7 nav: phone tabs on a compact second row - zero-JS, scrollable, no hamburger
The links were display:none under 720px (desktop-only nav). Phones now wrap the bar: brand +
theme toggle row 1, the five tabs row 2 (mono 0.78rem, overflow-x scroll, hidden scrollbar).
Media block placed AFTER the base rules - same-specificity source order was eating the first attempt.
2026-07-03 20:47:09 +10:00
jwright de7c257ab7 hero: wrap the fleet-fact bar on phones - nowrap+max-content busts a 390px viewport 2026-07-03 20:16:52 +10:00
jwright 71c3f00618 facelift: the Bat-Computer redesign - constellation hero, bento, dossiers, receipt
Promotes the blog art identity to the whole site. Fleet constellation hero (six real nodes,
dual-trace packets, typing fleet facts), Chakra Petch + JetBrains Mono self-hosted, bento
capabilities, dossier work cards, security-series badges, featured blog index, GitOps receipt
footer, native view transitions, light blueprint theme. CSP strict throughout; blog content
byte-identical.
2026-07-03 20:08:12 +10:00
jwright 4e13bf596c facelift: fix light-theme scrims/thumbs - :global() the data-theme overrides
Astro scopes compound selectors, so [data-theme=light] .hero__scrim inside a component style
block could never match html[data-theme] - the light theme rendered dark scrims (embossed
low-contrast copy). Wrap the theme prefix in :global() in Hero, ProjectCard and the blog index.
Light 'blueprint' hero now reads clean.
2026-07-03 19:50:18 +10:00
jwright 8a7ca88c40 facelift polish: native cross-document view transitions + og regen
@view-transition navigation:auto (pure CSS crossfade, 180ms; Firefox ignores, reduced-motion
disables) - zero JS, zero CSP surface. og.png regenerated from the electric-identity script.
Favicon already carries the identity (gradient jw chip + caret) - deliberately unchanged.
2026-07-03 19:47:03 +10:00
jwright de12f028eb facelift: blog index - featured security-series opener + image-led grid
Breadcrumb eyebrow (~/jwright/blog), 'Notes from the build.' H1, featured card for the series
opener (text left / hero art scrimmed right, series badge, eager-loaded), remaining posts in the
image-led responsive grid with series badges + reading time. Post CONTENT untouched (index only).
2026-07-03 19:46:07 +10:00
jwright 8d955aa802 facelift: the scroll story - bento capabilities, dossier cards, series badges, GitOps receipt
02 Capabilities becomes a 4-col bento (Edge AI 2x2 flagship w/ magenta corner radial, Platform
wide, magenta-flavoured security tile, Observability row-completer) driven by span/flavor fields
in skills.ts. 03 Selected Work becomes dossier cards: house-style SVG thumbnails (per-project
motif via DossierThumb), DOSSIER/00N corner tags, 'the argument' line from the outcome field,
chip tags, mono meta footer. 04 Writing cards gain the security-series badge (data/series.ts)
and sit three across. Section numbers glow cyan; a circuit divider (gradient hairlines + two
diamonds) closes the hero. Footer opens with the GitOps receipt - git sha + build date baked at
build time via lib/build-info.ts, zero runtime calls.
2026-07-03 19:43:45 +10:00
jwright 65ba1ec7dc facelift: fleet constellation hero + Bat-Computer type/token layer
Chakra Petch site-wide (300-700) + JetBrains Mono variable, all self-hosted (CSP zero-external
holds). Token layer gains the mock-exact text tiers, glass panel, cyan-tinted borders, glows,
tighter radii; light 'blueprint' variants included. New Constellation.astro: the six real nodes
as IC chips on dual-trace PCB paths (cyan out / magenta back, dasharray packets), radar ring on
the cave hub, vias at bends - pure SVG + scoped CSS keyframes, full reduced-motion fallback.
Hero rebuilt: scrim stack + masked grid over the board art, two-line gradient nameplate, typing
bar cycling six real fleet facts via the external site.js (facts from src/data/fleet.ts), scroll
indicator. Facts wrap, never clip.
2026-07-03 18:58:49 +10:00
jwright 08bab080b1 nav: stop tabs sticking lit on Projects/Blog pages
build-and-deploy / build (push) Failing after 14m26s
On pages with no scroll-spy sections, activeIdx stays -1 and indexOf(link)
is also -1, so the pointerleave 'keep lit if active' check matched every
hovered tab. Guard with activeIdx >= 0 so a hovered tab always retracts when
nothing is active.
2026-06-29 19:54:30 +10:00
jwright dae9bbc91a nav: animate Projects/Blog underlines + coherent directional slide
build-and-deploy / build (push) Failing after 12m11s
- Projects/Blog had no data-nav-id so the scroll-spy skipped them entirely;
  point them at the homepage #projects/#writing sections so all five animate
- Drive the underline declaratively (re-assert all on each change) so none can
  be left stranded mid-spring
- Slide follows scroll direction: activating underline grows from its trailing
  edge while the rest collapse toward the leading edge (down→right, up→left)
2026-06-29 19:46:46 +10:00
jwright 7c300faec8 home: fix nav active-section at page bottom + exact progress bar; add blog tags to Writing
build-and-deploy / build (push) Failing after 10m38s
- Nav underline highlighted the wrong section at the bottom (the short last
  section never crossed the threshold) — force the last section when scrolled
  to the bottom so Contact lights up instead of Skills
- Scroll-progress bar: compute progress from scrollY/maxScroll so it spans a
  true 0→1 (Motion's default topped out ~0.89 at the bottom)
- Writing section: add blog tag chips linking to /blog/tags/<tag>/
2026-06-29 19:08:17 +10:00
jwright 257af5d22e home: add Writing section; fix scroll-progress snap + nav underline reverse
build-and-deploy / build (push) Failing after 10m20s
- Scroll-progress bar: drive scaleX from scroll progress via callback form;
  the animation form snapped the bar to 0 at the bottom (progress=1 boundary)
- Nav active-underline: recompute active section from scroll position each
  frame so it tracks upward scroll too (inView-enter died on the way back up)
- Add a Writing section to the homepage with recent posts + a blog CTA
2026-06-29 18:53:35 +10:00
jwright 880b9daeac projects: reword index tagline; split global infra into estate + network-automation
build-and-deploy / build (push) Failing after 11m25s
- New projects tagline + open-minded lead
- Split the combined infra project: keep the estate/cloud modernisation,
  add a dedicated Network Automation at Fleet Scale case (Unimus + NetBox,
  config backup, bulk push, vaulted+rotated credentials) from the Linde role
2026-06-29 18:32:29 +10:00
jwright a685789322 blog: drop duplicate inline hero on secret-zero
build-and-deploy / build (push) Failing after 10m24s
The hero image is rendered from frontmatter; the leftover inline
markdown image rendered it a second time (and uncached, so the old
asset showed).
2026-06-29 18:13:32 +10:00
jwright 61642e8db8 blog: add secret-zero bootstrap diagram; clean up hero images
build-and-deploy / build (push) Failing after 13m51s
- Add the secret-zero bootstrap-chain diagram (was a broken image ref)
- Remove the corner sparkle artifact from all six blog hero images
- Bump hero/diagram cache-bust params so the updated assets serve
2026-06-29 18:05:03 +10:00
jwright bed2dbaa1f blog: add outbound + secret-zero hero images
build-and-deploy / build (push) Failing after 11m34s
outbound-only-exposure (corrected sealed-vault image) + secret-zero (the seed-
key circuit-tree, _R revision). Both verified against their prompts, optimized
to webp (17KB / ~120KB). Completes hero coverage for all 6 posts.
2026-06-29 17:47:23 +10:00
jwright 354475a453 blog: hero images + publish secret-zero
build-and-deploy / build (push) Failing after 11m14s
Add an optional hero field to the blog schema; render it on the post page and
as the index card thumbnail. Ship 4 verified Nano Banana hero images (shipping/
init-gating/authentik/workload) as optimized webp (64-134KB). Publish the
secret-zero post. (outbound hero pending — its file was a dup of shipping;
secret-zero hero pending generation.)
2026-06-29 17:37:40 +10:00
jwright d8a147aabe ui: design polish — themed scrollbar, focus-ring glow, card depth
build-and-deploy / build (push) Failing after 11m52s
Themed thin scrollbar (cyan→magenta thumb, Firefox + WebKit), an on-brand glow
halo on the keyboard focus ring (solid outline keeps WCAG visibility), and a 1px
inner top highlight so cards read raised at rest. CSS-only, token-pure.
2026-06-28 23:32:04 +10:00
jwright f6d92e5c87 ui: Motion-driven animation layer (motion.dev)
build-and-deploy / build (push) Failing after 14m48s
Unify scroll reveals, hero entrance, nav active-underline, button springs,
the scroll-progress bar and backdrop parallax under one Motion system
(src/scripts/anim.ts), bundled into a self-hosted /_astro module so CSP
script-src 'self' holds. Progress bar + parallax now scroll()-driven and
cross-browser (was Chromium-only animation-timeline). Fail-open, reduced-motion
aware, content fully visible with JS off.
2026-06-28 23:23:58 +10:00
jwright 5d439752de ui: retheme favicon + OG card to the cyan→magenta theme
build-and-deploy / build (push) Failing after 12m13s
The tab logo and social preview were still on the old teal palette. Retone both
to the electric cyan→magenta theme matching the hero nameplate:
- favicon.svg: jw monogram + prompt-line + cursor on a cyan→magenta gradient.
- og.png (gen-og.mjs): name in the cyan→violet→magenta hero gradient, cyan
  eyebrow/separators, magenta URL, cyan→magenta top bar + dual corner glows.
- bump ?v=2 on both refs (public/ assets are CF-edge-cached + social platforms
  cache OG hard) so the new art shows immediately.
2026-06-27 23:01:41 +10:00
jwright 0c93328a2d ui: vivid electric nameplate — fix invisible hero title in light mode
build-and-deploy / build (push) Failing after 10m26s
The hero name was a mostly---text gradient with a thin accent sweep; in light
mode it rested on low-contrast tones and the body text-halo washed it to a ghost.
Replace with a full cyan→violet→magenta fill driven by the theme-aware accent
tokens (deep on light, bright on dark) so it's unmissable in either mode, plus a
tight dark edge-shadow for legibility, the electric glow, and a one-shot light
sheen that parks off-screen. Opt the title out of the light text-halo.
2026-06-27 21:28:30 +10:00
jwright ef1719c84e ui: keep the board visible in light mode + bust diagram cache
build-and-deploy / build (push) Failing after 14m43s
Light mode: revert the heavy white-out scrim back to a moderate wash so the
board stays visible, and earn legibility with a soft light text-halo behind
running text instead (invisible on the solid cards, only works over the image).
Pairs with the darker light-mode text tokens.

Also append ?v=2 to every /diagrams/*.svg reference so the recoloured diagrams
bypass Cloudflare's 4h edge cache and show immediately (the SVG URLs aren't
content-hashed like the CSS bundle).
2026-06-27 21:19:13 +10:00
jwright e264f673d7 fix(ui): readable light mode over the board backdrop
build-and-deploy / build (push) Failing after 13m46s
Body text washed out in light mode — the secondary/tertiary text tokens were
too light against the bright sandstone board image. Darken light --text-dim and
--text-faint, and heavily strengthen the light-theme scrim so the board drops
to a faint texture behind content. Dark mode unchanged.
2026-06-27 21:15:08 +10:00
jwright 8702c8a293 ui: re-theme architecture diagrams to the cyan→magenta palette
build-and-deploy / build (push) Failing after 13m39s
The diagrams still used the old teal/green/blue scheme. Remap all six to the
electric theme: teal #0d9488 → cyan #0c8fce (primary), blue #3b6fb0 → violet
#7c3aed (containers/boundaries), green #2e8b57 → magenta #c026d3 (services).
Neutral slate + navy kept for lines and text; light card unchanged so the
fixed-colour art stays legible in both themes.
2026-06-27 21:10:17 +10:00
jwright b7c91081b2 fix(ui): scroll-coupled backdrop pan that works in every browser
build-and-deploy / build (push) Failing after 11m10s
CSS animation-timeline: scroll() is Chromium-only, so the backdrop didn't move
for non-Chromium browsers. Replace it with a tiny rAF-throttled scroll handler
in site.js (the existing same-origin script — CSP stays strict, no inline JS)
that drives a --bg-shift CSS var off scroll position. The board now pans
vertically in lock-step with scroll and sits still at rest, in all browsers.
2026-06-27 21:02:46 +10:00
jwright af7e0287d4 fix(ui): backdrop now moves in every browser
build-and-deploy / build (push) Failing after 14m47s
The backdrop's only motion was the scroll-driven pan (animation-timeline:
scroll()), which is Chromium-only — so in Firefox/Safari the board sat dead
still. Add a continuous timed top↔bottom drift (48s, alternates) as the
universal baseline; keep the scroll-coupled pan as a Chromium enhancement that
overrides it. Motion-safe: both gated behind prefers-reduced-motion.
2026-06-27 20:54:08 +10:00
jwright 2aae7c287e ui: backdrop pan — monotonic top→bottom drift
build-and-deploy / build (push) Failing after 11m19s
The S→E→SE→NE loop wandered back near its start, so on a long page the
per-screen movement was imperceptible (worse once the image was zoomed to
215%). Replace it with a single continuous vertical pan (~40% travel) so the
board obviously drifts downward as the page scrolls.
2026-06-27 20:47:36 +10:00
148 changed files with 6031 additions and 3287 deletions
+15
View File
@@ -41,3 +41,18 @@ secrets.*
# CI artifact
sbom.json
# Blog drafts are PRIVATE staging - this repo is public and anonymously readable.
# Nothing under drafts/ may ever be committed; a draft becomes public only by
# being moved into src/content/blog/ at publish time.
drafts/
# Publish-workflow archives stay local for the same reason.
published/
# Local blog-workshop tooling stays off the public repo.
scripts/make-ledger.py
scripts/sync-nas-blog.sh
# python bytecode from the local ledger script
scripts/__pycache__/
+4 -1
View File
@@ -13,7 +13,10 @@ WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Build the static site.
# Build the static site. The git sha arrives as a build-arg (.dockerignore excludes .git,
# so an in-container `git rev-parse` can never resolve — the footer receipt read "HEAD unknown").
ARG GIT_SHA
ENV GIT_SHA=${GIT_SHA}
COPY . .
RUN npm run build
+45 -54
View File
@@ -1,18 +1,22 @@
# bztmon-site
The source for **[www.bztmon.com](https://www.bztmon.com)** — Jonathon Wright's
portfolio / résumé site. A fast, animated, security-hardened static site for a
platform / infrastructure engineer.
Source for [www.bztmon.com](https://www.bztmon.com) - portfolio and blog for a
platform / infrastructure engineer. Static output, no server runtime, hardened
by default.
> This repo is **public**. It lives on a self-hosted public Gitea (`git.bztmon.com`),
> isolated from the private homelab GitOps. **Never commit secrets** — the static
> site needs none.
> This repo is public and lives on a self-hosted Gitea (`git.bztmon.com`),
> isolated from the private GitOps repos. The static site needs no secrets;
> none are committed.
## Stack
- **Astro** (static output) + **TypeScript** + **Tailwind v4**
- Zero JS by default; tiny islands for the theme toggle + scroll reveals
- Content & config are data-driven (`src/data/`) — adding a project never touches a component
- Astro (static output), TypeScript, Tailwind v4
- Static output with one self-hosted client bundle (~74 KB: Motion for scroll reveals, plus a
small pre-paint theme script). No inline script anywhere, so `script-src 'self'` holds.
- Content is visible with JavaScript disabled: reveal styles are gated behind an `html.js`
class the script adds, and `prefers-reduced-motion` forces the visible state
- Content and config are data-driven (`src/data/`, `src/content/`) - adding a
post or project never touches a component
## Develop
@@ -20,72 +24,59 @@ platform / infrastructure engineer.
npm install
npm run dev # http://localhost:4321
npm run check # astro check (types + diagnostics)
npm run build # static build dist/
npm run build # static build -> dist/
npm run preview # serve the build locally
npm run gen:og # regenerate the social-preview image (public/og.png)
```
## Project layout
## Layout
```
src/
data/ site.ts, socials.ts, skills.ts, projects.ts, experience.ts
components/ Hero, Nav, ThemeToggle, ProjectCard, SkillGroup, ...
content/blog/ posts (Markdown, zod-validated frontmatter)
content/projects/ project dossiers
data/ site.ts, series.ts, skills.ts, projects.ts, experience.ts
components/ Hero, Nav, PostList, ProjectCard, ...
layouts/ Layout.astro (SEO/OG, theme bootstrap)
pages/ index.astro, projects/, 404.astro
pages/ index, blog/, projects/, 404
styles/ tokens.css (theme), global.css
lib/ build-time helpers (cv detection)
scripts/ gen-og.mjs, build-image.sh
scripts/ gen-og.mjs, build-image.sh, new-post.mjs
public/ heroes (blog/), diagrams (diagrams/), og.png, cv
nginx/ default.conf (security headers, caching) baked into the image
Dockerfile Debian build stage nginx-unprivileged runtime
Dockerfile Debian build stage -> nginx-unprivileged runtime
```
## Content TODOs (Jonathon)
## Posts
- Drop a real CV at `public/cv.pdf` — the **Download CV** button appears automatically.
- Fill the `TODO(Jonathon)` markers in `src/data/experience.ts`, `projects.ts`, `socials.ts`
(employer names, dates, GitHub/LinkedIn handles).
## Publishing a post
A post is just a Markdown file in `src/content/blog/`. Write one by hand, or generate
a schema-valid one with the publish helper (this is the seam an IaC/CI step calls):
```bash
# from flags
node scripts/new-post.mjs --title "My post" --summary "One line" \
--tags "kubernetes,gpu" [--draft] [--bodyFile notes.md]
# from a JSON event (e.g. an Ansible/AWX deploy summary)
echo '{"title":"...","summary":"...","tags":["x"],"body":"## Hi\n..."}' \
| node scripts/new-post.mjs --stdin
```
Commit the file to `main` → CI rebuilds and ships. A malformed post **fails the build**
(frontmatter is zod-validated), so a bad pipeline event never reaches production.
A post is a Markdown file in `src/content/blog/` with zod-validated
frontmatter; a malformed post fails the build, so a bad file never reaches
production. `scripts/new-post.mjs` scaffolds one from flags or a JSON event on
stdin - the seam a CI step can call. Each post belongs to one numbered series
(`src/data/series.ts`), which drives the badge on the index cards.
## CI/CD
`.gitea/workflows/deploy.yml` runs on a self-hosted runner (a dedicated unprivileged
user on the bastion):
`.gitea/workflows/deploy.yml` runs on a self-hosted runner (a dedicated
unprivileged user on a utility host):
```
npm ci astro check audit-ci (high/critical gate) build scan dist
SBOM (CycloneDX) buildah build+push open a digest-bump PR to home-ops
npm ci -> astro check -> audit-ci (high/critical gate) -> build -> scan dist ->
SBOM (CycloneDX) -> buildah build+push -> open a digest-bump PR to home-ops
```
The PR is **never auto-merged**`home-ops` `main` is branch-protected; merging it is
what triggers the ArgoCD rollout. The runner holds only least-privilege creds (a
`home-ops`-scoped deploy key + a PR token + a registry push token).
The PR is never auto-merged - the deploy repo's `main` is branch-protected, and
merging the PR is what triggers the Argo CD rollout. The runner holds
least-privilege credentials only: a scoped deploy key, a PR token, and a
registry push token.
- `npm run scan` build-time gate: no secrets, no inline scripts, no third-party origins.
- `.audit-ci.json` — fails on high/critical advisories. One allowlisted: `GHSA-gv7w-rqvm-qjhr`
(esbuild install-integrity; build-time only, mitigated by the committed lockfile + trusted registry).
- `renovate.json` keeps npm deps and the digest-pinned base images current.
- `npm run scan` - build-time gate: no secrets, no inline scripts, no
third-party origins in `dist/`
- `.audit-ci.json` - fails the build on high/critical advisories
- `renovate.json` - keeps npm deps and the digest-pinned base images current
## Deploy
Built into a container image, served by nginx-unprivileged on a homelab Kubernetes
cluster, exposed via Cloudflare Tunnel. The image is pinned by digest in the private
`home-ops` repo and rolled out by ArgoCD. Manual/bootstrap build: `scripts/build-image.sh push`.
See `SECURITY.md` for the full security posture.
Built into a container image, served by nginx-unprivileged on Kubernetes,
exposed outbound-only via Cloudflare Tunnel. The image is pinned by digest in
the private deploy repo and rolled out by Argo CD. Manual bootstrap build:
`scripts/build-image.sh push`. Full posture in `SECURITY.md`.
+9 -9
View File
@@ -5,17 +5,17 @@ as acceptance criteria, not polish.
## Attack surface
- **Static output** no server runtime, no database, no user input, no forms.
- **Static output** - no server runtime, no database, no user input, no forms.
- Served by `nginx-unprivileged` (uid 101, read-only root filesystem, all Linux
capabilities dropped, no service-account token) on Kubernetes.
- Exposed **outbound-only via a Cloudflare Tunnel** no open inbound ports, a single
- Exposed **outbound-only via a Cloudflare Tunnel** - no open inbound ports, a single
public hostname, no catch-all.
## Headers
Split by where they belong:
**Origin (nginx, ships in the image `nginx/security-headers.conf`)**
**Origin (nginx, ships in the image - `nginx/security-headers.conf`)**
| Header | Value |
|---|---|
@@ -23,11 +23,11 @@ Split by where they belong:
| X-Content-Type-Options | `nosniff` |
| X-Frame-Options | `DENY` |
| Referrer-Policy | `strict-origin-when-cross-origin` |
| Permissions-Policy | camera/mic/geo/payment/usb all denied |
| Permissions-Policy | camera/mic/geo/payment/usb... all denied |
| Cross-Origin-Opener-Policy | `same-origin` |
| Cross-Origin-Resource-Policy | `same-origin` |
**Edge (Cloudflare dashboard)** HSTS, HTTPHTTPS redirect, SSL Full, Bot Fight,
**Edge (Cloudflare dashboard)** - HSTS, HTTP->HTTPS redirect, SSL Full, Bot Fight,
rate-limiting. HSTS ships **without** `includeSubDomains`/`preload` initially because
`*.bztmon.com` resolves to the WAN and the subdomain form would brick non-public hosts.
@@ -36,7 +36,7 @@ rate-limiting. HSTS ships **without** `includeSubDomains`/`preload` initially be
- **`script-src 'self'` with zero inline scripts.** All JS (pre-paint theme, toggle,
scroll reveal) lives in one external `/site.js`. No `unsafe-inline`, no `unsafe-eval`,
no hashes to maintain.
- **`style-src` allows `'unsafe-inline'`** the one conscious exception. Shiki's
- **`style-src` allows `'unsafe-inline'`** - the one conscious exception. Shiki's
dual-theme syntax highlighting emits per-token CSS custom-properties as inline `style`
attributes; hashing them is impractical (they vary per page). The security-critical
directive (`script-src`) stays strict. Everything else is self-hosted: fonts are system
@@ -55,9 +55,9 @@ Runs in CI before the image is built.
## Targets (verified post-deploy)
- securityheaders.com A+
- Mozilla Observatory A/A+ (small deduction expected for `style-src 'unsafe-inline'`)
- Lighthouse 95 across Performance / Accessibility / Best Practices / SEO
- securityheaders.com -> A+
- Mozilla Observatory -> A/A+ (small deduction expected for `style-src 'unsafe-inline'`)
- Lighthouse >= 95 across Performance / Accessibility / Best Practices / SEO
- CSP: zero console violations in a real browser (incl. code blocks + diagrams)
## Reporting
+4 -2
View File
@@ -8,7 +8,7 @@ import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
site: "https://www.bztmon.com",
// Always emit/expect trailing slashes so internal links hit the final URL directly
// (no nginx dir-redirect no stray :8080 / cached 301s).
// (no nginx dir-redirect -> no stray :8080 / cached 301s).
trailingSlash: "always",
integrations: [sitemap()],
build: {
@@ -16,8 +16,10 @@ export default defineConfig({
inlineStylesheets: "never",
},
markdown: {
// Keep rendered prose ASCII: no curly-quote/dash typesetting at build.
smartypants: false,
shikiConfig: {
// Dual theme via CSS variables colours switch with our data-theme, no
// Dual theme via CSS variables -> colours switch with our data-theme, no
// hard-coded per-token colours baked to one theme.
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
+599 -2560
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -19,6 +19,7 @@
"@astrojs/sitemap": "^3.7.3",
"@tailwindcss/vite": "^4.3.1",
"astro": "^7.0.3",
"motion": "^12.42.0",
"tailwindcss": "^4.3.1"
},
"devDependencies": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+101
View File
@@ -0,0 +1,101 @@
# Project cover prompts - bztmon.com
Two covers to generate: **The Exploded Cluster** and **The Mirror (pull-through registry)**.
Paste the block below the `---` for each one into Gemini / Nano Banana 2, generate ~4
variations, curate against the reject list, then process per "After generating".
Aspect: **3:2 landscape** (existing covers are 1600 x 1074).
---
## House style block (already inside each prompt - do not paste separately)
The site's covers are dark isometric hero renders: matte dark hardware, cyan and magenta
edge light, a subtly circuit-etched ground plane, thin atmospheric haze, and generous empty space on one
side so the card's title text has room.
Unlike the learn-site scene plates, covers MAY have a ground plane and atmosphere - they are
never sliced or animated.
## Reject checklist (per candidate, before you keep it)
- [ ] any text, letters, numbers, logos or UI chrome anywhere in the frame (reject hard)
- [ ] (!) THE GENERATOR WATERMARK - a small pale four-point star, usually bottom-right at
roughly 88% width / 82% height. It is NOT house style; earlier covers shipped with it
by mistake and had to be scrubbed. Check that corner on every candidate, and never ask
a prompt for a "sparkle highlight" (that instruction produced a second, drawn-in star
that sits on the artwork and cannot be cleanly removed)
- [ ] subject clipped by the frame edge, or centred so tightly there is no negative space
- [ ] colours drifting warm - the palette is cyan/magenta on near-black, no orange or gold
- [ ] photographic realism or stock-render gloss (it should read as an illustration)
- [ ] busy background competing with the subject
- [ ] the two covers looking like the same object (they must be distinguishable as a pair)
## After generating
1. Keep the best of each, name them `exploded-cluster.png` / `mirror-registry.png`.
2. Convert and place (from the repo root, on the box with the site checkout):
```
# full-size cover: 1600px wide
ffmpeg -y -i exploded-cluster.png -vf "scale=1600:-2" -q:v 80 public/covers/exploded-cluster.webp
ffmpeg -y -i mirror-registry.png -vf "scale=1600:-2" -q:v 80 public/covers/mirror-registry.webp
# card thumbnail: 640px wide
ffmpeg -y -i exploded-cluster.png -vf "scale=640:-2" -q:v 80 public/covers/thumb/exploded-cluster.webp
ffmpeg -y -i mirror-registry.png -vf "scale=640:-2" -q:v 80 public/covers/thumb/mirror-registry.webp
```
3. Both project files already reference these names (`cover: "exploded-cluster"`,
`cover: "mirror-registry"`), so the cards pick them up on the next build.
---
# PROMPT 1 - The Exploded Cluster
Dark isometric technical illustration in a cinematic sci-fi engineering style, 3:2 landscape.
Near-black background, hex #070b14, with a faint dark circuit-etched ground plane beneath the
subject and a thin drift of atmospheric haze in the upper right. Matte dark metal and smoked
glass surfaces with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta
#e879f9, cool white rim highlights. Even studio lighting, no lens flare, no depth-of-field
blur. ABSOLUTELY NO text, letters, numbers, logos, labels, arrows or UI elements anywhere in
the image - surface markings must be abstract geometric patterns only.
The subject: a single machine caught mid-explosion, its layers separated vertically like an
engineering teardown diagram, floating apart with clear gaps of void between them. From the
bottom up: a heavy dark chassis base with cyan-lit vents; above it a thick slab housing rows
of small identical cells; above that two thin translucent glass sheets etched with fine
circuit tracery, glowing faintly cyan; and at the top, held well clear of everything else, a
small bright magenta-lit cube - the smallest and most luminous part, clearly the payload the
whole machine exists to carry.
The parts hover in stable formation rather than flying apart violently - a considered
teardown, paused for inspection. Composition sits left of centre, leaving open dark space on
the right third of the frame.
---
# PROMPT 2 - The Mirror (pull-through registry)
Dark isometric technical illustration in a cinematic sci-fi engineering style, 3:2 landscape.
Near-black background, hex #070b14, with a faint dark circuit-etched ground plane beneath the
subject and a thin drift of atmospheric haze in the upper left. Matte dark metal and smoked
glass surfaces with glowing neon edge lighting: primary cyan #3fbaf5, secondary magenta
#e879f9, cool white rim highlights. Even studio lighting, no lens flare, no depth-of-field
blur. ABSOLUTELY NO text, letters, numbers, logos, labels, arrows or UI elements anywhere in
the image - surface markings must be abstract geometric patterns only.
The subject: a wide, low, heavily-armoured way-station machine sitting on the ground plane,
right of centre - a bunker-grade cache with a glass-fronted shelf running along its face,
holding a row of small identical luminous cyan bricks in cold storage. Its face has a single
broad output aperture ringed in cool white light, aimed left.
Above and behind it, arranged in a loose arc receding into the haze, five small dark intake
pods at clearly diminishing scale - each a compact rounded capsule with one cyan-lit aperture,
each floating alone with generous void between them, none touching the main machine. They
read as five distant sources feeding one local shelf.
Below and left of the way-station, close to the ground plane, a single luminous cyan brick
hovers in mid-transfer - small, bright, clearly in motion toward the viewer's side of the
frame, the one thing the whole arrangement is delivering.
Composition sits right of centre, leaving open dark space on the left third for title text.
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Executable
BIN
View File
Binary file not shown.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

+63
View File
@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="440" viewBox="0 0 1180 440" role="img" aria-label="One bastion control point holds the fleet's keys — one kubeconfig, one talosconfig, one age key, every ops repo — and everything is driven through it: laptops and desktops connect only via the bastion, which reaches out to the five clusters and the git server. The keys never leave the station.">
<rect width="1180" height="440" fill="#ffffff"></rect>
<defs>
<marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#0c8fce"></path>
</marker>
<marker id="ag" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#94a3b8"></path>
</marker>
</defs>
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">One place the keys live &#8212; everything transits it</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the bastion is a property, not a box: the set of places your keys exist has exactly one member</text>
<!-- operator devices (left) -->
<rect x="48" y="150" width="176" height="60" rx="12" fill="#ffffff" stroke="#94a3b8" stroke-width="1.4"></rect>
<text x="136" y="176" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#64748b">laptop</text>
<text x="136" y="196" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#94a3b8">holds no keys</text>
<rect x="48" y="230" width="176" height="60" rx="12" fill="#ffffff" stroke="#94a3b8" stroke-width="1.4"></rect>
<text x="136" y="256" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#64748b">desktop</text>
<text x="136" y="276" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#94a3b8">holds no keys</text>
<!-- the bastion (centre, the station) -->
<rect x="366" y="150" width="240" height="170" rx="16" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="2.4"></rect>
<text x="486" y="184" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="16" font-weight="700" fill="#7c3aed">bastion</text>
<text x="486" y="205" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">the pilot station &#183; 4-core VM</text>
<text x="486" y="232" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">1 kubeconfig &#183; 1 talosconfig</text>
<text x="486" y="252" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">1 age key &#183; every ops repo</text>
<text x="486" y="272" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">toolchain pinned to the fleet</text>
<text x="486" y="300" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" font-weight="700" fill="#7c3aed">keys never leave here</text>
<!-- transit arrows in -->
<path d="M224 180 C300 180 320 210 362 216" fill="none" stroke="#94a3b8" stroke-width="2" marker-end="url(#ag)"></path>
<path d="M224 260 C300 260 320 250 362 244" fill="none" stroke="#94a3b8" stroke-width="2" marker-end="url(#ag)"></path>
<text x="300" y="150" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">connect THROUGH</text>
<!-- the fleet (right, the ships) -->
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<rect x="760" y="120" width="150" height="40" rx="10" fill="#ffffff" stroke="#0c8fce" stroke-width="1.5"></rect>
<text x="835" y="145" text-anchor="middle">cave</text>
<rect x="940" y="120" width="150" height="40" rx="10" fill="#ffffff" stroke="#0c8fce" stroke-width="1.5"></rect>
<text x="1015" y="145" text-anchor="middle">alfred</text>
<rect x="760" y="176" width="150" height="40" rx="10" fill="#ffffff" stroke="#0c8fce" stroke-width="1.5"></rect>
<text x="835" y="201" text-anchor="middle">robin</text>
<rect x="940" y="176" width="150" height="40" rx="10" fill="#ffffff" stroke="#0c8fce" stroke-width="1.5"></rect>
<text x="1015" y="201" text-anchor="middle">wgirl</text>
<rect x="760" y="232" width="150" height="40" rx="10" fill="#ffffff" stroke="#0c8fce" stroke-width="1.5"></rect>
<text x="835" y="257" text-anchor="middle">elfastc</text>
<rect x="940" y="232" width="150" height="40" rx="10" fill="#ffffff" stroke="#7c3aed" stroke-width="1.5"></rect>
<text x="1015" y="257" text-anchor="middle" fill="#7c3aed">git server</text>
</g>
<!-- beams out to the fleet -->
<g fill="none" stroke="#0c8fce" stroke-width="2">
<path d="M606 200 C680 180 700 150 756 140" marker-end="url(#a)"></path>
<path d="M606 214 C700 210 720 196 936 196" marker-end="url(#a)"></path>
<path d="M606 232 C680 250 700 252 756 252" marker-end="url(#a)"></path>
</g>
<path d="M606 244 C740 300 860 270 936 252" fill="none" stroke="#7c3aed" stroke-width="2" marker-end="url(#a)"></path>
<text x="690" y="330" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">driven from one hostname &#183; auditing &#8220;where from?&#8221; takes a minute</text>
<text x="60" y="404" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" fill="#64748b">ships come and go &#8212; the charts never leave the station</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+60
View File
@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="430" viewBox="0 0 1180 430" role="img" aria-label="The broker-pattern flow: an untrusted agent asks a credential broker, which mints a scoped short-lived capability without exposing any credential; the agent hands that capability to a tool broker, which verifies it and performs the action using a credential it holds; any write is staged behind a human approval gate on a phone before it executes; the credential never leaves the trusted brokers.">
<rect width="1180" height="430" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">The broker pattern: ask, never hold</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the agent can only ask &#8212; every credential stays behind the brokers, every write waits on a human</text>
<!-- trust boundary: untrusted (left) vs trusted (right) -->
<rect x="36" y="196" width="230" height="164" rx="16" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="54" y="218" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#c026d3">untrusted executor</text>
<rect x="298" y="196" width="846" height="164" rx="16" fill="#0c8fce" fill-opacity="0.04" stroke="#0c8fce" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="316" y="218" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#0c8fce">trusted tier &#8212; holds every credential</text>
<!-- agent box -->
<rect x="56" y="236" width="188" height="100" rx="14" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="150" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15.5" font-weight="700" fill="#c026d3">agent</text>
<text x="150" y="298" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">sandboxed &#183; assume compromised</text>
<text x="150" y="319" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">holds a capability, not a key</text>
<!-- credential broker -->
<rect x="320" y="236" width="196" height="100" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="418" y="266" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">credential broker</text>
<text x="418" y="290" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">verifies the sandbox identity</text>
<text x="418" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">mints a scoped, one-time,</text>
<text x="418" y="324" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">expiring capability</text>
<!-- tool broker -->
<rect x="598" y="236" width="196" height="100" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="696" y="266" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">tool broker</text>
<text x="696" y="290" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">checks signature &#183; scope &#183; nonce</text>
<text x="696" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">runs the tool with a credential</text>
<text x="696" y="324" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">it holds &#8212; never the agent</text>
<!-- human approval gate -->
<rect x="876" y="236" width="196" height="100" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="974" y="266" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0e1726">human approval</text>
<text x="974" y="290" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">every write is staged, not run</text>
<text x="974" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">one-time token &#183; fail-closed</text>
<text x="974" y="324" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">timeout = denied</text>
<!-- arrows -->
<path d="M244 286 H314" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M304 280 L314 286 M304 292 L314 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M516 286 H592" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M582 280 L592 286 M582 292 L592 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M794 286 H870" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M860 280 L870 286 M860 292 L870 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<!-- arrow labels -->
<rect x="253" y="255" width="52" height="19" rx="9.5" fill="#ffffff"></rect>
<text x="279" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">asks</text>
<rect x="509" y="255" width="90" height="19" rx="9.5" fill="#ffffff"></rect>
<text x="554" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">capability</text>
<rect x="786" y="255" width="78" height="19" rx="9.5" fill="#ffffff"></rect>
<text x="825" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">stages write</text>
<!-- footer takeaway -->
<text x="60" y="398" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#64748b">The agent can only ask. Every credential, every write-authority, every scope decision lives at the broker &#8212; a compromised agent's worst case is a denied request.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1,54 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="450" viewBox="0 0 1180 450" role="img" aria-label="cert-manager writes the ACME challenge TXT record to Cloudflare, where it goes live on the public internet and Let's Encrypt validates it successfully. But its own self-check resolves through the cluster's split-horizon DNS — the internal Pi-hole that owns the zone and has no such record — so it loops forever on not-yet-propagated. The fix points the self-check at public resolvers, the same vantage the CA uses.">
<rect width="1180" height="450" fill="#ffffff"></rect>
<defs>
<marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#0c8fce"></path></marker>
<marker id="r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#c026d3"></path></marker>
<marker id="v" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#7c3aed"></path></marker>
</defs>
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">The proof is out there &#8212; the self-check looks inside</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">a check is only worth its vantage point: verify from where the judge stands, not from where you live</text>
<!-- cert-manager -->
<rect x="52" y="176" width="196" height="110" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="150" y="212" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0e1726">cert-manager</text>
<text x="150" y="238" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">writes the TXT, then</text>
<text x="150" y="256" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">self-checks before</text>
<text x="150" y="274" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">calling the CA</text>
<!-- write to Cloudflare (works) -->
<path d="M248 200 H432" fill="none" stroke="#0c8fce" stroke-width="2.4" marker-end="url(#g)"></path>
<text x="340" y="190" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0c8fce">write _acme-challenge TXT</text>
<rect x="440" y="150" width="220" height="96" rx="14" fill="#0c8fce" fill-opacity="0.05" stroke="#0c8fce" stroke-width="2"></rect>
<text x="550" y="182" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0c8fce">Cloudflare (public)</text>
<text x="550" y="206" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">record is LIVE</text>
<text x="550" y="226" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">dig @1.1.1.1 returns the token</text>
<!-- Let's Encrypt validates from public -->
<rect x="760" y="150" width="200" height="96" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="860" y="182" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#7c3aed">Let&#8217;s Encrypt</text>
<text x="860" y="206" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">validates via public DNS</text>
<text x="860" y="226" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">would pass right now</text>
<path d="M660 198 H752" fill="none" stroke="#7c3aed" stroke-width="2" marker-end="url(#v)"></path>
<!-- the broken self-check: through the Pi-hole (magenta) -->
<rect x="440" y="300" width="220" height="100" rx="14" fill="#c026d3" fill-opacity="0.05" stroke="#c026d3" stroke-width="2"></rect>
<text x="550" y="332" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#c026d3">internal Pi-hole</text>
<text x="550" y="356" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">owns bztmon.org internally</text>
<text x="550" y="376" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">no such TXT &#8594; returns nothing</text>
<path d="M150 286 C150 350 300 360 432 356" fill="none" stroke="#c026d3" stroke-width="2.4" marker-end="url(#r)"></path>
<text x="290" y="380" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#c026d3">self-check (cluster DNS)</text>
<path d="M470 360 C428 348 424 322 448 310" fill="none" stroke="#c026d3" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#r)"></path>
<text x="550" y="416" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#c026d3">loops forever: &#8220;not yet propagated&#8221;</text>
<!-- the fix -->
<rect x="760" y="300" width="360" height="100" rx="14" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="2"></rect>
<text x="784" y="330" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" font-weight="700" fill="#7c3aed">the fix &#8212; move the observer, not the record</text>
<text x="784" y="356" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">dns01RecursiveNameservers: 1.1.1.1,8.8.8.8</text>
<text x="784" y="376" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">dns01RecursiveNameserversOnly: true</text>
<path d="M660 350 C700 350 720 350 752 350" fill="none" stroke="#7c3aed" stroke-width="2" stroke-dasharray="5 5" marker-end="url(#v)"></path>
<text x="60" y="430" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" fill="#64748b">check the postbox the postman collects from &#8212; not the mail slot in your own hallway</text>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

+72
View File
@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="470" viewBox="0 0 1180 470" role="img" aria-label="A browser and a phone reach a code-server pod only through an Authentik proxy that redirects every path to login. The pod holds cloned repos, its own git key and its own logins on its own volume, and explicitly no age key. Its egress is limited to DNS, gitea SSH and 443. The bastion holding the age key, admin kubeconfigs and SSH keys sits outside the frame, unreachable from the workbench.">
<rect width="1180" height="470" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">A clone of the brain, never the keys</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">one guarded door to a room of photocopies &#8212; the originals never enter the frame</text>
<!-- clients -->
<rect x="60" y="150" width="150" height="60" rx="12" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="135" y="177" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" font-weight="700" fill="#0e1726">browser</text>
<text x="135" y="196" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">couch &#183; LAN</text>
<rect x="60" y="230" width="150" height="60" rx="12" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="135" y="257" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" font-weight="700" fill="#0e1726">phone</text>
<text x="135" y="276" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">mesh VPN</text>
<!-- arrows to proxy -->
<g stroke="#0c8fce" stroke-width="2" fill="none">
<line x1="210" y1="180" x2="285" y2="208"></line>
<line x1="210" y1="260" x2="285" y2="232"></line>
<polygon points="283,201 296,214 279,216" fill="#0c8fce" stroke="none"></polygon>
</g>
<!-- authentik proxy -->
<rect x="296" y="180" width="210" height="80" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="2.2"></rect>
<text x="401" y="210" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0c8fce">Authentik proxy</text>
<text x="401" y="232" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">every path 302s to login</text>
<text x="401" y="249" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">no carve-outs &#183; WebSockets too</text>
<!-- arrow to pod -->
<g stroke="#0c8fce" stroke-width="2" fill="none">
<line x1="506" y1="220" x2="566" y2="220"></line>
<polygon points="566,214 578,220 566,226" fill="#0c8fce" stroke="none"></polygon>
</g>
<!-- code-server pod -->
<rect x="578" y="130" width="300" height="196" rx="16" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="2.2"></rect>
<text x="728" y="160" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#7c3aed">code-server pod</text>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="604" y="190">own 20Gi volume:</text>
<text x="604" y="212">&#9679; cloned repos (photocopies)</text>
<text x="604" y="234">&#9679; own git key &#8212; revocable alone</text>
<text x="604" y="256">&#9679; own logins, warm</text>
</g>
<text x="604" y="288" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" font-weight="700" fill="#c026d3">&#10007; NO age key &#8212; blobs stay ciphertext</text>
<text x="604" y="310" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="10.5" fill="#64748b">default-deny floor beneath it all</text>
<!-- egress -->
<g stroke="#7c3aed" stroke-width="1.8" fill="none">
<line x1="878" y1="228" x2="938" y2="228"></line>
<polygon points="938,222 950,228 938,234" fill="#7c3aed" stroke="none"></polygon>
</g>
<rect x="950" y="180" width="170" height="96" rx="12" fill="#ffffff" stroke="#7c3aed" stroke-width="1.6"></rect>
<text x="1035" y="206" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#0e1726">egress, complete list</text>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">
<text x="1035" y="228" text-anchor="middle">DNS</text>
<text x="1035" y="246" text-anchor="middle">gitea SSH</text>
<text x="1035" y="264" text-anchor="middle">443 (APIs)</text>
</g>
<!-- the bastion, outside the frame -->
<rect x="296" y="360" width="582" height="70" rx="14" fill="#0e1726" fill-opacity="0.03" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="8 7"></rect>
<text x="587" y="388" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" font-weight="700" fill="#c026d3">the bastion &#8212; outside the frame, unreachable from the workbench</text>
<text x="587" y="412" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">age key &#183; admin kubeconfigs &#183; SSH keys &#8212; the originals, in the vault</text>
<!-- no-path marker between pod and bastion -->
<line x1="728" y1="326" x2="728" y2="360" stroke="#c026d3" stroke-width="2" stroke-dasharray="3 5"></line>
<text x="748" y="348" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#c026d3">no route exists</text>
<!-- footer -->
<text x="60" y="452" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" fill="#64748b">worst case, fully stolen session: a loud, attributed git push &#8212; revertible &#8212; never silent possession of the estate</text>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

+68
View File
@@ -0,0 +1,68 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="500" viewBox="0 0 1180 500" role="img" aria-label="Before and after the Cilium landing on one cluster: five separately versioned components - flannel, kube-proxy, MetalLB, Traefik, and a set of authored but unenforced network policies - collapse into a single Cilium engine shipped inside the Talos machine configuration. The transition is drawn as deletions, not installs.">
<rect width="1180" height="500" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Landed by deletion: five rooflines become one</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the same cluster before and after &#8212; the arrows are strike-throughs, not installs</text>
<!-- BEFORE column -->
<text x="70" y="112" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#64748b">BEFORE &#8212; five versions to hold</text>
<!-- flannel -->
<rect x="60" y="128" width="330" height="52" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="80" y="150" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">flannel</text>
<text x="80" y="169" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">CNI &#183; pod network &#183; its own version</text>
<line x1="52" y1="154" x2="398" y2="154" stroke="#c026d3" stroke-width="2.4" opacity="0.55"></line>
<!-- kube-proxy -->
<rect x="60" y="192" width="330" height="52" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="80" y="214" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">kube-proxy</text>
<text x="80" y="233" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">service path &#183; rewrites every service address</text>
<line x1="52" y1="218" x2="398" y2="218" stroke="#c026d3" stroke-width="2.4" opacity="0.55"></line>
<!-- MetalLB -->
<rect x="60" y="256" width="330" height="52" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="80" y="278" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">MetalLB</text>
<text x="80" y="297" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">VIP announcement &#183; answers ARP on the LAN</text>
<line x1="52" y1="282" x2="398" y2="282" stroke="#c026d3" stroke-width="2.4" opacity="0.55"></line>
<!-- Traefik / ingress-nginx -->
<rect x="60" y="320" width="330" height="52" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="80" y="342" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">Traefik <tspan font-size="11" fill="#64748b">(atop ingress-nginx's old IP)</tspan></text>
<text x="80" y="361" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">HTTP termination &#183; the fossil hop</text>
<line x1="52" y1="346" x2="398" y2="346" stroke="#c026d3" stroke-width="2.4" opacity="0.55"></line>
<!-- inert policies -->
<rect x="60" y="384" width="330" height="52" rx="12" fill="#ffffff" stroke="#64748b" stroke-width="1.6" stroke-dasharray="6 5"></rect>
<text x="80" y="406" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#64748b">NetworkPolicies</text>
<text x="80" y="425" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">authored, committed &#8212; never enforced</text>
<!-- transition arrows -->
<g stroke="#64748b" stroke-width="2" fill="none">
<line x1="420" y1="282" x2="560" y2="282"></line>
<polygon points="560,276 572,282 560,288" fill="#64748b" stroke="none"></polygon>
</g>
<text x="496" y="268" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#64748b">delete, delete,</text>
<text x="496" y="302" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#64748b">delete, delete</text>
<!-- AFTER: Talos machine document holding one Cilium engine -->
<text x="600" y="112" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0c8fce">AFTER &#8212; one engine, in the machine document</text>
<rect x="590" y="128" width="530" height="308" rx="16" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="608" y="152" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#7c3aed">Talos machine configuration &#8212; inline manifest, no Helm release</text>
<rect x="622" y="170" width="466" height="242" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="2"></rect>
<text x="855" y="200" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="17" font-weight="700" fill="#0c8fce">Cilium 1.19</text>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#0e1726">
<text x="655" y="232">&#9679; CNI &#8212; pod network</text>
<text x="655" y="258">&#9679; eBPF datapath &#8212; kube-proxy's old job</text>
<text x="655" y="284">&#9679; LB-IPAM + L2 &#8212; MetalLB's old VIPs, kept</text>
<text x="655" y="310">&#9679; Gateway API &#8212; ingress, where absorbed</text>
<text x="655" y="336">&#9679; NetworkPolicy &#8212; now actually enforced</text>
<text x="655" y="362">&#9679; Hubble &#8212; the flows, visible at last</text>
</g>
<text x="655" y="394" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">one version to pin &#183; one changelog &#183; one way to be paged</text>
<!-- footer -->
<text x="60" y="472" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" fill="#64748b">fleet result: kube-proxy deleted &#183; MetalLB extinct &#183; policies enforced &#183; zero Helm release secrets &#8212; same VIPs throughout</text>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

+77
View File
@@ -0,0 +1,77 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="480" viewBox="0 0 1180 480" role="img" aria-label="NetBox holds the device inventory as source of truth; Unimus reads it and drives scheduled config backup, diff-tracked change and bulk push across the AU, NZ and PNG switch fleet. Device credentials come from a central vault with rotation, never from scripts. Every backed-up config is a restore point, giving the network a real DR path.">
<rect width="1180" height="480" fill="#ffffff"></rect>
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">One source of truth, one pane, three regions</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the automation acts on an accurate model of the fleet &#8212; not on tribal knowledge</text>
<!-- SOURCE OF TRUTH -->
<rect x="60" y="150" width="228" height="150" rx="14" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="2.2"></rect>
<text x="174" y="182" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="16" font-weight="700" fill="#7c3aed">NetBox</text>
<text x="174" y="204" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">IPAM / source of truth</text>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="84" y="234">device inventory</text>
<text x="84" y="256">site + role model</text>
<text x="84" y="278">addressing</text>
</g>
<g stroke="#7c3aed" stroke-width="2.2" fill="none">
<line x1="288" y1="225" x2="356" y2="225"></line>
<polygon points="356,219 368,225 356,231" fill="#7c3aed" stroke="none"></polygon>
</g>
<text x="322" y="214" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">drives</text>
<!-- UNIMUS -->
<rect x="368" y="128" width="268" height="194" rx="14" fill="#0c8fce" fill-opacity="0.06" stroke="#0c8fce" stroke-width="2.4"></rect>
<text x="502" y="160" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="17" font-weight="700" fill="#0c8fce">Unimus</text>
<text x="502" y="181" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">vendor-agnostic NCM</text>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#0e1726">
<text x="394" y="212">&#9679; scheduled config backup</text>
<text x="394" y="238">&#9679; change tracking + diffs</text>
<text x="394" y="264">&#9679; mass / bulk config push</text>
</g>
<text x="394" y="296" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">minutes, not box-by-box days</text>
<!-- VAULT feeding Unimus -->
<rect x="368" y="356" width="268" height="76" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="2.2"></rect>
<text x="502" y="382" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" font-weight="700" fill="#c026d3">central credential vault</text>
<text x="502" y="403" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">scoped &#183; rotated</text>
<text x="502" y="421" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">never in a script, never by hand</text>
<g stroke="#c026d3" stroke-width="2" fill="none">
<line x1="502" y1="356" x2="502" y2="334"></line>
<polygon points="496,336 502,324 508,336" fill="#c026d3" stroke="none"></polygon>
</g>
<!-- FLEET -->
<g stroke="#0c8fce" stroke-width="2.2" fill="none">
<line x1="636" y1="225" x2="704" y2="225"></line>
<polygon points="704,219 716,225 704,231" fill="#0c8fce" stroke="none"></polygon>
</g>
<rect x="716" y="120" width="404" height="210" rx="16" fill="#0c8fce" fill-opacity="0.03" stroke="#0c8fce" stroke-width="1.8" stroke-dasharray="7 6"></rect>
<text x="918" y="148" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#0c8fce">multi-vendor switch fleet &#8212; one pane</text>
<rect x="742" y="168" width="112" height="66" rx="12" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="798" y="196" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">AU</text>
<text x="798" y="218" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">sites</text>
<rect x="862" y="168" width="112" height="66" rx="12" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="918" y="196" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">NZ</text>
<text x="918" y="218" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">sites</text>
<rect x="982" y="168" width="112" height="66" rx="12" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="1038" y="196" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">PNG</text>
<text x="1038" y="218" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">sites</text>
<text x="918" y="272" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">mixed vendors &#8212; the tooling is not tied to one OS</text>
<text x="918" y="300" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">live production &#8212; changed without taking sites down</text>
<!-- DR return path -->
<g stroke="#7c3aed" stroke-width="2" fill="none" stroke-dasharray="6 5">
<path d="M 918 330 C 918 392, 760 402, 700 402"></path>
<polygon points="702,396 690,402 702,408" fill="#7c3aed" stroke="none"></polygon>
</g>
<text x="812" y="428" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#7c3aed">every backup is a restore point</text>
<text x="812" y="446" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">config DR the fleet never had before</text>
<text x="60" y="466" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" fill="#64748b">community tooling, productionised under a corporate-supported licence</text>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

+81
View File
@@ -0,0 +1,81 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="470" viewBox="0 0 1180 470" role="img" aria-label="A traditional server with many interactive doors accumulating drift, contrasted with a sealed Talos node whose only interface is a mutual-TLS API accepting a versioned machine-config document and answering typed reads.">
<rect width="1180" height="470" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Many doors and drift, versus one API and a document</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">every interactive door is a place where reality and the record can quietly diverge</text>
<defs>
<marker id="arr3" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#64748b"></path>
</marker>
<marker id="arrm3" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#c026d3"></path>
</marker>
<marker id="arrc3" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#0c8fce"></path>
</marker>
</defs>
<!-- ===== left: the traditional box ===== -->
<rect x="60" y="110" width="440" height="290" rx="16" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="78" y="136" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#c026d3">a server that accepts hands</text>
<!-- the box itself -->
<rect x="150" y="200" width="260" height="150" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="280" y="268" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0e1726">the machine</text>
<text x="280" y="292" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#c026d3">state: whatever the last pair</text>
<text x="280" y="308" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#c026d3">of hands left behind</text>
<!-- doors (arrows in) -->
<text x="88" y="188" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">sshd :22</text>
<line x1="130" y1="184" x2="196" y2="216" stroke="#c026d3" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#arrm3)"></line>
<text x="72" y="248" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">console login</text>
<line x1="140" y1="252" x2="188" y2="262" stroke="#c026d3" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#arrm3)"></line>
<text x="66" y="312" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">package manager</text>
<line x1="140" y1="316" x2="188" y2="306" stroke="#c026d3" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#arrm3)"></line>
<text x="110" y="382" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">"just quickly fix it"</text>
<line x1="220" y1="374" x2="252" y2="354" stroke="#c026d3" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#arrm3)"></line>
<!-- drift callout -->
<text x="424" y="228" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#c026d3">drift:</text>
<text x="424" y="246" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">unrecorded &#183;</text>
<text x="424" y="262" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">load-bearing</text>
<text x="424" y="278" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">by next month</text>
<!-- ===== right: the sealed node ===== -->
<rect x="560" y="110" width="584" height="290" rx="16" fill="#0c8fce" fill-opacity="0.04" stroke="#0c8fce" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="578" y="136" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#0c8fce">a sealed node &#8212; the API is the only interface</text>
<!-- git document -->
<rect x="588" y="200" width="180" height="110" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="678" y="232" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#7c3aed">the document</text>
<text x="678" y="254" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">machine config in git</text>
<text x="678" y="270" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">disks &#183; NICs &#183; mirrors &#183; CNI</text>
<text x="678" y="286" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">secrets substituted at render</text>
<!-- API keyhole -->
<rect x="828" y="214" width="120" height="82" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="888" y="246" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0c8fce">the API</text>
<text x="888" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">mTLS gRPC</text>
<text x="888" y="284" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">no shell behind it</text>
<!-- the node -->
<rect x="1008" y="200" width="112" height="110" rx="14" fill="#0e1726" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="1064" y="252" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#ffffff">the node</text>
<text x="1064" y="274" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#7dd3fc">state = document</text>
<!-- flows -->
<line x1="768" y1="240" x2="824" y2="240" stroke="#7c3aed" stroke-width="2" marker-end="url(#arr3)"></line>
<text x="796" y="228" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#7c3aed">apply</text>
<line x1="948" y1="240" x2="1004" y2="240" stroke="#0c8fce" stroke-width="2" marker-end="url(#arrc3)"></line>
<!-- typed reads coming back -->
<path d="M 1008 290 L 954 290" fill="none" stroke="#0c8fce" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#arrc3)"></path>
<text x="700" y="342" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">typed reads out through the same keyhole:</text>
<text x="700" y="360" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">/proc/uptime (prove the reboot) &#183; logs &#183; image pull (prove the credential)</text>
<text x="700" y="378" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">dry-run diff: what changes, and whether it costs a reboot</text>
<!-- footnote -->
<text x="60" y="442" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" fill="#64748b">Rebuild = reprint the document. Drift has nowhere to live, because nobody can log in to create it.</text>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="430" viewBox="0 0 1180 430" role="img" aria-label="The observe-first rollout loop: watch real flows with Hubble, write the allowlist from that evidence, apply it out-of-band where rollback is one delete, verify it is enforcing with zero legitimate drops, and only then commit it to git where the reconciler defends it.">
<rect width="1180" height="430" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Observe first, deny second: the loop</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">every rule points at a flow you watched happen &#8212; and the policy only reaches git after it has proven itself</text>
<!-- arrow marker defs -->
<defs>
<marker id="arr" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#64748b"></path>
</marker>
<marker id="arrc" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#0c8fce"></path>
</marker>
</defs>
<!-- stage 1: observe -->
<rect x="56" y="150" width="188" height="110" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="150" y="182" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0c8fce">observe</text>
<text x="150" y="206" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">Hubble &#183; real flows</text>
<text x="150" y="222" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">source &#183; destination &#183; port</text>
<text x="150" y="238" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">before any policy exists</text>
<!-- stage 2: write -->
<rect x="292" y="150" width="188" height="110" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="386" y="182" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#7c3aed">write from evidence</text>
<text x="386" y="206" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">zero-peer default-deny</text>
<text x="386" y="222" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">+ allows for watched flows</text>
<text x="386" y="238" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">DNS by label &#183; host probes &#183; API</text>
<!-- stage 3: apply out-of-band (dashed boundary = temporary state) -->
<rect x="528" y="138" width="212" height="134" rx="16" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="634" y="176" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#c026d3">apply out-of-band</text>
<text x="634" y="200" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">kubectl, not git</text>
<text x="634" y="216" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">the reconciler can't fight</text>
<text x="634" y="232" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">what it can't see</text>
<text x="634" y="256" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#c026d3">rollback = one delete</text>
<!-- stage 4: verify -->
<rect x="788" y="150" width="188" height="110" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="882" y="182" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0c8fce">verify both ways</text>
<text x="882" y="206" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">status: Valid=True</text>
<text x="882" y="222" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">forbidden flow drops</text>
<text x="882" y="238" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">zero legitimate drops</text>
<!-- stage 5: commit -->
<rect x="1024" y="150" width="120" height="110" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="1084" y="188" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0e1726">commit</text>
<text x="1084" y="212" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">GitOps adopts</text>
<text x="1084" y="228" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">+ defends it</text>
<!-- forward arrows -->
<line x1="244" y1="205" x2="288" y2="205" stroke="#64748b" stroke-width="2" marker-end="url(#arr)"></line>
<line x1="480" y1="205" x2="524" y2="205" stroke="#64748b" stroke-width="2" marker-end="url(#arr)"></line>
<line x1="740" y1="205" x2="784" y2="205" stroke="#64748b" stroke-width="2" marker-end="url(#arr)"></line>
<line x1="976" y1="205" x2="1020" y2="205" stroke="#64748b" stroke-width="2" marker-end="url(#arr)"></line>
<!-- loop-back: verify failure returns to observe -->
<path d="M 882 264 L 882 330 L 150 330 L 150 268" fill="none" stroke="#0c8fce" stroke-width="1.8" stroke-dasharray="6 5" marker-end="url(#arrc)"></path>
<text x="516" y="322" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0c8fce">a legitimate drop found? back to watching &#8212; the fence was wrong, not the animal</text>
<!-- footnote -->
<text x="60" y="396" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" fill="#64748b">After the commit, rollback changes shape: git revert &#8212; self-heal now restores the policy against any hand-delete, exactly as designed.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.6 KiB

@@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="480" viewBox="0 0 1180 480" role="img" aria-label="One hostname, two audiences. Human browsers converge on a single identity-provider proxy outpost which fronts the registry web UI and the browser IDE, each bound to the same outpost. Machine clients - node container runtimes and CI - split off at the gateway and hit the registry's v2 path directly with their own scoped read-only credential, never touching the SSO layer.">
<rect width="1180" height="480" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Route by audience, not by app</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">one hostname &#8212; the gateway splits people from machines before auth is even asked</text>
<!-- the gateway split -->
<rect x="80" y="200" width="170" height="80" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="2"></rect>
<text x="165" y="234" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0e1726">gateway</text>
<text x="165" y="256" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">one hostname, split routes</text>
<!-- human path (cyan, upper) -->
<g stroke="#0c8fce" stroke-width="2.4" fill="none">
<path d="M 250 220 C 310 190, 330 170, 388 152"></path>
<polygon points="384,146 400,148 388,161" fill="#0c8fce" stroke="none"></polygon>
</g>
<text x="300" y="164" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" font-weight="700" fill="#0c8fce">catch-all &#8594;</text>
<rect x="400" y="110" width="230" height="86" rx="14" fill="#0c8fce" fill-opacity="0.06" stroke="#0c8fce" stroke-width="2.4"></rect>
<text x="515" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0c8fce">proxy outpost</text>
<text x="515" y="162" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">the doorman &#8212; one instance</text>
<text x="515" y="180" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">every human path signs in here</text>
<!-- apps behind the doorman -->
<g stroke="#0c8fce" stroke-width="2" fill="none">
<line x1="630" y1="140" x2="716" y2="128"></line>
<polygon points="714,122 728,126 716,135" fill="#0c8fce" stroke="none"></polygon>
<line x1="630" y1="170" x2="716" y2="184"></line>
<polygon points="716,177 728,186 714,190" fill="#0c8fce" stroke="none"></polygon>
</g>
<rect x="730" y="100" width="220" height="56" rx="12" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="840" y="124" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#7c3aed">registry web UI</text>
<text x="840" y="144" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">a nameplate at the desk</text>
<rect x="730" y="168" width="220" height="56" rx="12" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="840" y="192" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#7c3aed">browser IDE</text>
<text x="840" y="212" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">WebSockets included &#8212; no carve-outs</text>
<!-- humans arriving -->
<rect x="80" y="110" width="120" height="52" rx="12" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="140" y="133" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#0c8fce">people</text>
<text x="140" y="152" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">interactive &#183; phishable</text>
<g stroke="#0c8fce" stroke-width="2" fill="none">
<line x1="165" y1="162" x2="165" y2="200"></line>
<polygon points="159,198 165,210 171,198" fill="#0c8fce" stroke="none"></polygon>
</g>
<!-- machine path (magenta, lower) -->
<rect x="80" y="330" width="120" height="52" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="140" y="353" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#c026d3">machines</text>
<text x="140" y="372" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">runtimes &#183; headless</text>
<g stroke="#c026d3" stroke-width="2" fill="none">
<line x1="165" y1="330" x2="165" y2="292"></line>
<polygon points="159,294 165,282 171,294" fill="#c026d3" stroke="none"></polygon>
</g>
<g stroke="#c026d3" stroke-width="2.4" fill="none">
<path d="M 250 262 C 340 300, 420 322, 508 336"></path>
<polygon points="504,329 520,338 506,349" fill="#c026d3" stroke="none"></polygon>
</g>
<text x="330" y="318" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" font-weight="700" fill="#c026d3">/v2 &#8594; direct</text>
<rect x="520" y="310" width="250" height="86" rx="14" fill="#c026d3" fill-opacity="0.05" stroke="#c026d3" stroke-width="2.2"></rect>
<text x="645" y="340" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#c026d3">registry /v2 &#8212; the dock</text>
<text x="645" y="362" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">scoped read-only credential</text>
<text x="645" y="380" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">in each node's registry config</text>
<!-- never-crosses note -->
<line x1="400" y1="240" x2="950" y2="240" stroke="#64748b" stroke-width="1.4" stroke-dasharray="4 6"></line>
<text x="675" y="258" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">the two paths never cross &#8212; neither door weakens for the other's traffic</text>
<!-- native-OIDC aside -->
<rect x="980" y="310" width="140" height="86" rx="12" fill="#ffffff" stroke="#64748b" stroke-width="1.4" stroke-dasharray="5 5"></rect>
<text x="1050" y="338" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#64748b">native OIDC apps</text>
<text x="1050" y="358" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">wire directly &#8212;</text>
<text x="1050" y="374" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10" fill="#64748b">no outpost needed</text>
<!-- footer -->
<text x="60" y="452" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" fill="#64748b">app N+1 behind the doorman costs a provider object and a route &#8212; a nameplate at the desk, never a second desk</text>
</svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="470" viewBox="0 0 1180 470" role="img" aria-label="One required value in the cluster's values file feeds four rendered artefacts: the application config, the network attachment definition, the egress policy and the secondary-interface policy. Because every consumer renders from the same field, the four copies cannot disagree - there is nothing to update twice. Underneath, the other half of the pattern: the schema marks the field required, so a cluster that has not supplied the value fails at template time, in the pipeline, instead of shipping enforcement pointed at nothing.">
<rect width="1180" height="470" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">One source, four renders</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">divergence becomes something the tooling can no longer express</text>
<!-- source -->
<rect x="80" y="160" width="240" height="90" rx="14" fill="#0c8fce" fill-opacity="0.07" stroke="#0c8fce" stroke-width="3"></rect>
<text x="200" y="196" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12.5" fill="#0c8fce" font-weight="700">cluster values</text>
<text x="200" y="218" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">device address &#183; one field</text>
<text x="200" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">lives once &#183; edited once</text>
<!-- fan lines -->
<g stroke="#64748b" stroke-width="1.5" fill="none">
<path d="M 320 205 C 420 205 460 105 560 105"></path>
<path d="M 320 205 C 420 205 460 172 560 172"></path>
<path d="M 320 205 C 420 205 460 239 560 239"></path>
<path d="M 320 205 C 420 205 460 306 560 306"></path>
</g>
<!-- consumers -->
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
<rect x="560" y="82" width="300" height="46" rx="10" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="1.5"></rect>
<text x="580" y="110">application config</text>
<rect x="560" y="149" width="300" height="46" rx="10" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="1.5"></rect>
<text x="580" y="177">network attachment</text>
<rect x="560" y="216" width="300" height="46" rx="10" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="1.5"></rect>
<text x="580" y="244">egress rule</text>
<rect x="560" y="283" width="300" height="46" rx="10" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="1.5"></rect>
<text x="580" y="311">secondary-interface policy</text>
</g>
<g font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">
<text x="890" y="110">what the workload calls</text>
<text x="890" y="177">the address it answers on</text>
<text x="890" y="244">what the traffic may do</text>
<text x="890" y="311">same, for the second leg</text>
</g>
<text x="710" y="352" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" font-weight="700" fill="#7c3aed">never independent copies &#8212; they render, they don't remember</text>
<!-- required strip -->
<rect x="80" y="378" width="1020" height="58" rx="12" fill="#c026d3" fill-opacity="0.05" stroke="#c026d3" stroke-width="1.5"></rect>
<text x="100" y="402" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#c026d3">absence fails loudly</text>
<text x="100" y="422" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#0e1726">field required in the schema &#8594; a cluster without it fails at template time, in the pipeline &#8212; not months later as policy pointed at nothing</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

+79
View File
@@ -0,0 +1,79 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="520" viewBox="0 0 1180 520" role="img" aria-label="The probation ladder for downloaded models, left to right. Arrive: the artefact is more than weights - tokeniser code, an executable chat template, a loader gated by trust_remote_code - pinned by digest with its runtime. Probation tier: a Sandbox resource wraps a pod whose kata runtime class boots a per-pod KVM microVM with its own guest kernel, inside a zero-peer default-deny namespace holding zero credentials; the broker tier above holds all real keys. Observe: real workloads through the harness build a record of egress and behaviour against the written declaration. Gate: when the record matches the declaration over time the model promotes to the standard serving tier; unproven models stay in probation indefinitely. A hardware boundary separates the container from the node kernel for the whole supervised shift.">
<rect width="1180" height="520" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">The probation ladder</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">a new model is a new hire &#8212; promotion is earned on the record, never granted on arrival</text>
<!-- spine -->
<line x1="80" y1="105" x2="1100" y2="105" stroke="#64748b" stroke-width="1.5"></line>
<circle cx="200" cy="105" r="5" fill="#7c3aed"></circle>
<circle cx="530" cy="105" r="5" fill="#0c8fce"></circle>
<circle cx="830" cy="105" r="5" fill="#64748b"></circle>
<circle cx="1020" cy="105" r="5" fill="#c026d3"></circle>
<!-- ARRIVE -->
<text x="200" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#7c3aed">ARRIVE</text>
<rect x="80" y="152" width="240" height="230" rx="14" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="2"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="100" y="182">not just weights:</text>
<text x="100" y="204">+ tokeniser code</text>
<text x="100" y="224">+ executable chat template</text>
<text x="100" y="244">+ loader (trust_remote_code)</text>
<text x="100" y="264">+ sometimes its own runtime</text>
</g>
<text x="100" y="300" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" font-weight="700" fill="#7c3aed">pinned by digest,</text>
<text x="100" y="318" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" font-weight="700" fill="#7c3aed">artefact + runtime</text>
<text x="100" y="352" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">the CV: an account name and</text>
<text x="100" y="368" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">a download count &#8212; uncheckable</text>
<!-- PROBATION (highlighted) -->
<text x="530" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0c8fce">PROBATION &#8212; the supervised shift</text>
<rect x="360" y="152" width="340" height="230" rx="14" fill="#0c8fce" fill-opacity="0.07" stroke="#0c8fce" stroke-width="3"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="380" y="182">Sandbox CR &#8594; pod &#8594; runtimeClass kata</text>
<text x="380" y="204">&#8594; per-pod KVM microVM,</text>
<text x="392" y="222">its OWN guest kernel</text>
<text x="380" y="248">namespace: default-deny, zero peers</text>
<text x="380" y="268">credentials mounted: none</text>
<text x="380" y="288">egress: only what is declared</text>
<text x="380" y="308">keys: held by the broker tier above</text>
</g>
<line x1="380" y1="326" x2="680" y2="326" stroke="#0c8fce" stroke-width="1.5" stroke-dasharray="6 5"></line>
<text x="380" y="348" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#0c8fce">hardware boundary: an escape lands in a guest kernel,</text>
<text x="380" y="366" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#0c8fce">not on the node</text>
<!-- OBSERVE -->
<text x="830" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#64748b">OBSERVE</text>
<rect x="740" y="152" width="180" height="230" rx="14" fill="#64748b" fill-opacity="0.04" stroke="#64748b" stroke-width="2"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="758" y="182">real workloads</text>
<text x="758" y="202">through the</text>
<text x="758" y="222">harness</text>
<text x="758" y="252">the record:</text>
<text x="758" y="272">egress log vs</text>
<text x="758" y="292">declaration</text>
<text x="758" y="312">behaviour vs</text>
<text x="758" y="332">expectation</text>
</g>
<text x="758" y="366" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">long enough, boring enough</text>
<!-- GATE -->
<text x="1020" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#c026d3">GATE</text>
<rect x="950" y="152" width="150" height="230" rx="14" fill="#c026d3" fill-opacity="0.05" stroke="#c026d3" stroke-width="2"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11">
<text x="968" y="188" fill="#0c8fce">record matches</text>
<text x="968" y="208" fill="#0c8fce">&#8594; PROMOTE to</text>
<text x="980" y="228" fill="#0c8fce">serving tier</text>
<text x="968" y="266" fill="#c026d3">unproven</text>
<text x="968" y="286" fill="#c026d3">&#8594; stays in the</text>
<text x="980" y="306" fill="#c026d3">VM. forever</text>
<text x="980" y="326" fill="#c026d3">if need be</text>
</g>
<text x="968" y="362" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">permanent probation</text>
<text x="968" y="378" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">costs almost nothing</text>
<!-- footer -->
<text x="590" y="452" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#0e1726">trust is a history, not a property &#8212; observed behaviour, under constraint, accumulated until it's boring</text>
<text x="590" y="478" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">the tax (seconds to boot, 350 MiB per microVM, batch-shaped work) isn't a flaw in the ladder &#8212; the tax IS the ladder</text>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

+55
View File
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="440" viewBox="0 0 1180 440" role="img" aria-label="Nightly backups stream from the clusters into a repository on the NAS and every job reports green; the highlighted drill loop pulls a snapshot back out into a scratch pod, verifies files and configs, and produces a pass or fail — the loop out of the vault is the proof, not the data going in.">
<rect width="1180" height="440" fill="#ffffff"></rect>
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#94a3b8"></path>
</marker>
<marker id="arrC" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0 0 L10 5 L0 10 z" fill="#0c8fce"></path>
</marker>
</defs>
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">The drill loop is the receipt</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">data flowing in happens every night &#8212; only data pulled back OUT proves the backup is real</text>
<!-- clusters -->
<rect x="56" y="150" width="180" height="84" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="146" y="184" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0e1726">cluster apps</text>
<text x="146" y="208" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">PVCs &#183; dumps &#183; state</text>
<rect x="56" y="250" width="180" height="60" rx="14" fill="#ffffff" stroke="#94a3b8" stroke-width="1.4"></rect>
<text x="146" y="278" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#64748b">nightly CronJob</text>
<text x="146" y="296" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">exit 0 &#8594; green tick</text>
<!-- repository vault -->
<rect x="380" y="150" width="230" height="160" rx="16" fill="#7c3aed" fill-opacity="0.05" stroke="#7c3aed" stroke-width="2"></rect>
<text x="495" y="186" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15.5" font-weight="700" fill="#7c3aed">repository</text>
<text x="495" y="210" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">restic on the NAS &#183; encrypted</text>
<text x="495" y="230" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">key held offline &#8212; never</text>
<text x="495" y="246" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">stored beside the data</text>
<text x="495" y="284" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">a green tick ends here</text>
<!-- backup arrows (routine, grey) -->
<path d="M236 192 H372" fill="none" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr)"></path>
<text x="304" y="182" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">snapshot in</text>
<path d="M236 280 C300 280 320 250 372 236" fill="none" stroke="#94a3b8" stroke-width="2" marker-end="url(#arr)"></path>
<!-- THE DRILL LOOP (highlighted, cyan) -->
<rect x="700" y="150" width="210" height="110" rx="14" fill="#0c8fce" fill-opacity="0.05" stroke="#0c8fce" stroke-width="2.2"></rect>
<text x="805" y="184" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0c8fce">scratch pod</text>
<text x="805" y="208" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">restore into nothing</text>
<text x="805" y="228" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">files pulled &#183; configs read</text>
<text x="805" y="244" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">services prove they start</text>
<rect x="980" y="164" width="140" height="82" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="2"></rect>
<text x="1050" y="199" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="26" font-weight="700" fill="#0c8fce">&#10003;</text>
<text x="1050" y="228" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">pass / fail</text>
<path d="M610 205 H692" fill="none" stroke="#0c8fce" stroke-width="3" marker-end="url(#arrC)"></path>
<text x="651" y="195" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" font-weight="700" fill="#0c8fce">snapshot OUT</text>
<path d="M910 205 H972" fill="none" stroke="#0c8fce" stroke-width="3" marker-end="url(#arrC)"></path>
<path d="M805 260 C805 330 560 330 500 314" fill="none" stroke="#0c8fce" stroke-width="2" stroke-dasharray="6 5" marker-end="url(#arrC)"></path>
<text x="672" y="342" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0c8fce">drill on a schedule &#183; failed drill = real incident</text>
<text x="60" y="404" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" fill="#64748b">the vault is not the proof &#8212; the loop out of it is</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="430" viewBox="0 0 1180 430" role="img" aria-label="The secret-zero bootstrap chain: one out-of-band machine identity (read-only, per-cluster, applied by hand, never in git) unlocks the secret store; a ClusterSecretStore and ExternalSecrets live in git as pointers (names, not values); the External Secrets Operator resolves each pointer and reads the real values from Infisical to materialise a Kubernetes Secret in-cluster.">
<rect width="1180" height="430" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Secret zero: the seed outside GitOps</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">one out-of-band credential unlocks the store &#8212; the repo only ever holds pointers</text>
<!-- Infisical store (real values) -->
<ellipse cx="975" cy="96" rx="78" ry="14" fill="#eef2f7" stroke="#0c8fce" stroke-width="1.6"></ellipse>
<path d="M897 96 V154" fill="none" stroke="#0c8fce" stroke-width="1.6"></path>
<path d="M1053 96 V154" fill="none" stroke="#0c8fce" stroke-width="1.6"></path>
<path d="M897 154 A78 14 0 0 0 1053 154" fill="none" stroke="#0c8fce" stroke-width="1.6"></path>
<text x="975" y="101" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" font-weight="700" fill="#0e1726">Infisical</text>
<text x="975" y="130" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">the real values</text>
<!-- store -> materialised Secret (real values flow down) -->
<path d="M975 156 V232" fill="none" stroke="#0c8fce" stroke-width="2" stroke-dasharray="2 5" stroke-linecap="round"></path>
<path d="M969 222 L975 233 M981 222 L975 233" fill="none" stroke="#0c8fce" stroke-width="2" stroke-linecap="round"></path>
<text x="992" y="200" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">real values</text>
<text x="992" y="216" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">(authed by secret zero)</text>
<!-- git repo boundary (holds the pointers) -->
<rect x="310" y="196" width="508" height="160" rx="18" fill="#0c8fce" fill-opacity="0.04" stroke="#0c8fce" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="328" y="218" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#0c8fce">in git &#8212; pointers, never values</text>
<!-- out-of-band pill -->
<rect x="56" y="206" width="128" height="23" rx="11.5" fill="#ffffff" stroke="#c026d3" stroke-width="1.4"></rect>
<text x="120" y="222" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#c026d3">out-of-band</text>
<!-- secret zero box -->
<rect x="56" y="236" width="194" height="100" rx="14" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="153" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15.5" font-weight="700" fill="#c026d3">secret zero</text>
<text x="153" y="298" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">read-only &#183; per-cluster</text>
<text x="153" y="319" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">applied by hand &#183; never in git</text>
<!-- ClusterSecretStore box -->
<rect x="330" y="236" width="194" height="100" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="427" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">ClusterSecretStore</text>
<text x="427" y="298" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">store URL + ref &#8594; secret zero</text>
<text x="427" y="319" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">wires the cluster up</text>
<!-- ExternalSecret box -->
<rect x="604" y="236" width="194" height="100" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="701" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0e1726">ExternalSecret &#215;N</text>
<text x="701" y="298" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">a pointer per app</text>
<text x="701" y="319" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">a name, never a value</text>
<!-- Kubernetes Secret box (materialised in-cluster) -->
<rect x="878" y="236" width="194" height="100" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="975" y="272" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14.5" font-weight="700" fill="#0e1726">Kubernetes Secret</text>
<text x="975" y="298" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">materialised in-cluster</text>
<text x="975" y="319" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">kept in sync by ESO</text>
<!-- arrows (run clean under the labels) -->
<path d="M250 286 H324" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M314 280 L324 286 M314 292 L324 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M524 286 H598" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M588 280 L598 286 M588 292 L598 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M798 286 H872" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<path d="M862 280 L872 286 M862 292 L872 286" fill="none" stroke="#64748b" stroke-width="2.2" stroke-linecap="round"></path>
<!-- arrow labels on clean white pills, seated above each arrow -->
<rect x="261" y="255" width="58" height="19" rx="9.5" fill="#ffffff"></rect>
<text x="290" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">unlocks</text>
<rect x="527" y="255" width="74" height="19" rx="9.5" fill="#ffffff"></rect>
<text x="564" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">references</text>
<rect x="807" y="255" width="62" height="19" rx="9.5" fill="#ffffff"></rect>
<text x="838" y="268" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">resolves</text>
<!-- footer takeaway -->
<text x="60" y="398" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#64748b">Exactly one credential lives outside GitOps &#8212; read-only, per-cluster, the seed the whole tree grows from. Everything else is just a pointer.</text>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

+44
View File
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="500" viewBox="0 0 1180 500" role="img" aria-label="Two views of the same six days. What the platform saw: the container running, the process alive, the port answering, logs present - every default check passing. What was true: the tunnel the workload exists to use was down, nothing had moved for six days, and the retry backoff made the logs quieter each day. A timeline underneath runs from the failure on day zero to discovery on day six - found by a person asking a question, not by the stack. The fix strip: probe the capability, not the container - the tunnel's own health check gates the workload, so health only reports when traffic genuinely egresses.">
<rect width="1180" height="500" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Running was never the job</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the gap between &#34;the process is running&#34; and &#34;the process is doing its job&#34; is where invisible outages live</text>
<!-- platform view -->
<text x="315" y="112" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0c8fce">WHAT THE PLATFORM SAW</text>
<rect x="70" y="126" width="490" height="170" rx="14" fill="#0c8fce" fill-opacity="0.05" stroke="#0c8fce" stroke-width="2"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
<text x="100" y="160">container: Running</text>
<text x="100" y="184">process: alive, answers, holds its port</text>
<text x="100" y="208">every default check: passing</text>
<text x="100" y="232">logs: present &#8212; and getting quieter</text>
</g>
<text x="100" y="272" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">entirely satisfied &#183; no reason to think otherwise</text>
<!-- truth view -->
<text x="865" y="112" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#c026d3">WHAT WAS TRUE</text>
<rect x="620" y="126" width="490" height="170" rx="14" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="2"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
<text x="650" y="160">the tunnel: down</text>
<text x="650" y="184">traffic moved: none, for six days</text>
<text x="650" y="208">retry backoff: growing &#8212; the failure</text>
<text x="662" y="230">was quietly obscuring itself</text>
</g>
<text x="650" y="272" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">alive, responsive, and completely useless</text>
<!-- timeline -->
<line x1="90" y1="340" x2="1090" y2="340" stroke="#64748b" stroke-width="1.5"></line>
<circle cx="130" cy="340" r="6" fill="#c026d3"></circle>
<circle cx="1050" cy="340" r="6" fill="#0c8fce"></circle>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5">
<text x="130" y="322" text-anchor="middle" fill="#c026d3">day 0: tunnel dies</text>
<text x="1050" y="322" text-anchor="middle" fill="#0c8fce">day 6: a person asks</text>
</g>
<text x="590" y="366" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">six days of nothing &#8212; discovered by accident, the way every unwatched failure is discovered</text>
<!-- fix strip -->
<rect x="70" y="396" width="1040" height="70" rx="12" fill="#0c8fce" fill-opacity="0.05" stroke="#0c8fce" stroke-width="1.5"></rect>
<text x="90" y="422" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#0c8fce">probe the capability, not the container</text>
<text x="90" y="444" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#0e1726">the tunnel's own health check gates the workload &#8594; passes only when traffic genuinely egresses &#8594; silent failure becomes a restart, then an alert</text>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+60
View File
@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="470" viewBox="0 0 1180 470" role="img" aria-label="Three node power states - Ready, off by choice, and parked broken - drawn above one continuous document layer in git holding the machine config, inline CNI, workloads and credentials. Ready and off-by-choice exchange through a power button; parked-broken exits only through a repair. The document layer persists identically beneath all three states.">
<rect width="1180" height="470" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Power is a state; the node is a document</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the states sit on top &#8212; the document layer underneath never changes</text>
<!-- READY -->
<rect x="80" y="110" width="280" height="150" rx="14" fill="#0c8fce" fill-opacity="0.06" stroke="#0c8fce" stroke-width="2.2"></rect>
<text x="220" y="145" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="16" font-weight="700" fill="#0c8fce">READY</text>
<text x="220" y="172" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">lit &#183; workloads reconciled</text>
<text x="220" y="192" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">cave &#183; alfred</text>
<text x="220" y="236" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">the always-on core</text>
<!-- OFF BY CHOICE -->
<rect x="450" y="110" width="280" height="150" rx="14" fill="#0e1726" fill-opacity="0.05" stroke="#0e1726" stroke-width="2"></rect>
<text x="590" y="145" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="16" font-weight="700" fill="#0e1726">OFF BY CHOICE</text>
<text x="590" y="172" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">dark &#183; healthy at shutdown</text>
<text x="590" y="192" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">robin &#183; kate &#183; wgirl</text>
<text x="590" y="236" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">recorded as rest, in the log</text>
<!-- power-button exchange between READY and OFF-BY-CHOICE -->
<g stroke="#0c8fce" stroke-width="2" fill="none">
<line x1="360" y1="165" x2="450" y2="165"></line>
<polygon points="450,159 462,165 450,171" fill="#0c8fce" stroke="none"></polygon>
<line x1="462" y1="205" x2="372" y2="205"></line>
<polygon points="372,199 360,205 372,211" fill="#0c8fce" stroke="none"></polygon>
</g>
<text x="411" y="152" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#0c8fce">talosctl shutdown</text>
<text x="411" y="226" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#0c8fce">one power button</text>
<!-- PARKED BROKEN, set apart -->
<rect x="850" y="110" width="270" height="150" rx="14" fill="#c026d3" fill-opacity="0.05" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="985" y="145" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="16" font-weight="700" fill="#c026d3">PARKED BROKEN</text>
<text x="985" y="172" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">dark &#183; hardware fault</text>
<text x="985" y="192" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">elfastc</text>
<text x="985" y="236" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" font-weight="700" fill="#c026d3">only exit: a screwdriver</text>
<!-- separator emphasising the two kinds of dark -->
<line x1="790" y1="110" x2="790" y2="260" stroke="#64748b" stroke-width="1.5" stroke-dasharray="4 6"></line>
<text x="790" y="98" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">identical from the network &#8212; different in the books</text>
<!-- the document layer -->
<rect x="80" y="300" width="1040" height="110" rx="16" fill="#7c3aed" fill-opacity="0.07" stroke="#7c3aed" stroke-width="2.6"></rect>
<text x="600" y="334" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="16" font-weight="700" fill="#7c3aed">the document layer &#8212; git, continuous beneath every state</text>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">
<text x="180" y="374">machine config</text>
<text x="420" y="374">CNI inline manifest</text>
<text x="700" y="374">workloads (GitOps)</text>
<text x="950" y="374">credentials (sops)</text>
</g>
<g stroke="#7c3aed" stroke-width="1.6" fill="none" opacity="0.7">
<line x1="220" y1="260" x2="220" y2="300"></line>
<line x1="590" y1="260" x2="590" y2="300"></line>
<line x1="985" y1="260" x2="985" y2="300"></line>
</g>
<!-- footer -->
<text x="60" y="448" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" fill="#64748b">a node boots into exactly the machine its documents describe &#8212; power is just whether the document is currently being read</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+87
View File
@@ -0,0 +1,87 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="470" viewBox="0 0 1180 470" role="img" aria-label="Two paths for the same client packet: with externalTrafficPolicy Cluster the node SNATs the connection so policy sees the un-restrictable world identity; with externalTrafficPolicy Local the client's real address survives to policy evaluation and a precise allow rule can match it.">
<rect width="1180" height="470" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Policies bind to what arrives, not what was sent</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the same client, the same service &#8212; one traffic-policy line decides whether the policy ever meets the sender</text>
<defs>
<marker id="arr2" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#64748b"></path>
</marker>
<marker id="arrm" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#c026d3"></path>
</marker>
<marker id="arrc2" markerWidth="10" markerHeight="8" refX="8" refY="4" orient="auto">
<path d="M0,0 L9,4 L0,8 z" fill="#0c8fce"></path>
</marker>
</defs>
<!-- ===== top lane: ETP Cluster ===== -->
<rect x="36" y="100" width="1108" height="150" rx="16" fill="#c026d3" fill-opacity="0.04" stroke="#c026d3" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="54" y="124" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#c026d3">externalTrafficPolicy: Cluster &#8212; the default, and the depot stamp</text>
<!-- client -->
<rect x="66" y="146" width="170" height="84" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="151" y="178" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">client</text>
<text x="151" y="200" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0c8fce">src 10.0.11.42</text>
<text x="151" y="217" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">an honest return address</text>
<!-- LB VIP -->
<rect x="306" y="146" width="150" height="84" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="381" y="184" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#7c3aed">LB VIP</text>
<text x="381" y="206" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">any node accepts</text>
<!-- SNAT -->
<rect x="526" y="146" width="200" height="84" rx="14" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="626" y="176" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#c026d3">node SNAT</text>
<text x="626" y="198" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">src &#8594; the node's own</text>
<text x="626" y="215" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">stamped over in transit</text>
<!-- policy sees world -->
<rect x="796" y="146" width="200" height="84" rx="14" fill="#ffffff" stroke="#c026d3" stroke-width="1.8"></rect>
<text x="896" y="176" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#c026d3">policy evaluates</text>
<text x="896" y="198" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#c026d3">identity: world</text>
<text x="896" y="215" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">the sender is gone</text>
<text x="1052" y="182" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#c026d3">no rule can</text>
<text x="1052" y="200" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#c026d3">name the client</text>
<line x1="236" y1="188" x2="302" y2="188" stroke="#64748b" stroke-width="2" marker-end="url(#arr2)"></line>
<line x1="456" y1="188" x2="522" y2="188" stroke="#64748b" stroke-width="2" marker-end="url(#arr2)"></line>
<line x1="726" y1="188" x2="792" y2="188" stroke="#c026d3" stroke-width="2" marker-end="url(#arrm)"></line>
<!-- ===== bottom lane: ETP Local ===== -->
<rect x="36" y="272" width="1108" height="150" rx="16" fill="#0c8fce" fill-opacity="0.04" stroke="#0c8fce" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="54" y="296" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13" font-weight="700" fill="#0c8fce">externalTrafficPolicy: Local &#8212; the sender survives (commit it, or self-heal undoes the fix)</text>
<!-- client -->
<rect x="66" y="318" width="170" height="84" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="151" y="350" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">client</text>
<text x="151" y="372" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0c8fce">src 10.0.11.42</text>
<!-- LB VIP backend-local -->
<rect x="306" y="318" width="200" height="84" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="406" y="348" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#7c3aed">LB VIP &#8212; backend node only</text>
<text x="406" y="370" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">no cross-node forward</text>
<text x="406" y="387" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">no SNAT needed</text>
<!-- policy sees real client -->
<rect x="576" y="318" width="220" height="84" rx="14" fill="#ffffff" stroke="#0c8fce" stroke-width="1.8"></rect>
<text x="686" y="348" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0c8fce">policy evaluates</text>
<text x="686" y="370" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0c8fce">identity: 10.0.11.42</text>
<text x="686" y="387" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">the envelope kept its address</text>
<!-- precise rule -->
<rect x="866" y="318" width="200" height="84" rx="14" fill="#ffffff" stroke="#0e1726" stroke-width="1.6"></rect>
<text x="966" y="348" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">a precise allow</text>
<text x="966" y="370" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">fromCIDR the LAN &#183; a /32 peer</text>
<text x="966" y="387" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">least privilege, possible again</text>
<line x1="236" y1="360" x2="302" y2="360" stroke="#64748b" stroke-width="2" marker-end="url(#arr2)"></line>
<line x1="506" y1="360" x2="572" y2="360" stroke="#64748b" stroke-width="2" marker-end="url(#arr2)"></line>
<line x1="796" y1="360" x2="862" y2="360" stroke="#0c8fce" stroke-width="2" marker-end="url(#arrc2)"></line>
<!-- footnote -->
<text x="60" y="446" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" fill="#64748b">Caveats that keep it honest: overlay/subnet routers still masquerade regardless &#8212; and swapping the LB plumbing itself (kube-proxy &#8594; eBPF) changes what arrives under an unchanged policy. Observe before you allow.</text>
</svg>

After

Width:  |  Height:  |  Size: 8.5 KiB

+71
View File
@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="490" viewBox="0 0 1180 490" role="img" aria-label="A three-stage timeline for turning off anonymous registry pulls. Stage: one pull credential fanned out inert into six node config repos while the registry door stays propped open with anonymous read on. Prove: each node reboots onto the credential and is gated by a real pull of a mirror-only image through its own runtime - three of six passed, one parked for hardware repair. Flip: anonymous read goes off only at six of six. The per-node gate is the highlighted step, not the flip.">
<rect width="1180" height="490" fill="#ffffff"></rect>
<!-- title -->
<text x="60" y="46" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="23" font-weight="700" fill="#0e1726">Stage &#8594; prove &#8594; flip: the split in time</text>
<text x="61" y="70" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="13.5" fill="#64748b">the flip is the trivial step &#8212; the per-node gate is where the safety lives</text>
<!-- timeline spine -->
<line x1="80" y1="105" x2="1100" y2="105" stroke="#64748b" stroke-width="1.5"></line>
<circle cx="230" cy="105" r="5" fill="#7c3aed"></circle>
<circle cx="590" cy="105" r="5" fill="#0c8fce"></circle>
<circle cx="950" cy="105" r="5" fill="#c026d3"></circle>
<!-- STAGE -->
<text x="230" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#7c3aed">STAGE</text>
<rect x="90" y="152" width="280" height="200" rx="14" fill="#7c3aed" fill-opacity="0.04" stroke="#7c3aed" stroke-width="2"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="110" y="182">one pull credential</text>
<text x="110" y="202">&#8594; six node repos, via git</text>
<text x="110" y="222">&#8594; sops placeholder, never</text>
<text x="122" y="240">the value</text>
</g>
<text x="110" y="272" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" font-weight="700" fill="#0e1726">door stays propped:</text>
<text x="110" y="292" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">anonymous read still ON</text>
<text x="110" y="330" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">inert &#183; reviewable &#183; free to revert</text>
<!-- PROVE (highlighted) -->
<text x="590" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#0c8fce">PROVE &#8212; the gate</text>
<rect x="450" y="152" width="280" height="200" rx="14" fill="#0c8fce" fill-opacity="0.07" stroke="#0c8fce" stroke-width="3"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="470" y="182">per node: reboot onto auth,</text>
<text x="470" y="202">then a REAL pull of a</text>
<text x="470" y="222">mirror-only image through</text>
<text x="470" y="242">the node's own runtime</text>
</g>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12">
<text x="470" y="280" fill="#0c8fce">&#10003; &#10003; &#10003; three gated</text>
<text x="470" y="302" fill="#64748b">&#9675; &#9675; awaiting their window</text>
<text x="470" y="324" fill="#c026d3">&#9888; one parked &#8212; hardware</text>
</g>
<text x="470" y="344" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11" fill="#64748b">no cache or fallback can fake a pass</text>
<!-- FLIP -->
<text x="950" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="15" font-weight="700" fill="#c026d3">FLIP &#8212; only at 6/6</text>
<rect x="810" y="152" width="280" height="200" rx="14" fill="#ffffff" stroke="#c026d3" stroke-width="2" stroke-dasharray="7 6"></rect>
<g font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#0e1726">
<text x="830" y="182">registry config:</text>
<text x="830" y="202">anonymous read &#8594; OFF</text>
</g>
<text x="830" y="240" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" font-weight="700" fill="#c026d3">blocked, deliberately,</text>
<text x="830" y="260" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" font-weight="700" fill="#c026d3">until the last key turns</text>
<text x="830" y="298" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">a staged lock costs nothing;</text>
<text x="830" y="316" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">a turned lock with one unproven</text>
<text x="830" y="334" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="11.5" fill="#64748b">key costs a boot loop</text>
<!-- arrows between stages -->
<g stroke="#64748b" stroke-width="2" fill="none">
<line x1="370" y1="252" x2="438" y2="252"></line>
<polygon points="438,246 450,252 438,258" fill="#64748b" stroke="none"></polygon>
<line x1="730" y1="252" x2="798" y2="252"></line>
<polygon points="798,246 810,252 798,258" fill="#64748b" stroke="none"></polygon>
</g>
<!-- failure-surface note -->
<rect x="90" y="384" width="1000" height="52" rx="12" fill="#0e1726" fill-opacity="0.03" stroke="#64748b" stroke-width="1.4" stroke-dasharray="5 5"></rect>
<text x="590" y="406" text-anchor="middle" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#0e1726">why the split: this change fails only at the NEXT BOOT &#8212; and the failing node is the one that can't report it</text>
<text x="590" y="426" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11" fill="#64748b">containerd 401-fallback: unreliable (issues #7321, #9997) &#8212; never boot-and-hope</text>
<!-- footer -->
<text x="60" y="472" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12" fill="#64748b">readiness is never inferred from the node coming back &#8212; readiness is the key turning in the door, observed, per node</text>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

+69
View File
@@ -0,0 +1,69 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1180" height="440" viewBox="0 0 1180 440" role="img" aria-label="Two isolation models compared. Left: three containers as processes sharing one host kernel, separated only by dotted namespace lines - an escape lands on the host. Right: each pod boots inside its own microVM with its own guest kernel behind the hardware hypervisor - an escape lands inside a disposable virtual machine, not on the host.">
<rect width="1180" height="440" fill="#ffffff"></rect>
<!-- titles -->
<text x="60" y="52" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="17" font-weight="600" fill="#0f172a">namespaces: one shared kernel</text>
<text x="640" y="52" font-family="ui-monospace,Menlo,Consolas,monospace" font-size="17" font-weight="600" fill="#0f172a">Kata: a kernel per pod</text>
<!-- ===== LEFT: shared kernel ===== -->
<!-- three container processes, dotted separations -->
<g font-family="ui-monospace,Menlo,Consolas,monospace" font-size="14" text-anchor="middle">
<rect x="60" y="84" width="130" height="86" rx="8" fill="none" stroke="#0c8fce" stroke-width="2" stroke-dasharray="6 5"/>
<text x="125" y="122" fill="#0f172a">container</text><text x="125" y="142" fill="#64748b">process</text>
<rect x="215" y="84" width="130" height="86" rx="8" fill="none" stroke="#0c8fce" stroke-width="2" stroke-dasharray="6 5"/>
<text x="280" y="122" fill="#0f172a">container</text><text x="280" y="142" fill="#64748b">process</text>
<rect x="370" y="84" width="130" height="86" rx="8" fill="none" stroke="#c026d3" stroke-width="2" stroke-dasharray="6 5"/>
<text x="435" y="122" fill="#0f172a">untrusted</text><text x="435" y="142" fill="#64748b">process</text>
<!-- escape arrow: through the dotted floor into the kernel -->
<path d="M435 172 L435 208" stroke="#c026d3" stroke-width="2.5" marker-end="url(#am)"/>
<text x="522" y="196" fill="#c026d3" font-size="13">escape = host</text>
<!-- one kernel slab -->
<rect x="60" y="214" width="440" height="64" rx="8" fill="#eef2f7" stroke="#334155" stroke-width="2"/>
<text x="280" y="242" fill="#0f172a" font-weight="600">ONE host kernel</text>
<text x="280" y="262" fill="#64748b">shared by every container and the node</text>
<!-- host hardware -->
<rect x="60" y="292" width="440" height="46" rx="8" fill="none" stroke="#94a3b8" stroke-width="2"/>
<text x="280" y="321" fill="#64748b">host hardware</text>
<text x="280" y="376" fill="#64748b" font-size="13">dotted lines are namespaces:</text>
<text x="280" y="396" fill="#64748b" font-size="13">visibility control, not a security boundary</text>
</g>
<!-- ===== RIGHT: kata microVMs ===== -->
<g font-family="ui-monospace,Menlo,Consolas,monospace" font-size="14" text-anchor="middle">
<!-- two microVMs, solid walls, each with its own guest kernel -->
<g>
<rect x="640" y="84" width="220" height="150" rx="10" fill="none" stroke="#334155" stroke-width="3"/>
<text x="750" y="112" fill="#0f172a">pod</text>
<rect x="662" y="126" width="176" height="42" rx="6" fill="none" stroke="#0c8fce" stroke-width="2"/>
<text x="750" y="152" fill="#0f172a">container process</text>
<rect x="662" y="180" width="176" height="38" rx="6" fill="#eef2f7" stroke="#7c3aed" stroke-width="2"/>
<text x="750" y="204" fill="#7c3aed">own guest kernel</text>
</g>
<g>
<rect x="890" y="84" width="220" height="150" rx="10" fill="none" stroke="#334155" stroke-width="3"/>
<text x="1000" y="112" fill="#0f172a">untrusted pod</text>
<rect x="912" y="126" width="176" height="42" rx="6" fill="none" stroke="#c026d3" stroke-width="2"/>
<text x="1000" y="152" fill="#0f172a">agent process</text>
<rect x="912" y="180" width="176" height="38" rx="6" fill="#eef2f7" stroke="#7c3aed" stroke-width="2"/>
<text x="1000" y="204" fill="#7c3aed">own guest kernel</text>
</g>
<!-- escape arrow: contained inside the VM wall -->
<path d="M1000 160 Q 1052 168 1046 186" fill="none" stroke="#c026d3" stroke-width="2.5" marker-end="url(#am)"/>
<text x="1010" y="256" fill="#c026d3" font-size="13">escape = still inside a throwaway VM</text>
<!-- hypervisor + hardware -->
<rect x="640" y="272" width="470" height="46" rx="8" fill="#eef2f7" stroke="#334155" stroke-width="2"/>
<text x="875" y="301" fill="#0f172a" font-weight="600">hypervisor (KVM) - CPU-enforced boundary</text>
<rect x="640" y="332" width="470" height="42" rx="8" fill="none" stroke="#94a3b8" stroke-width="2"/>
<text x="875" y="359" fill="#64748b">host hardware (VT-x)</text>
<text x="875" y="406" fill="#64748b" font-size="13">runtimeClassName: kata - same kubectl, different boundary</text>
</g>
<!-- divider -->
<line x1="573" y1="70" x2="573" y2="400" stroke="#e2e8f0" stroke-width="2"/>
<defs>
<marker id="am" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0 0L10 5L0 10z" fill="#c026d3"/>
</marker>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+13 -13
View File
@@ -13,39 +13,39 @@
<!-- broken link: API -> pod (token never mounted) -->
<text x="336" y="180" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="11.5" fill="#64748b">no API token mounted</text>
<path d="M244 198 H424" fill="none" stroke="#64748b" stroke-width="2" stroke-dasharray="6 6" stroke-linecap="round"></path>
<path d="M328 190 L344 206 M344 190 L328 206" fill="none" stroke="#0d9488" stroke-width="2.6" stroke-linecap="round"></path>
<path d="M328 190 L344 206 M344 190 L328 206" fill="none" stroke="#0c8fce" stroke-width="2.6" stroke-linecap="round"></path>
<!-- namespace boundary -->
<rect x="430" y="92" width="630" height="252" rx="18" fill="#0d9488" fill-opacity="0.04" stroke="#0d9488" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="452" y="121" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0d9488">namespace</text>
<rect x="792" y="104" width="246" height="30" rx="15" fill="#ffffff" stroke="#0d9488" stroke-width="1.4"></rect>
<text x="915" y="124" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0d9488">PodSecurity: baseline &#8594; restricted</text>
<rect x="430" y="92" width="630" height="252" rx="18" fill="#0c8fce" fill-opacity="0.04" stroke="#0c8fce" stroke-width="2.2" stroke-dasharray="7 6"></rect>
<text x="452" y="121" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0c8fce">namespace</text>
<rect x="792" y="104" width="246" height="30" rx="15" fill="#ffffff" stroke="#0c8fce" stroke-width="1.4"></rect>
<text x="915" y="124" text-anchor="middle" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0c8fce">PodSecurity: baseline &#8594; restricted</text>
<!-- workload pod -->
<rect x="470" y="156" width="420" height="176" rx="14" fill="#ffffff" stroke="#3b6fb0" stroke-width="1.8"></rect>
<rect x="470" y="156" width="420" height="176" rx="14" fill="#ffffff" stroke="#7c3aed" stroke-width="1.8"></rect>
<text x="490" y="184" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="14" font-weight="700" fill="#0e1726">workload pod</text>
<!-- badge 1 -->
<rect x="490" y="196" width="380" height="26" rx="13" fill="#f1f5f9"></rect>
<circle cx="507" cy="209" r="3.6" fill="#0d9488"></circle>
<circle cx="507" cy="209" r="3.6" fill="#0c8fce"></circle>
<text x="522" y="213" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">ServiceAccount: its own &#8212; not the default</text>
<!-- badge 2 -->
<rect x="490" y="228" width="380" height="26" rx="13" fill="#f1f5f9"></rect>
<circle cx="507" cy="241" r="3.6" fill="#0d9488"></circle>
<circle cx="507" cy="241" r="3.6" fill="#0c8fce"></circle>
<text x="522" y="245" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">automountServiceAccountToken: false</text>
<!-- badge 3 -->
<rect x="490" y="260" width="380" height="26" rx="13" fill="#f1f5f9"></rect>
<circle cx="507" cy="273" r="3.6" fill="#0d9488"></circle>
<circle cx="507" cy="273" r="3.6" fill="#0c8fce"></circle>
<text x="522" y="277" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">runAsNonRoot &#183; uid 1000 &#183; drop ALL caps</text>
<!-- badge 4 -->
<rect x="490" y="292" width="380" height="26" rx="13" fill="#f1f5f9"></rect>
<circle cx="507" cy="305" r="3.6" fill="#0d9488"></circle>
<circle cx="507" cy="305" r="3.6" fill="#0c8fce"></circle>
<text x="522" y="309" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="12" fill="#0e1726">seccompProfile: RuntimeDefault</text>
<!-- the exception: components that DO call the API -->
<rect x="60" y="286" width="300" height="80" rx="12" fill="#ffffff" stroke="#2e8b57" stroke-width="1.6"></rect>
<text x="78" y="309" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#2e8b57">the documented exception</text>
<rect x="60" y="286" width="300" height="80" rx="12" fill="#ffffff" stroke="#c026d3" stroke-width="1.6"></rect>
<text x="78" y="309" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif" font-size="12.5" font-weight="700" fill="#c026d3">the documented exception</text>
<text x="78" y="330" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#64748b">GitOps &#183; LoadBalancer &#183; secrets operator</text>
<text x="78" y="350" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">&#8594; scoped Role + token, nothing more</text>
<path d="M150 286 V246" fill="none" stroke="#2e8b57" stroke-width="1.6" stroke-linecap="round"></path>
<path d="M150 286 V246" fill="none" stroke="#c026d3" stroke-width="1.6" stroke-linecap="round"></path>
</svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+10 -4
View File
@@ -1,11 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="JW">
<defs>
<linearGradient id="jw" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#3fbaf5"/>
<stop offset="1" stop-color="#e879f9"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="#090c14"/>
<rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="#2dd4bf" stroke-opacity="0.5"/>
<rect x="0.5" y="0.5" width="63" height="63" rx="13.5" fill="none" stroke="url(#jw)" stroke-opacity="0.6"/>
<text x="32" y="33" font-family="ui-monospace, Menlo, Consolas, monospace" font-size="22"
font-weight="700" fill="#5eead4" text-anchor="middle" dominant-baseline="central"
font-weight="700" fill="url(#jw)" text-anchor="middle" dominant-baseline="central"
letter-spacing="1">jw</text>
<rect x="18" y="46" width="22" height="3" rx="1.5" fill="#2dd4bf"/>
<rect x="43" y="46" width="6" height="3" rx="1.5" fill="#2dd4bf">
<rect x="18" y="46" width="22" height="3" rx="1.5" fill="url(#jw)"/>
<rect x="43" y="46" width="6" height="3" rx="1.5" fill="#e879f9">
<animate attributeName="opacity" values="1;0;1" dur="1.1s" repeatCount="indefinite"/>
</rect>
</svg>

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 943 B

+93
View File
@@ -0,0 +1,93 @@
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 120 KiB

+50 -21
View File
@@ -12,6 +12,11 @@
document.documentElement.dataset.theme = "dark";
}
// Mark JS as live BEFORE first paint. CSS only hides the pre-reveal animation
// state under `html.js`, so with JS off every element stays visible (the
// Motion layer in /_astro reveals them once it runs). Progressive enhancement.
document.documentElement.classList.add("js");
function onReady(fn) {
if (document.readyState !== "loading") fn();
else document.addEventListener("DOMContentLoaded", fn);
@@ -33,27 +38,51 @@
});
}
// --- Reveal on scroll (progressive enhancement, motion-aware) ----------
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var targets = document.querySelectorAll("[data-reveal]");
if (!reduce && "IntersectionObserver" in window) {
targets.forEach(function (el) {
el.classList.add("reveal");
});
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
);
targets.forEach(function (el) {
io.observe(el);
});
// --- Hero typing loop (fleet facts) -------------------------------------
// Facts arrive via a data attribute (no inline JS → script-src 'self' holds).
// Reduced motion or JS-off: the first fact renders statically from markup.
var factBox = document.getElementById("fleet-fact");
var factText = document.getElementById("fleet-fact-text");
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (factBox && factText && !reduced) {
var facts;
try {
facts = JSON.parse(factBox.dataset.facts || "[]");
} catch (e) {
facts = [];
}
if (facts.length > 1) {
var fi = 0; // current fact
var ci = facts[0].length; // start fully typed (matches SSR markup)
var deleting = false;
var tick = function () {
var fact = facts[fi];
if (!deleting && ci === fact.length) {
// hold at end of fact, then start the next
deleting = true;
setTimeout(tick, 3200);
return;
}
if (deleting) {
// swap instantly to the next fact (clean cut, like a terminal)
deleting = false;
fi = (fi + 1) % facts.length;
ci = 0;
factText.textContent = "";
setTimeout(tick, 260);
return;
}
ci++;
factText.textContent = facts[fi].slice(0, ci);
setTimeout(tick, 45);
};
setTimeout(tick, 3200);
}
}
// Scroll reveals, hero entrance, backdrop parallax, scroll-progress bar,
// nav underline and button springs all live in the Motion layer
// (src/scripts/anim.ts → bundled /_astro/*.js). This file owns only the
// pre-paint theme + the typing loop so it stays a tiny blocking <head> script.
});
})();
+1 -1
View File
@@ -31,7 +31,7 @@ retry() {
}
echo ">> building ${REF}:${TAG}"
retry buildah build --retry 3 --retry-delay 5s --layers -t "${REF}:${TAG}" -t "${REF}:latest" .
retry buildah build --retry 3 --retry-delay 5s --layers --build-arg GIT_SHA="${TAG}" -t "${REF}:${TAG}" -t "${REF}:latest" .
if [[ "${1:-}" == "push" ]]; then
echo ">> pushing ${REF}:${TAG}"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env node
// Strip a corner watermark (the generator's four-point sparkle) from a cover/hero.
// Method per the astro-static-site skill: feathered disc mask at a HARD-CODED centre,
// seed with the border mean, then Jacobi-diffuse so the patch blends into the surround.
// Deliberately NOT brightness-auto-detected - that locks onto neon edges and highlights.
//
// node scripts/dewatermark.mjs <in> <out> [cx] [cy] [r] [iters]
//
// Default centre is the usual mark position on a 1600x1074 render.
import sharp from "sharp";
const [, , inPath, outPath, cxA, cyA, rA, itA] = process.argv;
if (!inPath || !outPath) {
console.error("usage: dewatermark.mjs <in> <out> [cx] [cy] [r] [iters]");
process.exit(1);
}
const R = Number(rA ?? 46);
const ITERS = Number(itA ?? 900);
const img = sharp(inPath);
const { width: W, height: H } = await img.metadata();
const CX = Number(cxA ?? 1451);
const CY = Number(cyA ?? 922);
const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
const CH = info.channels;
// feathered disc: 1 inside, 0 outside, smooth over the last 6px so the seam disappears
const mask = new Float32Array(W * H);
const x0 = Math.max(0, CX - R - 4), x1 = Math.min(W - 1, CX + R + 4);
const y0 = Math.max(0, CY - R - 4), y1 = Math.min(H - 1, CY + R + 4);
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
const d = Math.hypot(x - CX, y - CY);
mask[y * W + x] = d <= R - 6 ? 1 : d >= R ? 0 : (R - d) / 6;
}
}
// seed every masked pixel with the mean colour of the ring just outside the mask
const ring = [];
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
const d = Math.hypot(x - CX, y - CY);
if (d > R && d <= R + 4) ring.push((y * W + x) * CH);
}
}
const seed = [0, 0, 0];
for (const o of ring) for (let c = 0; c < 3; c++) seed[c] += data[o + c];
for (let c = 0; c < 3; c++) seed[c] = ring.length ? seed[c] / ring.length : 0;
const work = new Float32Array(W * H * 3);
for (let i = 0; i < W * H; i++) {
for (let c = 0; c < 3; c++) {
work[i * 3 + c] = mask[i] > 0 ? seed[c] : data[i * CH + c];
}
}
// Jacobi diffusion: each masked pixel relaxes toward its 4-neighbour average,
// known pixels stay pinned, so the fill inherits the surrounding gradient.
const next = new Float32Array(work);
for (let it = 0; it < ITERS; it++) {
for (let y = Math.max(1, y0); y <= Math.min(H - 2, y1); y++) {
for (let x = Math.max(1, x0); x <= Math.min(W - 2, x1); x++) {
const i = y * W + x;
if (mask[i] <= 0) continue;
for (let c = 0; c < 3; c++) {
next[i * 3 + c] =
(work[(i - 1) * 3 + c] + work[(i + 1) * 3 + c] +
work[(i - W) * 3 + c] + work[(i + W) * 3 + c]) / 4;
}
}
}
work.set(next);
}
// composite back through the feather so the edge of the patch is invisible
const out = Buffer.from(data);
for (let y = y0; y <= y1; y++) {
for (let x = x0; x <= x1; x++) {
const i = y * W + x, m = mask[i];
if (m <= 0) continue;
for (let c = 0; c < 3; c++) {
const o = i * CH + c;
out[o] = Math.round(data[o] * (1 - m) + work[i * 3 + c] * m);
}
}
}
await sharp(out, { raw: { width: W, height: H, channels: CH } })
.webp({ quality: 85 })
.toFile(outPath);
console.log(`dewatermarked ${inPath} -> ${outPath} (centre ${CX},${CY} r=${R}, ${ITERS} iters)`);
+22 -11
View File
@@ -12,26 +12,37 @@ const H = 630;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<defs>
<linearGradient id="title" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#e7edf4"/>
<stop offset="1" stop-color="#7fdcc9"/>
<!-- Electric cyan->violet->magenta — mirrors the live hero nameplate. -->
<linearGradient id="title" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#3fbaf5"/>
<stop offset="0.5" stop-color="#9a7cf2"/>
<stop offset="1" stop-color="#e879f9"/>
</linearGradient>
<radialGradient id="glow" cx="18%" cy="0%" r="80%">
<stop offset="0" stop-color="#2dd4bf" stop-opacity="0.22"/>
<stop offset="60%" stop-color="#2dd4bf" stop-opacity="0"/>
<linearGradient id="bar" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="#3fbaf5"/>
<stop offset="1" stop-color="#e879f9"/>
</linearGradient>
<radialGradient id="glow" cx="14%" cy="0%" r="85%">
<stop offset="0" stop-color="#3fbaf5" stop-opacity="0.20"/>
<stop offset="55%" stop-color="#3fbaf5" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glow2" cx="92%" cy="100%" r="70%">
<stop offset="0" stop-color="#e879f9" stop-opacity="0.16"/>
<stop offset="60%" stop-color="#e879f9" stop-opacity="0"/>
</radialGradient>
<pattern id="grid" width="44" height="44" patternUnits="userSpaceOnUse">
<path d="M44 0H0V44" fill="none" stroke="#94a3b8" stroke-opacity="0.06" stroke-width="1"/>
<path d="M44 0H0V44" fill="none" stroke="#3fbaf5" stroke-opacity="0.05" stroke-width="1"/>
</pattern>
</defs>
<rect width="${W}" height="${H}" fill="#090c14"/>
<rect width="${W}" height="${H}" fill="url(#grid)"/>
<rect width="${W}" height="${H}" fill="url(#glow)"/>
<rect x="0" y="0" width="${W}" height="6" fill="#2dd4bf"/>
<rect width="${W}" height="${H}" fill="url(#glow2)"/>
<rect x="0" y="0" width="${W}" height="6" fill="url(#bar)"/>
<text x="80" y="150" font-family="monospace" font-size="24" letter-spacing="4"
fill="#5eead4">~/ PLATFORM &amp; INFRASTRUCTURE ENGINEER</text>
fill="#3fbaf5">~/ PLATFORM &amp; INFRASTRUCTURE ENGINEER</text>
<text x="78" y="300" font-family="sans-serif" font-size="118" font-weight="700"
letter-spacing="-3" fill="url(#title)">Jonathon Wright</text>
@@ -42,9 +53,9 @@ const svg = `
<text x="80" y="434" font-family="C059, serif" font-style="italic" font-size="35"
fill="#aeb9c9">&#183; GPU-backed edge systems</text>
<text x="80" y="560" font-family="monospace" font-size="20" fill="#8aa0b8">RHEL <tspan fill="#5eead4">&#9656;</tspan> Kubernetes <tspan fill="#5eead4">&#9656;</tspan> GitOps <tspan fill="#5eead4">&#9656;</tspan> Ansible <tspan fill="#5eead4">&#9656;</tspan> Observability <tspan fill="#5eead4">&#9656;</tspan> Edge AI</text>
<text x="80" y="560" font-family="monospace" font-size="20" fill="#8aa0b8">RHEL <tspan fill="#3fbaf5">&#9656;</tspan> Kubernetes <tspan fill="#3fbaf5">&#9656;</tspan> GitOps <tspan fill="#3fbaf5">&#9656;</tspan> Ansible <tspan fill="#3fbaf5">&#9656;</tspan> Observability <tspan fill="#3fbaf5">&#9656;</tspan> Edge AI</text>
<text x="${W - 80}" y="560" text-anchor="end" font-family="monospace" font-size="22"
fill="#5eead4">www.bztmon.com</text>
fill="#e879f9">www.bztmon.com</text>
</svg>`;
await sharp(Buffer.from(svg)).png().toFile(out);
+42
View File
@@ -0,0 +1,42 @@
import { chromium } from "playwright";
import { preview } from "astro";
const server = await preview({ server: { port: 4399 } });
const base = "http://localhost:4399";
const browser = await chromium.launch();
async function shot(name, { theme = "dark", scrollY = 0, selector = null, path = "/", fullPage = false } = {}) {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 2,
});
await page.addInitScript((t) => {
try { localStorage.setItem("theme", t); } catch {}
}, theme);
await page.goto(base + path, { waitUntil: "networkidle" });
await page.evaluate(
(t) => document.documentElement.setAttribute("data-theme", t),
theme
);
if (scrollY) {
await page.evaluate((y) => window.scrollTo(0, y), scrollY);
await page.waitForTimeout(400);
}
await page.waitForTimeout(2700);
if (selector) {
const el = await page.$(selector);
await el.scrollIntoViewIfNeeded();
await page.waitForTimeout(2700);
await el.screenshot({ path: `/tmp/site-${name}.png` });
} else {
await page.screenshot({ path: `/tmp/site-${name}.png`, fullPage });
}
await page.close();
console.log("wrote /tmp/site-" + name + ".png");
}
await shot("light-hero", { theme: "light" });
await shot("dark-hero", { theme: "dark" });
await browser.close();
await server.stop();
+3 -3
View File
@@ -23,7 +23,7 @@ const olderCount = experience.length - recent.length;
</div>
</dl>
<h3 class="about__sub mono"># education</h3>
<h3 class="about__sub mono"><span class="about__hash">#</span> education</h3>
<ul class="about__education">
{education.map((e) => <li>{e}</li>)}
</ul>
@@ -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>
@@ -100,7 +100,7 @@ const olderCount = experience.length - recent.length;
font-size: var(--step--1);
letter-spacing: 0.02em;
}
.about__sub::first-letter {
.about__hash {
color: var(--accent);
}
.about__education {
+47
View File
@@ -0,0 +1,47 @@
---
// Circuit divider - gradient hairlines meeting two rotated-square "diamonds"
// (cyan, magenta). Pure CSS, from the design handoff.
---
<div class="divider container" aria-hidden="true">
<div class="divider__line divider__line--in"></div>
<div class="divider__diamond divider__diamond--cyan"></div>
<div class="divider__line divider__line--mid"></div>
<div class="divider__diamond divider__diamond--magenta"></div>
<div class="divider__line divider__line--out"></div>
</div>
<style>
.divider {
display: flex;
align-items: center;
}
.divider__line {
height: 1px;
}
.divider__line--in {
flex: 1;
background: linear-gradient(90deg, transparent, rgba(63, 186, 245, 0.5));
}
.divider__line--mid {
width: 120px;
background: rgba(63, 186, 245, 0.35);
}
.divider__line--out {
flex: 1;
background: linear-gradient(90deg, rgba(232, 121, 249, 0.5), transparent);
}
.divider__diamond {
width: 6px;
height: 6px;
transform: rotate(45deg);
margin: 0 10px;
flex-shrink: 0;
}
.divider__diamond--cyan {
border: 1px solid var(--accent);
}
.divider__diamond--magenta {
border: 1px solid var(--accent-2);
}
</style>
+266
View File
@@ -0,0 +1,266 @@
---
// 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)) -
// 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).
---
<svg
class="constellation"
viewBox="0 0 760 640"
role="img"
aria-label="Live topology of the six-node homelab fleet: hub cave reconciling alfred, robin, kate, wgirl and elfastc"
>
<defs>
<linearGradient id="lgA" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="var(--accent)"></stop>
<stop offset="1" stop-color="var(--accent-2)"></stop>
</linearGradient>
</defs>
<!-- ── PCB traces: dual parallel lines per route (out = cyan, back = magenta) ── -->
<!-- alfred -->
<g transform="translate(2,-3)">
<path class="t-glow t-cyan" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-base t-cyan" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-pkt pkt-fwd" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
</g>
<g transform="translate(-2,3)">
<path class="t-glow t-mag" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-base t-mag" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
<path class="t-pkt t-pkt--mag pkt-rev-slow" d="M380 310 H460 L520 250 V180 L572 128 H595"></path>
</g>
<!-- robin -->
<g transform="translate(2,-3)">
<path class="t-glow t-cyan" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-base t-cyan" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-pkt pkt-fwd-slow" d="M380 310 H470 L540 380 H602 L640 420"></path>
</g>
<g transform="translate(-2,3)">
<path class="t-glow t-mag" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-base t-mag" d="M380 310 H470 L540 380 H602 L640 420"></path>
<path class="t-pkt t-pkt--mag pkt-rev" d="M380 310 H470 L540 380 H602 L640 420"></path>
</g>
<!-- 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>
<path class="t-pkt t-pkt--solid pkt-fwd-slow" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
</g>
<g transform="translate(-2,3)" class="standby">
<path class="t-glow t-mag" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-base t-mag" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
<path class="t-pkt t-pkt--mag t-pkt--solid pkt-rev" d="M380 310 H300 L240 250 V202 L188 150 H176"></path>
</g>
<!-- wgirl (standby) -->
<g transform="translate(2,-3)" class="standby">
<path class="t-glow t-cyan" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-base t-cyan" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-pkt t-pkt--solid pkt-fwd" d="M380 310 H310 L244 376 V446 L214 474"></path>
</g>
<g transform="translate(-2,3)" class="standby">
<path class="t-glow t-mag" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-base t-mag" d="M380 310 H310 L244 376 V446 L214 474"></path>
<path class="t-pkt t-pkt--mag t-pkt--solid pkt-rev-slow" d="M380 310 H310 L244 376 V446 L214 474"></path>
</g>
<!-- elfastc (standby) -->
<g transform="translate(3,2)" class="standby">
<path class="t-glow t-cyan" d="M380 310 V400 L452 472 V532"></path>
<path class="t-base t-cyan" d="M380 310 V400 L452 472 V532"></path>
<path class="t-pkt t-pkt--solid pkt-fwd-slow" d="M380 310 V400 L452 472 V532"></path>
</g>
<g transform="translate(-3,-2)" class="standby">
<path class="t-glow t-mag" d="M380 310 V400 L452 472 V532"></path>
<path class="t-base t-mag" d="M380 310 V400 L452 472 V532"></path>
<path class="t-pkt t-pkt--mag t-pkt--solid pkt-rev" d="M380 310 V400 L452 472 V532"></path>
</g>
<!-- vias at trace bends -->
<circle class="via via--cyan" cx="460" cy="310" r="3"></circle>
<circle class="via via--cyan" cx="520" cy="250" r="3"></circle>
<circle class="via via--cyan" cx="520" cy="180" r="3"></circle>
<circle class="via via--mag" cx="470" cy="310" r="3"></circle>
<circle class="via via--mag" cx="540" cy="380" r="3"></circle>
<circle class="via via--dim" cx="300" cy="310" r="3"></circle>
<circle class="via via--dim" cx="240" cy="250" r="3"></circle>
<circle class="via via--dim" cx="310" cy="310" r="3"></circle>
<circle class="via via--dim" cx="244" cy="376" r="3"></circle>
<circle class="via via--dim" cx="380" cy="400" r="3"></circle>
<circle class="via via--dim" cx="452" cy="472" r="3"></circle>
<!-- ── hub: cave ── -->
<circle class="ring" cx="380" cy="310" r="46" fill="none" stroke="url(#lgA)" stroke-width="1.5"></circle>
<circle cx="380" cy="310" r="46" fill="rgba(63,186,245,0.05)" stroke="rgba(63,186,245,0.3)" stroke-width="1"></circle>
<!-- pin stubs -->
<g class="pins-cyan">
<line x1="352" y1="296" x2="340" y2="296"></line><line x1="352" y1="310" x2="340" y2="310"></line>
<line x1="352" y1="324" x2="340" y2="324"></line><line x1="366" y1="282" x2="366" y2="270"></line>
<line x1="380" y1="282" x2="380" y2="270"></line><line x1="366" y1="338" x2="366" y2="350"></line>
<line x1="380" y1="338" x2="380" y2="350"></line>
</g>
<g class="pins-mag">
<line x1="408" y1="296" x2="420" y2="296"></line><line x1="408" y1="310" x2="420" y2="310"></line>
<line x1="408" y1="324" x2="420" y2="324"></line><line x1="394" y1="282" x2="394" y2="270"></line>
<line x1="394" y1="338" x2="394" y2="350"></line>
</g>
<rect class="chip chip--hub" x="352" y="282" width="56" height="56" rx="8" stroke="url(#lgA)"></rect>
<rect x="364" y="294" width="32" height="32" rx="4" fill="none" stroke="rgba(63,186,245,0.4)" stroke-width="1"></rect>
<circle class="core core--cyan pulse" cx="380" cy="310" r="5"></circle>
<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) ── -->
<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>
<rect class="plate plate--intel" x="576" y="152" width="72" height="20" rx="3"></rect>
<text class="lbl" x="612" y="166" text-anchor="middle">alfred</text>
<!-- ── awake: robin ── -->
<circle cx="656" cy="420" r="24" fill="rgba(232,121,249,0.07)"></circle>
<rect class="chip chip--mag" x="639" y="403" width="34" height="34" rx="5"></rect>
<circle class="core core--mag pulse-slow" cx="656" cy="420" r="4"></circle>
<rect class="plate plate--mag" x="620" y="444" width="72" height="20" rx="3"></rect>
<text class="lbl" x="656" y="458" text-anchor="middle">robin</text>
<!-- ── standby: kate ── -->
<rect class="chip chip--cyan" x="145" y="135" width="30" height="30" rx="5"></rect>
<circle class="core core--cyan pulse-slow" cx="160" cy="150" r="3.5"></circle>
<rect class="plate plate--dim" x="126" y="172" width="68" height="20" rx="3"></rect>
<text class="lbl lbl--dim" x="160" y="186" text-anchor="middle">kate</text>
<!-- ── standby: wgirl ── -->
<rect class="chip chip--mag" x="199" y="471" width="30" height="30" rx="5"></rect>
<circle class="core core--mag pulse" cx="214" cy="486" r="3.5"></circle>
<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) ── -->
<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) ── -->
<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>
<rect class="core--intel" x="620" y="592" width="8" height="8" rx="2"></rect>
<text class="cap cap--key" x="634" y="600">intel compute</text>
<rect class="key--bat" x="620" y="612" width="8" height="8" rx="2"></rect>
<text class="cap cap--key" x="634" y="620">core / services</text>
</g>
</svg>
<style>
.constellation {
display: block;
width: 100%;
height: auto;
}
/* trace layers */
.t-glow { fill: none; stroke-width: 7; }
.t-glow.t-cyan { stroke: rgba(63, 186, 245, 0.12); }
.t-glow.t-mag { stroke: rgba(232, 121, 249, 0.1); }
.standby .t-glow.t-cyan { stroke: rgba(63, 186, 245, 0.1); }
.standby .t-glow.t-mag { stroke: rgba(232, 121, 249, 0.08); }
.t-base { fill: none; stroke-width: 2; }
.t-base.t-cyan { stroke: rgba(63, 186, 245, 0.45); }
.t-base.t-mag { stroke: rgba(232, 121, 249, 0.38); }
.standby .t-base.t-cyan { stroke: rgba(63, 186, 245, 0.35); }
.standby .t-base.t-mag { stroke: rgba(232, 121, 249, 0.3); }
/* the moving packets */
.t-pkt {
fill: none;
stroke: url(#lgA);
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 9 127;
filter: drop-shadow(0 0 6px rgba(63, 186, 245, 0.9));
}
.t-pkt--mag { filter: drop-shadow(0 0 6px rgba(232, 121, 249, 0.9)); }
.t-pkt--solid { stroke: rgba(63, 186, 245, 0.85); }
.t-pkt--solid.t-pkt--mag { stroke: rgba(232, 121, 249, 0.85); }
.pkt-fwd { animation: bzDash 8s linear infinite; }
.pkt-fwd-slow { animation: bzDash 11s linear infinite; }
.pkt-rev { animation: bzDashRev 9s linear infinite; }
.pkt-rev-slow { animation: bzDashRev 13s linear infinite; }
/* vias */
.via { fill: var(--panel-solid); stroke-width: 1; }
.via--cyan { stroke: rgba(63, 186, 245, 0.6); }
.via--mag { stroke: rgba(232, 121, 249, 0.6); }
.via--dim { stroke: rgba(126, 139, 161, 0.5); }
/* chips + cores + plates + labels */
.chip { fill: var(--panel-solid); }
.chip--hub { stroke-width: 2; filter: drop-shadow(0 0 10px rgba(63, 186, 245, 0.45)); }
.chip--cyan { stroke: var(--accent); stroke-width: 1.5; filter: drop-shadow(0 0 8px rgba(63, 186, 245, 0.55)); }
.chip--mag { stroke: var(--accent-2); stroke-width: 1.5; filter: drop-shadow(0 0 8px rgba(232, 121, 249, 0.55)); }
.chip--intel { stroke: var(--accent-intel); stroke-width: 1.5; filter: drop-shadow(0 0 8px rgba(0, 199, 253, 0.55)); }
.core--cyan { fill: var(--accent); filter: drop-shadow(0 0 6px rgba(63, 186, 245, 1)); }
.core--mag { fill: var(--accent-2); filter: drop-shadow(0 0 5px rgba(232, 121, 249, 1)); }
.core--intel { fill: var(--accent-intel); filter: drop-shadow(0 0 6px rgba(0, 199, 253, 1)); }
.pulse { animation: bzPulse 2.4s ease-in-out infinite; }
.pulse-slow { animation: bzPulse 3.4s ease-in-out infinite; }
.pins-cyan line { stroke: rgba(63, 186, 245, 0.6); stroke-width: 1.5; }
.pins-mag line { stroke: rgba(232, 121, 249, 0.6); stroke-width: 1.5; }
.plate { fill: rgba(13, 19, 34, 0.92); stroke-width: 1; }
.plate--hub { stroke: rgba(63, 186, 245, 0.4); }
.plate--cyan { stroke: rgba(63, 186, 245, 0.35); }
.plate--mag { stroke: rgba(232, 121, 249, 0.35); }
.plate--intel { stroke: rgba(0, 199, 253, 0.35); }
.plate--dim { stroke: rgba(126, 139, 161, 0.3); }
.lbl {
fill: #e8f2fb;
font-family: var(--font-mono);
font-size: 11px;
}
.lbl--hub { font-size: 12px; font-weight: 700; }
.lbl--dim { fill: #9fb0c8; }
.cap { font-family: var(--font-mono); }
.cap--hub {
fill: var(--accent);
font-size: 9.5px;
filter: drop-shadow(0 0 4px rgba(63, 186, 245, 0.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 */
.key--bat {
fill: #10131c;
stroke: rgba(245, 197, 24, 0.55);
stroke-width: 1;
filter: drop-shadow(0 0 4px rgba(245, 197, 24, 0.35));
}
/* radar ring off the hub */
.ring {
transform-box: fill-box;
transform-origin: center;
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
(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; } }
@keyframes bzPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
@keyframes bzRing {
/* start AND end at opacity 0 so the 3.6s restart never pops */
0% { transform: scale(0.5); opacity: 0; }
10% { opacity: 0.9; }
100% { transform: scale(1.7); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.pkt-fwd, .pkt-fwd-slow, .pkt-rev, .pkt-rev-slow, .pulse, .pulse-slow, .ring {
animation: none;
}
.ring { opacity: 0.5; }
}
</style>
+7 -1
View File
@@ -1,4 +1,5 @@
---
import { cvAvailable } from "../lib/assets";
import { site } from "../data/site";
import { socials } from "../data/socials";
---
@@ -7,12 +8,17 @@ 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}`}>
{site.email}
</a>
{cvAvailable && (
<a class="btn contact__cv" href="/cv.pdf" download>
Download CV (PDF)
</a>
)}
</div>
<ul class="contact__links">
+4 -4
View File
@@ -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;
@@ -10,11 +10,11 @@ interface Props {
const { name, caption } = Astro.props;
const captions: Record<string, string> = {
"edge-ai": "Design → single-press pipeline → readiness-gated GPU inference at the edge",
"edge-ai": "Design → single-touch pipeline → readiness-gated GPU inference at the edge",
"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];
---
@@ -22,7 +22,7 @@ const cap = caption ?? captions[name];
<figure class="diagram" data-reveal>
<img
class="diagram__svg"
src={`/diagrams/${name}.svg`}
src={`/diagrams/${name}.svg?v=2`}
alt={cap ?? `${name} architecture diagram`}
loading="lazy"
decoding="async"
+135
View File
@@ -0,0 +1,135 @@
---
// House-style mini-diagram for a dossier card thumbnail. Keyed by the project's
// `diagram` frontmatter; each motif is a tiny node/line sketch in the board palette.
// Pure SVG, pulsing cores only (reduced-motion turns them off).
interface Props {
kind?: string;
}
const { kind = "generic" } = Astro.props;
---
<svg viewBox="0 0 400 180" aria-hidden="true" class="thumb">
{kind === "edge-ai" ? (
<>
{/* GPU chip feeding a rack of stores */}
<line x1="70" y1="90" x2="330" y2="50" class="ln ln--c" />
<line x1="70" y1="90" x2="330" y2="90" class="ln ln--c" />
<line x1="70" y1="90" x2="330" y2="130" class="ln ln--m" />
<rect x="46" y="66" width="48" height="48" rx="6" class="nd nd--c" />
<text x="70" y="95" text-anchor="middle" class="tx">GPU</text>
<rect x="314" y="36" width="32" height="28" rx="4" class="nd nd--dim" />
<rect x="314" y="76" width="32" height="28" rx="4" class="nd nd--dim" />
<rect x="314" y="116" width="32" height="28" rx="4" class="nd nd--m" />
<circle cx="200" cy="70" r="2.5" class="dot dot--c pulse" />
<circle cx="200" cy="110" r="2.5" class="dot dot--m pulse-slow" />
</>
) : kind === "homelab" ? (
<>
{/* 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" />
<line x1="200" y1="90" x2="310" y2="135" class="ln ln--m" />
<circle cx="200" cy="90" r="24" class="hub" />
<circle cx="200" cy="90" r="3" class="dot dot--c pulse" />
<circle cx="90" cy="45" r="12" class="nd nd--c" />
<circle cx="90" cy="135" r="12" class="nd nd--c" />
<circle cx="310" cy="45" r="12" class="nd nd--m" />
<circle cx="310" cy="135" r="12" class="nd nd--m" />
</>
) : kind === "gpu" ? (
<>
{/* GPU die with passthrough lanes feeding two edge nodes */}
<rect x="150" y="55" width="100" height="70" rx="6" class="nd nd--c" />
<line x1="164" y1="55" x2="164" y2="40" class="ln ln--c" />
<line x1="186" y1="55" x2="186" y2="40" class="ln ln--c" />
<line x1="208" y1="55" x2="208" y2="40" class="ln ln--m" />
<line x1="230" y1="55" x2="230" y2="40" class="ln ln--m" />
<rect x="172" y="72" width="56" height="36" rx="3" fill="none" stroke="rgba(63,186,245,0.4)" stroke-width="1" />
<text x="200" y="94" text-anchor="middle" class="tx">GPU</text>
<line x1="250" y1="70" x2="330" y2="55" class="ln ln--c" />
<line x1="250" y1="110" x2="330" y2="125" class="ln ln--m" />
<rect x="330" y="40" width="30" height="26" rx="4" class="nd nd--dim" />
<rect x="330" y="112" width="30" height="26" rx="4" class="nd nd--m" />
<circle cx="290" cy="62" r="2.5" class="dot dot--c pulse" />
<circle cx="290" cy="117" r="2.5" class="dot dot--m pulse-slow" />
</>
) : kind === "globe" ? (
<>
{/* 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" />
<line x1="148" y1="90" x2="252" y2="90" stroke="rgba(63,186,245,0.25)" stroke-width="1" />
<circle cx="176" cy="62" r="4" class="nd nd--c" />
<circle cx="228" cy="66" r="4" class="nd nd--m" />
<circle cx="182" cy="118" r="4" class="nd nd--dim" />
<circle cx="222" cy="114" r="4" class="nd nd--c" />
<circle cx="200" cy="90" r="3" class="dot dot--c pulse" />
<line x1="176" y1="62" x2="200" y2="90" class="ln ln--c" />
<line x1="228" y1="66" x2="200" y2="90" class="ln ln--m" />
<line x1="182" y1="118" x2="200" y2="90" class="ln ln--c" />
<line x1="222" y1="114" x2="200" y2="90" class="ln ln--m" />
</>
) : kind === "network-fleet" ? (
<>
{/* fleet of switches, single-pane managed */}
<rect x="170" y="30" width="60" height="24" rx="4" class="nd nd--c" />
<text x="200" y="47" text-anchor="middle" class="tx">NCM</text>
<line x1="200" y1="54" x2="200" y2="70" class="ln ln--c" />
<line x1="80" y1="70" x2="320" y2="70" class="ln ln--c" />
<line x1="100" y1="70" x2="100" y2="120" class="ln ln--dim" />
<line x1="160" y1="70" x2="160" y2="120" class="ln ln--dim" />
<line x1="240" y1="70" x2="240" y2="120" class="ln ln--m" />
<line x1="300" y1="70" x2="300" y2="120" class="ln ln--m" />
<rect x="82" y="120" width="36" height="18" rx="3" class="nd nd--dim" />
<rect x="142" y="120" width="36" height="18" rx="3" class="nd nd--dim" />
<rect x="222" y="120" width="36" height="18" rx="3" class="nd nd--m" />
<rect x="282" y="120" width="36" height="18" rx="3" class="nd nd--m" />
<circle cx="100" cy="70" r="2.5" class="dot dot--c pulse" />
<circle cx="300" cy="70" r="2.5" class="dot dot--m pulse-slow" />
</>
) : (
<>
{/* 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" />
<rect x="272" y="70" width="40" height="40" rx="5" class="nd nd--m" />
<circle cx="60" cy="90" r="2.5" class="dot dot--c pulse" />
<circle cx="156" cy="90" r="2.5" class="dot dot--c pulse-slow" />
<circle cx="248" cy="90" r="2.5" class="dot dot--m pulse" />
<circle cx="340" cy="90" r="2.5" class="dot dot--m pulse-slow" />
</>
)}
</svg>
<style>
.thumb {
width: 100%;
height: 100%;
display: block;
}
.ln {
stroke-width: 1;
}
.ln--c { stroke: rgba(63, 186, 245, 0.3); }
.ln--m { stroke: rgba(232, 121, 249, 0.3); }
.nd { fill: var(--panel-solid); stroke-width: 1.5; }
.nd--c { stroke: var(--accent); }
.nd--m { stroke: rgba(232, 121, 249, 0.5); stroke-width: 1; }
.nd--dim { stroke: rgba(126, 139, 161, 0.5); stroke-width: 1; }
.hub { fill: rgba(63, 186, 245, 0.06); stroke: var(--accent); stroke-width: 1.5; }
.tx { fill: var(--accent); font-family: var(--font-mono); font-size: 10px; }
.dot--c { fill: var(--accent); }
.dot--m { fill: var(--accent-2); }
.pulse { animation: thumbPulse 2.4s ease-in-out infinite; }
.pulse-slow { animation: thumbPulse 3.4s ease-in-out infinite; }
@keyframes thumbPulse {
0%, 100% { opacity: 0.35; }
50% { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.pulse, .pulse-slow { animation: none; }
}
</style>
+220 -75
View File
@@ -1,120 +1,265 @@
---
import { site } from "../data/site";
import { socials } from "../data/socials";
import { buildInfo } from "../lib/build-info";
const year = 2026; // build-stamped; bump via the build, not a runtime Date()
const linkedinHref = socials.find((s) => s.label === "LinkedIn")?.href;
const emailHref = socials.find((s) => s.label === "Email")?.href;
---
<!-- One terminal session = the whole footer. Every value baked at build time, zero runtime calls. -->
<footer class="footer">
<div class="container footer__inner">
<div>
<p class="mono footer__name">{site.name}</p>
<p class="footer__meta">
{site.role} · Served from a homelab Kubernetes cluster over an encrypted tunnel.
</p>
<p class="footer__contact">
For a low-ms reply, 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.
</p>
<div class="container">
<div class="term mono" data-reveal>
<div class="term__bar">
<span class="term__chrome" aria-hidden="true">
<span class="term__led"></span><span class="term__led"></span><span class="term__led term__led--lit"></span>
</span>
<span class="term__title">jonny@bztmon:~ · session receipt</span>
<span class="term__health"><span class="term__ok">✓</span> Synced · Healthy</span>
</div>
<div class="term__body">
<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}
</p>
<p class="term__line"><span class="term__prompt">$</span> traceroute www.bztmon.com</p>
<p class="term__out">
served from a homelab Kubernetes cluster, over an encrypted tunnel
</p>
<p class="term__line"><span class="term__prompt">$</span> git log -1 --format=receipt</p>
<p class="term__out">
HEAD <span class="term__val">{buildInfo.sha}</span>
<span class="term__sep">·</span> built <span class="term__val">{buildInfo.builtAt}</span>
<span class="term__sep">·</span> reconciled by <span class="term__val">Argo CD</span>
</p>
<p class="term__line"><span class="term__prompt">$</span> contact</p>
<p class="term__out">
<a href={linkedinHref} rel="noopener noreferrer" target="_blank">LinkedIn</a>{" "}
or <a href={emailHref}>email</a>
</p>
<p class="term__line" aria-hidden="true">
<span class="term__prompt">$</span> <span class="term__cursor"></span>
</p>
</div>
<div class="term__statusbar">
<span>© {year} {site.name}</span>
<span class="term__sdot" aria-hidden="true">·</span>
<a
href={site.license.url}
rel="license noopener noreferrer"
target="_blank"
title={site.license.label}
>
{site.license.name}
</a>
<span class="term__sdot" aria-hidden="true">·</span>
<span>built with Astro, shipped via GitOps</span>
<span class="term__spring" aria-hidden="true"></span>
<span class="term__socials">
{
socials.map((s) => (
<a
href={s.href}
rel={s.external ? "noopener noreferrer" : undefined}
aria-label={s.label}
title={s.label}
>
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path d={s.icon} fill="currentColor" />
</svg>
</a>
))
}
</span>
</div>
</div>
<div class="footer__links">
{
socials.map((s) => (
<a
href={s.href}
rel={s.external ? "noopener noreferrer" : undefined}
aria-label={s.label}
title={s.label}
>
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
<path d={s.icon} fill="currentColor" />
</svg>
</a>
))
}
</div>
</div>
<div class="container footer__base">
<span class="mono">© {year} {site.name}</span>
<span class="mono footer__dot">·</span>
<span class="mono">
Content licensed under{" "}
<a
href={site.license.url}
rel="license noopener noreferrer"
target="_blank"
title={site.license.label}
>
{site.license.name}
</a>
</span>
<span class="mono footer__dot">·</span>
<span class="mono">built with Astro, shipped via GitOps</span>
</div>
</footer>
<style>
.footer {
border-top: 1px solid var(--border);
padding-block: var(--space-7) var(--space-6);
margin-top: var(--space-8);
}
/* ── the terminal panel ─────────────────────────────────────────────── */
.term {
position: relative;
border: 1px solid var(--border-accent);
border-radius: var(--radius);
background: var(--panel);
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
overflow: hidden;
font-size: 12.5px;
letter-spacing: 0.03em;
box-shadow: 0 0 24px -14px var(--accent-glow);
}
/* faint CRT scanlines across the whole session (static - not motion) */
.term::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
background: repeating-linear-gradient(
to bottom,
transparent 0 2px,
color-mix(in srgb, var(--text) 3%, transparent) 2px 3px
);
}
/* ── title bar ──────────────────────────────────────────────────────── */
.term__bar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 8px 14px;
border-bottom: 1px solid var(--border);
color: var(--text-faint);
font-size: 11px;
}
.term__chrome {
display: inline-flex;
gap: 5px;
}
.term__led {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--border-strong);
}
.term__led--lit {
background: var(--accent);
box-shadow: 0 0 6px var(--accent-glow);
}
.term__title {
letter-spacing: 0.08em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.term__health {
margin-left: auto;
white-space: nowrap;
color: var(--text-dim);
}
.footer__inner {
display: flex;
flex-wrap: wrap;
gap: var(--space-5);
justify-content: space-between;
align-items: center;
.term__ok {
color: var(--accent);
}
.footer__name {
/* ── session body ───────────────────────────────────────────────────── */
.term__body {
padding: 14px 16px 10px;
display: grid;
gap: 2px;
}
.term__line {
color: var(--text-dim);
}
.term__line + .term__out {
margin-bottom: 0.55rem;
}
.term__prompt {
color: var(--accent);
margin-right: 0.45rem;
}
.term__out {
color: var(--text-faint);
padding-left: 1.05rem;
max-width: 72ch;
}
.term__name {
color: var(--text);
font-weight: 600;
}
.footer__meta {
font-size: var(--step--1);
color: var(--text-faint);
margin-top: 0.25rem;
.term__sep {
opacity: 0.5;
margin-inline: 0.15rem;
}
.footer__contact {
font-size: var(--step--1);
color: var(--text-faint);
margin-top: 0.4rem;
.term__val {
color: var(--accent-2);
}
.footer__contact a {
.term__out a {
color: var(--text-dim);
text-decoration: underline;
text-underline-offset: 2px;
}
.footer__contact a:hover {
.term__out a:hover {
color: var(--accent);
}
.footer__links {
display: flex;
gap: var(--space-4);
/* blinking block cursor on the empty prompt */
.term__cursor {
display: inline-block;
width: 0.62em;
height: 1.05em;
vertical-align: text-bottom;
background: var(--accent);
animation: term-blink 1.1s steps(2, start) infinite;
}
.footer__links a {
color: var(--text-dim);
@keyframes term-blink {
to {
visibility: hidden;
}
}
.footer__links a:hover {
color: var(--accent);
@media (prefers-reduced-motion: reduce) {
.term__cursor {
animation: none;
}
}
.footer__base {
margin-top: var(--space-5);
font-size: 0.72rem;
color: var(--text-faint);
/* ── status bar (tmux-style) ────────────────────────────────────────── */
.term__statusbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
padding: 8px 14px;
border-top: 1px solid var(--border);
background: color-mix(in srgb, var(--panel) 60%, transparent);
color: var(--text-faint);
font-size: 0.72rem;
}
.footer__base a {
.term__sdot {
opacity: 0.5;
}
.term__statusbar a {
color: var(--text-dim);
text-decoration: underline;
text-underline-offset: 2px;
}
.footer__base a:hover {
.term__statusbar a:hover {
color: var(--accent);
}
.term__spring {
flex: 1;
}
.term__socials {
display: inline-flex;
gap: var(--space-3);
}
.term__socials a {
color: var(--text-dim);
text-decoration: none;
}
.term__socials a:hover {
color: var(--accent);
}
@media (max-width: 560px) {
.term__health {
display: none;
}
.term__spring {
flex-basis: 100%;
}
}
</style>
+249 -84
View File
@@ -1,91 +1,202 @@
---
import { site } from "../data/site";
import { cvAvailable } from "../lib/assets";
import { fleetFacts } from "../data/fleet";
import Constellation from "./Constellation.astro";
---
<section class="hero">
<!-- hero-scoped atmosphere over the site-wide board art: directional scrims,
a static radial glow, and a 56px grid masked to an ellipse -->
<div class="hero__scrim hero__scrim--x" aria-hidden="true"></div>
<div class="hero__scrim hero__scrim--y" aria-hidden="true"></div>
<div class="hero__glow" aria-hidden="true"></div>
<div class="hero__grid" aria-hidden="true"></div>
<div class="container hero__inner">
<p class="eyebrow hero__eyebrow">{site.role}</p>
<div class="hero__copy">
<p class="eyebrow hero__eyebrow">{site.role}</p>
<h1 class="hero__title">
{site.name}
</h1>
<h1 class="hero__title">
{site.name}
</h1>
<p class="hero__positioning">{site.positioning}</p>
<p class="hero__positioning">{site.positioning}</p>
<p class="hero__tags mono" aria-label="Core technologies">
{site.tagline.map((t, i) => (
<Fragment>
{i > 0 && <span class="hero__sep" aria-hidden="true">▸</span>}
<span>{t}</span>
</Fragment>
))}
</p>
<p class="hero__tags mono" aria-label="Core technologies">
{site.tagline.map((t, i) => (
<Fragment>
{i > 0 && <span class="hero__sep" aria-hidden="true">▸</span>}
<span>{t}</span>
</Fragment>
))}
</p>
<div class="hero__cta">
<a class="btn btn--primary" href="/#projects">View Projects</a>
{cvAvailable && (
<a class="btn" href="/cv.pdf" download>
Download CV
</a>
)}
<a class="btn" href="/#contact">Contact</a>
<div class="hero__cta">
<a class="btn btn--primary" href="/#projects">View Projects</a>
{cvAvailable && (
<a class="btn" href="/cv.pdf" download>
Download CV
</a>
)}
<a class="btn" href="/#contact">Contact</a>
</div>
<!-- the typing fleet-fact bar: real numbers, cycled by site.js (external, CSP-safe);
with JS off / reduced motion it shows the first fact statically -->
<div
class="hero__fact mono"
id="fleet-fact"
data-facts={JSON.stringify(fleetFacts)}
>
<span class="hero__fact-prompt">~/{site.handle} ▸</span>
<span class="hero__fact-line"
><span id="fleet-fact-text">{fleetFacts[0]}</span><span
class="hero__caret"
aria-hidden="true">▮</span
></span
>
</div>
</div>
<div class="hero__viz" aria-hidden="false">
<Constellation />
</div>
</div>
<div class="hero__scroll mono" aria-hidden="true">
<span class="hero__diamond"></span>
<span>SCROLL - DESCENDING: EDGE / CLUSTER / WORKLOAD / SECURITY</span>
</div>
</section>
<style>
.hero {
position: relative;
min-height: min(88vh, 760px);
min-height: min(92vh, 820px);
display: flex;
align-items: center;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid var(--border-accent);
overflow: hidden;
}
/* atmosphere layers (scoped to the hero, over the global board art) */
.hero__scrim,
.hero__glow,
.hero__grid {
position: absolute;
inset: 0;
pointer-events: none;
}
.hero__scrim--x {
background: linear-gradient(
90deg,
rgba(9, 12, 20, 0.96) 0%,
rgba(9, 12, 20, 0.88) 42%,
rgba(9, 12, 20, 0.62) 70%,
rgba(9, 12, 20, 0.55) 100%
);
}
.hero__scrim--y {
background: linear-gradient(
180deg,
rgba(9, 12, 20, 0.55) 0%,
transparent 30%,
transparent 70%,
rgba(9, 12, 20, 0.9) 100%
);
}
:global([data-theme="light"]) .hero__scrim--x {
background: linear-gradient(
90deg,
rgba(246, 248, 251, 0.96) 0%,
rgba(246, 248, 251, 0.86) 42%,
rgba(246, 248, 251, 0.6) 70%,
rgba(246, 248, 251, 0.5) 100%
);
}
:global([data-theme="light"]) .hero__scrim--y {
background: linear-gradient(
180deg,
rgba(246, 248, 251, 0.55) 0%,
transparent 30%,
transparent 70%,
rgba(246, 248, 251, 0.92) 100%
);
}
.hero__glow {
background:
radial-gradient(900px 600px at 78% 40%, rgba(232, 121, 249, 0.1), transparent 65%),
radial-gradient(1000px 700px at 15% 25%, rgba(63, 186, 245, 0.12), transparent 60%);
}
.hero__grid {
background-image:
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: radial-gradient(ellipse 80% 70% at 60% 40%, black 30%, transparent 75%);
}
.hero__inner {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 640px) minmax(0, 1fr);
align-items: center;
gap: var(--space-6);
padding-block: var(--space-9);
width: 100%;
}
.hero__copy {
display: flex;
flex-direction: column;
}
.hero__viz {
justify-self: end;
width: min(48vw, 720px);
margin-right: -8%;
}
.hero__eyebrow {
margin-bottom: var(--space-4);
letter-spacing: 0.18em;
text-shadow: 0 0 14px rgba(63, 186, 245, 0.5);
}
.hero__title {
font-family: var(--font-display);
font-size: var(--step-5);
line-height: 1.12;
/* Chakra Petch is geometric/wide — looser tracking than Inter's -0.03em. */
line-height: 1.05;
letter-spacing: -0.005em;
/* Extend the paint box below the baseline so background-clip:text doesn't
crop glyph descenders (the "g" in Wright). */
padding-bottom: 0.14em;
/* Electric kinetic fill: light text with a cyan→magenta band — the same
sweep as the Bat-Computer board — gliding across once on load (heroSheen)
over an electric halo, tying the headline into the backdrop. */
background: linear-gradient(
100deg,
var(--text) 0%,
var(--text) 34%,
var(--accent) 46%,
var(--accent-2) 56%,
var(--text) 66%,
var(--text) 100%
);
background-size: 230% 100%;
background-position: 130% 0;
/* 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:
linear-gradient(
105deg,
transparent 0%,
rgba(255, 255, 255, 0.9) 50%,
transparent 100%
),
linear-gradient(100deg, var(--accent) 0%, #7c3aed 50%, var(--accent-2) 100%);
background-size: 220% 100%, 100% 100%;
background-position: 135% 0, 0 0;
background-repeat: no-repeat;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
filter: drop-shadow(0 0 26px var(--accent-glow));
text-shadow: none;
filter: drop-shadow(0 1px 1px rgba(2, 6, 23, 0.22))
drop-shadow(0 6px 22px var(--accent-glow));
}
.hero__positioning {
margin-top: var(--space-5);
max-width: 42rem;
font-size: var(--step-2);
line-height: 1.35;
color: var(--text);
font-weight: 500;
letter-spacing: -0.01em;
max-width: 32rem;
font-size: var(--step-1);
line-height: 1.55;
color: var(--text-dim);
font-weight: 400;
text-wrap: pretty;
}
.hero__tags {
margin: var(--space-5) 0 0;
@@ -95,7 +206,7 @@ import { cvAvailable } from "../lib/assets";
align-items: center;
gap: 0.4rem 0.6rem;
font-size: var(--step--1);
color: var(--text-dim);
color: var(--text-label);
line-height: 1.7;
}
.hero__sep {
@@ -108,48 +219,102 @@ import { cvAvailable } from "../lib/assets";
gap: var(--space-3);
}
/* Staggered entrance — CSS-only (runs with JS off), motion-aware. */
/* the typing bar - glass panel */
.hero__fact {
margin-top: var(--space-6);
display: flex;
align-items: center;
gap: 10px;
padding: 13px 16px;
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: var(--radius-btn);
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 */
width: max-content;
max-width: min(52rem, calc(100vw - 2 * var(--space-5)));
min-height: 20px;
font-size: 11.5px;
}
.hero__fact-prompt {
color: var(--accent);
flex-shrink: 0;
}
.hero__fact-line {
color: var(--text-body-strong);
white-space: nowrap;
line-height: 1.4;
}
.hero__caret {
color: var(--accent);
animation: bzCaret 1.1s step-end infinite;
}
.hero__scroll {
position: absolute;
left: max(calc((100vw - var(--measure)) / 2), var(--space-5));
bottom: 28px;
display: flex;
align-items: center;
gap: 10px;
font-size: 11px;
color: var(--text-faint);
z-index: 1;
}
.hero__diamond {
width: 8px;
height: 8px;
border: 1px solid var(--accent);
transform: rotate(45deg);
display: block;
}
@keyframes bzCaret {
0%, 49% { opacity: 1; }
50%, 100% { opacity: 0; }
}
@media (prefers-reduced-motion: no-preference) {
.hero__eyebrow,
.hero__title,
.hero__positioning,
.hero__tags,
.hero__cta {
animation: heroIn 0.7s var(--ease) both;
}
/* Entrance + a one-shot electric sheen sweeping across the name. */
.hero__title {
animation:
heroIn 0.7s var(--ease) 0.06s both,
heroSheen 1.6s var(--ease) 0.7s both;
}
.hero__positioning {
animation-delay: 0.14s;
}
.hero__tags {
animation-delay: 0.22s;
}
.hero__cta {
animation-delay: 0.3s;
animation: heroSheen 1.6s var(--ease) 0.7s both;
}
}
@keyframes heroIn {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.hero__caret { animation: none; }
}
/* A bright royal band sweeps left→right across the glyphs, then rests. */
@keyframes heroSheen {
from {
background-position: 130% 0;
from { background-position: 135% 0, 0 0; }
to { background-position: -35% 0, 0 0; }
}
/* responsive: constellation folds under, then hides on small screens */
@media (max-width: 1100px) {
.hero__inner {
grid-template-columns: 1fr;
}
to {
background-position: 0% 0;
.hero__viz {
justify-self: center;
width: min(88vw, 560px);
margin-right: 0;
order: 2;
}
}
@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 -
the desktop one-liner (nowrap + max-content) busts a 390px viewport */
.hero__fact {
font-size: 12px;
width: 100%;
max-width: 100%;
}
.hero__fact-line {
white-space: normal;
overflow-wrap: anywhere;
line-height: 1.5;
}
}
</style>
+49 -2
View File
@@ -2,23 +2,34 @@
import ThemeToggle from "./ThemeToggle.astro";
import { site } from "../data/site";
// Plain links. Hover shows the underline; there is no scroll-spy - tracking the
// active section as you scrolled read as clunky, so the underline is hover-only.
const links = [
{ label: "About", href: "/#about" },
{ label: "Skills", href: "/#skills" },
{ label: "Projects", href: "/projects/" },
{ label: "Blog", href: "/blog/" },
{ label: "Contact", href: "/#contact" },
{ label: "Learn", href: "https://learn.bztmon.com", external: true },
];
---
<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>
<nav class="nav__links" aria-label="Primary">
{links.map((l) => <a href={l.href}>{l.label}</a>)}
{links.map((l) => (
<a
href={l.href}
target={l.external ? "_blank" : undefined}
rel={l.external ? "noopener noreferrer" : undefined}
>
{l.label}
</a>
))}
</nav>
<div class="nav__actions">
@@ -80,12 +91,20 @@ const links = [
font-size: var(--step--1);
}
.nav__links a {
position: relative;
color: var(--text-dim);
}
.nav__links a:hover {
color: var(--text);
text-decoration: none;
}
/* Hover = glow, not an underline (owner call 2026-08-18: the bar read as clunky).
Keyboard focus gets the same treatment so the affordance is not mouse-only. */
.nav__links a:hover,
.nav__links a:focus-visible {
color: var(--accent);
text-shadow: 0 0 10px var(--accent-glow), 0 0 22px var(--accent-glow);
}
.nav__actions {
display: flex;
align-items: center;
@@ -96,4 +115,32 @@ const links = [
display: flex;
}
}
/* 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 {
height: auto;
flex-wrap: wrap;
row-gap: 0;
padding-block: 0.55rem 0.6rem;
}
.nav__links {
display: flex;
order: 3;
width: 100%;
gap: var(--space-4);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
font-size: 0.78rem;
padding-top: 0.5rem;
}
.nav__links::-webkit-scrollbar {
display: none;
}
.nav__links a {
flex-shrink: 0;
padding-bottom: 0.15rem;
}
}
</style>
+55 -12
View File
@@ -2,6 +2,7 @@
import type { CollectionEntry } from "astro:content";
import { formatDate } from "../lib/blog";
import { readingTime } from "../lib/reading";
import { series } from "../data/series";
interface Props {
posts: CollectionEntry<"blog">[];
@@ -14,18 +15,30 @@ const { posts } = Astro.props;
posts.map((post) => (
<li class="post" data-reveal>
<a class="post__link" href={`/blog/${post.id}/`}>
<div class="post__meta mono">
<time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time>
<span class="post__sep">·</span>
<span>{readingTime(post.body)}</span>
{post.data.hero && (
<div class="post__thumb">
<img src={`${post.data.hero}?v=3`} alt="" width="1600" height="1073" loading="lazy" decoding="async" />
</div>
)}
<div class="post__card-body">
<div class="post__meta mono">
{series[post.id] && (
<span class="post__series">
{series[post.id].name} · {String(series[post.id].number).padStart(2, "0")}
</span>
)}
<time datetime={post.data.date.toISOString()}>{formatDate(post.data.date)}</time>
<span class="post__sep">·</span>
<span>{readingTime(post.body)}</span>
</div>
<h3 class="post__title">{post.data.title}</h3>
<p class="post__summary">{post.data.summary}</p>
<ul class="post__tags">
{post.data.tags.map((t) => (
<li class="tag">{t}</li>
))}
</ul>
</div>
<h3 class="post__title">{post.data.title}</h3>
<p class="post__summary">{post.data.summary}</p>
<ul class="post__tags">
{post.data.tags.map((t) => (
<li class="tag">{t}</li>
))}
</ul>
</a>
</li>
))
@@ -38,17 +51,37 @@ const { posts } = Astro.props;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
gap: var(--space-4);
}
.post__link {
display: block;
padding: var(--space-5);
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
color: inherit;
transition: border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.post__thumb {
aspect-ratio: 1600 / 1073;
overflow: hidden;
border-bottom: 1px solid var(--border);
background: var(--surface-2);
}
.post__thumb img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s var(--ease);
}
.post__link:hover .post__thumb img {
transform: scale(1.03);
}
.post__card-body {
padding: var(--space-5);
}
.post__link:hover {
text-decoration: none;
border-color: var(--accent-line);
@@ -58,8 +91,18 @@ const { posts } = Astro.props;
font-size: 0.72rem;
color: var(--text-faint);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.post__series {
color: var(--accent-2);
border: 1px solid var(--border-magenta);
border-radius: 3px;
padding: 2px 7px;
letter-spacing: 0.1em;
font-size: 0.66rem;
}
.post__sep {
opacity: 0.6;
}
+151 -54
View File
@@ -1,89 +1,186 @@
---
// 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";
interface Props {
entry: CollectionEntry<"projects">;
/** 1-based position → the DOSSIER / 00N corner tag. */
index?: number;
}
const { entry } = Astro.props;
const { entry, index = 1 } = Astro.props;
const p = entry.data;
const href = `/projects/${entry.id}/`;
const dossierNo = String(index).padStart(3, "0");
---
<a class="project card" href={href} data-reveal>
<div class="project__top">
<h3 class="project__title">{p.title}</h3>
<span class="project__arrow" aria-hidden="true">→</span>
<a class="dossier" href={href} data-reveal>
<div class="dossier__thumb">
{p.cover
? <img
class="dossier__cover"
src={`/covers/thumb/${p.cover}.webp?v=3`}
alt=""
width="640"
height="429"
loading="lazy"
decoding="async"
/>
: <DossierThumb kind={p.diagram} />}
<span class="dossier__tag mono">DOSSIER / {dossierNo}</span>
</div>
<div class="dossier__body">
<h3 class="dossier__title">
{p.title} <span class="dossier__arrow" aria-hidden="true">→</span>
</h3>
<p class="dossier__argument"><span class="dossier__outcome-label">Outcome</span> {p.outcome}</p>
<ul class="dossier__chips mono" role="list">
{p.stack.map((s) => <li class="dchip">{s}</li>)}
</ul>
<p class="dossier__meta mono">{p.role} · {p.period}</p>
</div>
<p class="project__outcome">{p.outcome}</p>
<p class="project__summary">{p.summary}</p>
<ul class="project__stack">
{p.stack.map((s) => <li class="tag">{s}</li>)}
</ul>
<p class="project__meta mono">
<span>{p.role}</span><span class="project__sep">·</span><span>{p.period}</span>
</p>
</a>
<style>
.project {
padding: var(--space-5);
.dossier {
display: flex;
flex-direction: column;
gap: var(--space-3);
color: inherit;
height: 100%;
}
a.project:hover {
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: 10px;
overflow: hidden;
text-decoration: none;
color: inherit;
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
transition:
border-color 180ms var(--ease),
box-shadow 180ms var(--ease),
transform 180ms var(--ease);
}
.dossier:hover {
text-decoration: none;
border-color: rgba(63, 186, 245, 0.55);
box-shadow: 0 0 32px rgba(63, 186, 245, 0.15);
transform: translateY(-3px);
}
.project__top {
@media (prefers-reduced-motion: reduce) {
.dossier:hover { transform: none; }
}
.dossier__thumb {
height: 180px;
background: #0b101c;
position: relative;
overflow: hidden;
border-bottom: 1px solid rgba(63, 186, 245, 0.12);
}
/* Cover art fills the thumb; the corner tag sits over it, so a scrim keeps
the tag legible against the brighter parts of the art. */
.dossier__cover {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 320ms var(--ease);
}
.dossier:hover .dossier__cover {
transform: scale(1.04);
}
.dossier__thumb:has(.dossier__cover)::after {
content: "";
position: absolute;
inset: 0 auto auto 0;
width: 60%;
height: 46px;
background: linear-gradient(160deg, rgba(9, 12, 20, 0.85), transparent 78%);
pointer-events: none;
}
:global([data-theme="light"]) .dossier__thumb {
background: var(--surface-2);
border-bottom-color: var(--border-accent);
}
.dossier__tag {
position: absolute;
top: 12px;
left: 14px;
font-size: 10px;
letter-spacing: 0.14em;
color: var(--text-faint);
}
.dossier__body {
position: relative;
padding: 24px;
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
flex-direction: column;
gap: 12px;
flex: 1;
}
.project__title {
font-size: var(--step-1);
/* a faint on-theme grid fills the idle space above the chips/meta (the
margin-top:auto gap on shorter "argument" text) instead of a flat void */
.dossier__body::before {
content: "";
position: absolute;
inset: 68px 24px 64px;
background-image:
linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 22px 22px;
mask-image: linear-gradient(180deg, black, transparent 85%);
pointer-events: none;
}
.project__arrow {
.dossier__title {
margin: 0;
font-family: var(--font-display);
font-size: 20px;
font-weight: 600;
color: var(--text-strong);
}
.dossier__arrow {
color: var(--accent);
display: inline-block;
transition: transform 0.2s var(--ease);
}
a.project:hover .project__arrow {
.dossier:hover .dossier__arrow {
transform: translateX(4px);
}
.project__outcome {
color: var(--text);
font-weight: 500;
border-left: 2px solid var(--accent);
padding-left: var(--space-3);
.dossier__outcome-label {
font-family: var(--mono, ui-monospace, monospace);
font-size: 11px;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--accent, #3fbaf5);
margin-right: 6px;
}
.project__summary {
color: var(--text-dim);
font-size: var(--step--1);
.dossier__argument {
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
text-wrap: pretty;
}
.project__stack {
.dossier__chips {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: auto 0 0;
padding: 0;
margin: var(--space-2) 0 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.project__meta {
margin-top: auto;
padding-top: var(--space-3);
font-size: 0.72rem;
.dchip {
font-size: 11px;
color: var(--text-secondary);
background: rgba(63, 186, 245, 0.08);
border-radius: 3px;
padding: 4px 8px;
}
.dossier__meta {
margin: 0;
font-size: 11px;
color: var(--text-faint);
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.project__sep {
opacity: 0.6;
border-top: 1px solid rgba(63, 186, 245, 0.1);
padding-top: 12px;
}
</style>
+4 -2
View File
@@ -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;
@@ -40,7 +40,9 @@ const { id, eyebrow, title, index } = Astro.props;
box-shadow: 0 0 8px var(--accent-glow);
}
.section__index {
color: var(--text-faint);
color: var(--accent);
text-shadow: var(--glow-num);
font-size: 1.05em;
margin-right: 0.6rem;
}
.section__title {
+168 -66
View File
@@ -1,94 +1,196 @@
---
// 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";
interface Props {
group: SkillGroup;
}
const { group } = Astro.props;
const spanClass =
group.span === "hero"
? "tile--hero"
: group.span === "wide"
? "tile--wide"
: group.span === "full"
? "tile--full"
: "";
---
<article class="skill card" data-reveal>
<h3 class="skill__title">{group.title}</h3>
<p class="skill__blurb">{group.blurb}</p>
<ul class="skill__items">
<article
class:list={["tile", spanClass, group.flavor === "magenta" && "tile--magenta"]}
data-reveal
>
{group.span === "hero" && <div class="tile__corner" aria-hidden="true" />}
{group.flag && <p class="tile__flag mono">{group.flag}</p>}
<h3 class="tile__title">{group.title}</h3>
<p class="tile__blurb">{group.blurb}</p>
{group.points && (
<ul class="tile__points" role="list">
{group.points.map((point) => (
<li>{point}</li>
))}
</ul>
)}
<ul class="tile__chips mono" role="list">
{group.items.map((item) => (
<li class="chip"><span class="chip__node" aria-hidden="true"></span>{item}</li>
<li class:list={["chip", group.span === "hero" && "chip--lit"]}>{item}</li>
))}
</ul>
</article>
<style>
.skill {
padding: var(--space-5);
.tile {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: 12px;
padding: 28px;
background: var(--panel);
border: 1px solid var(--border-accent);
border-radius: 10px;
backdrop-filter: var(--panel-blur);
-webkit-backdrop-filter: var(--panel-blur);
transition:
border-color 180ms var(--ease),
box-shadow 180ms var(--ease);
}
.skill__title {
font-size: var(--step-1);
.tile:hover {
border-color: var(--border-accent-hover);
box-shadow: var(--glow-card);
}
.skill__title::before {
content: "# ";
color: var(--accent);
font-family: var(--font-mono);
.tile--magenta {
border-color: var(--border-magenta);
}
.skill__blurb {
color: var(--text-dim);
font-size: var(--step--1);
line-height: 1.5;
}
.skill__items {
list-style: none;
padding: 0;
margin: var(--space-2) 0 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
.tile--magenta:hover {
border-color: rgba(232, 121, 249, 0.5);
box-shadow: var(--glow-card-magenta);
}
/* Circuit-node chips — a glowing cyan→magenta node + mono label, wired to the
board palette. Gradient border via padding-box/border-box layering so the
radius survives. Lights to a full gradient outline + glow on hover. */
.chip {
--chip-grad: linear-gradient(100deg, var(--accent), var(--accent-2));
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-family: var(--font-mono);
font-size: 0.72rem;
letter-spacing: 0.01em;
.tile--hero {
grid-column: span 2;
grid-row: span 2;
padding: 32px;
gap: 16px;
border-color: rgba(63, 186, 245, 0.2);
}
.tile--wide {
grid-column: span 2;
}
.tile--full {
/* "full" = the row-completer: 3 columns, sharing its row with the 1×1 before it */
grid-column: span 3;
}
.tile__corner {
position: absolute;
inset: 0;
background: radial-gradient(
500px 320px at 85% 100%,
rgba(232, 121, 249, 0.1),
transparent 70%
);
pointer-events: none;
}
.tile__flag {
position: relative;
margin: 0;
font-size: 11px;
letter-spacing: 0.14em;
color: var(--accent-2);
}
.tile__title {
position: relative;
margin: 0;
font-family: var(--font-display);
font-size: 23px;
font-weight: 700;
color: var(--text-strong);
}
.tile--hero .tile__title {
font-size: 30px;
}
.tile__blurb {
position: relative;
margin: 0;
font-size: 14px;
line-height: 1.6;
color: var(--text-secondary);
text-wrap: pretty;
}
.tile--hero .tile__blurb {
font-size: 15px;
color: var(--text-dim);
padding: 0.3rem 0.62rem 0.3rem 0.55rem;
border-radius: 7px;
border: 1px solid var(--accent-line);
background: color-mix(in srgb, var(--surface-2) 88%, var(--accent) 12%);
transition: color 0.2s var(--ease), transform 0.2s var(--ease),
border-color 0.2s var(--ease), box-shadow 0.2s var(--ease),
background 0.2s var(--ease);
max-width: 420px;
}
/* The node: a small cyan→magenta square that glows, like a lit via/pad. */
.chip__node {
flex: none;
width: 6px;
height: 6px;
border-radius: 2px;
background: var(--chip-grad);
box-shadow: 0 0 7px var(--accent-glow);
.tile__points {
position: relative;
list-style: none;
display: flex;
flex-direction: column;
gap: 10px;
margin: 6px 0 0;
padding: 0;
font-size: 13.5px;
line-height: 1.55;
color: var(--text-dim);
}
.chip:hover {
color: var(--text);
transform: translateY(-1px);
border-color: transparent;
/* dark fill (padding-box) inside a live gradient border (border-box) */
background: linear-gradient(var(--surface-2), var(--surface-2)) padding-box,
var(--chip-grad) border-box;
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.32), 0 0 16px var(--accent-glow);
.tile__points li {
padding-left: 18px;
position: relative;
text-wrap: pretty;
}
@media (prefers-reduced-motion: reduce) {
.chip {
transition: none;
}
.chip:hover {
transform: none;
.tile__points li::before {
content: "";
position: absolute;
left: 0;
top: 0.55em;
width: 8px;
height: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
border-radius: 1px;
}
.tile--hero .tile__points {
font-size: 14.5px;
gap: 12px;
max-width: 460px;
}
.tile__chips {
position: relative;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: auto 0 0;
padding: 0;
font-size: 11.5px;
color: var(--text-dim);
}
.chip {
border: 1px solid rgba(126, 139, 161, 0.25);
border-radius: var(--radius-chip);
padding: 4px 9px;
}
.chip--lit {
font-size: 12px;
color: var(--text-body-strong);
border-color: rgba(63, 186, 245, 0.3);
padding: 5px 10px;
}
/* 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. */
@media (max-width: 1000px) {
.tile--hero,
.tile--wide,
.tile--full {
grid-column: 1 / -1;
grid-row: auto;
}
}
</style>
+8 -2
View File
@@ -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(),
@@ -18,13 +18,16 @@ const projects = defineCollection({
order: z.number().default(50),
// Optional diagram key → rendered by the Diagram component (M3).
diagram: z.string().optional(),
// Optional cover art → /covers/<slug>.webp, rendered above the case header.
// Sells the story at a glance; the diagram still carries the architecture.
cover: z.string().optional(),
links: z
.array(z.object({ label: z.string(), href: z.string().url() }))
.optional(),
}),
});
// 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({
@@ -33,6 +36,9 @@ 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.
hero: z.string().optional(),
heroAlt: z.string().optional(),
}),
});
+13 -11
View File
@@ -1,24 +1,26 @@
---
title: "SSO is a perimeter decision, not a login box"
date: 2026-06-23
summary: "Putting single sign-on in front of a fleet of self-hosted apps isn't about a nicer login screen it's about where your trust boundary lives, and how it fails."
summary: "Putting single sign-on in front of a fleet of self-hosted apps isn't about a nicer login screen - it's about where your trust boundary lives, and how it fails."
tags: ["authentik", "oidc", "sso", "security", "kubernetes"]
draft: false
hero: "/blog/authentik-sso-2fa.webp"
heroAlt: "A single central gate ringed by a second concentric seal radiates beams of light to a circle of app-doors inside a glowing perimeter."
---
Most write-ups treat single sign-on as a feature you bolt on: stand up an identity provider, point the
apps at it, enjoy one login. That framing misses the actual decision. SSO *moves your trust boundary*
every app now trusts one issuer, and every account is one credential away from all of them. Get it right
apps at it, enjoy one login. That framing misses the actual decision. SSO *moves your trust boundary* - every app now trusts one issuer, and every account is one credential away from all of them. Get it right
and you've hardened the whole estate at once. Get it wrong and you've built a single, central thing to lose.
Here's how I think about it after wiring an OIDC provider in front of a self-hosted fleet.
![Authentik SSO flow the browser authenticates against the identity provider over the front channel; each app validates tokens over an internal back channel](/diagrams/authentik-sso.svg)
![Authentik SSO flow - the browser authenticates against the identity provider over the front channel; each app validates tokens over an internal back channel](/diagrams/authentik-sso.svg?v=2)
## Enforce the second factor at the flow, not per app
The leverage is putting 2FA on the *authentication flow itself*, not on each application. Every app that
federates to the provider inherits it for free you can't forget to enable 2FA on app number nine, because
federates to the provider inherits it for free - you can't forget to enable 2FA on app number nine, because
app number nine never sees a password. Enrolment (TOTP / WebAuthn) is mandatory at the identity layer, and
the apps just receive an already-verified identity.
@@ -27,8 +29,8 @@ gets it.
## Keep a key under the mat
The flip side of "one issuer for everything" is that when the issuer is down or you fat-finger the auth
flow you can lock yourself out of *everything*, including the tools you'd use to fix it. So every critical
The flip side of "one issuer for everything" is that when the issuer is down, or you fat-finger the auth
flow - you can lock yourself out of *everything*, including the tools you'd use to fix it. So every critical
app keeps a **local break-glass admin** that bypasses SSO, and the recovery codes for 2FA enrolment live
offline.
@@ -40,19 +42,19 @@ Two traps ate real hours, and they're the same lesson wearing two hats:
- **A baked-in config file silently overrode environment variables.** I set the OIDC scopes via env, login
kept failing with *"missing fields: email"*, and the cause was a config file winning over the env. The
provider needs `openid email profile` explicitly set where the app actually reads it, not where you
provider needs `openid email profile` explicitly - set where the app actually reads it, not where you
assume it does.
- **In-cluster back-channels can't use the public hostname.** The browser hits the public login URL fine,
but the app's *server-side* token exchange runs inside the cluster, where split-horizon DNS doesn't
resolve the public name. The fix: point the back channel at the internal service address while the
browser-facing URL stays public.
Both say the same thing: SSO has two channels the one the user sees and the one the server uses and they
Both say the same thing: SSO has two channels - the one the user sees and the one the server uses, and they
don't live on the same network.
## The principle
## Design the concentration you just created
Centralising identity is a force multiplier, but it concentrates risk *by design*. So design the
concentration: enforce the strong factor at the boundary, plan the failure mode before you need it, and
remember that OIDC authenticates a *user* it doesn't, on its own, make a sensitive service safe to expose.
remember that OIDC authenticates a *user* - it doesn't, on its own, make a sensitive service safe to expose.
The login box is the easy part. The perimeter is the decision.
+78
View File
@@ -0,0 +1,78 @@
---
title: "The bastion is a pattern, not a box"
date: 2026-07-03
summary: "Admin credentials accumulate on whatever machine you happen to work from, until your daily driver is the real control plane of the fleet. A bastion fixes that, not by being special hardware, but by being the one place the keys live and the discipline that they never leave it."
tags: ["security", "operations", "homelab", "tailscale", "access"]
hero: "/blog/bastion-is-a-pattern.webp?v=1"
heroAlt: "A lone illuminated harbour pilot station on a dark sea, thin beams of cyan and magenta light reaching out to distant vessels - the charts and keys stay at the station, the ships only transit past it."
draft: false
---
Here's the uncomfortable default: your fleet's admin credentials live wherever you last needed them. A
kubeconfig on the laptop, another copy on the desktop, an age key pasted onto a third machine "just for one
decrypt", SSH keys sprouting wherever a clone was convenient. Nobody decided this. It accumulates.
Which means your **daily driver is the real control plane** - the machine that browses the web, opens email
attachments and runs whatever you installed last week is also the machine that can wipe a cluster. Lose it,
rebuild it, or compromise it, and you've done all three to the fleet.
The inversion is old and it still works: pick *one* point, put every key there, and make the rule that they
never leave it. Not a hardened appliance. A pattern.
<!-- DIAGRAM: one bastion node holding keys/contexts/repos, lines out to the clusters + git; laptops connect only THROUGH it -->
![One operator control point holding the keys, with everything driven through it](/diagrams/bastion-is-a-pattern.svg?v=1)
## Everything transits, nothing homes anywhere else
Think of a harbour pilot station. Every ship entering the harbour is guided by a pilot, but the charts and the
local knowledge never board the ships - they stay at the station, and the vessels transit past it. That's the
whole design: the dangerous knowledge has exactly one home, and traffic comes *to* it.
Mine is nothing special on purpose: a small Debian VM - four cores, no GPU passthrough, because a bastion needs
none. What makes it the bastion is what it holds. One kubeconfig with a context for every cluster. The
talosconfig for the OS layer beneath them. The SOPS age key that decrypts the fleet's secrets, sitting in
exactly one place at `~/.config/sops/age/keys.txt`. A checkout of every ops repo. And a toolchain pinned
deliberately - `talosctl` matched to the fleet's live Talos version, because a drifted client against a
production node is how apply-time surprises happen.
None of that is exotic. The pattern is the *singularity* of it: ask "where can the fleet be driven from?" and
the answer is one hostname, and auditing that answer takes a minute.
## The bastion is defined by what it refuses
The keys that live there matter less than the keys that were *refused*. When this box was stood up to take
over the operator role from an older machine - before that machine's wipe and rebirth as a GPU node - the
login credentials for my agent tooling were deliberately not copied across. Fresh auth on the new box beats a
credential that has now existed in two places, because the whole point collapses the moment "one place"
becomes "two, temporarily".
The stand-up also proved the pattern's diagnostic value. Every repo cloned fine, and then a push failed,
because the git remotes resolved through SSH host aliases that only existed on the *old* machine. Access had
been quietly welded to one particular computer for months, and nothing noticed while that computer was always
there. Consolidating forces the question: is this credential something the operator point holds, or something
one box happens to have? The fix was the pattern applied properly - a new key generated *on* the bastion,
registered once, never exported.
## The one that costs an evening
The bastion has to be reachable off-LAN, so it joined the tailnet as a subnet router, advertising its own
network. Here's the trap: it also sat *inside* a subnet another router already advertised, and bringing it
up with `--accept-routes` made it accept the overlay route *for its own LAN*. Local replies went into the
tunnel instead of out the NIC. ARP stayed healthy, every TCP connection died, and SSH to the box that holds
all the keys went dark.
The rule that falls out: a node on the only LAN **advertises routes but never accepts them** - it reaches that
network natively. And the recovery is itself a bastion lesson: you get back in *out-of-band*, through the
hypervisor console. The control point must sit on infrastructure you can still reach when the network lies.
## One place holds the keys
A bastion isn't a product you install; it's a property you enforce: **the set of places your keys exist has
exactly one member.** Any box can hold the role - a VM, a spare NUC, a cloud instance, and the role can move,
the way this one inherited it from a machine headed for a wipe. What never moves casually is the material
itself. Ships come and go all day.
The charts never leave the station.
*Live as a four-core Debian VM on the management LAN, driving a five-cluster Talos fleet and reachable over the tailnet as its second subnet router.*
+74
View File
@@ -0,0 +1,74 @@
---
title: "An agent should never hold the key it's using"
date: 2026-07-03
summary: "You want an AI agent that can actually do things - call APIs, touch real data. You also don't fully trust it. The resolution isn't a better sandbox; it's making sure the agent never possesses a credential at all. A broker holds the keys, mints short-lived capabilities, and gates every write behind a human. Here's the pattern."
tags: ["security", "ai-agents", "architecture", "zero-trust", "homelab"]
draft: false
hero: "/blog/broker-pattern.webp"
heroAlt: "An untrusted agent reaches through a sealed gate to ask; on the far side, a guardian holds a ring of keys the agent can never touch."
---
The previous post put untrusted code in a hardware-isolated VM, and ended on a caveat: isolation contains an
*escape*, but it does nothing about an agent **misusing a tool it was legitimately given**. If you hand an AI
agent a database credential so it can be useful, a single bad decision - a prompt injection, a confused chain
of reasoning - spends that credential. The sandbox did its job perfectly and you still got robbed, through the
front door you built.
So the real question isn't "how do I isolate the agent?" It's "how does the agent get work done *without ever
holding a key*?"
## The agent holds a capability, not a credential
The pattern is to put every credential, every tool, and every model endpoint **behind a broker**, and give the
agent only a *capability to ask*. The agent never sees a token. It calls the broker; the broker holds the real
credential, decides whether the request is allowed, and - if it is - performs the action itself and returns the
result. The key never leaves the broker.
That one inversion changes the threat model completely. A fully compromised agent can now do exactly one thing:
**ask**. And asking is answered by something it can't reach, can't impersonate, and can't bypass.
<!-- DIAGRAM: untrusted sandbox -> cred-broker (mint capability) -> mcp-broker (verify + run tool with held cred) -> human-approval gate -> downstream. Agent never touches the credential. -->
![The two-tier broker flow: capability in, action out, credential never exposed](/diagrams/broker-pattern.svg)
## Two clusters, two brokers
The trust boundary is physical, not just logical. The **untrusted executor** (the Kata sandboxes) lives on one
cluster; the **trusted tier** (the brokers, the model gateway, the real credentials) lives on a *separate*
cluster. A total compromise of the executor still can't reach the brokers' secrets except across a policed
network link - there's no shared kernel, no shared API server, nothing to pivot through.
On the trusted side there are two brokers, deliberately split:
- A **credential broker** validates the sandbox's identity (a short-lived, signed token unique to the task) and
mints a **capability** - a cryptographically signed, scoped, single-use, expiring grant. Not a credential. A
*permission to ask for one specific thing*.
- A **tool broker** takes that capability, verifies the signature, the scope, and the one-time nonce, and only
then runs the requested tool - using a credential *it* holds. The result comes back; the credential doesn't.
## A human gates every write
Reads are one thing. For anything that *changes the world* - creating, deleting, sending - the broker doesn't
just decide on policy. It **stages** the action and pings a human: an Approve/Deny prompt on my phone, carrying
a one-time token bound to the exact task, method, and arguments. Tap approve and it executes; tap deny, or
ignore it, and it doesn't. The gate is **fail-closed**: a timeout is a denial, a replayed token is rejected, an
unknown method is rejected. The default, always, is *no*.
The load-bearing principle underneath all of it: **enforcement that has to survive a compromised agent lives at
the broker, never at the agent or the orchestrator.** Approval, scope, budgets, write-authority - none of it
lives anywhere the agent's reasoning can touch. The agent can be wrong, jailbroken, or outright hostile, and the
worst-case is still just *a request that gets refused*.
## Useful and safe at the same time
It's tempting to think you have to choose: give the agent real power and accept real risk, or lock it down so
hard it can't do anything. The broker pattern is how you get both. The agent is *useful* - it can call real
tools against real systems. It's *safe* - it never holds a key, every write waits on a human, and the moment
something goes wrong, the blast radius is a denied request, not a spent credential.
Run agents like you'd run any other untrusted input: assume it's compromised, and make sure that assumption is
*boring*.
*Live as the trusted tier of a two-cluster AI-agent platform: a credential broker and a tool broker holding the
keys, a phone-based human-approval gate on every write, and an agent that - by construction - never sees a
secret.*

Some files were not shown because too many files have changed in this diff Show More