Bug-fix release. Behaviour changes for 6 users on deploy: sailing, windsurfing, surfing and motorboating stop syncing to Kanu-EFB unless explicitly enabled.
Reported by user #115: sailing trips logged on a Fenix as activity "Sail" were landing in eFB. He was right that it wasn't intended.
Root cause
scripts/garmin_fetch.py:is_water_sport admits anything with parentTypeId == 228, described in the code as "all water sport subtypes (kayaking_v2, paddling_v2, rowing_v2)". But 228 is Garmin's entire Water Sports category. The per-user filter couldn't help either — it knew only six paddle prefixes, and the engine deliberately kept unrecognised typeKeys. Sailing was both synced and unexcludable.
What changed
- Inverted the activity-type filter (migration 0014).
users.selected_activity_typesrecords what the user wants rather than what they excluded, superseding theexcluded_activity_typesmodel from v0.9.18. Defaults: paddle sports + another_watercatch-all on; sailing, windsurf, surfing, motorboat off unless ticked. Every prior exclusion is preserved by the backfill. - Established Garmin's parent-228 membership empirically, not by guessing:
sync_runs.type_keys_seenis pre-filter andactivities_foundpost-filter, so sweeping 216 users' histories reveals which typeKeys pass. Inside 228:sailing_v2,windsurfing_v2,boating_v2,wakeboarding_v2,surfing_v2. Outside it:open_water_swimming(0/137),lap_swimming(0/197),apnea_diving(0/26),single_gas_diving(0/10) — Garmin files swimming and diving under their own parents, so they never reach us and deliberately get no categories. other_waterstays on by design. We can't tell whether an unrecognised water sport is paddling, and losing a real trip is worse than an unwanted one. It makes a future new Garmin type tickable, not blocked — it is not a guarantee against another leak.- New dashboard banner. The "no matching activities" hint fired for exactly the users this change affects and told them to re-tag correctly-tagged Garmin activities. Runs with
excluded_count > 0now get a distinct banner pointing at /settings instead. match_by_nameactivities now obey the filter. They arrive as typeKeyotherunder parent 17, so unticking a category previously did nothing for the Venu-3 users that fallback exists for. Classified by name now, mirroringgarmin_fetch.py(widened to catch German "Paddeln").- Fail-open hardening. An empty selection means "sync nothing", so an unreadable column would have silently killed a user's imports:
decodeSelectedCategoriesand the column DEFAULT now fall back to the defaults, while a deliberate[]is still honoured. The 0014 backfill also guardsjson_type = 'array'andIS NOT NULL—json_validalone letnull/[null]through, wherejson_eachyields SQL NULL andNOT INbackfills an empty selection.
Verification
make test, make lint, make test-python, go vet green; CI green on master. Migration dry-run against a copy of the production DB (272 users): clean boot, 267 users → paddle set + other_water, the 5 with prior exclusions each preserved exactly, zero empty or invalid selections, idempotent on second boot.
Follow-up (not in this release)
The 6 affected users (63, 80, 115, 187, 219, 248) keep their already-uploaded sailing/windsurf/surfing/motorboat Fahrten — the connector never deletes from EFB, so those need removing by hand.