Commit Graph

73 Commits

Author SHA1 Message Date
Yusuf Suleman
1f32e5436e refine: scroll mark-as-read with visibility, dynamic threshold, stable deltas
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
1. Visibility requirement (new condition 5):
   - Tracks max visible ratio per entry via GeometryReader
   - Entry must have been >=50% visible at some point to qualify
   - Prevents marking entries that were never genuinely seen
   - Uses wasVisible set, populated by onChange(of: minY)

2. Dynamic activation threshold:
   - max(100pt, 20% of viewport height)
   - Taller screens (iPad) require proportionally more scroll
   - Measured via background GeometryReader on ScrollView

3. Stabilized scroll direction:
   - Ignores micro deltas <2pt (was 1pt)
   - Filters layout noise, rubber-banding, and momentum artifacts

Existing protections preserved:
   - trackingActive reset on onAppear (navigation protection)
   - downward-only marking
   - crossing detection (oldMaxY >= 0, newMaxY < 0)
   - markedByScroll dedup set

6 conditions must ALL be true to mark an entry:
   1. trackingActive (scrolled past threshold)
   2. isScrollingDown
   3. !entry.isRead
   4. !markedByScroll.contains(id)
   5. wasVisible.contains(id) — was >=50% visible
   6. bottom edge crossed above viewport

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 23:18:53 -05:00
Yusuf Suleman
532a071715 feat: scroll-based mark-as-read with geometry tracking + navigation protection
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
Uses GeometryReader + coordinate space to track entry positions —
NOT onAppear/onDisappear.

How it works:
1. ScrollView has a named coordinate space ("readerScroll")
2. Invisible anchor at top measures scroll offset via PreferenceKey
3. Each entry row has a background GeometryReader that tracks its
   frame in the scroll coordinate space
4. onChange(of: maxY) detects when an entry's bottom edge crosses
   above the viewport top (oldMaxY >= 0 → newMaxY < 0)
5. Entry is marked read only when ALL conditions are true:
   - trackingActive (user scrolled down >100pt)
   - isScrollingDown (current direction is down)
   - entry is unread
   - entry hasn't been marked by scroll already
   - entry's bottom edge just crossed above viewport

Navigation protection:
- onAppear resets trackingActive = false and cumulativeDown = 0
- When returning from an article, tracking is suspended
- User must scroll down 100pt before tracking reactivates
- This prevents all visible entries from being marked read on
  navigation back (they were already below viewport, not crossing)
- Scrolling up never marks anything (isScrollingDown = false)

State updates are local-first (immediate) with background API sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 23:04:32 -05:00
Yusuf Suleman
343abb0a80 perf: warm WebKit rendering pipeline with realistic HTML template
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
Empty HTML warmup only spun up the WebContent process. The first
real article still paid ~3s for CSS parsing, font measurement,
layout engine init, and compositing pipeline startup.

Now ArticleRenderer.init loads the full ArticleHTMLBuilder template
with sample content covering all styled elements (paragraphs,
headings, blockquotes, code blocks). WebKit performs real rendering
during app launch while the user is on Home/Fitness. By the time
they open the first article, CSS is parsed, fonts are measured,
and the pipeline is warm — the first article renders as fast as
subsequent ones.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:54:36 -05:00
Yusuf Suleman
c0078adeb7 fix: synchronous optimistic read-state on article open
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 4s
The local entries mutation now runs synchronously at the top of
.task, before any Task {} or await:

  1. vm.entries[idx].status = "read"  ← synchronous, immediate
  2. currentEntry syncs from mutated array  ← immediate
  3. Task { api.markEntries + getCounters }  ← background
  4. await getEntry  ← content fetch, merge preserves local status

Previously markAsRead was wrapped in Task {} (fire-and-forget),
which SCHEDULED the mutation but didn't execute it until the main
actor yielded. Line 2 read stale state because the mutation hadn't
run yet.

Now the @Observable array mutation happens before any async work,
so the ForEach row re-renders on the same run loop — the list
shows "read" even during the push animation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:13:56 -05:00
Yusuf Suleman
415b125fb7 refactor: simplify article toolbar to Save to Brain only + tappable title
All checks were successful
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 5s
Security Checks / dockerfile-lint (push) Successful in 4s
Toolbar:
- Removed star, read/unread, and ellipsis menu
- Single "Save to Brain" button (brain icon, turns green when saved)

Open original article:
- Title in HTML header is now a tappable link (when URL exists)
- Subtle ↗ icon after title indicates external link
- Tap opens in Safari via existing WKNavigationDelegate link handler
- No accidental triggers: styled as text link, not a button
- Active state dims to 0.6 opacity for tap feedback
- Dark mode: title link inherits text color (not accent)

No floating buttons added. No architecture changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 22:01:16 -05:00
Yusuf Suleman
5e13f92a00 fix: remove WKProcessPool — iOS 15+ shares process automatically
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
API deprecated since iOS 15. All WKWebViews already share one
WebContent process. Our singleton WKWebView + shared config is
sufficient.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:50:00 -05:00
Yusuf Suleman
670e2b2bac polish: scope content upgrade to #article-body container only
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
- HTML template now wraps article content in <div id="article-body">
- Content upgrade JS targets only #article-body.innerHTML, leaving
  header, CSS, and outer document structure untouched
- Returns 'ok'/'no-container' status for reliable fallback detection
- extractArticleBody() parses the #article-body content from HTML
- escapeForJS() separated into its own method
- Full reload fallback if container not found or JS fails

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:47:17 -05:00
Yusuf Suleman
f10c356199 polish: explicit WKProcessPool, scroll-preserving content upgrade, no reload flash
All checks were successful
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / dockerfile-lint (push) Successful in 4s
1. Explicit WKProcessPool — static shared instance assigned to
   WKWebViewConfiguration. Prevents any future divergence even
   though iOS 15+ shares by default.

2. Scroll-preserving content upgrade — when articleContent updates
   (partial → full), uses JavaScript DOM replacement instead of
   loadHTMLString. Captures window.scrollY before swap, restores
   after. No visible flash or scroll jump. Falls back to full
   reload if JS replacement fails.

3. No unnecessary reloads — coordinator tracks lastHTML. Only
   loads if content actually changed. First article open = full
   page load (lastHTML is nil). Content upgrade = DOM swap
   (lastHTML exists, new content is different).

4. Clean separation — isUpgrade flag distinguishes first load
   from content upgrade. First load uses loadHTMLString (needs
   full <html> document). Upgrade uses innerHTML replacement
   (preserves scroll, CSS, page state).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:42:59 -05:00
Yusuf Suleman
8ae1d48d68 perf: instant article open + non-blocking mark-read + static CSS
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
Changes:
1. Article opens INSTANTLY — articleContent initialized from entry's
   existing content in init(), not after API fetch. WebView renders
   immediately with whatever we have. Full content swaps in silently
   when getEntry returns (only if longer than current).

2. markAsRead is fire-and-forget — wrapped in detached Task inside
   .task, does not block the content display chain. Toolbar syncs
   from vm.entries immediately after.

3. CSS template pre-built as static string in ArticleHTMLBuilder.
   Avoids rebuilding ~2KB of CSS on every article open. HTML builder
   is a stateless enum with a single static method.

4. Removed isContentReady flag — no longer needed since content is
   available from init. Spinner only shows if entry truly has no
   content at all (rare edge case).

Flow is now:
  tap → ArticleView created with entry.articleHTML →
  WebView loads immediately → user can scroll →
  background: markAsRead fires, getEntry fetches full content →
  if full content is better, WebView updates silently

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:31:54 -05:00
Yusuf Suleman
18dd5aa44d fix: mark-read on first open + eliminate long-article 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 3s
## Bug 1: First open didn't mark as read

ROOT CAUSE: Race condition. markAsRead set local status="read",
then getEntry returned the server's status="unread" (API sync
hadn't completed yet) and overwrote the local mutation.

FIX:
- markAsRead runs FIRST, before getEntry (was concurrent before)
- After getEntry, merge server response but PRESERVE local
  status/starred (which may differ from server due to race)
- currentEntry syncs from vm.entries after markAsRead, ensuring
  the toolbar reflects the correct state

## Bug 2: Long articles freeze before scrollable

ROOT CAUSE: WKWebView.scrollView.isScrollEnabled = false, embedded
inside SwiftUI ScrollView with .frame(height: webViewHeight).
For a 15000px article, WebKit had to render the entire document,
JavaScript measured document.body.scrollHeight, SwiftUI relaid out
the 15000px frame — all blocking before scroll became responsive.

FIX:
- WKWebView now handles its own scrolling (isScrollEnabled = true)
- Removed SwiftUI ScrollView wrapper around article
- Removed contentHeight binding and height measurement JavaScript
- Removed the Coordinator's didFinish height evaluation
- Article header (title, feed, time) moved into the HTML document
  so it scrolls naturally with the content
- WKWebView fills available space, scrolls natively via WebKit's
  compositor thread — immediate scroll response

Both fixes preserve the shared WKWebView architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:28:24 -05:00
Yusuf Suleman
05bc5f8047 fix: remove deprecated WKProcessPool — iOS 15+ shares automatically
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:20:13 -05:00
Yusuf Suleman
49c9b7871c fix: Reader architecture overhaul — persistent WKWebView, stable layout, local-first 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
Root-cause investigation identified 5 architectural issues. This commit
fixes all of them with structural changes, not patches.

## 1. Persistent ArticleRenderer (fixes first-article freeze)

BEFORE: Every article tap created a new WKWebView with a new
WKWebViewConfiguration and a new WKProcessPool. Each spawned a
WebContent process (~3s). The "warmer" used a different config,
warming a different process — useless.

AFTER: Single ArticleRenderer singleton owns one WKWebView with
one shared WKProcessPool + WKWebViewConfiguration. Created at app
launch via `_ = ArticleRenderer.shared` in ContentView.task.
ArticleWebView wraps the shared WKWebView in a container UIView.
SwiftUI owns the container lifecycle, not the WKWebView's.
Zero process launches after first warm-up.

## 2. Stable Reader layout (fixes tab jitter)

BEFORE: Sub-tabs and feed chips were conditionally rendered
(`if !vm.isLoading || !vm.entries.isEmpty`). When loading finished,
~80px of UI appeared suddenly, causing layout shift that rippled
to the tab bar.

AFTER: Sub-tabs and feed chip bar ALWAYS render. Feed chip bar has
fixed height (44px). No conditional wrappers in the layout hierarchy.
Content area shows LoadingView during fetch. Chrome never changes shape.

## 3. Local-first state updates (fixes mark-read lag)

BEFORE: markAsRead made 3 sequential API calls (mark, re-fetch entry,
re-fetch counters). toggleRead and toggleStar did the same. Each
action had 3 network round-trips before UI updated.

AFTER: Mutate local entries array immediately (status/starred are
now var). API sync happens in background via Task.detached. UI updates
instantly. Counter refresh happens async.

## 4. Atomic list replacement (fixes empty flash)

BEFORE: loadEntries(reset:true) set `entries = []` then
`entries = newList`. Two mutations = empty state flash + full
LazyVStack teardown/rebuild.

AFTER: Never clear entries. Fetch completes, then single atomic
`entries = newList`. SwiftUI diffs by Identifiable.id — only
changed rows update.

## 5. Reserved thumbnail space (fixes card layout jump)

BEFORE: AsyncImage default case was EmptyView() (0px). When image
loaded, 180px appeared. Cards jumped.

AFTER: Default case renders a placeholder Rectangle at 180px.
Card height is stable from first render.

## Additional: Pre-load moved off TabView

`.task { await readerVM.loadInitial() }` moved from TabView
(caused observable mutations during TabView body evaluation,
contributing to tab bar jitter) to the outer ZStack.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:17:41 -05:00
Yusuf Suleman
fc58791e5e fix: remove broken mark-as-read on scroll entirely
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
onDisappear fires when scrolling in BOTH directions and when
navigating, making it impossible to reliably detect scroll direction.
Reverted to simple behavior: articles only mark as read when you
tap into them (handled in ArticleView). Will revisit mark-on-scroll
with a proper ScrollViewReader approach later.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:06:17 -05:00
Yusuf Suleman
74e26ec36f fix: Reader stuck on loading — guard checked isLoading which was true
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
loadInitial() had guard !isLoading but isLoading defaults to true,
so it returned immediately without loading. Replaced with hasLoadedOnce
flag to prevent double-loading without blocking the first call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:00:34 -05:00
Yusuf Suleman
11fd59e88f feat: editable AI draft card — edit food/macros before adding (#16)
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
- FitnessDraft properties changed from let to var (mutable)
- New EditableDraftCard with Edit/Done toggle:
  - Editable food name
  - Editable macros (calories, protein, carbs, fat, sugar, fiber)
  - Meal type picker (dropdown menu)
  - Editable quantity
- Edited values flow through draftToDict → apply endpoint
- No backend changes needed — purely iOS UI
- Default view is read-only (same as before), tap Edit to modify

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:53:25 -05:00
Yusuf Suleman
a0d3f24614 fix: Reader pre-loads on app launch, no more glitchy initial 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
- ReaderViewModel starts with isLoading=true (shows spinner, not "No articles")
- MainTabView owns ReaderViewModel and pre-fetches in background on launch
- Sub-tabs and feed chips hidden during initial load (no tiny squished layout)
- VStack fills full screen with frame(maxWidth/maxHeight: .infinity)
- WebKit warmer triggers when Reader tab appears
- By the time user taps Reader, data is already loaded

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:41:13 -05:00
Yusuf Suleman
0b74493db0 feat: multi-photo support in feedback (up to 5 screenshots)
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
iOS:
- PhotosPicker with maxSelectionCount: 5
- Horizontal scroll preview strip with individual remove buttons
- Sends "images" array instead of single "image"

Server:
- Gateway accepts both "image" (single, backwards compat) and
  "images" (array) fields
- Uploads each as separate Gitea issue attachment

Also closed Gitea issues #11, #12, #13, #14.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:34:25 -05:00
Yusuf Suleman
f17279d5b8 fix: mark-as-read only on scroll (not navigation), Goals keyboard dismiss
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 3s
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Reader mark-as-read:
- Track visible entry IDs with onAppear/onDisappear
- Only mark as read when entry disappears AND other entries are still
  visible (meaning user is scrolling, not navigating to an article)
- Prevents the bug where opening an article marked all visible entries

Goals (#11):
- Added .scrollDismissesKeyboard(.interactively) for drag-to-dismiss
- Added tap-to-dismiss keyboard on background

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:28:19 -05:00
Yusuf Suleman
028e308588 feat: in-app dark mode toggle (System / Light / Dark)
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
- AppearanceManager with UserDefaults persistence
- Three modes: System (follows iOS), Light, Dark
- Toggle in Home screen profile menu under "Appearance"
- Applied via .preferredColorScheme at app root
- Persists across app launches

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:22:35 -05:00
Yusuf Suleman
da44ee8b73 fix: mark as read when entry scrolls OFF screen, not on appear
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
Uses .onDisappear instead of .onAppear — entries are marked as read
only when they scroll past the top of the viewport, not when the
list first renders. Same behavior as proper RSS readers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:19:06 -05:00
Yusuf Suleman
8cc58c23a0 fix: remove mark-as-read on scroll — was marking everything read on load
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
.onAppear fires for all visible rows when the list renders, marking
everything as read immediately. Removed — entries only mark as read
when you tap into the article (handled in ArticleView).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:17:33 -05:00
Yusuf Suleman
917a2c4621 feat: dark mode, mark-as-read on scroll, fix card tap 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
Dark mode:
- All colors in Color+Extensions now adaptive (UIColor with traits)
- Warm dark palette: dark brown canvas, brighter gold accent, warm cards
- Article HTML CSS supports prefers-color-scheme: dark
- Meal/macro colors unchanged (vivid on both themes)

Reader fixes:
- .contentShape(Rectangle()) on cards/rows — fixes tap target issues
  where small cards couldn't be clicked
- Context menu moved from card/row to the NavigationLink wrapper
  so it doesn't interfere with taps
- Mark as read on scroll via .onAppear on each entry
- Cards no longer pass vm (cleaner, context menu handled at list level)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:15:26 -05:00
Yusuf Suleman
678a71def7 fix: pre-warm WebKit engine when Reader tab loads
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 3s
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
First WKWebView creation in a session is slow (~2-3s) because iOS
lazily initializes the WebKit rendering engine. WebKitWarmer creates
a hidden 1x1 WKWebView with empty HTML on Reader tab load, forcing
the engine to initialize. Subsequent article opens are instant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:57:52 -05:00
Yusuf Suleman
3f16ca44be fix: wrap tabBarMinimizeBehavior in availability check for iOS 17 compat
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:50:57 -05:00
Yusuf Suleman
cfb09591cb feat: tab bar auto-hides on scroll (iOS 26 tabBarMinimizeBehavior)
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:46:36 -05:00
Yusuf Suleman
8b0987bcac perf: slim entries API + on-demand article loading for iOS 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
Server: add slim query param (default true) to entries list endpoint,
returning EntrySlimOut without content/full_content HTML — cuts payload
size dramatically for list views. Single entry endpoint still returns
full content.

iOS: ArticleView now fetches full entry content on demand when opened
instead of relying on list data. Shows loading indicator while fetching.
Mark-as-read is fire-and-forget to avoid blocking the view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:43:38 -05:00
Yusuf Suleman
a3eabf3e3b feat: thumbnail extraction for Reader — fixes all clients
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 3s
Server-side (dashboard + iOS + any client):
- Added thumbnail column to reader_entries
- Worker extracts from media:thumbnail, media:content, enclosures, HTML img
- API returns thumbnail in EntryOut with &amp; decoding
- Backfilled 260 existing entries

iOS:
- Prefers API thumbnail, falls back to client-side extraction
- Decodes HTML entities in URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:32:47 -05:00
Yusuf Suleman
798ba17a93 fix: resolve Reader freezing, article layout loop, and feedback issues
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
- ArticleWebView: remove dangerous intrinsicContentSize override, use
  height binding measured via JS after render
- ReaderViewModel: add @MainActor, replace didSet filter with explicit
  applyFilter() to avoid property observer reentrancy
- Thumbnail extraction: use precompiled NSRegularExpression, limit search
  to first 2000 chars, skip placeholder when no image found
- Card view: only show thumbnail when image exists (no placeholder)
- Feedback: add guard against double-tap, @MainActor on Task

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:16:57 -05:00
Yusuf Suleman
a496c3520b fix: re-add Reader files to Xcode project (lost in merge conflict)
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 19:09:04 -05:00
9c1a661099 resolve pbxproj merge conflict
All checks were successful
Security Checks / dependency-audit (push) Successful in 35s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
2026-04-03 19:06:44 -05:00
Yusuf Suleman
4d4e96c327 feat: card/list view toggle for Reader with thumbnails
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
- Card view: large thumbnail from article content, feed name, title, author, reading time
- List view: compact rows with mini thumbnail on right
- Toggle button in toolbar (grid/list icon)
- Thumbnail extracted from first <img> in article HTML
- Card view is default, warm Atelier styling preserved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 18:24:33 -05:00
Yusuf Suleman
426adb3442 feat: iOS Reader tab — full RSS reader with article reading pane
All checks were successful
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 3s
Security Checks / dependency-audit (push) Successful in 13s
New third tab in the iOS app with:
- ReaderModels matching Reader API response shapes
- ReaderAPI with all endpoints (entries, feeds, categories, counters)
- ReaderViewModel with filters (Unread/Starred/All), pagination, feed management
- ReaderTabView with sub-tabs and feed filter chips
- EntryListView with infinite scroll, context menus, read/unread state
- ArticleView with WKWebView HTML rendering, star/read toggles, Save to Brain
- ArticleWebView (UIViewRepresentable WKWebView wrapper)
- FeedManagementSheet with add/delete/refresh feeds, categories
- Warm Atelier design consistent with existing app

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:45:29 -05:00
Yusuf Suleman
8892124b8e revert: restore warm Atelier design, remove Liquid Glass changes
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
Reverts all Liquid Glass styling back to the original warm beige/brown
Atelier design system. Deployment target back to iOS 17.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:35:39 -05:00
Yusuf Suleman
bbc01cb42b fix: add opaque background to entry rows so delete button doesn't bleed through
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-03 16:33:31 -05:00
Yusuf Suleman
fcbbee2c8d fix: replace deprecated Text concatenation with HStack for iOS 26
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-03 16:31:47 -05:00
Yusuf Suleman
0309e6faba fix: resolve type mismatch between thinMaterial and clear in ternary
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:30:40 -05:00
Yusuf Suleman
05a8257bd1 chore: bump iOS deployment target to 26.0 for Liquid Glass APIs
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:28:58 -05:00
Yusuf Suleman
7569d1b505 style: adopt Liquid Glass design for iOS app
All checks were successful
Security Checks / dependency-audit (push) Successful in 15s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s
Migrate entire iOS app to Apple's Liquid Glass design language:
- System-adaptive colors (canvas, surfaces, text) for dark mode support
- Translucent material backgrounds (.thinMaterial, .ultraThinMaterial)
- Glass button styles (.glass, .glassProminent) on primary actions
- Liquid Glass navigation bar on Fitness tab
- Tab bar minimizes on scroll (.tabBarMinimizeBehavior)
- Increased corner radii (20pt) matching iOS 26 conventions
- Removed manual shadows (glass materials handle separation)

No business logic changes — purely cosmetic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 16:26:53 -05:00
Yusuf Suleman
c47db95938 add: app icon PNG (force-add past gitignore)
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 15:50:31 -05:00
Yusuf Suleman
96fa49fae2 feat: feedback with photo support + web dashboard feedback button
All checks were successful
Security Checks / dependency-audit (push) Successful in 15s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 5s
iOS:
- Photo picker in feedback sheet (screenshot/photo attachment)
- Image sent as base64, uploaded to Gitea issue as attachment

Web:
- Feedback button in sidebar rail
- Modal with text area + send
- Auto-labels same as iOS

Gateway:
- Multipart image upload to Gitea issue assets API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:31:20 -05:00
Yusuf Suleman
032785bdd8 fix: duplicate group ID F10020 — Feedback now F10021
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
F10020 was used for both Products and Feedback groups.
Xcode resolved it as Products, so FeedbackView path was wrong.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 13:01:08 -05:00
Yusuf Suleman
0bdcec7eca fix: FeedbackView rawPost API call signature
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:55:46 -05:00
Yusuf Suleman
687d6c5f12 feat: instant feedback button — creates Gitea issues with auto-labels
All checks were successful
Security Checks / dockerfile-lint (push) Successful in 4s
Security Checks / dependency-audit (push) Successful in 14s
Security Checks / secret-scanning (push) Successful in 3s
iOS:
- Subtle floating feedback button (bottom-left, speech bubble icon)
- Quick sheet: type → send → auto-creates Gitea issue
- Shows checkmark on success, auto-dismisses
- No friction — tap, type, done

Gateway:
- POST /api/feedback endpoint
- Auto-labels: bug/feature/enhancement + ios/web + fitness/brain/reader/podcasts
- Keyword detection for label assignment
- Creates issue via Gitea API with user name and source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:44:10 -05:00
Yusuf Suleman
60b28097ad fix: getGoalsForDate method name, non-optional goal
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:27:53 -05:00
Yusuf Suleman
01a15ae13e feat: app icon — beach family photo cropped to 1024x1024
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:22:12 -05:00
Yusuf Suleman
48bfdd96a2 fix: goals save API (void response), user greeting, goal field labels
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
- Goals PUT returns partial JSON, use putVoid + reload
- Home shows 'Hi, Yusuf' instead of 'Home'
- EmptyResponse type for void-like endpoints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:12:52 -05:00
Yusuf Suleman
401af3cb6d fix: replace deprecated NavigationLink(isActive:) with navigationDestination(isPresented:)
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:51:01 -05:00
Yusuf Suleman
85e0d07224 feat: sugar/fiber macros, editable goals, keyboard dismiss, entry editing
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
1. Sugar + Fiber bars in TodayView macro summary card
2. Goals page: editable text fields + Save button (PUT /api/fitness/goals)
3. AI Chat: keyboard dismisses on send + tap chat area to dismiss
4. Entry detail: edit quantity (stepper) + meal type picker + Save

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:49:07 -05:00
Yusuf Suleman
7f549cd6a0 fix: remove page swipe TabView — use tap-only tabs for swipe-to-delete
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:37:37 -05:00
Yusuf Suleman
2c5311264b fix: swipe vs tap — use highPriorityGesture + hidden NavigationLink
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
- DragGesture with minimumDistance:15 as highPriorityGesture
- Tap only navigates when not swiping
- Tapping while swiped closes the delete button
- Hidden NavigationLink for programmatic navigation
- Reverted FitnessTabView back to page-style TabView

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