@@ -153,6 +153,138 @@ export class JournalSync {
153153 await this . cleanupActorJournalDuplicates ( ) ;
154154 }
155155
156+ /**
157+ * Re-fetch every Chronicle entity and apply it to Foundry. Unlike `_onPullAll`
158+ * (which only creates journals for chronicle-only entities), this method also
159+ * UPDATES existing journals — refreshing content, name, and ownership/permissions.
160+ * This is the correct fix for journals that synced before a permission change.
161+ *
162+ * Mirrors MapSync.resyncAll / _runMapSync in structure:
163+ * - Paginated fetch of all entities (same source _prepareContext/_buildEntityGroups uses).
164+ * - For each entity: if a journal already exists → call `_onEntityUpdated` (which
165+ * re-runs `_buildOwnership` so permissions refresh); if none exists → create it.
166+ * - Respects `_syncing` guard (set by callers), `_isExcluded`, `_isHandledByActorSync`,
167+ * `isCalendarNoteJournal`, and `_isHandledByNoteSync`.
168+ * - Sequential/awaited (not parallelized) so a large campaign doesn't hammer the API.
169+ * - Returns a summary {updated, created, skipped, errors} for test assertions; also
170+ * fires a verbose `ui.notifications.info` when the option is set.
171+ *
172+ * @param {{verbose?: boolean} } [opts]
173+ * @returns {Promise<{updated: number, created: number, skipped: number, errors: number}> }
174+ */
175+ async resyncAll ( { verbose = true } = { } ) {
176+ if ( ! game . user . isGM || ! this . _api ) {
177+ return { updated : 0 , created : 0 , skipped : 0 , errors : 0 } ;
178+ }
179+ if ( ! getSetting ( 'syncJournals' ) ) {
180+ return { updated : 0 , created : 0 , skipped : 0 , errors : 0 } ;
181+ }
182+
183+ if ( verbose ) ui . notifications . info ( 'Chronicle: fetching entities for resync…' ) ;
184+
185+ // Paginated fetch — mirrors the dashboard's _buildEntityGroups page loop.
186+ let allEntities = [ ] ;
187+ try {
188+ let page = 1 ;
189+ let hasMore = true ;
190+ while ( hasMore && page <= 5 ) {
191+ const result = await this . _api . get ( `/entities?per_page=100&page=${ page } ` ) ;
192+ const entities = Array . isArray ( result ) ? result
193+ : ( Array . isArray ( result ?. entities ) ? result . entities
194+ : ( Array . isArray ( result ?. data ) ? result . data : [ ] ) ) ;
195+ if ( entities . length > 0 ) {
196+ allEntities . push ( ...entities ) ;
197+ hasMore = entities . length === 100 ;
198+ page ++ ;
199+ } else {
200+ hasMore = false ;
201+ }
202+ }
203+ } catch ( err ) {
204+ const status = err ?. status || null ;
205+ console . warn ( `Chronicle JournalSync.resyncAll: GET /entities failed (${ status || 'network' } )` , err ) ;
206+ ui . notifications . error (
207+ `Chronicle: could not fetch entities from Chronicle${ status ? ` (HTTP ${ status } )` : '' } . Verify apiUrl, apiKey, and campaignId in Module Settings.`
208+ ) ;
209+ return { updated : 0 , created : 0 , skipped : 0 , errors : 1 } ;
210+ }
211+
212+ console . debug ( `Chronicle: resyncAll fetched ${ allEntities . length } entity(ies).` ) ;
213+
214+ // Build a fast lookup of already-linked journals by chronicle entity id.
215+ const journalByEntityId = new Map ( ) ;
216+ for ( const j of game . journal . contents ) {
217+ const eid = j . getFlag ( FLAG_SCOPE , 'entityId' ) ;
218+ if ( eid ) journalByEntityId . set ( eid , j ) ;
219+ }
220+
221+ let updated = 0 ;
222+ let created = 0 ;
223+ let skipped = 0 ;
224+ let errors = 0 ;
225+
226+ for ( const entity of allEntities ) {
227+ if ( ! entity ?. id ) { skipped ++ ; continue ; }
228+
229+ // Skip: excluded by dashboard settings.
230+ if ( this . _isExcluded ( entity ) ) { skipped ++ ; continue ; }
231+
232+ // Skip: character entities are handled by ActorSync.
233+ if ( this . _isHandledByActorSync ( entity ) ) { skipped ++ ; continue ; }
234+
235+ const journal = journalByEntityId . get ( entity . id ) ;
236+
237+ try {
238+ if ( journal ) {
239+ // Journal exists → fetch full entity data and update (refreshes
240+ // content + re-runs _buildOwnership so permissions are current).
241+ let fullEntity = entity ;
242+ try {
243+ fullEntity = ( await this . _api . get ( `/entities/${ entity . id } ` ) ) || entity ;
244+ } catch ( fetchErr ) {
245+ console . warn ( `Chronicle: resyncAll — failed to fetch full entity ${ entity . id } , updating with summary` , fetchErr ) ;
246+ }
247+ // _onEntityUpdated skips character entities again via _isHandledByActorSync,
248+ // but the check above already guards against that; calling it handles the
249+ // update path (name, content pages, ownership) cleanly.
250+ await this . _onEntityUpdated ( fullEntity ) ;
251+ updated ++ ;
252+ } else {
253+ // No journal yet → fetch full entity and create.
254+ let fullEntity = entity ;
255+ try {
256+ fullEntity = ( await this . _api . get ( `/entities/${ entity . id } ` ) ) || entity ;
257+ } catch ( fetchErr ) {
258+ console . warn ( `Chronicle: resyncAll — failed to fetch full entity ${ entity . id } , creating with summary` , fetchErr ) ;
259+ }
260+ // Double-check actor routing on the full payload.
261+ if ( this . _isHandledByActorSync ( fullEntity ) ) { skipped ++ ; continue ; }
262+ await this . _createJournalFromEntity ( fullEntity ) ;
263+ created ++ ;
264+ }
265+ } catch ( err ) {
266+ errors ++ ;
267+ console . warn ( `Chronicle: resyncAll failed for entity "${ entity . name || entity . id } ":` , err ) ;
268+ }
269+ }
270+
271+ console . debug ( `Chronicle: resyncAll complete — updated=${ updated } created=${ created } skipped=${ skipped } errors=${ errors } ` ) ;
272+
273+ if ( verbose ) {
274+ if ( errors > 0 ) {
275+ ui . notifications . warn (
276+ `Chronicle: resynced ${ updated + created } of ${ allEntities . length - skipped } entity(ies); ${ errors } failed. See the sync dashboard for details.`
277+ ) ;
278+ } else {
279+ ui . notifications . info (
280+ `Chronicle: resynced ${ updated + created } entity(ies) (${ updated } updated, ${ created } created).`
281+ ) ;
282+ }
283+ }
284+
285+ return { updated, created, skipped, errors } ;
286+ }
287+
156288 /**
157289 * Clean up hooks on destroy.
158290 */
0 commit comments