From efdae32ae1364a287dd22ca15dcd97b3ac427351 Mon Sep 17 00:00:00 2001 From: john kevin asprec Date: Mon, 17 Aug 2026 22:27:25 +0800 Subject: [PATCH] fix: serve the website dist so Coolify answers GET / Nixpacks built Vite but had no production start command, so Coolify returned Cannot GET /. Co-authored-by: Cursor --- AGENTS.md | 2 +- CLAUDE.md | 1 + docs/EVALS.md | 10 +++- docs/HANDOFF.md | 10 ++-- docs/LESSONS_LEARNED.md | 10 +++- docs/MEMORY.md | 4 +- docs/PROGRESS.md | 28 +++++++----- package.json | 1 + packages/website/.dockerignore | 3 ++ packages/website/Dockerfile | 12 +++++ packages/website/README.md | 48 +++++++++++++------ packages/website/nginx.conf | 16 +++++++ packages/website/nixpacks.toml | 3 ++ packages/website/package.json | 1 + packages/website/server.mjs | 81 +++++++++++++++++++++++++++++++++ packages/website/vite.config.ts | 1 - 16 files changed, 193 insertions(+), 38 deletions(-) create mode 100644 packages/website/.dockerignore create mode 100644 packages/website/Dockerfile create mode 100644 packages/website/nginx.conf create mode 100644 packages/website/server.mjs diff --git a/AGENTS.md b/AGENTS.md index 5c787d7..acc6249 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -199,7 +199,7 @@ When the user corrects you, a test or review proves a mistake, or you discover a ## Lessons -- *(Add one-line prevention rules here as mistakes are verified.)* +- Website Coolify deploys must `npm start` (`server.mjs`) or the Dockerfile; Vite preview is not production. ## Memory protocol diff --git a/CLAUDE.md b/CLAUDE.md index a9705a0..8c6b3c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -336,6 +336,7 @@ Keep rules general, evidence-backed, and under 20 words. Never add secrets, pers - Bump `version` in `package.json` only (+ lockfile); the manifest derives it via `pkg.version`. - Style inline; Tailwind classes do nothing until PostCSS is wired. - Verify CHANGELOG entries against code, not commit subjects or handoff summaries. +- Website Coolify deploys must `npm start` (`server.mjs`) or the Dockerfile; Vite preview is not production. ## Memory protocol diff --git a/docs/EVALS.md b/docs/EVALS.md index 8576a3e..f5bf6b8 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -4,7 +4,15 @@ This file contains small, repeatable checks derived from verified failures. Pref ## Active failure-derived checks -_No failure-derived checks yet._ +### E-2026-08-17-01 — Website production server answers GET / + +- **Prevents:** L-2026-08-17-01 (`Cannot GET /` on Coolify) +- **Type:** command +- **How to run:** `npm run website:build`; then from `packages/website` `PORT=34567 node server.mjs` and GET `http://127.0.0.1:34567/` +- **Pass condition:** HTTP 200 and body contains `LexAI`; `/healthz` returns `ok` +- **Failure signal:** `Cannot GET /`, connection refused, or 500 `dist/ missing` +- **Cost:** fast +- **Last verified:** 2026-08-17 pass (status 200, title present) ## Eval template diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 8a35bad..bc97f00 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -4,8 +4,8 @@ ## Current state -- **Outcome:** `packages/website` now uses the four-page structure: Home, Docs, Contribute, Community. -- **Home:** hero + features + Getting started (Chrome / VS Code / CLI) + privacy. -- **Routes:** `/` `/docs` `/contribute` `/community` (HashRouter). -- **Verified:** `website:typecheck` + `website:build` passed. -- **Next smallest action:** Deploy `packages/website` on Coolify (base `/packages/website`, static, publish `/dist`). +- **Outcome:** Coolify `Cannot GET /` fixed — website now has a production static server and a Dockerfile. +- **Cause:** Nixpacks built Vite `dist/` but had no `start` command; README also told Coolify to publish `/dist` (absolute empty path). +- **Shipped:** `packages/website/server.mjs` (`npm start` on `0.0.0.0:$PORT`); `Dockerfile` + `nginx.conf`; `nixpacks.toml` `[start]`; Vite `base` is `/`. +- **Verified:** `website:typecheck` + `website:build`; local `node server.mjs` → GET `/` 200 (LexAI HTML), `/healthz` 200, JS asset `text/javascript`. +- **Next smallest action:** Redeploy the Coolify app with Base Directory `packages/website` (no leading slash), static site **off**, Dockerfile port 80 or Nixpacks port 3000. diff --git a/docs/LESSONS_LEARNED.md b/docs/LESSONS_LEARNED.md index db17f3e..1f0bdaa 100644 --- a/docs/LESSONS_LEARNED.md +++ b/docs/LESSONS_LEARNED.md @@ -6,7 +6,15 @@ This is the project’s shared, evidence-backed memory of mistakes worth prevent `AGENTS.md` → `## Lessons` is the canonical active rule list loaded every session. Keep supporting evidence here; mirror an active rule here only when its detail is useful for maintenance. Keep at most 12 active rules, each short and imperative. -_No active guardrails yet._ +### L-2026-08-17-01 — Serve website `dist/` on `0.0.0.0:$PORT`; do not use Vite preview in production + +- **Status:** active +- **Trigger:** Coolify deploy of `packages/website` returned Express/Connect `Cannot GET /`. +- **Root cause / failure boundary:** No `start` script; Nixpacks built `dist/` then started a Node process that did not serve it. README Publish Directory `/dist` is an absolute empty path. +- **Prevention:** `packages/website/server.mjs` + `npm start`, or `packages/website/Dockerfile`. Coolify: Base Directory `packages/website`, static site off. +- **Evidence:** Local `node server.mjs` GET `/` → 200 with LexAI HTML; `/healthz` → 200. +- **Eval:** E-2026-08-17-01 +- **Owner / review:** lead / after first successful Coolify deploy ## Recording policy diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 932ae45..5d5bac8 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -17,7 +17,7 @@ - Style Chrome UI with inline styles (Tailwind not wired). - Prefer encrypted Chrome key path; keep plaintext `apiKey` fallback until migration. - VS Code / CLI must not import `@lib/crypto` or `@lib/messaging`. -- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`. +- Root scripts: `install:all`, `chrome:*`, `vscode:*`, `cli:*`, `website:*`. ## Environment quirks @@ -32,7 +32,7 @@ | Shared lib | `src/lib/` | | VS Code | `packages/vscode/` | | CLI | `packages/cli/` | -| Website | `packages/website/` (Vite React; Home, Docs, Contribute, Community) | +| Website | `packages/website/` (Vite React; Home, Docs, Contribute, Community). Production: `npm start` → `server.mjs`, or `Dockerfile` (nginx :80). Coolify base `packages/website`, static site off. | | OSS meta | root `README.md`, `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` | | Issue/PR templates | `.gitea/ISSUE_TEMPLATE/`, `.gitea/PULL_REQUEST_TEMPLATE.md` | | CI workflows | `.gitea/workflows/ci-{chrome,vscode,cli,website}.yml` (no deploy/release yet) | diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index b73ce25..be49449 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,28 +2,32 @@ > For the owner. What works, how to see it, and what's waiting on you — plain language. -**Updated:** 2026-08-13 · **Overall:** Chrome LexAI unchanged; VS Code LexAI v1 ready to try locally +**Updated:** 2026-08-17 · **Overall:** Website Coolify 404 is fixed in the repo; needs a redeploy ## What works now -- Chrome extension (existing): select on any page → AI action → Replace/Copy -- VS Code extension (new): select in editor → LexAI context menu / Command Palette → selection replaced -- Shared LLM core (`src/lib`) used by both; VS Code key lives in Secret Storage +- Chrome extension: select on any page → AI action → Replace/Copy +- VS Code extension: select in editor → LexAI context menu / Command Palette → selection replaced +- Website (`packages/website`): Home, Docs, Contribute, Community — production server answers `GET /` (this was the Coolify `Cannot GET /` failure) -## See it yourself (VS Code) — about five minutes +## See it yourself — website on Coolify -1. From repo root: `npm run vscode:install` then `npm run vscode:build` -2. Open `packages/vscode` in VS Code/Cursor → Run and Debug → **Run LexAI Extension** (F5), or `npm run vscode:package` and Install from VSIX -3. Command Palette → **LexAI: Set API Key** → pick provider → paste key -4. Select ≥10 characters in an editor → right-click → **LexAI** → Fix Grammar (or another action) +1. Pull/push this change, then in Coolify set **Base Directory** to `packages/website` (no leading slash). +2. Turn **Is it a static site?** **off**. +3. Either **Build Pack: Dockerfile** and port **80**, or **Nixpacks** and port **3000**. +4. Redeploy, then open the domain — you should see the LexAI home page, not `Cannot GET /`. +5. Docs live at `https://your-domain/#/docs`. + +Local check: `npm run website:build` then `npm run website:start` and visit http://127.0.0.1:3000/ ## Waiting on you | # | Decision | Options (recommended bold) | What it unblocks | | --- | --- | --- | --- | -| 1 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch for builders/critics | -| 2 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet | -| 3 | Publish VS Code extension to Marketplace? | **Stay local for now** / set publisher + publish | public install | +| 1 | Redeploy the Coolify website with the settings above | **Dockerfile + port 80** / Nixpacks + port 3000 | public LexAI site | +| 2 | Confirm Cursor mid/strong model picker IDs | **Keep written IDs** / send exact picker strings | reliable Task dispatch | +| 3 | Grammarly reference screenshots? | **Later** / capture into `docs/reference/` | Chrome UX gauntlet | +| 4 | Publish VS Code extension to Marketplace? | **Stay local for now** / set publisher + publish | public install | ## Next up — proceeds without you diff --git a/package.json b/package.json index fb0ee0a..ac29f27 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "website:dev": "npm run dev --prefix packages/website", "website:build": "npm run build --prefix packages/website", "website:preview": "npm run preview --prefix packages/website", + "website:start": "npm start --prefix packages/website", "website:typecheck": "npm run typecheck --prefix packages/website", "install:all": "npm run chrome:install && npm run vscode:install && npm run cli:install && npm run website:install", "typecheck:all": "npm run chrome:typecheck && npm run vscode:typecheck && npm run cli:typecheck && npm run website:typecheck", diff --git a/packages/website/.dockerignore b/packages/website/.dockerignore new file mode 100644 index 0000000..25fb987 --- /dev/null +++ b/packages/website/.dockerignore @@ -0,0 +1,3 @@ +node_modules +dist +*.md diff --git a/packages/website/Dockerfile b/packages/website/Dockerfile new file mode 100644 index 0000000..1dad470 --- /dev/null +++ b/packages/website/Dockerfile @@ -0,0 +1,12 @@ +# syntax=docker/dockerfile:1 +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 diff --git a/packages/website/README.md b/packages/website/README.md index 2e97908..2daee74 100644 --- a/packages/website/README.md +++ b/packages/website/README.md @@ -24,27 +24,45 @@ npm run website:build # → packages/website/dist/ ``` -Preview production build: `npm run website:preview`. +Preview production build locally: `npm run website:preview` (Vite, localhost only). -Uses HashRouter so the site works on static hosts without server-side rewrite rules. +Production start (what Coolify should run): `npm start` — serves `dist/` on `0.0.0.0:$PORT` (default 3000). + +Uses HashRouter so deep links work even without SPA rewrite rules. The production server still maps unknown paths to `index.html`. ## Deploy on Coolify -This package is a **static** Vite build (`dist/`). No LexAI API keys or env vars. +This package is a static Vite build. No LexAI API keys or env vars. Do not add `LEXAI_API_KEY`. -1. In Coolify: **+ New** → **Resource** → **Private Repository (with deploy key)** (Gitea). -2. Repo SSH URL, for example `git@git.juankibin.space:kibin/LexAI.git`. If Gitea SSH is not on port 22, use `ssh://git@git.juankibin.space:PORT/kibin/LexAI.git` (Coolify sometimes needs `ssh:///` with three slashes). +`Cannot GET /` means Coolify started a Node process that is not serving `dist/` (no `start` command, or "static site" pointed at the wrong folder). Fix: use one of the two setups below, then **Redeploy**. + +**Required on either setup** + +1. **+ New** → **Resource** → **Private Repository (with deploy key)** (Gitea). +2. Repo SSH URL, for example `git@git.juankibin.space:kibin/LexAI.git`. If Gitea SSH is not on port 22, use `ssh://git@git.juankibin.space:PORT/kibin/LexAI.git`. 3. Add Coolify’s public deploy key to the Gitea repo: **Settings → Deploy / Access Keys** (read-only). 4. Branch: `dev` or `main`. -5. **Build Pack:** Nixpacks. -6. **Base Directory:** `/packages/website` — required. Root `npm run build` builds Chrome, not this site. -7. Enable **Is it a static site?** -8. **Publish Directory:** `/dist` -9. **Install command** (if the build says `vite: not found`): `npm ci` (do not omit devDependencies — Vite lives there). -10. Optional env: `NIXPACKS_NODE_VERSION=22` -11. Optional **Watch Paths:** `packages/website` so Chrome/VS Code/CLI commits do not redeploy. -12. Set the domain, **Deploy**, then open `https://your-domain/#/docs`. +5. **Base Directory:** `packages/website` — no leading slash. Root `npm run build` builds Chrome, not this site. +6. Optional **Watch Paths:** `packages/website`. +7. Set the domain, deploy, open `https://your-domain/` (docs: `https://your-domain/#/docs`). + +### Option A — Dockerfile (recommended) + +1. **Build Pack:** Dockerfile. +2. Leave **Is it a static site?** **off**. +3. **Ports Exposes:** `80`. +4. If Coolify asks for a Dockerfile path: `packages/website/Dockerfile` (or omit it when Base Directory is already `packages/website`). + +### Option B — Nixpacks + +1. **Build Pack:** Nixpacks. +2. Leave **Is it a static site?** **off**. Nixpacks runs `npm run build` then `node server.mjs`. +3. **Start command** (if the field is shown): `npm start` or `node server.mjs`. +4. **Ports Exposes:** `3000`. +5. Optional env: `NIXPACKS_NODE_VERSION=22`. Do not set `NODE_ENV=production` at **build** time — Vite is a devDependency and `npm ci` would skip it. + +Do **not** tick "Is it a static site?" with Publish Directory `/dist`. The leading slash is an absolute path (`/dist` on the container), which is empty, and Coolify then answers `Cannot GET /`. + +If you already created the resource: uncheck static site, set Base Directory and port as above, then Redeploy (no need to recreate the app). Auto-deploy: Coolify resource → **Webhooks** → copy the URL + secret into Gitea **Settings → Webhooks** (push events). - -Do not add `LEXAI_API_KEY` or any provider key to this Coolify app. diff --git a/packages/website/nginx.conf b/packages/website/nginx.conf new file mode 100644 index 0000000..e9e06c8 --- /dev/null +++ b/packages/website/nginx.conf @@ -0,0 +1,16 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location = /healthz { + access_log off; + add_header Content-Type text/plain; + return 200 'ok'; + } + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/packages/website/nixpacks.toml b/packages/website/nixpacks.toml index a99c45c..07279af 100644 --- a/packages/website/nixpacks.toml +++ b/packages/website/nixpacks.toml @@ -6,3 +6,6 @@ cmds = ["npm ci"] [phases.build] cmds = ["npm run build"] + +[start] +cmd = "node server.mjs" diff --git a/packages/website/package.json b/packages/website/package.json index 7bd220e..8cc7fdd 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -10,6 +10,7 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "preview": "vite preview", + "start": "node server.mjs", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/website/server.mjs b/packages/website/server.mjs new file mode 100644 index 0000000..118f064 --- /dev/null +++ b/packages/website/server.mjs @@ -0,0 +1,81 @@ +import { createReadStream, existsSync, statSync } from 'node:fs'; +import { createServer } from 'node:http'; +import { extname, join, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(fileURLToPath(new URL('./dist', import.meta.url))); +const port = Number(process.env.PORT) || 3000; +const host = process.env.HOST || '0.0.0.0'; + +const MIME = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.txt': 'text/plain; charset=utf-8', + '.webp': 'image/webp', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +function fileUnderRoot(urlPath) { + const decoded = decodeURIComponent((urlPath ?? '/').split('?')[0] || '/'); + const relative = decoded.replace(/^\/+/, '').replaceAll('/', sep); + const resolved = resolve(root, relative); + const prefix = root.endsWith(sep) ? root : root + sep; + if (resolved !== root && !resolved.startsWith(prefix)) return null; + return resolved; +} + +function send(res, status, body, type = 'text/plain; charset=utf-8') { + res.writeHead(status, { 'Content-Type': type, 'Cache-Control': 'no-cache' }); + res.end(body); +} + +const server = createServer((req, res) => { + if (req.method === 'GET' && (req.url === '/healthz' || req.url?.startsWith('/healthz?'))) { + send(res, 200, 'ok'); + return; + } + + if (req.method !== 'GET' && req.method !== 'HEAD') { + send(res, 405, 'Method not allowed'); + return; + } + + const candidate = fileUnderRoot(req.url ?? '/'); + if (!candidate) { + send(res, 400, 'Bad request'); + return; + } + + let file = candidate; + if (!existsSync(file) || statSync(file).isDirectory()) { + file = join(root, 'index.html'); + } + if (!existsSync(file)) { + send(res, 500, 'dist/ missing — run npm run build before start'); + return; + } + + const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream'; + res.writeHead(200, { 'Content-Type': type }); + if (req.method === 'HEAD') { + res.end(); + return; + } + createReadStream(file).pipe(res); +}); + +if (!existsSync(join(root, 'index.html'))) { + console.error('dist/index.html missing — run npm run build before start'); + process.exit(1); +} + +server.listen(port, host, () => { + console.log(`LexAI website listening on http://${host}:${port}`); +}); diff --git a/packages/website/vite.config.ts b/packages/website/vite.config.ts index 01fbbfd..0466183 100644 --- a/packages/website/vite.config.ts +++ b/packages/website/vite.config.ts @@ -3,5 +3,4 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], - base: './', }); -- 2.43.0