feat: Add new OG images
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m17s
Build Docker / BuildImage (push) Successful in 2m39s

This commit is contained in:
2026-02-25 22:25:38 +11:00
parent 9a6748b156
commit f0d233b2f6
7 changed files with 325 additions and 65 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ __pycache__/
.vs/ .vs/
.venv/ .venv/
fireexplorer.db fireexplorer.db
.vscode

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "python-webserver-template" name = "fireexplorer"
version = "0.1.0" version = "0.1.0"
description = "Add your description here" description = "A hot new Handshake Blockchain Explorer"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [

View File

@@ -147,7 +147,7 @@ cryptography==46.0.3 \
--hash=sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849 \ --hash=sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849 \
--hash=sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963 \ --hash=sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963 \
--hash=sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018 --hash=sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018
# via python-webserver-template # via fireexplorer
distlib==0.4.0 \ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \ --hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d --hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
@@ -163,11 +163,11 @@ filelock==3.20.0 \
flask==3.1.2 \ flask==3.1.2 \
--hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \ --hash=sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87 \
--hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c --hash=sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c
# via python-webserver-template # via fireexplorer
gunicorn==23.0.0 \ gunicorn==23.0.0 \
--hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \ --hash=sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d \
--hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec --hash=sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec
# via python-webserver-template # via fireexplorer
h11==0.16.0 \ h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
@@ -289,7 +289,7 @@ pysocks==1.7.1 \
python-dotenv==1.2.1 \ python-dotenv==1.2.1 \
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \ --hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61 --hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
# via python-webserver-template # via fireexplorer
pyyaml==6.0.3 \ pyyaml==6.0.3 \
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
@@ -328,7 +328,7 @@ requests==2.32.3 \
requests-doh==1.0.0 \ requests-doh==1.0.0 \
--hash=sha256:6ce8bc96245030a198ef20d2100b4dcb3b120a05a58df703f8be121a79f8f2fb \ --hash=sha256:6ce8bc96245030a198ef20d2100b4dcb3b120a05a58df703f8be121a79f8f2fb \
--hash=sha256:eea6583b792b7d3dfde74fd28eedc2b95d6ea896368119eede31f0d6ff2c838c --hash=sha256:eea6583b792b7d3dfde74fd28eedc2b95d6ea896368119eede31f0d6ff2c838c
# via python-webserver-template # via fireexplorer
ruff==0.14.5 \ ruff==0.14.5 \
--hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \ --hash=sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68 \
--hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \ --hash=sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78 \

282
server.py
View File

@@ -12,6 +12,8 @@ import os
import requests import requests
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from urllib.parse import urlencode
from xml.sax.saxutils import escape
import dotenv import dotenv
from tools import hip2, wallet_txt from tools import hip2, wallet_txt
@@ -20,6 +22,8 @@ dotenv.load_dotenv()
app = Flask(__name__) app = Flask(__name__)
DATABASE = os.getenv("DATABASE_PATH", "fireexplorer.db") DATABASE = os.getenv("DATABASE_PATH", "fireexplorer.db")
HSD_API_BASE = os.getenv("HSD_API_BASE", "https://hsd.hns.au/api/v1")
PUBLIC_BASE_URL = os.getenv("PUBLIC_BASE_URL", "").rstrip("/")
def get_db(): def get_db():
@@ -60,6 +64,175 @@ def find(name, path):
return os.path.join(root, name) return os.path.join(root, name)
def _truncate(text: str, max_length: int) -> str:
if len(text) <= max_length:
return text
return text[: max_length - 1] + ""
def _safe_hns_value(value) -> str:
try:
return f"{float(value) / 1e6:,.2f} HNS"
except Exception:
return "Unknown"
def _format_int(value, fallback: str = "?") -> str:
try:
return f"{int(value):,}"
except Exception:
return fallback
def _ellipsize_middle(text: str, max_length: int = 48) -> str:
if len(text) <= max_length:
return text
if max_length < 7:
return _truncate(text, max_length)
left = (max_length - 1) // 2
right = max_length - left - 1
return f"{text[:left]}{text[-right:]}"
def _fetch_explorer_json(endpoint: str):
try:
req = requests.get(f"{HSD_API_BASE}/{endpoint}", timeout=4)
if req.status_code == 200:
return req.json(), None
return None, f"HTTP {req.status_code}"
except Exception as e:
return None, str(e)
def _get_public_base_url() -> str:
if PUBLIC_BASE_URL:
return PUBLIC_BASE_URL
return request.url_root.rstrip("/")
def _build_og_context(
search_type: str | None = None, search_value: str | None = None
) -> dict:
default_title = "Fire Explorer"
default_description = "A hot new Handshake Blockchain Explorer"
if not search_type or not search_value:
return {
"title": default_title,
"description": default_description,
"image_query": {"title": default_title, "subtitle": default_description},
}
type_labels = {
"block": "Block",
"header": "Header",
"tx": "Transaction",
"address": "Address",
"name": "Name",
}
label = type_labels.get(search_type, "Search")
fallback_title = f"{label} {search_value} | Fire Explorer"
fallback_description = f"View Handshake {label.lower()} details for {search_value}"
og = {
"title": _truncate(fallback_title, 100),
"description": _truncate(fallback_description, 200),
"image_query": {
"type": label,
"value": search_value,
"title": fallback_title,
"subtitle": fallback_description,
},
}
if search_type == "block":
block, err = _fetch_explorer_json(f"block/{search_value}")
if not err and block:
tx_count = len(block.get("txs", []))
height = block.get("height", "?")
timestamp = block.get("time")
time_str = (
datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M UTC")
if timestamp
else "Unknown time"
)
height_fmt = _format_int(height)
tx_count_fmt = _format_int(tx_count, "0")
subtitle = f"Height {height_fmt}{tx_count_fmt} txs • {time_str}"
og["title"] = _truncate(f"Block {height_fmt} | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "header":
header, err = _fetch_explorer_json(f"header/{search_value}")
if not err and header:
height = header.get("height", "?")
bits = header.get("bits", "?")
subtitle = f"Height {height} • Bits {bits}"
og["title"] = _truncate(f"Header {height} | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "tx":
tx, err = _fetch_explorer_json(f"tx/{search_value}")
if not err and tx:
inputs = len(tx.get("inputs", []))
outputs = len(tx.get("outputs", []))
fee = _safe_hns_value(tx.get("fee"))
subtitle = f"{inputs} inputs • {outputs} outputs • Fee {fee}"
og["title"] = _truncate("Transaction | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "address":
txs, err = _fetch_explorer_json(f"tx/address/{search_value}")
if not err and isinstance(txs, list):
subtitle = f"{len(txs)} related transactions found"
og["title"] = _truncate("Address | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
elif search_type == "name":
name, err = _fetch_explorer_json(f"name/{search_value}")
if not err and name:
info = name.get("info", name)
state = info.get("state", "Unknown")
owner = info.get("owner", {}).get("hash", "Unknown")
subtitle = f"State: {state} • Owner: {_truncate(owner, 26)}"
og["title"] = _truncate(f"Name {search_value} | Fire Explorer", 100)
og["description"] = _truncate(subtitle, 200)
og["image_query"]["subtitle"] = subtitle
og["image_query"]["title"] = og["title"]
return og
def _render_index(search_type: str | None = None, search_value: str | None = None):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p")
og_context = _build_og_context(search_type, search_value)
image_query = og_context["image_query"]
public_base_url = _get_public_base_url()
og_image_url = public_base_url + "/og-image?" + urlencode(image_query)
og_url = public_base_url + request.full_path
if og_url.endswith("?"):
og_url = og_url[:-1]
twitter_domain = request.host
if PUBLIC_BASE_URL and "//" in PUBLIC_BASE_URL:
twitter_domain = PUBLIC_BASE_URL.split("//", 1)[1]
return render_template(
"index.html",
datetime=current_datetime,
meta_title=og_context["title"],
meta_description=og_context["description"],
og_url=og_url,
og_image=og_image_url,
twitter_domain=twitter_domain,
)
# Assets routes # Assets routes
@app.route("/assets/<path:path>") @app.route("/assets/<path:path>")
def send_assets(path): def send_assets(path):
@@ -108,44 +281,125 @@ def wellknown(path):
# region Main routes # region Main routes
@app.route("/") @app.route("/")
def index(): def index():
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index()
return render_template("index.html", datetime=current_datetime)
@app.route("/tx/<path:tx_hash>") @app.route("/tx/<path:tx_hash>")
def tx_route(tx_hash): def tx_route(tx_hash):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("tx", tx_hash)
return render_template("index.html", datetime=current_datetime)
@app.route("/block/<path:block_id>") @app.route("/block/<path:block_id>")
def block_route(block_id): def block_route(block_id):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("block", block_id)
return render_template("index.html", datetime=current_datetime)
@app.route("/header/<path:block_id>") @app.route("/header/<path:block_id>")
def header_route(block_id): def header_route(block_id):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("header", block_id)
return render_template("index.html", datetime=current_datetime)
@app.route("/address/<path:address>") @app.route("/address/<path:address>")
def address_route(address): def address_route(address):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("address", address)
return render_template("index.html", datetime=current_datetime)
@app.route("/name/<path:name>") @app.route("/name/<path:name>")
def name_route(name): def name_route(name):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("name", name)
return render_template("index.html", datetime=current_datetime)
@app.route("/coin/<path:coin_hash>/<int:index>") @app.route("/coin/<path:coin_hash>/<int:index>")
def coin_route(coin_hash, index): def coin_route(coin_hash, index):
current_datetime = datetime.now().strftime("%d %b %Y %I:%M %p") return _render_index("coin", f"{coin_hash}:{index}")
return render_template("index.html", datetime=current_datetime)
@app.route("/og-image")
def og_image():
title = _truncate(request.args.get("title", "Fire Explorer"), 90)
subtitle = _truncate(
request.args.get("subtitle", "A hot new Handshake Blockchain Explorer"),
140,
)
search_type = _truncate(request.args.get("type", "Explorer"), 24)
search_value = request.args.get("value", "")
normalized_type = search_type.strip().lower()
value_max_length = 44
if normalized_type in {"transaction", "tx"}:
value_max_length = 30
display_value = _ellipsize_middle(search_value, value_max_length)
value_font_size = "30"
if len(display_value) > 34:
value_font_size = "26"
is_default_card = not search_value.strip() and normalized_type in {
"explorer",
"search",
"",
}
type_text = escape(search_type)
value_text = escape(display_value)
title_text = escape(title)
subtitle_text = escape(subtitle)
if is_default_card:
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">
<defs>
<linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">
<stop offset=\"0%\" stop-color=\"#0b1220\" />
<stop offset=\"100%\" stop-color=\"#1f2937\" />
</linearGradient>
<linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">
<stop offset=\"0%\" stop-color=\"#ef4444\" />
<stop offset=\"100%\" stop-color=\"#f97316\" />
</linearGradient>
</defs>
<rect width=\"1200\" height=\"630\" fill=\"url(#bg)\" />
<circle cx=\"1100\" cy=\"80\" r=\"180\" fill=\"#ef4444\" opacity=\"0.16\" />
<circle cx=\"120\" cy=\"610\" r=\"240\" fill=\"#f97316\" opacity=\"0.14\" />
<rect x=\"86\" y=\"74\" width=\"1028\" height=\"482\" rx=\"28\" fill=\"#0f172a\" stroke=\"#374151\" />
<rect x=\"130\" y=\"126\" width=\"176\" height=\"42\" rx=\"21\" fill=\"url(#accent)\" />
<text x=\"218\" y=\"153\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"19\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">Handshake</text>
<text x=\"130\" y=\"246\" fill=\"#ffffff\" font-size=\"82\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"800\">{title_text}</text>
<text x=\"130\" y=\"302\" fill=\"#d1d5db\" font-size=\"32\" font-family=\"Inter, Arial, sans-serif\">{subtitle_text}</text>
<line x1=\"130\" y1=\"338\" x2=\"1070\" y2=\"338\" stroke=\"#374151\" />
<text x=\"130\" y=\"402\" fill=\"#f9fafb\" font-size=\"34\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"600\">Blocks • Transactions • Addresses • Names</text>
<text x=\"130\" y=\"452\" fill=\"#9ca3af\" font-size=\"28\" font-family=\"Inter, Arial, sans-serif\">Real-time status, searchable history, and rich on-chain data.</text>
<text x=\"86\" y=\"608\" fill=\"#9ca3af\" font-size=\"24\" font-family=\"Inter, Arial, sans-serif\">explorer.hns.au</text>
</svg>"""
else:
svg = f"""<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1200\" height=\"630\" viewBox=\"0 0 1200 630\">
<defs>
<linearGradient id=\"bg\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">
<stop offset=\"0%\" stop-color=\"#111827\" />
<stop offset=\"100%\" stop-color=\"#1f2937\" />
</linearGradient>
<linearGradient id=\"accent\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">
<stop offset=\"0%\" stop-color=\"#ef4444\" />
<stop offset=\"100%\" stop-color=\"#f97316\" />
</linearGradient>
<clipPath id=\"valueClip\">
<rect x=\"96\" y=\"496\" width=\"1008\" height=\"52\" rx=\"4\" />
</clipPath>
</defs>
<rect width=\"1200\" height=\"630\" fill=\"url(#bg)\" />
<circle cx=\"1040\" cy=\"120\" r=\"180\" fill=\"#ef4444\" opacity=\"0.14\" />
<circle cx=\"160\" cy=\"580\" r=\"220\" fill=\"#f97316\" opacity=\"0.12\" />
<rect x=\"72\" y=\"64\" width=\"240\" height=\"44\" rx=\"22\" fill=\"url(#accent)\" />
<text x=\"192\" y=\"92\" text-anchor=\"middle\" fill=\"#ffffff\" font-size=\"20\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">{type_text}</text>
<text x=\"72\" y=\"188\" fill=\"#ffffff\" font-size=\"62\" font-family=\"Inter, Arial, sans-serif\" font-weight=\"700\">{title_text}</text>
<text x=\"72\" y=\"252\" fill=\"#d1d5db\" font-size=\"33\" font-family=\"Inter, Arial, sans-serif\">{subtitle_text}</text>
<rect x=\"72\" y=\"472\" width=\"1056\" height=\"96\" rx=\"16\" fill=\"#0b1220\" stroke=\"#374151\" />
<text x=\"96\" y=\"530\" clip-path=\"url(#valueClip)\" fill=\"#f9fafb\" font-size=\"{value_font_size}\" font-family=\"'JetBrains Mono', 'Consolas', monospace\">{value_text}</text>
<text x=\"72\" y=\"610\" fill=\"#9ca3af\" font-size=\"24\" font-family=\"Inter, Arial, sans-serif\">explorer.hns.au</text>
</svg>"""
response = make_response(svg)
response.headers["Content-Type"] = "image/svg+xml; charset=utf-8"
response.headers["Cache-Control"] = "public, max-age=300"
return response
@app.route("/<path:path>") @app.route("/<path:path>")

View File

@@ -836,3 +836,7 @@ a:hover {
.tx-item { .tx-item {
animation: staggerFade 0.4s ease forwards; animation: staggerFade 0.4s ease forwards;
} }
small {
color: var(--text-secondary);
}

View File

@@ -4,30 +4,30 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fire Explorer</title> <title>{{ meta_title or 'Fire Explorer' }}</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png"> <link rel="icon" href="/assets/img/favicon.png" type="image/png">
<link rel="stylesheet" href="/assets/css/index.css"> <link rel="stylesheet" href="/assets/css/index.css">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<meta name="description" content="A hot new Handshake Blockchain Explorer"> <meta name="description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
<!-- Open Graph Meta Tags --> <!-- Open Graph Meta Tags -->
<meta property="og:url" content="https://explorer.hns.au/"> <meta property="og:url" content="{{ og_url or 'https://explorer.hns.au/' }}">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:title" content="Fire Explorer"> <meta property="og:title" content="{{ meta_title or 'Fire Explorer' }}">
<meta property="og:description" content="A hot new Handshake Blockchain Explorer"> <meta property="og:description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
<meta property="og:image" content="https://explorer.hns.au/assets/img/og.png"> <meta property="og:image" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
<meta property="og:image:width" content="1200"> <meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630"> <meta property="og:image:height" content="630">
<!-- Twitter Meta Tags --> <!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<meta property="twitter:domain" content="explorer.hns.au"> <meta property="twitter:domain" content="{{ twitter_domain or 'explorer.hns.au' }}">
<meta property="twitter:url" content="https://explorer.hns.au/"> <meta property="twitter:url" content="{{ og_url or 'https://explorer.hns.au/' }}">
<meta name="twitter:title" content="Fire Explorer"> <meta name="twitter:title" content="{{ meta_title or 'Fire Explorer' }}">
<meta name="twitter:description" content="A hot new Handshake Blockchain Explorer"> <meta name="twitter:description" content="{{ meta_description or 'A hot new Handshake Blockchain Explorer' }}">
<meta name="twitter:image" content="https://explorer.hns.au/assets/img/og.png"> <meta name="twitter:image" content="{{ og_image or 'https://explorer.hns.au/assets/img/og.png' }}">
</head> </head>
<body> <body>
@@ -119,6 +119,7 @@
<div class="container"> <div class="container">
<p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p> <p>Fire Explorer - Handshake Blockchain Explorer | Powered by <a href="https://hns.au" target="_blank">HNSAU</a> & <a href="https://hsd.hns.au" target="_blank">Fire HSD</a></p>
<p class="timestamp">Last updated: {{ datetime }}</p> <p class="timestamp">Last updated: {{ datetime }}</p>
<small>Note: Date and times are in UTC</small>
</div> </div>
</footer> </footer>

66
uv.lock generated
View File

@@ -239,6 +239,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
] ]
[[package]]
name = "fireexplorer"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cryptography" },
{ name = "flask" },
{ name = "gunicorn" },
{ name = "python-dotenv" },
{ name = "requests-doh" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "flask", specifier = ">=3.1.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "requests-doh", specifier = ">=1.0.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.4.0" },
{ name = "ruff", specifier = ">=0.14.5" },
]
[[package]] [[package]]
name = "flask" name = "flask"
version = "3.1.2" version = "3.1.2"
@@ -497,39 +530,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
] ]
[[package]]
name = "python-webserver-template"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cryptography" },
{ name = "flask" },
{ name = "gunicorn" },
{ name = "python-dotenv" },
{ name = "requests-doh" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "flask", specifier = ">=3.1.2" },
{ name = "gunicorn", specifier = ">=23.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "requests-doh", specifier = ">=1.0.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.4.0" },
{ name = "ruff", specifier = ">=0.14.5" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"