Skip to content

fix: surface API errors, compress HEIF images, fix sessions vanished, fix slow load#173

Merged
ChristopherRotnes merged 30 commits into
masterfrom
fix/surface-api-error-detail
May 14, 2026
Merged

fix: surface API errors, compress HEIF images, fix sessions vanished, fix slow load#173
ChristopherRotnes merged 30 commits into
masterfrom
fix/surface-api-error-detail

Conversation

@ChristopherRotnes
Copy link
Copy Markdown
Owner

@ChristopherRotnes ChristopherRotnes commented May 14, 2026

Summary

Five fixes. Branch: fix/surface-api-error-detail. Staging: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net


What changed

1. Surface Anthropic error detail (MuscleMap.jsx, History.jsx)

Components read data?.error?.message — always undefined because data.error is a string, not an object. Changed to data?.detail || data?.error?.message.

2. HEIF photo exceeds 5 MB limit (utils.jscompressImage)

Three iterations were needed to crack this one:

Iteration 1 — Added compressImage() (iterative quality reduction). Canvas used img.src = dataUrl (the ~9 MB base64 string from FileReader). iOS Safari silently zeroes naturalWidth/naturalHeight for large data URLs → 0×0 canvas → blank "compressed" image → still sent the original 5.25 MB to Anthropic.

Iteration 2 — Switched to URL.createObjectURL(file) as image source. iOS decodes blob URLs correctly. Added 4500 px long-edge cap to avoid iOS GPU memory limits. Quality stepped 0.9→0.3.

Iteration 3 (latest, commit 0915690) — Quality stepping still failed: iOS Safari silently ignores the quality parameter in canvas.toDataURL, so all 7 steps produced the same ~5.25 MB output, and the || quality <= 0.3 fallback resolved the Promise with the still-oversize image. Root cause confirmed by identical byte count (5,246,896) across all iterations.

Current fix: Start at 2048 px long edge (not 4500 px — this alone keeps most iPhone photos under 5 MB even at iOS default quality). After quality steps exhausted, shrink dimensions by 30% and retry from quality 0.9: 2048 → 1434 → 1004 → 803 → ... with floor at 800 px. Dimension reduction is the only reliable size control on iOS.

3. ALL CAPS exercise names (MuscleMap.jsx)

Canvas quality reduction degrades the image enough for Claude to return names in ALL CAPS. normalizeExName converts fully-uppercase strings to title case. Permanent safety net.

4. "Siste økt" showing empty (db.jsfetchLastSession, commit 0915690)

Two iterations here too:

Iteration 1 — Removed secondary .order("created_at") sort (ambiguous with session_exercises.created_at). Did not fix the issue.

Actual root cause.limit(1).maybeSingle() silently breaks when the table has 2+ rows. PostgREST evaluates the Accept: application/vnd.pgrst.object+json single-row constraint before applying LIMIT, returns 406. Supabase JS v2 .maybeSingle() silently converts 406 → { data: null, error: null }. DB confirmed to return the session correctly via raw SQL simulation; the bug is entirely in the JS/PostgREST layer.

Fix: Replaced .limit(1).maybeSingle() with a plain array query; return data?.[0] ?? null.

5. Slow app load (App.jsx)

onAuthStateChange called ensureGymMembership() + ensureDisplayName() on every Supabase auth event (INITIAL_SESSION, TOKEN_REFRESHED, etc.) — 3–4 redundant upserts per page load. Now only fires on SIGNED_IN; initial getSession().then(...) path unchanged.


Files changed

File Change
app/src/lib/utils.js compressImage: blob URL + dimension-reduction fallback
app/src/lib/db.js fetchLastSession: drop .maybeSingle(), use data?.[0] ?? null; removed ambiguous secondary sort
app/src/components/MuscleMap.jsx Error detail surfacing; normalizeExName ALL CAPS fix
app/src/components/History.jsx Error detail surfacing
app/src/App.jsx onAuthStateChange fires membership upserts on SIGNED_IN only
CHANGELOG.md v1.2.9 entry
CLAUDE.md Two new known-pitfall entries (iOS quality param, .maybeSingle() + multi-row)

Current status (as of latest push 0915690)

Fix Status
Error detail surfacing ✅ Deployed to staging — not re-tested, low risk
ALL CAPS normalization ✅ Deployed, visual-only fix
Slow app load ✅ Deployed — not re-tested
"Siste økt" sessions missing Needs verification — root cause confirmed via DB SQL simulation; .maybeSingle() fix deployed in 0915690
HEIF 5 MB image Needs verification — dimension-reduction fallback deployed in 0915690; staging build pending

How to verify

  1. Open staging URL (wait for Azure bot to confirm build complete after 0915690)
  2. Sessions: Home → "Siste økt" should show the May 13 CROSSTRAINING session
  3. Image: Upload an iPhone HEIF photo → analysis should succeed (no 400)
  4. Slow load: DevTools Network → confirm single POST /user_gyms on page load

Known outstanding work (not in this PR)

  • Missing DB indexes on user_gyms(sporty_business_unit_id), session_templates(user_id), exercise_library(user_id), session_template_exercises(template_id) — these would improve RLS EXISTS subquery performance but are non-urgent
  • Per CLAUDE.md policy: issues must be developer-verified before closing on GitHub

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ

The proxy returns { error: '...', detail: '<Anthropic message>' } but
both MuscleMap and History were reading data?.error?.message — always
undefined because .error is a string. Changed to data?.detail so the
actual Anthropic error (e.g. "model_not_found", "invalid_request_error")
is shown directly in the error notification on mobile and desktop.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

iPhone 17 Pro (and all iPhones) shoot in HEIF by default. iOS Safari
converts HEIF to JPEG when reading via FileReader/canvas, but the
resulting JPEG inflates the 2.6 MB HEIF to ~5.2 MB — over Anthropic's
5 MB per-image limit, causing a 400 error.

New compressImage() in utils.js:
- Always outputs image/jpeg via canvas (converts HEIF, WebP, PNG, etc.)
- Iteratively lowers JPEG quality (0.85 → 0.3) then reduces dimensions
  (75% steps) until decoded byte size is under 5 MB
- Whiteboard photos typically compress to <2 MB with no visible loss

Replaces toBase64 + detectMediaType + the old file-size guard in both
MuscleMap and History. The size guard was checking file.size (the
compressed HEIF size) which is unrelated to the decoded JPEG size
Anthropic actually enforces.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@ChristopherRotnes ChristopherRotnes changed the title fix: surface Anthropic error detail in image analysis error messages fix: surface API error detail in UI + auto-compress images for Anthropic May 14, 2026
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

…only

The previous approach tried reducing JPEG quality iteratively on the
full 24MP frame. A 4284x5712 image at quality 0.85 still outputs ~6 MB
because quality alone can't compensate for that many pixels.

New strategy: scale to max 2048px on the longest side first, then
reduce JPEG quality only if still over 5 MB. 2048px is sufficient for
whiteboard OCR; a 1533x2048 JPEG at quality 0.85 is ~0.5-1.5 MB.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Gym whiteboards are typically written in ALL CAPS. Claude returns names
verbatim, so parsed exercises came back as e.g. "BENKPRESS MED MANUALER".

normalizeExName() converts to title case only when the entire string is
uppercase — mixed-case names (already correct) are left untouched.
Applies to both name and standardName fields on ANALYZE_SUCCESS.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

claude added 2 commits May 14, 2026 00:57
2048px was too aggressive — it degraded whiteboard text enough for
Claude to return exercise names in ALL CAPS where it previously didn't.
3000px (2253×3000 for the iPhone 17 Pro frame) preserves enough
resolution for accurate OCR while keeping JPEG well under 5 MB.

Reverts the ALL CAPS → title case normalization (was masking the
quality regression, not fixing the root cause).

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
The normalizeExName function was reverted but its two call sites in the
ANALYZE_SUCCESS dispatch were not cleaned up, causing no-undef lint
errors and a CI failure.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

…ce quality

Explicit dimension scaling (2048px, 3000px) was causing Claude to return
ALL CAPS exercise names — the downsampled image degraded whiteboard text
enough to change OCR behaviour.

New strategy:
1. FileReader reads the file — iOS converts HEIF→JPEG at full native
   resolution (4284×5712 for iPhone 17 Pro). If the result is ≤5 MB,
   use it directly with zero quality loss.
2. Only if over 5 MB: draw to canvas at natural dimensions and reduce
   JPEG quality (0.75 → 0.65 → ... → 0.3). No explicit width/height
   cap — iOS applies its own canvas limits internally at a much higher
   resolution than our previous 2048/3000px cap.

For the iPhone 17 Pro: HEIF→JPEG ≈5.2 MB → canvas at quality 0.75
≈3.5-4 MB → under limit, full resolution preserved for OCR.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Gym whiteboards are written in ALL CAPS by convention. When the canvas
fallback in compressImage reduces JPEG quality (trading resolution for
size), Claude tends to return exercise names verbatim in ALL CAPS.
normalizeExName converts fully-uppercase strings to title case so names
display consistently regardless of image resolution.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

A 4284×5712 (24.5MP) canvas needs ~98MB GPU backing store. iOS Safari
silently refuses to allocate it, so toDataURL returned the original
un-compressed data unchanged — causing Anthropic to still see the 5MB
image even after the FileReader path correctly detected it was over the
limit and entered the canvas fallback.

Capping at 4500px (3375×4500 = ~61MB) keeps the canvas within iOS limits.
OCR quality is preserved — 4500px is well above the 3000px that previously
caused ALL CAPS output. Quality reduction starts at 0.9 since we have
already reduced pixel count.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Setting img.src to a ~9 MB data URL string caused iOS Safari to silently
fail the Image decode (naturalWidth/naturalHeight = 0), producing a blank
0×0 canvas. toDataURL on a 0×0 canvas passes the size check and resolves
with garbage base64, leaving the original over-5MB data in state.

Using URL.createObjectURL(file) gives iOS a short blob:// reference to
the original file instead — iOS decodes it reliably via its native image
pipeline. Also retains the 4500px long-edge cap (15 MP canvas ≈ 61 MB)
to stay within iOS GPU memory limits.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

…ate auth upserts

fetchLastSession had a secondary .order("created_at") that PostgREST treated as
ambiguous because session_exercises (joined in the same query) also has a
created_at column — it returned 0 rows silently, causing Home to show
"Ingen økter logget ennå" despite sessions existing.

App.jsx called ensureGymMembership + ensureDisplayName on every Supabase auth
event (INITIAL_SESSION, TOKEN_REFRESHED, etc.) causing 3-4 redundant upserts
per page load. Now only fires on SIGNED_IN.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@ChristopherRotnes ChristopherRotnes changed the title fix: surface API error detail in UI + auto-compress images for Anthropic fix: surface API errors, compress HEIF images, fix sessions vanished, fix slow load May 14, 2026
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

…hLastSession using maybeSingle

compressImage: iOS Safari silently ignores canvas.toDataURL quality param, so
quality stepping 0.9→0.3 produced the same ~5.25 MB output every time, then
the || quality<=0.3 guard resolved with the oversize image. Added dimension
reduction (2048px start, 70% each round to 800px floor) as the reliable
fallback when quality is ineffective.

fetchLastSession: .limit(1).maybeSingle() breaks silently once the sessions
table has 2+ rows — PostgREST evaluates the single-row constraint before LIMIT
and returns 406; .maybeSingle() converts 406 to {data:null,error:null}.
Replaced with a plain array query + data?.[0] ?? null.

https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

…abel on Home

- compressImage: happy path was hardcoding mediaType 'image/jpeg' for all
  files; now reads actual type from FileReader dataUrl so PNG screenshots
  are not sent to Claude with the wrong MIME type
- Home: second chip on last-session card used history.exerciseCount for
  muscle count — now uses home.muscleCount (added to all three locales)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

ChristopherRotnes and others added 2 commits May 14, 2026 04:32
…recursive 0.7x factor

The previous algorithm started at 2048px and reduced by 0.7x each step. For photos
already smaller than 2048px (e.g. 1440x1080), no resize happened on the first pass,
and the 0.7x steps (2048→1434) produced almost no reduction from the original 1440px
width — both yielding the same 5,246,896 byte output since iOS ignores the quality
parameter entirely.

New approach: iterate over fixed targets [1600, 1200, 960, 768, 600]. A 1440px photo
skips 1600 (no resize), gets capped at 1200x900 on the next step, and produces a JPEG
well under 5 MB. Large iPhone photos (4032×3024) are capped at 1600px immediately.
Uses fixed quality 0.85; iOS ignores it anyway, so dimension is the only lever.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSession().then() and onAuthStateChange(SIGNED_IN) can both fire on the
same page load (observed on iOS and Chrome staging). The previous SIGNED_IN
guard still allowed a double-call when getSession() resolved first with an
existing session. A ref flag ensures the ensures run exactly once per mount
regardless of which path fires first or whether SIGNED_IN fires redundantly.
Also extends the guard to INITIAL_SESSION so returning users are covered even
if SIGNED_IN doesn't fire on their platform.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

1 similar comment
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

ChristopherRotnes and others added 2 commits May 14, 2026 05:50
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

1 similar comment
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

…imit

Anthropic enforces the limit on the base64 character count, not decoded
byte size. The previous check (b64.length * 0.75 <= 5 MB) allowed strings
up to ~6.67 M chars, causing a 3.75 MB decoded image (5.25 M base64 chars)
to pass client-side but fail at the API with 5246896 > 5242880 bytes.

Changed all checks in compressImage to b64.length <= MAX_B64_CHARS and
updated the canvas compression target accordingly. Also removed all
diagnostic alerts and logging added during investigation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net

@ChristopherRotnes ChristopherRotnes marked this pull request as ready for review May 14, 2026 04:38
@ChristopherRotnes ChristopherRotnes merged commit a562263 into master May 14, 2026
2 checks passed
@ChristopherRotnes ChristopherRotnes deleted the fix/surface-api-error-detail branch May 14, 2026 04:39
@ChristopherRotnes
Copy link
Copy Markdown
Owner Author

Verified on production. Fixed by comparing base64 string length directly against Anthropic's 5 MB limit () — the previous check used decoded bytes which allowed strings up to ~6.67 M chars through. Tested with original HEIF photo (5.25 M base64 chars) and a 7.2 MB PNG. Both pass. Merged in #173.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants