From fb61e72e06cb4fa3fc86d719ddc4877fa3d9b8d3 Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Fri, 1 May 2026 10:27:16 +0200 Subject: [PATCH] frontend: deep-link via ?instance=; client/frontend default to :9090; Makefile help; v1.1.0 - cmd/frontend/web: honour ?instance= 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) --- Makefile | 58 +++++++++++-------- cmd/client/main.go | 12 +++- cmd/frontend/main.go | 8 ++- .../web/dist/assets/index-3m4Pjc8_.js | 1 - .../web/dist/assets/index-AJWk_JCf.js | 1 + cmd/frontend/web/dist/index.html | 2 +- cmd/frontend/web/src/stores/scope.ts | 22 ++++++- internal/netutil/netutil.go | 32 ++++++++++ internal/netutil/netutil_test.go | 35 +++++++++++ 9 files changed, 142 insertions(+), 29 deletions(-) delete mode 100644 cmd/frontend/web/dist/assets/index-3m4Pjc8_.js create mode 100644 cmd/frontend/web/dist/assets/index-AJWk_JCf.js create mode 100644 internal/netutil/netutil.go create mode 100644 internal/netutil/netutil_test.go diff --git a/Makefile b/Makefile index 1548b2d..6b8b3b3 100644 --- a/Makefile +++ b/Makefile @@ -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 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) DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \ @@ -54,25 +54,33 @@ GO_VERSION ?= 1.25.0 # make install-deps GOLANGCI_LINT_VERSION=2.0.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 \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) 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)/maglevd-frontend ./cmd/frontend/ 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 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/maglevd-frontend ./cmd/frontend/ 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 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/ @@ -82,12 +90,12 @@ build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST) # 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 # 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) 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 arm64 $(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) # 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. -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 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 # materialise the combined manifest. Assumes the caller is already # 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 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 ./... -proto: $(GEN_FILES) +proto: $(GEN_FILES) ## Regenerate gRPC stubs from proto/maglev.proto $(GEN_FILES): $(PROTO_FILE) 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 # 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 -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 || { \ echo "installing binapi-generator..."; \ go install go.fd.io/govpp/cmd/binapi-generator@v0.12.0; \ @@ -146,13 +154,17 @@ vpp-binapi: lb lb_types 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 . -fixstyle-web: - cd cmd/frontend/web && npx prettier --write . +fixstyle-web: ## Run prettier over cmd/frontend/web (skip if dependencies are not installed) + @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 ./... # 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. # # 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 "==> All build dependencies installed." @echo " Make sure these are on PATH:" @echo " /usr/local/go/bin (Go toolchain)" @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; \ if [ "$$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo"; fi; \ 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 # the layout that go.dev recommends and that most Debian setups use for # "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; \ if [ "$$(id -u)" = 0 ]; then SUDO=""; else SUDO="sudo"; fi; \ 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 # 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. -install-deps-go-tools: +install-deps-go-tools: ## Install protoc-gen-go, protoc-gen-go-grpc, and golangci-lint @set -eu; \ if ! command -v go >/dev/null 2>&1; then \ export PATH="/usr/local/go/bin:$$PATH"; \ @@ -269,9 +281,9 @@ tests/.venv: tests/requirements.txt python3 -m venv tests/.venv 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) -clean: +clean: ## Remove build/ and generated proto stubs rm -rf build/ rm -f $(GEN_FILES) diff --git a/cmd/client/main.go b/cmd/client/main.go index 216b0ca..79bb84a 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -14,8 +14,15 @@ import ( buildinfo "git.ipng.ch/ipng/vpp-maglev/cmd" "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 ":" so operators can type "--server chbtl2" +// instead of "--server chbtl2:9090". +const defaultGRPCPort = "9090" + func main() { if err := run(); err != nil { 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())) if err != nil { - return fmt.Errorf("connect %s: %w", *serverAddr, err) + return fmt.Errorf("connect %s: %w", addr, err) } defer func() { _ = conn.Close() }() diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index f76bfa9..71523d3 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -16,8 +16,14 @@ import ( "time" 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() { if err := run(); err != nil { slog.Error("startup-fatal", "err", err) @@ -133,7 +139,7 @@ func parseServers(s string) []string { var out []string for _, part := range strings.Split(s, ",") { if p := strings.TrimSpace(part); p != "" { - out = append(out, p) + out = append(out, netutil.EnsurePort(p, defaultGRPCPort)) } } return out diff --git a/cmd/frontend/web/dist/assets/index-3m4Pjc8_.js b/cmd/frontend/web/dist/assets/index-3m4Pjc8_.js deleted file mode 100644 index 7f1bfe1..0000000 --- a/cmd/frontend/web/dist/assets/index-3m4Pjc8_.js +++ /dev/null @@ -1 +0,0 @@ -(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(s){if(s.ep)return;s.ep=!0;const i=n(s);fetch(s.href,i)}})();const gt=!1,ht=(e,t)=>e===t,G=Symbol("solid-proxy"),Ce=Symbol("solid-track"),de={equals:ht};let Ke=Ye;const V=1,ge=2,Ge={owned:null,cleanups:null,context:null,owner:null};var k=null;let Ee=null,bt=null,_=null,O=null,R=null,_e=0;function ie(e,t){const n=_,r=k,s=e.length===0,i=t===void 0?r:t,o=s?Ge:{owned:null,cleanups:null,context:i?i.context:null,owner:i},l=s?e:()=>e(()=>U(()=>oe(o)));k=o,_=null;try{return Y(l,!0)}finally{_=n,k=r}}function A(e,t){t=t?Object.assign({},de,t):de;const n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},r=s=>(typeof s=="function"&&(s=s(n.value)),Xe(n,s));return[qe.bind(n),r]}function E(e,t,n){const r=Le(e,t,!1,V);ce(r)}function K(e,t,n){Ke=kt;const r=Le(e,t,!1,V);(!n||!n.render)&&(r.user=!0),R?R.push(r):ce(r)}function B(e,t,n){n=n?Object.assign({},de,n):de;const r=Le(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,ce(r),qe.bind(r)}function mt(e){return Y(e,!1)}function U(e){if(_===null)return e();const t=_;_=null;try{return e()}finally{_=t}}function pt(e,t,n){const r=Array.isArray(e);let s,i=n&&n.defer;return o=>{let l;if(r){l=Array(e.length);for(let c=0;ct(l,s,o));return s=l,a}}function $t(e){K(()=>U(e))}function q(e){return k===null||(k.cleanups===null?k.cleanups=[e]:k.cleanups.push(e)),e}function Oe(){return _}function vt(){return k}function wt(e,t){const n=k,r=_;k=e,_=null;try{return Y(t,!0)}catch(s){Ne(s)}finally{k=n,_=r}}function qe(){if(this.sources&&this.state)if(this.state===V)ce(this);else{const e=O;O=null,Y(()=>be(this),!1),O=e}if(_){const e=this.observers?this.observers.length:0;_.sources?(_.sources.push(this),_.sourceSlots.push(e)):(_.sources=[this],_.sourceSlots=[e]),this.observers?(this.observers.push(_),this.observerSlots.push(_.sources.length-1)):(this.observers=[_],this.observerSlots=[_.sources.length-1])}return this.value}function Xe(e,t,n){let r=e.value;return(!e.comparator||!e.comparator(r,t))&&(e.value=t,e.observers&&e.observers.length&&Y(()=>{for(let s=0;s1e6)throw O=[],new Error},!1)),t}function ce(e){if(!e.fn)return;oe(e);const t=_e;yt(e,e.value,t)}function yt(e,t,n){let r;const s=k,i=_;_=k=e;try{r=e.fn(t)}catch(o){return e.pure&&(e.state=V,e.owned&&e.owned.forEach(oe),e.owned=null),e.updatedAt=n+1,Ne(o)}finally{_=i,k=s}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&"observers"in e?Xe(e,r):e.value=r,e.updatedAt=n)}function Le(e,t,n,r=V,s){const i={fn:e,state:r,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:k,context:k?k.context:null,pure:n};return k===null||k!==Ge&&(k.owned?k.owned.push(i):k.owned=[i]),i}function he(e){if(e.state===0)return;if(e.state===ge)return be(e);if(e.suspense&&U(e.suspense.inFallback))return e.suspense.effects.push(e);const t=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt<_e);)e.state&&t.push(e);for(let n=t.length-1;n>=0;n--)if(e=t[n],e.state===V)ce(e);else if(e.state===ge){const r=O;O=null,Y(()=>be(e,t[0]),!1),O=r}}function Y(e,t){if(O)return e();let n=!1;t||(O=[]),R?n=!0:R=[],_e++;try{const r=e();return _t(n),r}catch(r){n||(R=null),O=null,Ne(r)}}function _t(e){if(O&&(Ye(O),O=null),e)return;const t=R;R=null,t.length&&Y(()=>Ke(t),!1)}function Ye(e){for(let t=0;t=0;t--)oe(e.tOwned[t]);delete e.tOwned}if(e.owned){for(t=e.owned.length-1;t>=0;t--)oe(e.owned[t]);e.owned=null}if(e.cleanups){for(t=e.cleanups.length-1;t>=0;t--)e.cleanups[t]();e.cleanups=null}e.state=0}function St(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function Ne(e,t=k){throw St(e)}const xt=Symbol("fallback");function Ue(e){for(let t=0;t1?[]:null;return q(()=>Ue(i)),()=>{let a=e()||[],c=a.length,u,f;return a[Ce],U(()=>{let g,w,v,T,C,p,m,y,S;if(c===0)o!==0&&(Ue(i),i=[],r=[],s=[],o=0,l&&(l=[])),n.fallback&&(r=[xt],s[0]=ie(x=>(i[0]=x,n.fallback())),o=1);else if(o===0){for(s=new Array(c),f=0;f=p&&y>=p&&r[m]===a[y];m--,y--)v[y]=s[m],T[y]=i[m],l&&(C[y]=l[m]);for(g=new Map,w=new Array(y+1),f=y;f>=p;f--)S=a[f],u=g.get(S),w[f]=u===void 0?-1:u,g.set(S,f);for(u=p;u<=m;u++)S=r[u],f=g.get(S),f!==void 0&&f!==-1?(v[f]=s[u],T[f]=i[u],l&&(C[f]=l[u]),f=w[f],g.set(S,f)):i[u]();for(f=p;fe(t||{}))}const Et=e=>`Stale read from <${e}>.`;function Q(e){const t="fallback"in e&&{fallback:()=>e.fallback};return B(At(()=>e.each,e.children,t||void 0))}function L(e){const t=e.keyed,n=B(()=>e.when,void 0,void 0),r=t?n:B(n,void 0,{equals:(s,i)=>!s==!i});return B(()=>{const s=r();if(s){const i=e.children;return typeof i=="function"&&i.length>0?U(()=>i(t?s:()=>{if(!U(r))throw Et("Show");return n()})):i}return e.fallback},void 0,void 0)}const I=e=>B(()=>e());function Ct(e,t,n){let r=n.length,s=t.length,i=r,o=0,l=0,a=t[s-1].nextSibling,c=null;for(;ou-l){const w=t[o];for(;l{s=i,t===document?e():d(t,e(),t.firstChild?null:void 0,n)},r.owner),()=>{s(),t.textContent=""}}function b(e,t,n,r){let s;const i=()=>{const l=document.createElement("template");return l.innerHTML=e,l.content.firstChild},o=()=>(s||(s=i())).cloneNode(!0);return o.cloneNode=o,o}function ke(e,t=window.document){const n=t[We]||(t[We]=new Set);for(let r=0,s=e.length;re(t,n))}function d(e,t,n,r){if(n!==void 0&&!r&&(r=[]),typeof t!="function")return me(e,t,r,n);E(s=>me(e,t(),s,n),r)}function Lt(e){let t=e.target;const n=`$$${e.type}`,r=e.target,s=e.currentTarget,i=a=>Object.defineProperty(e,"target",{configurable:!0,value:a}),o=()=>{const a=t[n];if(a&&!t.disabled){const c=t[`${n}Data`];if(c!==void 0?a.call(t,c,e):a.call(t,e),e.cancelBubble)return}return t.host&&typeof t.host!="string"&&!t.host._$host&&t.contains(e.target)&&i(t.host),!0},l=()=>{for(;o()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return t||document}}),e.composedPath){const a=e.composedPath();i(a[0]);for(let c=0;c{let l=t();for(;typeof l=="function";)l=l();n=me(e,l,n,r)}),()=>n;if(Array.isArray(t)){const l=[],a=n&&Array.isArray(n);if(Te(l,t,n,s))return E(()=>n=me(e,l,n,r,!0)),()=>n;if(l.length===0){if(n=J(e,n,r),o)return n}else a?n.length===0?He(e,l,r):Ct(e,n,l):(n&&J(e),He(e,l));n=l}else if(t.nodeType){if(Array.isArray(n)){if(o)return n=J(e,n,r,t);J(e,n,null,t)}else n==null||n===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}}return n}function Te(e,t,n,r){let s=!1;for(let i=0,o=t.length;i=0;o--){const l=t[o];if(s!==l){const a=l.parentNode===e;!i&&!o?a?e.replaceChild(s,l):e.insertBefore(s,n):a&&l.remove()}else i=!0}}else e.insertBefore(s,n);return[s]}const Nt="http://www.w3.org/2000/svg";function It(e,t=!1,n=void 0){return t?document.createElementNS(Nt,e):document.createElement(e,{is:n})}function Mt(e){const{useShadow:t}=e,n=document.createTextNode(""),r=()=>e.mount||document.body,s=vt();let i;return K(()=>{i||(i=wt(s,()=>B(()=>e.children)));const o=r();if(o instanceof HTMLHeadElement){const[l,a]=A(!1),c=()=>a(!0);ie(u=>d(o,()=>l()?u():i(),null)),q(c)}else{const l=It(e.isSVG?"g":"div",e.isSVG),a=t&&l.attachShadow?l.attachShadow({mode:"open"}):l;Object.defineProperty(l,"_$host",{get(){return n.parentNode},configurable:!0}),d(a,i),o.appendChild(l),e.ref&&e.ref(l),q(()=>o.removeChild(l))}},void 0,{render:!0}),n}async function Ze(e){const t=await fetch(e,{credentials:"same-origin"});if(!t.ok)throw new Error(`${e}: ${t.status} ${t.statusText}`);return await t.json()}function ze(){return Ze("/view/api/state")}function jt(){return Ze("/view/api/version")}const pe=Symbol("store-raw"),z=Symbol("store-node"),D=Symbol("store-has"),Qe=Symbol("store-self");function et(e){let t=e[G];if(!t&&(Object.defineProperty(e,G,{value:t=new Proxy(e,Bt)}),!Array.isArray(e))){const n=Object.keys(e),r=Object.getOwnPropertyDescriptors(e);for(let s=0,i=n.length;se[G][t]),n}function tt(e){Oe()&&ae($e(e,z),Qe)()}function Rt(e){return tt(e),Reflect.ownKeys(e)}const Bt={get(e,t,n){if(t===pe)return e;if(t===G)return n;if(t===Ce)return tt(e),n;const r=$e(e,z),s=r[t];let i=s?s():e[t];if(t===z||t===D||t==="__proto__")return i;if(!s){const o=Object.getOwnPropertyDescriptor(e,t);Oe()&&(typeof i!="function"||e.hasOwnProperty(t))&&!(o&&o.get)&&(i=ae(r,t,i)())}return ee(i)?et(i):i},has(e,t){return t===pe||t===G||t===Ce||t===z||t===D||t==="__proto__"?!0:(Oe()&&ae($e(e,D),t)(),t in e)},set(){return!0},deleteProperty(){return!0},ownKeys:Rt,getOwnPropertyDescriptor:Dt};function ne(e,t,n,r=!1){if(!r&&e[t]===n)return;const s=e[t],i=e.length;n===void 0?(delete e[t],e[D]&&e[D][t]&&s!==void 0&&e[D][t].$()):(e[t]=n,e[D]&&e[D][t]&&s===void 0&&e[D][t].$());let o=$e(e,z),l;if((l=ae(o,t,s))&&l.$(()=>n),Array.isArray(e)&&e.length!==i){for(let a=e.length;a1){r=t.shift();const o=typeof r,l=Array.isArray(e);if(Array.isArray(r)){for(let a=0;a1){se(e[r],t,[r].concat(n));return}s=e[r],n=[r].concat(n)}let i=t[0];typeof i=="function"&&(i=i(s,n),i===s)||r===void 0&&i==null||(i=te(i),r===void 0||ee(s)&&ee(i)&&!Array.isArray(i)?nt(s,i):ne(e,r,i))}function Wt(...[e,t]){const n=te(e||{}),r=Array.isArray(n),s=et(n);function i(...o){mt(()=>{r&&o.length===1?Ut(n,o[0]):se(n,o)})}return[s,i]}const ve=new WeakMap,rt={get(e,t){if(t===pe)return e;const n=e[t];let r;return ee(n)?ve.get(n)||(ve.set(n,r=new Proxy(n,rt)),r):n},set(e,t,n){return ne(e,t,te(n)),!0},deleteProperty(e,t){return ne(e,t,void 0,!0),!0}};function F(e){return t=>{if(ee(t)){let n;(n=ve.get(t))||ve.set(t,n=new Proxy(t,rt)),e(n)}return t}}const[Ht,Ft]=A(0);setInterval(()=>Ft(e=>e+1),5e3);function Ie(e){const t={};for(const n of e.backends)t[n.name]=n.state;for(const n of e.frontends){let r=0;for(let a=0;a0){c=!0;break}if(c){r=a;break}}let s=!1,i=!1,o=!0;const l=new Set;for(let a=0;a0&&(s=!0),l.has(c.name)||(l.add(c.name),i=!0,u!=="unknown"&&(o=!1))}!i||o?n.state="unknown":s?n.state="up":n.state="down"}}const[X,W]=Wt({byName:{},settling:{}}),Vt=2e3,le=new Map;function Kt(e,t){return`${e}\0${t}`}function st(e,t){W(F(s=>{s.settling[e]||(s.settling[e]={}),s.settling[e][t]=!0}));const n=Kt(e,t),r=le.get(n);r&&clearTimeout(r),le.set(n,setTimeout(()=>{le.delete(n),W(F(s=>{s.settling[e]&&delete s.settling[e][t]}))},Vt))}function Gt(e){for(const[t,n]of le)t.startsWith(e+"\0")&&(clearTimeout(n),le.delete(t));W(F(t=>{t.settling[e]&&(t.settling[e]={})}))}function it(e){const t={};for(const n of e)Ie(n),t[n.maglevd.name]=n;W({byName:t})}function qt(e,t){W(F(r=>{const s=r.byName[e];if(!s)return;const i=s.backends.find(o=>o.name===t.backend);i&&(i.state=t.transition.to,i.enabled=t.transition.to!=="disabled",i.last_transition=t.transition,i.transitions||(i.transitions=[]),i.transitions.push(t.transition),i.transitions.length>20&&(i.transitions=i.transitions.slice(i.transitions.length-20)),Ie(s))}));const n=X.byName[e];if(n)for(const r of n.frontends)r.pools.some(s=>s.backends.some(i=>i.name===t.backend))&&st(e,r.name)}function Xt(e,t){W(F(n=>{const r=n.byName[e];if(!r)return;const s=t.per_frontend;if(!s||Object.keys(s).length===0){r.lb_state!==void 0&&(r.lb_state=void 0);return}r.lb_state||(r.lb_state={per_frontend:{}});const o=r.lb_state.per_frontend;for(const l of Object.keys(s)){o[l]||(o[l]={});const a=o[l],c=s[l];for(const u of Object.keys(c))a[u]!==c[u]&&(a[u]=c[u]);for(const u of Object.keys(a))u in c||delete a[u]}for(const l of Object.keys(o))l in s||delete o[l]})),Gt(e)}function Yt(e,t,n){return e?.lb_state?.per_frontend?.[t]?.[n]}function Jt(e,t){W(F(n=>{const r=n.byName[e];r&&(r.vpp_state=t)}))}function Zt(e,t){W(F(n=>{const r=n.byName[e];r&&(r.maglevd.connected=t.connected,r.maglevd.last_error=t.last_error)}))}function zt(e,t,n,r,s){W(F(i=>{const o=i.byName[e];if(!o)return;const l=o.frontends.find(u=>u.name===t);if(!l)return;const a=l.pools.find(u=>u.name===n);if(!a)return;const c=a.backends.find(u=>u.name===r);c&&(c.weight=s,Ie(o))})),st(e,t)}function Qt(e,t){const n={};for(const f of e.backends)n[f.name]=f.state;const r=!!e.lb_state,s=e.lb_state?.per_frontend?.[t.name],i=!!X.settling[e.maglevd.name]?.[t.name];let o=!1,l=!1;for(const f of t.pools)for(const $ of f.backends)if(n[$.name]!=="up"&&(o=!0),!i&&r&&$.effective_weight>0){const g=s?.[$.name];(g===void 0||g===0)&&(l=!0)}const a=t.pools[0],c=!!a&&a.backends.some(f=>f.weight>0),u=!a||a.backends.every(f=>f.effective_weight===0);return!o&&c&&!l?"ok":l?"bug-buckets":u?"primary-drained":o?"degraded":"unknown"}function en(e,t){switch(Qt(e,t)){case"ok":return"✅";case"bug-buckets":return"‼️";case"primary-drained":return"❗";case"degraded":return"⚠️";case"unknown":return"❓"}}function tn(e,t){return e.includes(":")?`[${e}]:${t}`:`${e}:${t}`}function nn(e){if(Ht(),!e||!e.at_unix_ns||e.at_unix_ns<=0)return"";const t=Date.now()-e.at_unix_ns/1e6,n=Math.floor(t/1e3);if(n<=1)return"now";const r=n%60,s=Math.floor(n/60);if(s<1)return`${n}s ago`;const i=s%60,o=Math.floor(s/60);if(o<1)return`${i}m${r}s ago`;const l=o%24,a=Math.floor(o/24);return a<1?`${o}h${i}m ago`:`${a}d${l}h ago`}const Fe=500,[we,rn]=A([]);function sn(e){rn(t=>{const n=[...t,e];return n.length>Fe?n.slice(n.length-Fe):n})}const ln=1e4,on=3e4;function an(){let e,t=!1;const n=l=>{try{const a=JSON.parse(l.data);cn(a)}catch(a){console.error("sse parse error",a,l.data)}},r=async()=>{try{const l=await ze();it(l)}catch(l){console.error("resync refetch failed",l)}},s=()=>{e&&(e.close(),e=void 0),e=new EventSource("/view/api/events"),e.onmessage=n,e.addEventListener("resync",r),e.onerror=l=>{console.debug("sse error, browser will reconnect",l)}},i=l=>{t||(t=!0,console.info("sse reconnecting:",l),s(),setTimeout(()=>{t=!1},1e3))};let o=Date.now();setInterval(()=>{const l=Date.now(),a=l-o;o=l,a>on&&i(`wake detected (${Math.round(a/1e3)}s gap)`)},ln),s()}function cn(e){switch(sn(e),e.type){case"backend":qt(e.maglevd,e.payload);break;case"frontend":e.maglevd,e.payload;break;case"maglevd-status":Zt(e.maglevd,e.payload);break;case"vpp-status":Jt(e.maglevd,e.payload.state);break;case"lb-state":Xt(e.maglevd,e.payload);break}}const ye="maglev_scope",un=60*60*24*365;function fn(){try{const e=document.cookie.split("; ").find(n=>n.startsWith(ye+"="));return e&&decodeURIComponent(e.slice(ye.length+1))||void 0}catch{return}}function dn(e){try{if(!e){document.cookie=`${ye}=; Path=/; Max-Age=0; SameSite=Lax`;return}const t=encodeURIComponent(e);document.cookie=`${ye}=${t}; Path=/; Max-Age=${un}; SameSite=Lax`}catch{}}const[xe,gn]=A(fn());function lt(e){gn(e),dn(e)}var hn=b("