Compare commits

..

5 Commits

Author SHA1 Message Date
b6662f400a feat: Add support for DATABASE in dockerfile volume
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
Check Code Quality / RuffCheck (push) Successful in 49s
2025-11-21 13:13:35 +11:00
1c51e97354 feat: Add database for namehash caching 2025-11-21 13:11:44 +11:00
206b323be6 feat: Update mobile layout
All checks were successful
Check Code Quality / RuffCheck (push) Successful in 2m0s
Build Docker / BuildImage (push) Successful in 2m13s
2025-11-21 12:45:09 +11:00
400897319f feat: Update OG image
All checks were successful
Build Docker / BuildImage (push) Successful in 42s
Check Code Quality / RuffCheck (push) Successful in 50s
2025-11-21 00:53:07 +11:00
a36e467bd4 Merge pull request 'Redo theming to align with branding' (#1) from feat/gemini_copilot into main
All checks were successful
Build Docker / BuildImage (push) Successful in 39s
Check Code Quality / RuffCheck (push) Successful in 49s
Reviewed-on: #1
2025-11-21 00:39:54 +11:00
6 changed files with 135 additions and 12 deletions

1
.gitignore vendored
View File

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

View File

@@ -22,6 +22,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
# Add mount point for data volume # Add mount point for data volume
ENV BASE_DIR=/data ENV BASE_DIR=/data
ENV DATABASE_PATH=/data/fireexplorer.db
VOLUME /data VOLUME /data
EXPOSE 5000 EXPOSE 5000

View File

@@ -5,9 +5,12 @@ from flask import (
send_from_directory, send_from_directory,
send_file, send_file,
jsonify, jsonify,
g,
request,
) )
import os import os
import requests import requests
import sqlite3
from datetime import datetime from datetime import datetime
import dotenv import dotenv
from tools import hip2, wallet_txt from tools import hip2, wallet_txt
@@ -16,6 +19,40 @@ dotenv.load_dotenv()
app = Flask(__name__) app = Flask(__name__)
DATABASE = os.getenv("DATABASE_PATH", "fireexplorer.db")
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
def init_db():
with app.app_context():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS names (
namehash TEXT PRIMARY KEY,
name TEXT NOT NULL
)
"""
)
db.commit()
init_db()
def find(name, path): def find(name, path):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
@@ -133,6 +170,32 @@ def catch_all(path: str):
return render_template("404.html"), 404 return render_template("404.html"), 404
# endregion
# region API routes
@app.route("/api/v1/namehash/<namehash>")
def namehash_api(namehash):
db = get_db()
cur = db.execute("SELECT * FROM names WHERE namehash = ?", (namehash,))
row = cur.fetchone()
if row is None:
# Get namehash from hsd.hns.au
req = requests.get(f"https://hsd.hns.au/api/v1/namehash/{namehash}")
if req.status_code == 200:
name = req.json().get("result")
if not name:
return jsonify({"name": "Error", "namehash": namehash})
# Insert into database
db.execute(
"INSERT OR REPLACE INTO names (namehash, name) VALUES (?, ?)",
(namehash, name),
)
db.commit()
return jsonify({"name": name, "namehash": namehash})
return jsonify(dict(row))
@app.route("/api/v1/status") @app.route("/api/v1/status")
def api_status(): def api_status():
return jsonify( return jsonify(
@@ -153,6 +216,7 @@ def hip02(domain: str):
"success": True, "success": True,
"address": hip2_record, "address": hip2_record,
"method": "hip02", "method": "hip02",
"name": domain,
} }
) )
@@ -163,16 +227,30 @@ def hip02(domain: str):
"success": True, "success": True,
"address": wallet_record, "address": wallet_record,
"method": "wallet_txt", "method": "wallet_txt",
"name": domain,
} }
) )
return jsonify( return jsonify(
{ {
"success": False, "success": False,
"name": domain,
"error": "No HIP02 or WALLET record found for this domain", "error": "No HIP02 or WALLET record found for this domain",
} }
) )
@app.route("/api/v1/covenant", methods=["POST"])
def covenant_api():
data = request.get_json()
return jsonify(
{
"success": True,
"data": data,
}
)
# endregion # endregion

View File

@@ -109,6 +109,7 @@ section {
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
opacity: 0; /* Start hidden for animation */ opacity: 0; /* Start hidden for animation */
min-width: 0; /* Prevent grid overflow */
} }
.card:hover { .card:hover {
@@ -159,7 +160,7 @@ section {
/* Info Grid */ /* Info Grid */
.info-grid { .info-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem; gap: 1rem;
} }
@@ -168,6 +169,7 @@ section {
background: rgba(15, 23, 42, 0.4); background: rgba(15, 23, 42, 0.4);
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--card-border); border: 1px solid var(--card-border);
min-width: 0; /* Prevent grid overflow */
} }
.info-item.no-border { .info-item.no-border {
@@ -265,6 +267,7 @@ section {
transition: all 0.2s ease; transition: all 0.2s ease;
animation: staggerFade 0.4s ease forwards; animation: staggerFade 0.4s ease forwards;
cursor: pointer; cursor: pointer;
min-width: 0; /* Prevent flex overflow */
} }
.tx-item:hover { .tx-item:hover {
@@ -282,6 +285,7 @@ section {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
min-width: 0; /* Prevent flex overflow */
} }
.tx-view-btn { .tx-view-btn {
@@ -388,6 +392,7 @@ section {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
margin: 0; margin: 0;
word-break: break-all;
} }
/* Transaction Details */ /* Transaction Details */
@@ -418,6 +423,7 @@ section {
border: 1px solid var(--card-border); border: 1px solid var(--card-border);
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
min-width: 0; /* Prevent overflow */
} }
.tx-io-header { .tx-io-header {
@@ -611,6 +617,7 @@ section {
font-size: 0.9rem; font-size: 0.9rem;
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all;
} }
.result-box .error { .result-box .error {
@@ -622,6 +629,16 @@ section {
border: 1px solid rgba(239, 68, 68, 0.2); border: 1px solid rgba(239, 68, 68, 0.2);
} }
.success-message {
color: var(--success-color);
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
word-break: break-all;
}
/* Scrollbar */ /* Scrollbar */
.result-box::-webkit-scrollbar { .result-box::-webkit-scrollbar {
width: 8px; width: 8px;
@@ -700,6 +717,26 @@ a:hover {
padding: 0.5rem; padding: 0.5rem;
font-size: 0.85rem; font-size: 0.85rem;
} }
.info-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.card {
padding: 1rem;
}
.container {
padding: 0 1rem;
}
.tx-io-header {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
} }
/* Loading Animation */ /* Loading Animation */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1055,7 +1055,7 @@
const note = document.createElement('div'); const note = document.createElement('div');
note.className = 'success-message'; note.className = 'success-message';
note.style.marginBottom = '1rem'; note.style.marginBottom = '1rem';
note.innerHTML = `Resolved alias <strong>${hip02Result.name || address}</strong> to address`; note.innerHTML = `Resolved <strong>${hip02Result.name || address}</strong> to address <br><span class="mono">${address}</span>`;
resultElement.parentNode.insertBefore(note, resultElement); resultElement.parentNode.insertBefore(note, resultElement);
setTimeout(() => note.remove(), 5000); setTimeout(() => note.remove(), 5000);
} else { } else {
@@ -1178,17 +1178,23 @@
} }
showLoading('name-result'); showLoading('name-result');
const data = await apiCall(`namehash/${nameHash}`);
// Check if result is valid and redirect to name page try {
const resultElement = document.getElementById('name-result'); const response = await fetch(`/api/v1/namehash/${nameHash}`);
if (data.error) { const data = await response.json();
resultElement.innerHTML = `<div class="error">Error: ${data.error.message ? data.error.message : "Failed to lookup hash"}</div>`;
} else if (data.result && typeof data.result === 'string') { const resultElement = document.getElementById('name-result');
// Valid name found, redirect to name page if (data.error) {
window.location.href = `/name/${data.result}`; resultElement.innerHTML = `<div class="error">Error: ${data.error}</div>`;
} else { } else if (data.name) {
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`; // Valid name found, redirect to name page
window.location.href = `/name/${data.name}`;
} else {
resultElement.innerHTML = `<div class="error">No name found for this hash</div>`;
}
} catch (e) {
const resultElement = document.getElementById('name-result');
resultElement.innerHTML = `<div class="error">Error: ${e.message}</div>`;
} }
} }