[object Object]

← back to Crazy News Channel Shadowman

Stamp lastUpdated at state-entry, not in render; clear grid at zero stories

de89fd44dbcb477612246cbba77ecb3a3258cfa3 · 2026-09-23 18:42:58 -0700 · Steve Abrams

Cody gate on e1d52f1 reproduced that the in-template lastUpdated assignment
ran after persistStories(), so Reset to Defaults saved lastUpdated:null and a
reload showed a new time. Now stampLastUpdated() runs at load and reset before
anything persists, templates are read-only, and init persists once so a fresh
load's stamps survive reload. Also patchStoryGrid([]) on the zero-stories path
so no orphan card lingers (pre-existing). Verified: reset+reload time stable,
0 grid children after deleting all, race/Cody/hash regressions still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011iuURMrsPBwAqMc5LgvFLH

Files touched

Diff

commit de89fd44dbcb477612246cbba77ecb3a3258cfa3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 23 18:42:58 2026 -0700

    Stamp lastUpdated at state-entry, not in render; clear grid at zero stories
    
    Cody gate on e1d52f1 reproduced that the in-template lastUpdated assignment
    ran after persistStories(), so Reset to Defaults saved lastUpdated:null and a
    reload showed a new time. Now stampLastUpdated() runs at load and reset before
    anything persists, templates are read-only, and init persists once so a fresh
    load's stamps survive reload. Also patchStoryGrid([]) on the zero-stories path
    so no orphan card lingers (pre-existing). Verified: reset+reload time stable,
    0 grid children after deleting all, race/Cody/hash regressions still pass.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011iuURMrsPBwAqMc5LgvFLH
---
 index.html | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/index.html b/index.html
index c4ee823..66660c8 100644
--- a/index.html
+++ b/index.html
@@ -1134,7 +1134,15 @@ function persistStories() {
 // `let`, not `const` — deleteStory()/resetToDefaults() reassign the whole
 // array (filter / clone). All readers (tick, renderStories, buildTickerItems)
 // reference the outer STORIES binding at call-time, so this is safe.
-let STORIES = loadStories();
+// Every story carries a real lastUpdated from the moment it enters state, so
+// render stays read-only and whatever gets persisted matches what's shown.
+function stampLastUpdated(list) {
+  const now = new Date().toLocaleTimeString();
+  list.forEach((s) => { if (!s.lastUpdated) s.lastUpdated = now; });
+  return list;
+}
+
+let STORIES = stampLastUpdated(loadStories());
 
 /* ---------- B. State ---------- */
 const state = {
@@ -1238,7 +1246,7 @@ function storyCardHTML(story, isLead) {
       <p class="story-meta">
         <span>📍 ${escapeHTML(story.location)}</span>
         <span>🖋 ${escapeHTML(story.byline)}</span>
-        <span>🕒 Updated ${story.lastUpdated || (story.lastUpdated = new Date().toLocaleTimeString())}</span>
+        <span>🕒 Updated ${escapeHTML(story.lastUpdated || "—")}</span>
       </p>
       <a class="story-read-more" href="#/article/${escapeHTML(story.id)}" data-open-id="${escapeHTML(story.id)}">Read full article →</a>
     </article>
@@ -1299,6 +1307,7 @@ function renderStories() {
   // didn't exist before the admin panel could delete the seeded 3) — this
   // is distinct from the per-category empty state below.
   if (STORIES.length === 0) {
+    patchStoryGrid([]);
     els.storyGrid.hidden = true;
     els.emptyState.hidden = true;
     els.meltdownBanner.hidden = true;
@@ -1668,7 +1677,7 @@ function deleteStory(id) {
 }
 
 function resetToDefaults() {
-  STORIES = cloneStories(DEFAULT_STORIES);
+  STORIES = stampLastUpdated(cloneStories(DEFAULT_STORIES));
   state.meltdownShown = false;
   state.justCreatedId = null;
   refreshAfterStoriesChanged();
@@ -1836,7 +1845,7 @@ function buildLiveUpdateInnerHTML(story) {
   const stage = story.stages[story.stageIndex];
   const concluded = story.stageIndex === story.stages.length - 1;
   return `
-    <span class="live-update-label">🔴 Live Update — ${escapeHTML(story.lastUpdated || (story.lastUpdated = new Date().toLocaleTimeString()))}</span>
+    <span class="live-update-label">🔴 Live Update — ${escapeHTML(story.lastUpdated || "—")}</span>
     <p class="live-update-headline">${escapeHTML(stage.headline)}</p>
     <p class="live-update-detail">${escapeHTML(stage.detail)}</p>
     ${concluded ? `<p class="live-update-concluded"><strong>STORY CONCLUDED</strong> — ${escapeHTML(stage.conclusion || "")}</p>` : ""}
@@ -2014,6 +2023,7 @@ function init() {
   updateMoodUI();
   setTickerMode(state.reducedMotion);
   refreshTicker();
+  persistStories(); // lock in the load-time lastUpdated stamps so a reload shows the same times
   // Start the 1000ms tick loop BEFORE rendering route, so malformed hashes
   // can't abort init() and freeze the clock/escalation forever
   setInterval(tick, 1000);

← e1d52f1 Fix story-grid click/focus race: keyed in-place patch + stab  ·  back to Crazy News Channel Shadowman  ·  Give admin-created stories 8 distinct escalation arcs instea e4c7519 →