Files
bal-welist-website/index.html
2026-07-21 22:47:53 +00:00

748 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BAL Server List</title>
<link rel="icon" href="Logo_nero.ico" type="image/x-icon">
<script src="qrcode.min.js"></script>
<script src="purify.min.js"></script>
<style>
:root{
--maxw:1120px;
--radius:10px;
--bg:#eef1f4;
--panel:#ffffff;
--ink:#1a2027;
--muted:#5b6672;
--line:#dfe4ea;
--line-strong:#cbd2da;
--accent:#2d6cdf;
--accent-soft:#e6eefc;
--ok:#1f9d57;
--ok-soft:#e3f4ea;
--off:#9aa4b0;
--off-soft:#eceff3;
--rank:#94a0ad;
--display:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;
--body:"Inter","Segoe UI",system-ui,-apple-system,sans-serif;
--mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace;
--shadow:0 1px 2px rgba(20,30,50,.05);
}
*{box-sizing:border-box}
html,body{margin:0}
body{
background:var(--bg);
color:var(--ink);
font-family:var(--body);
line-height:1.5;
-webkit-font-smoothing:antialiased;
/* WHY: iOS Safari inflates font sizes when a phone is rotated to landscape,
which breaks the header and the server cards. Locking the adjustment to
100% keeps the layout identical in both orientations. No effect on desktop. */
-webkit-text-size-adjust:100%;
}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 20px}
header.hero{padding:48px 0 28px;border-bottom:1px solid var(--line)}
.hero .wrap{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;flex-wrap:wrap}
.brand{display:flex;align-items:center;gap:16px}
.brand .mark{
width:46px;height:46px;border-radius:12px;flex:none;
display:grid;place-items:center;
background:#fff;color:var(--ink);
border:1px solid var(--line-strong);box-shadow:var(--shadow);overflow:hidden;
}
.brand .mark .brand-logo{width:34px;height:34px;display:block}
.brand h1{
font-family:var(--display);font-weight:600;
font-size:30px;line-height:1.05;margin:0;letter-spacing:-.01em;
}
.brand .sub{color:var(--muted);font-size:14px;margin-top:2px}
.hero-stats{display:flex;gap:28px}
.stat .n{font-family:var(--display);font-size:26px;font-weight:600;line-height:1}
.stat .l{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:6px}
.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:22px 0 14px}
.seg{display:inline-flex;border:1px solid var(--line-strong);border-radius:8px;overflow:hidden}
.seg button{
font:inherit;font-size:13px;padding:8px 13px;border:0;cursor:pointer;
background:var(--panel);color:var(--muted);border-right:1px solid var(--line);
}
.seg button:last-child{border-right:0}
.seg button[aria-pressed="true"]{background:var(--accent-soft);color:var(--ink);font-weight:600}
.field{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted)}
.toggle{position:relative;width:38px;height:22px;flex:none}
.toggle input{opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
.toggle .track{position:absolute;inset:0;border-radius:999px;background:var(--line-strong);transition:.18s}
.toggle .knob{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:.18s;box-shadow:0 1px 2px rgba(0,0,0,.25)}
.toggle input:checked ~ .track{background:var(--ok)}
.toggle input:checked ~ .knob{transform:translateX(16px)}
.search{flex:1 1 200px;min-width:160px}
.search input{
width:100%;font:inherit;font-size:14px;padding:9px 12px;
border:1px solid var(--line-strong);border-radius:8px;
background:var(--panel);color:var(--ink);
}
.search input::placeholder{color:var(--muted)}
.btn-ghost{
font:inherit;font-size:13px;cursor:pointer;text-decoration:none;
padding:9px 14px;border-radius:8px;border:1px solid var(--line-strong);
background:var(--panel);color:var(--ink);display:inline-flex;gap:7px;align-items:center;
}
.btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
.table-card{
background:var(--panel);border:1px solid var(--line);
border-radius:var(--radius);box-shadow:var(--shadow);
max-height:350px;overflow:auto;
}
table{width:100%;border-collapse:collapse;font-size:14px}
thead th{
text-align:left;font-weight:600;color:var(--muted);
font-size:11px;letter-spacing:.08em;text-transform:uppercase;
padding:14px 14px;border-bottom:1px solid var(--line-strong);
white-space:nowrap;
position:sticky;top:0;background:var(--panel);z-index:2;
}
thead th.num,tbody td.num{text-align:right}
tbody td{padding:14px 14px;border-bottom:1px solid var(--line);vertical-align:middle}
tbody tr:last-child td{border-bottom:0}
tbody tr:hover{background:color-mix(in srgb,var(--accent-soft) 50%,transparent)}
.rank{font-family:var(--display);font-weight:600;color:var(--rank);font-size:15px;width:34px}
.rank.top{color:var(--accent)}
.url a{color:var(--ink);text-decoration:none;font-weight:600}
.url a:hover{color:var(--accent);text-decoration:underline}
.info{color:var(--muted);font-size:13px;max-width:200px}
.mono{font-family:var(--mono);font-size:13px}
.num{font-family:var(--mono);font-variant-numeric:tabular-nums}
.score{font-family:var(--display);font-weight:600;font-size:15px}
.add-grid{display:grid;grid-template-columns:1.3fr 1fr;gap:24px;margin:40px 0;align-items:stretch}
.add-grid>.panel{display:flex;flex-direction:column}
.panel{
background:var(--panel);border:1px solid var(--line);
border-radius:var(--radius);padding:24px;box-shadow:var(--shadow);
}
.panel h2{font-family:var(--display);font-size:19px;margin:0 0 6px}
.panel p.lead{color:var(--muted);font-size:14px;margin:0 0 18px}
.form-row{display:flex;gap:10px;flex-wrap:wrap}
.form-row input{
flex:1 1 220px;font:inherit;font-size:14px;padding:11px 13px;
border:1px solid var(--line-strong);border-radius:8px;
background:var(--bg);color:var(--ink);font-family:var(--mono);
}
.btn{
font:inherit;font-size:14px;font-weight:600;cursor:pointer;
padding:11px 20px;border-radius:8px;border:1px solid var(--accent);
background:var(--accent);color:#fff;
}
.btn:hover{filter:brightness(1.06)}
.btn:disabled{opacity:.45;cursor:not-allowed;filter:none}
.note{font-size:13px;color:var(--muted);margin-top:16px;line-height:1.55}
.note strong{color:var(--ink)}
.freshness-note{
font-size:12px;line-height:1.4;color:var(--muted);
background:var(--accent-soft);border:1px solid var(--line);border-radius:8px;
padding:7px 12px;margin:0 0 12px;
}
.freshness-note strong{color:var(--ink)}
.btn-share{
display:block;margin:0 auto 8px;font:inherit;font-size:13px;cursor:pointer;
padding:8px 16px;border-radius:8px;border:1px solid var(--line-strong);
background:var(--panel);color:var(--accent);
}
.invoice{display:none}
.invoice.show{display:block}
.invoice .row{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border-bottom:1px solid var(--line);font-size:13px}
.invoice .row:last-child{border-bottom:0}
.invoice .row span:first-child{color:var(--muted)}
.invoice .row span:last-child{font-family:var(--mono);text-align:right;word-break:break-all}
.invoice .row a{color:var(--accent);text-decoration:none}
.invoice .row a:hover{text-decoration:underline}
#qrcode{margin:18px auto 4px;width:160px}
#qrcode canvas,#qrcode img{width:160px!important;height:160px!important;max-width:100%}
#qr-overlay{
display:none;position:fixed;inset:0;z-index:999;
background:rgba(0,0,0,.7);cursor:pointer;
align-items:center;justify-content:center;
}
#qr-overlay.show{display:flex}
#qr-big{padding:24px;background:#fff;border-radius:12px}
#qr-big canvas,#qr-big img{width:280px!important;height:280px!important;max-width:90vw}
.ok-line{color:var(--ok);font-weight:600;font-size:14px;margin-bottom:14px}
.err-line{color:#d32f2f;font-weight:600;font-size:14px;margin-bottom:14px}
.terms-box{display:none;margin:16px 0;padding:20px;background:var(--panel);border:1px solid var(--line);border-radius:var(--radius)}
.terms-box.show{display:block}
.terms-box .lead{margin:0 0 12px;color:var(--ink);font-weight:600}
.terms-box a.mono{font-size:14px;word-break:break-all}
.terms-box label{font-size:14px;cursor:pointer}
.terms-box .btn{margin-top:14px}
.intro-card{
background:var(--panel);border:1px solid var(--line);
border-left:4px solid var(--accent);
border-radius:var(--radius);box-shadow:var(--shadow);
padding:28px 32px;margin:28px 0 8px;
}
.intro-card h2{font-family:var(--display);font-size:24px;margin:0 0 10px;letter-spacing:-.01em}
.intro-card p{margin:0;color:var(--muted);font-size:15px;line-height:1.65;max-width:820px}
.fee-disclaimer{margin:12px 0 0;color:var(--muted);font-size:13px;line-height:1.5;font-style:italic}
.fee-disclaimer a{color:var(--accent)}
.info-section{margin:48px 0}
.info-section h2{font-family:var(--display);font-size:22px;margin:0 0 8px}
.info-section .sub{color:var(--muted);font-size:15px;line-height:1.6;margin:0 0 28px;max-width:760px}
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.info-block{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:22px 24px;box-shadow:var(--shadow)}
.info-block .ic{font-family:var(--mono);font-size:12px;color:var(--accent);letter-spacing:.1em;display:block;margin-bottom:10px}
.info-block h3{font-size:15px;margin:0 0 8px;letter-spacing:.01em}
.info-block p{font-size:14px;color:var(--muted);margin:0;line-height:1.6}
footer{border-top:1px solid var(--line);margin-top:48px;padding:28px 0;color:var(--muted);font-size:13px}
footer .wrap{display:flex;justify-content:space-between;flex-wrap:wrap;gap:12px}
footer a{color:var(--accent);text-decoration:none}
.mobile-list{display:none}
@media (max-width:860px){
.table-card{display:none}
/* WHY: the previous "max-height:350px;overflow:auto" turned the server list
into a small scrollable window nested inside the page. On a touch screen
that inner scroll competes with the normal page scroll and makes the list
hard to read. On mobile the list now flows naturally with the page. */
.mobile-list{display:grid;gap:12px}
.add-grid{grid-template-columns:1fr}
.info-grid{grid-template-columns:1fr}
.hero-stats{gap:20px}
.brand h1{font-size:24px}
.srv{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow)}
.srv .top{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:12px}
.srv .rk{font-family:var(--display);font-weight:600;color:var(--rank);font-size:18px}
/* WHY: server URLs have no spaces, so a long URL cannot break by itself and
would overflow the card. "anywhere" lets it wrap at any character.
"min-width:0" is required because a flex child defaults to min-width:auto,
which prevents it from shrinking below its content width. */
.srv .top>div{min-width:0}
.srv .top a{display:inline-block;max-width:100%;overflow-wrap:anywhere}
/* WHY: with two fixed columns the fifth field (Height) was left alone on a
half-empty row. auto-fit fills the available width with as many columns as
fit, so the five fields always line up tidily. */
.srv .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(84px,1fr));gap:10px;font-size:13px}
.srv .grid div span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.srv .grid div b{font-family:var(--mono);font-weight:600;overflow-wrap:anywhere}
}
/* ---------------------------------------------------------------------------
Phone layout (<=600px).
Scope: header, filter bar and server list only. The "Add your Will Executor"
panel, the invoice and the QR code are intentionally left untouched, because
payments are expected to be made from a desktop browser.
--------------------------------------------------------------------------- */
@media (max-width:600px){
.controls{gap:10px}
/* WHY: the four chain buttons form a single non-wrapping row about 340px wide.
Inside a 360-390px phone screen (minus page padding) it overflowed the
viewport and produced horizontal scrolling. A 2x2 grid across the full
width removes the overflow. min-height:44px matches the recommended
minimum touch target size. */
.seg{display:grid;grid-template-columns:1fr 1fr;width:100%;border-radius:8px}
.seg button{font-size:14px;min-height:44px;padding:8px 10px;border-bottom:1px solid var(--line)}
.seg button:nth-child(2n){border-right:0} /* right column: no inner divider */
.seg button:nth-child(n+3){border-bottom:0} /* last row: no bottom divider */
/* Give the toggle and the JSON link a full-size touch area. */
.field{min-height:44px}
.btn-ghost{min-height:44px}
/* WHY: the search field takes the full width so it is comfortable to type in,
and the font is raised to 16px. Below 16px, iOS Safari automatically zooms
the whole page when the field receives focus, and the user then has to
pinch back out. 16px disables that behaviour. */
.search{flex:1 1 100%}
.search input{font-size:16px}
}
/* ---------------------------------------------------------------------------
Narrow phone layout (<=480px): tighter spacing to recover horizontal space.
--------------------------------------------------------------------------- */
@media (max-width:480px){
/* Recovers 12px of usable width on every row of the page. */
.wrap{padding:0 14px}
header.hero{padding:28px 0 20px}
.hero .wrap{gap:16px}
.brand{gap:12px}
.brand .mark{width:40px;height:40px;border-radius:10px}
.brand .mark .brand-logo{width:28px;height:28px}
.brand h1{font-size:20px}
.brand .sub{font-size:13px}
.stat .n{font-size:22px}
.controls{padding:16px 0 12px}
.srv{padding:14px}
.intro-card{padding:20px 18px;margin:22px 0 8px}
.intro-card h2{font-size:20px}
.intro-card p{font-size:14px}
.info-section{margin:32px 0}
.info-block{padding:18px 16px}
/* WHY: the four footer items were spread with space-between and wrapped into
an uneven, hard-to-read block. A single column reads cleanly on a phone. */
footer{margin-top:32px;padding:22px 0}
footer .wrap{flex-direction:column;gap:8px}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important}}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
</style>
</head>
<body>
<header class="hero">
<div class="wrap">
<div class="brand">
<div class="mark"><svg class="brand-logo" viewBox="0 0 1036 1036" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Bitcoin After Life logo"><g transform="translate(0.000000,1036.000000) scale(0.100000,-0.100000)" fill="currentColor" stroke="none"><path d="M6130 10203 c-153 -318 -283 -665 -344 -916 -61 -250 -71 -335 -71 -602 1 -223 3 -249 28 -370 52 -244 140 -460 260 -633 72 -106 225 -257 320 -316 179 -111 335 -154 777 -211 232 -30 367 -61 442 -102 76 -41 195 -164 248 -257 170 -302 311 -774 362 -1211 19 -167 16 -591 -6 -745 -68 -484 -201 -871 -436 -1270 -541 -917 -1492 -1525 -2549 -1630 -132 -13 -453 -13 -596 0 -268 24 -530 77 -757 151 -1063 350 -1871 1192 -2187 2280 -63 217 -111 495 -127 742 -7 104 -7 107 -30 107 l-24 0 0 -167 c0 -286 27 -508 95 -773 148 -587 427 -1083 854 -1523 231 -237 423 -391 703 -563 685 -420 1543 -589 2343 -462 1346 213 2458 1211 2814 2524 89 329 116 543 115 919 0 349 -23 544 -100 847 -68 273 10 632 176 799 91 92 165 104 491 77 320 -26 499 36 699 242 171 175 285 422 320 690 15 116 12 401 -6 535 -13 100 -32 200 -91 478 -3 15 -8 27 -12 27 -3 0 -21 -30 -40 -66 -20 -39 -63 -96 -105 -139 -127 -130 -287 -216 -616 -330 -297 -104 -420 -161 -551 -256 -159 -117 -268 -285 -325 -504 -36 -135 -37 -312 -4 -500 12 -71 24 -132 26 -134 2 -2 41 33 87 78 115 115 199 177 369 270 267 146 442 260 590 384 38 32 72 57 74 54 7 -6 -107 -108 -193 -173 -88 -67 -259 -175 -402 -256 -58 -34 -146 -88 -196 -122 -107 -73 -276 -237 -332 -323 -50 -76 -100 -193 -116 -274 l-12 -60 -87 173 c-194 389 -411 680 -731 980 -375 353 -563 553 -677 723 -54 79 -170 297 -170 318 0 7 21 -22 46 -63 179 -288 481 -616 885 -962 265 -227 363 -338 496 -560 32 -54 60 -97 63 -98 9 0 100 229 141 352 99 302 114 589 44 869 -105 419 -325 659 -955 1039 -333 201 -500 322 -642 465 -148 150 -254 318 -297 475 -11 40 -22 75 -25 77 -2 2 -27 -44 -56 -104z"/><path d="M4436 9470 c-1069 -83 -2060 -527 -2825 -1265 -567 -548 -975 -1198 -1195 -1909 -248 -796 -290 -1575 -130 -2365 213 -1049 795 -2006 1636 -2688 727 -590 1604 -940 2558 -1023 189 -16 648 -14 825 5 601 63 1114 212 1645 481 834 421 1518 1083 1967 1904 296 540 464 1079 540 1725 23 203 23 683 -1 908 -39 374 -122 744 -237 1062 l-31 84 -151 -53 c-141 -49 -152 -54 -151 -77 1 -13 20 -94 42 -179 303 -1165 101 -2402 -553 -3395 -410 -622 -943 -1098 -1600 -1431 -469 -237 -913 -369 -1450 -430 -202 -24 -622 -24 -830 -1 -739 82 -1409 331 -1990 741 -293 206 -640 542 -869 840 -474 616 -756 1323 -843 2111 -20 185 -22 593 -4 785 112 1186 708 2250 1662 2965 619 465 1347 745 2139 826 127 13 546 18 745 9 l110 -5 21 160 c12 88 21 161 20 162 -4 4 -149 25 -275 39 -161 18 -620 26 -775 14z"/><path d="M3870 6492 c-52 -26 -73 -71 -78 -169 l-5 -83 -98 0 -99 0 0 -225 0 -225 100 0 100 0 0 -355 0 -355 -100 0 -100 0 0 -230 0 -230 100 0 100 0 0 -355 0 -355 -100 0 -100 0 0 -225 0 -225 100 0 100 0 0 -80 c0 -97 17 -142 62 -170 29 -17 50 -20 169 -20 125 0 139 2 170 23 43 29 58 71 58 170 l1 77 64 0 64 0 4 -111 c3 -107 4 -112 31 -135 27 -23 34 -24 182 -24 221 0 235 10 243 172 l5 98 261 0 c227 0 272 3 345 20 264 64 482 254 576 505 119 313 36 650 -216 878 l-72 64 33 47 c43 63 94 176 116 256 25 91 25 299 0 395 -59 230 -220 428 -431 530 -141 69 -182 77 -412 82 l-203 5 0 77 c0 104 -14 147 -53 171 -28 18 -51 20 -185 20 l-154 0 -34 -34 -34 -34 0 -101 0 -101 -65 0 -65 0 0 85 c0 162 -28 185 -222 185 -97 0 -130 -4 -158 -18z m1307 -732 c74 -45 103 -184 85 -416 -12 -151 -31 -208 -85 -248 -28 -21 -39 -21 -478 -24 l-449 -3 0 356 0 355 448 0 c426 0 448 -1 479 -20z m150 -1172 c46 -30 93 -97 125 -177 18 -45 22 -77 22 -161 1 -98 -1 -110 -30 -171 -35 -73 -92 -142 -135 -165 -23 -12 -115 -14 -544 -14 l-515 0 0 360 0 360 518 -3 518 -2 41 -27z"/></g></svg></div>
<div>
<h1>Will Executor — Server List</h1>
<div class="sub"><a href="https://bitcoin-after.life" target="_blank" rel="noopener">Bitcoin After Life</a> · Will-executor node leaderboard</div>
</div>
</div>
<div class="hero-stats">
<div class="stat"><div class="n" id="s-online">0</div><div class="l">Online</div></div>
<div class="stat"><div class="n" id="s-total">0</div><div class="l">Servers</div></div>
</div>
</div>
</header>
<main class="wrap">
<div class="controls">
<div class="seg" id="chainSeg" role="group" aria-label="Chain">
<button data-c="bitcoin" aria-pressed="true">Bitcoin</button>
<button data-c="testnet" aria-pressed="false">Testnet</button>
<button data-c="testnet4" aria-pressed="false">Testnet4</button>
<button data-c="regtest" aria-pressed="false">Regtest</button>
</div>
<label class="field">
<span class="toggle">
<input type="checkbox" id="onion" checked>
<span class="track"></span><span class="knob"></span>
</span>
Include Tor
</label>
<div class="search">
<input type="search" id="filter" placeholder="Filter by URL or info…" aria-label="Filter">
</div>
<a class="btn-ghost" id="download" href="#" download="willexecutors.json">↓ JSON</a>
</div>
<p class="freshness-note">
<strong>Fee/description/version</strong> refresh about once a day (timing varies). <strong>Deposits</strong> are credited instantly after 3 confirmations (~30 min).
</p>
<div class="table-card">
<table id="tbl">
<thead>
<tr>
<th class="num">#</th>
<th>URL</th>
<th>Info*</th>
<th class="num">Fee*</th>
<th>Version*</th>
<th class="num">Height</th>
<th class="num">Wins</th>
<th class="num">Score</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="mobile-list" id="mlist"></div>
<p class="fee-disclaimer">* Info, Fee and Version data may not reflect actual values or may be outdated. Refer to the <a href="https://bitcoin-after.life" target="_blank" rel="noopener">Bitcoin After Life plugin</a> for up-to-date information.</p>
<section class="intro-card">
<h2>Will Executor Server List</h2>
<p>This page displays all Will Executor servers that manage the inheritance processing for the Bitcoin After Life plugin on Electrum. Will Executors operate in a competitive environment, racing to broadcast inheritance transactions to network nodes and earn the associated transaction fees.</p>
</section>
<div class="add-grid">
<div class="panel">
<h2>Add your Will Executor</h2>
<p class="lead">Enter your server URL. It will appear in the list once your deposit is confirmed (typically about 30 minutes).</p>
<div class="form-row">
<input type="text" id="url" placeholder="https://we.bitcoin-after.life">
</div>
<div style="margin-top:8px">
<input type="text" id="contact" placeholder="Email, npub or Telegram (optional)" style="width:100%;font:inherit;font-size:14px;padding:9px 12px;border:1px solid var(--line-strong);border-radius:8px;background:var(--panel);color:var(--ink)">
</div>
<button class="btn" id="submit" style="margin-top:8px">Add server</button>
<div class="ok-line" id="iv-status"></div>
<div class="terms-box" id="terms-box">
<p class="lead">You are about to add this server:</p>
<p><a id="terms-url" href="#" target="_blank" rel="noopener" class="mono"></a></p>
<p style="font-size:13px;margin-top:4px"><strong>Make sure the link above works correctly before paying. No refunds after payment. URL changes require proof of ownership — see the <a href="/terms.html" target="_blank">Terms</a>.</strong></p>
<p style="font-size:14px">
<label>
<input type="checkbox" id="terms-check">
I accept the <a href="/terms.html" target="_blank">terms and conditions</a>
</label>
</p>
<button class="btn" id="terms-accept" disabled>Accept & Show Invoice</button>
</div>
<div class="invoice" id="invoice">
<div class="row"><span>Annual fee</span><span id="iv-min"></span></div>
<div class="row"><span>Chain</span><span id="iv-chain"></span></div>
<div class="row"><span>Address</span><span id="iv-addr"></span></div>
<div class="row"><span>Balance</span><span id="iv-balance"></span></div>
<div class="row"><span>Donations</span><span id="iv-donations"></span></div>
<div class="row"><span>Contact</span><span id="iv-contact"></span></div>
<div class="row"><span>Server</span><span id="iv-url"></span></div>
<div class="row"><span>Will Executor</span><span><a id="iv-weinfo" href="#" target="_blank"></a></span></div>
<div class="row" id="iv-nextdep-row" style="display:none"><span>Next renewal</span><span id="iv-nextdep"></span></div>
<div id="qrcode"></div>
<button class="btn-share" id="share-btn">Share invoice</button>
<p class="note">
<strong>Please make sure your server is online before you attempt to add it.</strong>
</p>
</div>
</div>
<div class="panel">
<h2>How ranking works</h2>
<p class="lead">Ordered by excess donated, then total paid, then base fee (lower first), then WeTime points.</p>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--line);font-size:13px"><span style="color:var(--muted)">Yearly fee</span><b class="mono"><span id="thr-min">50,000</span> sats</b></div>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--line);font-size:13px"><span style="color:var(--muted)">Listing period</span><b>365 days</b></div>
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--line);font-size:13px"><span style="color:var(--muted)">Renewal window</span><b>from 6 months before expiry</b></div>
<div style="display:flex;justify-content:space-between;padding:8px 0;font-size:13px"><span style="color:var(--muted)">Plugin integration</span><b>automatic</b></div>
</div>
</div>
<section class="info-section">
<div class="info-grid">
<div class="info-block">
<span class="ic">01</span>
<h3>Benefits of Listing</h3>
<p>Listed Will Executors are automatically integrated into the plugin when inheritance arrangements are made, providing direct access to potential clients.</p>
</div>
<div class="info-block">
<span class="ic">02</span>
<h3>Ranking System</h3>
<p>This leaderboard is ordered by excess donated (over the yearly fee), then total amount paid, then lower base fee, then WeTime points.</p>
</div>
<div class="info-block">
<span class="ic">03</span>
<h3>Listing &amp; Renewal</h3>
<p>A flat yearly fee buys 365 days of presence in the list, starting when your payment reaches 3 confirmations. You can renew for one additional year at any time from 6 months before expiry; listings that are not renewed are removed at expiry. See the <a href="/terms.html" target="_blank">Terms</a> for details.</p>
</div>
<div class="info-block">
<span class="ic">04</span>
<h3>Revenue Expectations</h3>
<p>Operating as a Will Executor is not a get-rich-quick opportunity. This service requires patience and long-term commitment, but can provide substantial returns over time for dedicated operators.</p>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap">
<span>Bitcoin After Life — Will Executor List</span>
<a href="mailto:welist@bitcoin-after.life">welist@bitcoin-after.life</a>
<a href="/terms.html">Terms</a>
<a class="mono" id="commit-link" href="https://bitcoin-after.life/gitea/bitcoinafterlife/bal-welist-website/commit/COMMIT_HASH_PLACEHOLDER">COMMIT_HASH_PLACEHOLDER</a>
</div>
</footer>
<script>
let allServers = [];
let withTor = true, q = "";
let minAmount = 50000;
const fmt = n => (n != null ? Number(n).toLocaleString('en-US') : '—');
const trunc = (s, m=40) => s && s.length > m ? s.slice(0, m) + '…' : s;
function getChain() {
const btn = document.querySelector('#chainSeg [aria-pressed="true"]');
return btn ? btn.dataset.c : 'bitcoin';
}
function getFiltered() {
return allServers
.filter(d => (d.url + " " + (d.info||"")).toLowerCase().includes(q.toLowerCase()));
}
function render() {
const list = getFiltered();
const tb = document.getElementById('tbody');
const ml = document.getElementById('mlist');
tb.innerHTML = ""; ml.innerHTML = "";
if (!list.length) {
tb.innerHTML = '<tr><td colspan="8" style="padding:28px;text-align:center;color:var(--muted)">No servers found.</td></tr>';
}
list.forEach((d, i) => {
const rank = i + 1;
const weLink = `/weinfo/${d.chain||getChain()}/${d.id}`;
tb.insertAdjacentHTML('beforeend', `
<tr>
<td class="num"><span class="rank ${rank<=3?'top':''}">${rank}</span></td>
<td class="url"><a href="${weLink}" title="${DOMPurify.sanitize(d.url)}">${DOMPurify.sanitize(trunc(d.url))}</a></td>
<td class="info">${DOMPurify.sanitize(d.info||'—')}</td>
<td class="num">${fmt(d.base_fee)}</td>
<td class="mono">${DOMPurify.sanitize(d.version||'—')}</td>
<td class="num">${fmt(d.last_block)}</td>
<td class="num">${d.count_win != null ? d.count_win : '—'}</td>
<td class="num"><span class="score">${fmt(d.points)}</span></td>
</tr>`);
ml.insertAdjacentHTML('beforeend', `
<div class="srv">
<div class="top">
<div><div class="rk">#${rank}</div><a href="${weLink}" title="${DOMPurify.sanitize(d.url)}" style="color:var(--ink);font-weight:600;text-decoration:none;font-size:14px">${DOMPurify.sanitize(trunc(d.url))}</a></div>
</div>
<div class="grid">
<div><span>Score</span><b>${fmt(d.points)}</b></div>
<div><span>Wins</span><b>${d.count_win != null ? d.count_win : '—'}</b></div>
<div><span>Fee</span><b>${fmt(d.base_fee)} sat</b></div>
<div><span>Version</span><b>${DOMPurify.sanitize(d.version||'—')}</b></div>
<div><span>Height</span><b>${fmt(d.last_block)}</b></div>
</div>
</div>`);
});
const online = allServers.filter(d => d.status === "OK").length;
document.getElementById('s-online').textContent = online;
document.getElementById('s-total').textContent = allServers.length;
}
async function fetchData() {
const chain = getChain();
const params = new URLSearchParams({ page: 0, limit: 100 });
if (!withTor) params.append("onion", "no");
try {
// NOTE: /data/{chain} returns a HashMap, whose JSON key order is not
// guaranteed (it can change on every backend restart). /datalist/{chain}
// returns an ordered array (Vec) that preserves the server-side sort
// (donation DESC, balance DESC, base_fee ASC, wetime_points DESC) — see terms.html §3.
const response = await fetch(`/datalist/${chain}?${params}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const list = await response.json();
allServers = list;
render();
} catch (err) {
console.error('Fetch error:', err);
allServers = [];
render();
}
}
async function fetchMinAmount() {
const chain = getChain();
try {
const r = await fetch('/min_deposit/' + chain);
const val = parseInt(await r.text(), 10);
if (isNaN(val)) throw new Error('invalid');
minAmount = val;
document.getElementById('thr-min').textContent = fmt(val);
} catch (err) {
minAmount = 50000;
document.getElementById('thr-min').textContent = '50,000';
}
}
document.getElementById('onion').addEventListener('change', e => {
withTor = e.target.checked;
fetchData();
});
document.getElementById('filter').addEventListener('input', e => {
q = e.target.value;
render();
});
document.getElementById('chainSeg').addEventListener('click', e => {
const b = e.target.closest('button');
if (!b) return;
[...e.currentTarget.children].forEach(x => x.setAttribute('aria-pressed', 'false'));
b.setAttribute('aria-pressed', 'true');
fetchData();
fetchMinAmount();
});
document.getElementById('submit').addEventListener('click', async () => {
const urlInput = document.getElementById('url');
let url = urlInput.value.trim();
if (!url) return;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
urlInput.value = url;
}
const inv = document.getElementById('invoice');
inv.classList.remove('show');
document.getElementById('iv-status').className = '';
document.getElementById('iv-status').textContent = '';
document.getElementById('iv-min').textContent = '—';
document.getElementById('iv-chain').textContent = '—';
document.getElementById('iv-addr').textContent = '—';
document.getElementById('iv-balance').textContent = '—';
document.getElementById('iv-donations').textContent = '—';
document.getElementById('iv-url').textContent = '—';
document.getElementById('iv-contact').textContent = '—';
document.getElementById('iv-weinfo').href = '#';
document.getElementById('iv-weinfo').textContent = '—';
document.getElementById('iv-nextdep-row').style.display = 'none';
document.getElementById('iv-nextdep').textContent = '—';
document.getElementById('iv-min').parentElement.style.display = '';
document.getElementById('iv-addr').parentElement.style.display = '';
document.getElementById('qrcode').style.display = '';
document.getElementById('share-btn').style.display = '';
document.getElementById('qrcode').innerHTML = '';
document.getElementById('qrcode').style.cursor = '';
document.getElementById('qr-overlay').classList.remove('show');
document.getElementById('share-btn').textContent = 'Share invoice';
document.getElementById('terms-box').classList.remove('show');
document.getElementById('terms-box').style.display = '';
document.getElementById('terms-check').checked = false;
document.getElementById('terms-accept').disabled = true;
const chain = getChain();
const infoUrl = url + '/' + chain + '/info';
document.getElementById('terms-url').href = infoUrl;
document.getElementById('terms-url').textContent = url;
document.getElementById('terms-box').classList.add('show');
document.getElementById('terms-box').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
});
document.getElementById('terms-check').addEventListener('change', function () {
document.getElementById('terms-accept').disabled = !this.checked;
});
document.getElementById('terms-accept').addEventListener('click', async function () {
document.getElementById('terms-box').classList.remove('show');
const urlInput = document.getElementById('url');
let url = urlInput.value.trim();
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const chain = getChain();
try {
const response = await fetch('/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, chain, contact: document.getElementById('contact').value.trim() || undefined })
});
const result = await response.json();
if (!response.ok) {
document.getElementById('iv-status').className = 'err-line';
document.getElementById('iv-status').textContent = 'Error: ' + (result.message || result.error || JSON.stringify(result));
return;
}
document.getElementById('iv-min').textContent = fmt(result.min_amount != null ? result.min_amount : minAmount) + ' sats';
document.getElementById('iv-chain').textContent = result.chain || chain;
document.getElementById('iv-addr').textContent = result.address || '—';
const balStr = result.balance != null ? fmt(result.balance) : null;
const uncStr = result.unconfirmed_balance != null ? fmt(result.unconfirmed_balance) : null;
document.getElementById('iv-balance').textContent = balStr ? (uncStr ? balStr + ' (' + uncStr + ')' : balStr) : '—';
document.getElementById('iv-donations').textContent = result.donation != null ? fmt(result.donation) : '—';
document.getElementById('iv-contact').textContent = document.getElementById('contact').value.trim() || '—';
const weLink = result.id ? `/weinfo/${result.chain || chain}/${result.id}` : null;
const weinfoEl = document.getElementById('iv-weinfo');
if (weLink) {
weinfoEl.href = weLink;
weinfoEl.textContent = 'View page';
} else {
weinfoEl.href = '#';
weinfoEl.textContent = '—';
}
const serverUrl = result.url || url;
document.getElementById('iv-url').textContent = serverUrl;
if (result.payment_needed === false) {
document.getElementById('iv-min').parentElement.style.display = 'none';
document.getElementById('iv-addr').parentElement.style.display = 'none';
document.getElementById('qrcode').style.display = 'none';
document.getElementById('share-btn').style.display = 'none';
document.getElementById('iv-nextdep').textContent = result.next_deposit_date || '—';
document.getElementById('iv-nextdep-row').style.display = 'flex';
document.getElementById('iv-status').className = 'ok-line';
document.getElementById('iv-status').textContent = 'Server is already listed. Next renewal: ' + (result.next_deposit_date || '—');
const inv = document.getElementById('invoice');
inv.classList.add('show');
inv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
return;
}
const amountSats = result.min_amount != null ? result.min_amount : minAmount;
const amountBtc = (amountSats / 1e8).toFixed(8).replace(/\.?0+$/, '');
const invoiceuri = `bitcoin:${result.address}?amount=${amountBtc}&label=BAL&message=Order+For+url+%26+${encodeURIComponent(url)}`;
const qrEl = document.getElementById('qrcode');
qrEl.innerHTML = '';
new QRCode(qrEl, { text: invoiceuri, width: 160, height: 160 });
qrEl.style.cursor = 'pointer';
qrEl.onclick = function () {
const overlay = document.getElementById('qr-overlay');
const big = document.getElementById('qr-big');
big.innerHTML = '';
new QRCode(big, { text: invoiceuri, width: 280, height: 280 });
overlay.classList.add('show');
};
document.getElementById('qr-overlay').onclick = function () {
this.classList.remove('show');
};
const shareBtn = document.getElementById('share-btn');
shareBtn.textContent = 'Share invoice';
shareBtn.onclick = function () {
if (navigator.share) {
navigator.share({ title: 'BAL Invoice', text: invoiceuri });
} else {
navigator.clipboard.writeText(invoiceuri).then(() => {
this.textContent = 'Copied!';
setTimeout(() => this.textContent = 'Share invoice', 2000);
});
}
};
document.getElementById('iv-status').className = 'ok-line';
document.getElementById('iv-status').textContent = '✓ Server verified: ' + serverUrl + '. Pay the invoice — it will be listed after 3 confirmations.';
const inv = document.getElementById('invoice');
inv.classList.add('show');
inv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (err) {
document.getElementById('iv-status').className = 'err-line';
document.getElementById('iv-status').textContent = 'Network error. Please try again.';
}
});
document.getElementById('url').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('submit').click();
});
(async function init() {
await Promise.all([fetchData(), fetchMinAmount()]);
const blob = new Blob([JSON.stringify(allServers, null, 2)], { type: 'application/json' });
document.getElementById('download').href = URL.createObjectURL(blob);
})();
</script>
<div id="qr-overlay"><div id="qr-big"></div></div>
</body>
</html>