@@ -291,6 +291,22 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
291291 'apps.viewTest' : 'Test ↗' ,
292292 'apps.versionStable' : 'stable' ,
293293 'apps.versionTest' : 'test' ,
294+ 'apps.storeUnavailable' : 'Not available in App Store' ,
295+ 'apps.storeLookupFailed' : 'App Store lookup failed' ,
296+ 'apps.sdk' : 'SDK' ,
297+ 'apps.runtime' : 'runtime' ,
298+ 'apps.platforms' : 'platforms' ,
299+ 'apps.compatibility' : 'requires' ,
300+ 'apps.permissions' : 'permissions' ,
301+ 'apps.category' : 'category' ,
302+ 'apps.author' : 'author' ,
303+ 'apps.private' : 'private' ,
304+ 'apps.changelog' : 'Changelog' ,
305+ 'apps.changelogHeader' : 'App Store changelog' ,
306+ 'apps.changelogLoading' : 'Loading changelog…' ,
307+ 'apps.changelogUnavailable' : 'Not available' ,
308+ 'apps.changelogTest' : 'test' ,
309+ 'apps.changelogStable' : 'stable' ,
294310 'apps.crashed' : 'Crashed' ,
295311 'apps.disabled' : 'Disabled' ,
296312 'apps.memory' : 'Memory' ,
@@ -392,6 +408,22 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
392408 'apps.viewTest' : 'Test ↗' ,
393409 'apps.versionStable' : 'stabil' ,
394410 'apps.versionTest' : 'test' ,
411+ 'apps.storeUnavailable' : 'Ikke tilgjengelig i App Store' ,
412+ 'apps.storeLookupFailed' : 'Oppslag mot App Store feilet' ,
413+ 'apps.sdk' : 'SDK' ,
414+ 'apps.runtime' : 'runtime' ,
415+ 'apps.platforms' : 'plattformer' ,
416+ 'apps.compatibility' : 'krever' ,
417+ 'apps.permissions' : 'tillatelser' ,
418+ 'apps.category' : 'kategori' ,
419+ 'apps.author' : 'utvikler' ,
420+ 'apps.private' : 'privat' ,
421+ 'apps.changelog' : 'Endringslogg' ,
422+ 'apps.changelogHeader' : 'Endringslogg fra App Store' ,
423+ 'apps.changelogLoading' : 'Laster endringslogg…' ,
424+ 'apps.changelogUnavailable' : 'Ikke tilgjengelig' ,
425+ 'apps.changelogTest' : 'test' ,
426+ 'apps.changelogStable' : 'stabil' ,
395427 'apps.crashed' : 'Krasjet' ,
396428 'apps.disabled' : 'Deaktivert' ,
397429 'apps.memory' : 'Minne' ,
@@ -493,6 +525,22 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
493525 'apps.viewTest' : 'Test ↗' ,
494526 'apps.versionStable' : 'stabil' ,
495527 'apps.versionTest' : 'test' ,
528+ 'apps.storeUnavailable' : 'Nicht im App Store verfügbar' ,
529+ 'apps.storeLookupFailed' : 'App-Store-Abfrage fehlgeschlagen' ,
530+ 'apps.sdk' : 'SDK' ,
531+ 'apps.runtime' : 'runtime' ,
532+ 'apps.platforms' : 'Plattformen' ,
533+ 'apps.compatibility' : 'erfordert' ,
534+ 'apps.permissions' : 'Berechtigungen' ,
535+ 'apps.category' : 'Kategorie' ,
536+ 'apps.author' : 'Entwickler' ,
537+ 'apps.private' : 'privat' ,
538+ 'apps.changelog' : 'Änderungslog' ,
539+ 'apps.changelogHeader' : 'Änderungslog aus dem App Store' ,
540+ 'apps.changelogLoading' : 'Änderungslog wird geladen…' ,
541+ 'apps.changelogUnavailable' : 'Nicht verfügbar' ,
542+ 'apps.changelogTest' : 'Test' ,
543+ 'apps.changelogStable' : 'stabil' ,
496544 'apps.crashed' : 'Abgestürzt' ,
497545 'apps.disabled' : 'Deaktiviert' ,
498546 'apps.memory' : 'Speicher' ,
@@ -594,6 +642,22 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
594642 'apps.viewTest' : 'Test ↗' ,
595643 'apps.versionStable' : 'stabiel' ,
596644 'apps.versionTest' : 'test' ,
645+ 'apps.storeUnavailable' : 'Niet beschikbaar in de App Store' ,
646+ 'apps.storeLookupFailed' : 'App Store-opzoeking mislukt' ,
647+ 'apps.sdk' : 'SDK' ,
648+ 'apps.runtime' : 'runtime' ,
649+ 'apps.platforms' : 'platformen' ,
650+ 'apps.compatibility' : 'vereist' ,
651+ 'apps.permissions' : 'machtigingen' ,
652+ 'apps.category' : 'categorie' ,
653+ 'apps.author' : 'ontwikkelaar' ,
654+ 'apps.private' : 'privé' ,
655+ 'apps.changelog' : 'Wijzigingslog' ,
656+ 'apps.changelogHeader' : 'Wijzigingslog uit de App Store' ,
657+ 'apps.changelogLoading' : 'Wijzigingslog laden…' ,
658+ 'apps.changelogUnavailable' : 'Niet beschikbaar' ,
659+ 'apps.changelogTest' : 'test' ,
660+ 'apps.changelogStable' : 'stabiel' ,
597661 'apps.crashed' : 'Gecrasht' ,
598662 'apps.disabled' : 'Uitgeschakeld' ,
599663 'apps.memory' : 'Geheugen' ,
@@ -833,11 +897,32 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
833897 try {
834898 const url = `https://apps-api.athom.com/api/v1/app/${ encodeURIComponent ( a . id ) } ` ;
835899 const response = await fetch ( url , { headers : { Accept : 'application/json' } } ) ;
836- if ( ! response . ok ) return ;
900+ if ( ! response . ok ) {
901+ a . storeStatus = response . status === 404 ? 'missing' : 'error' ;
902+ return ;
903+ }
837904 const entry = await response . json ( ) ;
905+ const liveBuild = entry . liveBuild || { } ;
906+ const testBuild = entry . testBuild || { } ;
907+ const permissions = this . normalizeStoreArray ( liveBuild . permissions || entry . permissions ) ;
908+ const platforms = this . normalizeStoreArray ( liveBuild . platforms || entry . platforms ) ;
909+ a . storeStatus = 'available' ;
838910 a . stableVersion = this . normalizeVersionValue ( entry . liveVersion || ( entry . liveBuild && entry . liveBuild . version ) || entry . version ) ;
839911 a . testVersion = this . normalizeVersionValue ( entry . testVersion || ( entry . testBuild && entry . testBuild . version ) ) ;
912+ a . storeMeta = {
913+ author : entry . author && entry . author . name ? entry . author . name : null ,
914+ category : liveBuild . category || entry . category || null ,
915+ compatibility : liveBuild . compatibility || entry . compatibility || null ,
916+ runtime : liveBuild . runtime || entry . runtime || null ,
917+ sdk : liveBuild . sdk || entry . sdk || null ,
918+ platforms,
919+ permissionsCount : permissions . length ,
920+ private : ! ! entry . private ,
921+ liveState : liveBuild . state || null ,
922+ testState : testBuild . state || null ,
923+ } ;
840924 } catch ( _ ) {
925+ a . storeStatus = 'error' ;
841926 // Missing/private apps or transient network errors should not block the scan.
842927 }
843928 } ;
@@ -850,13 +935,107 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
850935 this . renderApps ( ) ;
851936 }
852937
938+ normalizeStoreArray ( value ) {
939+ if ( ! value ) return [ ] ;
940+ if ( Array . isArray ( value ) ) return value . filter ( v => v != null && v !== '' ) ;
941+ return [ value ] ;
942+ }
943+
853944 normalizeVersionValue ( value ) {
854945 if ( ! value ) return null ;
855946 if ( typeof value === 'string' ) return value ;
856947 if ( typeof value === 'object' ) return value . version || value . latestVersion || null ;
857948 return String ( value ) ;
858949 }
859950
951+ localizeStoreValue ( value ) {
952+ if ( ! value ) return null ;
953+ if ( typeof value === 'object' && ! Array . isArray ( value ) ) {
954+ return value [ this . lang ] || value . en || Object . values ( value ) . find ( Boolean ) || null ;
955+ }
956+ return String ( value ) ;
957+ }
958+
959+ appStoreChangelogUrl ( appId ) {
960+ return `https://apps-api.athom.com/api/v1/app/${ encodeURIComponent ( appId ) } /changelog` ;
961+ }
962+
963+ chooseAppChangelog ( app , changelog ) {
964+ const entries = changelog && typeof changelog === 'object' ? changelog : { } ;
965+ const byVersion = ( version ) => version && entries [ version ] ? { version, entry : entries [ version ] } : null ;
966+ const byState = ( state ) => {
967+ const matches = Object . entries ( entries )
968+ . filter ( ( [ , entry ] ) => entry && entry . state === state )
969+ . sort ( ( a , b ) => new Date ( b [ 1 ] . createdAt || 0 ) - new Date ( a [ 1 ] . createdAt || 0 ) ) ;
970+ return matches . length ? { version : matches [ 0 ] [ 0 ] , entry : matches [ 0 ] [ 1 ] } : null ;
971+ } ;
972+ const test = byVersion ( app . testVersion ) || ( app . testVersion ? byState ( 'test' ) : null ) ;
973+ if ( test ) return { channel : 'test' , ...test } ;
974+ const stable = byVersion ( app . stableVersion ) || ( app . stableVersion ? byState ( 'live' ) : null ) ;
975+ if ( stable ) return { channel : 'stable' , ...stable } ;
976+ return null ;
977+ }
978+
979+ renderAppChangelog ( app ) {
980+ if ( ! app . stableVersion && ! app . testVersion ) {
981+ return `<span class="text-gray-500">${ this . t ( 'apps.changelogUnavailable' ) } </span>` ;
982+ }
983+ if ( app . changelogStatus === 'loading' ) {
984+ return `<span class="text-gray-500">${ this . t ( 'apps.changelogLoading' ) } </span>` ;
985+ }
986+ if ( app . changelogStatus === 'loaded' && app . changelogText ) {
987+ const label = app . changelogChannel === 'test' ? this . t ( 'apps.changelogTest' ) : this . t ( 'apps.changelogStable' ) ;
988+ const text = this . escape ( app . changelogText ) . replace ( / \n / g, '<br>' ) ;
989+ return `<div class="flex items-center gap-2 mb-2">
990+ <span class="badge ${ app . changelogChannel === 'test' ? '' : 'badge-ok' } ">${ label } ${ this . escape ( app . changelogVersion || '' ) } </span>
991+ </div>
992+ <div class="text-xs text-gray-700 leading-relaxed">${ text } </div>` ;
993+ }
994+ if ( app . changelogStatus === 'loaded' || app . changelogStatus === 'error' ) {
995+ return `<span class="text-gray-500">${ this . t ( 'apps.changelogUnavailable' ) } </span>` ;
996+ }
997+ return `<span class="text-gray-500">${ this . t ( 'apps.changelogLoading' ) } </span>` ;
998+ }
999+
1000+ async loadAppChangelog ( appId , panel ) {
1001+ const app = ( this . appsList || [ ] ) . find ( a => a . id === appId ) ;
1002+ if ( ! app ) return ;
1003+ const content = panel ? panel . querySelector ( '[data-changelog-content]' ) : null ;
1004+ const update = ( ) => { if ( content ) content . innerHTML = this . renderAppChangelog ( app ) ; } ;
1005+ if ( ! app . stableVersion && ! app . testVersion ) {
1006+ app . changelogStatus = 'loaded' ;
1007+ update ( ) ;
1008+ return ;
1009+ }
1010+ if ( app . changelogStatus === 'loading' || app . changelogStatus === 'loaded' ) {
1011+ update ( ) ;
1012+ return ;
1013+ }
1014+ app . changelogStatus = 'loading' ;
1015+ update ( ) ;
1016+ try {
1017+ const response = await fetch ( this . appStoreChangelogUrl ( app . id ) , { headers : { Accept : 'application/json' } } ) ;
1018+ if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` ) ;
1019+ const changelog = await response . json ( ) ;
1020+ const selected = this . chooseAppChangelog ( app , changelog ) ;
1021+ if ( ! selected ) {
1022+ app . changelogStatus = 'loaded' ;
1023+ app . changelogText = null ;
1024+ update ( ) ;
1025+ return ;
1026+ }
1027+ const rawText = this . localizeStoreValue ( selected . entry && selected . entry . changelog ) ;
1028+ app . changelogStatus = 'loaded' ;
1029+ app . changelogChannel = selected . channel ;
1030+ app . changelogVersion = selected . version ;
1031+ app . changelogText = rawText || null ;
1032+ update ( ) ;
1033+ } catch ( _ ) {
1034+ app . changelogStatus = 'error' ;
1035+ update ( ) ;
1036+ }
1037+ }
1038+
8601039 extractStoreVersion ( app ) {
8611040 // Homey itself sets `updateAvailable` on the app payload when a newer version
8621041 // is in the App Store. The shape varies: sometimes a string, sometimes an object.
@@ -924,6 +1103,10 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
9241103 const apps = ( this . appsList || [ ] ) . map ( a => ( {
9251104 id : a . id , name : a . name , version : a . version ,
9261105 storeVersion : a . storeVersion || null ,
1106+ stableVersion : a . stableVersion || null ,
1107+ testVersion : a . testVersion || null ,
1108+ storeStatus : a . storeStatus || null ,
1109+ storeMeta : a . storeMeta || null ,
9271110 channel : a . channel || null ,
9281111 origin : a . origin || null ,
9291112 hasUpdate : ! ! a . hasUpdate , enabled : ! ! a . enabled ,
@@ -1498,6 +1681,25 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
14981681 storeLinks . push ( `<a href="${ testUrl } " target="_blank" rel="noopener" class="text-xs text-blue-600 hover:underline">${ this . t ( 'apps.viewTest' ) } </a>` ) ;
14991682 }
15001683 const storeLinksHtml = storeLinks . join ( '' ) ;
1684+ const meta = a . storeMeta || { } ;
1685+ const storeMetaParts = [ ] ;
1686+ if ( a . storeStatus === 'available' ) {
1687+ if ( meta . sdk ) storeMetaParts . push ( `${ this . t ( 'apps.sdk' ) } ${ this . escape ( meta . sdk ) } ` ) ;
1688+ if ( meta . runtime ) storeMetaParts . push ( `${ this . t ( 'apps.runtime' ) } ${ this . escape ( meta . runtime ) } ` ) ;
1689+ if ( meta . platforms && meta . platforms . length ) storeMetaParts . push ( `${ this . t ( 'apps.platforms' ) } ${ this . escape ( meta . platforms . join ( ', ' ) ) } ` ) ;
1690+ if ( meta . compatibility ) storeMetaParts . push ( `${ this . t ( 'apps.compatibility' ) } ${ this . escape ( meta . compatibility ) } ` ) ;
1691+ if ( meta . category ) storeMetaParts . push ( `${ this . t ( 'apps.category' ) } ${ this . escape ( this . localizeStoreValue ( meta . category ) ) } ` ) ;
1692+ if ( meta . author ) storeMetaParts . push ( `${ this . t ( 'apps.author' ) } ${ this . escape ( meta . author ) } ` ) ;
1693+ if ( meta . permissionsCount ) storeMetaParts . push ( `${ meta . permissionsCount } ${ this . t ( 'apps.permissions' ) } ` ) ;
1694+ if ( meta . private ) storeMetaParts . push ( this . t ( 'apps.private' ) ) ;
1695+ } else if ( a . storeStatus === 'missing' ) {
1696+ storeMetaParts . push ( this . t ( 'apps.storeUnavailable' ) ) ;
1697+ } else if ( a . storeStatus === 'error' ) {
1698+ storeMetaParts . push ( this . t ( 'apps.storeLookupFailed' ) ) ;
1699+ }
1700+ const storeMetaHtml = storeMetaParts . length
1701+ ? `<div class="text-xs text-gray-500 mt-1">${ storeMetaParts . join ( ' · ' ) } </div>`
1702+ : '' ;
15011703 const flowsCount = a . usage || 0 ;
15021704 const devicesCount = a . deviceCount || 0 ;
15031705 const flowsHtml = flowsCount > 0
@@ -1506,7 +1708,8 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
15061708 const devicesHtml = devicesCount > 0
15071709 ? `<button type="button" data-toggle-app="${ this . escape ( a . id ) } " data-toggle-kind="devices" class="app-toggle text-xs text-blue-600 hover:underline">${ devicesCount } ${ this . t ( 'apps.devices' ) } <span class="chevron">▸</span></button>`
15081710 : `<span class="text-xs text-gray-400">0 ${ this . t ( 'apps.devices' ) } </span>` ;
1509- const usageHtml = `${ devicesHtml } ${ flowsHtml } ` ;
1711+ const changelogButtonHtml = `<button type="button" data-toggle-app="${ this . escape ( a . id ) } " data-toggle-kind="changelog" class="app-toggle text-xs text-blue-600 hover:underline">${ this . t ( 'apps.changelog' ) } <span class="chevron">▸</span></button>` ;
1712+ const usageHtml = `${ devicesHtml } ${ flowsHtml } ${ changelogButtonHtml } ` ;
15101713 const extendsHtml = a . parentAppId
15111714 ? `<div class="text-xs text-gray-500 mt-1 italic">${ this . t ( 'apps.extends' ) } ${ this . escape ( a . parentAppName || a . parentAppId ) } </div>`
15121715 : '' ;
@@ -1545,6 +1748,10 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
15451748 } ) . join ( '' ) } </ul>
15461749 </div>`
15471750 : '' ;
1751+ const changelogPanelHtml = `<div class="app-panel hidden mt-3 pt-3 border-t border-gray-100" data-panel-kind="changelog">
1752+ <div class="text-xs font-semibold text-gray-700 mb-1">${ this . t ( 'apps.changelogHeader' ) } </div>
1753+ <div data-changelog-content>${ this . renderAppChangelog ( a ) } </div>
1754+ </div>` ;
15481755
15491756 const div = document . createElement ( 'div' ) ;
15501757 const isExt = ! ! a . parentAppId ;
@@ -1562,6 +1769,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
15621769 ${ disabledBadge }
15631770 </div>
15641771 <div class="text-xs text-gray-600 mt-1">${ versionInfo } </div>
1772+ ${ storeMetaHtml }
15651773 ${ extendsHtml }
15661774 ${ resourceHtml }
15671775 <div class="text-xs text-gray-400 mt-1 font-mono">${ this . escape ( a . id ) } </div>
@@ -1573,6 +1781,7 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
15731781 </div>
15741782 ${ devicesPanelHtml }
15751783 ${ flowsPanelHtml }
1784+ ${ changelogPanelHtml }
15761785 ` ;
15771786 list . appendChild ( div ) ;
15781787 }
@@ -1587,6 +1796,9 @@ <h2 class="text-xl font-bold text-gray-900" data-i18n="bug.title">Report a bug</
15871796 const nowHidden = panel . classList . toggle ( 'hidden' ) ;
15881797 const chev = btn . querySelector ( '.chevron' ) ;
15891798 if ( chev ) chev . textContent = nowHidden ? '▸' : '▾' ;
1799+ if ( ! nowHidden && kind === 'changelog' ) {
1800+ this . loadAppChangelog ( btn . getAttribute ( 'data-toggle-app' ) , panel ) ;
1801+ }
15901802 } ) ;
15911803 } ) ;
15921804 }
0 commit comments