fix: surface API errors, compress HEIF images, fix sessions vanished, fix slow load#173
Conversation
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
|
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
|
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
|
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
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net |
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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>
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net |
…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>
|
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>
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net |
1 similar comment
|
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>
|
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>
|
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>
…nction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net |
1 similar comment
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
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>
|
Azure Static Web Apps: Your stage site is ready! Visit it here: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.net |
|
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. |
Summary
Five fixes. Branch:
fix/surface-api-error-detail. Staging: https://white-island-090dfd003-173.westeurope.7.azurestaticapps.netWhat changed
1. Surface Anthropic error detail (
MuscleMap.jsx,History.jsx)Components read
data?.error?.message— alwaysundefinedbecausedata.erroris a string, not an object. Changed todata?.detail || data?.error?.message.2. HEIF photo exceeds 5 MB limit (
utils.js—compressImage)Three iterations were needed to crack this one:
Iteration 1 — Added
compressImage()(iterative quality reduction). Canvas usedimg.src = dataUrl(the ~9 MB base64 string from FileReader). iOS Safari silently zeroesnaturalWidth/naturalHeightfor 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 thequalityparameter incanvas.toDataURL, so all 7 steps produced the same ~5.25 MB output, and the|| quality <= 0.3fallback 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.
normalizeExNameconverts fully-uppercase strings to title case. Permanent safety net.4. "Siste økt" showing empty (
db.js—fetchLastSession, commit0915690)Two iterations here too:
Iteration 1 — Removed secondary
.order("created_at")sort (ambiguous withsession_exercises.created_at). Did not fix the issue.Actual root cause —
.limit(1).maybeSingle()silently breaks when the table has 2+ rows. PostgREST evaluates theAccept: application/vnd.pgrst.object+jsonsingle-row constraint before applyingLIMIT, 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; returndata?.[0] ?? null.5. Slow app load (
App.jsx)onAuthStateChangecalledensureGymMembership()+ensureDisplayName()on every Supabase auth event (INITIAL_SESSION, TOKEN_REFRESHED, etc.) — 3–4 redundant upserts per page load. Now only fires onSIGNED_IN; initialgetSession().then(...)path unchanged.Files changed
app/src/lib/utils.jscompressImage: blob URL + dimension-reduction fallbackapp/src/lib/db.jsfetchLastSession: drop.maybeSingle(), usedata?.[0] ?? null; removed ambiguous secondary sortapp/src/components/MuscleMap.jsxnormalizeExNameALL CAPS fixapp/src/components/History.jsxapp/src/App.jsxonAuthStateChangefires membership upserts onSIGNED_INonlyCHANGELOG.mdCLAUDE.md.maybeSingle()+ multi-row)Current status (as of latest push
0915690).maybeSingle()fix deployed in09156900915690; staging build pendingHow to verify
0915690)POST /user_gymson page loadKnown outstanding work (not in this PR)
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-urgenthttps://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ