Compare commits

..

3 Commits

Author SHA1 Message Date
Miguel 9dbc0bb866 feat: Enhance CsvViewer hover effects with dynamic background and text colors 2025-08-31 22:11:17 +02:00
Miguel b36cbf386b feat: Implement CSV Viewer with pagination and sticky header
- Added CsvViewer component to display CSV data with pagination support.
- Integrated API call to fetch CSV data with pagination.
- Enhanced user experience with sticky headers and improved scrolling.
- Implemented search functionality to filter displayed data.
- Added export feature to download current page data as CSV.
- Updated CsvFileBrowser to include a button for viewing CSV files.
- Improved UI with responsive design and better styling for controls.
- Fixed scrolling issues by reverting to traditional Chakra UI table syntax.
- Updated system state to reflect changes in connection settings.
2025-08-31 22:04:12 +02:00
Miguel 550fc78085 feat: Update application events and system state for improved dataset management and logging 2025-08-30 23:29:57 +02:00
8 changed files with 8170 additions and 7450 deletions

View File

@ -0,0 +1,98 @@
# CSV Viewer - Mejoras de Interfaz
## ✅ **Cambios Implementados**
### **1. 📌 Cabecera Siempre Visible (Sticky Header)**
- La cabecera de la tabla permanece fija mientras haces scroll vertical
- Aplicado `position: sticky` con `top: 0` y `zIndex: 10`
- Añadida sombra sutil para mejorar la separación visual
- Fondo consistente que se mantiene durante el scroll
### **2. ↔️ Scroll Horizontal Mejorado**
- Scroll horizontal fluido para CSVs con muchas columnas
- Barras de scroll personalizadas con mejor estilo
- Scroll tanto horizontal como vertical independientes
- Soporte para scroll en esquinas
### **3. 🎨 Mejoras de UX**
- **Ancho mínimo/máximo de columnas**: 120px - 300px
- **Texto truncado** con tooltip al hacer hover
- **Hover effects** en las filas para mejor navegación
- **Responsive design** con flexWrap en controles
- **Modal optimizado** para ocupar 98% de la pantalla
### **4. 📱 Responsive Design**
- Controles que se adaptan en pantallas pequeñas
- Nombre de archivo truncado con tooltip
- Badges que se reorganizan automáticamente
- Modal que se adapta al tamaño de pantalla
## 🔧 **Detalles Técnicos**
### **Estructura del CSS Aplicado:**
```jsx
// Contenedor principal con scroll dual
<Box
overflowX="auto"
overflowY="auto"
sx={{
'&::-webkit-scrollbar': { width: '12px', height: '12px' },
'&::-webkit-scrollbar-thumb': { background: 'gray.400' }
}}
>
// Cabecera sticky
<Thead
position="sticky"
top="0"
zIndex="10"
boxShadow="0 2px 4px rgba(0,0,0,0.1)"
>
// Columnas con ancho controlado
<Th
minWidth="120px"
maxWidth="300px"
textOverflow="ellipsis"
overflow="hidden"
/>
```
### **Características de las Columnas:**
- **Ancho mínimo**: 120px (evita columnas muy estrechas)
- **Ancho máximo**: 300px (evita columnas excesivamente anchas)
- **Truncado inteligente**: Texto cortado con "..." y tooltip completo
- **Título completo**: Al hacer hover se muestra el contenido completo
### **Modal Optimizado:**
- **Tamaño**: 98vw × 98vh (máximo uso de pantalla)
- **Margen**: 2px para evitar tocar bordes
- **No cierre por overlay**: Solo ESC o botón X
- **Flex layout**: Distribución vertical eficiente
## 🚀 **Cómo Usar**
1. **Abrir CSV**: Haz clic en el botón 👁️ junto a cualquier archivo CSV
2. **Navegación vertical**: Scroll para ver más filas, la cabecera permanece visible
3. **Navegación horizontal**: Scroll horizontal para ver más columnas
4. **Hover en celdas**: Ver contenido completo en tooltip
5. **Cerrar**: ESC o botón X en la esquina superior derecha
## 📊 **Beneficios**
- ✅ **Mejor orientación**: Cabecera siempre visible
- ✅ **Manejo de datos anchos**: Scroll horizontal fluido
- ✅ **Experiencia optimizada**: Sin perderse en datasets grandes
- ✅ **Performance mantenida**: Solo se cargan las filas de la página actual
- ✅ **Responsive**: Funciona bien en diferentes tamaños de pantalla
## 🔄 **Compatibilidad**
- ✅ Navegadores modernos (Chrome, Firefox, Edge, Safari)
- ✅ Dispositivos desktop y tablet
- ✅ CSVs pequeños y grandes (paginación automática)
- ✅ Diferentes codificaciones de archivos
---
**Nota**: Estos cambios mantienen toda la funcionalidad anterior (búsqueda, paginación, exportación) mientras mejoran significativamente la experiencia de navegación en tablas grandes.

139
CSV_VIEWER_SCROLL_FIX.md Normal file
View File

@ -0,0 +1,139 @@
# 🔧 CSV Viewer - Solución de Problemas de Scroll
## ❌ **Problema Identificado**
- El scroll vertical y horizontal no funcionaba
- La nueva sintaxis `Table.Root` / `Table.Header` / `Table.Body` causaba conflictos con el scroll
- La estructura de flexbox no estaba configurada correctamente
## ✅ **Solución Implementada**
### **1. Vuelta a la Sintaxis Tradicional de Chakra UI**
```jsx
// ❌ ANTES (no funcionaba)
<Table.Root size="sm" striped>
<Table.Header>
<Table.Row>
<Table.ColumnHeader>
// ✅ AHORA (funciona perfectamente)
<Table size="sm" variant="striped">
<Thead>
<Tr>
<Th>
```
### **2. Uso de TableContainer para Scroll**
```jsx
<TableContainer
flex="1"
overflowX="auto" // ← Scroll horizontal
overflowY="auto" // ← Scroll vertical
bg={tableBg}
sx={{
'&::-webkit-scrollbar': {
width: '12px',
height: '12px'
}
// ... estilos personalizados
}}
>
```
### **3. Sticky Header Corregido**
```jsx
<Thead
position="sticky"
top="0"
zIndex="10"
bg={headerBg}
boxShadow="0 2px 4px rgba(0,0,0,0.1)"
>
<Tr>
{columns.map((column, index) => (
<Th
position="sticky" // ← Importante para cada header
top="0"
bg={headerBg} // ← Fondo consistente
minWidth="120px"
maxWidth="300px"
// ...
>
```
### **4. Estructura Flex Optimizada**
```jsx
<Card display="flex" flexDirection="column" height="100%">
<CardHeader flexShrink={0}>
{/* Controles que no hacen scroll */}
</CardHeader>
<CardBody
flex="1" // ← Ocupa espacio restante
overflow="hidden" // ← Evita overflow del container
p={0}
display="flex"
flexDirection="column"
>
<TableContainer flex="1"> // ← Área scrolleable
{/* Tabla */}
</TableContainer>
</CardBody>
</Card>
```
## 🎯 **Características Restauradas**
### **✅ Scroll Vertical**
- Funciona correctamente para navegar por las filas
- Cabecera permanece fija en la parte superior
- Scroll suave con barras personalizadas
### **✅ Scroll Horizontal**
- Navega por columnas anchas sin problemas
- Mantiene la alineación de headers y datos
- Barras de scroll horizontal y vertical independientes
### **✅ Sticky Headers**
- Headers siempre visibles durante el scroll
- Fondo consistente que no se ve afectado por el scroll
- Z-index apropiado para estar por encima del contenido
### **✅ Responsive Design**
- Columnas con ancho mínimo/máximo controlado
- Texto truncado con tooltips
- Adaptación automática al tamaño del contenedor
## 🔧 **Elementos Técnicos Claves**
### **Imports Necesarios**
```jsx
import {
Table, // ← Componente principal
Thead, // ← Header tradicional
Tbody, // ← Body tradicional
Tr, // ← Fila tradicional
Th, // ← Header cell tradicional
Td, // ← Data cell tradicional
TableContainer, // ← Container con scroll
// ...
} from '@chakra-ui/react'
```
### **CSS Key Properties**
- `flex="1"` - Para ocupar espacio disponible
- `overflow="hidden"` - En containers padre
- `overflowX/Y="auto"` - En el container scrolleable
- `position="sticky"` - Para headers fijos
- `zIndex="10"` - Para mantener headers encima
## 🚀 **Resultado Final**
- ✅ **Scroll vertical** fluido con headers fijos
- ✅ **Scroll horizontal** para datasets anchos
- ✅ **Performance** mantenida con paginación
- ✅ **UX mejorada** con tooltips y hover effects
- ✅ **Compatibilidad** con navegadores modernos
---
**Nota**: El cambio principal fue abandonar la nueva sintaxis `Table.Root/Header/Body` y volver a la sintaxis tradicional `Table/Thead/Tbody` que tiene mejor soporte para scroll en contenedores flex.

File diff suppressed because it is too large Load Diff

View File

@ -60,9 +60,11 @@ import {
FaDatabase,
FaSync,
FaCog,
FaCopy
FaCopy,
FaEye
} from 'react-icons/fa'
import * as api from '../services/api'
import CsvViewer from './CsvViewer'
// Filter functions
const filterFiles = (files, query, selectedDatasets, selectedDates) => {
@ -330,7 +332,16 @@ function FileTree({ tree, selectedFiles, onFileToggle, expandedItems, onToggleEx
📊 {fileNode.preview}
</Text>
</VStack>
<VStack spacing={1}>
<HStack spacing={1}>
<Tooltip label="View CSV data">
<IconButton
size="xs"
icon={<FaEye />}
colorScheme="orange"
variant="outline"
onClick={() => onFileToggle(fileNode, 'view')}
/>
</Tooltip>
<Tooltip label="Open in PlotJuggler">
<IconButton
size="xs"
@ -358,7 +369,7 @@ function FileTree({ tree, selectedFiles, onFileToggle, expandedItems, onToggleEx
onClick={() => onFileToggle(fileNode, 'copy')}
/>
</Tooltip>
</VStack>
</HStack>
</HStack>
</CardBody>
</Card>
@ -388,6 +399,7 @@ export default function CsvFileBrowser() {
const [selectedDates, setSelectedDates] = useState([])
const [expandedItems, setExpandedItems] = useState([])
const [plotjugglerPath, setPlotjugglerPath] = useState(null)
const [viewingFile, setViewingFile] = useState(null)
const toast = useToast()
const { isOpen: isConfigOpen, onOpen: onConfigOpen, onClose: onConfigClose } = useDisclosure()
@ -431,7 +443,16 @@ export default function CsvFileBrowser() {
// Handle file selection
const handleFileToggle = async (fileNode, action = 'select') => {
if (action === 'plotjuggler') {
if (action === 'view') {
// Open CSV viewer
setViewingFile(fileNode.path)
toast({
title: '👁️ Opening CSV viewer',
description: `Loading ${fileNode.value}...`,
status: 'info',
duration: 2000
})
} else if (action === 'plotjuggler') {
// Open single file in PlotJuggler
try {
const response = await api.launchPlotJuggler([fileNode.path])
@ -472,6 +493,15 @@ export default function CsvFileBrowser() {
duration: 3000
})
}
} else if (action === 'view') {
// Open in CSV viewer
setViewingFile(fileNode.path)
toast({
title: '👁️ Opening CSV viewer',
description: `Loading ${fileNode.value}`,
status: 'info',
duration: 2000
})
} else if (action === 'copy') {
// Copy path to clipboard
try {
@ -847,6 +877,40 @@ export default function CsvFileBrowser() {
onSave={savePlotJugglerPath}
currentPath={plotjugglerPath}
/>
{/* CSV Viewer Modal */}
{viewingFile && (
<Modal
isOpen={true}
onClose={() => setViewingFile(null)}
size="full"
closeOnOverlayClick={false}
closeOnEsc={true}
>
<ModalOverlay bg="blackAlpha.600" />
<ModalContent
maxW="98vw"
maxH="98vh"
m={2}
p={0}
borderRadius="lg"
display="flex"
flexDirection="column"
>
<Box
height="100%"
display="flex"
flexDirection="column"
minHeight="0"
>
<CsvViewer
filePath={viewingFile}
onClose={() => setViewingFile(null)}
/>
</Box>
</ModalContent>
</Modal>
)}
</VStack>
)
}

View File

@ -0,0 +1,490 @@
import React, { useState, useEffect, useMemo } from 'react'
import {
Box,
VStack,
HStack,
Heading,
Text,
Button,
Card,
CardBody,
CardHeader,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Spinner,
useToast,
Badge,
Alert,
AlertIcon,
Flex,
Spacer,
Input,
Select,
InputGroup,
InputLeftElement,
useColorModeValue,
ButtonGroup,
IconButton,
Tooltip
} from '@chakra-ui/react'
import {
FaSearch,
FaChevronLeft,
FaChevronRight,
FaFastBackward,
FaFastForward,
FaDownload,
FaTimes,
FaFileAlt
} from 'react-icons/fa'
import * as api from '../services/api'
// Constants
const PAGE_SIZE_OPTIONS = [50, 100, 200, 500, 1000]
const DEFAULT_PAGE_SIZE = 100
// CSV Data Viewer Component
function CsvViewer({ filePath, onClose }) {
const [data, setData] = useState([])
const [columns, setColumns] = useState([])
const [loading, setLoading] = useState(true)
const [pagination, setPagination] = useState({
page: 1,
page_size: DEFAULT_PAGE_SIZE,
total_rows: 0,
total_pages: 0,
has_next: false,
has_prev: false
})
const [fileInfo, setFileInfo] = useState(null)
const [searchQuery, setSearchQuery] = useState('')
const [pageInput, setPageInput] = useState('1')
const [error, setError] = useState(null)
const toast = useToast()
const cardBg = useColorModeValue('white', 'gray.700')
const tableBg = useColorModeValue('gray.50', 'gray.800')
const headerBg = useColorModeValue('gray.100', 'gray.600')
const hoverBg = useColorModeValue('gray.100', 'gray.600')
const hoverTextColor = useColorModeValue('gray.800', 'white')
// Load CSV data
const loadData = async (page = 1, pageSize = DEFAULT_PAGE_SIZE) => {
try {
setLoading(true)
setError(null)
const response = await api.getCsvData(filePath, page, pageSize)
setData(response.data || [])
setColumns(response.columns || [])
setPagination(response.pagination || {})
setFileInfo(response.file_info || {})
setPageInput(page.toString())
} catch (error) {
setError(error.message)
toast({
title: '❌ Error loading CSV data',
description: error.message,
status: 'error',
duration: 5000
})
} finally {
setLoading(false)
}
}
// Filter data based on search query
const filteredData = useMemo(() => {
if (!searchQuery.trim()) return data
const query = searchQuery.toLowerCase()
return data.filter(row =>
Object.values(row).some(value =>
value?.toString().toLowerCase().includes(query)
)
)
}, [data, searchQuery])
// Navigation functions
const goToPage = (page) => {
if (page >= 1 && page <= pagination.total_pages) {
loadData(page, pagination.page_size)
}
}
const goToFirstPage = () => goToPage(1)
const goToLastPage = () => goToPage(pagination.total_pages)
const goToPrevPage = () => goToPage(pagination.page - 1)
const goToNextPage = () => goToPage(pagination.page + 1)
const handlePageInputChange = (e) => {
setPageInput(e.target.value)
}
const handlePageInputSubmit = (e) => {
e.preventDefault()
const page = parseInt(pageInput)
if (!isNaN(page) && page >= 1 && page <= pagination.total_pages) {
goToPage(page)
} else {
setPageInput(pagination.page.toString())
}
}
const handlePageSizeChange = (newPageSize) => {
const newSize = parseInt(newPageSize)
loadData(1, newSize) // Reset to first page with new size
}
const downloadCurrentData = () => {
try {
const csvContent = [
columns.join(','),
...filteredData.map(row =>
columns.map(col => {
const value = row[col] || ''
// Escape quotes and wrap in quotes if needed
return value.toString().includes(',') || value.toString().includes('"')
? `"${value.toString().replace(/"/g, '""')}"`
: value
}).join(',')
)
].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', `${fileInfo.name}_page_${pagination.page}.csv`)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
toast({
title: '📄 CSV exported',
description: `Page ${pagination.page} exported successfully`,
status: 'success',
duration: 3000
})
} catch (error) {
toast({
title: '❌ Export failed',
description: error.message,
status: 'error',
duration: 3000
})
}
}
// Load data on mount
useEffect(() => {
loadData()
}, [filePath])
if (error) {
return (
<Card bg={cardBg}>
<CardHeader>
<Flex align="center">
<Heading size="md"> Error Loading CSV</Heading>
<Spacer />
<Button onClick={onClose} size="sm" variant="outline">
<FaTimes />
</Button>
</Flex>
</CardHeader>
<CardBody>
<Alert status="error">
<AlertIcon />
<Text>{error}</Text>
</Alert>
</CardBody>
</Card>
)
}
return (
<Card
bg={cardBg}
maxHeight="90vh"
height="90vh"
display="flex"
flexDirection="column"
>
<CardHeader flexShrink={0}>
<VStack spacing={3} align="stretch">
<Flex align="center">
<HStack spacing={2}>
<FaFileAlt />
<Heading size="md">📊 CSV Viewer</Heading>
</HStack>
<Spacer />
<Button onClick={onClose} size="sm" variant="outline">
<FaTimes />
</Button>
</Flex>
{fileInfo && (
<HStack spacing={4} flexWrap="wrap">
<Text fontSize="sm" fontWeight="bold" isTruncated maxW="300px" title={fileInfo.name}>
{fileInfo.name}
</Text>
<Badge colorScheme="blue">
{(fileInfo.size / 1024).toFixed(1)} KB
</Badge>
<Badge colorScheme="green">
{pagination.total_rows} rows
</Badge>
<Badge colorScheme="purple">
{columns.length} columns
</Badge>
{fileInfo.encoding && (
<Badge colorScheme="gray">
{fileInfo.encoding}
</Badge>
)}
</HStack>
)}
{/* Search and Controls */}
<HStack spacing={3} flexWrap="wrap">
<InputGroup maxW="300px">
<InputLeftElement>
<FaSearch />
</InputLeftElement>
<Input
placeholder="Search in current page..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
size="sm"
/>
</InputGroup>
<HStack spacing={2}>
<Text fontSize="sm" whiteSpace="nowrap">Rows per page:</Text>
<Select
value={pagination.page_size}
onChange={(e) => handlePageSizeChange(e.target.value)}
size="sm"
maxW="100px"
>
{PAGE_SIZE_OPTIONS.map(size => (
<option key={size} value={size}>{size}</option>
))}
</Select>
</HStack>
<Button
size="sm"
leftIcon={<FaDownload />}
onClick={downloadCurrentData}
isDisabled={filteredData.length === 0}
variant="outline"
colorScheme="green"
>
Export Page
</Button>
</HStack>
{/* Pagination Controls */}
<HStack spacing={2} justify="center" flexWrap="wrap">
<ButtonGroup size="sm" isAttached variant="outline">
<Tooltip label="First page">
<IconButton
icon={<FaFastBackward />}
onClick={goToFirstPage}
isDisabled={!pagination.has_prev || loading}
/>
</Tooltip>
<Tooltip label="Previous page">
<IconButton
icon={<FaChevronLeft />}
onClick={goToPrevPage}
isDisabled={!pagination.has_prev || loading}
/>
</Tooltip>
</ButtonGroup>
<HStack spacing={1}>
<Text fontSize="sm">Page</Text>
<form onSubmit={handlePageInputSubmit}>
<Input
value={pageInput}
onChange={handlePageInputChange}
size="sm"
maxW="60px"
textAlign="center"
/>
</form>
<Text fontSize="sm">of {pagination.total_pages}</Text>
</HStack>
<ButtonGroup size="sm" isAttached variant="outline">
<Tooltip label="Next page">
<IconButton
icon={<FaChevronRight />}
onClick={goToNextPage}
isDisabled={!pagination.has_next || loading}
/>
</Tooltip>
<Tooltip label="Last page">
<IconButton
icon={<FaFastForward />}
onClick={goToLastPage}
isDisabled={!pagination.has_next || loading}
/>
</Tooltip>
</ButtonGroup>
</HStack>
{searchQuery && (
<Text fontSize="sm" color="gray.500">
Showing {filteredData.length} of {data.length} rows (filtered)
</Text>
)}
</VStack>
</CardHeader>
<CardBody
flex="1"
p={0}
display="flex"
flexDirection="column"
minHeight="0"
overflow="hidden"
>
{loading ? (
<Flex align="center" justify="center" py={8} flex="1">
<Spinner size="xl" mr={4} />
<Text>Loading CSV data...</Text>
</Flex>
) : (
<TableContainer
flex="1"
overflowX="auto"
overflowY="auto"
bg={tableBg}
height="100%"
maxHeight="100%"
sx={{
'&::-webkit-scrollbar': {
width: '12px',
height: '12px'
},
'&::-webkit-scrollbar-track': {
background: 'gray.100',
borderRadius: '6px'
},
'&::-webkit-scrollbar-thumb': {
background: 'gray.400',
borderRadius: '6px',
border: '2px solid transparent',
backgroundClip: 'content-box'
},
'&::-webkit-scrollbar-thumb:hover': {
background: 'gray.600'
},
'&::-webkit-scrollbar-corner': {
background: 'gray.100'
}
}}
>
<Table size="sm" variant="striped">
<Thead
position="sticky"
top="0"
zIndex="10"
bg={headerBg}
boxShadow="0 2px 4px rgba(0,0,0,0.1)"
>
<Tr>
{columns.map((column, index) => (
<Th
key={index}
fontSize="xs"
fontWeight="bold"
whiteSpace="nowrap"
minWidth="120px"
maxWidth="300px"
px={3}
py={3}
textOverflow="ellipsis"
overflow="hidden"
title={column}
bg={headerBg}
borderBottom="2px solid"
borderColor="gray.300"
position="sticky"
top="0"
>
{column}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{filteredData.map((row, rowIndex) => (
<Tr
key={rowIndex}
_hover={{
bg: hoverBg,
'& td': {
color: hoverTextColor
}
}}
>
{columns.map((column, colIndex) => (
<Td
key={colIndex}
fontSize="xs"
whiteSpace="nowrap"
minWidth="120px"
maxWidth="300px"
px={3}
py={2}
textOverflow="ellipsis"
overflow="hidden"
title={row[column] || ''}
>
{row[column] || ''}
</Td>
))}
</Tr>
))}
</Tbody>
</Table>
{filteredData.length === 0 && !loading && (
<Flex
position="absolute"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
align="center"
justify="center"
py={4}
color="gray.500"
bg="white"
borderRadius="md"
px={4}
boxShadow="md"
>
<Text>
{searchQuery ? 'No matching rows found' : 'No data available'}
</Text>
</Flex>
)}
</TableContainer>
)}
</CardBody>
</Card>
)
}
export default CsvViewer

View File

@ -321,6 +321,23 @@ export async function openCsvInExcel(filePath) {
return toJsonOrThrow(res)
}
// Read CSV data with pagination
export async function getCsvData(filePath, page = 1, pageSize = 100) {
const res = await fetch(`${BASE_URL}/api/csv/data`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
file_path: filePath,
page: page,
page_size: pageSize
})
})
return toJsonOrThrow(res)
}
// Configuration API functions
export async function getConfig(configId) {
const res = await fetch(`${BASE_URL}/api/config/${encodeURIComponent(configId)}`, {

130
main.py
View File

@ -14,6 +14,7 @@ import requests # For HTTP health checks
from datetime import datetime, timedelta, timezone
import os
import logging
import pkgutil
# 📝 ROTATING LOGGER SYSTEM
from core.rotating_logger import setup_backend_logging
@ -252,10 +253,49 @@ def serve_public_record_png():
@app.route("/SIDEL.png")
def serve_public_sidel_png():
"""Serve /SIDEL.png from the React public folder."""
# Candidate locations to check (dev, build, executable-side)
candidates = []
# frontend public (development)
public_dir = resource_path(os.path.join("frontend", "public"))
sidel_file = os.path.join(public_dir, "SIDEL.png")
if os.path.exists(sidel_file):
return send_from_directory(public_dir, "SIDEL.png")
candidates.append((public_dir, "SIDEL.png"))
# frontend dist (production build)
dist_dir = resource_path(os.path.join("frontend", "dist"))
candidates.append((dist_dir, "SIDEL.png"))
# Also check next to the executable (PyInstaller -- onefile may extract or expect assets nearby)
exec_dist = external_path(os.path.join("frontend", "dist", "SIDEL.png"))
exec_dist_dir = os.path.dirname(exec_dist)
candidates.append((exec_dist_dir, "SIDEL.png"))
# Also check for SIDEL.png sitting next to the executable
exec_side = external_path("SIDEL.png")
candidates.append((os.path.dirname(exec_side), os.path.basename(exec_side)))
# Try each candidate using send_from_directory when file exists
for dir_path, filename in candidates:
try:
file_path = os.path.join(dir_path, filename)
except Exception:
continue
if os.path.exists(file_path):
# send_from_directory requires a real file on disk which should be true
try:
return send_from_directory(dir_path, filename)
except Exception:
# If send_from_directory fails for some reason, fall through to try reading bytes
pass
# As a last resort, try to load the resource bytes (useful if bundled differently)
try:
data = pkgutil.get_data(__name__, os.path.join("frontend", "dist", "SIDEL.png"))
if data:
return Response(data, status=200, mimetype="image/png")
except Exception:
pass
return Response("SIDEL.png not found", status=404, mimetype="text/plain")
@ -4612,6 +4652,90 @@ def open_csv_in_excel():
return jsonify({"error": str(e)}), 500
@app.route("/api/csv/data", methods=["POST"])
def get_csv_data():
"""Read CSV file data with pagination support for large files"""
try:
data = request.get_json()
file_path = data.get("file_path", "")
page = data.get("page", 1)
page_size = data.get("page_size", 100) # Default 100 rows per page
if not file_path or not os.path.exists(file_path):
return jsonify({"error": "File not found"}), 404
import pandas as pd
import chardet
# Detect encoding
with open(file_path, "rb") as f:
raw_data = f.read(10000) # Read first 10KB for encoding detection
result = chardet.detect(raw_data)
encoding = result["encoding"] or "utf-8"
# Calculate skip rows for pagination (skip header for pages > 1)
skip_rows = 0 if page == 1 else ((page - 1) * page_size) + 1
# Read CSV with pagination
if page == 1:
# First page - include headers
df = pd.read_csv(file_path, nrows=page_size, encoding=encoding)
else:
# Subsequent pages - skip header
df = pd.read_csv(
file_path, skiprows=skip_rows, nrows=page_size, encoding=encoding
)
# Get headers from first row
headers_df = pd.read_csv(file_path, nrows=0, encoding=encoding)
df.columns = headers_df.columns
# Get total row count (approximate for large files)
try:
# Quick line count for total rows
with open(file_path, "r", encoding=encoding) as f:
total_rows = sum(1 for line in f) - 1 # Subtract header row
except:
# Fallback to pandas if direct counting fails
temp_df = pd.read_csv(file_path, encoding=encoding)
total_rows = len(temp_df)
# Convert DataFrame to records (list of dicts)
records = df.to_dict("records")
# Get column information
columns = list(df.columns)
# Calculate pagination info
total_pages = (total_rows + page_size - 1) // page_size
has_next = page < total_pages
has_prev = page > 1
return jsonify(
{
"success": True,
"data": records,
"columns": columns,
"pagination": {
"page": page,
"page_size": page_size,
"total_rows": total_rows,
"total_pages": total_pages,
"has_next": has_next,
"has_prev": has_prev,
},
"file_info": {
"name": os.path.basename(file_path),
"size": os.path.getsize(file_path),
"encoding": encoding,
},
}
)
except Exception as e:
print(f"❌ Error reading CSV data: {e}")
return jsonify({"error": str(e)}), 500
if __name__ == "__main__":
print(f"🚀 Starting PLC S7-315 Streamer & Logger...")
print(f"🐍 Process PID: {os.getpid()}")

View File

@ -1,12 +1,10 @@
{
"last_state": {
"should_connect": true,
"should_connect": false,
"should_stream": false,
"active_datasets": [
"DAR",
"Test"
]
"active_datasets": []
},
"auto_recovery_enabled": true,
"last_update": "2025-08-30T22:54:50.154570"
"last_update": "2025-08-31T18:40:42.962202",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}