"use strict"; // Global variables let currentStartHeight = null; const mempoolBaseUrl = "https://mempool.guide"; // Switched from mempool.space to mempool.guide - more aligned with Ocean.xyz ethos let blocksCache = {}; let isLoading = false; // DOM ready initialization $(document).ready(function() { console.log("Blocks page initialized"); // Load timezone setting early (function loadTimezoneEarly() { // First try to get from localStorage for instant access try { const storedTimezone = localStorage.getItem('dashboardTimezone'); if (storedTimezone) { window.dashboardTimezone = storedTimezone; console.log(`Using cached timezone: ${storedTimezone}`); } } catch (e) { console.error("Error reading timezone from localStorage:", e); } // Then fetch from server to ensure we have the latest setting fetch('/api/timezone') .then(response => response.json()) .then(data => { if (data && data.timezone) { window.dashboardTimezone = data.timezone; console.log(`Set timezone from server: ${data.timezone}`); // Cache for future use try { localStorage.setItem('dashboardTimezone', data.timezone); } catch (e) { console.error("Error storing timezone in localStorage:", e); } } }) .catch(error => { console.error("Error fetching timezone:", error); }); })(); // Initialize notification badge initNotificationBadge(); // Load the latest blocks on page load loadLatestBlocks(); // Set up event listeners $("#load-blocks").on("click", function() { const height = $("#block-height").val(); if (height && !isNaN(height)) { loadBlocksFromHeight(height); } else { showToast("Please enter a valid block height"); } }); $("#latest-blocks").on("click", loadLatestBlocks); // Handle Enter key on the block height input $("#block-height").on("keypress", function(e) { if (e.which === 13) { const height = $(this).val(); if (height && !isNaN(height)) { loadBlocksFromHeight(height); } else { showToast("Please enter a valid block height"); } } }); // Close the modal when clicking the X or outside the modal $(".block-modal-close").on("click", closeModal); $(window).on("click", function(event) { if ($(event.target).hasClass("block-modal")) { closeModal(); } }); // Initialize BitcoinMinuteRefresh if available if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) { BitcoinMinuteRefresh.initialize(loadLatestBlocks); console.log("BitcoinMinuteRefresh initialized with refresh function"); } }); // Update unread notifications badge in navigation function updateNotificationBadge() { $.ajax({ url: "/api/notifications/unread_count", method: "GET", success: function (data) { const unreadCount = data.unread_count; const badge = $("#nav-unread-badge"); if (unreadCount > 0) { badge.text(unreadCount).show(); } else { badge.hide(); } } }); } // Initialize notification badge checking function initNotificationBadge() { // Update immediately updateNotificationBadge(); // Update every 60 seconds setInterval(updateNotificationBadge, 60000); } // Helper function to format timestamps as readable dates function formatTimestamp(timestamp) { const date = new Date(timestamp * 1000); const options = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true, timeZone: window.dashboardTimezone || 'America/Los_Angeles' // Use global timezone setting }; return date.toLocaleString('en-US', options); } // Helper function to format numbers with commas function numberWithCommas(x) { if (x == null) return "N/A"; return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); } // Helper function to format file sizes function formatFileSize(bytes) { if (bytes < 1024) return bytes + " B"; else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + " KB"; else return (bytes / 1048576).toFixed(2) + " MB"; } // Helper function to show toast messages function showToast(message) { // Check if we already have a toast container let toastContainer = $(".toast-container"); if (toastContainer.length === 0) { // Create a new toast container toastContainer = $("
", { class: "toast-container", css: { position: "fixed", bottom: "20px", right: "20px", zIndex: 9999 } }).appendTo("body"); } // Create a new toast const toast = $("
", { class: "toast", text: message, css: { backgroundColor: "#f7931a", color: "#000", padding: "10px 15px", borderRadius: "5px", marginTop: "10px", boxShadow: "0 0 10px rgba(247, 147, 26, 0.5)", fontFamily: "var(--terminal-font)", opacity: 0, transition: "opacity 0.3s ease" } }).appendTo(toastContainer); // Show the toast setTimeout(() => { toast.css("opacity", 1); // Hide and remove the toast after 3 seconds setTimeout(() => { toast.css("opacity", 0); setTimeout(() => toast.remove(), 300); }, 3000); }, 100); } // Pool color mapping function - add this after your existing helper functions function getPoolColor(poolName) { // Normalize the pool name (lowercase and remove special characters) const normalizedName = poolName.toLowerCase().replace(/[^a-z0-9]/g, ''); // Define color mappings for common mining pools with Ocean pool featured prominently const poolColors = { // OCEAN pool with a distinctive bright cyan color for prominence 'ocean': '#00ffff', // Bright Cyan for Ocean 'oceanpool': '#00ffff', // Bright Cyan for Ocean 'oceanxyz': '#00ffff', // Bright Cyan for Ocean 'ocean.xyz': '#00ffff', // Bright Cyan for Ocean // Other common mining pools with more muted colors 'f2pool': '#1a9eff', // Blue 'antpool': '#ff7e33', // Orange 'binancepool': '#f3ba2f', // Binance gold 'foundryusa': '#b150e2', // Purple 'viabtc': '#ff5c5c', // Red 'luxor': '#2bae2b', // Green 'slushpool': '#3355ff', // Bright blue 'btccom': '#ff3355', // Pink 'poolin': '#ffaa22', // Amber 'sbicrypto': '#cc9933', // Bronze 'mara': '#8844cc', // Violet 'ultimuspool': '#09c7be', // Teal 'unknown': '#999999' // Grey for unknown pools }; // Check for partial matches in pool names (for variations like "F2Pool" vs "F2pool.com") for (const [key, color] of Object.entries(poolColors)) { if (normalizedName.includes(key)) { return color; } } // If no match is found, generate a consistent color based on the pool name let hash = 0; for (let i = 0; i < poolName.length; i++) { hash = poolName.charCodeAt(i) + ((hash << 5) - hash); } // Generate HSL color with fixed saturation and lightness for readability // Use the hash to vary the hue only (0-360) const hue = Math.abs(hash % 360); return `hsl(${hue}, 70%, 60%)`; } // Function to create a block card function createBlockCard(block) { const timestamp = formatTimestamp(block.timestamp); const formattedSize = formatFileSize(block.size); const formattedTxCount = numberWithCommas(block.tx_count); // Get the pool name or "Unknown" const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown"; // Get pool color const poolColor = getPoolColor(poolName); // Check if this is an Ocean pool block for special styling const isOceanPool = poolName.toLowerCase().includes('ocean'); // Calculate total fees in BTC const totalFees = block.extras ? (block.extras.totalFees / 100000000).toFixed(8) : "N/A"; // Create the block card const blockCard = $("
", { class: "block-card", "data-height": block.height, "data-hash": block.id }); // Apply pool color border - with special emphasis for Ocean pool if (isOceanPool) { // Give Ocean pool blocks a more prominent styling blockCard.css({ "border": `2px solid ${poolColor}`, "box-shadow": `0 0 10px ${poolColor}`, "background": `linear-gradient(to bottom, rgba(0, 255, 255, 0.1), rgba(0, 0, 0, 0))` }); } else { // Standard styling for other pools blockCard.css({ "border-left": `4px solid ${poolColor}`, "border-top": `1px solid ${poolColor}30`, "border-right": `1px solid ${poolColor}30`, "border-bottom": `1px solid ${poolColor}30` }); } // Create the block header const blockHeader = $("
", { class: "block-header" }); blockHeader.append($("
", { class: "block-height", text: "#" + block.height })); blockHeader.append($("
", { class: "block-time", text: timestamp })); blockCard.append(blockHeader); // Create the block info section const blockInfo = $("
", { class: "block-info" }); // Add transaction count with conditional coloring based on count const txCountItem = $("
", { class: "block-info-item" }); txCountItem.append($("
", { class: "block-info-label", text: "Transactions" })); // Determine transaction count color based on thresholds let txCountClass = "green"; // Default for high transaction counts (2000+) if (block.tx_count < 500) { txCountClass = "red"; // Less than 500 transactions } else if (block.tx_count < 2000) { txCountClass = "yellow"; // Between 500 and 1999 transactions } txCountItem.append($("
", { class: `block-info-value ${txCountClass}`, text: formattedTxCount })); blockInfo.append(txCountItem); // Add size const sizeItem = $("
", { class: "block-info-item" }); sizeItem.append($("
", { class: "block-info-label", text: "Size" })); sizeItem.append($("
", { class: "block-info-value white", text: formattedSize })); blockInfo.append(sizeItem); // Add miner/pool with custom color const minerItem = $("
", { class: "block-info-item" }); minerItem.append($("
", { class: "block-info-label", text: "Miner" })); // Apply the custom pool color with special styling for Ocean pool const minerValue = $("
", { class: "block-info-value", text: poolName, css: { color: poolColor, textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, fontWeight: isOceanPool ? "bold" : "normal" } }); // Add a special indicator icon for Ocean pool if (isOceanPool) { minerValue.prepend($("", { html: "★ ", css: { color: poolColor } })); } minerItem.append(minerValue); blockInfo.append(minerItem); // Add Avg Fee Rate const feesItem = $("
", { class: "block-info-item" }); feesItem.append($("
", { class: "block-info-label", text: "Avg Fee Rate" })); feesItem.append($("
", { class: "block-info-value yellow", text: block.extras && block.extras.avgFeeRate ? block.extras.avgFeeRate + " sat/vB" : "N/A" })); blockInfo.append(feesItem); blockCard.append(blockInfo); // Add event listener for clicking on the block card blockCard.on("click", function () { showBlockDetails(block); }); return blockCard; } // Function to load blocks from a specific height function loadBlocksFromHeight(height) { if (isLoading) return; // Convert to integer height = parseInt(height); if (isNaN(height) || height < 0) { showToast("Please enter a valid block height"); return; } isLoading = true; currentStartHeight = height; // Check if we already have this data in cache if (blocksCache[height]) { displayBlocks(blocksCache[height]); isLoading = false; return; } // Show loading state $("#blocks-grid").html('
Loading blocks from height ' + height + '
'); // Fetch blocks from the API $.ajax({ url: `${mempoolBaseUrl}/api/v1/blocks/${height}`, method: "GET", dataType: "json", timeout: 10000, success: function(data) { // Cache the data blocksCache[height] = data; // Display the blocks displayBlocks(data); // Update latest block stats if (data.length > 0) { updateLatestBlockStats(data[0]); } }, error: function(xhr, status, error) { console.error("Error fetching blocks:", error); $("#blocks-grid").html('
Error fetching blocks. Please try again later.
'); // Show error toast showToast("Failed to load blocks. Please try again later."); }, complete: function() { isLoading = false; } }); } // Function to load the latest blocks and return a promise with the latest block height function loadLatestBlocks() { if (isLoading) return Promise.resolve(null); isLoading = true; // Show loading state $("#blocks-grid").html('
Loading latest blocks
'); // Fetch the latest blocks from the API return $.ajax({ url: `${mempoolBaseUrl}/api/v1/blocks`, method: "GET", dataType: "json", timeout: 10000, success: function (data) { // Cache the data (use the first block's height as the key) if (data.length > 0) { currentStartHeight = data[0].height; blocksCache[currentStartHeight] = data; // Update the block height input with the latest height $("#block-height").val(currentStartHeight); // Update latest block stats updateLatestBlockStats(data[0]); } // Display the blocks displayBlocks(data); }, error: function (xhr, status, error) { console.error("Error fetching latest blocks:", error); $("#blocks-grid").html('
Error fetching blocks. Please try again later.
'); // Show error toast showToast("Failed to load latest blocks. Please try again later."); }, complete: function () { isLoading = false; } }).then(data => data.length > 0 ? data[0].height : null); } // Refresh blocks page every 60 seconds if there are new blocks setInterval(function () { console.log("Checking for new blocks at " + new Date().toLocaleTimeString()); loadLatestBlocks().then(latestHeight => { if (latestHeight && latestHeight > currentStartHeight) { console.log("New blocks detected, refreshing the page"); location.reload(); } else { console.log("No new blocks detected"); } }); }, 60000); // Function to update the latest block stats section function updateLatestBlockStats(block) { if (!block) return; $("#latest-height").text(block.height); $("#latest-time").text(formatTimestamp(block.timestamp)); $("#latest-tx-count").text(numberWithCommas(block.tx_count)); $("#latest-size").text(formatFileSize(block.size)); $("#latest-difficulty").text(numberWithCommas(Math.round(block.difficulty))); // Pool info with color coding const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown"; const poolColor = getPoolColor(poolName); const isOceanPool = poolName.toLowerCase().includes('ocean'); // Clear previous content of the pool span const poolSpan = $("#latest-pool"); poolSpan.empty(); // Create the pool name element with styling const poolElement = $("", { text: poolName, css: { color: poolColor, textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, fontWeight: isOceanPool ? "bold" : "normal" } }); // Add star icon for Ocean pool if (isOceanPool) { poolElement.prepend($("", { html: "★ ", css: { color: poolColor } })); } // Add the styled element to the DOM poolSpan.append(poolElement); // If this is the latest block from Ocean pool, add a subtle highlight to the stats card const statsCard = $(".latest-block-stats").closest(".card"); if (isOceanPool) { statsCard.css({ "border": `2px solid ${poolColor}`, "box-shadow": `0 0 10px ${poolColor}`, "background": `linear-gradient(to bottom, rgba(0, 255, 255, 0.05), rgba(0, 0, 0, 0))` }); } else { // Reset to default styling if not Ocean pool statsCard.css({ "border": "", "box-shadow": "", "background": "" }); } // Average Fee Rate if (block.extras && block.extras.avgFeeRate) { $("#latest-fee-rate").text(block.extras.avgFeeRate + " sat/vB"); } else { $("#latest-fee-rate").text("N/A"); } } // Function to display the blocks in the grid function displayBlocks(blocks) { const blocksGrid = $("#blocks-grid"); // Clear the grid blocksGrid.empty(); if (!blocks || blocks.length === 0) { blocksGrid.html('
No blocks found
'); return; } // Create a card for each block blocks.forEach(function(block) { const blockCard = createBlockCard(block); blocksGrid.append(blockCard); }); // Add navigation controls if needed addNavigationControls(blocks); } // Function to add navigation controls to the blocks grid function addNavigationControls(blocks) { // Get the height of the first and last block in the current view const firstBlockHeight = blocks[0].height; const lastBlockHeight = blocks[blocks.length - 1].height; // Create navigation controls const navControls = $("
", { class: "block-navigation" }); // Newer blocks button (if not already at the latest blocks) if (firstBlockHeight !== currentStartHeight) { const newerButton = $("