Fix apps.list: filter compose services by environment_id, not non-existent project field

Coolify's /api/v1/services response does not include a `project` field.
Services belong to environments and environments belong to projects.
The old filter checked s.project.uuid (always undefined) and silently
dropped every service from the result, so compose-stack apps like
Twenty CRM never showed up in apps.list.

Now we resolve the project's environment IDs via getProject() and
filter services where environment_id is in that set. Also surface the
public service's fqdn in the response (extracted from s.applications)
so the AI can immediately tell the user where the app lives.

Made-with: Cursor
This commit is contained in:
2026-04-27 18:09:43 -07:00
parent 95ab91727e
commit 4c804d670b

View File

@@ -43,6 +43,7 @@ import {
listApplicationDeployments,
listApplicationEnvs,
listApplicationsInProject,
getProject,
projectUuidOf,
TenantError,
upsertApplicationEnv,
@@ -418,18 +419,28 @@ async function toolAppsList(principal: Principal) {
// Fetch Applications and Services in parallel.
// Services are compose stacks created via the composeRaw pathway;
// they live at /services not /applications.
const [apps, allServices] = await Promise.allSettled([
// Coolify's /services response does NOT include a `project` field — services
// belong to environments, and environments belong to projects. So we resolve
// the project's environment IDs first, then filter services by environment_id.
const [apps, allServices, project] = await Promise.allSettled([
listApplicationsInProject(projectUuid),
listAllServices(),
getProject(projectUuid),
]);
const appList = apps.status === 'fulfilled' ? apps.value : [];
const projectEnvIds = new Set<number>(
project.status === 'fulfilled'
? (project.value.environments ?? []).map((e) => e.id)
: [],
);
const serviceList = (allServices.status === 'fulfilled' && Array.isArray(allServices.value)
? (allServices.value as Array<Record<string, unknown>>)
: []
).filter(s => {
const proj = s.project as Record<string, unknown> | undefined;
return proj?.uuid === projectUuid;
).filter((s) => {
const envId = typeof s.environment_id === 'number' ? s.environment_id : Number(s.environment_id);
return projectEnvIds.has(envId);
});
return NextResponse.json({
@@ -443,15 +454,21 @@ async function toolAppsList(principal: Principal) {
gitBranch: a.git_branch ?? null,
resourceType: 'application',
})),
...serviceList.map(s => ({
uuid: String(s.uuid),
name: String(s.name ?? ''),
status: String(s.status ?? 'unknown'),
fqdn: null,
gitRepository: null,
gitBranch: null,
resourceType: 'service',
})),
...serviceList.map((s) => {
// Try to extract a usable URL from the service's applications array
// (compose stacks expose their public service's fqdn there).
const apps = (s.applications as Array<Record<string, unknown>>) || [];
const publicApp = apps.find((a) => a.fqdn);
return {
uuid: String(s.uuid),
name: String(s.name ?? ''),
status: String(s.status ?? 'unknown'),
fqdn: publicApp?.fqdn ? String(publicApp.fqdn) : null,
gitRepository: null,
gitBranch: null,
resourceType: 'service',
};
}),
],
});
}