Six interconnected workstreams addressing map sync migration, conflict resolution, folder organization, multi-page journal structure, and GM/Scribe notes.
Problem: Current map-sync.mjs syncs Chronicle maps to Foundry Scenes. The
map-collab-tool project already solved interactive map journal pages with
collaborative pin placement. We need to merge that approach.
Approach: Integrate the map-collab-tool's JournalPageSheet approach into
this module so Chronicle maps become Journal Entry pages (type "image" with
interactive pins/drawings overlay) rather than Scene objects.
-
Port core files from map-collab-tool into this module:
map-page-sheet.mjs→scripts/map-page-sheet.mjs(the interactive viewer)pin-config.mjs→scripts/pin-config.mjs(pin editing dialogs)socket-manager.mjs→ merge into existingapi-client.mjsor keep asscripts/map-socket.mjs(refactored to use Chronicle WS instead of Foundry sockets)- Templates and styles from map-collab-tool → merge into our templates/styles
-
Register the custom JournalEntryPage sheet in
module.mjs:- Register
MapPageSheetas a sheet for image-type journal pages - Chronicle map entities create JournalEntries with an "image" page using the Chronicle map's background image
- Register
-
Refactor
map-sync.mjs:- Remove Scene-based hooks (
createDrawing,updateDrawingon Scene, etc.) - Chronicle maps now create/update JournalEntry documents with:
- Page 1: Map image (the map background)
- Pins/annotations stored as flags on the journal page (not Scene drawings)
- Drawing/token/fog data stored in
flags['chronicle-sync'].mapDataon the journal page rather than as Scene embedded documents - Keep the coordinate conversion helpers (percentage ↔ pixel)
- Remove Scene-based hooks (
-
Pin ↔ Chronicle Drawing/Token mapping:
- Map-collab-tool pins (Location, Danger, Treasure, Quest, Note) map to Chronicle drawing types or token markers
- Pin create/move/delete → push to Chronicle API (
/maps/:id/drawings) - Chronicle
drawing.created/updated/deletedWS events → update pins on the journal map page in real-time
-
Fog of war as journal page overlay (stretch):
- Fog regions rendered as canvas overlay on the map page sheet instead of Scene drawings
- GM can toggle fog visibility per-region
-
Migration path:
- Existing Scene-linked maps: provide a dashboard action "Migrate to Journal Map" that creates a JournalEntry from the linked Scene data
- Keep Scene-based sync as deprecated fallback for one version cycle
- Possibly none — existing
/maps/:id/drawingsand/maps/:id/tokensendpoints should work. The pin types from map-collab-tool can map to Chronicle drawing types. - If Chronicle doesn't have a "pin" concept distinct from "drawing", we may want
a
drawing_type: 'pin'with apin_categoryfield. Let me know if this needs API changes.
Problem: The conflictResolution setting exists ("chronicle", "foundry",
"newest") but is never read by any sync module. Current behavior is last-write-wins
with no comparison or notification.
-
Add
updated_attracking to sync flags:- When syncing from Chronicle, store
chronicle-sync.remoteUpdatedAt= entity'supdated_attimestamp - When syncing from Foundry, store
chronicle-sync.localUpdatedAt=Date.now()ISO string - On each Foundry hook change (while GM is connected), set a
chronicle-sync.dirtyflag =true
- When syncing from Chronicle, store
-
Create
scripts/conflict-resolver.mjs:class ConflictResolver { // Compare local vs remote timestamps + dirty flags // Returns: 'apply_remote', 'keep_local', 'conflict' resolve(journal, remoteEntity, strategy) { ... } // For 'newest' strategy: compare remoteEntity.updated_at vs // journal flag localUpdatedAt // For 'chronicle'/'foundry': return the configured winner // For 'conflict' result: queue for GM notification } -
Wire into JournalSync._onEntityUpdated():
- Before overwriting, call
ConflictResolver.resolve() - If result is
'keep_local'→ skip the remote update, optionally push local to Chronicle - If result is
'conflict'→ queue a notification, don't auto-resolve - If result is
'apply_remote'→ proceed as now
- Before overwriting, call
-
Wire into JournalSync._handleUpdateJournal():
- Before pushing to Chronicle, check if the remote has been updated since our
last sync (
remoteUpdatedAt) - If remote is newer and strategy isn't "foundry wins" → conflict
- Before pushing to Chronicle, check if the remote has been updated since our
last sync (
-
GM Notification System (
scripts/sync-notifications.mjs):- Persistent notification area (sidebar widget or dashboard tab section)
- Shows: "Entity X was updated on Chronicle while you were offline. [Apply Remote] [Keep Local] [View Diff]"
- "View Diff" opens a simple side-by-side HTML diff of the content
- Notifications persist until resolved (stored in a world setting or flags)
- Batch notification on initial sync: "12 entities were updated while offline. [Review Changes]"
-
Offline change detection:
- On each Foundry document change (while connected), mark the journal's
dirtyflag = true andlocalUpdatedAt= now - On initial sync reconnect, for each mapping:
- Fetch the remote entity's
updated_at - Check if local journal has
dirtyflag - If both sides changed → conflict notification
- If only remote changed → apply per strategy
- If only local changed → push per strategy
- Fetch the remote entity's
- On each Foundry document change (while connected), mark the journal's
-
Apply to all sync modules (not just journals):
- ActorSync, CalendarSync, MapSync all get the same conflict check
updated_aton entity responses: The/entities/:idresponse likely already includesupdated_at. If not, it needs to be added.- Conditional update endpoint (nice-to-have):
PUT /entities/:idwithIf-Unmodified-Sinceheader orexpected_versionfield to prevent silent overwrites. This would let the module do optimistic concurrency. Check if this exists or needs adding.
Problem: Chronicle entities have categories (entity types). Foundry has a Folder system for JournalEntries. Currently, all synced journals land in the root with no folder organization.
-
Fetch categories from Chronicle API:
- On initial sync, call
GET /entity-types(or equivalent) to get the list of entity type categories - Each category has:
id,name, possiblyparent_idfor nested categories
- On initial sync, call
-
Create/sync Foundry Folders:
- For each Chronicle category, create a Foundry
Folder(type "JournalEntry") with a matching name - Store
chronicle-sync.categoryIdflag on each Folder - Nested categories → nested Folders (Foundry supports folder nesting)
- On category rename in Chronicle → rename Folder in Foundry
- For each Chronicle category, create a Foundry
-
Assign journals to folders on create/update:
- In
JournalSync._createJournalFromEntity(): look up the entity'sentity_type_idortype_name, find the matching Folder, setfolder: folderIdon the journal data - In
JournalSync._onEntityUpdated(): if entity type changed, move journal to the new folder
- In
-
Foundry → Chronicle folder assignment:
- When a journal is moved between folders in Foundry, detect via
updateJournalEntryhook (check forfolderinchange), and update the entity'sentity_type_idin Chronicle
- When a journal is moved between folders in Foundry, detect via
-
Create a folder manager utility (
scripts/folder-manager.mjs):class FolderManager { async syncFolders(api) // Pull categories, create/update folders getFolderForCategory(categoryId) // Lookup getCategoryForFolder(folderId) // Reverse lookup }
GET /entity-types(or/categories): Need endpoint that returns the category tree withid,name,parent_id. Confirm this exists.- Category change events via WS:
category.created,category.updated,category.deletedmessages so folders stay in sync real-time. May need adding.
Problem: Current journal sync splits Chronicle entry_html by headings into
pages. But Chronicle entities have structured content — character info fields,
an entry_html body, and potentially player-facing notes. These should map to
distinct Foundry journal pages.
-
Page structure for synced entities:
- Page 1: "Overview" — Entity image (if exists) + basic info summary (type, tags, custom fields rendered as a table)
- Page 2: "Content" — The
entry_htmlbody (or split by headings as now) - Page 3: "Player Notes" — Only created if the entity has a
player_notesorpublic_notesfield. This page getsownership.default = OBSERVERso players can see it even if the main entry is GM-only - Additional pages for heading splits of the main content (as current logic)
-
Modify
_createJournalFromEntity():- Build pages array with the structured layout above
- Tag each page with a
chronicle-sync.pageRoleflag:'overview','content','player_notes' - On update, match pages by role flag rather than by index position
-
Modify
_syncPagesToJournal():- Match existing pages by their
pageRoleflag - Update content in-place rather than positional index matching
- Only create "Player Notes" page if the entity has player notes content
- Remove "Player Notes" page if the field is emptied
- Match existing pages by their
-
Foundry → Chronicle for player notes:
- When a player-notes page is edited in Foundry, push it to the entity's
player_notesfield (not the mainentryfield) - Detect which page was edited by checking the
pageRoleflag
- When a player-notes page is edited in Foundry, push it to the entity's
-
Real-time page updates:
- WS
entity.updatedevents should include which fields changed. If onlyplayer_noteschanged, only update that page (avoid unnecessary re-renders of the full journal)
- WS
player_notesfield on entities: Confirm entities have a separate player-notes field (or equivalent likepublic_description). If not, this needs adding to the entity model.- Partial update events: WS
entity.updatedideally includes achanged_fieldsarray so the module knows which page(s) to update. Nice to have, not blocking.
Problem: Current sync works but has no multiplayer awareness. Multiple users editing the same entity on Chronicle website and in Foundry simultaneously need live updates without polling.
-
Already handled by WebSocket — the existing WS infrastructure routes
entity.updatedevents in real-time. The main gaps are:- No debouncing of rapid edits (Foundry user typing → each keystroke fires
updateJournalEntry) - No presence awareness ("User X is editing entity Y")
- No debouncing of rapid edits (Foundry user typing → each keystroke fires
-
Debounce Foundry → Chronicle pushes:
- Add a 2-second debounce timer per journal in
_handleUpdateJournal() - Collect changes during the window, push the final state
- Immediately push on tab close / page unload
- Add a 2-second debounce timer per journal in
-
Presence indicators (stretch goal):
- If Chronicle WS supports presence events (
user.editing.start/stop), show a small indicator on the journal entry header: "Being edited on Chronicle by [username]" - In Foundry's journal sidebar, show a colored dot on entries being edited remotely
- If Chronicle WS supports presence events (
- Debounce is client-side only — no API changes needed
- Presence events (stretch): WS messages
user.editing.start/user.editing.stopwith{ entity_id, user_name }. Only if you want this feature.
Problem: GMs and Scribes need a personal notes area that syncs between Chronicle and Foundry. This is distinct from entity content — it's session notes, GM prep, campaign plans, etc.
-
Identify the Chronicle concept:
- Does Chronicle have a "notes" or "journal" feature separate from entities? (Session notes, GM notes, campaign log?)
- If yes → sync as a dedicated Foundry JournalEntry folder "Chronicle Notes"
- If these are just entities with a specific type → handle via category/folder mapping (Step 3) with a "Notes" category
-
Create
scripts/note-sync.mjs:- Similar pattern to JournalSync but scoped to note-type entities
- Notes are GM-only by default (ownership NONE for non-GMs)
- Scribe role: if a Foundry user is mapped to a Chronicle Scribe, they get OWNER on note journals
-
Role mapping (prerequisite):
- Need a way to map Foundry user IDs to Chronicle user IDs
- Settings:
userMapping— JSON map of{ foundryUserId: chronicleUserId } - Dashboard tab or settings form to configure this mapping
- Once mapped, Scribe permissions can flow properly
-
WS events for notes:
- If notes are entities, already handled by
entity.created/updated/deleted - If notes are a separate resource type, need new WS message types
- If notes are entities, already handled by
- Clarify: Are GM/Scribe notes entities with a special type, or a separate resource? This determines whether we reuse JournalSync or build NotesSync.
- User ID mapping: Need an endpoint to look up Chronicle users by some
identifier (email, username) so we can build the mapping table. Something
like
GET /users?search=...orGET /campaign-members.
- Category → Folder mapping (Step 3) — foundational, relatively standalone
- Conflict resolution (Step 2) — critical safety feature before more sync
- Multi-page journals (Step 4) — builds on folder work, improves sync quality
- Map journal pages (Step 1) — largest change, refactors map-sync entirely
- GM/Scribe notes (Step 6) — depends on role mapping, may need API work
- Real-time enhancements (Step 5) — polish layer on top of everything
- Does Chronicle have a
player_notes/public_notesfield on entities? - Does the
/entity-typesendpoint exist and return parent/child relationships? - Are GM/Scribe notes stored as entities (with a type) or a separate resource?
- Does
PUT /entities/:idsupport conditional updates (versioning/etag)? - Should map pins from map-collab-tool map to Chronicle drawings, or do we need a new "pin" resource type on the API?
- Is there a
GET /campaign-membersendpoint for user ID mapping?