diff options
| author | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-07 11:42:30 -0300 |
|---|---|---|
| committer | Bitflash <admin@bitflash.network> | 2026-08-07 15:06:36 -0300 |
| commit | a0355492c3bae37ad57ef3e05302a3e5b1a80e3e (patch) | |
| tree | 72469f5b2096592d925e2e69afba8d49b36d54c7 /scripts | |
| parent | 8bf5b8cac5d91165b11d4ce65333b68edcc723b0 (diff) | |
tools: build a static pool directory
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/build-pool-index.py | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/scripts/build-pool-index.py b/scripts/build-pool-index.py new file mode 100644 index 0000000..40eb8f4 --- /dev/null +++ b/scripts/build-pool-index.py | |||
| @@ -0,0 +1,153 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | # | ||
| 3 | # Build a static public pool directory from one or more Bitflash pool_status.json | ||
| 4 | # files. Inputs may be local paths or http(s) URLs. | ||
| 5 | |||
| 6 | import argparse | ||
| 7 | import html | ||
| 8 | import json | ||
| 9 | import os | ||
| 10 | import time | ||
| 11 | import urllib.request | ||
| 12 | |||
| 13 | |||
| 14 | def load_json(source): | ||
| 15 | if source.startswith("http://") or source.startswith("https://"): | ||
| 16 | with urllib.request.urlopen(source, timeout=10) as r: | ||
| 17 | return json.loads(r.read().decode("utf-8")) | ||
| 18 | with open(source, "r", encoding="utf-8-sig") as f: | ||
| 19 | return json.load(f) | ||
| 20 | |||
| 21 | |||
| 22 | def normalize_pool(source, obj, now): | ||
| 23 | operator = obj.get("operator", {}) | ||
| 24 | pool = obj.get("pool", {}) | ||
| 25 | node = obj.get("node", {}) | ||
| 26 | updated = int(obj.get("updatedAt") or 0) | ||
| 27 | age = max(0, now - updated) if updated else None | ||
| 28 | online = bool(node.get("poolRunning")) and age is not None and age <= 120 | ||
| 29 | return { | ||
| 30 | "source": source, | ||
| 31 | "name": operator.get("name") or "Bitflash Pool", | ||
| 32 | "btfAddress": operator.get("btfAddress") or "", | ||
| 33 | "feePercent": float(operator.get("feePercent") or 0.0), | ||
| 34 | "dashboardUrl": operator.get("dashboardUrl") or "", | ||
| 35 | "height": int(node.get("height") or 0), | ||
| 36 | "online": online, | ||
| 37 | "ageSeconds": age, | ||
| 38 | "authorizedMiners": int(pool.get("authorizedMiners") or 0), | ||
| 39 | "blocksFoundSession": int(pool.get("blocksFoundSession") or 0), | ||
| 40 | "roundShares": int(pool.get("roundShares") or 0), | ||
| 41 | "hashRate": float(pool.get("hashRate") or 0.0), | ||
| 42 | } | ||
| 43 | |||
| 44 | |||
| 45 | def rank_key(pool): | ||
| 46 | return ( | ||
| 47 | 1 if pool["online"] else 0, | ||
| 48 | pool["hashRate"], | ||
| 49 | pool["authorizedMiners"], | ||
| 50 | -pool["feePercent"], | ||
| 51 | pool["name"].lower(), | ||
| 52 | ) | ||
| 53 | |||
| 54 | |||
| 55 | def render_html(pools, generated_at): | ||
| 56 | rows = [] | ||
| 57 | for p in pools: | ||
| 58 | dash = p["dashboardUrl"] | ||
| 59 | dash_html = ( | ||
| 60 | '<a href="{0}">dashboard</a>'.format(html.escape(dash, quote=True)) | ||
| 61 | if dash else "-" | ||
| 62 | ) | ||
| 63 | rows.append( | ||
| 64 | "<tr>" | ||
| 65 | "<td>{status}</td>" | ||
| 66 | "<td><strong>{name}</strong><br><code>{addr}</code></td>" | ||
| 67 | "<td>{fee:.2f}%</td>" | ||
| 68 | "<td>{miners}</td>" | ||
| 69 | "<td>{hashrate:.2f}</td>" | ||
| 70 | "<td>{blocks}</td>" | ||
| 71 | "<td>{height}</td>" | ||
| 72 | "<td>{age}</td>" | ||
| 73 | "<td>{dash}</td>" | ||
| 74 | "</tr>".format( | ||
| 75 | status="online" if p["online"] else "stale", | ||
| 76 | name=html.escape(p["name"]), | ||
| 77 | addr=html.escape(p["btfAddress"]), | ||
| 78 | fee=p["feePercent"], | ||
| 79 | miners=p["authorizedMiners"], | ||
| 80 | hashrate=p["hashRate"], | ||
| 81 | blocks=p["blocksFoundSession"], | ||
| 82 | height=p["height"], | ||
| 83 | age="{}s".format(p["ageSeconds"]) if p["ageSeconds"] is not None else "-", | ||
| 84 | dash=dash_html, | ||
| 85 | ) | ||
| 86 | ) | ||
| 87 | |||
| 88 | return """<!doctype html> | ||
| 89 | <html lang="en"> | ||
| 90 | <head> | ||
| 91 | <meta charset="utf-8"> | ||
| 92 | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 93 | <title>Bitflash Pools</title> | ||
| 94 | <style> | ||
| 95 | body {{ font: 15px/1.45 system-ui, sans-serif; margin: 32px; color: #111; }} | ||
| 96 | table {{ border-collapse: collapse; width: 100%; }} | ||
| 97 | th, td {{ border-bottom: 1px solid #ddd; padding: 10px; text-align: left; vertical-align: top; }} | ||
| 98 | th {{ font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }} | ||
| 99 | code {{ font-size: 12px; word-break: break-all; }} | ||
| 100 | .note {{ color: #555; max-width: 780px; }} | ||
| 101 | </style> | ||
| 102 | </head> | ||
| 103 | <body> | ||
| 104 | <h1>Bitflash Pools</h1> | ||
| 105 | <p class="note">This directory is informational. Bitflash nodes discover pools through Nostr and .btf; this page only helps humans compare public operators.</p> | ||
| 106 | <table> | ||
| 107 | <thead> | ||
| 108 | <tr><th>Status</th><th>Pool</th><th>Fee</th><th>Miners</th><th>H/s</th><th>Blocks</th><th>Height</th><th>Age</th><th>Link</th></tr> | ||
| 109 | </thead> | ||
| 110 | <tbody> | ||
| 111 | {rows} | ||
| 112 | </tbody> | ||
| 113 | </table> | ||
| 114 | <p class="note">Generated at {generated_at}. JSON: <a href="pools.json">pools.json</a>.</p> | ||
| 115 | </body> | ||
| 116 | </html> | ||
| 117 | """.format(rows="\n ".join(rows), generated_at=generated_at) | ||
| 118 | |||
| 119 | |||
| 120 | def main(): | ||
| 121 | ap = argparse.ArgumentParser() | ||
| 122 | ap.add_argument("--out-dir", default="pool-site") | ||
| 123 | ap.add_argument("sources", nargs="+", help="pool_status.json paths or URLs") | ||
| 124 | args = ap.parse_args() | ||
| 125 | |||
| 126 | now = int(time.time()) | ||
| 127 | pools = [] | ||
| 128 | errors = [] | ||
| 129 | for source in args.sources: | ||
| 130 | try: | ||
| 131 | pools.append(normalize_pool(source, load_json(source), now)) | ||
| 132 | except Exception as e: | ||
| 133 | errors.append({"source": source, "error": str(e)}) | ||
| 134 | |||
| 135 | pools.sort(key=rank_key, reverse=True) | ||
| 136 | os.makedirs(args.out_dir, exist_ok=True) | ||
| 137 | |||
| 138 | generated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)) | ||
| 139 | with open(os.path.join(args.out_dir, "pools.json"), "w", encoding="utf-8") as f: | ||
| 140 | json.dump({ | ||
| 141 | "schema": "bitflash-pool-directory-1", | ||
| 142 | "generatedAt": generated_at, | ||
| 143 | "pools": pools, | ||
| 144 | "errors": errors, | ||
| 145 | }, f, indent=2, sort_keys=True) | ||
| 146 | f.write("\n") | ||
| 147 | |||
| 148 | with open(os.path.join(args.out_dir, "index.html"), "w", encoding="utf-8") as f: | ||
| 149 | f.write(render_html(pools, generated_at)) | ||
| 150 | |||
| 151 | |||
| 152 | if __name__ == "__main__": | ||
| 153 | main() | ||