|
| 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? |
0 commit comments