diff --git a/application_events.json b/application_events.json index 7efc965..1d8a2b4 100644 --- a/application_events.json +++ b/application_events.json @@ -1,165 +1,5 @@ { "events": [ - { - "timestamp": "2025-07-17T18:00:01.179115", - "level": "info", - "event_type": "dataset_activated", - "message": "Dataset activated: DAR", - "details": { - "dataset_id": "dar", - "variables_count": 4, - "streaming_count": 4, - "prefix": "dar" - } - }, - { - "timestamp": "2025-07-17T18:01:12.766609", - "level": "info", - "event_type": "dataset_activated", - "message": "Dataset activated: DAR", - "details": { - "dataset_id": "dar", - "variables_count": 4, - "streaming_count": 4, - "prefix": "dar" - } - }, - { - "timestamp": "2025-07-17T18:01:12.769824", - "level": "info", - "event_type": "streaming_started", - "message": "Multi-dataset streaming started: 1 datasets activated", - "details": { - "activated_datasets": 1, - "total_datasets": 2, - "udp_host": "127.0.0.1", - "udp_port": 9870 - } - }, - { - "timestamp": "2025-07-17T18:01:26.427183", - "level": "info", - "event_type": "Application started", - "message": "Application initialization completed successfully", - "details": {} - }, - { - "timestamp": "2025-07-17T18:01:26.451766", - "level": "info", - "event_type": "plc_connection", - "message": "Successfully connected to PLC 10.1.33.11", - "details": { - "ip": "10.1.33.11", - "rack": 0, - "slot": 2 - } - }, - { - "timestamp": "2025-07-17T18:01:26.456954", - "level": "info", - "event_type": "dataset_activated", - "message": "Dataset activated: DAR", - "details": { - "dataset_id": "dar", - "variables_count": 4, - "streaming_count": 4, - "prefix": "dar" - } - }, - { - "timestamp": "2025-07-17T18:01:30.909879", - "level": "info", - "event_type": "dataset_activated", - "message": "Dataset activated: DAR", - "details": { - "dataset_id": "dar", - "variables_count": 4, - "streaming_count": 4, - "prefix": "dar" - } - }, - { - "timestamp": "2025-07-17T18:01:30.911944", - "level": "info", - "event_type": "streaming_started", - "message": "Multi-dataset streaming started: 1 datasets activated", - "details": { - "activated_datasets": 1, - "total_datasets": 2, - "udp_host": "127.0.0.1", - "udp_port": 9870 - } - }, - { - "timestamp": "2025-07-17T18:20:09.887378", - "level": "info", - "event_type": "Application started", - "message": "Application initialization completed successfully", - "details": {} - }, - { - "timestamp": "2025-07-17T18:20:09.913286", - "level": "info", - "event_type": "plc_connection", - "message": "Successfully connected to PLC 10.1.33.11", - "details": { - "ip": "10.1.33.11", - "rack": 0, - "slot": 2 - } - }, - { - "timestamp": "2025-07-17T18:20:09.917270", - "level": "info", - "event_type": "dataset_activated", - "message": "Dataset activated: DAR", - "details": { - "dataset_id": "dar", - "variables_count": 4, - "streaming_count": 4, - "prefix": "dar" - } - }, - { - "timestamp": "2025-07-18T09:39:20.220724", - "level": "info", - "event_type": "dataset_activated", - "message": "Dataset activated: DAR", - "details": { - "dataset_id": "dar", - "variables_count": 4, - "streaming_count": 3, - "prefix": "dar" - } - }, - { - "timestamp": "2025-07-18T09:39:20.226708", - "level": "info", - "event_type": "streaming_started", - "message": "Multi-dataset streaming started: 1 datasets activated", - "details": { - "activated_datasets": 1, - "total_datasets": 2, - "udp_host": "127.0.0.1", - "udp_port": 9870 - } - }, - { - "timestamp": "2025-07-18T09:40:58.642342", - "level": "info", - "event_type": "dataset_deactivated", - "message": "Dataset deactivated: DAR", - "details": { - "dataset_id": "dar" - } - }, - { - "timestamp": "2025-07-18T09:40:58.644338", - "level": "info", - "event_type": "streaming_stopped", - "message": "Multi-dataset streaming stopped: 1 datasets deactivated", - "details": {} - }, { "timestamp": "2025-07-18T09:40:58.647328", "level": "info", @@ -10414,8 +10254,155 @@ "trigger_variable": null, "auto_started": true } + }, + { + "timestamp": "2025-08-14T11:42:26.264053", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T11:42:26.345728", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 3, + "streaming_count": 3, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-14T11:42:26.362513", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T11:42:26.424426", + "level": "error", + "event_type": "csv_cleanup_failed", + "message": "CSV cleanup failed: 'max_hours'", + "details": {} + }, + { + "timestamp": "2025-08-14T11:49:00.624419", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T11:49:00.691022", + "level": "info", + "event_type": "dataset_activated", + "message": "Dataset activated: DAR", + "details": { + "dataset_id": "DAR", + "variables_count": 3, + "streaming_count": 3, + "prefix": "gateway_phoenix" + } + }, + { + "timestamp": "2025-08-14T11:49:00.703362", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T11:49:00.738130", + "level": "error", + "event_type": "csv_cleanup_failed", + "message": "CSV cleanup failed: 'max_hours'", + "details": {} + }, + { + "timestamp": "2025-08-14T12:00:00.237052", + "level": "error", + "event_type": "csv_cleanup_failed", + "message": "CSV cleanup failed: 'max_hours'", + "details": {} + }, + { + "timestamp": "2025-08-14T12:03:29.892080", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma" + ], + "time_window": 25, + "trigger_variable": null, + "auto_started": true + } + }, + { + "timestamp": "2025-08-14T12:05:58.231793", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-14T12:05:58.282781", + "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-14T12:05:58.294856", + "level": "info", + "event_type": "csv_recording_started", + "message": "CSV recording started: 1 datasets activated", + "details": { + "activated_datasets": 1, + "total_datasets": 3 + } + }, + { + "timestamp": "2025-08-14T12:05:58.314287", + "level": "error", + "event_type": "csv_cleanup_failed", + "message": "CSV cleanup failed: 'max_hours'", + "details": {} + }, + { + "timestamp": "2025-08-14T12:06:22.595559", + "level": "info", + "event_type": "plot_session_created", + "message": "Plot session 'UR29' created and started", + "details": { + "session_id": "plot_1", + "variables": [ + "UR29_Brix", + "UR29_ma" + ], + "time_window": 25, + "trigger_variable": null, + "auto_started": true + } } ], - "last_updated": "2025-08-14T11:34:45.734619", + "last_updated": "2025-08-14T12:06:22.595559", "total_entries": 1000 } \ No newline at end of file diff --git a/config/data/dataset_variables.json b/config/data/dataset_variables.json index 1106d75..cff7e7c 100644 --- a/config/data/dataset_variables.json +++ b/config/data/dataset_variables.json @@ -4,28 +4,20 @@ "dataset_id": "DAR", "variables": [ { - "area": "db", - "db": 1011, "name": "UR29_Brix", - "offset": 1322, - "streaming": true, - "type": "real" - }, - { "area": "db", "db": 1011, + "offset": 1322, + "type": "real", + "streaming": true + }, + { "name": "UR29_ma", - "offset": 1296, - "streaming": true, - "type": "real" - }, - { "area": "db", "db": 1011, - "name": "fUR29_Brix", - "offset": 1322, - "streaming": true, - "type": "real" + "offset": 1296, + "type": "real", + "streaming": true } ] }, diff --git a/config/schema/plot-variables.schema.json b/config/schema/plot-variables.schema.json index 4419ff5..f7e47c4 100644 --- a/config/schema/plot-variables.schema.json +++ b/config/schema/plot-variables.schema.json @@ -28,7 +28,12 @@ "variable_name": { "type": "string", "title": "Variable Name", - "description": "Name of the variable to plot (must exist in dataset variables)" + "description": "Name of the variable to plot (selected from dataset variables using search widget)" + }, + "label": { + "type": "string", + "title": "Display Label", + "description": "Label shown in the plot legend" }, "color": { "type": "string", @@ -37,6 +42,21 @@ "pattern": "^#[0-9A-Fa-f]{6}$", "default": "#3498db" }, + "line_width": { + "type": "number", + "title": "Line Width", + "description": "Width of the line in the plot", + "default": 2, + "minimum": 1, + "maximum": 10 + }, + "y_axis": { + "type": "string", + "title": "Y-Axis", + "description": "Which Y-axis to use for this variable", + "enum": ["left", "right"], + "default": "left" + }, "enabled": { "type": "boolean", "title": "Enable Plotting", @@ -46,7 +66,7 @@ }, "required": [ "variable_name", - "color" + "label" ] } } diff --git a/config/schema/ui/plot-variables.uischema.json b/config/schema/ui/plot-variables.uischema.json index ba76382..dc7fd6e 100644 --- a/config/schema/ui/plot-variables.uischema.json +++ b/config/schema/ui/plot-variables.uischema.json @@ -27,29 +27,49 @@ "items": { "ui:order": [ "variable_name", + "label", "color", + "line_width", + "y_axis", "enabled" ], "ui:layout": [ [ { "name": "variable_name", - "width": 6 + "width": 4 + }, + { + "name": "label", + "width": 2 }, { "name": "color", - "width": 3 + "width": 2 + }, + { + "name": "line_width", + "width": 2 + }, + { + "name": "y_axis", + "width": 1 }, { "name": "enabled", - "width": 3 + "width": 1 } ] ], "variable_name": { + "ui:widget": "variableSelector", + "ui:placeholder": "Search and select variable from datasets...", + "ui:help": "🔍 Search and select a variable from the configured datasets (includes live values and metadata)" + }, + "label": { "ui:widget": "text", - "ui:placeholder": "UR29_Brix", - "ui:help": "� Name of the variable to plot (must exist in dataset variables)" + "ui:placeholder": "Chart legend label...", + "ui:help": "📊 Label shown in the plot legend for this variable" }, "color": { "ui:widget": "color", @@ -72,6 +92,14 @@ ] } }, + "line_width": { + "ui:widget": "updown", + "ui:help": "📏 Width of the line in the plot (1-10 pixels)" + }, + "y_axis": { + "ui:widget": "select", + "ui:help": "📊 Which Y-axis to use for this variable (left or right)" + }, "enabled": { "ui:widget": "checkbox", "ui:help": "📊 Enable this variable to be displayed in the real-time plot" diff --git a/frontend/src/components/PlotManager.jsx b/frontend/src/components/PlotManager.jsx index 58a77ad..6843c7f 100644 --- a/frontend/src/components/PlotManager.jsx +++ b/frontend/src/components/PlotManager.jsx @@ -36,10 +36,12 @@ import validator from '@rjsf/validator-ajv8' import allWidgets from './widgets/AllWidgets' import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate' import PlotRealtimeSession from './PlotRealtimeSession' +import { useVariableContext } from '../contexts/VariableContext' import * as api from '../services/api' // Pure RJSF Plot Manager Component export default function PlotManager() { + const { triggerVariableRefresh } = useVariableContext() const [plots, setPlots] = useState({}) const [plotsSchemaData, setPlotsSchemaData] = useState(null) const [plotsVariablesSchemaData, setPlotsVariablesSchemaData] = useState(null) @@ -238,6 +240,8 @@ export default function PlotManager() { duration: 2000 }) setPlotsVariablesConfig(formData) + // Trigger refresh of variable selectors (though they don't depend on plot vars directly) + triggerVariableRefresh() } catch (error) { toast({ title: '❌ Failed to save plot variables', @@ -411,15 +415,10 @@ export default function PlotManager() { type: "object", title: "Plot Variable", properties: { - dataset_id: { - type: "string", - title: "Dataset Source", - description: "Which dataset contains this variable" - }, variable_name: { type: "string", title: "Variable Name", - description: "Name of the variable to plot" + description: "Select variable from datasets with search and metadata" }, label: { type: "string", @@ -445,7 +444,7 @@ export default function PlotManager() { default: "left" } }, - required: ["dataset_id", "variable_name", "label"] + required: ["variable_name", "label"] } } } @@ -455,13 +454,26 @@ export default function PlotManager() { variables: { items: { "ui:layout": [[ - { "name": "dataset_id", "width": 2 }, - { "name": "variable_name", "width": 3 }, + { "name": "variable_name", "width": 4 }, { "name": "label", "width": 2 }, { "name": "color", "width": 2 }, - { "name": "line_width", "width": 1 }, + { "name": "line_width", "width": 2 }, { "name": "y_axis", "width": 2 } - ]] + ]], + variable_name: { + "ui:widget": "variableSelector", + "ui:placeholder": "Search and select variable from datasets...", + "ui:help": "🔍 Search variables from configured datasets with live values and metadata" + }, + label: { + "ui:placeholder": "Chart legend label..." + }, + color: { + "ui:widget": "color" + }, + line_width: { + "ui:widget": "updown" + } } } } diff --git a/frontend/src/components/rjsf/VariableSelectorWidget.jsx b/frontend/src/components/rjsf/VariableSelectorWidget.jsx index 70894bb..cb45d81 100644 --- a/frontend/src/components/rjsf/VariableSelectorWidget.jsx +++ b/frontend/src/components/rjsf/VariableSelectorWidget.jsx @@ -1,14 +1,27 @@ -import React, { useState, useEffect, useMemo, useRef } from 'react' +import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react' import { FormControl, FormLabel, FormHelperText, Select, VStack, HStack, - Text, Badge, Box, Icon, Input, useColorModeValue, Spinner + Text, Badge, Box, Icon, Input, useColorModeValue, Spinner, IconButton, Tooltip } from '@chakra-ui/react' -import { SearchIcon } from '@chakra-ui/icons' +import { SearchIcon, RepeatIcon } from '@chakra-ui/icons' import { readConfig } from '../../services/api.js' +import { useVariableContext } from '../../contexts/VariableContext.jsx' // Widget for selecting existing dataset variables with filtering and search export function VariableSelectorWidget(props) { - const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [] } = props + const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [], options = {} } = props + + // Extract refresh trigger from options if provided + const refreshTrigger = options?.refreshTrigger || 0 + + // Use variable context if available, fallback to local state + let contextTrigger = 0 + try { + const variableContext = useVariableContext() + contextTrigger = variableContext?.variableRefreshTrigger || 0 + } catch { + // Context not available, use local trigger only + } const [datasetVariables, setDatasetVariables] = useState({}) const [loading, setLoading] = useState(true) @@ -16,6 +29,7 @@ export function VariableSelectorWidget(props) { const [selectedDataset, setSelectedDataset] = useState('all') const [liveValue, setLiveValue] = useState(undefined) const [liveStatus, setLiveStatus] = useState('idle') + const [refreshing, setRefreshing] = useState(false) const esRef = useRef(null) const borderColor = useColorModeValue('gray.300', 'gray.600') @@ -24,24 +38,67 @@ export function VariableSelectorWidget(props) { const selectedVarBgColor = useColorModeValue('blue.50', 'blue.900') const selectedVarBorderColor = useColorModeValue('blue.200', 'blue.700') - // Load dataset variables on mount - useEffect(() => { - const loadDatasetVariables = async () => { - try { - setLoading(true) - const response = await readConfig('dataset-variables') - setDatasetVariables(response.data?.dataset_variables || {}) - } catch (error) { - console.error('Error loading dataset variables:', error) - setDatasetVariables({}) - } finally { - setLoading(false) - } + // Load dataset variables function (extracted for reuse) + const loadDatasetVariables = useCallback(async () => { + try { + setLoading(true) + const response = await readConfig('dataset-variables') + // Handle the array-based structure: { variables: [{dataset_id, variables: [...]}] } + const datasetVariablesArray = response?.variables || [] + + // Convert array format to object format for easier processing + const datasetVariablesObj = {} + datasetVariablesArray.forEach(item => { + if (item.dataset_id && item.variables) { + datasetVariablesObj[item.dataset_id] = { + variables: {} + } + // Convert variables array to object with variable name as key + item.variables.forEach(variable => { + if (variable.name) { + datasetVariablesObj[item.dataset_id].variables[variable.name] = variable + } + }) + } + }) + + setDatasetVariables(datasetVariablesObj) + } catch (error) { + console.error('Error loading dataset variables:', error) + setDatasetVariables({}) + } finally { + setLoading(false) } - - loadDatasetVariables() }, []) + // Manual refresh function + const handleRefresh = useCallback(async () => { + setRefreshing(true) + await loadDatasetVariables() + setRefreshing(false) + }, [loadDatasetVariables]) + + // Load dataset variables on mount and when refresh trigger changes + useEffect(() => { + loadDatasetVariables() + }, [loadDatasetVariables, refreshTrigger, contextTrigger]) + + // Auto-refresh when component gains focus (optional behavior) + const handleFocusWithRefresh = useCallback((event) => { + // Call original onFocus if provided + if (onFocus) { + onFocus(id, event.target.value) + } + + // Auto-refresh data on focus (debounced to avoid excessive calls) + const now = Date.now() + const lastRefresh = window._lastVariableRefresh || 0 + if (now - lastRefresh > 30000) { // Refresh at most once every 30 seconds + window._lastVariableRefresh = now + handleRefresh() + } + }, [onFocus, id, handleRefresh]) + // Create flattened list of all variables with their metadata const allVariables = useMemo(() => { const variables = [] @@ -190,7 +247,7 @@ export function VariableSelectorWidget(props) { {label && {label}} - {/* Search and Dataset Filter */} + {/* Search and Dataset Filter with Refresh */} ))} + + } + onClick={handleRefresh} + isLoading={refreshing} + loadingText="Refreshing..." + size="md" + variant="outline" + colorScheme="blue" + aria-label="Refresh variables" + isDisabled={loading} + /> + {/* Variable Selection */} @@ -234,10 +304,11 @@ export function VariableSelectorWidget(props) { value={value || ''} onChange={(e) => onChange(e.target.value === '' ? undefined : e.target.value)} onBlur={onBlur && ((e) => onBlur(id, e.target.value))} - onFocus={onFocus && ((e) => onFocus(id, e.target.value))} + onFocus={handleFocusWithRefresh} borderColor={borderColor} _focus={{ borderColor: focusBorderColor }} bg={bgColor} + isDisabled={disabled || readonly} > {filteredVariables.map((variable, index) => ( diff --git a/frontend/src/components/widgets/AllWidgets.jsx b/frontend/src/components/widgets/AllWidgets.jsx index 73d94de..5a97837 100644 --- a/frontend/src/components/widgets/AllWidgets.jsx +++ b/frontend/src/components/widgets/AllWidgets.jsx @@ -1,5 +1,6 @@ import { customWidgets } from './CustomWidgets' import { widgets } from '../rjsf/widgets' +import VariableSelectorWidget from '../rjsf/VariableSelectorWidget' // Comprehensive widget collection that merges all available widgets // for full UI schema support with layouts @@ -17,9 +18,10 @@ export const allWidgets = { select: widgets.SelectWidget, checkbox: widgets.CheckboxWidget, - // Variable selector aliases - variableSelector: customWidgets.VariableSelectorWidget, - 'variable-selector': customWidgets.VariableSelectorWidget, + // Variable selector aliases - use the advanced version with search and metadata + variableSelector: VariableSelectorWidget, + 'variable-selector': VariableSelectorWidget, + VariableSelectorWidget: VariableSelectorWidget, // PLC-specific widget aliases (if available) plcArea: widgets.PlcAreaWidget, diff --git a/frontend/src/contexts/VariableContext.jsx b/frontend/src/contexts/VariableContext.jsx new file mode 100644 index 0000000..8bac30c --- /dev/null +++ b/frontend/src/contexts/VariableContext.jsx @@ -0,0 +1,34 @@ +import React, { createContext, useContext, useState, useCallback } from 'react' + +// Context for managing variable data updates across components +const VariableContext = createContext() + +export function VariableProvider({ children }) { + const [variableRefreshTrigger, setVariableRefreshTrigger] = useState(0) + + // Function to trigger refresh of all variable selectors + const triggerVariableRefresh = useCallback(() => { + setVariableRefreshTrigger(prev => prev + 1) + }, []) + + const value = { + variableRefreshTrigger, + triggerVariableRefresh + } + + return ( + + {children} + + ) +} + +export function useVariableContext() { + const context = useContext(VariableContext) + if (context === undefined) { + throw new Error('useVariableContext must be used within a VariableProvider') + } + return context +} + +export default VariableContext diff --git a/frontend/src/pages/DashboardNew.jsx b/frontend/src/pages/DashboardNew.jsx index 46e0d96..271849e 100644 --- a/frontend/src/pages/DashboardNew.jsx +++ b/frontend/src/pages/DashboardNew.jsx @@ -44,6 +44,7 @@ import validator from '@rjsf/validator-ajv8' import PlotManager from '../components/PlotManager' import allWidgets from '../components/widgets/AllWidgets' import LayoutObjectFieldTemplate from '../components/rjsf/LayoutObjectFieldTemplate' +import { VariableProvider, useVariableContext } from '../contexts/VariableContext' import * as api from '../services/api' // StatusBar Component - Real-time PLC status with action buttons @@ -312,6 +313,7 @@ function ConfigurationPanel({ schemaData, formData, onFormChange, onSave, saving // Dataset Manager - Type 3 Form Pattern implementation function DatasetManager() { + const { triggerVariableRefresh } = useVariableContext() const [datasetsConfig, setDatasetsConfig] = useState(null) const [variablesConfig, setVariablesConfig] = useState(null) const [datasetsSchemaData, setDatasetsSchemaData] = useState(null) @@ -379,6 +381,8 @@ function DatasetManager() { duration: 2000 }) setVariablesConfig(formData) + // Trigger refresh of all variable selector widgets + triggerVariableRefresh() } catch (error) { toast({ title: '❌ Failed to save variables', @@ -599,7 +603,10 @@ function DatasetManager() { templates={{ ObjectFieldTemplate: LayoutObjectFieldTemplate }} onSubmit={({ formData }) => { updateSelectedDatasetVariables(formData) - saveVariables(variablesConfig) + saveVariables(variablesConfig).then(() => { + // Additional trigger after successful save + triggerVariableRefresh() + }) }} onChange={({ formData }) => updateSelectedDatasetVariables(formData)} > @@ -707,8 +714,17 @@ function EventsDisplay({ events, loading, onRefresh }) { ) } -// Main Dashboard Component - PLC S7-31x Streamer & Logger +// Main Dashboard Component - PLC S7-31x Streamer & Logger export default function Dashboard() { + return ( + + + + ) +} + +// Dashboard Content Component (separated to use context) +function DashboardContent() { const [status, setStatus] = useState(null) const [statusLoading, setStatusLoading] = useState(true) const [statusError, setStatusError] = useState('') diff --git a/system_state.json b/system_state.json index 07b1a7c..ce76b17 100644 --- a/system_state.json +++ b/system_state.json @@ -3,11 +3,11 @@ "should_connect": true, "should_stream": false, "active_datasets": [ + "Test", "Fast", - "DAR", - "Test" + "DAR" ] }, "auto_recovery_enabled": true, - "last_update": "2025-08-14T11:34:34.761474" + "last_update": "2025-08-14T12:05:58.306284" } \ No newline at end of file