@@ -23,6 +23,7 @@ import {
2323 CAP_STATUS ,
2424} from './capability-inspector.mjs' ;
2525import { buildDiagnosticBundle } from './sync-diagnostic-bundle.mjs' ;
26+ import { buildOverviewModel } from './_overview-model.mjs' ;
2627import { log , getLogBuffer } from './logger.mjs' ;
2728const { ApplicationV2, HandlebarsApplicationMixin } = foundry . applications . api ;
2829
@@ -122,8 +123,17 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
122123 /** @type {string } Current search filter text. */
123124 this . _searchFilter = '' ;
124125
125- /** @type {string } Currently active tab (persisted per-client). */
126- this . _activeTab = getSetting ( 'dashboardActiveTab' ) || 'entities' ;
126+ /**
127+ * @type {string|null } Actor id the GM chose to inspect in the Status tab's
128+ * Capability Inspector. Null = auto-pick (first linked, else first of type).
129+ * Per-session instance state (like _searchFilter): survives re-renders,
130+ * resets when the dashboard window is closed.
131+ */
132+ this . _capabilityActorId = null ;
133+
134+ /** @type {string } Currently active tab (persisted per-client). Defaults to
135+ * the Overview cockpit so the GM lands on a calm summary, not a dense list. */
136+ this . _activeTab = getSetting ( 'dashboardActiveTab' ) || 'overview' ;
127137
128138 /** @type {Set<string> } Currently selected entity IDs for bulk operations. */
129139 this . _selectedEntities = new Set ( ) ;
@@ -255,6 +265,23 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
255265 // Build config tab data.
256266 const configData = this . _buildConfigData ( entityGroups ) ;
257267
268+ // Build the Overview cockpit from the pieces already computed above — a
269+ // calm landing view that routes problems to the tab that fixes each one.
270+ const overview = buildOverviewModel ( {
271+ connectionState : statusData . connectionState ,
272+ connectionLabel : statusData . connectionLabel ,
273+ syncedEntities : statusData . syncedEntities ,
274+ linkedScenes : statusData . linkedScenes ,
275+ characters : characterData . characters ,
276+ issuesCount : issuesData . count ,
277+ unmatchedMembers : membersData . unmatchedCount ,
278+ calendarAvailable : calendarData . available ,
279+ calendarInSync : calendarData . inSync ,
280+ errorCount : statusData . errorLog ?. length ?? 0 ,
281+ matchedSystem : statusData . matchedSystem ,
282+ lastSyncTime : statusData . lastSyncTime ,
283+ } ) ;
284+
258285 this . _loading = false ;
259286
260287 return {
@@ -265,6 +292,9 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
265292 loadErrors,
266293 hasLoadErrors : loadErrors . length > 0 ,
267294
295+ // Overview (landing cockpit) tab.
296+ overview,
297+
268298 // Issues (resolver) tab.
269299 issues : issuesData ,
270300
@@ -1026,14 +1056,36 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
10261056 ) ;
10271057 const actorType = actorSync ?. _actorType || actorSync ?. _adapter ?. actorType || 'character' ;
10281058
1029- // Sample a representative actor: prefer a linked one, else the first of the type.
10301059 const ofType = ( game . actors ?. contents || [ ] ) . filter ( ( a ) => a ?. type === actorType ) ;
1031- const sample = ofType . find ( ( a ) => a . getFlag ?. ( FLAG_SCOPE , 'entityId' ) ) || ofType [ 0 ] || null ;
1060+
1061+ // Which actor to inspect. If the GM picked one (and it still exists), use it;
1062+ // otherwise auto-pick a representative actor: prefer a Chronicle-linked one,
1063+ // else the first of the type. `autoPicked` drives the UI hint so the GM knows
1064+ // the panel chose for them and can switch with the dropdown.
1065+ const chosen = this . _capabilityActorId
1066+ ? ofType . find ( ( a ) => a . id === this . _capabilityActorId )
1067+ : null ;
1068+ const autoPicked = ! chosen ;
1069+ const sample = chosen
1070+ || ofType . find ( ( a ) => a . getFlag ?. ( FLAG_SCOPE , 'entityId' ) )
1071+ || ofType [ 0 ]
1072+ || null ;
10321073 if ( ! sample ) {
10331074 this . _capabilityReport = null ;
1034- return { available : false , actorType, error : `no "${ actorType } " actor to sample` } ;
1075+ return { available : false , actorType, actors : [ ] , error : `no "${ actorType } " actor to sample` } ;
10351076 }
10361077
1078+ // The full pick-list for the dropdown (linked actors flagged so the GM can
1079+ // tell which heroes already sync to Chronicle).
1080+ const actors = ofType
1081+ . map ( ( a ) => ( {
1082+ id : a . id ,
1083+ name : a . name || '(unnamed)' ,
1084+ linked : ! ! a . getFlag ?. ( FLAG_SCOPE , 'entityId' ) ,
1085+ selected : a . id === sample . id ,
1086+ } ) )
1087+ . sort ( ( x , y ) => x . name . localeCompare ( y . name ) ) ;
1088+
10371089 // Fetch the system's declared character fields (same source the adapter uses).
10381090 let fieldDefs = { fields : [ ] } ;
10391091 try {
@@ -1050,6 +1102,8 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
10501102 return {
10511103 available : ! ! report . ok ,
10521104 actorType,
1105+ actors,
1106+ autoPicked,
10531107 summary : report . summary ,
10541108 source : report . source ,
10551109 gaps : this . _capabilityGaps ( report ) ,
@@ -1263,6 +1317,27 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
12631317 } ) ;
12641318 } ) ;
12651319
1320+ // --- Overview cockpit: jump-links to other tabs ---
1321+ // Stat tiles and "needs attention" rows carry data-jump-tab and switch the
1322+ // active tab (no data-action, so they bypass the ApplicationV2 click router).
1323+ el . querySelectorAll ( '[data-jump-tab]' ) . forEach ( ( btn ) => {
1324+ btn . addEventListener ( 'click' , ( e ) => {
1325+ e . preventDefault ( ) ;
1326+ this . _switchTab ( e . currentTarget . dataset . jumpTab ) ;
1327+ } ) ;
1328+ } ) ;
1329+
1330+ // --- Status tab: Capability Inspector actor picker ---
1331+ // ApplicationV2 `actions` only delegate `click`, so the `<select>` change is
1332+ // wired here directly. Picking an actor re-renders the panel against it.
1333+ const capActorSelect = el . querySelector ( '.capability-actor-select' ) ;
1334+ if ( capActorSelect ) {
1335+ capActorSelect . addEventListener ( 'change' , ( e ) => {
1336+ this . _capabilityActorId = e . currentTarget . value || null ;
1337+ this . render ( { force : true } ) ;
1338+ } ) ;
1339+ }
1340+
12661341 // Map tab handlers are wired via the `open-map-journal` action above;
12671342 // no additional select listeners are needed in Path B.
12681343 }
@@ -1276,32 +1351,57 @@ export class SyncDashboard extends HandlebarsApplicationMixin(ApplicationV2) {
12761351 */
12771352 _initTabs ( el ) {
12781353 const tabs = el . querySelectorAll ( '.dashboard-tabs .item' ) ;
1279- const panels = el . querySelectorAll ( '.dashboard-content .tab' ) ;
1354+
1355+ // If the persisted tab no longer exists in the DOM (renamed/removed), fall
1356+ // back to the Overview cockpit (or the first available tab) so the window
1357+ // never opens to a blank panel.
1358+ const known = new Set ( [ ...tabs ] . map ( ( t ) => t . dataset . tab ) ) ;
1359+ if ( ! known . has ( this . _activeTab ) ) {
1360+ this . _activeTab = known . has ( 'overview' ) ? 'overview' : ( tabs [ 0 ] ?. dataset . tab || this . _activeTab ) ;
1361+ }
12801362
12811363 // Apply the stored active tab.
1282- tabs . forEach ( ( tab ) => {
1283- const tabName = tab . dataset . tab ;
1284- tab . classList . toggle ( 'active' , tabName === this . _activeTab ) ;
1285- } ) ;
1286- panels . forEach ( ( panel ) => {
1287- const tabName = panel . dataset . tab ;
1288- panel . classList . toggle ( 'active' , tabName === this . _activeTab ) ;
1289- } ) ;
1364+ this . _applyActiveTab ( el ) ;
12901365
1291- // Listen for tab clicks.
1366+ // Listen for rail clicks.
12921367 tabs . forEach ( ( tab ) => {
12931368 tab . addEventListener ( 'click' , ( e ) => {
12941369 e . preventDefault ( ) ;
1295- const tabName = tab . dataset . tab ;
1296- this . _activeTab = tabName ;
1297- setSetting ( 'dashboardActiveTab' , tabName ) ;
1298-
1299- tabs . forEach ( t => t . classList . toggle ( 'active' , t . dataset . tab === tabName ) ) ;
1300- panels . forEach ( p => p . classList . toggle ( 'active' , p . dataset . tab === tabName ) ) ;
1370+ this . _switchTab ( tab . dataset . tab ) ;
13011371 } ) ;
13021372 } ) ;
13031373 }
13041374
1375+ /**
1376+ * Toggle the `.active` class on the nav item + content panel matching
1377+ * `this._activeTab`. Shared by initial render and programmatic switches.
1378+ * @param {HTMLElement } el
1379+ * @private
1380+ */
1381+ _applyActiveTab ( el ) {
1382+ el . querySelectorAll ( '.dashboard-tabs .item' ) . forEach ( ( t ) =>
1383+ t . classList . toggle ( 'active' , t . dataset . tab === this . _activeTab ) ) ;
1384+ el . querySelectorAll ( '.dashboard-content .tab' ) . forEach ( ( p ) =>
1385+ p . classList . toggle ( 'active' , p . dataset . tab === this . _activeTab ) ) ;
1386+ }
1387+
1388+ /**
1389+ * Switch to a tab by name and persist it. Used by rail clicks and by the
1390+ * Overview cockpit's jump-links (`data-jump-tab`). Scrolls the content pane
1391+ * back to the top so the jumped-to tab starts at its heading.
1392+ * @param {string } tabName
1393+ * @private
1394+ */
1395+ _switchTab ( tabName ) {
1396+ if ( ! tabName ) return ;
1397+ this . _activeTab = tabName ;
1398+ setSetting ( 'dashboardActiveTab' , tabName ) ;
1399+ const el = this . element ;
1400+ if ( ! el ) return ;
1401+ this . _applyActiveTab ( el ) ;
1402+ el . querySelector ( '.dashboard-content' ) ?. scrollTo ?. ( { top : 0 } ) ;
1403+ }
1404+
13051405 // ---------------------------------------------------------------------------
13061406 // Action handlers (ApplicationV2 actions pattern)
13071407 // Called with `this` bound to the application instance by Foundry.
0 commit comments