- Add partial unique index on (project_id, port) for active dev servers so the SELECT-then-INSERT race can no longer create duplicate 'Port 3000' rows. - Make startDevServer race-safe: on unique violation, adopt the winning row instead of duplicating. - ensure route no longer marks a server 'running' before it binds the port; the readiness probe flips starting->running only after the port answers. Kills the '502 -> broken CSS -> works' refresh loop. - Deduplicate previews per-port in sortDevPreviewsFrontendFirst as a defensive backstop for the dropdown. - Revert iframe _refresh query-param hack (was forcing cold recompiles).
109 lines
2.9 KiB
TypeScript
109 lines
2.9 KiB
TypeScript
/**
|
|
* When several dev servers expose preview URLs, prefer the one that looks like
|
|
* the user-facing web app (monorepo-safe ordering for anatomy + tool listings).
|
|
*/
|
|
|
|
function previewFrontendRank(row: { name: string; command: string }): number {
|
|
const n = row.name.toLowerCase();
|
|
const c = row.command.toLowerCase();
|
|
const blob = `${n} ${c}`;
|
|
|
|
if (
|
|
/\bfrontend\b/.test(blob) ||
|
|
blob.includes("/frontend/") ||
|
|
blob.includes("\\frontend\\") ||
|
|
blob.includes("/apps/web") ||
|
|
blob.includes("apps/web/") ||
|
|
blob.includes("/packages/web") ||
|
|
blob.includes("packages/web/")
|
|
) {
|
|
return 0;
|
|
}
|
|
|
|
if (
|
|
blob.includes("next dev") ||
|
|
blob.includes("vite") ||
|
|
blob.includes("nuxt") ||
|
|
blob.includes("remix dev") ||
|
|
blob.includes("astro dev") ||
|
|
blob.includes("npm run dev") ||
|
|
blob.includes("pnpm dev") ||
|
|
blob.includes("yarn dev")
|
|
) {
|
|
return 1;
|
|
}
|
|
|
|
if (/\b(web|ui|client)\b/.test(n)) {
|
|
return 2;
|
|
}
|
|
|
|
if (
|
|
/\bapi\b/.test(n) ||
|
|
/\bbackend\b/.test(n) ||
|
|
blob.includes("fastapi") ||
|
|
blob.includes("uvicorn") ||
|
|
blob.includes("gunicorn") ||
|
|
blob.includes("django") ||
|
|
(blob.includes("rails ") && blob.includes("server"))
|
|
) {
|
|
return 10;
|
|
}
|
|
|
|
return 5;
|
|
}
|
|
|
|
function startedAtMs(startedAt: string | Date): number {
|
|
return typeof startedAt === "string"
|
|
? new Date(startedAt).getTime()
|
|
: startedAt.getTime();
|
|
}
|
|
|
|
export function compareDevPreviewFrontendFirst<
|
|
T extends {
|
|
name: string;
|
|
command: string;
|
|
port: number;
|
|
started_at: string | Date;
|
|
},
|
|
>(a: T, b: T): number {
|
|
const pa = previewFrontendRank(a);
|
|
const pb = previewFrontendRank(b);
|
|
if (pa !== pb) return pa - pb;
|
|
const ax = a.port === 3000 ? 0 : a.port;
|
|
const bx = b.port === 3000 ? 0 : b.port;
|
|
if (ax !== bx) return ax - bx;
|
|
return startedAtMs(b.started_at) - startedAtMs(a.started_at);
|
|
}
|
|
|
|
export function sortDevPreviewsFrontendFirst<
|
|
T extends {
|
|
name: string;
|
|
command: string;
|
|
port: number;
|
|
started_at: string | Date;
|
|
state?: string;
|
|
},
|
|
>(rows: T[]): T[] {
|
|
// Defensive dedup: collapse to a single row per port so a stale leftover row
|
|
// can never render two "Port 3000" entries in the preview dropdown. A running
|
|
// row always beats a non-running one; ties break on most-recent started_at.
|
|
const bestByPort = new Map<number, T>();
|
|
for (const row of rows) {
|
|
const existing = bestByPort.get(row.port);
|
|
if (!existing) {
|
|
bestByPort.set(row.port, row);
|
|
continue;
|
|
}
|
|
const rowRunning = row.state === "running" ? 1 : 0;
|
|
const existingRunning = existing.state === "running" ? 1 : 0;
|
|
if (rowRunning !== existingRunning) {
|
|
if (rowRunning > existingRunning) bestByPort.set(row.port, row);
|
|
continue;
|
|
}
|
|
if (startedAtMs(row.started_at) > startedAtMs(existing.started_at)) {
|
|
bestByPort.set(row.port, row);
|
|
}
|
|
}
|
|
return [...bestByPort.values()].sort(compareDevPreviewFrontendFirst);
|
|
}
|