DRY / Simplification Refactor Pass — 2026-05-11
A 9-commit DRY/simplification pass on master (d035235..ccf6f57, on top of 8fbfda1). Net: 148 files changed, ~9,359 lines deleted / ~4,332 added (≈5k net lines removed). Every phase gated on tsc -b clean + vitest (one pre-existing MobileHours.test.tsx failure, no new ones) + green build. This was the Tier 0 + 1 + 2 cut — Tier 3 (god-component decomposition) was explicitly out of scope.
For Agents
This pass introduced a batch of shared abstractions that future work should reuse rather than re-roll. See New shared abstractions — reuse these. It also deferred several known issues with rationale — see Deliberately NOT done — deferred. Two palette/behavior unifications were applied as a side effect — see Behavior changes worth knowing.
Phases
P0 — dead code deleted
web/src/lib/database.types.ts— 4141-line byte-identical orphan ofpackages/shared/src/database.types.ts.- 4 never-wired-up
packages/uicomponents:animated-list,bento-grid,chart,text-animate. web/src/components/crm/PageHeader.tsx— the OLD dead one (a newPageHeaderis introduced in P2a, see below).useReorderDayPlanBlocks+ its test;useDeleteOnCallSchedule;userspace/src/lib/hooks/useClientUser.ts.web/supabase/functions/notify/— the legacynotifyEdge Function, superseded by theevent → fn_event_to_notifications → emit_notificationchain.
CAVEAT for the user —
notifyEdge FunctionBefore
notifyis removed server-side (it’s only deleted from the repo here), confirm no Supabase-dashboard Database Webhook still points at it. The function source is gone fromweb/supabase/functions/but a dashboard webhook would still try to invoke a now-undeployable function.
P1a — hook factories
Extended the existing factory modules and converted ~50 hand-rolled CRUD/read hooks onto them.
web/src/lib/hooks/createTableQuery.ts— addedcreateMaybeSingleQuery,createSingleQuery,createTableQueryWithArgs,createMaybeSingleQueryWithArgs.web/src/lib/hooks/createMutation.ts— multi-key / function-forminvalidateKeys;returningfor.select().single(); newcreateRpcMutation.- Converted (~50 hooks): budget / on-call / tasks / inventory / project-members / datapoints / khr / standup / automations / rss / timesheet / vendor domains.
- Deliberately left hand-rolled: hooks doing transforms / key-remaps / upserts /
functions.invoke/ extra side-effects.
P1b — dialogs
Extended CreateEntityDialog into the standard form-dialog shell and migrated 10 hand-rolled dialogs onto it.
- New
CreateEntityDialogprops:submitLabel,pendingLabel,maxWidth,extraFooterLeft,showPendingSpinner,trigger; later additions:description: ReactNode, additivecontentClassName. - Migrated (10):
AddTransaction,AddLoanee,AddConfiscation,AddPayback,CreateAccount,Envelope,Transfer,LinkBillingo,CreateInvoice. (Note: that’s 9 names listed in the work log —AddPaybackis the 10th.) MappingRuleDialogNOT migrated —CreateEntityDialogwraps children in a<form>, andMappingActionBuilder’s “Add Action” / remove buttons lacktype="button"→ they’d submit the form. Needs that fixed first.- Mixed form libs left intentional: some migrated dialogs use
@tanstack/react-form, others keep localuseState— the latter need component-body cross-field reactivity that react-form’s render-prop model doesn’t give cheaply (EnvelopeDialog’s dynamic description,AddTransactionDialog’s conditional fields).
P1c — small dedupes
downloadBlobextracted intocsv-export.ts; deleted byte-identicaldownloadTimesheetCsv.getMonthRange→date-utils.ts.- userspace
statusColor/statusLabel→ newuserspace/src/lib/project-status.ts. - ~16 inline
.toISOString().slice(0,10)→toISODateString(). - Dropped private
toDateStr/toISODateStringcopies in favor oftoLocalDateString. CSVImportWizard/MappingRuleDialogerror banners →MUTATION_ERROR.
P2a — list-page primitives
New shared components + a URL-state hook, all with tests.
- New
web/src/components/crm/:PageHeader,ExportCsvButton,CreateButton,ErrorBanner,CardSkeleton(+SkeletonLines),ResourceTable. - New
web/src/lib/hooks/useUrlListState.ts. - Rolled out:
projects/partners/contracts/people(headers + buttons + error);tasks/invoices/inventory(buttons + error only — their headers are structural variants). tasks/index.tsxURL-param boilerplate →useUrlListState.dashboard StatCardSkeleton+automationsrunner skeleton →SkeletonLines(other bespoke skeletons left — replacing them would change loading visuals).- Side effect: 3 create buttons gained
data-create-action(thec-keyboard-shortcut hook).
P2b — project-detail tabs
Pushed the project filter down to Supabase instead of fetch-whole-table + client-filter.
useInvoices/useContracts/useInventory/useTasksnow accept an optionalprojectIdarg (createTableQueryWithArgs) →.eq('project' | 'for_project', projectId); no-arg behavior unchanged.- New query keys
projectInvoices/projectContracts/projectInventory/projectTasks/projectFiles— prefixed arrays, so existing['invoices']etc. invalidations still prefix-match. InvoicesTab/ContractsTab/InventoryTab→ResourceTable;TasksTab/FilesTabswitched to the scoped hooks (their rendering isn’t a plain table).TimesheetsTableft as-is (footer row +useTimesheetsnot in scope — still over-fetches).FilesTablist query → newuseProjectFileshook;OverviewTab.toggleOnCall→useUpdateProject— both moved off directuseSupabase().
InventoryTab filter change
InventoryTabswitched from name-match to FK-id-match filtering — more correct, but a behavior change to be aware of if anything depended on the old loose match.
P2c — shared hook logic + useSupabase() bypasses
on-call-rates.tsgotbuildRateMaps/entryHours/entryRate— the${person}_${project}rate maps + standby/activated/multipliers ternary were copy-pasted across 5 dashboard/on-call hooks. New tests for those and forresolveOnCallRate(previously untested financial code).makeStorageAttachmentHooks—useInvoicePdf/useContractPdfwere the same module twice (95/87 → 18/18 lines).foreign-resource.ts+ForeignResourceRowlifted tomatcher.ts— the copy-pasted interface + create-FR-then-insert-mappings / delete plumbing shared by 3 integration-mapping hooks. (useStatusMappings.createMappingleft inline — single-object insert vs array;useStorageProjectAssignmentscreate-branch later wired to the helper.)TaskChips.tsx+getTaskCardModel— dedupeTaskCard/KanbanCardchip rendering. FixedTaskCard’s emptyAvatarFallbackbug (now shows initials).TaskCardpriority pill now uses@/lib/colors(matches the Kanban dots — see Behavior changes worth knowing).DayPlanner/KanbanBoard/TaskDetailSheet/CSVImportWizardmoved off directuseSupabase()— newuseCreateTaskFromBlock,useBudgetTransactionLookups;KanbanBoard/TaskDetailSheetreuseuseToggleTaskStatus. (FilesTabstill usesuseSupabase()for one imperativecreateSignedUrldownload — doesn’t fit a query hook.)
P2d — colors typed
- Replaced stringly-typed
getStatusBorderColor / getStatusTextColor(domain: string, status: string)with typedgetProjectStatusBorderColor/getProjectStatusTextColor/getInvoiceStatusBorderColor/getInvoiceStatusTextColor(status: ProjectStatus | InvoiceStatus)— exhaustiveness now compile-checked. - Dropped the dead
textMaps.tenderPipeline/tenderStatusentries.
simplify-review + cleanup
Ran a 3-lens review (reuse / quality / efficiency) — verdict clean, no behavior bugs, server-side project filter confirmed a real win. Applied 9 small follow-ups: mixed label sizes, stray comments, missed ErrorBanner / SkeletonLines adoptions, no-op setSearchParams short-circuit, unused prop, lost bold name restored, tableBuilder / callRpc extracted to web/src/lib/hooks/supabase-builders.ts.
Behavior changes worth knowing
Two deliberate unifications + one bug fix
- TaskCard priority pill palette unified with the Kanban dots — now via
@/lib/colors. MEDIUM is amber (was copper), HIGH is red-500 (was thestatus-unpaidtoken). Flagged for the user as a palette change, not a regression.TaskCardemptyAvatarFallbackbug fixed — assignee avatars with no image now render initials instead of an empty circle.InventoryTabproject filter — name-match → FK-id-match (more correct).
New shared abstractions — reuse these
For future sessions
Before hand-rolling a list page, a form dialog, a CRUD hook, a CSV export, or on-call-rate math — check these first.
web/src/components/crm/
PageHeader— list-page header (title + actions slot).ExportCsvButton,CreateButton— standard list-page action buttons (CreateButtoncarriesdata-create-actionfor thecshortcut).ErrorBanner— standard mutation/query error banner.CardSkeleton+SkeletonLines— loading placeholders.ResourceTable(+Column<T>) — generic data table for list pages / project-detail tabs.TaskChips(severalTask*Chipexports) +getTaskCardModel/useTaskCardModel— shared task-card chip rendering forTaskCardandKanbanCard.CreateEntityDialog— the standard form-dialog shell. Props:submitLabel,pendingLabel,maxWidth,extraFooterLeft,showPendingSpinner,trigger,description: ReactNode,contentClassName. (Caveat: it wraps children in a<form>— nested buttons needtype="button".)
web/src/lib/hooks/
createTableQueryWithArgs,createMaybeSingleQuery,createSingleQuery,createMaybeSingleQueryWithArgs— query-hook factories with optional args (e.g. an optionalprojectIdthat becomes a.eq()).createRpcMutation— mutation factory for Supabase RPCs.makeStorageAttachmentHooks— factory foruse<Entity>Pdf-style storage-attachment hook pairs.useUrlListState— URL-search-param-backed list state (filters/sort/etc.).useCreateTaskFromBlock— create a task from a day-plan block (replaces aDayPlannerdirect-useSupabase()call).useBudgetTransactionLookups— budget-transaction reference lookups (replaces aCSVImportWizarddirect-useSupabase()call).useProjectFiles— project-scoped file list (replaces aFilesTabdirect query).supabase-builders.ts—tableBuilder/callRpclow-level builders shared by the factories.
web/src/lib/
on-call-rates.ts—buildRateMaps,entryHours,entryRate(+ existingresolveOnCallRate, now tested).foreign-resource.ts—createForeignResourceWithMappings,deleteForeignResource.matcher.ts— now also exportsForeignResourceRow,IntegrationMappingRow.csv-export.ts—downloadBlob(+ existing CSV helpers).date-utils.ts—getMonthRange,toISODateString(+ existingtoLocalDateString).colors.ts— typedgetProject/InvoiceStatusBorder/TextColor(status).
userspace/src/lib/
project-status.ts—statusColor,statusLabel.
Deliberately NOT done — deferred
For future sessions — known but intentionally untouched
These were considered and skipped this pass, each with a reason. Don’t assume they were missed.
- Edge-function
_shared/consolidation — the ~6 functions that hand-rollcreateClient/ OPTIONS-preflight / auth-checks. Not done: touches 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-bearingcf-access-auth/send-push. Warrants its own reviewed + deployed change. - UTC-drift bugs —
web/src/lib/hooks/useDayPlanSuggestions.ts(monday.toISOString().slice(0,10)) anduseOnCallMonthly.ts(holiday-date bounds) still use UTC-based date strings where local was probably intended. Left as-is: needs a deliberate UTC-vs-local behavior decision, not a mechanical dedupe. (The desktop fix elsewhere wastoLocalDateString.) Also tracked in Mobile Audit Follow-Ups (2026-05-09). packages/tender-pipeline— unshipped: nothing imports it, the intendedtender-syncedge function is an empty stub, 2 of its tests fail (TED v3 / EKR API drift). Candidate for extraction to its own repo or deletion. Untouched this pass (per explicit instruction not to delete the spec/package — seedocs/superpowers/specs/2026-04-26-tender-pipeline-design.mdand the2026-04-27-tender-pipeline-ui-design.mdspec).- God-component decomposition (Tier 3) —
CSVImportWizard(1497 lines),BudgetOverview, the god pages (invoices/settings/tasks/system-integrations/dashboard). Out of scope — this was the explicit cut line; Tier 0+1+2 only. Still tracked in Large Monolithic Components. - Correctness bugs found but not fixed (out of DRY scope):
billingo-sync-statuseshardcodes a1.27HUF VAT multiplier for all currencies (already in Hardcoded VAT Rate in Billingo Sync).useCreateInvoiceanduseCreateBudgetTransferare non-atomic client-side multi-step writes — should be Supabase RPCs (precedents:complete_block_with_draft_v1,dashboard_summary_v1). Already in Non-Atomic Mutations.web/src/mobile/lib/formatTime.tsuses local TZ where day-plan blocks (stored as1970-01-01THH:MM:00Z) need UTC.
web/supabase/functions/sync-outbound/— possibly-dead Linear-push edge function; left for the user to confirm (might be hit by an external scheduler). Same caveat-class as thenotifydeletion above.
Verification
tsc -bclean per phase.vitestper phase — one pre-existing failure (MobileHours.test.tsx), no new failures introduced.- Build green per phase.
- Final 3-lens review (reuse / quality / efficiency): verdict clean, no behavior bugs, server-side project filter confirmed a real perf win.
Related
- levandor-crm - Project overview
- tech-debt - Known technical debt (several items here cross-reference it; god-components, VAT, non-atomic mutations, UTC drift all still tracked there)
- agent-context-crm - Agent quick reference (data-layer pattern; updated with the new factories/primitives)
- debugging-log-crm - Past bugs (the
TaskCardAvatarFallback fix and the palette unification are behavior changes from this pass) - budget - Budget system (several budget hooks/dialogs migrated)
- timesheet-export - Timesheet export (
downloadBlobextraction,downloadTimesheetCsvdeletion) - kanban - Kanban board (
TaskChipsdedupe,useSupabase()removal) - day-planner - Day planner (
useCreateTaskFromBlock,useReorderDayPlanBlocksdeleted)