From 0a4c44cabcb768f1ca4ff3955a2e0cf8a7c85daa Mon Sep 17 00:00:00 2001 From: Mathieu Tarral Date: Tue, 8 Sep 2026 01:16:48 +0200 Subject: [PATCH] fix: serve the frontend at the domain root The production build used base '/frontend/', a sub-path that nothing serves. The container runs `serve -s dist` at the root and oswatcher-deploy's Traefik routes Host(``) to it with no path prefix, so every hashed asset reference (`/frontend/assets/*.js`) fell through to the SPA fallback and the page loaded as a blank shell. Set base '/' in all modes. Development already used it, so only the published image changes. Add a post-build smoke test (scripts/smoke-test.mjs, wired into build.yml) that runs the image and asserts what `npm run build-only` cannot see: assets load from `/assets/` with the right content type rather than the HTML fallback, the runtime API placeholder is substituted, and history-mode deep links serve the app shell. Verified: image built with production args, smoke test passes against the running container. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014WnrRvhmHcMPj6iwJ2NnzK --- .github/workflows/build.yml | 7 ++++++ CLAUDE.md | 2 +- docs/reference/architecture.md | 2 +- docs/reference/environment.md | 4 ++-- scripts/smoke-test.mjs | 41 ++++++++++++++++++++++++++++++++++ vite.config.ts | 3 ++- 6 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 scripts/smoke-test.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4382eb3..c4499ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,3 +46,10 @@ jobs: # failed with "vite: not found" while this job stayed green. - name: Docker build (production args) run: docker build --build-arg NODE_ENV=production -t frontend-ci-check . + + - name: Verify served assets and runtime configuration + run: | + docker run -d --name frontend-smoke -p 127.0.0.1:8080:8080 \ + -e VITE_OSWATCHER_API_URI=http://localhost:4000/ frontend-ci-check + trap 'docker rm -f frontend-smoke' EXIT + node scripts/smoke-test.mjs http://127.0.0.1:8080 diff --git a/CLAUDE.md b/CLAUDE.md index 508c4be..e2db78e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ public; there is no login flow and no token injection. 4. This frontend is not meant to run standalone against a self-hosted backend: point it at an `oswatcher-deploy` stack (Docker Compose orchestration for the GraphQL API, Neo4j, and MinIO). `.env` is conventionally a symlink to `../oswatcher-deploy/.env` when both repos are checked out as siblings. #### Build Configuration -- Production base path set to `/frontend/` in Vite config (assumes deployment behind `oswatcher-deploy`'s reverse proxy) +- Base path is `/` in all modes: the container and `oswatcher-deploy` serve the app at the domain root ### Documentation Structure The project includes comprehensive technical reference documentation: diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index 93810aa..3a2429b 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -493,7 +493,7 @@ Parsing and formatting utilities for PDB data: - **Environment**: Local API endpoints ### 2. Production Build -- **Base Path**: `/frontend/` (sub-path deployment) +- **Base Path**: `/` (served at the domain root) - **Optimization**: Code splitting, minification - **Assets**: Hashed filenames for caching - **Environment**: Production API endpoints diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 8cafde6..cd28c15 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -103,10 +103,10 @@ These variables are included for local development setup of the complete system ### Vite Configuration -The `vite.config.ts` sets different base paths for development vs production: +The `vite.config.ts` uses the domain root in development and production, matching the container and deployment proxy: ```typescript -base: process.env.NODE_ENV === 'production' ? '/frontend/' : '/' +base: '/' ``` ### TypeScript Environment Types diff --git a/scripts/smoke-test.mjs b/scripts/smoke-test.mjs new file mode 100644 index 0000000..1d6abb3 --- /dev/null +++ b/scripts/smoke-test.mjs @@ -0,0 +1,41 @@ +// Test the served container, including failures hidden by SPA fallback HTTP 200s. +import assert from 'node:assert/strict' +import { setTimeout as delay } from 'node:timers/promises' + +const base = process.argv[2] || 'http://127.0.0.1:8080' +const expectedApi = 'http://localhost:4000/' +let html +for (let attempt = 0; attempt < 30; attempt++) { + try { + const response = await fetch(base, { signal: AbortSignal.timeout(2000) }) + assert.equal(response.status, 200) + html = await response.text() + break + } catch (error) { + if (attempt === 29) throw error + await delay(500) + } +} + +const assets = [...html.matchAll(/(?:src|href)="([^"]+\.(?:js|css))"/g)].map(match => match[1]) +assert(assets.some(path => path.endsWith('.js')), 'Page must load JavaScript') +assert(assets.some(path => path.endsWith('.css')), 'Page must load CSS') +let configuredApi = false +for (const path of assets) { + assert(path.startsWith('/assets/'), `Asset must load from the root: ${path}`) + const response = await fetch(new URL(path, base), { signal: AbortSignal.timeout(10000) }) + assert.equal(response.status, 200, `Asset status: ${path}`) + const type = response.headers.get('content-type') || '' + assert(type.includes(path.endsWith('.js') ? 'javascript' : 'text/css'), + `Asset returned ${type}, possibly the SPA HTML fallback: ${path}`) + const content = await response.text() + assert(!content.includes('__OSW_API_URI__'), `Unresolved API placeholder: ${path}`) + configuredApi ||= content.includes(expectedApi) +} +assert(configuredApi, 'Runtime API URL must appear in the served bundle') + +// Vue Router uses history mode; direct navigation must also serve the app. +const route = await fetch(new URL('/inspect/smoke-test', base), { signal: AbortSignal.timeout(10000) }) +assert.equal(route.status, 200) +assert.equal(await route.text(), html, 'Deep links must serve the same app shell') +console.log(`PASS: ${assets.length} JS/CSS assets, runtime API URL, and history-route fallback`) diff --git a/vite.config.ts b/vite.config.ts index 637b10e..bcc2e44 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,8 @@ export default defineConfig({ vue(), vueJsx() ], - base: process.env.NODE_ENV === 'production' ? '/frontend/' : '/', + // The container and oswatcher-deploy both serve the app at the domain root. + base: '/', resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)),