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 > +
| Purchase Height | +Ticket Hash | + + + {{ range . }} +
|---|---|
| {{ .PurchaseHeight }} | ++ + | +
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")