This commit is contained in:
2026-08-20 11:24:42 -03:00
parent a65c92257d
commit 83b6cbebe3
64 changed files with 7688 additions and 0 deletions

9
rig/sample-rig/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# NOTE: generated/ is deliberately NOT ignored. The artifact IS the deliverable —
# the whole point is a folder you copy, apply and boot without generating
# anything first. Regenerate it with `make manifest` after editing bundle.json
# or app/serve.py, and commit the result.
#
# (This differs from rig's ctrl/k8s/generated, which is a local build artifact.)
__pycache__/
*.pyc

50
rig/sample-rig/Makefile Normal file
View File

@@ -0,0 +1,50 @@
# Thin control Makefile — one target per ctrl/ script, subcommand as an
# argument. Same shape as rig's, for the same reason: the logic lives in the
# script, never here.
#
# make up -> ctrl/bundle.sh up
# make bundle down
#
# This directory is a BUNDLE, not an installer. It needs a cluster, which rig
# owns:
#
# cd .. && make cluster up # kind cluster for this environment
# make up # then deploy this bundle into it
#
# Start with: make up
ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
ifneq ($(ARGS),)
$(eval $(ARGS):;@:)
endif
.DEFAULT_GOAL := help
.PHONY: help bundle manifest up down status url list dev
help: ## list targets
@grep -hE '^[a-z]+:.*?##' $(MAKEFILE_LIST) | sed 's/:.*##/\t/' | expand -t16
bundle: ## the bundle [manifest|up|down|status|url|list|dev]
bash ctrl/bundle.sh $(or $(ARGS),status)
# Shorthands for the ones used constantly.
manifest: ## regenerate generated/<slug>.yaml — no cluster needed
bash ctrl/bundle.sh manifest
up: ## deploy this rig (installs MetalLB if absent)
bash ctrl/bundle.sh up
down: ## remove this rig (leaves cluster, MetalLB, siblings)
bash ctrl/bundle.sh down
status: ## what is deployed for this rig
bash ctrl/bundle.sh status
url: ## the address MetalLB assigned
bash ctrl/bundle.sh url
list: ## every rig in this cluster, with addresses
bash ctrl/bundle.sh list
dev: ## run the UI locally with vite — no cluster needed
bash ctrl/bundle.sh dev

140
rig/sample-rig/README.md Normal file
View File

@@ -0,0 +1,140 @@
# sample-rig
A minimal, non-sensitive bundle that proves an installation works and shows what
shipped. Copy it, rename it, and you have another rig.
```bash
make manifest # generate the artifact — no cluster, no kubectl needed
make up # deploy it into the local cluster
make list # every rig in this cluster, with addresses
make dev # run the UI locally with vite, no cluster at all
```
`make up` prints an address. Open it and the page says **IT WORKS**, then lists
the tools and rigs in the bundle.
## What it is for
Three jobs, in the order you hit them:
1. **Prove the install.** kind is there, a cluster exists, MetalLB hands out an
address, a `type: LoadBalancer` Service actually resolves, and a pod serves.
If all of that works, the environment is sound.
2. **Say what shipped.** The page renders [`bundle.json`](bundle.json) —
standalone tools and rigs, flat, with none of soleprint's internal hierarchy.
Editing that file is the only step needed to change the listing.
3. **Stand in for the real thing.** Nothing here is sensitive. The real
architecture connects separately, against a setup already known to work.
## The UI is a complement, not the product
`rig-ui/` is just a vite app. It complements a rig; a rig is complete and useful
without it, and nothing depends on it being there. It is deliberately **not**
generated by kind or tilt — you copy the folder into a rig after that rig is
pulled, and apply one manifest:
```bash
kubectl apply -n <namespace> -f rig-ui/k8s.yaml
```
That file is the whole integration: one Pod running `npm run dev` on
`node:22-alpine`, one Service. A bare Pod rather than a Deployment because this
is a dev-loop convenience, not a workload to keep alive.
The app and `bundle.json` arrive as a ConfigMap, so nothing is baked into an
image and editing the bundle is the entire update cycle. The container runs
`npm install` at start, which needs egress to a registry — on a locked-down
cluster point npm at the internal one, or bake an image instead. Nothing else
changes if you do.
## One artifact, two destinations
`ctrl/manifest.py` emits `generated/<slug>.yaml` — namespace, the app and
bundle embedded in a ConfigMap, Pod, Service. It is self-contained and applies
unmodified anywhere:
```bash
kubectl apply -f generated/sample-rig.yaml # local kind, or an external cluster
```
`make up` applies **that same file**. There is no separate local path, so what
works here cannot quietly differ from what is applied elsewhere.
This is what `type: LoadBalancer` buys. MetalLB answers it on kind; the AWS load
balancer controller answers it on EKS. NodePort would not survive the trip — it
is a single cluster-wide port range, so two rigs would have to negotiate numbers.
**VPC-agnostic on purpose.** The target is EKS, but the Service carries no
annotations — no `aws-load-balancer-subnets`, no security groups, no `-scheme`,
no `-type: nlb`. Each of those encodes a specific network layout, and one of them
appearing here would pin the artifact to the account and VPC it was written
against, which is precisely what stops it also working on kind. Subnet discovery
is the cluster's business: EKS resolves it from the tags its own subnets carry.
That leaves one thing genuinely environment-specific — internal versus
internet-facing. A bare `LoadBalancer` provisions internet-facing, which a
regulated account will usually refuse, and should. That belongs in a
per-environment overlay applied on top, never inlined into this artifact.
**MetalLB only — no ingress-nginx.** Its controller supports a narrow window of
Kubernetes versions, so depending on it constrains which k8s a rig can be built
with. That undercuts running trailing-edge control planes to model a legacy
estate, which is the reason `versions.env` pins v1_33..v1_36. MetalLB carries no
such constraint, so reachability costs nothing in version coverage.
## Several rigs, one cluster
Identity follows the **folder name**, the same rule rig uses for cluster
identity. The namespace is the folder; resource names are generic, and names only
have to be unique within a namespace.
```bash
cp -r sample-rig corporate-rig
cd corporate-rig && make up # its own namespace, its own address
```
No edits, no collisions, both in the same local cluster. `make list` shows them
together. `make down` removes only this one — siblings, MetalLB and the cluster
are left alone.
Client rigs are gitignored (`*-rig/`, with `sample-rig/` the deliberate
exception): a rig's k8s files spell out a real architecture, and that is exactly
what must not land in this repo.
## Staging workstations
`ctrl/manifest.py` is stdlib-only on purpose: it runs on a bare machine before
anything is installed. The toolchain itself is rig's job — `make deps` installs
the pinned kind and tilt binaries, which is what makes a staging AWS workspace
reachable from the same commands as a laptop.
## Layout
```
sample-rig/
├── Makefile # thin — one target per ctrl/ script
├── bundle.json # what shipped; the UI renders THIS
├── rig-ui/ # the vite app — optional, copied into a rig to enable it
│ ├── k8s.yaml # how to plug it in: one Pod, one Service
│ ├── index.html
│ ├── package.json
│ ├── vite.config.js
│ └── src/{main.js,style.css}
├── ctrl/
│ ├── manifest.py # emits the artifact
│ └── bundle.sh # generate / deploy / inspect
└── generated/ # the artifact — committed, this is the deliverable
```
Editing `bundle.json` or anything in `rig-ui/` means re-running `make manifest`.
The ConfigMap carries a checksum of everything embedded, so a stale deployment is
visible rather than silent.
## Not built, but not foreclosed
Everything derives from `bundle.json` plus a target namespace. A Pulumi or
Terraform emitter would sit beside `ctrl/manifest.py` consuming the same inputs;
nothing above it assumes the artifact is YAML.
Licence terms for the compiled UI component belong in the soleprint-generated
bundle, not here — this sample carries no proprietary component.

View File

@@ -0,0 +1,51 @@
{
"_comment": "What this bundle contains. Single source of truth — the landing page renders THIS file, so adding an entry here is the only edit needed. Deliberately FLAT: standalone tools and rigs, with none of soleprint's internal hierarchy (no artery/atlas/station layering). Nothing here is sensitive; the real architecture connects separately.",
"bundle": {
"name": "sample-rig",
"description": "Non-sensitive sample bundle. Proves the kind install works and shows what ships.",
"sensitive": false
},
"tools": [
{
"name": "modelgen",
"summary": "Generate models from config",
"standalone": true
},
{
"name": "datagen",
"summary": "Generate test data from rig-owned generators",
"standalone": true
},
{
"name": "graphgen",
"summary": "Generate navigable model graphs",
"standalone": true
},
{
"name": "tester",
"summary": "HTTP contract test runner — one suite, any environment",
"standalone": true
},
{
"name": "databrowse",
"summary": "SQL data browser",
"standalone": true
},
{
"name": "sbwrapper",
"summary": "Sandbox wrapper",
"standalone": true
}
],
"rigs": [
{
"name": "sample-rig",
"summary": "This bundle — a minimal, copyable environment",
"active": true
}
],
"next": [
"Point MANIFESTS_DIR at the real manifests to connect the actual architecture.",
"Real k8s files are versioned separately and are not part of this bundle."
]
}

View File

@@ -0,0 +1,40 @@
{
"_comment": "MOCKED cluster state. Nothing here is read from a live cluster — it exists so the UI can be shown when there is no cluster at all (a locked-down machine, a laptop with no memory to spare, a demo where kind will not start). The page labels it as mocked; a demo that looks live but is not is worse than one that says so. When a real cluster is present the same shapes come from kubectl.",
"mocked": true,
"cluster": {
"name": "sample-rig",
"context": "kind-sample-rig",
"provider": "kind",
"profile": "minimal",
"k8s": "v1.36.1",
"nodes": 1
},
"workloads": [
{
"name": "rig-ui",
"summary": "Pod · node:22-alpine · vite on :5173",
"state": "Running"
},
{
"name": "metallb-system/controller",
"summary": "Deployment · assigns LoadBalancer addresses",
"state": "Running"
},
{
"name": "metallb-system/speaker",
"summary": "DaemonSet · answers ARP in layer 2 mode",
"state": "Running"
}
],
"services": [
{
"name": "rig-ui",
"summary": "LoadBalancer · 80 -> 5173 · no annotations, so it resolves on kind and on EKS alike",
"state": "172.18.255.200"
}
]
}

223
rig/sample-rig/ctrl/bundle.sh Executable file
View File

@@ -0,0 +1,223 @@
#!/usr/bin/env bash
# The rig bundle: generate it, deploy it, tear it down, find it.
#
# Usage: bundle.sh manifest | up | down | status | url | list | dev
#
# What `up` proves, in order: kind installed and a cluster exists, MetalLB can
# hand out an address, a Service of type LoadBalancer actually resolves, and a
# pod serves the bundle listing. If all of that works the installation is sound,
# and the only thing missing is the real architecture.
#
# ONE ARTIFACT
# `up` applies generated/<slug>.yaml — the same self-contained file you would
# hand to an external cluster. There is no separate local path, so what works
# here cannot quietly differ from the master deployment applied elsewhere.
#
# ONE CLUSTER, SEVERAL RIGS
# Identity follows the FOLDER NAME, exactly as rig's cluster identity does. This
# directory deploys into a namespace named after itself, so copying it to
# corporate-rig/ yields a second rig in the SAME local cluster with no edits and
# no collisions — different namespace, its own MetalLB address. `list` shows all
# of them. The cluster itself is rig's business; this only ever owns a namespace.
#
# MetalLB is installed by calling rig's own addon script rather than
# reimplementing it — deriving the pool from the kind Docker network is the
# fiddly part and there should be exactly one copy of it.
set -euo pipefail
cd "$(dirname "$0")/.."
BUNDLE_ROOT="$(pwd)"
RIG_CTRL="$(cd .. && pwd)/ctrl"
# The containing folder's name, reduced to a DNS label (same rule as rig's
# default_cluster_name and ctrl/manifest.py, so all three agree on the slug).
slug() {
local n
n=$(basename "$BUNDLE_ROOT")
n=$(echo "$n" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
n=$(echo "$n" | sed 's/^-*//; s/-*$//')
echo "${n:-rig-bundle}"
}
NS="$(slug)"
ARTIFACT="generated/${NS}.yaml"
# Resolved lazily, not at load time: `manifest` and `dev` deliberately work
# with no cluster and no kubectl at all, and a top-level check would break that.
#
# Follows whatever context rig's cluster.sh selected, so this bundle works in a
# copied-and-renamed environment without being told which cluster it is in.
init_kube() {
KUBECONTEXT="${KUBECONTEXT:-$(kubectl config current-context 2>/dev/null || true)}"
if [ -z "$KUBECONTEXT" ]; then
echo "no kubectl context — bring a cluster up first: (cd .. && make cluster up)" >&2
exit 1
fi
KCTX="kubectl --context ${KUBECONTEXT}"
K="kubectl --context ${KUBECONTEXT} --namespace ${NS}"
}
require_cluster() {
if ! $KCTX cluster-info >/dev/null 2>&1; then
echo "context '$KUBECONTEXT' does not reach a cluster" >&2
echo "bring one up: (cd .. && make cluster up)" >&2
exit 1
fi
}
ensure_metallb() {
if $KCTX get deployment -n metallb-system controller >/dev/null 2>&1; then
echo "metallb: present"
return 0
fi
# Only kind needs it. On a real cluster the cloud load balancer answers a
# `type: LoadBalancer` Service, and installing MetalLB there would be wrong.
case "$KUBECONTEXT" in
kind-*) ;;
*)
echo "metallb: skipped — '$KUBECONTEXT' is not a kind context"
echo " (a cloud load balancer answers LoadBalancer services there)"
return 0
;;
esac
if [ ! -f "$RIG_CTRL/addons/metallb.sh" ]; then
echo "metallb is not installed and rig's addon script was not found at" >&2
echo " $RIG_CTRL/addons/metallb.sh" >&2
echo "a Service of type LoadBalancer will sit at <pending> without it." >&2
exit 1
fi
# rig's addons derive their target cluster from RIG'S OWN folder name via
# load_config, so left alone this bundle would install into `kind-rig` —
# a cluster that need not exist — while deploying everything else into the
# context actually selected. CLUSTER is in load_config's overridable set,
# so passing it here points the addon at the same cluster we are using.
local target="${KUBECONTEXT#kind-}"
echo "metallb: installing via rig's addon into '$target'"
CLUSTER="$target" bash "$RIG_CTRL/addons/metallb.sh"
}
# Regenerate the artifact. No cluster and no kubectl required — this is the step
# a staging workstation runs before anything is installed.
manifest() {
mkdir -p generated
python3 ctrl/manifest.py "$NS" > "$ARTIFACT"
echo "wrote $ARTIFACT ($(wc -l < "$ARTIFACT") lines)"
echo " applies as-is anywhere: kubectl apply -f ${BUNDLE_ROOT}/${ARTIFACT}"
}
up() {
manifest
init_kube
require_cluster
ensure_metallb
echo
echo "applying '${NS}' to context '${KUBECONTEXT}'"
$KCTX apply -f "$ARTIFACT"
# `rollout status` does not work on a bare Pod — it only understands
# Deployments, StatefulSets and DaemonSets. Wait on the condition instead.
# This is the slow step: the container npm-installs before vite serves.
echo "waiting for the pod to be ready (npm install runs first)..."
$K wait --for=condition=Ready pod/rig-ui --timeout=300s
echo
url
}
down() {
init_kube
# Delete the namespace and everything in it goes with it. Scoped to THIS
# rig — a sibling rig in the same cluster is untouched.
$KCTX delete namespace "$NS" --ignore-not-found
echo "'${NS}' removed (cluster, metallb and any sibling rig are left alone)"
}
status() {
init_kube
require_cluster
if ! $KCTX get namespace "$NS" >/dev/null 2>&1; then
echo "'${NS}' is not deployed — run: make up"
return 0
fi
$K get pod,svc,configmap -o wide
}
# Every rig in this cluster, not just this one — the point of the namespace
# split is that several coexist, so there has to be a way to see them together.
list() {
init_kube
require_cluster
local names
names=$($KCTX get namespace -l rig.bundle/name \
-o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true)
if [ -z "$names" ]; then
echo "no rigs deployed in context '${KUBECONTEXT}'"
return 0
fi
printf "%-20s %-16s %s\n" RIG ADDRESS ""
local n ip
for n in $names; do
ip=$($KCTX -n "$n" get svc rig-ui \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
printf "%-20s %-16s %s\n" "$n" "${ip:-<pending>}" \
"$([ "$n" = "$NS" ] && echo '<- this one')"
done
}
# The address MetalLB (or a cloud load balancer) assigned. <pending> here is the
# classic silent failure: everything reports healthy and nothing is reachable.
url() {
init_kube
local ip
ip=$($K get svc rig-ui \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
if [ -z "$ip" ]; then
ip=$($K get svc rig-ui \
-o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || true)
fi
if [ -z "$ip" ]; then
echo "no external address yet — nothing has assigned one."
echo "on kind: kubectl --context $KUBECONTEXT -n metallb-system get pods"
return 1
fi
echo "IT WORKS -> http://${ip}/"
echo " bundle http://${ip}/bundle.json"
}
# Run the UI locally with no cluster at all — the fast way to iterate on
# bundle.json. Same vite command the pod runs, so what you see here is what
# gets served there.
dev() {
if ! command -v npm >/dev/null 2>&1; then
echo "npm not found — the UI needs node locally for this." >&2
echo "(in-cluster it runs on the node:22-alpine image instead)" >&2
exit 1
fi
# bundle.json lives one level up so it stays the rig's data rather than the
# app's; vite serves public/ at the root, which is where the app fetches it.
mkdir -p rig-ui/public
cp bundle.json rig-ui/public/bundle.json
# The mocked cluster is a DEMO asset and is deliberately not embedded in the
# deployed artifact — on a real rig the UI would then show canned values
# beside a live cluster, which is precisely the lie its banner warns about.
# It is served here, and in the static build for the public UI-only page.
cp cluster.mock.json rig-ui/public/cluster.mock.json
cd rig-ui
[ -d node_modules ] || npm install --no-audit --no-fund
VITE_RIG_NAME="$NS" npm run dev
}
case "${1:-status}" in
manifest) manifest ;;
up) up ;;
down) down ;;
status) status ;;
url) url ;;
list) list ;;
dev) dev ;;
*) echo "usage: $0 [manifest|up|down|status|url|list|dev]" >&2; exit 1 ;;
esac

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env python3
"""Emit the complete, self-contained deployment for this rig.
python3 ctrl/manifest.py [namespace] > generated/<slug>.yaml
The output is the ARTIFACT. It carries everything — namespace, the vite app and
bundle.json embedded in a ConfigMap, the Pod and the Service — so it applies
unmodified to any cluster:
kubectl apply -f generated/sample-rig.yaml
On kind, MetalLB answers the `type: LoadBalancer` Service. On a real external
cluster the cloud load balancer does. Same file, no edits, no branch — which is
the point: what runs locally is byte-identical to the deployment applied
elsewhere, so local success actually means something.
`ctrl/bundle.sh up` applies this same generated output rather than a separate
code path, so the local convenience wrapper can never drift from the artifact.
Stdlib only, deliberately: this must run on a bare staging workstation before
anything is installed, so it cannot depend on PyYAML or a template engine.
Open seam — not built: everything here derives from bundle.json plus a target
namespace. A Pulumi or Terraform emitter would sit beside this file consuming the
same inputs; nothing above it assumes the artifact is YAML.
"""
import json
import re
import sys
import zlib
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
UI = ROOT / "rig-ui"
# Files embedded into the ConfigMap, mounted read-only at /src in the pod and
# copied into vite's layout at start (see rig-ui/k8s.yaml). Flat on purpose:
# ConfigMap keys cannot contain '/'.
EMBEDDED = {
"bundle.json": ROOT / "bundle.json",
"package.json": UI / "package.json",
"vite.config.js": UI / "vite.config.js",
"index.html": UI / "index.html",
"main.js": UI / "src" / "main.js",
"style.css": UI / "src" / "style.css",
}
def slug(name: str) -> str:
"""Reduce a folder name to a DNS label, matching ctrl/bundle.sh's rule."""
out = re.sub(r"[^a-z0-9-]", "-", name.lower()).strip("-")
return out or "rig-bundle"
def block(text: str, indent: int) -> str:
"""Indent a file's contents for a YAML literal block scalar.
Blank lines are emitted truly empty rather than as whitespace: trailing
spaces on an otherwise blank line are legal YAML but show up as diff noise
in a committed artifact.
"""
pad = " " * indent
return "\n".join(pad + line if line.strip() else "" for line in text.splitlines())
def checksum(parts: list[str]) -> str:
"""Stable content hash of everything embedded, stamped as a label.
A mounted ConfigMap updates in place without restarting anything, so without
a visible change nothing signals that the pod is serving stale content.
"""
return str(zlib.crc32("".join(parts).encode()) & 0xFFFFFFFF)
def build(namespace: str) -> str:
contents = {}
for key, path in EMBEDDED.items():
if not path.exists():
sys.exit(f"missing input: {path}")
contents[key] = path.read_text()
# Fail loudly here rather than shipping an artifact that renders an error.
try:
json.loads(contents["bundle.json"])
except json.JSONDecodeError as exc:
sys.exit(f"bundle.json is not valid JSON: {exc}")
app = (UI / "k8s.yaml").read_text()
app = app.replace("__RIG_NAME__", namespace)
data = "\n".join(
f" {key}: |\n{block(text, 4)}" for key, text in sorted(contents.items())
)
return f"""# GENERATED by ctrl/manifest.py — do not edit.
# Regenerate with: make manifest
#
# Self-contained: applies as-is to any cluster, local kind or external.
# kubectl apply -f this-file.yaml
#
# Namespace carries the identity, so several rigs coexist in one cluster.
apiVersion: v1
kind: Namespace
metadata:
name: {namespace}
labels:
rig.bundle/name: {namespace}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rig-ui
namespace: {namespace}
labels:
rig.bundle/checksum: "{checksum(list(contents.values()))}"
data:
{data}
---
{_namespaced(app, namespace)}
"""
def _namespaced(doc: str, namespace: str) -> str:
"""Add `namespace:` to each resource so the artifact applies without -n.
rig-ui/k8s.yaml omits it on purpose — applied by hand it should land in
whatever namespace you choose. Pinning it belongs to the generated artifact,
which has to be self-contained.
"""
return re.sub(
r"^(metadata:\n(?:[ \t]+.*\n)*?)([ \t]+)(name: rig-ui)$",
lambda m: f"{m.group(1)}{m.group(2)}{m.group(3)}\n{m.group(2)}namespace: {namespace}",
doc.strip(),
flags=re.MULTILINE,
)
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else slug(ROOT.name)
sys.stdout.write(build(target))

View File

@@ -0,0 +1,406 @@
# GENERATED by ctrl/manifest.py — do not edit.
# Regenerate with: make manifest
#
# Self-contained: applies as-is to any cluster, local kind or external.
# kubectl apply -f this-file.yaml
#
# Namespace carries the identity, so several rigs coexist in one cluster.
apiVersion: v1
kind: Namespace
metadata:
name: sample-rig
labels:
rig.bundle/name: sample-rig
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rig-ui
namespace: sample-rig
labels:
rig.bundle/checksum: "2074194964"
data:
bundle.json: |
{
"_comment": "What this bundle contains. Single source of truth — the landing page renders THIS file, so adding an entry here is the only edit needed. Deliberately FLAT: standalone tools and rigs, with none of soleprint's internal hierarchy (no artery/atlas/station layering). Nothing here is sensitive; the real architecture connects separately.",
"bundle": {
"name": "sample-rig",
"description": "Non-sensitive sample bundle. Proves the kind install works and shows what ships.",
"sensitive": false
},
"tools": [
{
"name": "modelgen",
"summary": "Generate models from config",
"standalone": true
},
{
"name": "datagen",
"summary": "Generate test data from rig-owned generators",
"standalone": true
},
{
"name": "graphgen",
"summary": "Generate navigable model graphs",
"standalone": true
},
{
"name": "tester",
"summary": "HTTP contract test runner — one suite, any environment",
"standalone": true
},
{
"name": "databrowse",
"summary": "SQL data browser",
"standalone": true
},
{
"name": "sbwrapper",
"summary": "Sandbox wrapper",
"standalone": true
}
],
"rigs": [
{
"name": "sample-rig",
"summary": "This bundle — a minimal, copyable environment",
"active": true
}
],
"next": [
"Point MANIFESTS_DIR at the real manifests to connect the actual architecture.",
"Real k8s files are versioned separately and are not part of this bundle."
]
}
index.html: |
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>IT WORKS</title>
</head>
<body>
<main id="app"></main>
<script type="module" src="/src/main.js"></script>
</body>
</html>
main.js: |
import "./style.css";
/* The IT WORKS page: renders bundle.json as the list of what shipped.
*
* Plain vite, no framework — this is a complement to the rig, not part of it,
* and it should stay small enough that nobody has to adopt a stack to read it.
*
* bundle.json is fetched at runtime rather than imported, so the same built app
* serves whatever rig it was copied into. Editing the ConfigMap changes the page
* without rebuilding.
*
* Styling is a handful of rules on purpose. The real visual identity lives in
* the soleprint UI package; nothing here should grow into a theme.
*/
const esc = (s) =>
String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const tag = (text, on = false) =>
`<span class="tag${on ? " on" : ""}">${esc(text)}</span>`;
function items(list, activeKey) {
if (!list?.length) return `<li><span class="summary">nothing listed</span></li>`;
return list
.map((it) => {
const tags = [
it.standalone ? tag("standalone") : "",
it.state ? tag(it.state) : "",
activeKey && it[activeKey] ? tag("active", true) : "",
].join("");
return `<li><span class="name">${esc(it.name ?? "?")}</span>
<span class="summary">${esc(it.summary ?? "")}</span>${tags}</li>`;
})
.join("");
}
/* Cluster state, when there is any to show.
*
* Fetched separately and allowed to fail: the bundle listing is the point, and a
* rig with no cluster reachable is a normal state, not an error. Renders nothing
* at all when absent.
*
* When the payload says `mocked`, say so loudly. This exists to demo the UI on a
* machine where kind will not run — and a demo that looks live but is not is
* worse than one that admits it. */
function clusterSection(c) {
if (!c) return "";
const m = c.cluster ?? {};
const banner = c.mocked
? `<p class="mock">mocked — no cluster was queried; these are canned values</p>`
: "";
const meta = [m.context, m.k8s, m.profile ? `profile ${m.profile}` : "",
m.nodes ? `${m.nodes} node${m.nodes > 1 ? "s" : ""}` : ""]
.filter(Boolean).join(" · ");
return `
<h2>Cluster${c.mocked ? " (mocked)" : ""}</h2>
${banner}
${meta ? `<p class="sub">${esc(meta)}</p>` : ""}
<ul>${items(c.workloads)}</ul>
<h2>Services (${c.services?.length ?? 0})</h2>
<ul>${items(c.services)}</ul>`;
}
function render(b, name, cluster) {
const meta = b.bundle ?? {};
const next = (b.next ?? []).map((n) => `<li>${esc(n)}</li>`).join("");
return `
<h1><span class="ok">IT WORKS</span> — ${esc(name || meta.name || "rig")}</h1>
<p class="sub">${esc(meta.description ?? "")}</p>
<h2>Tools (${b.tools?.length ?? 0})</h2>
<ul>${items(b.tools)}</ul>
<h2>Rigs (${b.rigs?.length ?? 0})</h2>
<ul>${items(b.rigs, "active")}</ul>
${clusterSection(cluster)}
${next ? `<div class="next"><ul>${next}</ul></div>` : ""}`;
}
const app = document.getElementById("app");
const json = (path, required) =>
fetch(path).then((r) => {
if (r.ok) return r.json();
if (required) throw new Error(`${path} -> HTTP ${r.status}`);
return null; // optional: absent is a normal state, not an error
}).catch((err) => {
if (required) throw err;
return null;
});
Promise.all([json("/bundle.json", true), json("/cluster.mock.json", false)])
// RIG_NAME is injected by vite from the pod env, so two rigs sharing a
// cluster are distinguishable even if a copied bundle.json kept its old name.
.then(([b, cluster]) => {
app.innerHTML = render(b, import.meta.env.VITE_RIG_NAME, cluster);
})
.catch((err) => {
app.innerHTML = `<h1 class="err">bundle unavailable</h1>
<p class="sub">${esc(err.message)}</p>`;
});
package.json: |
{
"name": "rig-ui",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5173",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0 --port 5173"
},
"devDependencies": {
"vite": "^6"
}
}
style.css: |
/* Minimal, self-contained. The real visual identity ships with the soleprint UI
package, which is a separate artifact — nothing here should grow into a theme. */
body {
margin: 0;
padding: 2.5rem 1.5rem;
background: #0d0d0f;
color: #e8e8f0;
font: 14px/1.6 ui-monospace, "JetBrains Mono", Menlo, monospace;
}
main { max-width: 52rem; margin: 0 auto; }
h1 { margin: 0; font-size: 1.6rem; letter-spacing: 0.02em; }
h1 .ok { color: #3ecf8e; }
h1.err { color: #f06565; }
.sub { color: #8888a0; margin: 0.35rem 0 2.25rem; }
h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: #8888a0;
margin: 2rem 0 0.75rem;
font-weight: 600;
}
ul { list-style: none; margin: 0; padding: 0; }
li {
display: flex;
gap: 0.75rem;
align-items: baseline;
padding: 0.5rem 0.75rem;
border: 1px solid #2e2e38;
border-radius: 6px;
margin-bottom: 0.4rem;
background: #16161a;
}
.name { font-weight: 600; min-width: 9rem; }
.summary { color: #8888a0; flex: 1; }
.tag {
font-size: 0.7rem;
padding: 0.1rem 0.45rem;
border-radius: 3px;
background: #26262f;
color: #8888a0;
white-space: nowrap;
}
.tag.on { background: #3ecf8e; color: #0d0d0f; }
.next {
color: #555568;
font-size: 0.8rem;
margin-top: 2.5rem;
border-top: 1px solid #2e2e38;
padding-top: 1rem;
}
.next li {
display: list-item;
border: 0;
background: none;
padding: 0.15rem 0;
margin: 0 0 0 1.1rem;
list-style: disc;
}
/* Mocked-data banner. Deliberately loud: this only appears when the cluster
payload is canned, and a demo that looks live but is not is worse than one
that says so. */
.mock {
margin: 0 0 0.75rem;
padding: 0.4rem 0.75rem;
border: 1px dashed #f5a623;
border-radius: 6px;
color: #f5a623;
font-size: 0.8rem;
}
vite.config.js: |
import { defineConfig } from "vite";
/* Serves on 0.0.0.0 so the pod is reachable through the Service, and allows any
* Host header because the address is assigned at runtime (MetalLB locally, a
* cloud load balancer on EKS) and is never known at build time. */
export default defineConfig({
server: { host: "0.0.0.0", port: 5173, strictPort: true, allowedHosts: true },
preview: { host: "0.0.0.0", port: 5173, strictPort: true, allowedHosts: true },
});
---
# How to plug the UI into whatever k8s you generated. THIS IS THE WHOLE THING:
# one Pod running the vite app, one Service to reach it.
#
# Optional by design. The UI complements a rig; it is not part of the end
# product, and a rig is complete and useful without it. Apply this only when you
# want the listing:
#
# kubectl apply -n <your-namespace> -f rig-ui/k8s.yaml
#
# A bare Pod, not a Deployment — this is a dev-loop convenience, not a workload
# to keep alive. If it dies you re-apply it; nothing depends on it staying up.
#
# The app and bundle.json arrive as a ConfigMap named `rig-ui`, which
# ctrl/manifest.py generates from the folder. Nothing is baked into an image, so
# editing bundle.json and re-applying is the whole update cycle.
apiVersion: v1
kind: Pod
metadata:
name: rig-ui
namespace: sample-rig
labels:
app: rig-ui
spec:
containers:
- name: vite
image: node:22-alpine
workingDir: /app
# npm install at start: no image to build and no registry to publish to,
# which is the point of a minimal plug-in. It needs egress to a registry —
# on a locked-down cluster point npm at the internal one, or bake an image
# instead. Nothing else here changes if you do.
command: ["sh", "-c"]
# A ConfigMap mounts flat (keys cannot contain '/'), so the files are
# placed into vite's expected layout here. bundle.json goes to public/
# because that is what vite serves at /bundle.json, which is where the
# app fetches it.
args:
- |
mkdir -p /app/src /app/public &&
cp /src/package.json /src/vite.config.js /src/index.html /app/ &&
cp /src/main.js /src/style.css /app/src/ &&
cp /src/bundle.json /app/public/ &&
npm install --no-audit --no-fund &&
npm run dev
env:
# Rendered in the heading so two rigs sharing a cluster stay
# distinguishable. Set from the namespace by ctrl/manifest.py.
- name: VITE_RIG_NAME
value: sample-rig
ports:
- name: http
containerPort: 5173
volumeMounts:
# /src is read-only from the ConfigMap; the app is copied to a writable
# /app because npm install has to create node_modules.
- name: rig-ui
mountPath: /src
- name: app
mountPath: /app
readinessProbe:
httpGet: { path: /, port: 5173 }
# npm install decides how long this takes, and it is the slow part.
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 30
resources:
requests: { memory: 128Mi, cpu: 50m }
limits: { memory: 512Mi }
volumes:
- name: rig-ui
configMap:
name: rig-ui
- name: app
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: rig-ui
namespace: sample-rig
labels:
app: rig-ui
# No annotations, deliberately — see k8s/app.yaml. The target is EKS but this
# stays VPC-agnostic: no subnets, no security groups, no -scheme, no -type.
# A bare LoadBalancer is what lets one manifest work on kind and on EKS.
spec:
type: LoadBalancer
selector:
app: rig-ui
ports:
- name: http
port: 80
targetPort: 5173
protocol: TCP
# Pinned, because a LoadBalancer Service also allocates a NodePort and
# this is the only address that works everywhere.
#
# On WSL the MetalLB address is on a docker bridge INSIDE the Linux VM,
# and Windows has no route to it — the page looks broken while the
# cluster is perfectly healthy. 30080 is what rig's `hostport` ingress
# mode publishes to the host, so this is reachable at
# localhost:$HTTP_PORT from a Windows browser with nothing configured.
#
# Costs nothing elsewhere: MetalLB still assigns an external IP on Linux,
# and on EKS the load balancer targets this NodePort anyway. One Service,
# no per-environment branch.
#
# A pinned NodePort is cluster-unique, so two rigs must live in separate
# clusters — which is how they are run anyway.
nodePort: 30080

3
rig/sample-rig/rig-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
public/
dist/

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>IT WORKS</title>
</head>
<body>
<main id="app"></main>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,108 @@
# How to plug the UI into whatever k8s you generated. THIS IS THE WHOLE THING:
# one Pod running the vite app, one Service to reach it.
#
# Optional by design. The UI complements a rig; it is not part of the end
# product, and a rig is complete and useful without it. Apply this only when you
# want the listing:
#
# kubectl apply -n <your-namespace> -f rig-ui/k8s.yaml
#
# A bare Pod, not a Deployment — this is a dev-loop convenience, not a workload
# to keep alive. If it dies you re-apply it; nothing depends on it staying up.
#
# The app and bundle.json arrive as a ConfigMap named `rig-ui`, which
# ctrl/manifest.py generates from the folder. Nothing is baked into an image, so
# editing bundle.json and re-applying is the whole update cycle.
apiVersion: v1
kind: Pod
metadata:
name: rig-ui
labels:
app: rig-ui
spec:
containers:
- name: vite
image: node:22-alpine
workingDir: /app
# npm install at start: no image to build and no registry to publish to,
# which is the point of a minimal plug-in. It needs egress to a registry —
# on a locked-down cluster point npm at the internal one, or bake an image
# instead. Nothing else here changes if you do.
command: ["sh", "-c"]
# A ConfigMap mounts flat (keys cannot contain '/'), so the files are
# placed into vite's expected layout here. bundle.json goes to public/
# because that is what vite serves at /bundle.json, which is where the
# app fetches it.
args:
- |
mkdir -p /app/src /app/public &&
cp /src/package.json /src/vite.config.js /src/index.html /app/ &&
cp /src/main.js /src/style.css /app/src/ &&
cp /src/bundle.json /app/public/ &&
npm install --no-audit --no-fund &&
npm run dev
env:
# Rendered in the heading so two rigs sharing a cluster stay
# distinguishable. Set from the namespace by ctrl/manifest.py.
- name: VITE_RIG_NAME
value: __RIG_NAME__
ports:
- name: http
containerPort: 5173
volumeMounts:
# /src is read-only from the ConfigMap; the app is copied to a writable
# /app because npm install has to create node_modules.
- name: rig-ui
mountPath: /src
- name: app
mountPath: /app
readinessProbe:
httpGet: { path: /, port: 5173 }
# npm install decides how long this takes, and it is the slow part.
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 30
resources:
requests: { memory: 128Mi, cpu: 50m }
limits: { memory: 512Mi }
volumes:
- name: rig-ui
configMap:
name: rig-ui
- name: app
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: rig-ui
labels:
app: rig-ui
# No annotations, deliberately — see k8s/app.yaml. The target is EKS but this
# stays VPC-agnostic: no subnets, no security groups, no -scheme, no -type.
# A bare LoadBalancer is what lets one manifest work on kind and on EKS.
spec:
type: LoadBalancer
selector:
app: rig-ui
ports:
- name: http
port: 80
targetPort: 5173
protocol: TCP
# Pinned, because a LoadBalancer Service also allocates a NodePort and
# this is the only address that works everywhere.
#
# On WSL the MetalLB address is on a docker bridge INSIDE the Linux VM,
# and Windows has no route to it — the page looks broken while the
# cluster is perfectly healthy. 30080 is what rig's `hostport` ingress
# mode publishes to the host, so this is reachable at
# localhost:$HTTP_PORT from a Windows browser with nothing configured.
#
# Costs nothing elsewhere: MetalLB still assigns an external IP on Linux,
# and on EKS the load balancer targets this NodePort anyway. One Service,
# no per-environment branch.
#
# A pinned NodePort is cluster-unique, so two rigs must live in separate
# clusters — which is how they are run anyway.
nodePort: 30080

1164
rig/sample-rig/rig-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
{
"name": "rig-ui",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 5173",
"build": "vite build",
"preview": "vite preview --host 0.0.0.0 --port 5173"
},
"devDependencies": {
"vite": "^6"
}
}

View File

@@ -0,0 +1,136 @@
import "./style.css";
/* The IT WORKS page: renders bundle.json as the list of what shipped.
*
* Plain vite, no framework — this is a complement to the rig, not part of it,
* and it should stay small enough that nobody has to adopt a stack to read it.
*
* Laid out like soleprint's templated vein pages, because it does the same job:
* name each component, list what it exposes, show what comes back. Tool chrome
* and output are styled apart on purpose (see style.css) — that separation is
* what tells you whether you are reading the tool or its result.
*
* bundle.json is fetched at runtime rather than imported, so the same built app
* serves whatever rig it was copied into. Editing the ConfigMap changes the page
* without rebuilding.
*/
const esc = (s) =>
String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const tag = (text, on = false) =>
`<span class="tag${on ? " on" : ""}">${esc(text)}</span>`;
/* Tool chrome: one bordered card per component. */
function components(list, activeKey) {
if (!list?.length)
return `<div class="component"><p>nothing listed</p></div>`;
return list
.map((it) => {
const tags = [
it.standalone ? tag("standalone") : "",
it.state ? tag(it.state) : "",
activeKey && it[activeKey] ? tag("active", true) : "",
].join("");
return `<div class="component">
<h4>${esc(it.name ?? "?")} ${tags}</h4>
<p>${esc(it.summary ?? "")}</p>
</div>`;
})
.join("");
}
/* Endpoint rows: path on the left, what it returns on the right. */
function endpoints(list) {
return list
.map(
(e) => `<li><code>${esc(e.path)}</code>
<span class="desc">${esc(e.desc)}</span></li>`
)
.join("");
}
/* Output: what the endpoint above actually returns, so the page demonstrates
itself rather than describing what a demonstration would look like. */
function example(bundle) {
const sample = {
bundle: bundle.bundle?.name,
tools: (bundle.tools ?? []).map((t) => t.name),
rigs: (bundle.rigs ?? []).map((r) => r.name),
};
return `<pre class="output">${esc(JSON.stringify(sample, null, 2))}</pre>`;
}
/* Cluster state, when there is any to show.
*
* Fetched separately and allowed to fail: the bundle listing is the point, and a
* rig with no cluster reachable is a normal state, not an error. Renders nothing
* at all when absent. When the payload says `mocked`, say so loudly. */
function clusterSection(c) {
if (!c) return "";
const m = c.cluster ?? {};
const meta = [m.context, m.k8s, m.profile && `profile ${m.profile}`,
m.nodes && `${m.nodes} node${m.nodes > 1 ? "s" : ""}`]
.filter(Boolean).join(" · ");
return `
<h2>Cluster${c.mocked ? " (mocked)" : ""}</h2>
${c.mocked ? `<p class="mock">mocked — no cluster was queried; these are canned values</p>` : ""}
${meta ? `<p class="tagline">${esc(meta)}</p>` : ""}
<div class="components">${components(c.workloads)}</div>
<h2>Services</h2>
<div class="components">${components(c.services)}</div>`;
}
function render(b, name, cluster) {
const meta = b.bundle ?? {};
const next = (b.next ?? []).map((n) => `<li>${esc(n)}</li>`).join("");
return `
<h1><span class="ok">IT WORKS</span> — ${esc(name || meta.name || "rig")}</h1>
<p class="tagline">${esc(meta.description ?? "")}</p>
<h2>Tools (${b.tools?.length ?? 0})</h2>
<div class="components">${components(b.tools)}</div>
<h2>Rigs (${b.rigs?.length ?? 0})</h2>
<div class="components">${components(b.rigs, "active")}</div>
<h2>Endpoints</h2>
<ul class="endpoints">${endpoints([
{ path: "/", desc: "this page" },
{ path: "/bundle.json", desc: "the manifest it renders" },
])}</ul>
<h2>Example — GET /bundle.json</h2>
${example(b)}
${clusterSection(cluster)}
${next ? `<div class="next"><ul>${next}</ul></div>` : ""}`;
}
const app = document.getElementById("app");
const json = (path, required) =>
fetch(path)
.then((r) => {
if (r.ok) return r.json();
if (required) throw new Error(`${path} -> HTTP ${r.status}`);
return null; // optional: absent is a normal state, not an error
})
.catch((err) => {
if (required) throw err;
return null;
});
Promise.all([json("/bundle.json", true), json("/cluster.mock.json", false)])
// RIG_NAME is injected by vite from the pod env, so two rigs sharing a
// cluster are distinguishable even if a copied bundle.json kept its old name.
.then(([b, cluster]) => {
app.innerHTML = render(b, import.meta.env.VITE_RIG_NAME, cluster);
})
.catch((err) => {
app.innerHTML = `<h1 class="err">bundle unavailable</h1>
<p class="tagline">${esc(err.message)}</p>`;
});

View File

@@ -0,0 +1,139 @@
/* Minimal and self-contained — no framework dependency.
*
* The visual language follows soleprint's templated vein pages, because this
* page does the same job: say what a component is, list what it exposes, and
* show what comes back. Two treatments, deliberately distinct:
*
* TOOL CHROME bordered cards on the darker background, accent-coloured
* titles, endpoint rows separated by rules.
* OUTPUT a lighter raised block, monospace, pre-wrap and selectable —
* it is data, not furniture, and should read as a payload.
*
* Keeping them apart matters more than either looks: it is what tells you at a
* glance whether you are reading the tool or the thing it produced. */
:root {
--bg: #0d0d0f;
--surface: #16161a;
--surface-raised: #1e1e24;
--border: #2e2e38;
--border-strong: #3d3d4a;
--text: #e8e8f0;
--muted: #8888a0;
--accent: #3ecf8e;
--accent-dim: #f5a623;
--mono: "JetBrains Mono", "Cascadia Mono", Consolas, ui-monospace, monospace;
}
body {
margin: 0;
padding: 2.5rem 1.5rem;
background: var(--bg);
color: var(--text);
font: 14px/1.6 var(--mono);
}
main { max-width: 56rem; margin: 0 auto; }
h1 { margin: 0; font-size: 1.6rem; letter-spacing: 0.02em; }
h1 .ok { color: var(--accent); }
h1.err { color: #f06565; }
.tagline { color: var(--muted); margin: 0.35rem 0 2.25rem; }
h2 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
margin: 2.25rem 0 0.75rem;
font-weight: 600;
}
/* ── tool chrome ────────────────────────────────────────────────────────── */
.components {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.75rem;
}
.component {
background: var(--bg);
border: 1px solid var(--border-strong);
border-radius: 8px;
padding: 0.75rem;
}
.component h4 {
margin: 0 0 0.25rem;
font-size: 0.95rem;
color: var(--accent);
}
.component p {
margin: 0;
font-size: 0.85rem;
color: var(--muted);
}
.endpoints { list-style: none; margin: 0; padding: 0; }
.endpoints li {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.6rem 0;
border-bottom: 1px solid var(--border-strong);
}
.endpoints li:last-child { border-bottom: none; }
.endpoints code {
background: var(--surface);
color: var(--accent);
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.endpoints .desc { color: var(--muted); font-size: 0.9rem; }
.tag {
font-size: 0.7rem;
padding: 0.1rem 0.45rem;
border-radius: 3px;
background: var(--border);
color: var(--muted);
white-space: nowrap;
}
.tag.on { background: var(--accent); color: var(--bg); }
/* ── output ─────────────────────────────────────────────────────────────── */
.output {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-word;
user-select: text;
color: var(--text);
margin: 0;
}
.output .k { color: var(--accent); }
/* Mocked-data banner. Loud on purpose: it only appears when the payload is
canned, and a demo that looks live but is not is worse than one that says so. */
.mock {
margin: 0 0 0.75rem;
padding: 0.4rem 0.75rem;
border: 1px dashed var(--accent-dim);
border-radius: 6px;
color: var(--accent-dim);
font-size: 0.8rem;
}
.next {
color: #555568;
font-size: 0.8rem;
margin-top: 2.5rem;
border-top: 1px solid var(--border);
padding-top: 1rem;
}
.next ul { margin: 0; padding: 0 0 0 1.1rem; }
.next li { list-style: disc; padding: 0.15rem 0; }

View File

@@ -0,0 +1,9 @@
import { defineConfig } from "vite";
/* Serves on 0.0.0.0 so the pod is reachable through the Service, and allows any
* Host header because the address is assigned at runtime (MetalLB locally, a
* cloud load balancer on EKS) and is never known at build time. */
export default defineConfig({
server: { host: "0.0.0.0", port: 5173, strictPort: true, allowedHosts: true },
preview: { host: "0.0.0.0", port: 5173, strictPort: true, allowedHosts: true },
});