@@ -98,7 +98,7 @@ <h3 class="text-lg font-semibold mb-4 text-blue-800" data-i18n="apiSetup.instruc
9898 < li class ="flex gap-3 "> < span class ="font-bold text-blue-600 "> 2.</ span > < span data-i18n ="apiSetup.step2 "> Click "Add client".</ span > </ li >
9999 < li class ="flex gap-3 "> < span class ="font-bold text-blue-600 "> 3.</ span > < span data-i18n ="apiSetup.step3 "> Give it any name (e.g. "Flow Doctor").</ span > </ li >
100100 < li class ="flex gap-3 "> < span class ="font-bold text-blue-600 "> 4.</ span > < span data-i18n ="apiSetup.step4 "> Paste the Redirect URL shown below.</ span > </ li >
101- < li class ="flex gap-3 "> < span class ="font-bold text-blue-600 "> 5.</ span > < span data-i18n ="apiSetup.step5 "> Select scopes: </ span > < code class ="bg-gray-100 px-1 rounded "> homey.flow.readonly</ code > · < code class ="bg-gray-100 px-1 rounded "> homey.app.readonly</ code > · < code class ="bg-gray-100 px-1 rounded "> homey.device.readonly</ code > · < code class ="bg-gray-100 px-1 rounded "> homey.zone.readonly</ code > </ li >
101+ < li class ="flex gap-3 "> < span class ="font-bold text-blue-600 "> 5.</ span > < span data-i18n ="apiSetup.step5 "> Select scopes: </ span > < code class ="bg-gray-100 px-1 rounded "> homey.flow.readonly</ code > · < code class ="bg-gray-100 px-1 rounded "> homey.app.readonly</ code > · < code class ="bg-gray-100 px-1 rounded "> homey.app.control </ code > · < code class =" bg-gray-100 px-1 rounded " > homey. device.readonly</ code > · < code class ="bg-gray-100 px-1 rounded "> homey.zone.readonly</ code > </ li >
102102 < li class ="flex gap-3 "> < span class ="font-bold text-blue-600 "> 6.</ span > < span data-i18n ="apiSetup.step6 "> Click "Create" and copy the Client ID and Client Secret below.</ span > </ li >
103103 </ ol >
104104 </ div >
@@ -729,6 +729,8 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
729729} ;
730730
731731const FLAGS = { en : '🇬🇧' , no : '🇳🇴' , de : '🇩🇪' , nl : '🇳🇱' } ;
732+ const SMART_COMPONENTS_APP_ID = 'no.tiwas.booleantoolbox' ;
733+ const DEVICE_REGISTRY_SETTING_KEY = 'device_registry' ;
732734
733735class FlowDoctor {
734736 constructor ( ) {
@@ -737,6 +739,8 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
737739 this . cloudApi = null ;
738740 this . api = null ;
739741 this . results = [ ] ;
742+ this . deviceRegistry = null ;
743+ this . deviceRegistryStatus = 'not_loaded' ;
740744 }
741745
742746 t ( key ) { return ( I18N [ this . lang ] && I18N [ this . lang ] [ key ] ) || I18N . en [ key ] || key ; }
@@ -1172,6 +1176,78 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
11721176 return true ;
11731177 }
11741178
1179+ normalizeDeviceRegistry ( raw ) {
1180+ if ( ! raw ) return null ;
1181+ let registry = raw ;
1182+ if ( registry && typeof registry === 'object' && ! registry . entries && Object . prototype . hasOwnProperty . call ( registry , 'value' ) ) {
1183+ registry = registry . value ;
1184+ }
1185+ if ( typeof registry === 'string' ) {
1186+ try {
1187+ registry = JSON . parse ( registry ) ;
1188+ } catch ( _ ) {
1189+ return null ;
1190+ }
1191+ }
1192+ if ( ! registry || typeof registry !== 'object' || ! registry . entries || typeof registry . entries !== 'object' ) {
1193+ return null ;
1194+ }
1195+ return registry ;
1196+ }
1197+
1198+ async loadDeviceRegistry ( installedApps ) {
1199+ this . deviceRegistry = null ;
1200+ this . deviceRegistryStatus = 'not_available' ;
1201+
1202+ if ( ! installedApps || ! installedApps [ SMART_COMPONENTS_APP_ID ] ) {
1203+ return null ;
1204+ }
1205+
1206+ const appsApi = this . api && this . api . apps ;
1207+ if ( ! appsApi ) return null ;
1208+
1209+ try {
1210+ let raw = null ;
1211+ if ( typeof appsApi . getAppSettings === 'function' ) {
1212+ const settings = await appsApi . getAppSettings ( { id : SMART_COMPONENTS_APP_ID } ) ;
1213+ raw = settings && settings [ DEVICE_REGISTRY_SETTING_KEY ] ;
1214+ } else if ( typeof appsApi . getAppSetting === 'function' ) {
1215+ raw = await appsApi . getAppSetting ( {
1216+ id : SMART_COMPONENTS_APP_ID ,
1217+ name : DEVICE_REGISTRY_SETTING_KEY ,
1218+ } ) ;
1219+ }
1220+
1221+ const registry = this . normalizeDeviceRegistry ( raw ) ;
1222+ if ( registry ) {
1223+ this . deviceRegistry = registry ;
1224+ this . deviceRegistryStatus = 'loaded' ;
1225+ return registry ;
1226+ }
1227+ this . deviceRegistryStatus = 'empty' ;
1228+ return null ;
1229+ } catch ( e ) {
1230+ this . deviceRegistryStatus = 'error' ;
1231+ return null ;
1232+ }
1233+ }
1234+
1235+ deviceRegistryName ( deviceId ) {
1236+ const id = String ( deviceId || '' ) . trim ( ) ;
1237+ if ( ! id || ! this . deviceRegistry || ! this . deviceRegistry . entries ) return null ;
1238+ const entry = this . deviceRegistry . entries [ id ] ;
1239+ const name = entry && typeof entry . name === 'string' ? entry . name . trim ( ) : '' ;
1240+ return name && name !== id ? name : null ;
1241+ }
1242+
1243+ formatDeviceReference ( deviceId , fallbackName = null ) {
1244+ const id = String ( deviceId || '' ) . trim ( ) ;
1245+ if ( ! id ) return '' ;
1246+ const fallback = typeof fallbackName === 'string' ? fallbackName . trim ( ) : '' ;
1247+ const name = fallback && fallback !== id ? fallback : this . deviceRegistryName ( id ) || 'N/A' ;
1248+ return `${ name } (ID: ${ id } )` ;
1249+ }
1250+
11751251 buildDebugInfo ( ) {
11761252 // Privacy: no device names, flow names, homey-id, tokens, or any field that
11771253 // could correlate to a specific user. App names are kept (public marketing names).
@@ -1213,6 +1289,12 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
12131289 apps : { total : apps . length , crashed : nCrashed , update : nUpdate , unused : nUnused , ok : nAppsOk } ,
12141290 } ,
12151291 issuesByKey : issueKeyCount ,
1292+ deviceRegistry : {
1293+ status : this . deviceRegistryStatus ,
1294+ updatedAt : this . deviceRegistry ?. updatedAt || null ,
1295+ knownCount : this . deviceRegistry ?. knownCount || null ,
1296+ missingCount : this . deviceRegistry ?. missingCount || null ,
1297+ } ,
12161298 apps,
12171299 } ;
12181300 }
@@ -1292,6 +1374,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
12921374 const installedApps = { } ;
12931375 Object . values ( apps ) . forEach ( a => { installedApps [ a . id ] = a ; } ) ;
12941376 const installedAppIds = new Set ( Object . keys ( installedApps ) ) ;
1377+ await this . loadDeviceRegistry ( installedApps ) ;
12951378 const deviceIds = new Set ( Object . values ( devices ) . map ( d => d . id ) ) ;
12961379 const deviceToApp = { } ;
12971380 Object . values ( devices ) . forEach ( d => {
@@ -1337,7 +1420,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
13371420 const title = this . localizeStoreValue ( titleRaw ) ;
13381421 if ( id . startsWith ( 'homey:device:' ) ) {
13391422 const devId = id . substring ( 'homey:device:' . length ) . split ( ':' ) [ 0 ] ;
1340- const devName = devices [ devId ] ?. name || devId ;
1423+ const devName = this . formatDeviceReference ( devId , devices [ devId ] ?. name ) ;
13411424 return title ? `${ devName } → ${ title } ` : `${ devName } → ${ id . split ( ':' ) . pop ( ) } ` ;
13421425 }
13431426 if ( id . startsWith ( 'homey:app:' ) ) {
@@ -1371,7 +1454,33 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
13711454 }
13721455
13731456 let def = null ;
1374- if ( uri . startsWith ( 'homey:app:' ) || uri . startsWith ( 'homey:manager:' ) || uri . startsWith ( 'homey:device:' ) ) {
1457+ const deviceCardId = uri . startsWith ( 'homey:device:' )
1458+ ? uri . substring ( 'homey:device:' . length ) . split ( ':' ) [ 0 ]
1459+ : null ;
1460+
1461+ if ( deviceCardId ) {
1462+ if ( deviceIds . size > 0 && ! deviceIds . has ( deviceCardId ) ) {
1463+ issues . push ( { severity : 'error' , key : 'issue.missingDevice' , detail : describeCard ( card , null ) , appId } ) ;
1464+ return issues ;
1465+ }
1466+
1467+ const owningAppId = deviceToApp [ deviceCardId ] ;
1468+ if ( owningAppId ) {
1469+ appId = owningAppId ;
1470+ if ( ! installedAppIds . has ( owningAppId ) ) {
1471+ // Device exists but its owning app isn't installed — orphan device.
1472+ issues . push ( { severity : 'warn' , key : 'issue.deviceMissingApp' , detail : describeCard ( card , null ) + ' · app: ' + owningAppId , appId : owningAppId } ) ;
1473+ }
1474+ }
1475+
1476+ // Device-scoped cards are generated by the device/driver layer and are
1477+ // not always returned by the global flow-card lists. Treat a missing
1478+ // card definition as "not enough metadata", not as a removed card.
1479+ def = lookupCardDef ( card , kind ) ;
1480+ if ( def && def . deprecated ) {
1481+ issues . push ( { severity : 'warn' , key : 'issue.deprecatedCard' , detail : describeCard ( card , def ) , appId } ) ;
1482+ }
1483+ } else if ( uri . startsWith ( 'homey:app:' ) || uri . startsWith ( 'homey:manager:' ) ) {
13751484 def = lookupCardDef ( card , kind ) ;
13761485 if ( ! def ) {
13771486 issues . push ( { severity : 'error' , key : 'issue.unknownCard' , detail : describeCard ( card , null ) , appId } ) ;
@@ -1394,7 +1503,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
13941503 // Device reference check — only for actual device-typed args
13951504 if ( argDef . type === 'device' && val && typeof val === 'object' && val . id && typeof val . id === 'string' ) {
13961505 if ( deviceIds . size > 0 && ! deviceIds . has ( val . id ) && val . id . length >= 16 ) {
1397- issues . push ( { severity : 'error' , key : 'issue.missingDevice' , detail : describeCard ( card , def ) + ' · arg "' + argDef . name + '" → ' + ( val . name || val . id ) , appId } ) ;
1506+ issues . push ( { severity : 'error' , key : 'issue.missingDevice' , detail : describeCard ( card , def ) + ' · arg "' + argDef . name + '" → ' + this . formatDeviceReference ( val . id , val . name ) , appId } ) ;
13981507 }
13991508 }
14001509 }
@@ -1406,18 +1515,6 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
14061515 }
14071516 }
14081517
1409- // Device-card reference (homey:device:<id>)
1410- if ( uri . startsWith ( 'homey:device:' ) ) {
1411- const devId = uri . substring ( 'homey:device:' . length ) . split ( ':' ) [ 0 ] ;
1412- if ( deviceIds . size > 0 && devId && ! deviceIds . has ( devId ) ) {
1413- issues . push ( { severity : 'error' , key : 'issue.missingDevice' , detail : describeCard ( card , def ) , appId } ) ;
1414- } else if ( deviceToApp [ devId ] && ! installedAppIds . has ( deviceToApp [ devId ] ) ) {
1415- // Device exists but its owning app isn't installed — orphan device.
1416- const missingApp = deviceToApp [ devId ] ;
1417- issues . push ( { severity : 'warn' , key : 'issue.deviceMissingApp' , detail : describeCard ( card , def ) + ' · app: ' + missingApp , appId : missingApp } ) ;
1418- }
1419- }
1420-
14211518 return issues ;
14221519 } ;
14231520
@@ -1644,7 +1741,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
16441741 const typeLabel = r . type === 'advanced' ? this . t ( 'flowType.advanced' ) : this . t ( 'flowType.standard' ) ;
16451742 const issuesHtml = r . issues . map ( i => {
16461743 const cls = i . severity === 'error' ? 'badge-err' : i . severity === 'warn' ? 'badge-warn' : 'badge-info' ;
1647- return `<div class="text-sm py-1"><span class="badge ${ cls } ">${ i . severity . toUpperCase ( ) } </span> ${ this . t ( i . key ) } ${ i . detail ? `<code class="ml-2 text-xs bg-white px-1 rounded border border-gray-200">${ this . escape ( i . detail ) } </code>` : '' } </div>` ;
1744+ return `<div class="text-sm py-1"><span class="badge ${ cls } ">${ i . severity . toUpperCase ( ) } </span> ${ this . t ( i . key ) } ${ i . detail ? `<code class="ml-2 text-xs bg-white px-1 rounded border border-gray-200 inline-block max-w-full overflow-x-auto align-middle whitespace-nowrap ">${ this . escape ( i . detail ) } </code>` : '' } </div>` ;
16481745 } ) . join ( '' ) ;
16491746
16501747 const url = this . flowUrl ( r , r . type ) ;
0 commit comments