Skip to content

docs: diagnose the CARTO basemap outage, adopt the CARTO key, and record the nine-app rollout - #70

Merged
tgilbert14 merged 10 commits into
masterfrom
claude/neon-maps-api-key-eocg8r
Aug 31, 2026
Merged

docs: diagnose the CARTO basemap outage, adopt the CARTO key, and record the nine-app rollout#70
tgilbert14 merged 10 commits into
masterfrom
claude/neon-maps-api-key-eocg8r

Conversation

@tgilbert14

@tgilbert14 tgilbert14 commented Aug 28, 2026

Copy link
Copy Markdown
Owner

What happened

The maps across the NEON explorer suite started showing "API KEY REQUIRED — carto.com/basemaps/apikey" stamped across the basemap.

Root cause: CARTO now burns that watermark into unauthenticated raster tiles from basemaps.cartocdn.com, server-side. The tile request still returns HTTP 200 with a valid PNG, so nothing errors, logs, or falls back — the map simply renders defaced.

curl -s -o carto_light.png "https://a.basemaps.cartocdn.com/light_all/6/13/24.png"   # CartoDB.Positron
curl -s -o carto_dark.png  "https://a.basemaps.cartocdn.com/dark_all/6/13/24.png"    # CartoDB.DarkMatter

Blast radius: all nine companion apps, on first load — every one puts CartoDB.Positron on its landing/site-picker map, which needs no interaction. Driver-Cascade itself ships no Leaflet map and is unaffected.

Decision: take the free CARTO key — ADOPT

Authenticate the existing basemaps rather than replace them. Keeping CartoDB.Positron and CartoDB.DarkMatter is the only option that changes nothing visually: same tiles, same maxZoom = 20, same retina, zero blank tiles, no palette or CSS re-tuning, no contrast or attribution regression, and ground-beetle's dark theme keeps working untouched.

One step no agent can do: request the key at https://carto.com/basemaps/apikey — email + domain + one-line description, emailed straight back, no account, no approval queue, 5M tiles/month fair use.

Why not swap the provider — the first plan was refuted

The initial plan (blanket CartoDB.PositronEsri.WorldGrayCanvas plus a CSS-invert dark canvas) went through a nine-app audit and a four-lens adversarial review. All four lenses refuted it. Two findings were re-verified by hand:

1. The replacement is measurably BLANK at plot scale. Canvas/World_Light_Gray_Base has had no content update since 2021.

Tile (SCBI) Distinct RGB values
Light Gray Base z13 224 — hairline roads, no labels
Light Gray Base z16 1 — a single flat RGB(239,239,239)
World_Topo_Map z13 3,912
World_Topo_Map z16 717

Across all 46 NEON terrestrial sites, 17 (37%) return a single-colour blank tile at z16. Every other candidate — Esri Topo/Imagery, USGS, OSM, CARTO — is 0/46. The maxNativeZoom = 16 guard upscales an already-blank tile.

2. There IS a keyless dark canvas. Canvas/World_Dark_Gray_Base is real, keyless and labelled — so the CSS-invert workstream was unnecessary. The earlier "no keyless dark canvas exists" claim was false: it asked what leaflet-providers exposes, never what the provider serves.

Plus: the swap makes marker contrast ~8–10% worse; leaflet.providers emits a stale Esri attribution omitting the OpenStreetMap credit the service's own copyrightText requires; and one app uses leaflet::providers$CartoDB.Positron in object form, which a string grep misses.

Every one of those defects exists because the alternatives moved the basemap. The key doesn't.

The code

addProviderTiles() cannot carry the key — the pinned CartoDB template has an {r} retina slot but no {apikey} placeholder — so it needs a raw addTiles() with attribution by hand. §4.3 adds an add_suite_basemap() helper: keyed CARTO when the key is present, keyless Esri canvas when it isn't, so a missing or revoked key degrades to a clean canvas rather than a defaced one.

The key is not a secret. It rides in the tile URL and every request is client-side, so it lands in page source regardless; CARTO's terms §9.c bans server-side proxying, so it cannot be hidden. Sys.getenv("CARTO_BASEMAP_KEY") buys exactly two things — it keeps the key out of nine public git histories, and makes rotation a Connect Cloud setting instead of nine releases.

No negative test exists: an invalid key returns the byte-identical watermarked tile as no key at all (same ETag). Only the real key proves the URL form — smoke-test once before any rollout PR.

Rollout constraints found (§5)

  • The manifest is the real gate. ui.R/server.R/R/*.R/global.R are all on each repo's deploy surface with per-file checksums, so the manifest must be regenerated in the same commit. Never hand-edit it — Small Mammal and Vegetation carry a blessed regenerate-manifest.yml for exactly this.
  • neon-my-little-inverts and neon-waterchemistry-analyte-viewer-app have no ci.yml but still ship a manifest — nothing catches a stale one. Do them last.
  • No test anywhere in the suite asserts a provider string, and CI never fetches a tile. That is why this shipped unnoticed.
  • Small Mammal DEPLOY.md:9-13 forbids automation pushing to main — open PRs, do not merge them.

Changes

  • docs/SUITE-BASEMAP-INCIDENT-2026-08.md — diagnosis, non-causes eliminated with evidence, the refutation and its measurements, the decision, the helper code, per-repo rollout gates, all 22 call sites, and a resume checklist.
  • .claude/agents/LESSONS.md — two lessons: a free tile provider is an uncontracted runtime dependency no manifest pin covers; and "unwatermarked" is not "usable" — decode the tile and count colours, because HTTP 200 proves neither.
  • docs/BUILD-TEST-HANDOFF.md — three dated [Claude] entries.

Deploy safety

Docs only. docs/ and .claude/ are outside the DEPLOY_APP_FILES allowlist, so no artifact, manifest, or deploy surface moves.

Known limitation

No headless-browser check was possible in this container (Chromium has no egress; every navigation fails ERR_CONNECTION_RESET, including example.com). All evidence is tile-level and source-level — decisive for the cause and the blanking, but nobody has yet seen a fixed app.

🤖 Generated with Claude Code

https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1

Claude and others added 2 commits August 28, 2026 18:02
CARTO began burning an "API KEY REQUIRED" watermark into unauthenticated
raster tiles from basemaps.cartocdn.com, server-side. The request still
returns HTTP 200 with a valid PNG, so nothing errors, logs, or falls back
— the map just renders defaced. All nine companion apps are hit on first
load, because every one of them puts CartoDB.Positron on its landing
site-picker map. Driver-Cascade itself ships no Leaflet map and is
unaffected.

Adds docs/SUITE-BASEMAP-INCIDENT-2026-08.md with the verified diagnosis,
the reproduction, the non-causes eliminated with evidence (package
versions, referer/CORS, CSP, app strings), the per-app call-site table,
the decided fix (Esri.WorldGrayCanvas plus a CSS-inverted tile pane for
dark), the rejected alternatives, and a resume checklist. Records the
lesson and a dated handoff entry.

Docs only: docs/ and .claude/ are outside DEPLOY_APP_FILES, so no
artifact, manifest, or deploy surface moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
A nine-app audit and a four-lens adversarial review refuted the fix
recorded in the previous commit. The diagnosis is unchanged; the remedy
moved.

Measured: Canvas/World_Light_Gray_Base has had no content update since
2021. At z16 it returns a single-colour blank tile for 17 of NEON's 46
terrestrial sites; SCBI z15 and z16 are byte-identical; above z16 every
request is the same "Map data not yet available" placeholder. Verified by
hand for SCBI (z13 = 224 distinct colours, z16 = 1). Every other
candidate tested is 0/46 blank. So the swap is right for the national
pickers at z2-5 and wrong for every plot-scale map, and the proposed
maxNativeZoom guard upscales an already-blank tile rather than fixing it.

The CSS-invert dark workstream is cancelled, superseded rather than
deferred: Canvas/World_Dark_Gray_Base is a real, keyless, labelled dark
canvas, verified live. The earlier "there is no keyless dark canvas"
claim was false — it asked what leaflet-providers exposes, never what the
provider serves.

Three further defects: the swap makes marker contrast ~8-10% worse, not
better; leaflet.providers emits a stale Esri attribution that omits the
OpenStreetMap credit the service's own copyrightText requires, trading a
compliant credit line for a non-compliant one; and one app uses the
object form providers$CartoDB.Positron, which a quoted-string grep
misses.

Replaces the plan with a per-map-role one, records the 22 call sites
across 9 repos, maps the gate asymmetry (two repos have no ci.yml), and
raises two owner questions that block rollout.

Docs only: docs/ and .claude/ are outside DEPLOY_APP_FILES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
@tgilbert14 tgilbert14 changed the title docs: record the CARTO basemap API-key outage across the suite docs: diagnose the CARTO basemap outage, and correct the first fix Aug 28, 2026
Owner decision: authenticate the existing basemaps rather than replace
them. Keeping CartoDB.Positron and CartoDB.DarkMatter is the only option
that changes nothing visually — same tiles, same maxZoom 20, same retina,
zero blank tiles, no palette or CSS re-tuning, no contrast or attribution
regression, and ground-beetle's dark theme keeps working. Every defect in
the refutation exists because the alternatives moved the basemap.

Records the key mechanics: it is a ?key= query parameter, and
addProviderTiles() cannot carry it because the pinned CartoDB template
has an {r} slot but no {apikey} placeholder, so it needs a raw addTiles()
with attribution by hand. Adds an add_suite_basemap() helper, keyed with
a keyless Esri-canvas fallback so a missing or revoked key degrades to a
clean canvas instead of a defaced one.

States plainly that the key is not a secret: it rides in the tile URL and
every request is client-side, and CARTO's terms ban server-side proxying,
so it cannot be hidden. Sys.getenv keeps it out of nine public git
histories and makes rotation a Connect Cloud setting — nothing more.

Adds the rollout section: every companion repo has a byte-exact manifest
gate and all app sources are on the deploy surface, so the manifest must
be regenerated in the same commit, never by hand. Two repos have no CI at
all and ship a manifest nothing verifies. Small Mammal forbids automation
pushing to main, so this opens PRs rather than merging them.

Retires the Esri.WorldGrayCanvas swap and the CSS-invert dark canvas, and
keeps the per-role split only as the fallback path.

Docs only: docs/ and .claude/ are outside DEPLOY_APP_FILES.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
@tgilbert14 tgilbert14 changed the title docs: diagnose the CARTO basemap outage, and correct the first fix docs: diagnose the CARTO basemap outage and adopt the free CARTO key Aug 28, 2026
Claude and others added 2 commits August 28, 2026 22:24
Tested a Workspace API Access Token scoped to the Maps API against the
raster basemap CDN at confirmed origin cache misses under five auth
forms; every response was still watermarked. The token is valid, but the
maps scope covers CARTO's Maps API, not the public basemap CDN, and
CARTO's key page confirms platform credentials only cover in-platform
use. External embedding needs the form-issued basemap key. Also records
that the CDN ignores the query string in its cache key, so a key smoke
test must use an origin-MISS tile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
HubSpot's API-submission path for the basemap key form returns
FORM_HAS_RECAPTCHA_ENABLED, so the form must be human-submitted in a
browser. The page's own fallback is support-basemaps@carto.com, which
issues a key by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
The owner obtained the basemap key via the form; it verifies clean at
CDN origin misses for both variants, and previously-cached watermarked
zooms return clean with it, so the CDN caches keyed responses
separately. Records the canary flow: code first, manifest from the
pinned CI validator's artifact in a follow-up commit, first run
intentionally red at the byte gate. The key itself lives in Connect
Cloud variables and the owner's email, never in any repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
Claude and others added 4 commits August 29, 2026 02:49
The Ground Beetle canary merged, deployed green, and the owner confirmed
the live map, which closed the last unproven link — the Connect variable
really does reach the running app. All eight remaining repos are now
patched with PRs open.

Records the rollout table, the split default branches checked per repo,
and the two no-CI repos handled differently from each other and from the
rest: Inverts source-only because its AGENTS.md forbids regenerating the
manifest and its release identity hashes the source, Water Chemistry with
its one MD5 updated because a stale checksum there would silently drop
the fix.

Also notes why no ui.R dropdown needed changing: the helper accepts a
provider name or a CARTO variant, so choice vectors and defaults stay
exactly as they were.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
Six of nine are green or merged. Every manifest shuttle showed the same
thing: manifest.json was the only file that differed, with all data,
indexes and receipts already byte-identical to the branch.

Three are blocked on the owner for two distinct reasons. Mosquito is a CI
shape gap — its artifact upload is conditional on an earlier failure and
sits before the byte gate, so the validated manifest is discarded exactly
when it is needed; five siblings upload unconditionally. Breeding Birds
and Inverts bind the app source into generated authority, so regenerating
needs R in the pinned validator; both PRs carry the validator's own
commands verbatim.

Also corrects a mistake worth keeping: no ci.yml is not no CI. Inverts
and Water Chemistry both validate from refresh-data.yml, and Water
Chemistry's connect_cold_start actually cold-boots the deploy bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
…nd the last two shuttles

Adds section 10 to the incident record and three lessons.

Two problems the rollout had not anticipated:

- Breeding Birds' master carries an unresolved merge -- nine conflict-marker
  lines inside manifest.json's files map -- so the file is invalid JSON on the
  branch Connect Cloud watches, and CI failed on it. Cause and blast radius
  documented; the repair rides in PR #6.

- Water Chemistry's map is blank for a reason that is not in the code. The
  live app provably runs the merged bytes and all three tile endpoints answer
  200, which leaves the key's value: a missing key is loud (CARTO's watermark),
  a whitespace-padded one is silent. That asymmetry is the lesson worth keeping.

Also records how Birds' authority was regenerated without an R runtime -- by
reproducing the known-good stamp byte-for-byte first -- and how the Mosquito
and Inverts shuttles were validated.

Flags the outstanding work plainly: the hardening is in one repo, the other
eight still carry the unhardened helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
…llout

Adds section 10.7 and brings the rollout table up to date.

Birds' rerun failed at the second stamp verify, and the failure named a
real design flaw rather than a mistake: schema-v3 hashes the whole packages
block into manifest_contract_sha256, so package metadata the change never
touched invalidates the release identity.

The open question was whether adopting the validator's manifest converges
or loops. It converges, and that is now measured rather than assumed: Birds
and Mosquito, different repos and different runners, produced byte-identical
Built values for all 83 packages they share, in the same 2026-08-12 window,
and Mosquito's rerun after shuttling went green. Those are Posit Package
Manager's binary build times for the pinned snapshot, not per-run compiles.

Worth keeping for connor/neonize: in Mosquito this drift only trips a
byte-diff gate, but in Birds it corrupts the release identity of an
unchanged app -- the strongest case yet for promoting compare_manifests.R.

Also records how to spot a Connect variable that was missed: that app comes
up on the grey Esri canvas instead of Positron.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01738HeGPEU9NEoczL8RXnu1
@tgilbert14 tgilbert14 changed the title docs: diagnose the CARTO basemap outage and adopt the free CARTO key docs: diagnose the CARTO basemap outage, adopt the CARTO key, and record the nine-app rollout Aug 31, 2026
@tgilbert14
tgilbert14 marked this pull request as ready for review August 31, 2026 16:31
@tgilbert14
tgilbert14 merged commit e5151fa into master Aug 31, 2026
7 checks passed
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.

2 participants