Skip to content

Commit 8a3cb54

Browse files
authored
Merge pull request #9 from keyxmakerx/claude/foundry-multiplayer-sync-SZHH2
Claude/foundry multiplayer sync szhh2
2 parents a232bfc + b616563 commit 8a3cb54

12 files changed

Lines changed: 1844 additions & 14 deletions

PLAN.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# Implementation Plan: Map Journal Pages, Conflict Resolution, Folders & Multi-Page Journals
2+
3+
## Overview
4+
5+
Six interconnected workstreams addressing map sync migration, conflict resolution,
6+
folder organization, multi-page journal structure, and GM/Scribe notes.
7+
8+
---
9+
10+
## 1. Map Sync → Journal Map Pages (migrate from Scene-based)
11+
12+
**Problem**: Current `map-sync.mjs` syncs Chronicle maps to Foundry Scenes. The
13+
`map-collab-tool` project already solved interactive map journal pages with
14+
collaborative pin placement. We need to merge that approach.
15+
16+
**Approach**: Integrate the map-collab-tool's `JournalPageSheet` approach into
17+
this module so Chronicle maps become Journal Entry pages (type "image" with
18+
interactive pins/drawings overlay) rather than Scene objects.
19+
20+
### Steps
21+
22+
1. **Port core files from map-collab-tool into this module**:
23+
- `map-page-sheet.mjs``scripts/map-page-sheet.mjs` (the interactive viewer)
24+
- `pin-config.mjs``scripts/pin-config.mjs` (pin editing dialogs)
25+
- `socket-manager.mjs` → merge into existing `api-client.mjs` or keep as
26+
`scripts/map-socket.mjs` (refactored to use Chronicle WS instead of
27+
Foundry sockets)
28+
- Templates and styles from map-collab-tool → merge into our templates/styles
29+
30+
2. **Register the custom JournalEntryPage sheet** in `module.mjs`:
31+
- Register `MapPageSheet` as a sheet for image-type journal pages
32+
- Chronicle map entities create JournalEntries with an "image" page using the
33+
Chronicle map's background image
34+
35+
3. **Refactor `map-sync.mjs`**:
36+
- Remove Scene-based hooks (`createDrawing`, `updateDrawing` on Scene, etc.)
37+
- Chronicle maps now create/update **JournalEntry** documents with:
38+
- Page 1: Map image (the map background)
39+
- Pins/annotations stored as flags on the journal page (not Scene drawings)
40+
- Drawing/token/fog data stored in `flags['chronicle-sync'].mapData` on the
41+
journal page rather than as Scene embedded documents
42+
- Keep the coordinate conversion helpers (percentage ↔ pixel)
43+
44+
4. **Pin ↔ Chronicle Drawing/Token mapping**:
45+
- Map-collab-tool pins (Location, Danger, Treasure, Quest, Note) map to
46+
Chronicle drawing types or token markers
47+
- Pin create/move/delete → push to Chronicle API (`/maps/:id/drawings`)
48+
- Chronicle `drawing.created/updated/deleted` WS events → update pins on the
49+
journal map page in real-time
50+
51+
5. **Fog of war as journal page overlay** (stretch):
52+
- Fog regions rendered as canvas overlay on the map page sheet instead of
53+
Scene drawings
54+
- GM can toggle fog visibility per-region
55+
56+
6. **Migration path**:
57+
- Existing Scene-linked maps: provide a dashboard action "Migrate to Journal
58+
Map" that creates a JournalEntry from the linked Scene data
59+
- Keep Scene-based sync as deprecated fallback for one version cycle
60+
61+
### API Changes Needed (Chronicle website)
62+
- **Possibly none** — existing `/maps/:id/drawings` and `/maps/:id/tokens`
63+
endpoints should work. The pin types from map-collab-tool can map to Chronicle
64+
drawing types.
65+
- If Chronicle doesn't have a "pin" concept distinct from "drawing", we may want
66+
a `drawing_type: 'pin'` with a `pin_category` field. **Let me know if this
67+
needs API changes.**
68+
69+
---
70+
71+
## 2. Conflict Resolution & Merge Support
72+
73+
**Problem**: The `conflictResolution` setting exists ("chronicle", "foundry",
74+
"newest") but is never read by any sync module. Current behavior is last-write-wins
75+
with no comparison or notification.
76+
77+
### Steps
78+
79+
1. **Add `updated_at` tracking to sync flags**:
80+
- When syncing from Chronicle, store `chronicle-sync.remoteUpdatedAt` =
81+
entity's `updated_at` timestamp
82+
- When syncing from Foundry, store `chronicle-sync.localUpdatedAt` =
83+
`Date.now()` ISO string
84+
- On each Foundry hook change (while GM is connected), set a
85+
`chronicle-sync.dirty` flag = `true`
86+
87+
2. **Create `scripts/conflict-resolver.mjs`**:
88+
```
89+
class ConflictResolver {
90+
// Compare local vs remote timestamps + dirty flags
91+
// Returns: 'apply_remote', 'keep_local', 'conflict'
92+
resolve(journal, remoteEntity, strategy) { ... }
93+
94+
// For 'newest' strategy: compare remoteEntity.updated_at vs
95+
// journal flag localUpdatedAt
96+
// For 'chronicle'/'foundry': return the configured winner
97+
// For 'conflict' result: queue for GM notification
98+
}
99+
```
100+
101+
3. **Wire into JournalSync._onEntityUpdated()**:
102+
- Before overwriting, call `ConflictResolver.resolve()`
103+
- If result is `'keep_local'` → skip the remote update, optionally push
104+
local to Chronicle
105+
- If result is `'conflict'` → queue a notification, don't auto-resolve
106+
- If result is `'apply_remote'` → proceed as now
107+
108+
4. **Wire into JournalSync._handleUpdateJournal()**:
109+
- Before pushing to Chronicle, check if the remote has been updated since our
110+
last sync (`remoteUpdatedAt`)
111+
- If remote is newer and strategy isn't "foundry wins" → conflict
112+
113+
5. **GM Notification System** (`scripts/sync-notifications.mjs`):
114+
- Persistent notification area (sidebar widget or dashboard tab section)
115+
- Shows: "Entity X was updated on Chronicle while you were offline.
116+
[Apply Remote] [Keep Local] [View Diff]"
117+
- "View Diff" opens a simple side-by-side HTML diff of the content
118+
- Notifications persist until resolved (stored in a world setting or flags)
119+
- Batch notification on initial sync: "12 entities were updated while offline.
120+
[Review Changes]"
121+
122+
6. **Offline change detection**:
123+
- On each Foundry document change (while connected), mark the journal's
124+
`dirty` flag = true and `localUpdatedAt` = now
125+
- On initial sync reconnect, for each mapping:
126+
- Fetch the remote entity's `updated_at`
127+
- Check if local journal has `dirty` flag
128+
- If both sides changed → conflict notification
129+
- If only remote changed → apply per strategy
130+
- If only local changed → push per strategy
131+
132+
7. **Apply to all sync modules** (not just journals):
133+
- ActorSync, CalendarSync, MapSync all get the same conflict check
134+
135+
### API Changes Needed (Chronicle website)
136+
- **`updated_at` on entity responses**: The `/entities/:id` response likely
137+
already includes `updated_at`. If not, it needs to be added.
138+
- **Conditional update endpoint** (nice-to-have): `PUT /entities/:id` with
139+
`If-Unmodified-Since` header or `expected_version` field to prevent silent
140+
overwrites. This would let the module do optimistic concurrency. **Check if
141+
this exists or needs adding.**
142+
143+
---
144+
145+
## 3. Category → Folder Mapping
146+
147+
**Problem**: Chronicle entities have categories (entity types). Foundry has a
148+
Folder system for JournalEntries. Currently, all synced journals land in the
149+
root with no folder organization.
150+
151+
### Steps
152+
153+
1. **Fetch categories from Chronicle API**:
154+
- On initial sync, call `GET /entity-types` (or equivalent) to get the list
155+
of entity type categories
156+
- Each category has: `id`, `name`, possibly `parent_id` for nested categories
157+
158+
2. **Create/sync Foundry Folders**:
159+
- For each Chronicle category, create a Foundry `Folder` (type "JournalEntry")
160+
with a matching name
161+
- Store `chronicle-sync.categoryId` flag on each Folder
162+
- Nested categories → nested Folders (Foundry supports folder nesting)
163+
- On category rename in Chronicle → rename Folder in Foundry
164+
165+
3. **Assign journals to folders on create/update**:
166+
- In `JournalSync._createJournalFromEntity()`: look up the entity's
167+
`entity_type_id` or `type_name`, find the matching Folder, set
168+
`folder: folderId` on the journal data
169+
- In `JournalSync._onEntityUpdated()`: if entity type changed, move journal
170+
to the new folder
171+
172+
4. **Foundry → Chronicle folder assignment**:
173+
- When a journal is moved between folders in Foundry, detect via
174+
`updateJournalEntry` hook (check for `folder` in `change`), and update
175+
the entity's `entity_type_id` in Chronicle
176+
177+
5. **Create a folder manager utility** (`scripts/folder-manager.mjs`):
178+
```
179+
class FolderManager {
180+
async syncFolders(api) // Pull categories, create/update folders
181+
getFolderForCategory(categoryId) // Lookup
182+
getCategoryForFolder(folderId) // Reverse lookup
183+
}
184+
```
185+
186+
### API Changes Needed (Chronicle website)
187+
- **`GET /entity-types`** (or `/categories`): Need endpoint that returns the
188+
category tree with `id`, `name`, `parent_id`. **Confirm this exists.**
189+
- **Category change events via WS**: `category.created`, `category.updated`,
190+
`category.deleted` messages so folders stay in sync real-time. **May need
191+
adding.**
192+
193+
---
194+
195+
## 4. Multi-Page Journal Entries (Character Info + Player Notes)
196+
197+
**Problem**: Current journal sync splits Chronicle `entry_html` by headings into
198+
pages. But Chronicle entities have structured content — character info fields,
199+
an `entry_html` body, and potentially player-facing notes. These should map to
200+
distinct Foundry journal pages.
201+
202+
### Steps
203+
204+
1. **Page structure for synced entities**:
205+
- **Page 1: "Overview"** — Entity image (if exists) + basic info summary
206+
(type, tags, custom fields rendered as a table)
207+
- **Page 2: "Content"** — The `entry_html` body (or split by headings as now)
208+
- **Page 3: "Player Notes"** — Only created if the entity has a
209+
`player_notes` or `public_notes` field. This page gets
210+
`ownership.default = OBSERVER` so players can see it even if the main
211+
entry is GM-only
212+
- Additional pages for heading splits of the main content (as current logic)
213+
214+
2. **Modify `_createJournalFromEntity()`**:
215+
- Build pages array with the structured layout above
216+
- Tag each page with a `chronicle-sync.pageRole` flag: `'overview'`,
217+
`'content'`, `'player_notes'`
218+
- On update, match pages by role flag rather than by index position
219+
220+
3. **Modify `_syncPagesToJournal()`**:
221+
- Match existing pages by their `pageRole` flag
222+
- Update content in-place rather than positional index matching
223+
- Only create "Player Notes" page if the entity has player notes content
224+
- Remove "Player Notes" page if the field is emptied
225+
226+
4. **Foundry → Chronicle for player notes**:
227+
- When a player-notes page is edited in Foundry, push it to the entity's
228+
`player_notes` field (not the main `entry` field)
229+
- Detect which page was edited by checking the `pageRole` flag
230+
231+
5. **Real-time page updates**:
232+
- WS `entity.updated` events should include which fields changed. If only
233+
`player_notes` changed, only update that page (avoid unnecessary re-renders
234+
of the full journal)
235+
236+
### API Changes Needed (Chronicle website)
237+
- **`player_notes` field on entities**: Confirm entities have a separate
238+
player-notes field (or equivalent like `public_description`). If not, this
239+
needs adding to the entity model.
240+
- **Partial update events**: WS `entity.updated` ideally includes a
241+
`changed_fields` array so the module knows which page(s) to update. **Nice
242+
to have, not blocking.**
243+
244+
---
245+
246+
## 5. Real-Time Sync Emphasis
247+
248+
**Problem**: Current sync works but has no multiplayer awareness. Multiple users
249+
editing the same entity on Chronicle website and in Foundry simultaneously need
250+
live updates without polling.
251+
252+
### Steps
253+
254+
1. **Already handled by WebSocket** — the existing WS infrastructure routes
255+
`entity.updated` events in real-time. The main gaps are:
256+
- No debouncing of rapid edits (Foundry user typing → each keystroke fires
257+
`updateJournalEntry`)
258+
- No presence awareness ("User X is editing entity Y")
259+
260+
2. **Debounce Foundry → Chronicle pushes**:
261+
- Add a 2-second debounce timer per journal in `_handleUpdateJournal()`
262+
- Collect changes during the window, push the final state
263+
- Immediately push on tab close / page unload
264+
265+
3. **Presence indicators** (stretch goal):
266+
- If Chronicle WS supports presence events (`user.editing.start/stop`), show
267+
a small indicator on the journal entry header: "Being edited on Chronicle
268+
by [username]"
269+
- In Foundry's journal sidebar, show a colored dot on entries being edited
270+
remotely
271+
272+
### API Changes Needed (Chronicle website)
273+
- **Debounce is client-side only** — no API changes needed
274+
- **Presence events** (stretch): WS messages `user.editing.start` /
275+
`user.editing.stop` with `{ entity_id, user_name }`. **Only if you want
276+
this feature.**
277+
278+
---
279+
280+
## 6. GM/Scribe Note Sync
281+
282+
**Problem**: GMs and Scribes need a personal notes area that syncs between
283+
Chronicle and Foundry. This is distinct from entity content — it's session notes,
284+
GM prep, campaign plans, etc.
285+
286+
### Steps
287+
288+
1. **Identify the Chronicle concept**:
289+
- Does Chronicle have a "notes" or "journal" feature separate from entities?
290+
(Session notes, GM notes, campaign log?)
291+
- If yes → sync as a dedicated Foundry JournalEntry folder "Chronicle Notes"
292+
- If these are just entities with a specific type → handle via category/folder
293+
mapping (Step 3) with a "Notes" category
294+
295+
2. **Create `scripts/note-sync.mjs`**:
296+
- Similar pattern to JournalSync but scoped to note-type entities
297+
- Notes are GM-only by default (ownership NONE for non-GMs)
298+
- Scribe role: if a Foundry user is mapped to a Chronicle Scribe, they get
299+
OWNER on note journals
300+
301+
3. **Role mapping** (prerequisite):
302+
- Need a way to map Foundry user IDs to Chronicle user IDs
303+
- Settings: `userMapping` — JSON map of `{ foundryUserId: chronicleUserId }`
304+
- Dashboard tab or settings form to configure this mapping
305+
- Once mapped, Scribe permissions can flow properly
306+
307+
4. **WS events for notes**:
308+
- If notes are entities, already handled by `entity.created/updated/deleted`
309+
- If notes are a separate resource type, need new WS message types
310+
311+
### API Changes Needed (Chronicle website)
312+
- **Clarify**: Are GM/Scribe notes entities with a special type, or a separate
313+
resource? This determines whether we reuse JournalSync or build NotesSync.
314+
- **User ID mapping**: Need an endpoint to look up Chronicle users by some
315+
identifier (email, username) so we can build the mapping table. Something
316+
like `GET /users?search=...` or `GET /campaign-members`.
317+
318+
---
319+
320+
## Execution Order (Recommended)
321+
322+
1. **Category → Folder mapping** (Step 3) — foundational, relatively standalone
323+
2. **Conflict resolution** (Step 2) — critical safety feature before more sync
324+
3. **Multi-page journals** (Step 4) — builds on folder work, improves sync quality
325+
4. **Map journal pages** (Step 1) — largest change, refactors map-sync entirely
326+
5. **GM/Scribe notes** (Step 6) — depends on role mapping, may need API work
327+
6. **Real-time enhancements** (Step 5) — polish layer on top of everything
328+
329+
## Questions Before Starting
330+
331+
1. Does Chronicle have a `player_notes` / `public_notes` field on entities?
332+
2. Does the `/entity-types` endpoint exist and return parent/child relationships?
333+
3. Are GM/Scribe notes stored as entities (with a type) or a separate resource?
334+
4. Does `PUT /entities/:id` support conditional updates (versioning/etag)?
335+
5. Should map pins from map-collab-tool map to Chronicle drawings, or do we
336+
need a new "pin" resource type on the API?
337+
6. Is there a `GET /campaign-members` endpoint for user ID mapping?

lang/en.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
"SyncCharacters": {
3434
"Name": "Sync Characters",
3535
"Hint": "Sync Foundry actors with Chronicle character entities (requires matching game system)"
36+
},
37+
"SyncNotes": {
38+
"Name": "Sync Notes",
39+
"Hint": "Sync Chronicle campaign notes with Foundry journal entries"
3640
}
3741
},
3842
"Status": {
@@ -46,6 +50,11 @@
4650
"JournalSynced": "Journal synced from Chronicle: {name}",
4751
"MapSynced": "Map synced from Chronicle: {name}",
4852
"SyncConflict": "Sync conflict detected for: {name}",
53+
"ConflictKeptChronicle": "Conflict on \"{name}\" — kept Chronicle version.",
54+
"ConflictKeptFoundry": "Conflict on \"{name}\" — kept Foundry version.",
55+
"ConflictNewerChronicle": "Conflict on \"{name}\" — Chronicle version was newer.",
56+
"ConflictNewerFoundry": "Conflict on \"{name}\" — Foundry version was newer.",
57+
"MembersMatched": "Auto-matched {count} Chronicle members to Foundry users.",
4958
"ConnectionLost": "Lost connection to Chronicle. Retrying...",
5059
"ConnectionRestored": "Connection to Chronicle restored"
5160
},
@@ -58,6 +67,7 @@
5867
"Entities": "Entities",
5968
"Shops": "Shops",
6069
"Maps": "Maps",
70+
"Notes": "Notes",
6171
"Characters": "Characters",
6272
"Calendar": "Calendar",
6373
"Status": "Status"
@@ -113,6 +123,17 @@
113123
"NoModuleHint": "Install Calendaria or Simple Calendar to enable calendar sync.",
114124
"UnableToRead": "Unable to read"
115125
},
126+
"Notes": {
127+
"Synced": "Synced",
128+
"ChronicleOnly": "Chronicle Only",
129+
"Pull": "Pull",
130+
"Shared": "Shared",
131+
"Private": "Private",
132+
"NoNotes": "No Chronicle notes found.",
133+
"NoNotesHint": "Notes created in Chronicle will appear here when note sync is enabled.",
134+
"Disabled": "Note sync is disabled.",
135+
"DisabledHint": "Enable \"Sync Notes\" in Module Settings."
136+
},
116137
"Characters": {
117138
"Synced": "Synced",
118139
"NotLinked": "Not linked",

0 commit comments

Comments
 (0)