diff --git a/Workflow/app/api/cipc/search/route.ts b/Workflow/app/api/cipc/search/route.ts index cf25f95..a4e7854 100644 --- a/Workflow/app/api/cipc/search/route.ts +++ b/Workflow/app/api/cipc/search/route.ts @@ -492,6 +492,42 @@ function badRequest(message: string) { return NextResponse.json({ ok: false, error: message }, { status: 400 }); } +// =================================================================== +// RUNTIME CAPABILITY CHECK +// =================================================================== +// CIPC/BizPortal search is not an ordinary server-side job: it drives a +// real Chrome session through agent-browser to log into BizPortal and read +// the result pane. That toolchain exists only on the Windows dev machine. +// The Coolify image is a Linux container with no browser tooling at all — +// see the NOTE at the top of the Dockerfile, which says exactly this. +// +// Until 2026-10-20 this was not detected up front. The request fell +// through to the legacy `spawnSync` fallback, which builds a Windows-style +// interpreter path even on Linux (`.venv/Scripts/` + `python`), so `/bin/sh` +// answered "not found" with exit code 127, `extractError()` found no ERROR: +// line in stdout and returned "Unknown error", and the UI rendered that over +// a raw shell message. A dead end for anyone who tried the search on the +// deployed URL. +// +// Returns a plain-language reason when the runtime can't be used here, or +// null when it can. Re-checked per request so it never goes stale. +function cipcUnavailableReason(): string | null { + const isWindows = process.platform === "win32"; + const pythonBin = isWindows + ? path.join(SCRIPTS_DIR, ".venv", "Scripts", "python.exe") + : path.join(SCRIPTS_DIR, ".venv", "bin", "python"); + + if (!fs.existsSync(pythonBin)) { + return ( + "CIPC search runs on the local Workflow app on your PC (npm run dev), " + + "not on the deployed server — this server has no BizPortal browser " + + `runtime installed (no Python at ${pythonBin}). ` + + "Open the app on your machine to run this search." + ); + } + return null; +} + /** Extract the markdown block from the full stdout. */ function extractMarkdown(stdout: string): string { const idx = stdout.indexOf("\n#"); @@ -535,6 +571,24 @@ export async function POST(req: NextRequest) { } if (!rawQuery) return badRequest("query is required"); + // Refuse early, and in words the operator can act on, when the browser + // runtime this route needs is not present on the host (the deployed Linux + // container). Better a clear 503 than a 165 s hang or a shell error. + const unavailable = cipcUnavailableReason(); + if (unavailable) { + return NextResponse.json( + { + ok: false, + error: unavailable, + raw: unavailable, + search_kind: kind, + query: rawQuery, + runtime_missing: true, + }, + { status: 503 } + ); + } + let resolvedQuery = rawQuery; let resolvedVia: "k_number" | "company_name" | "director_id" | null = null;