diff --git a/api/api.go b/api/api.go
index f4b3031..15e9a7a 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,48 @@ 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")
+ if _, err := buf.WriteTo(w); err != nil {
+ slog.Error("failed send event", slog.Any("error", err))
+ }
+ 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/
"+n+"",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("",1);case"col":return s("",2);case"tr":return s("",2);case"td":case"th":return s("",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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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
+
+
+
+
+ for _, e := range events {
+ @Row(e)
+ }
+
+
+}
+
+templ Row(e model.Event) {
+ { 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("")
+ 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("")
+ 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("")
+ 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 = ``
+ 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
+}