Skip to content

fix(viewer): drop Google Fonts link + surface dashboard load errors (closes #323)#335

Merged
rohitg00 merged 1 commit into
mainfrom
fix/323-viewer-csp-fonts-and-error-surface
May 13, 2026
Merged

fix(viewer): drop Google Fonts link + surface dashboard load errors (closes #323)#335
rohitg00 merged 1 commit into
mainfrom
fix/323-viewer-csp-fonts-and-error-surface

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented May 13, 2026

Reproduction

@hem57 on #323: viewer dashboard tab stuck on "Loading dashboard..." under v0.9.11 on Windows with CSP violations visible in browser dev tools.

Root cause (verified)

Viewer CSP at src/auth.ts:16-30:

default-src 'none'
style-src 'unsafe-inline'   // ← no external hosts
font-src 'self'             // ← no external hosts
script-src 'nonce-${nonce}'
script-src-attr 'none'

src/viewer/index.html:7 shipped a <link rel="stylesheet"> pointing at fonts.googleapis.com. CSP style-src does NOT fall back to default-src, so:

  1. Browser blocks the external stylesheet → CSP violation logged.
  2. Even if stylesheet loaded, the actual font files from fonts.gstatic.com are blocked by font-src 'self' → more violations.

That's the violation @hem57 saw. On Windows Edge in particular, the pile of CSP errors in the console matched the issue title verbatim.

Compounding: when loadDashboard() (or renderDashboard()) hits an exception mid-flight, the placeholder text "Loading dashboard..." sits there forever — no surfaced error, no console hint of which call failed. apiGet() already swallows network/HTTP errors and returns null, so the most-common failure mode is silent — but renderDashboard itself can still throw on edge shapes.

Fix

1. Drop the Google Fonts link

-  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display...">
+  <!-- removed; CSP-incompatible, system-font fallbacks already declared -->

The viewer's font stack already has system fallbacks on every --font-* CSS variable (lines 39-42):

  • 'Playfair Display', Georgia, ...
  • 'Lora', Georgia, serif
  • 'Inter', -apple-system, sans-serif
  • 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace

Dropping the <link> falls to system fonts. Visually almost identical on macOS / Windows / most Linux. Zero CSP violations.

2. Surface load errors in the dashboard panel

loadDashboard() body wrapped in try/catch that renders the error inline:

} catch (err) {
  console.error('[viewer] loadDashboard failed:', err);
  el.innerHTML =
    '<div class="loading" style="color:var(--accent);">' +
    'Dashboard failed to load: ' + msg + ...
}

Future "stuck Loading" reports come with concrete error messages instead of a screenshot of the placeholder.

Out-of-scope (deferred follow-up)

  • Inline event handlers (oninput= / onchange= at lines 2706, 2765, 2766, 2852) are blocked by script-src-attr 'none' — browser logs CSP violations per element, handlers don't bind, but the dashboard still loads. Eliminating these requires moving to delegated listeners (~50 lines across four search/filter controls). Separate PR.

  • Self-host the fonts would preserve the design intent but adds 200-400KB to dist. Acceptable tradeoff: fonts go, system stack is fine.

Test plan

  • npm test — 877 / 877 pass.
  • npm run build — tsdown clean.
  • Visual: open localhost:3113 in Chrome dev tools, confirm no CSP violations from font-related requests.
  • Force a 502 from one apiGet endpoint (e.g. block the REST proxy mid-flight), confirm dashboard shows the error message instead of sticking on "Loading...".

Closes #323. Thanks @hem57 for the Windows repro screenshot.

Summary by CodeRabbit

  • Bug Fixes
    • Dashboard now displays helpful error messages when loading fails, including guidance on content security policy violations and viewer version information.
    • Font loading updated to use CSS fallbacks instead of external sources, addressing content security policy constraints.

Review Change Stack

…loses #323)

@hem57 reported the viewer dashboard tab stuck on "Loading
dashboard..." on Windows under v0.9.11 with CSP violations visible in
the browser console.

Two narrow fixes that together close the bug premise:

1. Drop the <link rel="stylesheet" href="https://fonts.googleapis.com/
   css2?..."> tag.

   The viewer ships a strict CSP (src/auth.ts:16-30):
     default-src 'none'
     style-src 'unsafe-inline'
     font-src 'self'

   Note that style-src does NOT include the Google Fonts origin and
   font-src does NOT include fonts.gstatic.com. Per CSP semantics
   style-src does not fall back to default-src, so the external
   stylesheet was blocked outright. On every viewer load the browser
   logged a CSP violation for the <link>, plus more for each blocked
   font file. On Windows Edge this produced the screenshot @hem57
   shared.

   The viewer's font stack already declares system fallbacks on every
   --font-* CSS variable (src/viewer/index.html:39-42):
     --font-display: 'Playfair Display', Georgia, ...
     --font-body:    'Lora', Georgia, serif;
     --font-ui:      'Inter', -apple-system, sans-serif;
     --font-mono:    'JetBrains Mono', 'SF Mono', ...

   Dropping the <link> falls back to the system stack with no visual
   loss on platforms that ship Georgia / SF Mono / system serifs (most
   macOS, Windows, Linux distros).

2. Wrap loadDashboard() body in try/catch that surfaces failures in
   the dashboard panel.

   Before this fix, any uncaught error in the Promise.all of apiGet
   calls (or in renderDashboard) left the dashboard stuck on the
   "Loading dashboard..." text forever, with no indication of what
   went wrong. apiGet() already swallows network/HTTP errors and
   returns null, so most errors are caught upstream — but renderDashboard
   can still throw on shape surprises (CSP-blocked event handler
   binding, undefined object access, etc.).

   The new catch renders an inline error block with the error message
   and a pointer to the browser console for full detail. Future
   reports of "stuck Loading" will at least come with a concrete error
   message instead of just a screenshot of the placeholder.

Out-of-scope (deferred to follow-up):

- CSP script-src-attr 'none' blocks inline event handlers in
  innerHTML-injected dynamic markup (oninput= / onchange= at lines
  2706, 2765, 2766, 2852). Browser logs a CSP violation per element
  but the handlers simply don't bind — the dashboard still loads.
  Eliminating these requires moving from inline strings to delegated
  event listeners, ~50 lines of refactor across four search/filter
  controls. Separate PR.

- Self-hosting the fonts (instead of dropping them) would preserve
  the design intent but adds 200-400KB of font files to the dist.
  Acceptable tradeoff for now: fonts go, system stack is fine.

Test plan:
- npm test — 877/877 pass.
- npm run build — tsdown clean, dist/viewer/index.html regenerated.
- Visual sanity check pending: open localhost:3113 in Chrome dev
  tools, confirm no CSP violations from font-related requests; force
  a 502 from one apiGet endpoint and confirm dashboard shows the
  error message instead of sticking.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentmemory Ready Ready Preview, Comment May 13, 2026 1:05pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

📝 Walkthrough

Walkthrough

The PR addresses a blocked dashboard by removing an external Google Fonts dependency that violated Content Security Policy and adding error handling to the dashboard load function. The dashboard now displays a helpful error message instead of remaining stuck on "Loading dashboard..." when fetch or rendering failures occur.

Changes

Viewer dashboard resilience and CSP compliance

Layer / File(s) Summary
CSP compliance and font fallback
src/viewer/index.html
Google Fonts <link> element removed with explanatory CSP comment; font display falls back to existing CSS variables.
Dashboard error handling and user feedback
src/viewer/index.html
loadDashboard() wraps API fetches and rendering in try/catch; populates state.dashboard fields on success and displays error message with CSP debugging guidance on failure, preventing indefinite loading.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A dashboard once stuck on its loading screen,
Now catches its breath with error-handling grace.
Fonts fall back gently, CSP rules are kind,
And users see messages when things go awry.
Hops onward, problem solved! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: removing Google Fonts link and surfacing dashboard load errors, directly addressing the linked issue #323.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from #323: eliminates CSP violations by removing the Google Fonts link, implements error handling in loadDashboard() with try/catch, and surfaces errors instead of leaving the UI stuck.
Out of Scope Changes check ✅ Passed All changes are within scope. The modifications to index.html and loadDashboard() directly address the CSP violations and error surfacing objectives, with out-of-scope items properly deferred.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/323-viewer-csp-fonts-and-error-surface

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/viewer/index.html`:
- Around line 1117-1160: The error message string `msg` is injected into HTML
unescaped in the catch block (used when setting `el.innerHTML`), which can lead
to XSS; fix it by passing the message through the existing `esc()` helper (e.g.
use `esc(msg)` or assign `var safeMsg = esc(msg)`) before concatenating into the
HTML, and update the `el.innerHTML` construction to use that escaped value;
check the catch block around `loadDashboard`/`renderDashboard` where `msg` and
`el.innerHTML` are defined to apply this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 48811f4e-407e-404a-ab3c-5815377b74f8

📥 Commits

Reviewing files that changed from the base of the PR and between cba234d and 43b26aa.

📒 Files selected for processing (1)
  • src/viewer/index.html

Comment thread src/viewer/index.html
Comment on lines +1117 to +1160
try {
var results = await Promise.all([
apiGet('health'),
apiGet('sessions'),
apiGet('memories?latest=true'),
apiGet('graph/stats'),
apiGet('audit?limit=5'),
apiGet('semantic'),
apiGet('procedural'),
apiGet('relations'),
apiGet('lessons'),
apiGet('crystals')
]);
state.dashboard.health = results[0];
state.dashboard.sessions = (results[1] && results[1].sessions) || [];
state.dashboard.memories = (results[2] && results[2].memories) || [];
state.dashboard.graphStats = results[3];
state.dashboard.recentAudit = (results[4] && results[4].entries) || [];
state.dashboard.semantic = (results[5] && results[5].facts) || (results[5] && results[5].semantic) || [];
state.dashboard.procedural = (results[6] && results[6].procedures) || (results[6] && results[6].procedural) || [];
state.dashboard.lessons = (results[8] && results[8].lessons) || [];
state.dashboard.crystals = (results[9] && results[9].crystals) || [];
state.dashboard.relations = (results[7] && results[7].relations) || [];
state.dashboard.loaded = true;
renderDashboard();
} catch (err) {
// Without this catch, any uncaught error in the await Promise.all
// or the renderDashboard call leaves the dashboard stuck on
// "Loading dashboard..." forever with no indication to the user
// (#323). apiGet() already swallows network/HTTP errors and
// returns null, but renderDashboard can still throw on shape
// surprises (CSP-blocked event handler binding, undefined fields).
// Surface the error in the panel + log full detail to console.
var msg = (err && err.message) ? err.message : String(err);
console.error('[viewer] loadDashboard failed:', err);
el.innerHTML =
'<div class="loading" style="color:var(--accent);">' +
'Dashboard failed to load: ' + msg +
'<br><br><span style="font-size:12px;color:var(--ink-muted);">' +
'Check the browser console for the full error. If you see CSP ' +
'violations, please open an issue with the agentmemory version ' +
'(top-right of the viewer) and the violation text.' +
'</span></div>';
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape the error message to prevent potential XSS.

The error message variable msg on line 1150 is concatenated directly into HTML on line 1154 without escaping. Although JavaScript runtime errors are typically safe, error messages can contain property names or values from malformed data. Use the existing esc() helper to eliminate the XSS risk.

🔒 Proposed fix
         el.innerHTML =
           '<div class="loading" style="color:var(--accent);">' +
-          'Dashboard failed to load: ' + msg +
+          'Dashboard failed to load: ' + esc(msg) +
           '<br><br><span style="font-size:12px;color:var(--ink-muted);">' +

Note: The try/catch approach is excellent and correctly addresses #323 by surfacing errors instead of leaving the UI stuck. The thorough comment explaining the rationale (lines 1143–1149) is also helpful.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/viewer/index.html` around lines 1117 - 1160, The error message string
`msg` is injected into HTML unescaped in the catch block (used when setting
`el.innerHTML`), which can lead to XSS; fix it by passing the message through
the existing `esc()` helper (e.g. use `esc(msg)` or assign `var safeMsg =
esc(msg)`) before concatenating into the HTML, and update the `el.innerHTML`
construction to use that escaped value; check the catch block around
`loadDashboard`/`renderDashboard` where `msg` and `el.innerHTML` are defined to
apply this change.

@rohitg00 rohitg00 merged commit 117f49a into main May 13, 2026
5 checks passed
@rohitg00 rohitg00 deleted the fix/323-viewer-csp-fonts-and-error-surface branch May 13, 2026 13:38
rohitg00 added a commit that referenced this pull request May 13, 2026
…ardening

Three landed PRs since v0.9.11:
  - #327 (#295) — BM25 tokenizer now accepts non-ASCII (Greek,
    accented Latin, Hebrew, Arabic, Cyrillic), VectorIndex.add now
    actually called at runtime via vectorIndexAddGuarded helper with
    dim guard + input clip, migrateVectorIndex for dim migrations.
  - #326 (#277) — RetentionScore type no longer declares source
    twice; JSDoc back-compat note no longer shadowed.
  - #335 (#323) — viewer drops Google Fonts <link> (CSP-blocked),
    loadDashboard now surfaces load errors inline instead of
    sticking on "Loading dashboard…".

Bumping 0.9.11 -> 0.9.12 across the 9 standard files:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version literal)
- src/functions/export-import.ts (supportedVersions)
- test/export-import.test.ts (round-trip expectation)
- CHANGELOG.md (new 0.9.12 entry)

886 / 886 tests pass. Build clean.
rohitg00 added a commit that referenced this pull request May 13, 2026
…ardening (#337)

Three landed PRs since v0.9.11:
  - #327 (#295) — BM25 tokenizer now accepts non-ASCII (Greek,
    accented Latin, Hebrew, Arabic, Cyrillic), VectorIndex.add now
    actually called at runtime via vectorIndexAddGuarded helper with
    dim guard + input clip, migrateVectorIndex for dim migrations.
  - #326 (#277) — RetentionScore type no longer declares source
    twice; JSDoc back-compat note no longer shadowed.
  - #335 (#323) — viewer drops Google Fonts <link> (CSP-blocked),
    loadDashboard now surfaces load errors inline instead of
    sticking on "Loading dashboard…".

Bumping 0.9.11 -> 0.9.12 across the 9 standard files:
- package.json
- packages/mcp/package.json
- plugin/.claude-plugin/plugin.json
- plugin/.codex-plugin/plugin.json
- src/version.ts
- src/types.ts (ExportData.version literal)
- src/functions/export-import.ts (supportedVersions)
- test/export-import.test.ts (round-trip expectation)
- CHANGELOG.md (new 0.9.12 entry)

886 / 886 tests pass. Build clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Viewer dashboard tab stuck on 'Loading dashboard...'

1 participant