From 35643fd774aeae9d432d9fbbb8dc6bfb33460d8a Mon Sep 17 00:00:00 2001 From: Pim van Pelt Date: Mon, 13 Apr 2026 00:13:47 +0200 Subject: [PATCH] =?UTF-8?q?Rename=20maglev-frontend=20=E2=86=92=20maglevd-?= =?UTF-8?q?frontend;=20v0.9.1;=20API=20RX/TX=20pulse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the web dashboard binary to maglevd-frontend and move it to /usr/sbin (it's a daemon and belongs with maglevd). The systemd unit name stays vpp-maglev-frontend.service since that prefix is the package name. Manpage, README, user-guide, and debian packaging all updated in lockstep; bump to 0.9.1 for the first real release. All frontend env vars are now prefixed MAGLEV_FRONTEND_ so a single /etc/default/vpp-maglev can be shared with maglevd without collisions. Every flag has an env equivalent for Docker use. MAGLEV_FRONTEND_USER and MAGLEV_FRONTEND_PASSWORD still gate the /admin surface. VPPInfoPanel now pulses "API: ↑↓" indicators in the zippy title whenever a vpp-api-send / vpp-api-recv log event arrives on the SSE stream for the scoped maglevd — 250ms blue flash, re-triggerable, with the two arrows tightly kerned via negative letter-spacing. --- Makefile | 16 +-- README.md | 81 ++++++++---- cmd/frontend/handlers.go | 2 +- cmd/frontend/main.go | 12 +- cmd/frontend/types.go | 2 +- .../web/dist/assets/index-C-XMkBf5.js | 1 - ...{index-CxDuAfMR.css => index-CExoCDXh.css} | 2 +- .../web/dist/assets/index-DjixLt11.js | 1 + cmd/frontend/web/dist/index.html | 4 +- cmd/frontend/web/src/styles/theme.css | 39 ++++++ cmd/frontend/web/src/views/Overview.tsx | 2 +- cmd/frontend/web/src/views/VPPInfoPanel.tsx | 54 +++++++- debian/build-deb.sh | 18 +-- debian/control.in | 2 +- debian/default.vpp-maglev | 15 ++- debian/vpp-maglev-frontend.service | 7 +- docs/maglevc.1 | 3 +- .../{maglev-frontend.8 => maglevd-frontend.8} | 121 ++++++++++++++---- docs/maglevd.8 | 1 + docs/user-guide.md | 80 ++++++++++++ 20 files changed, 380 insertions(+), 83 deletions(-) delete mode 100644 cmd/frontend/web/dist/assets/index-C-XMkBf5.js rename cmd/frontend/web/dist/assets/{index-CxDuAfMR.css => index-CExoCDXh.css} (88%) create mode 100644 cmd/frontend/web/dist/assets/index-DjixLt11.js rename docs/{maglev-frontend.8 => maglevd-frontend.8} (56%) diff --git a/Makefile b/Makefile index 7702358..6682563 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -BINARIES := maglevd maglevc maglev-frontend +BINARIES := maglevd maglevc maglevd-frontend MODULE := git.ipng.ch/ipng/vpp-maglev PROTO_DIR := proto PROTO_FILE := $(PROTO_DIR)/maglev.proto @@ -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 := 0.1.1 +VERSION := 0.9.1 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)' \ @@ -26,7 +26,7 @@ TEST ?= tests/ VPP_API_DIR ?= $(HOME)/src/vpp/build-root/install-vpp_debug-native/vpp/share/vpp/api -.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglev-frontend-web +.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglevd-frontend-web all: build @@ -34,24 +34,24 @@ build: $(GEN_FILES) $(FRONTEND_WEB_DIST) mkdir -p build/$(NATIVE_ARCH) go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/maglevd/ go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevc ./cmd/maglevc/ - go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglev-frontend ./cmd/frontend/ + go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd-frontend ./cmd/frontend/ build-amd64: $(GEN_FILES) $(FRONTEND_WEB_DIST) mkdir -p build/amd64 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd ./cmd/maglevd/ GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevc ./cmd/maglevc/ - GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglev-frontend ./cmd/frontend/ + GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd-frontend ./cmd/frontend/ build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST) mkdir -p build/arm64 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd ./cmd/maglevd/ GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevc ./cmd/maglevc/ - GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglev-frontend ./cmd/frontend/ + GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd-frontend ./cmd/frontend/ -# maglev-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 # this target picks up any asset changes automatically. -maglev-frontend-web: $(FRONTEND_WEB_DIST) +maglevd-frontend-web: $(FRONTEND_WEB_DIST) $(FRONTEND_WEB_DIST): $(FRONTEND_WEB_SRC) cd cmd/frontend/web && npm install && npm run build diff --git a/README.md b/README.md index 8f041d7..ba4a745 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,89 @@ -# maglevd +# vpp-maglev -Health checker and gRPC control plane for VPP Maglev load balancing. +Health checker, gRPC control plane, CLI, and web dashboard for the VPP +`lb` (load-balancer) plugin. Runs as a set of three binaries under one +Debian package: -## Build and Install +- **`maglevd`** — the long-running health-checker daemon. Probes backends + (HTTP, TCP, ICMP), tracks their aggregate state, programs the VPP + dataplane via the `lb` plugin binary API, and exposes everything + over a gRPC API + Prometheus `/metrics` endpoint. +- **`maglevc`** — the interactive CLI client. Tab-completing shell with + inline help; also runs one-shot commands for scripting. +- **`maglevd-frontend`** — optional web dashboard. One binary with the + SolidJS SPA embedded via `//go:embed`; connects to one or more + maglevds over gRPC and serves a live HTTP view (read-only `/view/` + and optional basic-auth `/admin/`). + +## Build and install ```sh -make # builds build//maglevd and build//maglevc +make # builds build//{maglevd,maglevc,maglevd-frontend} make test # runs all tests -make pkg-deb # Creates a debian package for arm64 and amd64 +make pkg-deb # creates a Debian package for amd64 and arm64 ``` -Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go` and -`protoc-gen-go-grpc`. +Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go` +and `protoc-gen-go-grpc`. The SolidJS bundle under +`cmd/frontend/web/` is built automatically via `make` through the +`maglevd-frontend-web` target, which needs `npm`. -Produces `vpp-maglev__amd64.deb` and `vpp-maglev__arm64.deb` -in the `build/` directory by cross-compiling with `GOOS=linux GOARCH=`. -Requires `dpkg-deb` (available on any Debian/Ubuntu host). The installed -binaries report the exact git commit via `maglevd --version` (and -similarly for `maglevc` / `maglev-frontend`). +Produces `vpp-maglev__amd64.deb` and +`vpp-maglev__arm64.deb` in the `build/` directory by +cross-compiling with `GOOS=linux GOARCH=`. Requires `dpkg-deb` +(available on any Debian/Ubuntu host). The installed binaries report +the exact git commit via `maglevd --version` (and similarly for +`maglevc` / `maglevd-frontend`). ## Running -After installing, the unit is enabled but not started automatically: +After installing, `maglevd` is enabled automatically but +`maglevd-frontend` is **not** — it's opt-in, so the web dashboard +doesn't surprise anyone who just wanted the daemon: ```sh # edit /etc/vpp-maglev/maglev.yaml, then: systemctl enable --now vpp-maglev + +# optional: web dashboard. Edit /etc/default/vpp-maglev to set +# MAGLEV_FRONTEND_ARGS and (optionally) MAGLEV_FRONTEND_USER / +# MAGLEV_FRONTEND_PASSWORD for /admin/ access, then: +systemctl enable --now vpp-maglev-frontend ``` -Or run the server and client by hand: +Or run the components by hand: ```sh maglevd --config /etc/vpp-maglev/maglev.yaml --grpc-addr :9090 -maglevd --version # print version and exit +maglevd --version # print version and exit -maglevc --server localhost:9090 # interactive shell -maglevc show frontends # one-shot -maglevc -color=false show backends # one-shot, no ANSI color +maglevc --server localhost:9090 # interactive shell +maglevc show frontends # one-shot +maglevc -color=false show backends # one-shot, no ANSI color maglevc set backend nginx0-ams pause + +maglevd-frontend -server localhost:9090 -listen :8080 ``` Send `SIGHUP` to `maglevd` to reload config without restarting. `maglevd` requires `CAP_NET_RAW` for ICMP health checks. -Check out a minimal configuration file in [[debian/maglev.yaml](debian/maglev.yaml)]. -See [docs/user-guide.md](docs/user-guide.md) for flags, signals, and `maglevc` usage. -See [docs/config-guide.md](docs/config-guide.md) for the full configuration reference. -See [docs/healthchecks.md](docs/healthchecks.md) for health state machine details. +Every flag on every binary also has an environment-variable +equivalent (e.g. `MAGLEV_CONFIG`, `MAGLEV_GRPC_ADDR`, +`MAGLEV_SERVERS`, `MAGLEV_LISTEN`, `MAGLEV_LOG_LEVEL`) so all three +programs can be driven entirely via env in containerized +deployments. + +## Documentation + +- A minimal configuration file in + [debian/maglev.yaml](debian/maglev.yaml) shows every knob. +- [docs/user-guide.md](docs/user-guide.md) — flags, signals, and + `maglevc` command reference. +- [docs/config-guide.md](docs/config-guide.md) — full YAML reference. +- [docs/healthchecks.md](docs/healthchecks.md) — health state + machine, probe scheduling, rise/fall semantics. +- Manpages: `maglevd(8)`, `maglevc(1)`, `maglevd-frontend(8)`. ## Docker diff --git a/cmd/frontend/handlers.go b/cmd/frontend/handlers.go index 62cd066..d689c54 100644 --- a/cmd/frontend/handlers.go +++ b/cmd/frontend/handlers.go @@ -106,7 +106,7 @@ func registerHandlers(mux *http.ServeMux, clients []*maglevClient, broker *Broke adminAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handleAdminAPI(w, r, byName) }) - realm := "maglev-frontend admin" + realm := "maglevd-frontend admin" // Register /admin/api/ before /admin/ so the more specific // pattern wins in net/http's ServeMux. mux.Handle("/admin/api/", basicAuth(realm, admin.User, admin.Password, adminAPI)) diff --git a/cmd/frontend/main.go b/cmd/frontend/main.go index aca7bc1..bd15371 100644 --- a/cmd/frontend/main.go +++ b/cmd/frontend/main.go @@ -26,14 +26,18 @@ func main() { } func run() error { + // All env vars are prefixed with MAGLEV_FRONTEND_ so a single + // /etc/default/vpp-maglev (or a Docker env file) can be shared + // with maglevd without its MAGLEV_LOG_LEVEL / MAGLEV_GRPC_ADDR / + // etc. leaking into this process's config. printVersion := flag.Bool("version", false, "print version and exit") - servers := stringFlag("server", "", "MAGLEV_SERVERS", "comma-separated maglevd gRPC addresses (required)") - listen := stringFlag("listen", ":8080", "MAGLEV_LISTEN", "HTTP listen address") - logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log verbosity (debug|info|warn|error)") + servers := stringFlag("server", "", "MAGLEV_FRONTEND_SERVERS", "comma-separated maglevd gRPC addresses (required)") + listen := stringFlag("listen", ":8080", "MAGLEV_FRONTEND_LISTEN", "HTTP listen address") + logLevel := stringFlag("log-level", "info", "MAGLEV_FRONTEND_LOG_LEVEL", "log verbosity (debug|info|warn|error)") flag.Parse() if *printVersion { - fmt.Printf("maglev-frontend %s (commit %s, built %s)\n", + fmt.Printf("maglevd-frontend %s (commit %s, built %s)\n", buildinfo.Version(), buildinfo.Commit(), buildinfo.Date()) return nil } diff --git a/cmd/frontend/types.go b/cmd/frontend/types.go index a50c854..965aac2 100644 --- a/cmd/frontend/types.go +++ b/cmd/frontend/types.go @@ -25,7 +25,7 @@ type MaglevdInfo struct { LastError string `json:"last_error,omitempty"` } -// VersionInfo is the build metadata of this maglev-frontend binary +// VersionInfo is the build metadata of this maglevd-frontend binary // plus runtime capability flags the SPA needs to know at mount time. type VersionInfo struct { Version string `json:"version"` diff --git a/cmd/frontend/web/dist/assets/index-C-XMkBf5.js b/cmd/frontend/web/dist/assets/index-C-XMkBf5.js deleted file mode 100644 index cb4169c..0000000 --- a/cmd/frontend/web/dist/assets/index-C-XMkBf5.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 l of s)if(l.type==="childList")for(const i of l.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(s){const l={};return s.integrity&&(l.integrity=s.integrity),s.referrerPolicy&&(l.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?l.credentials="include":s.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(s){if(s.ep)return;s.ep=!0;const l=n(s);fetch(s.href,l)}})();const at=!1,ct=(e,t)=>e===t,W=Symbol("solid-proxy"),xe=Symbol("solid-track"),ce={equals:ct};let Fe=Ke;const U=1,ue=2,We={owned:null,cleanups:null,context:null,owner:null};var v=null;let Se=null,ut=null,p=null,E=null,D=null,pe=0;function te(e,t){const n=p,r=v,s=e.length===0,l=t===void 0?r:t,i=s?We:{owned:null,cleanups:null,context:l?l.context:null,owner:l},o=s?e:()=>e(()=>M(()=>re(i)));v=i,p=null;try{return G(o,!0)}finally{p=n,v=r}}function x(e,t){t=t?Object.assign({},ce,t):ce;const n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},r=s=>(typeof s=="function"&&(s=s(n.value)),Ge(n,s));return[He.bind(n),r]}function C(e,t,n){const r=Pe(e,t,!1,U);le(r)}function H(e,t,n){Fe=pt;const r=Pe(e,t,!1,U);(!n||!n.render)&&(r.user=!0),D?D.push(r):le(r)}function F(e,t,n){n=n?Object.assign({},ce,n):ce;const r=Pe(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,le(r),He.bind(r)}function ft(e){return G(e,!1)}function M(e){if(p===null)return e();const t=p;p=null;try{return e()}finally{p=t}}function dt(e,t,n){const r=Array.isArray(e);let s,l=n&&n.defer;return i=>{let o;if(r){o=Array(e.length);for(let u=0;ut(o,s,i));return s=o,a}}function gt(e){H(()=>M(e))}function Y(e){return v===null||(v.cleanups===null?v.cleanups=[e]:v.cleanups.push(e)),e}function Ae(){return p}function ht(){return v}function bt(e,t){const n=v,r=p;v=e,p=null;try{return G(t,!0)}catch(s){Oe(s)}finally{v=n,p=r}}function He(){if(this.sources&&this.state)if(this.state===U)le(this);else{const e=E;E=null,G(()=>de(this),!1),E=e}if(p){const e=this.observers?this.observers.length:0;p.sources?(p.sources.push(this),p.sourceSlots.push(e)):(p.sources=[this],p.sourceSlots=[e]),this.observers?(this.observers.push(p),this.observerSlots.push(p.sources.length-1)):(this.observers=[p],this.observerSlots=[p.sources.length-1])}return this.value}function Ge(e,t,n){let r=e.value;return(!e.comparator||!e.comparator(r,t))&&(e.value=t,e.observers&&e.observers.length&&G(()=>{for(let s=0;s1e6)throw E=[],new Error},!1)),t}function le(e){if(!e.fn)return;re(e);const t=pe;mt(e,e.value,t)}function mt(e,t,n){let r;const s=v,l=p;p=v=e;try{r=e.fn(t)}catch(i){return e.pure&&(e.state=U,e.owned&&e.owned.forEach(re),e.owned=null),e.updatedAt=n+1,Oe(i)}finally{p=l,v=s}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&"observers"in e?Ge(e,r):e.value=r,e.updatedAt=n)}function Pe(e,t,n,r=U,s){const l={fn:e,state:r,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:v,context:v?v.context:null,pure:n};return v===null||v!==We&&(v.owned?v.owned.push(l):v.owned=[l]),l}function fe(e){if(e.state===0)return;if(e.state===ue)return de(e);if(e.suspense&&M(e.suspense.inFallback))return e.suspense.effects.push(e);const t=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt=0;n--)if(e=t[n],e.state===U)le(e);else if(e.state===ue){const r=E;E=null,G(()=>de(e,t[0]),!1),E=r}}function G(e,t){if(E)return e();let n=!1;t||(E=[]),D?n=!0:D=[],pe++;try{const r=e();return $t(n),r}catch(r){n||(D=null),E=null,Oe(r)}}function $t(e){if(E&&(Ke(E),E=null),e)return;const t=D;D=null,t.length&&G(()=>Fe(t),!1)}function Ke(e){for(let t=0;t=0;t--)re(e.tOwned[t]);delete e.tOwned}if(e.owned){for(t=e.owned.length-1;t>=0;t--)re(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 wt(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function Oe(e,t=v){throw wt(e)}const vt=Symbol("fallback");function Me(e){for(let t=0;t1?[]:null;return Y(()=>Me(l)),()=>{let a=e()||[],u=a.length,f,c;return a[xe],M(()=>{let b,y,S,P,B,m,w,_,A;if(u===0)i!==0&&(Me(l),l=[],r=[],s=[],i=0,o&&(o=[])),n.fallback&&(r=[vt],s[0]=te(k=>(l[0]=k,n.fallback())),i=1);else if(i===0){for(s=new Array(u),c=0;c=m&&_>=m&&r[w]===a[_];w--,_--)S[_]=s[w],P[_]=l[w],o&&(B[_]=o[w]);for(b=new Map,y=new Array(_+1),c=_;c>=m;c--)A=a[c],f=b.get(A),y[c]=f===void 0?-1:f,b.set(A,c);for(f=m;f<=w;f++)A=r[f],c=b.get(A),c!==void 0&&c!==-1?(S[c]=s[f],P[c]=l[f],o&&(B[c]=o[f]),c=y[c],b.set(A,c)):l[f]();for(c=m;ce(t||{}))}const _t=e=>`Stale read from <${e}>.`;function J(e){const t="fallback"in e&&{fallback:()=>e.fallback};return F(yt(()=>e.each,e.children,t||void 0))}function O(e){const t=e.keyed,n=F(()=>e.when,void 0,void 0),r=t?n:F(n,void 0,{equals:(s,l)=>!s==!l});return F(()=>{const s=r();if(s){const l=e.children;return typeof l=="function"&&l.length>0?M(()=>l(t?s:()=>{if(!M(r))throw _t("Show");return n()})):l}return e.fallback},void 0,void 0)}const L=e=>F(()=>e());function kt(e,t,n){let r=n.length,s=t.length,l=r,i=0,o=0,a=t[s-1].nextSibling,u=null;for(;if-o){const y=t[i];for(;o{s=l,t===document?e():d(t,e(),t.firstChild?null:void 0,n)},r.owner),()=>{s(),t.textContent=""}}function h(e,t,n,r){let s;const l=()=>{const o=document.createElement("template");return o.innerHTML=e,o.content.firstChild},i=()=>(s||(s=l())).cloneNode(!0);return i.cloneNode=i,i}function we(e,t=window.document){const n=t[Be]||(t[Be]=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 ge(e,t,r,n);C(s=>ge(e,t(),s,n),r)}function Ct(e){let t=e.target;const n=`$$${e.type}`,r=e.target,s=e.currentTarget,l=a=>Object.defineProperty(e,"target",{configurable:!0,value:a}),i=()=>{const a=t[n];if(a&&!t.disabled){const u=t[`${n}Data`];if(u!==void 0?a.call(t,u,e):a.call(t,e),e.cancelBubble)return}return t.host&&typeof t.host!="string"&&!t.host._$host&&t.contains(e.target)&&l(t.host),!0},o=()=>{for(;i()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return t||document}}),e.composedPath){const a=e.composedPath();l(a[0]);for(let u=0;u{let o=t();for(;typeof o=="function";)o=o();n=ge(e,o,n,r)}),()=>n;if(Array.isArray(t)){const o=[],a=n&&Array.isArray(n);if(Ce(o,t,n,s))return C(()=>n=ge(e,o,n,r,!0)),()=>n;if(o.length===0){if(n=K(e,n,r),i)return n}else a?n.length===0?Re(e,o,r):kt(e,n,o):(n&&K(e),Re(e,o));n=o}else if(t.nodeType){if(Array.isArray(n)){if(i)return n=K(e,n,r,t);K(e,n,null,t)}else n==null||n===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}}return n}function Ce(e,t,n,r){let s=!1;for(let l=0,i=t.length;l=0;i--){const o=t[i];if(s!==o){const a=o.parentNode===e;!l&&!i?a?e.replaceChild(s,o):e.insertBefore(s,n):a&&o.remove()}else l=!0}}else e.insertBefore(s,n);return[s]}const Et="http://www.w3.org/2000/svg";function Pt(e,t=!1,n=void 0){return t?document.createElementNS(Et,e):document.createElement(e,{is:n})}function Ot(e){const{useShadow:t}=e,n=document.createTextNode(""),r=()=>e.mount||document.body,s=ht();let l;return H(()=>{l||(l=bt(s,()=>F(()=>e.children)));const i=r();if(i instanceof HTMLHeadElement){const[o,a]=x(!1),u=()=>a(!0);te(f=>d(i,()=>o()?f():l(),null)),Y(u)}else{const o=Pt(e.isSVG?"g":"div",e.isSVG),a=t&&o.attachShadow?o.attachShadow({mode:"open"}):o;Object.defineProperty(o,"_$host",{get(){return n.parentNode},configurable:!0}),d(a,l),i.appendChild(o),e.ref&&e.ref(o),Y(()=>i.removeChild(o))}},void 0,{render:!0}),n}async function Ye(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 Je(){return Ye("/view/api/state")}function Tt(){return Ye("/view/api/version")}const he=Symbol("store-raw"),q=Symbol("store-node"),j=Symbol("store-has"),Xe=Symbol("store-self");function ze(e){let t=e[W];if(!t&&(Object.defineProperty(e,W,{value:t=new Proxy(e,It)}),!Array.isArray(e))){const n=Object.keys(e),r=Object.getOwnPropertyDescriptors(e);for(let s=0,l=n.length;se[W][t]),n}function Qe(e){Ae()&&se(be(e,q),Xe)()}function Lt(e){return Qe(e),Reflect.ownKeys(e)}const It={get(e,t,n){if(t===he)return e;if(t===W)return n;if(t===xe)return Qe(e),n;const r=be(e,q),s=r[t];let l=s?s():e[t];if(t===q||t===j||t==="__proto__")return l;if(!s){const i=Object.getOwnPropertyDescriptor(e,t);Ae()&&(typeof l!="function"||e.hasOwnProperty(t))&&!(i&&i.get)&&(l=se(r,t,l)())}return X(l)?ze(l):l},has(e,t){return t===he||t===W||t===xe||t===q||t===j||t==="__proto__"?!0:(Ae()&&se(be(e,j),t)(),t in e)},set(){return!0},deleteProperty(){return!0},ownKeys:Lt,getOwnPropertyDescriptor:Nt};function Q(e,t,n,r=!1){if(!r&&e[t]===n)return;const s=e[t],l=e.length;n===void 0?(delete e[t],e[j]&&e[j][t]&&s!==void 0&&e[j][t].$()):(e[t]=n,e[j]&&e[j][t]&&s===void 0&&e[j][t].$());let i=be(e,q),o;if((o=se(i,t,s))&&o.$(()=>n),Array.isArray(e)&&e.length!==l){for(let a=e.length;a1){r=t.shift();const i=typeof r,o=Array.isArray(e);if(Array.isArray(r)){for(let a=0;a1){ee(e[r],t,[r].concat(n));return}s=e[r],n=[r].concat(n)}let l=t[0];typeof l=="function"&&(l=l(s,n),l===s)||r===void 0&&l==null||(l=z(l),r===void 0||X(s)&&X(l)&&!Array.isArray(l)?Ze(s,l):Q(e,r,l))}function Dt(...[e,t]){const n=z(e||{}),r=Array.isArray(n),s=ze(n);function l(...i){ft(()=>{r&&i.length===1?jt(n,i[0]):ee(n,i)})}return[s,l]}const me=new WeakMap,et={get(e,t){if(t===he)return e;const n=e[t];let r;return X(n)?me.get(n)||(me.set(n,r=new Proxy(n,et)),r):n},set(e,t,n){return Q(e,t,z(n)),!0},deleteProperty(e,t){return Q(e,t,void 0,!0),!0}};function ye(e){return t=>{if(X(t)){let n;(n=me.get(t))||me.set(t,n=new Proxy(t,et)),e(n)}return t}}const[Mt,Bt]=x(0);setInterval(()=>Bt(e=>e+1),5e3);function Te(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){u=!0;break}if(u){r=a;break}}let s=!1,l=!1,i=!0;const o=new Set;for(let a=0;a0&&(s=!0),o.has(u.name)||(o.add(u.name),l=!0,f!=="unknown"&&(i=!1))}!l||i?n.state="unknown":s?n.state="up":n.state="down"}}const[$e,ie]=Dt({byName:{}});function tt(e){const t={};for(const n of e)Te(n),t[n.maglevd.name]=n;ie({byName:t})}function Rt(e,t){ie(ye(n=>{const r=n.byName[e];if(!r)return;const s=r.backends.find(l=>l.name===t.backend);s&&(s.state=t.transition.to,s.enabled=t.transition.to!=="disabled",s.last_transition=t.transition,s.transitions||(s.transitions=[]),s.transitions.push(t.transition),s.transitions.length>20&&(s.transitions=s.transitions.slice(s.transitions.length-20)),Te(r))}))}function Ut(e,t){ie(ye(n=>{const r=n.byName[e];r&&(r.vpp_state=t)}))}function Vt(e,t){ie(ye(n=>{const r=n.byName[e];r&&(r.maglevd.connected=t.connected,r.maglevd.last_error=t.last_error)}))}function Ft(e,t,n,r,s){ie(ye(l=>{const i=l.byName[e];if(!i)return;const o=i.frontends.find(f=>f.name===t);if(!o)return;const a=o.pools.find(f=>f.name===n);if(!a)return;const u=a.backends.find(f=>f.name===r);u&&(u.weight=s,Te(i))}))}function Wt(e,t){return e.includes(":")?`[${e}]:${t}`:`${e}:${t}`}function Ht(e){if(Mt(),!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 l=s%60,i=Math.floor(s/60);if(i<1)return`${l}m${r}s ago`;const o=i%24,a=Math.floor(i/24);return a<1?`${i}h${l}m ago`:`${a}d${o}h ago`}const Ue=500,[Ee,Gt]=x([]);function Kt(e){Gt(t=>{const n=[...t,e];return n.length>Ue?n.slice(n.length-Ue):n})}function qt(){const e=new EventSource("/view/api/events");return e.onmessage=t=>{try{const n=JSON.parse(t.data);Yt(n)}catch(n){console.error("sse parse error",n,t.data)}},e.addEventListener("resync",async()=>{try{const t=await Je();tt(t)}catch(t){console.error("resync refetch failed",t)}}),e.onerror=t=>{console.debug("sse error, browser will reconnect",t)},e}function Yt(e){switch(Kt(e),e.type){case"backend":Rt(e.maglevd,e.payload);break;case"frontend":e.maglevd,e.payload;break;case"maglevd-status":Vt(e.maglevd,e.payload);break;case"vpp-status":Ut(e.maglevd,e.payload.state);break}}const[_e,nt]=x(void 0);var Jt=h("