From 74eaa258e1c5d5e8ed90586bc7a547d6432c57a8 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Mon, 18 May 2026 17:14:14 +0200 Subject: [PATCH 1/4] sodistore grid --- .../dashboards/BatteryView/BatteryView.tsx | 102 ++++---- .../DetailedBatteryViewSodistore.tsx | 4 +- .../dashboards/BatteryView/MainStats.tsx | 36 +-- .../dashboards/Installations/Installation.tsx | 124 +++++++++- .../src/content/dashboards/Log/graph.util.tsx | 17 +- .../dashboards/Overview/chartOptions.tsx | 48 +++- .../content/dashboards/Overview/overview.tsx | 94 +++++-- .../Topology/TopologySodistoreGrid.tsx | 232 ++++++++++++++++++ .../dashboards/Topology/topologyBox.tsx | 2 +- .../frontend-marios2/src/interfaces/Chart.tsx | 145 +++++++++-- 10 files changed, 668 insertions(+), 136 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/Topology/TopologySodistoreGrid.tsx diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx index 62a780dc4..1b10000ce 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryView.tsx @@ -45,14 +45,36 @@ function BatteryView(props: BatteryViewProps) { const navigate = useNavigate(); const { product, setProduct } = useContext(ProductIdContext); - const sortedBatteryView = - props.values != null && props.values?.Battery?.Devices - ? Object.entries(props.values.Battery.Devices) - .map(([BatteryId, battery]) => { - return { BatteryId, battery }; // Here we return an object with the id and device - }) - .sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId)) - : []; + // SodiStoreGrid (product 4) keeps batteries under ListOfBatteriesRecord[cluster].Devices[id]. + // Flatten into a single list with composite IDs "{cluster}-{device}" so the existing + // BatteryView table renders without further changes. + const sortedBatteryView = (() => { + if (props.values == null) return []; + + if ( + product === 4 && + props.values.ListOfBatteriesRecord + ) { + const flat: { BatteryId: string; battery: any }[] = []; + Object.entries(props.values.ListOfBatteriesRecord).forEach( + ([clusterId, cluster]: [string, any]) => { + if (cluster?.Devices) { + Object.entries(cluster.Devices).forEach(([devId, dev]) => { + flat.push({ BatteryId: `${clusterId}-${devId}`, battery: dev }); + }); + } + } + ); + return flat.sort((a, b) => a.BatteryId.localeCompare(b.BatteryId)); + } + + if (props.values?.Battery?.Devices) { + return Object.entries(props.values.Battery.Devices) + .map(([BatteryId, battery]) => ({ BatteryId, battery })) + .sort((a, b) => parseInt(b.BatteryId) - parseInt(a.BatteryId)); + } + return []; + })(); const [loading, setLoading] = useState(sortedBatteryView.length == 0); @@ -177,39 +199,37 @@ function BatteryView(props: BatteryViewProps) { } /> {product === 0 - ? Object.entries(props.values.Battery.Devices).map( - ([BatteryId, battery]) => ( - - } - /> - ) + ? sortedBatteryView.map(({ BatteryId, battery }) => ( + + } + /> + )) + : sortedBatteryView.map(({ BatteryId, battery }) => ( + + } + /> ) - : Object.entries(props.values.Battery.Devices).map( - ([BatteryId, battery]) => ( - - } - /> - ) )} @@ -262,7 +282,7 @@ function BatteryView(props: BatteryViewProps) { component="th" scope="row" align="center" - sx={{ fontWeight: 'bold' }} + sx={{ fontWeight: 'bold', whiteSpace: 'nowrap', minWidth: '100px' }} > { - if ( - Object.hasOwnProperty.call(chartData[category].data, devicePath) && - chartData[category].data[devicePath].data.length != 0 - ) { + presentPaths.forEach((devicePath) => { + if (chartData[category].data[devicePath]?.data?.length) { + const palette = + color === 'blue' + ? blueColors + : color === 'red' + ? redColors + : orangeColors; series.push({ ...chartData[category].data[devicePath], - color: - color === 'blue' - ? blueColors[i] - : color === 'red' - ? redColors[i] - : orangeColors[i] + color: palette[i % palette.length] }); } i++; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index 3aae45443..41c6c1e81 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -25,7 +25,7 @@ import Information from '../Information/Information'; import { UserType } from '../../../interfaces/UserTypes'; import HistoryOfActions from '../History/History'; import Topology from '../Topology/Topology'; -import TopologySodistoreHome from '../Topology/TopologySodistoreHome'; +import TopologySodistoreGrid from '../Topology/TopologySodistoreGrid'; import BatteryView from '../BatteryView/BatteryView'; import Configuration from '../Configuration/Configuration'; import PvView from '../PvView/PvView'; @@ -51,6 +51,9 @@ function Installation(props: singleInstallationProps) { const [currentTab, setCurrentTab] = useState(undefined); const [values, setValues] = useState(null); const status = props.current_installation.status; + // For SodiStoreGrid (product 4), backend heartbeat path is broken (SinexcelCommunication + // hardcodes Product=2), so installation.status stays -1 even when S3 is fresh. + // Treat as Green when our S3 fetch succeeded (connected === true). const [connected, setConnected] = useState(true); const [loading, setLoading] = useState(true); @@ -80,11 +83,61 @@ function Installation(props: singleInstallationProps) { //While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return) const continueFetching = useRef(false); + // SodiStoreGrid (Sinexcel) uploads chunked files every ~150s with arbitrary + // unix-second filenames (read key denies LIST — only GET is allowed). + // Probe a wide window in parallel batches with 1-second step. + // `checkContinue` lets the loop short-circuit when the user leaves Live, + // bounding wasted in-flight GETs to a single 20-request batch. + const probeLatestGridChunk = async ( + maxAgeSeconds: number = 600, + checkContinue: () => boolean = () => true + ): Promise<{ res: any; ts: any } | null> => { + const batchSize = 20; + const step = 1; + for (let batchStart = 0; batchStart < maxAgeSeconds; batchStart += batchSize * step) { + if (!checkContinue()) return null; + const offsets: number[] = []; + for (let j = 0; j < batchSize; j++) { + const offset = batchStart + j * step; + if (offset < maxAgeSeconds) offsets.push(offset); + } + const now = UnixTime.now(); + const results = await Promise.all( + offsets.map(async (offset) => { + const ts = now.earlier(TimeSpan.fromSeconds(offset)); + const r = await fetchDataJson(ts, s3Credentials, false); + return r !== FetchResult.notAvailable && r !== FetchResult.tryLater + ? { res: r, ts } + : null; + }) + ); + const hit = results.find((r) => r !== null); + if (hit) return hit; + } + return null; + }; + const fetchDataForOneTime = async () => { var timeperiodToSearch = 70; let res; let timestampToFetch; + if (props.current_installation.product === 4) { + const hit = await probeLatestGridChunk(600); + if (!hit) { + setConnected(false); + setLoading(false); + return false; + } + setConnected(true); + setLoading(false); + const timestamps = Object.keys(hit.res).sort( + (a, b) => Number(a) - Number(b) + ); + setValues(hit.res[timestamps[timestamps.length - 1]]); + return true; + } + for (var i = timeperiodToSearch; i > 0; i -= 2) { timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); try { @@ -122,6 +175,55 @@ function Installation(props: singleInstallationProps) { let res; let timestampToFetch; + // SodiStoreGrid: probe a wide window in parallel (read key denies LIST), + // stream through that chunk's timestamps, then refresh. + // Backoff schedule on consecutive misses to cap S3 cost on offline installs. + if (props.current_installation.product === 4) { + let firstHit = false; + let consecutiveMisses = 0; + const backoffMs = [30000, 60000, 120000, 300000]; // 30s → 60s → 2m → 5m (cap) + while (continueFetching.current) { + // Narrow window after first hit; widen back if we lose it. + const window = firstHit && consecutiveMisses === 0 ? 200 : 600; + const hit = await probeLatestGridChunk( + window, + () => continueFetching.current + ); + if (!continueFetching.current) break; + if (!hit) { + consecutiveMisses += 1; + // Always reflect disconnection in the UI — even after a prior hit, + // so the user sees a stale-data signal instead of a frozen chart. + setConnected(false); + if (!firstHit) setLoading(false); + const wait = backoffMs[Math.min(consecutiveMisses - 1, backoffMs.length - 1)]; + await timeout(wait); + continue; + } + consecutiveMisses = 0; + if (!firstHit) { + firstHit = true; + setLoading(false); + } + setConnected(true); + // Stream through chunk timestamps in ascending order (chunk = ~15 records, ~10s apart) + const orderedTs = Object.keys(hit.res).sort( + (a, b) => Number(a) - Number(b) + ); + for (const t of orderedTs) { + if (!continueFetching.current) { + setFetchFunctionCalled(false); + return false; + } + setValues(hit.res[t]); + await timeout(2000); + } + await timeout(30000); + } + setFetchFunctionCalled(false); + return false; + } + for (var i = 0; i < timeperiodToSearch; i += 2) { if (!continueFetching.current) { return false; @@ -194,12 +296,14 @@ function Installation(props: singleInstallationProps) { setCurrentTab(path[path.length - 1]); }, [location]); - //If status is -1, the topology will be empty and "Unable to communicate with the installation" should be rendered from the Topology component + //If status is -1, the topology will be empty and "Unable to communicate with the installation" should be rendered from the Topology component. + //SodiStoreGrid (product 4) is excluded: backend heartbeat path is broken + //because SinexcelCommunication hardcodes Product=2 — trust S3 freshness instead. useEffect(() => { - if (status === -1) { + if (status === -1 && props.current_installation.product !== 4) { setConnected(false); } - }, [status]); + }, [status, props.current_installation.product]); useEffect(() => { if ( @@ -276,7 +380,7 @@ function Installation(props: singleInstallationProps) { - {currentTab == 'live' && values && ( + {currentTab == 'live' && values && values.EssControl?.Mode && (
- {status === -1 ? ( + {status === -1 && !(props.current_installation.product === 4 && connected) ? ( + > ) : ( { 'InverterRecord.TotalBatteryPower', 'InverterRecord.TotalPhotovoltaicPower', 'InverterRecord.TotalLoadPower', - 'InverterRecord.TotalGridPower' + 'InverterRecord.TotalGridPower', + 'InverterRecord.ActivePowerW', + 'DcDc.Dc.Battery.Power' ]; // Helper function to safely get a value from a nested path diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx index a44863d5c..84bea55c0 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/chartOptions.tsx @@ -7,7 +7,9 @@ export const getChartOptions = ( type: string, dateList: string[], stacked: Boolean, - voltageInfo?: chartInfoInterface + voltageInfo?: chartInfoInterface, + powerLabel?: string, + temperatureInfo?: chartInfoInterface ): ApexOptions => { return type.includes('daily') ? { @@ -57,7 +59,7 @@ export const getChartOptions = ( type === 'dailyoverview' ? [ { - seriesName: 'Grid Power', + seriesName: powerLabel ?? 'Grid Power', tickAmount: 6, min: chartInfo.min >= 0 @@ -94,7 +96,7 @@ export const getChartOptions = ( } }, { - seriesName: 'Grid Power', + seriesName: powerLabel ?? 'Grid Power', show: false, tickAmount: 6, min: @@ -123,7 +125,7 @@ export const getChartOptions = ( } }, { - seriesName: 'Grid Power', + seriesName: powerLabel ?? 'Grid Power', show: false, tickAmount: 6, min: @@ -192,6 +194,27 @@ export const getChartOptions = ( return Math.round(value).toString(); } } + }] : []), + ...(temperatureInfo ? [{ + seriesName: 'Battery Temperature', + opposite: true, + tickAmount: 5, + min: Math.floor((temperatureInfo.min - 5) / 5) * 5, + max: Math.ceil((temperatureInfo.max + 5) / 5) * 5, + title: { + text: '(°C)', + style: { + fontSize: '12px' + }, + offsetY: -190, + offsetX: -65, + rotate: 0 + }, + labels: { + formatter: function (value: number) { + return Math.round(value).toString(); + } + } }] : []) ] : { @@ -241,18 +264,27 @@ export const getChartOptions = ( }, y: { formatter: function (val, { seriesIndex, w }) { + // `shared: true` calls this for every series at the hovered x, + // even when a particular series has no data point there → val undefined. + if (val === undefined || val === null || Number.isNaN(val)) { + return '-'; + } const seriesName = w.config.series[seriesIndex].name; if (seriesName === 'Battery SOC') { return val.toFixed(2) + ' %'; } else if (seriesName === 'Battery Voltage') { return val.toFixed(2) + ' (V)'; + } else if (seriesName === 'Battery Temperature') { + return val.toFixed(2) + ' (°C)'; } else { + const formatted = formatPowerForGraph(val, chartInfo.magnitude); + const raw = formatted?.value; return ( - formatPowerForGraph(val, chartInfo.magnitude).value.toFixed( - 2 - ) + + (raw === undefined || raw === null || Number.isNaN(raw) + ? '-' + : raw.toFixed(2)) + ' ' + - chartInfo.unit + (chartInfo.unit ?? '') ); } } diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 1854b361e..f4961e7e6 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -709,11 +709,22 @@ function Overview(props: OverviewProps) { @@ -1215,6 +1258,8 @@ function Overview(props: OverviewProps) { )} + {/* PV Production + Grid Power — hidden for SodiStoreGrid (no PV, no grid meter) */} + {product !== 4 && ( + )} {aggregatedData && (product === 2 || product === 5) && ( )} - {dailyData && ( + {dailyData && product !== 4 && ( () => { + setShowValues(!showValues); + }; + + const isMobile = window.innerWidth <= 1490; + + const inv = props.values?.InverterRecord; + const dcdc = props.values?.DcDc; + const clusters = props.values?.ListOfBatteriesRecord ?? {}; + const clusterIds = Object.keys(clusters).sort( + (a, b) => Number(a) - Number(b) + ); + + const acDcPower = Number(inv?.ActivePowerW ?? 0); + const dcLinkPower = Number(dcdc?.Dc?.Link?.Power ?? 0); + const dcLinkVoltage = Number(dcdc?.Dc?.Link?.Voltage ?? 0); + const dcdcBatteryVoltage = Number(dcdc?.Dc?.Battery?.Voltage ?? 0); + const dcdcBatteryPower = Number(dcdc?.Dc?.Battery?.Power ?? 0); + + return ( + + + {!props.connected && !props.loading && ( + + + + + + + + + + )} + + {props.connected && ( + <> + +
+ + Display Values + + +
+
+ + + {/* AC-DC */} + + + {/* DC Link */} + + + {/* DC-DC */} + + + {/* Battery clusters — one column per cluster, no chained lines */} + {clusterIds.map((id) => { + const c = clusters[id]; + return ( + + ); + })} + + + )} +
+
+ ); +} + +export default TopologySodistoreGrid; diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx index 2af57880b..4757e6e33 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/topologyBox.tsx @@ -67,7 +67,7 @@ function TopologyBox(props: TopologyBoxProps) { width: isMobile ? '90px' : '104px', height: - props.title === 'Battery' + props.title === 'Battery' || (props.title && props.title.startsWith('Battery ')) ? '165px' : props.title === 'AC Loads' || props.title === 'DC Loads' || diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index 012d45d0d..3b46936c9 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -112,18 +112,23 @@ export const transformInputToBatteryViewDataJson = async ( '.Dc.Current' ]; - const pathsToSearch = [ - 'Battery.Devices.1', - 'Battery.Devices.2', - 'Battery.Devices.3', - 'Battery.Devices.4', - 'Battery.Devices.5', - 'Battery.Devices.6', - 'Battery.Devices.7', - 'Battery.Devices.8', - 'Battery.Devices.9', - 'Battery.Devices.10' - ]; + // SodiStoreGrid (product 4) keeps batteries under ListOfBatteriesRecord[cluster].Devices[id]. + // Built dynamically below from the first JSON chunk; other products use the fixed list. + const pathsToSearch: string[] = + product === 4 + ? [] + : [ + 'Battery.Devices.1', + 'Battery.Devices.2', + 'Battery.Devices.3', + 'Battery.Devices.4', + 'Battery.Devices.5', + 'Battery.Devices.6', + 'Battery.Devices.7', + 'Battery.Devices.8', + 'Battery.Devices.9', + 'Battery.Devices.10' + ]; const pathsToSave = []; @@ -163,7 +168,6 @@ export const transformInputToBatteryViewDataJson = async ( //navigate(routes.login); } }); - for (var i = 0; i < timestampArray.length; i++) { timestampPromises.push( fetchJsonDataForOneTime( @@ -311,21 +315,44 @@ export const transformInputToBatteryViewDataJson = async ( }); }); } else { - // SaliMax, Salidomo, SodistoreMax: existing logic - const battery_nodes = - result.Config.Devices.BatteryNodes.toString().split(','); - - //Initialize the chartData structure based on the node names extracted from the first result + // SaliMax, Salidomo, SodistoreMax, SodistoreGrid: existing logic + // SodistoreGrid (product 4) batteries live under ListOfBatteriesRecord[cluster].Devices[id]; + // enumerate them dynamically and build pathsToSearch in parallel with pathsToSave. let old_length = pathsToSave.length; - if (battery_nodes.length > old_length) { - battery_nodes.forEach((node) => { - const node_number = - product == 3 || product == 4 ? Number(node) + 1 : Number(node) - 1; - if (!pathsToSave.includes('Node' + node_number)) { - pathsToSave.push('Node' + node_number); - } + if (product === 4) { + const lobr = (result as any)?.ListOfBatteriesRecord ?? {}; + const clusters = Object.keys(lobr).sort( + (a, b) => Number(a) - Number(b) + ); + clusters.forEach((clusterId) => { + const devices = Object.keys(lobr[clusterId]?.Devices ?? {}).sort( + (a, b) => Number(a) - Number(b) + ); + devices.forEach((deviceId) => { + const nodeName = `Node${clusterId}-${deviceId}`; + if (!pathsToSave.includes(nodeName)) { + pathsToSave.push(nodeName); + pathsToSearch.push( + `ListOfBatteriesRecord.${clusterId}.Devices.${deviceId}` + ); + } + }); }); + } else { + const battery_nodes = + result.Config.Devices.BatteryNodes.toString().split(','); + + //Initialize the chartData structure based on the node names extracted from the first result + if (battery_nodes.length > old_length) { + battery_nodes.forEach((node) => { + const node_number = + product == 3 ? Number(node) + 1 : Number(node) - 1; + if (!pathsToSave.includes('Node' + node_number)) { + pathsToSave.push('Node' + node_number); + } + }); + } } if (initialiation) { @@ -341,7 +368,7 @@ export const transformInputToBatteryViewDataJson = async ( }); } - if (battery_nodes.length > old_length) { + if (pathsToSave.length > old_length) { categories.forEach((category) => { pathsToSave.forEach((path) => { if (pathsToSave.indexOf(path) >= old_length) { @@ -454,6 +481,23 @@ export const transformInputToDailyDataJson = async ( null, // DCLoad not available for SodioHome 'SODIOHOME_BATTERY_VOLTAGE' ] + : product == 4 + ? [ + // SodistoreGrid: placeholders — actual extraction runs in the + // product===4 switch below; nulls only mark "no data path", so + // the forEach skips the irrelevant categories. Battery voltage + // (index 8) MUST be a non-null placeholder, otherwise the entry + // is skipped before the switch can populate it. + 'ListOfBatteriesRecord', // 0 soc + 'ListOfBatteriesRecord', // 1 temperature + 'ListOfBatteriesRecord', // 2 dcPower (battery power) + null, // 3 gridPower — no grid meter + null, // 4 pvProduction — no PV + 'DcDc.Dc.Link.Voltage', // 5 dcBusVoltage + null, // 6 ACLoad — no AC load meter + null, // 7 DCLoad — no DC load meter + 'ListOfBatteriesRecord' // 8 batteryVoltage (cluster avg) + ] : [ 'Battery.Soc', product == 0 ? 'Battery.Temperature' : 'Battery.TemperatureCell1', @@ -599,6 +643,46 @@ export const transformInputToDailyDataJson = async ( break; } } + } else if (product === 4) { + // SodiStoreGrid: only battery + DC-side metrics exist. + // No grid meter, no PV, no AC/DC load on this product — those series stay empty. + const lobr: Record = + (result as any)?.ListOfBatteriesRecord ?? {}; + const clusterValues = Object.values(lobr) as any[]; + const avgOf = (pick: (c: any) => number | undefined): number | undefined => { + const xs = clusterValues + .map(pick) + .filter((v): v is number => typeof v === 'number'); + return xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : undefined; + }; + const sumOf = (pick: (c: any) => number | undefined): number | undefined => { + const xs = clusterValues + .map(pick) + .filter((v): v is number => typeof v === 'number'); + return xs.length ? xs.reduce((a, b) => a + b, 0) : undefined; + }; + switch (category_index) { + case 0: // soc + value = avgOf((c) => c?.Soc); + break; + case 1: // temperature + value = avgOf((c) => c?.TemperatureCell1); + break; + case 2: // battery power (sum of cluster powers, else DcDc battery-side) + value = + sumOf((c) => c?.Power) ?? + (result as any)?.DcDc?.Dc?.Battery?.Power; + break; + case 5: // dc bus voltage + value = (result as any)?.DcDc?.Dc?.Link?.Voltage; + break; + case 8: // battery voltage (cluster average, else DcDc battery side) + value = + avgOf((c) => c?.Voltage) ?? + (result as any)?.DcDc?.Dc?.Battery?.Voltage; + break; + // case 3 (grid), 4 (PV), 6 (AC load), 7 (DC load) — not available on SodiStoreGrid + } } else if (category_index === 4) { // Salimax/Salidomo: Custom logic for 'PvOnDc.Dc.Power' if (get(result, path) !== undefined) { @@ -651,6 +735,15 @@ export const transformInputToDailyDataJson = async ( chartOverview[category].magnitude = magnitude; }); + // SodistoreGrid: 18 parallel battery devices easily swing into the kW range. + // Pin Battery Power to at least kW so the axis stays stable across day/night cycles. + if (product === 4) { + chartOverview.dcPower.magnitude = Math.max( + chartOverview.dcPower.magnitude, + 1 + ); + } + chartOverview.soc.unit = '(%)'; chartOverview.soc.min = 0; chartOverview.soc.max = 100; From c1b456639ab0e57cc71ae62bcb26f6dc9fa6e0f4 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 19 May 2026 09:10:15 +0200 Subject: [PATCH 2/4] fix Last Week issue --- .../src/content/dashboards/Overview/overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index f4961e7e6..0c7e7a2ed 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -663,7 +663,7 @@ function Overview(props: OverviewProps) { )} - {!loading && dailyDataArray.length > 0 && ( + {!loading && ((dailyData && dailyDataArray.length > 0) || (aggregatedData && aggregatedDataArray.length > 0)) && ( {dailyData && ( Date: Tue, 19 May 2026 09:36:12 +0200 Subject: [PATCH 3/4] comment Last Week button since there is no aggregated data --- .../src/content/dashboards/Overview/overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 0c7e7a2ed..35f85c0f8 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -566,7 +566,7 @@ function Overview(props: OverviewProps) { > - {props.device !== 3 && props.product !== 2 && ( + {props.device !== 3 && props.product !== 2 && props.product !== 4 && ( + + {loading && ( + + )} + + {updated && ( + + + setUpdated(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} + + {error && ( + + + setError(false)} + sx={{ marginLeft: '4px' }} + > + + + + )} +
+ + + + + + ); +} + +export default ConfigurationSodistoreGrid; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index 41c6c1e81..1e423f162 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -28,6 +28,7 @@ import Topology from '../Topology/Topology'; import TopologySodistoreGrid from '../Topology/TopologySodistoreGrid'; import BatteryView from '../BatteryView/BatteryView'; import Configuration from '../Configuration/Configuration'; +import ConfigurationSodistoreGrid from '../Configuration/ConfigurationSodistoreGrid'; import PvView from '../PvView/PvView'; import InstallationTicketsTab from '../Tickets/InstallationTicketsTab'; import DocumentsTab from '../Documents/DocumentsTab'; @@ -605,20 +606,10 @@ function Installation(props: singleInstallationProps) { path={routes.configuration} element={ props.current_installation.product === 4 ? ( - // TODO: SodistoreGrid — implement actual configuration - - - Configuration not yet available - - + ) : ( = new Set([2, 4, 5]); +export const CHECKLIST_ENABLED_PRODUCTS: ReadonlySet = new Set([2, 5]); export const UPLOADABLE_SUBTASK_KEYS: ReadonlySet = new Set([ 'checklistStep8Sub1',