Skip to content

v4.32.0#675

Merged
rzueger merged 62 commits intomasterfrom
develop
Mar 29, 2026
Merged

v4.32.0#675
rzueger merged 62 commits intomasterfrom
develop

Conversation

@rzueger
Copy link
Copy Markdown
Member

@rzueger rzueger commented Mar 29, 2026

No description provided.

rzueger and others added 30 commits March 17, 2026 10:43
gulp 5 / vinyl-fs 4 changed the default encoding from binary
to UTF-8, corrupting PNG/ICO bytes > 0x7F during copyFavicons.
Add encoding: false to gulp.src() to restore binary copying.
Add Node.js setup and npm ci steps to the Claude workflow
so it can run tests and type checks. Create repo-level
.claude/settings.json granting npm/npx permissions.
Grant contents:write permission and full git history so Claude
can rebase PR branches. Add git command permissions to settings.
- Add iOS/Apple PWA meta tags (apple-mobile-web-app-capable,
  status-bar-style, title) to index.html
- Replace hardcoded white theme-color with per-project brand color
  via HtmlWebpackPlugin options
- Add themeColor and shortName to all project JSON configs
- Add GenerateSW (Workbox) service worker in webpack for production
  builds with NetworkFirst/CacheFirst runtime caching
- Register service worker in src/app.tsx (production only)
- Add generateManifest gulp task that generates build/favicons/
  manifest.json with correct name, short_name, theme_color,
  start_url, scope, and orientation per aerodrome
- Add workbox-webpack-plugin devDependency
- Add service-worker.js no-cache header in firebase.json
- Add SPA catch-all rewrite in firebase.json for deep links

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Service worker registration in app.tsx runs whenever !__DEV__,
but GenerateSW was only included when ENV=production. Test
deployments (e.g. lsze-test.web.app) built without ENV=production
had no service-worker.js, causing Firebase's catch-all rewrite to
serve index.html with a text/html MIME type error.

Fix: use !process.env.DEV as the condition for GenerateSW,
matching the registration condition in app.tsx.

Fixes #640

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Replace the email magic link authentication with a 6-digit OTP
code flow that works reliably in iOS PWA standalone mode and
avoids link expiry/mangling issues.

New Cloud Functions:
- generateSignInCode: generates crypto-random 6-digit code,
  stores SHA-256 hash in RTDB /signInCodes (admin-only path)
- verifySignInCode: verifies code against stored hash, rate
  limits to 5 attempts, deletes code on success (single-use),
  returns Firebase custom auth token

Updated Cloud Functions:
- sendSignInEmail: accepts signInCode instead of signInLink
- emailTemplates: updated to render OTP code in email body
- Old generateSignInLink function kept (will be removed later)

Frontend changes:
- OtpCodeForm: world-class 6-box OTP input with auto-advance,
  backspace navigation, paste support, autoComplete="one-time-code"
- EmailLoginForm: shows OtpCodeForm after email is sent
- EmailLoginFormContainer: wires verifyOtpCode, removes magic
  link props
- firebase.ts: adds requestSignInCode/verifyOtpCode, removes
  isSignInWithEmail/signInWithEmail
- Auth module: adds VERIFY_OTP_CODE/OTP_VERIFICATION_FAILURE
  actions, removes magic link completion actions/sagas
- RTDB rules: /signInCodes path set to read:false/write:false

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
- Fix critical: generateSignInCode now sends email server-side
  and returns only { success: true } — plaintext code is never
  sent to the client, closing the authentication bypass
- Fix high: remove sendSignInEmail as a standalone HTTP endpoint
  (open email relay); refactor into an internal helper module
- Add cleanupExpiredSignInCodes scheduled Cloud Function to
  prune stale entries from /signInCodes every 60 minutes
- Fix medium: remove error details (error.message) from all
  500 responses in generateSignInCode and verifySignInCode
- Fix low: simplify attempt-tracking logic in verifySignInCode
  by removing the dead-code fallback double-iteration block
- Fix usability: add "Resend code" button to OtpCodeForm with
  a 60-second cooldown timer; wire up in EmailLoginForm
- Update firebase.ts requestSignInCode to pass airportName and
  themeColor to the server (needed for server-side email)
- Update auth saga to call requestSignInCode with all params
  and remove the now-redundant sendSignInEmail saga step
- Add comprehensive tests: generateSignInCode.spec.js,
  verifySignInCode.spec.js, cleanupExpiredSignInCodes.spec.js
- Update sendSignInEmail.spec.js and firebase.spec.ts to match
  new API; update sagas.spec.ts for simplified flow

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Fix the attempt-tracking test: move attemptsUpdates apply before
the validKey check so non-matching codes are incremented on both
success and failure paths (not just failure).

Add missing test files for previously untested functions:
- api/basicAuth.spec.js
- api/fetchAerodromeStatus.spec.js
- api/fetchUserInvoiceRecipients.spec.js
- invoiceRecipients/invoiceRecipientsTrigger.spec.js

All 213 function tests now pass across 24 test suites.

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
- Add SEND_AUTHENTICATION_EMAIL_SUCCESS handler to set submitting: false,
  so OTP form inputs are not permanently disabled after email is sent
- Clear otpVerificationFailure in SET_SUBMITTING handler so the error
  message does not persist when resend is triggered
- Update reducer test: verify SET_SUBMITTING clears otpVerificationFailure
- Add test for SEND_AUTHENTICATION_EMAIL_SUCCESS handler

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
- Handle SEND_AUTHENTICATION_EMAIL_FAILURE in auth reducer to reset
  submitting and set failure, preventing the email form from freezing
  permanently on network/server errors
- Clear OTP digits and refocus first input on resend to prevent
  auto-submission of stale digits mixed with new code
- Remove ineffective Math.random spy in generateSignInCode.spec.js
  (crypto.randomInt does not call Math.random)

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
- OtpCodeForm: show OTP-specific error message instead of generic
  login failure; use Trans component for grammatically complete
  German instruction sentence with email interpolation
- loginPage reducer: handle SEND_AUTHENTICATION_EMAIL_FAILURE by
  resetting emailSent to false, returning user to email form on
  resend failure so the error is visible
- cleanupExpiredSignInCodes: also delete codes with attempts >=
  MAX_ATTEMPTS (exhausted codes), not only expired ones
- Update tests for all changed files

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
36 tests covering: rendering, digit input/navigation, backspace,
arrow key navigation, paste handling, auto-submit, submit button,
resend cooldown, digit clearing on resend, and failure messages.

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Add a responsive breakpoint at 480px so the 6-digit OTP row
fits within the 1em-padded container on all portrait phones.
Also add max-width: 100% to the row to prevent overflow.

Breakpoints:
- default:  2.8rem wide (~309px total row)
- ≤480px:  2.4rem wide (~270px total row)
- ≤360px:  2.0rem wide (~232px total row)

Fixes #644

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Adds a "Wrong email? Change it" link below the OTP code inputs that
resets the emailSent state, returning the user to the email input
form pre-filled with their current email for easy correction.

- Add RESET_OTP action and handler to loginPage reducer
- Add onChangeEmail prop to OtpCodeForm component
- Wire up resetOtp in EmailLoginFormContainer
- Add German translation for otpChangeEmail
- Add tests for the new change email button

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Smart inline card that suggests installing Flightbox as a PWA.
Shows between EntryPoints and MarketingLink only when all
eligibility criteria are met: regular user (not guest/kiosk/
ipauth), not already installed, browser supports installation,
3+ distinct visit days, and not dismissed. Supports native
install prompt on Chromium and manual instructions on iOS Safari.
Dismissal is temporary (90 days) on first use, permanent on
second.
- Move recordVisitDay() into useState initializer to avoid
  localStorage writes on every render (issue #1)
- Add comment explaining dual beforeinstallprompt listeners
  (module-level for early events, useEffect for post-mount) (issue #2)
- Clear promptRef and cachedPromptEvent before calling prompt()
  to prevent stale reference after event is consumed (issue #3)
- Replace withTranslation() HOC with useTranslation() hook
  to match codebase convention of hooks over HOCs (issue #4)
- Add @internal JSDoc tag to _resetCachedPromptForTesting (issue #5)
- Merge IosInstructions into styled(Description) extension
- Fix navigator.platform cleanup in iOS Safari test
- Simplify react-i18next mock (remove unused withTranslation mock)

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
- Export AuthData interface and reuse it in InstallCard.tsx to
  eliminate duplication
- Extract isStandalone() to a local variable in usePwaInstall to
  avoid calling it twice per render
- Add setPromptAvailable(false) in install() so the card hides
  immediately after the native prompt is triggered, regardless of
  whether the user accepts or dismisses it
- Add TODO comment for deprecated navigator.platform
- Add test coverage for install button hiding the card (both
  dismissed and accepted outcomes); add act() wrapper to flush
  async state update in accepted case

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
- Remove dead userChoice.then callback after install() — setPromptAvailable
  already hides the card synchronously, and the async setState risked a
  warning if the component unmounted before userChoice resolved
- Add test for exact VISIT_THRESHOLD boundary (count=3)
- Add test for macOS Safari 17+ UA code path
- Drop email field from regularAuthData/ipauthData test fixtures (not
  part of AuthData interface)

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Add createdBy_orderKey to .indexOn for departures and arrivals
in Firebase rules so non-admin movement queries return results.

Add email claim to custom token in verifySignInCode so the
email is explicitly available in the JWT.
Poll for SW updates every 3 minutes so deploys are detected
quickly. When the new SW takes control, reload the page to
ensure the client runs code matching the new Cloud Functions.

Remove runtime caching rules from GenerateSW — the NetworkFirst
catch-all cached API responses (stale data risk) and the
CacheFirst rule for static assets was redundant with webpack
content hashing. Precaching alone is sufficient.
Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Distinguish macOS Safari from iOS Safari so the install card
shows "Zum Dock hinzufügen" instead of "Zum Home-Bildschirm".
Use platform-neutral description text that works for all
platforms.
moment('24:00:00', 'hh:mm:ss') uses 12-hour format, making '24'
out of range. This caused setTimeout to fire immediately, creating
an infinite reload loop. Use moment().endOf('day') instead.
Wrap the React tree with Sentry.ErrorBoundary so that unhandled
render errors show a fallback UI with a reload button instead of
unmounting the entire app to a blank white screen.
Return a fallback channel with null instead of undefined when
watchAuthState throws. This triggers the normal unauthenticated
flow instead of crashing the entire saga tree in a restart loop.
Add a 5-second cooldown between SW controller-change reloads using
sessionStorage. Prevents infinite reload loops when iOS kills and
resumes a PWA process, triggering controllerchange during init.
rzueger and others added 29 commits March 20, 2026 16:21
Add two Cloud Functions for DSGVO-compliant data retention:

- scheduledAnonymizeMovements: strips PII from movements older
  than the configured retention period, preserving statistical
  data for BAZL reporting
- scheduledCleanupMessages: deletes contact messages older than
  the configured retention period

Both functions are opt-in: they read retention days from Firebase
settings and skip entirely if no value is configured.

Guard existing onWrite triggers (associated movements, enrich)
against anonymized records to prevent cascading writes.
- Fix endAt/< boundary mismatch: use <= for dateTime comparison
  to be consistent with the inclusive Firebase endAt query
- Add comment explaining client-side filtering of already-anonymized
  records fetched by the endAt query
- Assert arrival updates independently in anonymization test
- Add test for partial PII fields: verify absent fields are not
  included in the anonymization update object

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
Add admin UI for managing DSGVO-related settings:
- Privacy policy URL (writable, was read-only)
- Movement retention days (new)
- Message retention days (new)

Gated behind `privacySettings` project config flag.
Add project config, theme, landing fee strategy, and
Firebase target for the new LSPL aerodrome.
Replace bare privacy policy link with consent sentence
"Mit der Anmeldung akzeptieren Sie die Datenschutzerklärung."
Hide the consent text on the OTP verification screen.
Move privacy policy link into MarketingLink component so
all pages share a consistent centered footer. Use flexbox
sticky footer layout to keep it at the viewport bottom.
Material Icons are already self-hosted via webpack bundle
in global-style.ts. The CDN link was redundant and caused
unnecessary data transfer to Google servers on every page
load.
Disable automatic PII collection (IP addresses, cookies,
user info). Only error messages and stack traces are needed
for debugging.
- Record `privacyPolicyAcceptedAt` timestamp on user profiles for
personal logins (on first login)
- Record `privacyPolicyAcceptedAt` timestamp on new movement records for
all user types (including guest/kiosk)
- Show consent text with privacy policy link on the last page of the
movement wizard (departure + arrival)
- Add Firebase validation rules for the new field on profiles,
departures, and arrivals
The Workbox SW precaches index.html but not the bundle (exceeds
the 2 MB default). After a deploy the old SW serves stale HTML
that references a bundle hash that no longer exists on the server.
Firebase's SPA catch-all rewrite returns index.html (HTML, 200)
for the missing JS URL, so the browser parses HTML as JavaScript
and the app shows a blank page.

Excluding index.html from precache lets the browser always fetch
fresh HTML from the network (Cache-Control: no-cache is already
set in firebase.json), which always references the current bundle.
Hide the privacy consent text in the movement wizard for personal
login users since they already consented on the login screen.
Only guest, kiosk and ipauth users see it in the wizard.

Record privacyPolicyAcceptedAt on the user profile upon login
by listening for FIREBASE_AUTHENTICATION_EVENT in the profile
saga. Previously this only ran when visiting the profile page.
Guard all firebaseToLocal(snapshot.val()) calls against null
values from deleted or missing records. editMovement navigates
back to the list, loadMovement returns early, and bulk load /
realtime listener paths skip null entries.

Fixes JAVASCRIPT-REACT-40
Fixes JAVASCRIPT-REACT-3W
Unsubscribe all Firebase realtime listeners (child events and
association watchers) when FIREBASE_AUTHENTICATION_EVENT fires
without authData. Prevents orphaned listeners from crashing on
undefined auth state after session expiry. Adds safety guard in
addMovementToState for events already queued in the channel.

Fixes JAVASCRIPT-REACT-49
- Capture generator.next(snapshot) result directly instead of calling
  generator.next() again on already-finished generator
- Remove unused unsubArrival variable in teardownOnAuthLost test

Co-authored-by: Roli Züger <rzueger@users.noreply.github.com>
The Cache-Control: no-cache header in firebase.json only matched
requests for /index.html, but the app uses hash history so the
browser only requests /. Firebase served / with max-age=3600,
causing the browser to cache HTML for up to 1 hour. After a new
deployment the cached HTML referenced a bundle hash that no
longer existed on the server, resulting in SyntaxError and a
blank screen.

Add no-cache header for / in firebase.json and add NetworkFirst
strategy for navigation requests in the service worker.
The bundled Material Icons font does not include the 'shield'
glyph. Replace with 'security' which is a shield shape and is
available in the font.
Wrap each field with its help text in a FieldGroup with a
bottom border separator so it is clear which description
belongs to which input.
Change manifest orientation from 'portrait' to 'any' so Android
Chrome allows the installed PWA to rotate to landscape. iOS Safari
ignores this field, so no impact there.

Note: existing installs need to be reinstalled to pick up the change.

https://claude.ai/code/session_01CX1JWcjRBXSgg8sVW58pQP
The copyFavicons and generateManifest gulp tasks ran in parallel,
causing the static manifest.json (which lacks orientation) to
overwrite the generated one. Now copyFavicons excludes manifest
files, and generateManifest runs after copyFavicons to guarantee
the generated manifest with orientation: 'any' is what ships.

https://claude.ai/code/session_01CX1JWcjRBXSgg8sVW58pQP
Bump manifest cache-buster query from ?v=2 to ?v=3 so Chrome
on Android fetches the updated manifest with orientation: any.
Also add no-cache header for the manifest in Firebase hosting.

https://claude.ai/code/session_01CX1JWcjRBXSgg8sVW58pQP
- Add privacyPolicyAcceptedAt (consent timestamp per movement)
- Add paymentMethod/invoiceRecipientName (nested PII)
- Remove aircraftType, mtow (not PII without immatriculation)
- Remove carriageVoucher (boolean, not PII)

https://claude.ai/code/session_01KHZDkEfyznUCcoGCWrGdsY
Anonymized records have immatriculation set to null, which
crashes localeCompare in compareDescending/compareAscending.
Fall back to empty string for null values.

https://claude.ai/code/session_01KHZDkEfyznUCcoGCWrGdsY
Anonymized records have null immatriculation, which can't be
meaningfully grouped in a per-aircraft landing summary. Skip
them and add defensive null-safe sorting.

https://claude.ai/code/session_01KHZDkEfyznUCcoGCWrGdsY
## Summary

- Treat anonymized movement records as locked in the movement list
- Admins see a lock icon and cannot edit/delete anonymized records
@rzueger rzueger merged commit 301399a into master Mar 29, 2026
14 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