Skip to content

Dashboard loads webfonts from fonts.googleapis.com; degrades on offline / egress-filtered networks #685

Description

@EnmaJim

What happened

Driving the dashboard in Google Chrome stable, two font requests fail:

GET https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2
    — net::ERR_FAILED
GET https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwg.woff2
    — net::ERR_FAILED

Source — src\ui\static\css\base.css:5:

@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap');

So a dashboard that otherwise runs entirely on localhost makes an outbound request to
fonts.googleapis.com (and then fonts.gstatic.com) on every page load. Two consequences:

  1. Offline / air-gapped / egress-filtered environments. The requests fail and the CRT-style
    typography the design depends on silently falls back to system fonts. The dashboard remains usable —
    this is not a functional break — but it does not look as intended, and there is no indication why.
  2. An unnecessary third-party dependency for a local developer tool. Every dashboard load tells
    Google that someone opened it. For a tool whose stated model is local-first and whose own docs
    emphasise an auditable local trail, that is surprising.

JetBrains Mono and Inter are both SIL Open Font License, so they can be vendored without licensing
friction.

What you expected

The dashboard renders identically with no network access. Self-host the two families under
src/ui/static/ (subset woff2, @font-face with font-display: swap) and drop the remote @import,
keeping a system-font stack as the fallback.

If a remote fetch is deliberate, it should at least be degradable on purpose — a local @font-face
fallback so the intended typography survives without egress.

Steps to reproduce

$env:DOTBOT_HOME = '<dotbot checkout>'
cd <an initialised dotbot project>
& $env:DOTBOT_HOME\bin\dotbot.ps1 go
# read the port from .bot\.control\ui-port, then open http://localhost:<port>

In Chrome DevTools → Network, filter gstatic: two woff2 requests are issued on load. To see the
failure mode directly, block fonts.googleapis.com and fonts.gstatic.com (DevTools → Network request
blocking, or an offline host) and reload — the requests fail and the fonts fall back.

Scripted, with Playwright against Chrome stable:

const failed: string[] = [];
page.on("requestfailed", r => failed.push(`${r.method()} ${r.url()}${r.failure()?.errorText}`));
await page.goto("/", { waitUntil: "domcontentloaded" });
// -> failed contains the two fonts.gstatic.com woff2 requests

Or with no browser at all:

Select-String -Path "$env:DOTBOT_HOME\src\ui\static\css\base.css" -Pattern 'fonts.googleapis.com'

Environment

OS: Windows 11 Pro 10.0.26200 | dotbot v4.0.2 (main @ 7c95b466)
pwsh 7.6.4 | Google Chrome stable (channel: "chrome" via Playwright 1.56.1)
Harness: standalone Playwright project outside DOTBOT_HOME; 4 of 5 checks passed
Network: outbound to fonts.gstatic.com blocked in this environment

Severity

low

Logs / screenshots

The rest of the dashboard checks passed in the same run, so this is isolated to the font fetch:

✓ /api/info and /api/state return 200 with a non-empty body
✓ CSRF guard refuses a state-changing POST without X-Dotbot-Request (403 "Missing CSRF header")
✓ upload filename sanitiser resists ../ traversal, backslash traversal, CON.md, invalid chars
✓ a second browser tab attaches without errors
✗ page load reports 2 failed requests — both fonts.gstatic.com woff2

Page title: "DOTBOT // Control Panel"
No uncaught JavaScript errors; no HTTP >= 400 from the dotbot server itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status
    Inbox

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions