frontend: deep-link via ?instance=; client/frontend default to :9090; Makefile help; v1.1.0
- cmd/frontend/web: honour ?instance=<hostname> query parameter on the initial scope hydration so /view/?instance=lb-ams opens the dashboard scoped to that maglevd. The cookie is updated on consumption; an unknown name still falls back to the first server via App.tsx. - cmd/client, cmd/frontend: --server now accepts bare hostnames. A new internal/netutil.EnsurePort canonicalises addresses by appending :9090 when no port is given, with bracketing for bare IPv6 literals. Unit test covers the IPv4/IPv6/bracketed/already-ported permutations. - Makefile: new self-documenting `help` target as the default rule; every user-facing target now carries a `## ` description that the awk-based help auto-extracts. fixstyle-web skips with a friendly message when prettier isn't installed instead of failing on npx. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ FRONTEND_WEB_SRC := $(shell find cmd/frontend/web/src -type f 2>/dev/null) \
|
|||||||
FRONTEND_WEB_DIST := cmd/frontend/web/dist/index.html
|
FRONTEND_WEB_DIST := cmd/frontend/web/dist/index.html
|
||||||
|
|
||||||
NATIVE_ARCH := $(shell go env GOARCH)
|
NATIVE_ARCH := $(shell go env GOARCH)
|
||||||
VERSION := 1.0.2
|
VERSION := 1.1.0
|
||||||
COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||||
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
|
LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
|
||||||
@@ -54,25 +54,33 @@ GO_VERSION ?= 1.25.0
|
|||||||
# make install-deps GOLANGCI_LINT_VERSION=2.0.0
|
# make install-deps GOLANGCI_LINT_VERSION=2.0.0
|
||||||
GOLANGCI_LINT_VERSION ?= 1.64.0
|
GOLANGCI_LINT_VERSION ?= 1.64.0
|
||||||
|
|
||||||
.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb docker docker-push robot-test clean maglevd-frontend-web install-deps install-deps-apt install-deps-go install-deps-go-tools
|
.PHONY: help all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb docker docker-push robot-test clean maglevd-frontend-web install-deps install-deps-apt install-deps-go install-deps-go-tools
|
||||||
|
|
||||||
all: build
|
# help is the default target — running `make` with no arguments prints
|
||||||
|
# every target that carries a "## " comment after its colon. New targets
|
||||||
|
# are picked up automatically, so the only thing to do when adding one
|
||||||
|
# is to put a short description after `## `.
|
||||||
|
help: ## Show this help
|
||||||
|
@printf "Usage: make <target>\n\nTargets:\n"
|
||||||
|
@awk -F ':.*## ' '/^[A-Za-z][A-Za-z0-9_-]*:.*## / {printf " %-24s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
build: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
all: build ## Alias for build (native-arch binaries)
|
||||||
|
|
||||||
|
build: $(GEN_FILES) $(FRONTEND_WEB_DIST) ## Build all binaries for the host architecture
|
||||||
mkdir -p build/$(NATIVE_ARCH)
|
mkdir -p build/$(NATIVE_ARCH)
|
||||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/server/
|
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/server/
|
||||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevc ./cmd/client/
|
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevc ./cmd/client/
|
||||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd-frontend ./cmd/frontend/
|
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd-frontend ./cmd/frontend/
|
||||||
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevt ./cmd/tester/
|
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevt ./cmd/tester/
|
||||||
|
|
||||||
build-amd64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
build-amd64: $(GEN_FILES) $(FRONTEND_WEB_DIST) ## Cross-build all binaries for linux/amd64
|
||||||
mkdir -p build/amd64
|
mkdir -p build/amd64
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd ./cmd/server/
|
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd ./cmd/server/
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevc ./cmd/client/
|
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevc ./cmd/client/
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd-frontend ./cmd/frontend/
|
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd-frontend ./cmd/frontend/
|
||||||
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevt ./cmd/tester/
|
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevt ./cmd/tester/
|
||||||
|
|
||||||
build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST) ## Cross-build all binaries for linux/arm64
|
||||||
mkdir -p build/arm64
|
mkdir -p build/arm64
|
||||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd ./cmd/server/
|
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd ./cmd/server/
|
||||||
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevc ./cmd/client/
|
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevc ./cmd/client/
|
||||||
@@ -82,12 +90,12 @@ build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
|
|||||||
# maglevd-frontend-web rebuilds the SolidJS bundle. The Go binary embeds the
|
# maglevd-frontend-web rebuilds the SolidJS bundle. The Go binary embeds the
|
||||||
# resulting cmd/frontend/web/dist/ via //go:embed, so a `go build` after
|
# resulting cmd/frontend/web/dist/ via //go:embed, so a `go build` after
|
||||||
# this target picks up any asset changes automatically.
|
# this target picks up any asset changes automatically.
|
||||||
maglevd-frontend-web: $(FRONTEND_WEB_DIST)
|
maglevd-frontend-web: $(FRONTEND_WEB_DIST) ## Rebuild the embedded SolidJS bundle
|
||||||
|
|
||||||
$(FRONTEND_WEB_DIST): $(FRONTEND_WEB_SRC)
|
$(FRONTEND_WEB_DIST): $(FRONTEND_WEB_SRC)
|
||||||
cd cmd/frontend/web && npm install && npm run build
|
cd cmd/frontend/web && npm install && npm run build
|
||||||
|
|
||||||
pkg-deb: build-amd64 build-arm64
|
pkg-deb: build-amd64 build-arm64 ## Build .deb packages for amd64 and arm64
|
||||||
debian/build-deb.sh vpp-maglevd amd64 $(VERSION)
|
debian/build-deb.sh vpp-maglevd amd64 $(VERSION)
|
||||||
debian/build-deb.sh vpp-maglevd arm64 $(VERSION)
|
debian/build-deb.sh vpp-maglevd arm64 $(VERSION)
|
||||||
debian/build-deb.sh vpp-maglev amd64 $(VERSION)
|
debian/build-deb.sh vpp-maglev amd64 $(VERSION)
|
||||||
@@ -100,7 +108,7 @@ pkg-deb: build-amd64 build-arm64
|
|||||||
# for a true multi-arch manifest. Each image is tagged both :v$(VERSION)
|
# for a true multi-arch manifest. Each image is tagged both :v$(VERSION)
|
||||||
# and :latest in one build, so bumping VERSION is the only change
|
# and :latest in one build, so bumping VERSION is the only change
|
||||||
# needed to cut a new release — no hand-edited tag lists to forget.
|
# needed to cut a new release — no hand-edited tag lists to forget.
|
||||||
docker:
|
docker: ## Build container images for the host arch and load them locally
|
||||||
docker buildx build --load --target maglevd -t git.ipng.ch/ipng/vpp-maglevd:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd:latest .
|
docker buildx build --load --target maglevd -t git.ipng.ch/ipng/vpp-maglevd:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd:latest .
|
||||||
docker buildx build --load --target frontend -t git.ipng.ch/ipng/vpp-maglevd-frontend:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd-frontend:latest .
|
docker buildx build --load --target frontend -t git.ipng.ch/ipng/vpp-maglevd-frontend:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd-frontend:latest .
|
||||||
|
|
||||||
@@ -110,14 +118,14 @@ docker:
|
|||||||
# result into the local daemon, so push is the only way to
|
# result into the local daemon, so push is the only way to
|
||||||
# materialise the combined manifest. Assumes the caller is already
|
# materialise the combined manifest. Assumes the caller is already
|
||||||
# logged in to git.ipng.ch.
|
# logged in to git.ipng.ch.
|
||||||
docker-push:
|
docker-push: ## Build and push multi-arch container manifests to the registry
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 --push --target maglevd -t git.ipng.ch/ipng/vpp-maglevd:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd:latest .
|
docker buildx build --platform linux/amd64,linux/arm64 --push --target maglevd -t git.ipng.ch/ipng/vpp-maglevd:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd:latest .
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 --push --target frontend -t git.ipng.ch/ipng/vpp-maglevd-frontend:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd-frontend:latest .
|
docker buildx build --platform linux/amd64,linux/arm64 --push --target frontend -t git.ipng.ch/ipng/vpp-maglevd-frontend:v$(VERSION) -t git.ipng.ch/ipng/vpp-maglevd-frontend:latest .
|
||||||
|
|
||||||
test: $(GEN_FILES)
|
test: $(GEN_FILES) ## Run all Go unit tests
|
||||||
go test ./...
|
go test ./...
|
||||||
|
|
||||||
proto: $(GEN_FILES)
|
proto: $(GEN_FILES) ## Regenerate gRPC stubs from proto/maglev.proto
|
||||||
|
|
||||||
$(GEN_FILES): $(PROTO_FILE)
|
$(GEN_FILES): $(PROTO_FILE)
|
||||||
protoc \
|
protoc \
|
||||||
@@ -130,7 +138,7 @@ $(GEN_FILES): $(PROTO_FILE)
|
|||||||
# messages (e.g. lb_conf_get, lb_as_v2_dump) require a VPP build that has
|
# messages (e.g. lb_conf_get, lb_as_v2_dump) require a VPP build that has
|
||||||
# them. Override VPP_API_DIR on the command line to point at another tree:
|
# them. Override VPP_API_DIR on the command line to point at another tree:
|
||||||
# make vpp-binapi VPP_API_DIR=/path/to/share/vpp/api
|
# make vpp-binapi VPP_API_DIR=/path/to/share/vpp/api
|
||||||
vpp-binapi:
|
vpp-binapi: ## Regenerate VPP API Go bindings from a local VPP build (set VPP_API_DIR)
|
||||||
@command -v binapi-generator >/dev/null 2>&1 || { \
|
@command -v binapi-generator >/dev/null 2>&1 || { \
|
||||||
echo "installing binapi-generator..."; \
|
echo "installing binapi-generator..."; \
|
||||||
go install go.fd.io/govpp/cmd/binapi-generator@v0.12.0; \
|
go install go.fd.io/govpp/cmd/binapi-generator@v0.12.0; \
|
||||||
@@ -146,13 +154,17 @@ vpp-binapi:
|
|||||||
lb lb_types
|
lb lb_types
|
||||||
rm -f internal/vpp/binapi/lb/lb_rpc.ba.go
|
rm -f internal/vpp/binapi/lb/lb_rpc.ba.go
|
||||||
|
|
||||||
fixstyle: fixstyle-web
|
fixstyle: fixstyle-web ## Format Go (gofmt) and the SolidJS bundle (prettier)
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
|
|
||||||
fixstyle-web:
|
fixstyle-web: ## Run prettier over cmd/frontend/web (skip if dependencies are not installed)
|
||||||
cd cmd/frontend/web && npx prettier --write .
|
@if [ -x cmd/frontend/web/node_modules/.bin/prettier ]; then \
|
||||||
|
cd cmd/frontend/web && npx prettier --write .; \
|
||||||
|
else \
|
||||||
|
echo "fixstyle-web: cmd/frontend/web/node_modules/.bin/prettier not found — run 'cd cmd/frontend/web && npm install' first; skipping"; \
|
||||||
|
fi
|
||||||
|
|
||||||
lint:
|
lint: ## Run golangci-lint across the Go tree
|
||||||
golangci-lint run ./...
|
golangci-lint run ./...
|
||||||
|
|
||||||
# install-deps is an opt-in "set up a fresh developer box" target. Tested
|
# install-deps is an opt-in "set up a fresh developer box" target. Tested
|
||||||
@@ -171,14 +183,14 @@ lint:
|
|||||||
# to understand Go 1.25 syntax.
|
# to understand Go 1.25 syntax.
|
||||||
#
|
#
|
||||||
# Each sub-target is idempotent and safe to re-run.
|
# Each sub-target is idempotent and safe to re-run.
|
||||||
install-deps: install-deps-apt install-deps-go install-deps-go-tools
|
install-deps: install-deps-apt install-deps-go install-deps-go-tools ## Install every build dependency (apt + Go toolchain + Go tools)
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "==> All build dependencies installed."
|
@echo "==> All build dependencies installed."
|
||||||
@echo " Make sure these are on PATH:"
|
@echo " Make sure these are on PATH:"
|
||||||
@echo " /usr/local/go/bin (Go toolchain)"
|
@echo " /usr/local/go/bin (Go toolchain)"
|
||||||
@echo " \$$(go env GOPATH)/bin (protoc-gen-go, golangci-lint, ...)"
|
@echo " \$$(go env GOPATH)/bin (protoc-gen-go, golangci-lint, ...)"
|
||||||
|
|
||||||
install-deps-apt:
|
install-deps-apt: ## Install Debian-packaged build dependencies via apt
|
||||||
@set -eu; \
|
@set -eu; \
|
||||||
if [ "$$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo"; fi; \
|
if [ "$$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo"; fi; \
|
||||||
echo "==> Installing apt packages (nodejs, npm, protoc, git, make, dpkg-dev)"; \
|
echo "==> Installing apt packages (nodejs, npm, protoc, git, make, dpkg-dev)"; \
|
||||||
@@ -192,7 +204,7 @@ install-deps-apt:
|
|||||||
# tarball (https://go.dev/dl/) and extracts it to /usr/local/go, matching
|
# tarball (https://go.dev/dl/) and extracts it to /usr/local/go, matching
|
||||||
# the layout that go.dev recommends and that most Debian setups use for
|
# the layout that go.dev recommends and that most Debian setups use for
|
||||||
# "Go newer than apt provides".
|
# "Go newer than apt provides".
|
||||||
install-deps-go:
|
install-deps-go: ## Ensure a recent enough Go toolchain is installed (downloads from go.dev if missing)
|
||||||
@set -eu; \
|
@set -eu; \
|
||||||
if [ "$$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo"; fi; \
|
if [ "$$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo"; fi; \
|
||||||
echo "==> Checking Go toolchain (required: $(GO_VERSION)+)"; \
|
echo "==> Checking Go toolchain (required: $(GO_VERSION)+)"; \
|
||||||
@@ -232,7 +244,7 @@ install-deps-go:
|
|||||||
# in $GOPATH/bin from a previous dev session doesn't silently get used
|
# in $GOPATH/bin from a previous dev session doesn't silently get used
|
||||||
# against Go 1.25 code it can't parse. Run `make install-deps
|
# against Go 1.25 code it can't parse. Run `make install-deps
|
||||||
# GOLANGCI_LINT_VERSION=2.0.0` if you want to enforce a tighter floor.
|
# GOLANGCI_LINT_VERSION=2.0.0` if you want to enforce a tighter floor.
|
||||||
install-deps-go-tools:
|
install-deps-go-tools: ## Install protoc-gen-go, protoc-gen-go-grpc, and golangci-lint
|
||||||
@set -eu; \
|
@set -eu; \
|
||||||
if ! command -v go >/dev/null 2>&1; then \
|
if ! command -v go >/dev/null 2>&1; then \
|
||||||
export PATH="/usr/local/go/bin:$$PATH"; \
|
export PATH="/usr/local/go/bin:$$PATH"; \
|
||||||
@@ -269,9 +281,9 @@ tests/.venv: tests/requirements.txt
|
|||||||
python3 -m venv tests/.venv
|
python3 -m venv tests/.venv
|
||||||
tests/.venv/bin/pip install -q -r tests/requirements.txt
|
tests/.venv/bin/pip install -q -r tests/requirements.txt
|
||||||
|
|
||||||
robot-test: build tests/.venv
|
robot-test: build tests/.venv ## Run the Robot Framework integration tests in Docker
|
||||||
tests/rf-run.sh docker $(TEST)
|
tests/rf-run.sh docker $(TEST)
|
||||||
|
|
||||||
clean:
|
clean: ## Remove build/ and generated proto stubs
|
||||||
rm -rf build/
|
rm -rf build/
|
||||||
rm -f $(GEN_FILES)
|
rm -f $(GEN_FILES)
|
||||||
|
|||||||
+10
-2
@@ -14,8 +14,15 @@ import (
|
|||||||
|
|
||||||
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||||
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
"git.ipng.ch/ipng/vpp-maglev/internal/grpcapi"
|
||||||
|
"git.ipng.ch/ipng/vpp-maglev/internal/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// defaultGRPCPort is the maglevd gRPC port (mirrors the server's
|
||||||
|
// -grpc-addr default in cmd/server/main.go). Used when -server is given
|
||||||
|
// without an explicit ":<port>" so operators can type "--server chbtl2"
|
||||||
|
// instead of "--server chbtl2:9090".
|
||||||
|
const defaultGRPCPort = "9090"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", formatError(err))
|
fmt.Fprintf(os.Stderr, "%s\n", formatError(err))
|
||||||
@@ -49,10 +56,11 @@ func run() error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
conn, err := grpc.NewClient(*serverAddr,
|
addr := netutil.EnsurePort(*serverAddr, defaultGRPCPort)
|
||||||
|
conn, err := grpc.NewClient(addr,
|
||||||
grpc.WithTransportCredentials(insecure.NewCredentials()))
|
grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("connect %s: %w", *serverAddr, err)
|
return fmt.Errorf("connect %s: %w", addr, err)
|
||||||
}
|
}
|
||||||
defer func() { _ = conn.Close() }()
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd"
|
||||||
|
"git.ipng.ch/ipng/vpp-maglev/internal/netutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// defaultGRPCPort is the maglevd gRPC port. Lets operators write
|
||||||
|
// "--server chbtl2" or MAGLEV_FRONTEND_SERVERS=chbtl2,chbtl3 without
|
||||||
|
// the redundant ":9090" suffix on every entry.
|
||||||
|
const defaultGRPCPort = "9090"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
slog.Error("startup-fatal", "err", err)
|
slog.Error("startup-fatal", "err", err)
|
||||||
@@ -133,7 +139,7 @@ func parseServers(s string) []string {
|
|||||||
var out []string
|
var out []string
|
||||||
for _, part := range strings.Split(s, ",") {
|
for _, part := range strings.Split(s, ",") {
|
||||||
if p := strings.TrimSpace(part); p != "" {
|
if p := strings.TrimSpace(part); p != "" {
|
||||||
out = append(out, p)
|
out = append(out, netutil.EnsurePort(p, defaultGRPCPort))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/x-icon" href="/view/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/view/favicon.ico" />
|
||||||
<title>maglev</title>
|
<title>maglev</title>
|
||||||
<script type="module" crossorigin src="/view/assets/index-3m4Pjc8_.js"></script>
|
<script type="module" crossorigin src="/view/assets/index-AJWk_JCf.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/view/assets/index-3BvNJ7QB.css">
|
<link rel="stylesheet" crossorigin href="/view/assets/index-3BvNJ7QB.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -37,7 +37,27 @@ function writeCookie(name: string | undefined) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [scope, setScopeRaw] = createSignal<string | undefined>(readCookie());
|
// readInitialScope honours a `?instance=<hostname>` deep-link query
|
||||||
|
// parameter ahead of the cookie, so a shared URL like /view/?instance=lb-ams
|
||||||
|
// opens the dashboard scoped to that maglevd. The named instance is also
|
||||||
|
// written back to the cookie so a subsequent reload without the param
|
||||||
|
// keeps the same selection. App.tsx still validates this against the
|
||||||
|
// fetched snapshot list, so a stale or unknown name falls back to the
|
||||||
|
// first server rather than leaving the SPA pinned to a ghost.
|
||||||
|
function readInitialScope(): string | undefined {
|
||||||
|
try {
|
||||||
|
const fromURL = new URLSearchParams(window.location.search).get("instance");
|
||||||
|
if (fromURL) {
|
||||||
|
writeCookie(fromURL);
|
||||||
|
return fromURL;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// window.location is unavailable in non-browser contexts.
|
||||||
|
}
|
||||||
|
return readCookie();
|
||||||
|
}
|
||||||
|
|
||||||
|
const [scope, setScopeRaw] = createSignal<string | undefined>(readInitialScope());
|
||||||
|
|
||||||
// setScope wraps the raw signal setter so every selection change writes
|
// setScope wraps the raw signal setter so every selection change writes
|
||||||
// back to the cookie. Callers use this exactly like the old setScope —
|
// back to the cookie. Callers use this exactly like the old setScope —
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// Package netutil holds tiny networking helpers shared across cmd/
|
||||||
|
// binaries. Kept deliberately minimal — anything more involved than a
|
||||||
|
// few lines belongs in its own package.
|
||||||
|
package netutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsurePort returns addr unchanged when it already carries a port, or
|
||||||
|
// addr with ":<defaultPort>" appended otherwise. Bare IPv6 literals
|
||||||
|
// ("2001:db8::1", "::1") are bracketed so the result is valid input to
|
||||||
|
// grpc.NewClient and net.Dial. An empty addr is returned unchanged.
|
||||||
|
func EnsurePort(addr, defaultPort string) string {
|
||||||
|
if addr == "" {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
if _, _, err := net.SplitHostPort(addr); err == nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
// Already-bracketed bare IPv6 ("[::1]") falls through to the plain
|
||||||
|
// concat at the bottom. Unbracketed IPv6 needs brackets first; we
|
||||||
|
// detect it by handing the literal to net.ParseIP, which only
|
||||||
|
// accepts the bare form (no brackets, no port).
|
||||||
|
if ip := net.ParseIP(addr); ip != nil && ip.To4() == nil && strings.Contains(addr, ":") {
|
||||||
|
return "[" + addr + "]:" + defaultPort
|
||||||
|
}
|
||||||
|
return addr + ":" + defaultPort
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package netutil
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEnsurePort(t *testing.T) {
|
||||||
|
const dp = "9090"
|
||||||
|
cases := []struct {
|
||||||
|
in, want string
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"chbtl2", "chbtl2:9090"},
|
||||||
|
{"chbtl2:9090", "chbtl2:9090"},
|
||||||
|
{"chbtl2:1234", "chbtl2:1234"},
|
||||||
|
{"lb-ams.example.com", "lb-ams.example.com:9090"},
|
||||||
|
{"lb-ams.example.com:9090", "lb-ams.example.com:9090"},
|
||||||
|
{"192.0.2.1", "192.0.2.1:9090"},
|
||||||
|
{"192.0.2.1:9090", "192.0.2.1:9090"},
|
||||||
|
{"::1", "[::1]:9090"},
|
||||||
|
{"2001:db8::1", "[2001:db8::1]:9090"},
|
||||||
|
{"[::1]:9090", "[::1]:9090"},
|
||||||
|
{"[2001:db8::1]:443", "[2001:db8::1]:443"},
|
||||||
|
// Already-bracketed but missing port: net.SplitHostPort rejects
|
||||||
|
// it, ParseIP rejects the bracketed textual form, so the plain
|
||||||
|
// concat path adds ":9090" and yields a valid host:port.
|
||||||
|
{"[::1]", "[::1]:9090"},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
got := EnsurePort(tc.in, dp)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("EnsurePort(%q) = %q, want %q", tc.in, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user