| 1367 | } |
| 1368 | |
| 1369 | static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { |
| 1370 | char project[256] = {0}; |
| 1371 | char max_str[32] = {0}; |
| 1372 | |
| 1373 | if (!cbm_http_query_param(req->query, "project", project, (int)sizeof(project)) || |
| 1374 | project[0] == '\0') { |
| 1375 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"missing project parameter\"}"); |
| 1376 | return; |
| 1377 | } |
| 1378 | |
| 1379 | int max_nodes = 0; /* 0 → layout default budget */ |
| 1380 | if (cbm_http_query_param(req->query, "max_nodes", max_str, (int)sizeof(max_str))) { |
| 1381 | int v = atoi(max_str); |
| 1382 | if (v > 0) |
| 1383 | max_nodes = v; |
| 1384 | } |
| 1385 | |
| 1386 | char db_path[1024]; |
| 1387 | db_path_for_project(project, db_path, sizeof(db_path)); |
| 1388 | |
| 1389 | if (!cbm_file_exists(db_path)) { |
| 1390 | cbm_http_replyf(c, 404, g_cors_json, "{\"error\":\"project not found\"}"); |
| 1391 | return; |
| 1392 | } |
| 1393 | |
| 1394 | cbm_store_t *store = cbm_store_open_path(db_path); |
| 1395 | if (!store) { |
| 1396 | cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"cannot open store\"}"); |
| 1397 | return; |
| 1398 | } |
| 1399 | |
| 1400 | cbm_layout_result_t *layout = |
| 1401 | cbm_layout_compute(store, project, CBM_LAYOUT_OVERVIEW, NULL, 0, max_nodes); |
| 1402 | |
| 1403 | /* Find linked projects from CROSS_* edges. Keep `store` open through the |
| 1404 | * linked-projects loop below so we can resolve target Route QNs against |
| 1405 | * the linked stores when populating cross_edges. */ |
| 1406 | char *linked[LAYOUT_MAX_LINKED]; |
| 1407 | int linked_count = find_cross_repo_targets(store, project, linked, LAYOUT_MAX_LINKED); |
| 1408 | |
| 1409 | if (!layout) { |
| 1410 | cbm_store_close(store); |
| 1411 | cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"layout computation failed\"}"); |
| 1412 | return; |
| 1413 | } |
| 1414 | |
| 1415 | /* Capture primary cluster radius before freeing the layout. */ |
| 1416 | double primary_radius = layout_radius(layout); |
| 1417 | |
| 1418 | /* Build JSON: primary layout + linked_projects */ |
| 1419 | char *primary_json = cbm_layout_to_json(layout); |
| 1420 | cbm_layout_free(layout); |
| 1421 | if (!primary_json) { |
| 1422 | cbm_store_close(store); |
| 1423 | cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"JSON serialization failed\"}"); |
| 1424 | return; |
| 1425 | } |
| 1426 |
no test coverage detected