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:
parent
a95b214b3f
commit
2f7c46e5f8
@ -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) {
|
||||
|
||||
3
main.go
3
main.go
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
7
webapi/public/css/vendor/bootstrap.min.css
vendored
Normal file
7
webapi/public/css/vendor/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
webapi/public/css/vendor/bootstrap.min.css.map
vendored
Normal file
1
webapi/public/css/vendor/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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";
|
||||
}
|
||||
|
||||
@ -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" . }}
|
||||
|
||||
5
webapi/templates/footer.html
Normal file
5
webapi/templates/footer.html
Normal file
@ -0,0 +1,5 @@
|
||||
{{ define "footer" }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
@ -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}}
|
||||
|
||||
@ -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" . }}
|
||||
|
||||
@ -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" . }}
|
||||
|
||||
36
webapi/templates/vsp-stats.html
Normal file
36
webapi/templates/vsp-stats.html
Normal 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 }}
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user