-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
480 lines (427 loc) · 20.9 KB
/
Copy pathMakefile
File metadata and controls
480 lines (427 loc) · 20.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# agent-pmo:2efd847
# =============================================================================
# Makefile — HealthcareSamples
# Cross-platform: Linux, macOS, Windows (via GNU Make)
# =============================================================================
.PHONY: build test lint fmt clean ci setup db-up db-down db-reset db-wait db-migrate start-stack start-local start-docker resume-docker deploy-dashboard dashboard-ts dashboard-ts-dev dashboard-ts-build dashboard-ts-lint dashboard-ts-test dashboard-ts-e2e dashboard-ts-check nuke _reclaim-ports _reclaim-e2e _ensure-embedding-service
# -----------------------------------------------------------------------------
# OS Detection
# -----------------------------------------------------------------------------
ifeq ($(OS),Windows_NT)
SHELL := powershell.exe
.SHELLFLAGS := -NoProfile -Command
RM = Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
MKDIR = New-Item -ItemType Directory -Force
HOME ?= $(USERPROFILE)
else
RM = rm -rf
MKDIR = mkdir -p
endif
# Per-project coverage thresholds live in this JSON file. Each test
# project gets its own minimum line-rate. `make test` enforces these after
# each project. Bump thresholds to floor(measured) - 1 when coverage increases.
COVERAGE_THRESHOLDS_FILE ?= coverage-thresholds.json
# Postgres dev database (docker compose). Override in CI via env vars.
DB_COMPOSE_FILE ?= docker/docker-compose.db.yml
DB_PASSWORD ?= changeme
DB_HOST ?= localhost
DB_PORT ?= 5432
PG_BASE_URL ?= Host=$(DB_HOST);Port=$(DB_PORT);Username=postgres;Password=$(DB_PASSWORD)
# =============================================================================
# PRIMARY TARGETS
# =============================================================================
## build: Compile/assemble all artifacts (requires running Postgres + migrated schemas)
build: db-migrate
@echo "==> Building..."
dotnet build HealthcareSamples.sln --configuration Release
@$(MAKE) dashboard-ts-build
# Test projects in execution order. Cheapest / most foundational first so a
# break in a lower layer fails the run immediately, before slower E2E suites.
TEST_PROJECTS = \
Gatekeeper/Gatekeeper.Api.Tests/Gatekeeper.Api.Tests.csproj \
Clinical/Clinical.Api.Tests/Clinical.Api.Tests.csproj \
Scheduling/Scheduling.Api.Tests/Scheduling.Api.Tests.csproj \
ICD10/ICD10.Api.Tests/ICD10.Api.Tests.csproj \
ICD10/ICD10.Cli.Tests/ICD10.Cli.Tests.csproj
## test: Run full test suite with coverage (FAIL FAST)
## - Stops at the first failing test inside an assembly (xunit stopOnFail)
## - Stops at the first failing assembly across the suite (set -e)
## - After each project, checks coverage against threshold from $(COVERAGE_THRESHOLDS_FILE)
## and fails immediately if below.
test: db-migrate _ensure-embedding-service
@echo "==> Testing (fail-fast)..."
@command -v jq >/dev/null 2>&1 || { echo "FAIL: jq is required (brew install jq / apt-get install jq)"; exit 1; }
@if [ ! -f "$(COVERAGE_THRESHOLDS_FILE)" ]; then \
echo "FAIL: $(COVERAGE_THRESHOLDS_FILE) not found"; exit 1; \
fi
@set -e; \
rm -rf TestResults; \
default=$$(jq -r '.default_threshold' $(COVERAGE_THRESHOLDS_FILE)); \
for proj in $(TEST_PROJECTS); do \
proj_dir=$$(dirname "$$proj"); \
echo ""; \
echo "==> Testing $$proj"; \
inc_filter=$$(jq -r --arg p "$$proj_dir" '.test_projects[$$p].include // ""' $(COVERAGE_THRESHOLDS_FILE)); \
source_name=$$(jq -r --arg p "$$proj_dir" '.test_projects[$$p].source // $$p' $(COVERAGE_THRESHOLDS_FILE)); \
if [ -n "$$inc_filter" ]; then \
dotnet test "$$proj" --configuration Release \
--settings coverlet.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory "TestResults/$$proj_dir" \
--verbosity normal \
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Include="$$inc_filter" \
|| { echo ""; echo "FAIL: $$proj failed -- aborting remaining test projects"; exit 1; }; \
else \
dotnet test "$$proj" --configuration Release \
--settings coverlet.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory "TestResults/$$proj_dir" \
--verbosity normal \
|| { echo ""; echo "FAIL: $$proj failed -- aborting remaining test projects"; exit 1; }; \
fi; \
cobertura=$$(find "TestResults/$$proj_dir" -name 'coverage.cobertura.xml' 2>/dev/null | head -1); \
threshold=$$(jq -r --arg p "$$proj_dir" --arg d "$$default" '.test_projects[$$p].threshold // ($$d | tonumber)' $(COVERAGE_THRESHOLDS_FILE)); \
if [ -z "$$cobertura" ]; then \
echo "FAIL ($$proj_dir): no coverage.cobertura.xml"; exit 1; \
fi; \
line_rate=$$(awk 'match($$0, /line-rate="[0-9.]+"/) { s=substr($$0, RSTART+11, RLENGTH-12); print s; exit }' "$$cobertura"); \
pct=$$(awk "BEGIN{printf \"%.1f\", $${line_rate:-0}*100}"); \
pct_int=$$(awk "BEGIN{printf \"%d\", $${line_rate:-0}*100}"); \
if [ "$$pct_int" -lt "$$threshold" ]; then \
printf "FAIL %-44s %s%% < %s%%\n" "$$source_name" "$$pct" "$$threshold"; \
exit 1; \
else \
printf "OK %-44s %s%% >= %s%%\n" "$$source_name" "$$pct" "$$threshold"; \
fi; \
done
@$(MAKE) dashboard-ts-test
@$(MAKE) dashboard-ts-e2e
## lint: Run all linters/analyzers (read-only). Does NOT format.
lint: db-migrate
@echo "==> Linting..."
dotnet build HealthcareSamples.sln --configuration Release
@$(MAKE) dashboard-ts-lint
## fmt: Format all code in-place. Pass CHECK=1 for read-only verify (CI use).
fmt:
ifdef CHECK
@echo "==> Checking format..."
dotnet csharpier check .
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm format
else
@echo "==> Formatting..."
dotnet csharpier format .
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm format:fix
endif
## clean: Remove all build artifacts
clean:
@echo "==> Cleaning..."
ifeq ($(OS),Windows_NT)
Get-ChildItem -Recurse -Directory -Include bin,obj | Remove-Item -Recurse -Force
$(RM) TestResults
else
find . -type d \( -name bin -o -name obj \) | xargs rm -rf
$(RM) TestResults
endif
## nuke: Absolute zero -- destroy both docker stacks, their volumes, their images,
## AND all local build artifacts. Next `make start-docker` rebuilds from scratch.
nuke: clean
@echo "==> NUKE: full docker stack + db-only stack + volumes + images"
-cd docker && docker compose down -v --rmi all --remove-orphans 2>/dev/null
-docker compose -f $(DB_COMPOSE_FILE) down -v --rmi all --remove-orphans 2>/dev/null
-docker rm -f $$(docker ps -aq --filter "name=healthcaresamples") 2>/dev/null
-docker rm -f $$(docker ps -aq --filter "name=docker-app") 2>/dev/null
-docker rm -f $$(docker ps -aq --filter "name=docker-dashboard") 2>/dev/null
-docker rm -f $$(docker ps -aq --filter "name=docker-db") 2>/dev/null
-docker volume rm -f docker_db-data 2>/dev/null
-docker volume rm -f healthcaresamples_db-data 2>/dev/null
-docker image rm -f docker-app docker-dashboard 2>/dev/null
@echo "==> Nuked. Run 'make start-docker' for a cold start."
## ci: fmt-check + lint + test + build (full CI simulation)
ci: _reclaim-ports _reclaim-e2e
$(MAKE) fmt CHECK=1
$(MAKE) lint
$(MAKE) test
$(MAKE) build
## setup: Post-create dev environment setup
setup:
@echo "==> Setting up development environment..."
dotnet tool restore
dotnet restore
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile
@echo "==> Setup complete. Run 'make ci' to validate."
# =============================================================================
# DEV DATABASE (Postgres via docker compose)
# =============================================================================
## db-up: Start Postgres (pgvector) container in background
db-up:
@echo "==> Starting Postgres..."
DB_PASSWORD=$(DB_PASSWORD) docker compose -f $(DB_COMPOSE_FILE) up -d
@$(MAKE) db-wait
## db-down: Stop and remove Postgres container (preserves volume)
db-down:
@echo "==> Stopping Postgres..."
docker compose -f $(DB_COMPOSE_FILE) down
## db-reset: Destroy DB volume and recreate from init scripts
db-reset:
@echo "==> Resetting Postgres (DESTRUCTIVE)..."
docker compose -f $(DB_COMPOSE_FILE) down -v
DB_PASSWORD=$(DB_PASSWORD) docker compose -f $(DB_COMPOSE_FILE) up -d
@$(MAKE) db-wait
## db-wait: Block until Postgres healthcheck reports healthy
db-wait:
@echo "==> Waiting for Postgres to be ready..."
@for i in $$(seq 1 60); do \
STATUS=$$(docker inspect --format '{{.State.Health.Status}}' healthcaresamples-db 2>/dev/null || echo "missing"); \
if [ "$$STATUS" = "healthy" ]; then echo "Postgres ready"; exit 0; fi; \
sleep 1; \
done; \
echo "FAIL: Postgres did not become healthy"; \
docker logs healthcaresamples-db 2>&1 | tail -50; \
exit 1
## db-migrate: Ensure DB is up and apply YAML schemas via DataProviderMigrate to all four databases
db-migrate: db-up
@echo "==> Migrating Postgres schemas..."
dotnet DataProviderMigrate --schema Gatekeeper/Gatekeeper.Api/gatekeeper-schema.yaml \
--output "$(PG_BASE_URL);Database=gatekeeper" --provider postgres
dotnet DataProviderMigrate --schema Clinical/Clinical.Api/clinical-schema.yaml \
--output "$(PG_BASE_URL);Database=clinical" --provider postgres
dotnet DataProviderMigrate --schema Scheduling/Scheduling.Api/scheduling-schema.yaml \
--output "$(PG_BASE_URL);Database=scheduling" --provider postgres
dotnet DataProviderMigrate --schema ICD10/ICD10.Api/icd10-schema.yaml \
--output "$(PG_BASE_URL);Database=icd10" --provider postgres
@echo "==> Reassigning table ownership and granting privileges to service users..."
@for db in gatekeeper clinical scheduling icd10; do \
PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -p $(DB_PORT) -U postgres -d $$db -q \
-c "REASSIGN OWNED BY postgres TO $$db;" \
> /dev/null 2>&1 || true; \
done
# =============================================================================
# RUN THE STACK
# =============================================================================
# Ports the stack binds on the host. Used by _reclaim-ports to forcibly
# evict any stale containers or host processes holding them before we bring
# the compose stack up.
STACK_PORTS := 5002 5080 5001 5090 8000 5173 5432
## _reclaim-ports: Kill anything (docker containers or host procs) bound to STACK_PORTS
_reclaim-ports:
@echo "==> Reclaiming stack ports: $(STACK_PORTS)"
@for port in $(STACK_PORTS); do \
cids=$$(docker ps -aq --filter "publish=$$port" 2>/dev/null); \
if [ -n "$$cids" ]; then \
echo " [:$$port] killing containers: $$cids"; \
docker rm -f $$cids >/dev/null 2>&1 || true; \
fi; \
pids=$$(lsof -nP -iTCP:$$port -sTCP:LISTEN -t 2>/dev/null || true); \
if [ -n "$$pids" ]; then \
echo " [:$$port] killing host PIDs: $$pids"; \
kill -9 $$pids 2>/dev/null || true; \
fi; \
done
## _reclaim-e2e: Kill stale dashboard Playwright runners and remove stale artifacts
_reclaim-e2e:
@echo "==> Reclaiming dashboard E2E runners and artifacts"
@pids=$$(ps -axo pid=,command= | awk '(/pnpm exec playwright test/ || /@playwright\/test\/cli\.js test/ || /Dashboard\/dashboard-ts\/node_modules\/.*playwright.*process\.js/) && !/awk/ {print $$1}' | sort -u); \
if [ -n "$$pids" ]; then \
echo " killing Playwright PIDs: $$pids"; \
kill -9 $$pids 2>/dev/null || true; \
fi
@rm -rf Dashboard/dashboard-ts/test-results Dashboard/dashboard-ts/playwright-report
## _ensure-embedding-service: Start the real ICD-10 embedding service required by RAG E2E tests
_ensure-embedding-service:
@echo "==> Ensuring ICD-10 embedding service is healthy on :8000"
@if curl -sf http://localhost:8000/health >/dev/null 2>&1; then \
echo "Embedding service ready"; \
else \
echo "Starting embedding service container..."; \
cd ICD10/embedding-service && docker compose up -d --build; \
for i in $$(seq 1 120); do \
if curl -sf http://localhost:8000/health >/dev/null 2>&1; then \
echo "Embedding service ready"; \
exit 0; \
fi; \
sleep 2; \
done; \
echo "FAIL: embedding service did not become healthy on :8000"; \
cd ICD10/embedding-service && docker compose logs --tail=120; \
exit 1; \
fi
## start-stack: Build and start the app + dashboard services from docker-compose.yml.
## Reclaims stale default-port listeners before starting only the app
## (all APIs + embedding) and dashboard containers, then waits for health endpoints.
start-stack: _reclaim-ports db-migrate
@echo "==> Starting app + dashboard via docker compose (forced rebuild)..."
DB_PASSWORD=$(DB_PASSWORD) docker compose -f docker/docker-compose.yml -f docker/docker-compose.ci.yml up -d --build --no-deps app dashboard
@echo "==> Waiting for all services to respond (any HTTP response = ready)..."
@for url in \
http://localhost:5002/health \
http://localhost:5080/health \
http://localhost:5001/health \
http://localhost:5090/health \
http://localhost:8000/health; do \
echo " Waiting for $$url..."; \
for i in $$(seq 1 90); do \
if curl -sf "$$url" > /dev/null 2>&1; then \
echo " $$url ready"; \
break; \
fi; \
if [ "$$i" = "90" ]; then \
echo "FAIL: $$url did not become healthy after 90 attempts"; \
docker compose -f docker/docker-compose.yml -f docker/docker-compose.ci.yml logs app; \
exit 1; \
fi; \
sleep 2; \
done; \
done
@echo "==> Full stack ready."
## resume-docker: Start the existing docker stack without rebuilding or reclaiming ports.
## Use this to bring containers back up after they were stopped. No builds, no
## port-killing, no data loss -- just `docker compose up -d` on whatever is there.
resume-docker:
@echo "==> Resuming docker stack (no rebuild)..."
cd docker && docker compose up -d
## dashboard-ts-dev: Run the new TypeScript dashboard dev server (vite)
dashboard-ts-dev:
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm dev
## dashboard-ts-build: Build the new TypeScript dashboard SPA (vite)
dashboard-ts-build:
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm build
## dashboard-ts-lint: Typecheck, lint, and format-check the new TypeScript dashboard
dashboard-ts-lint:
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm typecheck && pnpm lint && pnpm format
## dashboard-ts-test: Run unit tests with coverage for the new TypeScript dashboard
dashboard-ts-test:
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm test
## dashboard-ts-e2e: Rebuild/start default stack, then run Playwright e2e tests
## Set E2E_CLINICAL_URL, E2E_SCHEDULING_URL, E2E_GATEKEEPER_URL, E2E_ICD10_URL, E2E_DASHBOARD_URL
## to override the default localhost endpoints.
dashboard-ts-e2e: _reclaim-e2e start-stack
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm e2e
## dashboard-ts-check: Typecheck + lint + test + build for the new TypeScript dashboard
dashboard-ts-check:
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent && pnpm check
## dashboard-ts: Alias for dashboard-ts-check
dashboard-ts: dashboard-ts-check
## deploy-dashboard: Rebuild ONLY the dashboard image and restart ONLY the dashboard
## container. Leaves db/app containers untouched. Use this for CSS/HTML/JS changes
## when the full stack is already running.
deploy-dashboard:
@echo "==> Building TypeScript dashboard image..."
cd docker && docker compose up -d --build --no-deps dashboard
@echo "==> Dashboard redeployed at http://localhost:5173"
## start-docker: Start the full docker compose stack
## Always rebuilds images so Dockerfile / start-services.sh changes can't be masked
## by a stale cached image.
start-docker: _reclaim-ports
@echo "==> Starting docker stack (forced rebuild)..."
cd docker && docker compose up --build
# Embedded runner for the local dev stack. Inlined as a `define` block so the
# orchestration (background processes, trap-based cleanup, log prefixing) runs
# in a single shell — Make's default one-shell-per-line model can't express it.
define START_LOCAL_RUNNER
set -e
PIDS=()
cleanup() {
echo ""
echo "Shutting down..."
for pid in "$${PIDS[@]}"; do
kill "$$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
echo "All services stopped."
}
trap cleanup EXIT INT TERM
DB_PASS="$${DB_PASSWORD:-changeme}"
VENV_DIR="ICD10/.venv"
EMBED_DIR="ICD10/embedding-service"
echo "Starting Embedding Service on :8000 (model loading may take a moment)..."
"$$VENV_DIR/bin/python" -m uvicorn main:app --host 0.0.0.0 --port 8000 \
--app-dir "$$EMBED_DIR" 2>&1 | sed 's/^/ [embedding] /' &
PIDS+=($$!)
populate_icd10() {
local CONN_STR="Host=localhost;Database=icd10;Username=icd10;Password=$$DB_PASS"
local SCRIPTS_DIR="ICD10/scripts/CreateDb"
echo " [icd10-import] Waiting for ICD10 API..."
for i in $$(seq 1 60); do
if curl -sf http://localhost:5090/health >/dev/null 2>&1; then
echo " [icd10-import] ICD10 API is up."
break
fi
sleep 2
done
echo " [icd10-import] Waiting for embedding service..."
for i in $$(seq 1 120); do
if curl -sf http://localhost:8000/health >/dev/null 2>&1; then
echo " [icd10-import] Embedding service ready."
break
fi
sleep 2
done
local CHAPTERS
CHAPTERS=$$(curl -sf http://localhost:5090/api/icd10/chapters 2>/dev/null || echo "[]")
if [ "$$CHAPTERS" = "[]" ] || [ "$$CHAPTERS" = "" ]; then
echo " [icd10-import] No ICD10 data found. Running full Postgres import..."
EMBEDDING_SERVICE_URL="http://localhost:8000" \
"$$VENV_DIR/bin/python" "$$SCRIPTS_DIR/import_postgres.py" \
--connection-string "$$CONN_STR" \
|| echo " [icd10-import] Import encountered errors (check logs above)"
else
echo " [icd10-import] ICD10 codes already populated. Generating missing embeddings..."
EMBEDDING_SERVICE_URL="http://localhost:8000" \
"$$VENV_DIR/bin/python" "$$SCRIPTS_DIR/import_postgres.py" \
--connection-string "$$CONN_STR" --embeddings-only \
|| echo " [icd10-import] Embedding generation encountered errors"
fi
}
echo "Starting Gatekeeper.Api on :5002..."
ConnectionStrings__Postgres="Host=localhost;Database=gatekeeper;Username=gatekeeper;Password=$$DB_PASS" \
dotnet run --no-build --project Gatekeeper/Gatekeeper.Api/Gatekeeper.Api.csproj --no-launch-profile \
--urls "http://localhost:5002" 2>&1 | sed 's/^/ [gatekeeper] /' &
PIDS+=($$!)
echo "Starting Clinical.Api on :5080..."
ConnectionStrings__Postgres="Host=localhost;Database=clinical;Username=clinical;Password=$$DB_PASS" \
dotnet run --no-build --project Clinical/Clinical.Api/Clinical.Api.csproj --no-launch-profile \
--urls "http://localhost:5080" 2>&1 | sed 's/^/ [clinical] /' &
PIDS+=($$!)
echo "Starting Scheduling.Api on :5001..."
ConnectionStrings__Postgres="Host=localhost;Database=scheduling;Username=scheduling;Password=$$DB_PASS" \
dotnet run --no-build --project Scheduling/Scheduling.Api/Scheduling.Api.csproj --no-launch-profile \
--urls "http://localhost:5001" 2>&1 | sed 's/^/ [scheduling] /' &
PIDS+=($$!)
echo "Starting ICD10.Api on :5090..."
ConnectionStrings__Postgres="Host=localhost;Database=icd10;Username=icd10;Password=$$DB_PASS" \
dotnet run --no-build --project ICD10/ICD10.Api/ICD10.Api.csproj --no-launch-profile \
--urls "http://localhost:5090" 2>&1 | sed 's/^/ [icd10] /' &
PIDS+=($$!)
echo "Starting Dashboard on :5173..."
cd Dashboard/dashboard-ts && pnpm dev --host 0.0.0.0 2>&1 | sed 's/^/ [dashboard] /' &
PIDS+=($$!)
populate_icd10 &
PIDS+=($$!)
echo ""
echo "════════════════════════════════════════"
echo " Gatekeeper: http://localhost:5002"
echo " Clinical: http://localhost:5080"
echo " Scheduling: http://localhost:5001"
echo " ICD10: http://localhost:5090"
echo " Embedding: http://localhost:8000"
echo " Dashboard: http://localhost:5173"
echo "════════════════════════════════════════"
echo " Press Ctrl+C to stop all services"
echo ""
wait
endef
export START_LOCAL_RUNNER
## start-local: Run all APIs locally against the docker Postgres dev DB
## Builds API projects, installs dashboard packages, then runs everything in
## the foreground with prefixed log output. Ctrl+C cleans up all children.
start-local: _reclaim-ports db-up
@echo "==> Setting up Python environment..."
@if [ ! -d ICD10/.venv ]; then python3 -m venv ICD10/.venv; fi
@ICD10/.venv/bin/pip install -q -r ICD10/embedding-service/requirements.txt psycopg2-binary click requests
cd Dashboard/dashboard-ts && pnpm install --frozen-lockfile --silent
@echo "==> Building all projects..."
dotnet build Gatekeeper/Gatekeeper.Api/Gatekeeper.Api.csproj --nologo -v q
dotnet build Clinical/Clinical.Api/Clinical.Api.csproj --nologo -v q
dotnet build Scheduling/Scheduling.Api/Scheduling.Api.csproj --nologo -v q
dotnet build ICD10/ICD10.Api/ICD10.Api.csproj --nologo -v q
@bash -c "$$START_LOCAL_RUNNER"