improved balance of responsiveness of configurtaion page between UI and local changes
This commit is contained in:
parent
ec830b5800
commit
27b84a0d46
|
|
@ -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') {
|
if (currentTab == 'configuration') {
|
||||||
fetchDataForOneTime(); // Initial fetch
|
fetchDataForOneTime();
|
||||||
|
|
||||||
const configRefreshInterval = setInterval(() => {
|
const configRefreshInterval = setInterval(() => {
|
||||||
console.log('Refreshing configuration data from S3...');
|
|
||||||
fetchDataForOneTime();
|
fetchDataForOneTime();
|
||||||
}, 60000); // Refresh every 60 seconds (data uploads every ~150s)
|
}, 30000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
continueFetching.current = false;
|
continueFetching.current = false;
|
||||||
|
|
|
||||||
|
|
@ -85,35 +85,11 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
return OperatingPriorityOptions.indexOf(displayName);
|
return OperatingPriorityOptions.indexOf(displayName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [formValues, setFormValues] = useState<Partial<ConfigurationValues>>({
|
|
||||||
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)
|
// Storage key for pending config (optimistic update)
|
||||||
const pendingConfigKey = `pendingConfig_${props.id}`;
|
const pendingConfigKey = `pendingConfig_${props.id}`;
|
||||||
|
|
||||||
// Helper to get current S3 values
|
// Helper to build form values from S3 data
|
||||||
const getS3Values = () => ({
|
const getS3Values = (): Partial<ConfigurationValues> => ({
|
||||||
minimumSoC: props.values.Config.MinSoc,
|
minimumSoC: props.values.Config.MinSoc,
|
||||||
maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent,
|
maximumDischargingCurrent: props.values.Config.MaximumChargingCurrent,
|
||||||
maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent,
|
maximumChargingCurrent: props.values.Config.MaximumDischargingCurrent,
|
||||||
|
|
@ -135,49 +111,83 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
|
controlPermission: String(props.values.Config.ControlPermission).toLowerCase() === "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync form values when props.values changes
|
// Restore pending config from localStorage, converting date strings back to Date objects.
|
||||||
// Logic: Use localStorage only briefly after submit to prevent flicker, then trust S3
|
// 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<ConfigurationValues> = {
|
||||||
|
...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<Partial<ConfigurationValues>>(() => {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
const s3Values = getS3Values();
|
const s3Values = getS3Values();
|
||||||
const pendingConfigStr = localStorage.getItem(pendingConfigKey);
|
const pending = restorePendingConfig();
|
||||||
|
|
||||||
if (pendingConfigStr) {
|
if (pending) {
|
||||||
try {
|
const currentFingerprint = getS3ConfigFingerprint();
|
||||||
const pendingConfig = JSON.parse(pendingConfigStr);
|
const s3Changed = pending.s3ConfigSnapshot && currentFingerprint !== pending.s3ConfigSnapshot;
|
||||||
const submittedAt = pendingConfig.submittedAt || 0;
|
if (s3Changed) {
|
||||||
const timeSinceSubmit = Date.now() - submittedAt;
|
// S3 Config changed from snapshot → device uploaded new data → trust S3
|
||||||
|
|
||||||
// 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) {
|
|
||||||
localStorage.removeItem(pendingConfigKey);
|
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);
|
setFormValues(s3Values);
|
||||||
}, [props.values]);
|
}, [props.values]);
|
||||||
|
|
||||||
|
|
@ -250,12 +260,15 @@ function SodistoreHomeConfiguration(props: SodistoreHomeConfigurationProps) {
|
||||||
setUpdated(true);
|
setUpdated(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
// Save submitted values to localStorage for optimistic UI update
|
// Save submitted values + S3 snapshot to localStorage for optimistic UI update.
|
||||||
// This ensures the form shows correct values even before S3 syncs (up to 150 sec delay)
|
// s3ConfigSnapshot = fingerprint of S3 Config at submit time.
|
||||||
localStorage.setItem(pendingConfigKey, JSON.stringify({
|
// When S3 Config changes from this snapshot, the device has uploaded new data.
|
||||||
|
const cachePayload = {
|
||||||
values: formValues,
|
values: formValues,
|
||||||
submittedAt: Date.now()
|
submittedAt: Date.now(),
|
||||||
}));
|
s3ConfigSnapshot: getS3ConfigFingerprint(),
|
||||||
|
};
|
||||||
|
localStorage.setItem(pendingConfigKey, JSON.stringify(cachePayload));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue