# Frontend production build & deployment

**Audience:** anyone deploying the DALSEEN dashboard to a public URL.

## TL;DR

> If the production browser console shows
> `[vite] failed to connect to websocket` to `wss://localhost:5173`,
> the host is running the **Vite dev server**, not a production
> build. Fix the deployment.

The dashboard at `https://dashboard.ds-pl.xyz` (and any other public
URL) MUST be served as the static output of `npm run build`. The
Vite dev server (`npm run dev`) is for developer machines only — it
ships an HMR client that tries to open a WebSocket to the dev
server's host (default `localhost:5173`), which fails behind any
reverse proxy.

## How to build

```bash
cd app
npm ci
VITE_API_BASE_URL=https://api.ds-pl.xyz \
VITE_USE_MOCK_API=0 \
VITE_ALLOW_ALL_PERMISSIONS=false \
npm run build
```

This emits `app/dist/` containing:

- `index.html` (a single static page)
- `assets/*.js`, `assets/*.css` (hashed bundles)
- favicon, fonts, images

## How to serve

Any static file server works. Example nginx:

```nginx
server {
    listen 443 ssl http2;
    server_name dashboard.ds-pl.xyz;

    root /var/www/dashboard/current/dist;
    index index.html;

    # SPA fallback — every route serves index.html so React Router
    # can take over.
    location / {
        try_files $uri $uri/ /index.html;
    }

    # Long-cache hashed assets, no-cache the index.
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }
}
```

The Laravel API at `https://api.ds-pl.xyz` is a separate service —
the SPA's `VITE_API_BASE_URL` points at it directly. CORS must be
configured on the Laravel side to allow the dashboard's origin.

## Environment variables (build-time)

| Var | Required | Notes |
|---|---|---|
| `VITE_API_BASE_URL` | yes | e.g. `https://api.ds-pl.xyz` (no trailing slash). The SPA prefixes `/api/v1` itself. |
| `VITE_USE_MOCK_API` | yes (set to `0`) | `1` enables the in-memory mock adapter. **Must be `0` in production.** |
| `VITE_ALLOW_ALL_PERMISSIONS` | yes (set to `false`) | `true` is a developer escape hatch and would silently grant every permission. |

These are **build-time** — they are baked into the bundle. Changing
them requires a rebuild + redeploy.

## Anti-patterns (do not do)

- ❌ Run `npm run dev` on the production host and proxy port 5173
  through nginx. The HMR client connects to `localhost:5173` from
  the *browser*, not the server, and fails.
- ❌ Commit a built `dist/` to git. It's ignored by `.gitignore`;
  build on the deploy host instead.
- ❌ Serve `app/index.html` directly without running `vite build`.
  The dev `index.html` is a thin shell that pulls everything from
  the Vite dev server module graph.

## Remote-dev fallback (only if you really must)

If during a pilot you intentionally need to expose a Vite dev server
behind a reverse proxy, set these on the dev server's environment so
the HMR client connects to the public host instead of `localhost`:

```bash
VITE_DEV_PUBLIC_HOST=dashboard.ds-pl.xyz
VITE_DEV_PUBLIC_PROTOCOL=wss
VITE_DEV_PUBLIC_PORT=443
npm run dev
```

The reverse proxy must forward `/`, `/api/`, and the WebSocket
upgrade for `/`. This setup is **not** how production is supposed to
run; treat it as a debugging aid only.

## Verifying the deployed build is correct

Open the deployed URL in a browser and check the page source. A
correctly built deployment shows:

```html
<script type="module" crossorigin src="/assets/index-AbC123.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-AbC123.css">
```

If you see references to `/@vite/client`, `/src/main.jsx`, or any
`localhost:5173` URL, the deployment is the dev server — fix the
host, not the codebase.

## Related: production environment for the Laravel API

The Pay SQL portability fix (commit `0cde761`) replaced SQLite-only
`json_each(...)` with portable PHP. The production database target
is **MySQL**. Ensure the deployed Laravel API runs `MYSQL` and not
SQLite, and that:

```bash
php artisan config:cache
php artisan route:cache
php artisan migrate --force
```

are run on every deploy. A stale `bootstrap/cache/config.php` was
the source of "fixes work locally but not online" issues earlier in
the project.
