From facdaed52deb01e5c87bfae31ac1d9720a44e963 Mon Sep 17 00:00:00 2001 From: Henri Koski Date: Thu, 25 Apr 2024 09:45:43 +0300 Subject: [PATCH 1/7] Update header elements --- api/assets/style.css | 273 ++++++++++++++++++++++------------- api/templates/index.templ | 59 ++++---- api/templates/index_templ.go | 12 +- 3 files changed, 214 insertions(+), 130 deletions(-) diff --git a/api/assets/style.css b/api/assets/style.css index 8e6bb76..81194cd 100644 --- a/api/assets/style.css +++ b/api/assets/style.css @@ -76,55 +76,169 @@ body.dark { height: 100vh; display: flex; flex-direction: column; - margin: 0 10px; + margin-right: 10px; + margin-left: 10px; } #header { - display: inline-block; - height: 22px; - margin-top: 10px; + display: flex; + flex-wrap: wrap; + max-height: 53px; + flex-basis: 100%; font-size: 15px; font-weight: 700; - line-height: 18px; letter-spacing: 0em; + margin-top: 18px; } -#header-left { - display: inline-block; - width: 75%; +.dot { + min-width: 10px; + max-width: 10px; + width: 10px; + min-height: 10px; + height: 10px; + max-height: 10px; + display: inline-flex; + border-radius: 10px; } -#header-right { - display: inline-block; - width: 25%; +#logo { + width: 50%; } -#logo { - display: inline-block; - width: 34%; +#live { + width: 50%; + text-align: right; } -#search { - display: inline-block; +#live>.dot { + background-color: #fc4f4f; + animation: blinker 1s step-start infinite; + margin-left: 4px; +} + +@keyframes blinker { + 50% { + opacity: 0; + } +} + +#range { + width: 26%; + display: flex; +} + +.range-selector { + width: 30px; + height: 21px; + background: #EEEEEE; + border-radius: 40px; + border: 1px solid #EEEEEE; text-align: center; - width: 65%; + font-weight: 400; + font-size: 15px; + line-height: 21px; + text-align: center; + cursor: pointer; + margin-right: 5px; +} + +.range-selector.selected { + background: #FFFFFF; + border-radius: 40px; + border: 1px solid #CFCFCF; +} + +body.dark .range-selector { + background: #333333; + border: 1px solid #333333; + color: #FFFFFF; +} + +body.dark .range-selector.selected { + background: #000000; + border: 1px solid #CFCFCF; + color: #FFFFFF; +} + +#mode { + width: 26%; + font-weight: 400; + font-size: 15px; + line-height: 18px; + display: flex; +} + +#mode-wrap { + margin-left: auto; + border: 1px solid #B3B3B3; + border-radius: 40px; + max-height: 21px; + line-height: 21px; + padding: 0 5px; + display: flex; + cursor: pointer; +} + +#mode-left-wrap { + border-right: 1px solid #B3B3B3; + padding-right: 4px; +} + +#mode-right-wrap { + padding-left: 4px; +} + +#bar { + color: #B3B3B3; +} + +#light-dot { + background-color: #000000; +} + +#light { + color: #000000; +} + +#dark-dot { + background-color: #B3B3B3; +} + +body.dark #dark-dot { + background-color: #FFFFFF; +} + +body.dark #dark { + color: #FFFFFF; } -#search > input { +body.dark #light-dot { + background-color: #B3B3B3; +} + +body.dark #light { + color: #B3B3B3; +} + +#search { + width: 48%; +} + +#search>input { border: 0; outline: 0; background: transparent; border-bottom: 1px solid black; color: black; - width: 100%; text-transform: uppercase; font-size: 15px; font-weight: 700; - line-height: 18px; letter-spacing: 0em; + width: 100%; } -body.dark #search > input { +body.dark #search>input { border-bottom: 1px solid white; color: white; } @@ -161,52 +275,17 @@ input::-moz-placeholder { color: black; } -#mode { - display: inline-block; - width: 70%; -} - -#mode > span { - padding-left: 6px; -} - -#live { - display: inline-block; - width: 30%; - text-align: right; -} - -#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; + max-height: 246px; + flex-basis: 100%; } #left-stats, #right-stats { flex-direction: row; - flex-grow: 1; display: flex; + width: 50%; } .number-block { @@ -217,13 +296,15 @@ input::-moz-placeholder { border-radius: 2px; background: #f3f3f3; padding: 20px; + display: flex; + flex-direction: column; } -#left-stats > .number-block:first-child { +#left-stats>.number-block:first-child { margin-left: 0px; } -#right-stats > .number-block:last-child { +#right-stats>.number-block:last-child { margin-right: 0px; } @@ -241,7 +322,7 @@ body.dark .number-block { line-height: 111px; letter-spacing: 0em; text-align: left; - margin-bottom: 10px; + flex-grow: 1; } .number-title { @@ -317,31 +398,31 @@ body.dark .tr:nth-child(even) { background: #333333; } -.left > .td:nth-child(1) { +.left>.td:nth-child(1) { order: 1; } -.left > .td:nth-child(2) { +.left>.td:nth-child(2) { order: 2; } -.right > .td:nth-child(1) { +.right>.td:nth-child(1) { order: 1; } -.right > .td:nth-child(2) { +.right>.td:nth-child(2) { order: 2; } -.left > .th:nth-child(1), -.left > .td:nth-child(1) { +.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) { +.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; @@ -349,8 +430,8 @@ body.dark .tr:nth-child(even) { padding: 0 5px; } -.left > .td:nth-child(2) > div, -.right > .td:nth-child(1) > div { +.left>.td:nth-child(2)>div, +.right>.td:nth-child(1)>div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -416,18 +497,18 @@ body.dark .tag { font-weight: 700; } -#links > a { +#links>a { color: black; text-decoration: none; text-transform: none; padding: 0 10px; } -#links > a:first-child { +#links>a:first-child { padding-left: 0px; } -body.dark #links > a { +body.dark #links>a { color: white; } @@ -439,10 +520,6 @@ body.dark #links > a { color: black; } -body.dark .switch { - color: white; -} - .switch input { opacity: 0; width: 0; @@ -475,15 +552,15 @@ body.dark .switch { border-radius: 50%; } -input:checked + .slider { +input:checked+.slider { background-color: white; } -input:focus + .slider { +input:focus+.slider { box-shadow: 0; } -input:checked + .slider:before { +input:checked+.slider:before { background-color: black; -webkit-transform: translateX(14px); -ms-transform: translateX(14px); @@ -532,8 +609,8 @@ input:checked + .slider:before { width: 75%; } - #header-left > :first-child, - #header-right > :first-child { + #header-left> :first-child, + #header-right> :first-child { padding-bottom: 12px; } @@ -579,11 +656,11 @@ input:checked + .slider:before { height: 44px; } - #footer > #copyright { + #footer>#copyright { order: 2; } - #footer > #links { + #footer>#links { order: 1; } } @@ -594,8 +671,8 @@ input:checked + .slider:before { width: 50%; } - #header-left > :first-child, - #header-right > :first-child { + #header-left> :first-child, + #header-right> :first-child { padding-bottom: 12px; } @@ -613,11 +690,11 @@ input:checked + .slider:before { height: 318px; } - #left-stats > .number-block { + #left-stats>.number-block { margin-left: 0px; } - #right-stats > .number-block { + #right-stats>.number-block { margin-right: 0px; } @@ -651,33 +728,33 @@ input:checked + .slider:before { text-align: left; } - .tr > .left { + .tr>.left { margin-bottom: 12px; } - .left > .td:nth-child(1) { + .left>.td:nth-child(1) { order: 2; min-width: 0px; } - .left > .td:nth-child(2) { + .left>.td:nth-child(2) { order: 1; min-width: 0px; } - .right > .td:nth-child(1) { + .right>.td:nth-child(1) { order: 1; min-width: 0px; } - .right > .td:nth-child(2) { + .right>.td:nth-child(2) { order: 2; min-width: 0px; } } @media (max-width: 544px) { - #links > a { + #links>a { padding: 0 1px; } } diff --git a/api/templates/index.templ b/api/templates/index.templ index 1912d65..35dcdbd 100644 --- a/api/templates/index.templ +++ b/api/templates/index.templ @@ -30,21 +30,21 @@ templ Stats(stats model.Stats, interval time.Duration) {
{ format(stats.RegisteredUsers) }
-
Registered
Users
+
Registered Users
{ format(stats.ProversDeployed) }
-
Provers
Deployed
+
Provers Deployed
{ format(stats.ProofsGenerated) }
-
Proofs
Generated
+
Proofs Generated
{ format(stats.ProofsVerified) }
-
Proof
Verifications
+
Proof Verifications
@@ -137,30 +137,37 @@ templ head() { templ header() {
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Provers Deployed
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -132,7 +132,7 @@ func Stats(stats model.Stats, interval time.Duration) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proofs
Generated
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proofs Generated
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -145,7 +145,7 @@ func Stats(stats model.Stats, interval time.Duration) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proof
Verifications
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proof Verifications
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -359,7 +359,7 @@ func header() templ.Component { templ_7745c5c3_Var16 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Gevulot
Light Dark
Live
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Gevulot
Live
1w 1m 6m 1y
Light
Dark
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -390,7 +390,7 @@ func footer() templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(time.Now().Format("2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 169, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 176, Col: 61} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { From eac7bf339ea9237a3b311e7ab769f7a971689ad9 Mon Sep 17 00:00:00 2001 From: Henri Koski Date: Thu, 25 Apr 2024 12:00:14 +0300 Subject: [PATCH 2/7] Add percentages --- api/assets/style.css | 92 ++++++-------------- api/templates/index.templ | 28 +++++- api/templates/index_templ.go | 164 ++++++++++++++++++++++++----------- model/model.go | 12 ++- store/mock/store.go | 4 + 5 files changed, 176 insertions(+), 124 deletions(-) diff --git a/api/assets/style.css b/api/assets/style.css index 81194cd..4a4f872 100644 --- a/api/assets/style.css +++ b/api/assets/style.css @@ -325,13 +325,35 @@ body.dark .number-block { flex-grow: 1; } +.stat-bottom-row { + display: flex; + flex-direction: row; +} + .number-title { + order: 1; + flex-grow: 1; font-size: 20px; font-weight: 700; line-height: 24px; letter-spacing: 0em; } +.stat-delta { + order: 2; + color: #000000; + padding: 0 5px; + background: #B3FFAB; + border: 1px solid #B3B3B3; + border-radius: 40px; + text-align: center; + font-weight: 400; + font-size: 15px; + line-height: 21px; + max-height: 21px; + margin-top: auto; +} + #table { display: flex; flex-direction: column; @@ -591,81 +613,23 @@ input:checked+.slider:before { .rolling-number { font-size: 60px; } -} - -@media (max-width: 852px) { - #header { - display: inline-block; - margin-top: 10px; - font-size: 15px; - font-weight: 700; - line-height: 18px; - letter-spacing: 0em; - height: 38px; - } - #header-left { - display: inline-block; - width: 75%; - } - - #header-left> :first-child, - #header-right> :first-child { - padding-bottom: 12px; - } - - #header-right { - display: inline-block; - width: 25%; - } - - #logo { - display: inline-block; - width: 100%; - } - - #search { - display: inline-block; - text-align: center; - width: 99%; - } - - #mode { - display: inline-block; - width: 100%; - text-align: right; - } - - #live { - display: inline-block; - width: 100%; - text-align: right; - } - - #stats { - height: 159px; - } - - .rolling-number { - font-size: 40px; - line-height: 48px; - } - - #footer { + .stat-bottom-row { + display: flex; flex-direction: column; - height: 44px; } - #footer>#copyright { + .number-title { order: 2; } - #footer>#links { + .stat-delta { order: 1; + margin-right: auto; } } -@media (max-width: 680px) { +@media (max-width: 800px) { #header-left { display: inline-block; width: 50%; diff --git a/api/templates/index.templ b/api/templates/index.templ index 35dcdbd..6c47663 100644 --- a/api/templates/index.templ +++ b/api/templates/index.templ @@ -30,21 +30,33 @@ templ Stats(stats model.Stats, interval time.Duration) {
{ format(stats.RegisteredUsers) }
-
Registered Users
+
+
Registered Users
+
{ formatPercentage(stats.RegisteredUsersDelta) }
+
{ format(stats.ProversDeployed) }
-
Provers Deployed
+
+
Provers Deployed
+
{ formatPercentage(stats.ProversDeployedDelta) }
+
{ format(stats.ProofsGenerated) }
-
Proofs Generated
+
+
Proofs Generated
+
{ formatPercentage(stats.ProofsGeneratedDelta) }
+
{ format(stats.ProofsVerified) }
-
Proof Verifications
+
+
Proof Verifications
+
{ formatPercentage(stats.ProofsVerifiedDelta) }
+
@@ -193,3 +205,11 @@ func format(i uint64) string { d := (len(s) - 1) / 3 return strings.TrimSpace(s[:((len(s)-1)%3)+1] + suffixes[d:d+1]) } + +func formatPercentage(f float64) string { + sign := "+" + if f < 0 { + sign = "-" + } + return fmt.Sprintf("%s%.2f%s", sign, f, "%") +} diff --git a/api/templates/index_templ.go b/api/templates/index_templ.go index 377e437..7c08ee2 100644 --- a/api/templates/index_templ.go +++ b/api/templates/index_templ.go @@ -106,46 +106,98 @@ func Stats(stats model.Stats, interval time.Duration) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Registered Users
") + _, 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(format(stats.ProversDeployed)) + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(formatPercentage(stats.RegisteredUsersDelta)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 35, Col: 85} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 34, Col: 75} } _, 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
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(format(stats.ProofsGenerated)) + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(format(stats.ProversDeployed)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 41, Col: 85} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 38, Col: 85} } _, 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
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Provers Deployed
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(format(stats.ProofsVerified)) + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(formatPercentage(stats.ProversDeployedDelta)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 45, Col: 83} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 41, Col: 75} } _, 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("
Proof Verifications
") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(format(stats.ProofsGenerated)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 47, Col: 85} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + 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_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(formatPercentage(stats.ProofsGeneratedDelta)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 50, Col: 75} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + 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_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(format(stats.ProofsVerified)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 54, Col: 83} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Proof Verifications
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(formatPercentage(stats.ProofsVerifiedDelta)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 57, Col: 74} + } + _, 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 } @@ -164,9 +216,9 @@ func Table(events []model.Event, query url.Values) templ.Component { 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 + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
State
Transaction ID
Prover ID
Time
State
") 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...) + var templ_7745c5c3_Var13 = []any{"tag", e.State} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -232,7 +284,7 @@ func Row(e model.Event) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var9).String())) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ.CSSClasses(templ_7745c5c3_Var13).String())) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -240,12 +292,12 @@ func Row(e model.Event) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(e.State) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.State) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 79, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 91, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -253,12 +305,12 @@ func Row(e model.Event) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(e.TxID) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(e.TxID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 84, Col: 17} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 96, Col: 17} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -271,12 +323,12 @@ func Row(e model.Event) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Tag) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(e.Tag) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 92, Col: 41} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 104, Col: 41} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -289,12 +341,12 @@ func Row(e model.Event) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.ProverID) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(e.ProverID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 94, Col: 23} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 106, Col: 23} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -302,12 +354,12 @@ func Row(e model.Event) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.Timestamp.Format("03:04 PM, 02/01/06")) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, 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: `templates/index.templ`, Line: 100, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 112, Col: 70} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -330,9 +382,9 @@ func head() templ.Component { 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 + templ_7745c5c3_Var19 := templ.GetChildren(ctx) + if templ_7745c5c3_Var19 == nil { + templ_7745c5c3_Var19 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("Devnet Explorer") @@ -354,9 +406,9 @@ func header() templ.Component { 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 + templ_7745c5c3_Var20 := templ.GetChildren(ctx) + if templ_7745c5c3_Var20 == nil { + templ_7745c5c3_Var20 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Gevulot
Live
1w 1m 6m 1y
Light
Dark
") @@ -378,21 +430,21 @@ func footer() templ.Component { defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var17 := templ.GetChildren(ctx) - if templ_7745c5c3_Var17 == nil { - templ_7745c5c3_Var17 = templ.NopComponent + templ_7745c5c3_Var21 := templ.GetChildren(ctx) + if templ_7745c5c3_Var21 == nil { + templ_7745c5c3_Var21 = templ.NopComponent } ctx = templ.ClearChildren(ctx) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Copyright ©") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(time.Now().Format("2006")) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(time.Now().Format("2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 176, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 188, Col: 61} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -414,3 +466,11 @@ func format(i uint64) string { d := (len(s) - 1) / 3 return strings.TrimSpace(s[:((len(s)-1)%3)+1] + suffixes[d:d+1]) } + +func formatPercentage(f float64) string { + sign := "+" + if f < 0 { + sign = "-" + } + return fmt.Sprintf("%s%.2f%s", sign, f, "%") +} diff --git a/model/model.go b/model/model.go index eecfced..16f5b0a 100644 --- a/model/model.go +++ b/model/model.go @@ -3,10 +3,14 @@ package model import "time" type Stats struct { - RegisteredUsers uint64 `json:"registered_users"` - ProofsGenerated uint64 `json:"proofs_generated"` - ProversDeployed uint64 `json:"programs"` - ProofsVerified uint64 `json:"proofs_verified"` + RegisteredUsers uint64 `json:"registered_users"` + ProofsGenerated uint64 `json:"proofs_generated"` + ProversDeployed uint64 `json:"programs"` + ProofsVerified uint64 `json:"proofs_verified"` + RegisteredUsersDelta float64 `json:"registered_users_delta"` + ProofsGeneratedDelta float64 `json:"proofs_generated_delta"` + ProversDeployedDelta float64 `json:"programs_delta"` + ProofsVerifiedDelta float64 `json:"proofs_verified_delta"` } type Event struct { diff --git a/store/mock/store.go b/store/mock/store.go index a195551..25f162f 100644 --- a/store/mock/store.go +++ b/store/mock/store.go @@ -34,6 +34,10 @@ func (s *Store) Stats() (model.Stats, error) { s.stats.ProofsGenerated += rand.Uint64() % 9000 s.stats.ProofsVerified += rand.Uint64() % 9000 s.stats.RegisteredUsers += rand.Uint64() % 9000 + s.stats.ProversDeployedDelta = rand.Float64() * 100 + s.stats.ProofsGeneratedDelta = rand.Float64() * 100 + s.stats.ProofsVerifiedDelta = rand.Float64() * 100 + s.stats.RegisteredUsersDelta = rand.Float64() * 100 return s.stats, nil } From a4c83a78dc5f2bf97bfc6619265a7f85e15306b4 Mon Sep 17 00:00:00 2001 From: Henri Koski Date: Thu, 25 Apr 2024 12:43:57 +0300 Subject: [PATCH 3/7] Scale stats --- api/assets/style.css | 50 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/api/assets/style.css b/api/assets/style.css index 4a4f872..e9e85f5 100644 --- a/api/assets/style.css +++ b/api/assets/style.css @@ -337,6 +337,7 @@ body.dark .number-block { font-weight: 700; line-height: 24px; letter-spacing: 0em; + padding-top: 10px; } .stat-delta { @@ -605,6 +606,12 @@ input:checked+.slider:before { background: #b556ff; } +@media (max-width: 1500px) { + .number-title { + word-spacing: 1000px; + } +} + @media (max-width: 1200px) { #stats { height: 246px; @@ -630,18 +637,33 @@ input:checked+.slider:before { } @media (max-width: 800px) { - #header-left { - display: inline-block; + #header { + max-height: 90px; + } + + #logo { + order: 1; + width: 50%; + } + + #mode { + order: 2; width: 50%; } - #header-left> :first-child, - #header-right> :first-child { - padding-bottom: 12px; + #search { + order: 3; + width: 100%; + padding: 10px 0px; } - #header-right { - display: inline-block; + #range { + order: 4; + width: 50%; + } + + #live { + order: 5; width: 50%; } @@ -651,7 +673,9 @@ input:checked+.slider:before { } #stats { - height: 318px; + max-height: fit-content; + height: auto; + flex-basis: content; } #left-stats>.number-block { @@ -715,6 +739,16 @@ input:checked+.slider:before { order: 2; min-width: 0px; } + + #footer { + flex-direction: column; + height: auto; + } + + #footer #links { + margin-top: 10px; + } + } @media (max-width: 544px) { From dba37e5848067c3bb4bdf79e63fb631e5d011703 Mon Sep 17 00:00:00 2001 From: Henri Koski Date: Thu, 25 Apr 2024 15:18:33 +0300 Subject: [PATCH 4/7] Add ranged stats cache --- api/api.go | 33 ++++------------- api/assets/style.css | 16 +++++++- api/broadcaster.go | 8 +++- api/broadcaster_test.go | 3 +- api/server.go | 5 +-- api/server_test.go | 2 +- api/templates/index.templ | 26 +++++++++---- api/templates/index_templ.go | 18 +++------ app/app.go | 27 ++++++++++++-- store/cache/stats_cache.go | 71 ++++++++++++++++++++++++++++++++++++ store/mock/store.go | 2 +- store/pg/store.go | 3 +- 12 files changed, 152 insertions(+), 62 deletions(-) create mode 100644 store/cache/stats_cache.go diff --git a/api/api.go b/api/api.go index 3fbb787..8db1bdd 100644 --- a/api/api.go +++ b/api/api.go @@ -19,31 +19,21 @@ var assets embed.FS type Store interface { Search(filter string) ([]model.Event, error) - Stats() (model.Stats, error) + CachedStats(string) model.Stats Events() <-chan model.Event } -type statsCache struct { - ttl time.Duration - updated time.Time - stats model.Stats -} - type API struct { - r *http.ServeMux - s Store - b *Broadcaster - st statsCache + r *http.ServeMux + s Store + b *Broadcaster } -func New(s Store, b *Broadcaster, statsTTL time.Duration) (*API, error) { +func New(s Store, b *Broadcaster) (*API, error) { a := &API{ r: http.NewServeMux(), s: s, b: b, - st: statsCache{ - ttl: statsTTL, - }, } assetsFS, err := fs.Sub(assets, "assets") @@ -80,17 +70,8 @@ func (a *API) index(w http.ResponseWriter, r *http.Request) { } func (a *API) stats(w http.ResponseWriter, r *http.Request) { - if time.Since(a.st.updated) > time.Second*5 { - var err error - if a.st.stats, err = a.s.Stats(); err != nil { - slog.Error("failed to render stats", slog.Any("err", err)) - } - } - - if err := templates.Stats(a.st.stats, a.st.ttl).Render(r.Context(), w); err != nil { - slog.Error("failed to render stats", slog.Any("err", err)) - return - } + sr := r.URL.Query().Get("range") + templates.Stats(a.s.CachedStats(sr)).Render(r.Context(), w) } func (a *API) table(w http.ResponseWriter, r *http.Request) { diff --git a/api/assets/style.css b/api/assets/style.css index e9e85f5..fe26c8a 100644 --- a/api/assets/style.css +++ b/api/assets/style.css @@ -128,6 +128,18 @@ body.dark { display: flex; } +#range input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +#range form { + display: flex; +} + .range-selector { width: 30px; height: 21px; @@ -139,11 +151,11 @@ body.dark { font-size: 15px; line-height: 21px; text-align: center; - cursor: pointer; margin-right: 5px; + cursor: pointer; } -.range-selector.selected { +#range input:checked+label { background: #FFFFFF; border-radius: 40px; border: 1px solid #CFCFCF; diff --git a/api/broadcaster.go b/api/broadcaster.go index a7d7b8e..3456991 100644 --- a/api/broadcaster.go +++ b/api/broadcaster.go @@ -16,7 +16,7 @@ import ( const BufferSize = 50 type Broadcaster struct { - s Store + s EventStream clientsMu sync.Mutex nextID uint64 clients map[uint64]member @@ -33,7 +33,11 @@ type member struct { type Filter func(model.Event) bool -func NewBroadcaster(s Store, retryTimeout time.Duration) *Broadcaster { +type EventStream interface { + Events() <-chan model.Event +} + +func NewBroadcaster(s EventStream, retryTimeout time.Duration) *Broadcaster { return &Broadcaster{ s: s, clients: make(map[uint64]member), diff --git a/api/broadcaster_test.go b/api/broadcaster_test.go index 4744e73..e05127a 100644 --- a/api/broadcaster_test.go +++ b/api/broadcaster_test.go @@ -178,12 +178,11 @@ func TestBroadcasterRetry(t *testing.T) { type MockStore struct { stats model.Stats - statsErr error searchResult []model.Event searchErr error events chan model.Event } -func (m *MockStore) Stats() (model.Stats, error) { return m.stats, m.statsErr } +func (m *MockStore) CachedStats(string) model.Stats { return m.stats } func (m *MockStore) Events() <-chan model.Event { return m.events } func (m *MockStore) Search(string) ([]model.Event, error) { return m.searchResult, m.searchErr } diff --git a/api/server.go b/api/server.go index 719bed9..f2f5225 100644 --- a/api/server.go +++ b/api/server.go @@ -6,15 +6,14 @@ import ( "fmt" "log/slog" "net/http" - "time" ) type Server struct { srv *http.Server } -func NewServer(addr string, s Store, b *Broadcaster, statsTTL time.Duration) (*Server, error) { - a, err := New(s, b, statsTTL) +func NewServer(addr string, s Store, b *Broadcaster) (*Server, error) { + a, err := New(s, b) if err != nil { return nil, fmt.Errorf("failed to create api: %w", err) } diff --git a/api/server_test.go b/api/server_test.go index 0bc593e..fba9103 100644 --- a/api/server_test.go +++ b/api/server_test.go @@ -22,7 +22,7 @@ func TestServerMultipleClients(t *testing.T) { s := &MockStore{events: make(chan model.Event, numOfEvents+1)} b := api.NewBroadcaster(s, time.Second) - srv, err := api.NewServer("127.0.0.1:7645", s, b, time.Hour) + srv, err := api.NewServer("127.0.0.1:7645", s, b) require.NoError(t, err) r := app.NewRunner(b, srv) diff --git a/api/templates/index.templ b/api/templates/index.templ index 6c47663..fdfbeab 100644 --- a/api/templates/index.templ +++ b/api/templates/index.templ @@ -17,7 +17,7 @@ templ Index() {
@header() - @Stats(model.Stats{}, time.Millisecond) + @Stats(model.Stats{}) @Table(nil, url.Values{}) @footer()
@@ -25,8 +25,8 @@ templ Index() { } -templ Stats(stats model.Stats, interval time.Duration) { -
+templ Stats(stats model.Stats) { +
{ format(stats.RegisteredUsers) }
@@ -152,10 +152,22 @@ templ header() {
Live
- 1w - 1m - 6m - 1y +
+ + + + + + + + +