diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 7b976f8..01d0c66 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -101,6 +101,7 @@ const ApiServerProvider = ({ children }) => { newSocket.on('objectNew', handleObjectNew) newSocket.on('objectDelete', handleObjectDelete) newSocket.on('lockUpdate', handleLockUpdate) + newSocket.on('modelStats', handleModelStats) newSocket.on('disconnect', () => { logger.debug('Api Server disconnected') @@ -248,6 +249,33 @@ const ApiServerProvider = ({ children }) => { } } + const handleModelStats = async (data) => { + const objectType = data.objectType || 'unknown' + + const callbacksRefKey = `modelStats:${objectType}` + logger.debug('Notifying model stats update:', data) + console.log('handleModelStats', data) + if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) { + const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey) + logger.debug( + `Calling ${callbacks.length} callbacks for model stats:`, + callbacksRefKey + ) + callbacks.forEach((callback) => { + try { + callback(data.stats) + } catch (error) { + logger.error('Error in model stats callback:', error) + } + }) + } else { + logger.debug( + `No callbacks found for model stats: ${callbacksRefKey}, subscribed callbacks:`, + Array.from(subscribedCallbacksRef.current.keys()) + ) + } + } + const handleObjectNew = async (data) => { logger.debug('Notifying object new:', data) const objectType = data.objectType || 'unknown' @@ -514,6 +542,67 @@ const ApiServerProvider = ({ children }) => { [offObjectUpdatesEvent] ) + const offModelStats = useCallback((objectType, callback) => { + if (socketRef.current && socketRef.current.connected == true) { + const callbacksRefKey = `modelStats:${objectType}` + + // Remove callback from the subscribed callbacks map + if (subscribedCallbacksRef.current.has(callbacksRefKey)) { + const callbacks = subscribedCallbacksRef.current + .get(callbacksRefKey) + .filter((cb) => cb !== callback) + if (callbacks.length === 0) { + subscribedCallbacksRef.current.delete(callbacksRefKey) + socketRef.current.emit('unsubscribeModelStats', { + objectType + }) + } else { + subscribedCallbacksRef.current.set(callbacksRefKey, callbacks) + } + } + } + }, []) + + const subscribeToModelStats = useCallback( + (objectType, callback) => { + if (socketRef.current && socketRef.current.connected == true) { + const callbacksRefKey = `modelStats:${objectType}` + // Add callback to the subscribed callbacks map immediately + if (!subscribedCallbacksRef.current.has(callbacksRefKey)) { + subscribedCallbacksRef.current.set(callbacksRefKey, []) + } + + const callbacksLength = + subscribedCallbacksRef.current.get(callbacksRefKey).length + + if (callbacksLength <= 0) { + socketRef.current.emit( + 'subscribeToModelStats', + { + objectType: objectType + }, + (result) => { + if (result.success) { + logger.info('Subscribed to model stats:', objectType) + } + } + ) + } + logger.info( + 'Adding model stats callback:', + objectType, + 'callbacks length:', + callbacksLength + 1 + ) + subscribedCallbacksRef.current.get(callbacksRefKey).push(callback) + + // Return cleanup function + return () => offModelStats(objectType, callback) + } + }, + [offModelStats] + ) + const subscribeToObjectLock = useCallback( (id, type, callback) => { logger.debug('Subscribing to lock for object:', id, 'type:', type) @@ -834,6 +923,106 @@ const ApiServerProvider = ({ children }) => { } } + const getModelStats = async (objectType) => { + logger.debug('Fetching stats for model type:', objectType) + try { + let statsUrl + if (objectType === 'history') { + statsUrl = `${config.backendUrl}/stats/history` + } else { + statsUrl = `${config.backendUrl}/${objectType.toLowerCase()}s/stats` + } + + const response = await axios.get(statsUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + }) + logger.debug('Fetched stats for model type:', objectType) + return response.data + } catch (err) { + console.error(err) + showError(err, () => { + getModelStats(objectType) + }) + return null + } + } + + const getModelHistory = async (objectType, startDate, endDate) => { + logger.debug('Fetching history for model type:', objectType) + const encodedStartDate = encodeURIComponent(startDate.toISOString()) + const encodedEndDate = encodeURIComponent(endDate.toISOString()) + try { + const historyUrl = `${config.backendUrl}/${objectType.toLowerCase()}s/history?from=${encodedStartDate}&to=${encodedEndDate}` + + const response = await axios.get(historyUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + }) + logger.debug('Fetched history for model type:', objectType) + + // Calculate time range in milliseconds + const timeRangeMs = endDate.getTime() - startDate.getTime() + const oneHourMs = 60 * 60 * 1000 + const twelveHoursMs = 12 * 60 * 60 * 1000 + + // Determine interval based on time range + let intervalMinutes = 1 // Default: 1 minute + if (timeRangeMs > twelveHoursMs) { + intervalMinutes = 10 // Over 12 hours: 10 minutes + } else if (timeRangeMs > oneHourMs) { + intervalMinutes = 5 // Over 1 hour: 5 minutes + } + + const intervalMs = intervalMinutes * 60 * 1000 + + // Filter data to only include points at the specified intervals + if (response.data && Array.isArray(response.data)) { + const filteredData = [] + const seenIntervals = new Set() + + response.data.forEach((point) => { + const pointDate = new Date(point.date) + // Round down to the nearest interval + const roundedTime = + Math.floor(pointDate.getTime() / intervalMs) * intervalMs + const roundedDate = new Date(roundedTime) + const intervalKey = roundedDate.toISOString() + + // Only include if we haven't seen this interval yet + if (!seenIntervals.has(intervalKey)) { + seenIntervals.add(intervalKey) + // Update the point's date to the rounded interval + filteredData.push({ + ...point, + date: roundedDate.toISOString() + }) + } + }) + + // Sort by date to ensure chronological order + filteredData.sort((a, b) => new Date(a.date) - new Date(b.date)) + + logger.debug( + `Filtered history data: ${response.data.length} -> ${filteredData.length} points (${intervalMinutes} min intervals)` + ) + return filteredData + } + + return response.data + } catch (err) { + console.error(err) + showError(err, () => { + getModelHistory(objectType) + }) + return null + } + } + const fetchTemplatePreview = async ( id, content, @@ -1032,10 +1221,13 @@ const ApiServerProvider = ({ children }) => { subscribeToObjectEvent, subscribeToObjectTypeUpdates, subscribeToObjectLock, + subscribeToModelStats, fetchObject, fetchObjects, fetchObjectsByProperty, fetchSpotlightData, + getModelStats, + getModelHistory, fetchLoading, showError, fetchFileContent,