Misc front end improvements.

- Use bootstrap to improve layout.
- Add warning banners for webserver debug mode and vspd closed.

Admin page:
- Replace listing of all tickets with form to search by ticket hash
This commit is contained in:
jholdstock 2020-06-12 11:00:45 +01:00 committed by David Hill
parent a95b214b3f
commit 2f7c46e5f8
15 changed files with 232 additions and 89 deletions

View File

@ -193,13 +193,6 @@ func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) {
})
}
// GetAllTickets returns all tickets in the database.
func (vdb *VspDatabase) GetAllTickets() ([]Ticket, error) {
return vdb.filterTickets(func(t Ticket) bool {
return true
})
}
// filterTickets accepts a filter function and returns all tickets from the
// database which match the filter.
func (vdb *VspDatabase) filterTickets(filter func(Ticket) bool) ([]Ticket, error) {

View File

@ -74,9 +74,10 @@ func run(ctx context.Context) error {
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
AdminPass: cfg.AdminPass,
Debug: cfg.WebServerDebug,
}
err = webapi.Start(ctx, shutdownRequestChannel, &shutdownWg, cfg.Listen, db,
dcrd, wallets, cfg.WebServerDebug, apiCfg)
dcrd, wallets, apiCfg)
if err != nil {
log.Errorf("Failed to initialize webapi: %v", err)
requestShutdown()

View File

@ -15,19 +15,47 @@ func adminPage(c *gin.Context) {
admin := session.Values["admin"]
if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{})
return
}
tickets, err := db.GetAllTickets()
if err != nil {
log.Errorf("GetAllTickets error: %v", err)
c.String(http.StatusInternalServerError, "Error getting tickets from db")
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"VspStats": stats,
})
return
}
c.HTML(http.StatusOK, "admin.html", gin.H{
"Tickets": tickets,
"VspStats": stats,
})
}
// ticketSearch is the handler for "POST /admin/ticket". The "hash" param will
// be used to retrieve a ticket from the database if the current session is
// authenticated as an admin, otherwise the login template will be rendered.
func ticketSearch(c *gin.Context) {
session := c.MustGet("session").(*sessions.Session)
admin := session.Values["admin"]
if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"VspStats": stats,
})
return
}
hash := c.PostForm("hash")
ticket, found, err := db.GetTicketByHash(hash)
if err != nil {
log.Errorf("GetTicketByHash error: %v", err)
c.String(http.StatusInternalServerError, "Error getting ticket from db")
return
}
c.HTML(http.StatusOK, "admin.html", gin.H{
"SearchResult": gin.H{
"Hash": hash,
"Found": found,
"Ticket": ticket,
},
"VspStats": stats,
})
}
@ -39,6 +67,7 @@ func adminLogin(c *gin.Context) {
if password != cfg.AdminPass {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"VspStats": stats,
"IncorrectPassword": true,
})
return

View File

@ -64,9 +64,9 @@ func (e apiError) defaultMessage() string {
case errVspClosed:
return "vsp is closed"
case errFeeAlreadyReceived:
return "fee tx already received"
return "fee tx already received for ticket"
case errInvalidFeeTx:
return "invalid fee transaction"
return "invalid fee tx"
case errFeeTooSmall:
return "fee too small"
case errUnknownTicket:
@ -82,7 +82,7 @@ func (e apiError) defaultMessage() string {
case errInvalidPrivKey:
return "invalid private key"
case errFeeNotReceived:
return "no fee tx received for this ticket"
return "no fee tx received for ticket"
default:
return "unknown error"
}

View File

@ -18,6 +18,7 @@ type vspStats struct {
UpdateTime string
SupportEmail string
VspClosed bool
Debug bool
}
var stats *vspStats
@ -36,9 +37,12 @@ func updateVSPStats(db *database.VspDatabase, cfg Config) (*vspStats, error) {
UpdateTime: time.Now().Format("Mon Jan _2 15:04:05 2006"),
SupportEmail: cfg.SupportEmail,
VspClosed: cfg.VspClosed,
Debug: cfg.Debug,
}, nil
}
func homepage(c *gin.Context) {
c.HTML(http.StatusOK, "homepage.html", stats)
c.HTML(http.StatusOK, "homepage.html", gin.H{
"VspStats": stats,
})
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,20 @@ body {
background-color: #091440;
color: white;
}
img {
.navbar-brand img {
height: 70px;
}
th {
white-space: nowrap;
}
td {
font-family: "vspd-code";
word-break: break-word;
}
.code {
font-family: "vspd-code";
}

View File

@ -1,41 +1,83 @@
{{ template "header" . }}
<form action="/admin/logout" method="post">
{{ template "vsp-stats" .VspStats }}
<section>
<h3>Admin</h3>
<form class="my-2" action="/admin/logout" method="post">
<button type="submit">Logout</button>
</form>
</form>
<table>
<form class="my-2" action="/admin/ticket" method="post">
<input type="text" name="hash" size="64" minlength="64" maxlength="64" required placeholder="Ticket hash">
<button type="submit">Search</button>
</form>
{{ with .SearchResult }}
{{ if .Found }}
{{ with .Ticket }}
<table class="table table-dark my-2 ticket-table">
<tr>
<td>Hash</td>
<td>CommitmentAddress</td>
<td>FeeAddressIndex</td>
<td>FeeAddress</td>
<td>FeeAmount</td>
<td>FeeExpiration</td>
<td>Confirmed</td>
<td>VoteChoices</td>
<td>VotingWIF</td>
<td>FeeTxHex</td>
<td>FeeTxHash</td>
<td>FeeTxStatus</td>
<th>Hash</th>
<td>{{ .Hash }}</td>
</tr>
{{ range .Tickets }}
<tr>
<td>{{ printf "%.10s" .Hash }}...</td>
<td>{{ printf "%.10s" .CommitmentAddress }}...</td>
<td>{{ printf "%d" .FeeAddressIndex }}</td>
<td>{{ printf "%.10s" .FeeAddress }}...</td>
<td>{{ printf "%d" .FeeAmount }}</td>
<td>{{ printf "%d" .FeeExpiration }}</td>
<td>{{ printf "%t" .Confirmed }}</td>
<td>{{ printf "%.10s" .VoteChoices }}...</td>
<td>{{ printf "%.10s" .VotingWIF }}...</td>
<td>{{ printf "%.10s" .FeeTxHex }}...</td>
<td>{{ printf "%.10s" .FeeTxHash }}...</td>
<td>{{ printf "%s" .FeeTxStatus }}</td>
<th>Commitment Address</th>
<td>{{ .CommitmentAddress }}</td>
</tr>
<tr>
<th>Fee Address Index</th>
<td>{{ .FeeAddressIndex }}</td>
</tr>
<tr>
<th>Fee Address</th>
<td>{{ .FeeAddress }}</td>
</tr>
<tr>
<th>Fee Amount</th>
<td>{{ .FeeAmount }} atoms</td>
</tr>
<tr>
<th>Fee Expiration</th>
<td>{{ .FeeExpiration }}</td>
</tr>
<tr>
<th>Confirmed</th>
<td>{{ .Confirmed }}</td>
</tr>
<tr>
<th>Vote Choices</th>
<td>
{{ range $key, $value := .VoteChoices }}
{{ $key }}: {{ $value }} <br />
{{ end }}
</td>
</tr>
<tr>
<th>Voting WIF</th>
<td>{{ .VotingWIF }}</td>
</tr>
<tr>
<th>Fee Tx</th>
<td>{{ .FeeTxHex }}</td>
</tr>
<tr>
<th>Fee Tx Hash</th>
<td>{{ .FeeTxHash }}</td>
</tr>
<tr>
<th>Fee Tx Status</th>
<td>{{ .FeeTxStatus }}</td>
</tr>
<tr>
</tr>
</table>
{{ end }}
{{ else }}
<p>No ticket with hash <span class="code">"{{ .Hash }}"</span></p>
{{ end }}
{{ end }}
</table>
</body>
</html>
</section>
{{ template "footer" . }}

View File

@ -0,0 +1,5 @@
{{ define "footer" }}
</div>
</body>
</html>
{{ end }}

View File

@ -2,13 +2,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta charset="utf-8">
<title>vspd</title>
<link rel="stylesheet" type="text/css" href="/public/css/fonts.css" />
<link rel="stylesheet" type="text/css" href="/public/css/vspd.css" />
<link rel="stylesheet" href="/public/css/vendor/bootstrap.min.css" />
<link rel="stylesheet" href="/public/css/fonts.css" />
<link rel="stylesheet" href="/public/css/vspd.css" />
<!-- Custom favicon -->
<!-- Apple PWA -->
@ -41,5 +42,22 @@
</head>
<body>
<img src="/public/images/decred-logo.svg" />
<nav>
<div class="container">
<a class="navbar-brand" href="/"><img src="/public/images/decred-logo.svg" /></a>
</div>
</nav>
<div class="container">
{{ if .VspStats.Debug }}
<div class="alert alert-warning" role="alert">
Web server is running in debug mode - don't do this in production!
</div>
{{ end }}
{{ if .VspStats.VspClosed }}
<div class="alert alert-warning" role="alert">
This VSP is closed and not accepting new tickets.
</div>
{{ end }}
{{end}}

View File

@ -1,16 +1,5 @@
{{ template "header" . }}
<table>
<tr><td>Total tickets:</td><td>{{ .TotalTickets }}</td></tr>
<tr><td>Fee confirmed tickets:</td><td>{{ .FeeConfirmedTickets }}</td></tr>
<tr><td>VSP Fee:</td><td>{{ .VSPFee }}% of vote reward</td></tr>
<tr><td>Network:</td><td>{{ .Network }}</td></tr>
<tr><td>Support:</td><td>{{ .SupportEmail }}</td></tr>
<tr><td>Pubkey:</td><td>{{ printf "%x" .PubKey }}</td></tr>
<tr><td>VSP closed:</td><td>{{ .VspClosed }}</td></tr>
</table>
{{ template "vsp-stats" .VspStats }}
<p>Last updated: {{ .UpdateTime }}</p>
</body>
</html>
{{ template "footer" . }}

View File

@ -1,13 +1,15 @@
{{ template "header" . }}
<form action="/admin" method="post">
<section>
<h3>Login</h3>
<form action="/admin" method="post">
<input type="password" name="password" required placeholder="Enter password">
<button type="submit">Login</button>
</form>
</form>
{{ if .IncorrectPassword }}
{{ if .IncorrectPassword }}
<p>Incorrect password</p>
{{ end }}
{{ end }}
</section>
</body>
</html>
{{ template "footer" . }}

View File

@ -0,0 +1,36 @@
{{ define "vsp-stats" }}
<section>
<h3>VSP Stats</h3>
<p><em>Last updated: {{ .UpdateTime }}</em></p>
<table class="table table-dark">
<tr>
<th>Total tickets</th>
<td>{{ .TotalTickets }}</td>
</tr>
<tr>
<th>Fee confirmed tickets</th>
<td>{{ .FeeConfirmedTickets }}</td>
</tr>
<tr>
<th>VSP Fee</th>
<td>{{ .VSPFee }}% of vote reward</td>
</tr>
<tr>
<th>Network</th>
<td>{{ .Network }}</td>
</tr>
<tr>
<th>Support</th>
<td>{{ .SupportEmail }}</td>
</tr>
<tr>
<th>Pubkey</th>
<td class="code">{{ printf "%x" .PubKey }}</td>
</tr>
</table>
</section>
{{ end }}

View File

@ -25,6 +25,7 @@ type Config struct {
SupportEmail string
VspClosed bool
AdminPass string
Debug bool
}
const (
@ -45,7 +46,7 @@ var signPrivKey ed25519.PrivateKey
var signPubKey ed25519.PublicKey
func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *sync.WaitGroup,
listen string, vdb *database.VspDatabase, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect, debugMode bool, config Config) error {
listen string, vdb *database.VspDatabase, dcrd rpc.DcrdConnect, wallets rpc.WalletConnect, config Config) error {
cfg = config
db = vdb
@ -94,7 +95,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
log.Infof("Listening on %s", listen)
srv := http.Server{
Handler: router(debugMode, cookieSecret, dcrd, wallets),
Handler: router(cfg.Debug, cookieSecret, dcrd, wallets),
ReadTimeout: 5 * time.Second, // slow requests should not hold connections opened
WriteTimeout: 60 * time.Second, // hung responses must die
}
@ -131,7 +132,7 @@ func Start(ctx context.Context, requestShutdownChan chan struct{}, shutdownWg *s
// Use a ticker to update template data.
var refresh time.Duration
if debugMode {
if cfg.Debug {
refresh = 1 * time.Second
} else {
refresh = 5 * time.Minute
@ -201,6 +202,7 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
)
admin.GET("", adminPage)
admin.POST("", adminLogin)
admin.POST("/ticket", ticketSearch)
admin.POST("/logout", adminLogout)
// These API routes access dcrd and the voting wallets, and they need