Skip to content

feat: standard dev experience — single-command dev:portal with Vite HMR, remove IS_LOCAL mocks #5

Description

@qiaolei1973

Problem Statement

Running the portal in dev mode requires manual steps: build the frontend first (cd web && npx vite build), then start the server separately. The server has accumulated conditional guards (IS_LOCAL mocks, dist/web existence checks) that pollute production code paths. This is unlike the standard web dev experience where a single command starts both frontend and backend with hot reload, and the server code stays clean.

Solution

Adopt the standard Vite + backend dev pattern: npm run dev:portal runs both the Vite dev server (frontend with HMR) and the Fastify server (backend with tsx watch) concurrently. The Vite dev server proxies /api requests to the Fastify server, eliminating the need to build the frontend or serve static files in dev mode. Remove all IS_LOCAL mock guards and conditional static-serving logic from the server — the server becomes a pure API server in dev, with static serving only in production.

User Stories

  1. As a developer, I want to run npm run dev:portal and have both frontend and backend start with a single command, so that I don't need to manually build or start services separately
  2. As a developer, I want frontend changes to reflect instantly via HMR, so that I can iterate on UI quickly without rebuilding
  3. As a developer, I want backend changes to trigger automatic server restart, so that I can iterate on API logic quickly
  4. As a developer, I want the Vite dev server to proxy API calls to the Fastify backend transparently, so that the frontend works identically to production (same-origin, cookies, etc.)
  5. As a developer, I want npm run dev:cli to work as it does today (no frontend), so that CLI development is unaffected
  6. As a developer, I want the server source code to contain zero dev-mode conditional logic (no IS_LOCAL, no dist/web existence checks), so that production code paths remain clean and testable
  7. As a developer, I want external service failures (New API, Sandbox Manager) to degrade gracefully at the route level with proper error responses, so that the portal still starts and serves its UI even without backend infrastructure
  8. As a developer, I want the server to log clear warnings when external services are unavailable, so that I understand why certain features (sandbox creation, API key management) return errors

Implementation Decisions

1. Vite dev server with API proxy

Add a server block to server/web/vite.config.ts that proxies all /api requests to the Fastify server (default http://localhost:8080). This is the standard Vite proxy pattern. The frontend dev server runs on a separate port (e.g., 5173) and forwards API traffic transparently.

2. Concurrent dev script

Use concurrently (or npm-run-all) to run both processes from a single npm script. The server/package.json dev script changes to run both tsx watch src/index.ts (backend) and vite dev (frontend) in parallel.

The root dev:portal script delegates to the server workspace as before.

3. Frontend dev script in server/web/package.json

Add "dev": "vite" to server/web/package.json scripts. This is currently missing — the web package has no dev/build/start scripts at all.

4. Remove IS_LOCAL mock guards from server source

Remove the IS_LOCAL constant and all early-return mock guards from:

  • server/src/newapi/index.ts — 6 exported functions
  • server/src/sandbox-manager/client.ts — 8 exported functions
  • server/src/routes/sandboxes-ssh.ts — LOCAL-mode WebSocket guard

The startup calls (initNewApi, ensureNewApiChannel) already handle failure gracefully (non-fatal warnings). Route-level callers (sandbox creation, admin approval) already have try/catch with degraded responses. The mock guards are redundant — the existing error handling is sufficient, and returning mock data hides real integration issues.

5. Remove conditional static serving logic from index.ts

Remove the dist/web existence check and the if/else block around @fastify/static registration. The static serving and SPA fallback should always be registered — if dist/web doesn't exist (dev mode without prior build), the @fastify/static warn is acceptable since the Vite dev server handles frontend in dev. In production, dist/web is always populated by the build step.

6. Keep the dotenv loading and DB auto-mkdir

The server/src/env.ts preload module and the fs.mkdirSync in server/src/db/index.ts are legitimate startup infrastructure, not dev-mode hacks. These stay.

7. Environment variable for backend port

The Vite proxy target should read from an env var (e.g., PORTAL_PORT or PORT) with a default of 8080, so developers can override the backend port if needed. The existing .env already defines PORTAL_PORT=3080.

Testing Decisions

  • Manual verification: Run npm run dev:portal and confirm:
    • Both Vite and Fastify start in one terminal
    • Frontend loads with HMR at the Vite dev server URL
    • API calls proxy correctly (login, auth status, admin routes)
    • Backend hot-reloads on server file changes
    • Frontend hot-reloads on component changes
    • No IS_LOCAL, dist/web, or mock guard logic in server source
  • Production build: Run npm run build:portal and confirm the built server still serves static files from dist/web correctly
  • Route-level error handling: With external services unavailable, confirm that sandbox and admin routes return proper error responses (not crashes)

No automated test infrastructure exists in this project, so testing is manual verification.

Out of Scope

  • Docker Compose or k3d integration for running external services locally
  • Mocking external services (New API, Sandbox Manager) — services are either available or routes degrade gracefully
  • WebSocket SSH relay in dev mode — requires a running sandbox-manager, which is out of scope for pure frontend dev
  • CLI dev experience changes — npm run dev:cli already works as expected
  • Automated tests

Further Notes

The server/web directory is not listed in the root workspaces array (which only includes server and cli). Since server/web is a subdirectory of server, its dependencies are managed independently. The dev command should be invoked from server/web or via the concurrent script.

The current @fastify/static setup has a known issue: if dist/web doesn't exist at registration time, it logs a warning and fails to serve files even after they're built. This PRD resolves that by making static serving unconditional (production always has the build output) and using Vite dev server in development.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentPRD ready for autonomous agent implementation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions