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
watchchannel (7-day TTL, renewed<24 hout) forwards Google's push into a newcalendar-webhookedge function, which runs the samesyncTokendelta pull.CalendarProvidersubscribes tocalendar_eventson 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. Migrations147and148. - 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-webhookedge function →history.listdelta →email_eventsINSERT → Supabase Realtime → toast + debounced refetch inEmailProvider. Requires a one-time GCP Pub/Sub topic plusGMAIL_PUBSUB_TOPICandGMAIL_WEBHOOK_SECRETedge secrets — the app no-ops gracefully and keeps polling until they're set. Migration149. 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
nodragfor 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+applyTableFilterspipeline 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
EditTableSheetdirectly — 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 newViewSettingsSheet— 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
hiddenflag 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 flatbg-cardband 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 event →CreateEventSheetseeded 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,
useDialQueuecarries 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:
fromNumberauto-resolves. Sending now looks up the account's phone numbers viaGET /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_eventsand would fail on arecord_activitiesid. - Date-only tasks render as an
AllDayTaskChipin 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
applyDragPreviewhelper; column ties inlayoutDayEventsbreak 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-surfacein dark bumps 2% → 5% white so the two-layer widget card reads again. - Cron auth on Supabase.
process-calendar-remindersand the newgmail-watch-renewaljob readproject_url/service_role_keyfromvault.decrypted_secretsinstead of theapp.settings.*GUCs that Supabase forbids (ALTER DATABASE … SET app.settings.*is permission-denied) — otherwise every minute silently 401'd. Migration150. Thegmail-watchcron also capability-checks its bearer againstauth.admin.listUsersso a project holding the newsb_secret_…key format in env and the legacyservice_roleJWT in Vault doesn't reject itself. - Gmail-webhook token compare. URL
?token=param and the storedGMAIL_WEBHOOK_SECRETare trimmed before comparison — a paste with a trailing newline no longer silently 403s every push.
Under the hood
gmail_watch_state— per-integrationhistory_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 fromgoogle-calendar-sync.tsfor reuse ingmail-webhook/gmail-watch._shared/google-calendar-sync.ts— sync core shared bycalendar-sync(scheduled) andcalendar-webhook(push).SideSheet— the shared right-sheet chrome (backdrop, 320 px slide-in, Escape / click-away close) extracted fromEditTableSheet; bothEditTableSheetand the newViewSettingsSheetmount on it.computeViewCounts— pure function plus auseViewCountsmemo hook shared by the medium and large Data widgets;LargeWidgetpublishes counts through the existingonViewStateChangebridge 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+layoutDayEventsid-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
ensureRowsLoadedpath 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.
0.5.0 — Automations goes native and runs for real, dialer buckets speak the view language, records notify the room
Automations lands as a first-class native app with a real runtime — webhook receiver, schedule dispatcher, DB-triggered event outbox, server-side executor, per-run history, and validation-gated publish. Dialer buckets stop carrying their own filter dialect and instead source their filters and priority from a linked saved table view. Record creation now toasts the org, the record sheet gains a Tasks tab with live count badges, and views become drag-to-reorder with filter/sort editing right in their settings.
0.3.1 — Full-height sidebars for Data/Calendar/Dial, default appointment length, snap-to-create
The Data, Calendar, and Dial native surfaces share one layout — a full-height left sidebar with the app's toolbar on the darker top bar over the right column. Calendar gains a default appointment length setting and a click-to-create ghost preview that snaps to 15-min and centers on the pointer.