🔧 Trepo

Android build progress

The Android port, built by an autonomous loop against the live backend. This page rebuilds from the repo itself — the feature table below is the tracker the agent writes to, and the feed is every commit it has made.

17/22
Verified on device
19/22
Built or verified
195
Tasks completed
0 min ago
Last activity
verified on devicebuilt, not yet drivenin progressnot started

Milestone 1 — core loop

8 of 9 verified on device
#FeatureStatusWhat was observed
0 App installs, launches, renders on device verified on device 2026-08-18. Performing Streamed Install -> Success on the A36, am start brings up MainActivity, dumpsys confirms ResumedActivity: ai.trepo.android/.MainActivity, pid alive, zero entries in the crash buffer. Screenshot from the handset shows the design system rendering correctly: cream #F5E9D8 ground, the hard offset shadow with no blur under both the card and the button, heavy uppercase wordmark. It reads as Trepo rather than as stock Material, which was the point of porting the idiom rather than the pixels. Re-verified 2026-08-20 after the tab-bar capsule landed: installs and launches clean on the A36, crash buffer empty across three tab taps. Note for the next run - the runbook's «device address» refused the connection; the handset answered on USB (RFCY322W52W) and on «device address».
1 Phone auth (send + verify code) verified on device Flow verified against production by direct HTTP on 2026-08-17: send-code{"message":"Code sent successfully"} (200), verify-code → 200 returning user_id «account id» owner «id»", and a JWT. The documented path is wrong, see Spec corrections. App code stores user_id as the owner and never owner_id. On device 2026-08-18: tapped Send code, the code field appeared and the button flipped to Verify (so the round trip landed); entered «code»`, tapped Verify, and the app moved to the kitchen screen. No crash.
2 Kitchen tab: 2-column category card grid in progress GET /kitchen/{user_id} returns 200 with no auth header, shape {owner, items, count}. Sections render in enum order; rows keyed by _id, never index. Items whose category is outside the enum get their own "Unrecognised category" section rather than being dropped silently. On device 2026-08-18: three items added by text rendered under three correct section headers - PRODUCE / DAIRY & EGGS / PANTRY - in enum order, each in the hard-shadow card. Tapping Remove deleted the row and the server agreed (count 4 -> 3). No crash in the buffer. This row was signed off on 2026-08-18 and was BROKEN on 2026-08-21 by an ordinary add - see B6. The verification above was honest when written; what it could not cover is a wire shape this account had not yet produced. Re-driven 2026-08-21 after the fix: two items render under PREPARED & OTHER with correct quantity labels.
2a ↳ rename an item by hand verified on device iOS change #18: every field on an item was editable except its name, and the only route to it was the AI "fix this item" correction - a re-identification, not a rename, which is why users reported it as not saving. PUT /kitchen/{owner}/{item_id} has always accepted product_name. Shipped in 9b9280e. Probed against production first, and the probe found a trap: the server does NOT validate the name. {"product_name":""} returns 200 "Item updated successfully" and the item reads back with product_name: null - the name is destroyed and the row renders "Unnamed item" with no route back except another rename. A 200 gives the client nothing to catch, so blank is refused in the dialog *and* in renameKitchenItem. The optimistic update also needed a pending overlay, not just a local edit: settleEnrichment re-reads the whole kitchen several times after every add, and that server list still carries the OLD name until the PUT commits, so a plain optimistic write would snap back under the user's finger and flip forward seconds later - the B4 failure mode again. pendingRenames is keyed by item id, applied in acceptServerItems, which all six server-sourced load paths now route through so none can miss it; it is retired only if the in-flight name is still the pending one, so a second rename queued behind the first is not reverted, and it is cleared on sign-out. Driven on device 2026-08-20 against a deliberately adversarial fixture - two rows both named "Dill Pickles" - so a rename that matched by name instead of id would be visible. Tapped Rename on the first: the dialog is Trepo white with the card radius, prefilled, and Save is disabled on an unchanged name. Cleared the field and the guard fired - red border, "An item needs a name.", Save muted - and tapping Save while blank was inert, the dialog stayed open. Typed Grandmas Spears and saved: the first row changed and the second stayed "Dill Pickles", and the server agreed on readback (c4653c2a… -> 'Grandmas Spears', e87512a6… -> 'Dill Pickles'). Empty crash buffer. Both fixture items deleted afterwards, account back to count 0.
3 Check-in by photo → review → commit verified on device Whole flow written and driven on device 2026-08-18: tapped Check in by photo, granted the camera, captured, watched the analysing state, and landed on Review. The photo was of a dark surface so the analysis returned 0 candidates, which exercised the empty path: a yellow note reading "Nothing recognisable in that photo. Try again, closer.", the commit button correctly reading Add 0 to kitchen and disabled, and no crash. Populated path now verified on device too, 2026-08-18, using the photo picker rather than a live capture: picked a 12 line receipt, all 12 rows rendered with names, counts, category and storage chips, tapped Add 12 to kitchen, and the server confirmed count 12 with every category a valid enum value. Button said 12, 12 landed - no sign of the 20-vs-18 gap on this path. Fridge path verified separately 2026-08-18 with a fridge interior photo in bulk_inventory_deep: 6 candidates rendered (Maple Syrup, Tub of Spread, three condiment bottles, a tube of paste), all with valid enum categories. Then the include/exclude guard was tested deliberately: unticking one row moved the button from Add 6 to kitchen to Add 5 to kitchen, committing wrote exactly 5, and the excluded row was correctly withheld from the server. That is the 20-vs-18 shape probed directly and not reproduced.
3a ↳ category dropdown on each review row verified on device Options derive from KitchenCategory.entries. Went further: ReviewItem.category is typed as the enum, non-nullable, so the review screen is structurally incapable of sending an out-of-enum category. That is the bug that cost iOS a 36 row backfill; a type is a stronger guarantee than a well-behaved picker. Analysis returned leftovers for a takeout container, which is a valid enum value.
3b ↳ submit pinned above the keyboard verified on device Same imePadding() container as the text-add bar. The button label counts selected, the same list that becomes the payload, so the count and the commit body cannot disagree by construction - the cheapest possible way to never reproduce the 20-vs-18 shape.
4 Check-in by text verified on device POST /kitchen/{owner}/text-add verified against production: "eggs, oat milk, bananas" → 3 rows, correctly categorised dairy_eggs / pantry / produce, with storage inferred fridge / null / pantry. Surfaced as a permanent bar at the bottom of the kitchen screen, not buried in a menu. On device 2026-08-18: typed oat milk, bananas, cheddar cheese into the pinned bar, tapped Add, and all three landed correctly categorised. The bar stayed above the keyboard throughout.
5 Shopping list: add / check off / delete verified on device UNBLOCKED 2026-08-19, and the block was never going to lift by probing harder. The shopping list is not on the grocery host and it is not REST. It lives on the auth host - POST https://1zc0nh8x48....«aws host»/v1/list - and every action is one POST to that single path with an operation field in the body. That is why nine REST-shaped probes on {grocery} all 404'd: right idea, wrong host and wrong protocol shape. Found by reading the iOS client (TrepoAPIService.swift:12 base + :106 fetchItems), which is the source of truth the brief points at, then verified live against production rather than trusted: view / add / set_action / update_item / remove all round-tripped on the App Review account. Full contract in Spec corrections below. On device 2026-08-19: added oat milk, sourdough bread and bananas through the app's own add bar and watched all three render in hard-shadow cards with the iOS checkbox geometry; tapped the sourdough checkbox and it filled red with a tick, struck the label through, dimmed the row to 55% and dropped the header from 3 to buy to 2 to buy - the server independently read back CHECKED. Tapped the x on bananas and it left both the screen and the server. CLEAR CHECKED, which only renders while something is actually checked, removed sourdough and then removed itself. Zero entries in the crash buffer. Account left empty. Design-swept 2026-08-20: the header title was 28sp against iOS's 44 and the CLEAR CHECKED chip was missing its leading check glyph; both corrected in 5ab00ce/1df2f34 and re-driven on the handset. The row stays *verified on device* - the behaviour was never wrong. Both of this row's old caveats are now closed: the suggestions chip row shipped in 4ac308e, and the store *sections* - poster headers, the persisted colour rotation, collapse and a per-group composer - in f5aa551/9e02cb8, both verified on device. What is still NOT built and NOT covered here: drag-to-reorder, group rename, and Add New Group. See the store-groups sweep below.

Milestone 2 — saved recipes

4 of 6 verified on device
#FeatureStatusWhat was observed
6 Saved recipes list + save by link verified on device Shipped in 7a9450f; the row said not started until 2026-08-20, which was a bookkeeping failure, not a build one. Seen on device 2026-08-20: the Cook tab renders 12 saved in hard-shadow cards with RECIPE badges, and save-by-link is the pinned bottom bar. b457c57 later fixed two real semantics bugs here (a 202 carrying an id is not a save; a GET that omits a recipe is not a delete).
7 Custom tags: create + apply to a recipe verified on device many-to-many, orthogonal to meal_category. Shipped in 7a9450f. Seen on device 2026-08-20: the filter row renders All plus four real tags, and selecting one filters the list.
7a ↳ long-press a tag to RENAME verified on device 409 on duplicate name MUST be shown, not silent. Shipped in 7a9450f; the 409 path was exercised on device 2026-08-20 and it was broken - see B4. Fixed in c153bb3 and re-driven: opened the tag sheet on Homemade Garlic Bread, long-pressed MikeTest, renamed it to MikeTwo (an existing tag), and the server's own words - "A category with that name already exists" - rendered in a yellow note inside the sheet, directly above the tag rows. The name reverted to MikeTest in both the sheet list and the filter row, so the optimistic rename rolled back by id. Zero entries in the crash buffer. Nothing persisted, so no cleanup was owed.
7b ↳ long-press a tag to DELETE verified on device confirm with the recipe count; cascade keeps recipes. deleteTag exists and shipped in 7a9450f. Driven on device 2026-08-20, against the leftover MikeTest tag which the API showed carrying 2 assignments. Long-pressing the chip opens MikeTest / "Rename this tag, or remove it from your recipes." / Delete | Rename; Delete opens a second, counted confirm reading exactly Delete "MikeTest"? This removes the tag from 2 recipes. The recipes themselves are kept. - the count is real, not boilerplate, and it matched the two assignments GET /recipe-categories/{owner} had returned seconds earlier. On confirming, the chip left the filter strip without a refresh, the header still read 12 saved (cascade kept every recipe, as the copy promises), and the server agreed: the category was gone and assignments came back {}. Screenshot: docs/evidence/tag-delete-confirm-2026-08-20.png. Cleaning up a prior run's test tag was the fixture, so no production tag was destroyed to get this.
8 Multi-select delete via long-press built, not yet driven pre-select the long-pressed row - MainActivity.kt:472 does exactly this. Shipped in 7a9450f. Not exercised on device.
8a ↳ ownership gate in household scope built, not yet driven dim rows you do not own; keyed on savedBy. Shipped in 7a9450f. Unexercised and probably unexercisable here - the App Review account looks like a household of one, so the dimmed state may not be reachable. See the open question dated 2026-08-19.

Milestone 3 — meal calendar

3 of 3 verified on device
#FeatureStatusWhat was observed
9 Week view + add meal to a slot verified on device Household-scoped per change #17. Built in 62d1186. Driven on device 2026-08-20: the Cook tab's THIS WEEK section renders AUG 16 - 22 with a card per day, four slots each (BREAKFAST/LUNCH/DINNER/SNACK), a red TODAY badge on Thu 20, and the list auto-scrolls to today while the earlier days stay reachable by scrolling up. The </> buttons move the window a week at a time and refetch. Attribution is implemented - entries where added_by_me is false render added_by_name, falling back to "Household" because that field is nullable even for other people's entries - and delete is not gated on the creator. Attribution rendering verified on device 2026-08-20: the App Review account was temporarily moved into household 11586 alongside test fixture TestCompanion («account id») and a seeded entry "Partner Pasta Night" (Fri 2026-08-21, DINNER) was created by that companion. The server returned added_by_me=false, added_by_name="TestCompanion", and the handset rendered the entry card with the byline exactly "Added by TestCompanion" in teal beneath the title - NOT the null-fallback "Household". Screenshot: docs/evidence/attribution-byline-verified-2026-08-20.png. The shared-household state was torn down immediately afterwards (entry deleted, account left the household - both calendars verified empty). Leaving assigned the App Review account a fresh household id: «id» -> 11586 -> 73846. That is expected and harmless because everything keys off the user_id UUID «account id», which never changes.
10 Free-text entry in a slot verified on device source_type: manual. Driven on device 2026-08-20: tapped + on Tue 18 breakfast, got a BREAKFAST · TUE 18 dialog reading "What are you eating?", typed Oatmeal and Berries, tapped Add, and the entry rendered in the slot. Then navigated a week forward and back to force a refetch and it came back from the server, so it persisted rather than only appearing optimistically. Deleted afterwards; account left clean.
11 Generate shopping list from the week verified on device MealListPlanner is a port of iOS SavedRecipesView.swift:863 addIngredientsToList, including the three skip rules and the store = <recipe title> field reuse, with two divergences documented in the file (duplicates collapsed across the week; kitchen check is exact-name rather than personalization, which errs toward over-adding). Both paths driven on device 2026-08-20: with the week's ingredients already listed, ADD MISSING TO LIST reported "You already have everything for this week." - an explicit message, not a silent no-op. Cleared the list and re-ran: "Added 2 items to your list - skipped 4 you have or already listed.", and the List tab showed both rows grouped under the LEMON CHICKEN TRAYBAKE header, confirming the store-field reuse renders as intended. Zero entries in the crash buffer across the whole session.

Milestone 4 — dish log

2 of 4 verified on device
#FeatureStatusWhat was observed
12 Dish history, grouped by day + collapse in progress Collapse ported 2026-08-20 (e9ae100) - the row read plain *verified* while Android was rendering every group and iOS collapses to one. See the Health tab sweep below. Feed transport unchanged: GET {grocery}/dishes/{owner} -> 200 {owner, dishes:[...]}, same host as the kitchen. iOS also tries /dishes?owner= and swallows a 404 into an empty list; the first form answered, so Android calls it alone rather than dressing an outage as an empty log. On device 2026-08-20: the tab fetched, parsed the account's one real dish (Fried egg, 2026-07-02) and rendered the empty state - but the correct empty state: "Nothing logged in the last 7 days." with "1 older dish is on file." underneath. That distinction is not cosmetic. iOS filters the history to seven days, so an account whose only dish is older shows nothing at all, which is indistinguishable from a broken fetch. Scrolls cleanly, zero entries in the crash buffer. The dish row itself is unexercised - no dish exists inside the window on this account and nothing may be written to production to manufacture one, so the row layout is built to the iOS source and not yet seen.
13 Daily macro rings verified on device DailyReference denominators ported exactly (2000 / 150 / 250 / 70). On device 2026-08-20: the full-width calories card renders 0 CALORIES / consumed today with an empty flame ring, and three equal macro cards read 0g PROTEIN / 0g CARBS / 0g FAT, each with its own ring - all in the hard-shadow idiom. Zero is the true total for today on this account, and it is reached by summing the *local* day, not the UTC one. The ring fill is clamped to 1.0 and guarded against a non-finite fill, so an over-target day reads as a full ring rather than a second lap or a crashed draw.
14 Log a dish from the tab verified on device iOS puts a LOG A DISH CTA above the nutrition section. The CTA is now drawn and wired - this row read pipeline built, button not drawn until 2026-08-20, a bookkeeping failure and not a build one, the same way item 15 did. Verified by reading the code: DishLog.kt:185 draws the button (shadow fixed in a526f84), MainActivity.kt:1142 routes it into CaptureScreen(dishMode = true), onCapturedDish calls vm.logDish, and that hands off to DishUploadSession.start (MainActivity.kt:195), whose pending cards surface through vm.pendingDishes into the list at MainActivity.kt:1090. Drawn in a163ac6. The transport underneath shipped earlier in ca9d635: createDishPresign / uploadDishImage / pollDishResult on TrepoApi, plus DishUploadModels.kt, ported from iOS UploadAPIService and DishSessionManager.swift. The flow is POST {grocery}/presign with type=dish -> plain PUT of the JPEG to the returned S3 URL -> poll GET {grocery}/dish/result until is_terminal. Worth knowing: this is the app's first presign. Android's grocery check-in goes inline-base64 via /identify-async, so no part of the upload path was already there to reuse. Three ported guards: DishAnalysis is hand-parsed rather than @Serializable, because the Swift parser takes a number as int or double and ingredients/allergens as an array or a string holding one - and the int/double case is already MEASURED on this backend (calories came back 90.0, see DishRow), so strictness would land a dish the user waited on as blank; a 403/404 on the S3 PUT throws UploadNotRetryable rather than ApiMessage, because those mean the presign expired and retrying a dead URL only delays an honest error; and pollDishResult is one poll, not the loop, so the caller honours the server's poll_after_ms (this endpoint is shared with ~12,600 live iOS users) and can show the fast pass the moment it lands instead of blocking on final. The pending-dish card exists too: DishUploadSession carries a PendingStatus of UPLOADING/ANALYZING/FAST_READY/FINAL_READY/FAILED, so the card can show the server's fast pass the moment it lands rather than blocking on final, and dismiss(id) clears a row by id, never by an index captured across the suspend point (RELIABILITY R3). The compression trap this row used to warn about is closed: CaptureScreen now takes a dishMode flag and a single deliver lambda decides what a photo becomes, so the dish leg gets ImagePrep.jpegForDish (raw JPEG from 2000px, matching iOS compressForUpload) and cannot silently inherit the check-in path's 1600px base64. Still not verified on device: the whole flow writes a real dish to production and this backend is shared with ~12,600 live iOS users, so exercising it needs a deliberate write-then-delete pass, not a casual one. Built and wired to the iOS source; not yet seen end to end. | Driven end to end on the handset 2026-08-20, first time this row has been exercised rather than read. Tapped LOG A DISH on the Dish Log tab, got the dish-mode camera (it says PHOTOGRAPH YOUR PLATE, not the check-in copy), picked a photo, and landed back on the tab immediately with a pending card reading Analyzing your dish… / Estimating portion size… over a progress ring at 39%. About forty seconds later the card was gone and the real row was in RECENTLY UPLOADED / TODAY: "Chinese takeout noodles", 1:39 PM, 550 calories, 22g / 74g / 18g, with the emoji thumbnail. The daily rings recomputed at the same moment, 190 -> 740 cal, 6 -> 28g protein, 35 -> 109g carbs, 3 -> 21g fat, i.e. exactly the dish's macros added to the day. Ran a second dish afterwards on the fixed build (430 cal, 620 total) to confirm it repeats. So presign -> PUT -> poll -> feed all work against production from Android. Evidence: docs/evidence/dish-log-pending-card-2026-08-20.png, dish-log-result-2026-08-20.png. One real bug came out of driving it, now fixed in 7acca74: deleting a just-logged dish resurrected its pending card as a ghost stuck at 95% reading "Results will appear here shortly", because visible() only *hid* a settled pending row for as long as the feed carried the real dish — delete the dish and the hide condition flips back. refreshDishes now calls DishUploadSession.reconcile, which retires the pending copy for good once the feed has taken over. Re-verified on device: after deleting, the list goes straight from RECENTLY UPLOADED to TODAY with no ghost (docs/evidence/dish-delete-no-ghost-2026-08-20.png).
15 Dish detail / correction / delete in progress Shipped in 5ed5c19; the row read not started until 2026-08-20, a bookkeeping failure and not a build one. Tapping a dish row opens the detail sheet iOS reaches from health_dish_tap (MainTabView.swift:16806), with the correction and delete actions under it. Three ported guards, each a bug avoided: recharacterize returns the corrected dish under item without echoing _id, so it is parsed with the sent id as fallbackId rather than dropped; the corrected row keeps its original createdAt, because a server date that moved would relocate the dish in the grouped list or push it out of the seven-day window, which reads to a user as "my correction deleted my dish"; and delete restores by id, never by an index captured across the suspend point (RELIABILITY R3). deleteDish also stopped throwing a bare IllegalStateException, which had made the server's own error text unreachable. Layout measured on device 2026-08-20 - that is how the nav-bar overlap (3d8ee41) and the red Cancel button (51b8f40) were both caught. The round-trips are deliberately unexercised: the sheet only opens from a row inside the seven-day window, this account's one real dish sits outside it, and manufacturing or deleting production dishes to exercise a destructive path is not a trade worth making. Built to the iOS source, not claimed as verified. | Detail and delete driven on device 2026-08-20. Tapping a dish row opens the detail sheet and it renders in full: emoji thumbnail, title, Thursday, Aug 20 at 1:39 PM, the four macro tiles (550 / 22g / 74g / 18g), an ANALYSIS block carrying the server's prose and serving line ("Chinese-style noodles in a white takeout box with chopsticks. / Serving: 1 small takeout box (about 2 cups)"), an ESTIMATED INGREDIENTS list, and the CORRECT THIS DISH / DELETE DISH actions. Delete works on a dish the app just logged: the confirm dialog reads "Delete this dish? This can't be undone.", and on confirming, the row leaves the list and the daily rings fall back by exactly that dish's macros (740 -> 190 cal). Delete is NOT reachable for older dishes — on a dish logged an hour earlier the server answers 404 Dish with id 24ecd4fe-… not found and the sheet shows that message rather than pretending the row went away. Confirmed with curl that this is server-side and not the client's id handling; logged under Backend asks. CORRECT THIS DISH driven on device 2026-08-20, so every action on this sheet has now been exercised. Tapping it opens the correction screen (sparkle badge, "Tell us what's wrong and we'll fix it.", a hint reading "e.g. This was a Caesar salad, not a garden salad"), SUBMIT CORRECTION is correctly disabled until text is entered, and typing "This was steel cut oats with raspberries, not blueberries" enables it. The round-trip itself is blocked server-side, not by the client: submitting returned 404 Dish with id 24ecd4fe-… not found - the SAME dead zone already logged for DELETE under Backend asks, now confirmed to cover recharacterize too. Reproduced outside the app with curl on BOTH dishes on this account, including the July Fried egg row that iOS itself created. This is not an Android id-handling bug: iOS parses _id from the same feed into backendId (DishAPIService.swift:131) and sends exactly that to recharacterize (MainTabView.swift:17055), so iOS hits it identically. What IS verified here is the client's failure path, and it behaves: the screen shows the server's own message in red, keeps the user's typed correction rather than clearing it, and leaves SUBMIT CORRECTION enabled so the attempt is retryable - it does not fake success or blank the sheet. Evidence: docs/evidence/dish-correct-404-2026-08-20.png. The happy path stays unproven until the backend dead zone is fixed, so this row remains *partly* verified.

Commit activity, by hour

Each bar is one hour. The nightly gap is deliberate — the loop is scheduled 6am–11pm PT. Peak hour: 21 commits.

Fri 14 AugSat 15 AugSun 16 AugMon 17 AugTue 18 AugWed 19 AugThu 20 Aug

Every task completed, newest first