diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index 82d74d8a1..55e383be9 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -293,14 +293,13 @@ function SodioHomeInstallation(props: singleInstallationProps) { } } } - // Fetch periodically in configuration tab (every 30 seconds to detect S3 updates) + // Fetch periodically in configuration tab to detect S3 config updates if (currentTab == 'configuration') { - fetchDataForOneTime(); // Initial fetch + fetchDataForOneTime(); const configRefreshInterval = setInterval(() => { - console.log('Refreshing configuration data from S3...'); fetchDataForOneTime(); - }, 60000); // Refresh every 60 seconds (data uploads every ~150s) + }, 30000); return () => { continueFetching.current = false; diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx index 95ba5d25d..620a8f388 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodistoreHomeConfiguration.tsx @@ -85,35 +85,11 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { return OperatingPriorityOptions.indexOf(displayName); }; - const [formValues, setFormValues] = useState>({ - minimumSoC: props.values.Config.MinSoc, - maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent, - maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent, - operatingPriority: resolveOperatingPriorityIndex( - props.values.Config.OperatingPriority - ), - batteriesCount: props.values.Config.BatteriesCount, - clusterNumber: props.values.Config.ClusterNumber??1, - PvNumber: props.values.Config.PvNumber??0, - timeChargeandDischargePower: props.values.Config?.TimeChargeandDischargePower ?? 0, // default 0 W - startTimeChargeandDischargeDayandTime: - props.values.Config?.StartTimeChargeandDischargeDayandTime - ? dayjs(props.values.Config.StartTimeChargeandDischargeDayandTime).toDate() - : null, - stopTimeChargeandDischargeDayandTime: - props.values.Config?.StopTimeChargeandDischargeDayandTime - ? dayjs(props.values.Config.StopTimeChargeandDischargeDayandTime).toDate() - : null, - - // controlPermission: props.values.Config.ControlPermission??false, - controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true", - }); - // Storage key for pending config (optimistic update) const pendingConfigKey = `pendingConfig_${props.id}`; - // Helper to get current S3 values - const getS3Values = () => ({ + // Helper to build form values from S3 data + const getS3Values = (): Partial => ({ minimumSoC: props.values.Config.MinSoc, maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent, maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent, @@ -135,49 +111,83 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true", }); - // Sync form values when props.values changes - // Logic: Use localStorage only briefly after submit to prevent flicker, then trust S3 + // Restore pending config from localStorage, converting date strings back to Date objects. + // Returns { values, s3ConfigSnapshot } or null if no pending config. + const restorePendingConfig = () => { + try { + const pendingStr = localStorage.getItem(pendingConfigKey); + if (!pendingStr) return null; + + const pending = JSON.parse(pendingStr); + const v = pending.values; + const values: Partial = { + ...v, + // JSON.stringify converts Date→string; restore them back to Date objects + startTimeChargeandDischargeDayandTime: + v.startTimeChargeandDischargeDayandTime + ? dayjs(v.startTimeChargeandDischargeDayandTime).toDate() + : null, + stopTimeChargeandDischargeDayandTime: + v.stopTimeChargeandDischargeDayandTime + ? dayjs(v.stopTimeChargeandDischargeDayandTime).toDate() + : null, + }; + + return { values, s3ConfigSnapshot: pending.s3ConfigSnapshot || null }; + } catch (e) { + console.error('[Config:restore] Failed to parse localStorage', e); + localStorage.removeItem(pendingConfigKey); + return null; + } + }; + + // Fingerprint S3 Config for change detection (not value comparison) + const getS3ConfigFingerprint = () => JSON.stringify(props.values.Config); + + // Initialize form from localStorage (if pending submit exists) or from S3 + // This runs in the useState initializer so the component never renders stale values + const [formValues, setFormValues] = useState>(() => { + const pending = restorePendingConfig(); + const s3 = getS3Values(); + if (pending) { + // Check if S3 has new data since submit (fingerprint changed from snapshot) + const currentFingerprint = getS3ConfigFingerprint(); + const s3Changed = pending.s3ConfigSnapshot && currentFingerprint !== pending.s3ConfigSnapshot; + + if (s3Changed) { + // Device uploaded new data since our submit — trust S3 (device is authority) + localStorage.removeItem(pendingConfigKey); + return s3; + } + + // S3 still has same data as when we submitted — show pending values + return pending.values; + } + return s3; + }); + + // When S3 data updates (polled every 60s), reconcile with any pending localStorage. + // Strategy: device is the authority. Once S3 Config changes from the snapshot taken at + // submit time, the device has uploaded new data — trust S3 regardless of values. useEffect(() => { const s3Values = getS3Values(); - const pendingConfigStr = localStorage.getItem(pendingConfigKey); + const pending = restorePendingConfig(); - if (pendingConfigStr) { - try { - const pendingConfig = JSON.parse(pendingConfigStr); - const submittedAt = pendingConfig.submittedAt || 0; - const timeSinceSubmit = Date.now() - submittedAt; - - // Within 300 seconds of submit: use localStorage (waiting for S3 sync) - // This covers two full S3 upload cycles (150 sec × 2) to ensure new file is available - if (timeSinceSubmit < 300000) { - // Check if S3 now matches - if so, sync is complete - const s3MatchesPending = - s3Values.controlPermission === pendingConfig.values.controlPermission && - s3Values.minimumSoC === pendingConfig.values.minimumSoC && - s3Values.operatingPriority === pendingConfig.values.operatingPriority; - - if (s3MatchesPending) { - // S3 synced! Clear localStorage and use S3 from now on - console.log('S3 synced with submitted config'); - localStorage.removeItem(pendingConfigKey); - setFormValues(s3Values); - } else { - // Still waiting for sync, keep showing submitted values - console.log('Waiting for S3 sync, showing submitted values'); - setFormValues(pendingConfig.values); - } - return; - } - - // Timeout expired: clear localStorage, trust S3 completely - console.log('Timeout expired, trusting S3 data'); - localStorage.removeItem(pendingConfigKey); - } catch (e) { + if (pending) { + const currentFingerprint = getS3ConfigFingerprint(); + const s3Changed = pending.s3ConfigSnapshot && currentFingerprint !== pending.s3ConfigSnapshot; + if (s3Changed) { + // S3 Config changed from snapshot → device uploaded new data → trust S3 localStorage.removeItem(pendingConfigKey); + setFormValues(s3Values); + } else { + // S3 still has same data as at submit time — keep showing pending values + setFormValues(pending.values); } + return; } - // No localStorage or expired: always use S3 (source of truth) + // No pending config — trust S3 (source of truth) setFormValues(s3Values); }, [props.values]); @@ -250,12 +260,15 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) { setUpdated(true); setLoading(false); - // Save submitted values to localStorage for optimistic UI update - // This ensures the form shows correct values even before S3 syncs (up to 150 sec delay) - localStorage.setItem(pendingConfigKey, JSON.stringify({ + // Save submitted values + S3 snapshot to localStorage for optimistic UI update. + // s3ConfigSnapshot = fingerprint of S3 Config at submit time. + // When S3 Config changes from this snapshot, the device has uploaded new data. + const cachePayload = { values: formValues, - submittedAt: Date.now() - })); + submittedAt: Date.now(), + s3ConfigSnapshot: getS3ConfigFingerprint(), + }; + localStorage.setItem(pendingConfigKey, JSON.stringify(cachePayload)); } };