Harden cloud state sync

This commit is contained in:
Theodore Song
2026-07-12 15:37:23 -04:00
parent 5bd1f0779f
commit 2ca3473bac
2 changed files with 53 additions and 15 deletions
+15 -3
View File
@@ -19,6 +19,10 @@ function agentStateFromItems(items) {
} }
} }
function conflictResponse(res, error, current) {
return res.status(409).json({ ok: false, error, state: current });
}
export default async function handler(req, res) { export default async function handler(req, res) {
res.setHeader("Cache-Control", "no-store"); res.setHeader("Cache-Control", "no-store");
try { try {
@@ -38,11 +42,19 @@ export default async function handler(req, res) {
const current = await readJsonBlob(); const current = await readJsonBlob();
const currentAgents = agentStateFromItems(current && current.items); const currentAgents = agentStateFromItems(current && current.items);
const incomingAgents = agentStateFromItems(body.items); const incomingAgents = agentStateFromItems(body.items);
if (!body.force && currentAgents && incomingAgents) { if (!body.force && currentAgents) {
const sameHour = currentAgents.last_cycle_hour && currentAgents.last_cycle_hour === incomingAgents.last_cycle_hour; const currentHour = currentAgents.last_cycle_hour || "";
const incomingHour = incomingAgents && incomingAgents.last_cycle_hour ? incomingAgents.last_cycle_hour : "";
if (currentHour && (!incomingAgents || !incomingHour)) {
return conflictResponse(res, "Cloud already has a cycle result; refusing unscheduled local state", current);
}
if (currentHour && incomingHour && incomingHour < currentHour) {
return conflictResponse(res, "Incoming state is older than the shared cloud result", current);
}
const sameHour = currentHour && currentHour === incomingHour;
const differentRun = currentAgents.last_run && incomingAgents.last_run && currentAgents.last_run !== incomingAgents.last_run; const differentRun = currentAgents.last_run && incomingAgents.last_run && currentAgents.last_run !== incomingAgents.last_run;
if (sameHour && differentRun) { if (sameHour && differentRun) {
return res.status(409).json({ ok: false, error: "This cycle hour already has a cloud result", state: current }); return conflictResponse(res, "This cycle hour already has a cloud result", current);
} }
} }
const state = { version: 1, updated_at: new Date().toISOString(), items: body.items }; const state = { version: 1, updated_at: new Date().toISOString(), items: body.items };
+38 -12
View File
@@ -634,7 +634,8 @@ async function pushCloudState(force=false){
const d=await r.json().catch(()=>null); const d=await r.json().catch(()=>null);
if(d&&d.state&&d.state.items)applySyncItems(d.state.items); if(d&&d.state&&d.state.items)applySyncItems(d.state.items);
else await loadAuthoritativeCloudState(); else await loadAuthoritativeCloudState();
toast("This hour already ran on another device. Loaded the shared result."); toast("Loaded the newer shared cloud result.");
renderAll();
return false; return false;
} }
if(!r.ok)throw new Error("cloud sync unavailable"); if(!r.ok)throw new Error("cloud sync unavailable");
@@ -644,19 +645,33 @@ async function pushCloudState(force=false){
async function pullCloudState(){ async function pullCloudState(){
try{ try{
const r=await fetch("/api/state",{cache:"no-store"}); const r=await fetch("/api/state",{cache:"no-store"});
if(!r.ok)return false; if(!r.ok)return {ok:false,state:null};
const d=await r.json(); const d=await r.json();
if(d&&d.state)return d.state; return {ok:true,state:(d&&d.state)||null};
}catch(e){} }catch(e){}
return null; return {ok:false,state:null};
} }
async function loadAuthoritativeCloudState(){ async function loadAuthoritativeCloudState(){
const cloudState=await pullCloudState(); const cloud=await pullCloudState();
if(!cloud.ok)return {ok:false,loaded:false};
const cloudState=cloud.state;
if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){ if(cloudState&&cloudState.items&&stateFromSyncItems(cloudState.items)){
applySyncItems(cloudState.items); applySyncItems(cloudState.items);
return true; return {ok:true,loaded:true};
} }
return false; return {ok:true,loaded:false};
}
async function refreshFromCloudAndRender(silent=true){
const cloud=await loadAuthoritativeCloudState();
if(cloud.loaded){
renderAll();
setStatus("cloud synced",false);
if(!silent)toast("Shared cloud state refreshed.");
}else if(!cloud.ok){
setStatus("cloud sync unavailable",false);
if(!silent)toast("Cloud sync is unavailable. Results may be stale until it reconnects.");
}
return cloud;
} }
/* ---------- Agent mechanics ---------- */ /* ---------- Agent mechanics ---------- */
@@ -1086,7 +1101,8 @@ async function backtestWeek(st){
} }
async function runDailyCycle(){ async function runDailyCycle(){
await loadAuthoritativeCloudState(); const cloud=await loadAuthoritativeCloudState();
if(!cloud.ok)throw new Error("Cloud sync unavailable. Refusing to run a local-only cycle.");
let st=loadState(); let st=loadState();
const hour=currentCycleHour(); const hour=currentCycleHour();
if(st.seeded&&st.last_cycle_hour===hour){ if(st.seeded&&st.last_cycle_hour===hour){
@@ -1579,22 +1595,32 @@ $("resetBtn").addEventListener("click",()=>{
if(!confirm("Reset all ten agents to their $10,000 starting balance?"))return; if(!confirm("Reset all ten agents to their $10,000 starting balance?"))return;
saveState(defaultState());localStorage.removeItem(SUG_KEY);pushCloudState(true);toast("All agents reset.");renderAll(); saveState(defaultState());localStorage.removeItem(SUG_KEY);pushCloudState(true);toast("All agents reset.");renderAll();
}); });
window.addEventListener("focus",()=>refreshFromCloudAndRender(true));
document.addEventListener("visibilitychange",()=>{
if(!document.hidden)refreshFromCloudAndRender(true);
});
/* On load: first visit runs a 7-day backtest; afterwards the shared portfolio advances once per hour. */ /* On load: first visit runs a 7-day backtest; afterwards the shared portfolio advances once per hour. */
(async function init(){ (async function init(){
showTab(location.hash.slice(1)||"overview"); showTab(location.hash.slice(1)||"overview");
const cloudLoaded=await loadAuthoritativeCloudState(); const cloud=await loadAuthoritativeCloudState();
let st=loadState(); let st=loadState();
if(cloudLoaded){ if(cloud.loaded){
toast("Cloud state loaded for this device."); toast("Cloud state loaded for this device.");
}else if(st.seeded){ }else if(cloud.ok&&st.seeded){
pushCloudState(); pushCloudState();
}else if(!cloud.ok){
setStatus("cloud sync unavailable",false);
} }
renderAll(); renderAll();
const btn=$("runBtn"); const btn=$("runBtn");
if(!cloud.ok){
toast("Cloud sync is unavailable. Reconnect before running a cycle.");
return;
}
if(!st.seeded){ if(!st.seeded){
btn.disabled=true;btn.textContent="Backtesting…"; btn.disabled=true;btn.textContent="Backtesting…";
try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=currentCycleHour();saveState(st);await pushCloudState();renderAll(); try{await backtestWeek(st);st.date=todayStr();st.last_run=nowIso();st.last_cycle_hour=currentCycleHour();saveState(st);if(!(await pushCloudState()))throw new Error("Cloud sync unavailable");renderAll();
toast("7-day backtest complete — live tracking begins today.");} toast("7-day backtest complete — live tracking begins today.");}
catch(e){setStatus("backtest error — click Run",false);} catch(e){setStatus("backtest error — click Run",false);}
btn.disabled=false;btn.textContent="Run cycle"; btn.disabled=false;btn.textContent="Run cycle";