Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion module.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
203 changes: 50 additions & 153 deletions scripts/archivist-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 2 additions & 3 deletions scripts/dialogs/sync-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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 });
}
}

Expand Down Expand Up @@ -672,4 +672,3 @@ export class SyncDialog extends foundry.applications.api.HandlebarsApplicationMi

export const ArchivistSyncDialog = SyncDialog;


8 changes: 4 additions & 4 deletions scripts/dialogs/world-setup-dialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
};
Expand All @@ -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 = {
Expand Down