Changelog

0.2.2 — Server-side dial queue, authoritative leasing, sensitive Lead Details

The dial queue is now computed in Postgres with keyset pagination, two operators on the same bucket can no longer double-dial, and sensitive Lead Details fields reveal on click with an audit trail.

What's new

  • The dial queue runs in Postgres. The dialer no longer bulk-loads every row in the source table to apply filters / cap / cooldown / sort in the browser. The bucket's filter tree compiles to a SQL predicate and dialer_next_leads returns a keyset-paginated page of candidates. Buckets that were sluggish on big tables — or that just burned tab memory on the dial loop — now scale.
  • Two operators on the same bucket can no longer double-dial. Authoritative per-(bucket, row) leases live in dialer_lead_leases. Dialing claims the lease; advancing or dispositioning releases it; a one-minute heartbeat covers the active lead. dialer_next_leads excludes teammates' active leases from every page, so a leased row never appears in another operator's queue. The race window that the old presence soft-lock could only narrow to sub-second is now closed at the database.
  • Sensitive Lead Details fields reveal on click — with an audit log. Columns marked sensitive in the data-table schema now render masked in the dial Lead Details panel with an Eye / EyeOff toggle. Clicking the eye calls the audited user_table_row_reveal_sensitive RPC to decrypt that one value; double-clicking after reveal opens the inline editor as normal. Advancing to the next lead re-masks every sensitive field automatically — nothing decrypted leaks across leads.
  • Date pickers track their field. The shared DatePicker now measures its popover's real height and anchors flush upward, and repositions on scroll / resize so the calendar follows its trigger. The dialer's dob field that used to mis-position is the most visible beneficiary; the same fix applies to every DatePicker instance (data-filter menu, bucket builder, etc.).

Under the hood

Server (all SECURITY INVOKER, RLS-enforced, no new advisor warnings):

  • Migration 142app_hidden.dialer_filter_predicate. Injection- safe SQL mirror of the JS matchesFilter, built with format(%L).
  • Migrations 143 / 146dialer_next_leads. Filter / sort / cap / cooldown / archived-exclusion / teammates'-lease-exclusion + keyset pagination; include_row_ids carve-out for session pins; returns data_enc so the client can mask sensitive fields without a second round-trip.
  • Migration 144dialer_lead_leases + dialer_claim_lead / dialer_release_lead. Per-(bucket, row) lease with a TTL; conditional upsert ⇒ exactly one winner under concurrency.
  • Migration 145dialer_queue_stats. matching_count + capped calls_today for the header daily-progress bar.

Client:

  • New useDialQueue hook — keyset cursor, fetch-more, optimistic patchRow (data + data_enc).
  • DialWidget now drives off useDialQueue. Schedule, eligibility, and active-event stay JS post-filters over the page. Presence is retained for the live "Also dialing: ‹name›" chip only — state is held in the lease table, not in presence.
  • DialLeadDetailsPanel adds reveal / re-mask wiring; reveal-then-edit routes through the inline editors that already existed.
  • DatePicker adds the usePopoverPosition hook for height measurement, flush anchoring, and scroll / resize tracking.

What's next

  • Server-side header-search RPC + lazy record fetch, retiring the bulk ensureRowsLoaded that header search and the off-queue record viewer still rely on.
  • JS ↔ SQL filter-parity fixtures, pgTAP for the new RPCs, a two-session contention test, and CI wiring on PRs.

See Dial → Server-side queue and authoritative leasing.

On this page