Compare commits
72 Commits
9e177dcc41
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cf900b3409 | |||
| 4a8552c408 | |||
| b5e0b25667 | |||
| c5b1044d7a | |||
| 8d394fe2c7 | |||
| 21616762e6 | |||
| f6b683713e | |||
| b6d3d4487a | |||
| dd2207bfa6 | |||
| 51d1ccb149 | |||
| 36444c3424 | |||
| 7cbc3629a6 | |||
| c07f8433dc | |||
| 0f85bd9870 | |||
| 6155aca055 | |||
| 573e7a6002 | |||
| 25425d6f6a | |||
| 7542ae2665 | |||
| 43188589ab | |||
| 3cfd59a464 | |||
| 78e8740408 | |||
| e90bf177a5 | |||
| 6c0d7643a6 | |||
| 3a13cff1fa | |||
| df72a22952 | |||
| f89957370e | |||
| d26e981d7a | |||
| cbfc4007cf | |||
| cd08cd04c1 | |||
| ba81705a39 | |||
| 99fd652569 | |||
| afba75dd3a | |||
| f7e64cebe5 | |||
| 6c7e577633 | |||
| 6ea2582a66 | |||
| 317e5230b6 | |||
| 521cc2f066 | |||
| 91b3cff67a | |||
| a0d71388c4 | |||
| 3217d779b3 | |||
| 94698c6315 | |||
| 87cfe78ffc | |||
| 2896f89af3 | |||
| 908d311e5c | |||
| 0d05aae721 | |||
| c162f2e4e7 | |||
| de7c257ab7 | |||
| 71c3f00618 | |||
| 4e13bf596c | |||
| 8a7ca88c40 | |||
| de12f028eb | |||
| 8d955aa802 | |||
| 65ba1ec7dc | |||
| 08bab080b1 | |||
| dae9bbc91a | |||
| 7c300faec8 | |||
| 257af5d22e | |||
| 880b9daeac | |||
| a685789322 | |||
| 61642e8db8 | |||
| bed2dbaa1f | |||
| 354475a453 | |||
| d8a147aabe | |||
| f6d92e5c87 | |||
| 5d439752de | |||
| 0c93328a2d | |||
| ef1719c84e | |||
| e264f673d7 | |||
| 8702c8a293 | |||
| b7c91081b2 | |||
| af7e0287d4 | |||
| 2aae7c287e |
@@ -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__/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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, HTTP→HTTPS 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 237 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -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.
|
||||
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
@@ -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 — 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 · 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 · 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 · 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 · auditing “where from?” 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 — the charts never leave the station</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -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 — 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 — 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 · 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 · scope · 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 — 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 · 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 — 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 — 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’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 → 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: “not yet propagated”</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 — 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 — not the mail slot in your own hallway</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -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 — 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 · 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 · 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">● cloned repos (photocopies)</text>
|
||||
<text x="604" y="234">● own git key — revocable alone</text>
|
||||
<text x="604" y="256">● 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">✗ NO age key — 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 — 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 · admin kubeconfigs · SSH keys — 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 — revertible — never silent possession of the estate</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
@@ -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 — 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 — 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 · pod network · 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 · 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 · 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 · 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 — 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 — 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 — 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">● CNI — pod network</text>
|
||||
<text x="655" y="258">● eBPF datapath — kube-proxy's old job</text>
|
||||
<text x="655" y="284">● LB-IPAM + L2 — MetalLB's old VIPs, kept</text>
|
||||
<text x="655" y="310">● Gateway API — ingress, where absorbed</text>
|
||||
<text x="655" y="336">● NetworkPolicy — now actually enforced</text>
|
||||
<text x="655" y="362">● Hubble — 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 · one changelog · 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 · MetalLB extinct · policies enforced · zero Helm release secrets — same VIPs throughout</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -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 — 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">● scheduled config backup</text>
|
||||
<text x="394" y="238">● change tracking + diffs</text>
|
||||
<text x="394" y="264">● 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 · 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 — 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 — 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 — 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 |
@@ -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 ·</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 — 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 · NICs · mirrors · 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) · logs · 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 — 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 · 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 · destination · 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 · host probes · 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 — 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 — 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 — 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 →</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 — 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 — 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 · 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 · 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 → 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 — 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 — 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 —</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 — 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 · 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 · 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 — 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 → a cluster without it fails at template time, in the pipeline — not months later as policy pointed at nothing</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
@@ -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 — 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 — 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 — 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 → pod → runtimeClass kata</text>
|
||||
<text x="380" y="204">→ 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">→ 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">→ 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 — 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 — the tax IS the ladder</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
@@ -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 — 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 · dumps · 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 → 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 · 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 — 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 · 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">✓</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 · 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 — the loop out of it is</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -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 — 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 — 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 · 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 · 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 → 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 ×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 — 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 |
@@ -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 "the process is running" and "the process is doing its job" 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 — 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 · 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 — 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 — 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 → passes only when traffic genuinely egresses → silent failure becomes a restart, then an alert</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -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 — 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 · 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 · 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 · 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 · kate · 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 · 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 — 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 — 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 — power is just whether the document is currently being read</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -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 — 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 — 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 → 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 — 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 — 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 · 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 — and swapping the LB plumbing itself (kube-proxy → eBPF) changes what arrives under an unchanged policy. Observe before you allow.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.5 KiB |
@@ -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 → prove → 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 — 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">→ six node repos, via git</text>
|
||||
<text x="110" y="222">→ 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 · reviewable · 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 — 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">✓ ✓ ✓ three gated</text>
|
||||
<text x="470" y="302" fill="#64748b">○ ○ awaiting their window</text>
|
||||
<text x="470" y="324" fill="#c026d3">⚠ one parked — 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 — 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 → 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 — 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) — 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 — readiness is the key turning in the door, observed, per node</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
@@ -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,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 → 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 → 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 — 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 · uid 1000 · 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 · LoadBalancer · secrets operator</text>
|
||||
<text x="78" y="350" font-family="ui-monospace, 'SF Mono', Menlo, monospace" font-size="10.5" fill="#0e1726">→ 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 |
@@ -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 |
@@ -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.
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 120 KiB |
@@ -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.
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)`);
|
||||
@@ -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 & INFRASTRUCTURE ENGINEER</text>
|
||||
fill="#3fbaf5">~/ PLATFORM & 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">· GPU-backed edge systems</text>
|
||||
|
||||
<text x="80" y="560" font-family="monospace" font-size="20" fill="#8aa0b8">RHEL <tspan fill="#5eead4">▸</tspan> Kubernetes <tspan fill="#5eead4">▸</tspan> GitOps <tspan fill="#5eead4">▸</tspan> Ansible <tspan fill="#5eead4">▸</tspan> Observability <tspan fill="#5eead4">▸</tspan> Edge AI</text>
|
||||
<text x="80" y="560" font-family="monospace" font-size="20" fill="#8aa0b8">RHEL <tspan fill="#3fbaf5">▸</tspan> Kubernetes <tspan fill="#3fbaf5">▸</tspan> GitOps <tspan fill="#3fbaf5">▸</tspan> Ansible <tspan fill="#3fbaf5">▸</tspan> Observability <tspan fill="#3fbaf5">▸</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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -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 -->
|
||||

|
||||
|
||||
## 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.*
|
||||
@@ -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. -->
|
||||

|
||||
|
||||
## 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.*
|
||||