mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00

This commit introduces significant updates to the application, focusing on currency support and improved configuration management. Key changes include: - Added a `config_reset` flag in `update_metrics_job` in `App.py` to manage configuration resets. - Modified `update_config` to handle currency changes and update notifications accordingly. - Updated `MiningDashboardService` to fetch and include exchange rates and configured currency in metrics. - Introduced utility functions for currency formatting and symbol retrieval in `notification_service.py`. - Enhanced UI components in `main.js` and `boot.html` to support currency selection and display. - Adjusted Docker Compose file to remove hardcoded wallet and power settings for better flexibility. These changes enhance usability by allowing users to view financial data in their preferred currency and manage configurations more effectively.
1116 lines
35 KiB
JavaScript
1116 lines
35 KiB
JavaScript
/**
|
|
* BitcoinMinuteRefresh.js - Simplified Bitcoin-themed floating uptime monitor
|
|
*
|
|
* This module creates a Bitcoin-themed terminal that shows server uptime.
|
|
* Now includes DeepSea theme support.
|
|
*/
|
|
|
|
const BitcoinMinuteRefresh = (function () {
|
|
// Constants
|
|
const STORAGE_KEY = 'bitcoin_last_refresh_time';
|
|
// Default fallback colors if CSS vars aren't available
|
|
const FALLBACK_BITCOIN_COLOR = '#f2a900';
|
|
const FALLBACK_DEEPSEA_COLOR = '#0088cc';
|
|
|
|
const DOM_IDS = {
|
|
TERMINAL: 'bitcoin-terminal',
|
|
STYLES: 'bitcoin-terminal-styles',
|
|
CLOCK: 'terminal-clock',
|
|
UPTIME_HOURS: 'uptime-hours',
|
|
UPTIME_MINUTES: 'uptime-minutes',
|
|
UPTIME_SECONDS: 'uptime-seconds',
|
|
MINIMIZED_UPTIME: 'minimized-uptime-value',
|
|
SHOW_BUTTON: 'bitcoin-terminal-show'
|
|
};
|
|
const STORAGE_KEYS = {
|
|
THEME: 'useDeepSeaTheme',
|
|
COLLAPSED: 'bitcoin_terminal_collapsed',
|
|
SERVER_OFFSET: 'serverTimeOffset',
|
|
SERVER_START: 'serverStartTime',
|
|
REFRESH_EVENT: 'bitcoin_refresh_event'
|
|
};
|
|
const SELECTORS = {
|
|
HEADER: '.terminal-header',
|
|
TITLE: '.terminal-title',
|
|
TIMER: '.uptime-timer',
|
|
SEPARATORS: '.uptime-separator',
|
|
UPTIME_TITLE: '.uptime-title',
|
|
MINI_LABEL: '.mini-uptime-label',
|
|
TERMINAL_DOT: '.terminal-dot'
|
|
};
|
|
|
|
// Private variables
|
|
let terminalElement = null;
|
|
let uptimeElement = null;
|
|
let serverTimeOffset = 0;
|
|
let serverStartTime = null;
|
|
let uptimeInterval = null;
|
|
let isInitialized = false;
|
|
let refreshCallback = null;
|
|
let currentThemeColor = '';
|
|
let currentThemeRGB = '';
|
|
let dragListenersAdded = false;
|
|
|
|
/**
|
|
* Get theme colors from CSS variables
|
|
*/
|
|
function getThemeColors() {
|
|
// Try to get CSS variables from document root
|
|
const rootStyles = getComputedStyle(document.documentElement);
|
|
let primaryColor = rootStyles.getPropertyValue('--primary-color').trim();
|
|
let primaryColorRGB = rootStyles.getPropertyValue('--primary-color-rgb').trim();
|
|
|
|
// If CSS vars not available, use theme toggle state
|
|
if (!primaryColor) {
|
|
const isDeepSea = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
|
|
primaryColor = isDeepSea ? FALLBACK_DEEPSEA_COLOR : FALLBACK_BITCOIN_COLOR;
|
|
primaryColorRGB = isDeepSea ? '0, 136, 204' : '242, 169, 0';
|
|
}
|
|
|
|
return {
|
|
color: primaryColor,
|
|
rgb: primaryColorRGB
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Logging helper function
|
|
* @param {string} message - Message to log
|
|
* @param {string} level - Log level (log, warn, error)
|
|
*/
|
|
function log(message, level = 'log') {
|
|
const prefix = "BitcoinMinuteRefresh: ";
|
|
if (level === 'error') {
|
|
console.error(prefix + message);
|
|
} else if (level === 'warn') {
|
|
console.warn(prefix + message);
|
|
} else {
|
|
console.log(prefix + message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to set multiple styles on an element
|
|
* @param {Element} element - The DOM element to style
|
|
* @param {Object} styles - Object with style properties
|
|
*/
|
|
function applyStyles(element, styles) {
|
|
Object.keys(styles).forEach(key => {
|
|
element.style[key] = styles[key];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Apply the current theme color
|
|
*/
|
|
function applyThemeColor() {
|
|
// Get current theme colors
|
|
const theme = getThemeColors();
|
|
currentThemeColor = theme.color;
|
|
currentThemeRGB = theme.rgb;
|
|
|
|
// Don't try to update DOM elements if they don't exist yet
|
|
if (!terminalElement) return;
|
|
|
|
// Create theme config
|
|
const themeConfig = {
|
|
color: currentThemeColor,
|
|
borderColor: currentThemeColor,
|
|
boxShadow: `0 0 5px rgba(${currentThemeRGB}, 0.3)`,
|
|
textShadow: `0 0 5px rgba(${currentThemeRGB}, 0.8)`,
|
|
borderColorRGBA: `rgba(${currentThemeRGB}, 0.5)`,
|
|
textShadowStrong: `0 0 8px rgba(${currentThemeRGB}, 0.8)`
|
|
};
|
|
|
|
// Apply styles to terminal
|
|
applyStyles(terminalElement, {
|
|
borderColor: themeConfig.color,
|
|
color: themeConfig.color,
|
|
boxShadow: themeConfig.boxShadow
|
|
});
|
|
|
|
// Update header border
|
|
const headerElement = terminalElement.querySelector(SELECTORS.HEADER);
|
|
if (headerElement) {
|
|
headerElement.style.borderColor = themeConfig.color;
|
|
}
|
|
|
|
// Update terminal title
|
|
const titleElement = terminalElement.querySelector(SELECTORS.TITLE);
|
|
if (titleElement) {
|
|
applyStyles(titleElement, {
|
|
color: themeConfig.color,
|
|
textShadow: themeConfig.textShadow
|
|
});
|
|
}
|
|
|
|
// Update uptime timer border
|
|
const uptimeTimer = terminalElement.querySelector(SELECTORS.TIMER);
|
|
if (uptimeTimer) {
|
|
uptimeTimer.style.borderColor = themeConfig.borderColorRGBA;
|
|
}
|
|
|
|
// Update uptime separators
|
|
const separators = terminalElement.querySelectorAll(SELECTORS.SEPARATORS);
|
|
separators.forEach(sep => {
|
|
sep.style.textShadow = themeConfig.textShadowStrong;
|
|
});
|
|
|
|
// Update uptime title
|
|
const uptimeTitle = terminalElement.querySelector(SELECTORS.UPTIME_TITLE);
|
|
if (uptimeTitle) {
|
|
uptimeTitle.style.textShadow = themeConfig.textShadow;
|
|
}
|
|
|
|
// Update minimized view
|
|
const miniLabel = terminalElement.querySelector(SELECTORS.MINI_LABEL);
|
|
if (miniLabel) {
|
|
miniLabel.style.color = themeConfig.color;
|
|
}
|
|
|
|
// Update show button if it exists
|
|
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON);
|
|
if (showButton) {
|
|
showButton.style.backgroundColor = themeConfig.color;
|
|
showButton.style.boxShadow = `0 0 10px rgba(${currentThemeRGB}, 0.5)`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listen for theme changes
|
|
*/
|
|
function setupThemeChangeListener() {
|
|
// Listen for theme change events from localStorage
|
|
window.addEventListener('storage', function (e) {
|
|
if (e.key === STORAGE_KEYS.THEME) {
|
|
applyThemeColor();
|
|
}
|
|
});
|
|
|
|
// Listen for custom theme change events
|
|
document.addEventListener('themeChanged', function () {
|
|
applyThemeColor();
|
|
});
|
|
|
|
// Watch for class changes on HTML element that might indicate theme changes
|
|
const observer = new MutationObserver(function (mutations) {
|
|
mutations.forEach(function (mutation) {
|
|
if (mutation.attributeName === 'class') {
|
|
applyThemeColor();
|
|
}
|
|
});
|
|
});
|
|
|
|
observer.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ['class']
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Debounce function to limit execution frequency
|
|
*/
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function (...args) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Add dragging functionality to the terminal
|
|
*/
|
|
function addDraggingBehavior() {
|
|
// Find the terminal element
|
|
const terminal = document.getElementById(DOM_IDS.TERMINAL) ||
|
|
document.querySelector('.bitcoin-terminal') ||
|
|
document.getElementById('retro-terminal-bar');
|
|
|
|
if (!terminal) {
|
|
log('Terminal element not found for drag behavior', 'warn');
|
|
return;
|
|
}
|
|
|
|
let isDragging = false;
|
|
let startX = 0;
|
|
let startLeft = 0;
|
|
|
|
// Function to handle mouse down (drag start)
|
|
function handleMouseDown(e) {
|
|
// Only enable dragging in desktop view
|
|
if (window.innerWidth < 768) return;
|
|
|
|
// Don't handle drag if clicking on controls
|
|
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
|
|
|
|
isDragging = true;
|
|
terminal.classList.add('dragging');
|
|
|
|
// Calculate start position
|
|
startX = e.clientX;
|
|
|
|
// Get current left position accounting for different possible styles
|
|
const style = window.getComputedStyle(terminal);
|
|
if (style.left !== 'auto') {
|
|
startLeft = parseInt(style.left) || 0;
|
|
} else {
|
|
// Calculate from right if left is not set
|
|
startLeft = window.innerWidth -
|
|
(parseInt(style.right) || 0) -
|
|
terminal.offsetWidth;
|
|
}
|
|
|
|
e.preventDefault(); // Prevent text selection
|
|
}
|
|
|
|
// Function to handle mouse move (dragging) with debounce for better performance
|
|
const handleMouseMove = debounce(function (e) {
|
|
if (!isDragging) return;
|
|
|
|
// Calculate the horizontal movement - vertical stays fixed
|
|
const deltaX = e.clientX - startX;
|
|
let newLeft = startLeft + deltaX;
|
|
|
|
// Constrain to window boundaries
|
|
const maxLeft = window.innerWidth - terminal.offsetWidth;
|
|
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
|
|
|
// Update position - only horizontally along bottom
|
|
terminal.style.left = newLeft + 'px';
|
|
terminal.style.right = 'auto'; // Remove right positioning
|
|
terminal.style.transform = 'none'; // Remove transformations
|
|
}, 10);
|
|
|
|
// Function to handle mouse up (drag end)
|
|
function handleMouseUp() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
terminal.classList.remove('dragging');
|
|
}
|
|
}
|
|
|
|
// Find the terminal header for dragging
|
|
const terminalHeader = terminal.querySelector(SELECTORS.HEADER);
|
|
if (terminalHeader) {
|
|
terminalHeader.addEventListener('mousedown', handleMouseDown);
|
|
} else {
|
|
// If no header found, make the whole terminal draggable
|
|
terminal.addEventListener('mousedown', handleMouseDown);
|
|
}
|
|
|
|
// Add touch support for mobile/tablet
|
|
function handleTouchStart(e) {
|
|
if (window.innerWidth < 768) return;
|
|
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
|
|
|
|
const touch = e.touches[0];
|
|
isDragging = true;
|
|
terminal.classList.add('dragging');
|
|
|
|
startX = touch.clientX;
|
|
|
|
const style = window.getComputedStyle(terminal);
|
|
if (style.left !== 'auto') {
|
|
startLeft = parseInt(style.left) || 0;
|
|
} else {
|
|
startLeft = window.innerWidth - (parseInt(style.right) || 0) - terminal.offsetWidth;
|
|
}
|
|
|
|
e.preventDefault();
|
|
}
|
|
|
|
function handleTouchMove(e) {
|
|
if (!isDragging) return;
|
|
|
|
const touch = e.touches[0];
|
|
const deltaX = touch.clientX - startX;
|
|
let newLeft = startLeft + deltaX;
|
|
|
|
const maxLeft = window.innerWidth - terminal.offsetWidth;
|
|
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
|
|
|
|
terminal.style.left = newLeft + 'px';
|
|
terminal.style.right = 'auto';
|
|
terminal.style.transform = 'none';
|
|
|
|
e.preventDefault();
|
|
}
|
|
|
|
function handleTouchEnd() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
terminal.classList.remove('dragging');
|
|
}
|
|
}
|
|
|
|
if (terminalHeader) {
|
|
terminalHeader.addEventListener('touchstart', handleTouchStart);
|
|
} else {
|
|
terminal.addEventListener('touchstart', handleTouchStart);
|
|
}
|
|
|
|
// Add event listeners only once to prevent memory leaks
|
|
if (!dragListenersAdded) {
|
|
// Add mousemove and mouseup listeners to document
|
|
document.addEventListener('mousemove', handleMouseMove);
|
|
document.addEventListener('mouseup', handleMouseUp);
|
|
|
|
// Add touch event listeners
|
|
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
document.addEventListener('touchend', handleTouchEnd);
|
|
|
|
// Handle window resize to keep terminal visible
|
|
window.addEventListener('resize', function () {
|
|
if (window.innerWidth < 768) {
|
|
// Reset position for mobile view
|
|
terminal.style.left = '50%';
|
|
terminal.style.right = 'auto';
|
|
terminal.style.transform = 'translateX(-50%)';
|
|
} else {
|
|
// Ensure terminal stays visible in desktop view
|
|
const maxLeft = window.innerWidth - terminal.offsetWidth;
|
|
const currentLeft = parseInt(window.getComputedStyle(terminal).left) || 0;
|
|
|
|
if (currentLeft > maxLeft) {
|
|
terminal.style.left = maxLeft + 'px';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Mark listeners as added
|
|
dragListenersAdded = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create and inject the retro terminal element into the DOM
|
|
*/
|
|
function createTerminalElement() {
|
|
// Container element
|
|
terminalElement = document.createElement('div');
|
|
terminalElement.id = DOM_IDS.TERMINAL;
|
|
terminalElement.className = 'bitcoin-terminal';
|
|
|
|
// Terminal content - simplified for uptime-only
|
|
terminalElement.innerHTML = `
|
|
<div class="terminal-header">
|
|
<div class="terminal-title">SYSTEM MONITOR v.3</div>
|
|
<div class="terminal-controls">
|
|
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()">
|
|
<span class="control-symbol">-</span>
|
|
</div>
|
|
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()">
|
|
<span class="control-symbol">x</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="terminal-content">
|
|
<div class="status-row">
|
|
<div class="status-indicator">
|
|
<div class="status-dot connected"></div>
|
|
<span>LIVE</span>
|
|
</div>
|
|
<span id="${DOM_IDS.CLOCK}" class="terminal-clock">00:00:00</span>
|
|
</div>
|
|
<div id="uptime-timer" class="uptime-timer">
|
|
<div class="uptime-title">UPTIME</div>
|
|
<div class="uptime-display">
|
|
<div class="uptime-value">
|
|
<span id="${DOM_IDS.UPTIME_HOURS}" class="uptime-number">00</span>
|
|
<span class="uptime-label">H</span>
|
|
</div>
|
|
<div class="uptime-separator">:</div>
|
|
<div class="uptime-value">
|
|
<span id="${DOM_IDS.UPTIME_MINUTES}" class="uptime-number">00</span>
|
|
<span class="uptime-label">M</span>
|
|
</div>
|
|
<div class="uptime-separator">:</div>
|
|
<div class="uptime-value">
|
|
<span id="${DOM_IDS.UPTIME_SECONDS}" class="uptime-number">00</span>
|
|
<span class="uptime-label">S</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="terminal-minimized">
|
|
<div class="minimized-uptime">
|
|
<span class="mini-uptime-label">UPTIME</span>
|
|
<span id="${DOM_IDS.MINIMIZED_UPTIME}">00:00:00</span>
|
|
</div>
|
|
<div class="minimized-status-dot connected"></div>
|
|
</div>
|
|
`;
|
|
|
|
// Append to body
|
|
document.body.appendChild(terminalElement);
|
|
|
|
// Add dragging behavior
|
|
addDraggingBehavior();
|
|
|
|
// Cache element references
|
|
uptimeElement = document.getElementById('uptime-timer');
|
|
|
|
// Check if terminal was previously collapsed
|
|
if (localStorage.getItem(STORAGE_KEYS.COLLAPSED) === 'true') {
|
|
terminalElement.classList.add('collapsed');
|
|
}
|
|
|
|
// Add custom styles if not already present
|
|
if (!document.getElementById(DOM_IDS.STYLES)) {
|
|
addStyles();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add CSS styles for the terminal
|
|
*/
|
|
function addStyles() {
|
|
// Get current theme colors for initial styling
|
|
const theme = getThemeColors();
|
|
|
|
const styleElement = document.createElement('style');
|
|
styleElement.id = DOM_IDS.STYLES;
|
|
|
|
styleElement.textContent = `
|
|
/* Terminal Container */
|
|
.bitcoin-terminal {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
width: 230px;
|
|
background-color: #000000;
|
|
border: 1px solid var(--primary-color, ${theme.color});
|
|
color: var(--primary-color, ${theme.color});
|
|
font-family: 'VT323', monospace;
|
|
z-index: 9999;
|
|
overflow: hidden;
|
|
padding: 8px;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 0 5px rgba(var(--primary-color-rgb, ${theme.rgb}), 0.3);
|
|
}
|
|
|
|
/* Terminal Header */
|
|
.terminal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
border-bottom: 1px solid var(--primary-color, ${theme.color});
|
|
padding-bottom: 5px;
|
|
margin-bottom: 8px;
|
|
cursor: grab;
|
|
}
|
|
|
|
.terminal-header:active,
|
|
.bitcoin-terminal.dragging .terminal-header {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.terminal-title {
|
|
color: var(--primary-color, ${theme.color});
|
|
font-weight: bold;
|
|
font-size: 1.1rem;
|
|
animation: terminal-flicker 4s infinite;
|
|
}
|
|
|
|
/* Control Dots */
|
|
.terminal-controls {
|
|
display: flex;
|
|
gap: 5px;
|
|
margin-left: 5px;
|
|
}
|
|
|
|
.terminal-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background-color: #555;
|
|
cursor: pointer;
|
|
transition: background-color 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
|
|
.control-symbol {
|
|
color: #333;
|
|
font-size: 9px;
|
|
font-weight: bold;
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
line-height: 1;
|
|
}
|
|
|
|
.terminal-dot.minimize:hover {
|
|
background-color: #ffcc00;
|
|
}
|
|
|
|
.terminal-dot.minimize:hover .control-symbol {
|
|
color: #664e00;
|
|
}
|
|
|
|
.terminal-dot.close:hover {
|
|
background-color: #ff3b30;
|
|
}
|
|
|
|
.terminal-dot.close:hover .control-symbol {
|
|
color: #7a0200;
|
|
}
|
|
|
|
/* Terminal Content */
|
|
.terminal-content {
|
|
position: relative;
|
|
}
|
|
|
|
/* Status Row */
|
|
.status-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
/* Status Indicator */
|
|
.status-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.status-dot.connected {
|
|
background-color: #32CD32;
|
|
box-shadow: 0 0 5px #32CD32;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.terminal-clock {
|
|
font-size: 1rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Uptime Display */
|
|
.uptime-timer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 5px;
|
|
background-color: #111;
|
|
border: 1px solid rgba(var(--primary-color-rgb, ${theme.rgb}), 0.5);
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.uptime-display {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 2px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.uptime-value {
|
|
display: flex;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.uptime-number {
|
|
font-size: 1.4rem;
|
|
font-weight: bold;
|
|
background-color: #000;
|
|
padding: 2px 5px;
|
|
border-radius: 3px;
|
|
min-width: 32px;
|
|
display: inline-block;
|
|
text-align: center;
|
|
letter-spacing: 2px;
|
|
color: #dee2e6;
|
|
}
|
|
|
|
.uptime-label {
|
|
font-size: 0.7rem;
|
|
opacity: 0.7;
|
|
margin-left: 2px;
|
|
}
|
|
|
|
.uptime-separator {
|
|
font-size: 1.4rem;
|
|
font-weight: bold;
|
|
padding: 0 2px;
|
|
}
|
|
|
|
.uptime-title {
|
|
font-size: 0.7rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
letter-spacing: 2px;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
/* Show button */
|
|
#${DOM_IDS.SHOW_BUTTON} {
|
|
position: fixed;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
background-color: var(--primary-color, ${theme.color});
|
|
color: #000;
|
|
border: none;
|
|
padding: 8px 12px;
|
|
font-family: 'VT323', monospace;
|
|
cursor: pointer;
|
|
z-index: 9999;
|
|
display: none;
|
|
box-shadow: 0 0 10px rgba(var(--primary-color-rgb, ${theme.rgb}), 0.5);
|
|
}
|
|
|
|
/* CRT scanline effect */
|
|
.terminal-content::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
rgba(0, 0, 0, 0.15),
|
|
rgba(0, 0, 0, 0.15) 1px,
|
|
transparent 1px,
|
|
transparent 2px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Minimized view styling */
|
|
.terminal-minimized {
|
|
display: none;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 4px 10px;
|
|
background-color: #000;
|
|
position: relative;
|
|
}
|
|
|
|
.terminal-minimized::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
rgba(0, 0, 0, 0.15),
|
|
rgba(0, 0, 0, 0.15) 1px,
|
|
transparent 1px,
|
|
transparent 2px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 1;
|
|
}
|
|
|
|
.minimized-uptime {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
.mini-uptime-label {
|
|
font-size: 0.6rem;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
opacity: 0.7;
|
|
margin-left: 45px;
|
|
color: var(--primary-color, ${theme.color});
|
|
}
|
|
|
|
#${DOM_IDS.MINIMIZED_UPTIME} {
|
|
font-size: 0.9rem;
|
|
font-weight: bold;
|
|
margin-left: 45px;
|
|
color: #dee2e6;
|
|
}
|
|
|
|
.minimized-status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
margin-left: 10px;
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Collapsed state */
|
|
.bitcoin-terminal.collapsed {
|
|
width: auto;
|
|
max-width: 500px;
|
|
height: auto;
|
|
padding: 5px;
|
|
}
|
|
|
|
.bitcoin-terminal.collapsed .terminal-content {
|
|
display: none;
|
|
}
|
|
|
|
.bitcoin-terminal.collapsed .terminal-minimized {
|
|
display: flex;
|
|
}
|
|
|
|
.bitcoin-terminal.collapsed .terminal-header {
|
|
border-bottom: none;
|
|
margin-bottom: 2px;
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
/* Animations */
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.8; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
@keyframes terminal-flicker {
|
|
0% { opacity: 0.97; }
|
|
5% { opacity: 0.95; }
|
|
10% { opacity: 0.97; }
|
|
15% { opacity: 0.94; }
|
|
20% { opacity: 0.98; }
|
|
50% { opacity: 0.95; }
|
|
80% { opacity: 0.96; }
|
|
90% { opacity: 0.94; }
|
|
100% { opacity: 0.98; }
|
|
}
|
|
|
|
/* Media Queries */
|
|
@media (max-width: 768px) {
|
|
.bitcoin-terminal {
|
|
left: 50%;
|
|
right: auto;
|
|
transform: translateX(-50%);
|
|
width: 90%;
|
|
max-width: 320px;
|
|
bottom: 10px;
|
|
}
|
|
|
|
.bitcoin-terminal.collapsed {
|
|
width: auto;
|
|
max-width: 300px;
|
|
left: 50%;
|
|
right: auto;
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(styleElement);
|
|
}
|
|
|
|
/**
|
|
* Update the terminal clock
|
|
*/
|
|
function updateClock() {
|
|
try {
|
|
const now = new Date(Date.now() + (serverTimeOffset || 0));
|
|
// Use the global timezone setting if available
|
|
const timeZone = window.dashboardTimezone || 'America/Los_Angeles';
|
|
|
|
// Format the time in the configured timezone
|
|
const timeString = now.toLocaleTimeString('en-US', {
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true,
|
|
timeZone: timeZone
|
|
});
|
|
|
|
// Update clock in normal view
|
|
const clockElement = document.getElementById(DOM_IDS.CLOCK);
|
|
if (clockElement) {
|
|
clockElement.textContent = timeString;
|
|
}
|
|
} catch (e) {
|
|
log("Error updating clock: " + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the uptime display
|
|
*/
|
|
function updateUptime() {
|
|
if (serverStartTime) {
|
|
try {
|
|
const currentServerTime = Date.now() + serverTimeOffset;
|
|
const diff = currentServerTime - serverStartTime;
|
|
|
|
// Calculate hours, minutes, seconds
|
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
|
|
// Format numbers with leading zeros
|
|
const formattedTime = {
|
|
hours: String(hours).padStart(2, '0'),
|
|
minutes: String(minutes).padStart(2, '0'),
|
|
seconds: String(seconds).padStart(2, '0')
|
|
};
|
|
|
|
// Update the main uptime display with digital clock style
|
|
const elements = {
|
|
hours: document.getElementById(DOM_IDS.UPTIME_HOURS),
|
|
minutes: document.getElementById(DOM_IDS.UPTIME_MINUTES),
|
|
seconds: document.getElementById(DOM_IDS.UPTIME_SECONDS),
|
|
minimized: document.getElementById(DOM_IDS.MINIMIZED_UPTIME)
|
|
};
|
|
|
|
// Update each element if it exists
|
|
if (elements.hours) elements.hours.textContent = formattedTime.hours;
|
|
if (elements.minutes) elements.minutes.textContent = formattedTime.minutes;
|
|
if (elements.seconds) elements.seconds.textContent = formattedTime.seconds;
|
|
|
|
// Update the minimized uptime display
|
|
if (elements.minimized) {
|
|
elements.minimized.textContent = `${formattedTime.hours}:${formattedTime.minutes}:${formattedTime.seconds}`;
|
|
}
|
|
} catch (e) {
|
|
log("Error updating uptime: " + e.message, 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start animation frame loop for smooth updates
|
|
*/
|
|
function startAnimationLoop() {
|
|
let lastUpdate = 0;
|
|
const updateInterval = 1000; // Update every second
|
|
|
|
function animationFrame(timestamp) {
|
|
// Only update once per second to save resources
|
|
if (timestamp - lastUpdate >= updateInterval) {
|
|
updateClock();
|
|
updateUptime();
|
|
lastUpdate = timestamp;
|
|
}
|
|
|
|
// Continue the animation loop
|
|
requestAnimationFrame(animationFrame);
|
|
}
|
|
|
|
// Start the loop
|
|
requestAnimationFrame(animationFrame);
|
|
}
|
|
|
|
/**
|
|
* Notify other tabs that data has been refreshed
|
|
*/
|
|
function notifyRefresh() {
|
|
const now = Date.now();
|
|
localStorage.setItem(STORAGE_KEY, now.toString());
|
|
localStorage.setItem(STORAGE_KEYS.REFRESH_EVENT, 'refresh-' + now);
|
|
log("Notified other tabs of refresh at " + new Date(now).toISOString());
|
|
}
|
|
|
|
/**
|
|
* Initialize the uptime monitor
|
|
*/
|
|
function initialize(refreshFunc) {
|
|
// Store the refresh callback
|
|
refreshCallback = refreshFunc;
|
|
|
|
// Create the terminal element if it doesn't exist
|
|
if (!document.getElementById(DOM_IDS.TERMINAL)) {
|
|
createTerminalElement();
|
|
} else {
|
|
// Get references to existing elements
|
|
terminalElement = document.getElementById(DOM_IDS.TERMINAL);
|
|
uptimeElement = document.getElementById('uptime-timer');
|
|
}
|
|
|
|
// Apply theme colors
|
|
applyThemeColor();
|
|
|
|
// Set up listener for theme changes
|
|
setupThemeChangeListener();
|
|
|
|
// Try to get stored server time information
|
|
try {
|
|
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0');
|
|
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0');
|
|
} catch (e) {
|
|
log("Error reading server time from localStorage: " + e.message, 'error');
|
|
}
|
|
|
|
// Clear any existing intervals
|
|
if (uptimeInterval) {
|
|
clearInterval(uptimeInterval);
|
|
}
|
|
|
|
// Use requestAnimationFrame for smoother animations
|
|
startAnimationLoop();
|
|
|
|
// Listen for storage events to sync across tabs
|
|
window.removeEventListener('storage', handleStorageChange);
|
|
window.addEventListener('storage', handleStorageChange);
|
|
|
|
// Handle visibility changes
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
|
|
// Mark as initialized
|
|
isInitialized = true;
|
|
|
|
log("Initialized");
|
|
}
|
|
|
|
/**
|
|
* Handle storage changes for cross-tab synchronization
|
|
*/
|
|
function handleStorageChange(event) {
|
|
if (event.key === STORAGE_KEYS.REFRESH_EVENT) {
|
|
log("Detected refresh from another tab");
|
|
|
|
// If another tab refreshed, consider refreshing this one too
|
|
// But don't refresh if it was just refreshed recently (5 seconds)
|
|
const lastRefreshTime = parseInt(localStorage.getItem(STORAGE_KEY) || '0');
|
|
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
|
|
refreshCallback();
|
|
}
|
|
} else if (event.key === STORAGE_KEYS.SERVER_OFFSET || event.key === STORAGE_KEYS.SERVER_START) {
|
|
try {
|
|
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0');
|
|
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0');
|
|
} catch (e) {
|
|
log("Error reading updated server time: " + e.message, 'error');
|
|
}
|
|
} else if (event.key === STORAGE_KEYS.THEME) {
|
|
// Update theme when theme preference changes
|
|
applyThemeColor();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle visibility changes
|
|
*/
|
|
function handleVisibilityChange() {
|
|
if (!document.hidden) {
|
|
log("Page became visible, updating");
|
|
|
|
// Apply current theme when page becomes visible
|
|
applyThemeColor();
|
|
|
|
// Update immediately when page becomes visible
|
|
updateClock();
|
|
updateUptime();
|
|
|
|
// Check if we need to do a refresh based on time elapsed
|
|
if (typeof refreshCallback === 'function') {
|
|
const lastRefreshTime = parseInt(localStorage.getItem(STORAGE_KEY) || '0');
|
|
if (Date.now() - lastRefreshTime > 60000) { // More than a minute since last refresh
|
|
refreshCallback();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update server time information
|
|
*/
|
|
function updateServerTime(timeOffset, startTime) {
|
|
serverTimeOffset = timeOffset;
|
|
serverStartTime = startTime;
|
|
|
|
// Store in localStorage for cross-page sharing
|
|
localStorage.setItem(STORAGE_KEYS.SERVER_OFFSET, serverTimeOffset.toString());
|
|
localStorage.setItem(STORAGE_KEYS.SERVER_START, serverStartTime.toString());
|
|
|
|
// Update the uptime immediately
|
|
updateUptime();
|
|
|
|
log("Server time updated - offset: " + serverTimeOffset + " ms");
|
|
}
|
|
|
|
/**
|
|
* Toggle terminal collapsed state
|
|
*/
|
|
function toggleTerminal() {
|
|
if (!terminalElement) return;
|
|
|
|
terminalElement.classList.toggle('collapsed');
|
|
localStorage.setItem(STORAGE_KEYS.COLLAPSED, terminalElement.classList.contains('collapsed'));
|
|
}
|
|
|
|
/**
|
|
* Hide the terminal and show the restore button
|
|
*/
|
|
function hideTerminal() {
|
|
if (!terminalElement) return;
|
|
|
|
terminalElement.style.display = 'none';
|
|
|
|
// Create show button if it doesn't exist
|
|
if (!document.getElementById(DOM_IDS.SHOW_BUTTON)) {
|
|
const showButton = document.createElement('button');
|
|
showButton.id = DOM_IDS.SHOW_BUTTON;
|
|
showButton.textContent = 'Show Monitor';
|
|
showButton.onclick = showTerminal;
|
|
document.body.appendChild(showButton);
|
|
|
|
// Apply current theme to the button
|
|
const theme = getThemeColors();
|
|
showButton.style.backgroundColor = theme.color;
|
|
showButton.style.boxShadow = `0 0 10px rgba(${theme.rgb}, 0.5)`;
|
|
}
|
|
|
|
document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block';
|
|
}
|
|
|
|
/**
|
|
* Show the terminal and hide the restore button
|
|
*/
|
|
function showTerminal() {
|
|
if (!terminalElement) return;
|
|
|
|
terminalElement.style.display = 'block';
|
|
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON);
|
|
if (showButton) {
|
|
showButton.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Public API
|
|
return {
|
|
initialize: initialize,
|
|
notifyRefresh: notifyRefresh,
|
|
updateServerTime: updateServerTime,
|
|
toggleTerminal: toggleTerminal,
|
|
hideTerminal: hideTerminal,
|
|
showTerminal: showTerminal,
|
|
updateTheme: applyThemeColor
|
|
};
|
|
})();
|
|
|
|
// Auto-initialize when document is ready
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Check if manualRefresh function exists in global scope
|
|
if (typeof window.manualRefresh === 'function') {
|
|
BitcoinMinuteRefresh.initialize(window.manualRefresh);
|
|
} else {
|
|
console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually");
|
|
}
|
|
|
|
// Update theme based on current setting
|
|
setTimeout(() => BitcoinMinuteRefresh.updateTheme(), 100);
|
|
});
|