# BPFmap · Pre-Launch Audit · 2026-05-11

Consolidated report from 4 parallel subagent audits on [BPFmap.html](BPFmap.html). Festival window: **9–18 October 2026** — ~5 months runway.

**Scope respected:** no new feature suggestions, no accessibility, no refactor proposals for B4/P25 (post-festival CSS specificity debt).

---

## Executive summary

| Dimension | Verdict | Ship-blockers | High | Medium/Low |
|---|---|---|---|---|
| Cost (Mapbox) | ✅ **SAFE** | 0 | 1 (token restriction) | 4 |
| Security | 🔴 **BLOCKED** | 1 (PAT exposed) | 2 (XSS, `new Function`) | 3 |
| Functional/Stability | 🟠 **HIGH risk, no true blockers** | 0 (DEBUG_TODAY = pre-launch toggle, intentional) | 4 | 5 |
| UI/UX polish | 🟡 **GOOD craft, polish needed** | 0 | 9 | 13 |
| Strategic UX / category | 🟠 **HIGH craft, structural gaps** | 0 | 5 mandatory features | — |
| **TOTAL** | 🔴 Cannot launch publicly today | **2 unique** | **~16** | **~25** |

**Big picture:** Craftsmanship is high (motion system, cinematic gradients, observer-locked re-applies). What's blocking launch are 3 concrete, fixable issues — not architectural problems. Realistic time to "shippable": **1–2 focused sessions** + a small proxy deploy.

---

## 🔴 SHIP-BLOCKERS (must fix before any public link)

### SB-1 · Airtable PAT exposed in client
- **Location:** [BPFmap.html:5736-5737](BPFmap.html#L5736)
- **Risk:** Anyone viewing source has full read/write/delete on base `appQ7B5TqP7RRK5Xp`. Token already exists in Google Drive sync + has been exposed via Cloudflare tunnel links → treat as **leaked**.
- **Action plan:**
  1. ⬜ Revoke token in [Airtable Developer Hub](https://airtable.com/create/tokens)
  2. ⬜ Create new PAT, scoped **read-only** on this base only
  3. ⬜ Stand up Cloudflare Worker proxy (15 min) that holds the token server-side
  4. ⬜ Replace client `fetch(api.airtable.com/...)` with `fetch(your-worker.dev/api/...)`

### SB-2 · XSS sink — Airtable content into `innerHTML` without escaping
- **Locations (sample):** [6455](BPFmap.html#L6455), [6761](BPFmap.html#L6761), [7271](BPFmap.html#L7271), [8231](BPFmap.html#L8231), [10220](BPFmap.html#L10220), [10840](BPFmap.html#L10840), [13599](BPFmap.html#L13599) + ~25 more sites
- **Risk:** Combined with SB-1 — anyone with stolen PAT can inject `<img src=x onerror=...>` into a venue name → runs JS in every visitor's browser at festival.
- **Fix:** add an `escapeHTML(s)` helper, wrap every interpolated Airtable field. Special case: `festival_info_title` uses `\n → <br>` — escape first, then replace.
- ⬜ Done (estimate: 1 helper + ~30 wrappings, automatable)

---

## 🧭 Pre-launch toggles (dev affordances — intentional now, MUST flip before public launch)

| Item | Location | Dev value | Launch value | Notes |
|---|---|---|---|---|
| `DEBUG_TODAY` | [BPFmap.html:7102](BPFmap.html#L7102) | `"11 Oct"` (mid-festival simulation) | `""` (use real `new Date()`) | User cannot test festival states without this; flip on launch day |
| `TEST_ORIGIN` (geolocation) | [BPFmap.html:7869](BPFmap.html#L7869) area | Hardcoded Bucharest coords | Remove / use real `navigator.geolocation` | User not in Bucharest during dev; festival visitors will be on-site |
| Any other dev/debug flags | TBD | TBD | TBD | Audit before launch with grep `DEBUG_`, `TEST_`, `FAKE_`, `MOCK_` |

→ Suggest a single `__DEV_FLAGS__` block at top of file with all dev affordances, easy to flip in 1 commit before launch.

---

## 🟠 HIGH (fix before launch if possible — not blockers, but quality concerns)

### Security
- **H-S1** · **Mapbox token unrestricted** — Public `pk.*` token at [7760-7774](BPFmap.html#L7760) needs HTTP referrer restriction in Mapbox dashboard. Without it, anyone scraping the page can drain your quota. *Action: 2 min in Mapbox console.*
- **H-S2** · **`new Function()` used as global-scope reader** — ~17 sites ([13822, 14130, 14710, …](BPFmap.html#L13822)). Currently safe (hardcoded names) but fragile + blocks strict CSP. *Fix: replace with `window[name]` lookups.*

### Functional/Stability
- **H-F1** · **20+ boot setIntervals never time out** — [8893, 9061, 9317, 9504, …](BPFmap.html#L8893) — if Leaflet CDN flakes, phones burn battery on infinite polls. *Fix: wrap each in `setTimeout(() => clearInterval(_boot), 10000)`.*
- **H-F2** · **MutationObservers without disconnect** — V373 [14879-14898](BPFmap.html#L14879) + V381 [16120-16136](BPFmap.html#L16120). Re-attach on every welcome reappear. Compound CPU on long sessions. *Fix: store observer, `_obs?.disconnect()` before recreate.*
- **H-F3** · **Geolocation prompted twice on `#centerBtn`** — V378 [14020](BPFmap.html#L14020) + V331 [7436](BPFmap.html#L7436). Two parallel native dialogs on iOS can lock UI. *Fix: V378 reuses V331 success path.*
- **H-F4** · **Older V330 search handler still wired** — [6508-6525](BPFmap.html#L6508) uses `Number(el.dataset.id)` → NaN for string IDs. V375 fix at [14244](BPFmap.html#L14244) is canonical. *Fix: remove lines 6508-6525.*

### UI/UX
- **H-U1** · **No iOS safe-area handling** — viewport meta at [line 5](BPFmap.html#L5) lacks `viewport-fit=cover`; zero `env(safe-area-inset-*)` anywhere. Topbar clipped under notch, controls under home indicator. *Fix: meta tag + `padding: max(N, env(safe-area-inset-top))` on topbar/sheet.*
- **H-U2** · **Twin CTAs in venue sheet** — `Directions` + `Navigation` adjacent at [5260-5261](BPFmap.html#L5260). Near-identical meaning, confusing. *Fix: keep Directions primary; demote Navigation to small `Open in Maps ↗` link inside directions panel.*
- **H-U3** · **Three coexisting toast implementations** — [10127, 13940 override, 29301, 30908](BPFmap.html#L10127). Different bg, positions, durations. *Fix: consolidate to one `bpfToast(msg, ms)` helper using `.bpf-toast` class as base.*
- **H-U4** · **Airtable fetch silent failure on slow networks** — [6067-6099](BPFmap.html#L6067). Partner on hotel WiFi sees seed data forever. *Fix: skeleton shimmer after 600ms + silent toast on `catch`.*
- **H-U5** · **Stale `placeholder` ships then JS overrides** — HTML at [5064](BPFmap.html#L5064), JS at [13859](BPFmap.html#L13859). First-paint flash of old copy. *Fix: update HTML attribute directly.*
- **H-U6** · **`fitCity` divides by empty bounds** — [6160-6172](BPFmap.html#L6160) crashes if venues array empty. *Fix: guard `if (!venues.length) return setView(FALLBACK_CENTER, 13);` at top.*
- **H-U7** · **`FEST_START`/`FEST_END` redeclared 5×** — [14829, 14947, 15964, 16540, 17277](BPFmap.html#L14829). Fragile. *Fix: single top-level `const`, remove duplicates.*
- **H-U8** · **Filter menu casing mismatch** — `Open now` at [5073](BPFmap.html#L5073) vs `Open Now` everywhere else. *Fix: title-case.*
- **H-U9** · **"Festival ended" copy drift** — `Festival ended` vs `Festival ended — see you next year`. *Fix: pick one constant (warmer recommended).*

---

## 🟡 MEDIUM / LOW (post-festival or nice-to-have)

### Performance / behavior
- Filter-state polling every 250ms ([13869](BPFmap.html#L13869)) + programme-bg every 200ms ([30103](BPFmap.html#L30103)) — battery drain. Drive from renderAll wrapper instead.
- Geolocation timeout/maximumAge inconsistent across 3 sites (8s/6s/8s, 60s/120s/60s).
- `window.innerHeight * 0.80` ([9092, 14808](BPFmap.html#L9092)) drifts on iOS Safari URL bar collapse. Cache at sheet-open.
- Welcome overlay observer fires 3× at 200/800/1500ms regardless of success.

### Visual polish
- **Token drift**: cream (`247,224,146` vs `245,229,163` at [13718, 15449](BPFmap.html#L13718)); navy ~19 distinct shades; coral `#b66a4c` hardcoded 5× with `!important`. *Define `--bpf-cream`, `--bpf-navy`, `--bpf-coral` at top.*
- Marker selection feedback too subtle (`scale(1.02)`) at [2186-2192](BPFmap.html#L2186). Bump to 1.06 + 1px cream ring on `:hover`.
- `.popup-close` 16px glyph + 0.58 opacity reads as decoration. Raise to 0.78 + ≥40px hit area.
- Welcome overlay has no close × — discoverability gap if user lands mid-explore.
- Sub-10px fonts in 3 contexts ([286, 539, 556, 557](BPFmap.html#L286)). iOS auto-shrinks in landscape.
- 86 transitions with ≥4 easings — standardize on `cubic-bezier(.22,.8,.24,1)`.
- Welcome 3-CTA flex: 1 each — give `Explore the map` `flex: 1.4` to lead.

### Security hardening (post-launch OK)
- Add `Content-Security-Policy` meta (after `new Function` refactor, can drop `'unsafe-inline'` from script-src).
- `Referrer-Policy: strict-origin-when-cross-origin`, `X-Frame-Options: DENY` via host/CDN.
- Subresource Integrity hashes on Mapbox CDN script.
- Strip `console.log` debug dumps before prod.

### Cost optimizations (from #1 — all SAFE-tier polish)
1. **Session route cache** on [7758-7799](BPFmap.html#L7758) — eliminates duplicate Directions clicks (30-60% reduction).
2. **Remove `await refreshSettings()`** from inside `fetchStrictMapboxRoute` ([7759](BPFmap.html#L7759)) — refresh once at boot or 5-min TTL.
3. **Double-click guard** on `#directionsBtn` ([7902-7913](BPFmap.html#L7902)) — `__routeBusy` flag.
4. **Persist route cache** to `sessionStorage` — survives soft reloads.

---

## 🧭 Strategic UX audit — personas, category benchmark, mandatory features

**TL;DR:** Craft is high — motion, gradients, editorial typography read like a film festival, not a civic map. Distinctive strength: V381 welcome editorial is genuinely cinematic, better than any festival map app on the market. **Biggest UX gap:** there's no answer to the only question 80% of visitors ask — *"what's happening near me, right now, that I can still catch?"* Top mandatory feature: a true "Tonight / Now" surface that turns the map from a directory into a guide.

### Personas — first 2 min on site

| Persona | First impression (10s) | First friction (2 min) | Abandon risk |
|---|---|---|---|
| **A · Tourist 10am Sat** | Welcome overlay carries them; "not a Google Map, it's a festival" | Twin Directions/Navigation hesitation; can't tell which event is on TODAY in a venue | N (welcome saves it) |
| **B · Local cinephile 9pm Tue** | Dismisses welcome fast | `Open Now` filters *venues open*, not *events happening*. Wants list view "4 things in next 3h sorted by distance" — doesn't exist | **Y, low-medium** — loyalty carries them but printed flyer beats app for speed |
| **C · Press/partner desktop** | Welcome holds up at 1440px, looks elegant | No deep link to share. No press kit. No "view all photographers" list. Density too low for laptop | **Y** — keeps tab open, writes from PDF programme |

### Category benchmark — what BPFmap loses vs. what it wins

| Reference | Loses on… | Wins on… |
|---|---|---|
| **SXSW GO** | "Now / Next" feed (60-min sessions) | Sense of place — map is hero |
| **Glastonbury app** | Personal schedule + conflict warnings | Editorial tone (Glasto is corporate-utilitarian) |
| **Burning Man Pocket Guide** | Offline-first | Visual polish (Pocket Guide is functional-ugly) |
| **Tribeca FF** | Inline trailers + add-to-calendar + ticket flow | Map-native discovery (Tribeca buries the map) |
| **Smartify** | Saved/shortlist per artwork | Citywide orchestration (Smartify is per-venue) |
| **Atlas Obscura** | Editorial blurbs + photos per location | Real-time festival pulse (Atlas is evergreen) |
| **Citymapper** | ETA confidence + multi-stop routing | Cultural framing (RDP V355) |
| **Mubi (tone)** | — | Equal — V381 welcome copy is in the same league |

### Real gaps (structural, not nitpicks)

1. **No "Now / Tonight" view** — time is treated as a filter property, not as the primary axis. A festival is fundamentally *time-shaped*; this is the single biggest miss.
2. **No save / return** — P17 still a placeholder toast. Table stakes for 10-day festival.
3. **No add-to-calendar per event** — P7 backlog; ICS code already exists at line ~7097 but unwired.
4. **No share / deep link** — P20 backlog. Press blocked, word-of-mouth blocked.
5. **Filter language confuses "venue open" with "event happening"** — semantic mismatch will surface on every support DM.
6. **Programme is a flat 10-day list** — no "Today" pre-selected, no time-of-day grouping. Reads like a PDF, not a live app.
7. **No editorial pick on welcome** — P14/P16 backlog. Welcome could carry "Tonight's pick: Juhan Kuus at Cinema Elvire Popesco · 8pm".

### 🎯 Mandatory missing features (5, prioritized)

| # | Feature | Scope | Verdict | Why mandatory |
|---|---|---|---|---|
| **MF-1** | **"Tonight" surface** — chronological list of events in next ~6h, grouped `On now` / `Starting soon` / `Later tonight`, sorted by distance | M (~6-8h) | **Pre-launch · MUST** | Persona B fails without it; the single feature that turns "map of festival" into "guide to tonight". Every category reference has this. |
| **MF-2** | **Saved / Shortlist real impl** (P17) — bookmark on venue+event, panel listing, localStorage persisted, optional share-saved-URL | S (~3-4h) | **Pre-launch · MUST** | UI scaffolding exists. Toast placeholder is making a promise the app breaks. |
| **MF-3** | **Add to calendar per event** (P7) — `+ Calendar` button per event row, ICS already coded at ~line 7097 | S (~1-2h) | **Pre-launch · MUST** | Self-inflicted gap — implementation already exists, just needs wiring. |
| **MF-4** | **Deep link / share URL** (P20) — `?venue=…`, `?event=…`, Web Share API on venue sheet | S-M (~2-3h) | **Pre-launch · MUST** | Press persona blocked. Connective tissue to InLight (cross-app deep linking). Instagram bio links. |
| **MF-5** | **Programme defaults to "Today" + time-of-day grouping** — Morning/Afternoon/Evening/Late buckets, day-strip scrubber at top | M (~4-5h) | **Pre-launch · SHOULD** | Persona B's primary friction. If Sprint capacity tight, ship Day 2 as first patch. |

**Hard line — explicitly NOT recommended:** QR, AI chat, AR/VR, community/comments, accessibility (per user constraint), ratings/reviews, ticketing, push notifications, offline-first PWA (P21 nice-to-have, not mandatory for festival with cell coverage).

**Total mandatory dev effort:** ~16-22h pre-launch (5 months runway → comfortable).

---

## ✅ Strengths (keep doing — don't refactor away)

- Motion system at [2047-2186](BPFmap.html#L2047) — `cubic-bezier(.22,.8,.24,1)` 0.22s is editorial-grade. Don't refactor.
- Cinematic gradient stack (warm coral + cool navy + indigo) is the brand signature. Reuse the radial+linear layering on new surfaces.
- MutationObserver re-apply with `_v381Applying` recursion guard — fragile but pragmatic; document it as the official pattern.
- V374 sheet/popup coexistence fix (`.sheet-expanded` hides popup) — clean single-rule solution, model for future visual-noise fixes.

---

## 📋 Confirmed status of known bugs (NEXT_SESSIONS.md)

| Ref | Status | Notes |
|---|---|---|
| B1 — Grabber non-functional in empty state | ⚠️ Still present | [1813, 2270](BPFmap.html#L1813) |
| B2 — Popup over expanded sheet | ✅ Fixed | V374 [14273-14274](BPFmap.html#L14273) |
| B3 — Welcome entry animation re-trigger | ⚠️ Latent | matches H-F2 above |
| B4 / P25 — CSS specificity debt | ⏸ Post-festival per instruction | Not touched |

---

## 🎯 Recommended attack order — revised plan

5 months runway, ~16-22h of mandatory features + ~6h of fixes = comfortable schedule. Suggested cadence: one focused sprint per week.

**Sprint 1 (1.5h) · Polish + quick wins — DO NOW**
- A-bundle: all 5 quick wins (placeholder copy, casing, fitCity guard, FEST_START dedup, "Festival ended" canon) — 15 min
- H-U1 iOS safe-area — 20 min
- H-U4 loading skeleton + Airtable failure toast — 15 min
- H-U2 Twin CTAs cleanup — 10 min
- H-U3 Toast consolidation — 20 min
- Token CSS vars (`--bpf-cream/navy/coral`) — 20 min

**Sprint 2 (3-4h) · MF-3 + MF-4 — wire what's almost done**
- MF-3 Add-to-calendar wiring (ICS code already exists at line ~7097)
- MF-4 Deep link `?venue=…` + Web Share API on venue sheet

**Sprint 3 (4-5h) · MF-2 Saved/Shortlist (P17)**
- Bookmark icon on venue + event rows
- `savedBtn` panel listing
- localStorage persistence
- Optional: shareable `#saved=…` URL hash

**Sprint 4 (6-8h) · MF-1 Tonight surface — the strategic one**
- New chronological bottom-sheet view
- `On now / Starting soon / Later tonight` grouping
- Distance-sorted (haversine on user location)
- Welcome chip + persistent entry point

**Sprint 5 (2-3h) · Stability hardening**
- H-F1 boot interval timeouts
- H-F2 observer disconnect
- H-F4 remove V330 search handler
- H-F3 geolocation dedup (after TEST_ORIGIN flipped to real)

**Sprint 6 (4-5h) · MF-5 Programme "Today" default + time-of-day grouping** *(if capacity allows pre-launch, else Day 2 patch)*

**Sprint 7 (pre-launch, T-7 days) · Security + toggles**
- Revoke PAT, deploy Cloudflare Worker proxy → SB-1
- `escapeHTML()` helper + wrap interpolations → SB-2
- Flip `DEBUG_TODAY = ""`, remove `TEST_ORIGIN`
- Restrict Mapbox token by referrer
- Strip console.log debug dumps
- Add CSP meta

**Post-launch · Cost & specificity cleanup**
- Cost optimizations 1-4 (route cache, double-click guard, etc.)
- B4/P25 CSS consolidation
- H-S2 `new Function` → `window[name]` refactor

---

*Audit method: 4 parallel Claude Code subagents (Mapbox cost, Security, Functional/Stability, UI/UX) reading the file in isolation, ~3-4 min each. Re-run before final launch.*
