A lightweight Node.js app that lets users authenticate with just a username and chat with OpenAI’s ChatGPT API. Each exchange sends the five most recent messages as context so conversations stay coherent without storing huge histories. Sessions are persisted server-side so users can log out and back in without losing their thread. Conversations now persist per-username across sessions via a simple on-disk store, and the UI surfaces the running version (e.g., v1.1.0) in the header. The Dockerized deployment replaces the previous “Hello from the NAS” page and continues to serve on port 8081.
- Username-only login with session persistence (no passwords for this quick test).
- Chat UI built with vanilla HTML/CSS/JS, with Enter-to-send (Shift+Enter for a newline), streaming assistant responses, and a visible version pill.
- Server proxies requests to OpenAI, keeps the last five messages as context, and persists up to 40 messages per user to disk (backed by
data/history.json). - Logout / re-login support per session with automatic history restore.
- Helper script to expose Docker + HTTP proxies so Codex (or any remote automation) can drive your local environment safely.
- Node.js 18+ (20 recommended) and npm.
- Docker Desktop (only if you still use the earlier Compose workflows).
socat(brew install socat) for the proxy helper script.- An OpenAI API key with access to the chat model you want (
gpt-4o-miniby default).
-
Install dependencies
npm install
-
Configure environment
cp .env.example .env
Edit
.env(kept locally only) and set:OPENAI_API_KEY– your OpenAI key.SESSION_SECRET– any random string for signing session cookies.- Optional:
OPENAI_MODEL(defaults togpt-4o-mini),PORT(default8081), andHISTORY_MAX_MESSAGES(default100messages retained per user).
-
Persistent chat history directory
mkdir -p data
History is stored in
data/history.json(auto-created at runtime). It’s bind-mounted into the container so conversations survive restarts. -
Prepare production environment file
cp .env.example .env.production
This file travels with
docker-compose.prod.ymlon the NAS and stores the production OpenAI key/secret. Do not commit either.envfile. -
(Optional) Start helper proxies If you want me (or remote tooling) to access your local Docker daemon and the chat UI, run:
bash scripts/start-proxies.sh
Leave that terminal open; it exposes:
tcp://127.0.0.1:23750→/var/run/docker.sockhttp://127.0.0.1:23751→http://127.0.0.1:8081(change viaHTTP_FORWARD_TARGETif you ever move the app)
- A lightweight FastAPI service in
agent-service/connects to Docker’s MCP Gateway viaagno’sMCPTools. It lists every MCP tool, refreshes connections, and exposes a simple HTTP API (/tools,/call-tool). - The Node backend asks this service for tool metadata before every turn and passes them to OpenAI via the official function-calling interface. When the model chooses a tool, the backend calls the FastAPI service, posts the result into the transcript, then resumes the model with the new context.
- A built-in fallback tool
demo_generate_numberlives inside the service so local smoke tests work even before you plug in real MCP servers. - The Docker MCP Gateway container now mounts
mcp/catalog.yaml, which pins thefetchandnode-code-sandboxservers and injects the mounts they need (most importantly/var/run/docker.sockfor the sandbox). This keeps the configuration portable (NAS-friendly) and guarantees the toolbelt always matches the repo state.
- Review
mcp/catalog.yamlto confirm the servers you want (defaults to Fetch + Node.js Sandbox). Add secrets there if the chosen MCPs require them. - Provide any required secrets via the Docker MCP CLI, e.g.
docker mcp secret set fetch.api_key=<TOKEN>. - Update
.envwith the gateway URL the FastAPI service should call (defaults tohttp://mcp-gateway:8080inside Compose). docker compose -f docker-compose.dev.yml up --buildnow brings up three services:app: the existing Node/Express server + UIagent-service: FastAPI bridge that speaks MCPmcp-gateway: Docker’s official MCP gateway container
Set FAKE_OPENAI_MODE=1 (and keep the default trigger prompt please call JS that outputs 10) to avoid OpenAI calls during CI. In this mode the server:
- Detects the fake prompt and pretends the assistant chose the
demo_generate_numbertool. - Actually calls the tool via the FastAPI service so we still exercise the full MCP path.
- Streams a friendly explanation (“the tool returned 10”) after the synthetic tool call completes.
Playwright uses this mode to keep e2e coverage without burning tokens. Clear the flag in real environments so GPT-4o-mini can route tool calls on its own.
-
Development mode (host)
npm run dev
Visit http://localhost:8081. Enter a username, chat, and log out when done.
-
Production mode (host)
npm start
Ensure
NODE_ENV=productionso cookies are marked secure when hosted behind HTTPS.
-
Local Compose (mirrors NAS)
docker compose -f docker-compose.dev.yml up --build
Uses the same
.envfile for secrets and exposes http://localhost:8081. -
NAS deployment
- Copy
docker-compose.prod.yml,.env.production, and thedata/directory (or create/volume1/chat-relay/data/) on the NAS. - Replace
YOUR_DOCKERHUB_USERindocker-compose.prod.ymlwith your Docker Hub handle (or the registry/image the GitHub workflow publishes). - On the NAS:
The container listens on port
docker compose pull docker compose up -d --remove-orphans
8081, so existing reverse proxies/firewall rules keep working.
- Copy
server.jsruns an Express server withexpress-session. Upon login, it stores the username and initializes an empty message array in the session.- Each POST to
/api/chatappends the user’s message, grabs the last five conversation entries, and calls OpenAI with a short system prompt plus those messages. - The assistant reply is saved back into the session (capped at 40 stored messages to avoid unbounded growth) and returned to the client.
- The frontend (
public/app.js) handles login/logout, renders the transcript, and calls the chat endpoint.
.env(local development +docker-compose.dev.yml).env.production(NAS /docker-compose.prod.yml)
Both files follow the template in .env.example and must contain the OpenAI key and session secret.
- Specs live in
tests/e2eand run via@playwright/test. The shared config (playwright.config.ts) defaults tohttp://127.0.0.1:8081, so the CLI and the VS Code Playwright Test extension execute the exact same suite. - Important: On macOS and similar locked-down environments, Chromium needs elevated permissions to launch from automation. When running
npm run test:e2emake sure you allow the command to execute with elevated privileges (e.g., via the Codex CLI approval flow) so the headless browser can start. - Run locally:
or trigger the tests from the Playwright panel in VS Code. Set
npm install npm run test:e2e
E2E_BASE_URLwhen you need to point at another host/port (e.g., a remote staging box). chat.spec.tscovers version badge visibility, username-only login, Enter-to-send behavior, assistant replies, logout, and session persistence.
If you prefer executing tests on another machine/container, use scripts/run-e2e-playwright.sh. It SSHes into the host, runs the suite, and copies the HTML report/result artifacts back:
export PLAYWRIGHT_SSH_TARGET=anton@test-runner # SSH target that can reach your app
export PLAYWRIGHT_REMOTE_PATH=/workspace/VSCode2 # repo path on that host
bash scripts/run-e2e-playwright.shReports appear under ./playwright-report/index.html, and the script’s exit code mirrors Playwright’s so you can chain it into other automation.
These steps are the minimum required for me (or any automation) to refresh the development stack on your Mac:
- Start proxies (needed for Docker + HTTP access from this CLI):
bash scripts/start-proxies.sh # leave the terminal open - Rebuild & run the container via the Docker socket proxy:
DOCKER_HOST=tcp://127.0.0.1:23750 docker compose -f docker-compose.dev.yml up --build -d --remove-orphans
- Run Playwright tests (ensures UI + MCP toolbelt paths work):
npx playwright test - Investigate failures using:
playwright-report/index.html(detailed UI failures)test-results/.../error-context.mdDOCKER_HOST=… docker logs vscode2-app-1 --tail 200
Production/NAS deployment remains manual: build/tag/push the image you want, copy docker-compose.prod.yml, .env.production, and data/ to the NAS, set the correct image reference, then on the NAS run docker compose pull && docker compose up -d --remove-orphans.