Built in public · Changelog
How Supernova learned the job.
Product panorama
One deal, one continuous operating loop.
Seventeen agents live in the product today, two squads (7 sales-side, 10 delivery-side), propose-only, a named human at every gate. Everything below ships; nothing is a mock.
Sell
Daily Brief, Email Drafter, Call Notes, Deal Check, Meeting Prep, Promise Tracker and Enrich run the deal desk, evidence attached.
Spec
A won deal births the project; Persona Researcher and Spec Writer read the real corpus and draft the use-case tree, every requirement quoting its source.
Model
Data Modeler, Role Definer and Page Architect ratchet ontology, roles and the page atlas forward, one approved artifact feeding the next.
Design
Designer codifies the design language; Mock Builder renders one mock per page wearing the approved tokens.
Run
E2E Writer specs the tests; Tech Stack Advisor and Impact Analyst guard the plan; delivery runs five-state stories, bugs, QA gates and release exit criteria.
Audit
Every run is a session you can open: model calls, tool calls, cost, the audit trail behind every artifact.
18
Jul 2026
Saturday
1 shipped
Data21:09 ISTThe Zoho walk learns to turn the page properly
The hourly cron started failing with DISCRETE_PAGINATION_LIMIT_EXCEEDED on /crm/v8/Tasks: Zoho v8 caps discrete page pagination at 2000 records (page 11 × 200 does not exist no matter how…
The Zoho walk learns to turn the page properly
The hourly cron started failing with DISCRETE_PAGINATION_LIMIT_EXCEEDED on /crm/v8/Tasks: Zoho v8 caps discrete page pagination at 2000 records (page 11 × 200 does not exist no matter how…
The hourly cron started failing with DISCRETE_PAGINATION_LIMIT_EXCEEDED on /crm/v8/Tasks: Zoho v8 caps discrete page pagination at 2000 records (page 11 × 200 does not exist no matter how many tasks do). The REST walk now switches to cursor flow the moment Zoho hands back info.next_page_token, and when a window somehow exhausts 2000 records without ever offering a token it stops that walk and says so in the sync log instead of asking for page 11 and dying. One TypeScript wrinkle: the params object fed a generic inferred from itself (TS7022 circular inference) - an explicit Record<string, string | number> settles it.
16
Jul 2026
Thursday
1 shipped
Data09:10 ISTProspecting reaches the finish line: real Zoho writes, Apollo contacts, refine loop
The push is real (src/lib/prospecting/push.ts): "Save to Zoho CRM" now creates Accounts, Leads, and Contacts via the same zohoPost/zohoPut client Enrich writes with. A new company becomes…
Prospecting reaches the finish line: real Zoho writes, Apollo contacts, refine loop
The push is real (src/lib/prospecting/push.ts): "Save to Zoho CRM" now creates Accounts, Leads, and Contacts via the same zohoPost/zohoPut client Enrich writes with. A new company becomes…
- The push is real (
src/lib/prospecting/push.ts): "Save to Zoho CRM" now creates Accounts, Leads, and Contacts via the samezohoPost/zohoPutclient Enrich writes with. A new company becomes Account + Lead — the Lead owned by the pushing SDR (viazoho_user_mappings) so it lands in their book, its Description carrying the match reason + buying-signal verdict. Every create is mirrored into the staging tables immediately (best-effort, sync repairs gaps) so the lead book doesn't wait for the next sync. AuditedPROSPECTS_PUSHED, high-signal, outcome SUCCESS/PARTIAL/FAILURE; per-company outcomes land in the result screen and thepushedjsonb. - Dupes flipped from wall to doorway. The dedupe verdict used to LOCK a row; now it routes the push: existing ACCOUNT -> new decision-makers attach under it; existing LEAD -> fill-only field update (industry/employees/website written only where Zoho is empty — research never overwrites a human). Re-pushing an already-PUSHED row is a no-op, so a double-click can't double-create.
- Contact dedupe: each contact's email is checked against
zoho_contactsANDzoho_leadsbefore create; hits are skipped and counted ("3 already in CRM"). No email = create — name-matching a 170k-row mirror invents false merges, which are worse than dupes. - Apollo integration (
src/lib/prospecting/apollo.ts): decision-maker search (CEO/CTO/COO/Founder/VP-Eng titles) behind a new encryptedAPOLLO_API_KEYon/admin/settings. Verified emails + LinkedIn merge over the research agent's people by name; the agent roster remains the no-key fallback with emails honestly empty. Apollo's locked-email placeholder is treated as "no email", never stored. Apollo failure never kills an enrich run. - Refine loop: a "Find more" bar on the companies step — extra context ("only Bangalore", "500+ employees") rides WITH the original ICP, the agent is told what it already found, and a JS-side name/domain dedupe guarantees appends-only. Searches now audit as
PROSPECT_SEARCH_RUN(was mislabelledLEAD_NOTE_ADDED). - Verified without side effects: tsc + lint clean; every SQL statement in push.ts (mirrors, fill-only update, contact dedupe) executed against the real Azure schema inside a rolled-back transaction. The Zoho API call itself is deliberately un-fired — the first live push should be a one-company test from an SDR session.
15
Jul 2026
Wednesday
2 shipped
Data15:34 ISTProspecting workspace (dynamic), buying-signal scoring, audit surface, manual sync
Prospecting (/sdr/prospecting) — a real, DB-backed workspace: an ICP in plain English -> a web-search agent finds real companies (src/lib/prospecting/search.ts, reuses the Anthropic…
Prospecting workspace (dynamic), buying-signal scoring, audit surface, manual sync
Prospecting (/sdr/prospecting) — a real, DB-backed workspace: an ICP in plain English -> a web-search agent finds real companies (src/lib/prospecting/search.ts, reuses the Anthropic…
- Prospecting (`/sdr/prospecting`) — a real, DB-backed workspace: an ICP in plain English -> a web-search agent finds real companies (
src/lib/prospecting/search.ts, reuses the Anthropic web_search harness) -> dedupe against the Zoho staging tables (dedupe.ts, website/name match; verified 8/9 already-in-CRM live) -> store the keepers -> enrich each via the AI research agent -> push to Zoho (dry-run for now, no CRM writes). Four-step wizard (prospecting-wizard.tsx) with per-step selection gates, sticky action bars, and the entity colour language. New tables:prospecting_searches,prospect_companies,prospect_contacts,prospecting_preferences; new server actions insrc/app/sdr/prospecting/actions.ts. Tested end-to-end in Chrome. - Buying signals (per SDR) — a rep sets their own buying signals in
/sdr/settings(prospecting_preferences.buying_signals, audited). On enrich, a second scoring pass (score.ts,SignalScorer) reasons over the research dossier + the rep's signals and returns an evidence-linked verdict (verdict/score/opinion/matched[{signal,evidence}]/missing), stored onprospect_companies.buying_signal_scoreand shown in the enrich card. Verified live: OpenObserve scored Strong 85/100 with per-signal evidence. - Audit trail read surface (
/admin/audit) — stopped dumping raw JSON. Each event is now an English sentence (per-action templates inevent-view.tsx, defensive over the varying payloads; leads with the storedsummarywhere present, diffs before/after where not), with an icon + colour per category from the entity palette, day-grouped with signal hierarchy (high-signal = filled tile, sign-ins = quiet ledger). Expand shows a readable field diff + a labelled request-context grid (raw JSON one click away).::1->localhost. - Manual "Sync now" on
/sdr(force-sync-button.tsx+src/lib/sync/actions.ts): on-demand Zoho incremental sync then both Outlook (MS Graph) mailbox deltas, guarded against overlap, audited. The scheduled counterpart is the cron. - Toasts moved top-right (
ui/sonner.tsx) so they never cover a bottom action bar. - Local `.env` parity — added the Anthropic / Proshort / MS Graph keys locally (were only on the VM), so dev has the full agent + sync surface.
Data00:34 ISTEnrich closes the loop: the FIRST live Zoho write-back, plus company channels
Two upgrades the same day, from the user's asks ("a way to update Zoho with it"; "email phone linkedin needs to be figured out"):
Enrich closes the loop: the FIRST live Zoho write-back, plus company channels
Two upgrades the same day, from the user's asks ("a way to update Zoho with it"; "email phone linkedin needs to be figured out"):
Two upgrades the same day, from the user's asks ("a way to update Zoho with it"; "email phone linkedin needs to be figured out"):
- Zoho write-back, live: the dossier's proposed field updates now carry an Update Zoho button.
applyZohoUpdates(src/lib/enrichment/actions.ts) PUTs the fields to Zoho v8 (the first real Zoho write in the product —zohoPuthad never been used), then updates our mirror, clears the applied proposals from the stored dossier, and auditsZOHO_FIELDS_UPDATEDwith full before/after. Field map is deliberate: leads get Industry / No_of_Employees / Website / City / State / Country; accounts add Phone + billing address fields. A lead's Email/Phone/LinkedIn are the person's — company research never writes them. Verified live on the ANSCER lead: 6 empty fields (industry, employees, website, city, state, country) filled in Zoho + mirror, audit rowbeforeall null /afterall researched values, proposals section cleared itself. - Company channels in the dossier:
companygained website / linkedin (company page URL, validated) / email / phone, rendered as actionable chips (site ↗, LinkedIn ↗, mailto:, tel:) under the identity row. Panel redesigned: ICP read as a banded row (emerald/amber/zinc wash by score), news as a mono-dated timeline, key people with initial avatars, updates as field-shaped diff rows ("empty → Bengaluru"). The model prompt now demands field-shaped values (city = the city name alone) and channels it actually found, never guessed. - Fix en route: drizzle expands a JS array param into N placeholders, so
= any(${arr}::text[])broke —string_to_arrayon a joined param instead.
14
Jul 2026
Tuesday
3 shipped
Data19:38 ISTTech lead, from messy Zoho text to a resolved person on every deal
Zoho's Tech_Lead_Name_Involved is free text and a mess: first names, spelling variants (Radhakant / Radhakanth), multi-person strings ("Arka and Gorish"), and notes ("NA", "Through Prem,…
Tech lead, from messy Zoho text to a resolved person on every deal
Zoho's Tech_Lead_Name_Involved is free text and a mess: first names, spelling variants (Radhakant / Radhakanth), multi-person strings ("Arka and Gorish"), and notes ("NA", "Through Prem,…
Zoho's Tech_Lead_Name_Involved is free text and a mess: first names, spelling variants (Radhakant / Radhakanth), multi-person strings ("Arka and Gorish"), and notes ("NA", "Through Prem, hence no tech lead"). 205 deals tagged, 71 distinct values, only 14 linking to an app user. Fixed it as a resolution layer, not a blind rewrite:
- `tech_lead_map` (new table + Drizzle def): one row per distinct raw value, mapping it to a
clean_name(the canonical spelling we write back to Zoho) and, where the person is in the system, auser_id. Seeded byscripts/tech-lead/resolve.mjs, which auto-resolved 36 names / 143 of 205 deals (70%) by first-name match (TECH_LEAD/DEV preferred, spelling variants folded). - `/admin/tech-leads` review surface: the residue (unmatched names, multi-person, notes) is a human's call, so the page lists every value by status with a one-click suggestion, a teammate picker, a "just a name" option, "no tech lead", and an inline add-a-teammate flow for people not yet in the system (Arka = 24 deals, Divyansh, Sudhanshu…). Every resolution is audited (
TECH_LEAD_RESOLVED) and re-arms the Zoho write-back. - Shown everywhere a deal is (user: "super super important"): one
TECH_LEAD_VALUESQL expression resolves the display name (clean when mapped, raw until reviewed, null when cleared), threaded through the deal hub (clickable to the teammate), pipeline kanban cards, the Desk slipping list + deal hover-card, the Accounts table top-deal, the Inbox deal threads, the Follow-ups list, account + contact hubs, the team page, and the deal peek pane, all via the sharedDEAL_SELECT/TechLeadChip/ deal-row components. - Write-back of the cleaned names to the Zoho source is next (write scope now granted); the map is the reviewed outbox.
Data14:38 ISTRecording page v4: chat transcript, deadline chips, "not in your CRM" pulse + Add to CRM
Four upgrades on top of the tabbed rebuild, plus three new design principles:
Recording page v4: chat transcript, deadline chips, "not in your CRM" pulse + Add to CRM
Four upgrades on top of the tabbed rebuild, plus three new design principles:
Four upgrades on top of the tabbed rebuild, plus three new design principles:
- Chat transcript — buyer left (white cards) / seller right (clay wash), full-width, with the buyer/seller labels anchoring each side. The join is
proshort_recording_parties.speaker_name→ affiliation. You can read the negotiation's rhythm at a glance. - Parser coverage — action items without an "(Org)" ("Mahak - Share the framework… - No deadline specified") now parse: owner matched to the call's external parties for the right avatar tint, and the trailing deadline becomes a chip (amber when real, muted for "no deadline"). Checked: there is NO literal markdown (
**/##) anywhere in the 548 recordings — the "raw text" smell was unparsed structure, not un-rendered markdown. - "Not in your CRM" pulse + action —
getRecordingPartiesnow carriescrmKind(contact | lead | null by email). An external participant who exists nowhere in Zoho gets an attention Pulse + an Add to CRM button →proposeContactserver action logsCONTACT_ADD_PROPOSED(PREVIEW) with the full proposed record to the audit spine; button flips to "✓ proposed" + toast. Captured externals show a Lead/Contact chip. - Tabs look like tabs — the
linevariant: bold active label + clay underline.
DESIGN.md principles 13–15 (from this session's gaps): 13 Every action is acknowledged (toast naming what happened + what happens next, or the control flips to its done-state — never silence); 14 The text color scale (ink → body /85 → meta muted → mono eyebrow; tabular-nums; one clay number per surface; never bold whole sentences); 15 Parse structure out of strings (owner/title/deadline embedded in a string is structure wearing a costume — split and typeset it, never dump the bullet).
Data10:47 ISTCorporate-domain email matcher (#13): the whole company thread, not one contact
A user question — "why am I not seeing the latest emails on this deal?" — turned out to be two things. First, a diagnosis that cleared the air: nothing was stale. Both mirrors are current…
Corporate-domain email matcher (#13): the whole company thread, not one contact
A user question — "why am I not seeing the latest emails on this deal?" — turned out to be two things. First, a diagnosis that cleared the air: nothing was stale. Both mirrors are current…
A user question — "why am I not seeing the latest emails on this deal?" — turned out to be two things. First, a diagnosis that cleared the air: nothing was stale. Both mirrors are current (Zoho to 07-13/14, Graph email to today), and on the TruBoard Cleantech deal Jul 10 genuinely IS the newest email — confirmed by scanning the whole mirror for any TruBoard mail after it (zero). Anisha's mailbox is synced (anisha@antino.io — .io not .com; her login/CRM identity vs her mailbox differ, bridged by the mapping).
But the diagnosis surfaced the real limitation: the deal's email thread matched only its mapped Zoho contacts — here a single contact, S. Kadhirvel — while the person actually driving the thread, Sohail Usmani (@truboardpartners.com), isn't a Zoho contact at all. His mail showed only incidentally (he cc'd Kadhirvel); drop that cc and it'd vanish. The whole TruBoard account has a team in Zoho (Srickant, Vipul, Rakesh…), none attached to this deal.
The fix (#13): match a deal's emails by the company domain, not just its contacts. src/lib/graph/match.ts — a reusable matcher: corporateDomains() (derive domains from the deal + account contacts, dropping a personal/internal exclusion list — gmail/yahoo/… and antino.com/antino.io/goldenflitch.com, so we never over-match free providers or internal mail) + matchedMessageIds(emails, domains) (a UNION of four branches: from-email, from-domain, recipient-email, recipient-domain). Wired into getDealTimeline (the thread — full 4-way match) and getDealInbox (the left-pane ranking — exact-email + from-domain only). TruBoard now surfaces 10 of Sohail's emails directly, plus the rest of the account's conversation.
The performance was the hard part (the whole reason it's worth logging): the natural shapes are traps. A top-level OR … EXISTS predicate → 8,305 ms (seq-scans all 170k messages). IN (select … from cte) → 1,078 ms (a CTE defeats the functional index — Postgres hash-semi-joins with a full seq scan; confirmed via EXPLAIN). The fix: pass emails/domains as array parameters and use = any($arr) in a UNION of index-backed branches — 29 ms. Added one index (idx_gmr_domain_lower on lower(split_part(email,'@',2)) for the recipient side) to scripts/perf/add-indexes.mjs. The inbox ranking stayed lean (exact-email + from-domain, recipient-domain dropped — it cost +480ms for barely any ranking change): 162 ms → 54 ms. Lesson banked: never match email sets against a CTE with `IN`/`EXISTS`; materialize to an array and `= any` the functional index. Second gotcha (caught in the browser after the first commit): ` sql${jsArray}::text[] renders as a **row constructor** ($1,$2,…)::text[] — a cast error — because drizzle expands a JS array into a paren-list. Build the literal explicitly: sqlarray[${sql.join(vals.map(v => sql${v}), sql, )}]::text[] → array[$1,$2,…]::text[]. Verified via PgDialect().sqlToQuery + a live getDealTimeline run, not just a raw-pg` scratch (which had hidden the bug — raw pg accepts a single array param, drizzle doesn't render one).
13
Jul 2026
Monday
2 shipped
Data19:34 IST#17: Proshort tables in the Drizzle schema (db:push no longer drops them)
src/db/proshort-schema.ts already mirrored the 5 Proshort tables — but it was never registered in drizzle.config.ts, so drizzle-kit didn't read it and db:push still generated DROP…
#17: Proshort tables in the Drizzle schema (db:push no longer drops them)
src/db/proshort-schema.ts already mirrored the 5 Proshort tables — but it was never registered in drizzle.config.ts, so drizzle-kit didn't read it and db:push still generated DROP…
src/db/proshort-schema.ts already mirrored the 5 Proshort tables — but it was never registered in `drizzle.config.ts`, so drizzle-kit didn't read it and db:push still generated DROP statements for proshort_* (525 recordings + 22k insights = data loss). Fix: added it to the config's schema array, plus the one missing index def (proshort_parties_email_idx on lower(email)) so the schema matches the live DB exactly (columns, PKs, indexes all verified against information_schema). Did not run push to "prove" it — that's the hazard; the fix is that drizzle-kit now knows the tables. Updated the memory (never-drizzle-push → "cautious": the drop hazard is resolved, but push still applies all diffs at once, so prefer targeted ALTER TABLE) + DATA-REALITY.md.
Data07:23 ISTBreadcrumbs show entity names, not zoho PKs
The breadcrumb rendered the raw id for [id] routes (e.g. AE / Deals / 4350976000054900540) because AppBreadcrumbs is a client component with no way to resolve a name. Fixed with a tiny…
Breadcrumbs show entity names, not zoho PKs
The breadcrumb rendered the raw id for [id] routes (e.g. AE / Deals / 4350976000054900540) because AppBreadcrumbs is a client component with no way to resolve a name. Fixed with a tiny…
The breadcrumb rendered the raw id for [id] routes (e.g. AE / Deals / 4350976000054900540) because AppBreadcrumbs is a client component with no way to resolve a name. Fixed with a tiny context (src/components/breadcrumb-title.tsx): BreadcrumbTitleProvider wraps the shell content, and each detail page renders an invisible <BreadcrumbTitle name={entity.name} /> that registers its name via effect; AppBreadcrumbs reads it and swaps the final crumb's label. Wired into all 9 detail pages (deal, account, contact, teammate, email, recording, meeting-event, AE + SDR lead). Now it reads AE / Deals / Defect Detection on Steel Sheets.
12
Jul 2026
Sunday
4 shipped
Data18:18 ISTZoho coverage: audit + field fill (Next_Step, Mobile, Reports_To…)
Audited whether the Zoho mirror pulls everything (a subagent, task #15). Finding: coverage is selective by construction — src/lib/zoho/manifest.json is a hand-picked field list, no raw…
Zoho coverage: audit + field fill (Next_Step, Mobile, Reports_To…)
Audited whether the Zoho mirror pulls everything (a subagent, task #15). Finding: coverage is selective by construction — src/lib/zoho/manifest.json is a hand-picked field list, no raw…
Audited whether the Zoho mirror pulls everything (a subagent, task #15). Finding: coverage is selective by construction — src/lib/zoho/manifest.json is a hand-picked field list, no raw overflow (deliberate "fully-typed 1:1 mirror"). The forecasting spine is dense (probability/forecast/amount/close/stage all ~100%), but several workflow fields are structurally absent. Filled the real gaps (task #16): added zoho_deals.next_step + expected_revenue, zoho_contacts.reports_to + department, zoho_leads.mobile to the schema + manifest; the self-healing importer populates them on the next sync. Discovered a live hazard: drizzle-kit push wants to DROP the Proshort tables (525 recordings + 22k insights — they live outside the Drizzle schema, created by scripts/proshort/). The no-TTY guard blocked it; applied the columns via surgical ALTER TABLE ... ADD COLUMN IF NOT EXISTS instead. Recorded the never-push rule (memory + docs/DATA-REALITY.md, ported from antino3 with live 2026-07-12 fill rates — most account attributes are <2% filled, converted_s is 0%). Remaining #16: mirror CRM Attachments + deal line items (bigger builds); the raw jsonb idea is deferred — it contradicts the deliberate no-jsonb schema convention (flagged for a decision). Follow-up task #17: add Proshort tables to the Drizzle schema so push is safe.
Data16:56 ISTLead timeline: Zoho tasks (who_id is a dead link for leads)
The lead timeline queried tasks by who_id=leadId — but a DB check shows Zoho task who_id points at contacts (539,625) and never leads (0), so that block returned nothing on every lead.…
Lead timeline: Zoho tasks (who_id is a dead link for leads)
The lead timeline queried tasks by who_id=leadId — but a DB check shows Zoho task who_id points at contacts (539,625) and never leads (0), so that block returned nothing on every lead.…
The lead timeline queried tasks by who_id=leadId — but a DB check shows Zoho task who_id points at contacts (539,625) and never leads (0), so that block returned nothing on every lead. Leads are linked to their follow-ups by the subject convention instead ("Followup N with Lead {FirstName} from {Company}"), the same match getLeadTasks uses. Rewired getLeadTimeline's task block to match by the lead's owner + company + first name across all statuses (so the timeline shows the full done+pending chain, not just open). Added ownerId to LeadFull/getLead to scope it to the lead's actual owner (works for read-only cross-rep views too). Verified as Himanshu on Ishika's Collinson lead (read-only): "Followup 1/2 with Lead Saurabh from Collinson" now render in the timeline, while the owner-scoped "Open follow-ups" rail stays empty (correctly — they're not his to action). Both SDR and AE lead pages inherit this.
Data16:49 ISTDeal timeline: real-calendar meetings (the missing category)
The deal 360 was missing meetings. Grounded it in the data: for Himanshu's Tata Steel deal, zoho_events (CRM events, what_id) = 0, but the real Outlook calendar (graph_events) has meetings…
Deal timeline: real-calendar meetings (the missing category)
The deal 360 was missing meetings. Grounded it in the data: for Himanshu's Tata Steel deal, zoho_events (CRM events, what_id) = 0, but the real Outlook calendar (graph_events) has meetings…
The deal 360 was missing meetings. Grounded it in the data: for Himanshu's Tata Steel deal, zoho_events (CRM events, what_id) = 0, but the real Outlook calendar (graph_events) has meetings with the buying committee — the deal timeline only queried zoho_events, so they never showed. (Org-wide only 197 CRM events link via what_id at all; the real meeting record lives in graph_events.) Added a graph_events block to getDealTimeline, matched to the same buying-committee email set the email block uses (primary contact + contact-roles), deduped across mailboxes. Now the deal timeline is genuinely unified — CRM tasks/notes/calls + committee emails + real-calendar meetings + Proshort calls. Verified as Himanshu on Tata Steel: "TATA Steel <> Antino" meetings render (teal calendar icon, "· with the buying committee") interleaved with the MOM email, follow-up tasks, and the Proshort discovery call. Subtitle updated to "CRM + email + meetings + calls, unified". (Lead timeline already carried all three — tasks via who_id, meetings via graph_events, emails exact/domain.)
Data10:40 ISTFollow-up chains, unified calls (Proshort), user-scoped timeline
Follow-ups grouped into chains. DB study confirmed the shape: 763 leads carry a "Followup 1 → Followup 2" chain (1636/1639 tasks resolve to a lead). So /sdr/tasks now groups by lead — one…
Follow-up chains, unified calls (Proshort), user-scoped timeline
Follow-ups grouped into chains. DB study confirmed the shape: 763 leads carry a "Followup 1 → Followup 2" chain (1636/1639 tasks resolve to a lead). So /sdr/tasks now groups by lead — one…
- Follow-ups grouped into chains. DB study confirmed the shape: 763 leads carry a "Followup 1 → Followup 2" chain (1636/1639 tasks resolve to a lead). So
/sdr/tasksnow groups by lead — one card per lead with its chain and a Clear chain action — instead of a flat list. Ordered by lead recency (getTasksnow returnsleadRecency, cap lifted 300→2000 so the full 1,639 shows across 875 leads). - Proshort integration (conversation intelligence) — the call layer. New
proshort_*staging tables (scripts/proshort/create-tables.mjs, typed insrc/db/proshort-schema.ts) + backfill (scripts/proshort/backfill.mjs, v3 list→detail, throttled). Recorded calls (recap + overview: pain points, action items, risks, competitors, decision process) now surface ascallitems in the lead timeline. Seedocs/PROSHORT.md. Key in.env(rotate it). - User-scoped timeline (privacy).
getLeadTimeline(lead, userEmail)now scopes meetings to the signed-in SDR's own mailbox and Proshort calls to recordings they were a party on — a user sees only their meetings/calls, never everyone's. Emails + meetings + calls interleave by date; the Inbox/lead labels count each. - Threaded
user.emailthrough the timeline callers (inbox, lead detail) and the Writer draft path.
11
Jul 2026
Saturday
1 shipped
Data09:10 ISTFirst metrics dashboards on live Zoho data (docs catch-up)
Shipped inside a7fe7fe alongside the pool-retry work, documented here: /admin/command (company-wide command board: ₹-pipeline/lead/deal stat tiles, pipeline by stage, lead-status breakdown,…
First metrics dashboards on live Zoho data (docs catch-up)
Shipped inside a7fe7fe alongside the pool-retry work, documented here: /admin/command (company-wide command board: ₹-pipeline/lead/deal stat tiles, pipeline by stage, lead-status breakdown,…
- Shipped inside
a7fe7fealongside the pool-retry work, documented here: `/admin/command` (company-wide command board: ₹-pipeline/lead/deal stat tiles, pipeline by stage, lead-status breakdown, 12-week activity, top owners) and `/ae/team` (the same lens scoped to the signed-in AE's reporting subtree with per-member performance). - Foundations:
src/lib/metrics/queries.ts(SQL over thezoho_*staging layer joined to our org tree) +format.ts(₹ INR + count formatting), and dashboard primitivesStatTile/ChartCard/CategoryBarChart/WeeklyActivityChart— all charts on the entity color tokens, fixed order, per the color language.
10
Jul 2026
Friday
1 shipped
Data07:59 ISTMicrosoft Graph pipeline: multi-tenant email + calendar + attachment bytes
New graph_ staging layer (src/db/graph-schema.ts) — fully-typed, jsonb-free mirror of M365 email + calendar. Tenant-aware: every table keyed by an org discriminator (ANTINO | GOLDENFLITH),…
Microsoft Graph pipeline: multi-tenant email + calendar + attachment bytes
New graph_ staging layer (src/db/graph-schema.ts) — fully-typed, jsonb-free mirror of M365 email + calendar. Tenant-aware: every table keyed by an org discriminator (ANTINO | GOLDENFLITH),…
- *New `graph_
staging layer** (src/db/graph-schema.ts) — fully-typed, jsonb-free mirror of M365 email + calendar. **Tenant-aware**: every table keyed by anorgdiscriminator (ANTINO|GOLDENFLITH), because Antino and Goldenflith are separate Microsoft tenants with separate app registrations. Creds per-org env vars;graph_tenants` mirrors them (AES-encrypted secret) for a future admin UI. - Attachment bytes live in Postgres (
graph_message_attachments.contentbytea) — the explicit requirement. Inline path for ≤3 MB,/$valuefetch for larger; verified byte-perfect (a 25 MB PDF stored intact,%PDF…%%EOF). One heavy mailbox ≈ 900 MB of attachments. - Engine (
src/lib/graph/importer.ts+client.ts, CLIscripts/graph/run.ts): client-credentials auth per tenant; full pull (delta-primed) + every-15-min delta via storeddeltaLink; messages (body, recipients, threading), events (attendees), attachments. Hardened against transient Azure Postgres/network errors — per-mailbox retry-then-skip, poolkeepAlive+ timeouts, so one blip can't kill a multi-hour pull. Added a targetedmailbox <emails>command for re-syncing specific inboxes. - Access model:
User.Read.Alllets us enumerate the directory, but mail read is gated by an Exchange application access policy scoped to the sales team per tenant. Inaccessible + hard-excluded (CEO/leadership:vinay@,ceo@goldenflitch.com) mailboxes are marked and skipped, never fatal. - Antino: app access policy grants the sales team. 13 sales mailboxes pulling (Anisha, Nitish + the 10 SDR/closers, plus Harshad/Ishika/Priyadarshini once added to the policy mid-session). Vedant excluded (disabled account); Dhruv Raj + Abhishek Rana turned out to be Goldenflith, not Antino (their org-chart placeholder
@antino.comemails were wrong). - Goldenflith: separate tenant; seeded from its own directory (53
goldenflitch.commailboxes), access policy gates to the sales set. Pulling now. - 15-min delta cron added to
src/instrumentation.tsalongside the hourly Zoho sync — per-tenant, skips an org until its FULL pull completes, guards overlaps. - Creds for both tenants in
.env(gitignored).
08
Jul 2026
Wednesday
1 shipped
Data09:10 ISTZoho pipeline: full CRM in Postgres, Integrations page, hourly sync, webhook
All Zoho data is in our DB — 866k+ rows across a fully-typed, jsonb-free staging layer (zoho_): Tasks 587,297 · Contacts 140,499 · Accounts 50,904 · Notes 47,874 · Calls 19,384 · Leads…
Zoho pipeline: full CRM in Postgres, Integrations page, hourly sync, webhook
All Zoho data is in our DB — 866k+ rows across a fully-typed, jsonb-free staging layer (zoho_): Tasks 587,297 · Contacts 140,499 · Accounts 50,904 · Notes 47,874 · Calls 19,384 · Leads…
- All Zoho data is in our DB — 866k+ rows across a fully-typed, jsonb-free staging layer (
zoho_*): Tasks 587,297 · Contacts 140,499 · Accounts 50,904 · Notes 47,874 · Calls 19,384 · Leads 18,610 · Deals 1,660 · Events 891 + campaigns/products/vendors, plus related lists (stage history 3,642 · contact roles 1,615 · campaign members 559) andzoho_users(70) with thezoho_user_mappingsbridge to our users (email/local-part/name matching; catchesanisha@antino.com↔anisha@antino.io). - Schema is generated from Zoho's own field metadata (
scripts/zoho/generate-staging-schema.ts→src/db/zoho-schema.ts): one typed column per field,bigintlookup ids (indexed), multiselects as child tables. No hard FKs on staging by design (Zoho doesn't enforce integrity; truncate-reload + out-of-order sync) — orphan tallies instead (currently 0 dangling refs); the future domain layer gets hard FKs. - Engine (
src/lib/zoho/importer.ts, ultracode-built + live-tested): Bulk Read v8 full imports (200k-row CSV pages, chunked inserts, self-healing drop-and-retry for bulk-unsupported fields), hourly incremental sync (per-module Modified_Time checkpoints via COQL, deleted-records tracking, Notes REST walk), targeted webhook sync, Zoho watch registration. Usable from server actions, instrumentation, and CLI (scripts/zoho/run.ts full|sync|users|related|module <Name>). - `/admin/integrations` — connection status, per-module batch table, Import everything / Sync now / Enable push (audited:
ZOHO_IMPORT_TRIGGERED/ZOHO_SYNC_TRIGGERED/ZOHO_PUSH_ENABLED; admins notified), auto-refresh while running; in the admin sidebar. - Hourly cron in
src/instrumentation.ts(activates on server boot; skips until a FULL import exists; renews the Zoho watch whenAPP_PUBLIC_URLis set). Webhook/api/zoho/webhook(token-checked; proven live — a pushed vendor id round-tripped through targeted sync). - Verified: all 11 module counts match live Zoho exactly; lead-status distribution reproduces the audit (77.3% Open); the "Anisha's active leads" question answers identically from pure SQL (Open 1,272 / Working 2: MILIND + Srishti, stale since March) — and SQL surfaced a status (
Disqualified, 3 leads) the per-picklist API counting had missed. - Zoho credentials + webhook token live in
.env(gitignored).
07
Jul 2026
Tuesday
1 shipped
Data23:23 ISTZoho CRM current-state audit (docs/ZOHO-CRM.md)
Mapped the live Zoho CRM (read-only) and wrote it up in docs/ZOHO-CRM.md, linked from the docs index: module inventory + record counts, the full entity-relationship graph (lookups + related…
Zoho CRM current-state audit (docs/ZOHO-CRM.md)
Mapped the live Zoho CRM (read-only) and wrote it up in docs/ZOHO-CRM.md, linked from the docs index: module inventory + record counts, the full entity-relationship graph (lookups + related…
- Mapped the live Zoho CRM (read-only) and wrote it up in
docs/ZOHO-CRM.md, linked from the docs index: module inventory + record counts, the full entity-relationship graph (lookups + related lists), the Tasks linkage analysis, and the fault lines vs.SALES-PROCESS.md. - Entity relationships: hub-and-spoke around Accounts (company) + Contacts (people); Leads convert into Account/Contact/Deal; Deals hang off Account + Contact (+ Contact Roles M:M); Activities (Tasks/Calls/Meetings) attach polymorphically via
Who_Id/What_Id. The live graph is only Leads → Accounts/Contacts → Deals + Activities — the quote-to-cash chain, Cases, Price Books, and the custom Legal Contract Management module are 0 records (empty scaffolding). - Volumes: Contacts 140.5k, Tasks 587k, Accounts 50.9k, Calls 19.4k, Leads 18.5k, Deals 1.66k, Meetings 891; Quotes/Sales Orders/Invoices/Purchase Orders/Price Books/Cases/Legal Contract Management all 0.
- Tasks (587k) link mostly to leads/contacts, not deals; the convention shifted over time (older to Contacts 76%, newer to Leads 83%; Accounts steady ~13%; Deals 4-10%). Confirms activity lives at the top of the funnel, not on opportunities.
- Docs-only change; no code touched.
06
Jul 2026
Monday
1 shipped
Data22:40 ISTAuth, user import, admin user management
Schema: users gains status (user_status: ACTIVE | SUSPENDED), employee_code (unique), designation, department, password_hash. New audit_events table — the audit spine is live (recordAudit()…
Auth, user import, admin user management
Schema: users gains status (user_status: ACTIVE | SUSPENDED), employee_code (unique), designation, department, password_hash. New audit_events table — the audit spine is live (recordAudit()…
- Schema:
usersgainsstatus(user_status:ACTIVE | SUSPENDED),employee_code(unique),designation,department,password_hash. Newaudit_eventstable — the audit spine is live (recordAudit()insrc/lib/audit.ts, secrets redacted). - Import:
scripts/import-users.tsloads 226 employees from the designation sheet (scripts/data/designation-data.json) + seed admin (admin@glue.tools). All share the dev passworddev-test-pw-1(bcrypt). Roles mapped heuristically: IT leadership/leads → TECH_LEAD, IT rest → DEV, sales leadership/managers → AE, sales juniors → SDR, other departments → DEV (open question). Result: 1 ADMIN, 26 TECH_LEAD, 9 AE, 6 SDR, 185 DEV. Idempotent; every row audited. - Auth: email + password sign-in (
/sign-in), jose-signed JWT session cookie (7d) withuserId+ role. DAL (src/lib/auth/dal.ts):verifySession/getCurrentUser/requireRole. Suspended users blocked at sign-in and DAL. - URL convention locked in:
/<role-name>/<page-name>.src/proxy.ts(Next 16 renamed middleware → proxy) optimistically gates role areas and bounces wrong-role visitors to their own home; authoritative checks stay in layouts/actions. - User management:
/admin/users— search/filter 227 people, inline role change, manager assignment (self/cycle-safe), suspend/reactivate. Self-role-change and self-suspend blocked. Every mutation audited +revalidatePath. - Role home stubs for all five roles so every sign-in has a landing surface.
- Verified end-to-end against the dev server: sign-in (admin →
/admin, tech lead →/tech-lead, bad password rejected), wrong-role redirect, 227 rows SSR'd on/admin/users, audit counts (227USER_CREATED,USER_SIGNED_INon each sign-in).