Skip to content

Commit d01bea2

Browse files
Tiwasclaude
andcommitted
feat(flow-doctor): credit extension apps via id-prefix heuristic
Some apps extend another app via a sidecar package (e.g. MELCloud extension provides extra flow cards / runtime helpers for MELCloud). Homey's API doesn't expose runtime app-to-app dependencies, so a heuristic based on the installed-id prefix is the only signal: if com.mecloud.extension is installed alongside com.mecloud, the former is treated as an extension of the latter. When parent and extension coexist: - The extension's card shows "Extends <Parent name>" (italic, subtle) - The "Unused" badge / sev-info class / onlyUnused filter / tab badge breakdown all consider the parent's usage and device count when deciding whether the extension is in use i18n: apps.extends in en/no/de/nl. Verified live: MELCloud extension correctly resolves to com.mecloud as parent (only such pair on the user's Homey across 55 installed apps — no false positives), unused count drops 12 → 11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ca164d7 commit d01bea2

1 file changed

Lines changed: 35 additions & 4 deletions

File tree

docs/tools/flow-doctor.html

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
226226
'apps.unused': 'Unused by any flow',
227227
'apps.usedBy': 'flows',
228228
'apps.devices': 'devices',
229+
'apps.extends': 'Extends',
229230
'apps.installed': 'Installed',
230231
'apps.store': 'store',
231232
'apps.crashed': 'Crashed',
@@ -299,6 +300,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
299300
'apps.unused': 'Ikke brukt av noen flow',
300301
'apps.usedBy': 'flows',
301302
'apps.devices': 'enheter',
303+
'apps.extends': 'Utvider',
302304
'apps.installed': 'Installert',
303305
'apps.store': 'butikk',
304306
'apps.crashed': 'Krasjet',
@@ -372,6 +374,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
372374
'apps.unused': 'Von keinem Flow verwendet',
373375
'apps.usedBy': 'Flows',
374376
'apps.devices': 'Geräte',
377+
'apps.extends': 'Erweitert',
375378
'apps.installed': 'Installiert',
376379
'apps.store': 'Store',
377380
'apps.crashed': 'Abgestürzt',
@@ -445,6 +448,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
445448
'apps.unused': 'Door geen enkele flow gebruikt',
446449
'apps.usedBy': 'flows',
447450
'apps.devices': 'apparaten',
451+
'apps.extends': 'Breidt uit',
448452
'apps.installed': 'Geïnstalleerd',
449453
'apps.store': 'store',
450454
'apps.crashed': 'Gecrasht',
@@ -692,6 +696,15 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
692696
return `https://homey.app/a/${encodeURIComponent(appId)}`;
693697
}
694698

699+
isAppUnused(app) {
700+
if (app.usage > 0 || (app.deviceCount || 0) > 0) return false;
701+
if (app.parentAppId) {
702+
const parent = (this.appsList || []).find(p => p.id === app.parentAppId);
703+
if (parent && (parent.usage > 0 || (parent.deviceCount || 0) > 0)) return false;
704+
}
705+
return true;
706+
}
707+
695708
switchTab(name) {
696709
document.querySelectorAll('#tab-nav .tab-btn').forEach(btn => {
697710
const active = btn.getAttribute('data-tab') === name;
@@ -936,7 +949,21 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
936949
mem: a.usage && typeof a.usage.mem === 'number' ? a.usage.mem : null,
937950
usage: appUsage[a.id] || 0,
938951
deviceCount: appDeviceCount[a.id] || 0,
952+
parentAppId: null,
953+
parentAppName: null,
939954
}));
955+
// Heuristic: if an installed app's id starts with another installed app's id + '.',
956+
// treat it as an extension of that parent (e.g. com.mecloud.extension → com.mecloud).
957+
// Extensions inherit their parent's usage/device signal for the "unused" check,
958+
// since Homey's API doesn't expose runtime app-to-app dependencies.
959+
appsList.forEach(app => {
960+
const candidates = appsList.filter(p => p.id !== app.id && app.id.startsWith(p.id + '.'));
961+
if (candidates.length) {
962+
const parent = candidates.sort((x, y) => y.id.length - x.id.length)[0];
963+
app.parentAppId = parent.id;
964+
app.parentAppName = parent.name;
965+
}
966+
});
940967

941968
this.results = results;
942969
this.appsList = appsList;
@@ -995,7 +1022,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
9951022
const allApps = this.appsList || [];
9961023
const nCrashed = allApps.filter(a => a.crashed).length;
9971024
const nUpdate = allApps.filter(a => a.hasUpdate && !a.crashed).length;
998-
const nUnused = allApps.filter(a => !a.crashed && !a.hasUpdate && a.usage === 0 && (a.deviceCount || 0) === 0).length;
1025+
const nUnused = allApps.filter(a => !a.crashed && !a.hasUpdate && this.isAppUnused(a)).length;
9991026
const nAppsOk = allApps.length - nCrashed - nUpdate - nUnused;
10001027
document.getElementById('tab-apps-count').innerHTML =
10011028
`${allApps.length} (<span class="text-red-600">X:${nCrashed}</span> · <span class="text-amber-600">U:${nUpdate}</span> · <span class="text-blue-600">!:${nUnused}</span> · <span class="text-green-700">OK:${nAppsOk}</span>)`;
@@ -1064,7 +1091,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
10641091
list.innerHTML = '';
10651092
const apps = (this.appsList || []).filter(a => {
10661093
if (onlyUpdates && !a.hasUpdate) return false;
1067-
if (onlyUnused && (a.usage > 0 || (a.deviceCount || 0) > 0)) return false;
1094+
if (onlyUnused && !this.isAppUnused(a)) return false;
10681095
if (search && !(a.name.toLowerCase().includes(search) || a.id.toLowerCase().includes(search))) return false;
10691096
return true;
10701097
});
@@ -1094,11 +1121,11 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
10941121
} else {
10951122
statusBadge = `<span class="badge badge-ok">${this.t('apps.upToDate')}</span>`;
10961123
}
1097-
const unusedBadge = (a.usage === 0 && (a.deviceCount || 0) === 0) ? `<span class="badge badge-info">${this.t('apps.unused')}</span>` : '';
1124+
const unusedBadge = this.isAppUnused(a) ? `<span class="badge badge-info">${this.t('apps.unused')}</span>` : '';
10981125
const crashedBadge = a.crashed ? `<span class="badge badge-err">${this.t('apps.crashed')}${a.crashedCount > 1 ? ' ×' + a.crashedCount : ''}</span>` : '';
10991126
const disabledBadge = !a.enabled ? `<span class="badge badge-info">${this.t('apps.disabled')}</span>` : '';
11001127
if (a.crashed) sevClass = 'sev-error';
1101-
else if (a.usage === 0 && (a.deviceCount || 0) === 0 && !sevClass) sevClass = 'sev-info';
1128+
else if (this.isAppUnused(a) && !sevClass) sevClass = 'sev-info';
11021129
const memMb = a.mem != null ? (a.mem / 1048576).toFixed(0) : null;
11031130
const cpuPct = a.cpu != null ? a.cpu.toFixed(1) : null;
11041131
const resourceHtml = (memMb != null || cpuPct != null)
@@ -1116,6 +1143,9 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
11161143
const devicesText = `${a.deviceCount || 0} ${this.t('apps.devices')}`;
11171144
const devicesHtml = `<span class="text-xs ${(a.deviceCount || 0) > 0 ? 'text-gray-600' : 'text-gray-400'}">${devicesText}</span>`;
11181145
const usageHtml = `${devicesHtml}${flowsHtml}`;
1146+
const extendsHtml = a.parentAppId
1147+
? `<div class="text-xs text-gray-500 mt-1 italic">${this.t('apps.extends')} ${this.escape(a.parentAppName || a.parentAppId)}</div>`
1148+
: '';
11191149

11201150
const div = document.createElement('div');
11211151
div.className = `issue-row ${sevClass} bg-white border border-gray-200 rounded-lg p-4`;
@@ -1130,6 +1160,7 @@ <h3 class="font-bold text-red-800 mb-2" data-i18n="error.title">Something went w
11301160
${disabledBadge}
11311161
</div>
11321162
<div class="text-xs text-gray-600 mt-1">${versionInfo}</div>
1163+
${extendsHtml}
11331164
${resourceHtml}
11341165
<div class="text-xs text-gray-400 mt-1 font-mono">${this.escape(a.id)}</div>
11351166
</div>

0 commit comments

Comments
 (0)