Commit Graph

215 Commits

Author SHA1 Message Date
Yusuf Suleman
ae3b3f11bf fix: card thumbnail overflow — use GeometryReader to constrain image
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Images with .fill were expanding beyond the 180pt frame because
SwiftUI's .clipped() doesn't constrain the layout, only the
rendering. Now uses GeometryReader to set explicit width + height
on the image, then clips the container. Guarantees 180pt max
regardless of image aspect ratio.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:15:19 -05:00
Yusuf Suleman
92a44faac3 fix: three Reader bugs — image overflow, load more, refresh read state
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
#22 Image overflow: added .frame(maxWidth: .infinity) before
.frame(height: 180) on AsyncImage to constrain width within card.
Card's .clipShape already clips corners.

#23 Load more not triggering: added loadMoreIfNeeded(for:) that
fires onAppear for entries 5 from the bottom. No longer relies
solely on the bottom sentinel Color.clear which could be missed.
Also increased sentinel height from 1pt to 40pt.

#24 Refresh not updating read state: flushDeferredReads() now
called before vm.refresh() in .refreshable. Deferred marks are
synced to API before re-fetching, so the server returns correct
read states. Also clears markedByScroll set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:08:27 -05:00
Yusuf Suleman
d8f0e5d845 feat: animated macro rings, bars, and staggered meal card entrance
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
MacroRing:
- Animates from 0 to target on appear (spring, 1.0s response)
- Center value fades in + scales up with 0.4s delay
- Updates animate smoothly on data change
- .contentTransition(.numericText()) on calorie count

MacroBar:
- Bar width animates from 0 to target on appear (spring, 0.3s delay)
- Updates animate smoothly on data change
- .contentTransition(.numericText()) on values

TodayView:
- Meal sections stagger in: fade up with 0.08s delay per card
- Re-animates on tab switch (onAppear resets animated flag)
- Re-animates on date change
- Spring physics for natural feel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:20:21 -05:00
Yusuf Suleman
bf2ff59ade feat: auto-focus search field when Quick Add opens in sheet
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Keyboard appears automatically when opening Quick Add from the
food assistant sheet. 300ms delay lets the sheet animation finish
first so the keyboard doesn't interfere with the presentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 12:02:39 -05:00
Yusuf Suleman
0a10d297cd fix: keep pill selector + swipeable pages (Quick Add first)
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Pill tabs (Quick Add / AI Chat) stay visible and tappable.
Pages are also swipeable. Pills sync with swipe position.
Quick Add shown first, swipe left for AI Chat.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:53:32 -05:00
Yusuf Suleman
66ab375ee0 feat: swipeable food sheet — Quick Add first, swipe for AI Chat
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Replaced pill tab selector with swipeable TabView (.page style):
- Page 0: Quick Add (food search list) — shown first
- Page 1: AI Chat — swipe right to access
- Custom dot indicators replace the old pill tabs
- Swipe gesture between pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:52:01 -05:00
Yusuf Suleman
a5c95c2e5f feat: widget has two tap targets — fitness + add food
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s
Widget background tap → platform://fitness → opens Fitness tab
Widget + button tap → platform://add-food → opens Fitness + food assistant

Small widget: + button in bottom-right corner (emerald green)
Medium widget: + button in bottom-right corner
Lock screen widgets: single tap → fitness (no room for + button)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:45:08 -05:00
Yusuf Suleman
7cfe3eeed5 feat: tap widget → opens app to food search
All checks were successful
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 13s
Widget: .widgetURL(platform://add-food) on all widget sizes
App: .onOpenURL handles platform://add-food → switches to Fitness
tab and opens the food assistant sheet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:41:21 -05:00
3002c1f59d add widget Info.plist with NSExtension
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
2026-04-04 11:36:21 -05:00
Yusuf Suleman
5d51ac6833 harden: widget edge cases — expired session, account switch, cache
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s
1. Session expires: widget gets 401 → clears stale cookie from
   App Group → stops retrying with bad auth → shows cached data
   until user opens app and re-authenticates

2. Account switch: login() now calls clearWidgetAuth() BEFORE
   syncCookieToWidget() — clears previous user's cached calories
   before writing new user's cookie. No brief display of wrong data.

3. Logout: already correct — clearWidgetAuth removes cookie +
   cached data, widget shows 0/2000

4. Minimum data: only session cookie + 2 cached numbers + timestamp
   in App Group. No passwords, no user IDs, no PII.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:31:44 -05:00
Yusuf Suleman
e21a26db18 feat: widget fetches calories from API independently + shared auth
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s
Widget:
- Fetches /api/fitness/entries/totals and /api/fitness/goals/for-date
  directly from gateway using shared session cookie
- Falls back to cached data in App Group UserDefaults if network fails
- Refreshes every 15 minutes via WidgetKit timeline
- Each phone shows the logged-in user's own data

Auth sharing:
- AuthManager.syncCookieToWidget() copies the session cookie to
  App Group UserDefaults on login and auth check
- Widget reads cookie and makes authenticated API calls
- Logout clears widget auth + cached data

Data in App Group (group.com.quadjourney.platform):
- widget_sessionCookie: auth token for API calls
- widget_totalCalories: cached fallback
- widget_calorieGoal: cached fallback
- widget_lastUpdate: cache timestamp

HomeViewModel also writes cache on each loadTodayData() as fallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:29:52 -05:00
2d4cafa16e add App Group entitlements for both targets
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
2026-04-04 11:29:26 -05:00
Yusuf Suleman
9965b1d634 feat: calorie ring widget — home screen + lock screen
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Widget displays:
- systemSmall: calorie ring + "X left" text
- systemMedium: ring + "Calories" / "X of Y" / "X remaining"
- accessoryCircular: gauge ring for lock screen
- accessoryInline: "🔥 845 / 2000 cal" text for lock screen
- accessoryRectangular: linear gauge + calorie count

Data flow: main app writes totalCalories + calorieGoal to
UserDefaults on each loadTodayData(), then calls
WidgetCenter.shared.reloadAllTimelines(). Widget reads on
15-minute refresh cycle.

Note: currently uses standard UserDefaults (same app container).
For production, migrate to App Group UserDefaults so widget
process can read the data. Requires Xcode App Group setup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:21:40 -05:00
a4ebe77973 feat: add PlatformWidget extension target
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s
2026-04-04 11:19:18 -05:00
Yusuf Suleman
c13259c2b5 polish: depth, contrast, and layering refinements
All checks were successful
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 13s
1. Background — reduced warmth, more neutral:
   Dark:  #0e0d0b → #0d0d0b (less warm, more true black)
   Light: #EDE6DA → #EBE6DE (cooler sand, less yellow)

2. Cards — increased elevation:
   Shadow: 0.04/6/2 → 0.08/8/3 (more visible lift)
   Spacing: 12pt → 14pt between cards, 4pt → 8pt top padding

3. Text — more neutral for glass legibility:
   Primary light: 0.12 → 0.10 (darker)
   Secondary: warmer gray → neutral gray (0.40 uniform)
   Tertiary: warmer → neutral (0.58 uniform)

4. Accent — slightly deeper in light mode:
   #8B6914 → #805E0F (more contrast against glass)

5. Dark mode accent — slightly brighter:
   0.78/0.62/0.25 → 0.82/0.65/0.28

All changes are color/shadow/spacing only. No layout or
architectural changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:54:32 -05:00
Yusuf Suleman
f4b527e70b fix: increase surface contrast — cards now clearly separate from background
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Dark mode:
  Canvas: #1a1714 → #0e0d0b (deeper black, more separation)
  Card:   #26231f → #1e1b17 (warmer, 7% brighter than bg)

Light mode:
  Canvas: #F5EFE6 → #EDE6DA (cooler sand, slightly darker)
  Card:   #FFFCF8 → #FFFFFF (clean white, max contrast)

Both modes now have ~7% brightness gap + temperature contrast
(warm bg, cleaner card). Cards visually float without needing
borders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:40:45 -05:00
Yusuf Suleman
c74f36a94d fix: widen speed gaps — Slow 1.0x, Med 2.0x, Fast 3.5x (was 1.5/1.75/2.0)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
2026-04-04 10:31:55 -05:00
Yusuf Suleman
01c63d69d0 feat: auto-scroll speed cycles via tab bar button (Slow/Med/Fast)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Tab bar action button cycles through speeds on each tap:
- ▶ (play) → tap → Slow (1.5x, hare icon)
- Slow → tap → Med (1.75x, walk icon)
- Med → tap → Fast (2.0x, bolt icon)
- Fast → tap → back to Slow

Touch feed to stop → icon returns to ▶

Removed speed controls from Reader toolbar — all speed control
is now in the single tab bar button. Clean, no overlays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:28:03 -05:00
Yusuf Suleman
e0ae9cb95f fix: inline title (no large title drop) + restore canvas background
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s
1. .navigationBarTitleDisplayMode(.inline) — title stays at top
2. .background(Color.canvas) — restores warm cream background

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:11:09 -05:00
Yusuf Suleman
0f1a35ab84 simplify: remove sub-tabs, starred, feed filter chips from Reader
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Reader now shows only unread entries. Removed:
- Unread/Starred/All sub-tab selector
- Feed filter chip bar (categories)
- All related state (selectedSubTab) and helpers

Glass nav bar shows: "Reader" title + "74 unread" subtitle
Trailing toolbar: grid/list toggle + ellipsis menu
Auto-scroll: speed controls in leading toolbar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:07:40 -05:00
Yusuf Suleman
d75fb870d7 feat: Liquid Glass navigation bar for Reader (iOS 26 standard APIs)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Removed .navigationBarHidden(true) and all custom header layout.
Now uses standard iOS 26 navigation APIs that get Liquid Glass free:

- .navigationTitle("Reader") + .navigationSubtitle("74 unread")
- ToolbarItemGroup for sub-tabs (Unread/Starred/All) on leading
- ToolbarSpacer between groups
- ToolbarItemGroup for grid/list + menu on trailing
- Feed filter chips in .bottomBar toolbar
- When auto-scrolling: toolbar swaps to speed controls

The glass nav bar is translucent — content scrolls underneath.
Collapses to inline glass pill on scroll (system behavior).
No custom backgrounds, no custom layout — all system-managed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:03:58 -05:00
Yusuf Suleman
61cd78e080 feat: speed controls in collapsed tab bar via safeAreaBar
All checks were successful
Security Checks / dependency-audit (push) Successful in 27s
Security Checks / secret-scanning (push) Successful in 7s
Security Checks / dockerfile-lint (push) Successful in 6s
Uses .safeAreaBar(edge: .bottom) on the Reader content — the iOS 26
API that places content in the collapsed tab bar area, same position
as the Now Playing mini bar. Speed controls [ - ] 1.50x [ + ] appear
in the glass bar when auto-scrolling.

Removed the floating speed pill overlay from ContentView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 09:00:42 -05:00
Yusuf Suleman
e37444c62e fix: speed pill and feedback in same VStack — no more guessing position
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Speed pill and feedback button were in separate VStacks with
independent absolute padding, causing misalignment. Now they share
one VStack with .padding(.bottom, 70) at the container level.
The speed pill sits directly above the tab bar area, positioned
relative to the same anchor as all other bottom controls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:55:30 -05:00
Yusuf Suleman
a452c0d4f2 fix: move speed pill up to not overlap cards (padding.bottom 50)
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
2026-04-04 08:52:14 -05:00
Yusuf Suleman
416a6ed3f8 fix: adjust speed pill position — was clipped at bottom, now 16pt above safe area
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s
2026-04-04 08:47:07 -05:00
Yusuf Suleman
a39e0377b5 fix: move speed controls into tab bar area (tab bar hidden during scroll)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:43:52 -05:00
Yusuf Suleman
63b6027902 fix: move speed controls to bottom of screen, use regularMaterial
Some checks failed
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Has been cancelled
Speed pill was floating mid-screen (padding.bottom 90). Moved to
bottom (padding.bottom 8) to sit just above the tab bar area.
Switched from ultraThinMaterial to regularMaterial for better
Liquid Glass look with more opacity and stronger blur.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:43:33 -05:00
Yusuf Suleman
17d10ec4c1 fix: eliminate auto-scroll jitter by deferring mark-as-read visual updates
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
ROOT CAUSE (confirmed by instrumentation):
Every markRead during auto-scroll mutated entries[idx].status,
causing SwiftUI to re-render the row (bold→regular, dot removed).
This changed card height, causing contentSize jumps of 10-128pt
per entry — visible as jitter.

FIX: During auto-scroll, collect entry IDs in deferredReadIDs
instead of mutating entries array. When auto-scroll stops,
flushDeferredReads() applies all status changes at once and
sends a single batched API call.

Manual scroll still marks immediately (no deferral needed since
the user controls the scroll position).

Removed all debug instrumentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:27:37 -05:00
Yusuf Suleman
39b9303918 debug: auto-scroll jitter instrumentation — tick timing, contentSize, loadMore, markRead
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s
2026-04-04 08:19:30 -05:00
Yusuf Suleman
976469f5fe revert: remove fake delegate calls for tab bar minimize
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Calling scrollViewWillBeginDragging/DidEndDragging on the delegate
didn't trigger tabBarMinimizeBehavior — iOS 26 likely tracks actual
touch events, not delegate calls. Reverted to avoid side effects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:08:55 -05:00
Yusuf Suleman
a82ae267b6 fix: tab bar collapses during auto-scroll
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 5s
tabBarMinimizeBehavior needs scrollViewWillBeginDragging to recognize
a scroll session. Now ScrollViewDriver calls it on the original
delegate when auto-scroll starts, and scrollViewDidEndDragging +
scrollViewDidEndDecelerating when it stops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:03:23 -05:00
Yusuf Suleman
395cca08dd fix: action button on Reader now toggles auto-scroll (not food assistant)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Bug: when action tab (value=3) was tapped, selectedTab was already 3
by the time handleActionTap ran. The check 'if selectedTab == 2'
always failed, falling through to food assistant.

Fix: use onChange(of: selectedTab) oldValue to capture which tab the
user was on BEFORE tapping the action button. Pass that to
handleActionTap(from:). If from Reader (2), toggle auto-scroll.
If from Home/Fitness, open food assistant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:00:43 -05:00
Yusuf Suleman
e2fc87b6aa feat: Tab(role: .search) with context-dependent action per tab
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Single separated circle in tab bar (like Photos search icon):
- Home/Fitness: shows + icon, taps opens food assistant
- Reader idle: shows play icon, taps starts auto-scroll
- Reader playing: shows pause icon, taps stops auto-scroll

Icon updates dynamically via computed actionIcon property.
handleActionTap() routes the tap based on selectedTab.
After action, selectedTab returns to the previous tab (doesn't
stay on the invisible "action" tab).

Speed controls still appear as glass capsule overlay when playing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:55:50 -05:00
8a8f865702 xcode update
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
2026-04-04 07:42:56 -05:00
144f24b7a0 resolve pbxproj conflict 2026-04-04 07:42:56 -05:00
Yusuf Suleman
e9373ceac3 fix: context-dependent FAB — play/pause on Reader, + on other tabs
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Removed Tab(role: .search) — it was always visible on all tabs.
Now the floating action button changes based on selected tab:

- Home/Fitness: FAB (+) → opens food assistant (same as before)
- Reader: Play/Pause circle → toggles auto-scroll
  - Idle: play.fill icon, warm accent background
  - Playing: pause.fill icon, red background
  - Tapping toggles between play/pause

Speed controls appear as glass capsule above the FAB when playing.
Auto-scroll stops when switching away from Reader tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:41:50 -05:00
Yusuf Suleman
1205ac38d0 fix: resolve iOS 26 deprecation warnings
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
- Text '+' concatenation → HStack (MealSectionView)
- UIScreen.main → UIWindowScene.screen (EntryListView, ArticleWebView)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:39:41 -05:00
Yusuf Suleman
640d816690 chore: bump deployment target to iOS 26.0
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Both devices run iPhone 17 / iOS 26. Removes availability wrappers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:37:39 -05:00
Yusuf Suleman
8fadb3f3e9 feat: auto-scroll play button in tab bar using Tab(role: .search)
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Migrated to new Tab API (iOS 18+). The auto-scroll play button
uses Tab(role: .search) with systemImage: "play.fill" — this gives
the separated circular placement on the trailing side of the tab
bar, identical to the Photos app search icon.

Tapping the play icon:
- Switches to Reader tab
- Starts auto-scroll

When playing: a Liquid Glass speed control capsule appears above
the tab bar with [ - ] 1.00x [ + ] [ stop ].

Removed the old floating glass pill implementation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:34:35 -05:00
Yusuf Suleman
5d2262e17a feat: auto-scroll play button at tab bar level (like Photos search icon)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Moved auto-scroll control from ReaderTabView to MainTabView so it
sits at the tab bar level, trailing side — matching the iOS Photos
search icon placement.

Idle: 48px glass circle with play icon (bottom-right, tab bar row)
Playing: expands to capsule with [ - ] 1.00x [ + ] [ stop ]
Spring animation between states.

Grid/list toggle and ellipsis menu moved inline to the sub-tab
header row (next to Unread/Starred/All) so they're always visible
without needing a toolbar.

ReaderTabView now receives isAutoScrolling and scrollSpeed as
bindings from MainTabView.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:28:12 -05:00
Yusuf Suleman
7815f56b4f fix: hide FAB on Reader tab, fix tab bar collapse during auto-scroll, position glass bar bottom-right
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 5s
Security Checks / dockerfile-lint (push) Successful in 4s
1. FAB hidden when selectedTab == 2 (Reader) — no plus button
   on Reader since it's for food logging

2. Auto-scroll now notifies original UIScrollViewDelegate via
   scrollViewDidScroll after each contentOffset change — this
   triggers tabBarMinimizeBehavior so the tab bar collapses
   during auto-scroll just like manual scrolling

3. Glass control bar positioned bottom-right (like Photos search
   icon) instead of bottom-center

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:22:14 -05:00
Yusuf Suleman
1b23525493 feat: Liquid Glass control bar for Reader (replaces hidden toolbar)
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s
Floating glass pill above the tab bar with .ultraThinMaterial:

Idle state: [ ▶ ] [ grid/list ] [ ⋯ ]
- Play: starts auto-scroll
- Grid/list: toggles card/list view
- Ellipsis menu: mark all read, refresh, manage feeds, add feed

Playing state: [ - ] 1.00x [ + ] | [ ■ ]
- Speed adjustment in 0.25 increments (0.25x–3.0x)
- Stop button (red)
- Animated spring transition between states

Removed .navigationBarHidden(true) toolbar items — all controls
now in the glass bar. Nav bar stays hidden (no title needed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 07:12:42 -05:00
Yusuf Suleman
85c3bb7a42 feat: Phase 1 auto-scroll engine for Reader feed
All checks were successful
Security Checks / dependency-audit (push) Successful in 23s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
ENGINE (ScrollViewDriver.swift):
- UIViewRepresentable placed inside ScrollView (zero size)
- Finds parent UIScrollView via view hierarchy traversal
- CADisplayLink at 60fps drives contentOffset.y smoothly
- Speed: 1.0x = 60pt/sec, adjustable 0.25x–3.0x in 0.25 steps
- User touch detection: intercepts UIScrollViewDelegate
  scrollViewWillBeginDragging → stops auto-scroll immediately
- Stops at bottom (contentOffset >= maxOffset)
- Forwards all delegate methods to SwiftUI's original delegate

INTEGRATION (EntryListView):
- Accepts @Binding isAutoScrolling + scrollSpeed
- ScrollViewDriver placed as first child in ScrollView
- Auto-scroll stops on: user touch, navigation back (onAppear),
  filter change, sub-tab change, reaching bottom

CONTROLS (ReaderTabView — temporary, Phase 1):
- Play/Stop button in toolbar (play.fill / stop.fill)
- When playing: [-] speed [+] controls appear inline
- Speed shown as "1.00x" with monospacedDigit

MARK-AS-READ:
- Auto-scroll drives real UIScrollView contentOffset
- This moves LazyVStack rows, triggering their GeometryReader
  onChange callbacks — the existing mark-as-read system fires
  naturally with no special case or bypass needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 06:51:08 -05:00
Yusuf Suleman
6ed7f8a230 fix: restore WKWebView alpha to 1 when reparenting to article container
All checks were successful
Security Checks / dependency-audit (push) Successful in 15s
Security Checks / secret-scanning (push) Successful in 3s
Security Checks / dockerfile-lint (push) Successful in 4s
The GPU warmup set webView.alpha = 0 to keep it invisible while
attached to the window. makeUIView reparents it to the article
container but never restored alpha — articles rendered invisibly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 06:15:59 -05:00
Yusuf Suleman
db77a6d34d perf: re-warm WebKit GPU on Reader tab appear if idle >60s
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
GPU process can exit due to idle timeout if user waits before opening
Reader. reWarmIfNeeded() checks elapsed time since last warm — if >60s,
re-attaches WKWebView to window and loads minimal HTML to restart the
GPU process. Called on ReaderTabView.onAppear.

No timers, no keep-alive loops. Just a timestamp check on tab appear.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:49:26 -05:00
Yusuf Suleman
1579633da0 fix: keep WKWebView attached to window to prevent GPU process idle exit
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
The GPU process was exiting due to idle timeout between warmup and
first article open. Now the WKWebView stays attached to the window
(alpha=0, invisible) until first article use. makeUIView reparents
it to the article container via removeFromSuperview + addSubview.

Also: removed all debug logging (warmup, article open, WebView timing).

Confirmed by instrumentation:
- GPU process launches during warmup (1.3s, background)
- First article open: 22ms total, WebView finish: 3ms
- No user-visible freeze

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:48:04 -05:00
Yusuf Suleman
127da8feaa perf: force GPU process launch during warmup by attaching WKWebView to window
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
THEORY: WKWebView at frame .zero or detached from window skips GPU
compositor init. First real display triggers GPU process launch (~3s).
FIX: Create WKWebView at screen bounds, attach to key window (alpha=0)
during warmup. WebKit launches GPU process while user is on Home tab.
Remove from window after 2s (GPU process stays alive).

Also: ensureAttachedToWindow() fallback if init runs before window
exists. Called from ContentView.task where window is guaranteed.

Added 1x1 transparent GIF in warmup HTML to force image decoder init.

Kept all debug logging for verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:44:48 -05:00
Yusuf Suleman
62f9a2503a debug: timestamped logging for first-article-open stall investigation
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s
2026-04-04 00:35:53 -05:00
Yusuf Suleman
7938034d85 perf: lazy async image decoding + max-height to prevent scroll freeze
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 5s
ROOT CAUSE: Articles from sites like onemileatatime.com serve WebP
images with .jpeg extensions. WebKit attempts JPEG decode, fails,
retries as WebP — the retry loop blocks the compositor thread during
scroll, causing ~1s freezes per image.

FIX 1 — Image attributes (ArticleHTMLBuilder.optimizeImages):
Regex injects loading="lazy" decoding="async" on all <img> tags
that don't already have loading= set. This tells WebKit to:
- decode images off the main/compositor thread (decoding=async)
- only decode when approaching viewport (loading=lazy)

FIX 2 — CSS max-height:
img { max-height: 600px; object-fit: contain; }
Limits decoded image buffer size. A 1200x900 image still displays
at full width but WebKit doesn't need to composite oversized tiles.

Both fixes are content-rendering optimizations only — no changes
to WKWebView architecture, scrolling model, or layout system.

Also marked Atmos Rewards articles as unread for testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:30:37 -05:00
Yusuf Suleman
cb7907ef33 fix: scroll mark-as-read — three bugs found from round 3 logs
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
LOG EVIDENCE:
1. notVisible: wasVisible never set for entries already on screen
   at list load. Removed wasVisible guard — trackingActive (100pt
   scroll) is sufficient protection.

2. aboveVP: maxY never goes below 0. LazyVStack destroys views at
   ~maxY=0. Changed threshold from maxY<0 to maxY<30 (nearly off).

3. notDown flickering: per-entry deltas are ~1pt, causing direction
   to flip between down/not-down on every callback. Made direction
   sticky: scrollingDown stays true until 30pt of cumulative upward
   scroll is detected. Prevents jitter from sub-pixel noise.

Removed debug logging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 00:19:53 -05:00