Compare commits
75 Commits
d64f528ff7
..
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 | |||
| 9e177dcc41 | |||
| 99455d0168 | |||
| 03b0cf7c7f |
@@ -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,
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"@astrojs/rss": "^4.0.18",
|
||||
"@astrojs/sitemap": "^3.7.3",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"astro": "^6.4.7",
|
||||
"astro": "^7.0.3",
|
||||
"motion": "^12.42.0",
|
||||
"tailwindcss": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
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 2018 The Chakra Petch Project Authors (https://github.com/m4rc1e/Chakra-Petch.git)
|
||||
|
||||
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:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
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.
|
||||
@@ -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);
|
||||
// --- 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 = [];
|
||||
}
|
||||
});
|
||||
},
|
||||
{ rootMargin: "0px 0px -10% 0px", threshold: 0.1 },
|
||||
);
|
||||
targets.forEach(function (el) {
|
||||
io.observe(el);
|
||||
});
|
||||
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 {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
// Cheap, CSS-only topology motif: a faint grid + slow-drifting accent glows.
|
||||
// No canvas, no WebGL, no JS. Fully disabled under prefers-reduced-motion.
|
||||
---
|
||||
|
||||
<div class="bg" aria-hidden="true">
|
||||
<div class="bg__grid"></div>
|
||||
<div class="bg__glow bg__glow--a"></div>
|
||||
<div class="bg__glow bg__glow--b"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.bg__grid {
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
background-image:
|
||||
linear-gradient(to right, var(--grid-line) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, var(--grid-line) 1px, transparent 1px);
|
||||
background-size: 46px 46px;
|
||||
/* Fade the grid out toward the edges so it reads as a motif, not a table. */
|
||||
mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 78%);
|
||||
-webkit-mask-image: radial-gradient(120% 90% at 50% 0%, #000 35%, transparent 78%);
|
||||
}
|
||||
.bg__glow {
|
||||
position: absolute;
|
||||
width: 46rem;
|
||||
height: 46rem;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.bg__glow--a {
|
||||
top: -22rem;
|
||||
left: -10rem;
|
||||
background: radial-gradient(circle, var(--accent-glow), transparent 62%);
|
||||
animation: drift-a 26s var(--ease) infinite alternate;
|
||||
}
|
||||
.bg__glow--b {
|
||||
top: -16rem;
|
||||
right: -14rem;
|
||||
background: radial-gradient(circle, var(--accent-glow), transparent 65%);
|
||||
opacity: 0.35;
|
||||
animation: drift-b 32s var(--ease) infinite alternate;
|
||||
}
|
||||
@keyframes drift-a {
|
||||
to {
|
||||
transform: translate(6rem, 4rem) scale(1.12);
|
||||
}
|
||||
}
|
||||
@keyframes drift-b {
|
||||
to {
|
||||
transform: translate(-5rem, 3rem) scale(1.08);
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bg__glow {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,46 +1,57 @@
|
||||
---
|
||||
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.
|
||||
<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="footer__contact">
|
||||
For a low-ms reply, ping me on{" "}
|
||||
|
||||
<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> — everything else is best-effort delivery.
|
||||
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="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{" "}
|
||||
|
||||
<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"
|
||||
@@ -49,72 +60,206 @@ const emailHref = socials.find((s) => s.label === "Email")?.href;
|
||||
>
|
||||
{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>
|
||||
<span class="mono footer__dot">·</span>
|
||||
<span class="mono">built with Astro, shipped via GitOps</span>
|
||||
</div>
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
.footer__base {
|
||||
margin-top: var(--space-5);
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-faint);
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.term__cursor {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 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,12 +1,20 @@
|
||||
---
|
||||
import BackgroundGrid from "./BackgroundGrid.astro";
|
||||
import { site } from "../data/site";
|
||||
import { cvAvailable } from "../lib/assets";
|
||||
import { fleetFacts } from "../data/fleet";
|
||||
import Constellation from "./Constellation.astro";
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<BackgroundGrid />
|
||||
<!-- 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">
|
||||
<div class="hero__copy">
|
||||
<p class="eyebrow hero__eyebrow">{site.role}</p>
|
||||
|
||||
<h1 class="hero__title">
|
||||
@@ -25,7 +33,7 @@ import { cvAvailable } from "../lib/assets";
|
||||
</p>
|
||||
|
||||
<div class="hero__cta">
|
||||
<a class="btn btn--primary" href="/#projects">View Projects</a>
|
||||
<a class="btn btn--primary" href="/#projects">View Projects →</a>
|
||||
{cvAvailable && (
|
||||
<a class="btn" href="/cv.pdf" download>
|
||||
Download CV
|
||||
@@ -33,45 +41,162 @@ import { cvAvailable } from "../lib/assets";
|
||||
)}
|
||||
<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;
|
||||
letter-spacing: -0.03em;
|
||||
/* Extend the paint box below the baseline so background-clip:text doesn't
|
||||
crop glyph descenders (the "g" in Wright). */
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.005em;
|
||||
padding-bottom: 0.14em;
|
||||
background: linear-gradient(180deg, var(--text), color-mix(in srgb, var(--text) 62%, var(--accent)));
|
||||
/* 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;
|
||||
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;
|
||||
@@ -81,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 {
|
||||
@@ -94,36 +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;
|
||||
}
|
||||
.hero__title {
|
||||
animation-delay: 0.06s;
|
||||
}
|
||||
.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);
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero__caret { animation: none; }
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
@keyframes heroSheen {
|
||||
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;
|
||||
}
|
||||
.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,7 +15,18 @@ const { posts } = Astro.props;
|
||||
posts.map((post) => (
|
||||
<li class="post" data-reveal>
|
||||
<a class="post__link" href={`/blog/${post.id}/`}>
|
||||
{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>
|
||||
@@ -26,6 +38,7 @@ const { posts } = Astro.props;
|
||||
<li class="tag">{t}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>)}
|
||||
<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="project__meta mono">
|
||||
<span>{p.role}</span><span class="project__sep">·</span><span>{p.period}</span>
|
||||
</p>
|
||||
<p class="dossier__meta mono">{p.role} · {p.period}</p>
|
||||
</div>
|
||||
</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>
|
||||
|
||||