Fixes#1614
## Problem
`window.getTileUrl()` in `public/roles.js` returned the active
provider's `url` property as-is. After #1533 added carto/osm/stamen
providers with lazy-resolved URLs (`url: function () { ... }`), the
helper returned the function itself instead of a URL template string.
Callers handed that function to `L.tileLayer()`, which stringified the
source as the template — every tile 404'd, the map went blank, and
Leaflet logged no error.
User-visible impact: node-detail inset map and analytics minimap
rendered zero tiles whenever a function-`url` provider was the active
dark-theme pick.
## Root cause
`public/roles.js:365-381` — `return p.url || p.baseUrl;` with no `typeof
=== 'function'` invocation. The provider registry in
`public/map-tile-providers.js:45-53` declares almost every provider with
`url: function() { ... }` for lazy config resolution (cartocdn domain,
OSM provider/token, Stamen API key).
## Fix
One-line change in the consumer (`getTileUrl()`). Invoke `url` /
`baseUrl` if it's a function; otherwise return it verbatim.
`map-tile-providers.js` is not touched — it remains the source of truth
for the lazy-resolver pattern.
```js
var u = p.url || p.baseUrl;
return (typeof u === 'function') ? u() : u;
```
## Callers reviewed
| Caller | Disposition |
| --- | --- |
| `public/nodes.js:94` (`_applyTilesToNodeMap`) | Routes through
`window.getTileUrl()` → fixed transitively |
| `public/analytics.js:2055` (`L.tileLayer(getTileUrl(), …)`) | Routes
through `getTileUrl()` → fixed transitively |
| No other `getTileUrl()` callers | `grep -n "getTileUrl\b" public/*.js`
confirms only the two above |
## Commits (red → green)
- `a2b23392` — `test(#1614): red — getTileUrl() must return string, not
function` — adds `test-issue-1614-tile-url-function.js`. Verified to
fail on assertion (not build error) before the fix landed; passes after.
- `26fcacd1` — `fix(#1614): invoke provider url() when it's a function`
— minimal one-line fix in `roles.js` plus wiring the new test into
`deploy.yml` and `test-all.sh`.
## Tests
Unit test asserts the public contract from three angles so any
regression of either branch fails CI:
1. Dark + `url: function()` → returns a string template containing
`{z}/{x}/{y}`.
2. Dark + `url: 'https://…'` → returns the string verbatim (no
double-invoke).
3. Dark + `baseUrl: function()` fallback → also invoked, also returns a
string.
Wired into CI via `.github/workflows/deploy.yml` and `test-all.sh`.
## E2E coverage
Skipped intentionally. The existing Playwright harness
(`test-e2e-playwright.js`) runs against a deployed BASE_URL and is not
invoked from the Go CI workflow (`deploy.yml`). Adding a new E2E flow
there would require standing up a leaflet/tile-loading harness for a
single one-line regression. The unit test covers the exact
`getTileUrl()` contract that this bug violates and would have caught it;
if reviewers want a Playwright assertion later we can add it as a
follow-up. Manual verification was performed against staging
(`http://analyzer-stg.00id.net/#/nodes/...`).
## Preflight
`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`
— clean (all gates pass, PII clean, red commit verified).
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>