fix: authenticate the CARTO basemaps with the suite key - #12
Conversation
CARTO began watermarking unauthenticated basemaps.cartocdn.com raster
tiles with "API KEY REQUIRED" on 2026-08-26, so the national site-picker
renders defaced on first load, and the main map does too whenever a
visitor selects the "Light" basemap. The tile request still returns HTTP
200 with a valid PNG, so nothing errors or logs; full suite diagnosis in
NEON-Driver-Cascade docs/SUITE-BASEMAP-INCIDENT-2026-08.md.
Adds an add_suite_basemap() helper to global.R and routes all three
tile call sites through it. With CARTO_BASEMAP_KEY set (a Connect Cloud
content variable) it serves the exact same Positron tiles as before,
keyed, via addTiles() — addProviderTiles() cannot carry the key because
the bundled CartoDB template has no {apikey} placeholder. The key is a
public rate-limited identifier, not a credential: it rides in the
client-side tile URL by design, and the env var exists to keep it out of
git and make rotation a Connect setting rather than a release.
The helper accepts either a leaflet provider name or a CARTO variant, so
ui.R's Basemap choices vector is unchanged and the Esri Terrain and
Satellite options pass straight through to addProviderTiles() as before.
Without the key it falls back to Esri's keyless grey canvas so the map
degrades to a clean basemap rather than a defaced one, capped at that
canvas's real maxNativeZoom of 16.
manifest.json is intentionally not touched in this commit — it is
regenerated in the pinned CI validator per AGENTS.md, and the validated
candidate artifact from that run lands in the follow-up commit.
CI is red at the manifest byte gate, and this repo exports no artifact for me to shuttleFailing check: Everything before it passed — manifest generation, Why I can't complete it the way the siblings did. In Ground Beetle, Plant Diversity, Phenology, Vegetation and Small Mammal, CI uploads the validated manifest unconditionally, so I download that artifact and commit exactly those bytes — that is how Ground Beetle #22 went green and merged. This repo has only: - name: Upload unvalidated manifest for diagnosis
if: failure() && steps.manifest.outcome != 'skipped'That step sits before the byte gate, so when the gate is the only failure nothing has failed yet, the upload is skipped, and the generated manifest is discarded with the runner. I confirmed the run has zero artifacts. I can't regenerate it locally either — there's no R runtime in the patch environment, and the package To unblock, on this branchRscript --vanilla scripts/write_manifest.R
git commit -am "build: adopt the validated manifest for the basemap change"Optional, and worth considering separatelyMoving that upload above the byte gate and making it unconditional — as the five sibling repos already do — would let this repo self-serve a validated manifest on exactly this kind of run. I've deliberately not included that here: it's a CI change beyond what this PR is for, and it's your call. I'll keep watching and pick this up as soon as there's a new commit. Generated by Claude Code |
The basemap fix in this PR passed every substantive check — manifest generation, verify_bundle.R, sourcing the app offline, and rendering a real bundled site — and stopped only at "Require committed generated bytes to match", because manifest.json must come from the pinned validator rather than be written by hand. There was no way to complete that loop here. The existing upload is `if: failure() && steps.manifest.outcome != 'skipped'` and sits BEFORE the byte gate, so when the gate is the only failure nothing has failed yet, the upload is skipped, and the validated manifest is discarded with the runner. Five sibling repos (Ground Beetle, Plant Diversity, Plant Phenology, Vegetation Structure, Small Mammal) already upload theirs unconditionally, which is exactly how each of them reached green. Adds the same unconditional upload, matching the Ground Beetle step verbatim — same pinned action SHA, if-no-files-found: error, three-day retention. It is additive and placed after the app-render step and before the gate, so it weakens no check and changes no existing step; the diagnostic UNVALIDATED upload is left exactly as it was.
manifest.json must come from the pinned R 4.5.0 validator, not from a hand edit, so this is the artifact that run 33409103282 produced -- copied verbatim, byte-identical to the uploaded candidate. That run is the first one to export it: the preceding commit made the upload unconditional so the bytes survive a run whose only failure is the byte gate itself. The run was against this branch's exact head (43c8892), and its candidate is correct on the substance: all 112 file checksums in it match this tree, including the two the basemap change moves. global.R 171cc80c... -> db7dd408... server.R cc5212c5... -> fdb8660d... Everything else that differs from the previously committed manifest is a package "Built" timestamp -- 73 of them -- which records when the validator compiled each source package, not anything about this change. That is the known non-determinism this repo's byte-exact gate flaps on, and the reason the bytes have to be taken from the validator rather than reconstructed. NEON-Driver-Cascade's semantic compare_manifests.R is the fix; promoting it here is tracked in the suite lessons, not in this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
What
CARTO began watermarking unauthenticated
basemaps.cartocdn.comraster tiles with "API KEY REQUIRED" on 2026-08-26. The national site-picker renders defaced on first load, and the main map does too whenever a visitor picks the "Light" basemap. The request still returnsHTTP 200with a valid PNG, so nothing errored or logged.Full suite diagnosis:
docs/SUITE-BASEMAP-INCIDENT-2026-08.md· tgilbert14/NEON-Driver-Cascade#70. Proven first in Ground Beetle #22 — merged, deployed, live map confirmed correct.How
Adds
add_suite_basemap()toglobal.Rand routes all three tile call sites through it (server.R:537,:578,:588):CARTO_BASEMAP_KEYset (Connect Cloud content variable): the exact same Positron tiles as before, keyed, viaaddTiles()—addProviderTiles()cannot carry the key because the bundled CartoDB template has no{apikey}placeholder. Explicit OpenStreetMap + CARTO attribution, as CARTO's terms require.maxNativeZoom = 16, that canvas's real content limit.ui.Ris unchanged. The helper accepts either a leaflet provider name or a CARTO variant, so the Basemap choices vector stays as-is and the Esri Terrain/Satellite options pass straight through toaddProviderTiles()exactly as before.The key is a public, rate-limited identifier, not a credential — it rides in the client-side tile URL by design (CARTO's terms ban server-side proxying). The env var keeps it out of git and makes rotation a Connect setting rather than a release.
Manifest
manifest.jsonis intentionally untouched in the first commit — AGENTS.md forbids hand-editing it. The first CI run regenerates it in the pinned validator and uploads the validated manifest candidate artifact; the follow-up commit lands exactly those bytes. Expect the first run red at the byte-match gate — that is the designed flow, identical to how Ground Beetle went green.Before merging (owner)
CARTO_BASEMAP_KEY(thecb1_…key). Until it is set, the deployed app shows the clean Esri fallback rather than CARTO.Verified
grep -E 'CartoDB[."$]|cartocdn'finds no remaining unkeyed CARTO tile request; the onlyCartoDB.*strings left are theui.Rchoice labels, which now route through the helper.git diff --checkclean. No R interpreter in the patch environment, so CI's offline-source step is the syntax gate.🤖 Generated with Claude Code
https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
Generated by Claude Code