Skip to content

FM-CAL-BLACKOUT: handle Chronicle calendar rebuild (V5) - #85

Open
keyxmakerx wants to merge 3 commits into
mainfrom
claude/calendar-system-review-bprsvh
Open

FM-CAL-BLACKOUT: handle Chronicle calendar rebuild (V5)#85
keyxmakerx wants to merge 3 commits into
mainfrom
claude/calendar-system-review-bprsvh

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Cites: FM-CAL-BLACKOUT (2026-08-21); CLAUDE.md → "Calendar blackout"

Security implication: none — defensive handling of a known server-side outage

Consumer-verified: Chronicle syncapi /calendar* routes (34 endpoints) answer 503 {"error":"calendar_rebuilding","message":"…"} as of 2026-08-21

Foundry compatibility: verified against Foundry v12–v14 (no runtime changes)

Mockup: n/a

What this changes

Chronicle deleted its calendar plugin on 2026-08-21 for a ground-up rebuild (V5). All 34 calendar routes stay registered and answer 503 {"error":"calendar_rebuilding", …} — deliberately 503 rather than 404, so the module does not take its "that Chronicle is too old" compatibility path.

This PR adds FM-CAL-BLACKOUT: a session-scoped guard that arms on the first blackout response, tells the GM exactly once, and returns before spending a request on every later push. Twenty world-time ticks now cost one request instead of forty. The api-client attaches status, code, and serverMessage to thrown errors so callers key on structured data rather than regexing prose. Error-log repeats coalesce with a count, so one failing endpoint cannot flush the shared 50-entry ring that the dashboard and diagnostics both read. Pulls, journals, maps, characters, items and notes are unaffected — initial-sync isolation ensures one subsystem's outage cannot cost the others their sync.

Why

Without this, every Foundry world-time change fires two doomed requests (the real-time pre-push probe, then the push itself) plus a red console.error — un-debounced, forever. A GM running the in-game clock or advancing time per combat round drives that pair on every tick for the whole outage, flooding the one 50-entry error ring the dashboard and diagnostics share and evicting real map/actor/item/note errors. The blackout is expected and benign (nothing is broken on the GM's side), so it should not alarm them or destroy the diagnostic surface for subsystems that still work.

Test plan

  • npm test passes locally — added tools/test-calendar-blackout.mjs with 14 tests pinning:
    • Classification of 503/calendar_rebuilding as its own 'rebuilding' state (never 'absent' or 'auth')
    • Session guard arms once and stays armed
    • 20 world-time ticks cost 1 request and 1 notice, not 40 and 20
    • Error-log coalesces identical repeats instead of evicting neighbours
    • api-client attaches status/code/serverMessage to errors
    • Overview reports rebuild as info, never as structure mismatch
    • Sync-state classifier ranks "unavailable" above every other verdict
  • Manual verification in Foundry: dashboard Calendar tab shows rebuild notice (info severity, wrench icon); push sites return before spending requests once armed
  • CI passes

Tenet self-check

  • T-B1 security: no auth-token or signed-URL changes; error classification is defensive only
  • T-B2 plugin isolation: all changes stay within the Foundry module half
  • T-B3 production UI: rebuild notice is info severity (not alarming); Overview and dashboard both render gracefully; no animation/motion
  • T-B4 dual-audience docs: CLAUDE.md, API-CONTRACT.md, and TESTING.md all updated with blackout context and re-verify-by dates

Files changed

New:

  • scripts/_calendar-blackout-guard.mjs — session singleton guard
  • tools/test-calendar-blackout.mjs — 14 regression tests

Modified:

  • scripts/_calendar-probe-state.mjs — added 'rebuilding' state classification
  • scripts/_realtime-date-guard.mjs — probe failure now classified instead of swallowed
  • scripts/api-client.mjs — attach status

https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB

claude added 3 commits August 21, 2026 19:35
An audit of what the module ACTUALLY does when Chronicle answers 503
calendar_rebuilding confirmed half of the "degrades cleanly" claim and
disproved the other half. My earlier claim that the module already handled
this was only partly right, so here is the honest split.

CONFIRMED SAFE (now pinned, because these are what a future edit breaks
silently): a calendar 503 never aborts the rest of initial sync; no date is
ever written into the Foundry world; no local Calendaria note is ever
deleted; the structure-mismatch guard fails open rather than pausing; the
back-catalog fires zero requests, not thirty-six; the reentrancy guard
cannot leak; and the WebSocket raises no false disconnect.

FIXED — the module was lying to the GM. The dashboard's bare catch turned
EVERY calendar probe failure into "No calendar configured for this campaign
in Chronicle", stated as fact. During the rebuild that is precisely untrue,
and a 401, a proxy timeout and a DNS failure all read the same way. The
classifier that gets this right already existed and had been used by the
Sync Calendar editor for months; the dashboard never got it. It now has a
'rebuilding' state distinct from 'absent' (which advises importing a
calendar that has nowhere to go) and 'unreachable' (which blames settings
that are fine).

FIXED — the push storm. Every world-time change fired two doomed requests
and a red console error, un-debounced, forever; a GM running the in-game
clock drove that pair per tick for the whole outage. Twenty ticks now cost
ONE request and ONE notice instead of forty and twenty, via a session guard
shaped after the existing real-time guard.

FIXED — the shared error ring. One failing endpoint used to flush every
map, actor, item and note error out of the single 50-entry log the
dashboard and the diagnostics bundle both read, so an outage destroyed the
diagnostics for the subsystems that still worked. Identical repeats now
coalesce with a count.

Also: the Pull-date button logged a successful pull that had failed;
Push-date failed silently; the Overview's calendar alert keyed on a flag
nothing sets, so it was dead code, and hardcoded "different structures" as
the reason for any pause; and the initial-sync module loop had no
per-module try/catch, unlike the post-pass loop six lines below — so every
module's isolation rested on one catch inside an unrelated one.

API errors now carry status, code and the server's own message, so no
caller has to regex prose to find out what happened. Twelve tests, each
verified failing before passing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB
The module's docs advertised a Chronicle blocker that had been fixed 26 days
earlier — the whole calendar plugin was deleted before anyone noticed. That
is the failure being corrected here, not just the words.

CLAUDE.md gains a Calendar blackout section (what the server answers, why
503 and not 404, what the module does about it, and that pulls, journals,
maps, characters, items and notes are unaffected). The stale
calendar.worldstate.changed blocker is replaced with its own history —
closed by Chronicle f8d3550 on 2026-07-26, moot since the deletion — marked
do-not-re-open, and the three live-client checklists that ask a tester to
verify things that cannot happen are marked blocked rather than left as
standing instructions.

API-CONTRACT.md gains a 503 banner over the whole Calendar section (kept as
the starting point for V5, explicitly NOT a description of today), a
structured-errors subsection recording that this body inverts the documented
field roles — the "error" field carries a machine code here, not prose — and
a note that every calendar.* WebSocket type is dormant, including the
consequence that a pre-blackout mismatch pause cannot be cleared by its
documented recovery path until V5.

README.md is the one doc a non-developer reads, so it says plainly that
calendar sync is paused, that everything else still syncs, and that nothing
is wrong on their side. TESTING.md stops telling a tester the calendar API
returns 404 — it returns 503, and 404 is precisely the answer Chronicle
avoided — and gains the three checks that ARE the blackout test.

.ai.md stops carrying a third copy and points at the other two instead:
three copies of one cross-repo claim is why the last one went stale
unnoticed.

Every cross-repo claim now carries a "Re-verify by" date, and a claim past
its date is to be treated as unknown rather than as fact.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB
Closes the defence-in-depth gap booked in the previous pass. The dashboard
returns before reaching classifyCalendarSyncState during the blackout, so
this branch is unreachable today — which is precisely why it is worth
having and pinning.

Without it the classifier's fall-through answers 'date-drift' whenever a
date is missing, so a Chronicle-side outage would render as "out of sync
with Chronicle" and send the GM to check settings that are fine. That
invariant previously held only because every caller remembered to guard it,
and an invariant that depends on all its callers remembering is not one.

Ranked above 'paused' deliberately: with no server-side calendar there is
nothing to be paused against, and no other verdict below is computable.

The dashboard is NOT given a no-op `unavailable: false` argument — the
parameter already defaults to false, and passing it would imply the
dashboard computes something it does not.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBbG8VuBxaTjDHEXDYMvPB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants