Skip to content

fix(sync): make sync actually work — initial sync, honest badge, wire fixes (FM-SYNC-WIRE-FIX-R1)#81

Merged
keyxmakerx merged 3 commits into
mainfrom
claude/foundry-fm-sync-wire-fix-ku4cac
Jul 25, 2026
Merged

fix(sync): make sync actually work — initial sync, honest badge, wire fixes (FM-SYNC-WIRE-FIX-R1)#81
keyxmakerx merged 3 commits into
mainfrom
claude/foundry-fm-sync-wire-fix-ku4cac

Conversation

@keyxmakerx

Copy link
Copy Markdown
Owner

Cites: cordinator plans/2026-07-24-calendar-remodel-requirements.md §2 ("Sync broken exactly as experienced"); dispatches/foundry/FM-SYNC-WIRE-FIX-R1.md
Security implication: none — no new trust surface; visibility toggle now uses the correct write-permission endpoint it already had rights for.
Consumer-verified: Chronicle syncapi/routes.go:147-148 (flat PUT|DELETE /relations/:relationId), :181 (POST /entities/:id/reveal); syncapi/api_handler.go:571 (ToggleEntityReveal, binds {is_private}), :1219-1226 (apiCreateRelationRequest snake_case), :1249-1251 (target_entity_id required), :1267-1268 (apiUpdateRelationRequest binds only metadata); internal/widgets/relations/model.go:21-31 (camelCase read shape).
Foundry compatibility: logic-only; exercised headlessly (node --test). Not runtime-verified against a live client — see Coordinator must re-verify below (v12–v14 smoke-test recommended, can't be unit-tested).
Mockup: n/a (badge reuses existing .sync-badge styles + fa-* icons).

What this changes

Makes Chronicle⇄Foundry sync actually work. Five coordinator-verified defects had it reporting "syncing" while doing nothing: initial pull-sync never fired on any world, the SimpleCalendar structure guard was absent, the dashboard badge lied, the visibility toggle 400'd, and every item-relation write hit a 404 or 400. All fixes are module-side — the endpoints/behaviors they target already exist on Chronicle.

Why

plans/2026-07-24-calendar-remodel-requirements.md §2 records the operator experience ("Calendaria sync flat out doesn't work — says syncing but completely separate calendars") and the root causes; this PR is the FM-SYNC-WIRE-FIX-R1 dispatch that fixes them. Supersedes sweep BOX 3 (reports/meta/2026-07-18-audit-full-sweep-r1.md §4 box 3).

Step 0 — anchors verified on current main

Every dispatch anchor confirmed by reading source (both repos; Chronicle read-only):

  • FM-SYNC-1: api-client.mjs:808 emits _emit('sync.status', { status: 'connected' }); _emit (:921-926) passes that object straight to cb(data). The listener at sync-manager.mjs:156 read msg.payload?.status. Deterministic repro (no live world needed — the guard is pure):
    emitted = { status: 'connected' }
    old guard  msg.payload?.status === 'connected'         → false   ← initial sync never ran
    new guard  (msg.status ?? msg.payload?.status) === ... → true
    
    Because the guard was never true, _initialSyncDone stayed false, so the reconnect resync (:197, gated on _initialSyncDone) was also dead.
  • Fix 2: calendar-sync.mjs:452 gated the whole structure comparison on _calendarModule === 'calendaria'; the SimpleCalendar path fell through to _setLocalDate (:468) unguarded.
  • Fix 3: sync-dashboard.mjs:667-669 computed inSync from raw y/m/d equality; structureMismatch (:675) came from a flag the SC path never set.
  • Fix 4: sync-dashboard.mjs:2014 + :2898 PUT /entities/:id with only {is_private} → Chronicle UpdateEntity requires a name → 400, swallowed.
  • Fix 5: item-sync.mjs:351 DELETE and :385 PUT used nested /entities/:id/relations/:relId[/metadata]; create body (:306-315) was camelCase with a null target.

The five fixes (file:line)

# Fix Load-bearing change
1 Initial sync revived sync-manager.mjs — inline listener → _onSyncStatus(msg) reading msg.status ?? msg.payload?.status. Fix on the LISTENER, not the emit (emit shapes are intentionally mixed — sync.retryComplete uses {payload} — and the same object feeds the wildcard router). Revives _performInitialSync and the reconnect resync; idempotent via _initialSyncDone.
2 SC structure guard calendar-sync.mjs — new _readActiveSimpleCalendarStructure() (getCurrentCalendar()numberOfDays/weekdays, fallback getAllMonths()/getAllWeekdays()) + _readActiveFoundryStructure() dispatcher; guard condition widened from Calendaria-only. Still fails OPEN on an unreadable structure. Committed before fix 1 (commit 27e79f1 precedes 886eb24).
3 Honest four-state badge new pure scripts/_calendar-sync-state.mjsclassifyCalendarSyncState() yields in-sync / date-drift (with direction) / incompatible-structures (with counts) / paused. sync-dashboard.mjs::_buildCalendarData feeds it the live module's pause flag and its own compareCalendarStructures result (so incompatibility shows even when the module failed OPEN). templates/sync-dashboard.hbs renders all four.
4 Visibility via /reveal sync-dashboard.mjs::_onToggleVisibility + _onBulkSetVisibilityPOST /entities/:id/reveal with {is_private}.
5 Item relations item-sync.mjs — DELETE/PUT → flat /relations/:relationId; create body snake_case (target_entity_id/relation_type/reverse_relation_type) with object metadata; skips create when the item has no linked Chronicle entity (custom items have no target — logged once, no spam).

⚠ Mandatory disclosures (shipping WITH them)

  1. Fix 1 activates code that has NEVER run on any world — the full /sync/pull since epoch, every module's onInitialSync, and the journal duplicate-deletion post-pass (sync-manager.mjs:542-550). Please test on a world with existing synced data, not only a fresh world.
  2. Calendaria back-catalog mapping storage is per-GM-flag (pre-existing; fix 1 first EXPOSES it). _syncChronicleEventsToCalendariaNotes (calendar-sync.mjs) is idempotent per GM (seen-set + _getLocalEventId skip + _syncDepth echo guard), BUT mappings live in per-user GM flags (game.user.setFlag, ~:1484): a different GM account loading the world duplicates notes, and hand-authored Calendaria notes duplicate once. Documented in .ai.md; not re-architected here — flagged as a follow-up (world-scope flags).
  3. Drifted worlds will visibly date-jump on first load post-fix (plus a confirm beacon). Expected.

Deviations from the dispatch

  • Metadata encoding (ride-along within "fix body"). Item-relation create/update now send metadata as a raw object, not JSON.stringify(...). Chronicle binds metadata as json.RawMessage; the old stringified form stored a double-encoded JSON string. The module read path already handled both (item-sync.mjs:251), so this is a correctness improvement with no read regression. Called out per honest-deviation discipline.
  • Fix 3 adds a 4th independently-reachable state on purpose. incompatible-structures (module NOT paused, but the dashboard reads both structures and they differ) is distinct from paused (module stopped sync this session). This makes the badge honest in the fail-OPEN case (module skipped pausing on an unreadable structure at connect, or isn't running) rather than collapsing to three states.
  • Step-0 "console evidence" is a deterministic source-level repro, not a live-world console capture — this environment is headless (no Foundry client). The guard is pure, so the evaluation above is exact; a live console repro is left to coordinator smoke-test.

Test plan

  • node --test tools/test-*.mjs704 pass / 0 fail (was 675; +13 test-calendar-sync-state.mjs, +16 test-sync-wire-fix.mjs).
  • node tools/check-package-descriptor.mjs — OK (0 warnings).
  • New pins: fix 1 (both emit shapes + one-shot latch), fix 2 (SC reader via getCurrentCalendar + fallback + fail-open + onInitialSync pause/no-pause), fix 4 (POST /reveal, never a bare PUT), fix 5 (flat routes + snake_case body + skip-no-target), classifier (all four states reachable).
  • Manual verification in Foundry (coordinator — see below).
  • CI passes.

Coordinator must re-verify (live client)

  1. Fresh world: initial sync fires (console _performInitialSync / "Initial sync complete"); genuine reconnect re-pulls once.
  2. World with existing synced data: no duplicate journals/notes; date confirm beacon fires.
  3. SimpleCalendar world with a mismatched structure: sync pauses + badge reads Incompatible Structures / Sync Paused.
  4. Badge four states truthful on both Calendaria + SimpleCalendar.
  5. Entity visibility toggle round-trips (single + bulk); item add/remove/update relations round-trip.

Tenet self-check

  • T-B1 security: no new trust surface; /reveal needs the same write permission the old (failing) PUT did.
  • T-B2 plugin isolation: entirely within the foundry-vtt module half.
  • T-B3 production UI: badge has explicit states incl. paused/incompatible/drift + titles; no motion added.
  • T-B4 dual-audience docs: .ai.md decision log + API-CONTRACT.md errata updated alongside the code.

Generated by Claude Code

claude added 3 commits July 24, 2026 17:42
…path

FM-SYNC-WIRE-FIX-R1 fix 2. onInitialSync's structure guard was gated on
`_calendarModule === 'calendaria'`, so a SimpleCalendar world with a
structure that differs from Chronicle's fell straight through to
`_setLocalDate` and wrote Chronicle's coordinates into an incompatible
calendar. Add `_readActiveSimpleCalendarStructure()` (SimpleCalendar's
getCurrentCalendar() → numberOfDays/weekdays, with a
getAllMonths()/getAllWeekdays() fallback) and a `_readActiveFoundryStructure()`
dispatcher; the guard now covers both module paths and still fails OPEN on an
unreadable structure.

Committed BEFORE fix 1 (initial-sync revival) so the guard is hot in history
before revived initial sync can fire date-writes into an unguarded
SimpleCalendar world.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MFs4Q928arZHLwvWWYAQm
FM-SYNC-WIRE-FIX-R1 fix 1 (FM-SYNC-1). api-client emits sync.status
UNWRAPPED — `_emit('sync.status', { status: 'connected' })` fans the object
straight to listeners — but the SyncManager listener read only
`msg.payload?.status`, so its guard was never true. `_performInitialSync`
(the full `/sync/pull` since epoch, every module's onInitialSync, the journal
duplicate-deletion post-pass) and the reconnect resync (gated on the never-set
`_initialSyncDone`) were BOTH dead on every world.

Fix the LISTENER, not the emit: extract `_onSyncStatus(msg)` reading
`msg.status ?? msg.payload?.status`. The emit stays as-is because client emit
shapes are intentionally inconsistent (sync.retryComplete uses {payload}) and
the same object feeds the wildcard router, so normalizing at the emit would
ripple. The handler is idempotent via the `_initialSyncDone` latch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MFs4Q928arZHLwvWWYAQm
…outes

FM-SYNC-WIRE-FIX-R1 fixes 3–5 + tests + docs.

Fix 3 — honest four-state calendar badge. The dashboard computed "In Sync"
from raw y/m/d equality and drove "paused" from a flag the SimpleCalendar path
never set. Replace with a pure classifier (scripts/_calendar-sync-state.mjs)
producing exactly one of: in-sync / date-drift (with direction) /
incompatible-structures (with month & weekday counts) / paused. The dashboard
feeds it the live module's pause flag AND its own structure comparison, so an
incompatibility surfaces even when the module failed OPEN. Template renders all
four states.

Fix 4 — visibility toggle. Single + bulk toggles PUT /entities/:id with only
{is_private} → UpdateEntity requires a name → 400 (swallowed). Route to
POST /entities/:id/reveal (accepts exactly {is_private}).

Fix 5 — item relations. DELETE/PUT used nested
/entities/:id/relations/:relId[/metadata] (404); create body was camelCase
with a null target. Chronicle serves flat DELETE|PUT /relations/:relationId
and binds snake_case target_entity_id/relation_type/reverse_relation_type
(required target). Fix paths + body (snake_case, object metadata); skip
relation-create when the item has no linked Chronicle entity (logged once).

Tests: tools/test-calendar-sync-state.mjs (13) + tools/test-sync-wire-fix.mjs
(16) cover fixes 1,2,4,5 and the classifier. Full suite green (704).

Docs: .ai.md decision log + Files/discrepancies/limitations; API-CONTRACT.md
erratum notes on the nonexistent nested relation paths and the reveal body;
CLAUDE.md smoke-test checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018MFs4Q928arZHLwvWWYAQm
@keyxmakerx
keyxmakerx merged commit caad6c4 into main Jul 25, 2026
1 check passed
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