diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d13bd..2dfef20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to the Archivist Sync module will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.7] - 2025-12-05 + +### Fixed +- Realtime sync hooks now ignore core Foundry documents and third-party imports unless they carry Archivist flags, preventing conflicts with modules like PopOut and stopping unintended API POSTs for unrelated items/journals. + +### Changed +- World Setup and manual Sync dialogs now start with no rows selected so GMs must explicitly opt in to each import/diff, preventing accidental bulk operations. + ## [1.3.6] - 2025-11-19 ### Fixed diff --git a/module.json b/module.json index b276e38..c0487d8 100644 --- a/module.json +++ b/module.json @@ -8,7 +8,7 @@ "email": "cameron.b.llewellyn@gmail.com" } ], - "version": "1.3.6", + "version": "1.3.7", "compatibility": { "minimum": "13.341", "verified": "13.346" diff --git a/package.json b/package.json index a66c751..8dd976d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "archivist-sync", - "version": "1.3.6", + "version": "1.3.7", "description": "A simple Foundry VTT module for fetching world data from an API endpoint using an API key.", "type": "module", "scripts": { diff --git a/scripts/archivist-sync.js b/scripts/archivist-sync.js index 0ac8ba3..8ce28f1 100644 --- a/scripts/archivist-sync.js +++ b/scripts/archivist-sync.js @@ -988,6 +988,11 @@ function installRealtimeSyncListeners() { }; }; + // Helpers to scope realtime side-effects to Archivist-managed documents + const getPageMeta = (page) => Utils.getPageArchivistMeta(page) || {}; + const getPageMetaType = (page) => + String(getPageMeta(page)?.type || '').toLowerCase(); + // Create Hooks.on('createActor', async (doc) => { try { @@ -1003,25 +1008,6 @@ function installRealtimeSyncListeners() { console.warn('[RTS] createActor failed', e); } }); - Hooks.on('createItem', async (doc) => { - try { - if ( - !settingsManager.isRealtimeSyncEnabled?.() || - settingsManager.isRealtimeSyncSuppressed?.() - ) - return; - const id = doc.getFlag(CONFIG.MODULE_ID, 'archivistId'); - if (id) return; - const payload = toItemPayload(doc); - const res = await archivistApi.createItem(apiKey, payload); - if (res?.success && res?.data?.id) { - await doc.setFlag(CONFIG.MODULE_ID, 'archivistId', res.data.id); - await doc.setFlag(CONFIG.MODULE_ID, 'archivistWorldId', worldId); - } - } catch (e) { - console.warn('[RTS] createItem failed', e); - } - }); // JournalEntry create - create Archivist entities when a custom page-based sheet is created Hooks.on('createJournalEntry', async (entry, options, userId) => { @@ -1110,63 +1096,6 @@ function installRealtimeSyncListeners() { } }); - // JournalEntryPage create (Factions / Locations containers only) - const isFactionPage = (p) => p?.parent?.name === 'Factions'; - const isLocationPage = (p) => p?.parent?.name === 'Locations'; - const isRecapPage = (p) => p?.parent?.name === 'Recaps'; - - Hooks.on('createJournalEntryPage', async (page) => { - try { - if ( - !settingsManager.isRealtimeSyncEnabled?.() || - settingsManager.isRealtimeSyncSuppressed?.() - ) - return; - if (isRecapPage(page)) return; // Recaps are read-only for creation - const metaId = page.getFlag(CONFIG.MODULE_ID, 'archivistId'); - if (metaId) return; - if (isFactionPage(page)) { - const res = await archivistApi.createFaction( - apiKey, - toFactionPayload(page) - ); - if (res?.success && res?.data?.id) { - await Utils.setPageArchivistMeta( - page, - res.data.id, - 'faction', - worldId - ); - } else if (!res?.success && res?.isDescriptionTooLong) { - ui.notifications?.error?.( - `Failed to create ${res.entityName || page?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, - { permanent: true } - ); - } - } else if (isLocationPage(page)) { - const res = await archivistApi.createLocation( - apiKey, - toLocationPayload(page) - ); - if (res?.success && res?.data?.id) { - await Utils.setPageArchivistMeta( - page, - res.data.id, - 'location', - worldId - ); - } else if (!res?.success && res?.isDescriptionTooLong) { - ui.notifications?.error?.( - `Failed to create ${res.entityName || page?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, - { permanent: true } - ); - } - } - } catch (e) { - console.warn('[RTS] createJournalEntryPage failed', e); - } - }); - // Update Hooks.on('updateActor', async (doc, changes) => { try { @@ -1219,11 +1148,10 @@ function installRealtimeSyncListeners() { const mod = changes?.flags?.[CONFIG.MODULE_ID]; if (mod && Object.prototype.hasOwnProperty.call(mod, 'op')) return; } catch (_) {} - const meta = Utils.getPageArchivistMeta(page); - if (!meta?.id) return; + const meta = getPageMeta(page); + const metaType = getPageMetaType(page); let res; - // Faction pages: update Faction - if (isFactionPage(page)) { + if (meta?.id && metaType === 'faction') { res = await archivistApi.updateFaction( apiKey, meta.id, @@ -1235,8 +1163,9 @@ function installRealtimeSyncListeners() { { permanent: true } ); } - // Location pages: update Location - } else if (isLocationPage(page)) { + return; + } + if (meta?.id && metaType === 'location') { res = await archivistApi.updateLocation( apiKey, meta.id, @@ -1248,68 +1177,46 @@ function installRealtimeSyncListeners() { { permanent: true } ); } - // Recap pages: update Session title/summary only - } else if (isRecapPage(page)) { - // Recaps: update session summary/title only; do not create/delete + return; + } + if (meta?.id && metaType === 'recap') { const title = page.name; const html = Utils.extractPageHtml(page); await archivistApi.updateSession(apiKey, meta.id, { title, summary: Utils.toMarkdownIfHtml?.(html) || html, }); - } else { - // If the parent journal is flagged as character (pc/npc) or item, update those entities - const parent = page?.parent; - const flags = parent?.getFlag?.(CONFIG.MODULE_ID, 'archivist') || {}; - const html = Utils.extractPageHtml(page); - const isCharacter = - flags?.sheetType === 'pc' || - flags?.sheetType === 'npc' || - flags?.sheetType === 'character'; - if (isCharacter && flags.archivistId) { - res = await archivistApi.updateCharacter(apiKey, flags.archivistId, { - description: Utils.toMarkdownIfHtml?.(html) || html, - }); - if (!res?.success && res?.isDescriptionTooLong) { - ui.notifications?.error?.( - `Failed to sync ${res.entityName || parent?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, - { permanent: true } - ); - } - } - if (flags?.sheetType === 'item' && flags.archivistId) { - res = await archivistApi.updateItem(apiKey, flags.archivistId, { - description: Utils.toMarkdownIfHtml?.(html) || html, - }); - if (!res?.success && res?.isDescriptionTooLong) { - ui.notifications?.error?.( - `Failed to sync ${res.entityName || parent?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, - { permanent: true } - ); - } - } - if (flags?.sheetType === 'location' && flags.archivistId) { - res = await archivistApi.updateLocation(apiKey, flags.archivistId, { - description: Utils.toMarkdownIfHtml?.(html) || html, - }); - if (!res?.success && res?.isDescriptionTooLong) { - ui.notifications?.error?.( - `Failed to sync ${res.entityName || parent?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, - { permanent: true } - ); - } - } - if (flags?.sheetType === 'faction' && flags.archivistId) { - res = await archivistApi.updateFaction(apiKey, flags.archivistId, { - description: Utils.toMarkdownIfHtml?.(html) || html, - }); - if (!res?.success && res?.isDescriptionTooLong) { - ui.notifications?.error?.( - `Failed to sync ${res.entityName || parent?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, - { permanent: true } - ); - } - } + return; + } + + const parent = page?.parent; + const flags = parent?.getFlag?.(CONFIG.MODULE_ID, 'archivist') || {}; + const sheetType = String(flags?.sheetType || '').toLowerCase(); + if (!sheetType || !flags.archivistId) return; + const html = Utils.extractPageHtml(page); + const payload = { description: Utils.toMarkdownIfHtml?.(html) || html }; + + if (sheetType === 'pc' || sheetType === 'npc' || sheetType === 'character') { + res = await archivistApi.updateCharacter(apiKey, flags.archivistId, payload); + } else if (sheetType === 'item') { + res = await archivistApi.updateItem(apiKey, flags.archivistId, payload); + } else if (sheetType === 'location') { + res = await archivistApi.updateLocation(apiKey, flags.archivistId, payload); + } else if (sheetType === 'faction') { + res = await archivistApi.updateFaction(apiKey, flags.archivistId, payload); + } else if (sheetType === 'recap') { + await archivistApi.updateSession(apiKey, flags.archivistId, { + title: parent?.name || page.name, + summary: payload.description, + }); + return; + } + + if (res && !res?.success && res?.isDescriptionTooLong) { + ui.notifications?.error?.( + `Failed to sync ${res.entityName || parent?.name}: Description exceeds the maximum length of 10,000 characters. Please shorten the description and try again.`, + { permanent: true } + ); } } catch (e) { console.warn('[RTS] updateJournalEntryPage failed', e); @@ -1387,25 +1294,15 @@ function installRealtimeSyncListeners() { settingsManager.isRealtimeSyncSuppressed?.() ) return; - const meta = Utils.getPageArchivistMeta(page); + const meta = getPageMeta(page); + const metaType = getPageMetaType(page); if (!meta?.id) return; - if (isRecapPage(page)) return; // Recaps are read-only for delete - if (isFactionPage(page) && archivistApi.deleteFaction) { + if (metaType === 'recap') return; // Recaps still managed elsewhere + if (metaType === 'faction' && archivistApi.deleteFaction) { await archivistApi.deleteFaction(apiKey, meta.id); - } - if (isLocationPage(page) && archivistApi.deleteLocation) { + } else if (metaType === 'location' && archivistApi.deleteLocation) { await archivistApi.deleteLocation(apiKey, meta.id); } - // Character sheets: delete Character in Archivist when custom Character sheet root is deleted - const parent = page?.parent; - const flags = parent?.getFlag?.(CONFIG.MODULE_ID, 'archivist') || {}; - const isCharacter = - flags?.sheetType === 'pc' || - flags?.sheetType === 'npc' || - flags?.sheetType === 'character'; - if (isCharacter && flags.archivistId && archivistApi.deleteCharacter) { - await archivistApi.deleteCharacter(apiKey, flags.archivistId); - } } catch (e) { console.warn('[RTS] preDeleteJournalEntryPage failed', e); } diff --git a/scripts/dialogs/sync-dialog.js b/scripts/dialogs/sync-dialog.js index e632c39..6f96169 100644 --- a/scripts/dialogs/sync-dialog.js +++ b/scripts/dialogs/sync-dialog.js @@ -320,7 +320,7 @@ export class SyncDialog extends foundry.applications.api.HandlebarsApplicationMi if (!type) continue; const arch = byId[type].get(archId) || null; if (!arch) { - diffs.push({ type, id: archId, name: j.name, journalId: j.id, deleted: true, selected: true, changes: {} }); + diffs.push({ type, id: archId, name: j.name, journalId: j.id, deleted: true, selected: false, changes: {} }); continue; } const changes = {}; @@ -374,7 +374,7 @@ export class SyncDialog extends foundry.applications.api.HandlebarsApplicationMi if (toAdd.length || toRemove.length) changes.links = { add: toAdd, remove: toRemove }; } catch (_) { /* ignore */ } if (Object.keys(changes).length > 0) { - diffs.push({ type, id: archId, name: archName || j.name, journalId: j.id, changes, selected: true }); + diffs.push({ type, id: archId, name: archName || j.name, journalId: j.id, changes, selected: false }); } } @@ -672,4 +672,3 @@ export class SyncDialog extends foundry.applications.api.HandlebarsApplicationMi export const ArchivistSyncDialog = SyncDialog; - diff --git a/scripts/dialogs/world-setup-dialog.js b/scripts/dialogs/world-setup-dialog.js index 4410d52..e0a123b 100644 --- a/scripts/dialogs/world-setup-dialog.js +++ b/scripts/dialogs/world-setup-dialog.js @@ -3201,13 +3201,13 @@ WorldSetupDialog.prototype._buildReconciliationModel = function (input) { if (constraint && !constraint(L, R)) continue; if (normName(getLeftName(L)) === normName(getRightName(R))) { hit = R; break; } } - if (hit) { usedRight.add(hit.id); leftOut.push({ ...L, match: hit.id, selected: true }); } - else { leftOut.push({ ...L, match: null, selected: true }); } + if (hit) { usedRight.add(hit.id); leftOut.push({ ...L, match: hit.id, selected: false }); } + else { leftOut.push({ ...L, match: null, selected: false }); } } // Right side mirror const rightOut = right.map(R => { const L = leftOut.find(x => x.match === R.id) || null; - return { ...R, match: L ? L.id : null, selected: true }; + return { ...R, match: L ? L.id : null, selected: false }; }); return { leftOut, rightOut }; }; @@ -3230,7 +3230,7 @@ WorldSetupDialog.prototype._buildReconciliationModel = function (input) { const { leftOut: aLocsOut, rightOut: fScenesOut } = matchByName(aLocs, fScenes, x => x.name, x => x.name); // Factions — Foundry side empty by default - const aFactionsOut = aFactions.map(f => ({ ...f, match: null, selected: true })); + const aFactionsOut = aFactions.map(f => ({ ...f, match: null, selected: false })); const fFactionsOut = []; const result = {