A complete record of every bug found and fixed during the March 2026 audit. Organised by file. All fixes are included in the codebase.
- Double-rotation bug β
toPieceSpacere-appliedp.rotationon cached pieces where rotation was already baked in. Shadows and outlines were misaligned on all rotated pieces. Fixed with separatetoPieceSpaceCachedvstoPieceSpaceLivepaths.
- Wrong delta used for all pieces in a group β
computeMergedGroupBoardSnapResultused the reference delta from the first piece for every piece in the group. Pieces far from the reference snapped to the wrong position. Fixed to compute a per-piece delta.
- Performance timeline leak β 8
performance.mark/measurecalls fired on everypointerUp, accumulating thousands of entries over a session. Removed from production path. - Touch drag not counted β
dragCountwas not incremented for touch drags, only mouse drags. Fixed.
- Lock validity epsilon too tight β Epsilon was 4px. Responsive layout changes between sessions (different screen size or orientation) could invalidate legitimate saves. Raised to 8px.
- Saved game not found after deploy β
hasSavedGameused exact image URL matching. Vite content-hash suffixes change on every deploy, orphaning all in-progress saves. Fixed withimageIdentity()hash-stripping for stable matching.
- Locked pieces not clamped after unlock β
setPieceLockingEnabled(false)left previously-locked pieces outside the board interior. AddedclampAllBoardGroupsInsideBoardInterior()after unlock.
- Fog overlay bled into transparent padding β Fog used
fillRectover the full bounding box including piece tab padding. Fixed to clip toshapePathbefore filling.
- Hit slop made all pieces rectangular on mobile β
hitSlopPxfallback fired on anyisPointInPathmiss, treating every piece as a rectangle. Fixed to only accept slop when the pointer is near an actual edge.
- Bezier control points overshooting crown apex β
buildHorizontalConnectorandbuildVerticalConnectorproduced diamond shapes instead of round knobs. Fixed by lerping control points at 60% towardcrownY.
- Stale completion ref blocked re-init β
stateRef?.current?.isCompleteguard prevented the manager from re-initialising after completing a puzzle. Fixed to checkmanagerResult.getState()?.isCompletelive. - Same-route navigation didn't re-init β Navigating
/play β /playwith a new puzzle didn't trigger re-init because the pathname didn't change. AddeduseLocation()+locationPuzzleKeydep.
completionImageUrlintoDataURLeffect deps β Including it caused infinite loop potential on completion. Removed from deps.
- Save fired every elapsed second β Save effect had
elapsedSecondsin its deps, writing to localStorage 60 times a minute during active play. AddedlastSavedStateRefto skip saves when state is unchanged.
- Dead code never imported β Marked
@deprecated DEAD CODEwith JSDoc listing known bugs inside it so it's never accidentally wired up.
recordCompletionandrecordAdaptiveCompletionname collision β BothstatsServiceandadaptiveDifficultyServiceexportedrecordCompletion. Import sites used aliases but any future file importing both without aliasing would silently call the wrong one. Renamed adaptive service export torecordAdaptiveCompletion.
- Duplicate canvas snapshot effect β
toDataURLandexit_before_completionPostHog capture were both duplicated fromusePlayScreenLifecycleEffects. Removed duplicates β both effects were running twice on completion.
GRID_ONCE_KEYlost in StrictMode β The "use grid size once" key was read inuseMemothen deleted inuseEffect. In StrictMode double-invoke, the first mount's effect deleted the key before the second mount'suseMemocould read it. Fixed by capturing the value in auseStateinitializer (runs once per lifetime, before any effects).
- 22 separate
useEffecthooks β One per UI setting. React checked 22 dep arrays on every render. Collapsed into a single batched effect with all 24 deps.
- Dead branches in
getPieceLockingInitialβexplicit === "false"andexplicit === "0"are conditions that can never be written by the app. Annotated as dead code.
- Performance timeline leak in debug mode β
performance.mark("render-frame-end")andperformance.measure()ran every animation frame without cleanup, accumulating 60 entries/second. AddedclearMarks/clearMeasuresafter each measure.
- 100 array iterations per frame β
shouldPublishStatecalledst.pieces.filter((p) => p.isPlaced).lengthevery frame.st.placedCountalready tracks this. Switched to read the pre-computed value β eliminates ~360k redundant iterations per minute on a 100-piece puzzle.
- Pointer handlers rebuilt on every pinch frame β
useMemohadviewportin its deps.viewportis a state object that changes on every pan/pinch frame β rebuilding all handlers during active touch interaction caused dropped events on mobile. Fixed withctxRefpattern;viewportremoved from deps.
screenToBoardrebuilt on every piece move βuseCallbackdeps includedstate?.pieces, a new array reference on every drag update. This rebuilt pointer handler closures mid-drag, risking dropped events. Moved all layout-derived values intoscreenToBoardInputsRef, givingscreenToBoarda stable identity with[]deps.
isPanningRefandisPinchingRefwere module-level globals β Shared across alluseViewportinstances. StrictMode double-invoke or concurrent route transitions could corrupt the panning/pinching state. Moved inside the hook asuseRef().
- Content drifted off-screen when zoomed out β
clampPanallowed content to slide to the top-left corner when the puzzle was smaller than the container. Fixed to centre content whenscaledW <= containerW.
- 60 localStorage writes per second during pinch β
saveViewportfired on every viewport state change with no debounce. Added 300ms debounce per puzzle key.
recordDailyCompletionfired every elapsed second βelapsedSecondswas in the effect deps; on a completed daily puzzle the timer still ticks, triggering a localStorage write every second. AddeddailyRecordedRefguard β fires exactly once per mount.handleShareResultCardwas a duplicate ofhandleShareCardβ Both calledshareCard()with identical arguments andmode: "result". Removed the duplicate, replaced with an alias.
- Completion image priority was backwards β
imageUrlwasimageRefUrl ?? fallbackImageUrl ?? completionImageUrl. The canvastoDataURLsnapshot (the actual solved board) was last priority. Fixed tocompletionImageUrl ?? imageRefUrl ?? fallbackImageUrl.
- Supabase session created on every completion β The hook auto-created a Supabase session on completion just to build a share URL, even when the user never tapped Share. The URL fallback already handles this without a server call. Removed auto-create; added
createShareSession()called only when the user explicitly shares.
- Copied feedback timer race β
handleCopyResultsandhandleCopyChallengeeach set independentsetTimeout(, 2000)timers. Rapid tapping could have the first timer clear feedback from the second copy. AddedcopiedTimerRefthat cancels the previous timer before setting a new one.
- Side effects inside a state updater β The RAF tick called
manager.restoreFromSaved()andsetState()inside asetReplayIndex()functional updater. React functional updaters must be pure β calling another state setter inside one is undefined behaviour that re-executes in StrictMode. Refactored toreplayIndexReffor imperative tracking; side effects moved outside the updater. - Ghost frame after replay end β
cancelAnimationFramewas called on a potentially stale RAF ID before the next frame's ID was assigned. Fixed: cancel before scheduling, andreturnimmediately after. clearSnapshotscrashed during active replay β ClearingsnapshotsRef.currentwhile the RAF was running caused the next tick to callrestoreFromSaved(list[-1])=undefinedβ crash. Fixed to cancel the RAF and setisReplaying=falsebefore clearing.
- Duplicate completion rows β
recordCompletionunconditionally inserted a new row on every call. StrictMode double-invoke or replay-triggered re-renders produced duplicate records. Added a 60-second dedup window check before inserting. - Mastery streak destroyed on replay β If a user solved the daily with mastery then replayed without mastery, the
elsebranch resetmastery_streakto 0 β destroying a legitimate streak. Fixed: only break the streak if there is no mastery solve for today at all.
- N+1 Supabase inserts β One
INSERTper newly-unlocked achievement. On a first completion where many achievements unlock, up to 22 sequential round-trips. Changed to collect all inserts and fire a single batchedINSERT.
- Function name collision with
statsServiceβ Both exportedrecordCompletion. Renamed torecordAdaptiveCompletionwith deprecated alias for backwards compatibility.
- Duplicate entries per user β
getDailyLeaderboarddidn't deduplicate by user. A player who replayed could appear multiple times in the top 10. AddedbestByUserMap dedup matching the pattern already used ingetLeastMovesandgetCleanest.
- Unbounded
getPercentileRankquery β Fetched every completion row for a grid size with no date filter or limit. On a popular size this could return tens of thousands of rows. Added 90-day cutoff andlimit(5000).
- Local timezone in date range β
getDateRange()usednew Date()with local timezone arithmetic. A UTC+12 user at 11pm could get tomorrow's date as the range end. Fixed to useDate.now()and UTC ISO strings. getMonthlyTotalsLeaderboardnot cached β Every other leaderboard fetcher wraps withwithLeaderboardCache(). This one was missed, firing a fresh DB query on every render. Added the cache wrapper.getWeeklyTotalsLeaderboardhad no row limit β Query had noLIMIT, potentially fetching thousands of rows in a busy week. Added.limit(50000).
ensureSignedIncalled on every Supabase operation β Each service call (stats, achievements, leaderboard, profile) calledsupabase.auth.getSession()independently β up to 5+ sequential auth round-trips on puzzle completion. Added module-levelcachedUserIdβ returns immediately after first sign-in.
- SELECT before upsert β
updateMyProfiledid aSELECTto decide betweenINSERTandUPDATE. Two round-trips per call. Replaced with a single.upsert({ onConflict: "user_id" }).
useLayoutEffectran on every render β Missing[]dependency array. The RAF was scheduled and immediately cancelled on every re-render of the parent component. Added[].
enabledvalue was stale β Read once at render time with no reactivity. Toggling haptics from a keyboard shortcut wouldn't update toggle UI until something else caused a re-render. Changed touseState.
- Inconsistent CPU threshold β Used
<= 4cores whileusePlayScreenSceneStateused<= 6. A 5β6 core device triggered battery saver in scene state but not here. Aligned to<= 6.
- "Nice!" threshold too low β Fired at 2 combos within 2.5s, which happened constantly during normal play, making it meaningless. Raised to 3 / "Nice!", 5 / "Combo!", 8 / "Unstoppable!".
PlayScreennever fully remounted on same-route navigation β/play β /playwith a new puzzle didn't unmount the component tree. Only the manager re-initted; timer state, completion state, and other hooks kept stale values. ExtractedAppRoutescomponent withuseLocation(), addedkey={playKey}toErrorBoundarywrappingPlayScreen.
navigate("/play")didn't force remount β Same same-route bug asChoosePuzzleModalandPackDetailScreen. Fixed tonavigate("/play", { replace: true, state: { puzzleKey: Date.now() } }).- Streak not shown before starting β Users had no idea what their current streak was when opening the daily modal. Added streak badge.
- Streak value was stale β
useState(() => getCurrentStreak())ran once at mount. If the user completed the daily in another tab or the date rolled over while backgrounded, the streak shown was stale. Addedfocusandvisibilitychangelisteners to refresh. - Social proof threshold too high β "X players solved today" only appeared when X β₯ 10. Lowered to 3.
- Tap-to-pan could push pieces off-screen β
handlePointerDowncomputed new pan values without clamping. Tapping the minimap edge could viewport-pan all pieces out of view. AddedclampPan(). - Canvas redrawn on every drag frame β
drawdepended onpiecesdirectly.piecesis a new array reference on every piece move, causing 60fps minimap repaints during drag. Replacedpiecesdep withplacedByCell(theuseMemoderived value) β redraws only on actual placement changes.
- Auto-dismiss raced with first-snap detection β The "start" tip auto-dismissed at 3s via
setStep("done"). If the user snapped their first piece at 2.5s, the first-snap effect setstep = "firstSnapDone", then the 3s timer overrode it to"done"β permanently skipping the tray tip and zoom tip. Fixed with a functional state updater that only dismisses if still on"start".
QuotaExceededErrorswallowed silently βsetItemcaught all errors including storage quota errors. On mobile when storage is full, puzzle saves silently failed with no feedback. Addedconsole.warnfor quota errors.
- Prestige threshold too low β
PRESTIGE_MIN_LEVEL = 5was reachable in roughly 2 puzzles, making prestige meaningless. Raised to 10.
51 bugs fixed across 40 files. See README.md for project overview.