"use strict"; // Constants for configuration const REFRESH_INTERVAL = 60000; // 60 seconds const TOAST_DISPLAY_TIME = 3000; // 3 seconds const DEFAULT_TIMEZONE = 'America/Los_Angeles'; const SATOSHIS_PER_BTC = 100000000; const MAX_CACHE_SIZE = 20; // Number of block heights to cache // POOL configuration const POOL_CONFIG = { oceanPools: ['ocean', 'oceanpool', 'oceanxyz', 'ocean.xyz'], oceanColor: '#00ffff', defaultUnknownColor: '#999999' }; // 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; // Helper function for debouncing function debounce(func, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // Helper function to validate block height function isValidBlockHeight(height) { height = parseInt(height); if (isNaN(height) || height < 0) { showToast("Please enter a valid block height"); return false; } return height; } // Helper function to add items to cache with size management function addToCache(height, data) { blocksCache[height] = data; // Remove oldest entries if cache exceeds maximum size const cacheKeys = Object.keys(blocksCache).map(Number).sort((a, b) => a - b); if (cacheKeys.length > MAX_CACHE_SIZE) { const keysToRemove = cacheKeys.slice(0, cacheKeys.length - MAX_CACHE_SIZE); keysToRemove.forEach(key => delete blocksCache[key]); } } // Clean up event handlers when refreshing or navigating function cleanupEventHandlers() { $(window).off("click.blockModal"); $(document).off("keydown.blockModal"); } // Setup keyboard navigation for modal function setupModalKeyboardNavigation() { $(document).on('keydown.blockModal', function (e) { const modal = $("#block-modal"); if (modal.css('display') === 'block') { if (e.keyCode === 27) { // ESC key closeModal(); } } }); } // 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 = isValidBlockHeight($("#block-height").val()); if (height !== false) { loadBlocksFromHeight(height); } }); $("#latest-blocks").on("click", loadLatestBlocks); // Handle Enter key on the block height input with debouncing $("#block-height").on("keypress", debounce(function (e) { if (e.which === 13) { const height = isValidBlockHeight($(this).val()); if (height !== false) { loadBlocksFromHeight(height); } } }, 300)); // Close the modal when clicking the X or outside the modal $(".block-modal-close").on("click", closeModal); $(window).on("click.blockModal", 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"); } // Setup keyboard navigation for modals setupModalKeyboardNavigation(); // Cleanup before unload $(window).on('beforeunload', cleanupEventHandlers); }); // 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, REFRESH_INTERVAL); } // 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 || DEFAULT_TIMEZONE // 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 create common info items function createInfoItem(label, value, valueClass = '') { const item = $("
", { class: "block-info-item" }); item.append($("
", { class: "block-info-label", text: label })); item.append($("
", { class: `block-info-value ${valueClass}`, text: value })); return item; } // Helper function for creating detail items function createDetailItem(label, value, valueClass = '') { const item = $("
", { class: "block-detail-item" }); item.append($("
", { class: "block-detail-label", text: label })); item.append($("
", { class: `block-detail-value ${valueClass}`, text: value })); return item; } // 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 the configured time setTimeout(() => { toast.css("opacity", 0); setTimeout(() => toast.remove(), 300); }, TOAST_DISPLAY_TIME); }, 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': POOL_CONFIG.oceanColor, 'oceanpool': POOL_CONFIG.oceanColor, 'oceanxyz': POOL_CONFIG.oceanColor, 'ocean.xyz': POOL_CONFIG.oceanColor, // 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': POOL_CONFIG.defaultUnknownColor // 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 check if a pool is an Ocean pool function isOceanPool(poolName) { const normalizedName = poolName.toLowerCase(); return POOL_CONFIG.oceanPools.some(name => normalizedName.includes(name)); } // 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 isPoolOcean = isOceanPool(poolName); // Calculate total fees in BTC const totalFees = block.extras ? (block.extras.totalFees / SATOSHIS_PER_BTC).toFixed(8) : "N/A"; // Create the block card with accessibility attributes const blockCard = $("
", { class: "block-card", "data-height": block.height, "data-hash": block.id, tabindex: "0", // Make focusable role: "button", "aria-label": `Block ${block.height} mined by ${poolName} on ${timestamp}` }); // Apply pool color border - with special emphasis for Ocean pool if (isPoolOcean) { // 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" }); // 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 } // Add transaction count using helper blockInfo.append(createInfoItem("Transactions", formattedTxCount, txCountClass)); // Add size using helper blockInfo.append(createInfoItem("Size", formattedSize, "white")); // 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: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, fontWeight: isPoolOcean ? "bold" : "normal" } }); // Add a special indicator icon for Ocean pool if (isPoolOcean) { minerValue.prepend($("", { html: "★ ", css: { color: poolColor } })); } minerItem.append(minerValue); blockInfo.append(minerItem); // Add Avg Fee Rate using helper const feeRateText = block.extras && block.extras.avgFeeRate ? block.extras.avgFeeRate + " sat/vB" : "N/A"; blockInfo.append(createInfoItem("Avg Fee Rate", feeRateText, "yellow")); blockCard.append(blockInfo); // Add event listeners for clicking and keyboard on the block card blockCard.on("click", function () { showBlockDetails(block); }); blockCard.on("keypress", function (e) { if (e.which === 13 || e.which === 32) { // Enter or Space key 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 using helper addToCache(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; addToCache(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 - with smart refresh setInterval(function () { console.log("Checking for new blocks at " + new Date().toLocaleTimeString()); loadLatestBlocks().then(latestHeight => { if (latestHeight && latestHeight > currentStartHeight) { console.log("New blocks detected, loading latest blocks"); // Instead of reloading the page, just load the latest blocks currentStartHeight = latestHeight; loadLatestBlocks(); // Show a notification showToast("New blocks detected! View updated."); } else { console.log("No new blocks detected"); } }); }, REFRESH_INTERVAL); // 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 isPoolOcean = isOceanPool(poolName); // 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: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, fontWeight: isPoolOcean ? "bold" : "normal" } }); // Add star icon for Ocean pool if (isPoolOcean) { 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 (isPoolOcean) { 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; } // Use document fragment for batch DOM operations const fragment = document.createDocumentFragment(); // Create a card for each block blocks.forEach(function (block) { const blockCard = createBlockCard(block); fragment.appendChild(blockCard[0]); }); // Add all cards at once blocksGrid.append(fragment); // 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 = $("