diff --git a/application_events.json b/application_events.json index 137c564..20fccbb 100644 --- a/application_events.json +++ b/application_events.json @@ -2113,8 +2113,214 @@ "active_datasets_count": 3, "csv_recording_active": true } + }, + { + "timestamp": "2025-08-14T22:40:58.407368", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T22:40:58.488687", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-14T22:40:58.493692", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 1, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-14T22:40:58.500690", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T22:40:58.508089", + "level": "info", + "event_type": "udp_streaming_started", + "message": "UDP streaming to PlotJuggler started", + "details": { + "udp_host": "127.0.0.1", + "udp_port": 9870, + "datasets_available": 3 + } + }, + { + "timestamp": "2025-08-14T22:41:08.736742", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma", + "AUX Blink_1.0S" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T22:42:07.195383", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma", + "AUX Blink_1.0S" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T22:42:09.975760", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma", + "AUX Blink_1.0S" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T22:51:29.299710", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T22:51:29.367770", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 2, + "streaming_count": 2, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-14T22:51:29.372770", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: Fast", + "details": { + "dataset_id": "Fast", + "variables_count": 2, + "streaming_count": 1, + "prefix": "fast" + } + }, + { + "timestamp": "2025-08-14T22:51:29.375778", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 2 datasets activated", + "details": { + "activated_datasets": 2, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T22:51:29.380769", + "level": "info", + "event_type": "udp_streaming_started", + "message": "UDP streaming to PlotJuggler started", + "details": { + "udp_host": "127.0.0.1", + "udp_port": 9870, + "datasets_available": 3 + } + }, + { + "timestamp": "2025-08-14T22:52:10.248646", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma", + "AUX Blink_1.0S" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T22:55:17.545033", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma", + "AUX Blink_1.0S" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T22:57:05.817266", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma", + "AUX Blink_1.0S" + ], + "time_window": 20, + "trigger_variable": null, + "auto_started": true + } } ], - "last_updated": "2025-08-14T22:33:16.680664", - "total_entries": 207 + "last_updated": "2025-08-14T22:57:05.817266", + "total_entries": 223 } \ No newline at end of file diff --git a/config/data/plot_variables.json b/config/data/plot_variables.json index 14bc38d..08ae190 100644 --- a/config/data/plot_variables.json +++ b/config/data/plot_variables.json @@ -23,7 +23,7 @@ "variable_name": "AUX Blink_1.0S", "color": "#3498db", "line_width": 2, - "y_axis": "left", + "y_axis": "right", "enabled": true } ] diff --git a/core/plc_data_streamer.py b/core/plc_data_streamer.py index bf86689..94b6d3f 100644 --- a/core/plc_data_streamer.py +++ b/core/plc_data_streamer.py @@ -206,10 +206,10 @@ class PLCDataStreamer: try: self.config_manager.load_datasets() self.config_manager.sync_streaming_variables() - + # ๐Ÿ” NEW: Validate CSV headers for active datasets after configuration reload self._validate_csv_headers_after_config_change() - + self.event_logger.log_event( "info", "config_reload", @@ -218,16 +218,18 @@ class PLCDataStreamer: "datasets_count": len(self.config_manager.datasets), "active_datasets_count": len(self.config_manager.active_datasets), "csv_recording_active": self.data_streamer.is_csv_recording(), - } + }, ) if self.logger: - self.logger.info("Dataset configuration reloaded successfully with CSV header validation") + self.logger.info( + "Dataset configuration reloaded successfully with CSV header validation" + ) except Exception as e: self.event_logger.log_event( "error", "config_reload_failed", f"Failed to reload dataset configuration: {str(e)}", - {"error": str(e)} + {"error": str(e)}, ) if self.logger: self.logger.error(f"Failed to reload dataset configuration: {e}") @@ -237,7 +239,9 @@ class PLCDataStreamer: """Validate CSV headers for all active datasets after configuration changes""" if not self.data_streamer.is_csv_recording(): if self.logger: - self.logger.debug("CSV recording not active, skipping header validation") + self.logger.debug( + "CSV recording not active, skipping header validation" + ) return validated_datasets = [] @@ -251,19 +255,23 @@ class PLCDataStreamer: # Get current CSV file path csv_path = self.data_streamer.get_dataset_csv_file_path(dataset_id) - + if not os.path.exists(csv_path): continue # Get expected headers based on current configuration - dataset_variables = self.config_manager.get_dataset_variables(dataset_id) + dataset_variables = self.config_manager.get_dataset_variables( + dataset_id + ) expected_headers = ["timestamp"] + list(dataset_variables.keys()) # Read existing headers from the file existing_headers = self.data_streamer.read_csv_headers(csv_path) # Compare headers - if existing_headers and not self.data_streamer.compare_headers(existing_headers, expected_headers): + if existing_headers and not self.data_streamer.compare_headers( + existing_headers, expected_headers + ): # Header mismatch detected - close current file and rename it if dataset_id in self.data_streamer.dataset_csv_files: self.data_streamer.dataset_csv_files[dataset_id].close() @@ -272,37 +280,51 @@ class PLCDataStreamer: # Rename the file with timestamp prefix = self.config_manager.datasets[dataset_id]["prefix"] - renamed_path = self.data_streamer.rename_csv_file_with_timestamp(csv_path, prefix) - - header_mismatches.append({ - "dataset_id": dataset_id, - "dataset_name": self.config_manager.datasets[dataset_id]["name"], - "original_file": csv_path, - "renamed_file": renamed_path, - "expected_headers": expected_headers, - "existing_headers": existing_headers - }) + renamed_path = self.data_streamer.rename_csv_file_with_timestamp( + csv_path, prefix + ) + + header_mismatches.append( + { + "dataset_id": dataset_id, + "dataset_name": self.config_manager.datasets[dataset_id][ + "name" + ], + "original_file": csv_path, + "renamed_file": renamed_path, + "expected_headers": expected_headers, + "existing_headers": existing_headers, + } + ) # Create new file with correct headers (will be done on next write) # The setup_dataset_csv_file method will handle creating the new file - + if self.logger: self.logger.info( f"CSV header mismatch detected for dataset '{self.config_manager.datasets[dataset_id]['name']}' " f"after configuration reload. File renamed: {os.path.basename(csv_path)} -> {os.path.basename(renamed_path)}" ) - validated_datasets.append({ - "dataset_id": dataset_id, - "dataset_name": self.config_manager.datasets[dataset_id]["name"], - "headers_match": len(header_mismatches) == 0 or dataset_id not in [h["dataset_id"] for h in header_mismatches], - "expected_headers": expected_headers, - "existing_headers": existing_headers - }) + validated_datasets.append( + { + "dataset_id": dataset_id, + "dataset_name": self.config_manager.datasets[dataset_id][ + "name" + ], + "headers_match": len(header_mismatches) == 0 + or dataset_id + not in [h["dataset_id"] for h in header_mismatches], + "expected_headers": expected_headers, + "existing_headers": existing_headers, + } + ) except Exception as e: if self.logger: - self.logger.warning(f"Error validating CSV headers for dataset {dataset_id}: {e}") + self.logger.warning( + f"Error validating CSV headers for dataset {dataset_id}: {e}" + ) # Log summary of validation results if header_mismatches: @@ -313,12 +335,14 @@ class PLCDataStreamer: { "mismatched_datasets": len(header_mismatches), "total_validated": len(validated_datasets), - "details": header_mismatches - } + "details": header_mismatches, + }, ) else: if validated_datasets and self.logger: - self.logger.info(f"CSV headers validated for {len(validated_datasets)} active datasets - all headers match") + self.logger.info( + f"CSV headers validated for {len(validated_datasets)} active datasets - all headers match" + ) # Configuration Methods def update_plc_config(self, ip: str, rack: int, slot: int): diff --git a/frontend/src/components/ChartjsPlot.jsx b/frontend/src/components/ChartjsPlot.jsx index 68563f9..1c8e96d 100644 --- a/frontend/src/components/ChartjsPlot.jsx +++ b/frontend/src/components/ChartjsPlot.jsx @@ -255,9 +255,18 @@ const ChartjsPlot = ({ session, height = '400px' }) => { }, ...(zoomAvailable ? { zoom: { - // Evita listeners wheel/touch no-passive del plugin; usa drag + pan con modificador - pan: { enabled: true, mode: 'x', modifierKey: 'shift' }, - zoom: { drag: { enabled: true }, wheel: { enabled: false }, pinch: { enabled: false }, mode: 'x' } + // Solo habilitar zoom/pan en modo fullscreen + pan: { + enabled: !!session?.isFullscreen, + mode: 'x', + modifierKey: 'shift' + }, + zoom: { + drag: { enabled: !!session?.isFullscreen }, + wheel: { enabled: !!session?.isFullscreen }, + pinch: { enabled: !!session?.isFullscreen }, + mode: 'x' + } } } : {}) }, @@ -523,6 +532,26 @@ const ChartjsPlot = ({ session, height = '400px' }) => { setDataPointsCount(0); }, []); + const resetZoom = useCallback(() => { + if (!chartRef.current) return; + + try { + // Try to reset zoom using the zoom plugin + if (chartRef.current.resetZoom) { + chartRef.current.resetZoom(); + } else if (window.Chart?.helpers?.getRelativePosition) { + // Fallback: manually reset zoom by updating scale options + const chart = chartRef.current; + if (chart.options?.scales?.x?.realtime) { + // For realtime charts, just trigger an update + chart.update('none'); + } + } + } catch (error) { + console.warn('Failed to reset zoom:', error); + } + }, []); + // Update configuration directly (for real-time style changes) const updateConfig = useCallback(async (newConfig) => { try { @@ -615,11 +644,12 @@ const ChartjsPlot = ({ session, height = '400px' }) => { pauseStreaming, resumeStreaming, clearChart, + resetZoom, refreshConfiguration, updateConfig }); } - }, [pauseStreaming, resumeStreaming, clearChart, refreshConfiguration, updateConfig, session?.session_id, session?.onChartReady]); + }, [pauseStreaming, resumeStreaming, clearChart, resetZoom, refreshConfiguration, updateConfig, session?.session_id, session?.onChartReady]); // Update chart when session status changes useEffect(() => { @@ -637,6 +667,14 @@ const ChartjsPlot = ({ session, height = '400px' }) => { } }, [session?.is_active, session?.is_paused, pauseStreaming, resumeStreaming]); + // Recreate chart when fullscreen mode changes to enable/disable zoom + useEffect(() => { + if (chartRef.current && typeof session?.isFullscreen === 'boolean') { + console.log(`๐Ÿ”„ Fullscreen mode changed to ${session.isFullscreen}, recreating chart...`); + createStreamingChart(); + } + }, [session?.isFullscreen, createStreamingChart]); + // Initialize chart when config is resolved - simplified approach useEffect(() => { // Only create chart once when we have a session_id and canvas diff --git a/frontend/src/components/PlotRealtimeSession.jsx b/frontend/src/components/PlotRealtimeSession.jsx index f2708aa..a756250 100644 --- a/frontend/src/components/PlotRealtimeSession.jsx +++ b/frontend/src/components/PlotRealtimeSession.jsx @@ -30,9 +30,16 @@ import { Slider, SliderTrack, SliderFilledTrack, - SliderThumb + SliderThumb, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalCloseButton, + ModalBody, + useDisclosure, } from '@chakra-ui/react' -import { SettingsIcon, RepeatIcon } from '@chakra-ui/icons' +import { SettingsIcon, RepeatIcon, ViewIcon } from '@chakra-ui/icons' import ChartjsPlot from './ChartjsPlot.jsx' import * as api from '../services/api' @@ -57,6 +64,7 @@ export default function PlotRealtimeSession({ const [showSettings, setShowSettings] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) + const { isOpen: isFullscreen, onOpen: openFullscreen, onClose: closeFullscreen } = useDisclosure() const [localConfig, setLocalConfig] = useState({ time_window: plotDefinition.time_window || 60, y_min: plotDefinition.y_min, @@ -78,6 +86,27 @@ export default function PlotRealtimeSession({ // Track if we're in the middle of applying changes to avoid conflicts const applyingChangesRef = useRef(false) + // Handle fullscreen resize - force chart resize when modal opens/closes + useEffect(() => { + if (isFullscreen && chartControlsRef.current) { + // Delay to ensure modal is fully rendered + const timer = setTimeout(() => { + if (chartControlsRef.current?.refreshConfiguration) { + chartControlsRef.current.refreshConfiguration() + } + // Also try to trigger a window resize event to force Chart.js to recalculate + window.dispatchEvent(new Event('resize')) + }, 200) + return () => clearTimeout(timer) + } else if (!isFullscreen && chartControlsRef.current) { + // When exiting fullscreen, also trigger resize + const timer = setTimeout(() => { + window.dispatchEvent(new Event('resize')) + }, 100) + return () => clearTimeout(timer) + } + }, [isFullscreen]) + // Update localConfig when plotDefinition changes (but not during our own updates) useEffect(() => { if (!applyingChangesRef.current) { @@ -108,6 +137,7 @@ export default function PlotRealtimeSession({ is_active: session.is_active, is_paused: session.is_paused, variables_count: plotVariables.length, + isFullscreen: isFullscreen, config: { ...plotDefinition, ...localConfig, @@ -123,7 +153,8 @@ export default function PlotRealtimeSession({ session.is_active, session.is_paused, plotVariables, - localConfig + localConfig, + isFullscreen ]) // Load session status from backend (optional - session may not exist until started) @@ -430,6 +461,15 @@ export default function PlotRealtimeSession({ + } size="sm" @@ -695,8 +735,82 @@ export default function PlotRealtimeSession({ > โน๏ธ Stop + + + + {/* Fullscreen Modal */} + + + + + + ๐Ÿ“ˆ {plotDefinition.name} - Fullscreen Mode + + + Zoom: Drag to select area | Pan: Shift + Drag | Double-click to reset + + + + + + + + + + + + + + {chartControlsRef.current && ( + + )} + + + + ) } diff --git a/frontend/src/components/PlotRealtimeViewer.jsx b/frontend/src/components/PlotRealtimeViewer.jsx deleted file mode 100644 index 04c25d6..0000000 --- a/frontend/src/components/PlotRealtimeViewer.jsx +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { - Box, - VStack, - HStack, - Text, - Button, - Card, - CardBody, - CardHeader, - Heading, - useColorModeValue, - Badge, - IconButton, - Divider, - Spacer, -} from '@chakra-ui/react' -import { EditIcon, SettingsIcon, DeleteIcon } from '@chakra-ui/icons' -import ChartjsPlot from './ChartjsPlot.jsx' - -export default function PlotRealtimeViewer() { - const [sessions, setSessions] = useState(new Map()) - const [loading, setLoading] = useState(false) - const intervalRef = useRef(null) - const muted = useColorModeValue('gray.600', 'gray.300') - - const loadSessions = async () => { - try { - setLoading(true) - const res = await fetch('/api/plots') - const data = await res.json() - if (data && data.sessions) { - setSessions(prev => { - const next = new Map(prev) - const incomingIds = new Set() - for (const s of data.sessions) { - incomingIds.add(s.session_id) - const existing = next.get(s.session_id) - if (existing) { - // Mutate existing object to preserve reference - existing.name = s.name - existing.is_active = s.is_active - existing.is_paused = s.is_paused - existing.variables_count = s.variables_count - } else { - next.set(s.session_id, { ...s }) - } - } - // Remove sessions not present anymore - for (const id of Array.from(next.keys())) { - if (!incomingIds.has(id)) next.delete(id) - } - return next - }) - } else { - setSessions(new Map()) - } - } catch { - setSessions(new Map()) - } finally { - setLoading(false) - } - } - - const refreshSession = async (sessionId) => { - try { - const res = await fetch(`/api/plots/${sessionId}/config`) - const data = await res.json() - if (data && data.success && data.config) { - setSessions(prev => { - const n = new Map(prev) - const existing = n.get(sessionId) - const varsCount = Array.isArray(data.config.variables) - ? data.config.variables.length - : (data.config.variables ? Object.keys(data.config.variables).length : (existing?.variables_count || 0)) - if (existing) { - existing.name = data.config.name - existing.is_active = data.config.is_active - existing.is_paused = data.config.is_paused - existing.variables_count = varsCount - } else { - n.set(sessionId, { - session_id: sessionId, - name: data.config.name, - is_active: data.config.is_active, - is_paused: data.config.is_paused, - variables_count: varsCount, - }) - } - return n - }) - } - } catch { /* ignore */ } - } - - const controlSession = async (sessionId, action) => { - try { - await fetch(`/api/plots/${sessionId}/control`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ action }), - }) - await refreshSession(sessionId) - } catch { /* ignore */ } - } - - useEffect(() => { - loadSessions() - intervalRef.current = setInterval(loadSessions, 5000) - return () => { if (intervalRef.current) clearInterval(intervalRef.current) } - }, []) - - const sessionsList = useMemo(() => Array.from(sessions.values()), [sessions]) - - if (loading && sessionsList.length === 0) { - return Cargando sesiones de plotsโ€ฆ - } - - if (sessionsList.length === 0) { - return ( - - - No hay sesiones de plot. Cree o edite plots en la secciรณn superior. - - - ) - } - - return ( - - {sessionsList.map((session) => ( - - ))} - - ) -} - -function PlotRealtimeCard({ session, onControl, onRefresh }) { - const cardBg = useColorModeValue('white', 'gray.700') - const borderColor = useColorModeValue('gray.200', 'gray.600') - const muted = useColorModeValue('gray.600', 'gray.300') - const chartControlsRef = useRef(null) - - const handleChartReady = (controls) => { - chartControlsRef.current = controls - } - - const enhancedSession = { - ...session, - onChartReady: handleChartReady, - } - - const handleControlClick = async (action) => { - if (chartControlsRef.current) { - switch (action) { - case 'pause': - chartControlsRef.current.pauseStreaming() - break - case 'start': - case 'resume': - chartControlsRef.current.resumeStreaming() - break - case 'clear': - chartControlsRef.current.clearChart() - break - case 'stop': - chartControlsRef.current.pauseStreaming() - break - } - } - // No esperar a que el backend responda para aplicar efecto local - onControl(session.session_id, action) - } - - return ( - - - onRefresh(session.session_id)} /> - - - - - - - - - - - - ) -} - -function FlexHeader({ session, muted, onRefresh }) { - return ( - - - ๐Ÿ“ˆ {session.name || session.session_id} - - Variables: {session.variables_count || 0} | Status: {session.is_active ? (session.is_paused ? 'Paused' : 'Active') : 'Stopped'} - - - - - } size="sm" variant="outline" aria-label="Refresh status" onClick={onRefresh} /> - - - ) -} - - diff --git a/system_state.json b/system_state.json index 25c08dc..343a83b 100644 --- a/system_state.json +++ b/system_state.json @@ -3,11 +3,11 @@ "should_connect": true, "should_stream": true, "active_datasets": [ - "DAR", "Fast", + "DAR", "Test" ] }, "auto_recovery_enabled": true, - "last_update": "2025-08-14T22:33:00.768192" + "last_update": "2025-08-14T22:51:29.383787" } \ No newline at end of file diff --git a/test_config_reload.py b/test_config_reload.py new file mode 100644 index 0000000..539ab38 --- /dev/null +++ b/test_config_reload.py @@ -0,0 +1,129 @@ +""" +Test script to validate automatic configuration reloading +""" + +import json +import requests +import time + +# Configuration +BASE_URL = "http://localhost:5000" +TEST_DATASET_ID = "TestReload" + + +def test_config_reload(): + """Test that backend automatically reloads configuration when datasets are updated""" + + print("๐Ÿงช Testing automatic configuration reload...") + + try: + # Step 1: Get current dataset definitions + print("๐Ÿ“– Reading current dataset definitions...") + response = requests.get(f"{BASE_URL}/api/config/dataset-definitions") + if not response.ok: + print(f"โŒ Failed to read dataset definitions: {response.status_code}") + return False + + current_config = response.json() + datasets = current_config.get("data", {}).get("datasets", []) + print(f"Current datasets: {[d.get('id') for d in datasets]}") + + # Step 2: Add a test dataset + print(f"โž• Adding test dataset: {TEST_DATASET_ID}") + test_dataset = { + "id": TEST_DATASET_ID, + "name": "Test Reload Dataset", + "prefix": "test_reload", + "sampling_interval": 1.0, + "enabled": False, + } + + # Add to datasets list + new_datasets = [ + d for d in datasets if d.get("id") != TEST_DATASET_ID + ] # Remove if exists + new_datasets.append(test_dataset) + + new_config = { + "datasets": new_datasets, + "version": "1.0", + "last_update": f"{time.time()}", + } + + # Save configuration + response = requests.put( + f"{BASE_URL}/api/config/dataset-definitions", + headers={"Content-Type": "application/json"}, + json=new_config, + ) + + if not response.ok: + print(f"โŒ Failed to save dataset definitions: {response.status_code}") + return False + + print("โœ… Dataset definitions saved") + + # Step 3: Check if backend has reloaded the configuration + print("๐Ÿ” Checking if backend reloaded configuration...") + time.sleep(1) # Give backend a moment to reload + + # Get status from backend + response = requests.get(f"{BASE_URL}/api/status") + if not response.ok: + print(f"โŒ Failed to get status: {response.status_code}") + return False + + status = response.json() + backend_datasets = status.get("datasets", {}) + + if TEST_DATASET_ID in backend_datasets: + print(f"โœ… Backend successfully loaded new dataset: {TEST_DATASET_ID}") + print(f"Dataset details: {backend_datasets[TEST_DATASET_ID]}") + + # Step 4: Clean up - remove test dataset + print("๐Ÿงน Cleaning up test dataset...") + cleanup_datasets = [ + d for d in new_datasets if d.get("id") != TEST_DATASET_ID + ] + cleanup_config = { + "datasets": cleanup_datasets, + "version": "1.0", + "last_update": f"{time.time()}", + } + + response = requests.put( + f"{BASE_URL}/api/config/dataset-definitions", + headers={"Content-Type": "application/json"}, + json=cleanup_config, + ) + + if response.ok: + print("โœ… Test dataset cleaned up") + else: + print( + f"โš ๏ธ Warning: Failed to clean up test dataset: {response.status_code}" + ) + + return True + else: + print( + f"โŒ Backend did not reload configuration. Available datasets: {list(backend_datasets.keys())}" + ) + return False + + except requests.exceptions.ConnectionError: + print( + "โŒ Could not connect to backend. Make sure the Flask server is running on http://localhost:5000" + ) + return False + except Exception as e: + print(f"โŒ Test failed with error: {e}") + return False + + +if __name__ == "__main__": + success = test_config_reload() + if success: + print("\n๐ŸŽ‰ Configuration reload test PASSED!") + else: + print("\n๐Ÿ’ฅ Configuration reload test FAILED!")