WIP: Bootstrap-based responsive interface, CSS only
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
DataHoarder 2024-03-15 13:17:43 +01:00
parent d07987b750
commit 55661a12da
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
38 changed files with 1682 additions and 540 deletions

View file

@ -1207,7 +1207,7 @@ func main() {
http.Redirect(writer, request, fmt.Sprintf("%s/explorer/tx/%s", cmdutils.GetSiteUrl(cmdutils.SiteKeyP2PoolIo, request.Host == torHost), txId), http.StatusFound)
})
serveMux.HandleFunc("/api/redirect/block/{coinbase:[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
serveMux.HandleFunc("/api/redirect/coinbase/{coinbase:[0-9]+|.?[0-9A-Za-z]+$}", func(writer http.ResponseWriter, request *http.Request) {
foundTarget := index.QueryFirstResult(indexDb.GetFoundBlocks("WHERE side_height = $1", 1, utils.DecodeBinaryNumber(mux.Vars(request)["coinbase"])))
if foundTarget == nil {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")

62
cmd/httputils/compress.go Normal file
View file

@ -0,0 +1,62 @@
package httputils
import (
"slices"
"strings"
)
type ContentEncoding string
/*
func (e ContentEncoding) NewPipe(r io.Reader) (reader io.Reader, err error) {
switch e {
case ContentEncodingNone:
case ContentEncodingGzip:
case ContentEncodingBrotli:
case ContentEncodingZstd:
default:
panic("not supported")
}
}
func (e ContentEncoding) Compress(in, buf []byte) (out []byte, err error) {
switch e {
case ContentEncodingNone:
return append(buf, in...), nil
case ContentEncodingGzip:
case ContentEncodingBrotli:
case ContentEncodingZstd:
default:
panic("not supported")
}
}
*/
const (
ContentEncodingNone ContentEncoding = ""
ContentEncodingGzip ContentEncoding = "gzip"
ContentEncodingBrotli ContentEncoding = "br"
ContentEncodingZstd ContentEncoding = "zstd"
)
func SelectEncodingServerPreference(acceptEncoding string) (contentEncoding ContentEncoding) {
encodings := strings.Split(acceptEncoding, ",")
for i := range encodings {
//drop preference
e := strings.Split(encodings[i], ";")
encodings[i] = strings.TrimSpace(e[0])
}
if slices.Contains(encodings, string(ContentEncodingZstd)) {
return ContentEncodingZstd
} else if slices.Contains(encodings, string(ContentEncodingBrotli)) {
return ContentEncodingBrotli
} else if slices.Contains(encodings, string(ContentEncodingGzip)) {
return ContentEncodingGzip
} else {
return ContentEncodingNone
}
}

6
cmd/web/assets/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.1"
width="6000"
height="6000"
viewBox="0 0 6000 6000"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath16">
<path
d="M 0,4500 H 4500 V 0 H 0 Z"
id="path14" />
</clipPath>
</defs>
<g
id="g8"
transform="matrix(1.3333333,0,0,-1.3333333,0,6000)">
<g
id="g10">
<g
id="g12"
clip-path="url(#clipPath16)">
<g
id="g18"
transform="translate(4128.0488,2250.1904)">
<path
d="m 0,0 c 0,-1037.173 -840.791,-1878.052 -1878.064,-1878.052 -1037.22,0 -1878.025,840.879 -1878.025,1878.052 0,1037.269 840.805,1878.056 1878.025,1878.056 C -840.791,1878.056 0,1037.269 0,0"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path20" />
</g>
<g
id="g22"
transform="translate(2249.9844,4128.2461)">
<path
d="m 0,0 c -1036.895,0 -1879.119,-842.056 -1877.805,-1878.01 0.262,-207.264 33.308,-406.633 95.342,-593.116 h 561.884 V -891.229 L -0.023,-2111.813 1220.537,-891.229 v -1579.897 h 561.957 c 62.117,186.483 95.008,385.852 95.369,593.116 C 1879.672,-841.002 1036.976,-0.247 -0.023,-0.247 Z"
style="fill:#f26821;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path24" />
</g>
<g
id="g26"
transform="translate(1969.3037,1735.8262)">
<path
d="m 0,0 -532.671,532.703 v -994.141 h -203.637 -203.618 l -384.29,-0.07 c 329.634,-540.801 925.347,-902.564 1604.91,-902.564 679.54,0 1275.308,361.846 1604.969,902.647 l -384.445,-0.013 h -364.246 -43.028 V 532.703 L 561.246,0 280.639,-280.607 0.022,0 Z"
style="fill:#4d4d4d;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path28" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -7,6 +7,10 @@ type ApiPage struct {
PoolInfoExample cmdutils.PoolInfoResult
}
func (p *ApiPage) Name() string {
return "api"
}
%}
{% func (p *ApiPage) Style() %}

View file

@ -18,31 +18,38 @@ Page {
{% code
type ContextProviderPage interface {
Page
Name() string
Context() *GlobalRequestContext
}
type ContextSetterPage interface {
ContextProviderPage
SetContext(ctx *GlobalRequestContext)
}
type RefreshProviderPage interface {
Page
IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string)
}
%}
Page prints a page implementing Page interface.
{% func PageTemplate(p ContextProviderPage) %}
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title>{%=h p.Title() %}</title>
<link href="/assets/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<style>
{%= p.Style() %}
</style>
</head>
<body>
{%= Header(p.Context()) %}
<body tabindex="0">
{%= Header(p.Name(), p.Context()) %}
<div class="content center">
<div class="container-md my-3 text-center">
{%= p.Content() %}
</div>
@ -68,6 +75,9 @@ overriding only certain Page methods
func (p *BasePage) Context() *GlobalRequestContext {
return p.ctx
}
func (p *BasePage) Name() string {
return "base"
}
func (p *BasePage) SetContext(ctx *GlobalRequestContext) {
p.ctx = ctx
}

View file

@ -9,6 +9,15 @@ type BlocksPage struct {
FoundBlocks []*index.FoundBlock
Miner *address.Address
}
func (p *BlocksPage) IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string) {
return p.Miner == nil, p.Refresh > 0, p.Refresh, "/blocks?refresh", "/blocks"
}
func (p *BlocksPage) Name() string {
return "blocks"
}
%}
{% func (p *BlocksPage) Title() %}
@ -20,16 +29,6 @@ type BlocksPage struct {
{% endfunc %}
{% func (p *BlocksPage) Content() %}
{% if p.Miner == nil %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/blocks">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/blocks?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{%endif %}
<div style="text-align: center">
{% if p.Miner == nil %}
<h2>Recent Found Monero blocks</h2>

View file

@ -11,6 +11,10 @@ type CalculateShareTimePage struct {
EstimatedSharesPerDay float64
EstimatedBlocksPerDay float64
}
func (p *CalculateShareTimePage) Name() string {
return "calculate-share-time"
}
%}
{% code
@ -54,7 +58,7 @@ type CalculateShareTimePageEffortEntry struct {
</p>
<div>
<label for="hashrate">Your Hashrate</label><br/>
<input type="numeric" name="hashrate" id="hashrate" placeholder="100" size="8" class="mono" value="{% if p.Hashrate > 0 %}{%s str(p.Hashrate) %}{% endif %}"/>
<input type="number" name="hashrate" id="hashrate" placeholder="100" size="8" class="mono" value="{% if p.Hashrate > 0 %}{%s str(p.Hashrate) %}{% endif %}"/>
<select name="magnitude">
<option value="1"{% if p.Magnitude == 1 %} selected{% endif %}>H/s</option>
<option value="1000"{% if p.Magnitude == 1000 %} selected{% endif %}>KH/s</option>
@ -81,15 +85,15 @@ type CalculateShareTimePageEffortEntry struct {
<td style="width: 15em"><strong>P2Pool Difficulty</strong><br/>{%s si_units(p.Context().Pool.SideChain.LastBlock.Difficulty, 2) %}</td>
<td style="width: 15em"><strong>P2Pool Hashrate</strong><br/>{%s si_units(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime), 2) %}H/s</td>
<td style="width: 15em"><strong>Your Hashrate</strong><br/>{%s si_units(p.Hashrate * p.Magnitude, 2) %}H/s</td>
<td title="Mean frequency between P2Pool shares" style="width: 15em; border: #ff6600 dashed 1px;"><strong>Your Share Mean<br/>{%s time_duration_long(between) %}</strong></td>
<td title="Mean frequency between P2Pool shares" style="width: 15em; border: #ff6600 dashed 1px;"><strong>Your Share Mean<br/>{%s time_duration_long_pad(between) %}</strong></td>
<td title="Mean P2Pool shares per day" style="width: 15em; border: #ff6600 dashed 1px;"><strong>Your Daily Mean Shares<br/>{%f.3 p.EstimatedSharesPerDay %} share(s)</strong></td>
</tr>
<tr><th colspan="5">&nbsp;</th></tr>
<tr style="line-height: 1.5;">
<td><strong>Monero Difficulty</strong><br/>{%s si_units(p.Context().Pool.MainChain.Difficulty.Lo, 2) %}</td>
<td><strong>Monero Hashrate</strong><br/>{%s si_units(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime)), 2) %}H/s</td>
<td title="Mean frequency between P2Pool finds Monero Blocks"><strong>P2Pool Block Mean</strong><br/><em>{%s time_duration_long(float64(p.Context().Pool.MainChain.Difficulty.Lo) / float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime))) %}</em></td>
<td title="Mean frequency between Solo Monero Blocks (without P2Pool)"><strong>Your Solo Block Mean</strong><br/><em>{%s time_duration_long(between_solo) %}</em></td>
<td title="Mean frequency between P2Pool finds Monero Blocks"><strong>P2Pool Block Mean</strong><br/><em>{%s time_duration_long_pad(float64(p.Context().Pool.MainChain.Difficulty.Lo) / float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime))) %}</em></td>
<td title="Mean frequency between Solo Monero Blocks (without P2Pool)"><strong>Your Solo Block Mean</strong><br/><em>{%s time_duration_long_pad(between_solo) %}</em></td>
<td title="Mean Solo Monero Blocks (without P2Pool) per day"><strong>Your Solo Daily Mean Blocks</strong><br/><em>{%f.3 p.EstimatedBlocksPerDay %} block(s)</em></td>
</tr>
<tr><th colspan="5">&nbsp;</th></tr>
@ -115,10 +119,10 @@ type CalculateShareTimePageEffortEntry struct {
{%f.5 e.Probability %}%
</td>
<td>
{%s time_duration_long(e.Between) %}
{%s time_duration_long_pad(e.Between) %}
</td>
<td>
{%s time_duration_long(e.BetweenSolo) %}
{%s time_duration_long_pad(e.BetweenSolo) %}
</td>
</tr>
{% endfor %}
@ -145,7 +149,7 @@ type CalculateShareTimePageEffortEntry struct {
{%f.0 e.Effort %}%
</td>
<td>
{%s time_duration_long(e.Between) %}
{%s time_duration_long_pad(e.Between) %}
</td>
{% code var mode = utils.ProbabilityMode(e.ShareProbabilities...) %}
{% for _, p := range e.ShareProbabilities %}

View file

@ -14,6 +14,10 @@ type ConnectivityCheckPage struct {
OurTip *index.SideBlock
Check *p2pooltypes.P2PoolConnectionCheckInformation[*sidechain.PoolBlock]
}
func (p *ConnectivityCheckPage) Name() string {
return "connectivity-check"
}
%}
{% func (p *ConnectivityCheckPage) Title() %}

View file

@ -25,6 +25,7 @@ type GlobalRequestContext struct {
}
}
HexBuffer [types.HashSize * 2]byte
IsRefresh func() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string)
}
func (ctx *GlobalRequestContext) GetUrl(host string) string {

View file

@ -0,0 +1,17 @@
/* Fix spacing between buttons due to display: inline-block */
.page-tab.my-0, .page-tab.rounded-end-2 {
margin-left: -0.5ch;
}
/* Make them sticky! */
.page-tab {
position: -webkit-sticky;
position: sticky;
top: calc(0.1rem + 41px);
z-index: 1019;
}
/* Set background color */
.page-tab .btn:not(:hover):not(:focus-visible) {
background-color: rgba(var(--bs-dark-rgb), 255) !important;
}

View file

@ -1,10 +1,10 @@
body {
margin: 0;
padding: 0;
color: #d1d1d1;
background-color: #4c4c4c;
font-family: Helvetica, Arial, sans-serif;
html {
scroll-padding-top: calc(41px + 31px + 2rem);
}
*:target {
outline: none;
}
h1, h2, h3, h4, h5, h6 {
@ -15,13 +15,8 @@ ul{
list-style-position: inside;
}
.datatable tr, li, code.mono {
font-size: 12px;
}
.content {
margin-top: 15px;
margin-bottom: 30px;
.datatable tr, code.mono {
white-space: nowrap;
}
.center {
@ -34,6 +29,19 @@ ul{
background: rgba(0%, 75.3%, 0%, 0.5);
}
.position-chart{
font-family: "Lucida Console", Monaco, monospace;
white-space: nowrap;
line-break: strict;
/* dynamic font size */
font-size: 12px;
/*font-size: min(0.8vw, 14px);*/
}
.found-by a {
font-weight: bold;
}
.mono {
font-family: "Lucida Console", Monaco, monospace;
white-space: nowrap;
@ -41,13 +49,12 @@ ul{
}
a, a:hover, a:visited, a:link, a:active {
text-decoration: underline dotted #333;
text-decoration: underline dotted #777;
color: inherit;
}
form {
display: inline-block;
text-align: center;
.navbar a, a.btn, .card-header a {
text-decoration: none;
}
h1 {
@ -73,4 +80,118 @@ code.small {
code.smaller {
font-size: 8px;
}
nav.navbar {
padding: 0.1rem 0 0.1rem 0;
white-space: nowrap;
}
/* Make menu collapse work without JavaScript */
.navbar .navbar-toggler:focus~.navbar-collapse {
display: block;
}
.navbar .navbar-collapse:focus-within {
display: block;
}
.navbar .navbar-collapse:hover {
display: block;
}
/* Make dropdowns work without JavaScript */
.dropdown>.dropdown-toggle:focus~.dropdown-menu {
display: block;
}
.dropdown>.dropdown-menu:focus-within {
display: block;
}
.dropdown>.dropdown-menu:hover {
display: block;
}
/* Position graph */
.position-graph {
display: block;
white-space: nowrap;
line-break: strict;
position: relative;
background: #555;
margin-bottom: 1rem;
}
.position-graph .position-graph-divider {
padding: unset;
margin: unset;
position: absolute;
}
.position-graph .position-graph-divider.position-graph-divider-x {
height: 100%;
border-left: #ccc 1px dashed;
}
.position-graph .position-graph-divider.position-graph-divider-y {
width: 100%;
border-top: #ccc 1px solid;
}
.position-graph .position-graph-divider.position-graph-divider-x .position-graph-divider-label {
position: absolute;
top: 0;
font-size: 60%;
}
.position-graph .position-graph-divider.position-graph-divider-y .position-graph-divider-label {
margin-top: -0.25em;
float: left !important;
font-size: 60%;
}
.position-graph .position-data .p {
position: absolute;
}
.position-graph .position-data .p {
position: absolute;
border: #333 1px solid;
height: 10px;
width: 10px;
border-radius: 50%;
background-color: #fff;
cursor: pointer;
display: block;
}
[data-tooltip] {
&:hover {
&:before, &:after {
display:block;
position:absolute;
font-size:0.8em;
color:white;
}
&:before {
border-radius:0.2em;
content:attr(title);
background-color:rgba(0,0,0,0.9);
margin-top:-2.5em;
padding:0.3em;
}
&:after {
content:'';
margin-top:-2.1em;
margin-left:1em;
border-style:solid;
border-color:transparent;
border-top-color:rgba(0,0,0,0.9);
border-width:0.5em 0.5em 0 0.5em;
}
}
}
/* Missing bootstrap stuff */
@media (min-width: 1400px) {
.row-cols-xxl-8 > * {
flex: 0 0 auto;
width: 12.5%;
}
}

View file

@ -8,6 +8,10 @@ type ErrorPage struct {
Message string
Error any
}
func (p *ErrorPage) Name() string {
return "error"
}
%}
{% func (p *ErrorPage) Title() %}

View file

@ -1,15 +1,11 @@
{% func Footer(ctx *GlobalRequestContext) %}
<div>
<footer class="center" style="text-align: center; margin-bottom: 20px;">
<a href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer">source code</a>
&mdash;
<a href="https://{%s ctx.NetServiceAddress %}">{%s ctx.NetServiceAddress %}</a>
::
<footer class="center smaller" style="text-align: center; margin-bottom: 20px;">
ClearNet <a href="https://{%s ctx.NetServiceAddress %}">{%s ctx.NetServiceAddress %}</a>
<br/>
Tor <a href="http://{%s ctx.TorServiceAddress %}">{%s ctx.TorServiceAddress %}</a>
<br/>
Donate to P2Pool Observer developer on <a class="mono small" style="font-weight: bold" href="monero:{%s ctx.DonationAddress %}?tx_description=p2pool.observer">{%s ctx.DonationAddress %}</a> or OpenAlias <span class="mono">p2pool.observer</span> :: <a href="https://github.com/SChernykh/p2pool#donations">Donate to P2Pool Development</a>
<br/>
<strong>NOTE:</strong> This site is in development. You might find errors, incomplete or invalid data. :: <a target="_blank" href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer/issues?state=open">Report any issues on tracker</a>, via IRC on <a target="_blank" href="ircs://irc.libera.chat:6697/#p2pool-observer">#p2pool-observer@libera.chat</a>, or via <a rel="nofollow" href="https://matrix.to/#/#p2pool-observer:monero.social">Matrix</a>
<strong>NOTE:</strong> This site is in development. You might find errors, incomplete or invalid data.
</footer>
</div>
{% endfunc %}

View file

@ -13,6 +13,20 @@ import (
"time"
)
func slice_maxSize[T any](v []T, size int) []T {
if len(v) > size {
v = v[:size]
}
return v
}
func slice_modulo[T any](v []T, multiple int) []T {
if len(v) > multiple {
v = v[:len(v)-len(v)%multiple]
}
return v
}
func utc_date[T int64 | uint64 | int | float64](v T) string {
return time.Unix(int64(v), 0).UTC().Format("02-01-2006 15:04:05 MST")
}
@ -102,6 +116,50 @@ func time_duration_long[T int64 | uint64 | int | float64](v T) string {
return strings.Join(result, " ")
}
func time_duration_long_pad[T int64 | uint64 | int | float64](v T) string {
diff := time.Second * time.Duration(v)
diff += time.Microsecond * time.Duration((float64(v)-float64(int64(v)))*1000000)
days := int64(diff.Hours() / 24)
hours := int64(diff.Hours()) % 24
minutes := int64(diff.Minutes()) % 60
seconds := int64(diff.Seconds()) % 60
ms := int64(diff.Milliseconds()) % 1000
var result []string
if days > 0 {
result = append(result, strconv.FormatInt(days, 10)+"d")
}
if diff.Hours() >= 1 {
if hours < 10 {
result = append(result, "0"+strconv.FormatInt(hours, 10)+"h")
} else {
result = append(result, strconv.FormatInt(hours, 10)+"h")
}
}
if diff.Minutes() >= 1 {
if minutes < 10 {
result = append(result, "0"+strconv.FormatInt(minutes, 10)+"m")
} else {
result = append(result, strconv.FormatInt(minutes, 10)+"m")
}
}
if diff.Seconds() >= 1 {
if seconds < 10 {
result = append(result, "0"+strconv.FormatInt(seconds, 10)+"s")
} else {
result = append(result, strconv.FormatInt(seconds, 10)+"s")
}
}
if len(result) == 0 || (len(result) == 1 && seconds > 0) {
result = append(result, strconv.FormatInt(ms, 10)+"ms")
}
return strings.Join(result, " ")
}
func benc(n uint64) string {
return utils.EncodeBinaryNumber(n)
}

View file

@ -1,46 +1,170 @@
{% func Header(ctx *GlobalRequestContext) %}
<div>
<div class="center" style="text-align: center">
<h1 class="center"><a href="/">{% if ctx.SiteTitle == "" %}Monero P2Pool Observer{% else %}Monero {%s ctx.SiteTitle %}{% endif %}</a></h1>
<h4 style="font-size: 16px; margin: 0px">
<a href="https://gupax.io/" rel="nofollow">[Gupax] P2Pool plug and play Mining GUI, by <em>hinto-janai</em></a>
::
<a href="{%s ctx.GetUrl("p2pool.io") %}/#help" rel="nofollow">P2Pool Setup Help</a>
::
<a href="{%s ctx.GetUrl("p2pool.io") %}/#faq" rel="nofollow">P2Pool FAQ</a>
::
<a href="https://github.com/SChernykh/p2pool">What is P2Pool?</a>
::
<a href="/api">Observer API Documentation</a>
</h4>
{% if ctx.Socials.Irc.Link != "" %}
<h4 style="font-size: 16px; margin: 4px 0px 0px; font-weight: normal;">
Join chat to talk, send feedback, ask questions and be notified of changes on <a href="{%s ctx.Socials.Irc.Link %}" target="_blank"><strong>IRC on <code>{%s ctx.Socials.Irc.Title %}</code></strong></a>{% if ctx.Socials.Irc.WebChat != "" %}, on <a href="{%s ctx.Socials.Irc.WebChat %}" target="_blank"><strong>WebIRC</strong></a>{% endif %}{% if ctx.Socials.Matrix.Link != "" %}, on <a target="_blank" rel="nofollow" href="{%s ctx.Socials.Matrix.Link %}"><strong>Matrix</strong></a>{% endif %}
</h4>
{% endif %}
<h4 style="font-size: 13px; margin: 4px 0px 0px; font-weight: normal;">
<a href="https://github.com/SChernykh/p2pool#how-to-mine-on-p2pool">How to join P2Pool</a>
::
<a href="{%s ctx.GetUrl("sethforprivacy.com") %}/guides/run-a-p2pool-node/" rel="nofollow">Guide on how to run a Monero and P2Pool node by <em>Seth</em></a>
::
<a href="{%s ctx.GetUrl("xmrvsbeast.com") %}/p2pool/" rel="nofollow">P2Pool Bonus Hash Rate Raffle by <em>XMRvsBEAST</em></a>
</h4>
{% if pool := ctx.Pool; pool != nil %}
{% func HeaderLink(name, target, description, currentName string) %}
{% if name == currentName %}
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{%s target %}">{%s description %}</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{%s target %}">{%s description %}</a>
</li>
{% endif %}
{% endfunc %}
{% func Header(name string, ctx *GlobalRequestContext) %}
<nav class="navbar sticky-top navbar-expand-md bg-body-tertiary">
<div class="container-md">
<a class="navbar-brand" href="/">
<img src="/assets/monero-symbol.svg" alt="Monero" width="30" height="30" class="d-inline-block align-text-top"/>
<span class="d-inline-block align-text-top">{% if ctx.SiteTitle == "" %}P2Pool Observer{% else %}{%s ctx.SiteTitle %}{% endif %}</span>
</a>
<button class="navbar-toggler" type="button" aria-expanded="false" aria-label="Toggle navigation" tabindex="-1">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
{%= HeaderLink("blocks", "/blocks", "Blocks", name) %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" tabindex="-1">
Observer
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/miners">Current Window Miners</a></li>
<li><a class="dropdown-item" href="/miners?weekly">Weekly Miners</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/calculate-share-time">Share Time Calculator</a></li>
<li><a class="dropdown-item" href="/connectivity-check">Connectivity Checker</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/transaction-lookup">Sweep Transaction Lookup</a></li>
<li><a class="dropdown-item" href="/sweeps">Recent Likely Sweeps</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/api">Observer API</a></li>
<li><a class="dropdown-item" href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer">Observer source code</a></li>
<li><a class="dropdown-item" target="_blank" href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer/issues?state=open">Report Observer issues</a></li>
<li><a class="dropdown-item" href="ircs://irc.libera.chat:6697/#p2pool-observer" target="_blank">Observer on IRC <code>#p2pool-observer@libera.chat</code></a></li>
<li><a class="dropdown-item" href="https://matrix.to/#/#p2pool-observer:monero.social" target="_blank" rel="nofollow">Observer on Matrix</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<div class="d-grid gap-2 m-3">
<a class="btn btn-success btn-sm" href="monero:{%s ctx.DonationAddress %}?tx_description=p2pool.observer">Donate to P2Pool Observer maintainer</a>
<input class="form-control mono smaller user-select-all" style="height: calc(24px + 6px + 6px);" value="{%s ctx.DonationAddress %}" readonly />
<div>OpenAlias <span class="mono user-select-all">p2pool.observer</span></div>
</div>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" tabindex="-1">
Community
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{%s ctx.Socials.Irc.Link %}" target="_blank">Talk on IRC <code>{%s ctx.Socials.Irc.Title %}</code></a></li>
{% if ctx.Socials.Irc.WebChat != "" %}
<li><a class="dropdown-item" href="{%s ctx.Socials.Irc.WebChat %}" target="_blank">Talk on WebIRC</a></li>
{% endif %}
{% if ctx.Socials.Matrix.Link != "" %}
<li><a class="dropdown-item" href="{%s ctx.Socials.Matrix.Link %}" target="_blank" rel="nofollow">Talk on Matrix</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="https://gupax.io/" rel="nofollow">[Gupax] P2Pool plug and play Mining GUI, by <em>hinto-janai</em></a></li>
<li><a class="dropdown-item" href="{%s ctx.GetUrl("xmrvsbeast.com") %}/p2pool/" rel="nofollow">P2Pool Bonus Hash Rate Raffle by <em>XMRvsBEAST</em></a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{%s ctx.GetUrl("sethforprivacy.com") %}/guides/run-a-p2pool-node/" rel="nofollow">Guide on how to run a Monero and P2Pool node by <em>Seth</em></a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" tabindex="-1">
Help
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{%s ctx.GetUrl("p2pool.io") %}/#help" rel="nofollow">P2Pool Setup Help</a></li>
<li><a class="dropdown-item" href="{%s ctx.GetUrl("p2pool.io") %}/#faq" rel="nofollow">P2Pool FAQ</a></li>
<li><a class="dropdown-item" href="https://github.com/SChernykh/p2pool">What is P2Pool?</a></li>
<li><a class="dropdown-item" href="https://github.com/SChernykh/p2pool#how-to-mine-on-p2pool">How to join P2Pool</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{%s ctx.Socials.Irc.Link %}" target="_blank">Talk on IRC <code>{%s ctx.Socials.Irc.Title %}</code></a></li>
{% if ctx.Socials.Irc.WebChat != "" %}
<li><a class="dropdown-item" href="{%s ctx.Socials.Irc.WebChat %}" target="_blank">Talk on WebIRC</a></li>
{% endif %}
{% if ctx.Socials.Matrix.Link != "" %}
<li><a class="dropdown-item" href="{%s ctx.Socials.Matrix.Link %}" target="_blank" rel="nofollow">Talk on Matrix</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" target="_blank" href="https://github.com/SChernykh/p2pool/issues">Report P2Pool issues</a></li>
<li><a class="dropdown-item" target="_blank" href="{%s ctx.GetUrl("git.gammaspectra.live") %}/P2Pool/p2pool-observer/issues?state=open">Report Observer issues</a></li>
</ul>
</li>
{% if pool := ctx.Pool; pool != nil %}
<div class="nav-item vr d-none d-lg-flex"></div>
<li class="nav-item d-none d-lg-flex">
<a class="nav-link position-relative" href="{%s pool.Versions.P2Pool.Link %}" target="_blank">
P2Pool {%s pool.Versions.P2Pool.Version %}
{% if uint64(pool.Versions.P2Pool.Timestamp+3600*24*30) > pool.SideChain.LastBlock.Timestamp %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning">
{%f.0 float64((pool.SideChain.LastBlock.Timestamp - uint64(pool.Versions.P2Pool.Timestamp))) / (3600*24) %} days old!
</span>
{%endif%}
</a>
</li>
<li class="nav-item d-none d-lg-flex">
<a class="nav-link position-relative" href="{%s pool.Versions.Monero.Link %}" target="_blank">
Monero {%s pool.Versions.Monero.Version %}
{% if uint64(pool.Versions.Monero.Timestamp+3600*24*7) > pool.SideChain.LastBlock.Timestamp %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning">
{%f.0 float64((pool.SideChain.LastBlock.Timestamp - uint64(pool.Versions.Monero.Timestamp))) / (3600*24) %} days old!
</span>
{%endif%}
</a>
</li>
{% if uint64(pool.Versions.P2Pool.Timestamp+3600*24*30) > pool.SideChain.LastBlock.Timestamp || uint64(pool.Versions.Monero.Timestamp+3600*24*7) > pool.SideChain.LastBlock.Timestamp %}
<h4 style="font-size: 15px; margin: 4px 0px 0px; color:yellow;">
{% else %}
<h4 style="font-size: 13px; margin-top: 5px; ">
{% endif %}
Latest releases: <a href="{%s pool.Versions.P2Pool.Link %}" target="_blank">P2Pool {%s pool.Versions.P2Pool.Version %}</a> <em title="{%s utc_date(uint64(pool.Versions.P2Pool.Timestamp)) %}"><small>{%f.1 float64((pool.SideChain.LastBlock.Timestamp - uint64(pool.Versions.P2Pool.Timestamp))) / (3600*24) %} day(s) ago</small></em>
::
<a href="{%s pool.Versions.Monero.Link %}" target="_blank">Monero {%s pool.Versions.Monero.Version %}</a> <em title="{%s utc_date(uint64(pool.Versions.Monero.Timestamp)) %}"><small>{%f.1 float64((pool.SideChain.LastBlock.Timestamp - uint64(pool.Versions.Monero.Timestamp))) / (3600*24) %} day(s) ago</small></em>
</h4>
</ul>
{% if ctx.IsRefresh != nil %}
{% code ok, isRefresh, refresh, uriRefresh, uriStatic := ctx.IsRefresh() %}
{% if ok %}
<div class="d-flex me-2">
{% if isRefresh %}
<a class="btn btn-sm btn-success position-relative" href="{%s uriStatic %}">
{% else %}
<a class="btn btn-sm btn-outline-success position-relative" href="{%s uriRefresh %}">
{% endif %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41m-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9"/>
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5 5 0 0 0 8 3M3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9z"/>
</svg>
Refresh
{% if isRefresh %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">
{%d refresh %}s
</span>
{% endif %}
</a>
</div>
{% endif %}
{% endif %}
<form class="d-none d-xl-flex" role="search" action="/miner" method="get">
<input name="address" class="form-control me-2" type="search" placeholder="Address" aria-label="Lookup" size="10"
spellcheck="false" autocorrect="off"
pattern="([^0-9].+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)"
title="Only Primary Addresses supported (starts with 4, not 8), or chosen miner alias"
required>
<button class="btn btn-outline-success" type="submit">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
</svg>
</button>
</form>
</div>
</div>
</div>
</nav>
{% endfunc %}

View file

@ -13,125 +13,218 @@ type IndexPage struct {
Shares []*index.SideBlock
FoundBlocks []*index.FoundBlock
}
func (p *IndexPage) IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string) {
return true, p.Refresh > 0, p.Refresh, "/?refresh", "/"
}
func (p *IndexPage) Name() string {
return "index"
}
%}
{% func (p *IndexPage) Content() %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
<div class="container-fluid h-100 mb-3 p-2 bg-body-tertiary border rounded-2 text-center">
<div class="row align-items-start">
<div class="col">
<h2>P2Pool statistics</h2>
</div>
</div>
<div class="row row-cols-2 row-cols-lg-4 align-items-center">
<div class="col pb-2" title="{%= hex(p.Context(), p.Context().Pool.SideChain.LastBlock.TemplateId) %}">
<strong>P2Pool Height</strong>
<br/>
<a href="/share/{%= hex(p.Context(), p.Context().Pool.SideChain.LastBlock.TemplateId) %}">{%dul p.Context().Pool.SideChain.LastBlock.SideHeight %}</a>
</div>
<div class="col pb-2">
<strong>P2Pool Hashrate</strong>
<br/>
{%s si_units(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime), 2) %}H/s
</div>
<div class="col pb-2" title="{%= hex(p.Context(), p.Context().Pool.MainChain.Id) %}"><a href="/b/{%s benc(p.Context().Pool.MainChain.Height) %}">
<strong>Monero Height</strong>
<br/>
<a href="/b/{%s benc(p.Context().Pool.MainChain.Height) %}">{%dul p.Context().Pool.MainChain.Height %}</a>
</div>
<div class="col pb-2">
<strong>Monero Hashrate</strong>
<br/>
{%s si_units(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime)), 2) %}H/s
</div>
<div style="text-align: center">
<h2>P2Pool statistics</h2>
<table class="center" style="max-width: calc(15em + 15em + 15em + 15em)">
<tr>
<th style="width: 15em">P2Pool Height</th>
<th style="width: 15em">P2Pool Hashrate</th>
<th style="width: 15em">Monero Height</th>
<th style="width: 15em">Monero Hashrate</th>
</tr>
<tr>
<td title="{%= hex(p.Context(), p.Context().Pool.SideChain.LastBlock.TemplateId) %}"><a href="/share/{%= hex(p.Context(), p.Context().Pool.SideChain.LastBlock.TemplateId) %}">{%dul p.Context().Pool.SideChain.LastBlock.SideHeight %}</a></td>
<td>{%s si_units(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime), 2) %}H/s</td>
<td title="{%= hex(p.Context(), p.Context().Pool.MainChain.Id) %}"><a href="/b/{%s benc(p.Context().Pool.MainChain.Height) %}">{%dul p.Context().Pool.MainChain.Height %}</a></td>
<td>{%s si_units(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime)), 2) %}H/s</td>
</tr>
<tr><th colspan="4">&nbsp;</th></tr>
<tr>
<th>P2Pool Difficulty</th>
<th title="Percentage of Monero hashrate P2Pool has">P2Pool Monero %</th>
<th>Monero Difficulty</th>
<th title="This includes blocks the site knows about since it started observing. There might be more orphaned or produced by other sidechain not included here.">Blocks Found</th>
</tr>
<tr>
<td>{%s si_units(p.Context().Pool.SideChain.LastBlock.Timestamp, 2) %}</td>
<td>{%f.2 (float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime)) / float64(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime))))*100 %}%</td>
<td>{%s si_units(p.Context().Pool.MainChain.Difficulty.Lo, 2) %}</td>
<td>{%dul p.Context().Pool.SideChain.Found %}</td>
</tr>
<tr><th colspan="4">&nbsp;</th></tr>
<tr>
<th title="Miners that have ever mined a share on P2Pool">Miners Known</th>
<th>Average Effort</th>
<th title="Mean frequency between P2Pool finds Monero Blocks">Block Found Frequency</th>
<th>Last Found Block</th>
</tr>
<tr>
<td>{%dul p.Context().Pool.SideChain.Miners %}</td>
<td >
<span class="small" style="color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average10) %};" title="Last 10 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average10 %}%</span>
/
<span style="font-weight:bolder; color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average50) %};" title="Last 50 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average50 %}%</span>
/
<span class="small" style="color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average200) %};" title="Last 200 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average200 %}%</span>
</td>
<td>{%s time_duration_long(float64(p.Context().Pool.MainChain.Difficulty.Lo) / float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime))) %}</td>
<div class="col pb-2">
<strong>P2Pool Difficulty</strong>
<br/>
{%s si_units(p.Context().Pool.SideChain.LastBlock.Timestamp, 2) %}
</div>
<div class="col pb-1" title="Percentage of Monero hashrate this P2Pool has">
<strong>P2Pool Monero %</strong>
<br/>
{%f.2 (float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime)) / float64(diff_hashrate(p.Context().Pool.MainChain.Difficulty, uint64(p.Context().Pool.MainChain.Consensus.BlockTime))))*100 %}%
</div>
<div class="col pb-2">
<strong>Monero Difficulty</strong>
<br/>
{%s si_units(p.Context().Pool.MainChain.Difficulty.Lo, 2) %}
</div>
<div class="col pb-2" title="This includes blocks the site knows about since it started observing. There might be more orphaned or produced by other sidechain not included here.">
<strong>Blocks Found</strong>
<br/>
{%dul p.Context().Pool.SideChain.Found %}
</div>
<div class="col pb-2" title="Miners that have ever mined a share on P2Pool">
<strong>Miners Known</strong>
<br/>
{%dul p.Context().Pool.SideChain.Miners %}
</div>
<div class="col pb-2">
<strong>Average Effort</strong>
<br/>
<span class="small" style="color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average10) %};" title="Last 10 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average10 %}%</span>
/
<span style="font-weight:bolder; color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average50) %};" title="Last 50 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average50 %}%</span>
/
<span class="small" style="color: {%s effort_color(p.Context().Pool.SideChain.Effort.Average200) %};" title="Last 200 found blocks">{%f.2 p.Context().Pool.SideChain.Effort.Average200 %}%</span>
</div>
<div class="col pb-2" title="Mean frequency between P2Pool finds Monero Blocks">
<strong>Block Found Frequency</strong>
<br/>
{%s time_duration_long(float64(p.Context().Pool.MainChain.Difficulty.Lo) / float64(diff_hashrate(p.Context().Pool.SideChain.LastBlock.Difficulty, p.Context().Consensus.TargetBlockTime))) %}
</div>
<div class="col pb-2">
<strong>Last Found Block</strong>
<br/>
{% if p.Context().Pool.SideChain.LastFound != nil %}
<td title="{%s utc_date(p.Context().Pool.SideChain.LastFound.MainBlock.Timestamp) %}">{%s time_elapsed_short(p.Context().Pool.SideChain.LastFound.MainBlock.Timestamp) %}</td>
{% else %}
<td>-</td>
{% endif %}
</tr>
<tr><th colspan="4">&nbsp;</th></tr>
<tr>
<th title="Current miners on P2Pool PPLNS window"><a href="/miners">Window Miners</a></th>
<th>Current Effort</th>
<th>Window Blocks</th>
<th>Last Share</th>
</tr>
<tr>
<td><a href="/miners">{%d p.Context().Pool.SideChain.Window.Miners %}</a></td>
<td style="font-weight:bolder; color: {%s effort_color(p.Context().Pool.SideChain.Effort.Current) %};">
{%f.2 p.Context().Pool.SideChain.Effort.Current %}%
</td>
<td>{%d p.Context().Pool.SideChain.Window.Blocks %} blocks (+{%d p.Context().Pool.SideChain.Window.Uncles %} uncles)</td>
<td title="{%s utc_date(p.Context().Pool.SideChain.LastBlock.Timestamp) %}">{%s time_elapsed_short(p.Context().Pool.SideChain.LastBlock.Timestamp) %}</td>
</tr>
</table>
</div>
<div style="text-align: center">
<form action="/miner" method="get">
<h2>Lookup miner statistics</h2>
<div>
<label for="miner-address">Payout Monero address or Miner Alias</label><br/>
<input type="text" name="address" id="miner-address" placeholder="{%s p.Context().DonationAddress %}" size="96" class="mono"/>
</div>
<div style="margin-top: 10px">
<input type="submit" value="Lookup" style="width: 20em;"/>
<div class="col pb-2" title="Current miners on P2Pool PPLNS window">
<a href="/miners">
<strong>Window Miners</strong>
<br/>
{%d p.Context().Pool.SideChain.Window.Miners %}
</a>
</div>
</form>
<p>
<a href="/calculate-share-time">[Average share time calculator]</a> :: <a href="/connectivity-check">[Connectivity Check]</a>
</p>
<p>
<a href="/miners">[Current Window Miners]</a> :: <a href="/miners?weekly">[Weekly Miners]</a>
</p>
<p>
<a href="/transaction-lookup">[Sweep Transaction Lookup]</a> :: <a href="/sweeps">[Recent Likely Sweep Transactions]</a>
</p>
<div class="col pb-2">
<strong>Current Effort</strong>
<br/>
<span style="font-weight:bolder; color: {%s effort_color(p.Context().Pool.SideChain.Effort.Current) %};">{%f.2 p.Context().Pool.SideChain.Effort.Current %}%</span>
</div>
<div class="col pb-2">
<strong>Window Blocks</strong>
<br/>
{%d p.Context().Pool.SideChain.Window.Blocks %} blocks (+{%d p.Context().Pool.SideChain.Window.Uncles %} uncles)
</div>
<div class="col pb-2">
<strong>Last Share</strong>
<br/>
<span title="{%s utc_date(p.Context().Pool.SideChain.LastBlock.Timestamp) %}">{%s time_elapsed_short(p.Context().Pool.SideChain.LastBlock.Timestamp) %}</span>
</div>
</div>
</div>
<hr/>
<div class="container-fluid mb-2">
<div class="row">
<div class="col-12 col-md-6 mb-2 p-3 bg-body-tertiary border rounded-2">
<div class="h-100">
<h3>Lookup miner statistics</h3>
<div style="text-align: center">
<h2>Recent Monero blocks found by P2Pool miners</h2>
<form action="/miner" method="get">
<div class="mb-3">
<label for="miner-address">Payout Monero address or Miner Alias</label>
<input type="text" name="address" id="miner-address"
class="form-control mono smaller" style="height: calc(24px + 6px + 6px);"
placeholder="{%s p.Context().DonationAddress %}"
size="96" spellcheck="false" autocorrect="off"
pattern="([^0-9].+|4[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+)"
title="Only Primary Addresses supported (starts with 4, not 8), or chosen miner alias"
required/>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-secondary">Lookup</button>
</div>
</form>
</div>
</div>
{%= TemplateFoundBlocks(p.Context(), p.FoundBlocks, false) %}
<div class="center"><a href="/blocks">[show more found blocks]</a></div>
<div class="col-12 col-md-6 mb-2 p-0">
<div class="ms-md-2 p-3 bg-body-tertiary border rounded-2 h-100">
<h3>Share Time Calculator</h3>
<h3>Blocks found during last day</h3>
<code class="mono">{%s p.Positions.BlocksFound.String() %}</code>
<form action="/calculate-share-time" method="get">
<div class="row g-2 mb-3">
<div class="col-8">
<label for="hashrate">Your Hashrate</label>
<input type="text" inputmode="decimal" pattern="[0-9]+([\.,][0-9]+)?" name="hashrate" id="hashrate" class="form-control" placeholder="100" size="8" required/>
</div>
<div class="col-4">
<label for="magnitude"></label>
<select name="magnitude" class="form-select">
<option value="1">H/s</option>
<option value="1000" selected>KH/s</option>
<option value="1000000">MH/s</option>
<option value="1000000000">GH/s</option>
</select>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-secondary">Calculate</button>
</div>
</form>
</div>
</div>
</div>
</div>
<hr/>
<div style="text-align: center">
<h2>Recent P2Pool shares found</h2>
{%= TemplateShares(p.Context(), p.Shares, false, nil) %}
<div class="container-fluid mb-3 p-2 bg-body-tertiary border rounded-2 text-center">
<div class="row align-items-start">
<div class="col">
<h2>Recent Monero blocks found</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{% code dividersX, dividersY, points := NewBlocksPositionChart(p.Context(), p.FoundBlocks, DefaultBlocksPositionChartDuration) %}
{%= TemplatePositionGraph(dividersX, dividersY, points, 120, "24 hours ago", "Now") %}
</div>
<div class="col col-12">
{%= TemplateFoundBlocks(p.Context(), slice_maxSize(p.FoundBlocks, 12), false) %}
</div>
<div class="col my-3 col-12">
<a href="/blocks" class="btn btn-lg btn-primary">
More found blocks
</a>
</div>
</div>
</div>
<div class="container-fluid mb-3 p-2 bg-body-tertiary border rounded-2 text-center">
<div class="row align-items-start">
<div class="col">
<h2>Recent P2Pool shares found</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{%= TemplateShares(p.Context(), p.Shares, false, nil) %}
</div>
</div>
</div>
{% endfunc %}

View file

@ -9,6 +9,10 @@ type MinerOptionsPage struct {
SignedAction *cmdutils.SignedAction
WebHooks []*index.MinerWebHook
}
func (p *MinerOptionsPage) Name() string {
return "miner-options"
}
%}
{% func (p *MinerOptionsPage) Title() %}

View file

@ -3,6 +3,16 @@
{% import p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types" %}
{% import cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils" %}
{% import "fmt" %}
{% import (
_ "embed"
) %}
{% code
//go:embed "css/miner.css"
var minerCssContent string
%}
{% code
type MinerPage struct {
@ -25,6 +35,8 @@ type MinerPage struct {
Weight uint64
Miner *cmdutils.MinerInfoResult
LastPoolBlock *sidechain.PoolBlock
DayShares []*index.SideBlock
DaySharesEfforts []float64
LastShares []*index.SideBlock
LastSharesEfforts []float64
LastOrphanedShares []*index.SideBlock
@ -36,86 +48,76 @@ type MinerPage struct {
HashrateLocal float64
MagnitudeLocal float64
}
func (p *MinerPage) IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string) {
baseAddress := string(p.Miner.Address.ToBase58())
return true, p.Refresh > 0, p.Refresh, fmt.Sprintf("/miner/%s?refresh", baseAddress), fmt.Sprintf("/miner/%s", baseAddress)
}
func (p *MinerPage) Name() string {
return "miner"
}
%}
{% func (p *MinerPage) Style() %}
{%= p.BasePage.Style() %}
{%s= minerCssContent %}
{% endfunc %}
{% func (p *MinerPage) Title() %}
{%= p.BasePage.Title() %} - Miner {%z= p.Miner.Address.ToBase58() %}
{% endfunc %}
{% func (p *MinerPage) Content() %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/miner/{%z= p.Miner.Address.ToBase58() %}">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/miner/{%z= p.Miner.Address.ToBase58() %}?refresh">Autorefresh is OFF</a>
<div class="btn-group btn-group-sm page-tab" role="group">
<a href="#page-miner" class="btn p-1 btn-outline-secondary">Miner</a>
<a href="#page-payouts" class="btn p-1 btn-outline-secondary">Payouts</a>
<a href="#page-shares" class="btn p-1 btn-outline-secondary">Shares</a>
{% if len(p.LastFound) > 0 %}
<a href="#page-blocks" class="btn p-1 btn-outline-secondary">Blocks</a>
{% endif %}
<a href="#page-sweeps" class="btn p-1 btn-outline-secondary">Sweeps</a>
<a href="/miner-options/{%z= p.Miner.Address.ToBase58() %}" class="btn btn-outline-secondary">Options</a>
</div>
<div style="text-align: center">
<div id="page-miner" class="my-3">
<div style="text-align: center">
{% if p.Miner.LastShareTimestamp == 0 %}
<div style="border: #d1d1d1 1px dashed;">
<h3 style="color:#FF4433">No shares have been reported to this P2Pool network in the past for this miner.</h3>
<div class="alert alert-warning" role="alert">
<h3>No shares have been reported to this P2Pool network in the past for this miner.</h3>
<p>Finding shares is a random process based on your hashrate vs this P2Pool hashrate. This can take several days for low hashrate miners, and depending on your luck.</p>
<p>Please use the calculator below to find your average share time (with average luck).</p>
<p>You can also verify you are reachable by opening ports with the tool below.</p>
<p>Please use the Share Time Calculator below to find your average share time (with average luck).</p>
<p>You can also verify you are reachable by opening ports with the checker below.</p>
<br/>
<form action="/calculate-share-time" method="get">
<h3>Average Share Time Calculator</h3>
<label for="hashrate">Your Hashrate</label><br/>
<input type="numeric" name="hashrate" id="hashrate" placeholder="100" size="8" class="mono" value=""/>
<select name="magnitude">
<option value="1" selected>H/s</option>
<option value="1000">KH/s</option>
<option value="1000000">MH/s</option>
<option value="1000000000">GH/s</option>
</select>
<br/>
<input type="submit" value="Calculate" style="width: 20em; margin: 20px;"/>
</form>
<form action="/connectivity-check" method="get">
<h3>Connectivity Check</h3>
<div>
<label for="address">IP Address and Port</label><br/>
<input type="text" name="address" id="address" placeholder="5.9.17.234:{%dul uint64(p.Context().Consensus.DefaultPort()) %}" size="20" class="mono" value=""/>
</div>
<br/>
<div style="margin-top: 10px">
<input type="submit" value="Check" style="width: 20em;"/>
</div>
</form>
<a class="btn btn-lg btn-primary text-white" href="/calculate-share-time">Share Time Calculator</a>
<a class="btn btn-lg btn-primary text-white" href="/connectivity-check">Connectivity Check</a>
</div>
<hr/>
{% endif %}
{% if p.LastPoolBlock != nil && p.LastPoolBlock.ShareVersion() < sidechain.ShareVersion_V2 && p.LastPoolBlock.ShareVersionSignaling() != sidechain.ShareVersion_V2 %}
<div style="border: #d1d1d1 1px dashed;">
<h3 style="color:#FF4433" title="Share version {%s p.LastPoolBlock.ShareVersion().String() %}, signaling {%s p.LastPoolBlock.ShareVersionSignaling().String() %}">Recent shares indicate you are running an outdated version of P2Pool</h3>
<div class="alert alert-danger" role="alert">
<h3 title="Share version {%s p.LastPoolBlock.ShareVersion().String() %}, signaling {%s p.LastPoolBlock.ShareVersionSignaling().String() %}">Recent shares indicate you are running an outdated version of P2Pool</h3>
<p>A new version of <a href="https://github.com/SChernykh/p2pool/releases/tag/v3.3">P2Pool (v3.0+)</a> has been released with several improvements, which requires a consensus change.</p>
<p>P2Pool (not Monero!) has hardforked to new consensus rules on <strong>March 18th, 2023 at 21:00 UTC</strong>. All versions before P2Pool v3.0 are incompatible. P2Pool v3.3+ is recommended.</p>
<p>If you keep using previous versions, you will keep mining as usual, but become almost a solo miner, as incompatible clients will mine on their own.</p>
<p>{% if p.Context().NetServiceAddress == "p2pool.observer" %}After the fork, you can check on <a href="{%s p.Context().GetUrl("old.p2pool.observer") %}/miner/{%z= p.Miner.Address.ToBase58() %}">OLD.P2Pool.Observer</a>.{% elseif p.Context().NetServiceAddress == "mini.p2pool.observer" %}After the fork, you can check on <a href="https://{%s p.Context().GetUrl("old-mini.p2pool.observer") %}/miner/{%z= p.Miner.Address.ToBase58() %}">OLD-MINI.P2Pool.Observer</a>.{% else %}Please check on an observer tracking the old chain.{% endif %}</p>
<p>After upgrading to a supported P2Pool version and mining a share, this message will be dismissed.</p>
<br/>
</div>
<hr/>
{% elseif p.LastPoolBlock != nil && p.LastPoolBlock.ShareVersion() > sidechain.ShareVersion_V1 && p.LastPoolBlock.Side.ExtraBuffer.SoftwareId == p2pooltypes.SoftwareIdP2Pool && p.LastPoolBlock.Side.ExtraBuffer.SoftwareVersion.String() != p.Context().Pool.Versions.P2Pool.ShortVersion().String() %}
<div style="border: #d1d1d1 1px dashed;">
<h3 style="color:#FF4433">Recent shares indicate you are running an older version of P2Pool</h3>
<div class="alert alert-warning" role="alert">
<h3>Recent shares indicate you are running an older version of P2Pool</h3>
<p><a href="{%s p.Context().Pool.Versions.P2Pool.Link %}">P2Pool {%s p.Context().Pool.Versions.P2Pool.Version %}</a> has been released.</p>
<p>Your most recent share indicates are currently running {%= software_info(p.LastPoolBlock.Side.ExtraBuffer.SoftwareId, p.LastPoolBlock.Side.ExtraBuffer.SoftwareVersion) %}. It is recommended to upgrade.</p>
<p>After upgrading to this P2Pool version and mining a share, this message will be dismissed.</p>
<br/>
</div>
<hr/>
@ -163,115 +165,179 @@ type MinerPage struct {
<td>{%s monero_to_xmr(p.ExpectedRewardPerDay) %} XMR</td>
</tr>
</table>
</div>
</div>
<hr/>
<div class="container-fluid my-3 p-2 bg-body-tertiary border rounded-2">
<div style="text-align: center;">
<h2>Share positions</h2>
<p class="small">
Shares appear on the right, and get older towards the left.
<br/>
Number denotes the amount of shares per slice, with the plus (<code>+</code>) character being more than 9, and dot (<code>.</code>) being none.
<br/>
Each slice accounts for {%d p.Positions.Resolution %} P2Pool blocks, or around {%dul (uint64(p.Positions.Resolution) * p.Context().Consensus.TargetBlockTime) / 60 %} minutes.
</p>
<h3>Shares and uncles in PPLNS window</h3>
<p class="small">
Each slice accounts for {%d p.Positions.ResolutionWindow %} P2Pool block heights, or around {%dul (uint64(p.Positions.ResolutionWindow) * p.Context().Consensus.TargetBlockTime) / 60 %} minutes.
<br/>
Shares within the PPLNS window will be weighted towards receiving a payout when any Monero block is found by any P2Pool miner.
</p>
<code class="mono">{%s p.Positions.BlocksInWindow.String() %}</code>
<br/>
<code class="mono">{%s p.Positions.UnclesInWindow.String() %}</code>
<div class="row row-cols-1 row-cols-xxl-12">
<div class="col col-xxl-8 mb-2">
<h3>Shares during last day</h3>
<h3>Shares and uncles during last day</h3>
<p class="small">
Each slice accounts for {%d p.Positions.Resolution %} P2Pool block heights, or around {%dul (uint64(p.Positions.Resolution) * p.Context().Consensus.TargetBlockTime) / 60 %} minutes.
<br/>
The pipe (<code>|</code>) character denotes roughly the PPLNS window end.
</p>
<code class="mono">{%s p.Positions.Blocks.StringWithSeparator(p.Positions.SeparatorIndex) %}</code>
<br/>
<code class="mono">{%s p.Positions.Uncles.StringWithSeparator(p.Positions.SeparatorIndex) %}</code>
{% if p.HashrateSubmit || (p.Positions.Blocks.Total() + p.Positions.Uncles.Total()) > 2 %}
{% code dividersX, dividersY, points := NewSharesPositionChart(p.Context(), p.DayShares, &p.LastPayouts, &p.DaySharesEfforts, DefaultBlocksPositionChartDuration) %}
{%= TemplatePositionGraph(dividersX, dividersY, points, 120, "24 hours ago", "Now") %}
{% else %}
{% code dividersX, dividersY, points := NewSharesPositionChart(p.Context(), p.DayShares, &p.LastPayouts, nil, DefaultBlocksPositionChartDuration) %}
{%= TemplatePositionGraph(dividersX, dividersY, points, 120, "24 hours ago", "Now") %}
{% endif %}
</div>
<div class="col col-xxl-4 mb-2">
<h3>Shares in PPLNS window</h3>
{% if p.Positions.Payouts.Total() > 0 %}
<h3>Payouts during last day</h3>
<code class="mono">{%s p.Positions.Payouts.StringWithSeparator(p.Positions.SeparatorIndex) %}</code>
{% endif %}
<code class="position-chart">{%s p.Positions.BlocksInWindow.String() %}</code>
<br/>
<code class="position-chart">{%s p.Positions.UnclesInWindow.String() %}</code>
</div>
<div class="col col-xxl-12 mb-2">
<div class="alert alert-secondary small mt-2">
Shares appear on the right, and get older towards the left.
<br/>
Uncle shares are displayed as squares on the graph.
<br/>
Payouts are displayed via dashed vertical lines on the graph.
<br/>
Shares within the PPLNS window will be weighted towards receiving a payout when any Monero block is found by any P2Pool miner.
</div>
</div>
</div>
</div>
</div>
<br/>
<br/>
<hr/>
<div style="text-align: center">
<h2>Most recent payouts</h2>
{%= TemplatePayoutsSlice(p.Context(), p.LastPayouts) %}
<div class="center"><a href="/payouts/{%z= p.Miner.Address.ToBase58() %}">[show all historical payouts]</a></div>
</div>
<hr/>
<div style="text-align: center">
<h2>Most recent shares</h2>
{% if p.HashrateSubmit || (p.Positions.Blocks.Total() + p.Positions.Uncles.Total()) > 2 %}
{%= TemplateShares(p.Context(), p.LastShares, true, &p.LastSharesEfforts) %}
{% else %}
{%= TemplateShares(p.Context(), p.LastShares, true, nil) %}
{% if p.Miner.LastShareTimestamp != 0 %}
<div class="my-3" style="text-align: center">
<form action="/calculate-share-time" method="get">
<h3>Effort Calculation</h3>
<p>Local hashrate of each P2Pool miner is not known by the network. A guess is calculated based on daily estimation. If you provide a value here, it will be more accurate for effort calculation.</p>
<p>This data will not be saved.</p>
<label for="hashrate_local">Your Local Hashrate</label><br/>
<input type="number" name="hashrate" id="hashrate_local" placeholder="100" size="8" class="mono" value="{% if p.HashrateLocal > 0 %}{%s str(p.HashrateLocal) %}{% endif %}"/>
<select name="magnitude">
<option value="1"{% if p.MagnitudeLocal == 1 %} selected{% endif %}>H/s</option>
<option value="1000"{% if p.MagnitudeLocal == 1000 %} selected{% endif %}>KH/s</option>
<option value="1000000"{% if p.MagnitudeLocal == 1000000 %} selected{% endif %}>MH/s</option>
<option value="1000000000"{% if p.MagnitudeLocal == 1000000000 %} selected{% endif %}>GH/s</option>
</select>
<br/>
<input type="submit" value="Calculate" style="width: 20em; margin: 20px;"/>
</form>
</div>
{% endif %}
</div>
<hr/>
{% if len(p.LastOrphanedShares) > 0 %}
<div style="text-align: center">
<h2>Most recent orphaned shares</h2>
{%= TemplateShares(p.Context(), p.LastOrphanedShares, true, nil) %}
<div id="page-payouts" class="container-fluid my-3 p-2 bg-body-tertiary border rounded-2 text-center" tabindex="-1">
<div class="row align-items-start">
<div class="col">
<h2>Most recent payouts</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{% if len(p.LastPayouts) == 0 %}
No payouts have been reported.
{% else %}
{%= TemplatePayoutsSlice(p.Context(), slice_maxSize(p.LastPayouts, 12)) %}
{% endif %}
</div>
<div class="col col-12 mt-3">
<div class="alert alert-secondary small">
Payouts happen when a Monero block is found by P2Pool miners and shares are in the PPLNS window.
<br/>
There is no balance in P2Pool. All rewards are paid out instantly to everyone as part of the mining process.
</div>
</div>
<div class="col my-3 col-12">
<a href="/payouts/{%z= p.Miner.Address.ToBase58() %}" class="btn btn-lg btn-primary">
Payout History
</a>
</div>
</div>
</div>
<hr/>
{% endif %}
<div style="text-align: center">
<h2>Most recent Monero blocks found</h2>
{%= TemplateFoundBlocks(p.Context(), p.LastFound, true) %}
<div class="center"><a href="/blocks?miner={%z= p.Miner.Address.ToBase58() %}">[show more found blocks]</a></div>
</div>
<div id="page-shares">
<div class="container-fluid my-3 p-2 bg-body-tertiary border rounded-2 text-center" tabindex="-1">
<div class="row align-items-start">
<div class="col">
<h2>Most recent shares</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{% if len(p.LastShares) == 0 %}
No shares have been reported to P2Pool network.
{% else %}
{% if p.HashrateSubmit || (p.Positions.Blocks.Total() + p.Positions.Uncles.Total()) > 2 %}
{%= TemplateShares(p.Context(), p.LastShares, true, &p.LastSharesEfforts) %}
{% else %}
{%= TemplateShares(p.Context(), p.LastShares, true, nil) %}
{% endif %}
{% endif %}
</div>
</div>
<div class="col col-12 mt-3">
<div class="alert alert-secondary small">
Shares are listed here when they are accepted by the P2Pool network.
<br/>
Finding shares is a random, memory-less process, and effort may vary depending on your miner luck.
</div>
</div>
</div>
<hr/>
<div style="text-align: center">
<h2>Most recent likely sweeps</h2>
{%= TemplateSweeps(p.Context(), p.LastSweeps, true) %}
<div class="center"><a href="/sweeps?miner={%z= p.Miner.Address.ToBase58() %}">[show more likely sweeps]</a></div>
{% if len(p.LastOrphanedShares) > 0 %}
<div class="container-fluid my-3 p-2 bg-body-tertiary border rounded-2 text-center" tabindex="-1">
<div class="row align-items-start">
<div class="col">
<h2>Most recent orphaned shares</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{%= TemplateShares(p.Context(), p.LastOrphanedShares, true, nil) %}
</div>
</div>
</div>
{% endif %}
</div>
{% if p.Miner.LastShareTimestamp != 0 %}
<hr/>
<div style="text-align: center">
<form action="/calculate-share-time" method="get">
<h3>Effort Calculation</h3>
<p>Local hashrate of each P2Pool miner is not known by the network. A guess is calculated based on daily estimation. If you provide a value here, it will be more accurate for effort calculation.</p>
<p>This data will not be saved.</p>
<label for="hashrate_local">Your Local Hashrate</label><br/>
<input type="numeric" name="hashrate" id="hashrate_local" placeholder="100" size="8" class="mono" value="{% if p.HashrateLocal > 0 %}{%s str(p.HashrateLocal) %}{% endif %}"/>
<select name="magnitude">
<option value="1"{% if p.MagnitudeLocal == 1 %} selected{% endif %}>H/s</option>
<option value="1000"{% if p.MagnitudeLocal == 1000 %} selected{% endif %}>KH/s</option>
<option value="1000000"{% if p.MagnitudeLocal == 1000000 %} selected{% endif %}>MH/s</option>
<option value="1000000000"{% if p.MagnitudeLocal == 1000000000 %} selected{% endif %}>GH/s</option>
</select>
<br/>
<input type="submit" value="Calculate" style="width: 20em; margin: 20px;"/>
</form>
{% if len(p.LastFound) > 0 %}
<div id="page-blocks" class="container-fluid my-3 p-2 bg-body-tertiary border rounded-2 text-center" tabindex="-1">
<div class="row align-items-start">
<div class="col">
<h2>Most recent Monero blocks found</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{%= TemplateFoundBlocks(p.Context(), p.LastFound, true) %}
</div>
<div class="col my-3 col-12">
<a href="/blocks?miner={%z= p.Miner.Address.ToBase58() %}" class="btn btn-lg btn-primary">
More found blocks
</a>
</div>
</div>
</div>
{% endif %}
<div id="page-sweeps" class="container-fluid my-3 p-2 bg-body-tertiary border rounded-2 text-center" tabindex="-1">
<div class="row align-items-start">
<div class="col">
<h2>Most recent likely sweeps</h2>
</div>
</div>
<div class="row">
<div class="col col-12">
{%= TemplateSweeps(p.Context(), p.LastSweeps, true) %}
</div>
<div class="col my-3 col-12">
<a href="/sweeps?miner={%z= p.Miner.Address.ToBase58() %}" class="btn btn-lg btn-primary" tabindex="-1">
More likely sweeps
</a>
</div>
</div>
</div>
{% endfunc %}

View file

@ -12,6 +12,19 @@ type MinersPage struct {
Miners []*MinersPageMinerEntry
WindowWeight types.Difficulty
}
func (p *MinersPage) Name() string {
return "miners"
}
func (p *MinersPage) IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string) {
if p.Weekly {
return true, p.Refresh > 0, p.Refresh, "/miners?week&refresh", "/miners?week"
} else {
return true, p.Refresh > 0, p.Refresh, "/miners?refresh", "/miners"
}
}
%}
{% code
@ -35,24 +48,6 @@ type MinersPageMinerEntry struct {
{% endfunc %}
{% func (p *MinersPage) Content() %}
{% if p.Weekly %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/miners?week">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/blocks?week&refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{% else %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/miners">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/blocks?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{% endif %}
<div style="text-align: center">

View file

@ -1,5 +1,6 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address" %}
{% import "fmt" %}
{% code
type PayoutsPage struct {
@ -9,6 +10,16 @@ type PayoutsPage struct {
Miner *address.Address
Payouts <-chan *index.Payout
}
func (p *PayoutsPage) Name() string {
return "payouts"
}
func (p *PayoutsPage) IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string) {
baseAddress := string(p.Miner.ToBase58())
return true, p.Refresh > 0, p.Refresh, fmt.Sprintf("/payouts/%s?refresh", baseAddress), fmt.Sprintf("/payouts/%s", baseAddress)
}
%}
{% func (p *PayoutsPage) Title() %}
@ -17,15 +28,6 @@ type PayoutsPage struct {
{% func (p *PayoutsPage) Content() %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/payouts/{%z= p.Miner.ToBase58() %}">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/payouts/{%z= p.Miner.ToBase58() %}?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
<div style="text-align: center">
<h2>Historical miner payouts</h2>
<p><strong>Payout Address:</strong> <a href="/miner/{%z= p.Miner.ToBase58() %}"><span class="mono small">{%z= p.Miner.ToBase58() %}</span></a></p>

View file

@ -10,6 +10,10 @@ type ProofPage struct {
Block *index.SideBlock
Raw *sidechain.PoolBlock
}
func (p *ProofPage) Name() string {
return "proof"
}
%}
{% func (p *ProofPage) Title() %}

View file

@ -13,6 +13,10 @@ type SharePage struct {
SweepsCount int
Sweeps [][]*index.MainLikelySweepTransaction
}
func (p *SharePage) Name() string {
return "share"
}
%}
{% func (p *SharePage) Title() %}

View file

@ -9,6 +9,14 @@ type SweepsPage struct {
Sweeps <-chan *index.MainLikelySweepTransaction
Miner *address.Address
}
func (p *SweepsPage) IsRefresh() (ok, isRefresh bool, interval int, uriRefresh, uriStatic string) {
return true, p.Refresh > 0, p.Refresh,"/sweeps?refresh", "/sweeps"
}
func (p *SweepsPage) Name() string {
return "sweeps"
}
%}
{% func (p *SweepsPage) Title() %}
@ -20,15 +28,6 @@ type SweepsPage struct {
{% endfunc %}
{% func (p *SweepsPage) Content() %}
{% if p.Miner == nil %}
<div style="text-align: center; font-weight: bold;">
{% if p.Refresh > 0 %}
<a href="/sweeps">Autorefresh is ON ({%d p.Refresh %} s)</a>
{% else %}
<a href="/sweeps?refresh">Autorefresh is OFF</a>
{% endif %}
</div>
{%endif %}
<div style="text-align: center">
{% if p.Miner == nil %}

View file

@ -4,54 +4,89 @@
{% import "slices" %}
{% func TemplateFoundBlocks(ctx *GlobalRequestContext, foundBlocks []*index.FoundBlock, isMiner bool) %}
<table class="center datatable" style="max-width: calc(8em + 8em + 8em + 8em{% if isMiner %}{% else %} + 12em + 8em{% endif %} + 10em + 7em + 12em + 28em)">
<tr>
<th style="width: 8em;">Monero Height</th>
<th style="width: 8em;">P2Pool Height</th>
<th style="width: 8em;">Age <small>[h:m:s]</small></th>
{% if !isMiner %}
<th style="width: 8em;">Effort</th>
<th style="width: 12em;" title="The P2Pool miner who found this block">Found by</th>
{% endif %}
<th style="width: 8em;">Transactions</th>
<th style="width: 10em;">Total Reward</th>
<th style="width: 7em;" title="Number of miners that got paid on the Coinbase Transaction">Outputs</th>
<th style="width: 12em;">Coinbase Transaction</th>
<th style="width: 28em;" title="You can use this Private Key to verify payouts sent by P2Pool on each block through the Coinbase Transaction">Coinbase Tx Private Key</th>
</tr>
{% for i, b := range foundBlocks %}
<tr>
<th title="{%= hex(ctx, b.MainBlock.Id) %}"><a href="/b/{%s benc(b.MainBlock.Height) %}">{%dul b.MainBlock.Height %}</a></th>
{% if b.UncleOf != types.ZeroHash %}
<th title="{%= hex(ctx, b.MainBlock.SideTemplateId) %} is an uncle of height {%dul b.EffectiveHeight %}, {%= hex(ctx, b.UncleOf) %}">
<a href="/share/{%= hex(ctx, b.MainBlock.SideTemplateId) %}">{%dul b.SideHeight %}*</a>
</th>
{% else %}
<th title="{%= hex(ctx, b.MainBlock.SideTemplateId) %}">
<a href="/share/{%= hex(ctx, b.MainBlock.SideTemplateId) %}">{%dul b.SideHeight %}</a>
</th>
{% endif %}
<td title="{%s utc_date(b.MainBlock.Timestamp) %}">{%s date_diff_short(b.MainBlock.Timestamp) %}</td>
{% if !isMiner %}
{% if len(foundBlocks) > (i+1) %}
<td style="font-weight:bolder; color: {%s effort_color(found_block_effort(b, foundBlocks[i+1])) %};">
{%f.2 found_block_effort(b, foundBlocks[i+1]) %}%
</td>
{% elseif effortIndex := slices.IndexFunc(ctx.Pool.SideChain.Effort.Last, func(e cmdutils.PoolInfoResultSideChainEffortLastEntry) bool { return e.Id == b.MainBlock.Id }); effortIndex != -1 %}
<td style="font-weight:bolder; color: {%s effort_color(ctx.Pool.SideChain.Effort.Last[effortIndex].Effort) %};">
{%f.2 ctx.Pool.SideChain.Effort.Last[effortIndex].Effort %}%
</td>
{% else %}
<td>unknown</td>
{% endif %}
{%= TemplateRowMiner(ctx, b.MinerAddress, b.MinerAlias) %}
{% endif %}
<td>{%dul uint64(b.TransactionCount) %}</td>
<th class="small">{%s monero_to_xmr(b.MainBlock.Reward) %} XMR</th>
<td>{%dul uint64(b.WindowOutputs) %}</td>
<td title="{%= hex(ctx, b.MainBlock.CoinbaseId) %}" class="mono small"><a href="/t/{%= henc(b.MainBlock.CoinbaseId) %}">{%= shorten(ctx, b.MainBlock.CoinbaseId, 10) %}</a></td>
<td class="mono smaller">{%= hex(ctx, b.MainBlock.CoinbasePrivateKey) %}</td>
</tr>
{% endfor %}
</table>
{% code foundBlocks = slice_modulo(foundBlocks, 12) %}
<div class="container">
<div class="row">
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 row-cols-xxl-6 gx-0 gy-2">
{% for i, b := range foundBlocks %}
<div class="col">
<div class="card h-100 rounded-0">
<div class="card-header p-1 container text-center">
<div class="row row-cols-2 g-0">
<div class="col" title="{%= hex(ctx, b.MainBlock.Id) %}">
<a href="/b/{%s benc(b.MainBlock.Height) %}">
<img src="/assets/monero-symbol.svg" alt="Monero" width="24" height="24" class="d-inline-block align-text-top"/>
<span class="d-inline-block align-text-top">{%dul b.MainBlock.Height %}</span>
</a>
</div>
{% if b.UncleOf != types.ZeroHash %}
<div class="col" title="{%= hex(ctx, b.MainBlock.SideTemplateId) %} is an uncle of height {%dul b.EffectiveHeight %}, {%= hex(ctx, b.UncleOf) %}">
<small class="text-body-secondary"><a href="/share/{%= hex(ctx, b.MainBlock.SideTemplateId) %}">#{%dul b.SideHeight %}*</a></small>
</div>
{% else %}
<div class="col" title="{%= hex(ctx, b.MainBlock.SideTemplateId) %}">
<small class="text-body-secondary"><a href="/share/{%= hex(ctx, b.MainBlock.SideTemplateId) %}">#{%dul b.SideHeight %}</a></small>
</div>
{% endif %}
</div>
</div>
<div class="card-body p-1 container text-center">
<div class="row gy-1 gx-0">
{%= TemplateGridMinerWithClass(ctx, b.MinerAddress, b.MinerAlias, "col col-12 found-by", 4, "Found by ") %}
<div class="col col-12">
{%dul uint64(b.WindowOutputs) %} miners paid
</div>
<div class="col col-12">
{%dul uint64(b.TransactionCount) %} txs. included
</div>
<div class="col col-12">
{%s monero_to_xmr(b.MainBlock.Reward) %} XMR
</div>
<div class="col col-12 btn-group btn-group-sm">
<a class="btn btn-outline-secondary" href="/t/{%= henc(b.MainBlock.CoinbaseId) %}" title="{%= hex(ctx, b.MainBlock.CoinbaseId) %}">Coinbase</a>
<a class="btn btn-outline-secondary" href="/share/{%= hex(ctx, b.MainBlock.SideTemplateId) %}">Details</a>
</div>
</div>
</div>
<div class="card-footer p-0 container text-center">
{% if isMiner %}
<div class="row row-cols-1 g-0">
{% else %}
<div class="row row-cols-2 g-0">
{% endif %}
<div class="col" title="{%s utc_date(b.MainBlock.Timestamp) %}">
<small class="text-body-secondary">{%s time_elapsed_short(b.MainBlock.Timestamp) %}</small>
</div>
{% if !isMiner %}
{% code var effortNumber float64 %}
{% if len(foundBlocks) > (i+1) %}
{% code effortNumber = found_block_effort(b, foundBlocks[i+1]) %}
{% elseif effortIndex := slices.IndexFunc(ctx.Pool.SideChain.Effort.Last, func(e cmdutils.PoolInfoResultSideChainEffortLastEntry) bool { return e.Id == b.MainBlock.Id }); effortIndex != -1 %}
{% code effortNumber = ctx.Pool.SideChain.Effort.Last[effortIndex].Effort %}
{% endif %}
{% if effortNumber == 0.0 %}
<div class="col">
<strong>-</strong>
</div>
{% else %}
<div class="col" style="font-weight: bolder; color: {%s effort_color(effortNumber) %}">
<small>{%f.1 effortNumber %}%</small>
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfunc %}

View file

@ -1,52 +1,80 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% func TemplatePayouts(ctx *GlobalRequestContext, payouts <-chan *index.Payout, total *uint64) %}
<table class="center datatable" style="max-width: calc(8em + 8em + 8em + 10em + 10em + 7em + 12em + 10em)">
<tr>
<th style="width: 8em;">Monero Height</th>
<th style="width: 8em;">P2Pool Height</th>
<th style="width: 8em;">Age <small>[h:m:s]</small></th>
<th style="width: 10em;">Reward</th>
<th style="width: 10em;">Global Output Index</th>
<th style="width: 12em;">Coinbase Transaction</th>
<th style="width: 10em;">Payout Proof</th>
</tr>
{% for p := range payouts %}
{% code *total = *total + p.Reward %}
<tr>
<th title="{%= hex(ctx, p.MainId) %}"><a href="/b/{%s benc(p.MainHeight) %}">{%dul p.MainHeight %}</a></th>
<th title="{%= hex(ctx, p.TemplateId) %}"><a href="/share/{%= hex(ctx, p.TemplateId) %}">{%dul p.SideHeight %}</a></th>
<td title="{%s utc_date(p.Timestamp) %}">{%s date_diff_short(p.Timestamp) %}</td>
<td>{%s monero_to_xmr(p.Reward) %} XMR</td>
<td>{%dul p.GlobalOutputIndex %}</td>
<td title="{%= hex(ctx, p.CoinbaseId) %}" class="mono small"><a href="/t/{%= henc(p.CoinbaseId) %}">{%= shorten(ctx, p.CoinbaseId, 10) %}</a></td>
<td><a href="/proof/{%= hex(ctx, p.MainId) %}/{%dul uint64(p.Index) %}" title="Prove you have a matching output for your address on this transaction">[Payout Proof #{%dul uint64(p.Index) %}]</a></td>
</tr>
{% endfor %}
</table>
<div class="container">
<div class="row">
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 row-cols-xxl-6 gx-0 gy-2">
{% for p := range payouts %}
{% code *total = *total + p.Reward %}
{%= TemplatePayoutsCol(ctx, p) %}
{% endfor %}
</div>
</div>
</div>
{% endfunc %}
{% func TemplatePayoutsSlice(ctx *GlobalRequestContext, payouts []*index.Payout) %}
<table class="center datatable" style="max-width: calc(8em + 8em + 8em + 10em + 10em + 7em + 12em + 10em)">
<tr>
<th style="width: 8em;">Monero Height</th>
<th style="width: 8em;">P2Pool Height</th>
<th style="width: 8em;">Age <small>[h:m:s]</small></th>
<th style="width: 10em;">Reward</th>
<th style="width: 10em;">Global Output Index</th>
<th style="width: 12em;">Coinbase Transaction</th>
<th style="width: 10em;">Payout Proof</th>
</tr>
{% for _, p := range payouts %}
<tr>
<th title="{%= hex(ctx, p.MainId) %}"><a href="/b/{%s benc(p.MainHeight) %}">{%dul p.MainHeight %}</a></th>
<th title="{%= hex(ctx, p.TemplateId) %}"><a href="/share/{%= hex(ctx, p.TemplateId) %}">{%dul p.SideHeight %}</a></th>
<td title="{%s utc_date(p.Timestamp) %}">{%s date_diff_short(p.Timestamp) %}</td>
<td>{%s monero_to_xmr(p.Reward) %} XMR</td>
<td>{%dul p.GlobalOutputIndex %}</td>
<td title="{%= hex(ctx, p.CoinbaseId) %}" class="mono small"><a href="/t/{%= henc(p.CoinbaseId) %}">{%= shorten(ctx, p.CoinbaseId, 10) %}</a></td>
<td><a href="/proof/{%= hex(ctx, p.MainId) %}/{%dul uint64(p.Index) %}" title="Prove you have a matching output for your address on this transaction">[Payout Proof #{%dul uint64(p.Index) %}]</a></td>
</tr>
{% endfor %}
</table>
{% code payouts = slice_modulo(payouts, 12) %}
<div class="container">
<div class="row">
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 row-cols-xxl-6 gx-0 gy-2">
{% for _, p := range payouts %}
{%= TemplatePayoutsCol(ctx, p) %}
{% endfor %}
</div>
</div>
</div>
{% endfunc %}
{% func TemplatePayoutsCol(ctx *GlobalRequestContext, p *index.Payout) %}
<div class="col">
<div class="card h-100 rounded-0">
<div class="card-header p-1 container text-center">
<div class="row row-cols-2 g-0">
<div class="col" title="{%= hex(ctx, p.MainId) %}">
<a href="/b/{%s benc(p.MainHeight) %}">
<img src="/assets/monero-symbol.svg" alt="Monero" width="24" height="24" class="d-inline-block align-text-top"/>
<span class="d-inline-block align-text-top">{%dul p.MainHeight %}</span>
</a>
</div>
<div class="col" title="{%= hex(ctx, p.TemplateId) %}">
<small class="text-body-secondary"><a href="/share/{%= hex(ctx, p.TemplateId) %}">#{%dul p.SideHeight %}</a></small>
</div>
</div>
</div>
<div class="card-body p-1 container text-center">
<div class="row gy-1 gx-0">
<div class="col col-12">
Coinbase Id <span class="mono smaller"><a href="/t/{%= henc(p.CoinbaseId) %}" title="{%= hex(ctx, p.CoinbaseId) %}">{%= shorten(ctx, p.CoinbaseId, 6) %}</a></span>
</div>
<div class="col col-12" title="Reward">
{%s monero_to_xmr(p.Reward) %} XMR
</div>
<div class="col col-12 btn-group btn-group-sm">
<a class="btn btn-outline-secondary" href="/t/{%= henc(p.CoinbaseId) %}" title="{%= hex(ctx, p.CoinbaseId) %}">Coinbase</a>
<a class="btn btn-outline-secondary" href="/proof/{%= hex(ctx, p.MainId) %}/{%dul uint64(p.Index) %}"
title="Prove you have a matching output for your address on this transaction">Payout Proof</a>
</div>
</div>
</div>
<div class="card-footer p-0 container text-center">
<div class="row row-cols-2 g-0">
<div class="col" title="{%s utc_date(p.Timestamp) %}">
<small class="text-body-secondary">{%s time_elapsed_short(p.Timestamp) %}</small>
</div>
<div class="col" title="Global Output Index">
<small class="text-body-secondary">#{%dul p.GlobalOutputIndex %}</small>
</div>
</div>
</div>
</div>
</div>
{% endfunc %}

View file

@ -164,18 +164,19 @@
<h2>Coinbase Transaction</h2>
{% if sideBlock != nil && sideBlock.MinedMainAtHeight && sweepsCount > 0 %}
<table class="center" style="max-width: calc(8em + 28em + 12em + 10em + 10em + 8em + 12em)">
<table class="table table-striped table-hover table-borderless table-sm" style="max-width: calc(8em + 28em + 12em + 10em + 10em + 8em + 12em)">
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}7{% else %}6{% endif %}" class="mono smaller">{%s coinbase_extra(poolBlock) %}</td></tr>
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}7{% else %}6{% endif %}">&nbsp;</td></tr>
{% elseif sideBlock != nil && sideBlock.MinedMainAtHeight %}
<table class="center" style="max-width: calc(8em + 28em + 12em + 10em + 10em + 8em)">
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}6{% else %}5{% endif %}" class="mono smaller">{%s coinbase_extra(poolBlock) %}</td></tr>
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}6{% else %}5{% endif %}">&nbsp;</td></tr>
<table class="table table-striped table-hover table-borderless table-sm" style="max-width: calc(8em + 28em + 12em + 10em + 10em + 8em)">
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}6{% else %}5{% endif %}" class="mono smaller">{%s coinbase_extra(poolBlock) %}</td></tr>
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}6{% else %}5{% endif %}">&nbsp;</td></tr>
{% else %}
<table class="center" style="max-width: calc(8em + 28em + 12em + 10em)">
<table class="table table-striped table-hover table-borderless table-sm" style="max-width: calc(8em + 28em + 12em + 10em)">
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}4{% else %}3{% endif %}" class="mono smaller">{%s coinbase_extra(poolBlock) %}</td></tr>
<tr><td colspan="{% if len(coinbaseOutputs) >= len(poolBlock.Main.Coinbase.Outputs) %}4{% else %}3{% endif %}">&nbsp;</td></tr>
{% endif %}
<thead>
<tr>
<th style="width: 8em">Output Index</th>
<th style="width: 28em">Ephemeral Public Key</th>
@ -198,6 +199,8 @@
{% endif %}
{% endif %}
</tr>
</thead>
<tbody>
{% for _, t := range poolBlock.Main.Coinbase.Outputs %}
<tr>
<td>{%dul t.Index %}</td>
@ -225,6 +228,7 @@
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% if len(poolBlock.Main.Transactions) > 0 %}
<h2>Included Transactions</h2>

View file

@ -0,0 +1,175 @@
package views
import (
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"math"
"slices"
"strconv"
"time"
)
type PositionGraphDivider struct {
StartValue float64
EndValue float64
Label string
Color string
}
type PositionGraphPoint struct {
X, Y float64
Link, Color, Label string
Square bool
}
const DefaultBlocksPositionChartDuration = time.Hour * 24
func NewBlocksPositionChart(ctx *GlobalRequestContext, foundBlocks []*index.FoundBlock, x time.Duration) (dividersX, dividersY []PositionGraphDivider, points []PositionGraphPoint) {
current := time.Unix(int64(ctx.Pool.SideChain.LastBlock.Timestamp), 0).UTC()
secondsPerBlock := time.Second * time.Duration(ctx.Consensus.TargetBlockTime)
xSeconds := x.Seconds()
var highestEffort float64
for i, b := range foundBlocks {
var p PositionGraphPoint
var effortNumber float64
if len(foundBlocks) > (i + 1) {
effortNumber = found_block_effort(b, foundBlocks[i+1])
} else if effortIndex := slices.IndexFunc(ctx.Pool.SideChain.Effort.Last, func(e cmdutils.PoolInfoResultSideChainEffortLastEntry) bool { return e.Id == b.MainBlock.Id }); effortIndex != -1 {
effortNumber = ctx.Pool.SideChain.Effort.Last[effortIndex].Effort
}
p.Label = fmt.Sprintf("Block %d %s", b.MainBlock.Height, time_elapsed_short(b.MainBlock.Timestamp))
p.X = (xSeconds - current.Sub(time.Unix(int64(b.MainBlock.Timestamp), 0).UTC()).Seconds()) / xSeconds
if p.X < 0 {
//do not include if it is over chart
continue
}
p.Y = effortNumber
if effortNumber > highestEffort {
highestEffort = effortNumber
}
if effortNumber > 0.0 {
p.Label += fmt.Sprintf(" / %.2f%%", effortNumber)
p.Color = effort_color(effortNumber)
}
p.Link = "/share/" + hex(ctx, b.MainBlock.SideTemplateId)
points = append(points, p)
}
highestEffortInt := int(math.Ceil(highestEffort / 100))
highestEffort = float64(highestEffortInt * 100)
//adjust y
for i := range points {
points[i].Y = (highestEffort - points[i].Y) / highestEffort
}
for i := highestEffortInt; i > 0; i-- {
dividersY = append(dividersY, PositionGraphDivider{
StartValue: float64(i*100) / highestEffort,
EndValue: float64((i-1)*100) / highestEffort,
Label: strconv.FormatUint(uint64(i*100), 10) + "%",
})
}
dividersX = append(dividersX, PositionGraphDivider{
StartValue: (time.Duration(ctx.Pool.SideChain.Window.Blocks) * secondsPerBlock).Seconds() / xSeconds,
EndValue: 0,
Label: "PPLNS",
Color: "rgba(255, 102, 0, 0.5)",
})
return dividersX, dividersY, points
}
func NewSharesPositionChart(ctx *GlobalRequestContext, shares []*index.SideBlock, payouts *[]*index.Payout, efforts *[]float64, x time.Duration) (dividersX, dividersY []PositionGraphDivider, points []PositionGraphPoint) {
current := time.Unix(int64(ctx.Pool.SideChain.LastBlock.Timestamp), 0).UTC()
secondsPerBlock := time.Second * time.Duration(ctx.Consensus.TargetBlockTime)
xSeconds := x.Seconds()
var highestEffort float64
for i, s := range shares {
var p PositionGraphPoint
var effortNumber float64
if efforts != nil {
if effort := (*efforts)[i]; effort >= 0 {
effortNumber = effort
}
}
p.Label = fmt.Sprintf("Share %d %s", s.SideHeight, time_elapsed_short(s.Timestamp))
p.X = (xSeconds - current.Sub(time.Unix(int64(s.Timestamp), 0).UTC()).Seconds()) / xSeconds
if p.X < 0 {
//do not include if it is over chart
continue
}
p.Y = effortNumber
if effortNumber > highestEffort {
highestEffort = effortNumber
}
if effortNumber > 0.0 {
p.Label += fmt.Sprintf(" / %.2f%%", effortNumber)
p.Color = effort_color(effortNumber)
}
p.Link = "/share/" + hex(ctx, s.TemplateId)
if s.IsUncle() {
p.Label += " (uncle)"
p.Square = true
}
points = append(points, p)
}
highestEffortInt := max(3, int(math.Ceil(highestEffort/100)))
highestEffort = float64(highestEffortInt * 100)
//adjust y
for i := range points {
points[i].Y = (highestEffort - points[i].Y) / highestEffort
}
for i := highestEffortInt; i > 0; i-- {
dividersY = append(dividersY, PositionGraphDivider{
StartValue: float64(i*100) / highestEffort,
EndValue: float64((i-1)*100) / highestEffort,
Label: strconv.FormatUint(uint64(i*100), 10) + "%",
})
}
dividersX = append(dividersX, PositionGraphDivider{
StartValue: (time.Duration(ctx.Pool.SideChain.Window.Blocks) * secondsPerBlock).Seconds() / xSeconds,
EndValue: 0,
Label: "PPLNS",
Color: "rgba(255, 102, 0, 0.5)",
})
if payouts != nil {
for _, p := range *payouts {
x := (xSeconds - current.Sub(time.Unix(int64(p.Timestamp), 0).UTC()).Seconds()) / xSeconds
if x < 0 {
//do not include if it is over chart
continue
}
dividersX = append(dividersX, PositionGraphDivider{
StartValue: 1 - x,
EndValue: 1 - x,
})
}
}
return dividersX, dividersY, points
}

View file

@ -0,0 +1,25 @@
{% func TemplatePositionGraph(dividersX, dividersY []PositionGraphDivider, points []PositionGraphPoint, heightPx float64, legendStartX, legendEndX string) %}
<div class="position-graph w-100">
<div class="w-100" style="height: {%f.2 heightPx %}px;">
{% for _, d := range dividersX %}
<div class="position-graph-divider position-graph-divider-x h-100 d-flex justify-content-center" style="right: {%f.4 d.EndValue * 100 %}%; width: {%f.4 (d.StartValue - d.EndValue) * 100 %}%;{% if d.Color != "" %} background-color: {%s d.Color %}{% endif %}">
<div class="position-graph-divider-label">{%s d.Label %}</div>
</div>
{% endfor %}
{% for _, d := range dividersY %}
<div class="position-graph-divider position-graph-divider-y w-100" style="bottom: {%f.4 d.EndValue * 100 %}%; height: {%f.4 (d.StartValue - d.EndValue) * 100 %}%;{% if d.Color != "" %} background-color: {%s d.Color %}{% endif %}">
<div class="position-graph-divider-label">{%s d.Label %}</div>
</div>
{% endfor %}
<div class="position-data">
{% for _, p := range points %}
<a data-tooltip class="p{% if p.Square %} rounded-0{% endif %}" style="left: calc({%f.4 p.X * 100 %}% - 5px); top: calc({%f.4 p.Y * 100 %}% - 5px);{% if p.Color != "" %} background-color: {%s p.Color %}{% endif %}" title="{%s p.Label %}" href="{%s p.Link %}"></a>
{% endfor %}
</div>
</div>
<div class="position-header w-100">
<span class="float-start small">{%s legendStartX %}</span>
<span class="float-end small">{%s legendEndX %}</span>
</div>
</div>
{% endfunc %}

View file

@ -4,6 +4,10 @@
{%= TemplateRowMinerWithTag(ctx, addr, alias, "td") %}
{% endfunc %}
{% func TemplateGridMiner(ctx *GlobalRequestContext, addr *address.Address, alias string) %}
{%= TemplateGridMinerWithClass(ctx, addr, alias, "col", 10, "") %}
{% endfunc %}
{% func TemplateRowMinerWithTag(ctx *GlobalRequestContext, addr *address.Address, alias string, tag string) %}
{% code encodedMinerAddress := addr.ToBase58() %}
{% if alias != "" %}
@ -11,4 +15,13 @@
{% else %}
<{%s tag %} title="{%z= encodedMinerAddress %}" class="mono small"><a href="/miner/{%z= encodedMinerAddress %}">{%= shorten(ctx, encodedMinerAddress, 10) %}</a></{%s tag %}>
{% endif %}
{% endfunc %}
{% func TemplateGridMinerWithClass(ctx *GlobalRequestContext, addr *address.Address, alias string, tag string, size int, text string) %}
{% code encodedMinerAddress := addr.ToBase58() %}
{% if alias != "" %}
<div class="{%s tag %}" title="{%z= encodedMinerAddress %} ({%s alias %})" class="mono small">{%s text %}<a href="/miner/{%z= encodedMinerAddress %}">{%= shorten(ctx, alias, 10) %}</a></div>
{% else %}
<div class="{%s tag %}" title="{%z= encodedMinerAddress %}" class="mono small">{%s text %} <a href="/miner/{%z= encodedMinerAddress %}">{%= shorten(ctx, encodedMinerAddress, size) %}</a></div>
{% endif %}
{% endfunc %}

View file

@ -1,48 +1,96 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% func TemplateShares(ctx *GlobalRequestContext, shares []*index.SideBlock, isMiner bool, efforts *[]float64) %}
<table class="center datatable" style="max-width: calc(8em + 12em + 8em + 8em{% if efforts != nil %} + 8em{% endif %}{% if isMiner %}{% else %} + 12em{% endif %} + 10em + 8em + 12em)">
<tr>
<th style="width: 8em;">P2Pool Height</th>
<th style="width: 12em;">P2Pool Id</th>
<th style="width: 8em;">Monero Height</th>
<th style="width: 8em;">Age <small>[h:m:s]</small></th>
{% if efforts != nil %}
<th style="width: 8em;" title="Effort is estimated based on your daily hash rate. To be more accurate, specify your local hash rate below.">Estimated Effort</th>
{% endif %}
{% if !isMiner %}
<th style="width: 12em;" title="The P2Pool miner who found this share">Found by</th>
{% endif %}
<th style="width: 10em;" title="The consensus implementation name and version of the miner who found this share">Software</th>
<th style="width: 8em;" title="Weight is the difficulty of mined share, with any uncle adjustments. It is variable, and is used in reward calculations">Weight</th>
<th style="width: 12em;">Valuation</th>
</tr>
{% for i, s := range shares %}
<tr{% if s.MinedMainAtHeight %} class="hl-found"{% endif %}>
<th><a href="/share/{%= hex(ctx, s.TemplateId) %}">{%dul s.SideHeight %}</a></th>
<td class="mono smaller"><a href="/share/{%= hex(ctx, s.TemplateId) %}">{%= shorten(ctx, s.TemplateId, 10) %}</a></td>
{% if s.MinedMainAtHeight %}
<th title="{%= hex(ctx, s.MainId) %}"><a href="/b/{%s benc(s.MainHeight) %}">{%dul s.MainHeight %}</a></th>
{% else %}
<td title="{%= hex(ctx, s.MainId) %}">{%dul s.MainHeight %}</td>
{% endif %}
<td title="{%s utc_date(s.Timestamp) %}">{%s date_diff_short(s.Timestamp) %}</td>
{% if efforts != nil %}
{% if effort := (*efforts)[i]; effort >= 0 %}
<td style="font-weight:bolder; color: {%s effort_color(effort) %};">
{%f.2 effort %}%
</td>
{% else %}
<td>unknown</td>
{% endif %}
{% endif %}
{% if !isMiner %}
{%= TemplateRowMiner(ctx, s.MinerAddress, s.MinerAlias) %}
{% endif %}
<td>{%= software_info(s.SoftwareId, s.SoftwareVersion) %}</td>
<td title="{%dul side_block_weight(s, s.SideHeight, ctx.Consensus.ChainWindowSize, ctx.Consensus) %}">{%s si_units(side_block_weight(s, s.SideHeight, ctx.Consensus.ChainWindowSize, ctx.Consensus), 2) %}</td>
<td>{%= side_block_valuation(s, ctx.Consensus) %}</td>
</tr>
{% endfor %}
</table>
{% code shares = slice_modulo(shares, 12) %}
<div class="container">
<div class="row">
<div class="row row-cols-1 row-cols-sm-2 row-cols-lg-3 row-cols-xl-4 row-cols-xxl-6 gx-0 gy-2">
{% for i, s := range shares %}
<div class="col">
<div class="card h-100 rounded-0 {% if s.MinedMainAtHeight %}text-bg-success{% endif %}">
<div class="card-header p-1 container text-center">
<div class="row row-cols-2 g-0">
<div class="col" title="{%= hex(ctx, s.TemplateId) %}">
<strong><a href="/share/{%= hex(ctx, s.TemplateId) %}">#{%dul s.SideHeight %}</a></strong>
</div>
<div class="col" title="{%= hex(ctx, s.MainId) %}">
{% if s.MinedMainAtHeight %}
<small class="text-body-secondary">
<a href="/b/{%s benc(s.MainHeight) %}">
<img src="/assets/monero-symbol.svg" alt="Monero" width="21" height="21" class="d-inline-block align-text-top"/>
<span class="d-inline-block align-text-top">{%dul s.MainHeight %}</span>
</a>
</small>
{% else %}
<small class="text-body-secondary">{%dul s.MainHeight %}</small>
{% endif %}
</div>
</div>
</div>
<div class="card-body p-1 container text-center">
<div class="row gy-1 gx-0">
{% if !isMiner %}
{%= TemplateGridMinerWithClass(ctx, s.MinerAddress, s.MinerAlias, "col col-12 found-by", 4, "Found by ") %}
{% endif %}
<div class="col col-12">
P2Pool Id <span class="mono smaller"><a href="/share/{%= hex(ctx, s.TemplateId) %}" title="{%= hex(ctx, s.TemplateId) %}">{%= shorten(ctx, s.TemplateId, 6) %}</a></span>
</div>
<div class="col col-12">
Mined by <strong>{%= software_info(s.SoftwareId, s.SoftwareVersion) %}</strong>
</div>
<div class="col col-12" title="{%dul side_block_weight(s, s.SideHeight, ctx.Consensus.ChainWindowSize, ctx.Consensus) %}">
Weight {%s si_units(side_block_weight(s, s.SideHeight, ctx.Consensus.ChainWindowSize, ctx.Consensus), 2) %}
</div>
<div class="col col-12" title="Valuation">
<strong>{%= side_block_valuation(s, ctx.Consensus) %}</strong>
</div>
<div class="col col-12 btn-group btn-group-sm">
{% if s.MinedMainAtHeight %}
<a class="btn btn-primary" href="/c/{%s benc(s.SideHeight) %}">Coinbase</a>
<a class="btn btn-primary" href="/share/{%= hex(ctx, s.TemplateId) %}">Details</a>
{% else %}
<a class="btn btn-outline-secondary" href="/share/{%= hex(ctx, s.TemplateId) %}">Details</a>
{% endif %}
</div>
</div>
</div>
<div class="card-footer p-0 container text-center">
{% if efforts == nil %}
<div class="row row-cols-1 g-0">
{% else %}
<div class="row row-cols-2 g-0">
{% endif %}
<div class="col" title="{%s utc_date(s.Timestamp) %}">
<small class="text-body-secondary">{%s time_elapsed_short(s.Timestamp) %}</small>
</div>
{% if efforts != nil %}
{% if effort := (*efforts)[i]; effort >= 0 %}
<div class="col" style="font-weight: bolder; color: {%s effort_color(effort) %}" title="Estimated Effort">
<small>{%f.1 effort %}%</small>
</div>
{% else %}
<div class="col">
<strong>-</strong>
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfunc %}

View file

@ -1,11 +1,13 @@
{% import "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index" %}
{% func TemplateSweeps(ctx *GlobalRequestContext, sweeps <-chan *index.MainLikelySweepTransaction, isMiner bool) %}
<div class="table-responsive">
{% if isMiner %}
<table class="center datatable" style="max-width: calc(12em + 10em + 10em + 8em + 10em + 10em + 10em + 12em)">
<table class="table table-striped table-hover table-borderless table-sm datatable" style="max-width: calc(12em + 10em + 10em + 8em + 10em + 10em + 10em + 12em)">
{% else %}
<table class="center datatable" style="max-width: calc(12em + 10em + 12em + 8em + 10em + 10em + 10em + 10em + 12em)">
<table class="table table-striped table-hover table-borderless table-sm datatable" style="max-width: calc(12em + 10em + 12em + 8em + 10em + 10em + 10em + 10em + 12em)">
{% endif %}
<thead>
<tr>
<th style="width: 12em;">Transaction Id</th>
<th style="width: 8em;">Age <small>[h:m:s]</small></th>
@ -19,6 +21,8 @@
<th style="width: 10em;" title="The number and ratio of decoy inputs not owned by miners on this sweep">Unknown Decoys</th>
<th style="width: 10em;" title="The value known from Coinbase sources">Swept Coinbase Value</th>
</tr>
</thead>
<tbody>
{% for s := range sweeps %}
<tr>
<td class="mono small"><a href="/transaction-lookup?txid={%= hex(ctx, s.Id) %}">{%= shorten(ctx, s.Id, 10) %}</a></td>
@ -41,5 +45,7 @@
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfunc %}

View file

@ -33,6 +33,10 @@ type TransactionLookupPage struct {
NoMiner *cmdutils.PositionChart
}
}
func (p *TransactionLookupPage) Name() string {
return "transaction-lookup"
}
%}
{% func (p *TransactionLookupPage) Title() %}
@ -93,17 +97,17 @@ type TransactionLookupPage struct {
<tr><th colspan="7">&nbsp;</th></tr>
<tr><th colspan="7">
<h3>Miner inputs time scale (from {%s utc_date(p.BottomTimestamp) %} to {%s utc_date(p.TopTimestamp) %})</h3>
<code class="mono">{%s p.Positions.MinerCoinbase.String() %}</code>
<code class="mono">{%s p.Positions.MinerSweep.String() %}</code>
<code class="position-chart">{%s p.Positions.MinerCoinbase.String() %}</code>
<code class="position-chart">{%s p.Positions.MinerSweep.String() %}</code>
</th></tr>
<tr><th colspan="7">
<h3>Other Miner inputs time scale (from {%s utc_date(p.BottomTimestamp) %} to {%s utc_date(p.TopTimestamp) %})</h3>
<code class="mono">{%s p.Positions.OtherMinerCoinbase.String() %}</code>
<code class="mono">{%s p.Positions.OtherMinerSweep.String() %}</code>
<code class="position-chart">{%s p.Positions.OtherMinerCoinbase.String() %}</code>
<code class="position-chart">{%s p.Positions.OtherMinerSweep.String() %}</code>
</th></tr>
<tr><th colspan="7">
<h3>Unknown inputs time scale (from {%s utc_date(p.BottomTimestamp) %} to {%s utc_date(p.TopTimestamp) %})</h3>
<code class="mono">{%s p.Positions.NoMiner.String() %}</code>
<code class="position-chart">{%s p.Positions.NoMiner.String() %}</code>
</th></tr>
<tr><th colspan="7">&nbsp;</th></tr>
@ -177,7 +181,6 @@ type TransactionLookupPage struct {
<tr><th colspan="6">&nbsp;</th></tr>
{% endfor %}
</table>
{% endif %}
</div>
{% endif %}

View file

@ -2,28 +2,36 @@ package main
import (
"bytes"
"compress/gzip"
"embed"
"flag"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/httputils"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/index"
cmdutils "git.gammaspectra.live/P2Pool/p2pool-observer/cmd/utils"
"git.gammaspectra.live/P2Pool/p2pool-observer/cmd/web/views"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero"
address2 "git.gammaspectra.live/P2Pool/p2pool-observer/monero/address"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
types2 "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"github.com/andybalholm/brotli"
"github.com/goccy/go-json"
"github.com/gorilla/mux"
"github.com/klauspost/compress/zstd"
"github.com/valyala/quicktemplate"
"io"
"math"
"mime"
"net/http"
_ "net/http/pprof"
"net/netip"
"net/url"
"os"
"path"
"slices"
"strconv"
"strings"
@ -32,6 +40,112 @@ import (
"time"
)
//go:embed assets
var assets embed.FS
const (
compressionNone = iota
compressionGzip
compressionBrotli
compressionZstd
)
type compressedAsset struct {
Data []byte
ContentEncoding httputils.ContentEncoding
ETag string
}
var compressedAssets = make(map[string][]compressedAsset)
func init() {
fmt.Printf("Compressing assets")
start := time.Now()
dirList, err := assets.ReadDir("assets")
if err != nil {
panic(err)
}
for _, e := range dirList {
filePath := "assets/" + e.Name()
file, err := assets.ReadFile(filePath)
if err != nil {
panic(err)
}
compressedAssets[filePath] = append(compressedAssets[filePath], compressedAsset{
Data: file,
ContentEncoding: "",
ETag: fmt.Sprintf("\"%s\"", crypto.Keccak256Single(file).String()),
})
{
buf := bytes.NewBuffer(nil)
gzipWriter, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
if err != nil {
panic(err)
}
_, err = gzipWriter.Write(file)
if err != nil {
panic(err)
}
err = gzipWriter.Flush()
if err != nil {
panic(err)
}
gzipWriter.Close()
compressedAssets[filePath] = append(compressedAssets[filePath], compressedAsset{
Data: buf.Bytes(),
ContentEncoding: httputils.ContentEncodingGzip,
ETag: fmt.Sprintf("\"%s\"", crypto.Keccak256Single(buf.Bytes()).String()),
})
}
{
buf := bytes.NewBuffer(nil)
brotliWriter := brotli.NewWriterLevel(buf, brotli.BestCompression)
_, err = brotliWriter.Write(file)
if err != nil {
panic(err)
}
err = brotliWriter.Flush()
if err != nil {
panic(err)
}
brotliWriter.Close()
compressedAssets[filePath] = append(compressedAssets[filePath], compressedAsset{
Data: buf.Bytes(),
ContentEncoding: httputils.ContentEncodingBrotli,
ETag: fmt.Sprintf("\"%s\"", crypto.Keccak256Single(buf.Bytes()).String()),
})
}
{
zstdWriter, err := zstd.NewWriter(nil,
zstd.WithEncoderLevel(zstd.SpeedBestCompression),
//zstd.WithEncoderConcurrency(runtime.NumCPU()),
//zstd.WithEncoderCRC(false),
//zstd.WithWindowSize(zstd.MaxWindowSize),
//zstd.WithZeroFrames(true),
)
if err != nil {
panic(err)
}
buf := zstdWriter.EncodeAll(file, nil)
zstdWriter.Close()
compressedAssets[filePath] = append(compressedAssets[filePath], compressedAsset{
Data: buf,
ContentEncoding: httputils.ContentEncodingZstd,
ETag: fmt.Sprintf("\"%s\"", crypto.Keccak256Single(buf).String()),
})
}
}
fmt.Printf(" DONE in %s\n", time.Now().Sub(start).String())
}
func toUint64(t any) uint64 {
if x, ok := t.(uint64); ok {
return x
@ -215,6 +329,7 @@ func main() {
TorServiceAddress: os.Getenv("TOR_SERVICE_ADDRESS"),
Consensus: consensus,
Pool: nil,
IsRefresh: nil,
}
baseContext.Socials.Irc.Link = ircLink
@ -257,6 +372,10 @@ func main() {
}
}()
if refreshProvider, ok := page.(views.RefreshProviderPage); ok {
ctx.IsRefresh = refreshProvider.IsRefresh
}
page.SetContext(&ctx)
bufferedWriter := quicktemplate.AcquireWriter(w)
@ -266,6 +385,57 @@ func main() {
serveMux := mux.NewRouter()
serveMux.HandleFunc("/assets/{asset:.*}", func(writer http.ResponseWriter, request *http.Request) {
mimeType := mime.TypeByExtension(path.Ext(request.URL.Path))
if mimeType == "" {
mimeType = "application/octet-stream"
}
assetPath := "assets/" + mux.Vars(request)["asset"]
pref := httputils.SelectEncodingServerPreference(request.Header.Get("Accept-Encoding"))
encodings := strings.Split(request.Header.Get("Accept-Encoding"), ",")
for i := range encodings {
e := strings.Split(encodings[i], ";")
encodings[i] = strings.TrimSpace(e[0])
}
if entries, ok := compressedAssets[assetPath]; ok {
var e compressedAsset
switch pref {
case httputils.ContentEncodingZstd:
e = entries[compressionZstd]
case httputils.ContentEncodingBrotli:
e = entries[compressionBrotli]
case httputils.ContentEncodingGzip:
e = entries[compressionGzip]
default:
e = entries[compressionNone]
}
writer.Header().Set("ETag", e.ETag)
if request.Header.Get("If-None-Match") == e.ETag {
writer.WriteHeader(http.StatusNotModified)
return
}
if e.ContentEncoding != "" {
writer.Header().Set("Content-Encoding", string(e.ContentEncoding))
}
writer.Header().Set("Content-Type", mimeType)
writer.Header().Set("Content-Length", strconv.FormatUint(uint64(len(e.Data)), 10))
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write(e.Data)
return
} else {
writer.WriteHeader(http.StatusNotFound)
return
}
})
serveMux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
params := request.URL.Query()
refresh := 0
@ -291,10 +461,6 @@ func main() {
blocksFound.Add(int(tip-int64(b.SideHeight)), 1)
}
if len(blocks) > 20 {
blocks = blocks[:20]
}
renderPage(request, writer, &views.IndexPage{
Refresh: refresh,
Positions: struct {
@ -327,7 +493,7 @@ func main() {
params := request.URL.Query()
if params.Has("hashrate") {
hashRate = toFloat64(params.Get("hashrate"))
hashRate = toFloat64(strings.ReplaceAll(params.Get("hashrate"), ",", "."))
}
if params.Has("magnitude") {
magnitude = toFloat64(params.Get("magnitude"))
@ -335,7 +501,7 @@ func main() {
currentHashRate := magnitude * hashRate
var effortSteps = []float64{25, 50, 75, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000}
var effortSteps = []float64{10, 25, 50, 75, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000}
const shareSteps = 15
calculatePage := &views.CalculateShareTimePage{
@ -899,7 +1065,7 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
lastShares = getSideBlocksFromAPI(fmt.Sprintf("side_blocks?limit=50&miner=%d", miner.Id))
lastShares = getSideBlocksFromAPI(fmt.Sprintf("side_blocks?limit=60&miner=%d", miner.Id))
if len(lastShares) > 0 {
raw = getTypeFromAPI[sidechain.PoolBlock](fmt.Sprintf("block_by_id/%s/light", lastShares[0].MainId))
@ -911,19 +1077,14 @@ func main() {
wg.Add(1)
go func() {
defer wg.Done()
lastOrphanedShares = getSideBlocksFromAPI(fmt.Sprintf("side_blocks?limit=10&miner=%d&inclusion=%d", miner.Id, index.InclusionOrphan))
lastOrphanedShares = getSideBlocksFromAPI(fmt.Sprintf("side_blocks?limit=12&miner=%d&inclusion=%d", miner.Id, index.InclusionOrphan))
}()
wg.Add(1)
go func() {
defer wg.Done()
lastFound = getSliceFromAPI[*index.FoundBlock](fmt.Sprintf("found_blocks?limit=10&miner=%d", miner.Id))
}()
sweeps = getStreamFromAPI[*index.MainLikelySweepTransaction](fmt.Sprintf("sweeps/%d?limit=5", miner.Id))
wg.Add(1)
go func() {
defer wg.Done()
shares = getSideBlocksFromAPI(fmt.Sprintf("side_blocks_in_window/%d?from=%d&window=%d&noMiner&noMainStatus&noUncles", miner.Id, tipHeight, wsize))
lastFound = getSliceFromAPI[*index.FoundBlock](fmt.Sprintf("found_blocks?limit=12&miner=%d", miner.Id))
}()
sweeps = getStreamFromAPI[*index.MainLikelySweepTransaction](fmt.Sprintf("sweeps/%d?limit=12", miner.Id))
wg.Add(1)
go func() {
defer wg.Done()
@ -982,10 +1143,6 @@ func main() {
}
}
if len(payouts) > 10 {
payouts = payouts[:10]
}
minerPage := &views.MinerPage{
Refresh: refresh,
Positions: struct {
@ -1011,6 +1168,7 @@ func main() {
WindowWeight: windowDiff.Lo,
Miner: miner,
LastPoolBlock: raw,
DayShares: shares,
LastShares: lastShares,
LastOrphanedShares: lastOrphanedShares,
LastFound: lastFound,
@ -1018,11 +1176,13 @@ func main() {
LastSweeps: sweeps,
}
if windowDiff.Cmp64(0) > 0 {
if longDiff.Cmp64(0) > 0 {
longWindowWeight := poolInfo.SideChain.Window.Weight.Mul64(4).Mul64(poolInfo.SideChain.Consensus.ChainWindowSize).Div64(uint64(poolInfo.SideChain.Window.Blocks))
averageRewardPerBlock := longDiff.Mul64(poolInfo.MainChain.BaseReward).Div(longWindowWeight).Lo
minerPage.ExpectedRewardPerDay = longWindowWeight.Mul64(averageRewardPerBlock).Div(poolInfo.MainChain.NextDifficulty).Lo
}
if windowDiff.Cmp64(0) > 0 {
expectedRewardNextBlock := windowDiff.Mul64(poolInfo.MainChain.BaseReward).Div(poolInfo.SideChain.Window.Weight).Lo
minerPage.ExpectedRewardPerWindow = poolInfo.SideChain.Window.Weight.Mul64(expectedRewardNextBlock).Div(poolInfo.MainChain.NextDifficulty).Lo
}
@ -1059,6 +1219,22 @@ func main() {
minerPage.HashrateLocal = hashRate
minerPage.MagnitudeLocal = magnitude
dayEfforts := make([]float64, len(shares))
for i := len(shares) - 1; i >= 0; i-- {
s := shares[i]
if i == (len(shares) - 1) {
dayEfforts[i] = -1
continue
}
previous := shares[i+1]
timeDelta := uint64(max(int64(s.Timestamp)-int64(previous.Timestamp), 0))
expectedCumDiff := types.DifficultyFrom64(dailyHashRate).Mul64(timeDelta)
dayEfforts[i] = float64(expectedCumDiff.Mul64(100).Lo) / float64(s.Difficulty)
}
efforts := make([]float64, len(lastShares))
for i := len(lastShares) - 1; i >= 0; i-- {
s := lastShares[i]
@ -1074,6 +1250,7 @@ func main() {
efforts[i] = float64(expectedCumDiff.Mul64(100).Lo) / float64(s.Difficulty)
}
minerPage.DaySharesEfforts = dayEfforts
minerPage.LastSharesEfforts = efforts
renderPage(request, writer, minerPage, poolInfo)

View file

@ -31,7 +31,7 @@ server {
gzip on;
gzip_types text/html text/css text/xml text/plain text/javascript text/xml application/xml application/x-javascript application/javascript application/json image/svg+xml application/font-woff application/font-woff2 application/font-ttf application/octet-stream application/wasm;
gzip_min_length 1000;
gzip_proxied any;
gzip_proxied no_etag;
gzip_comp_level 5;
gzip_disable "MSIE [1-6]\.";
server_tokens off;

View file

@ -1 +1 @@
add_header Content-Security-Policy "default-src 'none'; img-src blob: data: ; object-src 'none'; style-src 'unsafe-inline'; style-src-elem 'unsafe-inline'; style-src-attr 'unsafe-inline'; prefetch-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; navigate-to * 'self'" always;
add_header Content-Security-Policy "default-src 'none'; img-src blob: data: 'self'; object-src 'none'; style-src 'unsafe-inline' 'self'; style-src-elem 'unsafe-inline' 'self'; style-src-attr 'unsafe-inline'; script-src 'none'; script-src-attr 'none'; script-src-elem 'none'; prefetch-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'; navigate-to * 'self'" always;

View file

@ -11,8 +11,4 @@ add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header X-Robots-Tag "nofollow, notranslate" always;
add_header Tk "N" always;
add_header Permissions-Policy "interest-cohort=(), autoplay=(), oversized-images=(), payment=(), fullscreen=(), camera=(), microphone=(), geolocation=(), usb=(), midi=()" always;
add_header Vary 'Origin, Accept-Encoding' always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Expect-CT "max-age=31536000, enforce" always;
add_header Expect-Staple 'max-age=31536000; includeSubDomains; preload' always;
add_header Vary 'Origin, Accept-Encoding' always;