diff --git a/config/local-nginx.conf b/config/local-nginx.conf index c231cf8a7..c8e32e611 100644 --- a/config/local-nginx.conf +++ b/config/local-nginx.conf @@ -1,6 +1,7 @@ # Local development nginx config for quarto-hub # Mirrors production setup but without TLS -# NOTE: This file is loaded dynamically by scripts/local-prod-nginx.sh with DIST_PATH substitution +# NOTE: This file is loaded dynamically by scripts/local-prod-nginx.sh with +# DIST_PATH and NGINX_PORT substitution # Map WebSocket upgrade header to connection type map $http_upgrade $connection_upgrade { @@ -26,7 +27,7 @@ server { } server { - listen 8080; + listen NGINX_PORT; server_name localhost; # Security headers (same as production) diff --git a/hub-client/README.md b/hub-client/README.md index 8adbfbe99..1294f6145 100644 --- a/hub-client/README.md +++ b/hub-client/README.md @@ -119,7 +119,7 @@ npm run local-prod -- --port 9000 Two modes available: - `local-prod` - Node.js proxy (fast, recommended) -- `local-prod:nginx` - nginx in Docker (tests actual nginx config) +- `local-prod:nginx` - local nginx (tests actual nginx config); takes the same `--port` option See [`../scripts/README.md`](../scripts/README.md) for details and troubleshooting. diff --git a/hub-client/changelog.md b/hub-client/changelog.md index 4302cd45e..a7680441b 100644 --- a/hub-client/changelog.md +++ b/hub-client/changelog.md @@ -23,6 +23,10 @@ WASM rebuild is needed for a changelog-only edit. --> +### 2026-08-27 + +- [`e279bc9c9`](https://github.com/quarto-dev/q2/commits/e279bc9c9): `local-prod:nginx` now accepts `--port` like plain `local-prod`, and starts correctly when `OIDC_CLIENT_ID` in your shell enables hub auth — the readiness check previously failed on the hub's 401 even though the hub was up. + ### 2026-08-26 - [`ca959a67`](https://github.com/quarto-dev/q2/commits/ca959a67): Comment bubbles now render comments richly — emphasis, inline code, quotes, links (external links open in a new tab, document links navigate the preview), and images (clamped to the bubble). Content that has no sensible bubble form, like nested editorial marks, shows an explicit "unsupported content" chip instead of rendering incorrectly. diff --git a/scripts/README.md b/scripts/README.md index e9e955e7e..822ceb2b4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -7,7 +7,7 @@ This directory contains automation scripts for the Kyoto project. Two modes available: **`local-prod.sh`** (Node.js proxy) - Quick setup, no dependencies -**`local-prod-nginx.sh`** (nginx in Docker) - Test actual nginx config +**`local-prod-nginx.sh`** (native nginx) - Test actual nginx config ### What it does @@ -78,6 +78,10 @@ Fast setup, tests WebSocket proxying and routing. Good for 90% of development. ```bash cd hub-client npm run local-prod:nginx + +# Custom port works here too (pass it to both build and run): +npm run build:local-prod -- --port 9000 +npm run local-prod:nginx -- --port 9000 ``` Tests the actual nginx configuration from production. Use when: @@ -85,6 +89,11 @@ Tests the actual nginx configuration from production. Use when: - Validating gzip compression, security headers - Debugging nginx-specific issues +If `OIDC_CLIENT_ID` is set in your shell, the local hub picks it up and runs +with auth enabled (the script logs "Auth enabled via OIDC_CLIENT_ID from the +environment"). The readiness check treats the resulting 401 as "up". Unset +the variable if you want the default no-auth local setup. + **Architecture differences:** - **Node.js mode:** Browser → Node.js proxy (port 8080) → hub (port 3000) - **Nginx mode:** Browser → nginx (Docker, port 8080) → hub (host, port 3000) diff --git a/scripts/local-prod-nginx.sh b/scripts/local-prod-nginx.sh index 6919512a1..af58cdb94 100755 --- a/scripts/local-prod-nginx.sh +++ b/scripts/local-prod-nginx.sh @@ -9,7 +9,7 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" HUB_CLIENT_DIR="$PROJECT_ROOT/hub-client" DATA_DIR="$PROJECT_ROOT/.local-prod-data" HUB_PORT=3000 -NGINX_PORT=8080 +NGINX_PORT="$(node "$SCRIPT_DIR/local-prod-port.mjs" "$@")" Q2_SANDBOXED_PREVIEW_PORT=8081 # Color output @@ -115,7 +115,7 @@ mkdir -p "$DATA_DIR" # Generate nginx config with absolute paths log_step "Generating nginx configuration..." DIST_PATH_ABSOLUTE="$HUB_CLIENT_DIR/dist" -sed "s|DIST_PATH|$DIST_PATH_ABSOLUTE|g" "$PROJECT_ROOT/config/local-nginx.conf" > "$DATA_DIR/nginx.conf" +sed "s|DIST_PATH|$DIST_PATH_ABSOLUTE|g; s|NGINX_PORT|$NGINX_PORT|g" "$PROJECT_ROOT/config/local-nginx.conf" > "$DATA_DIR/nginx.conf" # Add required nginx directives (pid, error_log, events, http wrapper) cat > "$DATA_DIR/nginx.conf.tmp" << EOF @@ -176,10 +176,15 @@ if ! kill -0 "$HUB_PID" 2>/dev/null; then exit 1 fi -# Wait for hub health endpoint +# Wait for hub health endpoint. Readiness = any HTTP response, not a 2xx: +# with auth enabled (OIDC_CLIENT_ID set in the environment), /health +# requires credentials and returns 401 even though the hub is up. for i in {1..10}; do - if curl -f http://127.0.0.1:$HUB_PORT/health >/dev/null 2>&1; then + if curl -s -o /dev/null http://127.0.0.1:$HUB_PORT/health 2>/dev/null; then log_info "Hub is ready (PID: $HUB_PID)" + if [ -n "${OIDC_CLIENT_ID:-}" ]; then + log_info "Auth enabled via OIDC_CLIENT_ID from the environment" + fi break fi if [ $i -eq 10 ]; then diff --git a/scripts/local-prod-port.test.mjs b/scripts/local-prod-port.test.mjs index d05a2abbf..04bc5e042 100644 --- a/scripts/local-prod-port.test.mjs +++ b/scripts/local-prod-port.test.mjs @@ -16,6 +16,31 @@ describe('parseLocalProdPort', () => { expect(() => parseLocalProdPort(['--port', '70000'])).toThrow(); }); + it('nginx launcher honors --port like the plain launcher', async () => { + const nginxLauncher = await readFile(new URL('./local-prod-nginx.sh', import.meta.url), 'utf8'); + const nginxConfig = await readFile(new URL('../config/local-nginx.conf', import.meta.url), 'utf8'); + + // The nginx launcher parses --port via the shared parser, same as local-prod.sh + expect(nginxLauncher).toContain('NGINX_PORT="$(node "$SCRIPT_DIR/local-prod-port.mjs" "$@")"'); + expect(nginxLauncher).not.toMatch(/^NGINX_PORT=8080$/m); + // ...and substitutes it into the generated nginx config + expect(nginxLauncher).toContain('s|NGINX_PORT|$NGINX_PORT|g'); + // The template carries the placeholder, not a hardcoded main port + expect(nginxConfig).toContain('listen NGINX_PORT;'); + expect(nginxConfig).not.toContain('listen 8080;'); + }); + + it('nginx launcher hub readiness check accepts 401 (auth-enabled setups)', async () => { + // With OIDC_CLIENT_ID in the environment the hub enables auth, and + // /health requires credentials: `curl -f` fails on the 401 even though + // the hub is up. Readiness must mean "any HTTP response", not "2xx". + const nginxLauncher = await readFile(new URL('./local-prod-nginx.sh', import.meta.url), 'utf8'); + const hubHealth = nginxLauncher.match(/curl [^\n]*\/health[^\n]*/); + expect(hubHealth).not.toBeNull(); + const healthCurl = hubHealth[0]; + expect(healthCurl).not.toContain(' -f'); + }); + it('keeps the hub proxy on port 3000', async () => { const serverScript = await readFile(new URL('./local-prod-server.mjs', import.meta.url), 'utf8'); const launcherScript = await readFile(new URL('./local-prod.sh', import.meta.url), 'utf8');