Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ab2e319
fix: prevent favicon corruption from gulp 5 encoding
rzueger Mar 17, 2026
41cf4fb
ci: enable npm commands for Claude in GitHub Actions
rzueger Mar 17, 2026
82beec8
ci: enable PR branch rebasing for Claude in GitHub Actions
rzueger Mar 17, 2026
4a854a1
feat: improve PWA support for native mobile experience
github-actions[bot] Mar 17, 2026
ef089ae
chore: update package-lock.json with workbox-webpack-plugin
github-actions[bot] Mar 17, 2026
57ecb16
fix: generate service worker for all non-dev builds
github-actions[bot] Mar 17, 2026
facbfc5
feat: replace magic link email auth with OTP code
github-actions[bot] Mar 17, 2026
cb334e4
fix: address all security and usability issues in OTP auth flow
github-actions[bot] Mar 17, 2026
5fa461b
test: fix failing verifySignInCode test and add missing coverage
github-actions[bot] Mar 17, 2026
3b06e9b
fix: reset submitting state after email sent, clear OTP error on resend
github-actions[bot] Mar 17, 2026
e018fed
fix: address remaining review findings in OTP auth flow
github-actions[bot] Mar 17, 2026
7ed8f9e
fix: address remaining review findings in OTP auth flow
github-actions[bot] Mar 17, 2026
9b8c9c5
test: add unit tests for OtpCodeForm component
github-actions[bot] Mar 17, 2026
59a15ac
fix: reduce OTP input width on mobile portrait
github-actions[bot] Mar 18, 2026
5c3ecee
fix: add change email option on OTP code form
github-actions[bot] Mar 18, 2026
dbdbf5e
feat: add PWA install card on StartPage
rzueger Mar 18, 2026
ac69be8
fix: address review findings in PWA install card
github-actions[bot] Mar 18, 2026
b29bd5a
fix: address review findings in PWA install card
github-actions[bot] Mar 18, 2026
9f01a61
fix: address review findings in PWA install card
github-actions[bot] Mar 18, 2026
57205cf
fix: movement list empty for OTP-authenticated users
rzueger Mar 18, 2026
5d09fe4
fix: auto-update service worker and reload on deploy
rzueger Mar 18, 2026
55b8776
fix: handle rejected SW update promise and log registration errors
github-actions[bot] Mar 18, 2026
aeca50b
fix: show correct install instructions for macOS Safari
rzueger Mar 18, 2026
933a7f4
chore: lower install card visit threshold for testing
rzueger Mar 18, 2026
e9ea33b
chore: reset install card visit threshold to 3
rzueger Mar 18, 2026
0e6e982
test: add Cypress E2E test for email user movement list
rzueger Mar 19, 2026
216c060
fix: correct midnight reload timer format to prevent reload loop
rzueger Mar 19, 2026
4a8cc58
fix: add error boundary to prevent blank screen on render errors
rzueger Mar 19, 2026
44df106
fix: handle auth channel creation failure gracefully
rzueger Mar 19, 2026
bab82e5
fix: guard against service worker reload loops on iOS
rzueger Mar 19, 2026
35a52a8
fix: prevent OTP input focus ring from overflowing viewport
rzueger Mar 19, 2026
a2cc65e
chore: update GitHub Actions to v4 and remove duplication
rzueger Mar 20, 2026
9a10047
feat: add privacy policy link for DSGVO compliance
rzueger Mar 20, 2026
efdd6ad
feat: add scheduled data retention cleanup jobs
rzueger Mar 20, 2026
143a686
fix: address review findings in anonymizeMovements
github-actions[bot] Mar 20, 2026
bc91835
feat: add privacy settings admin section
rzueger Mar 20, 2026
f285bf9
feat: add LSPL (Langenthal) aerodrome project
rzueger Mar 20, 2026
c2da20b
feat: add privacy consent text to login page
rzueger Mar 20, 2026
65a9694
feat: unify footer with privacy link and branding
rzueger Mar 20, 2026
a5a6844
feat: add LSPL favicons and square logo
rzueger Mar 20, 2026
024ef02
chore: Add lspl test env to dev deployment workflow
rzueger Mar 20, 2026
209e75d
fix: remove external Google Fonts CDN link
rzueger Mar 21, 2026
bf597c0
fix: remove sendDefaultPii from Sentry config
rzueger Mar 21, 2026
007b0e1
feat: add privacy consent tracking for DSGVO
rzueger Mar 21, 2026
c56487a
chore: replace MIT license with proprietary license
rzueger Mar 21, 2026
c14c3cb
fix: exclude index.html from service worker precache
rzueger Mar 21, 2026
3771e6b
fix: privacy consent for personal login users
rzueger Mar 21, 2026
e465cfa
fix: handle deleted movements in Firebase snapshots
rzueger Mar 22, 2026
71669a5
fix: tear down movement listeners on auth session loss
rzueger Mar 22, 2026
659c257
test: fix minor issues in movements saga tests
github-actions[bot] Mar 22, 2026
728dd2a
fix: prevent stale HTML cache causing blank screen on PWA
rzueger Mar 22, 2026
4321191
fix: use available icon for privacy admin nav item
rzueger Mar 22, 2026
27f570b
fix: improve visual grouping in privacy settings form
rzueger Mar 22, 2026
3774deb
chore: Change lspl example fees to net prices before VAT
rzueger Mar 22, 2026
24d7afd
Fix PWA locked to portrait on Android tablets
claude Mar 22, 2026
a1147dd
Fix static manifest overwriting generated one during build
claude Mar 22, 2026
6423a55
Bust browser cache for PWA manifest
claude Mar 22, 2026
4a1812d
feat: Remove author of AD status from status page
rzueger Mar 23, 2026
f64b062
Refine PII fields in anonymization job
claude Mar 25, 2026
94b8899
Handle null immatriculation in movement sorting
claude Mar 25, 2026
88ca9fd
Skip anonymized arrivals in landings report
claude Mar 25, 2026
8d4d3f3
Lock anonymized movements in movement list (#674)
rzueger Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(npm run:*)",
"Bash(npx jest:*)",
"Bash(npm install:*)",
"Bash(git fetch:*)",
"Bash(git rebase:*)",
"Bash(git push:*)",
"Bash(git status:*)",
"Bash(git log:*)"
]
}
}
21 changes: 19 additions & 2 deletions .firebaserc
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,22 @@
"projects": {
"staging": "mfgt-flights",
"production": "project-8979611309653332047"
}
}
},
"targets": {
"cypress-testing": {
"database": {
"main": [
"cypress-testing-eu"
]
}
},
"lspl-test": {
"database": {
"main": [
"lspl-test-default-rtdb"
]
}
}
},
"etags": {}
}
12 changes: 10 additions & 2 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
contents: write
pull-requests: read
issues: read
id-token: write
Expand All @@ -28,7 +28,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install dependencies
run: npm ci

- name: Run Claude Code
id: claude
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/firebase-hosting-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,19 @@ jobs:
environment:
- lszt_test
- lszm_test
- lspl_test
- lspv_test
- lszo_test
- lsze_test
environment: ${{ matrix.environment }}
steps:
- run: echo 'Running deplyoment for project ${{ vars.PROJECT }}'
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build --project=${{ vars.PROJECT }}
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: w9jds/setup-firebase@main
with:
project_id: ${{ vars.FIREBASE_PROJECT }}
Expand All @@ -40,6 +38,7 @@ jobs:
environment:
- lszt_test
- lszm_test
- lspl_test
- lspv_test
- lszo_test
- lsze_test
Expand Down
7 changes: 2 additions & 5 deletions .github/workflows/firebase-hosting-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,12 @@ jobs:
environment: ${{ matrix.environment }}
steps:
- run: echo 'Running deplyoment for project ${{ vars.PROJECT }}'
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run build:prod --project=${{ vars.PROJECT }}
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: w9jds/setup-firebase@main
with:
project_id: ${{ vars.FIREBASE_PROJECT }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ jobs:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
Expand Down
36 changes: 15 additions & 21 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
MIT License

Copyright (c) 2016-present, open digital Switzerland

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Copyright (c) 2016-present, Open Digital Switzerland. All rights reserved.

This software and associated documentation files (the "Software") are the
proprietary property of Open Digital Switzerland. Unauthorized copying,
modification, distribution, or use of the Software, in whole or in part,
is strictly prohibited without the prior written consent of Open Digital
Switzerland.

The Software is provided "as is", without warranty of any kind, express or
implied, including but not limited to the warranties of merchantability,
fitness for a particular purpose, and noninfringement. In no event shall
Open Digital Switzerland be liable for any claim, damages, or other
liability, whether in an action of contract, tort, or otherwise, arising
from, out of, or in connection with the Software or the use or other
dealings in the Software.
14 changes: 14 additions & 0 deletions cypress/CYPRESS_TESTING_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ Auth endpoint: `https://europe-west1-cypress-testing.cloudfunctions.net/auth`
| `foo` | `bar` | user |
| `admin` | `12345` | admin |

## Email user login (for `loginEmail` command)

The `cy.loginEmail()` command authenticates as `cypress-pilot@example.com` via the
`createTestEmailToken` Cloud Function. This function is gated behind a config flag
that must be enabled on the `cypress-testing` project:

```bash
firebase functions:config:set testing.enabled=true --project cypress-testing
firebase deploy --only functions --project cypress-testing
```

The function always creates a token for the hardcoded email `cypress-pilot@example.com`
and returns 403 on any project where `testing.enabled` is not set.

## Aerodrome settings (already in place)

- Runway `36` with `departureRoute` options including `west`
Expand Down
75 changes: 75 additions & 0 deletions cypress/integration/movements/email_user_movement_list_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
describe('movements', () => {
describe('email user movement list', () => {
let createdDepartureKey;

before(() => {
cy.visit('#/departure/new');
cy.loginEmail();
});

after(() => {
// Clean up as admin (email user may lack delete permission)
cy.logout();
cy.loginAdmin();

if (createdDepartureKey) {
cy.window().then(win => {
win.firebase.getRef(`/departures/${createdDepartureKey}`).remove();
});
}

cy.logout();
});

it('shows departure in movement list for email user', () => {
// Create a departure via the UI
cy.get(`[data-cy=immatriculation]`).type('HBKOF');
cy.get(`[data-cy=aircraftType]`).type('DR40');
cy.get(`[data-cy=mtow]`).type('1000');
cy.get(`[data-cy=aircraftCategory]`).click();
cy.get(`[data-cy=aircraftCategory-option-Flugzeug]`).click();
cy.get(`[data-cy=next-button]`).click();

cy.get(`[data-cy=lastname]`).type('Cypress');
cy.get(`[data-cy=firstname]`).type('Pilot');
cy.get(`[data-cy=email]`).type('pilot@example.com');
cy.get(`[data-cy=phone]`).type('0790000000');
cy.get(`[data-cy=next-button]`).click();

cy.get(`[data-cy=passengerCount-increment]`).click();
cy.get(`[data-cy=carriageVoucher-yes]`).click();
cy.get(`[data-cy=next-button]`).click();

cy.get(`[data-cy=date]`).click();
cy.get(`.DayPicker-Day[aria-disabled=false]`).last().click();
cy.get(`[data-cy=time-minutes-increment]`).click();
cy.get(`[data-cy=location]`).type('LSZR');
cy.get(`[data-cy=duration-hours-increment]`).click();
cy.get(`[data-cy=duration-minutes-increment]`).click();
cy.get(`[data-cy=next-button]`).click();
cy.get(`[data-cy=location-confirmation-dialog-confirm]`).click();

cy.get(`[data-cy=flightType-private]`).click();
cy.get(`[data-cy=runway-36]`).click();
cy.get(`[data-cy=departureRoute-west]`).click();
cy.get(`[data-cy=route]`).type('Testroute');
cy.get(`[data-cy=remarks]`).type('E2E email user test');
cy.get(`[data-cy=next-button]`).click();
cy.get(`[data-cy=commit-requirement-dialog-confirm]`).click();

cy.get(`[data-cy=finish-button]`).click();
cy.hash().should('eq', '#/');

// Navigate to movement list and verify exactly one departure appears
cy.visit('#/movements');
cy.get('[data-id]').should('have.length', 1);
cy.get('[data-id]')
.contains('HBKOF')
.closest('[data-id]')
.invoke('attr', 'data-id')
.then(key => {
createdDepartureKey = key;
});
});
});
});
9 changes: 9 additions & 0 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ Cypress.Commands.add('login', () => login('foo', 'bar'));

Cypress.Commands.add('loginAdmin', () => login('admin', '12345'));

Cypress.Commands.add('loginEmail', () => {
cy.request('POST', 'https://europe-west1-cypress-testing.cloudfunctions.net/createTestEmailToken')
.then((response) => {
cy.window().then(win => {
return win.firebase.authenticate(response.body.token);
});
});
});

Cypress.Commands.add('logout', () => {
cy.window().then(win => {
win.firebase.unauth();
Expand Down
35 changes: 33 additions & 2 deletions firebase-rules-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
".indexOn": [
"dateTime",
"negativeTimestamp",
"immatriculation"
"immatriculation",
"createdBy_orderKey"
],
"$departure_id": {
".write": "auth !== null && (!root.child('settings/lockDate').exists() || (!data.exists() && newData.exists() && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && !newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24 && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24))",
Expand Down Expand Up @@ -91,6 +92,9 @@
"customsFormUrl": {
".validate": "newData.isString()"
},
"privacyPolicyAcceptedAt": {
".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)"
},
"$other": {
".validate": false
}
Expand All @@ -101,7 +105,8 @@
".indexOn": [
"dateTime",
"negativeTimestamp",
"immatriculation"
"immatriculation",
"createdBy_orderKey"
],
"$arrival_id": {
".write": "auth !== null && (!root.child('settings/lockDate').exists() || (!data.exists() && newData.exists() && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && !newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24) || (data.exists() && newData.exists() && data.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24 && newData.child('negativeTimestamp').val() * -1 > root.child('settings/lockDate').val() + 1000 * 60 * 60 * 24))",
Expand Down Expand Up @@ -223,6 +228,9 @@
"customsFormUrl": {
".validate": "newData.isString()"
},
"privacyPolicyAcceptedAt": {
".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)"
},
"$other": {
".validate": false
}
Expand Down Expand Up @@ -287,6 +295,21 @@
".read": "auth !== null && root.child('admins/' + auth.uid).exists()",
".write": "auth !== null && root.child('admins/' + auth.uid).exists()"
},
"privacyPolicyUrl": {
".read": true,
".write": "auth !== null && root.child('admins/' + auth.uid).exists()",
".validate": "newData.isString()"
},
"movementRetentionDays": {
".read": "auth !== null && root.child('admins/' + auth.uid).exists()",
".write": "auth !== null && root.child('admins/' + auth.uid).exists()",
".validate": "newData.isNumber() && newData.val() > 0"
},
"messageRetentionDays": {
".read": "auth !== null && root.child('admins/' + auth.uid).exists()",
".write": "auth !== null && root.child('admins/' + auth.uid).exists()",
".validate": "newData.isNumber() && newData.val() > 0"
},
"$other": {
".validate": false
}
Expand Down Expand Up @@ -423,6 +446,11 @@
}
}
},
"signInCodes": {
".read": false,
".write": false,
".indexOn": ["email"]
},
"profiles": {
"$profile_id": {
".read": "auth !== null && $profile_id === auth.uid",
Expand Down Expand Up @@ -454,6 +482,9 @@
"mtow": {
".validate": "newData.isNumber() && newData.val() > 0"
},
"privacyPolicyAcceptedAt": {
".validate": "newData.isString() && newData.val().matches(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/)"
},
"$other": {
".validate": false
}
Expand Down
Loading
Loading