# Android ↔ PWA feature parity

Living matrix of what is **shared**, **android-only**, **pwa-only**, or **wip**.
Android is the product of record; the PWA is a companion that reimplements daily-driver UX and stays **data-interoperable** via export contracts.

For formula / wire-format correctness (not feature lists), see:

- [`CALCULATION_METHODS.md`](CALCULATION_METHODS.md): formula register + change checklist
- [`../testdata/parity/`](../testdata/parity/): shared golden fixtures (formulas, wire samples, AI defaults, pref defaults, goal-prompt fragments)
- [`../contracts/`](../contracts/): JSON Schemas for diary / body-metrics / meal-share (structural / version guard; see contracts README)
- `devenv tasks run release:check-parity`: PWA tests + schema validation + **this matrix’s structure check** (also runs inside `release:package`)

| Surface | Status | Android | PWA | Notes |
|---------|--------|---------|-----|-------|
| Home diary (meals, gauge, tubes/chips) | shared | `ui/home/` | `components/home-*` | Pref-driven tubes; fiber in default set; **53-week snap pager**; defaults locked in `testdata/parity/pref-defaults.json`. Optional **caffeine** tube shows the daily total against the daily max from Goals & Nutrition (2026-08, #55) |
| Water logging & history | shared | `ui/home/WaterViews.kt` (card, quick presets, custom amount, **history sheet: per-sip edit + delete**, Codeberg #58) | `components/diary-view.js` (quick chips, custom slider, **Undo last log only**) | Wire `water` records stay day-granular (`date`, `amount_ml`); Android keeps the sip time locally but it does not sync. History sheet is android-only (2026-08) and works for any selected day, not just today |
| Nicotine tracking (optional) | shared | `ui/home/NicotineViews.kt` (progress row, quick chips, custom count + mg, history sheet with edit/delete), settings sub-screen under Trackers & Reminders | `components/diary-view.js` (row with +1 kind chips, Custom, Undo), App settings toggle + daily limit | Off by default (`showNicotine`). Count-based entries (`NicotineEntry`: kind cigarette/vape/pouch/gum/patch/other, count, optional mg). Rides sync-1.2 as an **optional** `nicotine_entries` array + `nicotine` revisions kind (old docs parse with an empty array). **Not** in diary export / meal-share; **no Health Connect** (HC has no count-based nicotine record). Stays local + WebDAV sync; never sent to AI providers. Android-only: quick-kind configuration, mg wheel, per-day history edit/delete |
| Caffeine tracking (optional tracker) | shared | `ui/home/CaffeineViews.kt` (progress row, +1 quick chips, custom mg sheet, history sheet with edit/delete), settings sub-screen under Trackers & Reminders (enable, mg limit wheel, quick kinds) | `components/diary-view.js` (row with +1 kind chips, Custom, Undo), App settings toggle + daily limit | Off by default (`showCaffeine`). Mg-based entries (`CaffeineEntry`: kind coffee/tea/energy/other, mg; kind defaults 95/28/80 mg). Hero total = tracker logs **+ food-entry caffeine** for the day, against the daily mg limit (default 400). Rides sync-1.2 as an **optional** `caffeine_entries` array + `caffeine` revisions kind (old docs parse with an empty array). **Not** in diary export / meal-share; food-entry caffeine still rides `NutritionRecord.caffeine` to Health Connect. Stays local + WebDAV sync; never sent to AI providers. Android-only: quick-kind configuration, mg wheel, per-day history edit/delete (device-pass revision 2026-08-24 of the #55 nutrient shape) |
| Intermittent fasting (optional, local-only) | shared | `FastingRepository`/`FastingSession` + Home row + hub control (`FastingViews.kt`) + settings sub-screen under Trackers & Reminders + **goal-reached notification** + launcher quick action (#182) | `components/diary-view.js` fasting card (start/stop/cancel, minute ticker) + App settings section | Off by default (`showFasting`). Session state is a pure function of persisted scalars (survives restarts); optional goal hours (0 = none) with a one-shot reached notification (Android-only; PWA has no notifications). **Local prefs only**: not in diary-1.3 / meal-share / sync-1.2, no Health Connect, no contracts/goldens. Coach gets a fasting snapshot block (elapsed vs goal, no zone claims) on both clients; the block is the only thing sent to AI providers. #182 launcher toggle published only while enabled |
| Daily notes | shared | `ui/home/DailyNoteSheet.kt` (day-scoped card + bottom-sheet editor, 1000-char plain text) | `components/diary-view.js` note card + textarea | Day journaling (Codeberg #58a): one note per day, day-scoped like the water card (today or any selected past day). Wire: `daily_notes` in sync-1.2 (`contracts/sync-1.2.schema.json`), `days[].note` in diary-1.3; deterministic per-date record id (day count since epoch in the low 48 bits) so both apps merge to last-write-wins per day. Sync + diary export carry notes; backup restore restores them. CSV export stays entry-level (no day-note column). **Optional since 2026-08-24** (device pass): `showNotes`/`dailyNotesEnabled`, default off; card only renders when enabled |
| Progress (weight, BF, measurements, forecast) | shared | `ui/progress/` | `components/progress-*` | FCAST/ADAPT mirrored; interactive chart tips; equal range chips; **configurable default range** (`progressDefaultRangeId`, factory `1W`) with last-viewed precedence; display-only **7-day weight trend** overlay (not Adaptive Goals). Complete-day averages for calories and macros. **Nutrient averages** (Codeberg #75; same window, today excluded) are **off by default** (`progressNutrientAverages`); Customize Progress / App settings turn them on after a warning that barcodes and photos often omit those values, and the rows follow the **shared selection** (`progressNutrientAveragesSelection`: any goal-bearing non-macro nutrient, default fiber/sugar/sodium). **Android-only (2026-08):** `Customize Progress` settings sub-screen + optional per-site body-measurement trend plots (8 sites, off by default, `progressMeasurementSites` empty set; cm/in per height unit; one compact card per enabled site, range-chip filtered): closes Codeberg #18 |

| Settings (profile, goals, units, AI, data) | shared | `ui/settings/` hub + group sub-screens (Personal Info / Goals & Nutrition / **Food & Entry** / **Display** / **Trackers & Reminders** [+ Water, Nicotine, Reminders, Customize Progress sub-screens] / AI & Speech / Health & Data [+ **Sync** sub-screen]) | `settings-view.js` hub (Personal / Goals / App / AI / Data / About) | Android 2026-08 settings overhaul: **Food & Entry** group (logging, serving sizes, photo analysis), Water + Notifications + Sync as sub-screens, cross-linked rows between related screens (Goals ↔ Water, Water ↔ Notifications, AI ↔ Food & Entry, Data ↔ Sync), hub **Suggestions** card (dismissible nudges toward water/adaptive-goals/Health Connect/backup/notifications; Android-only), danger-zone separation for destructive actions. **Android 2026-02 findability pass:** hub split — **App & Display → Display** (look & feel + home/progress display; Week Starts On moved into Customize Progress) and new **Trackers & Reminders** group (Water / Nicotine / Reminders rows with live state); **Units** row in Personal Info (single Metric/Imperial toggle, mirrors onboarding; was only reachable inside the Height/Weight sheets); **hub search** (offline index of ~65 rows, label + group context, diacritic-insensitive; Android-only); Reminders naming unified (screen title = card label); Goals cross-links moved to a Related footer; Progress tab gains a one-tap **Customize progress** entry. **PWA still:** 5-group hub (no Trackers group, no search, no Units row); serving units/photo toggles stay under AI; Water stays under App; sync already a subpage. Custom P/C/F pins; browser speech language; Android-only called out. Accent default `system`; primary AI default `gemini`; food-row chips honor fiber when selected. **Android-only:** “Enter custom value” free-entry on every wheel-picked goal (calories, macros, keto net carbs, optional nutrients). **Goal locks:** Android shows a Locked/Auto chip on each calorie and macro row (tap to pin or release the current number; Recalculate and Adaptive leave Locked rows). PWA locks on Save when a custom calorie/macro field is filled, and Recalculate (AI or formula) honors those flags. **Android-only (2026-08):** appearance modes System / Light / Dark / **OLED** (true-black backgrounds, incl. all 4 Glance widgets; explicit modes also stop the widgets following the system); PWA keeps its own `data-theme` mechanism |
| Onboarding | shared | `ui/onboarding/` | `components/onboarding-*` | Birthday + editable plan-ready macros; BF stored as fraction |
| Food entry (manual, barcode OFF, AI photo/text) | shared | entry flows | entry + `photo-ai-flow` | **Android ahead (2026-08):** lightweight pre-Analyze staging: **text note required by default** before LLM (Skip note & analyze still available; after 3 empty skips offer “Don’t ask for a note again”; Settings → Ask for a photo note); optional label photo + grams; then Analyze morphs into the Log sheet with a ready-gate. Mid-flight tip/add-photo can re-analyze. **PWA still:** multi-photo review + analyze overlay, then FoodResult-like Log. Shared otherwise: **all images** to BYOK; barcode→OFF soft context; progressive phases + streamed partials; quantity/unit picker; constituents; Ask AI to correct; voice; barcode; progressive meal;  serving-unit inference. **Android-only (2026-08, #77):** session Log time on Add Food + apply date/time to the meal slot on Edit Food. Android-only: heuristic customization, **per-entry serving pencil** (rename / re-weight / delete leftover units; Codeberg #10 / #59 follow-up; PWA has no servingEdit UI), saved-photo reprocess, **relative quantity edits** (`+20` / `-10` deltas in the serving-quantity field), **analysis queue + failure auto-save** (Codeberg #53: failed AI calls persist photos + description; every prompt lands in a 7-day history; staged/queued prompts run later from the Add Food hub — Android-only device state, photos stay local, no sync/contract impact). Shared: **2D matrix codes** (QR / Data Matrix, GS1 GTIN extraction, shared `testdata/parity/barcode-codes.json`); live scan ignores non-product frames and keeps scanning until a GTIN-normalizable code is read (shared picker semantics, `docs/local/PLAN_BARCODE_SCAN_RETRY_FIX.md`). **Diary-card serving echo (2026-08, Codeberg #65):** Home food-log cards on both apps show the logged serving unit ("2 oz", "1 bowl") when it still matches a valid non-gram option; stored selections without an option or with gram/blank ids keep today's grams line. Display-only over existing `selected_serving_unit` fields — no wire/schema/export impact |
| Food database search (OFF + USDA + Swiss) | android-only | Add Food “Search food” sheet + `FoodDatabaseSearch` | — | Live **Open Food Facts** product/brand search + offline **USDA** (CC0) and **Swiss Food Composition DB** (naehrwertdaten.ch) SQLite indexes, per-source chips and provenance badges; picking a hit prefills the review sheet. Wire `source` = `search`, `grounding.sourceKind` round-trips `usda`/`openFoodFacts`/`swiss`. PWA has no OFF search client yet: parity tracked as follow-up. Offline indexes add ~5.3 MB to release APKs. search path hardened for keystroke cancellation (`cancellableExecute`, no piling-up calls) + serialized offline lookups + `FoodSearch` phase logs (Codeberg #26). |
| Saved meals / recipes / favorites | shared | models + UI | recipes / saved | Add Food heroes: Photo / Note / Recents; Frequent & Favorites in More. **Editable favorites (Codeberg #66, 2026-08):** favorites are a saved-foods library — every favorites row opens a permanent editor (Android `EditFavoriteSheet`; PWA `#/entry/favorite/<id>`): meal slot, name, serving, macros, note; saved edits update the favorite in place (id stable for sync LWW) and never touch the diary. Rename is collision-guarded (name is the identity key). Reviewing a saved meal now starts from its stored meal slot on both apps (was Android-only divergence: Android used the time-of-day guess); one-tap relog keeps logging into the current meal. **Recents (Codeberg #76):** all-time unique foods with search and Recent / A–Z / Size (kcal) sort; Log again heading opens this list. Hub chips stay 30/90 × 10. |
| Copy day / meal share | shared | `MealShare.kt` | `meal-share.js` | Native `chompass://`; PWA `#/add-meal?d=`; wire `v` **2** (`contracts/meal-share-v2.schema.json`; import still accepts v1) |
| Diary JSON export/import 1.4 | shared | `export/Diary*` | `diary-format.js` | Contract: `contracts/diary-1.4.schema.json` (import still accepts 1.0–1.3); free-string `meals[].type` + optional `export.meal_catalog` (Codeberg #61); serving units + embedded constituents; per-day `note`; optional per-item `caffeine_mg` |
| Meal catalog (custom names / extra meals) | shared | `MealCatalog` + Settings Meals sheet | `chompass-core/meal-catalog.js` + meal times | Builtin ids stay `breakfast/lunch/dinner/snack/other`; custom ids `c_*`; Home groups by id |
| Body-metrics JSON 1.0 | shared | `export/BodyMetrics*` | `body-metrics-format.js` | Contract: `contracts/body-metrics-1.0.schema.json` |
| User-hosted sync 1.2 (WebDAV / sync JSON) | shared | `sync/SyncRepository`, `export/SyncDocument` | `sync.js`, `chompass-core/sync-*.js` | Contract: `contracts/sync-1.2.schema.json` (import still accepts 1.0/1.1); excludes API keys and food photos; carries day-granular `daily_notes` (Codeberg #58a) |
| Deterministic goal formulas | shared | `UserProfile` + services | `chompass-core/formulas.js` | Goldens: `testdata/parity/formulas-expected.json`; custom macro pins honored |
| Macro day types / goal journal (#60) | shared | `MacroPlan` + `MacroPlanResolver` + `GoalJournal` + `DayTypeActiveStats` (Home chip, Settings editor, Progress journal-backed range goals + per-day bars, diary export per-day targets, `goal_journal` sync array, per-type typical active burn) | `chompass-core/macro-plan.js` + `macro-plan-edit.js` + `day-type-active.js` + Settings/Home/Progress/Coach/Recalculate mirrors | Day-type profiles (Training/Rest) with manual / weekday-map / repeating-cycle assignment, frozen per-day goal journal (MACRO-CYCLE-A/B/C/D). Per-type typical active (DTP-ACT, 28d / ≥3 samples, derived only). Goldens: `testdata/parity/macro-plan-expected.json`, `day-type-active-expected.json`; journal rows ride sync-1.2 `goal_journal`. Opt-in since 4.0.0 |
| AI Coach (BYOK cloud) | shared | coach + AI services | `lib/ai/` | Overlapping provider/model defaults: `testdata/parity/ai-provider-defaults.json` (Gemini / Anthropic Haiku / OpenAI mini); goal-prompt formula lines: `goal-formula-prompt-fragments.json` |
| Manual active burn | shared | `ManualActiveEntry` + Add Food (history list: name, edit, delete; Codeberg #77 pickers android-only) | `manual-active.js` + Add Food (named list with edit/delete) | Local prefs only (not diary/sync); merges into ADD_ACTIVE gauge with activity-level estimate |
| Health Connect | android-only | HC services | — | Measured TDEE path not on web. Nutrition writes carry optional `caffeine` (mg) on `NutritionRecord` (2026-08, #55) |
| Widgets / notifications | android-only | glance / NotificationService | — | |
| On-device LLM (Gemma / LiteRT) | android-only | debug + production flag | — | See `ON_DEVICE_LLM.md` |
| Grounded entry | wip / android | grounding services | — | Feature flag; see `GROUNDED_ENTRY.md` |
| Full i18n pack | shared | `res/values*` | `lib/i18n/` + catalogs | Shared 18-locale contract [`testdata/parity/locales.json`](../testdata/parity/locales.json); see [`LOCALIZATION.md`](LOCALIZATION.md). PWA covers core surfaces; Android has fuller resource packs with EN fallback |
| Service worker / installability | pwa-only | — | `sw.js`, manifest | |
| IndexedDB local store | pwa-only | DataStore | `db.js` | Optional WebDAV sync is shared |
| Desktop chrome (rail + wider column) | pwa-only | — | `css/main.css` `@media (min-width: 900px)` | Left nav rail, `56rem` content, content-pinned FAB/coach/banner, constrained sheets; phone shell unchanged below breakpoint |

## Maintenance rules

1. When adding a **user-visible** surface on one client, update this table in the same change. `scripts/check_parity_matrix.py` (via `release:check-parity`) fails if required surfaces or Status values go missing/malformed.
2. When changing formulas or export JSON, follow [`CALCULATION_METHODS.md`](CALCULATION_METHODS.md) and bump / extend `contracts/` + `testdata/parity/` as needed.
3. When changing shared BYOK model defaults or AI goal-prompt formula fragments, update both clients **and** the matching fixture under `testdata/parity/`.
4. Platform features (HC, widgets, on-device LLM) stay **android-only** by design: do not treat them as parity bugs.
