BUG: Frontend dashboard not served — missing express.static middleware (regression in v1.7.26) #57

Closed
opened 2026-05-27 23:05:44 +01:00 by Gandalf · 1 comment
Owner

Summary

The sofarr server starts successfully and the backend API works perfectly, but the frontend dashboard UI is never served.

Visiting the root URL (/) or any non-API path returns a 404/500 instead of the expected Vite-built SPA (public/index.html). The public/ directory (containing index.html, app.js, style.css, etc.) and the entire client/ Vite frontend source exist in the repository but are completely ignored by the Express application.

This explains the reported symptom: "the app is no longer accepting web connections for the front end despite running".


Steps to Reproduce

  1. Clone or pull the latest main branch.
  2. Run via Docker (docker compose up) or npm start.
  3. Open the application in a browser at http://localhost:3001 (or your configured PORT / domain).
  4. Observe the result:
    • Blank page, connection refused for UI assets, or HTTP 404/500 error.
  5. Confirm API still works: curl http://localhost:3001/health{"status":"ok"}

Expected Behavior

  • The server should serve the built frontend SPA from public/ at the root path.
  • GET / → serves public/index.html (the dashboard).
  • Client-side routing / deep links should fall back to index.html.
  • This matches the README documentation: "Access the dashboard at http://your-server:3001" and the project description as a "personal media download dashboard" with a web UI.

Actual Behavior

  • No static file middleware is registered anywhere in server/app.js or server/index.js.
  • All requests not matching exact API routes (/api/*, /health, /ready, /api/swagger*) fall through to the global error handler.
  • The frontend assets in public/ are present but never mounted.

Root Cause Analysis

File: server/app.js (the createApp() factory function)

After mounting all API routes, rate limiters, CSRF protection, Helmet CSP, Swagger UI, etc., there is zero code that serves the frontend:

// server/app.js — excerpt (current main)
app.use('/api', apiLimiter);
app.use('/api/auth', authRoutes);
// ... all other /api/* routes ...
app.use((err, req, res, next) => { /* error handler */ });

return app;   // ← no static serving ever added

File: server/index.js

const server = tlsCredentials
  ? https.createServer(tlsCredentials, app)
  : http.createServer(app);

server.listen(PORT, () => { /* logs "running on localhost:3001" */ });
  • listen(PORT) correctly binds to all interfaces (0.0.0.0).
  • TLS / snakeoil cert handling works.
  • But the app it serves has no frontend routes.

Evidence of intended frontend:

  • public/index.html, public/app.js, public/style.css exist.
  • client/ contains a full Vite + src/ SPA project (vite.config.js, package.json).
  • README repeatedly refers to "the dashboard" being served at port 3001.
  • Earlier versions or design intent clearly included a web UI.

This appears to be a regression — the static serving middleware was likely removed or never properly re-added when the app was refactored into the current createApp() + separate index.js structure.


Additional Context / Contributing Factors

  • Default TLS mode (TLS_ENABLED=true) uses self-signed snakeoil certs → browsers will also block the UI even if static serving were present.
  • Helmet CSP is correctly configured for 'self', but irrelevant without a frontend origin.
  • No express.static or SPA fallback (app.get('*', ...)) exists anywhere in the codebase.
  • The public/ folder is included in the Docker image (via .dockerignore rules) but remains unused.

Suggested Fix

Add the following in server/app.js inside createApp(), after all API routes but before the error handler:

// Serve built frontend SPA (Vite output in ../public)
const publicDir = path.join(__dirname, '../public');
app.use(express.static(publicDir));

// SPA fallback for client-side routing (history API)
app.get('*', (req, res) => {
  res.sendFile(path.join(publicDir, 'index.html'));
});

Also consider:

  • Moving the rate limiter / CSRF middleware so they do not apply to static assets.
  • Adding a production build step or note in README about how client/ is built into public/.
  • Updating the startup log in index.js to mention the frontend is being served.

Impact

  • New users / fresh deployments get a non-functional "dashboard".
  • Existing users upgrading to v1.7.26+ lose the UI.
  • The project appears broken on first run despite the backend being fully operational.
## Summary The sofarr server starts successfully and the backend API works perfectly, but **the frontend dashboard UI is never served**. Visiting the root URL (`/`) or any non-API path returns a 404/500 instead of the expected Vite-built SPA (`public/index.html`). The `public/` directory (containing `index.html`, `app.js`, `style.css`, etc.) and the entire `client/` Vite frontend source exist in the repository but are **completely ignored** by the Express application. This explains the reported symptom: *"the app is no longer accepting web connections for the front end despite running"*. --- ## Steps to Reproduce 1. Clone or pull the latest `main` branch. 2. Run via Docker (`docker compose up`) **or** `npm start`. 3. Open the application in a browser at `http://localhost:3001` (or your configured `PORT` / domain). 4. Observe the result: - Blank page, connection refused for UI assets, or HTTP 404/500 error. 5. Confirm API still works: `curl http://localhost:3001/health` → `{"status":"ok"}` --- ## Expected Behavior - The server should serve the built frontend SPA from `public/` at the root path. - `GET /` → serves `public/index.html` (the dashboard). - Client-side routing / deep links should fall back to `index.html`. - This matches the README documentation: *"Access the dashboard at http://your-server:3001"* and the project description as a "personal media download dashboard" with a web UI. --- ## Actual Behavior - No static file middleware is registered anywhere in `server/app.js` or `server/index.js`. - All requests not matching exact API routes (`/api/*`, `/health`, `/ready`, `/api/swagger*`) fall through to the global error handler. - The frontend assets in `public/` are present but never mounted. --- ## Root Cause Analysis **File:** `server/app.js` (the `createApp()` factory function) After mounting all API routes, rate limiters, CSRF protection, Helmet CSP, Swagger UI, etc., there is **zero** code that serves the frontend: ```js // server/app.js — excerpt (current main) app.use('/api', apiLimiter); app.use('/api/auth', authRoutes); // ... all other /api/* routes ... app.use((err, req, res, next) => { /* error handler */ }); return app; // ← no static serving ever added ``` **File:** `server/index.js` ```js const server = tlsCredentials ? https.createServer(tlsCredentials, app) : http.createServer(app); server.listen(PORT, () => { /* logs "running on localhost:3001" */ }); ``` - `listen(PORT)` correctly binds to all interfaces (`0.0.0.0`). - TLS / snakeoil cert handling works. - But the `app` it serves has no frontend routes. **Evidence of intended frontend:** - `public/index.html`, `public/app.js`, `public/style.css` exist. - `client/` contains a full Vite + `src/` SPA project (`vite.config.js`, `package.json`). - README repeatedly refers to "the dashboard" being served at port 3001. - Earlier versions or design intent clearly included a web UI. This appears to be a **regression** — the static serving middleware was likely removed or never properly re-added when the app was refactored into the current `createApp()` + separate `index.js` structure. --- ## Additional Context / Contributing Factors - **Default TLS mode** (`TLS_ENABLED=true`) uses self-signed snakeoil certs → browsers will also block the UI even if static serving were present. - Helmet CSP is correctly configured for `'self'`, but irrelevant without a frontend origin. - No `express.static` or SPA fallback (`app.get('*', ...)`) exists anywhere in the codebase. - The `public/` folder is included in the Docker image (via `.dockerignore` rules) but remains unused. --- ## Suggested Fix Add the following in `server/app.js` inside `createApp()`, **after** all API routes but **before** the error handler: ```js // Serve built frontend SPA (Vite output in ../public) const publicDir = path.join(__dirname, '../public'); app.use(express.static(publicDir)); // SPA fallback for client-side routing (history API) app.get('*', (req, res) => { res.sendFile(path.join(publicDir, 'index.html')); }); ``` Also consider: - Moving the rate limiter / CSRF middleware so they do **not** apply to static assets. - Adding a production build step or note in README about how `client/` is built into `public/`. - Updating the startup log in `index.js` to mention the frontend is being served. --- ## Impact - New users / fresh deployments get a non-functional "dashboard". - Existing users upgrading to v1.7.26+ lose the UI. - The project appears broken on first run despite the backend being fully operational.
Gandalf added the Kind/Bug
Priority
Critical
1
labels 2026-05-27 23:05:44 +01:00
Author
Owner

Resolved in commit 86277e2.

Resolved in commit 86277e2.
Gandalf added the Area/Frontend label 2026-05-28 11:57:51 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Gandalf/sofarr#57