decred / dcrstakepool

Stakepool for Decred.
Other
74 stars 75 forks source link

paginate/improve performance of tickets page #56

Closed jolan closed 7 years ago

jolan commented 8 years ago

The ticket page becomes slower to load over time because stakepooluserinfo returns every voted ticket.

Using my development/testnet stakepool, I can load the page OK for a user with 100,000 voted tickets with 3 wallets hosted on a LAN with very fast machines.

But once you have a few thousand on 3+ servers spread across the world on slowish VPSes you hit timeouts like @Dyrk reported seeing.

Real fix would be to either add sorting/pagination options to the RPC or overhaul the architecture of stakepool to cache the ticket info in the database.

Maybe the easiest thing to do would be to add an option to the stakepooluserinfo that only returns tickets of a certain status so voted tickets could be processed separately.

ghost commented 8 years ago

I've fixed this one in my pool. I've totally removed all tickets related logic from the Tickets() route. So, when user loading the page, he is seeing only Instructions. Then I've implemented new API endpoint stakepooluserinfo where authorized user can retrieve his tickets. With Ajax request I get all the info and then display it to the user.

So, now most part of the logic is asynchronous and managed by frontend for this page. If you like this solution, I can make a pull request, however, it won't be easy due to many changes from the frontend and template side.

img

jolan commented 8 years ago

@Dyrk Yes we'd like that solution so if you can make a PR that'd be great.

chappjc commented 8 years ago

even snippets of the AJAX parts would be helpful

ghost commented 8 years ago

@chappjc here is the client side JS:

            /* Retreive Tickets from the API */
            $(document).ready(function() {
                var xsrf_token = "";
                var cookies = document.cookie.split(';');
                 for (var i = 0; i < cookies.length; i++) {
                   if (cookies[i].trim().match(/XSRF-TOKEN=(.*)$/g)) {
                        xsrf_token = cookies[i].trim().replace('XSRF-TOKEN=', '');
                   }
                 }
                $.ajax({
                    "url" : "/api/v0.1/stakepooluserinfo",
                    "type" : "GET",
                    beforeSend: function (request) {
                         request.setRequestHeader("X-XSRF-TOKEN", xsrf_token);
                  },
                    success : function(data) {
                        var json = {};
                        try {
                            json = JSON.parse(data);
                        } catch(e) {
                            showError(data); return;
                        }

                        if (json.status == "success") {
                            displayTickets(json.data);
                            $(".additional-tickets-info").show();
                            $(".load-tickets").hide();
                            $("#collapse-info").removeClass('in');
                            $("#collapse-instr").removeClass('in');
                        } else {
                            showError(json);
                        }
                    },
                    error : function(data) {
                        var json = {};
                        try {
                            json = JSON.parse(data);
                        } catch(e) { }
                        showError(json); return;
                    }
                });

                function displayTickets(data) {

                    $('#LiveTickets').text(data.numLive || 0);
                    $('#MissedTickets').text(data.numMissed || 0);
                    $('#VotedTickets').text(data.numVoted || 0);
                    $('#ExpiredTickets').text(data.numExpired || 0);
                    $('#InvalidTickets').text(data.numInvalid || 0);

                    getTotalBalance();

                    fillTicketsTable(data.TicketsLive, 'ticketslive');
                    fillTicketsTable(data.TicketsVoted, 'ticketsvoted');
                    fillTicketsTable(data.TicketsMissed, 'ticketsmissed');
                    fillTicketsTable(data.TicketsExpired, 'ticketsexpired');
                    fillTicketsTable(data.TicketsInvalid, 'ticketsinvalid');

                }

                function fillTicketsTable(tickets, id) {
                    var table = $('#' + id + ' tbody'), html = "";
                    for (ticket in tickets) {
                        if (id == 'ticketslive') {
                            html += "<tr>";
                            html += "<td>";
                                html+= "<a href=\"https://mainnet.decred.org/tx/"+tickets[ticket]['Ticket']+"\" target=\"_blank\">"+tickets[ticket]['Ticket']+"</a>";
                            html+= "</td>";
                            html += "<td>"+tickets[ticket]['TicketHeight']+"</td>";
                            html += "<td>"+tickets[ticket]['VoteBits']+"</td>";
                            html += "</tr>";
                        } else if (id == 'ticketsvoted' || id == 'ticketsexpired' || id == 'ticketsmissed') {
                            html += "<tr>";
                            html += "<td>";
                                html+= "<a href=\"https://mainnet.decred.org/tx/"+tickets[ticket]['Ticket']+"\" target=\"_blank\">"+tickets[ticket]['Ticket']+"</a>";
                            html+= "</td>";
                            html += "<td>";
                                html+= "<a href=\"https://mainnet.decred.org/tx/"+tickets[ticket]['SpentBy']+"\" target=\"_blank\">"+tickets[ticket]['SpentByHeight']+"</a>";
                            html+= "</td>";
                            html += "<td>"+tickets[ticket]['TicketHeight']+"</td>";
                            html += "</tr>";
                        } else if (id == 'ticketsinvalid') {
                            html += "<tr>";
                            html += "<td>";
                                html+= "<a href=\"https://mainnet.decred.org/tx/"+tickets[ticket]['Ticket']+"\" target=\"_blank\">"+tickets[ticket]['Ticket']+"</a>";
                            html+= "</td>";
                            html += "</tr>";
                        }
                    }

                    if (! $.isEmptyObject(tickets)) {
                        var col = 1;
                        if (id == 'ticketsinvalid') { col = 0; }
                        $('#collapse-'+id).addClass('in');
                        $(table).html(html);
                        $('#' + id).DataTable({"order": [[ col, "desc"]]});
                    }
                }

                function showError(data) {
                    var text = data.message || "Cannot retreive the list of your tickets at the moment. Please try to refresh the page or write us an email: info@dcrstats.com";
                    $('.load-tickets').html('<div class="col-md-12"><div class="alert alert-warning">' + text + '</div></div>');
                }
            });

To the API endpoints you have to add the new one for tickets:

        case "stakepooluserinfo":
            data, response, err := controller.APIStakepoolUserInfo(c, r)
            return APIJSON(data, response, err), http.StatusOK

APIJSON (APIResponse doesn't print the valid json in this case):

// APIJSON formats a response
func APIJSON(data []string, response string, err error) string {
    if err != nil {
        return "{\"status\":\"error\"," +
            "\"message\":\"" + response + " - " + err.Error() + "\"}\n"
    }

    successResp := "{\"status\":\"success\"," +
        "\"message\":\"" + response + "\""

    if data != nil {
        // append the key+value pairs in data
        successResp = successResp + ",\"data\":{"
        for i := 0; i < len(data)-1; i = i + 2 {
            successResp = successResp + "\"" + data[i] + "\":" + data[i+1] + ","
        }
        successResp = strings.TrimSuffix(successResp, ",") + "}"
    }

    successResp = successResp + "}\n"

    return successResp
}

And this is the main logic of the APIStakepoolUserInfo. This the old Tikets (GET) logic (which should be totally removed, by the way, on page load you want to make only 1 DB query to show script, ticket/fee addresses etc)

func (controller *MainController) APIStakepoolUserInfo(c web.C, r *http.Request) ([]string, string, error) {
    var stats []string

    type TicketInfoHistoric struct {
        Ticket        string
        SpentBy       string
        SpentByHeight uint32
        TicketHeight  uint32
    }

    type TicketInfoInvalid struct {
        Ticket string
    }

    type TicketInfoLive struct {
        Ticket       string
        TicketHeight uint32
        VoteBits     uint16
    }

    ticketInfoInvalid := map[int]TicketInfoInvalid{}
    ticketInfoLive := map[int]TicketInfoLive{}
    ticketInfoMissed := map[int]TicketInfoHistoric{}
    ticketInfoExpired := map[int]TicketInfoHistoric{}
    ticketInfoVoted := map[int]TicketInfoHistoric{}

    session := controller.GetSession(c)

    if session.Values["UserId"] == nil {
        return nil, "tickets error", errors.New("Forbidden")
    }

    dbMap := controller.GetDbMap(c)
    user := models.GetUserById(dbMap, session.Values["UserId"].(int64))

    if user.MultiSigAddress == "" {
        log.Info("[API] Multisigaddress empty")
        return nil, "tickets error", errors.New("No multisig data has been generated")
    }

    ms, err := dcrutil.DecodeAddress(user.MultiSigAddress, controller.params)
    if err != nil {
        log.Infof("Invalid address %v in database: %v", user.MultiSigAddress, err)
        return nil, "tickets error", errors.New("Invalid multisig data in database")
    }

    spui := new(dcrjson.StakePoolUserInfoResult)
    spui, err = controller.rpcServers.StakePoolUserInfo(ms)
    if err != nil {
        // Log the error, but do not return. Consider reporting
        // the error to the user on the page. A blank tickets
        // page will be displayed in the meantime.
        log.Infof("RPC StakePoolUserInfo failed: %v", err)
    }

    if spui != nil && len(spui.Tickets) > 0 {
        var tickethashes []*chainhash.Hash

        for _, ticket := range spui.Tickets {
            th, err := chainhash.NewHashFromStr(ticket.Ticket)
            if err != nil {
                log.Infof("NewHashFromStr failed for %v", ticket)
                return nil, "tickets error", errors.New("NewHashFromStr failed")
            }
            tickethashes = append(tickethashes, th)
        }

        // TODO: only get votebits for live tickets.
        gtvb, err := controller.rpcServers.GetTicketsVoteBits(tickethashes)
        if err != nil {
            log.Infof("GetTicketsVoteBits failed %v", err)
            return nil, "tickets error", errors.New("GetTicketsVoteBits failed")
        }

        for idx, ticket := range spui.Tickets {
            switch {
            case ticket.Status == "live":
                ticketInfoLive[idx] = TicketInfoLive{
                    Ticket:       ticket.Ticket,
                    TicketHeight: ticket.TicketHeight,
                    VoteBits:     gtvb.VoteBitsList[idx].VoteBits,
                }
            case ticket.Status == "missed":
                ticketInfoMissed[idx] = TicketInfoHistoric{
                    Ticket:        ticket.Ticket,
                    SpentBy:       ticket.SpentBy,
                    SpentByHeight: ticket.SpentByHeight,
                    TicketHeight:  ticket.TicketHeight,
                }
            case ticket.Status == "voted":
                ticketInfoVoted[idx] = TicketInfoHistoric{
                    Ticket:        ticket.Ticket,
                    SpentBy:       ticket.SpentBy,
                    SpentByHeight: ticket.SpentByHeight,
                    TicketHeight:  ticket.TicketHeight,
                }
            case ticket.Status == "expired":
                ticketInfoExpired[idx] = TicketInfoHistoric{
                    Ticket:        ticket.Ticket,
                    SpentByHeight: ticket.SpentByHeight,
                    TicketHeight:  ticket.TicketHeight,
                }
            }
        }

        for idx, ticket := range spui.InvalidTickets {
            ticketInfoInvalid[idx] = TicketInfoInvalid{ticket}
        }
    }

    jsonInvalid, err := json.Marshal(ticketInfoInvalid)
    if err != nil {
        log.Info(err)
    }

    jsonLive, err := json.Marshal(ticketInfoLive)
    if err != nil {
        log.Info(err)
    }

    jsonMissed, err := json.Marshal(ticketInfoMissed)
    if err != nil {
        log.Info(err)
    }

    jsonVoted, err := json.Marshal(ticketInfoVoted)
    if err != nil {
        log.Info(err)
    }

    jsonExpired, err := json.Marshal(ticketInfoExpired)
    if err != nil {
        log.Info(err)
    }

    stats = append(stats,
        "TicketsInvalid", string(jsonInvalid),
        "TicketsLive", string(jsonLive),
        "TicketsMissed", string(jsonMissed),
        "TicketsExpired", string(jsonExpired),
        "TicketsVoted", string(jsonVoted),
        "numInvalid", fmt.Sprintf("%d", len(ticketInfoInvalid)),
        "numExpired", fmt.Sprintf("%d", len(ticketInfoExpired)),
        "numVoted", fmt.Sprintf("%d", len(ticketInfoVoted)),
        "numLive", fmt.Sprintf("%d", len(ticketInfoLive)),
        "numMissed", fmt.Sprintf("%d", len(ticketInfoMissed)),
    )

    return stats, "tickets successfully retrieved", nil
}

This is my tickets.html it's very different from the default one, but each class or id matters for frontend logic :(

{{define "tickets"}}
{{if .Error}}
    <div class="alert alert-danger">{{.Error}}</div><p>
{{end}}

<div class="row load-tickets">
  <div class="col-md-12">
        <h2>Trying to retrieve tickets from remote wallets</h2>
    <div class="progress">
        <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="45" aria-valuemin="0" aria-valuemax="100" style="width: 100%;">
        </div>
        </div>
        <p>Please don't worry, all your Live tickets are safe and they are voting.</p>
  </div>
</div>

<div class="additional-tickets-info">
    <div class="row">
      <div class="col-md-3">
        <div class="alert alert-success" role="alert" style="text-align: center;">
          <strong id="LiveTickets">{{ .numLive }}</strong> live tickets
        </div>
      </div>
      <div class="col-md-3">
        <div class="alert alert-info" role="alert" style="text-align: center;">
          <strong id="VotedTickets">{{ .numVoted }}</strong> voted tickets
        </div>
      </div>
      <div class="col-md-3">
        <div class="alert alert-warning" role="alert" style="text-align: center;">
          <strong id="MissedTickets">{{ .numMissed }}</strong> missed tickets
        </div>
      </div>
      <div class="col-md-3">
        <div class="alert alert-warning" role="alert" style="text-align: center;">
          <strong id="ExpiredTickets">{{ .numExpired }}</strong> expired tickets
        </div>
      </div>
    </div>

    <div class="row" id="secondInfoRow" style="display: none;">
      <div class="col-md-6">
        <div id="TotalBalance" class="alert alert-warning" role="alert" style="text-align: center;">

        </div>
      </div>
      <div class="col-md-6">
        <div id="AvgTicketPrice" class="alert alert-warning" role="alert" style="text-align: center;">

        </div>
      </div>
    </div>

    <div class="row" id="mobile-apps-announce" style="display: none;">
      <div class="col-md-12">
          <p style="padding: 15px 25px;text-align: center;background: #fff;font-size: 16px;">
                You can get fast access to your PoS-mining stats using our mobile apps. Try them now:
                <br><br>
                <a href="https://play.google.com/store/apps/details?id=com.ionicframework.myapp554035" target="_blank">
                    <img src="https://dcrstats.com/img/playstore.png" width="125" style="padding: 0 3px;" class="center">
                </a>
                <a href="https://itunes.apple.com/us/app/dcrstats/id1141383230" target="_blank">
                    <img src="https://dcrstats.com/img/appstore.png" width="125" style="padding: 0 3px;" class="center">
                </a>
                <br><br>
            <a id="hide-mobile-apps-announce" href="javascript:;" style="color: #969696;font-size: 13px;">
                    [x] Got it! Please do not show me this message again.
                </a>
            </p>
      </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-toggle="collapse" data-parent="#accordion" href="#collapse-info">Ticket Information</a>
        </h4>
    </div>
    <div id="collapse-info" class="panel-collapse collapse in">
        <div class="panel-body">
            <p style="font-size: large;"><b>P2SH Address:</b></p>
            <pre id="MultiSigAddress" style="font-size: larger;">{{ .User.MultiSigAddress }}</pre>
            <p><span style="font-size: large;"><b>Redeem Script:</b></span></p>
            <p class="long-string">{{ .User.MultiSigScript }}</p>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-toggle="collapse" data-parent="#accordion" href="#collapse-instr">Ticket Instructions</a>
        </h4>
    </div>
    <div id="collapse-instr" class="panel-collapse collapse {{if not .TicketsLive }}in{{end}}">
        <div class="panel-body">
            <p><span style="font-size: larger;"><b><u>Step 1</u></b></span></p>
            <p><span style="font-size: larger;"><b>It is recommended to use the latest versions of the Decred software before starting.&nbsp;</b></span>
            <a target="_blank" href="https://github.com/decred/decred-release/releases/latest"><span class="glyphicon glyphicon-download" aria-label="Download Decred Installer"></span> <span style="font-size: larger;">Installer</span></a> |
            <a target="_blank" href="https://github.com/decred/decred-binaries/releases/latest"><span class="glyphicon glyphicon-download" aria-label="Download Decred Binaries"></span> <span style="font-size: larger;">Binaries</span></a></p>
            </p>
            <p><span style="font-size: larger;"><b><u>Step 2</u></b></span></p>
            <p><span style="font-size: larger;">Your P2SH multisignature script for delegating votes has been generated. Please first import it locally into your wallet using <b>dcrctl</b> for safe keeping, so you can recover your funds and vote in the unlikely event of a pool failure:</span></p>

            <p><span style="font-size: larger;">dcrctl --wallet importscript "script"</span></p>
            <p><span style="font-size: larger;">For example:</span></p>
            <div class="cmd"><pre>dcrctl {{ if eq .Network "testnet"}}--testnet{{end}} --wallet importscript {{ .User.MultiSigScript }}</pre></div>

            <p><span style="font-size: larger;">After successfully importing the script into your wallet, you may generate tickets delegated to the pool in either of three ways:</span></p>

            <p><span style="font-size: larger;"><b><u>Step 3</u></b></span></p>

            <p><span style="font-size: larger;"><b>Option A - dcrticketbuyer - Automatic purchasing</b></span></p>
            <p><span style="font-size: larger;">Stop dcrticketbuyer if it is currently running and add the following to <b>ticketbuyer.conf</b>:</span></p>
            <p><span style="font-size: larger;">maxpriceabsolute=35</span></p>
            <p><span style="font-size: larger;">pooladdress={{ .User.UserFeeAddr }}</span></p>
            <p><span style="font-size: larger;">poolfees={{ .PoolFees }}</span></p>
            <p><span style="font-size: larger;">ticketaddress={{ .User.MultiSigAddress }}</span></p>
            <p><span style="font-size: larger;">Unlock dcrwallet, start dcrticketbuyer, and it will automatically purchase stake tickets delegated to the pool address.</span></p>

            <p><span style="font-size: larger;"><b>Option B - dcrwallet - Automatic purchasing</b></span></p>
            <p><span style="font-size: larger;">Stop dcrwallet if it is currently running and add the following to <b>dcrwallet.conf</b>:</span></p>
            <p><span style="font-size: larger;">enablestakemining=1</span></p>
            <p><span style="font-size: larger;">pooladdress={{ .User.UserFeeAddr }}</span></p>
            <p><span style="font-size: larger;">poolfees={{ .PoolFees }}</span></p>
            <p><span style="font-size: larger;">promptpass=1</span></p>
            <p><span style="font-size: larger;">ticketaddress={{ .User.MultiSigAddress }}</span></p>
            <p><span style="font-size: larger;">ticketmaxprice=35</span></p>
            <p><span style="font-size: larger;">Start dcrwallet and it will prompt your for your password. After unlocking, stake tickets delegated to the pool address will automatically be purchased as long as funds are available.</span></p>

            <p><span style="font-size: larger;"><b>Option C - dcrwallet - Manual purchasing</b></span></p>
            <p><span style="font-size: larger;">Start a wallet with funds available and manually purchase tickets with the following command using <b>dcrctl</b>:</span></p>
            <p><span style="font-size: larger;">dcrctl {{ if eq .Network "testnet"}}--testnet{{end}} --wallet purchaseticket "fromaccount" spendlimit minconf ticketaddress numtickets poolfeeaddress poolfeeamt</span></p>
            <div class="cmd"><pre>dcrctl {{ if eq .Network "testnet"}}--testnet{{end}} --wallet purchaseticket "voting" 35 1 {{ .User.MultiSigAddress }} 1 {{ .User.UserFeeAddr }} {{ .PoolFees}}</pre></div>
            <p><span style="font-size: larger;">Will purchase a ticket delegated to the pool using the voting account only if the current network price for a ticket is equal or less than 35.0 coins. <br><br> If you have any problems or questions please write us using the chat below or email: <a href="mailto:info@dcrstats.com">info@dcrstats.com</a></span></p>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-toggle="collapse" data-parent="#accordion" href="#collapse-ticketslive">Live/Immature Tickets</a>
        </h4>
    </div>
    <div id="collapse-ticketslive" class="panel-collapse collapse {{if .TicketsLive}}in{{end}}">
        <div class="panel-body">
            <table id="ticketslive" class="table table-condensed table-responsive datatablesort">
            <thead>
                <tr>
                    <th>Ticket</th>
                    <th>TicketHeight</th>
                    <!--th>Pool Control</th-->
                    <th>Votebits</th>
                </tr>
            </thead>
            <tfoot>
                <tr>
                    <th>Ticket</th>
                    <th>TicketHeight</th>
                    <!--th>Pool Control</th-->
                    <th>Votebits</th>
                </tr>
            </tfoot>
            <tbody>
                {{if .TicketsLive}}
                    {{ range $i, $data := .TicketsLive }}
                        <tr>
                            <td><a href="https://{{$.Network}}.decred.org/tx/{{$data.Ticket}}" target="_blank">{{$data.Ticket}}</a></td>
                            <td>{{ $data.TicketHeight }}</td>
                            <!--td>{{ if eq $data.VoteBits 1 }}Yes{{end}}{{ if ne $data.VoteBits 1}}No{{end}}</td-->
                            <td>{{ $data.VoteBits }}</td>
                        </tr>
                    {{end}}
                {{ else }}
                    <td colspan="3"><p style=" margin: 20px 0; "><span style="font-size: larger;">You don't have Live tickets.</span></p></td>
                {{end}}
            </tbody>
            </table>
        <form method="post" class="form-inline" role="form">
          <div id="votebits" class="form-group">
                <label class="control-label" for=""> &nbsp;Vote Bits: &nbsp;<span class="glyphicon glyphicon-question-sign" data-toggle="tooltip" data-placement="top" title="More options and their definitions will be added soon"></span></label>
                <label class="control-label" for="chooseallow"> &nbsp;Choose:</label>
                <select class="form-control" id="chooseallow" name="chooseallow">
                                    <option value=2>Use Pool Policy</option>
                                    <option value=1>Allow All Blocks</option>
                                    <option value=0>Disallow All Blocks</option>
                                </select>
                <!--<label class="control-label" for="votebitsmanual"> &nbsp;Manual: &nbsp;
                <input class="form-control input-sm" type="text" id="votebitsmanual" name="votebitsmanual" pattern="^0*(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$">-->
          </div>
          <div class="form-group">
            <div class="col-sm-2">
                 <button name="updatetickets" class="btn btn-primary">Update Tickets</button>
            </div>
          </div>
                    <p style=" margin: 15px 0; font-size: 16px; color: #9e9e9e; ">Please notice, that full re-sync may take up to 10-15 minutes, depends on how many Live tickets do you have. No need to submit your request few times.</p>
            <input type="hidden" name="{{.CsrfKey}}" value={{.CsrfToken}}>
        </form>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-toggle="collapse" data-parent="#accordion" href="#collapse-ticketsvoted">Voted Tickets</a>
        </h4>
    </div>

    <div id="collapse-ticketsvoted" class="panel-collapse collapse {{if .TicketsVoted}}in{{end}}">
        <div class="panel-body">

            <table id="ticketsvoted" class="table table-condensed table-responsive datatablesort">
            <thead>
                <tr>
                    <th>Ticket</th>
                    <th>SpentByHeight</th>
                    <th>TicketHeight</th>
                </tr>
            </thead>
            <tfoot>
                <tr>
                    <th>Ticket</th>
                    <th>SpentByHeight</th>
                    <th>TicketHeight</th>
                </tr>
            </tfoot>
            <tbody>
            {{if .TicketsVoted }}
                {{ range $i, $data := .TicketsVoted }}
                    <tr>
                        <td><a href="https://{{$.Network}}.decred.org/tx/{{$data.Ticket}}" target="_blank">{{$data.Ticket}}</a></td>
                        <td><a href="https://{{$.Network}}.decred.org/tx/{{$data.SpentBy}}" target="_blank">{{$data.SpentByHeight}}</a></td>
                        <td>{{$data.TicketHeight}}</td>
                    </tr>
                {{end}}
            {{ else }}
                <tr><td colspan="3"><p style=" margin: 20px 0; "><span style="font-size: larger;">You don't have Voted tickets.</span></p></td></tr>
            {{end}}
            </tbody>
            </table>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-toggle="collapse" data-parent="#accordion" href="#collapse-ticketsmissed">Missed Tickets</a>
        </h4>
    </div>
    <div id="collapse-ticketsmissed" class="panel-collapse collapse {{if .TicketsMissed}}in{{end}}">
        <div class="panel-body">

            <table id="ticketsmissed" class="table table-condensed table-responsive datatablesort">
            <thead>
                <div class="alert alert-warning"><b>Caution!</b> Missed tickets can be caused by a poorly connected miner and may not be any fault of the pool.</div>
                <tr>
                    <th>Ticket</th>
                    <th>SpentByHeight</th>
                    <th>TicketHeight</th>
                </tr>
            </thead>
            <tfoot>
                <tr>
                    <th>Ticket</th>
                    <th>SpentByHeight</th>
                    <th>TicketHeight</th>
                </tr>
            </tfoot>
            <tbody>
            {{if .TicketsMissed}}
                {{ range $i, $data := .TicketsMissed }}
                    <tr>
                        <td><a href="https://{{$.Network}}.decred.org/tx/{{$data.Ticket}}" target="_blank">{{$data.Ticket}}</a></td>
                        <td><a href="https://{{$.Network}}.decred.org/tx/{{$data.SpentBy}}" target="_blank">{{$data.SpentByHeight}}</a></td>
                        <td>{{$data.TicketHeight}}</td>
                    </tr>
                {{end}}
            {{ else }}
                <tr><td colspan="3"><p style=" margin: 20px 0; "><span style="font-size: larger;">You don't have Missed tickets.</span></p></td></tr>
            {{end}}
            </tbody>
            </table>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-class="collapsed" data-toggle="collapse" data-parent="#accordion" href="#collapse-ticketsexpired">Expired Tickets</a>
        </h4>
    </div>
    <div id="collapse-ticketsexpired" class="panel-collapse collapse {{if .TicketsExpired}}in{{end}}">
        <div class="panel-body">
            <table id="ticketsexpired" class="table table-condensed datatablesort">
            <thead>
                <tr>
                    <th>Ticket</th>
                    <th>SpentByHeight</th>
                    <th>TicketHeight</th>
                </tr>
            </thead>
            <tfoot>
                <tr>
                    <th>Ticket</th>
                    <th>SpentByHeight</th>
                    <th>TicketHeight</th>
                </tr>
            </tfoot>
            <tbody>
            {{if .TicketsExpired}}
                {{ range $i, $data := .TicketsExpired }}
                    <tr>
                        <td><a target="_blank" href="https://{{$.Network}}.decred.org/tx/{{$data.Ticket}}">{{$data.Ticket}}</a></td>
                        <td>{{$data.SpentByHeight}}</td>
                        <td>{{$data.TicketHeight}}</td>
                    </tr>
                {{end}}
            {{else}}
                <tr><td colspan="3"><p style=" margin: 20px 0; "><span style="font-size: larger;">You don't have Expired tickets.</span></p></td></tr>
            {{end}}
            </tbody>
            </table>
        </div>
    </div>
</div>

<div class="panel panel-default">
    <div class="panel-heading">
        <h4 class="panel-title">
            <a data-toggle="collapse" data-parent="#accordion" href="#collapse-ticketsinvalid">Invalid Tickets</a>
        </h4>
    </div>
    <div id="collapse-ticketsinvalid" class="panel-collapse collapse {{if .TicketsInvalid}}in{{end}}">
        <div class="panel-body">
            <table id="ticketsinvalid" class="table table-condensed table-responsive">
            <thead>
                <div id="invalid-warning" class="alert alert-danger"><b>Alert!</b> Tickets appearing here did not pay the correct pool fee.
                You will either need to vote these tickets yourself or contact your stake pool administrator to have them add
                the ticket to the pool manually.</div>
                <tr>
                    <th>Ticket</th>
                </tr>
            </thead>
            <tfoot>
                <tr>
                    <th>Ticket</th>
                </tr>
            </tfoot>
            <tbody>
            {{if .TicketsInvalid}}
                {{ range $i, $data := .TicketsInvalid }}
                    <tr>
                        <td><a href="https://{{$.Network}}.decred.org/tx/{{$data.Ticket}}">{{$data.Ticket}}</a></td>
                    </tr>
                {{end}}
            {{else}}
                <tr><td><p style=" margin: 20px 0; "><span style="font-size: larger;">You don't have Invalid tickets.</span></p></td></tr>
            {{end}}
            </tbody>
            </table>
        </div>
    </div>
</div>
{{end}}
chappjc commented 8 years ago

@Dyrk Great! The js and html parts were the big unknown for me. This helps a lot.

jolan commented 7 years ago

There's room for improvement but this is mostly addressed by #183.

Closing.