Fix: Prevent console message loop on maintenance updates

The console was generating excessive messages after schedule maintenance updates.
This commit is contained in:
Tola Leng
2025-05-26 16:42:05 +08:00
parent 23be1fca0f
commit e74bf35264
7 changed files with 175 additions and 94 deletions
+48
View File
@@ -35,4 +35,52 @@ const api = {
}
};
// Mock fetch override for development
const originalFetch = window.fetch;
window.fetch = async (url, options = {}) => {
// Check if this is an API request to our mock endpoints
if (typeof url === 'string' && url.startsWith('/api/')) {
console.log('Intercepting API request:', url, options);
try {
let body = {};
// Properly handle different body types
if (options.body) {
if (typeof options.body === 'string') {
body = JSON.parse(options.body);
} else {
// Handle ReadableStream or other BodyInit types
const bodyText = await new Response(options.body).text();
body = bodyText ? JSON.parse(bodyText) : {};
}
}
const result = await api.handleRequest(url, options.method || 'GET', body);
// Create a proper Response object
return new Response(JSON.stringify(result.json), {
status: result.status,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
console.error('Error in API handler:', error);
return new Response(JSON.stringify({
success: false,
message: 'Internal server error'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
},
});
}
}
// For all other requests, use the original fetch
return originalFetch(url, options);
};
export default api;
@@ -58,7 +58,6 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
// Prevent duplicate requests
if (currentRequestRef.current) {
console.log("Request already in progress, waiting...");
await currentRequestRef.current;
return;
}
@@ -72,14 +71,11 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
const requestPromise = (async () => {
try {
console.log("Fetching maintenance data...", force ? "(forced)" : "");
const data = await maintenanceService.getMaintenanceRecords();
// Check if component is still mounted before updating state
if (!mountedRef.current) return;
console.log(`Fetched ${data.length} maintenance records`);
// Update state with fetched data
setAllMaintenanceData(data);
setInitialized(true);
@@ -119,7 +115,6 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
// Initial fetch on mount - NO AUTOMATIC POLLING
useEffect(() => {
console.log("useMaintenanceData hook mounted, fetching initial data");
mountedRef.current = true;
// Only fetch initial data, no polling
@@ -134,7 +129,6 @@ export const useMaintenanceData = ({ refreshTrigger = 0 }: UseMaintenanceDataPro
// Handle refresh trigger changes - ONLY when explicitly triggered
useEffect(() => {
if (refreshTrigger > 0) {
console.log("Manual refresh triggered, forcing data fetch");
fetchMaintenanceData(true); // Force refresh to bypass cache
}
}, [refreshTrigger, fetchMaintenanceData]);
@@ -19,74 +19,157 @@ export const MaintenanceStatusChecker = ({
const { toast } = useToast();
const checkedItemsRef = useRef<Set<string>>(new Set());
const notificationSentRef = useRef<Set<string>>(new Set());
const intervalRef = useRef<number | null>(null);
useEffect(() => {
if (!maintenanceData || maintenanceData.length === 0) return;
// Check for maintenance items that need status update
const now = new Date();
const checkAndUpdateStatus = async () => {
const currentTime = new Date();
let hasUpdates = false;
console.log('MaintenanceStatusChecker: Checking status updates at', currentTime.toISOString());
console.log('MaintenanceStatusChecker: Checking', maintenanceData.length, 'maintenance items');
for (const item of maintenanceData) {
if (item.status.toLowerCase() !== 'scheduled') continue;
// Skip if we've already checked this item in this session
if (checkedItemsRef.current.has(item.id)) continue;
try {
const startTime = new Date(item.start_time);
const endTime = new Date(item.end_time);
const status = item.status.toLowerCase();
// If current time is past start time but before end time, update to in_progress
if (startTime <= now && now <= endTime) {
console.log(`Auto-updating maintenance ${item.title} to in_progress status`);
console.log(`MaintenanceStatusChecker: Item ${item.id} - Status: ${status}, Start: ${startTime.toISOString()}, End: ${endTime.toISOString()}, Current: ${currentTime.toISOString()}`);
// Check if scheduled maintenance should start (become in_progress)
if (status === 'scheduled' && currentTime >= startTime && currentTime <= endTime) {
const checkKey = `${item.id}-started`;
const notificationKey = `${item.id}-start-notification`;
// Update status to in_progress
await maintenanceService.updateMaintenanceStatus(item.id, 'in_progress');
// Only send notification if not sent before
if (!notificationSentRef.current.has(item.id)) {
console.log(`Sending start notification for maintenance ${item.title}`);
if (!checkedItemsRef.current.has(checkKey)) {
console.log(`MaintenanceStatusChecker: Starting maintenance ${item.id} at ${currentTime.toISOString()}`);
// Send notification for maintenance start
await maintenanceNotificationService.sendMaintenanceNotification({
maintenance: item,
notificationType: 'start'
// Update status to in_progress first
await maintenanceService.updateMaintenanceStatus(item.id, 'in_progress');
// Send start notification only once
if (!notificationSentRef.current.has(notificationKey)) {
try {
await maintenanceNotificationService.sendMaintenanceNotification({
maintenance: item,
notificationType: 'start'
});
notificationSentRef.current.add(notificationKey);
console.log(`MaintenanceStatusChecker: Start notification sent for ${item.id}`);
} catch (notificationError) {
console.log('MaintenanceStatusChecker: Start notification failed', notificationError);
}
}
toast({
title: t('maintenanceInProgress'),
description: `${item.title} ${t('isNowInProgress')}`,
});
// Mark as notified
notificationSentRef.current.add(item.id);
checkedItemsRef.current.add(checkKey);
hasUpdates = true;
}
toast({
title: t('maintenanceInProgress'),
description: `${item.title} ${t('isNowInProgress')}`,
});
onStatusUpdated();
}
// Mark as checked regardless of outcome
checkedItemsRef.current.add(item.id);
// Check if in_progress maintenance should be completed
if (status === 'in_progress' && currentTime >= endTime) {
const checkKey = `${item.id}-completed`;
const notificationKey = `${item.id}-end-notification`;
if (!checkedItemsRef.current.has(checkKey)) {
console.log(`MaintenanceStatusChecker: Completing maintenance ${item.id} at ${currentTime.toISOString()}`);
// Update status to completed first
await maintenanceService.updateMaintenanceStatus(item.id, 'completed');
// Send completion notification only once
if (!notificationSentRef.current.has(notificationKey)) {
try {
await maintenanceNotificationService.sendMaintenanceNotification({
maintenance: item,
notificationType: 'end'
});
notificationSentRef.current.add(notificationKey);
console.log(`MaintenanceStatusChecker: Completion notification sent for ${item.id}`);
} catch (notificationError) {
console.log('MaintenanceStatusChecker: Completion notification failed', notificationError);
}
}
toast({
title: t('maintenanceCompleted'),
description: `${item.title} ${t('hasBeenCompleted')}`,
});
checkedItemsRef.current.add(checkKey);
hasUpdates = true;
}
}
} catch (error) {
console.error(`Error auto-updating maintenance status for ${item.id}:`, error);
// Add a small delay before trying again to prevent rapid retry cycles
console.error('MaintenanceStatusChecker: Error updating status for item', item.id, error);
// Clear the check flags after 2 minutes to allow retry
setTimeout(() => {
checkedItemsRef.current.delete(item.id); // Allow retry on next check
}, 300000); // 5 minutes before retry
checkedItemsRef.current.delete(`${item.id}-started`);
checkedItemsRef.current.delete(`${item.id}-completed`);
notificationSentRef.current.delete(`${item.id}-start-notification`);
notificationSentRef.current.delete(`${item.id}-end-notification`);
}, 120000);
}
}
if (hasUpdates) {
console.log('MaintenanceStatusChecker: Status updates detected, triggering refresh');
// Force immediate refresh to update the UI
onStatusUpdated();
}
};
// Run the check immediately
// Clear the interval if it exists
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
// Initial check immediately
checkAndUpdateStatus();
// Set up interval to check every minute
const intervalId = setInterval(checkAndUpdateStatus, 60000);
// Check every 5 seconds for immediate status updates
intervalRef.current = window.setInterval(checkAndUpdateStatus, 5000);
return () => clearInterval(intervalId);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [maintenanceData, onStatusUpdated, t, toast]);
// This is a utility component with no UI render
// Clear check flags when maintenance data changes significantly
useEffect(() => {
const currentIds = new Set(maintenanceData.map(item => item.id));
// Clean up check flags for items that no longer exist
const keysToDelete = Array.from(checkedItemsRef.current).filter(key => {
const itemId = key.split('-')[0];
return !currentIds.has(itemId);
});
keysToDelete.forEach(key => {
checkedItemsRef.current.delete(key);
});
// Clean up notification flags for items that no longer exist
const notificationKeysToDelete = Array.from(notificationSentRef.current).filter(key => {
const itemId = key.split('-')[0];
return !currentIds.has(itemId);
});
notificationKeysToDelete.forEach(key => {
notificationSentRef.current.delete(key);
});
}, [maintenanceData]);
return null;
};
};
@@ -25,7 +25,6 @@ export const MaintenanceDetailContent = ({
queryKey: ['users'],
queryFn: async () => {
const usersList = await userService.getUsers();
console.log("Fetched users for maintenance detail:", usersList);
return usersList || [];
},
enabled: true
@@ -36,7 +35,6 @@ export const MaintenanceDetailContent = ({
queryKey: ['notificationChannels'],
queryFn: async () => {
const channels = await alertConfigService.getAlertConfigurations();
console.log("Fetched notification channels:", channels);
return channels || [];
},
enabled: true
@@ -45,9 +43,6 @@ export const MaintenanceDetailContent = ({
// Process user information when users data is available
useEffect(() => {
if (users.length > 0 && maintenance) {
console.log("Processing maintenance data with users:", maintenance);
console.log("Maintenance assigned_users:", maintenance.assigned_users);
// Create a copy of the maintenance object for modifications
const enhancedMaintenance = { ...maintenance };
@@ -58,7 +53,6 @@ export const MaintenanceDetailContent = ({
// Step 1: Extract user IDs from various formats
if (Array.isArray(maintenance.assigned_users)) {
userIds = maintenance.assigned_users;
console.log("Assigned users is an array:", userIds);
} else if (typeof maintenance.assigned_users === 'string') {
// Try parsing the string to extract user IDs
try {
@@ -67,34 +61,28 @@ export const MaintenanceDetailContent = ({
if (Array.isArray(parsedData)) {
// Direct array format
userIds = parsedData;
console.log("Parsed assigned_users from JSON array:", userIds);
} else if (typeof parsedData === 'string') {
// Nested JSON string
try {
const nestedData = JSON.parse(parsedData);
if (Array.isArray(nestedData)) {
userIds = nestedData;
console.log("Parsed assigned_users from nested JSON:", userIds);
}
} catch (e) {
// If nested parsing fails, treat as single ID
userIds = [parsedData];
console.log("Using parsed string as single ID:", userIds);
}
} else {
// Unknown format, use as is
userIds = [String(parsedData)];
console.log("Using parsed data as single ID:", userIds);
}
} catch (e) {
// If JSON parsing fails, try comma splitting
if (maintenance.assigned_users.includes(',')) {
userIds = maintenance.assigned_users.split(',').map(id => id.trim()).filter(Boolean);
console.log("Split assigned_users by comma:", userIds);
} else {
// Single ID
userIds = [maintenance.assigned_users];
console.log("Using string as single ID:", userIds);
}
}
}
@@ -107,20 +95,15 @@ export const MaintenanceDetailContent = ({
cleanId = cleanId.replace(/[\[\]"'\\]/g, '');
return cleanId;
}).filter(Boolean);
console.log("Cleaned user IDs:", userIds);
// Step 3: Find matching users from the users array
if (userIds.length > 0) {
const matchedUsers = users.filter(user => userIds.includes(user.id));
console.log("Matched assigned users:", matchedUsers);
setAssignedUsers(matchedUsers);
} else {
console.log("No user IDs found in assigned_users");
setAssignedUsers([]);
}
} else {
console.log("No assigned users found in maintenance data");
setAssignedUsers([]);
}
@@ -138,24 +121,15 @@ export const MaintenanceDetailContent = ({
const channelId = maintenance.notification_channel_id || maintenance.notification_id;
if (channelId && notificationChannels.length > 0) {
console.log("Looking for notification channel with ID:", channelId);
console.log("Available notification channels:", notificationChannels.map(c => ({ id: c.id, name: c.notify_name })));
const channel = notificationChannels.find(ch => ch.id === channelId);
if (channel) {
console.log("Found notification channel:", channel);
enhancedMaintenance.notification_channel_name = `${channel.notify_name} (${channel.notification_type})`;
} else {
console.log("No matching notification channel found for ID:", channelId);
enhancedMaintenance.notification_channel_name = `Channel ID: ${channelId}`;
}
} else {
console.log("No channel ID available or no notification channels loaded");
}
}
console.log("Enhanced maintenance with details:", enhancedMaintenance);
console.log("Assigned users for display:", assignedUsers);
setMaintenanceWithDetails(enhancedMaintenance);
}
}, [maintenance, users, notificationChannels]);
@@ -173,4 +147,4 @@ export const MaintenanceDetailContent = ({
/>
</>
);
};
};
@@ -13,14 +13,7 @@ const CACHE_EXPIRY_MS = 600000; // 10 minutes cache expiry (increased significan
*/
export const isCacheValid = (): boolean => {
const now = Date.now();
const isValid = !!(cachedMaintenanceRecords && now - lastFetchTimestamp < CACHE_EXPIRY_MS);
// Minimal logging to reduce console spam
if (isValid) {
console.log("Using cached maintenance data");
}
return isValid;
return !!(cachedMaintenanceRecords && now - lastFetchTimestamp < CACHE_EXPIRY_MS);
};
/**
@@ -28,7 +21,6 @@ export const isCacheValid = (): boolean => {
*/
export const getCachedRecords = () => {
if (cachedMaintenanceRecords) {
console.log("Returning cached maintenance records:", cachedMaintenanceRecords.length);
return [...cachedMaintenanceRecords]; // Return a copy to prevent mutation
}
return null;
@@ -40,7 +32,6 @@ export const getCachedRecords = () => {
export const updateCache = (data: MaintenanceItem[], timestamp?: number): void => {
cachedMaintenanceRecords = data;
lastFetchTimestamp = timestamp || Date.now();
console.log("Maintenance cache updated with", data.length, "records");
};
/**
@@ -49,7 +40,6 @@ export const updateCache = (data: MaintenanceItem[], timestamp?: number): void =
export const clearCache = (): void => {
cachedMaintenanceRecords = null;
lastFetchTimestamp = 0;
console.log("Maintenance cache cleared");
};
/**
@@ -19,7 +19,6 @@ export const fetchAllMaintenanceRecords = async (forceRefresh = false): Promise<
// If forced refresh, clear cache first
if (forceRefresh) {
clearCache();
console.log('Cache cleared for forced refresh');
}
// Return cached data if available and not forced refresh
@@ -29,14 +28,12 @@ export const fetchAllMaintenanceRecords = async (forceRefresh = false): Promise<
// Request deduplication: if a request is already in progress, wait for it
if (currentRequest) {
console.log('Request already in progress, waiting for completion');
return await currentRequest;
}
// Strict rate limiting - prevent requests too close together (unless forced)
const timeSinceLastRequest = now - lastRequestTime;
if (timeSinceLastRequest < MIN_REQUEST_INTERVAL && !forceRefresh) {
console.log(`Rate limiting: ${Math.round((MIN_REQUEST_INTERVAL - timeSinceLastRequest) / 1000)}s remaining`);
return getCachedRecords() || [];
}
@@ -66,8 +63,6 @@ export const fetchAllMaintenanceRecords = async (forceRefresh = false): Promise<
* Perform the actual API request
*/
const performRequest = async (timestamp: number): Promise<MaintenanceItem[]> => {
console.log('Making API request for maintenance records...');
// Create abort controller for request timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
@@ -95,7 +90,6 @@ const performRequest = async (timestamp: number): Promise<MaintenanceItem[]> =>
// Update cache
updateCache(normalizedData, timestamp);
console.log(`Successfully fetched ${normalizedData.length} maintenance records`);
return normalizedData;
} catch (error) {
@@ -61,14 +61,12 @@ const getCompletedMaintenance = async (): Promise<MaintenanceItem[]> => {
const createMaintenance = async (data: CreateMaintenanceInput): Promise<void> => {
await createMaintenanceRecord(data);
clearMaintenanceCache(); // Force cache refresh after creation
console.log('Maintenance created and cache cleared');
};
// Update an existing maintenance record
const updateMaintenanceRecord = async (id: string, data: Partial<MaintenanceItem>): Promise<void> => {
await updateMaintenance(id, data);
clearMaintenanceCache(); // Force cache refresh after update
console.log('Maintenance updated and cache cleared');
};
// Refresh cache manually