reorder txns newest on top; show confirmations tooltip
This commit is contained in:
@@ -131,6 +131,34 @@ def list_transactions():
|
|||||||
return jsonify({"error": str(exc)}), 500
|
return jsonify({"error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/network", methods=["GET"])
|
||||||
|
@require_token
|
||||||
|
def get_network_info():
|
||||||
|
try:
|
||||||
|
network_info = rpc_call("getnetworkinfo")
|
||||||
|
peer_info = rpc_call("getpeerinfo")
|
||||||
|
mempool = rpc_call("getrawmempool")
|
||||||
|
blockchain_info = rpc_call("getblockchaininfo")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"network": {
|
||||||
|
"connections": network_info.get("connections", 0),
|
||||||
|
"networkactive": network_info.get("networkactive", False),
|
||||||
|
"relayfee": network_info.get("relayfee", 0),
|
||||||
|
},
|
||||||
|
"peers": len(peer_info),
|
||||||
|
"mempool_size": len(mempool),
|
||||||
|
"blockchain": {
|
||||||
|
"blocks": blockchain_info.get("blocks", 0),
|
||||||
|
"headers": blockchain_info.get("headers", 0),
|
||||||
|
"difficulty": blockchain_info.get("difficulty", 0),
|
||||||
|
"chain": blockchain_info.get("chain", "unknown"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except RuntimeError as exc:
|
||||||
|
return jsonify({"error": str(exc)}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def root():
|
def root():
|
||||||
return send_from_directory(app.static_folder, "index.html")
|
return send_from_directory(app.static_folder, "index.html")
|
||||||
|
|||||||
@@ -147,6 +147,14 @@
|
|||||||
background: rgba(255, 152, 0, 0.2);
|
background: rgba(255, 152, 0, 0.2);
|
||||||
color: #ff9800;
|
color: #ff9800;
|
||||||
}
|
}
|
||||||
|
.tx-link {
|
||||||
|
color: #29b6f6;
|
||||||
|
text-decoration: none;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.tx-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
body {
|
body {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -207,6 +215,13 @@
|
|||||||
<h2>Recent Activity</h2>
|
<h2>Recent Activity</h2>
|
||||||
<ul class="transaction-list" id="txList"></ul>
|
<ul class="transaction-list" id="txList"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Network Status</h2>
|
||||||
|
<div id="networkInfo">
|
||||||
|
<div class="muted">Loading network information...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -217,6 +232,7 @@
|
|||||||
const sendStatus = document.getElementById('sendStatus');
|
const sendStatus = document.getElementById('sendStatus');
|
||||||
const generatedAddressCard = document.getElementById('generatedAddressCard');
|
const generatedAddressCard = document.getElementById('generatedAddressCard');
|
||||||
const generatedAddress = document.getElementById('generatedAddress');
|
const generatedAddress = document.getElementById('generatedAddress');
|
||||||
|
const networkInfo = document.getElementById('networkInfo');
|
||||||
|
|
||||||
let apiToken = localStorage.getItem('rinWebWalletToken');
|
let apiToken = localStorage.getItem('rinWebWalletToken');
|
||||||
|
|
||||||
@@ -268,22 +284,25 @@
|
|||||||
const type = tx.category === 'send' ? 'Sent' : 'Received';
|
const type = tx.category === 'send' ? 'Sent' : 'Received';
|
||||||
const confirmations = tx.confirmations || 0;
|
const confirmations = tx.confirmations || 0;
|
||||||
|
|
||||||
let status, statusClass;
|
let status, statusClass, tooltip = '';
|
||||||
if (confirmations === 0) {
|
if (confirmations === 0) {
|
||||||
status = 'Pending';
|
status = 'Pending';
|
||||||
statusClass = 'status-pending';
|
statusClass = 'status-pending';
|
||||||
|
tooltip = 'title="0 confirmations - waiting for network confirmation"';
|
||||||
} else if (confirmations < 20) {
|
} else if (confirmations < 20) {
|
||||||
status = `Immature (${confirmations}/20)`;
|
status = `Immature (${confirmations}/20)`;
|
||||||
statusClass = 'status-immature';
|
statusClass = 'status-immature';
|
||||||
|
tooltip = `title="${confirmations} confirmations - needs 20 for full confirmation"`;
|
||||||
} else {
|
} else {
|
||||||
status = 'Confirmed';
|
status = 'Confirmed';
|
||||||
statusClass = 'status-confirmed';
|
statusClass = 'status-confirmed';
|
||||||
|
tooltip = `title="${confirmations} confirmations - fully confirmed"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div><strong>${type}</strong> ${amount} RIN <span class="status-badge ${statusClass}">${status}</span></div>
|
<div><strong>${type}</strong> ${amount} RIN <span class="status-badge ${statusClass}" ${tooltip}>${status}</span></div>
|
||||||
<div class="muted">${tx.time ? new Date(tx.time * 1000).toLocaleString() : ''}</div>
|
<div class="muted">${tx.time ? new Date(tx.time * 1000).toLocaleString() : ''}</div>
|
||||||
<div class="muted">${tx.txid}</div>
|
<div class="muted"><a href="https://explorer.rin.so/tx/${tx.txid}" target="_blank" class="tx-link">${tx.txid}</a></div>
|
||||||
`;
|
`;
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
@@ -308,6 +327,8 @@
|
|||||||
txList.innerHTML = '<li class="muted">No transactions yet.</li>';
|
txList.innerHTML = '<li class="muted">No transactions yet.</li>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Sort transactions by time (newest first)
|
||||||
|
data.transactions.sort((a, b) => (b.time || 0) - (a.time || 0));
|
||||||
data.transactions.forEach((tx) => txList.appendChild(renderTx(tx)));
|
data.transactions.forEach((tx) => txList.appendChild(renderTx(tx)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,8 +402,54 @@
|
|||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchNetworkInfo() {
|
||||||
|
let res = await fetch('/api/network', { headers: authHeaders() });
|
||||||
|
let data = await res.json();
|
||||||
|
|
||||||
|
// If token is invalid, refresh and retry
|
||||||
|
if (data.error === 'invalid_token') {
|
||||||
|
await refreshToken();
|
||||||
|
res = await fetch('/api/network', { headers: authHeaders() });
|
||||||
|
data = await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
networkInfo.innerHTML = `<div class="muted">Error: ${data.error}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const network = data.network;
|
||||||
|
const blockchain = data.blockchain;
|
||||||
|
|
||||||
|
networkInfo.innerHTML = `
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
|
||||||
|
<div>
|
||||||
|
<div class="muted">Network Connections</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${network.connections}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="muted">Mempool Size</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${data.mempool_size}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="muted">Block Height</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${blockchain.blocks}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="muted">Chain</div>
|
||||||
|
<div style="font-size: 18px; font-weight: 600;">${blockchain.chain}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="muted" style="font-size: 12px;">
|
||||||
|
Network Active: ${network.networkactive ? 'Yes' : 'No'} |
|
||||||
|
Relay Fee: ${network.relayfee} RIN |
|
||||||
|
Difficulty: ${blockchain.difficulty.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
await Promise.all([fetchBalances(), fetchTransactions()]);
|
await Promise.all([fetchBalances(), fetchTransactions(), fetchNetworkInfo()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('refreshButton').addEventListener('click', refresh);
|
document.getElementById('refreshButton').addEventListener('click', refresh);
|
||||||
|
|||||||
Reference in New Issue
Block a user