Technical Debt
Known technical debt items in the Levandor CRM, organized by severity and effort.
2026-05-11 — DRY/simplification pass
A Tier 0+1+2 DRY pass landed (≈5k net lines removed). It closed the “No Query Key Constants” item below (hook factories now centralize keys), shrank “Direct Supabase Calls in Components” (5 of 8 moved off), and left a fresh deferred list — see Deferred from the 2026-05-11 DRY Pass. The “Large Monolithic Components” and “Non-Atomic Mutations” and “Hardcoded VAT” items below were explicitly out of scope for that pass and are unchanged.
Large Monolithic Components
Components that have grown beyond maintainable size and should be decomposed. (Explicitly out of scope for the 2026-05-11 DRY pass — that was Tier 0+1+2; this is Tier 3.)
| Component | Lines | Location | Notes |
|---|---|---|---|
| CSVImportWizard | 1497 | web/src/components/crm/CSVImportWizard.tsx | Largest component; handles CSV parsing, column mapping, preview, duplicate detection, and import. Should be split into wizard steps. (Was moved off direct useSupabase() in the DRY pass — now uses useBudgetTransactionLookups.) |
| Invoices page | 672 | web/src/pages/invoices/index.tsx | God component with table, filters, creation, PDF generation. Should extract table, filters, and dialogs. |
| BudgetOverview | 646 | web/src/components/crm/BudgetOverview.tsx | Envelope cards, account cards, sparklines, inline editing. Could extract card components. |
| Dashboard page | 551 | web/src/pages/dashboard/index.tsx | Fires 7 parallel queries. Should extract widget components and consolidate queries. |
Test Coverage
Low Coverage
Only 13 out of 82 hook files have tests (15.8% coverage). The remaining 69 hooks have zero test coverage.
Hooks WITH tests (13):
useCreateInvoice, useGenerateLineItems, useInvoiceLineItems, useInvoices, useMarkNotificationRead, useNotifications, useProjectMembers, useStorageBuckets, useStorageList, useStorageMutations, useTasksRealtime, useTimesheetGrid, useUpsertTimesheet
Untested areas of highest risk:
- Budget mutations (18 hooks in
useBudgetMutations.ts) — complex transfer logic, retroactive rule application - Day planner hooks — time block creation, reordering, status transitions
- Dashboard stats — aggregation logic
- Timesheet export — data joining and export formatting
Test infrastructure: Vitest + Testing Library + jsdom. Test files in web/src/lib/hooks/__tests__/.
No Route-Based Code Splitting
All 17 page modules are eagerly imported in App.tsx with static imports:
import DashboardPage from '@/pages/dashboard'
import ProjectsPage from '@/pages/projects'
// ... 15 more static importsNo React.lazy(), no Suspense boundaries, no dynamic import(). The entire app is bundled as a single chunk. With 17 routes and 101 CRM components, this impacts initial load time.
Fix: Wrap page imports in React.lazy() with Suspense fallbacks. React Router v7 supports this natively.
Direct Supabase Calls in Components
Partly addressed 2026-05-11
The DRY pass moved
DayPlanner,KanbanBoard,TaskDetailSheet,CSVImportWizard, andTaskCardoff directuseSupabase()(via newuseCreateTaskFromBlock/useBudgetTransactionLookups/ reuse ofuseToggleTaskStatusandgetTaskCardModel). Remaining bypasses below.FilesTabalso still usesuseSupabase()for one imperativecreateSignedUrldownload that doesn’t fit a query hook.
Components still bypassing the hook layer with useSupabase() directly:
| Component | File | Notes |
|---|---|---|
| AddLoaneeDialog | AddLoaneeDialog.tsx | |
| AddConfiscationDialog | AddConfiscationDialog.tsx | |
| CommandPalette | CommandPalette.tsx | |
| FilesTab | FilesTab.tsx | One imperative createSignedUrl download — list query was moved to useProjectFiles in the DRY pass |
Problem: Bypasses the query cache, can cause stale data, makes testing harder, and violates the project’s data access pattern.
Fix: Extract direct Supabase calls into dedicated hooks that use TanStack Query for caching and invalidation.
No Query Key Constants — RESOLVED (2026-05-11)
Closed by the DRY pass
The 2026-05-11 DRY pass converted ~50 hooks onto
createTableQuery/createMutationfactories (+createTableQueryWithArgs,createMaybeSingleQuery*,createRpcMutation,supabase-builders.ts). Query keys for those hooks are now built by the factory from a single declared key, and invalidation usesinvalidateKeys(multi-key / function-form). The prefixedproject*keys (e.g.['projectInvoices', projectId]) are deliberately chosen so existing['invoices']invalidations still prefix-match. The handful of hooks still hand-rolled (transforms / upserts /functions.invoke/ side-effects) still have inline key literals — not worth a separatequeryKeys.tsmodule now.
Non-Atomic Mutations
Invoice Creation
useCreateInvoice performs multiple sequential Supabase calls (create invoice, then create line items, then update status) without a transaction. If any step fails, the data is left in an inconsistent state (e.g., invoice exists with no line items).
Fix: Convert to a Supabase RPC (stored procedure) that creates the invoice and line items atomically in a single database transaction.
Budget Transfer
useCreateBudgetTransfer creates a transfer record and then links transactions to it in separate calls. If the linking step fails, orphaned transactions are left in the database with no parent transfer record.
Fix: Same pattern — convert to a Supabase RPC that performs the entire transfer (create record + link transactions) in a single atomic transaction.
Hardcoded VAT Rate in Billingo Sync
billingo-sync-statuses uses a hardcoded HUF VAT multiplier of 1.27 (Hungarian 27% VAT) when calculating gross amounts. This is applied to all currencies, not just HUF.
Impact: Vendor bills in EUR, USD, or other currencies will have incorrect gross amounts in the budget system.
Fix: Either look up the VAT rate from the Billingo invoice data itself (which includes tax breakdown) or store currency-specific VAT rates in a configuration table.
Dashboard Query Consolidation
The dashboard page fires 7 parallel queries on mount. Each hits Supabase independently.
Fix: Consider a dashboard-specific RPC that returns all dashboard stats in a single round trip, or at minimum consolidate related queries.
Lint Errors
Last checked: 2026-04-27
76 remaining ESLint errors, primarily
no-explicit-anyandno-unused-varsviolations.
These are suppressed warnings that don’t block the build but indicate type safety gaps. Most no-explicit-any violations are in hook files where Supabase response types are not narrowed, and in older components that predate the strict TypeScript configuration.
Mobile Audit Follow-Ups (2026-05-09)
Discovered during the mobile native-feel audit pass. Pre-existing issues, not new — captured here for future cleanup.
Same UTC date-drift bug pattern
Same class of bug as the hours-tab “garbage hours” issue (fixed via toLocalDateString() in web/src/lib/date-utils.ts):
useDayPlanSuggestions.ts:54-55—toISOString().slice(0,10)UTC buguseOnCallMonthly.ts:37— same pattern
Pre-existing, not in scope of mobile work. See Audit Pass and debugging-log-crm.
Mount-frozen date selectors
Partly fixed 2026-05-11
MobilePlan.date/eveningDatenow recompute ondocument.visibilitychange(so a long-lived PWA doesn’t go stale across midnight / the 15:00 evening-day boundary) — fixed alongside the evening-checklist day-boundary bug (commit1bf861a).MobileHours.activestill freezes at mount.
MobileHours.active freezes at mount — it won’t roll forward across midnight if the app stays open. Same pre-existing pattern as the desktop standup page (which uses a UTC-based toISODateString(new Date()) rather than local — could drift by a day near midnight UTC ≈ 1–2 am CEST; not fixed). Deliberately not fixed for MobileHours.
useUpdatePrompt setInterval has no cleanup
But AppRoutes lifetime = app lifetime, so practically harmless.
Stub aggregations in EveningReflectionCard
tasksCompletedCount={0} and hoursLogged={0} stubbed in EveningReflectionCard (mirrors desktop standup page). Would need new aggregations to populate.
Bundle-size warning persists
Chunks >500kB. Pre-existing per project CLAUDE.md, related to lack of route-based code splitting (above).
Per-user Evening Checklist prompts
Current standup_datapoint_config rows for EVENING_CHECKLIST phase are global. Per-user customization is a v2 feature. See Future Graduation Path.
Forecast formula doesn’t cap daily allocation
dashboard_summary_v1 sums avg_hrs × workdays × rate per project; with multiple parallel ONGOINGs, the total can exceed 8 hrs/day, overstating realistic billable. See Future Fix Paths for options.
Deferred from the 2026-05-11 DRY Pass
Considered and intentionally skipped during the DRY pass — each with a rationale (see that note for the full record). Listed here so they aren’t re-discovered as if missed.
Edge-function _shared/ consolidation
~6 edge functions hand-roll createClient / OPTIONS-preflight / auth-checks. Not touched because it changes deployed Supabase functions (one, service-health, is wired into the app shell/sidebar), can’t be verified or deployed from a refactor pass, and the win is small after excluding the load-bearing cf-access-auth / send-push. Warrants its own reviewed + deployed change.
notify Edge Function — repo-deleted, not yet server-removed
The DRY pass deleted web/supabase/functions/notify/ from the repo (it’s superseded by the event → fn_event_to_notifications → emit_notification chain). Before removing it server-side, confirm no Supabase-dashboard Database Webhook still points at it.
sync-outbound Edge Function — possibly dead
The Linear-push edge function may be dead, but might be hit by an external scheduler. Left for the user to confirm before deletion.
packages/tender-pipeline — unshipped
Nothing imports it; the intended tender-sync edge function is an empty stub; 2 of its tests fail (TED v3 / EKR API drift). Candidate for extraction to its own repo or outright deletion. Left untouched per instruction not to delete the spec/package (docs/superpowers/specs/2026-04-26-tender-pipeline-design.md, 2026-04-27-tender-pipeline-ui-design.md).
UTC drift — superset of the "Mobile Audit Follow-Ups" item above
useDayPlanSuggestions.tsanduseOnCallMonthly.tsstill use UTC-based date strings (toISOString().slice(0,10)/ holiday-date bounds) where local was probably intended. The DRY pass deliberately left these — they need a UTC-vs-local behavior decision, not a mechanical dedupe. (The desktop fix elsewhere wastoLocalDateString.)web/src/mobile/lib/formatTime.tsis the inverse: uses local TZ where day-plan blocks (stored as1970-01-01THH:MM:00Z) need UTC.
Behavior changes the DRY pass did make (for the record)
- TaskCard priority pill palette — now uses
@/lib/colorsto match the Kanban dots: MEDIUM is amber (was copper), HIGH is red-500 (was thestatus-unpaidtoken). A deliberate palette unification. - TaskCard empty
AvatarFallback— was rendering an empty circle for assignees with no image; now shows initials. InventoryTabproject filter — switched from name-match to FK-id-match (more correct).
Related
- levandor-crm - Project overview
- agent-context-crm - Agent quick reference
- debugging-log-crm - Bugs that have been fixed
- dry-refactor-2026-05-11 - The 2026-05-11 DRY/simplification pass (new shared abstractions, deferred items, behavior changes)