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 of packages/shared/src/database.types.ts.
  • 4 never-wired-up packages/ui components: animated-list, bento-grid, chart, text-animate.
  • web/src/components/crm/PageHeader.tsx — the OLD dead one (a new PageHeader is introduced in P2a, see below).
  • useReorderDayPlanBlocks + its test; useDeleteOnCallSchedule; userspace/src/lib/hooks/useClientUser.ts.
  • web/supabase/functions/notify/ — the legacy notify Edge Function, superseded by the event → fn_event_to_notifications → emit_notification chain.

CAVEAT for the user — notify Edge Function

Before notify is 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 from web/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 — added createMaybeSingleQuery, createSingleQuery, createTableQueryWithArgs, createMaybeSingleQueryWithArgs.
  • web/src/lib/hooks/createMutation.ts — multi-key / function-form invalidateKeys; returning for .select().single(); new createRpcMutation.
  • 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 CreateEntityDialog props: submitLabel, pendingLabel, maxWidth, extraFooterLeft, showPendingSpinner, trigger; later additions: description: ReactNode, additive contentClassName.
  • Migrated (10): AddTransaction, AddLoanee, AddConfiscation, AddPayback, CreateAccount, Envelope, Transfer, LinkBillingo, CreateInvoice. (Note: that’s 9 names listed in the work log — AddPayback is the 10th.)
  • MappingRuleDialog NOT migratedCreateEntityDialog wraps children in a <form>, and MappingActionBuilder’s “Add Action” / remove buttons lack type="button" → they’d submit the form. Needs that fixed first.
  • Mixed form libs left intentional: some migrated dialogs use @tanstack/react-form, others keep local useState — 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

  • downloadBlob extracted into csv-export.ts; deleted byte-identical downloadTimesheetCsv.
  • getMonthRangedate-utils.ts.
  • userspace statusColor / statusLabel → new userspace/src/lib/project-status.ts.
  • ~16 inline .toISOString().slice(0,10)toISODateString().
  • Dropped private toDateStr / toISODateString copies in favor of toLocalDateString.
  • CSVImportWizard / MappingRuleDialog error 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.tsx URL-param boilerplate → useUrlListState.
  • dashboard StatCardSkeleton + automations runner skeleton → SkeletonLines (other bespoke skeletons left — replacing them would change loading visuals).
  • Side effect: 3 create buttons gained data-create-action (the c-keyboard-shortcut hook).

P2b — project-detail tabs

Pushed the project filter down to Supabase instead of fetch-whole-table + client-filter.

  • useInvoices / useContracts / useInventory / useTasks now accept an optional projectId arg (createTableQueryWithArgs) → .eq('project' | 'for_project', projectId); no-arg behavior unchanged.
  • New query keys projectInvoices / projectContracts / projectInventory / projectTasks / projectFilesprefixed arrays, so existing ['invoices'] etc. invalidations still prefix-match.
  • InvoicesTab / ContractsTab / InventoryTabResourceTable; TasksTab / FilesTab switched to the scoped hooks (their rendering isn’t a plain table).
  • TimesheetsTab left as-is (footer row + useTimesheets not in scope — still over-fetches).
  • FilesTab list query → new useProjectFiles hook; OverviewTab.toggleOnCalluseUpdateProject — both moved off direct useSupabase().

InventoryTab filter change

InventoryTab switched 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.ts got buildRateMaps / 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 for resolveOnCallRate (previously untested financial code).
  • makeStorageAttachmentHooksuseInvoicePdf / useContractPdf were the same module twice (95/87 → 18/18 lines).
  • foreign-resource.ts + ForeignResourceRow lifted to matcher.ts — the copy-pasted interface + create-FR-then-insert-mappings / delete plumbing shared by 3 integration-mapping hooks. (useStatusMappings.createMapping left inline — single-object insert vs array; useStorageProjectAssignments create-branch later wired to the helper.)
  • TaskChips.tsx + getTaskCardModel — dedupe TaskCard / KanbanCard chip rendering. Fixed TaskCard’s empty AvatarFallback bug (now shows initials). TaskCard priority pill now uses @/lib/colors (matches the Kanban dots — see Behavior changes worth knowing).
  • DayPlanner / KanbanBoard / TaskDetailSheet / CSVImportWizard moved off direct useSupabase() — new useCreateTaskFromBlock, useBudgetTransactionLookups; KanbanBoard / TaskDetailSheet reuse useToggleTaskStatus. (FilesTab still uses useSupabase() for one imperative createSignedUrl download — doesn’t fit a query hook.)

P2d — colors typed

  • Replaced stringly-typed getStatusBorderColor / getStatusTextColor(domain: string, status: string) with typed getProjectStatusBorderColor / getProjectStatusTextColor / getInvoiceStatusBorderColor / getInvoiceStatusTextColor(status: ProjectStatus | InvoiceStatus)exhaustiveness now compile-checked.
  • Dropped the dead textMaps.tenderPipeline / tenderStatus entries.

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

  1. TaskCard priority pill palette unified with the Kanban dots — now via @/lib/colors. MEDIUM is amber (was copper), HIGH is red-500 (was the status-unpaid token). Flagged for the user as a palette change, not a regression.
  2. TaskCard empty AvatarFallback bug fixed — assignee avatars with no image now render initials instead of an empty circle.
  3. InventoryTab project 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 (CreateButton carries data-create-action for the c shortcut).
  • ErrorBanner — standard mutation/query error banner.
  • CardSkeleton + SkeletonLines — loading placeholders.
  • ResourceTable (+ Column<T>) — generic data table for list pages / project-detail tabs.
  • TaskChips (several Task*Chip exports) + getTaskCardModel / useTaskCardModel — shared task-card chip rendering for TaskCard and KanbanCard.
  • CreateEntityDialogthe standard form-dialog shell. Props: submitLabel, pendingLabel, maxWidth, extraFooterLeft, showPendingSpinner, trigger, description: ReactNode, contentClassName. (Caveat: it wraps children in a <form> — nested buttons need type="button".)

web/src/lib/hooks/

  • createTableQueryWithArgs, createMaybeSingleQuery, createSingleQuery, createMaybeSingleQueryWithArgs — query-hook factories with optional args (e.g. an optional projectId that becomes a .eq()).
  • createRpcMutation — mutation factory for Supabase RPCs.
  • makeStorageAttachmentHooks — factory for use<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 a DayPlanner direct-useSupabase() call).
  • useBudgetTransactionLookups — budget-transaction reference lookups (replaces a CSVImportWizard direct-useSupabase() call).
  • useProjectFiles — project-scoped file list (replaces a FilesTab direct query).
  • supabase-builders.tstableBuilder / callRpc low-level builders shared by the factories.

web/src/lib/

  • on-call-rates.tsbuildRateMaps, entryHours, entryRate (+ existing resolveOnCallRate, now tested).
  • foreign-resource.tscreateForeignResourceWithMappings, deleteForeignResource.
  • matcher.ts — now also exports ForeignResourceRow, IntegrationMappingRow.
  • csv-export.tsdownloadBlob (+ existing CSV helpers).
  • date-utils.tsgetMonthRange, toISODateString (+ existing toLocalDateString).
  • colors.ts — typed getProject/InvoiceStatusBorder/TextColor(status).

userspace/src/lib/

  • project-status.tsstatusColor, 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-roll createClient / 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-bearing cf-access-auth / send-push. Warrants its own reviewed + deployed change.
  • UTC-drift bugsweb/src/lib/hooks/useDayPlanSuggestions.ts (monday.toISOString().slice(0,10)) and useOnCallMonthly.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 was toLocalDateString.) Also tracked in Mobile Audit Follow-Ups (2026-05-09).
  • 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 deletion. Untouched this pass (per explicit instruction not to delete the spec/package — see docs/superpowers/specs/2026-04-26-tender-pipeline-design.md and the 2026-04-27-tender-pipeline-ui-design.md spec).
  • 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-statuses hardcodes a 1.27 HUF VAT multiplier for all currencies (already in Hardcoded VAT Rate in Billingo Sync).
    • useCreateInvoice and useCreateBudgetTransfer are 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.ts uses local TZ where day-plan blocks (stored as 1970-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 the notify deletion above.

Verification

  • tsc -b clean per phase.
  • vitest per 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.
  • 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 TaskCard AvatarFallback fix and the palette unification are behavior changes from this pass)
  • budget - Budget system (several budget hooks/dialogs migrated)
  • timesheet-export - Timesheet export (downloadBlob extraction, downloadTimesheetCsv deletion)
  • kanban - Kanban board (TaskChips dedupe, useSupabase() removal)
  • day-planner - Day planner (useCreateTaskFromBlock, useReorderDayPlanBlocks deleted)