feat: Implement CSV File Browser component, add API endpoints for CSV file management, and update system state for dataset activation

This commit is contained in:
Miguel 2025-08-15 20:15:27 +02:00
parent 4481eb33a7
commit 60db337284
6 changed files with 1149 additions and 6 deletions

View File

@ -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
}

View File

@ -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 (
<Modal isOpen={isOpen} onClose={onClose} size="lg">
<ModalOverlay />
<ModalContent>
<ModalHeader>🔧 Configure PlotJuggler Path</ModalHeader>
<ModalCloseButton />
<ModalBody>
<VStack spacing={4}>
<FormControl>
<FormLabel>PlotJuggler Executable Path</FormLabel>
<Input
value={path}
onChange={(e) => setPath(e.target.value)}
placeholder="C:\Program Files\PlotJuggler\plotjuggler.exe"
/>
</FormControl>
<Alert status="info">
<AlertIcon />
<Text fontSize="sm">
The system will automatically search common locations for PlotJuggler.
You only need to set this if PlotJuggler is installed in a custom location.
</Text>
</Alert>
<Text fontSize="sm" color="gray.500">
Common locations:
<br /> C:\Program Files\PlotJuggler\plotjuggler.exe
<br /> C:\Program Files (x86)\PlotJuggler\plotjuggler.exe
</Text>
</VStack>
</ModalBody>
<ModalFooter>
<HStack spacing={2}>
<Button
variant="outline"
onClick={testPath}
isLoading={testing}
loadingText="Testing..."
>
🧪 Test Path
</Button>
<Button colorScheme="blue" onClick={handleSave}>
💾 Save
</Button>
<Button variant="ghost" onClick={onClose}>
Cancel
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
)
}
// 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 (
<Box textAlign="center" py={8}>
<Text color="gray.500">No CSV files found</Text>
</Box>
)
}
return (
<Accordion
allowMultiple
index={expandedItems}
onChange={onToggleExpand}
>
{tree.map((dateNode, dateIndex) => (
<AccordionItem key={dateNode.id} border="1px solid" borderColor="gray.200" mb={2}>
<AccordionButton>
<Box flex="1" textAlign="left">
<HStack>
<FaCalendar />
<Text fontWeight="bold">{dateNode.name}</Text>
<Badge colorScheme="blue">
{dateNode.children?.reduce((sum, dataset) => sum + (dataset.children?.length || 0), 0)} files
</Badge>
</HStack>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<Accordion allowMultiple>
{dateNode.children?.map((datasetNode, datasetIndex) => (
<AccordionItem key={datasetNode.id}>
<AccordionButton>
<Box flex="1" textAlign="left">
<HStack>
<FaDatabase />
<Text>{datasetNode.name}</Text>
<Badge colorScheme="green">
{datasetNode.children?.length || 0} files
</Badge>
</HStack>
</Box>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pb={4}>
<VStack spacing={2} align="stretch">
{datasetNode.children?.map((fileNode) => (
<Card key={fileNode.id} size="sm" bg={cardBg}>
<CardBody py={2}>
<HStack spacing={3}>
<Checkbox
isChecked={selectedFiles.has(fileNode.path)}
onChange={() => onFileToggle(fileNode)}
/>
<FaFile />
<VStack spacing={1} align="start" flex={1}>
<Text fontSize="sm" fontWeight="medium">
{fileNode.value}
</Text>
<HStack spacing={2}>
<Badge size="sm" colorScheme="gray">
{fileNode.size_human}
</Badge>
<Badge size="sm" colorScheme="blue">
{fileNode.row_count} rows
</Badge>
<Badge size="sm" colorScheme="green">
{fileNode.column_count} cols
</Badge>
</HStack>
<Text fontSize="xs" color="gray.500">
📊 {fileNode.preview}
</Text>
</VStack>
<VStack spacing={1}>
<Tooltip label="Open in PlotJuggler">
<IconButton
size="xs"
icon={<FaPlay />}
colorScheme="blue"
variant="outline"
onClick={() => onFileToggle(fileNode, 'plotjuggler')}
/>
</Tooltip>
<Tooltip label="Open in Excel">
<IconButton
size="xs"
icon={<FaFileExcel />}
colorScheme="green"
variant="outline"
onClick={() => onFileToggle(fileNode, 'excel')}
/>
</Tooltip>
</VStack>
</HStack>
</CardBody>
</Card>
))}
</VStack>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
</AccordionPanel>
</AccordionItem>
))}
</Accordion>
)
}
// 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 (
<Card>
<CardBody>
<Flex align="center" justify="center" py={8}>
<Spinner size="xl" mr={4} />
<Text fontSize="lg">Loading CSV files...</Text>
</Flex>
</CardBody>
</Card>
)
}
return (
<VStack spacing={6} align="stretch">
{/* Header */}
<Flex align="center">
<Heading size="lg">📁 CSV File Browser</Heading>
<Spacer />
<HStack spacing={2}>
<Tooltip label="Configure PlotJuggler">
<IconButton
icon={<FaCog />}
size="sm"
variant="outline"
onClick={onConfigOpen}
/>
</Tooltip>
<Button
size="sm"
variant="outline"
leftIcon={<FaSync />}
onClick={loadFiles}
>
Refresh
</Button>
</HStack>
</Flex>
{/* PlotJuggler Status */}
{plotjugglerPath && (
<Alert status="success">
<AlertIcon />
<Text fontSize="sm">
PlotJuggler found: <code>{plotjugglerPath}</code>
</Text>
</Alert>
)}
{/* Search and Filters */}
<Card>
<CardHeader>
<Heading size="md">🔍 Search & Filter</Heading>
</CardHeader>
<CardBody>
<VStack spacing={4} align="stretch">
{/* Search */}
<HStack>
<FaSearch />
<Input
placeholder="Search files by name or dataset..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</HStack>
{/* Filters */}
<HStack spacing={4} align="start">
<Box flex={1}>
<Text fontSize="sm" fontWeight="bold" mb={2}>📊 Datasets</Text>
<Wrap>
{availableDatasets.map(dataset => (
<WrapItem key={dataset}>
<Tag
size="sm"
variant={selectedDatasets.includes(dataset) ? 'solid' : 'outline'}
colorScheme="blue"
cursor="pointer"
onClick={() => {
if (selectedDatasets.includes(dataset)) {
setSelectedDatasets(selectedDatasets.filter(d => d !== dataset))
} else {
setSelectedDatasets([...selectedDatasets, dataset])
}
}}
>
<TagLabel>{dataset}</TagLabel>
</Tag>
</WrapItem>
))}
</Wrap>
</Box>
<Box flex={1}>
<Text fontSize="sm" fontWeight="bold" mb={2}>📅 Dates</Text>
<Wrap>
{availableDates.map(date => (
<WrapItem key={date}>
<Tag
size="sm"
variant={selectedDates.includes(date) ? 'solid' : 'outline'}
colorScheme="green"
cursor="pointer"
onClick={() => {
if (selectedDates.includes(date)) {
setSelectedDates(selectedDates.filter(d => d !== date))
} else {
setSelectedDates([...selectedDates, date])
}
}}
>
<TagLabel>{date}</TagLabel>
</Tag>
</WrapItem>
))}
</Wrap>
</Box>
</HStack>
{/* Clear filters */}
{(searchQuery || selectedDatasets.length > 0 || selectedDates.length > 0) && (
<Button
size="sm"
variant="outline"
onClick={() => {
setSearchQuery('')
setSelectedDatasets([])
setSelectedDates([])
}}
>
🗑 Clear Filters
</Button>
)}
</VStack>
</CardBody>
</Card>
{/* Selected Files Actions */}
{selectedFiles.size > 0 && (
<Card>
<CardBody>
<HStack spacing={4}>
<Text fontWeight="bold">
{selectedFiles.size} file(s) selected
</Text>
<Button
leftIcon={<FaPlay />}
colorScheme="blue"
onClick={launchSelectedFiles}
>
Open in PlotJuggler
</Button>
<Button
variant="outline"
onClick={() => setSelectedFiles(new Set())}
>
Clear Selection
</Button>
</HStack>
</CardBody>
</Card>
)}
{/* File Tree */}
<Card>
<CardHeader>
<Flex align="center">
<Heading size="md">📂 File Tree</Heading>
<Spacer />
<Badge colorScheme="blue">
{filteredFiles.length} of {files.length} files
</Badge>
</Flex>
</CardHeader>
<CardBody>
<FileTree
tree={filteredTree}
selectedFiles={selectedFiles}
onFileToggle={handleFileToggle}
expandedItems={expandedItems}
onToggleExpand={setExpandedItems}
/>
</CardBody>
</Card>
{/* PlotJuggler Config Modal */}
<PlotJugglerConfigModal
isOpen={isConfigOpen}
onClose={onConfigClose}
onSave={savePlotJugglerPath}
currentPath={plotjugglerPath}
/>
</VStack>
)
}

View File

@ -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() {
<Tab>🔧 Configuration</Tab>
<Tab>📊 Datasets</Tab>
<Tab>📈 Plotting</Tab>
<Tab>📋 Events</Tab>
<Tab><EFBFBD> CSV Files</Tab>
<Tab><EFBFBD>📋 Events</Tab>
</TabList>
<TabPanels>
@ -1163,6 +1165,11 @@ function DashboardContent() {
<PlotManager />
</TabPanel>
<TabPanel p={0} pt={4}>
{/* CSV File Browser Section */}
<CsvFileBrowser />
</TabPanel>
<TabPanel p={0} pt={4}>
{/* Events Section */}
<EventsDisplay

View File

@ -227,4 +227,53 @@ export async function controlPlotSession(sessionId, action) {
return await controlPlot(sessionId, action)
}
// CSV File Browser API
export async function getCsvFiles() {
const res = await fetch(`${BASE_URL}/api/csv/files`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
}
// PlotJuggler integration
export async function launchPlotJuggler(filePaths) {
const res = await fetch(`${BASE_URL}/api/plotjuggler/launch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ files: filePaths })
})
return toJsonOrThrow(res)
}
export async function getPlotJugglerPath() {
const res = await fetch(`${BASE_URL}/api/plotjuggler/path`, { headers: { 'Accept': 'application/json' } })
return toJsonOrThrow(res)
}
export async function setPlotJugglerPath(path) {
const res = await fetch(`${BASE_URL}/api/plotjuggler/path`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ path })
})
return toJsonOrThrow(res)
}
// Open CSV in Excel
export async function openCsvInExcel(filePath) {
const res = await fetch(`${BASE_URL}/api/csv/open-excel`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ file_path: filePath })
})
return toJsonOrThrow(res)
}

308
main.py
View File

@ -2822,6 +2822,314 @@ def process_symbol_variables():
return jsonify({"success": False, "error": str(e)}), 500
# CSV File Browser Endpoints
@app.route("/api/csv/files", methods=["GET"])
def get_csv_files():
"""Get structured list of CSV files organized by dataset, date, and hour"""
try:
records_dir = resource_path("records")
if not os.path.exists(records_dir):
return jsonify({"files": [], "tree": []})
file_tree = []
files_flat = []
# Scan records directory
for date_dir in os.listdir(records_dir):
date_path = os.path.join(records_dir, date_dir)
if not os.path.isdir(date_path):
continue
date_node = {
"id": f"date_{date_dir}",
"name": f"📅 {date_dir}",
"value": date_dir,
"type": "date",
"children": [],
}
# Group files by dataset
datasets = {}
for filename in os.listdir(date_path):
if not filename.endswith(".csv"):
continue
file_path = os.path.join(date_path, filename)
# Extract dataset name (everything before the first underscore + number)
dataset_name = filename.split("_")[0]
if len(filename.split("_")) > 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

View File

@ -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"
}