Make devserver backend health check endpoint configurable, preserved in upgrades and optional.

This commit is contained in:
2026-03-07 23:55:10 +00:00
parent aac27da67c
commit 1aaf040db7
3 changed files with 60 additions and 5 deletions
+53
View File
@@ -110,6 +110,9 @@ def uv_add_packages(
# If vite == dev, dev is incremented by 100 # If vite == dev, dev is incremented by 100
DEFAULT_PORTS = (3100, 3100, 3200) DEFAULT_PORTS = (3100, 3100, 3200)
# Default health check path for devserver backend readiness check
DEFAULT_HEALTH = "/api/health?from=devserver.py"
# Marker comment indicating file can be auto-upgraded # Marker comment indicating file can be auto-upgraded
# Users should remove this line to prevent automatic updates # Users should remove this line to prevent automatic updates
UPGRADE_MARKER = "auto-upgrade@fastapi-vue-setup" UPGRADE_MARKER = "auto-upgrade@fastapi-vue-setup"
@@ -300,6 +303,29 @@ def extract_existing_ports(
return None return None
# Sentinel for "not found" in extract_existing_health
_HEALTH_NOT_FOUND = object()
def extract_existing_health(project_dir: Path) -> str | object:
"""Extract existing health path configuration from devserver.py.
Returns:
- The path string (may be empty to disable)
- _HEALTH_NOT_FOUND sentinel if not found or file doesn't exist
"""
devserver_file = project_dir / "scripts" / "devserver.py"
if not devserver_file.exists():
return _HEALTH_NOT_FOUND
content = devserver_file.read_text("UTF-8")
# Match HEALTH = "/path" or HEALTH = ""
match = re.search(r'^HEALTH\s*=\s*"([^"]*)"', content, re.MULTILINE)
if match:
return match.group(1)
return _HEALTH_NOT_FOUND
def load_template(path: str) -> str: def load_template(path: str) -> str:
"""Load a template file from the template directory.""" """Load a template file from the template directory."""
return (TEMPLATE_DIR / path).read_text("UTF-8") return (TEMPLATE_DIR / path).read_text("UTF-8")
@@ -1323,6 +1349,27 @@ def cmd_setup(args: argparse.Namespace) -> int:
f"📡 Ports: default={default_port}, vite={vite_port}, dev={dev_port} {ports_note}" f"📡 Ports: default={default_port}, vite={vite_port}, dev={dev_port} {ports_note}"
) )
# Determine health path configuration
# Priority: --health argument > existing project value > default
if args.health is not None:
health = args.health
health_note = "(--health)" if health else "(disabled via --health)"
else:
existing_health = extract_existing_health(project_dir)
if existing_health is not _HEALTH_NOT_FOUND:
# Explicitly configured (path string, possibly empty to disable)
health = existing_health
health_note = "(kept for upgrade)"
else:
# Not found - use default
health = DEFAULT_HEALTH
health_note = "(--health to override)"
if health:
print(f"🏥 Health check: {health} {health_note}")
else:
print(f"🏥 Health check: disabled {health_note}")
# Title for templates # Title for templates
project_title = module_name.replace("_", " ").title() project_title = module_name.replace("_", " ").title()
@@ -1338,6 +1385,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
"TEMPLATE_DEFAULT_PORT": str(default_port), "TEMPLATE_DEFAULT_PORT": str(default_port),
"TEMPLATE_VITE_PORT": str(vite_port), "TEMPLATE_VITE_PORT": str(vite_port),
"TEMPLATE_DEV_PORT": str(dev_port), "TEMPLATE_DEV_PORT": str(dev_port),
"TEMPLATE_HEALTH": f'"{health}"',
"ENVPREFIX": module_name.upper(), "ENVPREFIX": module_name.upper(),
"PROJECT_CLI": module_name, "PROJECT_CLI": module_name,
"MAIN_MODULE": main_module_path, "MAIN_MODULE": main_module_path,
@@ -1635,6 +1683,11 @@ Examples:
metavar="BACKEND,VITE,DEV", metavar="BACKEND,VITE,DEV",
help="Port configuration as comma-separated values (default: 3100,3100,3200)", help="Port configuration as comma-separated values (default: 3100,3100,3200)",
) )
parser.add_argument(
"--health",
metavar="PATH",
help="Health check path for devserver (default: /api/health?from=devserver.py, '' to disable)",
)
parser.add_argument( parser.add_argument(
"--dry", "--dry-run", action="store_true", help="Show what would be done" "--dry", "--dry-run", action="store_true", help="Show what would be done"
) )
+2 -1
View File
@@ -22,6 +22,7 @@ from devutil import ( # type: ignore
DEFAULT_VITE_PORT = TEMPLATE_VITE_PORT DEFAULT_VITE_PORT = TEMPLATE_VITE_PORT
DEFAULT_DEV_PORT = TEMPLATE_DEV_PORT DEFAULT_DEV_PORT = TEMPLATE_DEV_PORT
HEALTH = TEMPLATE_HEALTH
async def run_devserver( async def run_devserver(
@@ -45,7 +46,7 @@ async def run_devserver(
npm_i = await pg.spawn(*npm_install, cwd=front) npm_i = await pg.spawn(*npm_install, cwd=front)
await check_ports_free(viteurl, backurl) await check_ports_free(viteurl, backurl)
await pg.spawn(*MODULE_NAME, *(extra_args or [])) await pg.spawn(*MODULE_NAME, *(extra_args or []))
await pg.wait(npm_i, ready(backurl, path="/api/health?from=devserver.py")) await pg.wait(npm_i, ready(backurl, path=HEALTH))
await pg.spawn(*vite, cwd=front) await pg.spawn(*vite, cwd=front)
+5 -4
View File
@@ -111,18 +111,19 @@ async def check_ports_free(*urls: str) -> None:
await asyncio.gather(*[check(client, url) for url in urls]) await asyncio.gather(*[check(client, url) for url in urls])
async def ready(url: str, path: str = "") -> None: async def ready(url: str, path: str = "", max_attempts=50) -> None:
"""Wait for the server to be ready by polling an endpoint. """Wait for the server to be ready by polling an endpoint.
Use empty path to disable the check and make this return immediately.
Raises SystemExit(1) if server doesn't start in time. Raises SystemExit(1) if server doesn't start in time.
""" """
max_attempts = 50 if not path:
full_url = f"{url}{path}" return
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
for attempt in range(max_attempts): for attempt in range(max_attempts):
try: try:
await client.get(full_url, timeout=1.0) await client.get(f"{url}{path}", timeout=1.0)
logger.info("✓ Backend ready!") logger.info("✓ Backend ready!")
return return
except httpx.RequestError: except httpx.RequestError: