Introduces maglev-frontend, a responsive, real-time web dashboard for one
or more running maglevd instances. Source lives at cmd/frontend/; the
built binary is maglev-frontend. It is a single Go process with the
SolidJS SPA embedded via //go:embed — no runtime file dependencies.
Architecture
- One persistent gRPC connection per configured maglevd (-server A,B,C).
Each connection runs three background loops: a WatchEvents stream
subscribed at log_level=debug for live events, a 30s refresh loop as
a safety net for drift, and a 5s health loop that surfaces connection
drops quickly.
- In-process pub/sub broker with a 30s / 2000-event replay ring using
<epoch>-<seq> monotonic IDs. Short browser reconnects (nginx idle,
wifi flap, laptop wake) silently replay buffered events via the
EventSource Last-Event-ID header; longer outages or frontend restarts
fall through to a "resync" event that triggers a full state refetch.
- HTTP surface: /view/ (SPA), /view/api/state, /view/api/state/{name},
/view/api/maglevds, /view/api/version, /view/api/events (SSE),
/healthz, and an /admin/* placeholder returning 501 for a future
basic-auth mutation surface.
- SSE handler follows the full operational checklist: retry hint, 15s
: ping heartbeat, Flush after every write, r.Context().Done() teardown,
X-Accel-Buffering: no, and no gzip.
SolidJS SPA (cmd/frontend/web/, Vite + TypeScript)
- solid-js/store for a reactive per-maglevd state tree; reducers apply
backend transitions, maglevd-status flips, and resync refetches.
- Scope selector tabs for multi-maglevd support, per-maglevd frontend
cards with pool tables showing state, configured weight, effective
weight, and last-transition age.
- ProbeHeartbeat component turns a middle-dot into ❤️ on probe-start and
back on probe-done, driven by real log events; fixed-size wrapper so
the emoji swap doesn't jiggle the row.
- Flash wrapper animates any primitive on change (1s yellow fade via
Web Animations API, skipped on first mount). Wired into the state
badge, configured weight, and effective weight columns.
- DebugPanel: chronological rolling event tail with tail-style auto-
scroll, pause/resume, and scope/firehose filter. Syntactic highlight
for vpp-lb-sync-* events with fixed-order attribute formatting.
- Live effective_weight updates: vpp-lb-sync-as-added/removed/weight-
updated log events are routed through a reducer that walks the
snapshot's pool rows and sets effective_weight on every match
without waiting for the 30s refresh.
- Header shows build version + commit with build date in a tooltip,
fetched once from /view/api/version on mount.
- Prettier wired in as the web-side fixstyle; make fixstyle now tidies
both Go and web in one shot via a new fixstyle-web target.
Per-mutation VPP LB sync logging
- Promotes the addVIP/delVIP/addAS/delAS/setASWeight helpers from
slog.Debug to slog.Info and renames them from vpp-lbsync-* to
vpp-lb-sync-{vip-added,vip-removed,as-added,as-removed,as-weight-
updated}. Matching rename for vpp-lb-sync-start / -done / -error /
-vip-recreate. The Prometheus metric name (maglev_vpp_lbsync_total)
is left alone to preserve dashboards.
- setASWeight now takes the prior weight so the event can emit
from=X to=Y and the UI can show the delta.
- The vip field in every event is the bare address (no /32 or /128
mask), matching the CLI output style.
- Any listener on the gRPC WatchEvents stream — CLI watch events or
maglev-frontend — now sees every VIP/AS dataplane change in real
time without needing to raise the log level.
Build and tooling
- Makefile: maglev-frontend added to BINARIES; build / build-amd64 /
build-arm64 emit the binary alongside maglevd and maglevc. A new
maglev-frontend-web target rebuilds the SolidJS bundle via npm.
- web/dist/ is tracked so a bare `go build` keeps working for Go-only
contributors and CI.
- .gitignore skips cmd/frontend/web/node_modules/.
Stability fixes
- maglevd's WatchEvents synthetic replay events (from==to, at_unix_ns=0)
were corrupting the frontend's LastTransition cache with at=0,
rendering as "20555d ago" in the browser. Client now skips synthetic
events: the cache comes from refreshAll and doesn't need them.
- Frontends, Backends, and HealthChecks are now served in the order
returned by the corresponding List* RPC instead of Go map iteration
order, so reloads and refreshes keep the SPA stable.
2 lines
25 KiB
JavaScript
2 lines
25 KiB
JavaScript
(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 He=!1,qe=(e,t)=>e===t,L=Symbol("solid-proxy"),he=Symbol("solid-track"),Q={equals:qe};let Ae=Ee;const N=1,Y=2,ke={owned:null,cleanups:null,context:null,owner:null};var m=null;let fe=null,Ge=null,b=null,v=null,E=null,ie=0;function z(e,t){const n=b,r=m,s=e.length===0,l=t===void 0?r:t,i=s?ke:{owned:null,cleanups:null,context:l?l.context:null,owner:l},o=s?e:()=>e(()=>C(()=>H(i)));m=i,b=null;try{return V(o,!0)}finally{b=n,m=r}}function k(e,t){t=t?Object.assign({},Q,t):Q;const n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},r=s=>(typeof s=="function"&&(s=s(n.value)),Oe(n,s));return[xe.bind(n),r]}function _(e,t,n){const r=$e(e,t,!1,N);J(r)}function oe(e,t,n){Ae=et;const r=$e(e,t,!1,N);r.user=!0,E?E.push(r):J(r)}function I(e,t,n){n=n?Object.assign({},Q,n):Q;const r=$e(e,t,!0,0);return r.observers=null,r.observerSlots=null,r.comparator=n.equals||void 0,J(r),xe.bind(r)}function Je(e){return V(e,!1)}function C(e){if(b===null)return e();const t=b;b=null;try{return e()}finally{b=t}}function Xe(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 f=0;f<e.length;f++)o[f]=e[f]()}else o=e();if(l)return l=!1,i;const a=C(()=>t(o,s,i));return s=o,a}}function ze(e){oe(()=>C(e))}function Qe(e){return m===null||(m.cleanups===null?m.cleanups=[e]:m.cleanups.push(e)),e}function ge(){return b}function xe(){if(this.sources&&this.state)if(this.state===N)J(this);else{const e=v;v=null,V(()=>ee(this),!1),v=e}if(b){const e=this.observers?this.observers.length:0;b.sources?(b.sources.push(this),b.sourceSlots.push(e)):(b.sources=[this],b.sourceSlots=[e]),this.observers?(this.observers.push(b),this.observerSlots.push(b.sources.length-1)):(this.observers=[b],this.observerSlots=[b.sources.length-1])}return this.value}function Oe(e,t,n){let r=e.value;return(!e.comparator||!e.comparator(r,t))&&(e.value=t,e.observers&&e.observers.length&&V(()=>{for(let s=0;s<e.observers.length;s+=1){const l=e.observers[s],i=fe&&fe.running;i&&fe.disposed.has(l),(i?!l.tState:!l.state)&&(l.pure?v.push(l):E.push(l),l.observers&&Ce(l)),i||(l.state=N)}if(v.length>1e6)throw v=[],new Error},!1)),t}function J(e){if(!e.fn)return;H(e);const t=ie;Ye(e,e.value,t)}function Ye(e,t,n){let r;const s=m,l=b;b=m=e;try{r=e.fn(t)}catch(i){return e.pure&&(e.state=N,e.owned&&e.owned.forEach(H),e.owned=null),e.updatedAt=n+1,Ne(i)}finally{b=l,m=s}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&"observers"in e?Oe(e,r):e.value=r,e.updatedAt=n)}function $e(e,t,n,r=N,s){const l={fn:e,state:r,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:m,context:m?m.context:null,pure:n};return m===null||m!==ke&&(m.owned?m.owned.push(l):m.owned=[l]),l}function Z(e){if(e.state===0)return;if(e.state===Y)return ee(e);if(e.suspense&&C(e.suspense.inFallback))return e.suspense.effects.push(e);const t=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt<ie);)e.state&&t.push(e);for(let n=t.length-1;n>=0;n--)if(e=t[n],e.state===N)J(e);else if(e.state===Y){const r=v;v=null,V(()=>ee(e,t[0]),!1),v=r}}function V(e,t){if(v)return e();let n=!1;t||(v=[]),E?n=!0:E=[],ie++;try{const r=e();return Ze(n),r}catch(r){n||(E=null),v=null,Ne(r)}}function Ze(e){if(v&&(Ee(v),v=null),e)return;const t=E;E=null,t.length&&V(()=>Ae(t),!1)}function Ee(e){for(let t=0;t<e.length;t++)Z(e[t])}function et(e){let t,n=0;for(t=0;t<e.length;t++){const r=e[t];r.user?e[n++]=r:Z(r)}for(t=0;t<n;t++)Z(e[t])}function ee(e,t){e.state=0;for(let n=0;n<e.sources.length;n+=1){const r=e.sources[n];if(r.sources){const s=r.state;s===N?r!==t&&(!r.updatedAt||r.updatedAt<ie)&&Z(r):s===Y&&ee(r,t)}}}function Ce(e){for(let t=0;t<e.observers.length;t+=1){const n=e.observers[t];n.state||(n.state=Y,n.pure?v.push(n):E.push(n),n.observers&&Ce(n))}}function H(e){let t;if(e.sources)for(;e.sources.length;){const n=e.sources.pop(),r=e.sourceSlots.pop(),s=n.observers;if(s&&s.length){const l=s.pop(),i=n.observerSlots.pop();r<s.length&&(l.sourceSlots[i]=r,s[r]=l,n.observerSlots[r]=i)}}if(e.tOwned){for(t=e.tOwned.length-1;t>=0;t--)H(e.tOwned[t]);delete e.tOwned}if(e.owned){for(t=e.owned.length-1;t>=0;t--)H(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 tt(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function Ne(e,t=m){throw tt(e)}const nt=Symbol("fallback");function ye(e){for(let t=0;t<e.length;t++)e[t]()}function rt(e,t,n={}){let r=[],s=[],l=[],i=0,o=t.length>1?[]:null;return Qe(()=>ye(l)),()=>{let a=e()||[],f=a.length,u,c;return a[he],C(()=>{let p,y,w,P,T,S,A,x,D;if(f===0)i!==0&&(ye(l),l=[],r=[],s=[],i=0,o&&(o=[])),n.fallback&&(r=[nt],s[0]=z(Ke=>(l[0]=Ke,n.fallback())),i=1);else if(i===0){for(s=new Array(f),c=0;c<f;c++)r[c]=a[c],s[c]=z(h);i=f}else{for(w=new Array(f),P=new Array(f),o&&(T=new Array(f)),S=0,A=Math.min(i,f);S<A&&r[S]===a[S];S++);for(A=i-1,x=f-1;A>=S&&x>=S&&r[A]===a[x];A--,x--)w[x]=s[A],P[x]=l[A],o&&(T[x]=o[A]);for(p=new Map,y=new Array(x+1),c=x;c>=S;c--)D=a[c],u=p.get(D),y[c]=u===void 0?-1:u,p.set(D,c);for(u=S;u<=A;u++)D=r[u],c=p.get(D),c!==void 0&&c!==-1?(w[c]=s[u],P[c]=l[u],o&&(T[c]=o[u]),c=y[c],p.set(D,c)):l[u]();for(c=S;c<f;c++)c in w?(s[c]=w[c],l[c]=P[c],o&&(o[c]=T[c],o[c](c))):s[c]=z(h);s=s.slice(0,i=f),r=a.slice(0)}return s});function h(p){if(l[c]=p,o){const[y,w]=k(c);return o[c]=w,t(a[c],y)}return t(a[c])}}}function $(e,t){return C(()=>e(t||{}))}const st=e=>`Stale read from <${e}>.`;function q(e){const t="fallback"in e&&{fallback:()=>e.fallback};return I(rt(()=>e.each,e.children,t||void 0))}function ve(e){const t=e.keyed,n=I(()=>e.when,void 0,void 0),r=t?n:I(n,void 0,{equals:(s,l)=>!s==!l});return I(()=>{const s=r();if(s){const l=e.children;return typeof l=="function"&&l.length>0?C(()=>l(t?s:()=>{if(!C(r))throw st("Show");return n()})):l}return e.fallback},void 0,void 0)}const j=e=>I(()=>e());function lt(e,t,n){let r=n.length,s=t.length,l=r,i=0,o=0,a=t[s-1].nextSibling,f=null;for(;i<s||o<l;){if(t[i]===n[o]){i++,o++;continue}for(;t[s-1]===n[l-1];)s--,l--;if(s===i){const u=l<r?o?n[o-1].nextSibling:n[l-o]:a;for(;o<l;)e.insertBefore(n[o++],u)}else if(l===o)for(;i<s;)(!f||!f.has(t[i]))&&t[i].remove(),i++;else if(t[i]===n[l-1]&&n[o]===t[s-1]){const u=t[--s].nextSibling;e.insertBefore(n[o++],t[i++].nextSibling),e.insertBefore(n[--l],u),t[s]=n[l]}else{if(!f){f=new Map;let c=o;for(;c<l;)f.set(n[c],c++)}const u=f.get(t[i]);if(u!=null)if(o<u&&u<l){let c=i,h=1,p;for(;++c<s&&c<l&&!((p=f.get(t[c]))==null||p!==u+h);)h++;if(h>u-o){const y=t[i];for(;o<u;)e.insertBefore(n[o++],y)}else e.replaceChild(n[o++],t[i++])}else i++;else t[i++].remove()}}}const we="_$DX_DELEGATE";function it(e,t,n,r={}){let s;return z(l=>{s=l,t===document?e():d(t,e(),t.firstChild?null:void 0,n)},r.owner),()=>{s(),t.textContent=""}}function g(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 Pe(e,t=window.document){const n=t[we]||(t[we]=new Set);for(let r=0,s=e.length;r<s;r++){const l=e[r];n.has(l)||(n.add(l),t.addEventListener(l,at))}}function M(e,t,n){n==null?e.removeAttribute(t):e.setAttribute(t,n)}function ot(e,t){t==null?e.removeAttribute("class"):e.className=t}function Te(e,t,n){return C(()=>e(t,n))}function d(e,t,n,r){if(n!==void 0&&!r&&(r=[]),typeof t!="function")return te(e,t,r,n);_(s=>te(e,t(),s,n),r)}function at(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 f=t[`${n}Data`];if(f!==void 0?a.call(t,f,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 f=0;f<a.length-2&&(t=a[f],!!i());f++){if(t._$host){t=t._$host,o();break}if(t.parentNode===s)break}}else o();l(r)}function te(e,t,n,r,s){for(;typeof n=="function";)n=n();if(t===n)return n;const l=typeof t,i=r!==void 0;if(e=i&&n[0]&&n[0].parentNode||e,l==="string"||l==="number"){if(l==="number"&&(t=t.toString(),t===n))return n;if(i){let o=n[0];o&&o.nodeType===3?o.data!==t&&(o.data=t):o=document.createTextNode(t),n=B(e,n,r,o)}else n!==""&&typeof n=="string"?n=e.firstChild.data=t:n=e.textContent=t}else if(t==null||l==="boolean")n=B(e,n,r);else{if(l==="function")return _(()=>{let o=t();for(;typeof o=="function";)o=o();n=te(e,o,n,r)}),()=>n;if(Array.isArray(t)){const o=[],a=n&&Array.isArray(n);if(pe(o,t,n,s))return _(()=>n=te(e,o,n,r,!0)),()=>n;if(o.length===0){if(n=B(e,n,r),i)return n}else a?n.length===0?_e(e,o,r):lt(e,n,o):(n&&B(e),_e(e,o));n=o}else if(t.nodeType){if(Array.isArray(n)){if(i)return n=B(e,n,r,t);B(e,n,null,t)}else n==null||n===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}}return n}function pe(e,t,n,r){let s=!1;for(let l=0,i=t.length;l<i;l++){let o=t[l],a=n&&n[e.length],f;if(!(o==null||o===!0||o===!1))if((f=typeof o)=="object"&&o.nodeType)e.push(o);else if(Array.isArray(o))s=pe(e,o,a)||s;else if(f==="function")if(r){for(;typeof o=="function";)o=o();s=pe(e,Array.isArray(o)?o:[o],Array.isArray(a)?a:[a])||s}else e.push(o),s=!0;else{const u=String(o);a&&a.nodeType===3&&a.data===u?e.push(a):e.push(document.createTextNode(u))}}return s}function _e(e,t,n=null){for(let r=0,s=t.length;r<s;r++)e.insertBefore(t[r],n)}function B(e,t,n,r){if(n===void 0)return e.textContent="";const s=r||document.createTextNode("");if(t.length){let l=!1;for(let i=t.length-1;i>=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]}async function je(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 Le(){return je("/view/api/state")}function ct(){return je("/view/api/version")}const ne=Symbol("store-raw"),F=Symbol("store-node"),O=Symbol("store-has"),De=Symbol("store-self");function Be(e){let t=e[L];if(!t&&(Object.defineProperty(e,L,{value:t=new Proxy(e,dt)}),!Array.isArray(e))){const n=Object.keys(e),r=Object.getOwnPropertyDescriptors(e);for(let s=0,l=n.length;s<l;s++){const i=n[s];r[i].get&&Object.defineProperty(e,i,{enumerable:r[i].enumerable,get:r[i].get.bind(t)})}}return t}function R(e){let t;return e!=null&&typeof e=="object"&&(e[L]||!(t=Object.getPrototypeOf(e))||t===Object.prototype||Array.isArray(e))}function U(e,t=new Set){let n,r,s,l;if(n=e!=null&&e[ne])return n;if(!R(e)||t.has(e))return e;if(Array.isArray(e)){Object.isFrozen(e)?e=e.slice(0):t.add(e);for(let i=0,o=e.length;i<o;i++)s=e[i],(r=U(s,t))!==s&&(e[i]=r)}else{Object.isFrozen(e)?e=Object.assign({},e):t.add(e);const i=Object.keys(e),o=Object.getOwnPropertyDescriptors(e);for(let a=0,f=i.length;a<f;a++)l=i[a],!o[l].get&&(s=e[l],(r=U(s,t))!==s&&(e[l]=r))}return e}function re(e,t){let n=e[t];return n||Object.defineProperty(e,t,{value:n=Object.create(null)}),n}function G(e,t,n){if(e[t])return e[t];const[r,s]=k(n,{equals:!1,internal:!0});return r.$=s,e[t]=r}function ft(e,t){const n=Reflect.getOwnPropertyDescriptor(e,t);return!n||n.get||!n.configurable||t===L||t===F||(delete n.value,delete n.writable,n.get=()=>e[L][t]),n}function Ie(e){ge()&&G(re(e,F),De)()}function ut(e){return Ie(e),Reflect.ownKeys(e)}const dt={get(e,t,n){if(t===ne)return e;if(t===L)return n;if(t===he)return Ie(e),n;const r=re(e,F),s=r[t];let l=s?s():e[t];if(t===F||t===O||t==="__proto__")return l;if(!s){const i=Object.getOwnPropertyDescriptor(e,t);ge()&&(typeof l!="function"||e.hasOwnProperty(t))&&!(i&&i.get)&&(l=G(r,t,l)())}return R(l)?Be(l):l},has(e,t){return t===ne||t===L||t===he||t===F||t===O||t==="__proto__"?!0:(ge()&&G(re(e,O),t)(),t in e)},set(){return!0},deleteProperty(){return!0},ownKeys:ut,getOwnPropertyDescriptor:ft};function W(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[O]&&e[O][t]&&s!==void 0&&e[O][t].$()):(e[t]=n,e[O]&&e[O][t]&&s===void 0&&e[O][t].$());let i=re(e,F),o;if((o=G(i,t,s))&&o.$(()=>n),Array.isArray(e)&&e.length!==l){for(let a=e.length;a<l;a++)(o=i[a])&&o.$();(o=G(i,"length",l))&&o.$(e.length)}(o=i[De])&&o.$()}function Me(e,t){const n=Object.keys(t);for(let r=0;r<n.length;r+=1){const s=n[r];W(e,s,t[s])}}function ht(e,t){if(typeof t=="function"&&(t=t(e)),t=U(t),Array.isArray(t)){if(e===t)return;let n=0,r=t.length;for(;n<r;n++){const s=t[n];e[n]!==s&&W(e,n,s)}W(e,"length",r)}else Me(e,t)}function K(e,t,n=[]){let r,s=e;if(t.length>1){r=t.shift();const i=typeof r,o=Array.isArray(e);if(Array.isArray(r)){for(let a=0;a<r.length;a++)K(e,[r[a]].concat(t),n);return}else if(o&&i==="function"){for(let a=0;a<e.length;a++)r(e[a],a)&&K(e,[a].concat(t),n);return}else if(o&&i==="object"){const{from:a=0,to:f=e.length-1,by:u=1}=r;for(let c=a;c<=f;c+=u)K(e,[c].concat(t),n);return}else if(t.length>1){K(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=U(l),r===void 0||R(s)&&R(l)&&!Array.isArray(l)?Me(s,l):W(e,r,l))}function gt(...[e,t]){const n=U(e||{}),r=Array.isArray(n),s=Be(n);function l(...i){Je(()=>{r&&i.length===1?ht(n,i[0]):K(n,i)})}return[s,l]}const se=new WeakMap,Fe={get(e,t){if(t===ne)return e;const n=e[t];let r;return R(n)?se.get(n)||(se.set(n,r=new Proxy(n,Fe)),r):n},set(e,t,n){return W(e,t,U(n)),!0},deleteProperty(e,t){return W(e,t,void 0,!0),!0}};function me(e){return t=>{if(R(t)){let n;(n=se.get(t))||se.set(t,n=new Proxy(t,Fe)),e(n)}return t}}const[le,ae]=gt({byName:{}});function Re(e){const t={};for(const n of e)t[n.maglevd.name]=n;ae({byName:t})}function pt(e,t){ae(me(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.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)))}))}function bt(e,t){ae(me(n=>{const r=n.byName[e];r&&(r.maglevd.connected=t.connected,r.maglevd.last_error=t.last_error)}))}function ue(e,t,n){ae(me(r=>{const s=r.byName[e];if(!s)return;const l=s.backends.find(i=>i.address===t);if(l)for(const i of s.frontends)for(const o of i.pools)for(const a of o.backends)a.name===l.name&&(a.effective_weight=n)}))}function $t(e){if(!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<60)return`${n}s ago`;const r=Math.floor(n/60);if(r<60)return`${r}m ago`;const s=Math.floor(r/60);return s<48?`${s}h ago`:`${Math.floor(s/24)}d ago`}const Se=500,[be,mt]=k([]);function yt(e){mt(t=>{const n=[...t,e];return n.length>Se?n.slice(n.length-Se):n})}function vt(){const e=new EventSource("/view/api/events");return e.onmessage=t=>{try{const n=JSON.parse(t.data);wt(n)}catch(n){console.error("sse parse error",n,t.data)}},e.addEventListener("resync",async()=>{try{const t=await Le();Re(t)}catch(t){console.error("resync refetch failed",t)}}),e.onerror=t=>{console.debug("sse error, browser will reconnect",t)},e}function wt(e){switch(yt(e),e.type){case"backend":pt(e.maglevd,e.payload);break;case"frontend":e.maglevd,e.payload;break;case"maglevd-status":bt(e.maglevd,e.payload);break;case"log":_t(e.maglevd,e.payload);break}}function _t(e,t){if(!t.msg.startsWith("vpp-lb-sync-as-"))return;const n=t.attrs??{},r=n.address;if(r)switch(t.msg){case"vpp-lb-sync-as-added":ue(e,r,Number(n.weight??0));break;case"vpp-lb-sync-as-removed":ue(e,r,0);break;case"vpp-lb-sync-as-weight-updated":ue(e,r,Number(n.to??0));break}}const[ce,Ue]=k(void 0);var St=g("<nav class=scope-selector>"),At=g("<button class=scope-tab><span class=dot>");const kt=()=>{const e=()=>Object.keys(le.byName).sort();return(()=>{var t=St();return d(t,$(q,{get each(){return e()},children:n=>{const r=()=>le.byName[n],s=()=>r()?.maglevd.connected??!1;return(()=>{var l=At();return l.firstChild,l.$$click=()=>Ue(n),d(l,n,null),_(i=>{var o=ce()===n,a=!!s(),f=!s(),u=r()?.maglevd.address??"";return o!==i.e&&l.classList.toggle("active",i.e=o),a!==i.t&&l.classList.toggle("connected",i.t=a),f!==i.a&&l.classList.toggle("disconnected",i.a=f),u!==i.o&&M(l,"title",i.o=u),i},{e:void 0,t:void 0,a:void 0,o:void 0}),l})()}})),t})()};Pe(["click"]);var xt=g("<span class=status-badge>");const Ot=e=>(()=>{var t=xt();return d(t,()=>e.label??e.state),_(()=>M(t,"data-state",e.state)),t})();var Et=g("<span class=probe-heartbeat>");const Ct=e=>{const[t,n]=k(!1);return oe(()=>{const r=be();if(r.length===0)return;const s=r[r.length-1];if(s.type!=="log"||s.maglevd!==e.maglevd)return;const l=s.payload;l.attrs?.backend===e.backend&&(l.msg==="probe-start"?n(!0):l.msg==="probe-done"&&n(!1))}),(()=>{var r=Et();return d(r,()=>t()?"❤️":"·"),_(()=>r.classList.toggle("in-flight",!!t())),r})()};var Nt=g("<span class=flash-target>");const de=e=>{let t;return oe(Xe(()=>e.value,()=>{t?.animate([{backgroundColor:"#fefe27"},{backgroundColor:"transparent"}],{duration:1e3,easing:"ease-out"})},{defer:!0})),(()=>{var n=Nt(),r=t;return typeof r=="function"?Te(r,n):t=n,d(n,()=>e.children??e.value),n})()};var Pt=g("<tr class=backend-row><td class=backend-name></td><td class=backend-address></td><td></td><td class=numeric></td><td class=numeric></td><td class=age>"),Tt=g("<span class=tag>[disabled]");const jt=e=>{const t=()=>e.backend;return(()=>{var n=Pt(),r=n.firstChild,s=r.nextSibling,l=s.nextSibling,i=l.nextSibling,o=i.nextSibling,a=o.nextSibling;return d(r,$(Ct,{get maglevd(){return e.maglevd},get backend(){return t().name}}),null),d(r,()=>t().name,null),d(r,(()=>{var f=j(()=>!t().enabled);return()=>f()&&Tt()})(),null),d(s,()=>t().address),d(l,$(de,{get value(){return t().state},get children(){return $(Ot,{get state(){return t().state}})}})),d(i,$(de,{get value(){return e.poolBackend.weight}})),d(o,$(de,{get value(){return e.poolBackend.effective_weight}})),d(a,()=>$t(t().last_transition)),_(()=>M(n,"data-state",t().state)),n})()};var Lt=g("<section class=frontend-card><header class=frontend-header><h2></h2><div class=frontend-meta><span class=addr>:</span><span class=proto>"),Dt=g("<span class=tag>sticky"),Bt=g("<p class=frontend-desc>"),It=g("<div class=pool-block><h3 class=pool-name>pool: </h3><table class=backend-table><thead><tr><th>backend</th><th>address</th><th>state</th><th class=numeric>weight</th><th class=numeric>effective</th><th>last transition</th></tr></thead><tbody>");const Mt=e=>{const t=()=>Object.fromEntries(e.snap.backends.map(r=>[r.name,r])),n=()=>e.frontend;return(()=>{var r=Lt(),s=r.firstChild,l=s.firstChild,i=l.nextSibling,o=i.firstChild,a=o.firstChild,f=o.nextSibling;return d(l,()=>n().name),d(o,()=>n().address,a),d(o,()=>n().port,null),d(f,()=>n().protocol.toUpperCase()),d(i,(()=>{var u=j(()=>!!n().src_ip_sticky);return()=>u()&&Dt()})(),null),d(s,(()=>{var u=j(()=>!!n().description);return()=>u()&&(()=>{var c=Bt();return d(c,()=>n().description),c})()})(),null),d(r,$(q,{get each(){return n().pools},children:u=>(()=>{var c=It(),h=c.firstChild;h.firstChild;var p=h.nextSibling,y=p.firstChild,w=y.nextSibling;return d(h,()=>u.name,null),d(w,$(q,{get each(){return u.backends},children:P=>{const T=t()[P.name];return T?$(jt,{get maglevd(){return e.snap.maglevd.name},backend:T,poolBackend:P}):null}})),c})()}),null),r})()};var Ft=g("<details class=zippy><summary></summary><div class=zippy-body>");const We=e=>(()=>{var t=Ft(),n=t.firstChild,r=n.nextSibling;return d(n,()=>e.title),d(r,()=>e.children),_(()=>t.open=e.open),t})();var Rt=g("<dl class=kv><dt>version</dt><dd></dd><dt>build date</dt><dd></dd><dt>pid</dt><dd></dd><dt>booted</dt><dd></dd><dt>connected</dt><dd>");const Ut=e=>{if(!e.info)return null;const t=e.info,n=t.boottime_ns?new Date(t.boottime_ns/1e6).toISOString():"",r=t.connecttime_ns?new Date(t.connecttime_ns/1e6).toISOString():"";return $(We,{title:"VPP information",get children(){var s=Rt(),l=s.firstChild,i=l.nextSibling,o=i.nextSibling,a=o.nextSibling,f=a.nextSibling,u=f.nextSibling,c=u.nextSibling,h=c.nextSibling,p=h.nextSibling,y=p.nextSibling;return d(i,()=>t.version),d(a,()=>t.build_date),d(u,()=>t.pid),d(h,n),d(y,r),s}})};var Wt=g("<main class=overview>"),Vt=g("<p class=empty>No maglevd selected."),Kt=g('<div class="banner warn"> disconnected'),Ht=g("<div class=frontend-grid>");const qt=()=>{const e=()=>{const t=ce();return t?le.byName[t]:void 0};return(()=>{var t=Wt();return d(t,$(ve,{get when(){return e()},get fallback(){return Vt()},children:n=>[$(ve,{get when(){return!n().maglevd.connected},get children(){var r=Kt(),s=r.firstChild;return d(r,()=>n().maglevd.name,s),d(r,(()=>{var l=j(()=>!!n().maglevd.last_error);return()=>l()&&`: ${n().maglevd.last_error}`})(),null),r}}),(()=>{var r=Ht();return d(r,$(q,{get each(){return n().frontends},children:s=>$(Mt,{get snap(){return n()},frontend:s})})),r})(),$(Ut,{get info(){return n().vpp_info}})]})),t})()};var Gt=g("<ol class=event-tail>"),Jt=g("<div class=debug-toolbar><label><input type=checkbox>all maglevds</label><button></button><span class=count> events"),Xt=g("<li>");const zt=()=>{const[e,t]=k(!1),[n,r]=k(!1),[s,l]=k([]),i=I(()=>{const f=n()?s():be();if(e())return f;const u=ce();return u?f.filter(c=>c.maglevd===u):f}),o=()=>{n()?r(!1):(l([...be()]),r(!0))};let a;return oe(()=>{i(),!n()&&a&&(a.scrollTop=a.scrollHeight)}),$(We,{title:"Event stream",get children(){return[(()=>{var f=Gt(),u=a;return typeof u=="function"?Te(u,f):a=f,d(f,$(q,{get each(){return i()},children:c=>(()=>{var h=Xt();return d(h,()=>Zt(c)),_(p=>{var y=`event-row event-${c.type}`,w=!!Qt(c);return y!==p.e&&ot(h,p.e=y),w!==p.t&&h.classList.toggle("event-sync",p.t=w),p},{e:void 0,t:void 0}),h})()})),f})(),(()=>{var f=Jt(),u=f.firstChild,c=u.firstChild,h=u.nextSibling,p=h.nextSibling,y=p.firstChild;return c.addEventListener("change",w=>t(w.currentTarget.checked)),h.$$click=o,d(h,()=>n()?"resume":"pause"),d(p,()=>i().length,y),_(()=>c.checked=e()),f})()]}})};function Qt(e){return e.type!=="log"?!1:e.payload.msg.startsWith("vpp-lb-sync-")}function Yt(e){if(!e)return"";const t=["vip","protocol","port","address","weight","from","to","encap","src-ip-sticky","flush"],n=[],r=new Set;for(const s of t)s in e&&(n.push(`${s}=${e[s]}`),r.add(s));for(const[s,l]of Object.entries(e))r.has(s)||n.push(`${s}=${l}`);return n.join(" ")}function Zt(e){const t=new Date(e.at_unix_ns/1e6).toISOString().substring(11,23),n=`[${e.maglevd}]`;switch(e.type){case"backend":{const r=e.payload;return`${t} ${n} backend ${r.backend}: ${r.transition.from} → ${r.transition.to}`}case"frontend":{const r=e.payload;return`${t} ${n} frontend ${r.frontend}: ${r.transition.from} → ${r.transition.to}`}case"log":{const r=e.payload;if(r.msg.startsWith("vpp-lb-sync-"))return`${t} ${n} ${r.msg} ${Yt(r.attrs)}`.trimEnd();const s=r.attrs?Object.entries(r.attrs).map(([l,i])=>`${l}=${i}`).join(" "):"";return`${t} ${n} ${r.level} ${r.msg} ${s}`.trimEnd()}case"maglevd-status":return`${t} ${n} maglevd status: ${JSON.stringify(e.payload)}`;default:return`${t} ${n} ${e.type}`}}Pe(["click"]);var en=g("<div class=app><header class=app-header><div class=brand><strong>maglev</strong></div><span class=mode-tag></span><a class=admin-toggle>"),tn=g("<span class=version> (<!>)"),nn=g('<div class="banner err">'),rn=g("<p class=loading>Loading…");const X=window.location.pathname.startsWith("/admin"),sn=()=>{const[e,t]=k(),[n,r]=k();return ze(async()=>{try{const[s,l]=await Promise.all([Le(),ct()]);Re(s),r(l),!ce()&&s.length>0&&Ue(s[0].maglevd.name),vt()}catch(s){t(`${s}`)}}),(()=>{var s=en(),l=s.firstChild,i=l.firstChild;i.firstChild;var o=i.nextSibling,a=o.nextSibling;return d(i,(()=>{var f=j(()=>!!n());return()=>f()&&(()=>{var u=tn(),c=u.firstChild,h=c.nextSibling;return h.nextSibling,d(u,()=>n().version,c),d(u,()=>n().commit,h),_(()=>M(u,"title",`commit ${n().commit} · built ${n().date}`)),u})()})(),null),d(l,$(kt,{}),o),d(o,X?"admin":"view"),M(a,"href",X?"/view/":"/admin/"),M(a,"title",X?"exit admin mode":"enter admin mode"),d(a,X?"exit admin":"admin…"),d(s,(()=>{var f=j(()=>!!e());return()=>f()&&(()=>{var u=nn();return d(u,e),u})()})(),null),d(s,(()=>{var f=j(()=>!e()&&Object.keys(le.byName).length===0);return()=>f()&&rn()})(),null),d(s,$(qt,{}),null),d(s,$(zt,{}),null),s})()},Ve=document.getElementById("root");if(!Ve)throw new Error("no #root element");it(()=>$(sn,{}),Ve);
|