diff --git a/api/api.go b/api/api.go index f4b3031..165fd27 100644 --- a/api/api.go +++ b/api/api.go @@ -1,42 +1,46 @@ package api import ( + "bytes" "embed" "encoding/json" - "fmt" "io/fs" "log/slog" "net/http" + "github.com/a-h/templ" + "github.com/gevulotnetwork/devnet-explorer/api/templates" "github.com/gevulotnetwork/devnet-explorer/model" - "github.com/julienschmidt/httprouter" ) -//go:embed all:public -var public embed.FS +//go:embed all:assets +var assets embed.FS type Store interface { Stats() (model.Stats, error) + Events() <-chan model.Event } type API struct { - r *httprouter.Router + r *http.ServeMux s Store } func New(s Store) (*API, error) { a := &API{ - r: httprouter.New(), + r: http.NewServeMux(), s: s, } - publicFS, err := fs.Sub(public, "public") + assetsFS, err := fs.Sub(assets, "assets") if err != nil { - return nil, fmt.Errorf("failed to create public fs: %w", err) + return nil, err } - a.r.NotFound = http.FileServer(http.FS(publicFS)) - a.r.GET("/api/v1/stats", a.stats) + a.r.Handle("GET /", templ.Handler(templates.Index())) + a.r.HandleFunc("GET /api/v1/stats", a.stats) + a.r.HandleFunc("GET /api/v1/events", a.events) + a.r.Handle("GET /assets/", http.StripPrefix("/assets/", http.FileServer(http.FS(assetsFS)))) return a, nil } @@ -45,7 +49,7 @@ func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.r.ServeHTTP(w, r) } -func (a *API) stats(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { +func (a *API) stats(w http.ResponseWriter, r *http.Request) { stats, err := a.s.Stats() if err != nil { slog.Error("failed to get stats", slog.Any("error", err)) @@ -53,8 +57,46 @@ func (a *API) stats(w http.ResponseWriter, r *http.Request, _ httprouter.Params) return } - if err := json.NewEncoder(w).Encode(stats); err != nil { - slog.Error("failed encode stats", slog.Any("error", err)) + if r.Header.Get("Accept") == "application/json" { + if err := json.NewEncoder(w).Encode(stats); err != nil { + slog.Error("failed encode stats", slog.Any("error", err)) + return + } return } + + if err := templates.Stats(stats).Render(r.Context(), w); err != nil { + slog.Error("failed render stats", slog.Any("error", err)) + return + } +} + +func (a *API) events(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + slog.Info("client connected", slog.String("remote_addr", r.RemoteAddr)) + + for { + select { + case <-r.Context().Done(): // Client disconnected + slog.Info("client disconnected", slog.String("remote_addr", r.RemoteAddr)) + return + case event, ok := <-a.s.Events(): + if !ok { + return + } + + buf := &bytes.Buffer{} + buf.WriteString("data: ") + if err := templates.Row(event).Render(r.Context(), buf); err != nil { + slog.Error("failed render row", slog.Any("error", err)) + return + } + buf.WriteString("\n\n") + buf.WriteTo(w) + w.(http.Flusher).Flush() + } + } } diff --git a/api/assets/Inter-Black.ttf b/api/assets/Inter-Black.ttf new file mode 100644 index 0000000..b27822b Binary files /dev/null and b/api/assets/Inter-Black.ttf differ diff --git a/api/assets/Inter-Bold.ttf b/api/assets/Inter-Bold.ttf new file mode 100644 index 0000000..fe23eeb Binary files /dev/null and b/api/assets/Inter-Bold.ttf differ diff --git a/api/assets/Inter-ExtraBold.ttf b/api/assets/Inter-ExtraBold.ttf new file mode 100644 index 0000000..874b1b0 Binary files /dev/null and b/api/assets/Inter-ExtraBold.ttf differ diff --git a/api/assets/Inter-ExtraLight.ttf b/api/assets/Inter-ExtraLight.ttf new file mode 100644 index 0000000..c993e82 Binary files /dev/null and b/api/assets/Inter-ExtraLight.ttf differ diff --git a/api/assets/Inter-Light.ttf b/api/assets/Inter-Light.ttf new file mode 100644 index 0000000..71188f5 Binary files /dev/null and b/api/assets/Inter-Light.ttf differ diff --git a/api/assets/Inter-Medium.ttf b/api/assets/Inter-Medium.ttf new file mode 100644 index 0000000..a01f377 Binary files /dev/null and b/api/assets/Inter-Medium.ttf differ diff --git a/api/assets/Inter-Regular.ttf b/api/assets/Inter-Regular.ttf new file mode 100644 index 0000000..5e4851f Binary files /dev/null and b/api/assets/Inter-Regular.ttf differ diff --git a/api/assets/Inter-SemiBold.ttf b/api/assets/Inter-SemiBold.ttf new file mode 100644 index 0000000..ecc7041 Binary files /dev/null and b/api/assets/Inter-SemiBold.ttf differ diff --git a/api/assets/Inter-Thin.ttf b/api/assets/Inter-Thin.ttf new file mode 100644 index 0000000..fe77243 Binary files /dev/null and b/api/assets/Inter-Thin.ttf differ diff --git a/api/assets/htmx.min.js b/api/assets/htmx.min.js new file mode 100644 index 0000000..d68f3c6 --- /dev/null +++ b/api/assets/htmx.min.js @@ -0,0 +1 @@ +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:B,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Fr,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.11"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t=false){return new RegExp(`<${e}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${e}>`,t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s(""+n+"
",1);case"col":return s(""+n+"
",2);case"tr":return s(""+n+"
",2);case"td":case"th":return s(""+n+"
",3);case"script":case"style":return s("
"+n+"
",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function F(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function B(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=p(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=p(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=p(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=p(e);e.classList.toggle(t)}function W(e,t){e=p(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=p(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(g(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function p(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:p(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var pe=re().createElement("output");function me(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[pe]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Be(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Fe(r,o,a);Re(o);return Be(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){pt(e)})}},200)}}function pt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function mt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);pt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(!e.htmxExecuted&&Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Ft(o)}for(var l in r){Bt(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=F(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=me(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=me(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return mr(n)}else{return pr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:p(r),returnPromise:true})}else{return he(e,t,p(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:p(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==pe){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var p=ne(n,"hx-sync");var m=null;var x=false;if(p){var F=p.split(":");var B=F[0].trim();if(B==="this"){g=xe(n,"hx-sync")}else{g=ue(n,B)}p=(F[1]||"drop").trim();f=ae(g);if(p==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(p==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(p==="replace"){ce(g,"htmx:abort")}else if(p.indexOf("queue")===0){var V=p.split(" ");m=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(m==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){m=y.triggerSpec.queue}}if(m==null){m="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(m==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=pr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var p=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:p},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;p=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){p=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var m=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!p){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(m)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){m=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Fr(e){delete Xr[e]}function Br(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Br(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); \ No newline at end of file diff --git a/api/assets/sse.js b/api/assets/sse.js new file mode 100644 index 0000000..28c4dd3 --- /dev/null +++ b/api/assets/sse.js @@ -0,0 +1,369 @@ +/* +Server Sent Events Extension +============================ +This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions. + +*/ + +(function() { + + /** @type {import("../htmx").HtmxInternalApi} */ + var api; + + htmx.defineExtension("sse", { + + /** + * Init saves the provided reference to the internal HTMX API. + * + * @param {import("../htmx").HtmxInternalApi} api + * @returns void + */ + init: function(apiRef) { + // store a reference to the internal API. + api = apiRef; + + // set a function in the public API for creating new EventSource objects + if (htmx.createEventSource == undefined) { + htmx.createEventSource = createEventSource; + } + }, + + /** + * onEvent handles all events passed to this extension. + * + * @param {string} name + * @param {Event} evt + * @returns void + */ + onEvent: function(name, evt) { + + var parent = evt.target || evt.detail.elt; + switch (name) { + + case "htmx:beforeCleanupElement": + var internalData = api.getInternalData(parent) + // Try to remove remove an EventSource when elements are removed + if (internalData.sseEventSource) { + internalData.sseEventSource.close(); + } + + return; + + // Try to create EventSources when elements are processed + case "htmx:afterProcessNode": + ensureEventSourceOnElement(parent); + } + } + }); + + /////////////////////////////////////////////// + // HELPER FUNCTIONS + /////////////////////////////////////////////// + + + /** + * createEventSource is the default method for creating new EventSource objects. + * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. + * + * @param {string} url + * @returns EventSource + */ + function createEventSource(url) { + return new EventSource(url, { withCredentials: true }); + } + + function splitOnWhitespace(trigger) { + return trigger.trim().split(/\s+/); + } + + function getLegacySSEURL(elt) { + var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); + if (legacySSEValue) { + var values = splitOnWhitespace(legacySSEValue); + for (var i = 0; i < values.length; i++) { + var value = values[i].split(/:(.+)/); + if (value[0] === "connect") { + return value[1]; + } + } + } + } + + function getLegacySSESwaps(elt) { + var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); + var returnArr = []; + if (legacySSEValue != null) { + var values = splitOnWhitespace(legacySSEValue); + for (var i = 0; i < values.length; i++) { + var value = values[i].split(/:(.+)/); + if (value[0] === "swap") { + returnArr.push(value[1]); + } + } + } + return returnArr; + } + + /** + * registerSSE looks for attributes that can contain sse events, right + * now hx-trigger and sse-swap and adds listeners based on these attributes too + * the closest event source + * + * @param {HTMLElement} elt + */ + function registerSSE(elt) { + // Add message handlers for every `sse-swap` attribute + queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) { + // Find closest existing event source + var sourceElement = api.getClosestMatch(child, hasEventSource); + if (sourceElement == null) { + // api.triggerErrorEvent(elt, "htmx:noSSESourceError") + return null; // no eventsource in parentage, orphaned element + } + + // Set internalData and source + var internalData = api.getInternalData(sourceElement); + var source = internalData.sseEventSource; + + var sseSwapAttr = api.getAttributeValue(child, "sse-swap"); + if (sseSwapAttr) { + var sseEventNames = sseSwapAttr.split(","); + } else { + var sseEventNames = getLegacySSESwaps(child); + } + + for (var i = 0; i < sseEventNames.length; i++) { + var sseEventName = sseEventNames[i].trim(); + var listener = function(event) { + + // If the source is missing then close SSE + if (maybeCloseSSESource(sourceElement)) { + return; + } + + // If the body no longer contains the element, remove the listener + if (!api.bodyContains(child)) { + source.removeEventListener(sseEventName, listener); + return; + } + + // swap the response into the DOM and trigger a notification + if(!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) { + return; + } + swap(child, event.data); + api.triggerEvent(elt, "htmx:sseMessage", event); + }; + + // Register the new listener + api.getInternalData(child).sseEventListener = listener; + source.addEventListener(sseEventName, listener); + } + }); + + // Add message handlers for every `hx-trigger="sse:*"` attribute + queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) { + // Find closest existing event source + var sourceElement = api.getClosestMatch(child, hasEventSource); + if (sourceElement == null) { + // api.triggerErrorEvent(elt, "htmx:noSSESourceError") + return null; // no eventsource in parentage, orphaned element + } + + // Set internalData and source + var internalData = api.getInternalData(sourceElement); + var source = internalData.sseEventSource; + + var sseEventName = api.getAttributeValue(child, "hx-trigger"); + if (sseEventName == null) { + return; + } + + // Only process hx-triggers for events with the "sse:" prefix + if (sseEventName.slice(0, 4) != "sse:") { + return; + } + + // remove the sse: prefix from here on out + sseEventName = sseEventName.substr(4); + + var listener = function() { + if (maybeCloseSSESource(sourceElement)) { + return + } + + if (!api.bodyContains(child)) { + source.removeEventListener(sseEventName, listener); + } + } + }); + } + + /** + * ensureEventSourceOnElement creates a new EventSource connection on the provided element. + * If a usable EventSource already exists, then it is returned. If not, then a new EventSource + * is created and stored in the element's internalData. + * @param {HTMLElement} elt + * @param {number} retryCount + * @returns {EventSource | null} + */ + function ensureEventSourceOnElement(elt, retryCount) { + + if (elt == null) { + return null; + } + + // handle extension source creation attribute + queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) { + var sseURL = api.getAttributeValue(child, "sse-connect"); + if (sseURL == null) { + return; + } + + ensureEventSource(child, sseURL, retryCount); + }); + + // handle legacy sse, remove for HTMX2 + queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) { + var sseURL = getLegacySSEURL(child); + if (sseURL == null) { + return; + } + + ensureEventSource(child, sseURL, retryCount); + }); + + registerSSE(elt); + } + + function ensureEventSource(elt, url, retryCount) { + var source = htmx.createEventSource(url); + + source.onerror = function(err) { + + // Log an error event + api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source }); + + // If parent no longer exists in the document, then clean up this EventSource + if (maybeCloseSSESource(elt)) { + return; + } + + // Otherwise, try to reconnect the EventSource + if (source.readyState === EventSource.CLOSED) { + retryCount = retryCount || 0; + var timeout = Math.random() * (2 ^ retryCount) * 500; + window.setTimeout(function() { + ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1)); + }, timeout); + } + }; + + source.onopen = function(evt) { + api.triggerEvent(elt, "htmx:sseOpen", { source: source }); + } + + api.getInternalData(elt).sseEventSource = source; + } + + /** + * maybeCloseSSESource confirms that the parent element still exists. + * If not, then any associated SSE source is closed and the function returns true. + * + * @param {HTMLElement} elt + * @returns boolean + */ + function maybeCloseSSESource(elt) { + if (!api.bodyContains(elt)) { + var source = api.getInternalData(elt).sseEventSource; + if (source != undefined) { + source.close(); + // source = null + return true; + } + } + return false; + } + + /** + * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. + * + * @param {HTMLElement} elt + * @param {string} attributeName + */ + function queryAttributeOnThisOrChildren(elt, attributeName) { + + var result = []; + + // If the parent element also contains the requested attribute, then add it to the results too. + if (api.hasAttribute(elt, attributeName)) { + result.push(elt); + } + + // Search all child nodes that match the requested attribute + elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) { + result.push(node); + }); + + return result; + } + + /** + * @param {HTMLElement} elt + * @param {string} content + */ + function swap(elt, content) { + + api.withExtensions(elt, function(extension) { + content = extension.transformResponse(content, null, elt); + }); + + var swapSpec = api.getSwapSpecification(elt); + var target = api.getTarget(elt); + var settleInfo = api.makeSettleInfo(elt); + + api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo); + + settleInfo.elts.forEach(function(elt) { + if (elt.classList) { + elt.classList.add(htmx.config.settlingClass); + } + api.triggerEvent(elt, 'htmx:beforeSettle'); + }); + + // Handle settle tasks (with delay if requested) + if (swapSpec.settleDelay > 0) { + setTimeout(doSettle(settleInfo), swapSpec.settleDelay); + } else { + doSettle(settleInfo)(); + } + } + + /** + * doSettle mirrors much of the functionality in htmx that + * settles elements after their content has been swapped. + * TODO: this should be published by htmx, and not duplicated here + * @param {import("../htmx").HtmxSettleInfo} settleInfo + * @returns () => void + */ + function doSettle(settleInfo) { + + return function() { + settleInfo.tasks.forEach(function(task) { + task.call(); + }); + + settleInfo.elts.forEach(function(elt) { + if (elt.classList) { + elt.classList.remove(htmx.config.settlingClass); + } + api.triggerEvent(elt, 'htmx:afterSettle'); + }); + } + } + + function hasEventSource(node) { + return api.getInternalData(node).sseEventSource != null; + } + +})(); diff --git a/api/assets/style.css b/api/assets/style.css new file mode 100644 index 0000000..ee9cfba --- /dev/null +++ b/api/assets/style.css @@ -0,0 +1,413 @@ +@font-face { + font-family: "Inter"; + src: url("./Inter-Thin.ttf") format("truetype"); + font-weight: 100; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-Inter-ExtraLight.ttf") format("truetype"); + font-weight: 200; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-Light.ttf") format("truetype"); + font-weight: 300; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-Regular.ttf") format("truetype"); + font-weight: 400; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-Medium.ttf") format("truetype"); + font-weight: 500; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-SemiBold.ttf") format("truetype"); + font-weight: 600; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-Bold.ttf") format("truetype"); + font-weight: 700; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-ExtraBold.ttf") format("truetype"); + font-weight: 800; +} +@font-face { + font-family: "Inter"; + src: url("./Inter-Black.ttf") format("truetype"); + font-weight: 900; +} + +* { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: Inter; +} + +body { + text-transform: uppercase; + margin: 0; + background-color: #ffffff; + height: 100vh; + min-height: 100vh; +} + +#container { + height: 100vh; + display: flex; + flex-direction: column; + margin: 0 10px; +} + +#header { + display: flex; + flex-grow: 0; + flex-shrink: 0; + height: 22px; + margin-top: 10px; + font-size: 15px; + font-weight: 700; + line-height: 18px; + letter-spacing: 0em; + align-items: center; +} + +#logo { + text-align: left; + flex-grow: 2; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; +} + +#search { + margin: 0 10px; + text-align: center; + flex-grow: 4; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; +} + +#search > input { + border: 0; + outline: 0; + background: transparent; + border-bottom: 1px solid black; + width: 100%; + text-transform: uppercase; + color: black; + font-size: 15px; + font-weight: 700; + line-height: 18px; + letter-spacing: 0em; +} + +*::-webkit-input-placeholder { + color: black; +} +*:-moz-placeholder { + color: black; + opacity: 1; +} +*::-moz-placeholder { + color: black; + opacity: 1; +} +*:-ms-input-placeholder { + color: black; +} +*::-ms-input-placeholder { + color: black; +} + +*::placeholder { + color: black; +} + +#mode { + text-align: center; + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; +} + +#live { + text-align: right; + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; + margin-left: 4px; +} + +#live > #dot { + background-color: #fc4f4f; + min-width: 10px; + max-width: 10px; + width: 10px; + min-height: 10px; + height: 10px; + max-height: 10px; + display: inline-flex; + border-radius: 10px; + margin-left: 4px; + animation: blinker 1s step-start infinite; +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + +#stats { + display: flex; + height: 246px; + margin-top: 20px; +} + +.number-block { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; + border-radius: 2px; + background: #f3f3f3; + padding: 20px; +} + +.number-block + .number-block { + margin-left: 10px; +} + +.rolling-number { + font-size: 92px; + font-weight: 600; + line-height: 111px; + letter-spacing: 0em; + text-align: left; + margin-bottom: 10px; +} + +.number-title { + font-size: 20px; + font-weight: 700; + line-height: 24px; + letter-spacing: 0em; +} + +#table { + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: auto; +} + +.thead { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + font-size: 13px; + font-weight: 600; + line-height: 16px; + letter-spacing: 0em; +} + +.tbody { + height: 100%; + display: flex; + flex-direction: column; + font-size: 15px; + font-weight: 400; + line-height: 18px; + letter-spacing: 0em; + border: 1px solid #eeeeee; + overflow: auto; + scrollbar-color: #b556ff #e6e6e6; + scrollbar-width: 5px; +} + +.left, +.right { + display: flex; + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; +} + +.tr { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + height: 23px; +} + +.tr:nth-child(even) { + background: #eeeeee; +} + +.left > .th:nth-child(1), +.left > .td:nth-child(1) { + min-width: 106px; +} + +.left > .th:nth-child(2), +.left > .td:nth-child(2), +.right > .th:nth-child(1), +.right > .td:nth-child(1) { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0px; + min-width: 0px; + padding: 0 5px; +} + +.left > .td:nth-child(2), +.right > .td:nth-child(1) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.th:nth-child(2), +.td:nth-child(2) { + min-width: 164px; +} + +.th:nth-child(3), +.td:nth-child(3) { + min-width: 12px; +} + +.tag { + border-radius: 40px; + border-style: solid; + border-width: 1px; + padding: 2px; + margin-left: -2px; +} + +.proving { + background: #8db3b5; + border-color: #8db3b5; +} + +.submitted { + background: #b3b3b3; + border-color: #b3b3b3; +} + +.verifying { + background: #d5ea7f; + border-color: #d5ea7f; +} + +.complete { + background: #b556ff; + border-color: #b556ff; +} + +.datetime { + border-radius: 40px; + border-style: solid; + border-width: 1px; + padding: 2px; + border-color: #b3b3b3; +} + +#footer { + display: flex; + flex-grow: 0; + flex-shrink: 0; + height: 18px; + padding: 10px 0; + justify-content: space-between; +} + +#links > a { + color: #000000; + text-decoration: none; + text-transform: none; + padding: 0 10px; +} + +.switch { + position: relative; + display: inline-block; + width: 32px; + height: 18px; +} + +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #000000; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 34px; +} + +.slider:before { + position: absolute; + content: ""; + height: 10px; + width: 10px; + left: 4px; + bottom: 4px; + background-color: #ffffff; + -webkit-transition: 0.4s; + transition: 0.4s; + border-radius: 50%; +} + +input:checked + .slider { + background-color: #ffffff; +} + +input:focus + .slider { + box-shadow: 0; +} + +input:checked + .slider:before { + background-color: #000000; + -webkit-transform: translateX(14px); + -ms-transform: translateX(14px); + transform: translateX(14px); +} + +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: #e6e6e6; +} + +::-webkit-scrollbar-thumb { + background: #b556ff; +} + +::-webkit-scrollbar-thumb:hover { + background: #b556ff; +} diff --git a/api/public/assets/SpaceGrotesk-Bold.ttf b/api/public/assets/SpaceGrotesk-Bold.ttf deleted file mode 100644 index 0408641..0000000 Binary files a/api/public/assets/SpaceGrotesk-Bold.ttf and /dev/null differ diff --git a/api/public/assets/SpaceGrotesk-Light.ttf b/api/public/assets/SpaceGrotesk-Light.ttf deleted file mode 100644 index d41bccc..0000000 Binary files a/api/public/assets/SpaceGrotesk-Light.ttf and /dev/null differ diff --git a/api/public/assets/SpaceGrotesk-Regular.ttf b/api/public/assets/SpaceGrotesk-Regular.ttf deleted file mode 100644 index 981bcf5..0000000 Binary files a/api/public/assets/SpaceGrotesk-Regular.ttf and /dev/null differ diff --git a/api/public/assets/SpaceMono-Regular.ttf b/api/public/assets/SpaceMono-Regular.ttf deleted file mode 100644 index 04e56b9..0000000 Binary files a/api/public/assets/SpaceMono-Regular.ttf and /dev/null differ diff --git a/api/public/assets/gevulot-rain.js b/api/public/assets/gevulot-rain.js deleted file mode 100644 index 887d234..0000000 --- a/api/public/assets/gevulot-rain.js +++ /dev/null @@ -1,175 +0,0 @@ -(function initRAF() { - const vendors = ["webkit", "moz"]; - for (const vendor of vendors) { - if (window.requestAnimationFrame) break; - window.requestAnimationFrame = window[`${vendor}RequestAnimationFrame`]; - window.cancelAnimationFrame = - window[`${vendor}CancelAnimationFrame`] || - window[`${vendor}CancelRequestAnimationFrame`]; - } - - if (!window.requestAnimationFrame) { - let lastTime = 0; - window.requestAnimationFrame = function (callback) { - const currTime = new Date().getTime(); - const timeToCall = Math.max(0, 16 - (currTime - lastTime)); - const id = setTimeout(() => callback(currTime + timeToCall), timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - } - - if (!window.cancelAnimationFrame) { - window.cancelAnimationFrame = (id) => clearTimeout(id); - } -})(); - -class Matrix { - constructor(canvas) { - this.canvas = canvas; - this.updateDimensions(); - this.ctx = canvas.getContext("2d"); - - this.ctx.font = "30px Courier New"; - this.xSpacing = 10; - this.ySpacing = 10; - this.speed = 0.2; - this.devicePixelRatio = window.devicePixelRatio || 1; - - this.yPositions = Array(Math.ceil(this.width / this.xSpacing)) - .fill(0) - .map(() => Math.random() * (this.height / this.ySpacing)); - - this.directions = Array(Math.ceil(this.width / this.xSpacing)) - .fill(0) - .map(() => (Math.random() < 0.5 ? 1 : 1)); // 1 for down, -1 for up - - this.ySpeeds = this.yPositions.map( - () => (Math.random() + 0.2) * this.speed - ); - this.yTimes = this.yPositions.map(() => 0); - this.lastChars = this.yPositions.map(() => " "); - window.addEventListener("mousemove", (e) => this.onMouseMove(e)); - } - - onMouseMove(event) { - const rect = this.canvas.getBoundingClientRect(); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - - const col = Math.floor(x / this.xSpacing); - this.yPositions[col] = y / this.ySpacing; - this.yTimes[col] = 0; // Reset the time for this column - } - - updateDimensions() { - const dpr = window.devicePixelRatio || 1; - this.devicePixelRatio = dpr; // Store the dpr - this.width = window.innerWidth; - this.height = window.innerHeight; - this.canvas.width = this.width * dpr; - this.canvas.height = this.height * dpr; - this.canvas.style.width = `${this.width}px`; - this.canvas.style.height = `${this.height}px`; - } - - draw() { - requestAnimationFrame(this.draw.bind(this)); - - // Drawing logic here - this.ctx.fillStyle = "rgba(0, 0, 0, 0.05)"; - this.ctx.fillRect(0, 0, this.width, this.height); - this.ctx.fillStyle = "#A678ED"; - - const charArr = [ - "⫖", - "⫈", - "⪸", - "⪬", - "⫛", - "⫏", - "⫐", - "⩱", - "⩸", - "⩦", - "⩨", - "⩢", - "⩽", - "⨺", - "⨻", - "⩥", - "⩩", - "⫒", - "⫕", - "⪫", - "⪭", - "⫑", - "⫓", - "⪷", - "⪵", - ]; - - this.yPositions.forEach((y, i) => { - if (this.yTimes[i] > 1) { - const char = charArr[Math.floor(Math.random() * charArr.length)]; - this.lastChars[i] = char; - - this.ctx.fillText( - char, - i * this.xSpacing + 1, - y * this.ySpacing + this.ySpacing - ); - - this.yPositions[i] = - y + this.directions[i] < 0 - ? this.height / this.ySpacing - : y + this.directions[i] >= this.height / this.ySpacing - ? 0 - : y + this.directions[i]; - - this.yTimes[i] = 0; - } - this.yTimes[i] += this.ySpeeds[i]; - }); - } - - start() { - this.draw(); - } - - resize() { - this.updateDimensions(); - this.ctx.setTransform( - this.devicePixelRatio, - 0, - 0, - this.devicePixelRatio, - 0, - 0 - ); - - // You might also want to update the directions array here, if needed - - const columns = Math.ceil(this.width / this.xSpacing); - while (this.yPositions.length < columns) { - this.yPositions.push(Math.random() * (this.height / this.ySpacing)); - this.ySpeeds.push((Math.random() + 0.2) * this.speed); - this.yTimes.push(0); - this.lastChars.push(" "); - this.directions.push(Math.random() < 0.5 ? 1 : 1); - } - - if (this.yPositions.length > columns) { - this.yPositions = this.yPositions.slice(0, columns); - this.ySpeeds = this.ySpeeds.slice(0, columns); - this.yTimes = this.yTimes.slice(0, columns); - this.lastChars = this.lastChars.slice(0, columns); - this.directions = this.directions.slice(0, columns); - } - } -} - -const matrix = new Matrix(document.getElementById("matrixCanvas")); -window.addEventListener("resize", () => matrix.resize()); -matrix.resize(); // Initialize dimensions -matrix.start(); diff --git a/api/public/assets/numbers.js b/api/public/assets/numbers.js deleted file mode 100644 index 1bc1e35..0000000 --- a/api/public/assets/numbers.js +++ /dev/null @@ -1,97 +0,0 @@ -let ticks; -let fresh = true; -const time = 1000; -const minDigits = 6 - -function scrollNumber(counter, digits) { - counter.querySelectorAll('span[data-value]').forEach((tick, i) => { - tick.style.transform = `translateY(-${100 * parseInt(digits[i])}%)`; - }) - - counter.style.width = `${digits.length * 5.1}rem`; -} - -function addDigit(counter, digit, fresh) { - const spanList = Array(10) - .join(0) - .split(0) - .map((x, j) => `${j}`) - .join('') - - counter.insertAdjacentHTML( - "beforeend", - ` - ${spanList} - `) - - const firstDigit = counter.lastElementChild - - setTimeout(() => { - firstDigit.className = "visible"; - }, fresh ? 0 : 2000); -} - -function removeDigit(counter) { - const firstDigit = counter.lastElementChild - firstDigit.classList.remove("visible"); - setTimeout(() => { - firstDigit.remove(); - }, 2000); -} - -function setup(counter) { - console.log(counter) - startNum = 0 - const digits = startNum.toString().split('') - - for (let i = 0; i < minDigits; i++) { - addDigit(counter, '0', true) - } - - scrollNumber(counter, ['0']) - - setTimeout(() => scrollNumber(counter, digits), 2000) - - counter.dataset.value = startNum; -} - -function rollToNumber(idx, num) { - el.style.transform = `translateY(-${100 - num * 10}%)`; -} - -function update(counter, num) { - const toDigits = num.toString().split('') - const fromDigits = counter.dataset.value.toString().split('') - console.log(fromDigits, toDigits) - - for (let i = fromDigits.length - toDigits.length; i > 0; i--) { - removeDigit(counter) - } - for (let i = toDigits.length - fromDigits.length; i > 0; i--) { - addDigit(counter, toDigits[i]); - } - - scrollNumber(counter, toDigits) - counter.dataset.value = num -} - -function fetchData() { - fetch('/api/v1/stats') - .then(res => res.json()) - .then(data => { - for (let key in data) { - console.log(key, data[key]) - update(document.getElementById(key), data[key]) - } - }) - .catch(err => console.error(err)) -} - -function setupCounters() { - for (const element of document.getElementsByClassName('rolling-number')) { - setup(element); - } -} - -setupCounters(); -setInterval(fetchData, 5000) diff --git a/api/public/assets/style.css b/api/public/assets/style.css deleted file mode 100644 index 34480f2..0000000 --- a/api/public/assets/style.css +++ /dev/null @@ -1,196 +0,0 @@ -@font-face { - font-family: "SpaceGrotesk"; - src: url("./SpaceGrotesk-Light.ttf") format("truetype"); - font-weight: 300; - font-display: swap; -} - -@font-face { - font-family: "SpaceGrotesk"; - src: url("./SpaceGrotesk-Regular.ttf") format("truetype"); - font-weight: 400; - font-display: swap; -} - -@font-face { - font-family: "SpaceGrotesk"; - src: url("./SpaceGrotesk-Bold.ttf") format("truetype"); - font-weight: 500; - font-display: swap; -} - -@font-face { - font-family: "SpaceMono"; - src: url("./SpaceMono-Regular.ttf") format("truetype"); - font-weight: 400; - font-display: swap; -} - -* { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - overflow-x: hidden; - margin: 0; - background: #000; - font-family: "SpaceGrotesk", sans-serif; - color: #9973df; -} - -p { - color: #d0b7ff; -} - -header, -main { - font-size: 0.8rem; -} - -.hero { - height: 65vh; - max-width: 600px; - padding: 8rem 2rem; - width: 90%; - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.hero h1 { - text-align: center; - margin-top: 2rem; - padding-top: 2rem; - font-weight: 400; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -main h1, -main h2, -main h3, -main h4, -main h5 { - border-top: 1px solid rgba(255, 255, 255, 0.1); - margin-top: 2rem; - padding-top: 2rem; - font-weight: 400; - letter-spacing: 0.05em; - text-transform: uppercase; - font-family: "SpaceMono", monospace; -} - -.logo-container { - margin: 10px; - width: 70%; - max-width: 7rem; - position: relative; -} - -.logo-container svg { - position: absolute; -} - -.logo-container svg:nth-child(1) { - mix-blend-mode: screen; - margin: -1px 0 0 1px; - filter: blur(3px); - z-index: 3; -} - -.logo-container svg:nth-child(2) { - margin: 0; - z-index: 1; -} - -.logo-container svg:nth-child(3) { - mix-blend-mode: color-dodge; - margin: 2px 0 0 -2px; - filter: blur(3px); - z-index: 3; -} - -canvas { - display: block; - position: fixed; - width: 100vw; - height: 100vh; - top: 0; - left: 0; - z-index: -1; -} - -.animate.animating { - background-color: rgb(173, 134, 250); - color: black; - transition: background-color 0.2s, color 0.2s; -} - -.animate.animating-done { - background-color: rgba(188, 172, 219, 0); - display: inline-block; - color: #9973df; -} - -.animate { - font-family: "SpaceMono", monospace; - /* display: flex; - flex-wrap: wrap; */ -} - -.animate .word { - white-space: nowrap; -} - -.animate .animating, -.animate .animating-done { - display: inline-block; - width: 1ch; -} - -.animated-header { - font-size: 4rem; -} - -.devnet { - text-align: center; - display: flex; -} - -.number-block { - margin: 0rem 4rem; -} - -/* Roll CSS */ -.rolling-number { - position: relative; - display: flex; - margin-right: 0.2em; - width: 0; - overflow: hidden; - height: 8rem; - transition: width 1.8s ease; -} - -.rolling-number>span { - display: flex; - text-align: center; - flex-direction: column; - opacity: 0; - flex-shrink: 2; - flex-basis: 8rem; - position: absolute; - right: 0; - line-height: 8rem; - transition: all 2s ease; - font-size: 8rem; -} - -.rolling-number>span.visible { - position: static; - opacity: 1; - flex-shrink: 1; -} diff --git a/api/public/assets/typein.js b/api/public/assets/typein.js deleted file mode 100644 index 1aa9c23..0000000 --- a/api/public/assets/typein.js +++ /dev/null @@ -1,104 +0,0 @@ -const charArr = [ - "⫖", - "⫈", - "⪸", - "⪬", - "⫛", - "⫏", - "⫐", - "⩱", - "⩸", - "⩦", - "⩨", - "⩢", - "⩽", - "⨺", - "⨻", - "⩥", - "⩩", - "⫒", - "⫕", - "⪫", - "⪭", - "⫑", - "⫓", - "⪷", - "⪵", -]; - -let totalDuration = 400; // Total time for the animation in milliseconds - -function randomChar() { - return charArr[Math.floor(Math.random() * charArr.length)]; -} -function animateElement(target) { - let originalText = target.textContent; - - // Remove leading and trailing whitespaces - originalText = originalText.trim(); - - // Replace multiple whitespaces (including tabs and newlines) with a single space - originalText = originalText.replace(/\s+/g, " "); - - target.textContent = ""; - - let wordList = originalText.split(" "); - let flatCharacters = []; - - wordList.forEach((word, wordIndex) => { - const wordSpan = document.createElement("span"); - wordSpan.className = "word"; - - Array.from(word).forEach((char, charIndex) => { - let charSpan = document.createElement("span"); - charSpan.classList.add("animating"); - charSpan.style.opacity = "0.0"; - charSpan.textContent = randomChar(); - wordSpan.appendChild(charSpan); - - flatCharacters.push({ - wordIndex, - charIndex, - char, - }); - }); - - target.appendChild(wordSpan); - - // Don't add a space after the last word - if (wordIndex < wordList.length - 1) { - target.appendChild(document.createTextNode(" ")); - } - }); - - function reveal(index) { - let wordIndex = flatCharacters[index].wordIndex; - let charIndex = flatCharacters[index].charIndex; - let targetChar = flatCharacters[index].char; - - let span = target.querySelectorAll(".word")[wordIndex].children[charIndex]; - span.style.opacity = "1.0"; - - let interval = setInterval(() => { - span.textContent = randomChar(); - }, 50); - - setTimeout(() => { - clearInterval(interval); - span.textContent = targetChar; - span.classList.remove("animating"); - span.classList.add("animating-done"); - span.style.opacity = "1.0"; - }, 1000); - } - - for (let i = 0; i < flatCharacters.length; i++) { - let randomTime = Math.random() * totalDuration; - setTimeout(() => reveal(i), randomTime); - } -} - -const elementsToAnimate = document.querySelectorAll(".animate"); -elementsToAnimate.forEach((element) => { - animateElement(element); -}); diff --git a/api/public/index.html b/api/public/index.html deleted file mode 100644 index 79a2168..0000000 --- a/api/public/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - Devnet Explorer - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

Devnet explorer -

-
-
-

-

Registered Users

-
-
-

-

Proofs Generated

-
-
-

-

Programs

-
-
-

-

Proofs Verified

-
-
-
- Canvas is not supported in - your browser. - Canvas is not supported in your browser. - - - - - - - diff --git a/api/templates/index.templ b/api/templates/index.templ new file mode 100644 index 0000000..bafceb5 --- /dev/null +++ b/api/templates/index.templ @@ -0,0 +1,126 @@ +package templates + +import "github.com/gevulotnetwork/devnet-explorer/model" +import "strconv" + +templ Index() { + + + @head() + +
+ @header() + @Stats(model.Stats{}) + @Table(nil) + @footer() +
+ + +} + +templ Stats(stats model.Stats) { +
+
+
{ strconv.Itoa(int(stats.RegisteredUsers)) }
+
Registered
Users
+
+
+
{ strconv.Itoa(int(stats.ProversDeployed)) }
+
Provers
Deployed
+
+
+
{ strconv.Itoa(int(stats.ProofsGenerated)) }
+
Proofs
Generated
+
+
+
{ strconv.Itoa(int(stats.ProofsVerified)) }
+
Proofs
Verified
+
+
+} + +templ Table(events []model.Event) { +
+
+
+
State
+
Transaction ID
+
+
+
Prover ID
+
Time
+
+
+
+
+ for _, e := range events { + @Row(e) + } +
+
+} + +templ Row(e model.Event) { +
{ e.State }
{ e.TxID }
{ e.ProverID }
{ e.Timestamp.Format("03:04 PM, 02/01/06") }
+} + +templ head() { + + + + + + + + + + + + + + + + + + + + + + + Devnet Explorer + + + + +} + +templ header() { + +} + +templ footer() { + +} diff --git a/api/templates/index_templ.go b/api/templates/index_templ.go new file mode 100644 index 0000000..17003de --- /dev/null +++ b/api/templates/index_templ.go @@ -0,0 +1,341 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.598 +package templates + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +import "github.com/gevulotnetwork/devnet-explorer/model" +import "strconv" + +func Index() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = head().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = header().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Stats(model.Stats{}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Table(nil).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = footer().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func Stats(stats model.Stats) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(stats.RegisteredUsers))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 23, Col: 95} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Registered
Users
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(stats.ProversDeployed))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 27, Col: 95} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Provers
Deployed
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(stats.ProofsGenerated))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 31, Col: 95} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proofs
Generated
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(int(stats.ProofsVerified))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 35, Col: 93} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proofs
Verified
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func Table(events []model.Event) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
State
Transaction ID
Prover ID
Time
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, e := range events { + templ_7745c5c3_Err = Row(e).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func Row(e model.Event) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 = []any{"tag", e.State} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(e.State) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 63, Col: 91} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(e.TxID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 63, Col: 130} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.ProverID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 63, Col: 191} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.Timestamp.Format("03:04 PM, 02/01/06")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `api/templates/index.templ`, Line: 63, Col: 280} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func head() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Devnet Explorer") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func header() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Gevolut
Light Dark
Live
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func footer() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var16 := templ.GetChildren(ctx) + if templ_7745c5c3_Var16 == nil { + templ_7745c5c3_Var16 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Copyright ©2024 - Gevulot
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/app/app.go b/app/app.go index 9fe4171..e837639 100644 --- a/app/app.go +++ b/app/app.go @@ -10,37 +10,40 @@ import ( "github.com/gevulotnetwork/devnet-explorer/api" "github.com/gevulotnetwork/devnet-explorer/signalhandler" - "github.com/gevulotnetwork/devnet-explorer/store/cache" "github.com/gevulotnetwork/devnet-explorer/store/mock" "github.com/gevulotnetwork/devnet-explorer/store/pg" ) +type Store interface { + api.Store + Runnable +} + // Run starts the application and listens for OS signals to gracefully shutdown. func Run(args ...string) error { conf := ParseConfig(args...) - s, err := createStore(conf) - if err != nil { - return fmt.Errorf("failed to create store: %w", err) + + var s Store + if conf.MockStore { + s = mock.New() + } else { + var err error + s, err = pg.New(conf.DSN) + if err != nil { + return fmt.Errorf("failed to create store: %w", err) + } } - c := cache.New(s, conf.CacheRefreshInterval) - srv, err := api.NewServer(conf.ServerListenAddr, c) + srv, err := api.NewServer(conf.ServerListenAddr, s) if err != nil { return fmt.Errorf("failed to api server: %w", err) } sh := signalhandler.New(os.Interrupt) - r := NewRunner(sh, srv, c) + r := NewRunner(s, srv, sh) return r.Run() } -func createStore(c Config) (api.Store, error) { - if c.MockStore { - return mock.New(), nil - } - return pg.New(c.DSN) -} - type Config struct { ServerListenAddr string DSN string diff --git a/go.mod b/go.mod index d90e7e4..286b5c6 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/gevulotnetwork/devnet-explorer go 1.22.0 require ( + github.com/a-h/templ v0.2.598 github.com/go-gorp/gorp/v3 v3.1.0 github.com/golangci/golangci-lint v1.56.2 github.com/hashicorp/go-multierror v1.1.1 github.com/jackc/pgx/v5 v5.4.3 - github.com/julienschmidt/httprouter v1.3.0 github.com/magefile/mage v1.14.0 github.com/stretchr/testify v1.8.4 github.com/testcontainers/testcontainers-go/modules/compose v0.28.0 @@ -98,7 +98,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/firefart/nonamedreturns v1.0.4 // indirect github.com/fsnotify/fsevents v0.1.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.4 // indirect @@ -305,9 +305,9 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.19.0 // indirect go.opentelemetry.io/otel/trace v1.19.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect - go.uber.org/atomic v1.7.0 // indirect + go.uber.org/atomic v1.10.0 // indirect go.uber.org/mock v0.4.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect diff --git a/go.sum b/go.sum index bf2303e..6713046 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.0/go.mod h1:CIzddKRvLBC4Au5aYP/i3nyaWQ+C github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo= +github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/go-check-sumtype v0.1.4 h1:WCvlB3l5Vq5dZQTFmodqL2g68uHiSwwlWcT5a2FGK0c= @@ -287,8 +289,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsevents v0.1.1 h1:/125uxJvvoSDDBPen6yUZbil8J9ydKZnnl3TWWmvnkw= github.com/fsnotify/fsevents v0.1.1/go.mod h1:+d+hS27T6k5J8CRaPLKFgwKYcpS7GwW3Ule9+SC2ZRc= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fvbommel/sortorder v1.0.2 h1:mV4o8B2hKboCdkJm+a7uX/SIpZob4JzUpc5GGnM45eo= github.com/fvbommel/sortorder v1.0.2/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= @@ -543,7 +545,6 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.1.0 h1:F78HnrsjY3cR7j0etXy5+TU1Zuy7Xt08X/1aJnH5xXY= github.com/julz/importas v0.1.0/go.mod h1:oSFU2R4XK/P7kNBrnL/FEQlDGN1/6WoxXEjSSXO0DV0= @@ -984,15 +985,15 @@ go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmY go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= @@ -1161,7 +1162,6 @@ golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/integrationtests/integration_test.go b/integrationtests/integration_test.go index f20a5de..6985bd3 100644 --- a/integrationtests/integration_test.go +++ b/integrationtests/integration_test.go @@ -24,14 +24,19 @@ func TestIntegration(t *testing.T) { time.Sleep(1 * time.Second) for _, test := range []func(*testing.T){ - testEmptyStats, + testEmptyStatsJSON, + testEmptyStatsHTML, } { t.Run(testName(test), test) } } -func testEmptyStats(t *testing.T) { - resp, err := http.Get("http://127.0.0.1:8383/api/v1/stats") +func testEmptyStatsJSON(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8383/api/v1/stats", nil) + require.NoError(t, err) + + r.Header.Set("Accept", "application/json") + resp, err := (&http.Client{}).Do(r) require.NoError(t, err) data, err := io.ReadAll(resp.Body) @@ -40,3 +45,18 @@ func testEmptyStats(t *testing.T) { const expectedResp = `{"registered_users":0,"programs":0,"proofs_generated":0,"proofs_verified":0}` require.JSONEq(t, expectedResp, string(data)) } + +func testEmptyStatsHTML(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8383/api/v1/stats", nil) + require.NoError(t, err) + + r.Header.Set("Accept", "test/html") + resp, err := (&http.Client{}).Do(r) + require.NoError(t, err) + + data, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + const expectedResp = `
0
Registered
Users
0
Provers
Deployed
0
Proofs
Generated
0
Proofs
Verified
` + require.Equal(t, expectedResp, string(data)) +} diff --git a/model/model.go b/model/model.go index 896b7ca..e016484 100644 --- a/model/model.go +++ b/model/model.go @@ -1,8 +1,17 @@ package model +import "time" + type Stats struct { RegisteredUsers int64 `json:"registered_users"` ProofsGenerated int64 `json:"proofs_generated"` - Programs int64 `json:"programs"` + ProversDeployed int64 `json:"programs"` ProofsVerified int64 `json:"proofs_verified"` } + +type Event struct { + State string `json:"state"` + TxID string `json:"tx_id"` + ProverID string `json:"prover_id"` + Timestamp time.Time `json:"timestamp"` +} diff --git a/store/cache/cache.go b/store/cache/cache.go deleted file mode 100644 index dc83629..0000000 --- a/store/cache/cache.go +++ /dev/null @@ -1,65 +0,0 @@ -package cache - -import ( - "log/slog" - "sync/atomic" - "time" - - "github.com/gevulotnetwork/devnet-explorer/model" -) - -type Store interface { - Stats() (model.Stats, error) -} - -type Cache struct { - s Store - stats atomic.Value - interval time.Duration - stop chan struct{} -} - -func New(s Store, interval time.Duration) *Cache { - c := &Cache{ - s: s, - interval: interval, - stop: make(chan struct{}), - } - c.refreshStats() - return c -} - -func (c *Cache) Stats() (model.Stats, error) { - stats := c.stats.Load().(model.Stats) - return stats, nil -} - -func (c *Cache) Run() error { - slog.Info("starting cache", slog.String("refresh_interval", c.interval.String())) - t := time.NewTicker(c.interval) - defer t.Stop() - - for { - select { - case <-t.C: - c.refreshStats() - case <-c.stop: - return nil - } - } -} - -func (c *Cache) refreshStats() { - stats, err := c.s.Stats() - if err != nil { - slog.Error("cache failed to refresh stats", slog.Any("error", err)) - return - } - c.stats.Store(stats) -} - -func (c *Cache) Stop() error { - slog.Info("stopping cache") - close(c.stop) - return nil -} diff --git a/store/mock/store.go b/store/mock/store.go index f9d67b4..be1d8a8 100644 --- a/store/mock/store.go +++ b/store/mock/store.go @@ -2,26 +2,66 @@ package mock import ( + "crypto/sha512" + "encoding/hex" "math/rand" + "time" "github.com/gevulotnetwork/devnet-explorer/model" _ "github.com/jackc/pgx/v5/stdlib" ) type Store struct { - stats model.Stats + stats model.Stats + events chan model.Event + done chan struct{} } func New() *Store { return &Store{ - stats: model.Stats{}, + stats: model.Stats{}, + events: make(chan model.Event, 1000), + done: make(chan struct{}), } } func (s *Store) Stats() (model.Stats, error) { - s.stats.Programs += rand.Int63n(10) + s.stats.ProversDeployed += rand.Int63n(10) s.stats.ProofsGenerated += rand.Int63n(10) s.stats.ProofsVerified += rand.Int63n(10) s.stats.RegisteredUsers += rand.Int63n(10) return s.stats, nil } + +func (s *Store) Run() error { + defer close(s.events) + for { + select { + case <-s.done: + return nil + case s.events <- randomEvent(): + } + time.Sleep(5 * time.Second) + } +} + +func (s *Store) Events() <-chan model.Event { + return s.events +} + +func (s *Store) Stop() error { + close(s.done) + close(s.events) + return nil +} + +func randomEvent() model.Event { + txID := sha512.Sum512([]byte(time.Now().String())) + proverID := sha512.Sum512([]byte(time.Now().String())) + return model.Event{ + State: []string{"submitted", "verifying", "proving", "complete"}[rand.Intn(4)], + TxID: hex.EncodeToString(txID[:]), + ProverID: hex.EncodeToString(proverID[:]), + Timestamp: time.Now(), + } +} diff --git a/store/pg/store.go b/store/pg/store.go index 511305f..39605f2 100644 --- a/store/pg/store.go +++ b/store/pg/store.go @@ -10,7 +10,9 @@ import ( ) type Store struct { - db *gorp.DbMap + db *gorp.DbMap + events chan model.Event + done chan struct{} } func New(dsn string) (*Store, error) { @@ -20,15 +22,34 @@ func New(dsn string) (*Store, error) { } return &Store{ - db: &gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}, + db: &gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}, + events: make(chan model.Event, 1000), + done: make(chan struct{}), }, nil } +func (s *Store) Run() error { + defer close(s.events) + eventSource := make(chan model.Event) + for { + select { + case <-s.done: + return nil + case e := <-eventSource: + select { + case <-s.done: + return nil + case s.events <- e: + } + } + } +} + func (s *Store) Stats() (model.Stats, error) { const query = ` SELECT (SELECT COUNT(*) FROM acl_whitelist) as RegisteredUsers, - (SELECT COUNT(*)/2 FROM program) as Programs, + (SELECT COUNT(*)/2 FROM program) as ProofsGenerated, (SELECT COUNT(*) FROM transaction WHERE kind = 'proof' AND executed IS TRUE) as ProofsGenerated, (SELECT COUNT(*) FROM transaction WHERE kind = 'proof' AND executed IS TRUE) as ProofsVerified;` @@ -39,3 +60,12 @@ func (s *Store) Stats() (model.Stats, error) { return stats, nil } + +func (s *Store) Events() <-chan model.Event { + return s.events +} + +func (s *Store) Stop() error { + close(s.done) + return nil +}