diff --git a/src/content/docs/air-gapped-installation.mdx b/src/content/docs/air-gapped-installation.mdx
index 3c84875..2a99b6a 100644
--- a/src/content/docs/air-gapped-installation.mdx
+++ b/src/content/docs/air-gapped-installation.mdx
@@ -70,7 +70,8 @@ Collect **all of the following by digest** (immutable hash references, not mutab
- `ghcr.io/valgulnecron/gameplane/tunnel-tailscale` (via `tunnelImages.tailscale`, relay tunnel components, optional)
- `ghcr.io/valgulnecron/gameplane/tunnel-playit` (via `tunnelImages.playit`, relay tunnel components, optional)
- **Game modules** — all bundles from `ghcr.io/valgulnecron/gameplane-modules`, versioned per your module source config
-- **CRD manifests** — the eight CRD schemas (bundled in the chart)
+- **CRD manifests** — the CRD schemas (bundled in the chart)
+- **CRD-apply hook image** — the `kubectl` image behind `crds.autoApply.image` (default `registry.k8s.io/kubectl:v1.36.3`). The hook runs on every `helm upgrade`, and also on a `helm install` over Gameplane CRDs that an earlier, uninstalled release left behind. A genuinely fresh first install does not run it. Any image with `kubectl` on its `PATH` works.
- **Cosign public keys** — `cosign.pub` (current key, ECDSA P-256) and `cosign-legacy.pub` (pre-rotation, Ed25519)
- **Signatures** — Cosign signatures (recorded in Rekor for v0.2.0-beta.8 onward) and SBOMs, available as separate OCI artifacts in the same registry namespace
@@ -218,6 +219,12 @@ operator:
configInitImage: registry.internal.corp/busybox # instead of busybox
resticImage: registry.internal.corp/restic:latest # instead of restic/restic
sentinelImage: registry.internal.corp/gameplane/sentinel:0.2.0-beta.8
+
+# Mirror the CRD-apply hook's kubectl image (needed at upgrade time and on
+# an install over leftover CRDs)
+crds:
+ autoApply:
+ image: registry.internal.corp/kubectl:v1.36.3
```
### Pre-provision dependencies
diff --git a/src/content/docs/api-realtime-development.mdx b/src/content/docs/api-realtime-development.mdx
index 48dad1f..2fba442 100644
--- a/src/content/docs/api-realtime-development.mdx
+++ b/src/content/docs/api-realtime-development.mdx
@@ -55,7 +55,7 @@ Authorization is role-based with three built-in roles (admin, operator, viewer)
Every request has a logical cluster and namespace. The API resolves these via:
-- **Cluster:** extracted from `?cluster={name}` query param (defaults to `local` — the install cluster), validated against the Cluster CRD registry
+- **Cluster:** extracted from `?cluster={name}` query param (defaults to `local` — the install cluster), validated against the Cluster CRD registry. Collection reads under `/fleet/*` (v0.3.0) instead combine every authorized scope, with optional cluster and namespace filters; each item carries an explicit `{cluster, namespace, name, uid}` target, and unavailable or truncated scopes are reported rather than silently dropped.
- **Namespace:** extracted from the URL path (`/servers/{name}` → namespace from server lookup) or defaulted to the user's home namespace
Scope middleware (`internal/scope/`) extracts both before the handler runs. If a user tries to access a namespace they lack permission for, the handler returns `403 Forbidden` with a safe message (no namespace-existence leakage).
diff --git a/src/content/docs/architecture.mdx b/src/content/docs/architecture.mdx
index 362651f..c8ff333 100644
--- a/src/content/docs/architecture.mdx
+++ b/src/content/docs/architecture.mdx
@@ -17,7 +17,7 @@ import ArchFlow from "../../components/docs/ArchFlow.astro";
{ icon: "boxes", title: "Kubernetes", subtitle: "Cluster" },
{ icon: "cpu", title: "Agent", subtitle: "Sidecar" },
]}
- caption="Console and log traffic flow API ↔ Agent directly over mTLS; the Operator provisions and reconciles both."
+ caption="In the home cluster, console and log traffic flow API ↔ Agent directly over mTLS; the Operator provisions and reconciles both. Remote clusters go through an optional gateway (below)."
/>
## Why two control planes
@@ -32,6 +32,28 @@ A dashboard action cascades: API → Operator → StatefulSet → Agent → hear
ModuleSource CRs declare where templates come from — OCI registry, git, HTTP, local, or dashboard upload.
+## Multiple clusters and the gateway
+
+One central API and dashboard can manage independent clusters registered as `Cluster` resources. Each target cluster runs its own operator, and optionally a private gateway built from the API image. The gateway has no database, no user login and no browser or admin routes. Users authenticate only to the central API, which stays the authorization authority.
+
+
+
+- **Resource requests** carry `?cluster=` and go to that cluster's Kubernetes client. Omitting it targets the built-in `local` cluster. Collection reads under `/fleet/*` instead combine authorized scopes by default, with optional cluster and namespace filters; each item keeps its target identity and permissions, and partial results report unavailable or truncated scopes.
+- **Pod logs and PTY attach** resolve the Kubernetes client for each connection from the registry; unknown or removed clusters fail closed. The API checks the GameServer, StatefulSet and Pod owner UIDs first. Kubernetes log and attach APIs address Pods by name, so this is a preflight check, not an atomic guarantee across deletion and recreation.
+- **RCON, game log files, files, players, live status and agent-based mods** use the selected cluster's gateway. It verifies the central peer certificate, its own cluster ID, the GameServer UID and the owned agent Service, then reaches the agent over local DNS and agent mTLS. Versioned agent routes verify the UID on the final hop; older agents fail closed. Browser cookies, bearer tokens and CSRF headers are never forwarded.
+- **Capture downloads and cleanup** use a separate gateway route bound to both the GameServer and NetworkCapture UIDs; the sidecar verifies a persisted file identity.
+- **Existing local installs** keep direct API-to-agent connections. Remote gateway access needs both private gateway reachability and direct Kubernetes API access from the central API; it does not tunnel Kubernetes operations or replicate game storage.
+
+The `Cluster` health phase reports Kubernetes connectivity and does not probe the gateway. See the [remote agent gateway guide](../remote-agent-gateway/) for installation and the [multi-cluster guide](../multi-cluster/) for what works across clusters. The gateway ships in v0.3.0.
+
## Optional components
Beyond the core operator, API, and agent, Gameplane ships optional services for specialized tasks. Optional components (mcp-server, telemetry-receiver, audit-syslog-bridge) are covered in [Extension services](../extension-services/). For a full catalog of all CRDs and their fields, see the [CRD catalog](../crd-catalog/).
diff --git a/src/content/docs/audit-observability-admin.mdx b/src/content/docs/audit-observability-admin.mdx
index 77b2c77..e90f684 100644
--- a/src/content/docs/audit-observability-admin.mdx
+++ b/src/content/docs/audit-observability-admin.mdx
@@ -36,13 +36,9 @@ Use fleet metrics for symptom detection, Kubernetes events for control-plane cau
Configure event sinks, instance identity, external URL, update channel, and build information as shared operational contracts.
-
- Notifications (webhook, ntfy, Slack, SMTP) arrive in v0.3.0. Upgrade when available to route server lifecycle events, backups, and alerts to your team's incident channels.
-
-
### Notification sinks
-Once available, multiple sink kinds support event routing:
+Multiple sink kinds support event routing:
- **Webhook**: POST each event as JSON to a URL (e.g., a custom integration endpoint)
- **ntfy.sh**: POST to a public or self-hosted ntfy topic with optional authentication
diff --git a/src/content/docs/backups-recovery.mdx b/src/content/docs/backups-recovery.mdx
index 6eb911a..47b111d 100644
--- a/src/content/docs/backups-recovery.mdx
+++ b/src/content/docs/backups-recovery.mdx
@@ -22,7 +22,7 @@ Create recoverable snapshots, schedule retention-aware backups, and restore into
## Create and inspect backups
-Use per-server or global views to start snapshots, filter phases, and inspect destination, size, timestamps, and errors.
+Use per-server or global views to start snapshots, filter phases, and inspect destination, size, timestamps, and errors. From v0.3.0, the Backups page groups its filters in a popover on the right of the search row: Location (when several clusters are registered), Server and the current tab's Phase. Selections apply when you click Apply.
- The changes below (Unreleased) are included in v0.3.0-rc.1, tagged but not yet published as a release. They will be available in v0.3.0 GA when it ships.
+ The changes below (Unreleased) include v0.3.0-rc.1 (tagged 2026-09-23, not published) and v0.3.0-rc.2 (in development). They will be available in v0.3.0 GA when it ships.
+ **Multicluster gateways and unified server management:** an optional private mTLS agent gateway lets one dashboard and API manage authorized game servers across independent clusters, with optional location filters and routed console, files, players, mods and captures.
+ **User theme customization:** Settings → Theme & Appearance offers preset themes (Modern Pink, Legacy Orange) and custom color selection with live WCAG AA contrast validation.
**Network capture:** admin-only, on-demand packet capture from running game pods as ephemeral sidecars, exported as PCAPNG (Wireshark-compatible).
**Module authoring toolkit:** gp-module CLI and web dashboard for scaffolding, validating, previewing, and packaging custom game modules.
- **User theme customization:** preset themes (Modern Pink, Legacy Orange) and custom accent/surface colors with WCAG AA contrast validation.
- **13 new top-steam-game modules** (FiveM, Farming Simulator 25, Euro Truck Simulator 2, Mount & Blade Bannerlord, tModLoader, Team Fortress 2, BeamMP, Left 4 Dead 2, The Isle, ARK, Arma Reforger, Hell Let Loose, Squad).
+ **PostgreSQL end-to-end support:** the PostgreSQL driver now works end to end, with its own migration set, placeholder rebinding, and database tests in CI. It remains experimental: keep a single API replica.
**Install-time configuration:** Helm-seeded OIDC role mappings and default StorageClass for game-data PVCs.
**Share link expiry options:** no-expiry, 15/30/60/90-day presets, or custom date picker.
**Security audit remediation:** share link isolation, permission checks on GameServer mutations, Secret/ConfigMap reference validation, and symlink/zip-slip guards hardened across mod extraction.
+ **Servers page multi-namespace support:** the Servers list now shows servers from every namespace the viewer may read, not only the default games namespace.
+ **Playit tunnel improvements:** NetworkPolicy egress widened to reach relay, and playit-assigned addresses now show in status.
**REST and CLI console protocols** for games with HTTP admin APIs (FiveM) and PTY-only games (Terraria).
+ **14 new game modules** (FiveM, Farming Simulator 25, Euro Truck Simulator 2, Mount & Blade Bannerlord, tModLoader, Team Fortress 2, BeamMP, Left 4 Dead 2, The Isle, ARK: Survival Evolved, Arma Reforger, Hell Let Loose, Squad and Nuclear Option), for 30 in total (16 in v0.2.0-beta.8).
diff --git a/src/content/docs/ci-releases-signing.mdx b/src/content/docs/ci-releases-signing.mdx
index b24d72c..deb908b 100644
--- a/src/content/docs/ci-releases-signing.mdx
+++ b/src/content/docs/ci-releases-signing.mdx
@@ -86,6 +86,8 @@ A `v` tag builds and signs images, packages the OCI chart, creates release notes
Release images carry Cosign signatures recorded in the public Sigstore Rekor transparency log. Verify any released or edge image with the `cosign.pub` key at the repo root. Pre-rotation releases (v0.2.0-beta.7 and earlier) used the retired Ed25519 key and require `cosign-legacy.pub` with the `--insecure-ignore-tlog=true` flag (see [Key Rotation](../credential-rotation/) for trust continuity).
+Signing is mandatory and fail-closed: without `COSIGN_PRIVATE_KEY` the release job fails before anything is pushed. Every signing job (`release.yaml`, `publish-edge.yaml`, `images.yaml` and `republish-modules.yaml`) runs in the `release-signing` environment, whose deployment policy admits only `master` and `v*` tags, so a run on any other ref never receives the key. Images are pushed by digest, signed and verified, and only then tagged, so a published tag always names a signed image.
+
---
## Next guide
diff --git a/src/content/docs/cluster-nodes-kubeconfig.mdx b/src/content/docs/cluster-nodes-kubeconfig.mdx
index 6db5ee2..28bdd25 100644
--- a/src/content/docs/cluster-nodes-kubeconfig.mdx
+++ b/src/content/docs/cluster-nodes-kubeconfig.mdx
@@ -139,7 +139,7 @@ Mint a short-lived token for the intended cluster, run the generated join proced
- **Lifetime:** 24 hours from creation.
- **Single-use:** The token expires automatically after use; no revocation needed.
-- **Per-cluster:** Each bootstrap token is tied to the cluster the API server was reached from. Multi-cluster installs require generating a token for each control plane.
+- **Per-cluster:** Each bootstrap token is tied to the cluster the API server was reached from. Multi-cluster installs require generating a token for each control plane. Node-join token creation and kubeconfig issuance are local-cluster operations and are unavailable while viewing a remote cluster; remote node inventory instead needs the read permissions listed in the [remote agent gateway guide](../remote-agent-gateway/#grant-inventory-permissions).
- **CA cert hash:** The discovery token CA cert hash (`--discovery-token-ca-cert-hash`) pins the cluster's root CA and prevents man-in-the-middle attacks. It is always required.
### Troubleshooting join failures
@@ -166,4 +166,5 @@ Mint a short-lived token for the intended cluster, run the generated join proced
- **[Kubernetes Maintenance](../kubernetes-maintenance/)** — node upgrades, drain procedures, security patches.
- **[High Availability](../high-availability/)** — control plane redundancy, etcd backup, failover.
-- **[Multi-cluster Topology](../multi-cluster/)** — remote cluster registration and federation.
+- **[Multi-cluster Topology](../multi-cluster/)** — remote cluster registration and the unified cross-cluster dashboard.
+- **[Remote Agent Gateway](../remote-agent-gateway/)** — install and register the per-cluster gateway.
diff --git a/src/content/docs/comparison.mdx b/src/content/docs/comparison.mdx
index 53294b0..c91e75a 100644
--- a/src/content/docs/comparison.mdx
+++ b/src/content/docs/comparison.mdx
@@ -13,12 +13,12 @@ Pterodactyl and CubeCoders AMP are control panels; Agones is a Kubernetes operat
| --- | --- | --- | --- | --- |
| Deployment/runtime model | Kubernetes-native CRDs and controller-runtime operator; scales from k3s homelab to multi-node clusters. [BETA] | Self-hosted Panel with PHP/MySQL/Redis dependencies; Wings backend manages Docker containers. | Web-based control panel supporting Windows (native) and Linux (Debian 10+). | Kubernetes-native library extending K8s with GameServer/Fleet CRDs. |
| Scaling & auto-sleep | Opt-in idle auto-sleep with configurable wake windows, manual wake button, or wake-on-connect. Minecraft/Terraria full protocol support; others use packet heuristics. [optional] | Multi-node support with cron-based power scheduling; no dedicated idle/auto-sleep feature. | Instance Automatic Sleep feature; multi-server architecture with controller managing instances. | Fleet autoscaling via buffer/webhook strategies; no idle/sleep state. |
-| Inbound connectivity (NAT traversal, relay) | Integrated frp, Tailscale, playit relay sidecars; playit mappings user-managed via playit.gg account. [optional; disabled by default] | No integrated relay sidecars; manual proxy/port forwarding configuration required. | not publicly documented (checked 2026-09-02) | No relay or NAT traversal features documented. |
+| Inbound connectivity (NAT traversal, relay) | Integrated frp, Tailscale, playit relays (a separate tunnel pod per server); playit mappings user-managed via playit.gg account. [optional; disabled by default] | No integrated relay sidecars; manual proxy/port forwarding configuration required. | not publicly documented (checked 2026-09-02) | No relay or NAT traversal features documented. |
| Backup and restore | Restic snapshots to S3-compatible storage; on-demand or cron-scheduled via BackupSchedule; one-click restore. | Wings (local, default) or S3-compatible backup drivers; cron scheduling and on-demand backups. | not publicly documented (checked 2026-09-02) | not applicable (Agones is a Kubernetes operator library) |
| Access control & authentication | Local argon2id + OIDC (Keycloak/Google/GitHub); three built-in roles (admin/operator/viewer); custom roles supported. | 2FA configurable per account or admin-only; subuser management via Artisan CLI. | Role-based access control; OIDC single-sign-on in Advanced Edition. | not applicable (Agones is a Kubernetes operator library) |
-| Game template distribution | OCI bundles via ModuleSource (git/http/oci/local/upload); optional cosign signature verification per source. 16 ready-to-use templates shipped. | Community eggs repository (eggs.pterodactyl.io) with nests and custom egg creation support. | Customizable templates framework; community-contributed templates available via external repositories. | not applicable (Agones is a Kubernetes operator library) |
-| Multi-tenancy & multi-cluster | Cluster CRD for remote registration/monitoring; console/log streaming local-cluster only. [local cluster only for streaming] | Single Panel managing multiple nodes; no documented remote cluster or cross-cluster streaming. | Multi-server management via controller architecture; multi-tenancy not supported in AMP 2 (planned for AMP 3). | Multi-cluster allocation via GameServerAllocationPolicy; allocator service with mTLS authentication. |
+| Game template distribution | OCI bundles via ModuleSource (git/http/oci/local/upload); optional cosign signature verification per source. 30 ready-to-use templates on master (16 in v0.2.0-beta.8; the other 14 arrive in v0.3.0). | Community eggs repository (eggs.pterodactyl.io) with nests and custom egg creation support. | Customizable templates framework; community-contributed templates available via external repositories. | not applicable (Agones is a Kubernetes operator library) |
+| Multi-tenancy & multi-cluster | Cluster CRD for remote registration and health checks; in v0.2.0-beta.8 console/log streaming is local-cluster only. From v0.3.0: remote Pod logs/PTY, an optional private gateway for console, files, players, mods and UID-bound capture downloads/cleanup, modpack and ID-list configuration on the selected cluster, and a unified dashboard with location filters. Requires matching gateway, operator, agent and capture-sidecar versions. | Single Panel managing multiple nodes; no documented remote cluster or cross-cluster streaming. | Multi-server management via controller architecture; multi-tenancy not supported in AMP 2 (planned for AMP 3). | Multi-cluster allocation via GameServerAllocationPolicy; allocator service with mTLS authentication. |
| Licensing | GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). | MIT License (Panel and Wings). | Proprietary; per-instance tiers (Standard/Professional/Advanced/Enterprise). | Apache License 2.0. |
| Target operator scope (self-hosted vs. managed SaaS) | Self-hosted only; runs on Kubernetes (k3s, kubeadm, managed services); no managed SaaS offering. | Self-hosted only; requires Linux system capable of running Docker containers. | Self-installed on user hardware (Windows or Linux); no managed SaaS version. | Self-hosted operator software; runs anywhere Kubernetes can run. |
-Sources for every cell: [docs/comparison-sources.md](https://github.com/ValgulNecron/Gameplane/blob/master/docs/comparison-sources.md) — Checked 2026-09-02.
+Sources for every cell: [docs/comparison-sources.md](https://github.com/ValgulNecron/Gameplane/blob/master/docs/comparison-sources.md) — Checked 2026-10-02.
diff --git a/src/content/docs/console-events-logs.mdx b/src/content/docs/console-events-logs.mdx
index f98a9d2..42fb13b 100644
--- a/src/content/docs/console-events-logs.mdx
+++ b/src/content/docs/console-events-logs.mdx
@@ -77,6 +77,10 @@ The live console is an operational channel, not a substitute for configuration o
Both modes appear under the same **Console** tab in the dashboard. The template's `spec.consoleMode` field (or the default when `spec.rcon` is configured) determines which mode is available.
+### Command history
+
+The console input bar keeps the last 100 commands you sent (v0.3.0). Press ArrowUp to recall earlier commands and ArrowDown to move forward; pressing ArrowDown past the newest command restores the line you had not sent yet. History lasts only while the Console tab stays open and is never saved.
+
## Diagnose with logs
Switch between container and game logs, follow live output, and export evidence before changing state.
@@ -89,6 +93,8 @@ Container startup logs (stdout/stderr from the game process) stream via the Kube
The game's primary log file is configured in the template's `spec.logPath` (e.g., `/data/logs/latest.log` for Minecraft). The agent sidecar tails this file and streams it to the **Game log** source of the Logs tab via the `/logs/tail` WebSocket endpoint. Download the full log file without interrupting the stream using the **Download** button.
+**Log replay on tab open (v0.3.0):** When you open the Logs tab, the dashboard replays the last 500 lines of the game log before following new output, so the tab no longer opens empty. The agent caps any replay at 20,000 lines and 4 MiB, still delivers lines written while the history is sent, and does not replay again after a log rotation.
+
### Failed start triage
/gameplane-module.git
+```
+
+The full contribution guide, including per-component test commands and the release process, is in [contributing.md on GitHub](https://github.com/ValgulNecron/Gameplane/blob/master/docs/contributing.md). Release signing is covered in [CI, releases, and signing](../ci-releases-signing/).
diff --git a/src/content/docs/dashboard-tour.mdx b/src/content/docs/dashboard-tour.mdx
index dcfd3b7..31b5be5 100644
--- a/src/content/docs/dashboard-tour.mdx
+++ b/src/content/docs/dashboard-tour.mdx
@@ -14,27 +14,27 @@ import DocsCallout from "../../components/docs/DocsCallout.astro";
import DocsSectionList from "../../components/docs/DocsSectionList.astro";
import DocsQuickReference from "../../components/docs/DocsQuickReference.astro";
-Start with one cluster selected. Every list, metric, and action inherits that scope.
+The dashboard brings every server you are authorized to see into one view. Start from the fleet overview, narrow it with filters, and open a server to work on it.
-
- Start with one cluster selected. Every list, metric, and action inherits that scope.
+
+ In v0.2.0-beta.8 a top-bar cluster selector scopes every list, metric, and action to one cluster. From v0.3.0 the dashboard, Servers, Backups and Search combine every registered cluster you can access, and an optional Location filter narrows them.
## Orientation
-The dashboard is a cluster-scoped control surface. Use the top bar to change context without losing your place.
+Opening a server pins it to its own cluster and namespace, so changing a list filter never retargets the server you are working on.
diff --git a/src/content/docs/helm-values-reference.mdx b/src/content/docs/helm-values-reference.mdx
index 7e770e4..392197f 100644
--- a/src/content/docs/helm-values-reference.mdx
+++ b/src/content/docs/helm-values-reference.mdx
@@ -71,18 +71,48 @@ The following OIDC fields enable install-time role mapping and customization of
| Key | Type | Default | Description |
|---|---|---|---|
| `api.oidc.displayName` | string | `"Single sign-on"` | Label for the OIDC login button shown on the login screen (before authentication). |
-| `api.oidc.groupsClaim` | string | *(empty — defaults to "groups")* | OIDC claim name containing group or role memberships. Examples: `groups`, `roles`, `membership`, `department`. When empty, uses the default claim "groups". |
+| `api.oidc.groupsClaim` | string | *(empty — defaults to "groups")* | OIDC claim name containing group or role memberships. Examples: `groups`, `roles`, `membership`, `department`. When empty, the `groups` claim is read. This only chooses which claim is read; it does not turn group-based role mapping on or off (`roleMappings` does). Mapping is active whenever at least one `roleMappings` list is non-empty or an admin has set a mapping override in the dashboard. With neither, new OIDC users get `viewer` and existing users' roles are never re-evaluated. |
| `api.oidc.defaultRole` | string | *(empty — defaults to "viewer")* | Default dashboard role for new OIDC users when no group matches a configured mapping. Accepted: `viewer`, `operator`, `admin`, `deny` (deny = reject login). When empty, defaults to "viewer". |
| `api.oidc.roleMappings` | object | `{admin: [], operator: [], viewer: []}` | Map OIDC groups to dashboard roles at install/upgrade time. Example: `admin: ["gameplane-admins", "ops-team"]`. DB-managed role overrides (set via dashboard) take precedence and survive Helm upgrades. |
+## API replicas and trusted proxies
+
+| Key | Type | Default | Description |
+|---|---|---|---|
+| `api.replicas` | integer | `1` | Keep at `1`. SQLite is single-writer on a ReadWriteOnce volume; with `db.driver: postgres` (experimental) the user-management lock and audit hash chain are still per-process. |
+| `api.trustedProxies` | string | loopback + RFC 1918 + link-local + ULA ranges | Comma-separated CIDRs of trusted reverse proxies. The API reads `X-Forwarded-For` only when the TCP peer is inside one of these networks; any other peer is itself the client. Used for rate limiting and audit records. The default suits in-cluster ingress. If dashboard users connect from a private network and per-client limits matter for them, narrow it to the ranges your proxies run in (for example the pod CIDR). For direct exposure without an ingress, set it to `""`. |
+
+## CRD auto-apply
+
+`helm upgrade` never updates CRDs, so the chart applies them with a hook. See [CRD updates on upgrade](../upgrades-migrations/#crd-updates-on-upgrade).
+
+| Key | Type | Default | Description |
+|---|---|---|---|
+| `crds.autoApply.enabled` | boolean | `true` | Run a pre-install/pre-upgrade hook that applies the chart's CRDs with `kubectl apply --server-side`. It runs on every upgrade; from v0.3.0 it also runs on an install over CRDs that an earlier, uninstalled release left behind. |
+| `crds.autoApply.image` | string | `registry.k8s.io/kubectl:v1.36.3` | `kubectl` image the hook runs. Retag to a private mirror for air-gapped installs. |
+| `crds.autoApply.pullPolicy` | enum | `IfNotPresent` | Pull policy for the hook image, separate from `image.pullPolicy` so tracking `:edge` does not re-pull it on every upgrade. |
+
+## Agent mTLS and S3 audit sink
+
+| Key | Type | Default | Description |
+|---|---|---|---|
+| `api.agentMTLS.caSecretRef.name` | string | `gameplane-agent-ca` | Secret holding the CA for in-pod agent mTLS (`ca.crt`, `ca.key`). By default the chart generates it at install and reuses it on upgrade. From v0.3.0 you can bring your own: pre-create the Secret in the release namespace and set its name, and the chart does not generate it. Data key names are fixed. A referenced Secret that does not exist then fails a live install or upgrade, and rotating a custom Secret rolls the API and operator. |
+| `api.agentMTLS.clientCertRef.name` | string | `gameplane-agent-client` | Secret holding the API/operator client certificate (`tls.crt`, `tls.key`). Same rules as above. |
+| `api.audit.s3.endpoint` | string | *(empty)* | S3-compatible endpoint `host:port` (for example `minio:9000`). Leave empty to disable the S3 sink. |
+| `api.audit.s3.bucket` | string | *(empty)* | Bucket name (required when `endpoint` is set). |
+| `api.audit.s3.prefix` | string | *(empty)* | Object key prefix, for example `gameplane-audit`. Empty = bucket root. |
+| `api.audit.s3.region` | string | *(empty)* | S3 region. Empty defaults to `us-east-1`. |
+| `api.audit.s3.insecure` | boolean | `false` | Use plain HTTP instead of HTTPS (no TLS at all), for local S3-compatible endpoints. TLS verification is never skipped. |
+| `api.audit.s3.credentialsSecretRef` | object | *(empty name)* | Secret holding the S3 credentials. Leave `name` empty to disable the S3 sink. |
+
## API database and persistence
Configure data persistence, audit logging, and telemetry.
| Key | Type | Default | Description |
|---|---|---|---|
-| `api.db.driver` | enum | `sqlite` | Database backend: `sqlite` (single-node, built-in) or `postgres` (multi-node, external server). Requires `-tags postgres` image rebuild for PostgreSQL support. |
+| `api.db.driver` | enum | `sqlite` | Database backend: `sqlite` (built-in, production-tested) or `postgres` (external server, experimental). The published api image is built without PostgreSQL, so `postgres` needs an image rebuilt with `-tags postgres`; it is not yet covered by e2e or upgrade tests, and `api.replicas` must stay `1` with either driver. |
| `api.db.dsn` | string | `file:/data/gameplane.db?_pragma=journal_mode(WAL)` | Database connection string. SQLite path (only when `driver: sqlite`). For PostgreSQL, use a connection string like `postgres://gameplane:***@postgres/gameplane?sslmode=require`. |
| `api.storage.size` | string | `2Gi` | Size of the SQLite PersistentVolumeClaim (only when `db.driver: sqlite`). Adjust based on audit log retention and historical data. |
| `api.storage.storageClassName` | string | *(empty)* | Storage class for the SQLite PVC. When empty, uses the cluster's default storage class. |
@@ -132,6 +162,17 @@ Configure default-deny policies for game pods and backups.
| `networkPolicies.backupEgress.enabled` | boolean | `true` | Allow backup/restore Job pods to reach external restic repositories (S3, REST, SFTP, etc.). Covers HTTPS and SSH ports by default. |
| `networkPolicies.gameIngress.enabled` | boolean | `true` | Create per-GameServer Ingress NetworkPolicies allowing player traffic from `fromCIDRs`. Operator configures the exact advertised ports per server. |
| `networkPolicies.gameIngress.fromCIDRs` | list | `["0.0.0.0/0"]` | CIDR ranges allowed to reach game ports. Set to a LAN range (e.g., `["192.168.1.0/24"]`) to restrict access. Cannot be an empty list; set `enabled: false` instead. |
+| `networkPolicies.apiServerCIDRs` | list | `[]` (falls back to RFC 1918 + link-local) | CIDRs holding the kube-apiserver endpoints the agent sidecar reaches for its status heartbeat, on TCP 443 and 6443. Set it to narrow that egress to your API endpoints, or when they sit outside those ranges. |
+
+
+These network capture values are not available in v0.2.0-beta.8.
+
+| Key | Type | Default | Description |
+|---|---|---|---|
+| `capture.enabled` | boolean | `false` | Let admins opt individual GameServers into packet capture. Capture is always opt-in per server; a server that does not opt in gets no capture sidecar. |
+| `capture.defaultMaxDurationSeconds` | integer | `300` | Default maximum runtime per network capture (5 minutes). Captures stop automatically at the limit. |
+| `capture.defaultMaxSizeBytes` | integer | `943718400` | Default maximum capture size (900 MiB), kept under the 1 GiB `emptyDir` limit backing the capture volume. Captures stop automatically at the limit. |
+
## Advanced: MCP and telemetry
@@ -139,7 +180,7 @@ Configure optional integrations.
| Key | Type | Default | Description |
|---|---|---|---|
-| `mcpServer.enabled` | boolean | `false` | Deploy a read-only Model Context Protocol (MCP) server for AI assistant integrations. Read-only, stdio JSON-RPC only (no network port). See `mcp-server/README.md`. |
+| `mcpServer.enabled` | boolean | `false` | Deploy a read-only Model Context Protocol (MCP) server for AI assistant integrations. It reads 7 Gameplane CRDs (not `Cluster`, nor `NetworkCapture`, the ninth CRD added in v0.3.0), plus Pods, Events and pod logs. Read-only, stdio JSON-RPC only (no network port). See `mcp-server/README.md`. |
| `api.telemetry.receiver.enabled` | boolean | `false` | Deploy the bundled telemetry receiver (telemetry-receiver image) for opt-in anonymous usage stats. When enabled and `endpoint` is unset, the API auto-wires to it. Admin toggle in dashboard controls whether anything is sent. |
## Example: minimal production install
diff --git a/src/content/docs/high-availability.mdx b/src/content/docs/high-availability.mdx
index d311a35..1a94c80 100644
--- a/src/content/docs/high-availability.mdx
+++ b/src/content/docs/high-availability.mdx
@@ -36,16 +36,16 @@ Publish replica safety, leader election, migration ownership, session behavior,
]}
/>
-Before deploying with redundancy, decide which components will replicate and at what minimum count. The Gameplane control plane offers safe replication at 2+ replicas; game servers themselves are stateless replicas (one per instance) and scale independently.
+Before deploying with redundancy, decide which components will replicate and at what minimum count. The operator replicates safely at 2+ replicas, but the API must stay at 1 replica; game servers themselves are stateless replicas (one per instance) and scale independently.
- **Operator component:** Leader-elected across replicas. Safe at 2+ replicas with `operator.replicas` in Helm values. Leader election uses Kubernetes leases; the operator exits on missed lease renewal, triggering a pod restart. A single-node cluster with one operator replica is fault-tolerant only within that node.
-- **API component:** Database driver determines safety. With SQLite (default, production-tested), safe only at 1 replica (single writer on RWO volume). With PostgreSQL (experimental, in-progress), safe at 2+ replicas. Game agents talk to the API via Kubernetes Service discovery and automatically reconnect on pod restarts. The API stores game state, audit logs, and administrative data; loss of the API pod does not interrupt active game servers (they remain running and playable).
+- **API component:** Keep `api.replicas: 1` with either database driver: SQLite (default, production-tested) is single-writer on a RWO volume, and with PostgreSQL (experimental) the user-management lock and audit hash chain are still per-process, so extra replicas are not safe. Game agents talk to the API via Kubernetes Service discovery and automatically reconnect on pod restarts. The API stores game state, audit logs, and administrative data; loss of the API pod does not interrupt active game servers (they remain running and playable).
- **Game server pods:** Each GameServer pod is stateless application logic (plus ephemeral logs). World data persists on a PVC backed by your StorageClass. Multiple GameServers are independent replicas; loss of one pod does not affect others. Game servers are typically single-replica per instance; replication happens at the instance level, not the pod level.
- **Ingress controller, load balancer, and DNS:** Provided by your Kubernetes distribution and network provider. Gameplane does not manage these; your infrastructure must provide redundancy (e.g., multi-zone load balancer, multiple ingress replicas, DNS failover).
State database requirements and constraints:
- **SQLite (default):** Single writer on a RWO PVC. Database file is mutable in place; concurrent writers corrupt it. Backups via restic snapshots. Simpler operational model; suitable for single-operator setups and development.
-- **PostgreSQL (experimental):** Supports multiple concurrent writers. Replication, point-in-time recovery, and connection pooling are PostgreSQL-native concerns. Not yet recommended for production; tested in CI but not on multi-replica clusters.
+- **PostgreSQL (experimental):** An external, separately administered database. Replication, point-in-time recovery, and connection pooling are PostgreSQL-native concerns. Not yet recommended for production: the published api image is built without it, it is not yet covered by e2e or upgrade tests, and it does not make multiple API replicas safe.
Separate cluster-scoped dependencies from application replicas:
- The operator, API, and sentinel (wake-on-connect) are control-plane components that manage the cluster. They have their own replicas and heartbeat logic.
@@ -101,7 +101,7 @@ Drain or terminate one dependency at a time and measure:
diff --git a/src/content/docs/module-cluster-crds.mdx b/src/content/docs/module-cluster-crds.mdx
index 93e41e5..788d268 100644
--- a/src/content/docs/module-cluster-crds.mdx
+++ b/src/content/docs/module-cluster-crds.mdx
@@ -73,13 +73,15 @@ ModuleSource indexes a module catalog (OCI registry, git repo, HTTP archive, or
## Cluster registration
-The Cluster CRD registers a remote Kubernetes cluster for multi-cluster deployments. Each Cluster holds a kubeconfig Secret reference and reports health.
+The Cluster CRD registers a remote Kubernetes cluster for multi-cluster deployments. Each Cluster holds a kubeconfig Secret reference, optionally a reference to a private agent gateway (v0.3.0), and reports Kubernetes connectivity health. The `metadata.name` must equal the gateway's configured cluster ID.
| Field | Type | Required | Purpose |
|---|---|---|---|
| `spec.displayName` | string | no | Human-readable label for the cluster (appears in the dashboard) |
| `spec.kubeconfigSecret.name` | string | yes | Name of a Secret in the operator namespace holding the kubeconfig |
| `spec.kubeconfigSecret.key` | string | no | Secret data key where kubeconfig lives; defaults to `kubeconfig` |
+| `spec.agentGateway.url` | string | no | **(v0.3.0)** HTTPS origin of the cluster's agent gateway, with no userinfo, path, query or fragment (for example `https://remote-1-gateway.internal:8443`) |
+| `spec.agentGateway.tlsSecretRef.name` | string | yes, if `agentGateway` is set | **(v0.3.0)** Secret in the central API namespace holding `ca.crt`, `tls.crt` and `tls.key`, labeled `gameplane.local/agent-gateway-credentials: "true"`; values are never returned by the API |
| `status.phase` | enum | read-only | Unknown / Healthy / Unhealthy — reflects latest connectivity check |
| `status.lastCheckTime` | time | read-only | When health was last assessed |
| `status.message` | string | read-only | Details about the current phase (error reason if Unhealthy) |
@@ -107,3 +109,4 @@ The Cluster CRD registers a remote Kubernetes cluster for multi-cluster deployme
- **[CRD Catalog](../crd-catalog/)** — overview of all 9 Gameplane CRDs.
- **[Status, Phases & Conditions](../status-phases-conditions/)** — how to interpret phase and condition values.
- **[Multi-cluster Topology](../multi-cluster/)** — register and manage remote clusters.
+- **[Remote Agent Gateway](../remote-agent-gateway/)** — install the gateway and fill in `spec.agentGateway`.
diff --git a/src/content/docs/multi-cluster.mdx b/src/content/docs/multi-cluster.mdx
index e28361f..b7bb89a 100644
--- a/src/content/docs/multi-cluster.mdx
+++ b/src/content/docs/multi-cluster.mdx
@@ -4,7 +4,7 @@ description: Make cluster boundaries explicit and operate independent environmen
section: platform
group: cluster-storage
order: 3
-icon: boxeses
+icon: boxes
sidebarLabel: Multi-cluster
eyebrow: PLATFORM
readTime: 14 MIN
@@ -24,6 +24,57 @@ Make cluster boundaries explicit and operate independent environments without im
Share policy through GitOps and SSO; do not share live databases, controller state, or repository credentials across boundaries.
+
+ The optional agent gateway and the unified cross-cluster dashboard described below arrive in v0.3.0. In v0.2.0-beta.8 you can register clusters and manage their resources from one dashboard, but console and log streaming is scoped to the local control-plane cluster.
+
+
+## Manage independent clusters from one dashboard
+
+One central API and login manages independent registered clusters. Each remote cluster runs its own operator and, optionally, a private [agent gateway](../remote-agent-gateway/). Clusters stay independent execution and storage boundaries; only their presentation is combined.
+
+### What works across clusters
+
+The central API connects directly to each registered Kubernetes API for resource changes, Pod logs and PTY attach. The gateway adds the agent-based operations on top; it does not tunnel Kubernetes traffic.
+
+| Capability | How it reaches the remote cluster |
+|---|---|
+| Create, edit and delete GameServers, Backups and other resources | Central API to the cluster's Kubernetes API, using the registered kubeconfig |
+| Pod startup/stdout logs and PTY console attach | Selected cluster's Kubernetes API; no gateway needed |
+| RCON console, game log files, file manager, players, live status, agent-based mods, RCON module actions | Gateway, which forwards to the game Pod's agent over mTLS |
+| Stdin module actions | Selected cluster's Kubernetes client, after a workload-ownership check |
+| Packet-capture downloads and cleanup | Gateway capture route; requires an upgraded gateway and capture sidecar |
+| Registry browsing, modpacks, ID-list mods | Selected cluster's Kubernetes client and game template; provider credentials stay in the central installation |
+| Node inventory and storage totals | Central API reads the cluster's Kubernetes API with the registered kubeconfig's identity |
+
+The same server pages and permissions apply to local and remote servers. Every operation stays bound to its cluster, namespace and resource identity, and an unavailable remote never falls back to a local server with the same name.
+
+### What does not work across clusters
+
+- No cross-cluster scheduling, automatic failover, shared storage, or live game migration; move worlds with backup and restore (see [Promote and migrate](#promote-and-migrate)).
+- No second user database: accounts, roles, the module catalog, audit and installation settings stay centrally managed.
+- Node-join tokens, kubeconfig issuance and installation storage configuration are local-cluster operations and are unavailable while viewing a remote cluster.
+- Registration is an administrator step. The dashboard's **Clusters** page lists and selects existing registrations; you enroll a cluster through the `Cluster` resource and its credential Secrets.
+- A gateway outage does not stop games or their local operator, and the `Cluster` health status reports Kubernetes connectivity only, not gateway health.
+
+### Match component versions
+
+Use matching API, operator and agent versions in every cluster. Remote requests use only the UID-bound agent route; an older agent, or an operator that has not yet injected the server UID, fails closed with a 404 and there is no fallback to legacy agent paths. Upgrading the operator can roll existing game Pods, so schedule it for a maintenance window. Capture downloads and cleanup additionally need an upgraded capture sidecar: files written by an older sidecar stay available locally but are refused on the remote route. The [agent gateway guide](../remote-agent-gateway/) covers the upgrade details.
+
+### One view across locations
+
+The dashboard, Servers, Backups and Search combine every resource the user is authorized to see, across all registered clusters, with an optional **Location** filter. There is no global cluster switcher in the ordinary shell.
+
+- **Dashboard:** combined server and player counts; inventory cards combine only the sites where the user has inventory permission. CPU and memory ratios use summed usage over matching capacity, and unavailable metrics show as unknown rather than zero. Unavailable or truncated scopes are listed next to partial totals.
+- **Servers:** one list with the existing game, namespace, status and text filters plus Location, kept inside the Filter popover. Same-named servers in different clusters or namespaces stay distinct, and each row acts on its own cluster and namespace.
+- **Server detail:** links carry the cluster and namespace. Console, files, players, settings and backups inherit that target, so refreshing or opening a link in another tab never depends on a previous selection. Legacy links without a cluster mean the local cluster.
+- **Backups and Search:** snapshots, schedules, restores and server search combine authorized scopes and keep each result's cluster, namespace and name. Filters stage until you click Apply.
+- **Creating a server:** the target is chosen before creation. A single eligible placement is automatic; with several, you pick a location, with an eligible local placement preferred. Gameplane does not schedule across clusters or retry a failed creation on another site.
+- **Partial data:** a disconnected cluster or a list limit produces a visible partial-data state, and a failed remote query never falls back to local data.
+
+### Grant access per cluster
+
+Kubernetes credentials and dashboard grants are separate. The registered kubeconfig must be able to read the target cluster's resources, and users need a grant for the cluster and namespace they use. In **Users & RBAC**, create a role, edit the user, choose the remote cluster for a supplemental grant, pick the role and namespace, and add it. The user's local primary role stays separate, and changing grants revokes their sessions so they sign in again. For node inventory, use a role containing `cluster:read` with **All namespaces**; namespace-only server grants do not expose inventory. Remote all-namespace roles may hold only `cluster:read` and namespaced permissions; wildcard and central-administration permissions are rejected.
+
## Define the control boundary
Document the supported relationship between control plane, database, namespace scope, target cluster, and audit history.
@@ -98,3 +149,4 @@ Promote `Cluster` CRs, `GameTemplate`, and `Module` manifests through version co
- [High Availability](../high-availability/) — Replicate the control plane for production resilience.
- [Backups & Recovery](../backups-recovery/) — Back up game worlds and move them between clusters.
- [Users, Auth & RBAC](../users-auth-rbac/) — Set up authentication and role bindings per cluster.
+- [Remote agent gateway](../remote-agent-gateway/) — Install the per-cluster gateway, register it centrally, verify and troubleshoot it.
diff --git a/src/content/docs/network-automation.mdx b/src/content/docs/network-automation.mdx
index 780b66e..bda2c45 100644
--- a/src/content/docs/network-automation.mdx
+++ b/src/content/docs/network-automation.mdx
@@ -25,7 +25,7 @@ Expose game ports intentionally, restrict source networks, and automate recurrin
## Choose an exposure model
-Select ClusterIP, NodePort, or LoadBalancer based on where players connect and who controls the network edge.
+Select ClusterIP, NodePort, or LoadBalancer based on where players connect and who controls the network edge. When you have no public IP or no control over the network edge, layer a relay tunnel over the Service.
- Ownership, collaborators, transfer, wipe, and delete are already available in v0.2.0-beta.8. In beta.8, users with the operator role can still transfer ownership, wipe data, or delete servers they don't own; v0.3.0 restricts these four operations to the server's owner or a cluster admin only.
+ Ownership, collaborators, transfer, wipe, and delete are available in v0.2.0-beta.8, where operator-role users can also run them on servers they don't own. From v0.3.0, transferring ownership, editing collaborators, wiping data and deleting a server need the server's owner or an admin. Servers with no recorded owner (for example created with kubectl or GitOps) can then be transferred, wiped or deleted only by an admin.
Ownership controls accountability and destructive authority; collaboration grants operator access to one server. Both require explicit owner decisions before irreversible actions like data wiping and server deletion.
diff --git a/src/content/docs/platform-settings-telemetry.mdx b/src/content/docs/platform-settings-telemetry.mdx
index 88f56f9..6c7974a 100644
--- a/src/content/docs/platform-settings-telemetry.mdx
+++ b/src/content/docs/platform-settings-telemetry.mdx
@@ -28,8 +28,8 @@ Keep instance name, canonical external URL, and default namespace stable and env
items={[
{
icon: "check",
- label: "Use a name that distinguishes environments and clusters",
- text: "The instance name appears in logs, audit records, and the browser tab, helping you identify which Gameplane instance you're operating.",
+ label: "Use a name that distinguishes environments and clusters (optional)",
+ text: "The instance name is optional. When set, it names this install on the Cluster page and in notifications, so admins running several installs can tell them apart. When blank, notifications say \"Gameplane\" and the Cluster page shows the cluster ID.",
},
{
icon: "check",
diff --git a/src/content/docs/production-installation.mdx b/src/content/docs/production-installation.mdx
index 7d9139e..50b9a6a 100644
--- a/src/content/docs/production-installation.mdx
+++ b/src/content/docs/production-installation.mdx
@@ -29,7 +29,7 @@ Pin every artifact and make environment ownership explicit before the first work
Gameplane's Helm chart is distributed as an OCI artifact on GitHub Container Registry (GHCR). Every release includes pinned component image versions, so locking the chart version locks the software stack:
- **Chart version** pins the operator, API, and web dashboard images.
-- **Optional component images** (audit sidecar, telemetry receiver, sentinel, tunnel clients, MCP server) are pulled only when enabled.
+- **Optional component images** — `audit-syslog-bridge`, `telemetry-receiver`, `sentinel`, `tunnel-frp`, `tunnel-tailscale`, `tunnel-playit`, `mcp-server` and `capture-sidecar` — are pulled only when the optional component they belong to is enabled (and, for `sentinel` and `capture-sidecar`, only for GameServers that opt in). A default install pulls four images: `operator`, `api`, `web` and `agent`.
- **GameServer agent images** are pulled per-server, on-demand, so pre-staging is not required.
Before your first production install, document and approve the release you intend to deploy: the specific chart version (e.g., `0.2.0-beta.8`) and any image tag or pull secret overrides in your values file.
diff --git a/src/content/docs/remote-agent-gateway.mdx b/src/content/docs/remote-agent-gateway.mdx
new file mode 100644
index 0000000..5ae6c02
--- /dev/null
+++ b/src/content/docs/remote-agent-gateway.mdx
@@ -0,0 +1,316 @@
+---
+title: Remote agent gateway
+description: Install the optional per-cluster gateway, register it with the central API over mTLS, and verify and troubleshoot remote console, file, player and capture access.
+section: platform
+group: cluster-storage
+order: 10
+icon: network
+sidebarLabel: Agent gateway
+eyebrow: PLATFORM
+readTime: 14 MIN
+covers: ["Gateway", "mTLS", "Cluster registration", "Troubleshooting"]
+---
+
+import DocsCallout from "../../components/docs/DocsCallout.astro";
+import DocsSectionList from "../../components/docs/DocsSectionList.astro";
+import DocsQuickReference from "../../components/docs/DocsQuickReference.astro";
+import DocsArticleMeta from "../../components/docs/DocsArticleMeta.astro";
+
+
+
+Run one central Gameplane API and dashboard, plus an operator and an optional gateway in each remote cluster. The gateway proxies approved agent operations over mTLS so the central dashboard can reach consoles, files, players and captures in clusters it does not run in.
+
+
+ The agent gateway ships in v0.3.0 and is not available in v0.2.0-beta.8.
+
+
+
+ The central API still connects directly to each registered Kubernetes API for resource changes, Pod logs, PTY attach and stdin module actions. The gateway is not a Kubernetes tunnel, does not replicate storage, and has no database or user login. Users authenticate to the central API, which keeps authorization.
+
+
+## What the gateway carries
+
+The gateway extends registered clusters with the RCON console, game log files, file management, player operations, live status, agent-based mods and capture files. Registry browsing, modpacks and ID-list mods use the selected cluster's Kubernetes client and game template, and provider credentials stay in the central installation. See [Multi-cluster topology](../multi-cluster/) for the full list of what works and what does not across clusters.
+
+## Prepare the remote cluster
+
+Use matching API, operator and agent versions in the remote cluster. The gateway runs the API image's `gateway` subcommand under a dedicated ServiceAccount with read-only `get` access to GameServers, NetworkCaptures and Services in its configured namespaces. It has no database and no user login.
+
+Create these Secrets in the chart release namespace before enabling the gateway:
+
+| Secret value | Required keys | Purpose |
+|---|---|---|
+| `gateway.serverTLSSecret` | `tls.crt`, `tls.key` | Gateway server identity, with a DNS SAN matching the configured endpoint |
+| `gateway.centralClientCASecret` | `ca.crt` | CA trusted to issue central API client certificates |
+| `gateway.agentClientSecret` | `tls.crt`, `tls.key` | Existing local agent client credentials; defaults to `gameplane-agent-client` |
+| `gateway.agentCASecret` | `ca.crt` | Existing local agent trust; defaults to `gameplane-agent-ca` |
+
+The gateway also requires an exact `gateway.peerURI` URI SAN in the central client certificate. Use a separate management trust root from the local agent CA. The chart mounts only `ca.crt` from the agent CA Secret, never its signing key, and keeps provisioning the local agent CA and client Secrets when the central API is disabled.
+
+### Certificate reload and stream lifetime
+
+- The gateway reloads its server and trust material on new TLS handshakes, rechecks central-peer trust on requests, and reloads agent credentials for new upstream requests. Allow time for Kubernetes Secret volume projection to update, and overlap client and server trust before retiring old certificates.
+- Streams and transfers have no fixed total-duration cap by default (`gateway.maxRequestDuration: 0s`). They still end at the central client certificate's expiry, when the caller disconnects, or when the gateway shuts down. A positive value such as `1h` adds a total lifetime, capped by certificate expiry.
+- Ordinary operations and Kubernetes identity lookups stay bounded to 30 seconds, and connection, TLS handshake and response-header timeouts remain in place.
+- Removing trust or permissions prevents new operations but does not immediately revoke an existing stream. Restart the gateway to end active sessions sooner, or set a maximum lifetime. Reconnecting checks authorization and trust again.
+- When upgrading with reused Helm values, change an existing `gateway.maxRequestDuration: 5m` to `0s` to remove the old cutoff.
+
+## Deploy the gateway with Helm
+
+Adapt the [example values](https://github.com/ValgulNecron/Gameplane/blob/master/charts/gameplane/examples/gateway-values.yaml) for a fresh remote installation:
+
+```yaml
+api:
+ enabled: false
+gateway:
+ enabled: true
+ clusterID: remote-1
+ peerURI: spiffe://gameplane.example/central-api
+ namespaces: [gameplane-games]
+ serverTLSSecret: gameplane-gateway-server
+ centralClientCASecret: gameplane-central-client-ca
+ networkPolicy:
+ peerCIDRs: [10.20.30.40/32]
+ apiServerCIDRs: [10.96.0.1/32, 10.0.0.10/32]
+```
+
+```sh
+helm upgrade --install gameplane ./charts/gameplane \
+ --namespace gameplane-system --create-namespace \
+ --values gateway-values.yaml
+```
+
+
+
+`api.enabled: false` omits the API Deployment, Service, ServiceAccount and RBAC, the SQLite PVC, the dashboard and its ingress, the cluster-operations grants, the API ServiceMonitor, and the API-only audit and telemetry receivers. The operator, CRDs, agent mTLS and operator/agent monitoring remain. Default installations keep `api.enabled: true` and `gateway.enabled: false`.
+
+
+ This profile suits a fresh remote installation. For an existing API and database, preserve its PVC and data before disabling the API. Chart-created SQLite PVCs carry `helm.sh/resource-policy: keep`, and a live Helm render refuses to disable an older unannotated PVC. That check cannot run during offline `helm template` or GitOps rendering, and non-Helm pruning controllers may ignore the annotation, so configure your deployment controller's retention before changing an existing installation. The profile does not migrate the database or its users.
+
+
+### Gateway Helm values
+
+| Value | Default | Purpose |
+|---|---|---|
+| `gateway.enabled` | `false` | Deploy the gateway |
+| `gateway.replicas` | `1` | Gateway replicas |
+| `gateway.clusterID` | empty | Must match the central `Cluster` resource name |
+| `gateway.peerURI` | empty | Exact URI SAN trusted in the central API client certificate |
+| `gateway.namespaces` | `[]` | Namespaces the gateway may reach; empty selects only `gamesNamespace` |
+| `gateway.serverTLSSecret` | empty | Secret with `tls.crt` and `tls.key` for the gateway server |
+| `gateway.centralClientCASecret` | empty | Secret with `ca.crt` for the CA issuing central client certificates |
+| `gateway.agentCASecret` | `gameplane-agent-ca` | Existing local agent trust |
+| `gateway.agentClientSecret` | `gameplane-agent-client` | Existing local agent client credentials |
+| `gateway.maxRequestDuration` | `0s` | Optional total lifetime for operations and streams; `0s` disables the cap |
+| `gateway.networkPolicy.peerCIDRs` / `peerSelectors` | empty | Allowed sources of incoming TCP 8443; at least one is required |
+| `gateway.networkPolicy.apiServerCIDRs` | empty | Local Kubernetes API endpoint addresses; required |
+| `gateway.networkPolicy.dnsNamespace`, `dnsPodLabels`, `dnsCIDRs` | `kube-system`, `k8s-app: kube-dns`, empty | Cluster-local DNS; add the exact CIDR for NodeLocal DNS |
+
+## Grant inventory permissions
+
+The central API reads node inventory and storage totals directly from the selected cluster's Kubernetes API. Bind these read permissions to the identity in that cluster's **registered kubeconfig**, in addition to its game-resource permissions.
+
+| API group | Resource or path | Verbs | Purpose |
+|---|---|---|---|
+| core | `nodes` | `list` | Node identities, readiness and capacity |
+| core | `persistentvolumes` | `list` | Capacity of bound volumes for provisioned-storage totals |
+| `metrics.k8s.io` | `nodes` | `list` | Optional current node CPU and memory usage |
+| non-resource | `/version`, `/version/` | `get` | Target Kubernetes version; also needed for registration health |
+
+Create this supplemental ClusterRole in the **remote cluster** and bind it only to the ServiceAccount used by the registered kubeconfig:
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: gameplane-central-inventory
+rules:
+ - apiGroups: [""]
+ resources: [nodes, persistentvolumes]
+ verbs: [list]
+ # Optional: omit this rule when node usage metrics are not required.
+ - apiGroups: [metrics.k8s.io]
+ resources: [nodes]
+ verbs: [list]
+ - nonResourceURLs: [/version, /version/]
+ verbs: [get]
+```
+
+Remote capture management additionally needs these rules in the registered identity's namespace-scoped Role, in each managed game namespace:
+
+```yaml
+- apiGroups: [gameplane.local]
+ resources: [networkcaptures]
+ verbs: [get, list, create, patch, delete]
+- apiGroups: [gameplane.local]
+ resources: [networkcaptures/status]
+ verbs: [update]
+```
+
+The API initializes a new capture to Pending through the status subresource. Without that permission, a start request can return 403 after the capture was created, so inspect the capture list before submitting another start request. The gateway ServiceAccount does not need the inventory grants or any write access to capture resources.
+
+Inventory needs no `watch`, namespace or StorageClass enumeration, Secret access, node proxy access or cluster-admin. The namespace list comes from the central API's configured game-namespace allowlist and the user's grants, not from discovering every namespace. Provisioned storage is the sum of bound PV capacity, not measured disk use. Without metrics-server access, node capacity stays visible and current CPU and memory usage is unknown. A denied node or PV read is reported as an inventory error; it is never shown as an empty healthy cluster or retried against the central cluster.
+
+To let a dashboard user see a remote cluster's inventory, create a role containing `cluster:read` in **Users & RBAC**, edit the user, choose the remote cluster, select the role and **All namespaces**, and add the grant. The user must sign in again because grant changes revoke sessions.
+
+## Private networking
+
+The gateway Service is **ClusterIP on TCP 8443**. Provide private routing or a private TLS-passthrough relay to reach it; the chart creates no public ingress, NodePort or LoadBalancer. TLS must reach the gateway unchanged, because the gateway verifies the central client's certificate itself.
+
+A dedicated NetworkPolicy is installed whenever the gateway is enabled, even if the chart's game network policies are disabled. It permits only:
+
+- Incoming TCP 8443 from the configured `peerCIDRs` or `peerSelectors`.
+- DNS on TCP and UDP 53 to the configured DNS pods, or to exact `dnsCIDRs` for NodeLocal DNS.
+- TCP 443 and 6443 to `apiServerCIDRs` in the local cluster.
+- TCP 8090 to game agents in the explicitly allowed namespaces.
+- TCP 9091 to capture sidecars in those same namespaces.
+
+Use the source IPs the target cluster actually observes, accounting for private relay or SNAT behavior. `peerSelectors` identify pods inside this cluster, such as a relay, and cannot select pods by labels in another cluster; give both a namespace selector and a pod selector to narrow a relay allowance. Include the Kubernetes Service and post-DNAT API endpoint addresses your CNI needs. Enforcement requires a supporting CNI, and overlapping policies are additive.
+
+The chart uses TCP liveness and readiness probes so no unauthenticated HTTP endpoint is exposed. These show an open listener, not an end-to-end authenticated agent health check.
+
+## Register the gateway centrally
+
+The gateway needs an HTTPS certificate valid for its configured URL and must trust the central API's dedicated client certificate. Restrict network access to the central API through private networking or an explicit firewall rule.
+
+Create a credential Secret in the central API namespace (normally `gameplane-system`):
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: remote-1-gateway-client
+ namespace: gameplane-system
+ labels:
+ gameplane.local/agent-gateway-credentials: "true"
+type: Opaque
+stringData:
+ ca.crt:
+ tls.crt:
+ tls.key:
+```
+
+These placeholders describe the keys; supply real credentials through your normal Secret-management workflow and never commit private keys to Git.
+
+Then add the optional gateway reference to the existing `Cluster` resource:
+
+```yaml
+apiVersion: gameplane.local/v1alpha1
+kind: Cluster
+metadata:
+ name: remote-1
+spec:
+ kubeconfigSecret:
+ name: remote-1-kubeconfig
+ agentGateway:
+ url: https://remote-1-gateway.internal:8443
+ tlsSecretRef:
+ name: remote-1-gateway-client
+```
+
+The registration name must match the gateway's `clusterID`. The URL is an HTTPS origin with no userinfo, path, query or fragment. Credentials are read only from the central API namespace and must carry the label above. Register the cluster through the central registration API or its `Cluster` resource, and set the gateway field through Kubernetes. The dashboard's **Clusters** page selects existing registrations; enrollment stays an operator-managed step. See [Module & Cluster CRDs](../module-cluster-crds/) for the field reference.
+
+## How requests are routed
+
+The central API authorizes the user, reads the cluster's GameServer UID, and sends an allowlisted operation to the gateway at `/v1/clusters/{cluster}/namespaces/{namespace}/servers/{name}/uids/{uid}/{operation}`. The gateway authenticates the central API, verifies its own cluster ID and the live GameServer and agent Service ownership, and connects to the local agent's UID route, which rejects a different UID. User cookies, bearer tokens and CSRF headers are never forwarded.
+
+A trusted gateway client certificate grants delegated access to the allowlisted agent operations; it is not a user credential, and the gateway does not reconstruct central role bindings. Keep that private key restricted to the central API workload.
+
+Internal mod-update reads use the same selected cluster for agent data and template lookup. RCON module actions use the gateway. Stdin module actions use the selected Kubernetes client and verify workload ownership before attach; Kubernetes attach has no atomic UID precondition, so this is a preflight check.
+
+### Failure behavior
+
+- The central API reads the Cluster and credential Secret for every new gateway operation. Missing, deleted, unlabeled or malformed credentials fail closed with no fallback to a local namesake.
+- Removing the `agentGateway` reference removes new interactive access without removing Kubernetes management.
+- Redirects are not followed, and writes are not automatically retried after an ambiguous failure.
+- Gateway health and Kubernetes reachability are separate: the `Cluster` health phase describes Kubernetes connectivity only. A gateway outage does not stop games or their local operator.
+
+### Captures
+
+Capture files use a separate allowlisted route that accepts only GET and DELETE and is bound to both the server UID and the capture UID. The gateway checks the live server, capture ownership, completion state and retention before reaching the fixed local capture sidecar port (9091), and the sidecar verifies both UIDs against persisted file identity, including after a restart. Remote deletion removes the file before deleting the NetworkCapture resource, so an unavailable gateway leaves the resource for retry. Files written by an older sidecar, without the identity binding, cannot be fetched remotely. No arbitrary sidecar path, address or legacy fallback is accepted.
+
+`GET /servers/{name}/capabilities` reports the selected server's identity and its cluster's capture support, enabled state, retention limits and start defaults. The central installation's capture flag does not enable or disable a remote cluster. Missing or incompatible gateway capabilities disable capture controls, and permissions still come from normal server and capture authorization. Historical downloads remain available after a cluster disables new captures, provided its gateway and bound files remain reachable.
+
+## Verify the installation
+
+1. Confirm the gateway Pod is Ready in the remote cluster. Readiness is a TCP check and shows only an open listener.
+2. Confirm the `Cluster` resource reports `Healthy`. That proves Kubernetes connectivity, not the gateway.
+3. In the dashboard, open a test server in that cluster and try the console, file manager and players. These routes prove the central API can authenticate to the gateway and reach the agent.
+4. If you use captures, confirm the capture controls are enabled; they stay disabled when the gateway's capabilities are missing or incompatible.
+
+Confirm a test server works end to end before relying on interactive management.
+
+## Upgrade behavior
+
+From v0.3.0 the operator injects `GAMEPLANE_SERVER_UID` into agents. Updating the operator can change existing StatefulSet templates and roll game Pods during reconciliation, so schedule it for a maintenance window. Remote requests use only `/v1/targets/{uid}/...`; old agents, or an operator that has not populated the UID, fail closed with 404 and there is no fallback to legacy agent paths.
+
+Capture sidecars also receive the server UID, and new capture starts bind the output to both the server UID and NetworkCapture UID on disk. Files from an older sidecar stay available through the local path but fail closed on the remote route. Upgrade all components before expecting remote capture downloads or cleanup.
+
+## Troubleshoot
+
+| Symptom | Likely cause and fix |
+|---|---|
+| Remote console, files or players return 404 | The agent is older than the UID-bound route, or the operator has not populated the server UID. Upgrade the operator and let game Pods roll so agents match. |
+| Every gateway operation fails, Cluster shows Healthy | Gateway health is separate from Kubernetes health. Check that the credential Secret exists in the central API namespace, carries `gameplane.local/agent-gateway-credentials: "true"` and holds `ca.crt`, `tls.crt` and `tls.key`; missing or malformed credentials fail closed. |
+| Gateway rejects the central API | The central client certificate must chain to `gateway.centralClientCASecret` and carry the exact `gateway.peerURI` URI SAN. The server certificate needs a DNS SAN matching the registered URL, and TLS must pass through any relay unchanged. |
+| Connection times out | Check `gateway.networkPolicy.peerCIDRs` or `peerSelectors` against the source IPs the remote cluster actually sees (SNAT), and `apiServerCIDRs` against post-DNAT API endpoints. |
+| Cluster name mismatch | The `Cluster` resource name must equal `gateway.clusterID`. |
+| Node inventory shows an error | The registered kubeconfig identity lacks the inventory ClusterRole; a denied read is an error, never an empty cluster. |
+| Capture start returns 403 after the capture appears | The registered identity lacks `update` on `networkcaptures/status`. Check the capture list before retrying. |
+| Capture controls are disabled | The gateway's capabilities are missing or incompatible, or that cluster has captures disabled. Upgrade the gateway, operator and capture sidecar together. |
+| Old capture will not download remotely | The file predates the identity binding. Use the site's local maintenance path. |
+| A revoked certificate still works on an open stream | Trust removal blocks new operations only. Restart the gateway or set `gateway.maxRequestDuration`. |
+
+### Capture cleanup recovery
+
+For an unreconciled (empty-phase), Pending or Running capture, request `:capture-stop`, wait for the operator to report a terminal phase, then retry deletion. The stop flow also handles Pending captures whose sidecar started before a status write failed. Repair operator permissions on `networkcaptures/status` if completion cannot persist.
+
+An upgraded sidecar returns 204 after deleting a matching bound file, or 410 after confirming that neither its identity nor the PCAP exists and no writer is active. A retry after a lost response uses the retained identity tombstone. The API keeps the record on ambiguous 404, 409 or 5xx responses, transport errors and unavailable sidecars. Repair gateway connectivity, credentials or sidecar availability and retry; none of these failures proves the file is gone. The API skips file cleanup only for an operator-confirmed never-started capture with no recorded Pod UID.
+
+If the site cannot be restored, an administrator must inspect the capture's `gameplane.local/capture-pod-uid` annotation, the actual Pod UID and capture storage. A container restart preserves emptyDir data, while replacing the Pod removes the old emptyDir. Confirm capture activity has stopped before any manual cleanup. Only then may an administrator delete the record on the selected cluster:
+
+```sh
+kubectl --context -n delete networkcapture
+```
+
+Record-only deletion does not free retained file storage.
+
+
+
+## Related topics
+
+- [Multi-cluster topology](../multi-cluster/) — What works across clusters and the unified dashboard.
+- [Module & Cluster CRDs](../module-cluster-crds/) — The Cluster resource field reference.
+- [Cluster Nodes & Kubeconfig](../cluster-nodes-kubeconfig/) — Registered kubeconfig credentials and node inventory.
+- [Security](../security/) — Trust boundaries between API, gateway and agent.
diff --git a/src/content/docs/repository-components.mdx b/src/content/docs/repository-components.mdx
index 26b7d2e..af26ca1 100644
--- a/src/content/docs/repository-components.mdx
+++ b/src/content/docs/repository-components.mdx
@@ -70,7 +70,7 @@ The repository joins runtime components, shared libraries, optional services, pa
- **`web/`** — React 19 + TypeScript + Vite dashboard with TanStack Router, Query, and Tailwind CSS. Routes are defined in `src/routes/` and compiled into the API's static file server.
- **`charts/`** — Helm chart for production installations; includes Kubernetes manifests generated from operator CRDs.
- **`deploy/kind/`** — Local Kind cluster scripts for development.
-- **`modules/`** — Submodule linking to `gameplane-module` repository; contains 16 game templates in v0.2.0-beta.8, expanding to ~30 in the next release.
+- **`modules/`** — Submodule linking to `gameplane-module` repository; contains 30 game templates on master: 16 shipped in v0.2.0-beta.8 and the other 14 arrive in v0.3.0.
- **`website/`** — Submodule linking to `gameplane-website` repository; Astro-based marketing and documentation site.
## Runtime request path
diff --git a/src/content/docs/rest-api-reference.mdx b/src/content/docs/rest-api-reference.mdx
index 107e990..7325723 100644
--- a/src/content/docs/rest-api-reference.mdx
+++ b/src/content/docs/rest-api-reference.mdx
@@ -53,7 +53,7 @@ Every request must include:
3. **Namespace Selector** — passed as `?namespace=` for scoped resources (GameServers, Backups, etc.). Defaults to the user's default namespace if absent. Users can only access namespaces they have been granted permission to.
-4. **Cluster Selector** — passed as `?cluster=` for multi-cluster installs. Defaults to the local (home) cluster. Users can only access clusters they have been granted permission to.
+4. **Cluster Selector** — passed as `?cluster=` for multi-cluster installs. Defaults to the local (home) cluster. Users can only access clusters they have been granted permission to. Collection reads under `/fleet/*` (v0.3.0) ignore the default and combine every authorized cluster, with optional cluster and namespace filters, returning each item's `{cluster, namespace, name, uid}` target and flagging unavailable or truncated scopes.
Cluster and namespace selectors follow the same scope rules as the dashboard UI. A user bound to a specific namespace or remote cluster cannot bypass that scope by changing query parameters.
diff --git a/src/content/docs/roadmap.mdx b/src/content/docs/roadmap.mdx
index e4a1627..3ad649d 100644
--- a/src/content/docs/roadmap.mdx
+++ b/src/content/docs/roadmap.mdx
@@ -22,8 +22,8 @@ Core workflows — deploy a game server, console, files, backups/restore, module
title="Known limitations"
icon="minus"
items={[
- "WebSocket console/log/file streams are local-cluster-scoped — remote clusters are registered and health-checked but not yet streamed.",
- "The PostgreSQL driver is experimental (build-tag-gated, work-in-progress); SQLite is the production-tested database.",
+ "WebSocket console/log/file streams are local-cluster-scoped in v0.2.0-beta.8 — remote clusters are registered and health-checked but not yet streamed. v0.3.0 adds remote access through an optional agent gateway.",
+ "The PostgreSQL driver is experimental (build-tag-gated); SQLite is the production-tested database. v0.3.0 makes the driver work end to end, but it stays experimental: keep a single API replica, and it is not yet covered by e2e or upgrade tests.",
]}
/>
diff --git a/src/content/docs/runtime-capabilities-actions.mdx b/src/content/docs/runtime-capabilities-actions.mdx
index bbc0fcc..e10337a 100644
--- a/src/content/docs/runtime-capabilities-actions.mdx
+++ b/src/content/docs/runtime-capabilities-actions.mdx
@@ -73,7 +73,7 @@ Use strict templates and defensive parsers for user actions, live status, player
{
icon: "check",
label: "Declare parameters, validation, labels, icons, and capability gates.",
- text: "Each spec.capabilities.actions entry has an id, displayName, description, icon, command or commands template, parameter declarations with validation, and optional confirm/danger flags.",
+ text: "Each spec.capabilities.actions entry has an id, displayName, description, icon, command or commands template, parameter declarations with validation, and optional confirm/danger flags. A parameter can include currentFrom to pre-fill its value from a live status metric.",
},
{
icon: "check",
@@ -88,6 +88,31 @@ Use strict templates and defensive parsers for user actions, live status, player
]}
/>
+### Pre-fill parameters from status (v0.3.0)
+
+A parameter with `currentFrom: ` names a `spec.capabilities.status.metrics[].id`. When the action dialog opens, the dashboard pre-fills the parameter with that metric's live value, so a "set" action starts on the current setting:
+
+- **Enum parameters:** the option matching case-insensitively (a reading of `Easy` selects `easy`)
+- **Boolean parameters:** `true` or `false`
+- **Integer parameters:** an integer parsed from the reading
+- **No reading or no valid match:** the parameter falls back to its `default`
+
+The user's own choice always wins; a reading that arrives later does not override it. Status metrics need RCON. Minecraft's `set-difficulty` action uses it like this:
+
+```yaml
+capabilities:
+ actions:
+ - id: set-difficulty
+ displayName: Set difficulty
+ command: "difficulty {{.Params.level}}"
+ params:
+ - name: level
+ type: enum
+ enum: ["peaceful", "easy", "normal", "hard"]
+ default: normal
+ currentFrom: difficulty # pre-fill from status metric
+```
+
## Mods and registries
Declare directories, install methods, IDs, compatibility, providers, restart behavior, and safe download/extraction rules.
diff --git a/src/content/docs/security.mdx b/src/content/docs/security.mdx
index 26b60d0..3a49449 100644
--- a/src/content/docs/security.mdx
+++ b/src/content/docs/security.mdx
@@ -19,12 +19,16 @@ Local accounts with argon2id hashing, or OIDC (Keycloak, Google, GitHub) with JW
## Authorization
-Permission-based RBAC with admin/operator/viewer roles, scoped per namespace or cluster-wide.
+Permission-based RBAC with admin/operator/viewer roles, scoped per namespace or cluster-wide. Registering a cluster grants no user access by itself: remote access uses supplemental per-cluster grants, and a global union of permission names never authorizes a row in another cluster.
## API ↔ Agent
mTLS with an operator-managed CA; the agent refuses plain HTTP once TLS material is present.
+## API ↔ remote gateway
+
+In v0.3.0, remote clusters can run an optional [agent gateway](../remote-agent-gateway/). The central API reaches it with a dedicated mTLS identity: separate trust material from the local agent CA, and an exact URI SAN the gateway requires in the client certificate. The central API remains the user-authorization authority, so a trusted client certificate grants delegated access to the gateway's allowlisted agent operations only. It is not a user credential, so keep its private key restricted to the central API workload. The gateway exposes only allowlisted operations in its configured namespaces, never forwards browser cookies, bearer tokens or CSRF headers, and reaches agents over local agent mTLS on versioned, GameServer-UID-bound routes. Missing or malformed credentials fail closed with no fallback to a same-named local server. Removing trust or permissions blocks new operations but does not immediately end an existing stream; restart the gateway or set `gateway.maxRequestDuration` to bound that window.
+
Report it privately via GitHub Security Advisories rather than a public issue — we'll coordinate
a fix before public disclosure.
diff --git a/src/content/docs/server-networking-tunnels.mdx b/src/content/docs/server-networking-tunnels.mdx
new file mode 100644
index 0000000..2b54153
--- /dev/null
+++ b/src/content/docs/server-networking-tunnels.mdx
@@ -0,0 +1,357 @@
+---
+title: "Server Networking & Tunnels"
+description: "Route players through external relays when you lack public IP or load balancers. Compare frp, Tailscale, and playit; configure credentials via dashboard, API, or kubectl."
+section: operate
+group: network-automation
+order: 3
+icon: globe
+eyebrow: CONFIGURE
+readTime: "18 MIN"
+covers: ["Tunnels", "Relay", "NAT traversal"]
+---
+
+import DocsCallout from "../../components/docs/DocsCallout.astro";
+import DocsArticleMeta from "../../components/docs/DocsArticleMeta.astro";
+
+
+
+Route player connections through an external relay when you lack a public IP, port-forwarding, or a cluster load balancer. A tunnel layers over the server's backing Service; it is not a fifth `expose` mode. Use it only when direct exposure is unavailable.
+
+
+ Each tunnel-enabled server gets its own always-on tunnel pod (a separate `-tunnel` Deployment, not a sidecar in the game pod). It keeps running while the game sleeps so it can hold the public address. Budget for this when planning idle auto-sleep.
+
+
+
+ Tunnels, the `TunnelReady` condition and the `:tunnel-credentials` API are available in v0.2.0-beta.8. From v0.3.0 the operator refuses a credentials Secret that is not owned by its GameServer, the playit-assigned address appears in `status.endpoints`, and the playit tunnel's NetworkPolicy lets it reach its relay. In v0.2.0-beta.8 a playit tunnel cannot reach its relay while the chart's NetworkPolicies are enabled (the default).
+
+
+## When to use a tunnel
+
+Use a tunnel if:
+
+- You have no public IP (CGNAT, shared ISP, corporate network).
+- Port-forwarding is unavailable (router locked by ISP or admin).
+- LoadBalancer is not an option (on-premises cluster with no load-balancer service).
+- You're behind a restrictive NAT and cannot reach your cluster from the public internet.
+
+Do **not** use a tunnel if:
+
+- You already have a public IP with working port-forwarding — it's simpler and cheaper.
+- You have a working LoadBalancer (cloud or MetalLB/Cilium) and it reaches your cluster.
+- You're only deploying to private networks (no public play access needed).
+
+## Choose a provider
+
+Three relay providers are supported, each with different trade-offs:
+
+| Feature | frp | Tailscale | playit.gg |
+|---|---|---|---|
+| **Exposure** | Public internet | Private (tailnet only) | Public internet |
+| **UDP** | Yes | Yes | Yes |
+| **Address type** | Static (you set) | MagicDNS hostname | Dynamic (assigned at runtime) |
+| **Server needed** | Yes (you run frps) | No | No |
+| **Cost** | Your VPS | Free | Free tier (8 hrs/day); paid unlimited |
+| **Setup** | 30–60 min | 5 min | 2 min |
+| **Best for** | Production, custom domains | Friends/family on tailnet | Casual play, quick testing |
+
+For frp, each forwarded port's protocol (TCP or UDP) comes from that port's `protocol` field in the GameTemplate; it is not set separately in the frp tunnel spec.
+
+### frp (fast reverse proxy)
+
+**Use case:** Production servers with custom public domains.
+
+Self-hosted relay. You deploy `frps` on a VPS you control, configure a shared token, and Gameplane runs `frpc` in a tunnel pod that dials your `frps` and holds reverse-proxy tunnels. Players connect to `:`. The address is static and under your control.
+
+Setup time is longer because you must provision and configure a VPS, but you get full control and a production-grade solution.
+
+### Tailscale
+
+**Use case:** Private networks — friends and family on your Tailscale tailnet.
+
+No VPS needed. The tunnel pod joins your Tailscale network and becomes a device reachable via MagicDNS. Only users on your tailnet can connect — this is **not** public-internet exposure. Tailscale Funnel does not help here (Funnel only supports TLS on ports 443/8443/10000, not arbitrary game ports/UDP).
+
+Ideal for private play: no VPS, and setup needs only an auth key.
+
+### playit.gg
+
+**Use case:** Free public relay for casual play and quick testing.
+
+No VPS needed. Sign up, get a secret key, and Gameplane writes it to a config file. The tunnel pod connects and playit assigns a public address at runtime. From v0.3.0 the address appears in `status.endpoints` once the tunnel pod reports it; the pod re-checks every 30 seconds, so a changed address shows up without a restart.
+
+The tunnel pod matches each playit tunnel to a game port by the **local port** the tunnel forwards to, so set each tunnel's local port in the playit dashboard to the game's container port (for example `25565` for Minecraft Java). If the template advertises a single port and the account has a single enabled tunnel, they are paired even when the ports differ.
+
+The free tier allows 8 hours a day per secret key; paid plans unlock 24/7. The address can change when the tunnel pod restarts, because playit does not guarantee address stability on the free tier.
+
+## Set up a tunnel
+
+### Step 1: Choose and prepare your relay
+
+**For frp:**
+Deploy `frps` on a VPS (see [frp documentation](https://github.com/fatedier/frp)) with a config file like:
+```toml
+bindAddr = "0.0.0.0"
+bindPort = 7000
+
+auth.method = "token"
+auth.additionalScopes = ["api"]
+auth.token = "your-secure-token-here"
+```
+Note the server address (e.g., `frp.example.com`) and port (e.g., `7000`).
+
+**For Tailscale:**
+Log into [login.tailscale.com](https://login.tailscale.com) → **Settings → Keys** → create a **Reusable key** and copy it.
+
+**For playit.gg:**
+Visit [playit.gg](https://playit.gg), sign up, and copy your **Secret Key** from **Settings**.
+
+### Step 2: Create a GameServer with tunnel enabled
+
+Set `spec.networking.tunnel.enabled: true`, pick a provider, and reference the credential Secret (name will be written by the dashboard or API):
+
+**frp example:**
+```yaml
+apiVersion: gameplane.local/v1alpha1
+kind: GameServer
+metadata:
+ name: minecraft-prod
+ namespace: gameplane-games
+spec:
+ templateRef:
+ name: minecraft-java
+ networking:
+ expose: ClusterIP # Tunnel handles connectivity
+ tunnel:
+ enabled: true
+ provider: frp
+ credentialsSecretRef:
+ name: minecraft-prod-tunnel-auth
+ frp:
+ serverAddr: frp.example.com
+ serverPort: 7000
+ remotePorts:
+ - name: game
+ remotePort: 25565
+```
+
+**Tailscale example:**
+```yaml
+apiVersion: gameplane.local/v1alpha1
+kind: GameServer
+metadata:
+ name: minecraft-tailnet
+ namespace: gameplane-games
+spec:
+ templateRef:
+ name: minecraft-java
+ networking:
+ expose: ClusterIP
+ tunnel:
+ enabled: true
+ provider: tailscale
+ credentialsSecretRef:
+ name: minecraft-tailnet-tunnel-auth
+ tailscale:
+ hostname: my-minecraft-server
+ tags:
+ - tag:gameplane
+ - tag:minecraft
+```
+
+**playit.gg example:**
+```yaml
+apiVersion: gameplane.local/v1alpha1
+kind: GameServer
+metadata:
+ name: minecraft-playit
+ namespace: gameplane-games
+spec:
+ templateRef:
+ name: minecraft-java
+ networking:
+ expose: ClusterIP
+ tunnel:
+ enabled: true
+ provider: playit
+ credentialsSecretRef:
+ name: minecraft-playit-tunnel-auth
+```
+
+### Step 3: Provide the credential
+
+From v0.3.0 the operator only mounts a credentials Secret whose `ownerReference` matches this GameServer's name **and UID**. A plain `kubectl create secret generic` Secret has none, and a Secret left over from a deleted same-name GameServer has a stale UID, so the operator refuses it: `TunnelReady` turns `False` with reason `TunnelCredentialRefused` (naming the Secret, never its contents) and the tunnel Deployment is scaled to zero. Provide the credential in one of these ways:
+
+**Dashboard (recommended):**
+1. Create the server (or edit an existing one).
+2. Open the **Networking** tab.
+3. Enter the credential (frp token, Tailscale auth key, or playit secret key).
+4. Save — the dashboard creates the Secret with the correct ownerReference.
+
+**API:**
+```bash
+curl -X PUT \
+ -b "gameplane_session=$SESSION" \
+ -H "X-Gameplane-CSRF: $CSRF" \
+ -H "Content-Type: application/json" \
+ -d '{"provider": "frp", "values": {"token": "your-secure-token-here"}}' \
+ "https://gameplane.example.com/servers/minecraft-prod:tunnel-credentials?namespace=gameplane-games"
+```
+
+For Tailscale: `{"provider": "tailscale", "values": {"authKey": "tskey-client-xxxxx"}}`
+
+For playit: `{"provider": "playit", "values": {"secretKey": ""}}`
+
+**kubectl with ownerReference:**
+If you must create the Secret directly, first get the GameServer's UID:
+```bash
+kubectl -n gameplane-games get gameserver minecraft-prod -o jsonpath='{.metadata.uid}'
+```
+
+Then create a Secret with an ownerReference:
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: minecraft-prod-tunnel-auth
+ namespace: gameplane-games
+ ownerReferences:
+ - apiVersion: gameplane.local/v1alpha1
+ kind: GameServer
+ name: minecraft-prod
+ uid:
+type: Opaque
+stringData:
+ token: "your-secure-token-here" # for frp
+ # authKey: "tskey-client-xxxxx" # for Tailscale
+ # secretKey: "" # for playit
+```
+
+**Credential refused?** Check the server's `TunnelReady` condition:
+```bash
+kubectl -n gameplane-games get gameserver minecraft-prod \
+ -o jsonpath='{.status.conditions[?(@.type=="TunnelReady")]}' | jq
+```
+
+If the reason is `TunnelCredentialRefused`, the Secret lacks an ownerReference or has a stale one. The dashboard and API always write `-tunnel-auth`: for a refused Secret with any other name, they create an owned one and point `credentialsSecretRef` at it. If the refused Secret is itself named `-tunnel-auth` (as in the examples above), they answer `409 Conflict` instead; add the ownerReference to it, or delete it and save the credential again.
+
+## Tailscale ACL tags
+
+If you set `spec.networking.tunnel.tailscale.tags`, the tunnel pod requests those tags when registering with your tailnet. **Your tailnet's ACL must grant `tagOwners` for those tags to the auth key's owner**, otherwise Tailscale refuses the request.
+
+If the ACL doesn't grant the tags:
+1. The tunnel pod logs the error.
+2. It tries again without tags.
+3. If the auth key is single-use, this fallback fails and the device stays logged out.
+
+Configure `tagOwners` in your Tailscale ACL before setting tags on the GameServer.
+
+Each tag must be `tag:` or a bare ``, where the name starts with a letter and holds only letters, digits and hyphens. If any tag is invalid, the whole list is logged and ignored and the device registers untagged.
+
+## Cost and resource considerations
+
+A tunnel pod runs 24/7 to hold the public address, even when the game server is idle or asleep:
+
+- **Running game:** You pay for 2 pods (game + tunnel).
+- **Idle game:** You pay for 1 pod (tunnel only).
+- **Tunnel resources:** ~50 MiB memory, negligible CPU — small but not free.
+
+To minimize idle costs:
+
+- Use playit's free tier (8 hrs/day) or a paid tunnel provider with time-based billing.
+- Disable idle auto-sleep (`spec.idle.enabled: false`) and stop servers manually.
+- Use Tailscale (free, open-source).
+- Route multiple servers through one relay (if your relay supports it).
+
+## Interaction with other networking settings
+
+Tunnels layer over your backing Service. Other networking fields apply to the Service, not tunnel traffic:
+
+- **`hostname` / external-dns:** Applies to the Service, not the tunnel address. If your tunnel provider assigns a public address, point your DNS CNAME to that address, not to the `hostname` field.
+- **`sourceRanges` (IP allow-list):** Applies to the Service port. Tunnel traffic originates from the relay, not the player, so `sourceRanges` does not gate it. The relay provider (frp, Tailscale, playit) handles authentication and access control.
+- **`portOverrides` (NodePort specifics):** Applies to NodePort-mode exposures only. Tunnels ignore this.
+
+## Interaction with idle auto-sleep
+
+When a server enters idle sleep:
+
+- **Tunnel pod stays running** — it holds the relay connection and keeps the public address advertised.
+- **Game pod sleeps** — the game container is scaled down.
+- **Wake-on-connect works** — if a player connects to the public address while asleep, the relay wakes the game pod (if `spec.idle.wakeOnConnect` is enabled). Depending on the relay provider, the player may need to reconnect once after the game wakes.
+
+To avoid reconnects on wake:
+
+- Use Tailscale (better connection state handling).
+- Keep `spec.idle.enabled: false` and stop servers manually.
+- Disable `spec.idle.wakeOnConnect` so players only join running servers.
+
+## Troubleshooting
+
+### No address appears in `status.endpoints`
+
+1. **Check the tunnel pod is running:**
+ ```bash
+ kubectl -n gameplane-games get pods -l app.kubernetes.io/name=gameplane-tunnel,app.kubernetes.io/instance=minecraft-prod
+ ```
+
+2. **Check tunnel pod logs:**
+ ```bash
+ kubectl -n gameplane-games logs -f
+ ```
+ Look for connection errors, auth failures, or provider-specific messages.
+
+3. **Verify the credential Secret's keys** (without printing the credential):
+ ```bash
+ kubectl -n gameplane-games get secret minecraft-prod-tunnel-auth -o jsonpath='{.data}' | jq 'keys'
+ ```
+ Ensure the key name matches the provider (`token` for frp, `authKey` for Tailscale, `secretKey` for playit).
+
+4. **Check the TunnelReady condition:**
+ ```bash
+ kubectl -n gameplane-games get gameserver minecraft-prod \
+ -o jsonpath='{.status.conditions[?(@.type=="TunnelReady")]}' | jq
+ ```
+ If the reason is `TunnelCredentialRefused`, add an ownerReference to the Secret or delete it and re-enter the credential via the dashboard. While the Secret is refused, the tunnel Deployment stays scaled to zero.
+
+5. **Check NetworkPolicy:**
+ With `networkPolicies.enabled` (the default), the games namespace denies egress by default, and the operator creates a `-tunnel-egress` NetworkPolicy that lets the tunnel pod reach DNS (TCP/UDP 53), the game's advertised container ports, and its relay:
+ - **frp:** the frp server's `serverAddr:serverPort` (default TCP 7000)
+ - **Tailscale:** the control plane on TCP 443 and DERP relays on UDP 41641
+ - **playit:** any destination on any port, because playit publishes no fixed relay endpoints (from v0.3.0; in v0.2.0-beta.8 playit cannot reach its relay under this policy)
+
+ If the tunnel pod cannot reach the relay, verify the NetworkPolicy is not blocking it:
+ ```bash
+ kubectl -n gameplane-games get networkpolicy
+ kubectl -n gameplane-games exec -- nc -zv
+ ```
+
+### Address keeps changing (playit.gg)
+
+On playit's free tier, addresses are not persistent — they may change if the pod restarts or the 8-hour daily window closes. Upgrade to a paid plan for stable addresses.
+
+### Players can connect but get connection refused
+
+1. Verify the game pod is running and ready:
+ ```bash
+ kubectl -n gameplane-games get pods minecraft-prod-0 -o wide
+ ```
+
+2. Check the game is listening on the expected port inside the pod.
+
+3. Verify the relay is configured to forward the port (check your frp config, playit dashboard, etc.).
+
+### Tailscale: server not appearing in `tailscale status`
+
+1. Check pod logs for auth errors or ACL tag rejections.
+2. Verify the auth key is valid and not expired (log into login.tailscale.com).
+3. Confirm the `hostname` in `spec.networking.tunnel.tailscale.hostname` (defaults to GameServer name if empty).
+
+## Security
+
+- **Credentials are Secrets:** Relay credentials (frp token, Tailscale auth key, playit secret key) are stored as Kubernetes Secrets in the games namespace and are never returned through the API. Only cluster administrators can read them directly.
+- **Tunnel images are signed:** Tunnel client images (frp, Tailscale, playit) are cosign-signed like all Gameplane images. Verify them with `cosign verify --key cosign.pub`.
+- **Relay trust model:** You are trusting the relay operator (your own frps VPS, Tailscale Inc., or playit.gg) not to inspect or modify player connections. For production, self-hosted frp gives you full control; Tailscale and playit are third-party services — review their privacy policies before use.
+- **Tailscale is invite-only:** Tailscale servers are only reachable by tailnet members, so they don't add public-internet attack surface.
+- **frp and playit are public:** Both expose servers to the internet — apply the same security practices as you would for a public LoadBalancer (disable admin consoles, keep images updated, restrict in-game permissions).
+
+---
+
+**Next guide:** [Backups and recovery](../backups-recovery/)
diff --git a/src/content/docs/sign-in-recovery.mdx b/src/content/docs/sign-in-recovery.mdx
index 0dd5dcb..153fbfa 100644
--- a/src/content/docs/sign-in-recovery.mdx
+++ b/src/content/docs/sign-in-recovery.mdx
@@ -46,11 +46,7 @@ Local accounts use a Gameplane credential; provider buttons delegate password an
**Local auth** requires the account to exist in Gameplane's local user list — created manually under **Admin Settings → Users** or automatically on first OIDC login (assigned the viewer role by default). Each local account stores an `argon2id`-hashed password; the API enforces a 12-character minimum.
-**OIDC** (OpenID Connect) delegates authentication to a third-party identity provider (Keycloak, Authentik, Google Workspace, Okta, etc.). The provider handles password storage and MFA; Gameplane stores only the OIDC subject ID (issuer + subject claim). Each provider can map IdP groups to dashboard roles (a groups claim, per-role group lists and a default role); a user with no matching group gets the default role, `viewer` unless you change it. Seeding these mappings from Helm values arrives in v0.3.0. Providers are configured under **Admin Settings → Authentication** and take effect on the next user login (no API restart required).
-
-
- The Helm chart will be able to seed each provider's group-to-role mappings from values (`api.oidc.groupsClaim`, `roleMappings`, `defaultRole`), so a fresh install derives roles on first login without post-deployment configuration.
-
+**OIDC** (OpenID Connect) delegates authentication to a third-party identity provider (Keycloak, Authentik, Google Workspace, Okta, etc.). The provider handles password storage and MFA; Gameplane stores only the OIDC subject ID (issuer + subject claim). Each provider can map IdP groups to dashboard roles (a groups claim, per-role group lists and a default role); a user with no matching group gets the default role, `viewer` unless you change it. From v0.3.0, Helm values (`api.oidc.groupsClaim`, `roleMappings`, `defaultRole`) seed these mappings for the Helm-configured provider, so a fresh OIDC-only install derives roles on first login without post-deployment configuration. Providers are configured under **Admin Settings → Authentication** and take effect on the next user login (no API restart required).
For provider-specific setup steps, see [Users, Auth & RBAC](../users-auth-rbac/).
diff --git a/src/content/docs/status-phases-conditions.mdx b/src/content/docs/status-phases-conditions.mdx
index e473b6f..74e6a80 100644
--- a/src/content/docs/status-phases-conditions.mdx
+++ b/src/content/docs/status-phases-conditions.mdx
@@ -40,6 +40,7 @@ GameServer conditions align with the Kubernetes Condition API (`metav1.Condition
| **Progressing** | A spec change is being rolled out | True / False |
| **Healthy** | Liveness/readiness probes pass | True / False |
| **AddressAssignment** | External address allocated (LoadBalancer mode) | True / False / Unknown |
+| **TunnelReady** | Tunnel Deployment is ready and its provider config is complete (ports mapped, credentials present); present only while a tunnel is enabled. From v0.3.0, reason `TunnelCredentialRefused` means the credential Secret is not owned by the GameServer | True / False / (absent) |
| **DataWipe** | Wipe-data job succeeded | True / False / (absent) |
## Backup & Restore
@@ -90,7 +91,7 @@ A ModuleSource indexes a catalog (Git, OCI registry, HTTP, or local) and caches
## Cluster health phases
-A remote Cluster's phase reflects the last health check:
+A remote Cluster's phase reflects the last Kubernetes API health check. It does not probe the optional agent gateway, so a Healthy cluster can still have an unreachable gateway:
| Phase | Meaning |
|-------|---------|
diff --git a/src/content/docs/testing-coverage-e2e.mdx b/src/content/docs/testing-coverage-e2e.mdx
index b9c6ea7..d3b64b0 100644
--- a/src/content/docs/testing-coverage-e2e.mdx
+++ b/src/content/docs/testing-coverage-e2e.mdx
@@ -61,6 +61,8 @@ The 11 buckets partition tests by **login pressure, not feature area**. The API
Use `BUCKET= make test-e2e-bucket` to run a single bucket locally against a live cluster.
+The separate `ingress-nginx smoke (kind)` CI job boots a Kind cluster with the node config `make dev-up` uses, applies the dev ingress-nginx manifest pinned in `deploy/kind/up.sh` and waits for the controller rollout. It runs only when `deploy/kind/**` or the CI workflow file changes.
+
+## CRD updates on upgrade
+
+`helm upgrade` never updates CRDs (a Helm limitation: files under a chart's `crds/` directory are installed once and ignored afterwards). The chart works around this with `crds.autoApply` (enabled by default), a pre-install/pre-upgrade hook that runs `kubectl apply --server-side` over the current CRDs. No manual `kubectl apply` step is needed. The hook runs on every `helm upgrade`, and on a `helm install` only when the cluster holds CRDs from a different chart version that an earlier, uninstalled release left behind. A genuinely fresh install never depends on pulling the hook's `kubectl` image.
+
+If you set `crds.autoApply.enabled=false`, apply the CRDs yourself:
+
+```sh
+kubectl apply --server-side -f charts/gameplane/crds/
+```
+
+- **Helm 3, reinstalling over leftover CRDs:** the hook runs, so air-gapped clusters need `crds.autoApply.image` mirrored for that case.
+- **Helm 4:** `crds/` is applied natively with a server-side apply under the field manager `helm`. CRDs last applied by v0.2.0-beta.8 or earlier were applied under the `kubectl` manager, so a Helm 4 `helm install` over such leftovers stops with `conflict with "kubectl" ... .spec.versions`. Re-run it once with `--force-conflicts`:
+
+```sh
+helm install gameplane charts/gameplane -n gameplane-system --create-namespace --force-conflicts
+```
+
+
+The Helm 4 field-manager behavior and the hook-on-reinstall behavior above are part of v0.3.0.
+
+
+## Upgrade notes for v0.3.0
+
+Read these before upgrading from v0.2.0-beta.8:
+
+- **Every existing GameServer rolls once.** Network capture needs a pre-provisioned volume on every game pod, because Kubernetes ephemeral containers can add containers to a running pod but not volumes. The capture `emptyDir` is therefore added to every pod template, and each pod is recreated once on upgrade, interrupting active player sessions. This does not repeat on later starts. Servers that do not opt into capture carry only an empty, unused volume: no capture sidecar, no `CAP_NET_RAW`, no network change. Plan the upgrade window accordingly.
+- **Owner-only server operations.** Transferring ownership, editing collaborators, wiping data and deleting a server now need the server's owner or an admin. Operator-role users keep their other server permissions but can no longer run these four operations on servers they do not own. Servers with no recorded owner (for example created with kubectl or GitOps) can be transferred, wiped or deleted only by an admin.
+- **Playit tunnel egress widened.** The operator-managed NetworkPolicy for a GameServer using the `playit` tunnel provider now adds an unrestricted egress rule (all ports and protocols, any destination) alongside the DNS/advertised-ports rule every provider gets. Previously playit could not reach its relay.
+- **Helm 3 and Helm 4 CRD handling.** See the section above, including the one-time `--force-conflicts` for Helm 4.
+- **Tunnel credential Secrets must be owned by their GameServer.** The operator refuses a tunnel credentials Secret whose `ownerReference` does not match the GameServer's name and UID, scales the tunnel to zero and reports `TunnelReady=False` with reason `TunnelCredentialRefused`. Secrets created with plain `kubectl create secret` for v0.2.0-beta.8 are refused after the upgrade: re-enter the credential in the server's Networking tab (which writes an owned `-tunnel-auth` Secret) or add the ownerReference. See [Server Networking & Tunnels](../server-networking-tunnels/).
+
## Rollback decision
Close the rollback window only when:
diff --git a/src/content/docs/user-administration.mdx b/src/content/docs/user-administration.mdx
index f9fb679..0cf83cf 100644
--- a/src/content/docs/user-administration.mdx
+++ b/src/content/docs/user-administration.mdx
@@ -16,8 +16,8 @@ import DocsQuickReference from "../../components/docs/DocsQuickReference.astro";
Invite and maintain local users, reset credentials, assign roles, and distinguish current capabilities from planned service accounts.
-
- Service-account creation is availability-dependent; do not document a token workflow until the shipped UI and API support it.
+
+ Gameplane has no service accounts or API tokens yet; machine-to-machine tokens are planned for v1.1. Until then, give automation its own local user with the narrowest role that works, and never share a person's login.
## Invite a local user
@@ -65,9 +65,10 @@ Role changes affect global authorization; local and OIDC credentials have differ
- MFA and account disablement are provider-controlled
- The user's role and email in Gameplane mirror the provider's state
-
- Install-time OIDC role auto-mapping via `api.oidc.roleMappings` ships in v0.3.0. In beta.8, map OIDC roles manually after each login.
-
+Users who log in via OIDC have their role determined by:
+- **Per-provider group mappings** configured under **Admin Settings → Authentication**, re-evaluated against the user's IdP groups on each login
+- **Helm-seeded group mappings** (`api.oidc.groupsClaim`, `api.oidc.roleMappings`) for the Helm-configured provider, from v0.3.0; an admin can override them in the dashboard
+- **Manual assignment** when no group mapping is configured: new OIDC users get the default role (`viewer`) and an admin promotes them
## Review lifecycle and availability
diff --git a/src/content/docs/users-auth-rbac.mdx b/src/content/docs/users-auth-rbac.mdx
index 97f0e77..b15318f 100644
--- a/src/content/docs/users-auth-rbac.mdx
+++ b/src/content/docs/users-auth-rbac.mdx
@@ -25,7 +25,7 @@ Keep a local admin session open while testing the identity provider, and verify
## Users and service accounts
-Invite and edit local users, reset credentials deliberately, and use API tokens for automation rather than shared logins.
+Invite and edit local users, reset credentials deliberately, and give automation its own account rather than a shared login.
-Local users are invited via the dashboard (**Admin Settings → Users → Add user**). Set the user's initial password directly (or leave it blank for an OIDC-only account) and share it with them out of band; there is no built-in email invite flow. To create service accounts for automation, create a local user without initial password access, grant them an appropriate custom role or built-in role (usually `viewer` or `operator` scoped to specific namespaces), and have them generate API tokens from their account settings.
+Local users are invited via the dashboard (**Admin Settings → Users → Add user**). Set the user's initial password directly (or leave it blank for an OIDC-only account) and share it with them out of band; there is no built-in email invite flow. Gameplane has no API tokens yet. For automation, create a dedicated local user with an appropriate custom or built-in role (usually `viewer` or `operator` scoped to specific namespaces) and have scripts sign in as it, sending the session cookie and CSRF header as shown in the [REST API reference](../rest-api-reference/).
## Roles and permissions
@@ -115,13 +115,15 @@ When a user logs in via OIDC for the first time, Gameplane creates a user row wi
### Install-time role mappings
-> **Coming in v0.3.0**
->
-> Install-time Helm role mappings eliminate the need for a bootstrap-admin account in OIDC-only deployments. Operators configure group mappings at install time, and the first user to log in receives the correct role immediately based on their IdP group membership.
+
+ Install-time role mappings are not available in v0.2.0-beta.8.
+
+
+Helm role mappings eliminate the need for a `bootstrap-admin` account in OIDC-only deployments. Operators configure group mappings at install time, and the first user to log in receives the correct role immediately based on their IdP group membership.
-When deploying with Helm, set `api.oidc.groupsClaim` to the name of your IdP's groups claim (e.g., `"groups"`), and configure `api.oidc.roleMappings.{admin,operator,viewer}` with a comma-separated list of group names for each role. On first login, Gameplane matches the user's group membership against the mappings and assigns the highest-privilege matching role.
+When deploying with Helm, set `api.oidc.enabled=true`, `api.oidc.groupsClaim` to the name of your IdP's groups claim (e.g., `"groups"`), and configure `api.oidc.roleMappings.{admin,operator,viewer}` with a list of group names for each role. On first login, Gameplane matches the user's group membership against the mappings and assigns the highest-privilege matching role.
-After deployment, admins can override role mappings through the dashboard without restarting the API. Helm upgrades preserve dashboard customizations — they do not overwrite group mappings that have already been edited in the UI.
+After deployment, admins can override role mappings through the dashboard without restarting the API. Helm upgrades preserve dashboard customizations — they do not overwrite group mappings that have already been edited in the UI. For detailed Helm value examples and troubleshooting, see the [OIDC provider setup documentation](https://github.com/ValgulNecron/Gameplane/blob/master/docs/oidc.md#role-mapping-at-install-time-no-bootstrap-admin-required) on GitHub.
diff --git a/src/content/docs/websocket-event-streams.mdx b/src/content/docs/websocket-event-streams.mdx
index 5e07378..5c2d200 100644
--- a/src/content/docs/websocket-event-streams.mdx
+++ b/src/content/docs/websocket-event-streams.mdx
@@ -25,6 +25,10 @@ Game servers expose real-time console and log output via WebSocket. Both require
| `/ws/servers/{name}/logs` | WebSocket | `servers:read` | Tail the configured game log file through the agent. |
| `/ws/servers/{name}/logs/pod?from=start\|end` | WebSocket | `servers:read` | Stream container stdout through Kubernetes; `from=start` stitches init containers and setup logs before the game container, `from=end` tails recent game output only. |
+
+ Sockets target the cluster of the server you opened. Pod stdout/startup logs and PTY attach use that cluster's Kubernetes API. The RCON console, game log file tail and other agent-based streams go through the cluster's optional [agent gateway](../remote-agent-gateway/). A cluster without a compatible gateway fails these streams rather than reaching a same-named local server.
+
+
## Events and lifecycle
The `/events` stream (Server-Sent Events) broadcasts Kubernetes watch notifications for all resources in scope. File downloads use plain HTTP rather than WebSocket.
@@ -33,7 +37,7 @@ The `/events` stream (Server-Sent Events) broadcasts Kubernetes watch notificati
|--------|-----------|------------|-------------|
| `/events` | SSE | `servers:read` | Cluster-resource invalidation events (not durable history; clients must poll for any events they missed while disconnected). |
| `[GET] /servers/{name}/logs/download` | HTTP | `servers:read` | Download a server's complete game log file without opening a socket. |
-| `cluster change` | client lifecycle | `scope-aware` | When switching clusters or workspaces, cancel old WebSocket connections and open new ones in the new scope to avoid stale state. |
+| `cluster change` | client lifecycle | `scope-aware` | A socket stays bound to the cluster and namespace of the server that opened it, and reconnects retain that target. Leaving the server view closes its streams, so a new view opens fresh sockets in its own scope and never inherits stale state. |
| `disconnect` | close + UI state | — | Use bounded exponential backoff on reconnect and display stale/disconnected state to the user instead of silently retrying forever. |
## Authentication and scope
diff --git a/website-export/MANIFEST.md b/website-export/MANIFEST.md
index 7288542..5ff4efd 100644
--- a/website-export/MANIFEST.md
+++ b/website-export/MANIFEST.md
@@ -40,3 +40,22 @@ The lunaris library (`H:`-prefixed ids and variables) was removed from the desig
of `design-assets/gameplane-icon.png`; Not Found `vh8hz` shows `design-assets/gameplane-mark.png` (PNGs rendered from
the SVG logos, since Pencil does not render SVG image fills).
- Re-exported: JSON for the 102 screens/components (8 changed), screenshots for all 102 (component instances changed).
+
+## 2026-10-06 incremental: docs sync with the main repo (v0.3.0 docs)
+
+Docs screens updated to mirror the `docs: sync the docs with the main repo's October changes` commit (pages under
+`src/content/docs/`). Condensed text only, plus new sections where a page gained an H2; v0.3.0-only content is marked.
+
+- New screens: `QHFdH` (Docs — Remote Agent Gateway, copied from `CuW6O`) and `wCt6V` (Docs — Server Networking &
+ Tunnels, copied from `s3NwF`).
+- New sections: `OIN7m` (Multiple clusters and the gateway), `CuW6O` (Manage independent clusters), `D4SNhN` (CRD updates
+ on upgrade, Upgrade notes for v0.3.0), `KFLCi` (API ↔ remote gateway), `N4TBV` (Submitting a change, Game modules and
+ submodules), `TESyw` (folder delete item), `s3NwF` (relay tunnels item).
+- Text and pager updates: `zeM79` `A2sAtw` `SKMOT` `QdLzE` `P5qsD` `rW1Ux` `n8pnD` `HpLFh` `X0DeYH` `p5Gci` `BEcZK`
+ `b5u3Ui` `o7PQv` `ydcNa` `cpCjN` `Nnd3Y` `rqx1v` `b5srx` `aZAEs` `gEOuV` `mYDXb` `yuCG9` `ZwM1N` `VgF1s`.
+- Heights changed: `OIN7m` 1000, `CuW6O` 1120, `D4SNhN` 1000, `N4TBV` 960, `HpLFh` 1280, `BEcZK` 1000, `VgF1s` 1000,
+ `QHFdH` 1140.
+- Reviewed with no change: `VtYGz` `kTRsn` `ZTnNa` `upO5i` `eDaA6` `ac8lF` (nothing on the screen maps to the page diff;
+ not re-exported).
+- JSON: `Print(JSON.stringify(Get(id, {depth: 30, includePathGeometry: true})))`, re-serialized with `jq .`; `jq empty`
+ passes, zero `"..."` elisions, node counts match the live file. Screenshots: `Export([id], "png", …, {scale: 2})`.
diff --git a/website-export/json/A2sAtw.json b/website-export/json/A2sAtw.json
index 0c93b75..23779e3 100644
--- a/website-export/json/A2sAtw.json
+++ b/website-export/json/A2sAtw.json
@@ -1,943 +1 @@
-{
- "type": "frame",
- "id": "A2sAtw",
- "x": 0,
- "y": 88768,
- "name": "Screen/Docs — API & Realtime Development",
- "theme": {
- "semantic": "dark"
- },
- "clip": true,
- "width": 1440,
- "height": 900,
- "fill": "$background/background",
- "children": [
- {
- "id": "rPLWm",
- "type": "ref",
- "ref": "K2Rt4H",
- "height": "fill_container",
- "name": "DocsSidebar/Expanded",
- "width": 280
- },
- {
- "type": "frame",
- "id": "cs3r4",
- "name": "Docs Main",
- "width": "fill_container",
- "height": "fill_container",
- "fill": "$background/background",
- "layout": "vertical",
- "children": [
- {
- "id": "fCFDT",
- "type": "ref",
- "ref": "SOdlr",
- "name": "Docs Top Bar",
- "width": "fill_container",
- "descendants": {
- "NImey": {
- "content": "Develop"
- },
- "ikbi6": {
- "content": "API & Realtime"
- }
- }
- },
- {
- "type": "frame",
- "id": "IAsmJ",
- "name": "Docs Article Body",
- "clip": true,
- "width": "fill_container",
- "height": "fill_container",
- "gap": 32,
- "padding": 32,
- "children": [
- {
- "type": "frame",
- "id": "Jg1dx",
- "name": "Docs Article",
- "width": 780,
- "layout": "vertical",
- "gap": 14,
- "children": [
- {
- "type": "text",
- "id": "C7uJdH",
- "name": "Article Eyebrow",
- "fill": "$accent/accent",
- "content": "DEVELOP",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11,
- "fontWeight": "600",
- "letterSpacing": 0.8
- },
- {
- "type": "text",
- "id": "NsLjr",
- "name": "Article Title",
- "fill": "$foreground/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "API and realtime development",
- "lineHeight": 1.1,
- "fontFamily": "JetBrains Mono",
- "fontSize": 30,
- "fontWeight": "700"
- },
- {
- "type": "text",
- "id": "irtr9",
- "name": "Article Lead",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Add REST, SSE, or WebSocket behavior while preserving authentication, scoping, audit, and agent-proxy invariants.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 15,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "DDMKj",
- "name": "Article Metadata",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "id": "IEQYX",
- "type": "ref",
- "ref": "NZPtq",
- "name": "Meta Chip 0",
- "descendants": {
- "NLG8t": {
- "content": "DEVELOPER GUIDE"
- }
- }
- },
- {
- "id": "uLXEY",
- "type": "ref",
- "ref": "EbSsA",
- "name": "Meta Chip 1",
- "descendants": {
- "yb6zz": {
- "content": "v0.2"
- }
- }
- },
- {
- "id": "y0JNS",
- "type": "ref",
- "ref": "EbSsA",
- "name": "Meta Chip 2",
- "descendants": {
- "yb6zz": {
- "content": "18 MIN"
- }
- }
- }
- ]
- },
- {
- "type": "frame",
- "id": "jESit",
- "name": "Article Callout",
- "width": "fill_container",
- "fill": "$accent/soft",
- "cornerRadius": "$radius/xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "gap": 10,
- "padding": [
- 10,
- 12
- ],
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "rEdo7",
- "name": "Article Callout Icon",
- "width": 16,
- "height": 16,
- "icon": "radio-tower",
- "library": "lucide",
- "fill": "$accent/soft-foreground"
- },
- {
- "type": "text",
- "id": "iqabn",
- "name": "Article Callout Text",
- "fill": "$accent/soft-foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "A route is incomplete until permission, cluster and namespace scope, safe errors, audit, tests, and docs are defined.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "qDKh5",
- "name": "Guide Section 1",
- "width": "fill_container",
- "layout": "vertical",
- "gap": 6,
- "children": [
- {
- "type": "text",
- "id": "ziBVE",
- "name": "Section Title 1",
- "fill": "$foreground/foreground",
- "content": "REST gateway conventions",
- "fontFamily": "JetBrains Mono",
- "fontSize": 17,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "tly2e",
- "name": "Section Body 1",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Handlers stay close to Kubernetes resources while enforcing session, CSRF, permission, scope, and safe-error contracts.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 13,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "A6Axjr",
- "name": "Section List 1",
- "width": "fill_container",
- "fill": "$surface/surface",
- "cornerRadius": "$radius/2xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 5,
- "padding": [
- 8,
- 10
- ],
- "children": [
- {
- "type": "frame",
- "id": "TRuKF",
- "name": "Section 1 Item 0",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "S0a5Z",
- "name": "Section 1 Item Icon 0",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "u50kf",
- "name": "Section 1 Item Text 0",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Declare auth/RBAC, namespace, cluster, errors, and audit for every route.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "n67936",
- "name": "Section 1 Item 1",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "Z51Alr",
- "name": "Section 1 Item Icon 1",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "FizhH",
- "name": "Section 1 Item Text 1",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Mutations require session credentials and CSRF.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "yH9SU",
- "name": "Section 1 Item 2",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "BLHYT",
- "name": "Section 1 Item Icon 2",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "lkgtX",
- "name": "Section 1 Item Text 2",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Document external service-account behavior explicitly when available.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "jvLOC",
- "name": "Guide Section 2",
- "width": "fill_container",
- "layout": "vertical",
- "gap": 6,
- "children": [
- {
- "type": "text",
- "id": "S4F4BJ",
- "name": "Section Title 2",
- "fill": "$foreground/foreground",
- "content": "Realtime and proxy paths",
- "fontFamily": "JetBrains Mono",
- "fontSize": 17,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "QqzUG",
- "name": "Section Body 2",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Cluster events, console, logs, files, players, mods, status, and actions use different transports and upstreams.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 13,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "o1o0x",
- "name": "Section List 2",
- "width": "fill_container",
- "fill": "$surface/surface",
- "cornerRadius": "$radius/2xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 5,
- "padding": [
- 8,
- 10
- ],
- "children": [
- {
- "type": "frame",
- "id": "BFHJw",
- "name": "Section 2 Item 0",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "U94mhA",
- "name": "Section 2 Item Icon 0",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "q9mQs",
- "name": "Section 2 Item Text 0",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "/events is authenticated SSE for invalidation.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "RMHI7",
- "name": "Section 2 Item 1",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "X0ZrLY",
- "name": "Section 2 Item Icon 1",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "o51cc7",
- "name": "Section 2 Item Text 1",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Console and game logs are WebSockets through the agent.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "o76S0",
- "name": "Section 2 Item 2",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "riRcq",
- "name": "Section 2 Item Icon 2",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "vd2nI",
- "name": "Section 2 Item Text 2",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Pod logs come from Kubernetes and fail differently from agent streams.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "nNreg",
- "name": "Guide Section 3",
- "width": "fill_container",
- "layout": "vertical",
- "gap": 6,
- "children": [
- {
- "type": "text",
- "id": "x1GeE",
- "name": "Section Title 3",
- "fill": "$foreground/foreground",
- "content": "Add an endpoint",
- "fontFamily": "JetBrains Mono",
- "fontSize": 17,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "ZBxl2",
- "name": "Section Body 3",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Mount once, add permission where needed, map errors centrally, test the right layer, and update UI and reference docs together.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 13,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "dv4Gt",
- "name": "Quick Reference",
- "width": "fill_container",
- "fill": "$surface/tertiary",
- "cornerRadius": "$radius/xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 3,
- "padding": 12,
- "children": [
- {
- "type": "text",
- "id": "gPq0B",
- "name": "Quick Reference Label",
- "fill": "$accent/accent",
- "content": "API CHANGE",
- "fontFamily": "JetBrains Mono",
- "fontSize": 9,
- "fontWeight": "600",
- "letterSpacing": 0.6
- },
- {
- "type": "text",
- "id": "CaiKh",
- "name": "Quick Reference Line 1",
- "fill": "$foreground/foreground",
- "content": "01 cd api && go test ./...",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11.5,
- "fontWeight": "normal"
- },
- {
- "type": "text",
- "id": "DcR5Q",
- "name": "Quick Reference Line 2",
- "fill": "$foreground/foreground",
- "content": "02 make test-integration",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11.5,
- "fontWeight": "normal"
- },
- {
- "type": "text",
- "id": "R0dExb",
- "name": "Quick Reference Line 3",
- "fill": "$foreground/foreground",
- "content": "03 rg 'Mount|/ws/|/events' api/cmd api/internal",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11.5,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "kPxlJ",
- "name": "Article Next Link",
- "width": "fill_container",
- "stroke": "$border/border",
- "strokeWidth": {
- "top": 1
- },
- "strokeAlignment": "inner",
- "strokeLinejoin": "round",
- "padding": [
- 10,
- 0,
- 0,
- 0
- ],
- "justifyContent": "space_between",
- "alignItems": "center",
- "children": [
- {
- "type": "frame",
- "id": "xdLtu",
- "name": "Next Link Text",
- "layout": "vertical",
- "gap": 2,
- "children": [
- {
- "type": "text",
- "id": "xjU3H",
- "name": "Next Link Eyebrow",
- "fill": "$field/placeholder",
- "content": "NEXT GUIDE",
- "fontFamily": "Geist",
- "fontSize": 10,
- "fontWeight": "normal"
- },
- {
- "type": "text",
- "id": "oceKE",
- "name": "Next Link Title",
- "fill": "$foreground/foreground",
- "content": "Operator and CRD development",
- "fontFamily": "JetBrains Mono",
- "fontSize": 13,
- "fontWeight": "500"
- }
- ]
- },
- {
- "type": "icon",
- "id": "eil64",
- "name": "Next Link Icon",
- "width": 15,
- "height": 15,
- "icon": "arrow-right",
- "library": "lucide",
- "fill": "$muted"
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "m7Qyt",
- "name": "Docs Page Aside",
- "width": 250,
- "layout": "vertical",
- "gap": 16,
- "children": [
- {
- "type": "frame",
- "id": "tZZd1",
- "name": "Docs Table of Contents",
- "width": "fill_container",
- "fill": "$surface/secondary",
- "cornerRadius": "$radius/2xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 10,
- "padding": 16,
- "children": [
- {
- "type": "text",
- "id": "jKISY",
- "name": "TOC Label",
- "fill": "$field/placeholder",
- "content": "ON THIS PAGE",
- "fontFamily": "JetBrains Mono",
- "fontSize": 9,
- "fontWeight": "600",
- "letterSpacing": 0.7
- },
- {
- "type": "frame",
- "id": "Zm1pW",
- "name": "TOC Row 0",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "DS8yv",
- "name": "TOC Icon 0",
- "width": 12,
- "height": 12,
- "icon": "hash",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "QcN6C",
- "name": "TOC Text 0",
- "fill": "$foreground/foreground",
- "content": "REST",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "500"
- }
- ]
- },
- {
- "type": "frame",
- "id": "cobHQ",
- "name": "TOC Row 1",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "lPGxs",
- "name": "TOC Icon 1",
- "width": 12,
- "height": 12,
- "icon": "hash",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "x3CCO",
- "name": "TOC Text 1",
- "fill": "$muted",
- "content": "Realtime",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "Kv3aq",
- "name": "TOC Row 2",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "c8bsWD",
- "name": "TOC Icon 2",
- "width": 12,
- "height": 12,
- "icon": "hash",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "N6aIUG",
- "name": "TOC Text 2",
- "fill": "$muted",
- "content": "Endpoint workflow",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "rectangle",
- "id": "u2tfyJ",
- "name": "TOC Divider",
- "fill": "$border/border",
- "width": "fill_container",
- "height": 1
- },
- {
- "type": "text",
- "id": "mn7Tc",
- "name": "Coverage Label",
- "fill": "$field/placeholder",
- "content": "COVERS",
- "fontFamily": "JetBrains Mono",
- "fontSize": 9,
- "fontWeight": "600",
- "letterSpacing": 0.7
- },
- {
- "type": "frame",
- "id": "ag7PW",
- "name": "Coverage Row 0",
- "width": "fill_container",
- "gap": 7,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "giBO5",
- "name": "Coverage Icon 0",
- "width": 12,
- "height": 12,
- "icon": "book-open",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "t4CpwF",
- "name": "Coverage Text 0",
- "fill": "$muted",
- "content": "REST",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "KxnkC",
- "name": "Coverage Row 1",
- "width": "fill_container",
- "gap": 7,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "pCu5g",
- "name": "Coverage Icon 1",
- "width": 12,
- "height": 12,
- "icon": "book-open",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "bMj9M",
- "name": "Coverage Text 1",
- "fill": "$muted",
- "content": "SSE",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "z45bSf",
- "name": "Coverage Row 2",
- "width": "fill_container",
- "gap": 7,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "SWW58",
- "name": "Coverage Icon 2",
- "width": 12,
- "height": 12,
- "icon": "book-open",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "nfc2w",
- "name": "Coverage Text 2",
- "fill": "$muted",
- "content": "WebSockets",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "enFBJ",
- "name": "Edit Page Card",
- "width": "fill_container",
- "fill": "$background/background",
- "cornerRadius": "$radius/3xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 6,
- "padding": 14,
- "children": [
- {
- "type": "text",
- "id": "OdGM0",
- "name": "Edit Page Title",
- "fill": "$foreground/foreground",
- "content": "Improve this page",
- "fontFamily": "JetBrains Mono",
- "fontSize": 12,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "D0SsOg",
- "name": "Edit Page Body",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Found a gap? Open a docs issue or suggest an edit on GitHub.",
- "lineHeight": 1.4,
- "fontFamily": "Geist",
- "fontSize": 11.5,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "Gz2T2",
- "name": "Edit Page Link",
- "gap": 6,
- "alignItems": "center",
- "children": [
- {
- "type": "text",
- "id": "lLDlB",
- "name": "Edit Page Link Text",
- "fill": "$accent/soft-foreground",
- "content": "Edit this page",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "500"
- },
- {
- "type": "icon",
- "id": "LNe6j",
- "name": "Edit Page Link Icon",
- "width": 12,
- "height": 12,
- "icon": "external-link",
- "library": "lucide",
- "fill": "$accent/accent"
- }
- ]
- }
- ]
- }
- ]
- }
- ]
- }
- ]
- }
- ]
-}
+{"type":"frame","id":"A2sAtw","x":0,"y":88768,"name":"Screen/Docs — API & Realtime Development","theme":{"semantic":"dark"},"clip":true,"width":1440,"height":900,"fill":"$background/background","children":[{"id":"rPLWm","type":"ref","ref":"K2Rt4H","height":"fill_container","name":"DocsSidebar/Expanded","width":280},{"type":"frame","id":"cs3r4","name":"Docs Main","width":"fill_container","height":"fill_container","fill":"$background/background","layout":"vertical","children":[{"id":"fCFDT","type":"ref","ref":"SOdlr","name":"Docs Top Bar","width":"fill_container","descendants":{"NImey":{"content":"Develop"},"ikbi6":{"content":"API & Realtime"}}},{"type":"frame","id":"IAsmJ","name":"Docs Article Body","clip":true,"width":"fill_container","height":"fill_container","gap":32,"padding":32,"children":[{"type":"frame","id":"Jg1dx","name":"Docs Article","width":780,"layout":"vertical","gap":14,"children":[{"type":"text","id":"C7uJdH","name":"Article Eyebrow","fill":"$accent/accent","content":"DEVELOP","fontFamily":"JetBrains Mono","fontSize":11,"fontWeight":"600","letterSpacing":0.8},{"type":"text","id":"NsLjr","name":"Article Title","fill":"$foreground/foreground","textGrowth":"fixed-width","width":"fill_container","content":"API and realtime development","lineHeight":1.1,"fontFamily":"JetBrains Mono","fontSize":30,"fontWeight":"700"},{"type":"text","id":"irtr9","name":"Article Lead","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Add REST, SSE, or WebSocket behavior while preserving authentication, scoping, audit, and agent-proxy invariants.","lineHeight":1.45,"fontFamily":"Geist","fontSize":15,"fontWeight":"normal"},{"type":"frame","id":"DDMKj","name":"Article Metadata","gap":8,"alignItems":"center","children":[{"id":"IEQYX","type":"ref","ref":"NZPtq","name":"Meta Chip 0","descendants":{"NLG8t":{"content":"DEVELOPER GUIDE"}}},{"id":"uLXEY","type":"ref","ref":"EbSsA","name":"Meta Chip 1","descendants":{"yb6zz":{"content":"v0.2"}}},{"id":"y0JNS","type":"ref","ref":"EbSsA","name":"Meta Chip 2","descendants":{"yb6zz":{"content":"18 MIN"}}}]},{"type":"frame","id":"jESit","name":"Article Callout","width":"fill_container","fill":"$accent/soft","cornerRadius":"$radius/xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","gap":10,"padding":[10,12],"alignItems":"center","children":[{"type":"icon","id":"rEdo7","name":"Article Callout Icon","width":16,"height":16,"icon":"radio-tower","library":"lucide","fill":"$accent/soft-foreground"},{"type":"text","id":"iqabn","name":"Article Callout Text","fill":"$accent/soft-foreground","textGrowth":"fixed-width","width":"fill_container","content":"A route is incomplete until permission, cluster and namespace scope, safe errors, audit, tests, and docs are defined.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"qDKh5","name":"Guide Section 1","width":"fill_container","layout":"vertical","gap":6,"children":[{"type":"text","id":"ziBVE","name":"Section Title 1","fill":"$foreground/foreground","content":"REST gateway conventions","fontFamily":"JetBrains Mono","fontSize":17,"fontWeight":"600"},{"type":"text","id":"tly2e","name":"Section Body 1","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Handlers stay close to Kubernetes resources while enforcing session, CSRF, permission, scope, and safe-error contracts.","lineHeight":1.45,"fontFamily":"Geist","fontSize":13,"fontWeight":"normal"},{"type":"frame","id":"A6Axjr","name":"Section List 1","width":"fill_container","fill":"$surface/surface","cornerRadius":"$radius/2xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":5,"padding":[8,10],"children":[{"type":"frame","id":"TRuKF","name":"Section 1 Item 0","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"S0a5Z","name":"Section 1 Item Icon 0","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"u50kf","name":"Section 1 Item Text 0","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Declare auth/RBAC, namespace, cluster, errors, and audit for every route.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"n67936","name":"Section 1 Item 1","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"Z51Alr","name":"Section 1 Item Icon 1","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"FizhH","name":"Section 1 Item Text 1","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Mutations require session credentials and CSRF.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"yH9SU","name":"Section 1 Item 2","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"BLHYT","name":"Section 1 Item Icon 2","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"lkgtX","name":"Section 1 Item Text 2","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Document external service-account behavior explicitly when available.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]}]}]},{"type":"frame","id":"jvLOC","name":"Guide Section 2","width":"fill_container","layout":"vertical","gap":6,"children":[{"type":"text","id":"S4F4BJ","name":"Section Title 2","fill":"$foreground/foreground","content":"Realtime and proxy paths","fontFamily":"JetBrains Mono","fontSize":17,"fontWeight":"600"},{"type":"text","id":"QqzUG","name":"Section Body 2","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Cluster events, console, logs, files, players, mods, status, and actions use different transports and upstreams.","lineHeight":1.45,"fontFamily":"Geist","fontSize":13,"fontWeight":"normal"},{"type":"frame","id":"o1o0x","name":"Section List 2","width":"fill_container","fill":"$surface/surface","cornerRadius":"$radius/2xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":5,"padding":[8,10],"children":[{"type":"frame","id":"BFHJw","name":"Section 2 Item 0","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"U94mhA","name":"Section 2 Item Icon 0","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"q9mQs","name":"Section 2 Item Text 0","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"/events is authenticated SSE for invalidation.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"RMHI7","name":"Section 2 Item 1","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"X0ZrLY","name":"Section 2 Item Icon 1","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"o51cc7","name":"Section 2 Item Text 1","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Console and game logs are WebSockets through the agent.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"o76S0","name":"Section 2 Item 2","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"riRcq","name":"Section 2 Item Icon 2","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"vd2nI","name":"Section 2 Item Text 2","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Pod logs come from Kubernetes and fail differently from agent streams.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]}]}]},{"type":"frame","id":"nNreg","name":"Guide Section 3","width":"fill_container","layout":"vertical","gap":6,"children":[{"type":"text","id":"x1GeE","name":"Section Title 3","fill":"$foreground/foreground","content":"Add an endpoint","fontFamily":"JetBrains Mono","fontSize":17,"fontWeight":"600"},{"type":"text","id":"ZBxl2","name":"Section Body 3","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Mount once, add permission where needed, map errors centrally, test the right layer, and update UI and reference docs together.","lineHeight":1.45,"fontFamily":"Geist","fontSize":13,"fontWeight":"normal"},{"type":"frame","id":"dv4Gt","name":"Quick Reference","width":"fill_container","fill":"$surface/tertiary","cornerRadius":"$radius/xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":3,"padding":12,"children":[{"type":"text","id":"gPq0B","name":"Quick Reference Label","fill":"$accent/accent","content":"API CHANGE","fontFamily":"JetBrains Mono","fontSize":9,"fontWeight":"600","letterSpacing":0.6},{"type":"text","id":"CaiKh","name":"Quick Reference Line 1","fill":"$foreground/foreground","content":"01 cd api && go test ./...","fontFamily":"JetBrains Mono","fontSize":11.5,"fontWeight":"normal"},{"type":"text","id":"DcR5Q","name":"Quick Reference Line 2","fill":"$foreground/foreground","content":"02 make test-integration","fontFamily":"JetBrains Mono","fontSize":11.5,"fontWeight":"normal"},{"type":"text","id":"R0dExb","name":"Quick Reference Line 3","fill":"$foreground/foreground","content":"03 rg 'Mount|/ws/|/events' api/cmd api/internal","fontFamily":"JetBrains Mono","fontSize":11.5,"fontWeight":"normal"}]}]},{"type":"frame","id":"kPxlJ","name":"Article Next Link","width":"fill_container","stroke":"$border/border","strokeWidth":{"top":1},"strokeAlignment":"inner","strokeLinejoin":"round","padding":[10,0,0,0],"justifyContent":"space_between","alignItems":"center","children":[{"type":"frame","id":"xdLtu","name":"Next Link Text","layout":"vertical","gap":2,"children":[{"type":"text","id":"xjU3H","name":"Next Link Eyebrow","fill":"$field/placeholder","content":"NEXT GUIDE","fontFamily":"Geist","fontSize":10,"fontWeight":"normal"},{"type":"text","id":"oceKE","name":"Next Link Title","fill":"$foreground/foreground","content":"Kubectl & GitOps Recipes","fontFamily":"JetBrains Mono","fontSize":13,"fontWeight":"500"}]},{"type":"icon","id":"eil64","name":"Next Link Icon","width":15,"height":15,"icon":"arrow-right","library":"lucide","fill":"$muted"}]}]},{"type":"frame","id":"m7Qyt","name":"Docs Page Aside","width":250,"layout":"vertical","gap":16,"children":[{"type":"frame","id":"tZZd1","name":"Docs Table of Contents","width":"fill_container","fill":"$surface/secondary","cornerRadius":"$radius/2xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":10,"padding":16,"children":[{"type":"text","id":"jKISY","name":"TOC Label","fill":"$field/placeholder","content":"ON THIS PAGE","fontFamily":"JetBrains Mono","fontSize":9,"fontWeight":"600","letterSpacing":0.7},{"type":"frame","id":"Zm1pW","name":"TOC Row 0","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"DS8yv","name":"TOC Icon 0","width":12,"height":12,"icon":"hash","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"QcN6C","name":"TOC Text 0","fill":"$foreground/foreground","content":"REST","fontFamily":"Geist","fontSize":12.5,"fontWeight":"500"}]},{"type":"frame","id":"cobHQ","name":"TOC Row 1","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"lPGxs","name":"TOC Icon 1","width":12,"height":12,"icon":"hash","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"x3CCO","name":"TOC Text 1","fill":"$muted","content":"Realtime","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"Kv3aq","name":"TOC Row 2","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"c8bsWD","name":"TOC Icon 2","width":12,"height":12,"icon":"hash","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"N6aIUG","name":"TOC Text 2","fill":"$muted","content":"Endpoint workflow","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"rectangle","id":"u2tfyJ","name":"TOC Divider","fill":"$border/border","width":"fill_container","height":1},{"type":"text","id":"mn7Tc","name":"Coverage Label","fill":"$field/placeholder","content":"COVERS","fontFamily":"JetBrains Mono","fontSize":9,"fontWeight":"600","letterSpacing":0.7},{"type":"frame","id":"ag7PW","name":"Coverage Row 0","width":"fill_container","gap":7,"alignItems":"center","children":[{"type":"icon","id":"giBO5","name":"Coverage Icon 0","width":12,"height":12,"icon":"book-open","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"t4CpwF","name":"Coverage Text 0","fill":"$muted","content":"REST","fontFamily":"Geist","fontSize":12,"fontWeight":"normal"}]},{"type":"frame","id":"KxnkC","name":"Coverage Row 1","width":"fill_container","gap":7,"alignItems":"center","children":[{"type":"icon","id":"pCu5g","name":"Coverage Icon 1","width":12,"height":12,"icon":"book-open","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"bMj9M","name":"Coverage Text 1","fill":"$muted","content":"SSE","fontFamily":"Geist","fontSize":12,"fontWeight":"normal"}]},{"type":"frame","id":"z45bSf","name":"Coverage Row 2","width":"fill_container","gap":7,"alignItems":"center","children":[{"type":"icon","id":"SWW58","name":"Coverage Icon 2","width":12,"height":12,"icon":"book-open","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"nfc2w","name":"Coverage Text 2","fill":"$muted","content":"WebSockets","fontFamily":"Geist","fontSize":12,"fontWeight":"normal"}]}]},{"type":"frame","id":"enFBJ","name":"Edit Page Card","width":"fill_container","fill":"$background/background","cornerRadius":"$radius/3xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":6,"padding":14,"children":[{"type":"text","id":"OdGM0","name":"Edit Page Title","fill":"$foreground/foreground","content":"Improve this page","fontFamily":"JetBrains Mono","fontSize":12,"fontWeight":"600"},{"type":"text","id":"D0SsOg","name":"Edit Page Body","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Found a gap? Open a docs issue or suggest an edit on GitHub.","lineHeight":1.4,"fontFamily":"Geist","fontSize":11.5,"fontWeight":"normal"},{"type":"frame","id":"Gz2T2","name":"Edit Page Link","gap":6,"alignItems":"center","children":[{"type":"text","id":"lLDlB","name":"Edit Page Link Text","fill":"$accent/soft-foreground","content":"Edit this page","fontFamily":"Geist","fontSize":12,"fontWeight":"500"},{"type":"icon","id":"LNe6j","name":"Edit Page Link Icon","width":12,"height":12,"icon":"external-link","library":"lucide","fill":"$accent/accent"}]}]}]}]}]}]}
diff --git a/website-export/json/BEcZK.json b/website-export/json/BEcZK.json
index b2c1236..0137a74 100644
--- a/website-export/json/BEcZK.json
+++ b/website-export/json/BEcZK.json
@@ -3,7 +3,7 @@
"id": "BEcZK",
"x": 0,
"y": 62548,
- "name": "Screen/Docs — High Availability",
+ "name": "Screen/Docs \u2014 High Availability",
"theme": {
"semantic": "dark"
},
@@ -539,7 +539,7 @@
"id": "yS5D3",
"name": "Quick Reference Line 1",
"fill": "$foreground/foreground",
- "content": "01 Replicate only components documented as replica-safe",
+ "content": "01 Replicate only replica-safe parts (Operator 2+; API stays at 1)",
"fontFamily": "JetBrains Mono",
"fontSize": 11.5,
"fontWeight": "normal"
@@ -940,4 +940,4 @@
]
}
]
-}
+}
\ No newline at end of file
diff --git a/website-export/json/CuW6O.json b/website-export/json/CuW6O.json
index 14aa4c6..85a3a55 100644
--- a/website-export/json/CuW6O.json
+++ b/website-export/json/CuW6O.json
@@ -9,7 +9,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1120,
"fill": "$background/background",
"children": [
{
@@ -568,6 +568,195 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "qDBwV",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "GCxoO",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "milestone",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "kAfre",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The optional agent gateway and the unified cross-cluster dashboard described below arrive in v0.3.0. In v0.2.0-beta.8 you can register clusters and manage their resources from one dashboard, but console and log streaming is scoped to the local control-plane cluster.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "QFjvS",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "gCqrN",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "Manage independent clusters from one dashboard",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "Q1D92r",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "One central API and login manages independent registered clusters.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "TF8SV",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "W0h9A",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "e6KU4m",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "W7pZuF",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Central API uses each cluster's Kubernetes API for resource changes, Pod logs, and PTY attach",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "uzizP",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "UCZZI",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "xev04",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Dashboard combines resources across clusters with optional Location filter (v0.3.0)",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "sU1Zi",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "t7sn2N",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "U4QiGv",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Requires matching API, operator, and agent versions in all clusters",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
{
"type": "frame",
"id": "fYKw1",
@@ -610,7 +799,7 @@
"id": "lJ4Me",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Air-gapped installation",
+ "content": "High availability and failure domains",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -754,6 +943,36 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "wXygh",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "rHyAr",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "ppwi8",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "Manage clusters",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "rectangle",
"id": "QVp3M",
diff --git a/website-export/json/D4SNhN.json b/website-export/json/D4SNhN.json
index 393941d..46c7460 100644
--- a/website-export/json/D4SNhN.json
+++ b/website-export/json/D4SNhN.json
@@ -9,7 +9,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -568,6 +568,72 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "P1ch8s",
+ "name": "Guide Section 4",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "GE524",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "CRD updates on upgrade",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "wHtaW",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Helm cannot auto-update CRDs, but the chart's autoApply hook applies them server-side. From v0.3.0, Helm 4 needs --force-conflicts on a first install over v0.2 CRDs.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "w2Xb8",
+ "name": "Guide Section 5",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "D5jOxT",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "Upgrade notes for v0.3.0",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "O2vzsD",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "GameServers roll once for network capture volume. Owner-only server operations (transfer, collaborate, wipe, delete). Playit tunnel egress widened. Tunnel Secrets must be owned by GameServer.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "frame",
"id": "GzehF",
@@ -610,7 +676,7 @@
"id": "pVDYw",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Troubleshooting",
+ "content": "Executable upgrades and rollback",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -754,6 +820,66 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "zrPcr",
+ "name": "TOC Row 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "UCM3z",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "f34OII",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "CRD updates",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "dHQHZ",
+ "name": "TOC Row 4",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "lA9d1",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "CZ9Tf",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "v0.3.0 notes",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "rectangle",
"id": "h94HEV",
diff --git a/website-export/json/HpLFh.json b/website-export/json/HpLFh.json
index c265e68..1c584a9 100644
--- a/website-export/json/HpLFh.json
+++ b/website-export/json/HpLFh.json
@@ -9,7 +9,7 @@
},
"clip": true,
"width": 1440,
- "height": 1200,
+ "height": 1280,
"fill": "$background/background",
"children": [
{
@@ -360,7 +360,7 @@
"fill": "$foreground/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Integrated frp, Tailscale, playit relay sidecars; playit mappings user-managed via playit.gg account. [optional; disabled by default]",
+ "content": "Integrated frp, Tailscale, playit relays (a separate tunnel pod per server); playit mappings user-managed via playit.gg account. [optional; disabled by default]",
"lineHeight": 1.4,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -597,7 +597,7 @@
"fill": "$foreground/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "OCI bundles via ModuleSource (git/http/oci/local/upload); optional cosign signature verification per source. 16 ready-to-use templates shipped.",
+ "content": "OCI bundles via ModuleSource (git/http/oci/local/upload); optional cosign signature verification per source. 30 ready-to-use templates on master (16 in v0.2.0-beta.8; the other 14 arrive in v0.3.0).",
"lineHeight": 1.4,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -676,7 +676,7 @@
"fill": "$foreground/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Cluster CRD for remote registration/monitoring; console/log streaming local-cluster only. [local cluster only for streaming]",
+ "content": "Cluster CRD for remote registration and health checks; in v0.2.0-beta.8 console/log streaming is local-cluster only. From v0.3.0: remote Pod logs/PTY, optional private gateway, and unified dashboard with location filters.",
"lineHeight": 1.4,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -945,7 +945,7 @@
"type": "text",
"id": "IlV6C",
"fill": "$foreground/foreground",
- "content": "Contributing",
+ "content": "Dashboard tour",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -979,7 +979,7 @@
"type": "text",
"id": "iWfmq",
"fill": "$foreground/foreground",
- "content": "Roadmap",
+ "content": "Sign-in & Account Recovery",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
diff --git a/website-export/json/KFLCi.json b/website-export/json/KFLCi.json
index b41e03d..8aa4116 100644
--- a/website-export/json/KFLCi.json
+++ b/website-export/json/KFLCi.json
@@ -169,7 +169,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Permission-based RBAC with admin/operator/viewer roles, scoped per namespace or cluster-wide.",
+ "content": "Permission-based RBAC with admin/operator/viewer roles, scoped per namespace or cluster-wide. Registering a cluster grants no user access by itself: remote access uses supplemental per-cluster grants, and a global union of permission names never authorizes a row in another cluster.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 14,
@@ -208,6 +208,37 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "nak8d",
+ "name": "API ↔ remote gateway",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "EltoE",
+ "fill": "$foreground/foreground",
+ "content": "API ↔ remote gateway",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "UYIWs",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "In v0.3.0, optional agent gateway for remote clusters. Central API reaches it with dedicated mTLS identity, separate trust material from local agent CA. Gateway exposes only allowlisted operations in configured namespaces.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"id": "jFnN9",
"type": "ref",
@@ -316,7 +347,7 @@
"type": "text",
"id": "Q7UBD",
"fill": "$foreground/foreground",
- "content": "Module Authoring",
+ "content": "Operate the server fleet",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
diff --git a/website-export/json/N4TBV.json b/website-export/json/N4TBV.json
index ae53fe5..a5f7f69 100644
--- a/website-export/json/N4TBV.json
+++ b/website-export/json/N4TBV.json
@@ -9,7 +9,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 960,
"fill": "$background/background",
"children": [
{
@@ -221,7 +221,69 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Go via gofmt/vet/golangci-lint; TypeScript strict with ESLint — comments only when the why isn't obvious.",
+ "content": "Indentation: tabs in Go, 2 spaces elsewhere. Go: gofmt, vet, golangci-lint. TypeScript: strict with ESLint + Prettier. Comments only when why isn't obvious.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "bZQPI",
+ "name": "Submitting a change",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "NhhOO",
+ "fill": "$foreground/foreground",
+ "content": "Submitting a change",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "vcqC2",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Fork the repository and create a feature branch. Run only quick compile checks locally. For UI work, include Pencil node IDs in PR description. Sign your commits. Verify PR labels: type: and area:.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "ceKhm",
+ "name": "Game modules and submodules",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "Od21k",
+ "fill": "$foreground/foreground",
+ "content": "Game modules and submodules",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "yIzMh",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Game-module changes belong in gameplane-module repository. Open the module PR there; once it merges, bump the submodule pointer. Repoint submodules locally without committing.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 14,
@@ -282,7 +344,7 @@
"type": "text",
"id": "dXLka",
"fill": "$foreground/foreground",
- "content": "Module Authoring",
+ "content": "Local development setup",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -293,10 +355,46 @@
},
{
"type": "frame",
- "id": "tjyxc",
- "width": 1,
- "height": 1,
- "layout": "none"
+ "id": "g1nn6",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "MZxVB",
+ "layout": "vertical",
+ "alignItems": "end",
+ "children": [
+ {
+ "type": "text",
+ "id": "HdvO5",
+ "fill": "$muted",
+ "content": "Next",
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "A2zon",
+ "fill": "$foreground/foreground",
+ "content": "Contribution & Design Workflow",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 14,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "knnkZ",
+ "width": 14,
+ "height": 14,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
}
]
}
diff --git a/website-export/json/Nnd3Y.json b/website-export/json/Nnd3Y.json
index 091aac4..b0025a5 100644
--- a/website-export/json/Nnd3Y.json
+++ b/website-export/json/Nnd3Y.json
@@ -610,7 +610,7 @@
"id": "Y5Qg0",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Ingress, TLS, and external access",
+ "content": "Air-gapped and private-registry installation",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/OIN7m.json b/website-export/json/OIN7m.json
index 8eff098..243e793 100644
--- a/website-export/json/OIN7m.json
+++ b/website-export/json/OIN7m.json
@@ -9,7 +9,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -477,7 +477,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": 820,
- "content": "Console and log traffic flow API ↔ Agent directly over mTLS; the Operator provisions and reconciles both.",
+ "content": "In the home cluster, console and log traffic flow API ↔ Agent directly over mTLS; the Operator provisions and reconciles both. Remote clusters go through an optional gateway (below).",
"textAlign": "center",
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -578,6 +578,37 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "QIGcw",
+ "name": "Multiple clusters and the gateway",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "H5vmfu",
+ "fill": "$foreground/foreground",
+ "content": "Multiple clusters and the gateway (v0.3.0)",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "m5Qk68",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "One central API and dashboard can manage independent clusters registered as Cluster resources. Each target cluster runs its own operator, and optionally a private gateway built from the API image. The gateway has no database, no user login and no browser or admin routes. Users authenticate only to the central API, which stays the authorization authority.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "frame",
"id": "xACvH",
diff --git a/website-export/json/P5qsD.json b/website-export/json/P5qsD.json
index 3b24cfd..870fa22 100644
--- a/website-export/json/P5qsD.json
+++ b/website-export/json/P5qsD.json
@@ -140,7 +140,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Added: fleet metrics, server-side audit export, audit webhook sink, syslog bridge. Changed: default module source is now git-based.",
+ "content": "Multicluster gateways, theme customization, PostgreSQL support, network capture, module authoring, 14 new game modules.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -821,7 +821,7 @@
"type": "text",
"id": "GDxqj",
"fill": "$foreground/foreground",
- "content": "FAQ",
+ "content": "Roadmap & Known Limitations",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -832,10 +832,46 @@
},
{
"type": "frame",
- "id": "XMvnR",
- "width": 1,
- "height": 1,
- "layout": "none"
+ "id": "sVgzD",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "r0pfb",
+ "layout": "vertical",
+ "alignItems": "end",
+ "children": [
+ {
+ "type": "text",
+ "id": "exnC9",
+ "fill": "$muted",
+ "content": "Next",
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "G6krZ3",
+ "fill": "$foreground/foreground",
+ "content": "Get Involved",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 14,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "EYNPT",
+ "width": 14,
+ "height": 14,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
}
]
}
diff --git a/website-export/json/QHFdH.json b/website-export/json/QHFdH.json
new file mode 100644
index 0000000..13732e6
--- /dev/null
+++ b/website-export/json/QHFdH.json
@@ -0,0 +1,1202 @@
+{
+ "type": "frame",
+ "id": "QHFdH",
+ "x": 0,
+ "y": 118088,
+ "name": "Screen/Docs — Remote Agent Gateway",
+ "theme": {
+ "semantic": "dark"
+ },
+ "clip": true,
+ "width": 1440,
+ "height": 1140,
+ "fill": "$background/background",
+ "children": [
+ {
+ "id": "R1n5h",
+ "type": "ref",
+ "ref": "K2Rt4H",
+ "height": "fill_container",
+ "name": "DocsSidebar/Expanded",
+ "width": 280
+ },
+ {
+ "type": "frame",
+ "id": "PlDdL",
+ "name": "Docs Main",
+ "width": "fill_container",
+ "height": "fill_container",
+ "fill": "$background/background",
+ "layout": "vertical",
+ "children": [
+ {
+ "id": "Ku9Il",
+ "type": "ref",
+ "ref": "SOdlr",
+ "name": "Docs Top Bar",
+ "width": "fill_container",
+ "descendants": {
+ "NImey": {
+ "content": "Platform"
+ },
+ "ikbi6": {
+ "content": "Agent gateway"
+ }
+ }
+ },
+ {
+ "type": "frame",
+ "id": "lOEwV",
+ "name": "Docs Article Body",
+ "clip": true,
+ "width": "fill_container",
+ "height": "fill_container",
+ "gap": 32,
+ "padding": 32,
+ "children": [
+ {
+ "type": "frame",
+ "id": "l6IQjM",
+ "name": "Docs Article",
+ "width": 780,
+ "layout": "vertical",
+ "gap": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "nWAT3",
+ "name": "Article Eyebrow",
+ "fill": "$accent/accent",
+ "content": "PLATFORM",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 0.8
+ },
+ {
+ "type": "text",
+ "id": "TmIfW",
+ "name": "Article Title",
+ "fill": "$foreground/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Remote agent gateway",
+ "lineHeight": 1.1,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 30,
+ "fontWeight": "700"
+ },
+ {
+ "type": "text",
+ "id": "q7kq5v",
+ "name": "Article Lead",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Run one central Gameplane API and dashboard, plus an operator and an optional gateway in each remote cluster. The gateway proxies approved agent operations over mTLS so the central dashboard can reach consoles, files, players and captures in clusters it does not run in.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 15,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "nvPIU",
+ "name": "Article Metadata",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "id": "dEhXo",
+ "type": "ref",
+ "ref": "NZPtq",
+ "name": "Meta Chip 0",
+ "descendants": {
+ "NLG8t": {
+ "content": "GATEWAY"
+ }
+ }
+ },
+ {
+ "id": "DhSsr",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 1",
+ "descendants": {
+ "yb6zz": {
+ "content": "v0.2"
+ }
+ }
+ },
+ {
+ "id": "BhSkh",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 2",
+ "descendants": {
+ "yb6zz": {
+ "content": "14 MIN"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "jkIg1",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Fnyay",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "milestone",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "jMSXT",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The gateway ships in v0.3.0 and is not available in v0.2.0-beta.8.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "eLuG5",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "CdVif",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "shield-alert",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "Y2TRsJ",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The central API still reaches each Kubernetes API directly for resources, Pod logs and PTY attach. The gateway is not a Kubernetes tunnel and has no database or login.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "KKCN3",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "w2uan",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "What the gateway carries",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "nZN8l",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The gateway extends registered clusters with console, file management, player operations, live status and captures. Registry browsing and modpacks use the selected cluster's Kubernetes client and game template.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "XSm6h",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "ZgHNP",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "WgsPX",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "GJtxm",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "RCON console, game logs, file manager, player operations and server status",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "nwaqW",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "JYqrP",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "SxtmS",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Agent-based mods and UID-bound capture files",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "k78RF",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "sR1Pz",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "T8UjFc",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Registry browsing and modpacks use the selected cluster's Kubernetes client",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "U9vhG",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "iDFlx",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "Deploy the gateway with Helm",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "GyyNp",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use matching API, operator and agent versions. Create Secrets for server TLS, central client CA, and agent credentials, then install with Helm setting api.enabled=false and gateway.enabled=true.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "h3s4p",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "LutgN",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "nnifv",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "i5HYm",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "clusterID must match the Cluster resource name exactly",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "E04BRW",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "HND61",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "reKxX",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Create Role and RoleBinding in each configured namespace with read-only get access",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "bCHex",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Jvo3Q",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "iEsvP",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Configure NetworkPolicy for central API CIDRs, Kubernetes API endpoints, DNS, and game agent traffic",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "WpDkw",
+ "name": "Guide Section 2",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "wvNDk",
+ "name": "Section Title 2",
+ "fill": "$foreground/foreground",
+ "content": "Register the gateway centrally",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "I93Tv",
+ "name": "Section Body 2",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Create a credential Secret in the central API namespace, then add the gateway reference to the existing Cluster resource with URL and tlsSecretRef.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "GAWwN",
+ "name": "Section List 2",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "WzK19",
+ "name": "Section 2 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "q3XxK6",
+ "name": "Section 2 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "gvp64",
+ "name": "Section 2 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Create Secret with ca.crt, tls.crt and tls.key for central client authentication",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "LH8B7",
+ "name": "Section 2 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "PrWrT",
+ "name": "Section 2 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "XTaAg",
+ "name": "Section 2 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Client certificate must chain to gateway.centralClientCASecret and carry the gateway URI SAN",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "HIxz3",
+ "name": "Section 2 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "sN0AY",
+ "name": "Section 2 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "mj0mq",
+ "name": "Section 2 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Registration name must match the gateway's clusterID",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "N9XImu",
+ "name": "Guide Section 3",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "f81U3u",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "Verify the installation",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "Covjz",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Confirm the gateway Pod is Ready, the Cluster resource shows Healthy, and test console, file manager and players on a test server.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "eWmil",
+ "name": "Quick Reference",
+ "width": "fill_container",
+ "fill": "$surface/tertiary",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 3,
+ "padding": 12,
+ "children": [
+ {
+ "type": "text",
+ "id": "nVmyZ",
+ "name": "Quick Reference Label",
+ "fill": "$accent/accent",
+ "content": "GATEWAY",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.6
+ },
+ {
+ "type": "text",
+ "id": "ymqKR",
+ "name": "Quick Reference Line 1",
+ "fill": "$foreground/foreground",
+ "content": "01 Secrets first: server TLS, central client CA, agent client + CA",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "JWm24",
+ "name": "Quick Reference Line 2",
+ "fill": "$foreground/foreground",
+ "content": "02 Helm: api.enabled=false, gateway.enabled=true, clusterID, peerURI, NetworkPolicy CIDRs",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "Rzlo6",
+ "name": "Quick Reference Line 3",
+ "fill": "$foreground/foreground",
+ "content": "03 Central: labeled credential Secret + Cluster spec.agentGateway",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "GD8Xx",
+ "name": "Quick Reference Line 4",
+ "fill": "$foreground/foreground",
+ "content": "04 Verify a test server end to end; Cluster health is Kubernetes-only",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "dU3I4",
+ "name": "Article Next Link",
+ "width": "fill_container",
+ "stroke": "$border/border",
+ "strokeWidth": {
+ "top": 1
+ },
+ "strokeAlignment": "inner",
+ "strokeLinejoin": "round",
+ "padding": [
+ 10,
+ 0,
+ 0,
+ 0
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "Z5ZHl0",
+ "name": "Next Link Text",
+ "layout": "vertical",
+ "gap": 2,
+ "children": [
+ {
+ "type": "text",
+ "id": "Vk5Du",
+ "name": "Next Link Eyebrow",
+ "fill": "$field/placeholder",
+ "content": "NEXT GUIDE",
+ "fontFamily": "Geist",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "s9jYyT",
+ "name": "Next Link Title",
+ "fill": "$foreground/foreground",
+ "content": "Users, authentication, and RBAC",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 13,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "Jq5Gc",
+ "name": "Next Link Icon",
+ "width": 15,
+ "height": 15,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "iu5q0",
+ "name": "Docs Page Aside",
+ "width": 250,
+ "layout": "vertical",
+ "gap": 16,
+ "children": [
+ {
+ "type": "frame",
+ "id": "H4BGa",
+ "name": "Docs Table of Contents",
+ "width": "fill_container",
+ "fill": "$surface/secondary",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 10,
+ "padding": 16,
+ "children": [
+ {
+ "type": "text",
+ "id": "te7uU",
+ "name": "TOC Label",
+ "fill": "$field/placeholder",
+ "content": "ON THIS PAGE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "tyZnG",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "AyYi6",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "URbLj",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "What the gateway carries",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "eqrHv",
+ "name": "TOC Row 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "fZU0m",
+ "name": "TOC Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "fSxnl",
+ "name": "TOC Text 0",
+ "fill": "$foreground/foreground",
+ "content": "Deploy the gateway with Helm",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "q1p03z",
+ "name": "TOC Row 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "J0DD1k",
+ "name": "TOC Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "zN8Gg",
+ "name": "TOC Text 1",
+ "fill": "$muted",
+ "content": "Register the gateway centrally",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "yuicL",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "b1ub5e",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "FpN27",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "Verify the installation",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "rectangle",
+ "id": "LCZH6",
+ "name": "TOC Divider",
+ "fill": "$border/border",
+ "width": "fill_container",
+ "height": 1
+ },
+ {
+ "type": "text",
+ "id": "c0cu9",
+ "name": "Coverage Label",
+ "fill": "$field/placeholder",
+ "content": "COVERS",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "Bzy8z",
+ "name": "Coverage Row 0",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "mOWpx",
+ "name": "Coverage Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "RLO4Z",
+ "name": "Coverage Text 0",
+ "fill": "$muted",
+ "content": "Gateway",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "tzu9a",
+ "name": "Coverage Row 1",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "SHP2w",
+ "name": "Coverage Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "M1h7f",
+ "name": "Coverage Text 1",
+ "fill": "$muted",
+ "content": "mTLS",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "D2Oyu",
+ "name": "Coverage Row 2",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "z9Dzh",
+ "name": "Coverage Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "pPDHq",
+ "name": "Coverage Text 2",
+ "fill": "$muted",
+ "content": "Cluster registration",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "Ivwze",
+ "name": "Coverage Row 2",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Zauoi",
+ "name": "Coverage Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "GAfxZ",
+ "name": "Coverage Text 2",
+ "fill": "$muted",
+ "content": "Troubleshooting",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "z7siBh",
+ "name": "Edit Page Card",
+ "width": "fill_container",
+ "fill": "$background/background",
+ "cornerRadius": "$radius/3xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 6,
+ "padding": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "nRoS1",
+ "name": "Edit Page Title",
+ "fill": "$foreground/foreground",
+ "content": "Improve this page",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 12,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "TNTaK",
+ "name": "Edit Page Body",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Found a gap? Open a docs issue or suggest an edit on GitHub.",
+ "lineHeight": 1.4,
+ "fontFamily": "Geist",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "Uw52v",
+ "name": "Edit Page Link",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "dg0IA",
+ "name": "Edit Page Link Text",
+ "fill": "$accent/soft-foreground",
+ "content": "Edit this page",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "500"
+ },
+ {
+ "type": "icon",
+ "id": "CkFKe",
+ "name": "Edit Page Link Icon",
+ "width": 12,
+ "height": 12,
+ "icon": "external-link",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/website-export/json/QdLzE.json b/website-export/json/QdLzE.json
index 989e728..a3ec916 100644
--- a/website-export/json/QdLzE.json
+++ b/website-export/json/QdLzE.json
@@ -207,7 +207,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Use per-server or global views to start snapshots, filter phases, and inspect destination, size, timestamps, and errors.",
+ "content": "Use per-server or global views to start snapshots, filter phases, and inspect destination, size, timestamps, and errors. From v0.3.0, the Backups page groups its filters in a popover on the right of the search row: Location (when several clusters are registered), Server and the current tab's Phase. Selections apply when you click Apply.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -610,7 +610,7 @@
"id": "g06VqS",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Cluster and storage operations",
+ "content": "Backup Destinations",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/SKMOT.json b/website-export/json/SKMOT.json
index a9530e9..479511b 100644
--- a/website-export/json/SKMOT.json
+++ b/website-export/json/SKMOT.json
@@ -610,7 +610,7 @@
"id": "GgseU",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Modules and sources",
+ "content": "System log viewer",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/TESyw.json b/website-export/json/TESyw.json
index 8026586..8740919 100644
--- a/website-export/json/TESyw.json
+++ b/website-export/json/TESyw.json
@@ -403,7 +403,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Use valid names and verify the breadcrumb before confirming.",
+ "content": "Use valid names and verify the breadcrumb before confirming",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -467,7 +467,39 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Verify upload size, type, provenance, and ownership after completion.",
+ "content": "Verify upload size, type, provenance, and ownership after completion",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "O5wilB",
+ "name": "Section 2 Item 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "cSTIi",
+ "name": "Section 2 Item Icon 3",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "c3eCc",
+ "name": "Section 2 Item Text 3",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Delete folders from the tree (v0.3.0)",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
diff --git a/website-export/json/VgF1s.json b/website-export/json/VgF1s.json
index 484ccd4..d91ba46 100644
--- a/website-export/json/VgF1s.json
+++ b/website-export/json/VgF1s.json
@@ -9,7 +9,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -1234,7 +1234,7 @@
"id": "KsrS8",
"name": "Reference 2 Row 2",
"width": "fill_container",
- "height": 38,
+ "height": 60,
"fill": "$background/background",
"stroke": "$separator/separator",
"strokeWidth": {
@@ -1341,7 +1341,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Cancel old sockets before reconnecting in new scope.",
+ "content": "A socket stays bound to the cluster and namespace of the server that opened it, and reconnects retain that target. Leaving the server view closes its streams, so a new view opens fresh sockets in its own scope and never inherits stale state.",
"fontFamily": "Geist",
"fontSize": 11,
"fontWeight": "normal"
diff --git a/website-export/json/X0DeYH.json b/website-export/json/X0DeYH.json
index 72d349f..6b03d24 100644
--- a/website-export/json/X0DeYH.json
+++ b/website-export/json/X0DeYH.json
@@ -175,7 +175,7 @@
"fill": "$accent/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Start with one cluster selected. Every list, metric, and action inherits that scope.",
+ "content": "In v0.2.0-beta.8, a cluster selector scopes everything. From v0.3.0, the dashboard combines every authorized cluster with an optional Location filter.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -207,7 +207,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "The dashboard is a cluster-scoped control surface. Use the top bar to change context without losing your place.",
+ "content": "Opening a server pins it to its own cluster and namespace, so changing a list filter never retargets the server you are working on.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -255,7 +255,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Cluster selector scopes fleet metrics, lists, and creation flows.",
+ "content": "Narrows the dashboard, Servers and Backups to one cluster; combined view is default (v0.3.0).",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -287,7 +287,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Global search jumps to servers, backups, users, and modules.",
+ "content": "Finds any server by name across the clusters you can access.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
diff --git a/website-export/json/ZwM1N.json b/website-export/json/ZwM1N.json
index 1533659..810fee4 100644
--- a/website-export/json/ZwM1N.json
+++ b/website-export/json/ZwM1N.json
@@ -467,7 +467,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "SSE/WebSocket clients cancel and reconnect visibly on scope changes.",
+ "content": "Sockets bind to cluster and namespace; leaving the server view closes streams (v0.3.0).",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
diff --git a/website-export/json/aZAEs.json b/website-export/json/aZAEs.json
index 41c4368..a0a8f30 100644
--- a/website-export/json/aZAEs.json
+++ b/website-export/json/aZAEs.json
@@ -403,7 +403,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Declare parameters, validation, labels, icons, and capability gates.",
+ "content": "Declare parameters, validation, labels, icons, and gates; currentFrom pre-fills from live status (v0.3.0).",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -610,7 +610,7 @@
"id": "LSjd1",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Test and publish modules",
+ "content": "Test, Publish & Sign Modules",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/b5srx.json b/website-export/json/b5srx.json
index b7c60be..10ee999 100644
--- a/website-export/json/b5srx.json
+++ b/website-export/json/b5srx.json
@@ -157,7 +157,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "WebSocket console/log/file streams are local-cluster-scoped — remote clusters are registered and health-checked but not yet streamed.",
+ "content": "WebSocket console/log/file streams are local-cluster-scoped in v0.2.0-beta.8 — remote clusters are registered and health-checked but not yet streamed. v0.3.0 adds remote access through an optional agent gateway.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -189,7 +189,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "The PostgreSQL driver is experimental (build-tag-gated, work-in-progress); SQLite is the production-tested database.",
+ "content": "The PostgreSQL driver is experimental (build-tag-gated); SQLite is the production-tested database. v0.3.0 makes the driver work end to end, but it stays experimental: keep a single API replica, and it is not yet covered by e2e or upgrade tests.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -265,7 +265,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Server-side audit-log export and webhook push sink.",
+ "content": "Multicluster agent gateway — optional private mTLS gateway per cluster, with combined dashboard, Servers, Backups and Search views.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -294,7 +294,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Audit-to-syslog bridge.",
+ "content": "PostgreSQL driver works end to end — still experimental, keep a single API replica.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -388,7 +388,7 @@
"type": "text",
"id": "LuSUV",
"fill": "$foreground/foreground",
- "content": "Comparison",
+ "content": "Frequently Asked Questions",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -422,7 +422,7 @@
"type": "text",
"id": "U1eKI",
"fill": "$foreground/foreground",
- "content": "FAQ",
+ "content": "Changelog",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
diff --git a/website-export/json/b5u3Ui.json b/website-export/json/b5u3Ui.json
index 842b196..2b69ec9 100644
--- a/website-export/json/b5u3Ui.json
+++ b/website-export/json/b5u3Ui.json
@@ -610,7 +610,7 @@
"id": "XEhHY",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Upgrade and migrate",
+ "content": "Dashboard tour",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/cpCjN.json b/website-export/json/cpCjN.json
index 6cde3fc..61e5e66 100644
--- a/website-export/json/cpCjN.json
+++ b/website-export/json/cpCjN.json
@@ -255,7 +255,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Use a name that distinguishes environments and clusters.",
+ "content": "Use a name that distinguishes environments and clusters (optional)",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -610,7 +610,7 @@
"id": "biGQY",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Using the mobile dashboard",
+ "content": "Modules & Sources",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/gEOuV.json b/website-export/json/gEOuV.json
index b2943a2..1759a1a 100644
--- a/website-export/json/gEOuV.json
+++ b/website-export/json/gEOuV.json
@@ -559,7 +559,7 @@
"id": "c2mN5N",
"name": "Quick Reference Line 3",
"fill": "$foreground/foreground",
- "content": "03 Recovery → self-service or audited admin reset; OIDC resets upstream",
+ "content": "03 Recovery → audited admin reset or bootstrap-admin break-glass; OIDC resets upstream",
"fontFamily": "JetBrains Mono",
"fontSize": 11.5,
"fontWeight": "normal"
@@ -610,7 +610,7 @@
"id": "yTLSd",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Template-specific configuration",
+ "content": "Using the Mobile Dashboard",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/mYDXb.json b/website-export/json/mYDXb.json
index d09d1c5..dfe1622 100644
--- a/website-export/json/mYDXb.json
+++ b/website-export/json/mYDXb.json
@@ -175,7 +175,7 @@
"fill": "$accent/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Service-account creation is availability-dependent; do not document a token workflow until the shipped UI and API support it.",
+ "content": "Gameplane has no service accounts or API tokens yet; machine-to-machine tokens are planned for v1.1. Until then, give automation its own local user with the narrowest role that works, and never share a person's login.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -610,7 +610,7 @@
"id": "kBBIl",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "System log viewer",
+ "content": "Permission Catalog",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/n8pnD.json b/website-export/json/n8pnD.json
index bb0d872..cd9edf0 100644
--- a/website-export/json/n8pnD.json
+++ b/website-export/json/n8pnD.json
@@ -610,7 +610,7 @@
"id": "roSmX",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "User administration",
+ "content": "Multi-cluster Topology",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/o7PQv.json b/website-export/json/o7PQv.json
index c60b483..27d0fc8 100644
--- a/website-export/json/o7PQv.json
+++ b/website-export/json/o7PQv.json
@@ -610,7 +610,7 @@
"id": "GcFnz",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Ownership and danger zone",
+ "content": "Ownership, collaborators, and danger zone",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/p5Gci.json b/website-export/json/p5Gci.json
index e24b2b7..15cbaf7 100644
--- a/website-export/json/p5Gci.json
+++ b/website-export/json/p5Gci.json
@@ -1,943 +1 @@
-{
- "type": "frame",
- "id": "p5Gci",
- "x": 0,
- "y": 59128,
- "name": "Screen/Docs — Database Configuration & Lifecycle",
- "theme": {
- "semantic": "dark"
- },
- "clip": true,
- "width": 1440,
- "height": 900,
- "fill": "$background/background",
- "children": [
- {
- "id": "SblbP",
- "type": "ref",
- "ref": "K2Rt4H",
- "height": "fill_container",
- "name": "DocsSidebar/Expanded",
- "width": 280
- },
- {
- "type": "frame",
- "id": "iLeaa",
- "name": "Docs Main",
- "width": "fill_container",
- "height": "fill_container",
- "fill": "$background/background",
- "layout": "vertical",
- "children": [
- {
- "id": "mpJgT",
- "type": "ref",
- "ref": "SOdlr",
- "name": "Docs Top Bar",
- "width": "fill_container",
- "descendants": {
- "NImey": {
- "content": "Platform"
- },
- "ikbi6": {
- "content": "Database"
- }
- }
- },
- {
- "type": "frame",
- "id": "a6pOmF",
- "name": "Docs Article Body",
- "clip": true,
- "width": "fill_container",
- "height": "fill_container",
- "gap": 32,
- "padding": 32,
- "children": [
- {
- "type": "frame",
- "id": "mgxfD",
- "name": "Docs Article",
- "width": 780,
- "layout": "vertical",
- "gap": 14,
- "children": [
- {
- "type": "text",
- "id": "r55DX",
- "name": "Article Eyebrow",
- "fill": "$accent/accent",
- "content": "DATA",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11,
- "fontWeight": "600",
- "letterSpacing": 0.8
- },
- {
- "type": "text",
- "id": "z69lHR",
- "name": "Article Title",
- "fill": "$foreground/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Database configuration and lifecycle",
- "lineHeight": 1.1,
- "fontFamily": "JetBrains Mono",
- "fontSize": 30,
- "fontWeight": "700"
- },
- {
- "type": "text",
- "id": "tZ8sO",
- "name": "Article Lead",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Select a supported topology, secure its connection, control migrations, and recover platform state independently of game worlds.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 15,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "cZCFi",
- "name": "Article Metadata",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "id": "kaitV",
- "type": "ref",
- "ref": "NZPtq",
- "name": "Meta Chip 0",
- "descendants": {
- "NLG8t": {
- "content": "PLATFORM GUIDE"
- }
- }
- },
- {
- "id": "WG6lI",
- "type": "ref",
- "ref": "EbSsA",
- "name": "Meta Chip 1",
- "descendants": {
- "yb6zz": {
- "content": "v0.2"
- }
- }
- },
- {
- "id": "Y7dcih",
- "type": "ref",
- "ref": "EbSsA",
- "name": "Meta Chip 2",
- "descendants": {
- "yb6zz": {
- "content": "18 MIN"
- }
- }
- }
- ]
- },
- {
- "type": "frame",
- "id": "N3EVIF",
- "name": "Article Callout",
- "width": "fill_container",
- "fill": "$accent/soft",
- "cornerRadius": "$radius/xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "gap": 10,
- "padding": [
- 10,
- 12
- ],
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "HA9YI",
- "name": "Article Callout Icon",
- "width": 16,
- "height": 16,
- "icon": "database",
- "library": "lucide",
- "fill": "$accent/soft-foreground"
- },
- {
- "type": "text",
- "id": "ybd2Y",
- "name": "Article Callout Text",
- "fill": "$accent/soft-foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Game-world backups do not contain users, roles, sessions, settings, or audit state stored in the platform database.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "DKfDF",
- "name": "Guide Section 1",
- "width": "fill_container",
- "layout": "vertical",
- "gap": 6,
- "children": [
- {
- "type": "text",
- "id": "HE1q6",
- "name": "Section Title 1",
- "fill": "$foreground/foreground",
- "content": "Select and provision",
- "fontFamily": "JetBrains Mono",
- "fontSize": 17,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "TvmbH",
- "name": "Section Body 1",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Document engine, version, availability model, storage, ownership, connection limits, and TLS.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 13,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "ynE3D",
- "name": "Section List 1",
- "width": "fill_container",
- "fill": "$surface/surface",
- "cornerRadius": "$radius/2xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 5,
- "padding": [
- 8,
- 10
- ],
- "children": [
- {
- "type": "frame",
- "id": "pvWnO",
- "name": "Section 1 Item 0",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "GdFLV",
- "name": "Section 1 Item Icon 0",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "Q8fWmI",
- "name": "Section 1 Item Text 0",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Use SQLite only within its documented single-instance constraints.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "SBFHT",
- "name": "Section 1 Item 1",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "i5GXC",
- "name": "Section 1 Item Icon 1",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "ZCbCA",
- "name": "Section 1 Item Text 1",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "For PostgreSQL, define DNS, TLS/CA, user ownership, pooling, and limits.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "AvNey",
- "name": "Section 1 Item 2",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "NE9tI",
- "name": "Section 1 Item Icon 2",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "mh1iU",
- "name": "Section 1 Item Text 2",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Keep credentials in a Kubernetes Secret and database traffic private.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "NzSDt",
- "name": "Guide Section 2",
- "width": "fill_container",
- "layout": "vertical",
- "gap": 6,
- "children": [
- {
- "type": "text",
- "id": "p7PONH",
- "name": "Section Title 2",
- "fill": "$foreground/foreground",
- "content": "Migrate and operate",
- "fontFamily": "JetBrains Mono",
- "fontSize": 17,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "b2gdm",
- "name": "Section Body 2",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Tie schema migrations to a pinned release and observe connectivity, locks, latency, growth, and failures.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 13,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "sLqyb",
- "name": "Section List 2",
- "width": "fill_container",
- "fill": "$surface/surface",
- "cornerRadius": "$radius/2xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 5,
- "padding": [
- 8,
- 10
- ],
- "children": [
- {
- "type": "frame",
- "id": "x9gdW",
- "name": "Section 2 Item 0",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "RZoOF",
- "name": "Section 2 Item Icon 0",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "vseqb",
- "name": "Section 2 Item Text 0",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "State whether migrations run automatically or as an explicit job.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "S2lL6o",
- "name": "Section 2 Item 1",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "CIvJb",
- "name": "Section 2 Item Icon 1",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "RP3M6",
- "name": "Section 2 Item Text 1",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Monitor saturation, locks, latency, storage, and failed migrations.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "Yi1AW",
- "name": "Section 2 Item 2",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "lQhsC",
- "name": "Section 2 Item Icon 2",
- "width": 13,
- "height": 13,
- "icon": "check",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "yMz9n",
- "name": "Section 2 Item Text 2",
- "fill": "$default/foreground",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Rotate credentials with overlap and test reconnection before revocation.",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "lZ5lO",
- "name": "Guide Section 3",
- "width": "fill_container",
- "layout": "vertical",
- "gap": 6,
- "children": [
- {
- "type": "text",
- "id": "IZqRA",
- "name": "Section Title 3",
- "fill": "$foreground/foreground",
- "content": "Back up and recover",
- "fontFamily": "JetBrains Mono",
- "fontSize": 17,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "zja4I",
- "name": "Section Body 3",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Restore database, values, identity configuration, CRDs, and Secrets as one version-compatible recovery set.",
- "lineHeight": 1.45,
- "fontFamily": "Geist",
- "fontSize": 13,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "dtFsR",
- "name": "Quick Reference",
- "width": "fill_container",
- "fill": "$surface/tertiary",
- "cornerRadius": "$radius/xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 3,
- "padding": 12,
- "children": [
- {
- "type": "text",
- "id": "k3SY5d",
- "name": "Quick Reference Label",
- "fill": "$accent/accent",
- "content": "DATABASE RUNBOOK",
- "fontFamily": "JetBrains Mono",
- "fontSize": 9,
- "fontWeight": "600",
- "letterSpacing": 0.6
- },
- {
- "type": "text",
- "id": "N6Rm7",
- "name": "Quick Reference Line 1",
- "fill": "$foreground/foreground",
- "content": "01 Record engine/version, endpoint, TLS, Secret, schema, backup",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11.5,
- "fontWeight": "normal"
- },
- {
- "type": "text",
- "id": "TpIkh",
- "name": "Quick Reference Line 2",
- "fill": "$foreground/foreground",
- "content": "02 Upgrade gate → backup + migration notes + connection headroom",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11.5,
- "fontWeight": "normal"
- },
- {
- "type": "text",
- "id": "C8taog",
- "name": "Quick Reference Line 3",
- "fill": "$foreground/foreground",
- "content": "03 Restore DB → matching release → validate schema, login, audit, settings",
- "fontFamily": "JetBrains Mono",
- "fontSize": 11.5,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "mniyp",
- "name": "Article Next Link",
- "width": "fill_container",
- "stroke": "$border/border",
- "strokeWidth": {
- "top": 1
- },
- "strokeAlignment": "inner",
- "strokeLinejoin": "round",
- "padding": [
- 10,
- 0,
- 0,
- 0
- ],
- "justifyContent": "space_between",
- "alignItems": "center",
- "children": [
- {
- "type": "frame",
- "id": "r4q7Uw",
- "name": "Next Link Text",
- "layout": "vertical",
- "gap": 2,
- "children": [
- {
- "type": "text",
- "id": "enAQR",
- "name": "Next Link Eyebrow",
- "fill": "$field/placeholder",
- "content": "NEXT GUIDE",
- "fontFamily": "Geist",
- "fontSize": 10,
- "fontWeight": "normal"
- },
- {
- "type": "text",
- "id": "li2nn",
- "name": "Next Link Title",
- "fill": "$foreground/foreground",
- "content": "Multi-cluster topology",
- "fontFamily": "JetBrains Mono",
- "fontSize": 13,
- "fontWeight": "500"
- }
- ]
- },
- {
- "type": "icon",
- "id": "lwCjR",
- "name": "Next Link Icon",
- "width": 15,
- "height": 15,
- "icon": "arrow-right",
- "library": "lucide",
- "fill": "$muted"
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "TSXo3",
- "name": "Docs Page Aside",
- "width": 250,
- "layout": "vertical",
- "gap": 16,
- "children": [
- {
- "type": "frame",
- "id": "IyLl6",
- "name": "Docs Table of Contents",
- "width": "fill_container",
- "fill": "$surface/secondary",
- "cornerRadius": "$radius/2xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 10,
- "padding": 16,
- "children": [
- {
- "type": "text",
- "id": "dFnGx",
- "name": "TOC Label",
- "fill": "$field/placeholder",
- "content": "ON THIS PAGE",
- "fontFamily": "JetBrains Mono",
- "fontSize": 9,
- "fontWeight": "600",
- "letterSpacing": 0.7
- },
- {
- "type": "frame",
- "id": "w7yEQP",
- "name": "TOC Row 0",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "WFoNi",
- "name": "TOC Icon 0",
- "width": 12,
- "height": 12,
- "icon": "hash",
- "library": "lucide",
- "fill": "$accent/accent"
- },
- {
- "type": "text",
- "id": "Tnc5J",
- "name": "TOC Text 0",
- "fill": "$foreground/foreground",
- "content": "Provision",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "500"
- }
- ]
- },
- {
- "type": "frame",
- "id": "RduIF",
- "name": "TOC Row 1",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "bzx4w",
- "name": "TOC Icon 1",
- "width": 12,
- "height": 12,
- "icon": "hash",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "Ve1Ke",
- "name": "TOC Text 1",
- "fill": "$muted",
- "content": "Migrate and operate",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "G7JCWk",
- "name": "TOC Row 2",
- "width": "fill_container",
- "gap": 8,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "P8iKp",
- "name": "TOC Icon 2",
- "width": 12,
- "height": 12,
- "icon": "hash",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "uYiM7",
- "name": "TOC Text 2",
- "fill": "$muted",
- "content": "Recover",
- "fontFamily": "Geist",
- "fontSize": 12.5,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "rectangle",
- "id": "OIFwR",
- "name": "TOC Divider",
- "fill": "$border/border",
- "width": "fill_container",
- "height": 1
- },
- {
- "type": "text",
- "id": "Pu8zV",
- "name": "Coverage Label",
- "fill": "$field/placeholder",
- "content": "COVERS",
- "fontFamily": "JetBrains Mono",
- "fontSize": 9,
- "fontWeight": "600",
- "letterSpacing": 0.7
- },
- {
- "type": "frame",
- "id": "ZhJKd",
- "name": "Coverage Row 0",
- "width": "fill_container",
- "gap": 7,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "HYVNh",
- "name": "Coverage Icon 0",
- "width": 12,
- "height": 12,
- "icon": "book-open",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "hu6Qi",
- "name": "Coverage Text 0",
- "fill": "$muted",
- "content": "SQLite",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "Z6QBc",
- "name": "Coverage Row 1",
- "width": "fill_container",
- "gap": 7,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "kmUF2",
- "name": "Coverage Icon 1",
- "width": 12,
- "height": 12,
- "icon": "book-open",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "lDsTG",
- "name": "Coverage Text 1",
- "fill": "$muted",
- "content": "PostgreSQL",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "normal"
- }
- ]
- },
- {
- "type": "frame",
- "id": "hhIqz",
- "name": "Coverage Row 2",
- "width": "fill_container",
- "gap": 7,
- "alignItems": "center",
- "children": [
- {
- "type": "icon",
- "id": "ZVMmt",
- "name": "Coverage Icon 2",
- "width": 12,
- "height": 12,
- "icon": "book-open",
- "library": "lucide",
- "fill": "$field/placeholder"
- },
- {
- "type": "text",
- "id": "pf0vi",
- "name": "Coverage Text 2",
- "fill": "$muted",
- "content": "Migrations",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "normal"
- }
- ]
- }
- ]
- },
- {
- "type": "frame",
- "id": "Ptk6I",
- "name": "Edit Page Card",
- "width": "fill_container",
- "fill": "$background/background",
- "cornerRadius": "$radius/3xl",
- "stroke": "$border/border",
- "strokeWidth": 1,
- "strokeAlignment": "inner",
- "layout": "vertical",
- "gap": 6,
- "padding": 14,
- "children": [
- {
- "type": "text",
- "id": "n4mDoU",
- "name": "Edit Page Title",
- "fill": "$foreground/foreground",
- "content": "Improve this page",
- "fontFamily": "JetBrains Mono",
- "fontSize": 12,
- "fontWeight": "600"
- },
- {
- "type": "text",
- "id": "r4lkRF",
- "name": "Edit Page Body",
- "fill": "$muted",
- "textGrowth": "fixed-width",
- "width": "fill_container",
- "content": "Found a gap? Open a docs issue or suggest an edit on GitHub.",
- "lineHeight": 1.4,
- "fontFamily": "Geist",
- "fontSize": 11.5,
- "fontWeight": "normal"
- },
- {
- "type": "frame",
- "id": "IdWes",
- "name": "Edit Page Link",
- "gap": 6,
- "alignItems": "center",
- "children": [
- {
- "type": "text",
- "id": "H2omr",
- "name": "Edit Page Link Text",
- "fill": "$accent/soft-foreground",
- "content": "Edit this page",
- "fontFamily": "Geist",
- "fontSize": 12,
- "fontWeight": "500"
- },
- {
- "type": "icon",
- "id": "s6iraX",
- "name": "Edit Page Link Icon",
- "width": 12,
- "height": 12,
- "icon": "external-link",
- "library": "lucide",
- "fill": "$accent/accent"
- }
- ]
- }
- ]
- }
- ]
- }
- ]
- }
- ]
- }
- ]
-}
+{"type":"frame","id":"p5Gci","x":0,"y":59128,"name":"Screen/Docs — Database Configuration & Lifecycle","theme":{"semantic":"dark"},"clip":true,"width":1440,"height":900,"fill":"$background/background","children":[{"id":"SblbP","type":"ref","ref":"K2Rt4H","height":"fill_container","name":"DocsSidebar/Expanded","width":280},{"type":"frame","id":"iLeaa","name":"Docs Main","width":"fill_container","height":"fill_container","fill":"$background/background","layout":"vertical","children":[{"id":"mpJgT","type":"ref","ref":"SOdlr","name":"Docs Top Bar","width":"fill_container","descendants":{"NImey":{"content":"Platform"},"ikbi6":{"content":"Database"}}},{"type":"frame","id":"a6pOmF","name":"Docs Article Body","clip":true,"width":"fill_container","height":"fill_container","gap":32,"padding":32,"children":[{"type":"frame","id":"mgxfD","name":"Docs Article","width":780,"layout":"vertical","gap":14,"children":[{"type":"text","id":"r55DX","name":"Article Eyebrow","fill":"$accent/accent","content":"DATA","fontFamily":"JetBrains Mono","fontSize":11,"fontWeight":"600","letterSpacing":0.8},{"type":"text","id":"z69lHR","name":"Article Title","fill":"$foreground/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Database configuration and lifecycle","lineHeight":1.1,"fontFamily":"JetBrains Mono","fontSize":30,"fontWeight":"700"},{"type":"text","id":"tZ8sO","name":"Article Lead","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Select a supported topology, secure its connection, control migrations, and recover platform state independently of game worlds.","lineHeight":1.45,"fontFamily":"Geist","fontSize":15,"fontWeight":"normal"},{"type":"frame","id":"cZCFi","name":"Article Metadata","gap":8,"alignItems":"center","children":[{"id":"kaitV","type":"ref","ref":"NZPtq","name":"Meta Chip 0","descendants":{"NLG8t":{"content":"PLATFORM GUIDE"}}},{"id":"WG6lI","type":"ref","ref":"EbSsA","name":"Meta Chip 1","descendants":{"yb6zz":{"content":"v0.2"}}},{"id":"Y7dcih","type":"ref","ref":"EbSsA","name":"Meta Chip 2","descendants":{"yb6zz":{"content":"18 MIN"}}}]},{"type":"frame","id":"N3EVIF","name":"Article Callout","width":"fill_container","fill":"$accent/soft","cornerRadius":"$radius/xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","gap":10,"padding":[10,12],"alignItems":"center","children":[{"type":"icon","id":"HA9YI","name":"Article Callout Icon","width":16,"height":16,"icon":"database","library":"lucide","fill":"$accent/soft-foreground"},{"type":"text","id":"ybd2Y","name":"Article Callout Text","fill":"$accent/soft-foreground","textGrowth":"fixed-width","width":"fill_container","content":"Game-world backups do not contain users, roles, sessions, settings, or audit state stored in the platform database.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"DKfDF","name":"Guide Section 1","width":"fill_container","layout":"vertical","gap":6,"children":[{"type":"text","id":"HE1q6","name":"Section Title 1","fill":"$foreground/foreground","content":"Select and provision","fontFamily":"JetBrains Mono","fontSize":17,"fontWeight":"600"},{"type":"text","id":"TvmbH","name":"Section Body 1","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Document engine, version, availability model, storage, ownership, connection limits, and TLS.","lineHeight":1.45,"fontFamily":"Geist","fontSize":13,"fontWeight":"normal"},{"type":"frame","id":"ynE3D","name":"Section List 1","width":"fill_container","fill":"$surface/surface","cornerRadius":"$radius/2xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":5,"padding":[8,10],"children":[{"type":"frame","id":"pvWnO","name":"Section 1 Item 0","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"GdFLV","name":"Section 1 Item Icon 0","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"Q8fWmI","name":"Section 1 Item Text 0","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Use SQLite only within its documented single-instance constraints.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"SBFHT","name":"Section 1 Item 1","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"i5GXC","name":"Section 1 Item Icon 1","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"ZCbCA","name":"Section 1 Item Text 1","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"For PostgreSQL, define DNS, TLS/CA, user ownership, pooling, and limits.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"AvNey","name":"Section 1 Item 2","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"NE9tI","name":"Section 1 Item Icon 2","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"mh1iU","name":"Section 1 Item Text 2","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Keep credentials in a Kubernetes Secret and database traffic private.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]}]}]},{"type":"frame","id":"NzSDt","name":"Guide Section 2","width":"fill_container","layout":"vertical","gap":6,"children":[{"type":"text","id":"p7PONH","name":"Section Title 2","fill":"$foreground/foreground","content":"Migrate and operate","fontFamily":"JetBrains Mono","fontSize":17,"fontWeight":"600"},{"type":"text","id":"b2gdm","name":"Section Body 2","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Tie schema migrations to a pinned release and observe connectivity, locks, latency, growth, and failures.","lineHeight":1.45,"fontFamily":"Geist","fontSize":13,"fontWeight":"normal"},{"type":"frame","id":"sLqyb","name":"Section List 2","width":"fill_container","fill":"$surface/surface","cornerRadius":"$radius/2xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":5,"padding":[8,10],"children":[{"type":"frame","id":"x9gdW","name":"Section 2 Item 0","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"RZoOF","name":"Section 2 Item Icon 0","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"vseqb","name":"Section 2 Item Text 0","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"State whether migrations run automatically or as an explicit job.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"S2lL6o","name":"Section 2 Item 1","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"CIvJb","name":"Section 2 Item Icon 1","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"RP3M6","name":"Section 2 Item Text 1","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Monitor saturation, locks, latency, storage, and failed migrations.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"Yi1AW","name":"Section 2 Item 2","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"lQhsC","name":"Section 2 Item Icon 2","width":13,"height":13,"icon":"check","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"yMz9n","name":"Section 2 Item Text 2","fill":"$default/foreground","textGrowth":"fixed-width","width":"fill_container","content":"Rotate credentials with overlap and test reconnection before revocation.","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]}]}]},{"type":"frame","id":"lZ5lO","name":"Guide Section 3","width":"fill_container","layout":"vertical","gap":6,"children":[{"type":"text","id":"IZqRA","name":"Section Title 3","fill":"$foreground/foreground","content":"Back up and recover","fontFamily":"JetBrains Mono","fontSize":17,"fontWeight":"600"},{"type":"text","id":"zja4I","name":"Section Body 3","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Restore database, values, identity configuration, CRDs, and Secrets as one version-compatible recovery set.","lineHeight":1.45,"fontFamily":"Geist","fontSize":13,"fontWeight":"normal"},{"type":"frame","id":"dtFsR","name":"Quick Reference","width":"fill_container","fill":"$surface/tertiary","cornerRadius":"$radius/xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":3,"padding":12,"children":[{"type":"text","id":"k3SY5d","name":"Quick Reference Label","fill":"$accent/accent","content":"DATABASE RUNBOOK","fontFamily":"JetBrains Mono","fontSize":9,"fontWeight":"600","letterSpacing":0.6},{"type":"text","id":"N6Rm7","name":"Quick Reference Line 1","fill":"$foreground/foreground","content":"01 Record engine/version, endpoint, TLS, Secret, schema, backup","fontFamily":"JetBrains Mono","fontSize":11.5,"fontWeight":"normal"},{"type":"text","id":"TpIkh","name":"Quick Reference Line 2","fill":"$foreground/foreground","content":"02 Upgrade gate → backup + migration notes + connection headroom","fontFamily":"JetBrains Mono","fontSize":11.5,"fontWeight":"normal"},{"type":"text","id":"C8taog","name":"Quick Reference Line 3","fill":"$foreground/foreground","content":"03 Restore DB → matching release → validate schema, login, audit, settings","fontFamily":"JetBrains Mono","fontSize":11.5,"fontWeight":"normal"}]}]},{"type":"frame","id":"mniyp","name":"Article Next Link","width":"fill_container","stroke":"$border/border","strokeWidth":{"top":1},"strokeAlignment":"inner","strokeLinejoin":"round","padding":[10,0,0,0],"justifyContent":"space_between","alignItems":"center","children":[{"type":"frame","id":"r4q7Uw","name":"Next Link Text","layout":"vertical","gap":2,"children":[{"type":"text","id":"enAQR","name":"Next Link Eyebrow","fill":"$field/placeholder","content":"NEXT GUIDE","fontFamily":"Geist","fontSize":10,"fontWeight":"normal"},{"type":"text","id":"li2nn","name":"Next Link Title","fill":"$foreground/foreground","content":"Platform Disaster Recovery","fontFamily":"JetBrains Mono","fontSize":13,"fontWeight":"500"}]},{"type":"icon","id":"lwCjR","name":"Next Link Icon","width":15,"height":15,"icon":"arrow-right","library":"lucide","fill":"$muted"}]}]},{"type":"frame","id":"TSXo3","name":"Docs Page Aside","width":250,"layout":"vertical","gap":16,"children":[{"type":"frame","id":"IyLl6","name":"Docs Table of Contents","width":"fill_container","fill":"$surface/secondary","cornerRadius":"$radius/2xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":10,"padding":16,"children":[{"type":"text","id":"dFnGx","name":"TOC Label","fill":"$field/placeholder","content":"ON THIS PAGE","fontFamily":"JetBrains Mono","fontSize":9,"fontWeight":"600","letterSpacing":0.7},{"type":"frame","id":"w7yEQP","name":"TOC Row 0","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"WFoNi","name":"TOC Icon 0","width":12,"height":12,"icon":"hash","library":"lucide","fill":"$accent/accent"},{"type":"text","id":"Tnc5J","name":"TOC Text 0","fill":"$foreground/foreground","content":"Provision","fontFamily":"Geist","fontSize":12.5,"fontWeight":"500"}]},{"type":"frame","id":"RduIF","name":"TOC Row 1","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"bzx4w","name":"TOC Icon 1","width":12,"height":12,"icon":"hash","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"Ve1Ke","name":"TOC Text 1","fill":"$muted","content":"Migrate and operate","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"frame","id":"G7JCWk","name":"TOC Row 2","width":"fill_container","gap":8,"alignItems":"center","children":[{"type":"icon","id":"P8iKp","name":"TOC Icon 2","width":12,"height":12,"icon":"hash","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"uYiM7","name":"TOC Text 2","fill":"$muted","content":"Recover","fontFamily":"Geist","fontSize":12.5,"fontWeight":"normal"}]},{"type":"rectangle","id":"OIFwR","name":"TOC Divider","fill":"$border/border","width":"fill_container","height":1},{"type":"text","id":"Pu8zV","name":"Coverage Label","fill":"$field/placeholder","content":"COVERS","fontFamily":"JetBrains Mono","fontSize":9,"fontWeight":"600","letterSpacing":0.7},{"type":"frame","id":"ZhJKd","name":"Coverage Row 0","width":"fill_container","gap":7,"alignItems":"center","children":[{"type":"icon","id":"HYVNh","name":"Coverage Icon 0","width":12,"height":12,"icon":"book-open","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"hu6Qi","name":"Coverage Text 0","fill":"$muted","content":"SQLite","fontFamily":"Geist","fontSize":12,"fontWeight":"normal"}]},{"type":"frame","id":"Z6QBc","name":"Coverage Row 1","width":"fill_container","gap":7,"alignItems":"center","children":[{"type":"icon","id":"kmUF2","name":"Coverage Icon 1","width":12,"height":12,"icon":"book-open","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"lDsTG","name":"Coverage Text 1","fill":"$muted","content":"PostgreSQL","fontFamily":"Geist","fontSize":12,"fontWeight":"normal"}]},{"type":"frame","id":"hhIqz","name":"Coverage Row 2","width":"fill_container","gap":7,"alignItems":"center","children":[{"type":"icon","id":"ZVMmt","name":"Coverage Icon 2","width":12,"height":12,"icon":"book-open","library":"lucide","fill":"$field/placeholder"},{"type":"text","id":"pf0vi","name":"Coverage Text 2","fill":"$muted","content":"Migrations","fontFamily":"Geist","fontSize":12,"fontWeight":"normal"}]}]},{"type":"frame","id":"Ptk6I","name":"Edit Page Card","width":"fill_container","fill":"$background/background","cornerRadius":"$radius/3xl","stroke":"$border/border","strokeWidth":1,"strokeAlignment":"inner","layout":"vertical","gap":6,"padding":14,"children":[{"type":"text","id":"n4mDoU","name":"Edit Page Title","fill":"$foreground/foreground","content":"Improve this page","fontFamily":"JetBrains Mono","fontSize":12,"fontWeight":"600"},{"type":"text","id":"r4lkRF","name":"Edit Page Body","fill":"$muted","textGrowth":"fixed-width","width":"fill_container","content":"Found a gap? Open a docs issue or suggest an edit on GitHub.","lineHeight":1.4,"fontFamily":"Geist","fontSize":11.5,"fontWeight":"normal"},{"type":"frame","id":"IdWes","name":"Edit Page Link","gap":6,"alignItems":"center","children":[{"type":"text","id":"H2omr","name":"Edit Page Link Text","fill":"$accent/soft-foreground","content":"Edit this page","fontFamily":"Geist","fontSize":12,"fontWeight":"500"},{"type":"icon","id":"s6iraX","name":"Edit Page Link Icon","width":12,"height":12,"icon":"external-link","library":"lucide","fill":"$accent/accent"}]}]}]}]}]}]}
diff --git a/website-export/json/rW1Ux.json b/website-export/json/rW1Ux.json
index 7188a53..7d4d12b 100644
--- a/website-export/json/rW1Ux.json
+++ b/website-export/json/rW1Ux.json
@@ -503,7 +503,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "A v tag builds and signs images, packages the OCI chart, creates release notes, and publishes official modules.",
+ "content": "A v tag builds and signs images, packages the OCI chart, creates release notes, and publishes official modules. Signing is mandatory and fail-closed: the job fails without COSIGN_PRIVATE_KEY before anything is pushed.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
diff --git a/website-export/json/rqx1v.json b/website-export/json/rqx1v.json
index c5f9d83..faf271f 100644
--- a/website-export/json/rqx1v.json
+++ b/website-export/json/rqx1v.json
@@ -610,7 +610,7 @@
"id": "N3G8uU",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Web dashboard development",
+ "content": "Local development setup",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/s3NwF.json b/website-export/json/s3NwF.json
index 5cddd7d..6fd9111 100644
--- a/website-export/json/s3NwF.json
+++ b/website-export/json/s3NwF.json
@@ -207,7 +207,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Select ClusterIP, NodePort, or LoadBalancer based on where players connect and who controls the network edge.",
+ "content": "Select ClusterIP, NodePort, or LoadBalancer based on where players connect and who controls the network edge. When you have no public IP or no control over the network edge, layer a relay tunnel over the Service.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -325,6 +325,38 @@
"fontWeight": "normal"
}
]
+ },
+ {
+ "type": "frame",
+ "id": "uvoEx",
+ "name": "Section 1 Item 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "yeKwY",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "y0JVjj",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Layer relay tunnels (frp, Tailscale, playit) over the Service when lacking public IP or LoadBalancer.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
}
]
}
@@ -355,7 +387,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Treat every port override and CIDR allow-list as part of the server’s documented interface.",
+ "content": "Treat every port override and CIDR allow-list as part of the server's documented interface.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
diff --git a/website-export/json/wCt6V.json b/website-export/json/wCt6V.json
new file mode 100644
index 0000000..10e6edb
--- /dev/null
+++ b/website-export/json/wCt6V.json
@@ -0,0 +1,975 @@
+{
+ "type": "frame",
+ "id": "wCt6V",
+ "x": 0,
+ "y": 117108,
+ "name": "Screen/Docs — Server Networking & Tunnels",
+ "theme": {
+ "semantic": "dark"
+ },
+ "clip": true,
+ "width": 1440,
+ "height": 900,
+ "fill": "$background/background",
+ "children": [
+ {
+ "id": "w6l1R",
+ "type": "ref",
+ "ref": "K2Rt4H",
+ "height": "fill_container",
+ "name": "DocsSidebar/Expanded",
+ "width": 280
+ },
+ {
+ "type": "frame",
+ "id": "fhXV7",
+ "name": "Docs Main",
+ "width": "fill_container",
+ "height": "fill_container",
+ "fill": "$background/background",
+ "layout": "vertical",
+ "children": [
+ {
+ "id": "e43QYM",
+ "type": "ref",
+ "ref": "SOdlr",
+ "name": "Docs Top Bar",
+ "width": "fill_container",
+ "descendants": {
+ "NImey": {
+ "content": "Operate"
+ },
+ "ikbi6": {
+ "content": "Server Networking & Tunnels"
+ }
+ }
+ },
+ {
+ "type": "frame",
+ "id": "LwBO9",
+ "name": "Docs Article Body",
+ "clip": true,
+ "width": "fill_container",
+ "height": "fill_container",
+ "gap": 32,
+ "padding": 32,
+ "children": [
+ {
+ "type": "frame",
+ "id": "YWwHg",
+ "name": "Docs Article",
+ "width": 780,
+ "layout": "vertical",
+ "gap": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "NH0sG",
+ "name": "Article Eyebrow",
+ "fill": "$accent/accent",
+ "content": "CONFIGURE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 0.8
+ },
+ {
+ "type": "text",
+ "id": "gp569",
+ "name": "Article Title",
+ "fill": "$foreground/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Server Networking & Tunnels",
+ "lineHeight": 1.1,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 30,
+ "fontWeight": "700"
+ },
+ {
+ "type": "text",
+ "id": "i2Wqc",
+ "name": "Article Lead",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Route player connections through an external relay when you lack a public IP, port-forwarding, or a cluster load balancer. A tunnel layers over the server's backing Service; it is not a fifth expose mode. Use it only when direct exposure is unavailable.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 15,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "wMeUk",
+ "name": "Article Metadata",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "id": "urMRD",
+ "type": "ref",
+ "ref": "NZPtq",
+ "name": "Meta Chip 0",
+ "descendants": {
+ "NLG8t": {
+ "content": "NETWORKING"
+ }
+ }
+ },
+ {
+ "id": "LBuDl",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 1",
+ "descendants": {
+ "yb6zz": {
+ "content": "v0.2"
+ }
+ }
+ },
+ {
+ "id": "vyBDU",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 2",
+ "descendants": {
+ "yb6zz": {
+ "content": "18 MIN"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "QpxML",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "tqABK",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "workflow",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "ib4Nk",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Tunnels add operational cost. Each tunnel-enabled server gets its own always-on tunnel pod to hold the public address, even while the game sleeps.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "ecL1w",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "x3LjQ",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "When to use a tunnel",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "w26CKe",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use a tunnel when you lack a public IP, port-forwarding, or cluster load balancer access—but not if you already have a working LoadBalancer or public IP.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "LQST4",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "xPwvX",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "E5sNSW",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "d2qBrm",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use when behind CGNAT or restrictive ISP",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "axBJn",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "dJ9Ku",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "brkGI",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use when port-forwarding is unavailable",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "pVX9U",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "kpMqf",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "WTt4d",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use when LoadBalancer is not an option",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "Ro06n",
+ "name": "Section 1 Item 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Y8GG6",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "At1A7",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Do not use if you already have public IP and working port-forwarding",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "p23tk",
+ "name": "Guide Section 2",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "umjnk",
+ "name": "Section Title 2",
+ "fill": "$foreground/foreground",
+ "content": "Choose a provider",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "Ze6o1",
+ "name": "Section Body 2",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Three relay providers are supported: frp for production, Tailscale for private networks, and playit.gg for casual testing.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "nsT2h",
+ "name": "Section List 2",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "E58cZg",
+ "name": "Section 2 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "xVTSt",
+ "name": "Section 2 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "F4ZRG4",
+ "name": "Section 2 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use frp for production servers with custom public domains",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "gfsap",
+ "name": "Section 2 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "OvSBS",
+ "name": "Section 2 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "cB687",
+ "name": "Section 2 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use Tailscale for private play on your tailnet (free, no VPS)",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "WnLt6",
+ "name": "Section 2 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "de5Cc",
+ "name": "Section 2 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "mvLoP",
+ "name": "Section 2 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use playit.gg for free public relay and quick testing",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "jQuKg",
+ "name": "Guide Section 3",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "Q1UYn",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "Get the tunnel running",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "mOiu6",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Set credentials via dashboard (recommended), API, or kubectl with proper ownerReferences. The tunnel pod connects and serves players even while the game sleeps.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "WCLNY",
+ "name": "Quick Reference",
+ "width": "fill_container",
+ "fill": "$surface/tertiary",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 3,
+ "padding": 12,
+ "children": [
+ {
+ "type": "text",
+ "id": "v9skG",
+ "name": "Quick Reference Label",
+ "fill": "$accent/accent",
+ "content": "SETUP CHECKLIST",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.6
+ },
+ {
+ "type": "text",
+ "id": "Uj44H",
+ "name": "Quick Reference Line 1",
+ "fill": "$foreground/foreground",
+ "content": "01 Prepare relay credentials (frp token, Tailscale key, or playit secret)",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "y7vKcf",
+ "name": "Quick Reference Line 2",
+ "fill": "$foreground/foreground",
+ "content": "02 Create GameServer with tunnel enabled and provider configured",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "VSWdf",
+ "name": "Quick Reference Line 3",
+ "fill": "$foreground/foreground",
+ "content": "03 Provide credentials via dashboard; verify TunnelReady condition",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "NjQFY",
+ "name": "Article Next Link",
+ "width": "fill_container",
+ "stroke": "$border/border",
+ "strokeWidth": {
+ "top": 1
+ },
+ "strokeAlignment": "inner",
+ "strokeLinejoin": "round",
+ "padding": [
+ 10,
+ 0,
+ 0,
+ 0
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "v96YGE",
+ "name": "Next Link Text",
+ "layout": "vertical",
+ "gap": 2,
+ "children": [
+ {
+ "type": "text",
+ "id": "j8FigB",
+ "name": "Next Link Eyebrow",
+ "fill": "$field/placeholder",
+ "content": "NEXT GUIDE",
+ "fontFamily": "Geist",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "HGwKX",
+ "name": "Next Link Title",
+ "fill": "$foreground/foreground",
+ "content": "Cluster and storage operations",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 13,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "OpG7W",
+ "name": "Next Link Icon",
+ "width": 15,
+ "height": 15,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "xZXwa",
+ "name": "Docs Page Aside",
+ "width": 250,
+ "layout": "vertical",
+ "gap": 16,
+ "children": [
+ {
+ "type": "frame",
+ "id": "T1B3x",
+ "name": "Docs Table of Contents",
+ "width": "fill_container",
+ "fill": "$surface/secondary",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 10,
+ "padding": 16,
+ "children": [
+ {
+ "type": "text",
+ "id": "djHkp",
+ "name": "TOC Label",
+ "fill": "$field/placeholder",
+ "content": "ON THIS PAGE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "A14VL",
+ "name": "TOC Row 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "NbJZz",
+ "name": "TOC Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "WHrxu",
+ "name": "TOC Text 0",
+ "fill": "$foreground/foreground",
+ "content": "When to use",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "q3eqR",
+ "name": "TOC Row 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "hmKLi",
+ "name": "TOC Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "MHQKJ",
+ "name": "TOC Text 1",
+ "fill": "$muted",
+ "content": "Choose provider",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "w7ejRv",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "D1SRJ",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "Unf3w",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "Setup",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "rectangle",
+ "id": "l7z12m",
+ "name": "TOC Divider",
+ "fill": "$border/border",
+ "width": "fill_container",
+ "height": 1
+ },
+ {
+ "type": "text",
+ "id": "xuKZW",
+ "name": "Coverage Label",
+ "fill": "$field/placeholder",
+ "content": "COVERS",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "lzYNT",
+ "name": "Coverage Row 0",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "wi1jd",
+ "name": "Coverage Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "bVJ0m",
+ "name": "Coverage Text 0",
+ "fill": "$muted",
+ "content": "Tunnels",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "d11U9k",
+ "name": "Coverage Row 1",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "IxKhV",
+ "name": "Coverage Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "iZ8XR",
+ "name": "Coverage Text 1",
+ "fill": "$muted",
+ "content": "Relay",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "hscYM",
+ "name": "Coverage Row 2",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "k8XzND",
+ "name": "Coverage Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "xbzGV",
+ "name": "Coverage Text 2",
+ "fill": "$muted",
+ "content": "NAT traversal",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "L2y0YZ",
+ "name": "Edit Page Card",
+ "width": "fill_container",
+ "fill": "$background/background",
+ "cornerRadius": "$radius/3xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 6,
+ "padding": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "cKx0M",
+ "name": "Edit Page Title",
+ "fill": "$foreground/foreground",
+ "content": "Improve this page",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 12,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "iHwdm",
+ "name": "Edit Page Body",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Found a gap? Open a docs issue or suggest an edit on GitHub.",
+ "lineHeight": 1.4,
+ "fontFamily": "Geist",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "dSWJG",
+ "name": "Edit Page Link",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "M4qDPO",
+ "name": "Edit Page Link Text",
+ "fill": "$accent/soft-foreground",
+ "content": "Edit this page",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "500"
+ },
+ {
+ "type": "icon",
+ "id": "cHbQk",
+ "name": "Edit Page Link Icon",
+ "width": 12,
+ "height": 12,
+ "icon": "external-link",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/website-export/json/ydcNa.json b/website-export/json/ydcNa.json
index 73d6c69..49a4f63 100644
--- a/website-export/json/ydcNa.json
+++ b/website-export/json/ydcNa.json
@@ -175,7 +175,7 @@
"fill": "$accent/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Wipe and Delete have different resource consequences, but both require a verified backup and an explicit owner decision.",
+ "content": "Ownership, collaborators, transfer, wipe, and delete are available in v0.2.0-beta.8, where operator-role users can also run them on servers they don't own. From v0.3.0, transferring ownership, editing collaborators, wiping data and deleting a server need the server's owner or an admin. Servers with no recorded owner (for example created with kubectl or GitOps) can then be transferred, wiped or deleted only by an admin.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -355,7 +355,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Select an existing user who accepts the server’s data and operational obligations, then verify every downstream access path.",
+ "content": "Select an existing user who accepts the server's data and operational obligations, then verify every downstream access path.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -610,7 +610,7 @@
"id": "xiNqn",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Backup destinations",
+ "content": "Console, Events & Logs",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/json/yuCG9.json b/website-export/json/yuCG9.json
index 949d994..7988d95 100644
--- a/website-export/json/yuCG9.json
+++ b/website-export/json/yuCG9.json
@@ -207,7 +207,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Invite and edit local users, reset credentials deliberately, and use service accounts for automation rather than shared logins.",
+ "content": "Invite and edit local users, reset credentials deliberately, and give automation its own account rather than a shared login.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -287,7 +287,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Scope service accounts to the minimum permissions and rotate credentials.",
+ "content": "Give each automation its own local user with the minimum role, and rotate its password.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
diff --git a/website-export/json/zeM79.json b/website-export/json/zeM79.json
index dea6ee1..b3d9047 100644
--- a/website-export/json/zeM79.json
+++ b/website-export/json/zeM79.json
@@ -610,7 +610,7 @@
"id": "p9RSW",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "High availability",
+ "content": "Agent gateway",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
diff --git a/website-export/screenshots/A2sAtw.png b/website-export/screenshots/A2sAtw.png
index d165e24..efc6cc2 100644
Binary files a/website-export/screenshots/A2sAtw.png and b/website-export/screenshots/A2sAtw.png differ
diff --git a/website-export/screenshots/BEcZK.png b/website-export/screenshots/BEcZK.png
index 8a927b6..aa9b259 100644
Binary files a/website-export/screenshots/BEcZK.png and b/website-export/screenshots/BEcZK.png differ
diff --git a/website-export/screenshots/CuW6O.png b/website-export/screenshots/CuW6O.png
index 0d30cca..02cc465 100644
Binary files a/website-export/screenshots/CuW6O.png and b/website-export/screenshots/CuW6O.png differ
diff --git a/website-export/screenshots/D4SNhN.png b/website-export/screenshots/D4SNhN.png
index 2a2783d..d3b332b 100644
Binary files a/website-export/screenshots/D4SNhN.png and b/website-export/screenshots/D4SNhN.png differ
diff --git a/website-export/screenshots/HpLFh.png b/website-export/screenshots/HpLFh.png
index 6f952d2..a08fd3e 100644
Binary files a/website-export/screenshots/HpLFh.png and b/website-export/screenshots/HpLFh.png differ
diff --git a/website-export/screenshots/KFLCi.png b/website-export/screenshots/KFLCi.png
index 9a18a6b..6dc572d 100644
Binary files a/website-export/screenshots/KFLCi.png and b/website-export/screenshots/KFLCi.png differ
diff --git a/website-export/screenshots/N4TBV.png b/website-export/screenshots/N4TBV.png
index ab82427..b1765fe 100644
Binary files a/website-export/screenshots/N4TBV.png and b/website-export/screenshots/N4TBV.png differ
diff --git a/website-export/screenshots/Nnd3Y.png b/website-export/screenshots/Nnd3Y.png
index ffee08f..bc84c06 100644
Binary files a/website-export/screenshots/Nnd3Y.png and b/website-export/screenshots/Nnd3Y.png differ
diff --git a/website-export/screenshots/OIN7m.png b/website-export/screenshots/OIN7m.png
index 0824f0d..ea08e44 100644
Binary files a/website-export/screenshots/OIN7m.png and b/website-export/screenshots/OIN7m.png differ
diff --git a/website-export/screenshots/P5qsD.png b/website-export/screenshots/P5qsD.png
index 0584d65..3a798b3 100644
Binary files a/website-export/screenshots/P5qsD.png and b/website-export/screenshots/P5qsD.png differ
diff --git a/website-export/screenshots/QHFdH.png b/website-export/screenshots/QHFdH.png
new file mode 100644
index 0000000..435d896
Binary files /dev/null and b/website-export/screenshots/QHFdH.png differ
diff --git a/website-export/screenshots/QdLzE.png b/website-export/screenshots/QdLzE.png
index 1ea80bb..cef6f9b 100644
Binary files a/website-export/screenshots/QdLzE.png and b/website-export/screenshots/QdLzE.png differ
diff --git a/website-export/screenshots/SKMOT.png b/website-export/screenshots/SKMOT.png
index d2a4721..dfe7b25 100644
Binary files a/website-export/screenshots/SKMOT.png and b/website-export/screenshots/SKMOT.png differ
diff --git a/website-export/screenshots/TESyw.png b/website-export/screenshots/TESyw.png
index 804ac3e..ba4f723 100644
Binary files a/website-export/screenshots/TESyw.png and b/website-export/screenshots/TESyw.png differ
diff --git a/website-export/screenshots/VgF1s.png b/website-export/screenshots/VgF1s.png
index 9d5516a..39f0edd 100644
Binary files a/website-export/screenshots/VgF1s.png and b/website-export/screenshots/VgF1s.png differ
diff --git a/website-export/screenshots/X0DeYH.png b/website-export/screenshots/X0DeYH.png
index b02b31a..ea22c7c 100644
Binary files a/website-export/screenshots/X0DeYH.png and b/website-export/screenshots/X0DeYH.png differ
diff --git a/website-export/screenshots/ZwM1N.png b/website-export/screenshots/ZwM1N.png
index 2ae108c..4be2d17 100644
Binary files a/website-export/screenshots/ZwM1N.png and b/website-export/screenshots/ZwM1N.png differ
diff --git a/website-export/screenshots/aZAEs.png b/website-export/screenshots/aZAEs.png
index 95dd8b4..c0efffd 100644
Binary files a/website-export/screenshots/aZAEs.png and b/website-export/screenshots/aZAEs.png differ
diff --git a/website-export/screenshots/b5srx.png b/website-export/screenshots/b5srx.png
index f9eaa1d..580cbce 100644
Binary files a/website-export/screenshots/b5srx.png and b/website-export/screenshots/b5srx.png differ
diff --git a/website-export/screenshots/b5u3Ui.png b/website-export/screenshots/b5u3Ui.png
index 41c0f9e..fc3ded5 100644
Binary files a/website-export/screenshots/b5u3Ui.png and b/website-export/screenshots/b5u3Ui.png differ
diff --git a/website-export/screenshots/cpCjN.png b/website-export/screenshots/cpCjN.png
index 9029390..ab11e67 100644
Binary files a/website-export/screenshots/cpCjN.png and b/website-export/screenshots/cpCjN.png differ
diff --git a/website-export/screenshots/gEOuV.png b/website-export/screenshots/gEOuV.png
index 6206ef2..adbaf66 100644
Binary files a/website-export/screenshots/gEOuV.png and b/website-export/screenshots/gEOuV.png differ
diff --git a/website-export/screenshots/mYDXb.png b/website-export/screenshots/mYDXb.png
index 24f8232..e3aa2e0 100644
Binary files a/website-export/screenshots/mYDXb.png and b/website-export/screenshots/mYDXb.png differ
diff --git a/website-export/screenshots/n8pnD.png b/website-export/screenshots/n8pnD.png
index 4d20379..7f47950 100644
Binary files a/website-export/screenshots/n8pnD.png and b/website-export/screenshots/n8pnD.png differ
diff --git a/website-export/screenshots/o7PQv.png b/website-export/screenshots/o7PQv.png
index f5676ac..e8a63c7 100644
Binary files a/website-export/screenshots/o7PQv.png and b/website-export/screenshots/o7PQv.png differ
diff --git a/website-export/screenshots/p5Gci.png b/website-export/screenshots/p5Gci.png
index f1e79c5..6a464cd 100644
Binary files a/website-export/screenshots/p5Gci.png and b/website-export/screenshots/p5Gci.png differ
diff --git a/website-export/screenshots/rW1Ux.png b/website-export/screenshots/rW1Ux.png
index 18e9697..3e10e97 100644
Binary files a/website-export/screenshots/rW1Ux.png and b/website-export/screenshots/rW1Ux.png differ
diff --git a/website-export/screenshots/rqx1v.png b/website-export/screenshots/rqx1v.png
index c6dc986..3e92def 100644
Binary files a/website-export/screenshots/rqx1v.png and b/website-export/screenshots/rqx1v.png differ
diff --git a/website-export/screenshots/s3NwF.png b/website-export/screenshots/s3NwF.png
index 2715ef9..f0473f4 100644
Binary files a/website-export/screenshots/s3NwF.png and b/website-export/screenshots/s3NwF.png differ
diff --git a/website-export/screenshots/wCt6V.png b/website-export/screenshots/wCt6V.png
new file mode 100644
index 0000000..958c200
Binary files /dev/null and b/website-export/screenshots/wCt6V.png differ
diff --git a/website-export/screenshots/ydcNa.png b/website-export/screenshots/ydcNa.png
index a8771d8..47bdcef 100644
Binary files a/website-export/screenshots/ydcNa.png and b/website-export/screenshots/ydcNa.png differ
diff --git a/website-export/screenshots/yuCG9.png b/website-export/screenshots/yuCG9.png
index fd473f4..5f04e41 100644
Binary files a/website-export/screenshots/yuCG9.png and b/website-export/screenshots/yuCG9.png differ
diff --git a/website-export/screenshots/zeM79.png b/website-export/screenshots/zeM79.png
index 6320dcf..f11a21f 100644
Binary files a/website-export/screenshots/zeM79.png and b/website-export/screenshots/zeM79.png differ
diff --git a/website.pen b/website.pen
index 7e73c68..1f09e73 100644
--- a/website.pen
+++ b/website.pen
@@ -1,5 +1,5 @@
{
- "version": "2.15",
+ "version": "2.20",
"children": [
{
"type": "frame",
@@ -1151,7 +1151,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-icon.png",
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": "$radius/md",
"justifyContent": "center",
@@ -4212,7 +4212,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -4680,7 +4680,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": 820,
- "content": "Console and log traffic flow API ↔ Agent directly over mTLS; the Operator provisions and reconciles both.",
+ "content": "In the home cluster, console and log traffic flow API ↔ Agent directly over mTLS; the Operator provisions and reconciles both. Remote clusters go through an optional gateway (below).",
"textAlign": "center",
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -4781,6 +4781,37 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "QIGcw",
+ "name": "Multiple clusters and the gateway",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "H5vmfu",
+ "fill": "$foreground/foreground",
+ "content": "Multiple clusters and the gateway (v0.3.0)",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "m5Qk68",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "One central API and dashboard can manage independent clusters registered as Cluster resources. Each target cluster runs its own operator, and optionally a private gateway built from the API image. The gateway has no database, no user login and no browser or admin routes. Users authenticate only to the central API, which stays the authorization authority.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "frame",
"id": "xACvH",
@@ -5065,7 +5096,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Permission-based RBAC with admin/operator/viewer roles, scoped per namespace or cluster-wide.",
+ "content": "Permission-based RBAC with admin/operator/viewer roles, scoped per namespace or cluster-wide. Registering a cluster grants no user access by itself: remote access uses supplemental per-cluster grants, and a global union of permission names never authorizes a row in another cluster.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 14,
@@ -5104,6 +5135,37 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "nak8d",
+ "name": "API ↔ remote gateway",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "EltoE",
+ "fill": "$foreground/foreground",
+ "content": "API ↔ remote gateway",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "UYIWs",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "In v0.3.0, optional agent gateway for remote clusters. Central API reaches it with dedicated mTLS identity, separate trust material from local agent CA. Gateway exposes only allowlisted operations in configured namespaces.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"id": "jFnN9",
"type": "ref",
@@ -5212,7 +5274,7 @@
"type": "text",
"id": "Q7UBD",
"fill": "$foreground/foreground",
- "content": "Module Authoring",
+ "content": "Operate the server fleet",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -5611,7 +5673,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 960,
"fill": "$background/background",
"children": [
{
@@ -5823,7 +5885,69 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Go via gofmt/vet/golangci-lint; TypeScript strict with ESLint — comments only when the why isn't obvious.",
+ "content": "Indentation: tabs in Go, 2 spaces elsewhere. Go: gofmt, vet, golangci-lint. TypeScript: strict with ESLint + Prettier. Comments only when why isn't obvious.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "bZQPI",
+ "name": "Submitting a change",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "NhhOO",
+ "fill": "$foreground/foreground",
+ "content": "Submitting a change",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "vcqC2",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Fork the repository and create a feature branch. Run only quick compile checks locally. For UI work, include Pencil node IDs in PR description. Sign your commits. Verify PR labels: type: and area:.",
+ "lineHeight": 1.5,
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 14,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "ceKhm",
+ "name": "Game modules and submodules",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "Od21k",
+ "fill": "$foreground/foreground",
+ "content": "Game modules and submodules",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 18,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "yIzMh",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Game-module changes belong in gameplane-module repository. Open the module PR there; once it merges, bump the submodule pointer. Repoint submodules locally without committing.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 14,
@@ -5884,7 +6008,7 @@
"type": "text",
"id": "dXLka",
"fill": "$foreground/foreground",
- "content": "Module Authoring",
+ "content": "Local development setup",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -5895,10 +6019,46 @@
},
{
"type": "frame",
- "id": "tjyxc",
- "width": 1,
- "height": 1,
- "layout": "none"
+ "id": "g1nn6",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "MZxVB",
+ "layout": "vertical",
+ "alignItems": "end",
+ "children": [
+ {
+ "type": "text",
+ "id": "HdvO5",
+ "fill": "$muted",
+ "content": "Next",
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "A2zon",
+ "fill": "$foreground/foreground",
+ "content": "Contribution & Design Workflow",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 14,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "knnkZ",
+ "width": 14,
+ "height": 14,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
}
]
}
@@ -5919,7 +6079,7 @@
},
"clip": true,
"width": 1440,
- "height": 1200,
+ "height": 1280,
"fill": "$background/background",
"children": [
{
@@ -6270,7 +6430,7 @@
"fill": "$foreground/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Integrated frp, Tailscale, playit relay sidecars; playit mappings user-managed via playit.gg account. [optional; disabled by default]",
+ "content": "Integrated frp, Tailscale, playit relays (a separate tunnel pod per server); playit mappings user-managed via playit.gg account. [optional; disabled by default]",
"lineHeight": 1.4,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -6507,7 +6667,7 @@
"fill": "$foreground/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "OCI bundles via ModuleSource (git/http/oci/local/upload); optional cosign signature verification per source. 16 ready-to-use templates shipped.",
+ "content": "OCI bundles via ModuleSource (git/http/oci/local/upload); optional cosign signature verification per source. 30 ready-to-use templates on master (16 in v0.2.0-beta.8; the other 14 arrive in v0.3.0).",
"lineHeight": 1.4,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -6586,7 +6746,7 @@
"fill": "$foreground/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Cluster CRD for remote registration/monitoring; console/log streaming local-cluster only. [local cluster only for streaming]",
+ "content": "Cluster CRD for remote registration and health checks; in v0.2.0-beta.8 console/log streaming is local-cluster only. From v0.3.0: remote Pod logs/PTY, optional private gateway, and unified dashboard with location filters.",
"lineHeight": 1.4,
"fontFamily": "$typography/font-sans",
"fontSize": 12,
@@ -6855,7 +7015,7 @@
"type": "text",
"id": "IlV6C",
"fill": "$foreground/foreground",
- "content": "Contributing",
+ "content": "Dashboard tour",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -6889,7 +7049,7 @@
"type": "text",
"id": "iWfmq",
"fill": "$foreground/foreground",
- "content": "Roadmap",
+ "content": "Sign-in & Account Recovery",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -7074,7 +7234,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "WebSocket console/log/file streams are local-cluster-scoped — remote clusters are registered and health-checked but not yet streamed.",
+ "content": "WebSocket console/log/file streams are local-cluster-scoped in v0.2.0-beta.8 — remote clusters are registered and health-checked but not yet streamed. v0.3.0 adds remote access through an optional agent gateway.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -7106,7 +7266,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "The PostgreSQL driver is experimental (build-tag-gated, work-in-progress); SQLite is the production-tested database.",
+ "content": "The PostgreSQL driver is experimental (build-tag-gated); SQLite is the production-tested database. v0.3.0 makes the driver work end to end, but it stays experimental: keep a single API replica, and it is not yet covered by e2e or upgrade tests.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -7182,7 +7342,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Server-side audit-log export and webhook push sink.",
+ "content": "Multicluster agent gateway — optional private mTLS gateway per cluster, with combined dashboard, Servers, Backups and Search views.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -7211,7 +7371,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Audit-to-syslog bridge.",
+ "content": "PostgreSQL driver works end to end — still experimental, keep a single API replica.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -7305,7 +7465,7 @@
"type": "text",
"id": "LuSUV",
"fill": "$foreground/foreground",
- "content": "Comparison",
+ "content": "Frequently Asked Questions",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -7339,7 +7499,7 @@
"type": "text",
"id": "U1eKI",
"fill": "$foreground/foreground",
- "content": "FAQ",
+ "content": "Changelog",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -7840,7 +8000,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Added: fleet metrics, server-side audit export, audit webhook sink, syslog bridge. Changed: default module source is now git-based.",
+ "content": "Multicluster gateways, theme customization, PostgreSQL support, network capture, module authoring, 14 new game modules.",
"lineHeight": 1.5,
"fontFamily": "$typography/font-sans",
"fontSize": 13,
@@ -8521,7 +8681,7 @@
"type": "text",
"id": "GDxqj",
"fill": "$foreground/foreground",
- "content": "FAQ",
+ "content": "Roadmap & Known Limitations",
"fontFamily": "$typography/font-mono",
"fontSize": 14,
"fontWeight": "500"
@@ -8532,10 +8692,46 @@
},
{
"type": "frame",
- "id": "XMvnR",
- "width": 1,
- "height": 1,
- "layout": "none"
+ "id": "sVgzD",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "r0pfb",
+ "layout": "vertical",
+ "alignItems": "end",
+ "children": [
+ {
+ "type": "text",
+ "id": "exnC9",
+ "fill": "$muted",
+ "content": "Next",
+ "fontFamily": "$typography/font-sans",
+ "fontSize": 11,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "G6krZ3",
+ "fill": "$foreground/foreground",
+ "content": "Get Involved",
+ "fontFamily": "$typography/font-mono",
+ "fontSize": 14,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "EYNPT",
+ "width": 14,
+ "height": 14,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
}
]
}
@@ -14655,7 +14851,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-mark.png",
- "mode": "fit"
+ "mode": "contain"
}
},
{
@@ -14812,7 +15008,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-icon.png",
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": "$radius/md",
"justifyContent": "center",
@@ -15679,7 +15875,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-icon.png",
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": "$radius/md",
"justifyContent": "center",
@@ -15896,7 +16092,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-icon.png",
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": "$radius/md",
"justifyContent": "center",
@@ -17352,7 +17548,7 @@
"fill": "$accent/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Start with one cluster selected. Every list, metric, and action inherits that scope.",
+ "content": "In v0.2.0-beta.8, a cluster selector scopes everything. From v0.3.0, the dashboard combines every authorized cluster with an optional Location filter.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -17384,7 +17580,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "The dashboard is a cluster-scoped control surface. Use the top bar to change context without losing your place.",
+ "content": "Opening a server pins it to its own cluster and namespace, so changing a list filter never retargets the server you are working on.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -17432,7 +17628,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Cluster selector scopes fleet metrics, lists, and creation flows.",
+ "content": "Narrows the dashboard, Servers and Backups to one cluster; combined view is default (v0.3.0).",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -17464,7 +17660,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Global search jumps to servers, backups, users, and modules.",
+ "content": "Finds any server by name across the clusters you can access.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -23042,7 +23238,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Select ClusterIP, NodePort, or LoadBalancer based on where players connect and who controls the network edge.",
+ "content": "Select ClusterIP, NodePort, or LoadBalancer based on where players connect and who controls the network edge. When you have no public IP or no control over the network edge, layer a relay tunnel over the Service.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -23160,6 +23356,38 @@
"fontWeight": "normal"
}
]
+ },
+ {
+ "type": "frame",
+ "id": "uvoEx",
+ "name": "Section 1 Item 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "yeKwY",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "y0JVjj",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Layer relay tunnels (frp, Tailscale, playit) over the Service when lacking public IP or LoadBalancer.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
}
]
}
@@ -23985,7 +24213,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Use per-server or global views to start snapshots, filter phases, and inspect destination, size, timestamps, and errors.",
+ "content": "Use per-server or global views to start snapshots, filter phases, and inspect destination, size, timestamps, and errors. From v0.3.0, the Backups page groups its filters in a popover on the right of the search row: Location (when several clusters are registered), Server and the current tab's Phase. Selections apply when you click Apply.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -24388,7 +24616,7 @@
"id": "g06VqS",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Cluster and storage operations",
+ "content": "Backup Destinations",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -25871,7 +26099,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Invite and edit local users, reset credentials deliberately, and use service accounts for automation rather than shared logins.",
+ "content": "Invite and edit local users, reset credentials deliberately, and give automation its own account rather than a shared login.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -25951,7 +26179,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Scope service accounts to the minimum permissions and rotate credentials.",
+ "content": "Give each automation its own local user with the minimum role, and rotate its password.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -27217,7 +27445,7 @@
"id": "GgseU",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Modules and sources",
+ "content": "System log viewer",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -30046,7 +30274,7 @@
"id": "XEhHY",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Upgrade and migrate",
+ "content": "Dashboard tour",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -30388,7 +30616,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -30947,6 +31175,72 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "P1ch8s",
+ "name": "Guide Section 4",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "GE524",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "CRD updates on upgrade",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "wHtaW",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Helm cannot auto-update CRDs, but the chart's autoApply hook applies them server-side. From v0.3.0, Helm 4 needs --force-conflicts on a first install over v0.2 CRDs.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "w2Xb8",
+ "name": "Guide Section 5",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "D5jOxT",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "Upgrade notes for v0.3.0",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "O2vzsD",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "GameServers roll once for network capture volume. Owner-only server operations (transfer, collaborate, wipe, delete). Playit tunnel egress widened. Tunnel Secrets must be owned by GameServer.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "frame",
"id": "GzehF",
@@ -30989,7 +31283,7 @@
"id": "pVDYw",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Troubleshooting",
+ "content": "Executable upgrades and rollback",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -31133,6 +31427,66 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "zrPcr",
+ "name": "TOC Row 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "UCM3z",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "f34OII",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "CRD updates",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "dHQHZ",
+ "name": "TOC Row 4",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "lA9d1",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "CZ9Tf",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "v0.3.0 notes",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "rectangle",
"id": "h94HEV",
@@ -34348,7 +34702,7 @@
"id": "c2mN5N",
"name": "Quick Reference Line 3",
"fill": "$foreground/foreground",
- "content": "03 Recovery → self-service or audited admin reset; OIDC resets upstream",
+ "content": "03 Recovery → audited admin reset or bootstrap-admin break-glass; OIDC resets upstream",
"fontFamily": "JetBrains Mono",
"fontSize": 11.5,
"fontWeight": "normal"
@@ -34399,7 +34753,7 @@
"id": "yTLSd",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Template-specific configuration",
+ "content": "Using the Mobile Dashboard",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -37964,7 +38318,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Use valid names and verify the breadcrumb before confirming.",
+ "content": "Use valid names and verify the breadcrumb before confirming",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -38028,7 +38382,39 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Verify upload size, type, provenance, and ownership after completion.",
+ "content": "Verify upload size, type, provenance, and ownership after completion",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "O5wilB",
+ "name": "Section 2 Item 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "cSTIi",
+ "name": "Section 2 Item Icon 3",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "c3eCc",
+ "name": "Section 2 Item Text 3",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Delete folders from the tree (v0.3.0)",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -40057,7 +40443,7 @@
"id": "Y5Qg0",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Ingress, TLS, and external access",
+ "content": "Air-gapped and private-registry installation",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -41943,7 +42329,7 @@
"id": "li2nn",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Multi-cluster topology",
+ "content": "Platform Disaster Recovery",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -42285,7 +42671,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1120,
"fill": "$background/background",
"children": [
{
@@ -42844,6 +43230,195 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "qDBwV",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "GCxoO",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "milestone",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "kAfre",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The optional agent gateway and the unified cross-cluster dashboard described below arrive in v0.3.0. In v0.2.0-beta.8 you can register clusters and manage their resources from one dashboard, but console and log streaming is scoped to the local control-plane cluster.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "QFjvS",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "gCqrN",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "Manage independent clusters from one dashboard",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "Q1D92r",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "One central API and login manages independent registered clusters.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "TF8SV",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "W0h9A",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "e6KU4m",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "W7pZuF",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Central API uses each cluster's Kubernetes API for resource changes, Pod logs, and PTY attach",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "uzizP",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "UCZZI",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "xev04",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Dashboard combines resources across clusters with optional Location filter (v0.3.0)",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "sU1Zi",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "t7sn2N",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "U4QiGv",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Requires matching API, operator, and agent versions in all clusters",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
{
"type": "frame",
"id": "fYKw1",
@@ -42886,7 +43461,7 @@
"id": "lJ4Me",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Air-gapped installation",
+ "content": "High availability and failure domains",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -43030,6 +43605,36 @@
}
]
},
+ {
+ "type": "frame",
+ "id": "wXygh",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "rHyAr",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "ppwi8",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "Manage clusters",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
{
"type": "rectangle",
"id": "QVp3M",
@@ -43829,7 +44434,7 @@
"id": "p9RSW",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "High availability",
+ "content": "Agent gateway",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -44171,7 +44776,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -44369,7 +44974,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Publish replica safety, leader election, migration ownership, session behavior, and remaining singleton constraints.",
+ "content": "Publish replica safety, leader election, migration ownership, session behavior, and remaining single points of failure.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -44676,6 +45281,7 @@
"id": "aKK2B",
"name": "Quick Reference",
"width": "fill_container",
+ "height": 130,
"fill": "$surface/tertiary",
"cornerRadius": "$radius/xl",
"stroke": "$border/border",
@@ -44701,7 +45307,7 @@
"id": "yS5D3",
"name": "Quick Reference Line 1",
"fill": "$foreground/foreground",
- "content": "01 Replicate only components documented as replica-safe",
+ "content": "01 Replicate only replica-safe parts (Operator 2+; API stays at 1)",
"fontFamily": "JetBrains Mono",
"fontSize": 11.5,
"fontWeight": "normal"
@@ -44772,7 +45378,7 @@
"id": "V8WoE",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Upgrade execution",
+ "content": "Kubernetes node maintenance and upgrades",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -53259,7 +53865,7 @@
"id": "GcFnz",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Ownership and danger zone",
+ "content": "Ownership, collaborators, and danger zone",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -53767,7 +54373,7 @@
"fill": "$accent/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Wipe and Delete have different resource consequences, but both require a verified backup and an explicit owner decision.",
+ "content": "Ownership, collaborators, transfer, wipe, and delete are available in v0.2.0-beta.8, where operator-role users can also run them on servers they don't own. From v0.3.0, transferring ownership, editing collaborators, wiping data and deleting a server need the server's owner or an admin. Servers with no recorded owner (for example created with kubectl or GitOps) can then be transferred, wiped or deleted only by an admin.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -54202,7 +54808,7 @@
"id": "xiNqn",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Backup destinations",
+ "content": "Console, Events & Logs",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -57974,7 +58580,7 @@
"id": "roSmX",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "User administration",
+ "content": "Multi-cluster Topology",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -58482,7 +59088,7 @@
"fill": "$accent/soft-foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Service-account creation is availability-dependent; do not document a token workflow until the shipped UI and API support it.",
+ "content": "Gameplane has no service accounts or API tokens yet; machine-to-machine tokens are planned for v1.1. Until then, give automation its own local user with the narrowest role that works, and never share a person's login.",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -58917,7 +59523,7 @@
"id": "kBBIl",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "System log viewer",
+ "content": "Permission Catalog",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -61391,7 +61997,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Use a name that distinguishes environments and clusters.",
+ "content": "Use a name that distinguishes environments and clusters (optional)",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -61746,7 +62352,7 @@
"id": "biGQY",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Using the mobile dashboard",
+ "content": "Modules & Sources",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -64575,7 +65181,7 @@
"id": "N3G8uU",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Web dashboard development",
+ "content": "Local development setup",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -65375,7 +65981,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "SSE/WebSocket clients cancel and reconnect visibly on scope changes.",
+ "content": "Sockets bind to cluster and namespace; leaving the server view closes streams (v0.3.0).",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -66461,7 +67067,7 @@
"id": "oceKE",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Operator and CRD development",
+ "content": "Kubectl & GitOps Recipes",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -72012,7 +72618,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "A v tag builds and signs images, packages the OCI chart, creates release notes, and publishes official modules.",
+ "content": "A v tag builds and signs images, packages the OCI chart, creates release notes, and publishes official modules. Signing is mandatory and fail-closed: the job fails without COSIGN_PRIVATE_KEY before anything is pushed.",
"lineHeight": 1.45,
"fontFamily": "Geist",
"fontSize": 13,
@@ -76627,7 +77233,7 @@
"fill": "$default/foreground",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Declare parameters, validation, labels, icons, and capability gates.",
+ "content": "Declare parameters, validation, labels, icons, and gates; currentFrom pre-fills from live status (v0.3.0).",
"fontFamily": "Geist",
"fontSize": 12.5,
"fontWeight": "normal"
@@ -76834,7 +77440,7 @@
"id": "LSjd1",
"name": "Next Link Title",
"fill": "$foreground/foreground",
- "content": "Test and publish modules",
+ "content": "Test, Publish & Sign Modules",
"fontFamily": "JetBrains Mono",
"fontSize": 13,
"fontWeight": "500"
@@ -81167,7 +81773,7 @@
},
"clip": true,
"width": 1440,
- "height": 900,
+ "height": 1000,
"fill": "$background/background",
"children": [
{
@@ -82392,7 +82998,7 @@
"id": "KsrS8",
"name": "Reference 2 Row 2",
"width": "fill_container",
- "height": 38,
+ "height": 60,
"fill": "$background/background",
"stroke": "$separator/separator",
"strokeWidth": {
@@ -82499,7 +83105,7 @@
"fill": "$muted",
"textGrowth": "fixed-width",
"width": "fill_container",
- "content": "Cancel old sockets before reconnecting in new scope.",
+ "content": "A socket stays bound to the cluster and namespace of the server that opened it, and reconnects retain that target. Leaving the server view closes its streams, so a new view opens fresh sockets in its own scope and never inherits stale state.",
"fontFamily": "Geist",
"fontSize": 11,
"fontWeight": "normal"
@@ -95855,7 +96461,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-icon.png",
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": "$radius/md",
"justifyContent": "center",
@@ -96044,7 +96650,7 @@
"type": "image",
"enabled": true,
"url": "design-assets/gameplane-icon.png",
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": "$radius/md",
"justifyContent": "center",
@@ -96855,7 +97461,7 @@
"fill": {
"type": "image",
"enabled": true,
- "mode": "fill"
+ "mode": "cover"
},
"cornerRadius": 9999
},
@@ -106753,6 +107359,2183 @@
]
}
]
+ },
+ {
+ "type": "frame",
+ "id": "wCt6V",
+ "x": 0,
+ "y": 117108,
+ "name": "Screen/Docs — Server Networking & Tunnels",
+ "theme": {
+ "semantic": "dark"
+ },
+ "clip": true,
+ "width": 1440,
+ "height": 900,
+ "fill": "$background/background",
+ "children": [
+ {
+ "id": "w6l1R",
+ "type": "ref",
+ "ref": "K2Rt4H",
+ "height": "fill_container",
+ "name": "DocsSidebar/Expanded",
+ "width": 280
+ },
+ {
+ "type": "frame",
+ "id": "fhXV7",
+ "name": "Docs Main",
+ "width": "fill_container",
+ "height": "fill_container",
+ "fill": "$background/background",
+ "layout": "vertical",
+ "children": [
+ {
+ "id": "e43QYM",
+ "type": "ref",
+ "ref": "SOdlr",
+ "name": "Docs Top Bar",
+ "width": "fill_container",
+ "descendants": {
+ "NImey": {
+ "content": "Operate"
+ },
+ "ikbi6": {
+ "content": "Server Networking & Tunnels"
+ }
+ }
+ },
+ {
+ "type": "frame",
+ "id": "LwBO9",
+ "name": "Docs Article Body",
+ "clip": true,
+ "width": "fill_container",
+ "height": "fill_container",
+ "gap": 32,
+ "padding": 32,
+ "children": [
+ {
+ "type": "frame",
+ "id": "YWwHg",
+ "name": "Docs Article",
+ "width": 780,
+ "layout": "vertical",
+ "gap": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "NH0sG",
+ "name": "Article Eyebrow",
+ "fill": "$accent/accent",
+ "content": "CONFIGURE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 0.8
+ },
+ {
+ "type": "text",
+ "id": "gp569",
+ "name": "Article Title",
+ "fill": "$foreground/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Server Networking & Tunnels",
+ "lineHeight": 1.1,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 30,
+ "fontWeight": "700"
+ },
+ {
+ "type": "text",
+ "id": "i2Wqc",
+ "name": "Article Lead",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Route player connections through an external relay when you lack a public IP, port-forwarding, or a cluster load balancer. A tunnel layers over the server's backing Service; it is not a fifth expose mode. Use it only when direct exposure is unavailable.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 15,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "wMeUk",
+ "name": "Article Metadata",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "id": "urMRD",
+ "type": "ref",
+ "ref": "NZPtq",
+ "name": "Meta Chip 0",
+ "descendants": {
+ "NLG8t": {
+ "content": "NETWORKING"
+ }
+ }
+ },
+ {
+ "id": "LBuDl",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 1",
+ "descendants": {
+ "yb6zz": {
+ "content": "v0.2"
+ }
+ }
+ },
+ {
+ "id": "vyBDU",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 2",
+ "descendants": {
+ "yb6zz": {
+ "content": "18 MIN"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "QpxML",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "tqABK",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "workflow",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "ib4Nk",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Tunnels add operational cost. Each tunnel-enabled server gets its own always-on tunnel pod to hold the public address, even while the game sleeps.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "ecL1w",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "x3LjQ",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "When to use a tunnel",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "w26CKe",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use a tunnel when you lack a public IP, port-forwarding, or cluster load balancer access—but not if you already have a working LoadBalancer or public IP.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "LQST4",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "xPwvX",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "E5sNSW",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "d2qBrm",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use when behind CGNAT or restrictive ISP",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "axBJn",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "dJ9Ku",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "brkGI",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use when port-forwarding is unavailable",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "pVX9U",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "kpMqf",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "WTt4d",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use when LoadBalancer is not an option",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "Ro06n",
+ "name": "Section 1 Item 3",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Y8GG6",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "At1A7",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Do not use if you already have public IP and working port-forwarding",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "p23tk",
+ "name": "Guide Section 2",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "umjnk",
+ "name": "Section Title 2",
+ "fill": "$foreground/foreground",
+ "content": "Choose a provider",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "Ze6o1",
+ "name": "Section Body 2",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Three relay providers are supported: frp for production, Tailscale for private networks, and playit.gg for casual testing.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "nsT2h",
+ "name": "Section List 2",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "E58cZg",
+ "name": "Section 2 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "xVTSt",
+ "name": "Section 2 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "F4ZRG4",
+ "name": "Section 2 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use frp for production servers with custom public domains",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "gfsap",
+ "name": "Section 2 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "OvSBS",
+ "name": "Section 2 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "cB687",
+ "name": "Section 2 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use Tailscale for private play on your tailnet (free, no VPS)",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "WnLt6",
+ "name": "Section 2 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "de5Cc",
+ "name": "Section 2 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "mvLoP",
+ "name": "Section 2 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use playit.gg for free public relay and quick testing",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "jQuKg",
+ "name": "Guide Section 3",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "Q1UYn",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "Get the tunnel running",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "mOiu6",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Set credentials via dashboard (recommended), API, or kubectl with proper ownerReferences. The tunnel pod connects and serves players even while the game sleeps.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "WCLNY",
+ "name": "Quick Reference",
+ "width": "fill_container",
+ "fill": "$surface/tertiary",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 3,
+ "padding": 12,
+ "children": [
+ {
+ "type": "text",
+ "id": "v9skG",
+ "name": "Quick Reference Label",
+ "fill": "$accent/accent",
+ "content": "SETUP CHECKLIST",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.6
+ },
+ {
+ "type": "text",
+ "id": "Uj44H",
+ "name": "Quick Reference Line 1",
+ "fill": "$foreground/foreground",
+ "content": "01 Prepare relay credentials (frp token, Tailscale key, or playit secret)",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "y7vKcf",
+ "name": "Quick Reference Line 2",
+ "fill": "$foreground/foreground",
+ "content": "02 Create GameServer with tunnel enabled and provider configured",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "VSWdf",
+ "name": "Quick Reference Line 3",
+ "fill": "$foreground/foreground",
+ "content": "03 Provide credentials via dashboard; verify TunnelReady condition",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "NjQFY",
+ "name": "Article Next Link",
+ "width": "fill_container",
+ "stroke": "$border/border",
+ "strokeWidth": {
+ "top": 1
+ },
+ "strokeAlignment": "inner",
+ "strokeLinejoin": "round",
+ "padding": [
+ 10,
+ 0,
+ 0,
+ 0
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "v96YGE",
+ "name": "Next Link Text",
+ "layout": "vertical",
+ "gap": 2,
+ "children": [
+ {
+ "type": "text",
+ "id": "j8FigB",
+ "name": "Next Link Eyebrow",
+ "fill": "$field/placeholder",
+ "content": "NEXT GUIDE",
+ "fontFamily": "Geist",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "HGwKX",
+ "name": "Next Link Title",
+ "fill": "$foreground/foreground",
+ "content": "Cluster and storage operations",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 13,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "OpG7W",
+ "name": "Next Link Icon",
+ "width": 15,
+ "height": 15,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "xZXwa",
+ "name": "Docs Page Aside",
+ "width": 250,
+ "layout": "vertical",
+ "gap": 16,
+ "children": [
+ {
+ "type": "frame",
+ "id": "T1B3x",
+ "name": "Docs Table of Contents",
+ "width": "fill_container",
+ "fill": "$surface/secondary",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 10,
+ "padding": 16,
+ "children": [
+ {
+ "type": "text",
+ "id": "djHkp",
+ "name": "TOC Label",
+ "fill": "$field/placeholder",
+ "content": "ON THIS PAGE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "A14VL",
+ "name": "TOC Row 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "NbJZz",
+ "name": "TOC Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "WHrxu",
+ "name": "TOC Text 0",
+ "fill": "$foreground/foreground",
+ "content": "When to use",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "q3eqR",
+ "name": "TOC Row 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "hmKLi",
+ "name": "TOC Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "MHQKJ",
+ "name": "TOC Text 1",
+ "fill": "$muted",
+ "content": "Choose provider",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "w7ejRv",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "D1SRJ",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "Unf3w",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "Setup",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "rectangle",
+ "id": "l7z12m",
+ "name": "TOC Divider",
+ "fill": "$border/border",
+ "width": "fill_container",
+ "height": 1
+ },
+ {
+ "type": "text",
+ "id": "xuKZW",
+ "name": "Coverage Label",
+ "fill": "$field/placeholder",
+ "content": "COVERS",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "lzYNT",
+ "name": "Coverage Row 0",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "wi1jd",
+ "name": "Coverage Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "bVJ0m",
+ "name": "Coverage Text 0",
+ "fill": "$muted",
+ "content": "Tunnels",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "d11U9k",
+ "name": "Coverage Row 1",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "IxKhV",
+ "name": "Coverage Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "iZ8XR",
+ "name": "Coverage Text 1",
+ "fill": "$muted",
+ "content": "Relay",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "hscYM",
+ "name": "Coverage Row 2",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "k8XzND",
+ "name": "Coverage Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "xbzGV",
+ "name": "Coverage Text 2",
+ "fill": "$muted",
+ "content": "NAT traversal",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "L2y0YZ",
+ "name": "Edit Page Card",
+ "width": "fill_container",
+ "fill": "$background/background",
+ "cornerRadius": "$radius/3xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 6,
+ "padding": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "cKx0M",
+ "name": "Edit Page Title",
+ "fill": "$foreground/foreground",
+ "content": "Improve this page",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 12,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "iHwdm",
+ "name": "Edit Page Body",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Found a gap? Open a docs issue or suggest an edit on GitHub.",
+ "lineHeight": 1.4,
+ "fontFamily": "Geist",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "dSWJG",
+ "name": "Edit Page Link",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "M4qDPO",
+ "name": "Edit Page Link Text",
+ "fill": "$accent/soft-foreground",
+ "content": "Edit this page",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "500"
+ },
+ {
+ "type": "icon",
+ "id": "cHbQk",
+ "name": "Edit Page Link Icon",
+ "width": 12,
+ "height": 12,
+ "icon": "external-link",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "QHFdH",
+ "x": 0,
+ "y": 118088,
+ "name": "Screen/Docs — Remote Agent Gateway",
+ "theme": {
+ "semantic": "dark"
+ },
+ "clip": true,
+ "width": 1440,
+ "height": 1140,
+ "fill": "$background/background",
+ "children": [
+ {
+ "id": "R1n5h",
+ "type": "ref",
+ "ref": "K2Rt4H",
+ "height": "fill_container",
+ "name": "DocsSidebar/Expanded",
+ "width": 280
+ },
+ {
+ "type": "frame",
+ "id": "PlDdL",
+ "name": "Docs Main",
+ "width": "fill_container",
+ "height": "fill_container",
+ "fill": "$background/background",
+ "layout": "vertical",
+ "children": [
+ {
+ "id": "Ku9Il",
+ "type": "ref",
+ "ref": "SOdlr",
+ "name": "Docs Top Bar",
+ "width": "fill_container",
+ "descendants": {
+ "NImey": {
+ "content": "Platform"
+ },
+ "ikbi6": {
+ "content": "Agent gateway"
+ }
+ }
+ },
+ {
+ "type": "frame",
+ "id": "lOEwV",
+ "name": "Docs Article Body",
+ "clip": true,
+ "width": "fill_container",
+ "height": "fill_container",
+ "gap": 32,
+ "padding": 32,
+ "children": [
+ {
+ "type": "frame",
+ "id": "l6IQjM",
+ "name": "Docs Article",
+ "width": 780,
+ "layout": "vertical",
+ "gap": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "nWAT3",
+ "name": "Article Eyebrow",
+ "fill": "$accent/accent",
+ "content": "PLATFORM",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11,
+ "fontWeight": "600",
+ "letterSpacing": 0.8
+ },
+ {
+ "type": "text",
+ "id": "TmIfW",
+ "name": "Article Title",
+ "fill": "$foreground/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Remote agent gateway",
+ "lineHeight": 1.1,
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 30,
+ "fontWeight": "700"
+ },
+ {
+ "type": "text",
+ "id": "q7kq5v",
+ "name": "Article Lead",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Run one central Gameplane API and dashboard, plus an operator and an optional gateway in each remote cluster. The gateway proxies approved agent operations over mTLS so the central dashboard can reach consoles, files, players and captures in clusters it does not run in.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 15,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "nvPIU",
+ "name": "Article Metadata",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "id": "dEhXo",
+ "type": "ref",
+ "ref": "NZPtq",
+ "name": "Meta Chip 0",
+ "descendants": {
+ "NLG8t": {
+ "content": "GATEWAY"
+ }
+ }
+ },
+ {
+ "id": "DhSsr",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 1",
+ "descendants": {
+ "yb6zz": {
+ "content": "v0.2"
+ }
+ }
+ },
+ {
+ "id": "BhSkh",
+ "type": "ref",
+ "ref": "EbSsA",
+ "name": "Meta Chip 2",
+ "descendants": {
+ "yb6zz": {
+ "content": "14 MIN"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "jkIg1",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Fnyay",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "milestone",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "jMSXT",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The gateway ships in v0.3.0 and is not available in v0.2.0-beta.8.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "eLuG5",
+ "name": "Article Callout",
+ "width": "fill_container",
+ "fill": "$accent/soft",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "gap": 10,
+ "padding": [
+ 10,
+ 12
+ ],
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "CdVif",
+ "name": "Article Callout Icon",
+ "width": 16,
+ "height": 16,
+ "icon": "shield-alert",
+ "library": "lucide",
+ "fill": "$accent/soft-foreground"
+ },
+ {
+ "type": "text",
+ "id": "Y2TRsJ",
+ "name": "Article Callout Text",
+ "fill": "$accent/soft-foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The central API still reaches each Kubernetes API directly for resources, Pod logs and PTY attach. The gateway is not a Kubernetes tunnel and has no database or login.",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "KKCN3",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "w2uan",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "What the gateway carries",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "nZN8l",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "The gateway extends registered clusters with console, file management, player operations, live status and captures. Registry browsing and modpacks use the selected cluster's Kubernetes client and game template.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "XSm6h",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "ZgHNP",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "WgsPX",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "GJtxm",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "RCON console, game logs, file manager, player operations and server status",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "nwaqW",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "JYqrP",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "SxtmS",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Agent-based mods and UID-bound capture files",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "k78RF",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "sR1Pz",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "T8UjFc",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Registry browsing and modpacks use the selected cluster's Kubernetes client",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "U9vhG",
+ "name": "Guide Section 1",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "iDFlx",
+ "name": "Section Title 1",
+ "fill": "$foreground/foreground",
+ "content": "Deploy the gateway with Helm",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "GyyNp",
+ "name": "Section Body 1",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Use matching API, operator and agent versions. Create Secrets for server TLS, central client CA, and agent credentials, then install with Helm setting api.enabled=false and gateway.enabled=true.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "h3s4p",
+ "name": "Section List 1",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "LutgN",
+ "name": "Section 1 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "nnifv",
+ "name": "Section 1 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "i5HYm",
+ "name": "Section 1 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "clusterID must match the Cluster resource name exactly",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "E04BRW",
+ "name": "Section 1 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "HND61",
+ "name": "Section 1 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "reKxX",
+ "name": "Section 1 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Create Role and RoleBinding in each configured namespace with read-only get access",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "bCHex",
+ "name": "Section 1 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Jvo3Q",
+ "name": "Section 1 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "iEsvP",
+ "name": "Section 1 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Configure NetworkPolicy for central API CIDRs, Kubernetes API endpoints, DNS, and game agent traffic",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "WpDkw",
+ "name": "Guide Section 2",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "wvNDk",
+ "name": "Section Title 2",
+ "fill": "$foreground/foreground",
+ "content": "Register the gateway centrally",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "I93Tv",
+ "name": "Section Body 2",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Create a credential Secret in the central API namespace, then add the gateway reference to the existing Cluster resource with URL and tlsSecretRef.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "GAWwN",
+ "name": "Section List 2",
+ "width": "fill_container",
+ "fill": "$surface/surface",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 5,
+ "padding": [
+ 8,
+ 10
+ ],
+ "children": [
+ {
+ "type": "frame",
+ "id": "WzK19",
+ "name": "Section 2 Item 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "q3XxK6",
+ "name": "Section 2 Item Icon 0",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "gvp64",
+ "name": "Section 2 Item Text 0",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Create Secret with ca.crt, tls.crt and tls.key for central client authentication",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "LH8B7",
+ "name": "Section 2 Item 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "PrWrT",
+ "name": "Section 2 Item Icon 1",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "XTaAg",
+ "name": "Section 2 Item Text 1",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Client certificate must chain to gateway.centralClientCASecret and carry the gateway URI SAN",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "HIxz3",
+ "name": "Section 2 Item 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "sN0AY",
+ "name": "Section 2 Item Icon 2",
+ "width": 13,
+ "height": 13,
+ "icon": "check",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "mj0mq",
+ "name": "Section 2 Item Text 2",
+ "fill": "$default/foreground",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Registration name must match the gateway's clusterID",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "N9XImu",
+ "name": "Guide Section 3",
+ "width": "fill_container",
+ "layout": "vertical",
+ "gap": 6,
+ "children": [
+ {
+ "type": "text",
+ "id": "f81U3u",
+ "name": "Section Title 3",
+ "fill": "$foreground/foreground",
+ "content": "Verify the installation",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 17,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "Covjz",
+ "name": "Section Body 3",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Confirm the gateway Pod is Ready, the Cluster resource shows Healthy, and test console, file manager and players on a test server.",
+ "lineHeight": 1.45,
+ "fontFamily": "Geist",
+ "fontSize": 13,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "eWmil",
+ "name": "Quick Reference",
+ "width": "fill_container",
+ "fill": "$surface/tertiary",
+ "cornerRadius": "$radius/xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 3,
+ "padding": 12,
+ "children": [
+ {
+ "type": "text",
+ "id": "nVmyZ",
+ "name": "Quick Reference Label",
+ "fill": "$accent/accent",
+ "content": "GATEWAY",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.6
+ },
+ {
+ "type": "text",
+ "id": "ymqKR",
+ "name": "Quick Reference Line 1",
+ "fill": "$foreground/foreground",
+ "content": "01 Secrets first: server TLS, central client CA, agent client + CA",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "JWm24",
+ "name": "Quick Reference Line 2",
+ "fill": "$foreground/foreground",
+ "content": "02 Helm: api.enabled=false, gateway.enabled=true, clusterID, peerURI, NetworkPolicy CIDRs",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "Rzlo6",
+ "name": "Quick Reference Line 3",
+ "fill": "$foreground/foreground",
+ "content": "03 Central: labeled credential Secret + Cluster spec.agentGateway",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "GD8Xx",
+ "name": "Quick Reference Line 4",
+ "fill": "$foreground/foreground",
+ "content": "04 Verify a test server end to end; Cluster health is Kubernetes-only",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "dU3I4",
+ "name": "Article Next Link",
+ "width": "fill_container",
+ "stroke": "$border/border",
+ "strokeWidth": {
+ "top": 1
+ },
+ "strokeAlignment": "inner",
+ "strokeLinejoin": "round",
+ "padding": [
+ 10,
+ 0,
+ 0,
+ 0
+ ],
+ "justifyContent": "space_between",
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "frame",
+ "id": "Z5ZHl0",
+ "name": "Next Link Text",
+ "layout": "vertical",
+ "gap": 2,
+ "children": [
+ {
+ "type": "text",
+ "id": "Vk5Du",
+ "name": "Next Link Eyebrow",
+ "fill": "$field/placeholder",
+ "content": "NEXT GUIDE",
+ "fontFamily": "Geist",
+ "fontSize": 10,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "text",
+ "id": "s9jYyT",
+ "name": "Next Link Title",
+ "fill": "$foreground/foreground",
+ "content": "Users, authentication, and RBAC",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 13,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "icon",
+ "id": "Jq5Gc",
+ "name": "Next Link Icon",
+ "width": 15,
+ "height": 15,
+ "icon": "arrow-right",
+ "library": "lucide",
+ "fill": "$muted"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "iu5q0",
+ "name": "Docs Page Aside",
+ "width": 250,
+ "layout": "vertical",
+ "gap": 16,
+ "children": [
+ {
+ "type": "frame",
+ "id": "H4BGa",
+ "name": "Docs Table of Contents",
+ "width": "fill_container",
+ "fill": "$surface/secondary",
+ "cornerRadius": "$radius/2xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 10,
+ "padding": 16,
+ "children": [
+ {
+ "type": "text",
+ "id": "te7uU",
+ "name": "TOC Label",
+ "fill": "$field/placeholder",
+ "content": "ON THIS PAGE",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "tyZnG",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "AyYi6",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "URbLj",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "What the gateway carries",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "eqrHv",
+ "name": "TOC Row 0",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "fZU0m",
+ "name": "TOC Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ },
+ {
+ "type": "text",
+ "id": "fSxnl",
+ "name": "TOC Text 0",
+ "fill": "$foreground/foreground",
+ "content": "Deploy the gateway with Helm",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "500"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "q1p03z",
+ "name": "TOC Row 1",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "J0DD1k",
+ "name": "TOC Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "zN8Gg",
+ "name": "TOC Text 1",
+ "fill": "$muted",
+ "content": "Register the gateway centrally",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "yuicL",
+ "name": "TOC Row 2",
+ "width": "fill_container",
+ "gap": 8,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "b1ub5e",
+ "name": "TOC Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "hash",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "FpN27",
+ "name": "TOC Text 2",
+ "fill": "$muted",
+ "content": "Verify the installation",
+ "fontFamily": "Geist",
+ "fontSize": 12.5,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "rectangle",
+ "id": "LCZH6",
+ "name": "TOC Divider",
+ "fill": "$border/border",
+ "width": "fill_container",
+ "height": 1
+ },
+ {
+ "type": "text",
+ "id": "c0cu9",
+ "name": "Coverage Label",
+ "fill": "$field/placeholder",
+ "content": "COVERS",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 9,
+ "fontWeight": "600",
+ "letterSpacing": 0.7
+ },
+ {
+ "type": "frame",
+ "id": "Bzy8z",
+ "name": "Coverage Row 0",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "mOWpx",
+ "name": "Coverage Icon 0",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "RLO4Z",
+ "name": "Coverage Text 0",
+ "fill": "$muted",
+ "content": "Gateway",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "tzu9a",
+ "name": "Coverage Row 1",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "SHP2w",
+ "name": "Coverage Icon 1",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "M1h7f",
+ "name": "Coverage Text 1",
+ "fill": "$muted",
+ "content": "mTLS",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "D2Oyu",
+ "name": "Coverage Row 2",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "z9Dzh",
+ "name": "Coverage Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "pPDHq",
+ "name": "Coverage Text 2",
+ "fill": "$muted",
+ "content": "Cluster registration",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "Ivwze",
+ "name": "Coverage Row 2",
+ "width": "fill_container",
+ "gap": 7,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "icon",
+ "id": "Zauoi",
+ "name": "Coverage Icon 2",
+ "width": 12,
+ "height": 12,
+ "icon": "book-open",
+ "library": "lucide",
+ "fill": "$field/placeholder"
+ },
+ {
+ "type": "text",
+ "id": "GAfxZ",
+ "name": "Coverage Text 2",
+ "fill": "$muted",
+ "content": "Troubleshooting",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "normal"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "frame",
+ "id": "z7siBh",
+ "name": "Edit Page Card",
+ "width": "fill_container",
+ "fill": "$background/background",
+ "cornerRadius": "$radius/3xl",
+ "stroke": "$border/border",
+ "strokeWidth": 1,
+ "strokeAlignment": "inner",
+ "layout": "vertical",
+ "gap": 6,
+ "padding": 14,
+ "children": [
+ {
+ "type": "text",
+ "id": "nRoS1",
+ "name": "Edit Page Title",
+ "fill": "$foreground/foreground",
+ "content": "Improve this page",
+ "fontFamily": "JetBrains Mono",
+ "fontSize": 12,
+ "fontWeight": "600"
+ },
+ {
+ "type": "text",
+ "id": "TNTaK",
+ "name": "Edit Page Body",
+ "fill": "$muted",
+ "textGrowth": "fixed-width",
+ "width": "fill_container",
+ "content": "Found a gap? Open a docs issue or suggest an edit on GitHub.",
+ "lineHeight": 1.4,
+ "fontFamily": "Geist",
+ "fontSize": 11.5,
+ "fontWeight": "normal"
+ },
+ {
+ "type": "frame",
+ "id": "Uw52v",
+ "name": "Edit Page Link",
+ "gap": 6,
+ "alignItems": "center",
+ "children": [
+ {
+ "type": "text",
+ "id": "dg0IA",
+ "name": "Edit Page Link Text",
+ "fill": "$accent/soft-foreground",
+ "content": "Edit this page",
+ "fontFamily": "Geist",
+ "fontSize": 12,
+ "fontWeight": "500"
+ },
+ {
+ "type": "icon",
+ "id": "CkFKe",
+ "name": "Edit Page Link Icon",
+ "width": 12,
+ "height": 12,
+ "icon": "external-link",
+ "library": "lucide",
+ "fill": "$accent/accent"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
}
],
"themes": {