thedotmack/claude-mem · error · Error
Viewer UI not found at any expected location
Error message
Viewer UI not found at any expected location
What it means
The GET handler serving the viewer UI throws when viewerHtmlBytes is null — i.e. viewer.html was not found at any of the candidate paths (packageRoot/ui/viewer.html or packageRoot/plugin/ui/viewer.html) at module load. The routes module reads the file once at boot and caches the bytes, so a missing file at startup means this error on every UI request.
Solutions
- Rebuild/reinstall the package so ui/viewer.html exists (npm run build-and-sync per project docs)
- Verify the file exists at packageRoot/ui/viewer.html or packageRoot/plugin/ui/viewer.html where getPackageRoot() points
- Check the 'Cached viewer.html at boot' vs missing-file log line at startup to see which root was resolved
- If serving from source, run the sync step that copies plugin assets to the marketplace/install location
Example fix
// before
const packageRoot = getPackageRoot(); // resolves to a dir without ui/viewer.html
// after
// rebuild & sync assets first:
// npm run build-and-sync
// then confirm:
// ls $(node -e "console.log(require('./src/shared/paths.js').getPackageRoot())")/ui/viewer.html Defensive patterns
Strategy: fallback
Validate before calling
import { existsSync } from 'fs';
import path from 'path';
const packageRoot = getPackageRoot();
const viewerHtmlExists =
existsSync(path.join(packageRoot, 'ui', 'viewer.html')) ||
existsSync(path.join(packageRoot, 'plugin', 'ui', 'viewer.html')); Try / catch
app.get('/viewer', (req, res) => {
try {
return viewerHandler(req, res);
} catch (err) {
if (err instanceof Error && err.message.includes('Viewer UI not found')) {
return res.status(503).send('Viewer UI asset missing — rebuild/reinstall the package.');
}
throw err;
}
}); Prevention
- Always run the full build/sync step after installing or updating the plugin
- Verify ui/viewer.html exists in your deployment artifact before starting the worker
- Check startup logs for the 'Cached viewer.html at boot' line as a health signal
- Pin package installs (lockfile) to avoid partial installs stripping assets
When it happens
Trigger: GET to the viewer UI endpoint when the plugin package was installed without the ui/viewer.html asset, the wrong package root was resolved, or a dev build skipped copying the UI files into plugin/ui/.
Common situations: Partial or corrupted npm install; running from source without running the build step that emits ui/viewer.html; getPackageRoot() resolving to a different directory after a version change or monorepo layout change; deployment pipeline that strips .html assets.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- Observation TV UI not found at any expected location
- Anthropic API error
- BadRequest
- BadRequest
- [claude-mem] Worker GET
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/1701c946d39625ca.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/worker/http/routes/ViewerRoutes.ts:183
}
private handleHealth = this.wrapHandler((req: Request, res: Response): void => {
const activeSessions = this.sessionManager.getActiveSessionCount();
res.json({
status: 'ok',
timestamp: Date.now(),
activeSessions,
// Per-process identity, so a caller waiting out a restart can tell the
// successor from the worker it just asked to die. The dying worker keeps
// answering here for the whole graceful-shutdown window.
pid: process.pid,
});
});
private handleViewerUI = this.wrapHandler((req: Request, res: Response): void => {
if (!viewerHtmlBytes) {
throw new Error('Viewer UI not found at any expected location');
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(viewerHtmlBytes);
});
/**
* Observation TV: the same /stream the viewer consumes, rendered as a
* full-screen fading title card. Static, dependency-free, and served from
* the same origin so the EventSource needs no CORS of its own.
*/
private handleTvUI = this.wrapHandler((req: Request, res: Response): void => {
if (!tvHtmlBytes) {
throw new Error('Observation TV UI not found at any expected location');
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(tvHtmlBytes);
});
View on GitHub (pinned to d8bc9755e7)