diff --git a/cmd/frontend/client.go b/cmd/frontend/client.go index b29529d..647fca9 100644 --- a/cmd/frontend/client.go +++ b/cmd/frontend/client.go @@ -33,6 +33,12 @@ type maglevClient struct { connected bool lastErr string cache cachedState + + // lbWakeCh is a buffer-1 trigger channel feeding lbStateLoop. Every + // backend transition (and a few other events) does a non-blocking send + // here; the loop coalesces bursts into at most one GetVPPLBState call + // per second. See lbStateLoop for the leading+trailing-edge debounce. + lbWakeCh chan struct{} } // cachedState is the per-maglevd snapshot served via the REST handlers. @@ -49,6 +55,7 @@ type cachedState struct { HealthCheckOrder []string VPPInfo *VPPInfoSnapshot VPPState string // "", "connected", "disconnected" + LBState *LBStateSnapshot LastRefresh time.Time } @@ -69,6 +76,7 @@ func newMaglevClient(address string, broker *Broker) (*maglevClient, error) { Backends: map[string]*BackendSnapshot{}, HealthChecks: map[string]*HealthCheckSnapshot{}, }, + lbWakeCh: make(chan struct{}, 1), }, nil } @@ -147,6 +155,7 @@ func (c *maglevClient) Start(ctx context.Context) { go c.watchLoop(ctx) go c.refreshLoop(ctx) go c.healthLoop(ctx) + go c.lbStateLoop(ctx) } func (c *maglevClient) setConnected(ok bool, errMsg string) { @@ -196,6 +205,7 @@ func (c *maglevClient) Snapshot() *StateSnapshot { HealthChecks: make([]*HealthCheckSnapshot, 0, len(c.cache.HealthCheckOrder)), VPPInfo: c.cache.VPPInfo, VPPState: c.cache.VPPState, + LBState: c.cache.LBState, } for _, name := range c.cache.FrontendsOrder { if f, ok := c.cache.Frontends[name]; ok { @@ -302,6 +312,11 @@ func (c *maglevClient) refreshAll(ctx context.Context) error { c.cache.VPPState = vppState c.cache.LastRefresh = time.Now() c.mu.Unlock() + // Best-effort LB state pull so /view/api/state served on a fresh + // page load already carries the bucket column. Errors are + // swallowed by fetchLBStateAndPublish (which clears the cache and + // emits an empty event so the SPA renders "—"). + c.fetchLBStateAndPublish(ctx) return nil } @@ -434,6 +449,11 @@ func (c *maglevClient) handleEvent(ev *grpcapi.Event) { AtUnixNs: tr.AtUnixNs, Payload: payload, }) + // A real transition means VPP is about to (or already did) + // reshuffle bucket allocations across the affected VIP. Wake + // the lb-state loop so the SPA's bucket column converges + // without waiting for the 30s refresh. + c.triggerLBStateFetch() case *grpcapi.Event_Frontend: fe := body.Frontend @@ -544,6 +564,11 @@ func (c *maglevClient) applyVPPLogHeartbeat(msg string) { AtUnixNs: time.Now().UnixNano(), Payload: payload, }) + // VPP just came back: pull fresh LB state so the bucket column + // repopulates immediately instead of waiting up to 30s for the + // next refresh tick. On vpp-disconnect the next fetch will fail + // and clear the cache, which is also the right behaviour. + c.triggerLBStateFetch() } func (c *maglevClient) applyBackendTransition(name string, tr *TransitionRecord) { @@ -677,6 +702,175 @@ func transitionFromProto(t *grpcapi.TransitionRecord) *TransitionRecord { } } +// triggerLBStateFetch sends a non-blocking wake to lbStateLoop. The +// channel has buffer 1 so coalesced bursts never block the publisher. +func (c *maglevClient) triggerLBStateFetch() { + select { + case c.lbWakeCh <- struct{}{}: + default: + } +} + +// lbStateLoop consumes wake signals and calls GetVPPLBState, with a +// leading+trailing-edge debounce so we never exceed one fetch per +// minLBInterval (1s). The leading edge means the very first wake after +// an idle period fires immediately — important so a single isolated +// transition isn't artificially delayed by a second. The trailing edge +// means a burst of wakes during the cool-down still gets one final +// fetch right after the gate opens, so the SPA always converges to a +// post-burst snapshot rather than missing the last update. +func (c *maglevClient) lbStateLoop(ctx context.Context) { + const minLBInterval = time.Second + var ( + timer *time.Timer + lastFetch time.Time + ) + timerCh := func() <-chan time.Time { + if timer == nil { + return nil + } + return timer.C + } + fire := func() { + if timer != nil { + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer = nil + } + c.fetchLBStateAndPublish(ctx) + lastFetch = time.Now() + } + for { + select { + case <-ctx.Done(): + return + case <-c.lbWakeCh: + wait := minLBInterval - time.Since(lastFetch) + if wait <= 0 { + fire() + } else if timer == nil { + timer = time.NewTimer(wait) + } + case <-timerCh(): + timer = nil + fire() + } + } +} + +// fetchLBStateAndPublish runs one GetVPPLBState round-trip, rebuilds +// the per-frontend bucket map, swaps it into the cache, and broadcasts +// a "lb-state" BrowserEvent. On error the cache is cleared and an +// empty event is published so the SPA can switch the bucket column to +// em-dashes — clear-on-error is simpler than stale-but-visible and +// doesn't risk showing a confusing snapshot from before VPP died. +func (c *maglevClient) fetchLBStateAndPublish(ctx context.Context) { + fctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + lbs, err := c.api.GetVPPLBState(fctx, &grpcapi.GetVPPLBStateRequest{}) + if err != nil { + c.mu.Lock() + had := c.cache.LBState != nil + c.cache.LBState = nil + c.mu.Unlock() + slog.Debug("lb-state-fetch", "maglevd", c.name, "err", err) + if had { + c.publishLBState(nil) + } + return + } + snap := c.buildLBStateSnapshot(lbs) + c.mu.Lock() + c.cache.LBState = snap + c.mu.Unlock() + c.publishLBState(snap.PerFrontend) +} + +func (c *maglevClient) publishLBState(perFrontend map[string]map[string]int32) { + payload, _ := json.Marshal(LBStatePayload{PerFrontend: perFrontend}) + c.broker.Publish(BrowserEvent{ + Maglevd: c.name, + Type: "lb-state", + AtUnixNs: time.Now().UnixNano(), + Payload: payload, + }) +} + +// buildLBStateSnapshot translates a VPP-side state record (keyed by +// CIDR/protocol/port and AS address) into a maglev-side record (keyed +// by frontend name and backend name). Unmatched VIPs and unmatched AS +// addresses are silently skipped — they're benign side effects of a +// transient sync gap or a backend address that's only present in one +// of the two universes. +func (c *maglevClient) buildLBStateSnapshot(lbs *grpcapi.VPPLBState) *LBStateSnapshot { + c.mu.RLock() + feByVIP := make(map[string]string, len(c.cache.Frontends)) + for _, f := range c.cache.Frontends { + feByVIP[lbVIPKey(f.Address, f.Protocol, f.Port)] = f.Name + } + backendByAddr := make(map[string]string, len(c.cache.Backends)) + for _, b := range c.cache.Backends { + backendByAddr[b.Address] = b.Name + } + c.mu.RUnlock() + + out := &LBStateSnapshot{PerFrontend: map[string]map[string]int32{}} + for _, v := range lbs.GetVips() { + feName, ok := feByVIP[lbVIPKey(stripLBHostMask(v.GetPrefix()), lbProtoString(v.GetProtocol()), v.GetPort())] + if !ok { + continue + } + row := out.PerFrontend[feName] + if row == nil { + row = map[string]int32{} + out.PerFrontend[feName] = row + } + for _, as := range v.GetApplicationServers() { + bname, ok := backendByAddr[as.GetAddress()] + if !ok { + continue + } + row[bname] = int32(as.GetNumBuckets()) + } + } + return out +} + +// lbVIPKey is the join key between a maglev FrontendSnapshot and a +// VPP-side VPPLBVIP record. Stripping the mask and lower-casing the +// protocol gives a canonical form that both sides can produce. +func lbVIPKey(addr, proto string, port uint32) string { + return fmt.Sprintf("%s/%s/%d", addr, strings.ToLower(proto), port) +} + +// lbProtoString mirrors maglevc's protoString — kept local to avoid a +// cross-package import for two trivial helpers. +func lbProtoString(p uint32) string { + switch p { + case 6: + return "tcp" + case 17: + return "udp" + case 255: + return "any" + } + return fmt.Sprintf("%d", p) +} + +// stripLBHostMask trims "/32" or "/128" from a VPP host-prefix VIP so +// it can be compared against a maglev FrontendSnapshot.Address (which +// is bare). Other shapes are returned unchanged. +func stripLBHostMask(prefix string) string { + if strings.HasSuffix(prefix, "/32") || strings.HasSuffix(prefix, "/128") { + return prefix[:strings.LastIndexByte(prefix, '/')] + } + return prefix +} + func healthCheckFromProto(h *grpcapi.HealthCheckInfo) *HealthCheckSnapshot { return &HealthCheckSnapshot{ Name: h.GetName(), diff --git a/cmd/frontend/handlers.go b/cmd/frontend/handlers.go index d689c54..1cb8805 100644 --- a/cmd/frontend/handlers.go +++ b/cmd/frontend/handlers.go @@ -37,6 +37,23 @@ func registerHandlers(mux *http.ServeMux, clients []*maglevClient, broker *Broke _, _ = w.Write([]byte("ok\n")) }) + // Favicon served from the same embedded dist tree Vite produced. + // Browsers auto-fetch /favicon.ico from the document root regardless + // of where the SPA itself is mounted, so we register a top-level + // handler in addition to whatever /view/favicon.ico picks up via the + // static file server below. Read once at registration so we don't + // touch the embed.FS on every request, and serve with a long + // max-age since the bytes never change for a given binary. + if favicon, ferr := fs.ReadFile(webFS, "web/dist/favicon.ico"); ferr == nil { + mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "image/x-icon") + w.Header().Set("Cache-Control", "public, max-age=86400") + _, _ = w.Write(favicon) + }) + } else { + slog.Warn("favicon-missing", "err", ferr) + } + mux.HandleFunc("/view/api/version", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, VersionInfo{ Version: buildinfo.Version(), @@ -188,6 +205,13 @@ func handleBackendLifecycle(w http.ResponseWriter, r *http.Request, c *maglevCli } slog.Info("admin-backend-action", "maglevd", c.name, "backend", name, "action", action, "state", snap.State) + // The maglevd→watch path will deliver a transition event that + // also wakes the lb-state loop, but firing here too makes the + // admin path self-contained and shaves the worst-case race + // where the SPA is still waiting on the WatchEvents replay + // when the POST response lands. The debouncer coalesces any + // duplicate wake. + c.triggerLBStateFetch() writeJSON(w, snap) } @@ -219,6 +243,14 @@ func handleBackendWeight(w http.ResponseWriter, r *http.Request, c *maglevClient slog.Info("admin-set-weight", "maglevd", c.name, "frontend", frontend, "pool", pool, "backend", backend, "weight", body.Weight, "flush", body.Flush) + // Weight changes never produce a transition event on the maglevd + // side (the backend's state is unchanged), so the WatchEvents + // stream won't wake the lb-state loop for us — without an explicit + // trigger here the SPA's bucket column would stay stale until the + // next 30s refresh tick. SyncLBStateVIP on the maglevd side has + // already pushed the new weights into VPP synchronously, so the + // fetch we kick off will see fresh post-mutation buckets. + c.triggerLBStateFetch() writeJSON(w, snap) } diff --git a/cmd/frontend/types.go b/cmd/frontend/types.go index 965aac2..828cbd8 100644 --- a/cmd/frontend/types.go +++ b/cmd/frontend/types.go @@ -15,6 +15,18 @@ type StateSnapshot struct { // from vpp-connect / vpp-disconnect / vpp-api-{send,recv} log // events and re-seeded on every refreshAll tick. VPPState string `json:"vpp_state,omitempty"` + // LBState is the most recent VPP LB plugin view of buckets-per-backend, + // keyed by frontend name → backend name → bucket count. nil when VPP is + // disconnected or no fetch has succeeded yet. + LBState *LBStateSnapshot `json:"lb_state,omitempty"` +} + +// LBStateSnapshot is a per-(frontend, backend) view of VPP's bucket +// allocation. The frontend collects this with GetVPPLBState and matches +// VPP's VIP records back to maglev frontend/backend names so the SPA +// never has to know about VPP-side prefixes or AS addresses. +type LBStateSnapshot struct { + PerFrontend map[string]map[string]int32 `json:"per_frontend"` } // MaglevdInfo is the per-maglevd connection status record. @@ -98,11 +110,19 @@ type VPPInfoSnapshot struct { // BrowserEvent is the wire shape sent over SSE to the browser. type BrowserEvent struct { Maglevd string `json:"maglevd"` - Type string `json:"type"` // log|backend|frontend|maglevd-status|resync + Type string `json:"type"` // log|backend|frontend|maglevd-status|vpp-status|lb-state|resync AtUnixNs int64 `json:"at_unix_ns"` Payload json.RawMessage `json:"payload"` } +// LBStatePayload rides on a "lb-state" BrowserEvent and carries the +// freshly-fetched bucket map. PerFrontend may be nil (or empty) to +// signal "no LB state available" — the SPA renders such backends +// with an em-dash in the buckets column. +type LBStatePayload struct { + PerFrontend map[string]map[string]int32 `json:"per_frontend"` +} + // BackendEventPayload is what we ship inside BrowserEvent.Payload for // type == "backend". type BackendEventPayload struct { diff --git a/cmd/frontend/web/dist/assets/index-3BvNJ7QB.css b/cmd/frontend/web/dist/assets/index-3BvNJ7QB.css new file mode 100644 index 0000000..1a1fc9f --- /dev/null +++ b/cmd/frontend/web/dist/assets/index-3BvNJ7QB.css @@ -0,0 +1 @@ +*,*:before,*:after{box-sizing:border-box}html,body{margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:14px;line-height:1.4;color:var(--fg);background:var(--bg)}h1,h2,h3,h4,p,dl,dd,ol,ul{margin:0;padding:0}ol,ul{list-style:none}a{color:inherit;text-decoration:none}button{font:inherit;color:inherit;background:none;border:1px solid var(--border);border-radius:4px;padding:4px 8px;cursor:pointer}button:hover{background:var(--bg-soft)}table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:4px 8px}code,pre,.mono{font-family:SF Mono,Menlo,Consolas,monospace}:root{--bg: #fafafa;--bg-soft: #f0f0f0;--bg-card: #ffffff;--fg: #0f172a;--fg-muted: #6b7280;--border: #e5e7eb;--accent: #2563eb;--state-up: #16a34a;--state-down: #dc2626;--state-paused: #2563eb;--state-disabled: #6b7280;--state-unknown: #eab308;--state-removed: #374151}.flash-target{display:inline-block;padding:0 4px;border-radius:3px;transform-origin:center center;will-change:transform,background-color,box-shadow}.backend-row td{overflow:visible}.app{max-width:1400px;margin:0 auto;padding:16px}.app-header{display:flex;align-items:center;gap:16px;padding:4px 0;border-bottom:1px solid var(--border);margin-bottom:16px}.brand{display:flex;align-items:center;gap:8px}.brand strong{font-size:18px}.brand-name{color:inherit;text-decoration:none}.brand-name:hover{color:var(--accent)}.brand-logo{display:inline-flex;align-items:center}.brand-logo img{height:56px;width:56px;display:block}.brand-logo:hover img{opacity:.8}.app-header .mode-tag{margin-left:auto;padding:2px 6px;border-radius:3px;background:var(--bg-soft);color:var(--fg-muted);font-size:11px;text-transform:uppercase}.brand .version{margin-left:8px;color:var(--fg-muted);font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;cursor:help}.admin-toggle{padding:4px 10px;border:1px solid var(--border);border-radius:4px;color:var(--accent)}.scope-selector{display:flex;gap:6px;flex-wrap:wrap}.scope-tab{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:20px}.scope-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}.scope-tab .dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--state-down)}.scope-tab.connected .dot{background:var(--state-up)}.status-badge{display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:500;color:#fff;text-transform:capitalize}.status-badge[data-state=up]{background:var(--state-up)}.status-badge[data-state=down]{background:var(--state-down)}.status-badge[data-state=paused]{background:var(--state-paused)}.status-badge[data-state=disabled]{background:var(--state-disabled)}.status-badge[data-state=unknown]{background:var(--state-unknown);color:#1f2937}.status-badge[data-state=removed]{background:var(--state-removed);text-decoration:line-through}.frontend-list{display:flex;flex-direction:column;gap:4px;margin-bottom:12px}.frontend-title{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap}.frontend-title-icon{display:inline-block;width:1.5em;text-align:center;font-size:14px;line-height:1}.frontend-title-name{font-size:15px;font-weight:600;display:inline-block;width:40ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.frontend-title-addr{font-family:SF Mono,Menlo,Consolas,monospace;font-size:12px;color:var(--fg-muted)}.frontend-title-proto{font-size:11px;font-weight:600;color:var(--fg-muted);text-transform:uppercase}.frontend-title-desc{font-size:12px;color:var(--fg-muted);font-style:italic;font-weight:400}.tag{display:inline-block;padding:1px 6px;border-radius:3px;background:var(--bg-soft);color:var(--fg-muted);font-size:11px;margin-left:4px}.backend-table{table-layout:fixed;width:100%}.backend-table th,.backend-table td{white-space:nowrap}.backend-table th{font-size:11px;color:var(--fg-muted);text-transform:uppercase;border-bottom:1px solid var(--border);padding:2px 8px}.backend-table .numeric{text-align:right}.backend-table .col-pool{width:10ch}.backend-row .col-pool{color:var(--fg-muted);font-weight:500}.backend-row.pool-standby>td:not(.actions){opacity:.35}.backend-table .col-address{width:42ch}.backend-table .col-state{width:90px}.backend-table .col-weight{width:80px}.backend-table .col-effective,.backend-table .col-buckets{width:95px}.backend-table .col-age{width:110px}.backend-row td.backend-name{overflow:hidden}.backend-name-text{display:inline-block;max-width:calc(100% - 22px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.backend-row td{border-bottom:1px solid var(--border);font-size:13px;padding:2px 8px}.backend-row .backend-name{font-weight:500}.backend-row .backend-address,.backend-row .age{color:var(--fg-muted);font-family:SF Mono,Menlo,Consolas,monospace;font-size:12px}.backend-table th.actions,.backend-row td.actions{width:28px;padding:0 4px;text-align:center}.kebab-wrap{position:relative;display:inline-block}.kebab-btn{width:20px;height:20px;padding:0;line-height:1;font-size:16px;color:var(--fg-muted);border:1px solid transparent;background:transparent;cursor:pointer;border-radius:3px}.kebab-btn:hover,.kebab-btn[aria-expanded=true]{color:var(--fg);background:var(--bg-soft);border-color:var(--border)}.kebab-menu{position:absolute;top:22px;right:0;z-index:20;min-width:120px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;box-shadow:0 4px 12px #0000001f;padding:4px 0}.kebab-item{display:block;width:100%;padding:6px 12px;border:none;background:transparent;text-align:left;cursor:pointer;font-size:13px;color:var(--fg)}.kebab-item:hover{background:var(--bg-soft)}.kebab-item:disabled{color:var(--fg-muted);cursor:wait}.kebab-error{padding:4px 12px 8px;color:var(--state-down);font-size:11px;font-family:SF Mono,Menlo,Consolas,monospace;max-width:260px;white-space:normal}.modal-backdrop{position:fixed;inset:0;background:#0f172a73;display:flex;align-items:center;justify-content:center;z-index:100;padding:16px}.modal-card{background:var(--bg-card);border:1px solid var(--border);border-radius:6px;box-shadow:0 20px 50px #00000040;width:100%;max-width:480px;display:flex;flex-direction:column}.modal-header{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border)}.modal-header h3{margin:0;font-size:15px;flex:1}.modal-close{width:28px;height:28px;padding:0;font-size:20px;line-height:1;border:none;background:transparent;color:var(--fg-muted);cursor:pointer;border-radius:3px}.modal-close:hover{background:var(--bg-soft);color:var(--fg)}.modal-body{padding:16px}.dialog-body{margin-bottom:12px}.dialog-target{margin-bottom:12px;font-size:13px;color:var(--fg-muted)}.dialog-target code{font-family:SF Mono,Menlo,Consolas,monospace;font-size:13px;background:var(--bg-soft);padding:1px 5px;border-radius:3px;color:var(--fg)}.dialog-consequence{font-size:13px;line-height:1.5;color:var(--fg)}.dialog-field{display:flex;flex-direction:column;margin-bottom:12px;font-size:13px}.dialog-field>span{font-weight:500;margin-bottom:4px}.dialog-field input[type=number]{font:inherit;padding:6px 8px;border:1px solid var(--border);border-radius:4px;width:100px}.dialog-field .weight-slider-label{display:flex;align-items:baseline;justify-content:space-between}.dialog-field .weight-slider-value{font-family:SF Mono,Menlo,Consolas,monospace;font-size:18px;font-weight:600;color:var(--accent);min-width:3ch;text-align:right}.dialog-field .weight-slider{width:100%;margin:4px 0;accent-color:var(--accent)}.dialog-field small{margin-top:4px;color:var(--fg-muted);font-size:11px}.dialog-field.checkbox{flex-direction:row;align-items:center;gap:8px}.dialog-field.checkbox>span{margin-bottom:0;font-weight:400}.dialog-note{font-size:12px;color:var(--fg-muted);line-height:1.5;padding:8px 10px;background:var(--bg-soft);border-radius:4px}.dialog-warn{font-size:12px;color:#991b1b;line-height:1.5;padding:8px 10px;background:#fee2e2;border:1px solid #fecaca;border-radius:4px;font-weight:500}.dialog-error{margin-top:8px;padding:8px 10px;background:#fee2e2;color:#991b1b;border-radius:4px;font-family:SF Mono,Menlo,Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word}.dialog-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:16px;padding-top:12px;border-top:1px solid var(--border)}.btn-primary,.btn-secondary{font:inherit;padding:6px 14px;border-radius:4px;cursor:pointer;border:1px solid var(--border)}.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover:not(:disabled){background:var(--accent);filter:brightness(1.12)}.btn-primary.btn-danger{background:var(--state-down);border-color:var(--state-down)}.btn-primary.btn-danger:hover:not(:disabled){background:var(--state-down);filter:brightness(1.12)}.btn-secondary{background:transparent;color:var(--fg)}.btn-secondary:hover:not(:disabled){background:var(--bg-soft)}.btn-primary:disabled,.btn-secondary:disabled{opacity:.6;cursor:wait}.probe-heartbeat{display:inline-block;width:16px;height:14px;line-height:14px;margin-right:6px;text-align:center;font-size:11px;color:var(--state-disabled);overflow:hidden;vertical-align:middle;transform-origin:center center}.probe-heartbeat.in-flight{color:inherit;font-size:10px}.banner{padding:8px 12px;border-radius:4px;margin-bottom:12px;font-size:13px}.banner.warn{background:#fef3c7;color:#92400e}.banner.err{background:#fee2e2;color:#991b1b}.loading,.empty{color:var(--fg-muted);padding:16px}.zippy{border:1px solid var(--border);border-radius:6px;background:var(--bg-card)}.zippy summary{padding:4px 12px;cursor:pointer;font-weight:500}.zippy-body{padding:4px 10px 6px;border-top:1px solid var(--border)}.zippy-title{display:inline-flex;align-items:center;gap:10px}.vpp-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;text-transform:lowercase;color:#fff}.vpp-badge[data-state=connected]{background:var(--state-up)}.vpp-badge[data-state=disconnected]{background:var(--state-down)}.vpp-io{display:inline-flex;align-items:center;gap:0;margin-left:4px;font-size:13px;font-weight:600;line-height:1;letter-spacing:-.15em;padding-right:.15em}.vpp-io-label{color:var(--fg-muted);font-weight:500;letter-spacing:normal;margin-right:4px}.vpp-tx,.vpp-rx{display:inline-block;color:var(--fg-muted);opacity:.25;transition:color .12s ease-out,opacity .12s ease-out,transform .12s ease-out;transform:translateY(0)}.vpp-tx.lit{color:var(--accent);opacity:1;transform:translateY(-1px)}.vpp-rx.lit{color:var(--accent);opacity:1;transform:translateY(1px)}.kv{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px}.kv dt{color:var(--fg-muted)}.debug-toolbar{display:flex;gap:12px;align-items:center;margin-top:8px;font-size:12px}.debug-toolbar .count{margin-left:auto;color:var(--fg-muted)}.event-tail{max-height:320px;overflow:auto;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:1.5}.event-row{padding:2px 4px;white-space:pre-wrap;word-break:break-all}.event-row.event-backend{color:var(--state-up)}.event-row.event-frontend{color:var(--accent)}.event-row.event-log{color:var(--fg-muted)}.event-row.event-maglevd-status{color:var(--state-down)}.event-row.event-sync{color:var(--state-paused);font-weight:500} diff --git a/cmd/frontend/web/dist/assets/index-CExoCDXh.css b/cmd/frontend/web/dist/assets/index-CExoCDXh.css deleted file mode 100644 index a835d5c..0000000 --- a/cmd/frontend/web/dist/assets/index-CExoCDXh.css +++ /dev/null @@ -1 +0,0 @@ -*,*:before,*:after{box-sizing:border-box}html,body{margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:14px;line-height:1.4;color:var(--fg);background:var(--bg)}h1,h2,h3,h4,p,dl,dd,ol,ul{margin:0;padding:0}ol,ul{list-style:none}a{color:inherit;text-decoration:none}button{font:inherit;color:inherit;background:none;border:1px solid var(--border);border-radius:4px;padding:4px 8px;cursor:pointer}button:hover{background:var(--bg-soft)}table{border-collapse:collapse;width:100%}th,td{text-align:left;padding:4px 8px}code,pre,.mono{font-family:SF Mono,Menlo,Consolas,monospace}:root{--bg: #fafafa;--bg-soft: #f0f0f0;--bg-card: #ffffff;--fg: #0f172a;--fg-muted: #6b7280;--border: #e5e7eb;--accent: #2563eb;--state-up: #16a34a;--state-down: #dc2626;--state-paused: #2563eb;--state-disabled: #6b7280;--state-unknown: #eab308;--state-removed: #374151}.flash-target{display:inline-block;padding:0 4px;border-radius:3px;transform-origin:center center;will-change:transform,background-color,box-shadow}.backend-row td{overflow:visible}.app{max-width:1400px;margin:0 auto;padding:16px}.app-header{display:flex;align-items:center;gap:16px;padding:4px 0;border-bottom:1px solid var(--border);margin-bottom:16px}.brand{display:flex;align-items:center;gap:8px}.brand strong{font-size:18px}.brand-name{color:inherit;text-decoration:none}.brand-name:hover{color:var(--accent)}.brand-logo{display:inline-flex;align-items:center}.brand-logo img{height:56px;width:56px;display:block}.brand-logo:hover img{opacity:.8}.app-header .mode-tag{margin-left:auto;padding:2px 6px;border-radius:3px;background:var(--bg-soft);color:var(--fg-muted);font-size:11px;text-transform:uppercase}.brand .version{margin-left:8px;color:var(--fg-muted);font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;cursor:help}.admin-toggle{padding:4px 10px;border:1px solid var(--border);border-radius:4px;color:var(--accent)}.scope-selector{display:flex;gap:6px;flex-wrap:wrap}.scope-tab{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:20px}.scope-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}.scope-tab .dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--state-down)}.scope-tab.connected .dot{background:var(--state-up)}.status-badge{display:inline-block;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:500;color:#fff;text-transform:capitalize}.status-badge[data-state=up]{background:var(--state-up)}.status-badge[data-state=down]{background:var(--state-down)}.status-badge[data-state=paused]{background:var(--state-paused)}.status-badge[data-state=disabled]{background:var(--state-disabled)}.status-badge[data-state=unknown]{background:var(--state-unknown);color:#1f2937}.status-badge[data-state=removed]{background:var(--state-removed);text-decoration:line-through}.frontend-list{display:flex;flex-direction:column;gap:4px;margin-bottom:12px}.frontend-title{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap}.frontend-title-name{font-size:15px;font-weight:600}.frontend-title-addr{font-family:SF Mono,Menlo,Consolas,monospace;font-size:12px;color:var(--fg-muted)}.frontend-title-proto{font-size:11px;font-weight:600;color:var(--fg-muted);text-transform:uppercase}.frontend-title-desc{font-size:12px;color:var(--fg-muted);font-style:italic;font-weight:400}.tag{display:inline-block;padding:1px 6px;border-radius:3px;background:var(--bg-soft);color:var(--fg-muted);font-size:11px;margin-left:4px}.backend-table{table-layout:fixed;width:100%}.backend-table th,.backend-table td{white-space:nowrap}.backend-table th{font-size:11px;color:var(--fg-muted);text-transform:uppercase;border-bottom:1px solid var(--border);padding:2px 8px}.backend-table .numeric{text-align:right}.backend-table .col-pool{width:10ch}.backend-row .col-pool{color:var(--fg-muted);font-weight:500}.backend-row.pool-standby>td:not(.actions){opacity:.35}.backend-table .col-address{width:42ch}.backend-table .col-state{width:90px}.backend-table .col-weight{width:80px}.backend-table .col-effective{width:95px}.backend-table .col-age{width:110px}.backend-row td.backend-name{overflow:hidden}.backend-name-text{display:inline-block;max-width:calc(100% - 22px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.backend-row td{border-bottom:1px solid var(--border);font-size:13px;padding:2px 8px}.backend-row .backend-name{font-weight:500}.backend-row .backend-address,.backend-row .age{color:var(--fg-muted);font-family:SF Mono,Menlo,Consolas,monospace;font-size:12px}.backend-table th.actions,.backend-row td.actions{width:28px;padding:0 4px;text-align:center}.kebab-wrap{position:relative;display:inline-block}.kebab-btn{width:20px;height:20px;padding:0;line-height:1;font-size:16px;color:var(--fg-muted);border:1px solid transparent;background:transparent;cursor:pointer;border-radius:3px}.kebab-btn:hover,.kebab-btn[aria-expanded=true]{color:var(--fg);background:var(--bg-soft);border-color:var(--border)}.kebab-menu{position:absolute;top:22px;right:0;z-index:20;min-width:120px;background:var(--bg-card);border:1px solid var(--border);border-radius:4px;box-shadow:0 4px 12px #0000001f;padding:4px 0}.kebab-item{display:block;width:100%;padding:6px 12px;border:none;background:transparent;text-align:left;cursor:pointer;font-size:13px;color:var(--fg)}.kebab-item:hover{background:var(--bg-soft)}.kebab-item:disabled{color:var(--fg-muted);cursor:wait}.kebab-error{padding:4px 12px 8px;color:var(--state-down);font-size:11px;font-family:SF Mono,Menlo,Consolas,monospace;max-width:260px;white-space:normal}.modal-backdrop{position:fixed;inset:0;background:#0f172a73;display:flex;align-items:center;justify-content:center;z-index:100;padding:16px}.modal-card{background:var(--bg-card);border:1px solid var(--border);border-radius:6px;box-shadow:0 20px 50px #00000040;width:100%;max-width:480px;display:flex;flex-direction:column}.modal-header{display:flex;align-items:center;padding:12px 16px;border-bottom:1px solid var(--border)}.modal-header h3{margin:0;font-size:15px;flex:1}.modal-close{width:28px;height:28px;padding:0;font-size:20px;line-height:1;border:none;background:transparent;color:var(--fg-muted);cursor:pointer;border-radius:3px}.modal-close:hover{background:var(--bg-soft);color:var(--fg)}.modal-body{padding:16px}.dialog-body{margin-bottom:12px}.dialog-target{margin-bottom:12px;font-size:13px;color:var(--fg-muted)}.dialog-target code{font-family:SF Mono,Menlo,Consolas,monospace;font-size:13px;background:var(--bg-soft);padding:1px 5px;border-radius:3px;color:var(--fg)}.dialog-consequence{font-size:13px;line-height:1.5;color:var(--fg)}.dialog-field{display:flex;flex-direction:column;margin-bottom:12px;font-size:13px}.dialog-field>span{font-weight:500;margin-bottom:4px}.dialog-field input[type=number]{font:inherit;padding:6px 8px;border:1px solid var(--border);border-radius:4px;width:100px}.dialog-field .weight-slider-label{display:flex;align-items:baseline;justify-content:space-between}.dialog-field .weight-slider-value{font-family:SF Mono,Menlo,Consolas,monospace;font-size:18px;font-weight:600;color:var(--accent);min-width:3ch;text-align:right}.dialog-field .weight-slider{width:100%;margin:4px 0;accent-color:var(--accent)}.dialog-field small{margin-top:4px;color:var(--fg-muted);font-size:11px}.dialog-field.checkbox{flex-direction:row;align-items:center;gap:8px}.dialog-field.checkbox>span{margin-bottom:0;font-weight:400}.dialog-note{font-size:12px;color:var(--fg-muted);line-height:1.5;padding:8px 10px;background:var(--bg-soft);border-radius:4px}.dialog-warn{font-size:12px;color:#991b1b;line-height:1.5;padding:8px 10px;background:#fee2e2;border:1px solid #fecaca;border-radius:4px;font-weight:500}.dialog-error{margin-top:8px;padding:8px 10px;background:#fee2e2;color:#991b1b;border-radius:4px;font-family:SF Mono,Menlo,Consolas,monospace;font-size:12px;white-space:pre-wrap;word-break:break-word}.dialog-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:16px;padding-top:12px;border-top:1px solid var(--border)}.btn-primary,.btn-secondary{font:inherit;padding:6px 14px;border-radius:4px;cursor:pointer;border:1px solid var(--border)}.btn-primary{background:var(--accent);color:#fff;border-color:var(--accent)}.btn-primary:hover:not(:disabled){background:var(--accent);filter:brightness(1.12)}.btn-primary.btn-danger{background:var(--state-down);border-color:var(--state-down)}.btn-primary.btn-danger:hover:not(:disabled){background:var(--state-down);filter:brightness(1.12)}.btn-secondary{background:transparent;color:var(--fg)}.btn-secondary:hover:not(:disabled){background:var(--bg-soft)}.btn-primary:disabled,.btn-secondary:disabled{opacity:.6;cursor:wait}.probe-heartbeat{display:inline-block;width:16px;height:14px;line-height:14px;margin-right:6px;text-align:center;font-size:11px;color:var(--state-disabled);overflow:hidden;vertical-align:middle;transform-origin:center center}.probe-heartbeat.in-flight{color:inherit;font-size:10px}.banner{padding:8px 12px;border-radius:4px;margin-bottom:12px;font-size:13px}.banner.warn{background:#fef3c7;color:#92400e}.banner.err{background:#fee2e2;color:#991b1b}.loading,.empty{color:var(--fg-muted);padding:16px}.zippy{border:1px solid var(--border);border-radius:6px;background:var(--bg-card)}.zippy summary{padding:4px 12px;cursor:pointer;font-weight:500}.zippy-body{padding:4px 10px 6px;border-top:1px solid var(--border)}.zippy-title{display:inline-flex;align-items:center;gap:10px}.vpp-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;text-transform:lowercase;color:#fff}.vpp-badge[data-state=connected]{background:var(--state-up)}.vpp-badge[data-state=disconnected]{background:var(--state-down)}.vpp-io{display:inline-flex;align-items:center;gap:0;margin-left:4px;font-size:13px;font-weight:600;line-height:1;letter-spacing:-.15em;padding-right:.15em}.vpp-io-label{color:var(--fg-muted);font-weight:500;letter-spacing:normal;margin-right:4px}.vpp-tx,.vpp-rx{display:inline-block;color:var(--fg-muted);opacity:.25;transition:color .12s ease-out,opacity .12s ease-out,transform .12s ease-out;transform:translateY(0)}.vpp-tx.lit{color:var(--accent);opacity:1;transform:translateY(-1px)}.vpp-rx.lit{color:var(--accent);opacity:1;transform:translateY(1px)}.kv{display:grid;grid-template-columns:max-content 1fr;gap:4px 12px}.kv dt{color:var(--fg-muted)}.debug-toolbar{display:flex;gap:12px;align-items:center;margin-top:8px;font-size:12px}.debug-toolbar .count{margin-left:auto;color:var(--fg-muted)}.event-tail{max-height:320px;overflow:auto;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:1.5}.event-row{padding:2px 4px;white-space:pre-wrap;word-break:break-all}.event-row.event-backend{color:var(--state-up)}.event-row.event-frontend{color:var(--accent)}.event-row.event-log{color:var(--fg-muted)}.event-row.event-maglevd-status{color:var(--state-down)}.event-row.event-sync{color:var(--state-paused);font-weight:500} diff --git a/cmd/frontend/web/dist/assets/index-DCJJqBMY.js b/cmd/frontend/web/dist/assets/index-DCJJqBMY.js new file mode 100644 index 0000000..4aa163d --- /dev/null +++ b/cmd/frontend/web/dist/assets/index-DCJJqBMY.js @@ -0,0 +1 @@ +(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 o of l.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).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 dt=!1,gt=(e,t)=>e===t,G=Symbol("solid-proxy"),Ee=Symbol("solid-track"),de={equals:gt};let Ve=Ye;const V=1,ge=2,Ke={owned:null,cleanups:null,context:null,owner:null};var k=null;let Ae=null,ht=null,_=null,O=null,R=null,ye=0;function le(e,t){const n=_,r=k,s=e.length===0,l=t===void 0?r:t,o=s?Ke:{owned:null,cleanups:null,context:l?l.context:null,owner:l},i=s?e:()=>e(()=>B(()=>oe(o)));k=o,_=null;try{return J(i,!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)),qe(n,s));return[Ge.bind(n),r]}function E(e,t,n){const r=Pe(e,t,!1,V);ce(r)}function K(e,t,n){Ve=_t;const r=Pe(e,t,!1,V);(!n||!n.render)&&(r.user=!0),R?R.push(r):ce(r)}function H(e,t,n){n=n?Object.assign({},de,n):de;const r=Pe(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,ce(r),Ge.bind(r)}function bt(e){return J(e,!1)}function B(e){if(_===null)return e();const t=_;_=null;try{return e()}finally{_=t}}function mt(e,t,n){const r=Array.isArray(e);let s,l=n&&n.defer;return o=>{let i;if(r){i=Array(e.length);for(let c=0;ct(i,s,o));return s=i,a}}function pt(e){K(()=>B(e))}function q(e){return k===null||(k.cleanups===null?k.cleanups=[e]:k.cleanups.push(e)),e}function Ce(){return _}function $t(){return k}function wt(e,t){const n=k,r=_;k=e,_=null;try{return J(t,!0)}catch(s){Ne(s)}finally{k=n,_=r}}function Ge(){if(this.sources&&this.state)if(this.state===V)ce(this);else{const e=O;O=null,J(()=>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 qe(e,t,n){let r=e.value;return(!e.comparator||!e.comparator(r,t))&&(e.value=t,e.observers&&e.observers.length&&J(()=>{for(let s=0;s1e6)throw O=[],new Error},!1)),t}function ce(e){if(!e.fn)return;oe(e);const t=ye;vt(e,e.value,t)}function vt(e,t,n){let r;const s=k,l=_;_=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{_=l,k=s}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&"observers"in e?qe(e,r):e.value=r,e.updatedAt=n)}function Pe(e,t,n,r=V,s){const l={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!==Ke&&(k.owned?k.owned.push(l):k.owned=[l]),l}function he(e){if(e.state===0)return;if(e.state===ge)return be(e);if(e.suspense&&B(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===V)ce(e);else if(e.state===ge){const r=O;O=null,J(()=>be(e,t[0]),!1),O=r}}function J(e,t){if(O)return e();let n=!1;t||(O=[]),R?n=!0:R=[],ye++;try{const r=e();return yt(n),r}catch(r){n||(R=null),O=null,Ne(r)}}function yt(e){if(O&&(Ye(O),O=null),e)return;const t=R;R=null,t.length&&J(()=>Ve(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 kt(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function Ne(e,t=k){throw kt(e)}const St=Symbol("fallback");function Be(e){for(let t=0;t1?[]:null;return q(()=>Be(l)),()=>{let a=e()||[],c=a.length,u,f;return a[Ee],B(()=>{let g,v,w,T,C,p,m,y,S;if(c===0)o!==0&&(Be(l),l=[],r=[],s=[],o=0,i&&(i=[])),n.fallback&&(r=[St],s[0]=le(x=>(l[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--)w[y]=s[m],T[y]=l[m],i&&(C[y]=i[m]);for(g=new Map,v=new Array(y+1),f=y;f>=p;f--)S=a[f],u=g.get(S),v[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?(w[f]=s[u],T[f]=l[u],i&&(C[f]=i[u]),f=v[f],g.set(S,f)):l[u]();for(f=p;fe(t||{}))}const At=e=>`Stale read from <${e}>.`;function Q(e){const t="fallback"in e&&{fallback:()=>e.fallback};return H(xt(()=>e.each,e.children,t||void 0))}function N(e){const t=e.keyed,n=H(()=>e.when,void 0,void 0),r=t?n:H(n,void 0,{equals:(s,l)=>!s==!l});return H(()=>{const s=r();if(s){const l=e.children;return typeof l=="function"&&l.length>0?B(()=>l(t?s:()=>{if(!B(r))throw At("Show");return n()})):l}return e.fallback},void 0,void 0)}const I=e=>H(()=>e());function Et(e,t,n){let r=n.length,s=t.length,l=r,o=0,i=0,a=t[s-1].nextSibling,c=null;for(;ou-i){const v=t[o];for(;i{s=l,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 l=()=>{const i=document.createElement("template");return i.innerHTML=e,i.content.firstChild},o=()=>(s||(s=l())).cloneNode(!0);return o.cloneNode=o,o}function _e(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 Pt(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}),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)&&l(t.host),!0},i=()=>{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();l(a[0]);for(let c=0;c{let i=t();for(;typeof i=="function";)i=i();n=me(e,i,n,r)}),()=>n;if(Array.isArray(t)){const i=[],a=n&&Array.isArray(n);if(Oe(i,t,n,s))return E(()=>n=me(e,i,n,r,!0)),()=>n;if(i.length===0){if(n=X(e,n,r),o)return n}else a?n.length===0?Ue(e,i,r):Et(e,n,i):(n&&X(e),Ue(e,i));n=i}else if(t.nodeType){if(Array.isArray(n)){if(o)return n=X(e,n,r,t);X(e,n,null,t)}else n==null||n===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}}return n}function Oe(e,t,n,r){let s=!1;for(let l=0,o=t.length;l=0;o--){const i=t[o];if(s!==i){const a=i.parentNode===e;!l&&!o?a?e.replaceChild(s,i):e.insertBefore(s,n):a&&i.remove()}else l=!0}}else e.insertBefore(s,n);return[s]}const Nt="http://www.w3.org/2000/svg";function Lt(e,t=!1,n=void 0){return t?document.createElementNS(Nt,e):document.createElement(e,{is:n})}function It(e){const{useShadow:t}=e,n=document.createTextNode(""),r=()=>e.mount||document.body,s=$t();let l;return K(()=>{l||(l=wt(s,()=>H(()=>e.children)));const o=r();if(o instanceof HTMLHeadElement){const[i,a]=A(!1),c=()=>a(!0);le(u=>d(o,()=>i()?u():l(),null)),q(c)}else{const i=Lt(e.isSVG?"g":"div",e.isSVG),a=t&&i.attachShadow?i.attachShadow({mode:"open"}):i;Object.defineProperty(i,"_$host",{get(){return n.parentNode},configurable:!0}),d(a,l),o.appendChild(i),e.ref&&e.ref(i),q(()=>o.removeChild(i))}},void 0,{render:!0}),n}async function Xe(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 Xe("/view/api/state")}function jt(){return Xe("/view/api/version")}const pe=Symbol("store-raw"),z=Symbol("store-node"),M=Symbol("store-has"),ze=Symbol("store-self");function Qe(e){let t=e[G];if(!t&&(Object.defineProperty(e,G,{value:t=new Proxy(e,Rt)}),!Array.isArray(e))){const n=Object.keys(e),r=Object.getOwnPropertyDescriptors(e);for(let s=0,l=n.length;se[G][t]),n}function et(e){Ce()&&ae($e(e,z),ze)()}function Mt(e){return et(e),Reflect.ownKeys(e)}const Rt={get(e,t,n){if(t===pe)return e;if(t===G)return n;if(t===Ee)return et(e),n;const r=$e(e,z),s=r[t];let l=s?s():e[t];if(t===z||t===M||t==="__proto__")return l;if(!s){const o=Object.getOwnPropertyDescriptor(e,t);Ce()&&(typeof l!="function"||e.hasOwnProperty(t))&&!(o&&o.get)&&(l=ae(r,t,l)())}return ee(l)?Qe(l):l},has(e,t){return t===pe||t===G||t===Ee||t===z||t===M||t==="__proto__"?!0:(Ce()&&ae($e(e,M),t)(),t in e)},set(){return!0},deleteProperty(){return!0},ownKeys:Mt,getOwnPropertyDescriptor:Dt};function ne(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[M]&&e[M][t]&&s!==void 0&&e[M][t].$()):(e[t]=n,e[M]&&e[M][t]&&s===void 0&&e[M][t].$());let o=$e(e,z),i;if((i=ae(o,t,s))&&i.$(()=>n),Array.isArray(e)&&e.length!==l){for(let a=e.length;a1){r=t.shift();const o=typeof r,i=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 l=t[0];typeof l=="function"&&(l=l(s,n),l===s)||r===void 0&&l==null||(l=te(l),r===void 0||ee(s)&&ee(l)&&!Array.isArray(l)?tt(s,l):ne(e,r,l))}function Wt(...[e,t]){const n=te(e||{}),r=Array.isArray(n),s=Qe(n);function l(...o){bt(()=>{r&&o.length===1?Bt(n,o[0]):se(n,o)})}return[s,l]}const we=new WeakMap,nt={get(e,t){if(t===pe)return e;const n=e[t];let r;return ee(n)?we.get(n)||(we.set(n,r=new Proxy(n,nt)),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=we.get(t))||we.set(t,n=new Proxy(t,nt)),e(n)}return t}}const[Ut,Ht]=A(0);setInterval(()=>Ht(e=>e+1),5e3);function Le(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,l=!1,o=!0;const i=new Set;for(let a=0;a0&&(s=!0),i.has(c.name)||(i.add(c.name),l=!0,u!=="unknown"&&(o=!1))}!l||o?n.state="unknown":s?n.state="up":n.state="down"}}const[Y,W]=Wt({byName:{},settling:{}}),Ft=2e3,ie=new Map;function Vt(e,t){return`${e}\0${t}`}function rt(e,t){W(F(s=>{s.settling[e]||(s.settling[e]={}),s.settling[e][t]=!0}));const n=Vt(e,t),r=ie.get(n);r&&clearTimeout(r),ie.set(n,setTimeout(()=>{ie.delete(n),W(F(s=>{s.settling[e]&&delete s.settling[e][t]}))},Ft))}function Kt(e){for(const[t,n]of ie)t.startsWith(e+"\0")&&(clearTimeout(n),ie.delete(t));W(F(t=>{t.settling[e]&&(t.settling[e]={})}))}function st(e){const t={};for(const n of e)Le(n),t[n.maglevd.name]=n;W({byName:t})}function Gt(e,t){W(F(r=>{const s=r.byName[e];if(!s)return;const l=s.backends.find(o=>o.name===t.backend);l&&(l.state=t.transition.to,l.enabled=t.transition.to!=="disabled",l.last_transition=t.transition,l.transitions||(l.transitions=[]),l.transitions.push(t.transition),l.transitions.length>20&&(l.transitions=l.transitions.slice(l.transitions.length-20)),Le(s))}));const n=Y.byName[e];if(n)for(const r of n.frontends)r.pools.some(s=>s.backends.some(l=>l.name===t.backend))&&rt(e,r.name)}function qt(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 i of Object.keys(s)){o[i]||(o[i]={});const a=o[i],c=s[i];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 i of Object.keys(o))i in s||delete o[i]})),Kt(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 Xt(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(l=>{const o=l.byName[e];if(!o)return;const i=o.frontends.find(u=>u.name===t);if(!i)return;const a=i.pools.find(u=>u.name===n);if(!a)return;const c=a.backends.find(u=>u.name===r);c&&(c.weight=s,Le(o))})),rt(e,t)}function zt(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],l=!!Y.settling[e.maglevd.name]?.[t.name];let o=!1,i=!1;for(const f of t.pools)for(const $ of f.backends)if(n[$.name]!=="up"&&(o=!0),!l&&r&&$.effective_weight>0){const g=s?.[$.name];(g===void 0||g===0)&&(i=!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&&!i?"ok":i?"bug-buckets":u?"primary-drained":o?"degraded":"unknown"}function Qt(e,t){switch(zt(e,t)){case"ok":return"✅";case"bug-buckets":return"‼️";case"primary-drained":return"❗";case"degraded":return"⚠️";case"unknown":return"❓"}}function en(e,t){return e.includes(":")?`[${e}]:${t}`:`${e}:${t}`}function tn(e){if(Ut(),!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,o=Math.floor(s/60);if(o<1)return`${l}m${r}s ago`;const i=o%24,a=Math.floor(o/24);return a<1?`${o}h${l}m ago`:`${a}d${i}h ago`}const He=500,[ve,nn]=A([]);function rn(e){nn(t=>{const n=[...t,e];return n.length>He?n.slice(n.length-He):n})}const sn=1e4,ln=3e4;function on(){let e,t=!1;const n=i=>{try{const a=JSON.parse(i.data);an(a)}catch(a){console.error("sse parse error",a,i.data)}},r=async()=>{try{const i=await Ze();st(i)}catch(i){console.error("resync refetch failed",i)}},s=()=>{e&&(e.close(),e=void 0),e=new EventSource("/view/api/events"),e.onmessage=n,e.addEventListener("resync",r),e.onerror=i=>{console.debug("sse error, browser will reconnect",i)}},l=i=>{t||(t=!0,console.info("sse reconnecting:",i),s(),setTimeout(()=>{t=!1},1e3))};let o=Date.now();setInterval(()=>{const i=Date.now(),a=i-o;o=i,a>ln&&l(`wake detected (${Math.round(a/1e3)}s gap)`)},sn),s()}function an(e){switch(rn(e),e.type){case"backend":Gt(e.maglevd,e.payload);break;case"frontend":e.maglevd,e.payload;break;case"maglevd-status":Xt(e.maglevd,e.payload);break;case"vpp-status":Jt(e.maglevd,e.payload.state);break;case"lb-state":qt(e.maglevd,e.payload);break}}const[Se,lt]=A(void 0);var cn=b("