thedotmack/claude-mem · error

ViewerUnavailable

ViewerUnavailable

Error message

Viewer UI not found at any expected location

What it means

ServerViewerRoutes serves the bundled viewer UI; at module load it probes the npm-package ui/ directory and the plugin/ui directory for viewer.html. If neither ships the file, viewerHtmlBytes stays null and GET / responds 503 ViewerUnavailable. API routes are unaffected — only the web viewer is missing.

Source

Thrown at src/server/runtime/ServerViewerRoutes.ts:57

  });
} else {
  logger.warn('SYSTEM', 'viewer.html not found for server runtime', {
    candidates: VIEWER_HTML_CANDIDATE_PATHS,
  });
}

export class ServerViewerRoutes implements RouteHandler {
  setupRoutes(app: Application): void {
    const packageRoot = getPackageRoot();
    // Serve static assets from BOTH the npm-package `ui` dir and the plugin
    // `plugin/ui` dir, matching the worker's resolution order so the viewer
    // loads regardless of which layout the server image ships.
    app.use(express.static(path.join(packageRoot, 'ui')));
    app.use(express.static(path.join(packageRoot, 'plugin', 'ui')));

    app.get('/', (_req: Request, res: Response) => {
      if (!viewerHtmlBytes) {
        res.status(503).json({ error: 'ViewerUnavailable', message: 'Viewer UI not found at any expected location' });
        return;
      }
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      res.send(viewerHtmlBytes);
    });
  }

  // Exposed for tests: did the build ship a viewer.html the server can serve?
  static hasViewerHtml(): boolean {
    return viewerHtmlBytes !== null;
  }
}

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Build or reinstall the package so viewer.html exists under ui/ or plugin/ui/
  2. Verify getPackageRoot() resolves to the directory that actually contains the ui folder for your layout
  3. If you do not need the UI, ignore the 503 — JSON API endpoints still work
  4. Use ServerViewerRoutes.hasViewerHtml() in smoke tests to assert the artifact shipped
Defensive patterns

Strategy: validation

Validate before calling

async function viewerAvailable(base: string): Promise<boolean> {
  const res = await fetch(`${base}/`); // 200 = html, 503 ViewerUnavailable = not shipped
  return res.status === 200;
}

if (!(await viewerAvailable(base))) {
  console.warn('viewer UI not shipped in this build; API still usable');
}

Type guard

function isViewerUnavailable(status: number, body: unknown): body is { error: 'ViewerUnavailable'; message: string } {
  return status === 503 && typeof body === 'object' && body !== null && (body as { error?: string }).error === 'ViewerUnavailable';
}

Try / catch

const res = await fetch(`${base}/`);
const body = await res.json().catch(() => ({}));
if (isViewerUnavailable(res.status, body)) {
  // packaging defect: rebuild/reinstall so ui/viewer.html ships; API endpoints are unaffected
  disableUi();
}

Prevention

When it happens

Trigger: Opening the server root URL on a deployment where viewer.html exists under neither <packageRoot>/ui nor <packageRoot>/plugin/ui — custom Docker images, source checkouts without a viewer build, or getPackageRoot() resolving elsewhere (bundled single-file layouts).

Common situations: Image build copying only server dist and dropping the ui folder; publish/.npmignore rules excluding built assets; running from a bundled artifact where __dirname-based root resolution lands in a temp dir.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/22622fe57840f8d1. Report an issue: GitHub.