Skip to content

Commit 87e33b4

Browse files
committed
feat(flow-doctor): show app store diagnostics and changelogs
1 parent 63ef248 commit 87e33b4

1 file changed

Lines changed: 214 additions & 2 deletions

File tree

docs/tools/flow-doctor.html

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)