Changelog

0.4.0 — Real-time Calendar & Inbox, Data views tree + settings gears, top-bar Create

Google Calendar and Gmail push notifications land — bookings and new mail toast on every app surface. The Data sidebar gains a full views tree with record counts and per-table / per-view settings gears. Every surface now carries a global Create button and a click-only profile menu. Dial gets a per-lead Tasks tab, and calendar tasks render with their own styling.

What's new

Real-time Calendar and Inbox

  • Google Calendar push notifications. A booking made through a scheduling link, an edit in Google Calendar itself, an assistant scheduling on your behalf — all surface in Project 88 within seconds, on any app surface. A per-calendar watch channel (7-day TTL, renewed <24 h out) forwards Google's push into a new calendar-webhook edge function, which runs the same syncToken delta pull. CalendarProvider subscribes to calendar_events on Supabase Realtime and raises a success toast for new bookings / warning toast for cancellations on any app surface — Calendar, Data, Dial, Inbox, Home. Backfill after a resync can't toast-storm (10-min recency guard); your own edits don't toast you back (60 s self-echo suppression). A 60 s polling fallback covers domain-verification gaps. Migrations 147 and 148.
  • Gmail push notifications. New mail lands in the Inbox and toasts on any app surface without a manual reload. Pipeline: Gmail users.watch → GCP Pub/Sub → gmail-webhook edge function → history.list delta → email_events INSERT → Supabase Realtime → toast + debounced refetch in EmailProvider. Requires a one-time GCP Pub/Sub topic plus GMAIL_PUBSUB_TOPIC and GMAIL_WEBHOOK_SECRET edge secrets — the app no-ops gracefully and keeps polling until they're set. Migration 149. Scope: Gmail only for now — Microsoft accounts keep the existing 60 s poll.

Data app UX overhaul

  • Drag-to-reorder tables in the sidebar. Rows use a dedicated MIME type so table drags can't cross-react with pipeline-card drags on canvases, and are marked nodrag for React Flow.
  • Views tree under the focused table (Obsidian / VSCode-style): every table row carries an always-visible chevron; the focused table expands to All records, every saved view, Archived, and an inline Create view form indented behind a vertical guide. Exactly one row carries the selection highlight at a time.
  • Record counts next to every view name — sidebar tree and the grid header's view dropdown. Computed with the same applyPresetToWidget + applyTableFilters pipeline the grid filters with, so a view's count always equals what clicking it would show.
  • Per-table and per-view settings gears. Table sidebar rows swap their kebab for a gear that opens EditTableSheet directly — the old menu's Duplicate / Export CSV / Export JSON / Delete actions move into the sheet's Settings tab under Actions and Danger Zone. Delete now requires an explicit confirm step (the old menu wiped a table on a single misclick). Every view row (sidebar tree + grid dropdown) gets a hover gear that opens a new ViewSettingsSheet — rename, share-with-org scope toggle, delete-with-confirm — replacing the inline pencil/trash affordances.
  • Hide / unhide columns from the column-header menu. Hidden columns persist via a hidden flag on column metadata; the toolbar's eye-off button lists every hidden column with click-to-unhide and Unhide all.
  • Pinned (sticky) columns pick up row hover / focus / selection tints via new opaque bg-row-* theme tokens, fixing the flat bg-card band that used to sit over otherwise-highlighted rows.
  • Record detail sheet overhaul. Archive / Edit / Close live in the title row (two-click confirm-archive preserved), Tags render as a labeled TAGS section above Linked and Children, and a left-edge grip lets you drag-resize the sheet's width. Compact values truncate to one line with an ellipsis and hover tooltip instead of wrapping. When any dial bucket sources the record's table, a new Phone action deep-links to Dial and displays the lead via search-mode semantics (never enters the queue, never bypasses filters).

Global top-bar controls

  • Create button (right side, primary) on every surface — canvases and native apps. Menu offers Record → alphabetized table picker → AddRecordSheet, or Calendar eventCreateEventSheet seeded with today. Table rows in the record picker carry each table's own icon and follow sidebar order, so a user-reordered sidebar and the picker stay in lockstep.
  • Profile menu is click-only. Hovering the avatar no longer toggles the dropdown; clicking the avatar toggles the menu, and the Profile modal moved from the avatar-click onto the menu's Profile item.

Dial

  • Tasks tab in the Scripts panel — a per-lead peer of Scripts / Calendar / SMS / Email. Lists every task linked to the current lead grouped Overdue / Open / Completed, with a quick composer (title + optional due date / time) whose draft resets on lead advance. Toggling a task syncs the top-bar Tasks pill, the org-wide Tasks widget, and the calendar's task chips via the shared task bus.
  • In-call tag / field edits no longer yank the lead mid-call. When an edit pushes the current lead outside the bucket's filters, useDialQueue carries the pinned row over from the previous page instead of dropping it — bucket-scoped, session- only. The row falls out naturally on the first refetch after the operator dispositions or skips.
  • PitchPrfct SMS: fromNumber auto-resolves. Sending now looks up the account's phone numbers via GET /api/v1/phone-numbers (default-flagged number preferred, else first owned) and caches the answer for 10 min. localStorage['dial.sms.pitchprfct.fromNumber'] is retained as an explicit power-user override. Upstream error messages — opted-out contact, insufficient credits, no sending number — surface on the failed bubble's retry tooltip, so a failed send never lands as an unexplained "Failed" pill.

Calendar — tasks render as tasks

Tasks with a due_at were already unioned into the calendar feed, but rendered as chips indistinguishable from meetings, and date-only tasks silently dropped off the week grid. Migration 149 gives v_calendar_events_with_disposition a source_kind discriminator ('event' | 'task'); the client:

  • Task chips (week grid + Dial's day view) carry a dashed left stripe and a completion checkbox that optimistically drops the chip. Tasks are never draggable — the drag commit path writes calendar_events and would fail on a record_activities id.
  • Date-only tasks render as an AllDayTaskChip in a slim all-day strip between the day headers and the time grid, in the week widget and the Dial Calendar tab.
  • Clicking a task opens its parent record (not the event detail sheet, whose edit / delete would 404 on a task id).

Polish and fixes

  • Overlapping events: stable columns during click. Two events at the same time no longer swap columns mid-click, and clicking one selects the one you clicked. Drag activation is gated on a 4-pixel movement threshold; drag-preview merges happen in place via the shared applyDragPreview helper; column ties in layoutDayEvents break deterministically on event id.
  • Project 88 (internal) calendar checkbox now stays checked. The calendar-fetch reconciler is additive — internal and shared calendars aren't in Google's calendar-list response, so replacing the enabled set on each fetch used to drop them and lock the checkbox off on the next reload. Removal now happens only through user toggles.
  • Dark-mode elevation. Every hardcoded black shadow was invisible on the dark canvas. Elevation is now token-driven (--elevation-widget-*, --elevation-panel, --elevation-popover, --elevation-sheet, …) — light mode keeps its exact shadows, dark mode separates surfaces with borders and lifted surfaces; --widget-surface in dark bumps 2% → 5% white so the two-layer widget card reads again.
  • Cron auth on Supabase. process-calendar-reminders and the new gmail-watch-renewal job read project_url / service_role_key from vault.decrypted_secrets instead of the app.settings.* GUCs that Supabase forbids (ALTER DATABASE … SET app.settings.* is permission-denied) — otherwise every minute silently 401'd. Migration 150. The gmail-watch cron also capability-checks its bearer against auth.admin.listUsers so a project holding the new sb_secret_… key format in env and the legacy service_role JWT in Vault doesn't reject itself.
  • Gmail-webhook token compare. URL ?token= param and the stored GMAIL_WEBHOOK_SECRET are trimmed before comparison — a paste with a trailing newline no longer silently 403s every push.

Under the hood

  • gmail_watch_state — per-integration history_id + watch expiry, service-role only.
  • email_events — user-scoped realtime toast feed, unique per (integration, message), pruned after 30 days.
  • _shared/google-auth.ts — generic Google token helper extracted from google-calendar-sync.ts for reuse in gmail-webhook / gmail-watch.
  • _shared/google-calendar-sync.ts — sync core shared by calendar-sync (scheduled) and calendar-webhook (push).
  • SideSheet — the shared right-sheet chrome (backdrop, 320 px slide-in, Escape / click-away close) extracted from EditTableSheet; both EditTableSheet and the new ViewSettingsSheet mount on it.
  • computeViewCounts — pure function plus a useViewCounts memo hook shared by the medium and large Data widgets; LargeWidget publishes counts through the existing onViewStateChange bridge so the sidebar tree gets them for free.
  • reconcileEnabledCalendarIds — pure helper that merges a fetched calendar-slice into the enabled set additively (never replaces).
  • applyDragPreview + layoutDayEvents id-tiebreak — merges the drag preview in place instead of filter-and-append, and breaks perfect start/end ties by event id.

What's next

  • Microsoft Graph webhooks for parity with Gmail push on Outlook accounts.
  • Follow-up server-side search RPC + lazy record fetch to retire the last ensureRowsLoaded path in Dial header-search and off-queue record viewing.
  • Per-calendar defaults on top of the user-level appointment length preference.

See Calendar → Real-time sync, Inbox → Real-time inbox, Data → Layout, Data → Saved views, Data → Record detail sheet, Home → Global top bar, Dial → Tasks tab, Calendar → Tasks on the calendar, Gmail → Real-time inbox, Google Calendar → Real-time push, and PitchPrfct → fromNumber resolution.

On this page