30 KiB
Pitfalls Research
Domain: Adding wizard setup, auto-budget creation, design system rework, and page consolidation to an existing personal budget app (React + Supabase + TanStack Query) Researched: 2026-04-02 Confidence: HIGH (based on direct codebase analysis + known patterns for wizard state, auto-creation concurrency, and design system rework in existing apps)
Critical Pitfalls
Pitfall 1: Duplicate Budget Creation on Concurrent Auto-Create Calls
What goes wrong:
The v2.0 goal is "auto-create this month's budget on first visit." The current generateFromTemplate mutation in useBudgets.ts has no deduplication guard — it creates a budget row unconditionally. If the dashboard mounts, checks for a current-month budget, finds none, and fires the auto-create, there is a race: if the component re-renders (e.g., auth state settles, React Strict Mode double-invokes effects) or the user has two tabs open, two budget rows get created for the same month. Both have the same start_date/end_date and user_id, but different UUIDs. There is no unique constraint on (user_id, start_date) in the current 004_budgets.sql migration.
Why it happens:
Auto-create logic placed in a React component or effect reacts to data absence, but TanStack Query's enabled flag and mutation guards don't prevent double-invocation in Strict Mode or multi-tab scenarios. The "check then create" pattern is an inherently non-atomic operation in the current client-side architecture.
How to avoid:
- Add a database unique constraint:
UNIQUE (user_id, start_date)on thebudgetstable via a new migration. This makes duplicate creation fail at the DB level rather than silently succeeding. - In the auto-create logic, use an upsert (INSERT ... ON CONFLICT DO NOTHING) rather than a plain INSERT, so concurrent calls are idempotent.
- Use TanStack Query's mutation state to gate the auto-create: only fire if
mutation.isIdleand no existing budget is found, never fire inside auseEffectwithout a ref guard. - After auto-create, immediately invalidate the budgets list query so the new budget is fetched in the same round-trip.
Warning signs:
- Two budget entries appear for the same month on the BudgetListPage after first visit
console.errorshows a Supabase23505 unique_violationif the constraint is added and the race is triggered- Dashboard shows "Budget already exists" error toasts intermittently on page load
Phase to address: Auto-budget creation phase — add the DB constraint first, implement upsert-based creation second. Do not build the auto-create UI trigger before the constraint exists.
Pitfall 2: Wizard State Lost on Browser Back/Refresh — No Persistence Strategy
What goes wrong:
A multi-step wizard (step 1: pick categories, step 2: set amounts, step 3: confirm) built with local React state (useState) loses all progress if the user navigates away, refreshes, or presses the browser back button. For a setup wizard that may have 3–5 steps and requires selecting 10–20 template items with amounts, this is a critical UX failure on first run. The user loses all work and must restart from step 0.
Why it happens:
The natural implementation is const [step, setStep] = useState(0) and const [selections, setSelections] = useState([]) inside the wizard component. React state is ephemeral — it does not survive unmount. This works fine for modals but fails for a first-run setup wizard that the user may pause mid-way.
How to avoid:
- Persist wizard state to
localStoragekeyed by user ID (e.g.,wizard_state_${userId}) — read on mount, write on every step change, clear on completion/skip. - Use a simple serializable state shape:
{ step: number, selections: { categoryId, amount, tier }[] }. - On wizard mount: check localStorage first; if partial state exists, resume from the saved step with a "Continue where you left off" prompt.
- On wizard completion or explicit skip: clear the localStorage key.
- Do NOT use URL params to store full wizard state (too much data); use URL only to track the current step number (e.g.,
?setup-step=2) for browser back/forward support.
Warning signs:
- Wizard always starts at step 1 even after the user had reached step 3
- User reports losing selected categories after an accidental page reload
- No
localStoragekeys being set/read during wizard interaction
Phase to address: Wizard setup phase — define the persistence strategy before building any step components. The storage format must be finalized before the first step is wired.
Pitfall 3: Design Token Rework Breaks Existing Pages Silently
What goes wrong:
The v2.0 design system rework changes CSS variables in index.css (border-radius tokens from rounded to sharp, color palette from current OKLCH pastels to sharper/clearer ones, spacing adjustments). Because every existing page consumes these global tokens, any change to --radius, --background, --foreground, --border, or the six category color variables cascades to all 9 pages simultaneously. Changing --radius from 0.5rem to 0px sharps every Button, Card, Input, and Badge across the app in one line — but it also breaks charts (Recharts SVG rx attributes don't follow CSS variables) and any component that had visual design crafted around rounded corners.
Why it happens:
The current system uses shadcn/ui's CSS variable convention which is explicitly global-scope. Every component that uses rounded-lg (which resolves to var(--radius)) will change simultaneously. Pages that were "done" in v1.0 become visually broken the moment a global token changes.
How to avoid:
- Before touching any tokens, audit which existing pages use
--radius,--border, and--backgroundimplicitly (via Tailwind utilities likerounded-md,border,bg-background). This is every page. - Change tokens in isolation with a visual regression pass on ALL 9 pages before touching any page's component code. The token change should be one commit; the per-page fixup should be separate commits.
- For the category color variables (
--color-incomethrough--color-investment), verify Recharts SVG fills still resolve after the change — these are passed asfill={var(--color-income)}in JS and require testing in both the browser DOM and printed output. - Do not land the token rework mid-phase alongside page component changes — treat it as its own atomic commit.
Warning signs:
- Auth pages (login/register) have wrong border-radius after a
--radiustoken change - Chart SVG fills appear correct in DevTools computed styles but wrong visually on screen
- Any page that was marked "done" in v1.0 shows new visual regressions after token PR merges
Phase to address: Design system foundation phase — the first phase of v2.0. Token rework must land before any page is rebuilt. Requires an explicit visual regression check of all existing pages immediately after the token commit.
Pitfall 4: Quick-Add Page Removal Breaks Existing User Data and Nav References
What goes wrong:
The quick_add_items table and QuickAddPage exist in production. Existing users have data in this table (names, icons, sort_order). If the page is simply removed from routing and the nav (deleting <Route path="quick-add" /> and the nav item in AppLayout.tsx) without a data migration or a clear deprecation path, those users:
- Lose access to their quick-add items (no UI)
- Have orphaned
quick_add_itemsrows in the database that no longer serve any purpose - May hit broken bookmarks or history entries for
/quick-add
The new v2.0 model replaces quick-add with "add one-offs from category library directly in budget detail." This is a different data shape — category library items are categories, not quick_add_items. If a user had quick-add items that conceptually map to categories, that mapping is lost.
Why it happens: Page removal feels like a simple delete-the-route operation. The data persistence and existing user considerations are invisible during development because the developer's test account may have no quick-add data.
How to avoid:
- Audit: Does any existing user data in
quick_add_itemsmap to something meaningful in the new model? If quick-add items are just named shortcuts and the new model uses categories instead, the migration path is: surface existing quick-add items in the new "category library" flow, or clearly communicate to users that this data is being retired. - For the route: add a redirect
<Route path="quick-add" element={<Navigate to="/budgets" replace />} />rather than leaving/quick-addas a 404. This handles bookmarks and history. - Remove the nav link in
AppLayout.tsxat the same time as the redirect is added — never before. - Add a DB migration only if you decide to drop or repurpose the
quick_add_itemstable. If keeping the table dormant, document why. If dropping it, confirm no FK references exist (none in the current schema —budget_itemsdoes not referencequick_add_items).
Warning signs:
- Developer tests removal on a fresh account with no quick-add data, misses the issue
- Browser history entries for
/quick-addresult in a blank or redirect-looping page useQuickAddhook is still imported somewhere but the page is gone, causing a dead import
Phase to address: Page consolidation phase (when BudgetDetailPage gets inline add-from-library). Remove the page only after the replacement flow is functional and tested with existing user data.
Pitfall 5: Auto-Budget Creation Ignores User Currency Setting
What goes wrong:
The current generateFromTemplate mutation in useBudgets.ts accepts currency?: string with a default of "EUR". If the auto-create logic fires without reading the user's profile currency field first, every auto-created budget will be denominated in EUR regardless of the user's settings. A user who has set their currency to USD or GBP in Settings will get EUR-denominated budgets. The formatCurrency calls will then display amounts in the wrong currency until the user manually edits the budget.
Why it happens:
The auto-create is triggered by checking if a budget exists for the current month and calling generateFromTemplate({ month, year }) — the currency parameter is optional and defaults silently. During development on a EUR-default account, this works fine and the bug is invisible.
How to avoid:
- The auto-create trigger must first load the user's profile (from the
profilestable,currencycolumn) and pass that value explicitly:generateFromTemplate({ month, year, currency: profile.currency }). - The
useProfileoruseAuthhook should be resolved before the auto-create mutation fires. Use TanStack Query'senabledflag to chain the dependency: only auto-create when both the budgets list AND the profile are loaded. - Add an assertion in the mutation: if
currencyis undefined, throw rather than silently default.
Warning signs:
- Auto-created budgets always show EUR symbol even when user has set USD in Settings
- The profile
currencyis not read anywhere in the auto-create code path - Test passes for EUR users only; USD/GBP users never tested
Phase to address: Auto-budget creation phase — the profile dependency must be resolved as part of the auto-create implementation, not as a follow-up fix.
Pitfall 6: Wizard Creates Categories and Template Items Without Idempotency — Re-run Corrupts Data
What goes wrong:
The setup wizard pre-fills common items (rent, groceries, car insurance, etc.) and on completion creates: (a) category rows in categories, (b) template_items in template_items. If the user runs the wizard, quits mid-way, then re-runs it, or if the wizard's "complete" step fires twice due to a network retry, duplicate categories and template items are created. The categories table has no unique constraint on (user_id, name). The user ends up with "Rent" appearing twice as a category and twice as a template item.
Why it happens: Wizard completion is typically a single "save all" mutation. If the user navigates back to step 1 and completes again, or if a network error causes a retry, the entire creation sequence runs again. Category creation has no deduplication.
How to avoid:
- Add a database unique constraint on
categories (user_id, name)(or at minimum check for existence before insert). Use upsert for category creation in the wizard flow. - Gate the wizard's final "create" step behind a
isFirstRunflag stored in the user'sprofilestable (e.g., asetup_completedboolean). Oncetrue, the wizard's create mutation is a no-op. - Use a two-phase completion approach: "Preview what will be created" → "Confirm" — this prevents accidental double-submission.
- If using localStorage for wizard state, clear it atomically when the server-side creation succeeds, not before.
Warning signs:
- Running the wizard twice results in duplicated category rows in the Categories page
- Template page shows duplicate items with identical names
- A Supabase unique constraint error on
categoriesfires if the constraint is added after the wizard is built
Phase to address: Wizard setup phase — idempotency requirements must be specified as acceptance criteria before any wizard mutation code is written.
Pitfall 7: CSS Variable Scope Rework Breaks Recharts SVG Fill Resolution
What goes wrong:
v1.0 established that category colors are passed to Recharts as CSS variable references resolved in JavaScript (var(--color-income) etc. via categoryColors in palette.ts). If the v2.0 design token rework renames these variables (e.g., from --color-income to --category-income, or changes the OKLCH values such that the Recharts SVG fill attributes show visually different colors than intended), charts break silently — the SVG renders but fills are wrong.
Additionally, Recharts SVG elements operate outside the standard CSS cascade for some browsers. If OKLCH values are changed to be very light (high L) in pursuit of "clearer pastels," SVG fills may become nearly invisible on white chart backgrounds because SVG doesn't inherit background opacity in the same way HTML elements do.
Why it happens:
Design token rework focuses on HTML components and Tailwind utility class output. Recharts SVG fills are wired through palette.ts as literal CSS variable strings — not Tailwind classes — so they are easy to miss in a token audit.
How to avoid:
- After any
index.csstoken change, explicitly open the dashboard and inspect all three charts (donut, bar, horizontal bar) to verify fill colors match intention. - Keep
palette.tsas the single source of truth for chart colors — do not duplicate color definitions in component files. - If renaming CSS variable names (not just changing values), grep the entire codebase for the old variable name before deleting it.
- Test chart fill visibility against the new
--backgroundvalue — lighter backgrounds require slightly more saturated or darker chart fills to maintain visibility.
Warning signs:
- Chart fills appear white or very faint after a design token change
palette.tsstill references--color-incomeafter a rename to--category-income- Charts look correct in Storybook/isolation but wrong on the actual dashboard
Phase to address: Design system foundation phase (when tokens change) AND dashboard phase (verification).
Pitfall 8: "Template Empty" Edge Case Breaks Auto-Budget and New-User Wizard
What goes wrong:
The auto-budget creation flow depends on the template having items. The current generateFromTemplate code handles an empty template gracefully — it creates an empty budget. But the v2.0 UX intent is that visiting the dashboard for the first time should show a useful budget, not an empty one. If the wizard is bypassed (user skips setup), or if wizard completion fails silently, the auto-created budget has zero items, and the dashboard shows nothing meaningful.
A second edge case: a user who completed v1.0 setup manually (has categories + template items) visits v2.0 for the first time. The wizard should not re-run — but if the "has template items" check is the gate, and they had template items before, the wizard is skipped, and the auto-create fires correctly. If that check is wrong (e.g., checking template.name === "My Monthly Template" instead of template_items.length > 0), existing users get redirected into the wizard.
Why it happens: The first-run detection logic is complex: it must distinguish between (a) brand new user with no data, (b) existing user who set up manually in v1.0, (c) user who started wizard but didn't finish, (d) user who completed wizard. Each case needs different behavior and the logic branches are easy to conflate.
How to avoid:
- Define the first-run gate explicitly using the
profiles.setup_completedboolean (recommended in Pitfall 6) — this is the single authoritative signal, not inferred from data shape. - For existing v1.0 users: write a migration or a one-time check that sets
setup_completed = truefor any user who already has template items. This prevents the wizard from showing for users who have already configured their template. - Test all four user cases explicitly before shipping the wizard.
Warning signs:
- Existing users (who set up in v1.0) see the setup wizard on their next login
- New users who skip the wizard get a blank dashboard with no explanation
- The
setup_completedflag is never set totruefor existing users via migration
Phase to address: Wizard setup phase — the first-run gate and existing-user migration must be defined as part of the wizard's acceptance criteria, not as an afterthought.
Technical Debt Patterns
| Shortcut | Immediate Benefit | Long-term Cost | When Acceptable |
|---|---|---|---|
No UNIQUE (user_id, start_date) constraint on budgets — skip the migration |
Saves one migration | Duplicate budgets on concurrent auto-create; data corruption that is painful to clean | Never — add the constraint before any auto-create code ships |
Auto-create logic in a useEffect |
Familiar React pattern | Double-invocation in Strict Mode, fires on re-render, race conditions | Never for write operations; use TanStack Query useQuery with initialData or a mutation gated on query result |
Wizard state in component useState only |
Simplest to write | Lost on refresh; user frustration on first run | Only for wizards that complete in under 30 seconds and have no network steps |
Skipping the profiles.setup_completed flag and inferring first-run from data shape |
No migration needed | Complex conditional logic; wrong inference for edge-case users; breaks if data shape changes | Never — explicit flag is always more reliable than inferred state |
Removing /quick-add route without a redirect |
One less route to maintain | Broken bookmarks, broken history, 404 for any hardcoded link | Never — always add a redirect when removing a route |
| CSS token rework and page component rework in the same PR | Ships faster | Impossible to attribute regressions; review is overwhelming; rollback is all-or-nothing | Never for design-system-wide token changes — tokens must land as their own commit |
Using the quick_add_items table schema for the new category library |
Reuses existing table and hooks | Conflates two different concepts; data model becomes unclear; future features referencing categories become harder | Only if category library is purely a UI concern with no persistence — but it needs persistence, so use categories table |
Integration Gotchas
| Integration | Common Mistake | Correct Approach |
|---|---|---|
| Supabase + auto-create budget | Plain INSERT in a client-side effect, no deduplication | Use INSERT ... ON CONFLICT DO NOTHING with a unique constraint on (user_id, start_date) |
| TanStack Query + auto-create mutation | Firing mutation inside useEffect with data dependency |
Use useQuery to check existence, then fire mutation via user action or a stable effect with ref guard |
| localStorage + wizard state | Storing state without user-scoping — one user's wizard state leaks to another on shared device | Key all localStorage entries with ${userId} prefix |
| Supabase RLS + category library pre-population | Inserting pre-filled categories from client-side wizard without user_id | user_id must be set to auth.uid() on all inserts; Supabase RLS policy will reject rows with wrong user_id |
| Recharts + new design tokens | Assuming token rename in index.css auto-updates SVG fill values |
palette.ts references CSS variable names explicitly — any rename requires updating palette.ts too |
| React Router + wizard steps | Using browser back button to go "back" in wizard while step is tracked only in state | Use URL search params (?step=2) so browser back/forward navigate wizard steps correctly |
| TanStack Query + profile currency dependency | Auto-create fires before profile query resolves; uses default currency | Use enabled: profileLoaded && budgetMissing to chain query dependencies |
Performance Traps
| Trap | Symptoms | Prevention | When It Breaks |
|---|---|---|---|
| Wizard pre-populates 20+ categories in one bulk insert without optimistic UI | Wizard "complete" step hangs for 2–3 seconds; no feedback to user | Show a loading state immediately; use mutate with onMutate for optimistic feedback |
Every user on wizard completion |
| Auto-create budget fires on every dashboard mount during loading state | Multiple budget rows created; duplicate toasts; TanStack Query cache stale | Gate auto-create on budgets.isSuccess (not isLoading), and use a ref to prevent repeat fires |
Any fast page navigation that remounts the dashboard |
| Category library rendered as a flat list with 50+ items and no virtualization | Picker feels slow and sluggish when inline in BudgetDetailPage | Virtualize the list with TanStack Virtual or paginate by category type | Not an issue at 10 items; noticeable at 30+; problematic at 50+ |
| Re-fetching template items on every wizard step navigation | Network calls on every back/forward in wizard; slow perceived performance | Cache template items in TanStack Query with a long staleTime; prefetch on wizard mount |
Noticeable on slow connections even with 5 template items |
Security Mistakes
| Mistake | Risk | Prevention |
|---|---|---|
| Wizard inserts categories without user_id from auth.uid() | Categories created with wrong or null user_id bypass RLS or are inaccessible | Always derive user_id from supabase.auth.getUser() inside the mutation; never pass it from client state |
Setup wizard marks setup_completed = true client-side before DB write succeeds |
User appears "set up" but has no data if the write fails; auto-create never triggers again | Mark setup_completed = true only in the mutation's onSuccess callback |
| Exposing wizard pre-fill library (category names + amounts) as a static client bundle | Not a critical risk for personal finance app, but hardcoded amounts (e.g., "Groceries: $500") are visible in source | Acceptable for this use case; note it is not PII |
UX Pitfalls
| Pitfall | User Impact | Better Approach |
|---|---|---|
| Wizard with "skip" that results in empty dashboard | User skips setup, sees blank dashboard, doesn't know why nothing is there | If user skips, show a non-empty empty state with a clear "Set up your template" CTA |
| Auto-created budget with no items (empty template) silently creates an empty budget | Dashboard appears to "work" but shows zeros; user confused why their categories aren't there | Check if template has items before auto-creating; if not, show a "complete your template setup" prompt |
| Removing quick-add nav item without explanation | Users who relied on quick-add can't find the feature; assume it's broken | Add a deprecation note or in-app transition message ("Quick-add is now available inline in your budget") |
| Wizard "back" button erases selections made in the later step | User goes back to change one thing and loses all subsequent work | Wizard back/forward must preserve all step state, not reset forward steps |
| Auto-created budget uses wrong month if user's timezone differs from server | User visiting on Dec 31 in UTC+11 gets a January budget auto-created | Derive month from the client's local timezone, not the server's |
| Category library in BudgetDetailPage shows all categories including types that contradict the budget context | Income categories shown when adding a one-off expense item; confusing | Filter the inline picker by relevant category types for the context (expense-only for one-off adds) |
"Looks Done But Isn't" Checklist
- Auto-create: Verify that visiting the dashboard twice in quick succession (e.g., double-clicking the nav link) creates exactly one budget, not two — check the budgets list after each scenario
- Auto-create currency: Create a test account with currency set to USD in Settings, then visit the dashboard — verify the auto-created budget shows USD, not EUR
- Wizard re-run: Complete the wizard on a fresh account, then manually navigate back to the wizard URL — verify it does not create duplicate categories
- Existing user path: Log in with an account that has v1.0 template items configured — verify the wizard does NOT appear, the existing template is preserved, and a budget is auto-created correctly
- Wizard refresh: Start the wizard, reach step 3, refresh the browser — verify state is restored from localStorage and the wizard resumes at step 3
- Quick-add redirect: Navigate directly to
/quick-addafter the page is removed — verify a redirect to/budgets(or equivalent) occurs, not a blank page or 404 - Empty template budget: Remove all items from the template, then navigate to the dashboard — verify no crash, a meaningful empty state is shown, and no empty budget is silently created
- Token rework regression: After changing
--radiusor category color tokens, navigate through all 9 pages (dashboard, categories, template, budget list, budget detail, settings, login, register, and any wizard page) — verify no visual regressions - German locale wizard: Complete the entire wizard flow in German locale — verify no raw translation keys appear and all pre-filled item names are localized
- Design consistency: Navigate between the newly redesigned pages and the pages not yet touched in the current phase — verify no jarring visual inconsistencies at page transitions
Recovery Strategies
| Pitfall | Recovery Cost | Recovery Steps |
|---|---|---|
| Duplicate budgets from concurrent auto-create | MEDIUM | Write a cleanup query to find duplicate (user_id, start_date) pairs and delete the empty duplicates; add the unique constraint; patch the auto-create to use upsert |
| Wizard created duplicate categories | MEDIUM | Deduplicate categories by (user_id, name) — merge any budget_items or template_items pointing to the duplicate, then delete the duplicate category rows |
setup_completed flag never set for v1.0 users — wizard shows on login |
LOW | Write a Supabase migration that sets setup_completed = true for any user with template_items.count > 0; deploy before releasing v2.0 |
| Token rework broke 3 pages discovered post-merge | MEDIUM | Revert the token commit if pages are too broken to patch quickly, or fix page-by-page with a visual regression pass; establish page-level snapshot tests |
| Quick-add route removed without redirect — users hitting 404 | LOW | Add <Route path="quick-add" element={<Navigate to="/budgets" replace />} /> — one-line fix; deploy immediately |
| Auto-budget uses wrong currency for existing users | LOW | Add a one-time correction query: update budgets created since v2.0 launch that have EUR currency to the user's profile currency where they differ; patch the auto-create to read profile currency |
Pitfall-to-Phase Mapping
| Pitfall | Prevention Phase | Verification |
|---|---|---|
| Duplicate budget creation on concurrent auto-create | Auto-budget creation phase — add DB unique constraint before any auto-create code | Verify (user_id, start_date) unique constraint exists in migration; test double-navigation scenario |
| Wizard state lost on refresh | Wizard setup phase — define localStorage persistence strategy in design | Refresh browser at step 3; verify wizard resumes at step 3 |
| Design token rework breaks existing pages | Design system foundation phase — token commit is isolated and followed by full-app visual pass | All 9 pages visually checked after every token change |
| Quick-add removal breaks nav/bookmarks | Page consolidation phase — redirect added in same commit as route removal | Navigate to /quick-add directly; verify redirect |
| Auto-budget ignores user currency | Auto-budget creation phase — profile dependency in auto-create spec | Test with USD-configured account; verify budget currency |
| Wizard creates duplicate categories on re-run | Wizard setup phase — idempotency specified as acceptance criteria | Run wizard twice; verify no duplicates in Categories page |
| CSS variable rename breaks Recharts fills | Design system foundation phase — grep for all variable references before rename | Open dashboard after any token rename; inspect all chart fills |
| Empty template edge case shows empty dashboard | Auto-budget + wizard phases — explicit first-run gate with setup_completed |
Test "skip wizard" path; verify empty state shown, not silent empty budget |
| First-run gate triggers for existing v1.0 users | Wizard setup phase — migration to set setup_completed for users with existing template items |
Log in with a v1.0 account; verify wizard is skipped |
Sources
- Direct codebase analysis:
src/hooks/useBudgets.ts(generateFromTemplate),src/hooks/useTemplate.ts(getOrCreateTemplate),supabase/migrations/004_budgets.sql,supabase/migrations/005_quick_add.sql,src/lib/types.ts,src/App.tsx,src/components/AppLayout.tsx - TanStack Query mutation patterns: known double-invocation behavior in React 18 Strict Mode (
useEffectfiring twice) - Supabase upsert documentation: INSERT ... ON CONFLICT DO NOTHING / DO UPDATE pattern
- React Router v6 redirect patterns for deprecated routes
- localStorage-based wizard state persistence: common pattern in onboarding flow implementations
- PostgreSQL unique constraint behavior on concurrent inserts
Pitfalls research for: SimpleFinanceDash v2.0 — wizard setup, auto-budget creation, design system rework, page consolidation Researched: 2026-04-02