Fix concurrent checklist sync and consistent actions

This commit is contained in:
Codex 2026-10-04 20:19:03 +01:00
parent a33f7a297d
commit 8d5a94d7fa
10 changed files with 124 additions and 20 deletions

View file

@ -1,5 +1,6 @@
(() => {
const data = window.VALLEY_DATA;
const sync = window.ValleySync;
const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const seasonNames = { spring: "Spring", summer: "Summer", fall: "Fall", winter: "Winter" };
const state = {
@ -89,22 +90,31 @@
return data.crops.filter(crop => crop.season === state.season && crop.last === state.day && (!crop.year || crop.year === state.year));
}
function card(kind, label, title, copy) {
return `<article class="today-card ${kind}"><div class="card-label"><span></span>${escapeHtml(label)}</div><h3>${escapeHtml(title)}</h3><p>${escapeHtml(copy)}</p></article>`;
function dailyId(kind, title) {
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 36);
return `daily-${state.year}-${state.season}-${state.day}-${kind}-${slug}`;
}
function card(kind, label, title, copy, itemId = null) {
const checked = itemId ? Boolean(state.checks[itemId]) : false;
const content = `${itemId ? `<input type="checkbox" data-check-id="${itemId}" ${checked ? "checked" : ""} ${state.saving.has(itemId) ? "disabled" : ""}><span class="checkmark" aria-hidden="true"></span>` : ""}<div class="card-label"><span></span>${escapeHtml(label)}</div><h3>${escapeHtml(title)}</h3><p>${escapeHtml(copy)}</p>`;
return itemId
? `<label class="today-card ${kind} ${checked ? "complete" : ""}">${content}</label>`
: `<article class="today-card ${kind}">${content}</article>`;
}
function renderToday() {
const cards = [];
const birthday = currentBirthday();
const event = currentEvent();
if (birthday) cards.push(card("birthday", "Birthday", birthday, "A birthday gift gives 8× the usual friendship effect."));
if (event) cards.push(card("event", "Event", event[0], event[1]));
reminders().forEach(([title, copy]) => cards.push(card("reminder", "Reminder", title, copy)));
cropDeadlineCards().forEach(crop => cards.push(card("deadline", "Plant by today", crop.name, crop.purpose)));
if (birthday) cards.push(card("birthday", "Birthday", birthday, "A birthday gift gives 8× the usual friendship effect.", dailyId("birthday", birthday)));
if (event) cards.push(card("event", "Event", event[0], event[1], dailyId("event", event[0])));
reminders().forEach(([title, copy]) => cards.push(card("reminder", "Reminder", title, copy, dailyId("reminder", title))));
cropDeadlineCards().forEach(crop => cards.push(card("deadline", "Plant by today", crop.name, crop.purpose, itemKey(crop))));
if (!cards.length) cards.push(card("quiet", "Open day", "Make your own luck", "No fixed birthdays, festivals, or last planting dates today."));
$("#today-cards").innerHTML = cards.join("");
const noun = cards.length === 1 ? "thing" : "things";
$("#today-summary").textContent = `${cards.length} ${noun} on the calendar`;
$("#today-summary").textContent = `${cards.length} ${noun} on the calendar · tick cards when done`;
}
function renderCrops() {
@ -187,9 +197,11 @@
const response = await fetch("/api/state", { cache: "no-store" });
if (!response.ok) throw new Error(`State request failed (${response.status})`);
const snapshot = await response.json();
if (snapshot.revision !== state.revision) {
state.checks = snapshot.checks || {};
state.revision = snapshot.revision;
const merged = sync.mergeSnapshot(state.checks, state.saving, state.revision, snapshot);
if (merged.applied) {
state.checks = merged.checks;
state.revision = merged.revision;
renderToday();
renderCrops();
renderBundles();
}
@ -204,6 +216,7 @@
const previous = Boolean(state.checks[itemId]);
state.checks[itemId] = checked;
state.saving.add(itemId);
renderToday();
renderCrops();
renderBundles();
setSyncStatus("loading", "Saving…");
@ -215,15 +228,19 @@
});
if (!response.ok) throw new Error(`Save failed (${response.status})`);
const result = await response.json();
state.revision = Math.max(state.revision, result.revision);
const confirmed = sync.confirmWrite(state.checks, state.revision, result);
state.checks = confirmed.checks;
state.revision = confirmed.revision;
setSyncStatus("ready", "Shared progress synced");
} catch (error) {
console.error(error);
state.checks[itemId] = previous;
setSyncStatus("error", "Couldn’t save");
toast("That change wasn’t saved. Your previous shared state is restored.");
void loadState({ quiet: true });
} finally {
state.saving.delete(itemId);
renderToday();
renderCrops();
renderBundles();
}