littleshop/LittleShop/wwwroot/js/signalr-notifications.js
SysAdmin be91b3efd7 Add: SignalR real-time notifications for admin panel
- Created NotificationHub for instant browser notifications
- Updated CryptoPaymentService to broadcast via SignalR
- Added JavaScript client with toast notifications
- Works with custom SSL certificates (no FCM dependency)
- Automatic reconnection with exponential backoff
- Notification sound and visual indicators
- Bypasses all Web Push SSL certificate issues
2025-10-06 17:57:10 +01:00

207 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SignalR Real-Time Notifications for LittleShop Admin
// Connects to NotificationHub and displays toast notifications
class SignalRNotificationManager {
constructor() {
this.connection = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.init();
}
async init() {
console.log('SignalR Notifications: Initializing...');
try {
// Create SignalR connection
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/notificationHub")
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
// Exponential backoff: 0s, 2s, 10s, 30s, then 30s
if (retryContext.elapsedMilliseconds < 60000) {
return Math.min(1000 * Math.pow(2, retryContext.previousRetryCount), 30000);
}
return 30000;
}
})
.configureLogging(signalR.LogLevel.Information)
.build();
// Set up event handlers
this.setupEventHandlers();
// Start connection
await this.start();
} catch (error) {
console.error('SignalR Notifications: Failed to initialize:', error);
}
}
setupEventHandlers() {
// Handle incoming notifications
this.connection.on("ReceiveNotification", (notification) => {
console.log('SignalR Notifications: Received:', notification);
this.showNotification(notification);
});
// Connection lifecycle events
this.connection.onclose((error) => {
this.isConnected = false;
console.log('SignalR Notifications: Connection closed', error);
this.updateConnectionStatus('disconnected');
});
this.connection.onreconnecting((error) => {
console.log('SignalR Notifications: Reconnecting...', error);
this.updateConnectionStatus('reconnecting');
});
this.connection.onreconnected((connectionId) => {
this.isConnected = true;
console.log('SignalR Notifications: Reconnected', connectionId);
this.updateConnectionStatus('connected');
this.reconnectAttempts = 0;
});
}
async start() {
try {
await this.connection.start();
this.isConnected = true;
console.log('SignalR Notifications: Connected successfully');
this.updateConnectionStatus('connected');
} catch (error) {
console.error('SignalR Notifications: Failed to connect:', error);
this.updateConnectionStatus('disconnected');
// Retry connection
setTimeout(() => this.start(), 5000);
}
}
showNotification(notification) {
// Create toast notification
const toast = document.createElement('div');
toast.className = `alert alert-info alert-dismissible position-fixed`;
toast.style.cssText = `
top: 80px;
right: 20px;
z-index: 9999;
min-width: 350px;
max-width: 500px;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
animation: slideInRight 0.3s ease-out;
`;
// Notification icon based on type
const iconMap = {
'payment': '💰',
'order': '📦',
'info': '',
'warning': '⚠️',
'success': '✅',
'error': '❌'
};
const icon = notification.icon || iconMap[notification.type] || '🔔';
const url = notification.url || '#';
toast.innerHTML = `
<div class="d-flex align-items-start">
<div class="fs-2 me-3">${icon}</div>
<div class="flex-grow-1">
<h6 class="alert-heading mb-1">${notification.title}</h6>
<p class="mb-2">${notification.message}</p>
${url !== '#' ? `
<a href="${url}" class="btn btn-sm btn-primary">View Order</a>
` : ''}
<small class="text-muted d-block mt-1">${this.formatTimestamp(notification.timestamp)}</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.body.appendChild(toast);
// Play notification sound
this.playNotificationSound();
// Auto-remove after 10 seconds
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 10000);
// Update notification badge if it exists
this.updateNotificationBadge();
}
formatTimestamp(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = Math.floor((now - date) / 1000); // seconds
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return date.toLocaleString();
}
playNotificationSound() {
try {
// Simple beep sound (you can replace with a custom sound file)
const audio = new Audio('data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmwhBShzw+7bjT8JElyv5e2nVhYQTKXi0adkHAYnfcHr4JBNDA9PotTtqV4bCkSa5O/IbCcMKHTH9N+ROwgYarfs5o5MDQ5Pm+Xzr2YeCCF5wOnlkTkKG2a16+2iUxoOSKLe8bp4Kgk0fMnw24k6EBxmsuvsoFMYDk+m4PO2eCQMOXjE7t2OPhkZaK7n7aJbGw1Fn+Hxu3ooDzZ8yO/ciD8PG2+36OybUhoPSpzg8bhwKQo2e8bs3Y5CDQxVqerurF4bCkGc5PG9eywNNHnH9N+PQQ8bZ7Tr7aVVGQ5Fn+Hxtn0qDTZ6x+zelkAMEF216+6kVxwLQqHk8rhvKQs4esr13o9CDhxntujuoFYYDUad4vK9fS0OOX7K9N6OQw0QWK7n7qNYGw5Ip+LxvYAuETl+zPTfkUUNElyy6e6mWBsNRp/j8biCKg88fs/z4I9ADQxbsOXupVYaDkSg4vK3gzULPIHP8N6PTg0NXKvn7aRUFwxNoeXxt4QuCzh+zu/djkAPDVip5+2kVhgMSp/i8bZ8LQo5fc7w3Y5ADAxaqOftpVYZDUmg4/K4gSwKOn3O8N2OQAwMWqjn7aVWGQ1Joezi8biALgo6fc7w3Y5ADA1aqejtpFYYDUmf4/K4gSwJOn7O8N2PQA0MWqro7aRVGg1Jn+Pyu4UuCjp+zfDdjkAPDVqp6O2lVxoOSaHk8rqDLgo4fc3w3I1ADAxZqejto1caDUqh5PG5gjAJPn/N8d6MQQ0NWqrn7aNWGQxKn+Txt4EqCjl+zPHdjkAMDFuq6O2kVhgMTJ/i8rZ8KQo4fc3w3Y5ADA1bq+jtpFYYDU2f4vK2fCkKOH3N8N2OQAwLWqrn7aRWGQ1JoOPyuIArCjl+zPHcj0EPDVqr5+2kVhkLSZ/i8rZ7LQs5fs3w3I1ADAxdqufupFYYDUie4/K4gCwLOX3M8N2OQAwLXKvn7aRWGg1IoOPyuH8rCTl+zfDdjj8MDFuo6O2kVhkLSZ/i8rZ7LQo5fc3w3I1ADAxcq+jtpFYaDUie4/K5gCsJOX7M8d6OQAwLW6ro7aRVGQ1IoOPyt4ArCTl+zfHejT8MDFup6O2jVxkNSaDi8rd9Kgo4fs/w3Y4/DAxcqejvo1YaDUqg4vK3fSoKOH7P8NyNPwsLW6nn7aRXGQ1Jn+LytXwuCzh9zvDcjkAKC1yr5+2jVxkNSJ/i8rZ+Kgo5fs3w3I4/CwxbqefupFcZDEme4vK3fCoJOX7N8N2PQAsMXKnn7aRXGQ1IoOLytX4pCTh+zfDbjj8LC1qq6O2kVxkNSJ/i8rV+Kgo5fs3w3I4/DAtcqefto1caDEie4vK2fioJOX3N8N2OQAsMXKro7aNXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV+Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kwo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kwo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8MC1yo5+2kVxkNSJ/i8rV9Kgo5fs3w3Y4/DAtcqefto1cZDUie4vK1fioJOH7N8N2OQAwLXKnn7aRXGQ1InuLytXwqCTh+zfDdjj8=');
audio.volume = 0.3;
audio.play().catch(e => console.log('Cannot play sound:', e));
} catch (error) {
console.log('Notification sound error:', error);
}
}
updateConnectionStatus(status) {
// Update UI status indicator if exists
const statusIndicator = document.getElementById('signalr-connection-status');
if (statusIndicator) {
statusIndicator.className = `badge bg-${status === 'connected' ? 'success' : status === 'reconnecting' ? 'warning' : 'danger'}`;
statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
console.log('SignalR Notifications: Status:', status);
}
updateNotificationBadge() {
const badge = document.getElementById('notification-badge');
if (badge) {
const currentCount = parseInt(badge.textContent) || 0;
badge.textContent = currentCount + 1;
badge.style.display = 'inline';
}
}
async sendTestNotification() {
if (!this.isConnected) {
console.error('SignalR Notifications: Not connected');
return;
}
try {
await this.connection.invoke("SendTestNotification");
console.log('SignalR Notifications: Test notification requested');
} catch (error) {
console.error('SignalR Notifications: Failed to send test notification:', error);
}
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
window.signalRNotifications = new SignalRNotificationManager();
});
// Export for global access
window.SignalRNotificationManager = SignalRNotificationManager;