diff --git a/application_events.json b/application_events.json
index 8f1cf8f..732ed62 100644
--- a/application_events.json
+++ b/application_events.json
@@ -7561,8 +7561,49 @@
"activated_datasets": 2,
"total_datasets": 3
}
+ },
+ {
+ "timestamp": "2025-08-15T20:06:52.191312",
+ "level": "info",
+ "event_type": "application_started",
+ "message": "Application initialization completed successfully",
+ "details": {}
+ },
+ {
+ "timestamp": "2025-08-15T20:06:52.271192",
+ "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-15T20:06:52.281191",
+ "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-15T20:06:52.289232",
+ "level": "info",
+ "event_type": "csv_recording_started",
+ "message": "CSV recording started: 2 datasets activated",
+ "details": {
+ "activated_datasets": 2,
+ "total_datasets": 3
+ }
}
],
- "last_updated": "2025-08-15T19:53:52.599789",
- "total_entries": 616
+ "last_updated": "2025-08-15T20:06:52.289232",
+ "total_entries": 620
}
\ No newline at end of file
diff --git a/frontend/src/components/CsvFileBrowser.jsx b/frontend/src/components/CsvFileBrowser.jsx
new file mode 100644
index 0000000..9ad74f2
--- /dev/null
+++ b/frontend/src/components/CsvFileBrowser.jsx
@@ -0,0 +1,737 @@
+import React, { useState, useEffect } from 'react'
+import {
+ Box,
+ VStack,
+ HStack,
+ Heading,
+ Text,
+ Input,
+ Button,
+ Card,
+ CardBody,
+ CardHeader,
+ Flex,
+ Spacer,
+ Badge,
+ Spinner,
+ useToast,
+ Accordion,
+ AccordionItem,
+ AccordionButton,
+ AccordionPanel,
+ AccordionIcon,
+ Checkbox,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ IconButton,
+ Tooltip,
+ Alert,
+ AlertIcon,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ ModalCloseButton,
+ useDisclosure,
+ FormControl,
+ FormLabel,
+ Collapse,
+ Tag,
+ TagLabel,
+ Wrap,
+ WrapItem,
+ useColorModeValue
+} from '@chakra-ui/react'
+import {
+ FaPlay,
+ FaFileExcel,
+ FaFolder,
+ FaFile,
+ FaSearch,
+ FaFilter,
+ FaCalendar,
+ FaDatabase,
+ FaSync,
+ FaCog
+} from 'react-icons/fa'
+import * as api from '../services/api'
+
+// Filter functions
+const filterFiles = (files, query, selectedDatasets, selectedDates) => {
+ if (!files) return []
+
+ return files.filter(file => {
+ // Text search in filename
+ const matchesQuery = !query ||
+ file.name.toLowerCase().includes(query.toLowerCase()) ||
+ file.dataset.toLowerCase().includes(query.toLowerCase())
+
+ // Dataset filter
+ const matchesDataset = selectedDatasets.length === 0 ||
+ selectedDatasets.includes(file.dataset)
+
+ // Date filter
+ const matchesDate = selectedDates.length === 0 ||
+ selectedDates.includes(file.date)
+
+ return matchesQuery && matchesDataset && matchesDate
+ })
+}
+
+// Rebuild tree structure from filtered files
+const rebuildTreeFromFilteredFiles = (filteredFiles) => {
+ if (!filteredFiles || filteredFiles.length === 0) return []
+
+ const treeMap = new Map()
+
+ filteredFiles.forEach(file => {
+ const dateKey = file.date
+ const datasetKey = `${file.date}_${file.dataset}`
+
+ // Create date node if it doesn't exist
+ if (!treeMap.has(dateKey)) {
+ treeMap.set(dateKey, {
+ id: `date_${dateKey}`,
+ name: `๐
${dateKey}`,
+ value: dateKey,
+ type: "date",
+ children: new Map()
+ })
+ }
+
+ const dateNode = treeMap.get(dateKey)
+
+ // Create dataset node if it doesn't exist under this date
+ if (!dateNode.children.has(datasetKey)) {
+ dateNode.children.set(datasetKey, {
+ id: `dataset_${dateKey}_${file.dataset}`,
+ name: `๐ ${file.dataset}`,
+ value: file.dataset,
+ type: "dataset",
+ children: []
+ })
+ }
+
+ const datasetNode = dateNode.children.get(datasetKey)
+ datasetNode.children.push({
+ id: `file_${file.date}_${file.value}`,
+ name: `๐ ${file.value}`,
+ value: file.value,
+ type: "file",
+ path: file.path,
+ date: file.date,
+ dataset: file.dataset,
+ size: file.size,
+ size_human: file.size_human,
+ modified: file.modified,
+ modified_human: file.modified_human,
+ columns: file.columns,
+ column_count: file.column_count,
+ row_count: file.row_count,
+ preview: file.preview
+ })
+ })
+
+ // Convert Maps to Arrays and sort
+ const tree = Array.from(treeMap.values()).map(dateNode => ({
+ ...dateNode,
+ children: Array.from(dateNode.children.values()).sort((a, b) => a.value.localeCompare(b.value))
+ })).sort((a, b) => b.value.localeCompare(a.value)) // Sort dates in descending order
+
+ return tree
+}
+
+// PlotJuggler Configuration Modal
+function PlotJugglerConfigModal({ isOpen, onClose, onSave, currentPath }) {
+ const [path, setPath] = useState(currentPath || '')
+ const [testing, setTesting] = useState(false)
+ const toast = useToast()
+
+ useEffect(() => {
+ setPath(currentPath || '')
+ }, [currentPath])
+
+ const testPath = async () => {
+ if (!path) {
+ toast({
+ title: 'โ No path provided',
+ description: 'Please enter a path to test',
+ status: 'error',
+ duration: 2000
+ })
+ return
+ }
+
+ setTesting(true)
+ try {
+ // Simple test by trying to set the path
+ await api.setPlotJugglerPath(path)
+ toast({
+ title: 'โ
Path is valid',
+ description: 'PlotJuggler executable found',
+ status: 'success',
+ duration: 2000
+ })
+ } catch (error) {
+ toast({
+ title: 'โ Invalid path',
+ description: error.message,
+ status: 'error',
+ duration: 3000
+ })
+ } finally {
+ setTesting(false)
+ }
+ }
+
+ const handleSave = () => {
+ onSave(path)
+ onClose()
+ }
+
+ return (
+
+
+
+ ๐ง Configure PlotJuggler Path
+
+
+
+
+ PlotJuggler Executable Path
+ setPath(e.target.value)}
+ placeholder="C:\Program Files\PlotJuggler\plotjuggler.exe"
+ />
+
+
+
+
+
+ The system will automatically search common locations for PlotJuggler.
+ You only need to set this if PlotJuggler is installed in a custom location.
+
+
+
+
+ Common locations:
+
โข C:\Program Files\PlotJuggler\plotjuggler.exe
+
โข C:\Program Files (x86)\PlotJuggler\plotjuggler.exe
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// File Tree Component (Accordion-based since TreeView might not be available)
+function FileTree({ tree, selectedFiles, onFileToggle, expandedItems, onToggleExpand }) {
+ const cardBg = useColorModeValue('white', 'gray.700')
+
+ if (!tree || tree.length === 0) {
+ return (
+
+ No CSV files found
+
+ )
+ }
+
+ return (
+
+ {tree.map((dateNode, dateIndex) => (
+
+
+
+
+
+ {dateNode.name}
+
+ {dateNode.children?.reduce((sum, dataset) => sum + (dataset.children?.length || 0), 0)} files
+
+
+
+
+
+
+
+ {dateNode.children?.map((datasetNode, datasetIndex) => (
+
+
+
+
+
+ {datasetNode.name}
+
+ {datasetNode.children?.length || 0} files
+
+
+
+
+
+
+
+ {datasetNode.children?.map((fileNode) => (
+
+
+
+ onFileToggle(fileNode)}
+ />
+
+
+
+ {fileNode.value}
+
+
+
+ {fileNode.size_human}
+
+
+ {fileNode.row_count} rows
+
+
+ {fileNode.column_count} cols
+
+
+
+ ๐ {fileNode.preview}
+
+
+
+
+ }
+ colorScheme="blue"
+ variant="outline"
+ onClick={() => onFileToggle(fileNode, 'plotjuggler')}
+ />
+
+
+ }
+ colorScheme="green"
+ variant="outline"
+ onClick={() => onFileToggle(fileNode, 'excel')}
+ />
+
+
+
+
+
+ ))}
+
+
+
+ ))}
+
+
+
+ ))}
+
+ )
+}
+
+// Main CSV File Browser Component
+export default function CsvFileBrowser() {
+ const [loading, setLoading] = useState(true)
+ const [files, setFiles] = useState([])
+ const [originalTree, setOriginalTree] = useState([])
+ const [filteredTree, setFilteredTree] = useState([])
+ const [filteredFiles, setFilteredFiles] = useState([])
+ const [selectedFiles, setSelectedFiles] = useState(new Set())
+ const [searchQuery, setSearchQuery] = useState('')
+ const [selectedDatasets, setSelectedDatasets] = useState([])
+ const [selectedDates, setSelectedDates] = useState([])
+ const [expandedItems, setExpandedItems] = useState([])
+ const [plotjugglerPath, setPlotjugglerPath] = useState(null)
+
+ const toast = useToast()
+ const { isOpen: isConfigOpen, onOpen: onConfigOpen, onClose: onConfigClose } = useDisclosure()
+
+ // Load CSV files
+ const loadFiles = async () => {
+ try {
+ setLoading(true)
+ const response = await api.getCsvFiles()
+ setFiles(response.files || [])
+ setOriginalTree(response.tree || [])
+ setFilteredTree(response.tree || [])
+
+ toast({
+ title: 'โ
Files loaded',
+ description: `Found ${response.total_files || 0} CSV files`,
+ status: 'success',
+ duration: 2000
+ })
+ } catch (error) {
+ toast({
+ title: 'โ Failed to load files',
+ description: error.message,
+ status: 'error',
+ duration: 3000
+ })
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // Load PlotJuggler path
+ const loadPlotJugglerPath = async () => {
+ try {
+ const response = await api.getPlotJugglerPath()
+ setPlotjugglerPath(response.path)
+ } catch (error) {
+ console.warn('PlotJuggler path not configured')
+ }
+ }
+
+ // Handle file selection
+ const handleFileToggle = async (fileNode, action = 'select') => {
+ if (action === 'plotjuggler') {
+ // Open single file in PlotJuggler
+ try {
+ await api.launchPlotJuggler([fileNode.path])
+ toast({
+ title: '๐ PlotJuggler launched',
+ description: `Opened ${fileNode.value}`,
+ status: 'success',
+ duration: 2000
+ })
+ } catch (error) {
+ toast({
+ title: 'โ Failed to launch PlotJuggler',
+ description: error.message,
+ status: 'error',
+ duration: 3000
+ })
+ }
+ } else if (action === 'excel') {
+ // Open in Excel
+ try {
+ await api.openCsvInExcel(fileNode.path)
+ toast({
+ title: '๐ File opened',
+ description: `Opened ${fileNode.value} in Excel`,
+ status: 'success',
+ duration: 2000
+ })
+ } catch (error) {
+ toast({
+ title: 'โ Failed to open file',
+ description: error.message,
+ status: 'error',
+ duration: 3000
+ })
+ }
+ } else {
+ // Toggle selection
+ const newSelected = new Set(selectedFiles)
+ if (newSelected.has(fileNode.path)) {
+ newSelected.delete(fileNode.path)
+ } else {
+ newSelected.add(fileNode.path)
+ }
+ setSelectedFiles(newSelected)
+ }
+ }
+
+ // Launch PlotJuggler with selected files
+ const launchSelectedFiles = async () => {
+ if (selectedFiles.size === 0) {
+ toast({
+ title: 'โ ๏ธ No files selected',
+ description: 'Please select files to open in PlotJuggler',
+ status: 'warning',
+ duration: 2000
+ })
+ return
+ }
+
+ try {
+ const filePaths = Array.from(selectedFiles)
+ await api.launchPlotJuggler(filePaths)
+ toast({
+ title: '๐ PlotJuggler launched',
+ description: `Opened ${filePaths.length} file(s)`,
+ status: 'success',
+ duration: 2000
+ })
+ } catch (error) {
+ toast({
+ title: 'โ Failed to launch PlotJuggler',
+ description: error.message,
+ status: 'error',
+ duration: 3000
+ })
+ }
+ }
+
+ // Save PlotJuggler path
+ const savePlotJugglerPath = async (path) => {
+ try {
+ await api.setPlotJugglerPath(path)
+ setPlotjugglerPath(path)
+ toast({
+ title: 'โ
PlotJuggler path saved',
+ status: 'success',
+ duration: 2000
+ })
+ } catch (error) {
+ toast({
+ title: 'โ Failed to save path',
+ description: error.message,
+ status: 'error',
+ duration: 3000
+ })
+ }
+ }
+
+ // Get unique datasets and dates for filtering
+ const availableDatasets = [...new Set(files.map(f => f.dataset))].sort()
+ const availableDates = [...new Set(files.map(f => f.date))].sort().reverse()
+
+ // Filter files and rebuild tree based on search and filters
+ useEffect(() => {
+ const filtered = filterFiles(files, searchQuery, selectedDatasets, selectedDates)
+ setFilteredFiles(filtered)
+
+ // Rebuild tree with filtered files
+ const newFilteredTree = rebuildTreeFromFilteredFiles(filtered)
+ setFilteredTree(newFilteredTree)
+ }, [files, searchQuery, selectedDatasets, selectedDates])
+
+ // Load data on mount
+ useEffect(() => {
+ loadFiles()
+ loadPlotJugglerPath()
+ }, [])
+
+ if (loading) {
+ return (
+
+
+
+
+ Loading CSV files...
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Header */}
+
+ ๐ CSV File Browser
+
+
+
+ }
+ size="sm"
+ variant="outline"
+ onClick={onConfigOpen}
+ />
+
+ }
+ onClick={loadFiles}
+ >
+ Refresh
+
+
+
+
+ {/* PlotJuggler Status */}
+ {plotjugglerPath && (
+
+
+
+ PlotJuggler found: {plotjugglerPath}
+
+
+ )}
+
+ {/* Search and Filters */}
+
+
+ ๐ Search & Filter
+
+
+
+ {/* Search */}
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+ {/* Filters */}
+
+
+ ๐ Datasets
+
+ {availableDatasets.map(dataset => (
+
+ {
+ if (selectedDatasets.includes(dataset)) {
+ setSelectedDatasets(selectedDatasets.filter(d => d !== dataset))
+ } else {
+ setSelectedDatasets([...selectedDatasets, dataset])
+ }
+ }}
+ >
+ {dataset}
+
+
+ ))}
+
+
+
+
+ ๐
Dates
+
+ {availableDates.map(date => (
+
+ {
+ if (selectedDates.includes(date)) {
+ setSelectedDates(selectedDates.filter(d => d !== date))
+ } else {
+ setSelectedDates([...selectedDates, date])
+ }
+ }}
+ >
+ {date}
+
+
+ ))}
+
+
+
+
+ {/* Clear filters */}
+ {(searchQuery || selectedDatasets.length > 0 || selectedDates.length > 0) && (
+
+ )}
+
+
+
+
+ {/* Selected Files Actions */}
+ {selectedFiles.size > 0 && (
+
+
+
+
+ {selectedFiles.size} file(s) selected
+
+ }
+ colorScheme="blue"
+ onClick={launchSelectedFiles}
+ >
+ Open in PlotJuggler
+
+
+
+
+
+ )}
+
+ {/* File Tree */}
+
+
+
+ ๐ File Tree
+
+
+ {filteredFiles.length} of {files.length} files
+
+
+
+
+
+
+
+
+ {/* PlotJuggler Config Modal */}
+
+
+ )
+}
diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index bffc438..0daa50a 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -51,6 +51,7 @@ import validator from '@rjsf/validator-ajv8'
import PlotManager from '../components/PlotManagerSimple'
import allWidgets from '../components/widgets/AllWidgets'
import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate'
+import CsvFileBrowser from '../components/CsvFileBrowser'
import { VariableProvider, useVariableContext } from '../contexts/VariableContext'
import * as api from '../services/api'
import { useCoordinatedPolling } from '../hooks/useCoordinatedConnection'
@@ -1137,7 +1138,8 @@ function DashboardContent() {
๐ง Configuration
๐ Datasets
๐ Plotting
- ๐ Events
+ ๏ฟฝ CSV Files
+ ๏ฟฝ๐ Events
@@ -1163,6 +1165,11 @@ function DashboardContent() {
+
+ {/* CSV File Browser Section */}
+
+
+
{/* Events Section */}
1:
+ # If there's a second part, it might be part of dataset name
+ parts = filename.split("_")
+ if not parts[-1].replace(".csv", "").isdigit():
+ dataset_name = "_".join(parts[:-1])
+
+ # Get file info
+ try:
+ stat = os.stat(file_path)
+ file_size = stat.st_size
+ file_mtime = datetime.fromtimestamp(stat.st_mtime)
+
+ # Try to get CSV info (first 3 columns and row count)
+ csv_info = get_csv_file_info(file_path)
+
+ file_info = {
+ "id": f"file_{date_dir}_{filename}",
+ "name": f"๐ {filename}",
+ "value": filename,
+ "type": "file",
+ "path": file_path,
+ "date": date_dir,
+ "dataset": dataset_name,
+ "size": file_size,
+ "size_human": format_file_size(file_size),
+ "modified": file_mtime.isoformat(),
+ "modified_human": file_mtime.strftime("%H:%M:%S"),
+ **csv_info,
+ }
+
+ files_flat.append(file_info)
+
+ if dataset_name not in datasets:
+ datasets[dataset_name] = {
+ "id": f"dataset_{date_dir}_{dataset_name}",
+ "name": f"๐ {dataset_name}",
+ "value": dataset_name,
+ "type": "dataset",
+ "children": [],
+ }
+
+ datasets[dataset_name]["children"].append(file_info)
+
+ except Exception as e:
+ print(f"Error processing file {filename}: {e}")
+ continue
+
+ # Add datasets to date node
+ date_node["children"] = list(datasets.values())
+ if date_node["children"]: # Only add dates that have files
+ file_tree.append(date_node)
+
+ # Sort by date (newest first)
+ file_tree.sort(key=lambda x: x["value"], reverse=True)
+
+ return jsonify(
+ {"files": files_flat, "tree": file_tree, "total_files": len(files_flat)}
+ )
+
+ except Exception as e:
+ return jsonify({"error": str(e), "files": [], "tree": []}), 500
+
+
+def get_csv_file_info(file_path):
+ """Get CSV file information: first 3 columns and row count"""
+ try:
+ import csv
+
+ with open(file_path, "r", encoding="utf-8") as f:
+ reader = csv.reader(f)
+ header = next(reader, [])
+
+ # Count rows (approximately, for performance)
+ row_count = sum(1 for _ in reader)
+
+ # Get first 3 column names
+ first_columns = header[:3] if header else []
+
+ return {
+ "columns": first_columns,
+ "column_count": len(header),
+ "row_count": row_count,
+ "preview": ", ".join(first_columns[:3]),
+ }
+ except Exception:
+ return {"columns": [], "column_count": 0, "row_count": 0, "preview": "Unknown"}
+
+
+def format_file_size(size_bytes):
+ """Format file size in human readable format"""
+ if size_bytes == 0:
+ return "0 B"
+ size_names = ["B", "KB", "MB", "GB"]
+ import math
+
+ i = int(math.floor(math.log(size_bytes, 1024)))
+ p = math.pow(1024, i)
+ s = round(size_bytes / p, 2)
+ return f"{s} {size_names[i]}"
+
+
+@app.route("/api/plotjuggler/launch", methods=["POST"])
+def launch_plotjuggler():
+ """Launch PlotJuggler with selected CSV files"""
+ try:
+ data = request.get_json()
+ file_paths = data.get("files", [])
+
+ if not file_paths:
+ return jsonify({"error": "No files provided"}), 400
+
+ # Get PlotJuggler path from system state
+ plotjuggler_path = get_plotjuggler_path()
+ if not plotjuggler_path:
+ return jsonify({"error": "PlotJuggler not found"}), 404
+
+ # Launch PlotJuggler with files
+ import subprocess
+
+ if len(file_paths) == 1:
+ # Single file
+ cmd = [plotjuggler_path, "--nosplash", "--datafile", file_paths[0]]
+ else:
+ # Multiple files - launch separate instances
+ commands = []
+ for file_path in file_paths:
+ commands.append(
+ [plotjuggler_path, "--nosplash", "--datafile", file_path]
+ )
+
+ # Execute all commands
+ for cmd in commands:
+ subprocess.Popen(cmd, shell=True)
+
+ return jsonify(
+ {
+ "success": True,
+ "message": f"Launched {len(file_paths)} PlotJuggler instances",
+ "commands": [" ".join(cmd) for cmd in commands],
+ }
+ )
+
+ # Execute single command
+ subprocess.Popen(cmd, shell=True)
+
+ return jsonify(
+ {
+ "success": True,
+ "message": "PlotJuggler launched successfully",
+ "command": " ".join(cmd),
+ }
+ )
+
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+
+@app.route("/api/plotjuggler/path", methods=["GET"])
+def get_plotjuggler_path_endpoint():
+ """Get PlotJuggler executable path"""
+ try:
+ path = get_plotjuggler_path()
+ return jsonify({"path": path, "found": path is not None})
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+
+@app.route("/api/plotjuggler/path", methods=["POST"])
+def set_plotjuggler_path():
+ """Set PlotJuggler executable path"""
+ try:
+ data = request.get_json()
+ path = data.get("path", "")
+
+ if not path or not os.path.exists(path):
+ return jsonify({"error": "Invalid path provided"}), 400
+
+ # Save to system state
+ save_plotjuggler_path(path)
+
+ return jsonify(
+ {"success": True, "message": "PlotJuggler path saved", "path": path}
+ )
+
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+
+def get_plotjuggler_path():
+ """Get PlotJuggler executable path, search if not found"""
+ try:
+ # Load from system state
+ state_file = resource_path("system_state.json")
+ if os.path.exists(state_file):
+ with open(state_file, "r") as f:
+ state = json.load(f)
+ saved_path = state.get("plotjuggler_path")
+ if saved_path and os.path.exists(saved_path):
+ return saved_path
+
+ # Search for PlotJuggler
+ search_paths = [
+ r"C:\Program Files\PlotJuggler\plotjuggler.exe",
+ r"C:\Program Files (x86)\PlotJuggler\plotjuggler.exe",
+ r"C:\PlotJuggler\plotjuggler.exe",
+ ]
+
+ for path in search_paths:
+ if os.path.exists(path):
+ save_plotjuggler_path(path)
+ return path
+
+ return None
+
+ except Exception as e:
+ print(f"Error getting PlotJuggler path: {e}")
+ return None
+
+
+def save_plotjuggler_path(path):
+ """Save PlotJuggler path to system state"""
+ try:
+ state_file = resource_path("system_state.json")
+ state = {}
+
+ if os.path.exists(state_file):
+ with open(state_file, "r") as f:
+ state = json.load(f)
+
+ state["plotjuggler_path"] = path
+ state["last_update"] = datetime.now().isoformat()
+
+ with open(state_file, "w") as f:
+ json.dump(state, f, indent=4)
+
+ except Exception as e:
+ print(f"Error saving PlotJuggler path: {e}")
+
+
+@app.route("/api/csv/open-excel", methods=["POST"])
+def open_csv_in_excel():
+ """Open CSV file in Excel (or default spreadsheet app)"""
+ try:
+ data = request.get_json()
+ file_path = data.get("file_path", "")
+
+ if not file_path or not os.path.exists(file_path):
+ return jsonify({"error": "File not found"}), 404
+
+ import subprocess
+ import platform
+
+ system = platform.system()
+ if system == "Windows":
+ # Use os.startfile on Windows
+ os.startfile(file_path)
+ elif system == "Darwin": # macOS
+ subprocess.Popen(["open", file_path])
+ else: # Linux
+ subprocess.Popen(["xdg-open", file_path])
+
+ return jsonify(
+ {
+ "success": True,
+ "message": f"Opened {os.path.basename(file_path)} in default application",
+ }
+ )
+
+ except Exception as e:
+ return jsonify({"error": str(e)}), 500
+
+
if __name__ == "__main__":
try:
# Initialize streamer instance
diff --git a/system_state.json b/system_state.json
index 1caccae..e134174 100644
--- a/system_state.json
+++ b/system_state.json
@@ -3,11 +3,12 @@
"should_connect": true,
"should_stream": false,
"active_datasets": [
- "DAR",
+ "Fast",
"Test",
- "Fast"
+ "DAR"
]
},
"auto_recovery_enabled": true,
- "last_update": "2025-08-15T19:53:52.606916"
+ "last_update": "2025-08-15T20:15:35.459362",
+ "plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}
\ No newline at end of file