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

Updated the application to use a configurable timezone instead of hardcoding "America/Los_Angeles". This change impacts the dashboard, API endpoints, and worker services. Timezone is now fetched from a configuration file or environment variable, enhancing flexibility in time display. New API endpoints for available timezones and the current configured timezone have been added. The frontend now allows users to select their timezone from a dropdown menu, which is stored in local storage for future use. Timestamps in the UI have been updated to reflect the selected timezone.
503 lines
16 KiB
JavaScript
503 lines
16 KiB
JavaScript
"use strict";
|
|
|
|
// Global variables
|
|
let currentFilter = "all";
|
|
let currentOffset = 0;
|
|
const pageSize = 20;
|
|
let hasMoreNotifications = true;
|
|
let isLoading = false;
|
|
|
|
// Timezone configuration
|
|
let dashboardTimezone = 'America/Los_Angeles'; // Default
|
|
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
|
|
|
|
// Initialize when document is ready
|
|
$(document).ready(() => {
|
|
console.log("Notification page initializing...");
|
|
|
|
// Fetch timezone configuration
|
|
fetchTimezoneConfig();
|
|
|
|
// Set up filter buttons
|
|
$('.filter-button').click(function () {
|
|
$('.filter-button').removeClass('active');
|
|
$(this).addClass('active');
|
|
currentFilter = $(this).data('filter');
|
|
resetAndLoadNotifications();
|
|
});
|
|
|
|
// Set up action buttons
|
|
$('#mark-all-read').click(markAllAsRead);
|
|
$('#clear-read').click(clearReadNotifications);
|
|
$('#clear-all').click(clearAllNotifications);
|
|
$('#load-more').click(loadMoreNotifications);
|
|
|
|
// Initial load of notifications
|
|
loadNotifications();
|
|
|
|
// Start polling for unread count
|
|
startUnreadCountPolling();
|
|
|
|
// Initialize BitcoinMinuteRefresh if available
|
|
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
|
|
BitcoinMinuteRefresh.initialize(refreshNotifications);
|
|
console.log("BitcoinMinuteRefresh initialized with refresh function");
|
|
}
|
|
|
|
// Start periodic update of notification timestamps every 30 seconds
|
|
setInterval(updateNotificationTimestamps, 30000);
|
|
});
|
|
|
|
// Fetch timezone configuration from server
|
|
function fetchTimezoneConfig() {
|
|
return fetch('/api/timezone')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data && data.timezone) {
|
|
dashboardTimezone = data.timezone;
|
|
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
|
|
console.log(`Notifications page using timezone: ${dashboardTimezone}`);
|
|
|
|
// Store in localStorage for future use
|
|
try {
|
|
localStorage.setItem('dashboardTimezone', dashboardTimezone);
|
|
} catch (e) {
|
|
console.error("Error storing timezone in localStorage:", e);
|
|
}
|
|
|
|
// Update all timestamps with the new timezone
|
|
updateNotificationTimestamps();
|
|
return dashboardTimezone;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching timezone config:', error);
|
|
return null;
|
|
});
|
|
}
|
|
|
|
// Load notifications with current filter
|
|
function loadNotifications() {
|
|
if (isLoading) return;
|
|
|
|
isLoading = true;
|
|
showLoading();
|
|
|
|
const params = {
|
|
limit: pageSize,
|
|
offset: currentOffset
|
|
};
|
|
|
|
if (currentFilter !== "all") {
|
|
params.category = currentFilter;
|
|
}
|
|
|
|
$.ajax({
|
|
url: `/api/notifications?${$.param(params)}`,
|
|
method: "GET",
|
|
dataType: "json",
|
|
success: (data) => {
|
|
renderNotifications(data.notifications, currentOffset === 0);
|
|
updateUnreadBadge(data.unread_count);
|
|
|
|
// Update load more button state
|
|
hasMoreNotifications = data.notifications.length === pageSize;
|
|
$('#load-more').prop('disabled', !hasMoreNotifications);
|
|
|
|
isLoading = false;
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error loading notifications:", error);
|
|
showError("Failed to load notifications. Please try again.");
|
|
isLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Reset offset and load notifications
|
|
function resetAndLoadNotifications() {
|
|
currentOffset = 0;
|
|
loadNotifications();
|
|
}
|
|
|
|
// Load more notifications
|
|
function loadMoreNotifications() {
|
|
if (!hasMoreNotifications || isLoading) return;
|
|
|
|
currentOffset += pageSize;
|
|
loadNotifications();
|
|
}
|
|
|
|
// Refresh notifications (for periodic updates)
|
|
function refreshNotifications() {
|
|
// Only refresh if we're on the first page
|
|
if (currentOffset === 0) {
|
|
resetAndLoadNotifications();
|
|
} else {
|
|
// Just update the unread count
|
|
updateUnreadCount();
|
|
}
|
|
}
|
|
|
|
// This refreshes all timestamps on the page periodically
|
|
function updateNotificationTimestamps() {
|
|
$('.notification-item').each(function () {
|
|
const timestampStr = $(this).attr('data-timestamp');
|
|
if (timestampStr) {
|
|
try {
|
|
const timestamp = new Date(timestampStr);
|
|
|
|
// Update relative time
|
|
$(this).find('.notification-time').text(formatTimestamp(timestamp));
|
|
|
|
// Update full timestamp with configured timezone
|
|
if ($(this).find('.full-timestamp').length) {
|
|
const options = {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true,
|
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
|
};
|
|
|
|
const fullTimestamp = timestamp.toLocaleString('en-US', options);
|
|
$(this).find('.full-timestamp').text(fullTimestamp);
|
|
}
|
|
} catch (e) {
|
|
console.error("Error updating timestamp:", e, timestampStr);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Show loading indicator
|
|
function showLoading() {
|
|
if (currentOffset === 0) {
|
|
// First page load, show loading message
|
|
$('#notifications-container').html('<div class="loading-message">Loading notifications<span class="terminal-cursor"></span></div>');
|
|
} else {
|
|
// Pagination load, show loading below
|
|
$('#load-more').prop('disabled', true).text('Loading...');
|
|
}
|
|
}
|
|
|
|
// Show error message
|
|
function showError(message) {
|
|
$('#notifications-container').html(`<div class="error-message">${message}</div>`);
|
|
$('#load-more').hide();
|
|
}
|
|
|
|
// Render notifications in the container
|
|
function renderNotifications(notifications, isFirstPage) {
|
|
const container = $('#notifications-container');
|
|
|
|
// If first page and no notifications
|
|
if (isFirstPage && (!notifications || notifications.length === 0)) {
|
|
container.html($('#empty-template').html());
|
|
$('#load-more').hide();
|
|
return;
|
|
}
|
|
|
|
// If first page, clear container
|
|
if (isFirstPage) {
|
|
container.empty();
|
|
}
|
|
|
|
// Render each notification
|
|
notifications.forEach(notification => {
|
|
const notificationElement = createNotificationElement(notification);
|
|
container.append(notificationElement);
|
|
});
|
|
|
|
// Show/hide load more button
|
|
$('#load-more').show().prop('disabled', !hasMoreNotifications);
|
|
}
|
|
|
|
// Create notification element from template
|
|
function createNotificationElement(notification) {
|
|
const template = $('#notification-template').html();
|
|
const element = $(template);
|
|
|
|
// Set data attributes
|
|
element.attr('data-id', notification.id)
|
|
.attr('data-level', notification.level)
|
|
.attr('data-category', notification.category)
|
|
.attr('data-read', notification.read)
|
|
.attr('data-timestamp', notification.timestamp);
|
|
|
|
// Set icon based on level
|
|
const iconElement = element.find('.notification-icon i');
|
|
switch (notification.level) {
|
|
case 'success':
|
|
iconElement.addClass('fa-check-circle');
|
|
break;
|
|
case 'info':
|
|
iconElement.addClass('fa-info-circle');
|
|
break;
|
|
case 'warning':
|
|
iconElement.addClass('fa-exclamation-triangle');
|
|
break;
|
|
case 'error':
|
|
iconElement.addClass('fa-times-circle');
|
|
break;
|
|
default:
|
|
iconElement.addClass('fa-bell');
|
|
}
|
|
|
|
// Important: Do not append "Z" here, as that can cause timezone issues
|
|
// Create a date object from the notification timestamp
|
|
let notificationDate;
|
|
try {
|
|
// Parse the timestamp directly without modifications
|
|
notificationDate = new Date(notification.timestamp);
|
|
|
|
// Validate the date object - if invalid, try alternative approach
|
|
if (isNaN(notificationDate.getTime())) {
|
|
console.warn("Invalid date from notification timestamp, trying alternative format");
|
|
|
|
// Try adding Z to make it explicit UTC if not already ISO format
|
|
if (!notification.timestamp.endsWith('Z') && !notification.timestamp.includes('+')) {
|
|
notificationDate = new Date(notification.timestamp + 'Z');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Error parsing notification date:", e);
|
|
notificationDate = new Date(); // Fallback to current date
|
|
}
|
|
|
|
// Format the timestamp using the configured timezone
|
|
const options = {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: true,
|
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
|
};
|
|
|
|
// Format full timestamp with configured timezone
|
|
let fullTimestamp;
|
|
try {
|
|
fullTimestamp = notificationDate.toLocaleString('en-US', options);
|
|
} catch (e) {
|
|
console.error("Error formatting timestamp with timezone:", e);
|
|
fullTimestamp = notificationDate.toLocaleString('en-US'); // Fallback without timezone
|
|
}
|
|
|
|
// Append the message and formatted timestamp
|
|
const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`;
|
|
element.find('.notification-message').html(messageWithTimestamp);
|
|
|
|
// Set metadata for relative time display
|
|
element.find('.notification-time').text(formatTimestamp(notificationDate));
|
|
element.find('.notification-category').text(notification.category);
|
|
|
|
// Set up action buttons
|
|
element.find('.mark-read-button').on('click', (e) => {
|
|
e.stopPropagation();
|
|
markAsRead(notification.id);
|
|
});
|
|
element.find('.delete-button').on('click', (e) => {
|
|
e.stopPropagation();
|
|
deleteNotification(notification.id);
|
|
});
|
|
|
|
// Hide mark as read button if already read
|
|
if (notification.read) {
|
|
element.find('.mark-read-button').hide();
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
function formatTimestamp(timestamp) {
|
|
// Ensure we have a valid date object
|
|
let dateObj = timestamp;
|
|
if (!(timestamp instanceof Date) || isNaN(timestamp.getTime())) {
|
|
try {
|
|
dateObj = new Date(timestamp);
|
|
} catch (e) {
|
|
console.error("Invalid timestamp in formatTimestamp:", e);
|
|
return "unknown time";
|
|
}
|
|
}
|
|
|
|
// Calculate time difference in local timezone context
|
|
const now = new Date();
|
|
const diffMs = now - dateObj;
|
|
const diffSec = Math.floor(diffMs / 1000);
|
|
const diffMin = Math.floor(diffSec / 60);
|
|
const diffHour = Math.floor(diffMin / 60);
|
|
const diffDay = Math.floor(diffHour / 24);
|
|
|
|
if (diffSec < 60) {
|
|
return "just now";
|
|
} else if (diffMin < 60) {
|
|
return `${diffMin}m ago`;
|
|
} else if (diffHour < 24) {
|
|
return `${diffHour}h ago`;
|
|
} else if (diffDay < 30) {
|
|
return `${diffDay}d ago`;
|
|
} else {
|
|
// Format as date for older notifications using configured timezone
|
|
const options = {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
|
};
|
|
return dateObj.toLocaleDateString('en-US', options);
|
|
}
|
|
}
|
|
|
|
// Mark a notification as read
|
|
function markAsRead(notificationId) {
|
|
$.ajax({
|
|
url: "/api/notifications/mark_read",
|
|
method: "POST",
|
|
data: JSON.stringify({ notification_id: notificationId }),
|
|
contentType: "application/json",
|
|
success: (data) => {
|
|
// Update UI
|
|
$(`[data-id="${notificationId}"]`).attr('data-read', 'true');
|
|
$(`[data-id="${notificationId}"]`).find('.mark-read-button').hide();
|
|
|
|
// Update unread badge
|
|
updateUnreadBadge(data.unread_count);
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error marking notification as read:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Mark all notifications as read
|
|
function markAllAsRead() {
|
|
$.ajax({
|
|
url: "/api/notifications/mark_read",
|
|
method: "POST",
|
|
data: JSON.stringify({}),
|
|
contentType: "application/json",
|
|
success: (data) => {
|
|
// Update UI
|
|
$('.notification-item').attr('data-read', 'true');
|
|
$('.mark-read-button').hide();
|
|
|
|
// Update unread badge
|
|
updateUnreadBadge(0);
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error marking all notifications as read:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Delete a notification
|
|
function deleteNotification(notificationId) {
|
|
$.ajax({
|
|
url: "/api/notifications/delete",
|
|
method: "POST",
|
|
data: JSON.stringify({ notification_id: notificationId }),
|
|
contentType: "application/json",
|
|
success: (data) => {
|
|
// Remove from UI with animation
|
|
$(`[data-id="${notificationId}"]`).fadeOut(300, function () {
|
|
$(this).remove();
|
|
|
|
// Check if container is empty now
|
|
if ($('#notifications-container').children().length === 0) {
|
|
$('#notifications-container').html($('#empty-template').html());
|
|
$('#load-more').hide();
|
|
}
|
|
});
|
|
|
|
// Update unread badge
|
|
updateUnreadBadge(data.unread_count);
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error deleting notification:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Clear read notifications
|
|
function clearReadNotifications() {
|
|
if (!confirm("Are you sure you want to clear all read notifications?")) {
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: "/api/notifications/clear",
|
|
method: "POST",
|
|
data: JSON.stringify({
|
|
// Special parameter to clear only read notifications
|
|
read_only: true
|
|
}),
|
|
contentType: "application/json",
|
|
success: () => {
|
|
// Reload notifications
|
|
resetAndLoadNotifications();
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error clearing read notifications:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Clear all notifications
|
|
function clearAllNotifications() {
|
|
if (!confirm("Are you sure you want to clear ALL notifications? This cannot be undone.")) {
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: "/api/notifications/clear",
|
|
method: "POST",
|
|
data: JSON.stringify({}),
|
|
contentType: "application/json",
|
|
success: () => {
|
|
// Reload notifications
|
|
resetAndLoadNotifications();
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error clearing all notifications:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update unread badge
|
|
function updateUnreadBadge(count) {
|
|
$('#unread-badge').text(count);
|
|
|
|
// Add special styling if unread
|
|
if (count > 0) {
|
|
$('#unread-badge').addClass('has-unread');
|
|
} else {
|
|
$('#unread-badge').removeClass('has-unread');
|
|
}
|
|
}
|
|
|
|
// Update unread count from API
|
|
function updateUnreadCount() {
|
|
$.ajax({
|
|
url: "/api/notifications/unread_count",
|
|
method: "GET",
|
|
success: (data) => {
|
|
updateUnreadBadge(data.unread_count);
|
|
},
|
|
error: (xhr, status, error) => {
|
|
console.error("Error updating unread count:", error);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Start polling for unread count
|
|
function startUnreadCountPolling() {
|
|
// Update every 30 seconds
|
|
setInterval(updateUnreadCount, 30000);
|
|
} |