Skip to content

Commit dbf61fa

Browse files
committed
Merge develop into the AI branch again, for 2.1.0 and everything after
The branch this PR targets was 29 commits behind develop — the PR had gone stale since 27 August, and merging it as it stood would have landed the AI branch two days back, missing the whole 2.1.0 tail. Ten conflicts, of two kinds, both "each side added something": **ios/Runner/Info.plist** — this branch's NSLocalNetworkUsageDescription against develop's NSHealthUpdateUsageDescription (#957). Both kept; the plist parses at 25 keys with all four purpose strings present. **All nine ARBs** — this branch extended settingsDeleteAllDataConfirmContent to mention the AI provider key and server address, while develop added the four policyChangeNotice* keys beside it. Resolved as a key-aware union: this branch's string retained, develop's keys added, parity held at 1030 keys in every locale. Verified rather than assumed, because a clean merge is not a correct one — the discipline this PR's own description set out: - Every string develop changed since the merge base survives. The first check compared for equality and reported eight locales as losing develop's wording; that was the wrong test. This branch had already taken #918's recipe-deletion fix and inserted its own sentence into it, so its version is a superset. Confirmed by sentence-level containment in all nine, including zh, where sentence splitting needs 。rather than a full stop. - Both sides' work is present: develop's _stillOnProfile guard, the HealthKit purpose string, the AGENTS.md autoclose note and the android-deploy tolerance; this branch's read_meal_photo, read_meal_text and resolve_parsed_meals use cases. The policy-change notice call is correctly absent, as #955 removed it. - flutter analyze on lib and test: clean. Codegen re-run.
2 parents ec56c38 + 903151d commit dbf61fa

61 files changed

Lines changed: 3713 additions & 127 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/add-issues-to-projects.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@
1717
#
1818
# Re-adding an item that is already on a board is a no-op (the underlying
1919
# addProjectV2ItemById mutation returns the existing item), so the repeated
20-
# `labeled` firings do not create duplicates.
20+
# `labeled` firings do not create duplicates — but only when they arrive one
21+
# at a time. Filing an issue *with* labels attached fires `opened` and one
22+
# `labeled` per label within the same second, and two of those runs adding the
23+
# same item concurrently makes the API answer one of them with
24+
# "Content already exists in this project", which fails the job. The
25+
# concurrency group below serialises runs per issue so the second one is the
26+
# harmless no-op this paragraph describes. It went unnoticed for a long time
27+
# because issues are normally labelled minutes or days after they are filed.
2128
#
2229
# REQUIRED SECRET: `PROJECTS_TOKEN` — this must be a *classic* PAT carrying the
2330
# `project` scope (plus `public_repo`). Fine-grained tokens cannot do this job:
@@ -38,6 +45,14 @@ on:
3845
permissions:
3946
contents: read
4047

48+
# Keyed on the issue, not the workflow: two different issues filed at once are
49+
# not in conflict and should still run in parallel. `cancel-in-progress` stays
50+
# false because each run is doing real work — cancelling the first would drop
51+
# the add it was in the middle of, which is the opposite of the fix.
52+
concurrency:
53+
group: add-issues-to-projects-${{ github.event.issue.number }}
54+
cancel-in-progress: false
55+
4156
jobs:
4257
roadmap:
4358
name: Add to Roadmap board

.github/workflows/default_workflow.yml

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ jobs:
6161
- name: Install Flutter packages
6262
run: just install
6363

64-
- name: Generate localizations
65-
run: just gen_l10n
64+
# check_l10n generates, then fails if any locale is missing a key.
65+
# `flutter gen-l10n` alone exits 0 on a missing translation and lets it
66+
# ship as English, and nothing downstream sees the difference.
67+
- name: Generate localizations (fails if a locale is missing keys)
68+
run: just check_l10n
6669

6770
- name: Generate code (env.g.dart, Hive adapters, JSON serializers)
6871
run: just build
@@ -932,12 +935,80 @@ jobs:
932935
name: android-aab
933936
path: release-assets/android
934937

938+
# Tolerates exactly one upstream defect, and nothing else.
939+
#
940+
# Since 2.1.0 the bundle declares `android.permission.health.*`, and the
941+
# Play Publishing API rejects health-permission bundles at
942+
# `EditService.Validate`/`Commit` with "You must let us know whether your
943+
# app includes any health features" — **regardless of the declaration**,
944+
# which is complete and was re-verified across four console surfaces
945+
# (#942). Uploading the same bundle by hand through the console asks no
946+
# health question at all and succeeds. It is a known, open, unowned
947+
# defect: fastlane#22204 was closed unfixed, fastlane#27960 reopened it,
948+
# and expo/eas-cli#3275 reports it from a different toolchain, which
949+
# rules out fastlane's request construction as the sole cause.
950+
#
951+
# So the attempt stays. Deleting the step would go silently green and
952+
# nobody would notice the day Google fixes it; `continue-on-error` would
953+
# swallow real failures too. Matching the one error string keeps every
954+
# other failure — bad credentials, a consumed versionCode, a rejected
955+
# bundle — loud, and lets this heal itself with no further change.
956+
#
957+
# Note what a failing upload also costs, learned the hard way in #959:
958+
# the API never reaches the point where Play returns its release
959+
# warnings, so a minSdk bump that dropped 1,399 device models went
960+
# unseen for eight days. The manual upload is where those warnings
961+
# appear, which is why the summary below insists on reading them.
935962
- name: Upload Android App Bundle to Google Play Internal Testing via Fastlane
936963
working-directory: android
937-
run: bundle exec fastlane android internal
938964
env:
939965
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
940966
ANDROID_AAB_PATH: ${{ github.workspace }}/release-assets/android/app-release.aab
967+
run: |
968+
set +e
969+
log="$RUNNER_TEMP/play-upload.log"
970+
bundle exec fastlane android internal 2>&1 | tee "$log"
971+
status=${PIPESTATUS[0]}
972+
set -e
973+
974+
[ "$status" -eq 0 ] && exit 0
975+
976+
if ! grep -qF \
977+
'You must let us know whether your app includes any health features' \
978+
"$log"; then
979+
echo "::error::Play upload failed for a reason other than the known health-declaration defect (#942). Not tolerated."
980+
exit "$status"
981+
fi
982+
983+
echo "::warning::Play rejected the API upload with the known health-declaration defect (#942). The Android bundle needs uploading by hand; iOS is unaffected."
984+
985+
{
986+
echo '### Android upload needs doing by hand'
987+
echo
988+
echo 'The Play Publishing API rejected this bundle with:'
989+
echo
990+
echo '> Google Api Error: Invalid request - You must let us know whether your app includes any health features.'
991+
echo
992+
echo 'This is [#942](https://github.com/simonoppowa/OpenNutriTracker/issues/942):'
993+
echo 'a known upstream defect, not a missing declaration. The declaration is'
994+
echo 'complete, and the console asks no health question when the same bundle is'
995+
echo 'uploaded by hand.'
996+
echo
997+
echo '**To finish the release:**'
998+
echo
999+
echo '1. Download the `android-aab` artifact from this run, or take the AAB'
1000+
echo ' attached to the GitHub release this workflow creates.'
1001+
echo '2. Play Console → Testen und veröffentlichen → Interner Test →'
1002+
echo ' **Neuen Release erstellen**, and drop the AAB in.'
1003+
echo '3. **Read the warnings on the review step before publishing.** This is the'
1004+
echo ' only place Play reports them, and a failing API upload never gets far'
1005+
echo ' enough to return them — which is how the minSdk regression in'
1006+
echo ' [#959](https://github.com/simonoppowa/OpenNutriTracker/issues/959) went'
1007+
echo ' unnoticed for eight days.'
1008+
echo
1009+
echo 'This step will start passing on its own once Google fixes the API; nothing'
1010+
echo 'here needs changing when that happens.'
1011+
} >> "$GITHUB_STEP_SUMMARY"
9411012
9421013
github-release:
9431014
if: |
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# Keep a change record of the two published privacy-policy documents, and fail
2+
# when they drift apart.
3+
#
4+
# iubenda offers no version history and no "what changed" view (#871), so the
5+
# only way to answer "what did the policy say in March" is to keep the text
6+
# somewhere that has history. Committing it here makes git that record, which
7+
# is what #887 settled on when deciding what is owed to users who acknowledged
8+
# an older policy.
9+
#
10+
# The check half exists because **every clause in this policy is custom**, and
11+
# custom clauses do not propagate between languages in iubenda. #888 caught the
12+
# German document left behind in exactly that way. A check that needs no
13+
# discipline beats one that relies on someone being conscientious.
14+
#
15+
# What it compares, and why only those things: `tool/policy_snapshot.dart`
16+
# carries the reasoning. In short, only signals that survive translation can be
17+
# compared — the purpose taxonomy ids, the service count per purpose, and the
18+
# last-updated date. Service ids cannot: iubenda mints them per language.
19+
#
20+
# Divergences the maintainer cannot fix only warn. iubenda's own template text
21+
# differs between its languages in places (measured 2026-08-27: the English
22+
# App Store Connect section links support.apple.com for opt-out guidance, the
23+
# German one omits the sentence), and a check nobody can satisfy is a check
24+
# that gets ignored.
25+
#
26+
# The snapshot has the owner's postal address replaced with a placeholder. It is
27+
# on the published page already, so nothing is being hidden — but a public
28+
# repository's history is permanent in a way an editable page is not, and #886
29+
# recorded a preference for a c/o address over the residential one. The script
30+
# throws rather than writing a snapshot it cannot redact.
31+
#
32+
# TWO THINGS TO KNOW ABOUT WHERE THIS RUNS:
33+
#
34+
# * Workflow files only take effect once they are on the **default branch**, so
35+
# neither the schedule nor the manual trigger does anything while this lives
36+
# on a feature branch. That is also why the first snapshot was committed by
37+
# hand from a local run: a before-and-after diff of the policy correction was
38+
# only available before the edits were made, and CI could not have run in
39+
# time to capture it.
40+
# * A scheduled run therefore starts on `main` — but `CONTRIBUTING.md` reserves
41+
# `main` for release merges only. So this checks out and commits to
42+
# `develop` explicitly rather than to whatever ref it was triggered on.
43+
#
44+
# No secrets. The read API is public and unauthenticated.
45+
46+
name: Privacy policy snapshot
47+
48+
on:
49+
schedule:
50+
# Weekly. The documents change a handful of times a year, and a daily run
51+
# would mostly be a daily no-op against a third party.
52+
- cron: '23 6 * * 1'
53+
workflow_dispatch:
54+
# Also on any change to the checker itself, so a broken parser is caught by
55+
# the pull request that breaks it rather than by a Monday-morning cron.
56+
pull_request:
57+
paths:
58+
- 'tool/policy_snapshot.dart'
59+
- '.github/workflows/policy-snapshot.yml'
60+
61+
permissions:
62+
contents: read
63+
64+
jobs:
65+
# On a pull request: check only. Nothing may be committed — the branch is not
66+
# necessarily ours, and a bot commit would rewrite a contributor's PR.
67+
check:
68+
if: github.event_name == 'pull_request'
69+
runs-on: ubuntu-latest
70+
permissions:
71+
contents: read
72+
steps:
73+
- name: Checkout code
74+
uses: actions/checkout@v7
75+
76+
- name: Setup Flutter + cache packages
77+
uses: ./.github/actions/setup-flutter-cache
78+
79+
- name: Install Flutter packages
80+
run: flutter pub get
81+
82+
- name: Check the two documents agree
83+
run: dart run tool/policy_snapshot.dart --check
84+
85+
snapshot:
86+
if: github.event_name != 'pull_request'
87+
runs-on: ubuntu-latest
88+
permissions:
89+
contents: write
90+
steps:
91+
- name: Checkout develop
92+
uses: actions/checkout@v7
93+
with:
94+
# Not the triggering ref. A scheduled run fires on the default
95+
# branch, and `main` takes release merges only.
96+
ref: develop
97+
98+
- name: Setup Flutter + cache packages
99+
uses: ./.github/actions/setup-flutter-cache
100+
101+
- name: Install Flutter packages
102+
run: flutter pub get
103+
104+
- name: Snapshot both documents and check they agree
105+
run: dart run tool/policy_snapshot.dart
106+
107+
# Runs even when the check failed: a divergence is exactly the state
108+
# worth having a record of, and discarding the snapshot would throw away
109+
# the evidence of the thing the job just complained about.
110+
- name: Commit the snapshot if it changed
111+
if: always()
112+
run: |
113+
set -euo pipefail
114+
if git diff --quiet -- docs/privacy-policy; then
115+
echo "Neither document changed."
116+
exit 0
117+
fi
118+
git config user.name "github-actions[bot]"
119+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
120+
git add docs/privacy-policy
121+
git commit -m "docs: snapshot the published privacy policy"
122+
git push origin HEAD:develop

AGENTS.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Semantics(
177177
|---|---|
178178
| `ListTile` / `InkWell` / `GestureDetector` with an `onTap` | Pure display — `Text`, `Icon`, `Image`, `Divider`, charts |
179179
| Buttons — `ElevatedButton`, `TextButton`, `IconButton`, `FloatingActionButton`, `FilledButton` (when they have `onPressed`) | Layout — `Container` without `onTap`, `Padding`, `SizedBox`, `Row`, `Column` |
180-
| Input — `TextField`, `TextFormField`, `Slider`, `Switch`, `SwitchListTile`, `Checkbox` (the actual checkbox, not its label) | Generated code (`*.g.dart`, `messages_*.dart`, `l10n.dart`) |
180+
| Input — `TextField`, `TextFormField`, `Slider`, `Switch`, `SwitchListTile`, `Checkbox` (the actual checkbox, not its label) | Generated code (`*.g.dart`, `l10n.dart`, `l10n_<locale>.dart`) |
181181
| Selection — `ChoiceChip`, `FilterChip`, `RadioListTile`, `SegmentedButton`, `DropdownButton` | Theming, transitions, decorative wrappers |
182182
| Bottom sheets, dialog action buttons (Save/Cancel/OK) | Items inside `ListView.builder` / `GridView.builder` (see below) |
183183

@@ -334,7 +334,7 @@ lib/
334334
settings/ # App settings, data export/import, day-start, theme picker
335335
onboarding/ # First-run user setup flow
336336
dev/ # Dev-only main_dev.dart entry point (never shipped) — see "Demo data" above
337-
generated/ # Intl filesmaintained manually (see Localization above)
337+
generated/ # gen-l10n outputgitignored, never edited by hand (see Localization above)
338338
l10n/ # Source ARB translation files
339339
```
340340

@@ -421,6 +421,14 @@ When filing issues or opening PRs, prefer these templates. Product/food-database
421421

422422
Blank issues are disabled (`blank_issues_enabled: false`). Add or edit YAML forms in `.github/ISSUE_TEMPLATE/`; keep labels (`bug`, `enhancement`, `question`) aligned with any repo label setup.
423423

424+
### `Fixes #N` does not close the issue here
425+
426+
Feature work targets **`develop`**, but the default branch is **`main`**. GitHub only acts on a closing keyword when the referencing commit reaches the *default* branch, so a `Fixes #123` in a PR merged into `develop` **leaves the issue open** — often for weeks, until a release merge carries it to `main`.
427+
428+
Write the reference anyway: it links the PR to the issue and closes it when the release lands. But **close the issue by hand once the PR merges**, with a comment saying where the fix is. Three issues sat open for exactly this reason on 2026-08-29 alone.
429+
430+
The exception is a PR that targets `main` directly — a release PR or a hotfix — where the keyword behaves as expected.
431+
424432
## Naming Conventions
425433

426434
| Suffix | Meaning |

CONTRIBUTING.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ architecture conventions live in [AGENTS.md](AGENTS.md).
3030

3131
## Adding or changing localized strings
3232

33-
Source strings live in `lib/l10n/intl_en.arb`. Translations live in a separate ARB file per supported locale, plus manually-maintained Dart files under `lib/generated/`.
33+
Source strings live in `lib/l10n/intl_en.arb`, with one ARB file per supported locale beside it. The Dart files under `lib/generated/` are produced from those ARBs by `flutter gen-l10n` (configured in `l10n.yaml`) — they are gitignored, and CI regenerates them from scratch on every run.
3434

3535
> [!IMPORTANT]
36-
> **For now**, the files under `lib/generated/` carry a `// GENERATED CODE - DO NOT MODIFY BY HAND` header but are **maintained manually** in this project — the upstream generator's output conflicts with the repo's 120-character formatting and would fail CI. Until the generation pipeline is reconciled with the formatting rules, edit those files by hand. Do **not** run `intl_translation:generate_from_arb` — this caveat will go away once the generator output is fixed.
36+
> Never hand-edit anything under `lib/generated/`. The directory is listed in `.gitignore`, so hand-edits are never committed, and the next `flutter gen-l10n` overwrites them locally — the work is lost silently. Edit the ARB files and regenerate.
3737
3838
When adding a new string key in the same PR you must:
3939

@@ -53,11 +53,13 @@ When adding a new string key in the same PR you must:
5353

5454
Provide a real translation for each locale — do not leave the English string in as a placeholder. If you only speak one of the languages, machine translation is acceptable as a starting point; native-speaker review is welcome post-merge.
5555

56-
2. **Add a getter to `lib/generated/l10n.dart`**, following the existing style.
56+
All nine files stay at the same key count. Placeholder metadata (`"@key": {"placeholders": ...}`) only needs to be declared in the template, `intl_en.arb`.
5757

58-
3. **Add a matching `MessageLookupByLibrary.simpleMessage(...)` entry to each `lib/generated/intl/messages_<locale>.dart` file**, one per locale.
58+
2. **Regenerate with `just gen_l10n`** (`flutter gen-l10n`). This rewrites `lib/generated/l10n.dart` and one `lib/generated/l10n_<locale>.dart` per locale, which is where your `S.of(context).yourNewKey` getter comes from. Nothing under `lib/generated/` belongs in the commit — the ARB files are the whole change.
5959

60-
4. **Verify with `just check_intl`** — this is what CI runs and will fail the PR if any of the above is missing or out of sync.
60+
3. **Leave no locale behind.** `flutter gen-l10n` exits 0 on a missing translation — it records the key in `l10n_untranslated.json` at the repo root (also gitignored) and the string falls back to English at runtime. `just check_l10n` is what turns that into a failure, and it is what CI runs; when it fails, that file names the locale and the key.
61+
62+
4. **Run `flutter analyze` and `just test`** before opening the PR — or `just ci` for the whole pre-flight in one go.
6163

6264
## Code generation
6365

@@ -68,7 +70,7 @@ Some files are produced by `build_runner` (Hive type adapters and JSON serializa
6870
- 120-character line width (configured in `analysis_options.yaml`).
6971
- Format with `just format` before committing — this targets only `lib/core`, `lib/features`, `lib/l10n`, and `test` and deliberately skips `lib/generated/`.
7072
- Run `flutter analyze` and `just test` locally before opening the PR.
71-
- `just ci` runs the full CI pipeline (install, format check, intl check, build, analyze, test) and is the closest thing to a one-shot pre-flight check.
73+
- `just ci` runs the full CI pipeline (install, format check, l10n generation and completeness, build, analyze, test) and is the closest thing to a one-shot pre-flight check.
7274

7375
## Commit messages
7476

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ No account, no sign-in, no analytics, no ads. Your profile, diary, activities, w
130130

131131
| Destination | When | What is sent |
132132
| :-- | :-- | :-- |
133-
| [Open Food Facts](https://world.openfoodfacts.org/) | Food search or barcode scan | The search term or barcode, plus a country tag from your device locale for ranking |
133+
| [Open Food Facts](https://world.openfoodfacts.org/) | Food search or barcode scan | The search term or barcode. A word search also sends your device's **language** code to rank results; the fallback search and the barcode lookup send no locale at all. Your **country is never sent** — the country boost is applied on your device, to the results that come back |
134134
| Supabase reference backend | Food search | The search term |
135135
| [Anthropic](https://www.anthropic.com/) | **Only if you save your own API key and pick Anthropic** | The meal line you type on the multi-item add screen, or a meal photo you choose to read there, and your app language |
136136
| [OpenAI](https://openai.com/) | **Only if you save your own API key and pick OpenAI** | As the Anthropic row, plus the model you chose |

android/app/build.gradle

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,16 @@ android {
6161
defaultConfig {
6262
applicationId "com.opennutritracker.ont.opennutritracker"
6363
// The health package's Health Connect client requires API 26; Flutter's
64-
// default is lower, so the floor is pinned here.
64+
// default is 24, so the floor is pinned here.
65+
//
66+
// The cost, which #651 raised the floor without measuring: this drops
67+
// Android 7.0 and 7.1. Play counts that as 1,399 device models — a
68+
// number that sounds alarming and is not, because measured against
69+
// real installs on 2026-08-29 it was **3 of 9,580 active users**,
70+
// 0.03% (#959). Accepted deliberately on that evidence: Health
71+
// Connect cannot ship below 26, so the alternative was dropping the
72+
// feature, and 1,399 models is a device-catalogue figure rather than
73+
// an audience.
6574
minSdkVersion 26
6675
targetSdkVersion 36
6776
versionCode flutterVersionCode.toInteger()

0 commit comments

Comments
 (0)