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.)

ComponentLinesLocationNotes
CSVImportWizard1497web/src/components/crm/CSVImportWizard.tsxLargest 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 page672web/src/pages/invoices/index.tsxGod component with table, filters, creation, PDF generation. Should extract table, filters, and dialogs.
BudgetOverview646web/src/components/crm/BudgetOverview.tsxEnvelope cards, account cards, sparklines, inline editing. Could extract card components.
Dashboard page551web/src/pages/dashboard/index.tsxFires 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 imports

No 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, and TaskCard off direct useSupabase() (via new useCreateTaskFromBlock / useBudgetTransactionLookups / reuse of useToggleTaskStatus and getTaskCardModel). Remaining bypasses below. FilesTab also still uses useSupabase() for one imperative createSignedUrl download that doesn’t fit a query hook.

Components still bypassing the hook layer with useSupabase() directly:

ComponentFileNotes
AddLoaneeDialogAddLoaneeDialog.tsx
AddConfiscationDialogAddConfiscationDialog.tsx
CommandPaletteCommandPalette.tsx
FilesTabFilesTab.tsxOne 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/createMutation factories (+ createTableQueryWithArgs, createMaybeSingleQuery*, createRpcMutation, supabase-builders.ts). Query keys for those hooks are now built by the factory from a single declared key, and invalidation uses invalidateKeys (multi-key / function-form). The prefixed project* 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 separate queryKeys.ts module 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-any and no-unused-vars violations.

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-55toISOString().slice(0,10) UTC bug
  • useOnCallMonthly.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 / eveningDate now recompute on document.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 (commit 1bf861a). MobileHours.active still 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.ts and useOnCallMonthly.ts still 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 was toLocalDateString.) web/src/mobile/lib/formatTime.ts is the inverse: uses local TZ where day-plan blocks (stored as 1970-01-01THH:MM:00Z) need UTC.

Behavior changes the DRY pass did make (for the record)

  • TaskCard priority pill palette — now uses @/lib/colors to match the Kanban dots: MEDIUM is amber (was copper), HIGH is red-500 (was the status-unpaid token). A deliberate palette unification.
  • TaskCard empty AvatarFallback — was rendering an empty circle for assignees with no image; now shows initials.
  • InventoryTab project filter — switched from name-match to FK-id-match (more correct).