From 4f01b3f1afe3ef050fb98e5b3b279956008d8c4f Mon Sep 17 00:00:00 2001 From: Pagwin Date: Sun, 5 Jul 2026 02:59:30 -0400 Subject: [PATCH] session restore added --- index.html | 124 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 109 insertions(+), 15 deletions(-) diff --git a/index.html b/index.html index dbf4b90..74d53b5 100644 --- a/index.html +++ b/index.html @@ -1755,19 +1755,23 @@ } // --- seed with the example problem --- - addCity({name: "0", type: "hub", renown: 2}); - addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1}); - addCity({name: "2", type: "hub", renown: 2}); - addCity({name: "3", type: "foundry", renown: 2}); - addCity({name: "4", type: "monument", renown: 2}); - addAgent({kind: "Planner"}); - addTerms({ - "renown": 0, - "luxuries": 1, - "steel": 2, - "brass": 1, - "electrum": 2 - }) + // Only used for a fresh tab; a tab reload/restore refills the form from + // sessionStorage instead (see restoreSession below). + function seedDefault() { + addCity({name: "0", type: "hub", renown: 2}); + addCity({name: "1", type: "foundry", renown: 2, vat_steel: 1, vat_brass: 1, vat_electrum: 1}); + addCity({name: "2", type: "hub", renown: 2}); + addCity({name: "3", type: "foundry", renown: 2}); + addCity({name: "4", type: "monument", renown: 2}); + addAgent({kind: "Planner"}); + addTerms({ + "renown": 0, + "luxuries": 1, + "steel": 2, + "brass": 1, + "electrum": 2 + }); + } // --- deep links --- // /?solve= loads one solve; /?solves=,,… loads an arbitrary @@ -1785,13 +1789,103 @@ document.getElementById("output").prepend(card); pending.set(token, {token, card, n, confirmed: true}); } + + // --- session persistence ------------------------------------------- + // The browser natively restores plain form controls (the Game inputs, + // start-resource numbers, tradeable checkboxes) on a reload / "reopen + // closed tab", but not the SPA-built cards (cities, agents, terms, …) nor + // the queue of solve cards under #output — those are recreated by script + // each load. We mirror both into sessionStorage (per-tab, and preserved + // across tab restore) so the whole page comes back as it was left. + const SESSION_KEY = "dws_session"; + + // Tokens of every solve card currently on the page, top-to-bottom. + function currentTokens() { + return [...document.querySelectorAll("#output [data-token]")] + .map(e => e.dataset.token); + } + + function saveSession() { + // buildProblem() samples log terms and can throw on a half-typed + // expression; if so, keep whatever problem we last stored so a + // transiently-invalid form doesn't wipe the saved inputs. + let problem = null, maxTime; + try { + problem = buildProblem(); + maxTime = +document.getElementById("time").value; + } catch (e) {/* keep previously-saved problem */} + let prev = {}; + try {prev = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "{}");} + catch (e) {/* ignore corrupt/absent state */} + const data = { + problem: problem || prev.problem || null, + maxTime: problem ? maxTime : prev.maxTime, + tokens: currentTokens(), + solutionCount, + }; + try {sessionStorage.setItem(SESSION_KEY, JSON.stringify(data));} + catch (e) {/* storage may be full/unavailable; state just won't persist */} + } + + // Debounced: coalesce bursts of edits (and card add/remove mutations) + // into a single write. + let saveTimer = null; + function scheduleSave() { + clearTimeout(saveTimer); + saveTimer = setTimeout(saveSession, 300); + } + + // Refill the form and re-open the solve queue from a prior session. + // Returns true if there was a session to restore. + function restoreSession() { + let data; + try {data = JSON.parse(sessionStorage.getItem(SESSION_KEY) || "null");} + catch (e) {return false;} + if (!data) return false; + if (data.problem) applyProblem(data.problem, data.maxTime); + const tokens = data.tokens || []; + // Reserve a card per token and let the shared /job_status stream + // resolve each (rendering a finished solve fetched from the server, + // or tracking one still queued/running). Load in reverse so the + // first token ends up on top, mirroring how they were prepended. + for (const token of tokens.slice().reverse()) loadSharedSolve(token); + if (tokens.length) syncStream(); + // Keep future solve numbering above anything from the old session. + if (typeof data.solutionCount === "number") + solutionCount = Math.max(solutionCount, data.solutionCount); + return true; + } + + // --- startup --- + const restored = restoreSession(); + if (!restored) seedDefault(); + + // Deep links still work on top of a restored session; skip any token the + // restored queue already holds so it isn't duplicated. const params = new URLSearchParams(location.search); const sharedTokens = (params.get("solves") || params.get("solve") || "") .split(",").map(s => s.trim()).filter(Boolean); + const present = new Set(currentTokens()); + const newShared = sharedTokens.filter(t => !present.has(t)); // Load in reverse so the first id ends up on top (each load prepends). - for (const token of sharedTokens.slice().reverse()) loadSharedSolve(token); - if (sharedTokens.length) syncStream(); + for (const token of newShared.slice().reverse()) loadSharedSolve(token); + if (newShared.length) syncStream(); updateNotifyUi(); + + // Wire autosave only after the initial load has settled, so the seed / + // restore / deep-link work above doesn't churn through it. Edits to any + // control save; card add/remove and queue changes save via observers. + document.addEventListener("input", scheduleSave); + document.addEventListener("change", scheduleSave); + const cardObserver = new MutationObserver(scheduleSave); + for (const id of ["cities", "agents", "terms", "constraints", + "conversions", "optional_conversions"]) + cardObserver.observe(document.getElementById(id), {childList: true}); + // The solve queue: card add/remove plus the data-token that appears when a + // pending card is replaced by its rendered solution. + new MutationObserver(scheduleSave).observe(document.getElementById("output"), + {childList: true, subtree: true, attributes: true, attributeFilter: ["data-token"]}); + saveSession();