From 57ef07cfb6a19d889a8de9a82a68032499746bbb Mon Sep 17 00:00:00 2001 From: Shayan Eskandari Date: Fri, 30 May 2025 00:50:14 -0400 Subject: [PATCH 1/5] FAQ first draft, desktop notification beeps but no show yet, notification settings refactored --- app.js | 486 ++++++++++++++++++++++++++++++++++++++++++++--------- index.html | 210 ++++++++++++++++++++--- styles.css | 228 +++++++++++++++++++++++-- 3 files changed, 813 insertions(+), 111 deletions(-) diff --git a/app.js b/app.js index a0f5da9..dfcc3d7 100644 --- a/app.js +++ b/app.js @@ -163,6 +163,13 @@ class ValidatorDutiesTracker { document.getElementById('enableTelegramNotifications').addEventListener('click', () => this.enableTelegramNotifications()); document.getElementById('updateTelegramSubscription').addEventListener('click', () => this.updateTelegramSubscription()); + // Test notification button - will be created dynamically + document.addEventListener('click', (e) => { + if (e.target.id === 'testBrowserNotification') { + this.testBrowserNotification(); + } + }); + // Load and apply notification settings this.loadNotificationSettings(); } @@ -191,8 +198,8 @@ class ValidatorDutiesTracker { async enableBrowserNotifications() { try { - if (!('serviceWorker' in navigator) || !('PushManager' in window)) { - throw new Error('Push notifications not supported'); + if (!('Notification' in window)) { + throw new Error('Desktop notifications not supported in this browser'); } const permission = await Notification.requestPermission(); @@ -200,44 +207,137 @@ class ValidatorDutiesTracker { throw new Error('Notification permission denied'); } - // Register the service worker and wait for it to be ready - const registration = await navigator.serviceWorker.register('/sw.js'); + // Enable push notifications if available + if ('serviceWorker' in navigator && 'PushManager' in window) { + try { + // Register the service worker and wait for it to be ready + const registration = await navigator.serviceWorker.register('/sw.js'); + + // Wait for the service worker to be ready + await navigator.serviceWorker.ready; + + // Ensure we have VAPID key + if (this.vapidPublicKey) { + // Check if we already have a subscription + let subscription = await registration.pushManager.getSubscription(); + + if (!subscription) { + // Create a new subscription + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) + }); + } + + this.pushSubscription = subscription; + + await fetch(`${this.serverUrl}/api/notifications/subscribe`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + subscription, + validators: this.validators.map(v => v.id || v) + }) + }); + } + } catch (pushError) { + console.warn('Push notifications not available, using standard notifications:', pushError); + } + } - // Wait for the service worker to be ready - await navigator.serviceWorker.ready; + this.showNotificationStatus('browser', 'Desktop notifications enabled', true); + sessionStorage.setItem('browserNotifications', 'true'); - // Ensure we have VAPID key - if (!this.vapidPublicKey) { - throw new Error('VAPID public key not available. Please check server configuration.'); + // Show test button + const testBtn = document.getElementById('testBrowserNotification'); + if (testBtn) testBtn.style.display = 'inline-block'; + } catch (error) { + console.error('Browser notification error:', error); + this.showNotificationStatus('browser', error.message, false); + } + } + + async testBrowserNotification() { + try { + // First check if notifications are supported and permitted + if (!('Notification' in window)) { + this.showNotificationStatus('browser', 'Notifications not supported in this browser', false); + return; } - // Check if we already have a subscription - let subscription = await registration.pushManager.getSubscription(); - - if (!subscription) { - // Create a new subscription - subscription = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey) - }); + if (Notification.permission !== 'granted') { + this.showNotificationStatus('browser', 'Notification permission not granted. Please enable desktop notifications first.', false); + return; } - this.pushSubscription = subscription; + // Force page to lose focus to ensure notification shows + const tempLink = document.createElement('a'); + tempLink.href = '#'; + tempLink.style.position = 'absolute'; + tempLink.style.top = '-9999px'; + document.body.appendChild(tempLink); + tempLink.focus(); + + // Small delay to ensure focus change + await new Promise(resolve => setTimeout(resolve, 100)); + + const iconUrl = window.location.origin + '/favicon.ico'; + const options = { + body: 'Desktop notifications are working! You will receive alerts for your validator duties.', + icon: iconUrl, + badge: iconUrl, + tag: 'test-notification-' + Date.now(), // Unique tag + requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, + vibrate: [200, 100, 200], + timestamp: Date.now(), + silent: false, + renotify: true + }; - await fetch(`${this.serverUrl}/api/notifications/subscribe`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - subscription, - validators: this.validators.map(v => v.id || v) - }) - }); + console.log('Creating test notification with options:', options); - this.showNotificationStatus('browser', 'Browser notifications enabled', true); - sessionStorage.setItem('browserNotifications', 'true'); + const notification = new Notification('🎉 ETH Duties Test Notification', options); + + notification.onclick = () => { + window.focus(); + notification.close(); + document.body.removeChild(tempLink); + }; + + notification.onshow = () => { + console.log('Test notification shown successfully'); + this.showNotificationStatus('browser', 'Test notification sent! Check your desktop.', true); + setTimeout(() => document.body.removeChild(tempLink), 1000); + }; + + notification.onerror = (error) => { + console.error('Test notification error:', error); + this.showNotificationStatus('browser', 'Failed to show notification. Check browser settings.', false); + document.body.removeChild(tempLink); + }; + + // Play sound if enabled + if (document.getElementById('desktopSound')?.checked !== false) { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = 800; + gainNode.gain.value = 0.1; + + oscillator.start(); + oscillator.stop(audioContext.currentTime + 0.1); + } catch (soundError) { + console.error('Failed to play test sound:', soundError); + } + } } catch (error) { - console.error('Browser notification error:', error); - this.showNotificationStatus('browser', error.message, false); + console.error('Test notification error:', error); + this.showNotificationStatus('browser', `Error: ${error.message}`, false); } } @@ -765,26 +865,91 @@ class ValidatorDutiesTracker { console.log('Telegram notification not sent - conditions not met'); } - // Send browser notification - if (browserEnabled && this.pushSubscription) { - try { - await fetch(`${this.serverUrl}/api/notify`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: 'Block Confirmed', - validator: validator, - validatorDisplay, - duty: { - slot: slot, - timeUntil: 'confirmed', - blockDetails: details - }, - urgency: 'success' - }) - }); - } catch (error) { - console.error('Error sending browser block details notification:', error); + // Send desktop notification + if (browserEnabled) { + // Get validator label for display + const validatorLabel = this.getValidatorLabel(validator); + let displayName = validatorLabel; + if (proposerDuty && proposerDuty.pubkey) { + displayName = `${validatorLabel} (${proposerDuty.pubkey.slice(0, 10)}...)`; + } + + const title = '🎉💰 BLOCK CONFIRMED! 🎉💰'; + const body = `${displayName} - ${details.totalReward.toFixed(3)} ETH earned! (${details.txCount} txs)`; + + // Show desktop notification + if ('Notification' in window && Notification.permission === 'granted') { + try { + const iconUrl = window.location.origin + '/favicon.ico'; + const notification = new Notification(title, { + body: body, + icon: iconUrl, + badge: iconUrl, + tag: `block-${slot}`, + requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, + vibrate: [200, 100, 200, 100, 200], + timestamp: Date.now(), + silent: false, + renotify: true + }); + + notification.onclick = () => { + window.open(`https://beaconcha.in/slot/${slot}`, '_blank'); + notification.close(); + }; + + notification.onshow = () => { + console.log('Block confirmation desktop notification shown'); + }; + + // Play sound if enabled + if (document.getElementById('desktopSound')?.checked !== false) { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + // Celebration sound - ascending tones + oscillator.frequency.setValueAtTime(600, audioContext.currentTime); + oscillator.frequency.linearRampToValueAtTime(800, audioContext.currentTime + 0.1); + oscillator.frequency.linearRampToValueAtTime(1000, audioContext.currentTime + 0.2); + gainNode.gain.value = 0.1; + + oscillator.start(); + oscillator.stop(audioContext.currentTime + 0.3); + } catch (soundError) { + console.error('Failed to play block confirmation sound:', soundError); + } + } + } catch (notifError) { + console.error('Failed to create block notification:', notifError); + } + } + + // Also send push notification if available + if (this.pushSubscription) { + try { + await fetch(`${this.serverUrl}/api/notify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type: 'Block Confirmed', + validator: validator, + validatorDisplay, + duty: { + slot: slot, + timeUntil: 'confirmed', + blockDetails: details + }, + urgency: 'success' + }) + }); + } catch (error) { + console.error('Error sending push notification for block:', error); + } } } } @@ -915,33 +1080,132 @@ class ValidatorDutiesTracker { // Send browser notification if enabled const browserEnabled = sessionStorage.getItem('browserNotifications') === 'true'; - if (browserEnabled && this.pushSubscription) { + if (browserEnabled) { try { console.log('Sending browser notification for validator:', validator); - // Format validator display similar to Telegram - let validatorDisplay = validator; + // Format validator display + const validatorLabel = this.getValidatorLabel(validator); + let validatorDisplay = validatorLabel; if (duty.pubkey) { - validatorDisplay = `${validator} (${duty.pubkey.slice(0, 10)})`; + validatorDisplay = `${validatorLabel} (${duty.pubkey.slice(0, 10)}...)`; } - const response = await fetch(`${this.serverUrl}/api/notify`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type, - validator: validator, // Send the raw index, not the label - validatorDisplay, // Send the formatted display - duty: { - slot: duty.slot, - timeUntil: this.formatTimeUntil(timeUntil) - }, - urgency - }) - }); + // Create notification title and body + let title, body; + if (type === 'Proposer') { + title = '🎉💰 BLOCK PROPOSAL! 🎉💰'; + body = `Validator ${validatorDisplay} - ${this.formatTimeUntil(timeUntil)}`; + } else if (type === 'Attester') { + title = '📝 Attestation Duty'; + body = `Validator ${validatorDisplay} - ${this.formatTimeUntil(timeUntil)}`; + } else if (type === 'Sync Committee') { + title = '🔐💎 SYNC COMMITTEE 💎🔐'; + body = `Validator ${validatorDisplay} - Active now`; + } else if (type === 'Next Sync Committee') { + title = '🔮💎 NEXT SYNC COMMITTEE 💎🔮'; + body = `Validator ${validatorDisplay} - Starting ${this.formatTimeUntil(timeUntil)}`; + } else { + title = `${type} Duty`; + body = `Validator ${validatorDisplay} - ${this.formatTimeUntil(timeUntil)}`; + } - if (!response.ok) { - console.error('Browser notification response not ok:', await response.text()); + // Show desktop notification + if ('Notification' in window && Notification.permission === 'granted') { + console.log('Creating desktop notification:', { + title, + body, + isFocused: document.hasFocus(), + visibility: document.visibilityState + }); + + try { + // Use absolute URL for icon + const iconUrl = window.location.origin + '/favicon.ico'; + + // Create notification immediately + const showNotification = () => { + const notification = new Notification(title, { + body: body, + icon: iconUrl, + badge: iconUrl, + tag: dutyKey, + requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, + vibrate: [200, 100, 200], + timestamp: Date.now(), + silent: false, + renotify: true + }); + + notification.onclick = () => { + window.focus(); + notification.close(); + }; + + notification.onerror = (error) => { + console.error('Notification error:', error); + }; + + notification.onshow = () => { + console.log('Desktop notification shown successfully'); + }; + }; + + // Show immediately regardless of focus + showNotification(); + + } catch (notifError) { + console.error('Failed to create notification:', notifError); + } + + // Play sound if enabled + if (document.getElementById('desktopSound')?.checked !== false) { + try { + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + const gainNode = audioContext.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(audioContext.destination); + + oscillator.frequency.value = urgency === 'critical' ? 1000 : urgency === 'urgent' ? 900 : 800; + gainNode.gain.value = 0.1; + + oscillator.start(); + oscillator.stop(audioContext.currentTime + (urgency === 'critical' ? 0.3 : 0.1)); + + console.log('Notification sound played'); + } catch (soundError) { + console.error('Failed to play sound:', soundError); + } + } + } else { + console.log('Desktop notifications not available or not granted:', { + hasNotification: 'Notification' in window, + permission: Notification.permission + }); + } + + // Also send push notification if available + if (this.pushSubscription) { + const response = await fetch(`${this.serverUrl}/api/notify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + type, + validator: validator, + validatorDisplay, + duty: { + slot: duty.slot, + timeUntil: this.formatTimeUntil(timeUntil) + }, + urgency + }) + }); + + if (!response.ok) { + console.error('Push notification response not ok:', await response.text()); + } } } catch (error) { console.error('Browser notification error:', error); @@ -1410,7 +1674,12 @@ class ValidatorDutiesTracker { // Since we now always store indices, all validators should be indices let indexDisplay = ` ${displayLabel} - + `; let pubkeyPreview = `Loading...`; @@ -1553,8 +1822,11 @@ class ValidatorDutiesTracker { if (labelDisplay && labelInput) { labelDisplay.style.display = 'none'; labelInput.style.display = 'inline'; - labelInput.focus(); - labelInput.select(); + // Small delay to ensure the input is visible before focusing + setTimeout(() => { + labelInput.focus(); + labelInput.select(); + }, 10); } } } @@ -2386,6 +2658,10 @@ class ValidatorDutiesTracker { if (this.duties.sync.length === 0) { panel.innerHTML = ` +
+

Current/Next Sync Committee member

+

Sync Committee

+

No sync committee duties found for your validators

Sync committees are selected every ~27 hours

@@ -2395,7 +2671,7 @@ class ValidatorDutiesTracker { } const currentEpoch = Math.floor(this.getCurrentSlotSync() / 32); - const html = this.duties.sync.map(duty => { + const dutyHtml = this.duties.sync.map(duty => { let dutyInfo = ''; let timeDisplay = ''; @@ -2432,6 +2708,14 @@ class ValidatorDutiesTracker { `; }).join(''); + const html = ` +
+

Current/Next Sync Committee member

+

Sync Committee

+
+ ${dutyHtml} + `; + panel.innerHTML = html; } @@ -2724,7 +3008,9 @@ class ValidatorDutiesTracker { notifyAttester: sessionStorage.getItem('notifyAttester') !== 'false', notifySync: sessionStorage.getItem('notifySync') !== 'false', notifyMissed: sessionStorage.getItem('notifyMissed') === 'true', // Default to false - notifyMinutes: sessionStorage.getItem('notifyMinutes') || '10' + notifyMinutes: sessionStorage.getItem('notifyMinutes') || '10', + desktopSound: sessionStorage.getItem('desktopSound') !== 'false', + desktopPersistent: sessionStorage.getItem('desktopPersistent') !== 'false' }; // Apply settings to UI @@ -2734,6 +3020,18 @@ class ValidatorDutiesTracker { document.getElementById('notifyMissed').checked = settings.notifyMissed; document.getElementById('notifyMinutes').value = settings.notifyMinutes; + // Apply desktop settings if elements exist + if (document.getElementById('desktopSound')) { + document.getElementById('desktopSound').checked = settings.desktopSound; + document.getElementById('desktopPersistent').checked = settings.desktopPersistent; + } + + // Check if browser notifications are enabled and show settings + if (sessionStorage.getItem('browserNotifications') === 'true') { + const testBtn = document.getElementById('testBrowserNotification'); + if (testBtn) testBtn.style.display = 'inline-block'; + } + // Save settings on change ['notifyProposer', 'notifyAttester', 'notifySync'].forEach(id => { document.getElementById(id).addEventListener('change', (e) => { @@ -2742,12 +3040,48 @@ class ValidatorDutiesTracker { }); }); + // Save desktop settings on change + ['desktopSound', 'desktopPersistent'].forEach(id => { + if (document.getElementById(id)) { + document.getElementById(id).addEventListener('change', (e) => { + sessionStorage.setItem(id, e.target.checked); + }); + } + }); + // Don't add event listener for disabled notifyMissed checkbox document.getElementById('notifyMinutes').addEventListener('change', (e) => { sessionStorage.setItem('notifyMinutes', e.target.value); this.sendNotificationSettingsUpdate(); }); + + // Also add event listeners for Telegram settings + ['notifyProposerTelegram', 'notifyAttesterTelegram', 'notifySyncTelegram'].forEach(id => { + const elem = document.getElementById(id); + if (elem) { + elem.addEventListener('change', (e) => { + const baseId = id.replace('Telegram', ''); + sessionStorage.setItem(baseId, e.target.checked); + this.sendNotificationSettingsUpdate(); + }); + // Sync initial state with regular settings + const baseId = id.replace('Telegram', ''); + elem.checked = sessionStorage.getItem(baseId) !== 'false'; + } + }); + + const telegramMinutesElem = document.getElementById('notifyMinutesTelegram'); + if (telegramMinutesElem) { + telegramMinutesElem.addEventListener('change', (e) => { + sessionStorage.setItem('notifyMinutes', e.target.value); + // Sync both dropdowns + document.getElementById('notifyMinutes').value = e.target.value; + this.sendNotificationSettingsUpdate(); + }); + // Sync initial value + telegramMinutesElem.value = settings.notifyMinutes; + } } cacheDuties() { diff --git a/index.html b/index.html index 3f72154..ff7bd41 100644 --- a/index.html +++ b/index.html @@ -23,6 +23,7 @@

Ethereum Validator Duties Tracker

+ @@ -131,27 +132,83 @@

Beacon Node Configuration

Notifications

-
-
-

Browser Notifications

- -

-
- -
+
+ +

Telegram Notifications

Start a chat with @EthDuties_bot and send /start to get your chat ID
- +

-
-

Notification Settings

+
+

Desktop Notifications

+
+ + +
+
+
+ +
+
+ +
+
+

+
+ + +
+

Telegram Duty Settings

+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+

Desktop Duty Settings

+ + +
+
+

Frequently Asked Questions

+ +
+

What is ETH Duties Tracker?

+

ETH Duties Tracker is a web application that helps Ethereum validators monitor their upcoming duties including block proposals, attestations, and sync committee participation. It provides real-time notifications and a dashboard view of all validator activities.

+
+ +
+

How do I add my validators?

+

You can add validators by entering either their validator index (e.g., 123456) or public key (0x...) in the "Add Validators" section. You can also add multiple validators at once by separating them with commas, or import them from a JSON file.

+
+ +
+

What types of duties are tracked?

+

The tracker monitors three types of validator duties:

+
    +
  • Proposer Duties: When your validator is selected to propose a block
  • +
  • Attester Duties: Regular attestation duties that happen every epoch
  • +
  • Sync Committee: Special duties when your validator is part of the sync committee
  • +
+
+ +
+

How do notifications work?

+

ETH Duties Tracker supports two types of notifications:

+
    +
  • Desktop Notifications: Browser-based notifications that appear on your desktop when duties are approaching
  • +
  • Telegram Notifications: Get alerts sent directly to your Telegram messenger
  • +
+

You can configure when to receive notifications (5 minutes to 1 hour before duties) and which types of duties to be notified about.

+
+ +
+

How do I set up Telegram notifications?

+

1. Start a chat with @EthDuties_bot
+ 2. Send /start to get your chat ID
+ 3. Enter your chat ID in the Settings page
+ 4. Click "Enable Telegram Notifications"

+
+ +
+

What is Dashboard Mode?

+

Dashboard Mode provides a full-screen, real-time view of your validators, upcoming duties, network status, and recent blocks. It's perfect for displaying on a dedicated monitor or TV screen. Access it by clicking the "Dashboard Mode" button at the bottom of the page.

+
+ +
+

Can I use a public beacon node?

+

Yes! The app comes with several public beacon nodes pre-configured. You can select one from the dropdown in Settings, or enter your own beacon node URL if you're running a local node. Note that public nodes may have rate limits.

+
+ +
+

How do I customize validator labels?

+

Click on any validator's public key (the shortened address like "0x1234...abcd") to edit its label. You can give your validators meaningful names like "Main Validator" or "Node 1" to easily identify them.

+
+ +
+

Is my data stored securely?

+

All data is stored locally in your browser's session storage. No validator information is sent to external servers except for the beacon node queries and optional notification services. The data is cleared when you close your browser session.

+
+ +
+

What do the duty countdown colors mean?

+

The countdown timers use colors to indicate urgency:

+
    +
  • Red: Less than 1 minute (critical)
  • +
  • Orange: 1-5 minutes (urgent)
  • +
  • Blue: More than 5 minutes (normal)
  • +
+
+ +
+

Can I export/import my validator list?

+

Yes! Use the Export button to save your validator list as a JSON file, and Import to load validators from a previously exported file. This is useful for backing up your configuration or sharing it between devices.

+
+ +
+

Need more help?

+

For bug reports, feature requests, or contributions, visit the GitHub repository. You can also reach out on Twitter @sbetamc.

+
+
+
@@ -269,22 +411,40 @@

🌐 Network State

diff --git a/styles.css b/styles.css index 4eb47e7..0323a76 100644 --- a/styles.css +++ b/styles.css @@ -441,6 +441,8 @@ input[type="checkbox"] { font-size: 1rem; width: 80px; font-weight: 600; + position: relative; + z-index: 10; } .remove-validator-compact { @@ -752,6 +754,158 @@ footer a:hover { text-decoration: underline; } +.notification-grid-2x2 { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + gap: 20px; +} + +.grid-cell { + background: var(--bg-color); + padding: 20px; + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.grid-cell h3 { + font-size: 1.1rem; + margin-bottom: 15px; + color: var(--primary-color); +} + +.platform-controls { + display: flex; + gap: 10px; + margin-bottom: 15px; +} + +.desktop-options { + margin-bottom: 15px; + padding: 15px; + background: rgba(0, 0, 0, 0.03); + border-radius: 6px; +} + +.telegram-setup { + grid-column: 1; + grid-row: 1; +} + +.desktop-setup { + grid-column: 2; + grid-row: 1; +} + +.telegram-duties { + grid-column: 1; + grid-row: 2; +} + +.desktop-duties { + grid-column: 2; + grid-row: 2; +} + +@media (max-width: 768px) { + .notification-grid-2x2 { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto auto; + } + + .telegram-setup { + grid-column: 1; + grid-row: 1; + } + + .desktop-setup { + grid-column: 1; + grid-row: 2; + } + + .telegram-duties { + grid-column: 1; + grid-row: 3; + } + + .desktop-duties { + grid-column: 1; + grid-row: 4; + } +} + +/* Panel Header Styles */ +.panel-header { + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid var(--border-color); +} + +.panel-header h3 { + font-size: 1.2rem; + color: var(--primary-color); + margin-bottom: 5px; +} + +.panel-subtitle { + font-size: 0.9rem; + color: var(--text-secondary); + margin: 0; +} + +/* FAQ Styles */ +.faq-section { + background: var(--card-bg); + padding: 30px; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.faq-item { + margin-bottom: 25px; + padding-bottom: 25px; + border-bottom: 1px solid var(--border-color); +} + +.faq-item:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.faq-item h3 { + color: var(--primary-color); + font-size: 1.2rem; + margin-bottom: 10px; + font-weight: 600; +} + +.faq-item p { + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 10px; +} + +.faq-item ul { + margin-left: 20px; + margin-top: 10px; + color: var(--text-secondary); +} + +.faq-item ul li { + margin-bottom: 8px; + line-height: 1.6; +} + +.faq-item a { + color: var(--secondary-color); + text-decoration: none; +} + +.faq-item a:hover { + text-decoration: underline; +} + /* Modal Styles */ .modal { display: none; @@ -1453,22 +1607,74 @@ footer { } .footer-content { - display: flex; - justify-content: space-between; - align-items: center; max-width: 1200px; margin: 0 auto; - padding: 20px; + padding: 20px 20px 10px 20px; } .footer-info { - flex: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.footer-row { + width: 100%; + margin: 5px 0; } -.footer-controls { +.footer-row p { + margin: 0; +} + +.footer-social { display: flex; + justify-content: center; align-items: center; - gap: 15px; + gap: 30px; +} + +.social-item { + display: flex; + align-items: center; +} + +.social-icon { + color: var(--text-secondary); + transition: color 0.2s ease, transform 0.2s ease; + display: flex; + align-items: center; + gap: 8px; +} + +.social-icon:hover { + color: var(--secondary-color); + transform: translateY(-2px); + text-decoration: none; +} + +.social-icon span { + font-size: 0.9rem; +} + +.footer-version { + color: var(--text-secondary); + font-size: 0.9rem; + text-decoration: none; + transition: color 0.2s ease; +} + +.footer-version:hover { + color: var(--secondary-color); + text-decoration: underline; +} + +.footer-bottom { + padding: 10px 20px 20px 20px; + text-align: center; + border-top: 1px solid var(--border-color); + margin-top: 10px; } .dashboard-toggle { @@ -2099,9 +2305,11 @@ footer { } .footer-content { - flex-direction: column; - gap: 15px; - text-align: center; + padding: 15px 15px 10px 15px; + } + + .footer-bottom { + padding: 10px 15px 15px 15px; } .dashboard-mode .footer-content { From 9083d374445ae1485f41179d04202f9a39ee484b Mon Sep 17 00:00:00 2001 From: Shayan Eskandari Date: Fri, 30 May 2025 01:36:49 -0400 Subject: [PATCH 2/5] Add dark mode, fix the notification issue and texts --- app.js | 201 +++++++++++++++++++++++++--------------------- index.html | 10 +++ package-lock.json | 4 +- styles.css | 151 ++++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+), 92 deletions(-) diff --git a/app.js b/app.js index dfcc3d7..277fb5e 100644 --- a/app.js +++ b/app.js @@ -52,6 +52,7 @@ class ValidatorDutiesTracker { this.loadCachedDuties(); this.initializeNotifications(); this.initializeDashboard(); + this.initializeDarkMode(); this.startCountdownTimer(); } @@ -149,6 +150,9 @@ class ValidatorDutiesTracker { // Dashboard mode toggle document.getElementById('dashboardModeToggle').addEventListener('click', () => this.toggleDashboardMode()); + // Dark mode toggle + document.getElementById('darkModeToggle').addEventListener('click', () => this.toggleDarkMode()); + // Dashboard exit button document.getElementById('dashboardExitBtn').addEventListener('click', () => this.exitDashboardMode()); @@ -259,7 +263,6 @@ class ValidatorDutiesTracker { async testBrowserNotification() { try { - // First check if notifications are supported and permitted if (!('Notification' in window)) { this.showNotificationStatus('browser', 'Notifications not supported in this browser', false); return; @@ -270,50 +273,30 @@ class ValidatorDutiesTracker { return; } - // Force page to lose focus to ensure notification shows - const tempLink = document.createElement('a'); - tempLink.href = '#'; - tempLink.style.position = 'absolute'; - tempLink.style.top = '-9999px'; - document.body.appendChild(tempLink); - tempLink.focus(); - - // Small delay to ensure focus change - await new Promise(resolve => setTimeout(resolve, 100)); - const iconUrl = window.location.origin + '/favicon.ico'; - const options = { + const notification = new Notification('🎉 ETH Duties Test Notification', { body: 'Desktop notifications are working! You will receive alerts for your validator duties.', icon: iconUrl, badge: iconUrl, - tag: 'test-notification-' + Date.now(), // Unique tag + tag: 'test-notification-' + Date.now(), requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, vibrate: [200, 100, 200], - timestamp: Date.now(), silent: false, renotify: true - }; - - console.log('Creating test notification with options:', options); - - const notification = new Notification('🎉 ETH Duties Test Notification', options); + }); notification.onclick = () => { window.focus(); notification.close(); - document.body.removeChild(tempLink); }; notification.onshow = () => { - console.log('Test notification shown successfully'); this.showNotificationStatus('browser', 'Test notification sent! Check your desktop.', true); - setTimeout(() => document.body.removeChild(tempLink), 1000); }; notification.onerror = (error) => { console.error('Test notification error:', error); this.showNotificationStatus('browser', 'Failed to show notification. Check browser settings.', false); - document.body.removeChild(tempLink); }; // Play sound if enabled @@ -326,8 +309,14 @@ class ValidatorDutiesTracker { oscillator.connect(gainNode); gainNode.connect(audioContext.destination); - oscillator.frequency.value = 800; - gainNode.gain.value = 0.1; + // Retro test sound - short blip + oscillator.type = 'square'; + oscillator.frequency.value = 392; // G4 note + + // Subtle volume with envelope + gainNode.gain.setValueAtTime(0, audioContext.currentTime); + gainNode.gain.linearRampToValueAtTime(0.03, audioContext.currentTime + 0.01); + gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.1); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.1); @@ -867,20 +856,22 @@ class ValidatorDutiesTracker { // Send desktop notification if (browserEnabled) { - // Get validator label for display + // Get validator label for display (without pubkey for concise desktop notifications) const validatorLabel = this.getValidatorLabel(validator); - let displayName = validatorLabel; - if (proposerDuty && proposerDuty.pubkey) { - displayName = `${validatorLabel} (${proposerDuty.pubkey.slice(0, 10)}...)`; - } const title = '🎉💰 BLOCK CONFIRMED! 🎉💰'; - const body = `${displayName} - ${details.totalReward.toFixed(3)} ETH earned! (${details.txCount} txs)`; + let body = `Validator: ${validatorLabel}\nSlot: ${slot}\n\n${details.totalReward.toFixed(3)} ETH earned!\n${details.txCount} transactions`; + + // Add graffiti if available + if (details.graffiti && details.graffiti.trim()) { + body += `\n\nGraffiti: "${details.graffiti.trim()}"`; + } // Show desktop notification if ('Notification' in window && Notification.permission === 'granted') { try { const iconUrl = window.location.origin + '/favicon.ico'; + const notification = new Notification(title, { body: body, icon: iconUrl, @@ -888,7 +879,6 @@ class ValidatorDutiesTracker { tag: `block-${slot}`, requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, vibrate: [200, 100, 200, 100, 200], - timestamp: Date.now(), silent: false, renotify: true }); @@ -898,8 +888,8 @@ class ValidatorDutiesTracker { notification.close(); }; - notification.onshow = () => { - console.log('Block confirmation desktop notification shown'); + notification.onerror = (error) => { + console.error('Block notification error:', error); }; // Play sound if enabled @@ -912,11 +902,16 @@ class ValidatorDutiesTracker { oscillator.connect(gainNode); gainNode.connect(audioContext.destination); - // Celebration sound - ascending tones - oscillator.frequency.setValueAtTime(600, audioContext.currentTime); - oscillator.frequency.linearRampToValueAtTime(800, audioContext.currentTime + 0.1); - oscillator.frequency.linearRampToValueAtTime(1000, audioContext.currentTime + 0.2); - gainNode.gain.value = 0.1; + // Retro celebration sound - 8-bit style ascending tones + oscillator.type = 'square'; // 8-bit style waveform + oscillator.frequency.setValueAtTime(440, audioContext.currentTime); + oscillator.frequency.exponentialRampToValueAtTime(659.25, audioContext.currentTime + 0.15); + oscillator.frequency.exponentialRampToValueAtTime(880, audioContext.currentTime + 0.25); + + // Subtle volume with envelope + gainNode.gain.setValueAtTime(0, audioContext.currentTime); + gainNode.gain.linearRampToValueAtTime(0.05, audioContext.currentTime + 0.01); + gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.3); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.3); @@ -1084,75 +1079,55 @@ class ValidatorDutiesTracker { try { console.log('Sending browser notification for validator:', validator); - // Format validator display + // Format validator display (without pubkey for concise desktop notifications) const validatorLabel = this.getValidatorLabel(validator); - let validatorDisplay = validatorLabel; - if (duty.pubkey) { - validatorDisplay = `${validatorLabel} (${duty.pubkey.slice(0, 10)}...)`; - } - // Create notification title and body + // Create notification title and body using Telegram format but without pubkey let title, body; if (type === 'Proposer') { title = '🎉💰 BLOCK PROPOSAL! 🎉💰'; - body = `Validator ${validatorDisplay} - ${this.formatTimeUntil(timeUntil)}`; + body = `In ${minutesUntil} minute${minutesUntil === 1 ? '' : 's'}\n\nValidator: ${validatorLabel}\nSlot: ${duty.slot}`; } else if (type === 'Attester') { title = '📝 Attestation Duty'; - body = `Validator ${validatorDisplay} - ${this.formatTimeUntil(timeUntil)}`; + body = `In ${minutesUntil} minute${minutesUntil === 1 ? '' : 's'}\n\nValidator: ${validatorLabel}\nSlot: ${duty.slot}`; } else if (type === 'Sync Committee') { title = '🔐💎 SYNC COMMITTEE 💎🔐'; - body = `Validator ${validatorDisplay} - Active now`; + body = `Validator: ${validatorLabel}\n${duty.period === 'current' ? 'Currently active' : 'Starting soon'}\n~27 hours of enhanced rewards`; } else if (type === 'Next Sync Committee') { title = '🔮💎 NEXT SYNC COMMITTEE 💎🔮'; - body = `Validator ${validatorDisplay} - Starting ${this.formatTimeUntil(timeUntil)}`; + body = `Starting in ${minutesUntil} minute${minutesUntil === 1 ? '' : 's'}\n\nValidator: ${validatorLabel}\n~27 hours of enhanced rewards ahead!`; } else { title = `${type} Duty`; - body = `Validator ${validatorDisplay} - ${this.formatTimeUntil(timeUntil)}`; + body = `In ${minutesUntil} minute${minutesUntil === 1 ? '' : 's'}\n\nValidator: ${validatorLabel}\nSlot: ${duty.slot}`; } // Show desktop notification if ('Notification' in window && Notification.permission === 'granted') { - console.log('Creating desktop notification:', { - title, - body, - isFocused: document.hasFocus(), - visibility: document.visibilityState - }); - try { - // Use absolute URL for icon const iconUrl = window.location.origin + '/favicon.ico'; - // Create notification immediately - const showNotification = () => { - const notification = new Notification(title, { - body: body, - icon: iconUrl, - badge: iconUrl, - tag: dutyKey, - requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, - vibrate: [200, 100, 200], - timestamp: Date.now(), - silent: false, - renotify: true - }); - - notification.onclick = () => { - window.focus(); - notification.close(); - }; - - notification.onerror = (error) => { - console.error('Notification error:', error); - }; - - notification.onshow = () => { - console.log('Desktop notification shown successfully'); - }; + const notification = new Notification(title, { + body: body, + icon: iconUrl, + badge: iconUrl, + tag: dutyKey, + requireInteraction: document.getElementById('desktopPersistent')?.checked !== false, + vibrate: [200, 100, 200], + silent: false, + renotify: true + }); + + notification.onclick = () => { + window.focus(); + notification.close(); + if (validator) { + window.open(`https://beaconcha.in/validator/${validator}`, '_blank'); + } }; - // Show immediately regardless of focus - showNotification(); + notification.onerror = (error) => { + console.error('Notification error:', error); + }; } catch (notifError) { console.error('Failed to create notification:', notifError); @@ -1168,11 +1143,18 @@ class ValidatorDutiesTracker { oscillator.connect(gainNode); gainNode.connect(audioContext.destination); - oscillator.frequency.value = urgency === 'critical' ? 1000 : urgency === 'urgent' ? 900 : 800; - gainNode.gain.value = 0.1; + // Retro duty notification sound - different pitched square waves + oscillator.type = 'square'; + const baseFreq = urgency === 'critical' ? 523.25 : urgency === 'urgent' ? 440 : 349.23; // C5, A4, F4 + oscillator.frequency.value = baseFreq; + + // Subtle volume with envelope + gainNode.gain.setValueAtTime(0, audioContext.currentTime); + gainNode.gain.linearRampToValueAtTime(0.03, audioContext.currentTime + 0.01); + gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + (urgency === 'critical' ? 0.25 : 0.15)); oscillator.start(); - oscillator.stop(audioContext.currentTime + (urgency === 'critical' ? 0.3 : 0.1)); + oscillator.stop(audioContext.currentTime + (urgency === 'critical' ? 0.25 : 0.15)); console.log('Notification sound played'); } catch (soundError) { @@ -3521,6 +3503,24 @@ class ValidatorDutiesTracker { } } + initializeDarkMode() { + // Load saved theme preference or default to light + const savedTheme = sessionStorage.getItem('darkMode') || 'light'; + document.documentElement.setAttribute('data-theme', savedTheme); + + // Update toggle button icons + const sunIcon = document.querySelector('#darkModeToggle .sun-icon'); + const moonIcon = document.querySelector('#darkModeToggle .moon-icon'); + + if (savedTheme === 'dark') { + sunIcon.classList.add('hidden'); + moonIcon.classList.remove('hidden'); + } else { + sunIcon.classList.remove('hidden'); + moonIcon.classList.add('hidden'); + } + } + toggleDashboardMode() { this.isDashboardMode = !this.isDashboardMode; sessionStorage.setItem('dashboardMode', this.isDashboardMode.toString()); @@ -3581,6 +3581,27 @@ class ValidatorDutiesTracker { this.switchToNormal(); } + toggleDarkMode() { + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + const newTheme = isDark ? 'light' : 'dark'; + + // Update theme + document.documentElement.setAttribute('data-theme', newTheme); + sessionStorage.setItem('darkMode', newTheme); + + // Update toggle button icons + const sunIcon = document.querySelector('#darkModeToggle .sun-icon'); + const moonIcon = document.querySelector('#darkModeToggle .moon-icon'); + + if (newTheme === 'dark') { + sunIcon.classList.add('hidden'); + moonIcon.classList.remove('hidden'); + } else { + sunIcon.classList.remove('hidden'); + moonIcon.classList.add('hidden'); + } + } + startDashboardUpdates() { if (this.dashboardUpdateInterval) { clearInterval(this.dashboardUpdateInterval); diff --git a/index.html b/index.html index ff7bd41..49cb3c8 100644 --- a/index.html +++ b/index.html @@ -426,6 +426,16 @@

🌐 Network State

sbetamc +
+ +
diff --git a/package.json b/package.json index 306ea82..04907fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eth-duties-tracker", - "version": "1.2.1", + "version": "1.2.5", "description": "Ethereum Validator Duties Tracker with notifications", "main": "server.js", "scripts": { From 5b46b56bcaaf24c7b6f54f1684bf8fb318178817 Mon Sep 17 00:00:00 2001 From: Shayan Eskandari Date: Fri, 30 May 2025 02:02:22 -0400 Subject: [PATCH 5/5] minor fix --- app.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index ac7331e..505b5ea 100644 --- a/app.js +++ b/app.js @@ -2679,11 +2679,11 @@ class ValidatorDutiesTracker { ${label}
- Sync Committee + ${dutyInfo} ${timeDisplay}
-
${dutyInfo}
+
Sync Committee