From a254e943f79fbb78e27d1d6fece04eba71e991c5 Mon Sep 17 00:00:00 2001 From: Jamie Holdstock Date: Tue, 26 Sep 2023 17:18:32 +0100 Subject: [PATCH] webapi: Add missed tickets to admin page. A new tab on the admin page displays a list of all tickets which were registered with the VSP but missed their votes. Clicking on the ticket hash redirects to the Ticket Search tab with the details of the missed ticket displayed. --- database/ticket.go | 14 +++++++++++ internal/webapi/admin.go | 36 +++++++++++++++++++++------- internal/webapi/formatting.go | 13 ++++++++++ internal/webapi/public/css/vspd.css | 8 ++++++- internal/webapi/templates/admin.html | 36 ++++++++++++++++++++++++++-- internal/webapi/webapi.go | 1 + 6 files changed, 97 insertions(+), 11 deletions(-) diff --git a/database/ticket.go b/database/ticket.go index 98f0906..38a444f 100644 --- a/database/ticket.go +++ b/database/ticket.go @@ -6,6 +6,7 @@ package database import ( "fmt" + "sort" "time" bolt "go.etcd.io/bbolt" @@ -116,6 +117,12 @@ func (t TicketList) EarliestPurchaseHeight() int64 { return oldestHeight } +func (t TicketList) SortByPurchaseHeight() { + sort.Slice(t, func(i, j int) bool { + return t[i].PurchaseHeight > t[j].PurchaseHeight + }) +} + func (t *Ticket) FeeExpired() bool { now := time.Now() return now.After(time.Unix(t.FeeExpiration, 0)) @@ -385,6 +392,13 @@ func (vdb *VspDatabase) GetMissingPurchaseHeight() (TicketList, error) { }) } +// GetMissedTickets returns all tickets which have outcome == missed. +func (vdb *VspDatabase) GetMissedTickets() (TicketList, error) { + return vdb.filterTickets(func(t *bolt.Bucket) bool { + return TicketOutcome(t.Get(outcomeK)) == Missed + }) +} + // filterTickets accepts a filter function and returns all tickets from the // database which match the filter. func (vdb *VspDatabase) filterTickets(filter func(*bolt.Bucket) bool) (TicketList, error) { diff --git a/internal/webapi/admin.go b/internal/webapi/admin.go index a13cf9d..e98a093 100644 --- a/internal/webapi/admin.go +++ b/internal/webapi/admin.go @@ -146,11 +146,21 @@ func (w *WebAPI) statusJSON(c *gin.Context) { func (w *WebAPI) adminPage(c *gin.Context) { cacheData := c.MustGet(cacheKey).(cacheData) + missed, err := w.db.GetMissedTickets() + if err != nil { + w.log.Errorf("db.GetMissedTickets error: %v", err) + c.String(http.StatusInternalServerError, "Error getting missed tickets from db") + return + } + + missed.SortByPurchaseHeight() + c.HTML(http.StatusOK, "admin.html", gin.H{ - "WebApiCache": cacheData, - "WebApiCfg": w.cfg, - "WalletStatus": w.walletStatus(c), - "DcrdStatus": w.dcrdStatus(c), + "WebApiCache": cacheData, + "WebApiCfg": w.cfg, + "WalletStatus": w.walletStatus(c), + "DcrdStatus": w.dcrdStatus(c), + "MissedTickets": missed, }) } @@ -212,6 +222,15 @@ func (w *WebAPI) ticketSearch(c *gin.Context) { feeTxDecoded = string(decoded) } + missed, err := w.db.GetMissedTickets() + if err != nil { + w.log.Errorf("db.GetMissedTickets error: %v", err) + c.String(http.StatusInternalServerError, "Error getting missed tickets from db") + return + } + + missed.SortByPurchaseHeight() + c.HTML(http.StatusOK, "admin.html", gin.H{ "SearchResult": searchResult{ Hash: hash, @@ -222,10 +241,11 @@ func (w *WebAPI) ticketSearch(c *gin.Context) { VoteChanges: voteChanges, MaxVoteChanges: w.cfg.MaxVoteChangeRecords, }, - "WebApiCache": cacheData, - "WebApiCfg": w.cfg, - "WalletStatus": w.walletStatus(c), - "DcrdStatus": w.dcrdStatus(c), + "WebApiCache": cacheData, + "WebApiCfg": w.cfg, + "WalletStatus": w.walletStatus(c), + "DcrdStatus": w.dcrdStatus(c), + "MissedTickets": missed, }) } diff --git a/internal/webapi/formatting.go b/internal/webapi/formatting.go index de6b10d..30219fc 100644 --- a/internal/webapi/formatting.go +++ b/internal/webapi/formatting.go @@ -62,3 +62,16 @@ func atomsToDCR(atoms int64) string { func float32ToPercent(input float32) string { return fmt.Sprintf("%.2f%%", input*100) } + +// pluralize suffixes the provided noun with "s" if n is not 1, then +// concatenates n and noun with a space between them. For example: +// +// (0, "biscuit") will return "0 biscuits" +// (1, "biscuit") will return "1 biscuit" +// (3, "biscuit") will return "3 biscuits" +func pluralize(n int, noun string) string { + if n != 1 { + noun += "s" + } + return fmt.Sprintf("%d %s", n, noun) +} diff --git a/internal/webapi/public/css/vspd.css b/internal/webapi/public/css/vspd.css index 206568f..a62cf29 100644 --- a/internal/webapi/public/css/vspd.css +++ b/internal/webapi/public/css/vspd.css @@ -92,7 +92,7 @@ footer .code { } } -.btn { +.btn-primary, .btn-secondary { outline: 0; -webkit-box-shadow: 1px 3px 14px 0px rgba(0,0,0,0.19); box-shadow: 1px 3px 14px 0px rgba(0,0,0,0.19); @@ -206,6 +206,12 @@ footer .code { padding-left: 40px; } +table.missed-tickets th, +table.missed-tickets td { + border: 1px solid #edeff1; + vertical-align: middle; + text-align: center; +} .tabset > input { display:block; /* "enable" hidden elements in IE/edge */ diff --git a/internal/webapi/templates/admin.html b/internal/webapi/templates/admin.html index d638911..df41781 100644 --- a/internal/webapi/templates/admin.html +++ b/internal/webapi/templates/admin.html @@ -45,11 +45,18 @@ id="tabset_1_4" hidden > +
@@ -190,6 +197,31 @@ {{ end }} +
+

{{ pluralize (len .MissedTickets) "Missed Ticket" }}

+ {{ with .MissedTickets }} + + + + + + + {{ range . }} + + + + + {{ end }} + +
Purchase HeightTicket Hash
{{ .PurchaseHeight }} +
+ + +
+
+ {{ end}} +
+

Database size: {{ .WebApiCache.DatabaseSize }}

Download Backup diff --git a/internal/webapi/webapi.go b/internal/webapi/webapi.go index a8f27d4..760f176 100644 --- a/internal/webapi/webapi.go +++ b/internal/webapi/webapi.go @@ -217,6 +217,7 @@ func (w *WebAPI) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W "atomsToDCR": atomsToDCR, "float32ToPercent": float32ToPercent, "comma": humanize.Comma, + "pluralize": pluralize, }) router.LoadHTMLGlob("internal/webapi/templates/*.html")