observabilitya client crash reported from Firefox or Safari no longer reaches Sentry with zero frames. Client errors are beaconed to the server and re-thrown there, so they are read by the Node Sentry SDK, whose only stack parser understands V8's `at fn (url:line:col)` form — Firefox and Safari write `fn@url:line:col`, so a perfectly good browser stack was thrown away frame by frame and the issue arrived with a type, a message and no location at all (a Firefox `TypeError: t is undefined` on a takeoff page was untriageable for exactly this reason). Those stacks are now translated into the form the parser reads, led by the header line it requires — it skips the first line as the message, which would otherwise swallow the frame that actually crashed. The translation happens server-side, so it fixes reports from all four paths at once — uncaught errors, unhandled promise rejections and both React error boundaries — including browsers still running a cached bundle
redisthe app losing the boot race against the local Redis service no longer raises a high-priority Sentry issue. ioredis runs its own reconnection loop and emits an error for *every* failed attempt, and each one was reported — so a gap of a few seconds at startup, which the client closes by itself with nothing lost (commands wait in the offline queue), filed an `AggregateError: connect ECONNREFUSED ::1:6379` once per PM2 worker. Connection failures now stay in the server log as warnings for the first 30 seconds and reach Sentry only once the client has kept failing past that, then are suppressed until it recovers — so a Redis that is genuinely gone is one issue instead of one per retry, and its return is logged with how long it was down
routinga page that does not exist now actually says so. Asking for an unknown takeoff, doc page, doc category or shared recap returned "200 OK" with the not-found page painted on top — the page looked right to a person but told search engines, link checkers and monitoring that the URL was fine. The cause was a single app-wide loading screen: it commits the response before the page has had a chance to decide anything, which also downgraded every genuine redirect into a one-second "refresh this page" stall. The loading screen now sits only on the sections that have a real wait to cover (cockpit, logbook, profile, admin, the community feeds), so the rest of the app can answer with the right status again. The last legacy `/takeoff-finder` link is now an instant permanent redirect instead of a stall
takeoffsopening a takeoff page is no longer a one-second stall followed by the page loading twice. Every link into a takeoff detail page went through up to two server-side redirects (`/takeoff-finder/<id>` → `/takeoffs/<id>` → `/takeoffs/<id>/<slug>`), and because the app's root loading state flushes the page shell before the redirect is decided, the browser could not be sent on with a normal redirect — it got a one-second "refresh this page" instruction instead, each time. Links now point straight at the full URL, the trailing name in the address bar is decorative (any form of it renders, with search engines pointed at the canonical one), and the single legacy URL still in the wild is answered with a real, instant redirect
replaythe Fullscreen button no longer crashes the 3D replay and the flight comparison on Safari. Safari never shipped the unprefixed Fullscreen API — the desktop browser only has the `webkit*` aliases, and Safari on iPhone has no element-level fullscreen at all — so `element.requestFullscreen()` threw "requestFullscreen is not a function" and took the whole view down with it. Every fullscreen call now goes through one feature-detecting helper: it uses the vendor alias where there is one, hides the button entirely where fullscreen cannot work (iPhone) rather than offering a control that throws, and treats the browser's own refusal (no user gesture, iframe without the permission) as a refusal instead of a crash report. Menus opened from inside fullscreen keep working on Safari too, now that the portal target reads the prefixed fullscreen element (#922)
observabilitya `logger.error()` call that reports a *condition* rather than a caught exception no longer invents one. The absent third argument was still run through `toError()`, which fabricated an `UnknownError: An unknown error occurred` whose only stack frame sat in `error-utils.ts` — so all ~55 message-only call sites in the codebase reported as the same synthetic exception and collapsed into a single un-triageable Sentry issue, while the `captureMessage` path that groups on the real log message was left unreachable. Nothing thrown now means no exception to report (#920)
logbookdeleting a flight that is already gone, or one owned by someone else, is logged at `warn` instead of `error` — a stale flight list, a double-submitted delete, or a concurrent delete in another tab all land there, and none of them is a server fault. The line stays in the PM2 log (it is also the signal for an ownership-probing attempt) but no longer raises a Sentry error (#920)
observabilitya client crash that the browser reports without an `Error` object no longer reaches Sentry with zero frames. WebKit — Safari, and so every iOS browser including Chrome iOS — leaves `ErrorEvent.error` null for some crashes (stack exhaustion among them), and the reporter read only `event.error?.stack`, so the report arrived with a type, a message and no location at all. The `filename`/`lineno`/`colno` the event *does* carry are now turned into a parseable frame (and beaconed as a plain `source` field), which the source maps uploaded on every build resolve to a named function. The server-side reconstruction also stops leaving the API route's own stack on the error, which had been attributing these crashes to `/api/public/client-error` and collapsing unrelated client crashes into a single group (#873)
observabilitya failed network request no longer lands in Sentry as a "ClientRuntimeError: Load failed" crash — the browser refuses to say which request failed or why, so every report collapsed into one un-triageable group while the causes (lost signal, tab closed mid-request, content blocker) all sit outside the app. Every browser's phrasing of a transport-level `fetch` failure is now filtered client-side before the beacon and server-side as a backstop, from a single shared list (`src/lib/observability/client-error-noise.ts`) so the two can't drift; a real API failure returns a status code and is still reported (#917)
redisa `NOSCRIPT` reply from local Redis (its Lua script cache is emptied on restart/failover/`SCRIPT FLUSH`) is no longer reported to Sentry as an error — it is an expected, self-healing condition that `@upstash/ratelimit` recovers from by re-sending the script via `EVAL`, so the ioredis adapter now logs it at `warn` and re-throws for the fallback instead of `logger.error` (#912)
leonardothe Leonardo upload auth-failure response now names the actual mistake instead of a bare "Invalid User account" — it keeps that literal prefix (which Flyskyhy matches on) and appends a hint distinguishing "you sent your account login password; use a `wf_key_` device password" from "your device password was rejected (revoked/expired/unscoped)". The most common integration snag was pilots pasting their login password, which is rejected by design
apithe device-authorization guide is corrected to allow **public clients** (the code always did; native apps should register as public, no embedded secret), documents the access-token **refresh/rotation lifecycle** and 401/403 handling, and replaces the inaccurate `slow_down` claim with the real 429 rate-limit behaviour; the API-keys guide now states a key is the whole credential (no `client_id`) and shows an IGC upload; the internal Leonardo doc's throttle row is fixed from `429` to `200`
mapstop the "Cannot read properties of undefined (reading '_leaflet_pos')" crash when a map is unmounted mid zoom-animation — Leaflet's fallback timer fired `_onZoomTransitionEnd` on a removed map (its panes already deleted); a global guard makes the handler a no-op once the panes are gone (#865)
xc-planner`publicAppOrigin` now derives the public origin from the pinned `AUTOROUTER_REDIRECT_URI` (a runtime var) before `NEXT_PUBLIC_APP_URL` — the latter is inlined at build time, so a production build that lacked it still bounced the pilot to `http://localhost:3010/...` after a successful connect; the runtime pin makes the redirect correct without a rebuild that carries the public URL
xc-plannerthe autorouter OAuth callback and authorize routes now build their browser redirects from the public origin (`NEXT_PUBLIC_APP_URL`) via a shared `publicAppOrigin` helper — behind the reverse proxy they were built from the internal request origin, so after authorising, the pilot was bounced to `http://localhost:3010/...` (connection refused); the same helper now backs the `redirect_uri` derivation
xc-plannerthe autorouter OAuth `redirect_uri` now derives from `NEXT_PUBLIC_APP_URL` instead of the request origin — behind the reverse proxy the origin was the internal bind address (`http://localhost:3010`), so the authorize URL and token exchange used a localhost callback autorouter rejects; production now sends `https://we-fly.cloud/api/connect/autorouter/callback` with no `AUTOROUTER_REDIRECT_URI` pin needed
surveythe campaign ledger row is now released when a send fails, and the run aborts on a Resend quota/rate-limit error — previously a failed send (e.g. hitting the free-plan 100/day cap) left a permanent "contacted" tombstone, so those pilots were silently skipped on relaunch; adds `campaign:reconcile-survey-sends` to reconcile the ledger against Resend's delivery status and re-target pilots who never received the invite
emailescape the user's display name in the account-deletion and inactivity-warning emails — it was interpolated into the HTML unescaped
emailsend the inactivity-warning email to pilots with no display name — the greeting rendered "Hi undefined,"
cockpitsource lowest-low-save tile from detected low saves
adminmake survey text-answer panel scrollable (#837)
observabilityreport client errors as real Sentry exceptions (#838)
buildguard nullable useSearchParams/usePathname derefs
observabilityfilter Next.js deploy-skew Server Action errors from Sentry (#841)
flightsguard null glideRatio in phases tab glide extremes (#842)
observabilitysurvive third-party DOM tampering (removeChild NotFoundError) (#849)
profilestop overflow container from clipping card borders in connected-services dialog (#855)
connectivityanswer 200 on GET/HEAD for Leonardo submit endpoint (#856)
connectivityparse multipart + IGCigcIGC for Leonardo uploads (#857)
depsrestore pruned @emnapi lockfile entries to unbreak npm ci
xc-plannerdon't zoom-out and drop NOTAMs when clicking a shape (#867)
notamsresolve radius-less named-area NOTAMs to their real outline (#868)
notamsresolve glider-sector NOTAMs to their real outline (#869)
notamsresolve leading-token named-area NOTAMs to their real outline (#870)
notamsresolve every viewport candidate, not just the first 20 (#871)
observabilityrecover the real client error type from beacons (#874)
depssupport sharp 0.35 and bump js-yaml to 5.2.1 (#880)
securitypatch NextAuth/@auth/core auth advisories and nested sharp CVEs (#892)
connected-servicesallow re-copying a just-generated device password (#893)
observabilityignore benign web-streams transformAlgorithm rejection (#895)
—fix: date range picker off-by-one (#899) and xc-planner mobile topbar overlap (#900) (#903)
flightsfilter GPS glitch speed spikes below the 150 km/h gate (#907)
xc-plannerkeep single-click add working while airspaces are shown (#932)
xc-plannerlet the 3D review orbit the whole plan before any scrub (#934)
—docs: add interactive IGC & OpenAir format specifications (#887)