Built in public · Changelog
How Supernova learned the job.
Product panorama
One deal, one continuous operating loop.
Six agents live in the product today, propose-only, a named human at every gate. Everything below ships; nothing is a mock.
Brief
The Daily Brief opens each cockpit: meetings with exact times, deals needing a touch, the one move.
Draft
The Email Drafter investigates the thread through scoped tools, then a fresh-context critic verifies every claim.
Qualify
Deal Check scores MEDDPICC with the evidence that earned each score, never vibes.
Prep
Meeting Prep briefs every call: who is in the room, where it stands, what to ask.
Track
The Promise Tracker extracts what each side committed to, source attached, resolved in place.
Audit
Every run costed and receipted: model, tokens, verdict, the audit trail behind every artifact.
15
Jul 2026
Wednesday
1 shipped
DataEnrich 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
DataTech 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.
DataCorporate-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).
DataRecording 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).
13
Jul 2026
Monday
2 shipped
Data#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.
DataBreadcrumbs 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
DataZoho 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.
DataLead 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.
DataDeal 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.)
DataFollow-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
DataFirst 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
DataMicrosoft 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
DataZoho 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
DataZoho 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
DataAuth, 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).