fix(maps): let users and admins enter a CARTO API key (#2060)

CARTO stamps an "API KEY REQUIRED" watermark onto every basemap tile
fetched without a key since 26.08.2026, and both CartoDB presets plus the
built-in fallback are CARTO. The key is free and needs no account, so add
it as a setting rather than dropping the provider.

carto_api_key sits next to mapbox_access_token: per user in Settings/Map,
instance-wide under Admin/User Defaults, encrypted at rest, injected from
CARTO_API_KEY on managed instances. Unlike Mapbox it never switches the
map provider, since a raster key says nothing about the renderer.

The key is appended as ?key= at render time instead of being stored in the
template, so a saved URL stays portable and survives a rotation. That is
also what reaches the four maps that pinned CARTO and ignored the user's
tile setting: atlas, collections, the public trip share and the public
journey. The two share pages read it from their token-gated payload, the
same way baseCurrency already travels.

Also lifts the release notice above page chrome. Its overlay sat at
z-index 50, under the Vacay mode toolbar at 61, so the dialog was neither
on top nor modal. Adds a six-step layer scale to index.css and moves the
three notice shells onto it.

Closes #2054
Closes #2052
This commit is contained in:
Maurice
2026-08-27 12:06:55 +02:00
committed by GitHub
parent 521e52c853
commit 1f66ef1d32
99 changed files with 1258 additions and 67 deletions
@@ -670,7 +670,7 @@ function MobileNoticeSheet(S: NoticeState) {
const nextNotice = notices[idx + 1] ?? null;
return (
<div className="fixed inset-0 z-50" role="presentation">
<div className="fixed inset-0 z-[var(--z-notice)]" role="presentation">
{/* Screen-reader page announcements */}
<span className="sr-only" role="status" aria-live="polite" aria-atomic="true">{pageAnnouncement}</span>
{/* Backdrop */}
@@ -842,7 +842,7 @@ function DesktopNoticeModal(S: NoticeState) {
return (
<div
className={`fixed inset-0 z-50 bg-slate-950/40 backdrop-blur-[2px] transition-opacity ${dur} ${ease} ${visible ? 'opacity-100' : 'opacity-0'}`}
className={`fixed inset-0 z-[var(--z-notice)] bg-slate-950/40 backdrop-blur-[2px] transition-opacity ${dur} ${ease} ${visible ? 'opacity-100' : 'opacity-0'}`}
role="presentation"
>
{/* Screen-reader page announcements */}
@@ -19,7 +19,9 @@
position: fixed;
inset: 0;
z-index: 50;
/* Above the navbar, the page-bound sticky bars and the page-owned
overlays: the notice is app-wide, so nothing a page draws may cover it. */
z-index: var(--z-notice, 99998);
display: flex;
align-items: center;
justify-content: center;
@@ -0,0 +1,39 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, it, expect } from 'vitest'
// Nothing in a rendered test can see the notice's stacking order: jsdom applies
// no author stylesheet and resolves no custom properties, so the two real files
// are the only place the answer lives. Issue #2052 is exactly that gap, the
// overlay sat at a bare 50 and the Vacay mode bar covered it.
describe('release notice css', () => {
// Vitest runs with the client package as its root, so cwd is stable here.
const read = (rel: string): string => readFileSync(resolve(process.cwd(), rel), 'utf8')
const notice = read('src/components/SystemNotices/releaseNotice.css')
const tokens = read('src/index.css')
const block = (selector: string): string => {
const at = notice.indexOf(selector)
expect(at, `${selector} missing from releaseNotice.css`).toBeGreaterThan(-1)
return notice.slice(at, notice.indexOf('}', at) + 1)
}
const step = (name: string): number => {
const found = new RegExp(`${name}:\\s*(\\d+);`).exec(tokens)
expect(found, `${name} missing from index.css`).not.toBeNull()
return Number(found![1])
}
it('FE-COMP-RELEASENOTICECSS-001: the overlay stacks from the token, not a bare 50', () => {
const overlay = block('.rn-overlay {')
expect(overlay).toMatch(/z-index:\s*var\(--z-notice/)
expect(overlay).not.toMatch(/z-index:\s*50\b/)
})
it('FE-COMP-RELEASENOTICECSS-002: the notice step clears the navbar and the Vacay toolbar', () => {
// The Vacay calendar's mode bar is sticky at 61 and no stacking context
// separates the two, so it comes down to a plain number comparison.
expect(step('--z-notice')).toBeGreaterThan(step('--z-nav'))
expect(step('--z-notice')).toBeGreaterThan(61)
})
})