From 9a0e151fa553e31c5fd7053d73ad727388715912 Mon Sep 17 00:00:00 2001 From: Steve Field Date: Sat, 29 Jan 2022 16:37:02 +0000 Subject: [PATCH] Add auto-refresh option --- app.js | 2 + app/config.js | 3 +- routes/baseRouter.js | 9 +- views/blocks.pug | 3 + views/includes/blocks-list.pug | 139 ++++++++++++++++++++++++-- views/includes/connection-warning.pug | 4 + views/includes/time-ago-text.pug | 13 ++- views/index.pug | 2 + views/layout.pug | 10 +- 9 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 views/includes/connection-warning.pug diff --git a/app.js b/app.js index 69576c09d..3a13fe24e 100755 --- a/app.js +++ b/app.js @@ -769,6 +769,7 @@ expressApp.use(function(req, res, next) { userSettings.localCurrency = (userSettings.localCurrency || config.displayDefaults.localCurrency); userSettings.uiTimezone = (userSettings.uiTimezone || config.displayDefaults.timezone); userSettings.uiTheme = (userSettings.uiTheme || config.displayDefaults.theme); + userSettings.autoRefresh = (userSettings.autoRefresh || config.displayDefaults.autoRefresh); // make available in templates @@ -778,6 +779,7 @@ expressApp.use(function(req, res, next) { res.locals.uiTheme = userSettings.uiTheme; res.locals.userTzOffset = userSettings.userTzOffset || "unset"; res.locals.browserTzOffset = userSettings.browserTzOffset || "0"; + res.locals.autoRefresh = userSettings.autoRefresh; if (!["/", "/connect"].includes(req.originalUrl)) { diff --git a/app/config.js b/app/config.js index fda0ab518..015eceda6 100644 --- a/app/config.js +++ b/app/config.js @@ -92,7 +92,8 @@ module.exports = { displayCurrency: (process.env.BTCEXP_DISPLAY_CURRENCY || "btc"), localCurrency: (process.env.BTCEXP_LOCAL_CURRENCY || "usd"), theme: (process.env.BTCEXP_UI_THEME || "dark"), - timezone: (process.env.BTCEXP_UI_TIMEZONE || "local") + timezone: (process.env.BTCEXP_UI_TIMEZONE || "local"), + autoRefresh: (process.env.BTCEXP_AUTO_REFRESH || "off") }, cookieSecret: cookieSecret, diff --git a/routes/baseRouter.js b/routes/baseRouter.js index 3ff89a228..bb62ef411 100644 --- a/routes/baseRouter.js +++ b/routes/baseRouter.js @@ -277,7 +277,7 @@ router.get("/", asyncHandler(async (req, res, next) => { await utils.timePromise("homepage.render", async () => { - res.render("index"); + res.render("index", {"currentBlockHeight":currentBlock.height, "periodicRefresh":true}); }, perfResults); next(); @@ -629,7 +629,12 @@ router.get("/blocks", asyncHandler(async (req, res, next) => { await utils.awaitPromises(promises); await utils.timePromise("blocks.render", async () => { - res.render("blocks"); + // If this page shows the latest block at the top, provide the current block height to tell the page + // to auto-refresh (if enabled) when a new block arrives. + if (offset == 0 && sort == "desc") + res.render("blocks", {"currentBlockHeight":getblockchaininfo.blocks}); + else + res.render("blocks"); }, perfResults); next(); diff --git a/views/blocks.pug b/views/blocks.pug index 08e85ee70..8b26ef0ae 100644 --- a/views/blocks.pug +++ b/views/blocks.pug @@ -20,6 +20,9 @@ block content a.page-link(href=(sort == "asc" ? "javascript:void(0)" : `./blocks?limit=${limit}&offset=0&sort=asc`)) span(aria-hidden="true") Oldest blocks first + if (autoRefresh == "on") + include includes/connection-warning.pug + if (blocks) +contentSection include includes/blocks-list.pug diff --git a/views/includes/blocks-list.pug b/views/includes/blocks-list.pug index 9e6a80abe..4b49b343e 100644 --- a/views/includes/blocks-list.pug +++ b/views/includes/blocks-list.pug @@ -69,8 +69,10 @@ a(href=`./block-height/${block.height}`) #{block.height.toLocaleString()} - - var timeAgoTime = moment.utc(new Date()).diff(moment.utc(new Date(parseInt(block.time) * 1000))); - - var timeAgo = moment.duration(timeAgoTime); + // This variable was called "timeAgoTime" but it's been renamed to avoid confusing it with the "timeAgoTime" variable + // used to pass the block time into time-ago-text.pug. + - var timeAgoTimeBlocksList = moment.utc(new Date()).diff(moment.utc(new Date(parseInt(block.time) * 1000))); + - var timeAgo = moment.duration(timeAgoTimeBlocksList); - var timeDiff = null; @@ -88,12 +90,17 @@ td.text-end - if (sort != "asc" && blockIndex == 0 && offset == 0 && timeAgoTime > (15 * 60 * 1000)) - - var timeAgoTime = block.time; - span.text-danger.border-dotted(title="It's been > 15 min since this latest block.", data-bs-toggle="tooltip") - include ./time-ago-text.pug + - var timeAgoTime = block.time; + if (sort != "asc" && blockIndex == 0 && offset == 0) + -var latestBlock = true; + if (autoRefresh == "off" && timeAgoTimeBlocksList > (15 * 60 * 1000)) + span.text-danger.border-dotted(title="It's been > 15 min since this latest block.", data-bs-toggle="tooltip") + include ./time-ago-text.pug + else + span(id="old-block-warning") + include ./time-ago-text.pug else - - var timeAgoTime = block.time; + -var latestBlock = false; include ./time-ago-text.pug td.text-end.d-none.d-sm-table-cell @@ -263,3 +270,121 @@ i.fas.fa-backspace.text-danger - var lastBlock = block; + +if (autoRefresh == "on") + script. + var allowTimeAgoUpdates = true; + // We don't have access to the moments library on the client side. It's easy enough to implement + // minutes/hours/days/weeks, but beyond that it gets complex. The initial string representations + // are generated on the server side by moments: if we see "mo" (months) or "y" (years) anywhere + // we disable updates. No one is likely to notice, and this seems better than letting a page + // gradually accumulate inconsistencies where times gradually increment up from "4w 3d" to "4w 4d" + // instead of "1mo 1d" or similar. + var timeAgoSpans = document.querySelectorAll('[id=timeAgo]'); + timeAgoSpans.forEach(item => { + if (item.textContent.includes("mo") || item.textContent.includes("y")) { + allowTimeAgoUpdates = false; + } + }); + + function updateTimeAgo() { + if (!allowTimeAgoUpdates) + return; + + var now = new Date(); + timeAgoSpans.forEach(item => { + var latestBlock = (item.getAttribute("data-latestblock") == "true"); + var timeAgoTime = new Date(parseInt(item.getAttribute("data-timeagotime")) * 1000); + var timeAgoRawSeconds = Math.floor((now - timeAgoTime) / 1000); + // We use >= here instead of > in the non-auto-refresh case because it means the age changes to 15m + // and turns red simultaneously, which I think looks neater. + if (latestBlock && timeAgoRawSeconds >= (15 * 60)) { + warningSpan = document.getElementById('old-block-warning'); + warningSpan.setAttribute("class", "text-danger border-dotted"); + warningSpan.setAttribute("title", "It's been > 15 min since this latest block."); + warningSpan.setAttribute("data-bs-toggle", "tooltip"); + } + + var timeAgoHours = Math.floor(timeAgoRawSeconds / 3600); + var timeAgoMinutes = Math.floor((timeAgoRawSeconds - timeAgoHours * 3600) / 60); + var timeAgoString; + if (timeAgoHours < 1) { + if (timeAgoMinutes < 1) + timeAgoString = `${timeAgoRawSeconds}s` + else + timeAgoString = `${timeAgoMinutes}m` + } else { + if (timeAgoHours < 24) { + timeAgoString = `${timeAgoHours}h` + if (timeAgoMinutes > 0) + timeAgoString += ` ${timeAgoMinutes}m`; + } else { + // We use "hr" instead of "h" here to match the behaviour of time-ago-text.pug. + timeAgoDays = Math.floor(timeAgoHours / 24); + timeAgoHours -= timeAgoDays * 24; + if (timeAgoDays < 7) { + timeAgoString = `${timeAgoDays}d`; + if (timeAgoHours > 0) + timeAgoString += ` ${timeAgoHours}hr`; + } else { + timeAgoWeeks = Math.floor(timeAgoDays / 7); + timeAgoDays -= timeAgoWeeks * 7; + timeAgoString = `${timeAgoWeeks}w` + if (timeAgoHours > 0) + timeAgoString += ` ${timeAgoDays}d ${timeAgoHours}hr` + else if (timeAgoDays > 0) + timeAgoString += ` ${timeAgoDays}d` + } + } + } + item.textContent = timeAgoString; + }); + + setTimeout(updateTimeAgo, 1000); + }; + + updateTimeAgo(); + + // Having currentBlockHeight defined here triggers polling of the current block height and a page refresh + // when it changes. This is used on the home page and on the block browser when it shows the most recent + // blocks. + if (periodicRefresh) + script. + function periodicRefresh() { + // Reload the page occasionally in order to force the network summary and predicted next block + // to refresh. + currentTime = new Date(); + if ((currentTime - pageLoadTime) >= 5 * 60 * 1000) { + window.location.reload(); + } + } + else + script. + function periodicRefresh() { + } + if (currentBlockHeight) + script. + var pageLoadTime = new Date(); + function checkRefresh() { + // H/T: https://learnwithparam.com/blog/how-to-handle-fetch-errors/ + fetch('/api/blocks/tip/height') + .then((response) => { + if (response.status >= 200 && response.status <= 299) { + return response.json(); + } else { + throw Error(response.statusText); + } + }) + .then((newBlockHeight) => { + // Refresh the page if the block height has changed. + if (newBlockHeight != !{currentBlockHeight}) { + window.location.reload(); + } + periodicRefresh(); + document.getElementById('connection-warning').style.display='none'; + }).catch((error) => { + document.getElementById('connection-warning').style.display='block'; + }); + setTimeout(checkRefresh, 10 * 1000); + }; + checkRefresh(); diff --git a/views/includes/connection-warning.pug b/views/includes/connection-warning.pug new file mode 100644 index 000000000..5c3182b95 --- /dev/null +++ b/views/includes/connection-warning.pug @@ -0,0 +1,4 @@ +div.alert.alert-warning(id="connection-warning", style="display:none") + div.fw-bold.mb-1 Unable to connect to node to check for new blocks... + + div.mb-1 Auto-refresh is on but this explorer is failing to connect to your node so new blocks cannot be detected. Retries will be attempted automatically. diff --git a/views/includes/time-ago-text.pug b/views/includes/time-ago-text.pug index 23091953a..52a4d1a52 100644 --- a/views/includes/time-ago-text.pug +++ b/views/includes/time-ago-text.pug @@ -2,16 +2,15 @@ if (timeAgo.asHours() < 1) if (timeAgo.asMinutes() < 1) - span #{timeAgo.seconds()}s + span(id="timeAgo", data-timeagotime=timeAgoTime.toString(), data-latestblock=latestBlock.toString()) #{timeAgo.seconds()}s else - span #{timeAgo.minutes()}m + span(id="timeAgo", data-timeagotime=timeAgoTime.toString(), data-latestblock=latestBlock.toString()) #{timeAgo.minutes()}m else if (timeAgo.asHours() >= 1 && timeAgo.asHours() < 24) - span #{timeAgo.hours()}h - if (timeAgo.minutes() > 0) - span #{timeAgo.minutes()}m - + span(id="timeAgo", data-timeagotime=timeAgoTime.toString(), data-latestblock=latestBlock.toString()) #{timeAgo.hours()}h #{timeAgo.minutes()}m + else + span(id="timeAgo", data-timeagotime=timeAgoTime.toString(), data-latestblock=latestBlock.toString()) #{timeAgo.hours()}h else - span #{utils.shortenTimeDiff(timeAgo.format()).split(", ").join(" ")} \ No newline at end of file + span(id="timeAgo", data-timeagotime=timeAgoTime.toString(), data-latestblock=latestBlock.toString()) #{utils.shortenTimeDiff(timeAgo.format()).split(", ").join(" ")} diff --git a/views/index.pug b/views/index.pug index 715485fe0..73a0bd219 100644 --- a/views/index.pug +++ b/views/index.pug @@ -68,6 +68,8 @@ block content span.fw-bold Progress: span #{new Decimal(getblockchaininfo.verificationprogress).times(100).toDP(3)}% + if (autoRefresh == "on") + include includes/connection-warning.pug include includes/index-network-summary.pug diff --git a/views/layout.pug b/views/layout.pug index 6d39b37fa..d35e08fa0 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -183,7 +183,7 @@ html(lang="en") i.fas.fa-moon hr.dropdown-divider.mt-2.mb-2 - span.dropdown-header Dislay Timezone + span.dropdown-header Display Timezone - var items = ["fa", "sat"]; if (config.queryExchangeRates && global.exchangeRates) @@ -197,6 +197,14 @@ html(lang="en") span#browser-tz-offset.badge.bg-light.text-dark.ms-1.border i.fas.fa-info-circle.ms-1(title="This value comes from your brower. If you wish to set a custom offset, you can do so below, in 'More settings...'", data-bs-toggle="tooltip") + hr.dropdown-divider.mt-2.mb-2 + span.dropdown-header Auto Refresh + - var items = ["off", "on"]; + + .btn-group.ms-3 + a.btn.btn-sm.px-3(href=`./changeSetting?name=autoRefresh&value=off`, class=(userSettings.autoRefresh == "off" ? "btn-primary" : "btn-outline-primary")) Off + a.btn.btn-sm.px-3(href=`./changeSetting?name=autoRefresh&value=on`, class=(userSettings.autoRefresh == "off" ? "btn-outline-primary" : "btn-primary")) On + hr.dropdown-divider.mt-2.mb-2 a.dropdown-item(href=`./user-settings`) More settings...