Skip to content

Commit 0d931c2

Browse files
committed
feat: add Sync Dashboard UI to Foundry VTT module
Add a tabbed dashboard window (Entities, Maps, Calendar, Status) accessible via a sidebar button. Provides full visibility and control over Chronicle sync: per-entity/type exclusions, pull/push operations, map linking, calendar date sync, connection status, and activity logging. New files: - sync-dashboard.mjs: SyncDashboard Application class with all tab logic - sync-dashboard.hbs: Handlebars template with 4-tab layout Modified files: - module.mjs: sidebar button + dashboard instantiation - settings.mjs: syncExclusions setting + helper functions - sync-manager.mjs: activity log (logActivity/getActivityLog/clearActivityLog) - journal-sync.mjs: exclusion checks in auto-sync handlers - chronicle-sync.css: full dashboard styling (tabs, entity rows, maps, calendar, status) - en.json: all dashboard UI strings https://claude.ai/code/session_01XMwxFR8BCi5XvgaSVMSBZB
1 parent 828d853 commit 0d931c2

8 files changed

Lines changed: 2034 additions & 1 deletion

File tree

foundry-module/lang/en.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,72 @@
4444
"SyncConflict": "Sync conflict detected for: {name}",
4545
"ConnectionLost": "Lost connection to Chronicle. Retrying...",
4646
"ConnectionRestored": "Connection to Chronicle restored"
47+
},
48+
"Dashboard": {
49+
"Title": "Chronicle Sync",
50+
"OpenSettings": "Open Settings",
51+
"NotConfigured": "Chronicle Sync is not configured.",
52+
"NotConfiguredHint": "Go to Module Settings to enter your Chronicle URL, API key, and Campaign ID.",
53+
"Tabs": {
54+
"Entities": "Entities",
55+
"Maps": "Maps",
56+
"Calendar": "Calendar",
57+
"Status": "Status"
58+
},
59+
"Entities": {
60+
"PullAll": "Pull All",
61+
"PushAll": "Push All",
62+
"Refresh": "Refresh",
63+
"SearchPlaceholder": "Search entities...",
64+
"Synced": "Synced",
65+
"Modified": "Modified",
66+
"ChronicleOnly": "Chronicle",
67+
"FoundryOnly": "Foundry",
68+
"Pull": "Pull",
69+
"Push": "Push",
70+
"SyncEnabled": "Sync enabled",
71+
"SyncDisabled": "Sync disabled",
72+
"EnableSync": "Enable sync for this type",
73+
"DisableSync": "Disable sync for this type",
74+
"FoundryOnlyJournals": "Foundry-Only Journals",
75+
"Unlinked": "unlinked",
76+
"PrivateClickPublic": "Private — click to make public",
77+
"PublicClickPrivate": "Public — click to make private",
78+
"EmptyState": "No entities found. Create entities in Chronicle or journals in Foundry to get started."
79+
},
80+
"Maps": {
81+
"ChronicleMaps": "Chronicle Maps",
82+
"UnlinkedScenes": "Unlinked Foundry Scenes",
83+
"LinkToScene": "Link to scene...",
84+
"LinkToMap": "Link to map...",
85+
"Unlink": "Unlink",
86+
"NoMaps": "No maps found in this Chronicle campaign."
87+
},
88+
"Calendar": {
89+
"CalendarModule": "Calendar Module",
90+
"Chronicle": "Chronicle",
91+
"Foundry": "Foundry",
92+
"InSync": "In Sync",
93+
"OutOfSync": "Out of Sync",
94+
"PullDate": "Pull Date from Chronicle",
95+
"PushDate": "Push Date to Chronicle",
96+
"NoCalendar": "No calendar configured for this campaign in Chronicle.",
97+
"Disabled": "Calendar sync is disabled. Enable it in Module Settings.",
98+
"NoModule": "No calendar module detected.",
99+
"NoModuleHint": "Install Calendaria or Simple Calendar to enable calendar sync.",
100+
"UnableToRead": "Unable to read"
101+
},
102+
"StatusTab": {
103+
"Connection": "Connection",
104+
"EntitiesSynced": "Entities Synced",
105+
"MapsLinked": "Maps Linked",
106+
"LastSync": "Last sync",
107+
"RecentActivity": "Recent Activity",
108+
"NoActivity": "No recent activity.",
109+
"ClearLog": "Clear Log",
110+
"Settings": "Settings",
111+
"Reconnect": "Reconnect"
112+
}
47113
}
48114
}
49115
}

foundry-module/scripts/journal-sync.mjs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* - Foundry → Chronicle: JournalEntry changes detected via Hooks, push to Chronicle API.
1010
*/
1111

12-
import { getSetting } from './settings.mjs';
12+
import { getSetting, getSyncExclusions } from './settings.mjs';
1313

1414
// Flag namespace for Chronicle data stored on Foundry documents.
1515
const FLAG_SCOPE = 'chronicle-sync';
@@ -111,6 +111,9 @@ export class JournalSync {
111111
async _onEntityCreated(entity) {
112112
if (!entity?.id) return;
113113

114+
// Skip if entity or its type is excluded from sync.
115+
if (this._isExcluded(entity)) return;
116+
114117
// Check if we already have a journal for this entity.
115118
const existing = game.journal.find(
116119
(j) => j.getFlag(FLAG_SCOPE, 'entityId') === entity.id
@@ -136,6 +139,9 @@ export class JournalSync {
136139
async _onEntityUpdated(entity) {
137140
if (!entity?.id) return;
138141

142+
// Skip if entity or its type is excluded from sync.
143+
if (this._isExcluded(entity)) return;
144+
139145
const journal = game.journal.find(
140146
(j) => j.getFlag(FLAG_SCOPE, 'entityId') === entity.id
141147
);
@@ -409,4 +415,17 @@ export class JournalSync {
409415
console.warn('Chronicle: Failed to delete entity on Chronicle', err);
410416
}
411417
}
418+
419+
/**
420+
* Check if an entity is excluded from auto-sync via dashboard settings.
421+
* @param {object} entity - Chronicle entity with id and optionally entity_type_id.
422+
* @returns {boolean}
423+
* @private
424+
*/
425+
_isExcluded(entity) {
426+
const exclusions = getSyncExclusions();
427+
if (exclusions.excludedEntities.includes(entity.id)) return true;
428+
if (entity.entity_type_id && exclusions.excludedTypes.includes(entity.entity_type_id)) return true;
429+
return false;
430+
}
412431
}

foundry-module/scripts/module.mjs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import { JournalSync } from './journal-sync.mjs';
1212
import { MapSync } from './map-sync.mjs';
1313
import { ShopWidget } from './shop-widget.mjs';
1414
import { CalendarSync } from './calendar-sync.mjs';
15+
import { SyncDashboard } from './sync-dashboard.mjs';
1516

1617
/** @type {SyncManager|null} */
1718
let syncManager = null;
1819

20+
/** @type {SyncDashboard|null} */
21+
let dashboard = null;
22+
1923
/**
2024
* Module initialization — register settings.
2125
* This runs before the game is fully ready.
@@ -44,10 +48,39 @@ Hooks.once('ready', async () => {
4448
// Start the sync manager (connects WebSocket, performs initial sync).
4549
await syncManager.start();
4650

51+
// Create the sync dashboard (singleton, rendered on demand).
52+
dashboard = new SyncDashboard();
53+
dashboard.bind(syncManager);
54+
4755
// Add sync status indicator to the UI.
4856
_addStatusIndicator();
4957
});
5058

59+
/**
60+
* Add a Chronicle Sync button to Foundry's scene controls toolbar.
61+
* Visible only to GMs. Opens the Sync Dashboard on click.
62+
*/
63+
Hooks.on('getSceneControlButtons', (controls) => {
64+
if (!game.user.isGM) return;
65+
66+
controls.push({
67+
name: 'chronicle-sync',
68+
title: 'Chronicle Sync',
69+
icon: 'fa-solid fa-rotate',
70+
layer: 'controls',
71+
visible: true,
72+
tools: [{
73+
name: 'dashboard',
74+
title: 'Open Chronicle Sync Dashboard',
75+
icon: 'fa-solid fa-rotate',
76+
button: true,
77+
onClick: () => {
78+
if (dashboard) dashboard.render(true);
79+
},
80+
}],
81+
});
82+
});
83+
5184
/**
5285
* Clean up when the module is deactivated.
5386
*/
@@ -152,7 +185,9 @@ Hooks.once('ready', () => {
152185
if (module) {
153186
module.api = {
154187
syncManager,
188+
dashboard,
155189
getAPI: () => syncManager?.api,
190+
openDashboard: () => dashboard?.render(true),
156191
};
157192
}
158193
});

foundry-module/scripts/settings.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ export function registerSettings() {
9090
type: String,
9191
default: '',
9292
});
93+
94+
// Internal: per-type and per-entity sync exclusions (not shown in settings UI).
95+
// Stored as JSON: { excludedTypes: [typeId, ...], excludedEntities: ["entityId", ...] }
96+
game.settings.register(MODULE_ID, 'syncExclusions', {
97+
scope: 'world',
98+
config: false,
99+
type: String,
100+
default: '{"excludedTypes":[],"excludedEntities":[]}',
101+
});
93102
}
94103

95104
/**
@@ -110,6 +119,26 @@ export async function setSetting(key, value) {
110119
await game.settings.set(MODULE_ID, key, value);
111120
}
112121

122+
/**
123+
* Get sync exclusions (excluded types and entities).
124+
* @returns {{ excludedTypes: number[], excludedEntities: string[] }}
125+
*/
126+
export function getSyncExclusions() {
127+
try {
128+
return JSON.parse(getSetting('syncExclusions'));
129+
} catch {
130+
return { excludedTypes: [], excludedEntities: [] };
131+
}
132+
}
133+
134+
/**
135+
* Save sync exclusions.
136+
* @param {{ excludedTypes: number[], excludedEntities: string[] }} exclusions
137+
*/
138+
export async function setSyncExclusions(exclusions) {
139+
await setSetting('syncExclusions', JSON.stringify(exclusions));
140+
}
141+
113142
/**
114143
* Check if the module is properly configured (URL + key + campaign).
115144
* @returns {boolean}

0 commit comments

Comments
 (0)