@@ -30,6 +30,27 @@ let dashboard = null;
3030/** @type {HTMLElement|null } Sidebar status indicator, re-attached on re-render. */
3131let _statusIndicatorEl = null ;
3232
33+ /**
34+ * Open the Sync Dashboard, recreating the singleton if it isn't currently
35+ * rendered. An ApplicationV2 instance can be left in a non-re-renderable state
36+ * after a close (notably when the close interrupts an in-flight render), which
37+ * made the dashboard intermittently fail to reopen. Mirrors the SyncCalendar
38+ * singleton helper: bring an already-open window to the front; otherwise build,
39+ * (re)bind, and render a fresh instance. `bind()` only sets a reference, so
40+ * recreating leaks nothing.
41+ * @returns {SyncDashboard|null }
42+ */
43+ function openDashboard ( ) {
44+ if ( dashboard && dashboard . rendered ) {
45+ dashboard . bringToFront ?. ( ) ;
46+ return dashboard ;
47+ }
48+ dashboard = new SyncDashboard ( ) ;
49+ if ( syncManager ) dashboard . bind ( syncManager ) ;
50+ dashboard . render ( { force : true } ) ;
51+ return dashboard ;
52+ }
53+
3354/**
3455 * Module initialization — register settings.
3556 * This runs before the game is fully ready.
@@ -207,9 +228,9 @@ async function _runtimeValidateDescriptor() {
207228Hooks . on ( 'getSceneControlButtons' , ( controls ) => {
208229 if ( ! game . user . isGM ) return ;
209230
210- const openDashboard = ( ) => {
211- if ( dashboard ) dashboard . render ( { force : true } ) ;
212- } ;
231+ // openDashboard() is the module-level helper above: it recreates the
232+ // dashboard when a prior close left the instance non-re-renderable, fixing
233+ // the intermittent "can't reopen the dashboard" bug.
213234 // FM-CAL-DASHBOARD-LINK: surface SyncCalendarApplication as a second
214235 // tool in the Chronicle Sync scene-control group. The singleton helper
215236 // is GM-only by contract; the outer GM guard above is also enforced.
@@ -342,15 +363,15 @@ function _addStatusIndicator() {
342363 if ( syncManager . api . state === 'disconnected' ) {
343364 syncManager . api . connect ( ) ;
344365 ui . notifications . info ( 'Chronicle: Reconnecting...' ) ;
345- } else if ( dashboard ) {
346- dashboard . render ( { force : true } ) ;
366+ } else {
367+ openDashboard ( ) ;
347368 }
348369 } ) ;
349370
350371 // Right-click always opens the dashboard (even when disconnected).
351372 indicator . addEventListener ( 'contextmenu' , ( e ) => {
352373 e . preventDefault ( ) ;
353- if ( dashboard ) dashboard . render ( { force : true } ) ;
374+ openDashboard ( ) ;
354375 } ) ;
355376
356377 // Flash the dot briefly when a WS message arrives (activity indicator).
@@ -431,9 +452,11 @@ Hooks.once('ready', () => {
431452 if ( module ) {
432453 module . api = {
433454 syncManager,
434- dashboard,
455+ // Getter (not a snapshot): openDashboard() may recreate the instance, so
456+ // expose the live reference rather than the one captured at ready time.
457+ get dashboard ( ) { return dashboard ; } ,
435458 getAPI : ( ) => syncManager ?. api ,
436- openDashboard : ( ) => dashboard ?. render ( { force : true } ) ,
459+ openDashboard : ( ) => openDashboard ( ) ,
437460 // Version-independent diagnostics escape hatch. Lets an operator always
438461 // produce the System Debug snapshot from the console even if the toolbar
439462 // button or sidebar indicator regress on a Foundry upgrade. DOM-
0 commit comments