upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/scripts/build-pool-index.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/build-pool-index.py')
-rw-r--r--scripts/build-pool-index.py153
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
6import argparse
7import html
8import json
9import os
10import time
11import urllib.request
12
13
14def 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
22def 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
45def 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
55def 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
120def 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
152if __name__ == "__main__":
153 main()