Latest improvements to MC20 — auto-published from our internal release notes.
Audited transactional email path via mail-tester.com. Initial score 7.4/10 caught a 2.5-point SpamAssassin penalty (FREEMAIL_FORGED_REPLYTO) on every audit/plan email — these had From: hello@needawebnow.com but Reply-To: needawebnow@gmail.com, which pattern-matches phishing.
Patched 8 production endpoints to set Reply-To matching the From domain:
/api/inbox-leads/send-{audit,plan}/api/customers/[id]/send-{audit,plan}/api/prospects/[id]/send-{audit,plan}/api/outreach-contacts/[id]/send-{audit,plan}Re-tested: 9.9/10. Remaining -0.1 is HEADER_FROM_DIFFERENT_DOMAINS (Resend uses AWS SES; envelope sender = amazonses.com). Fixable with a custom bounce domain on Resend (~30 min DNS), but not material above 9/10.
Real impact: every audit/plan email Eddy sent prior to today carried a -2.5 spam penalty. Lead-conversion outreach was being silently downgraded to spam folders for a meaningful fraction of recipients. Going forward, transactional email lands in inbox.
Other infra confirmed today:
Built a unified e-commerce layer so any project can connect Shopify OR WooCommerce (or both, side-by-side). Sets up cleanly for the planned migration of htndey-vx.myshopify.com to self-hosted WooCommerce + Stripe on the VPS.
EcommerceConnection — kind: "shopify" or "woocommerce", store_url, AES-256-GCM-encrypted credentials, display_name, last_sync timestamps + status. Unique per project+kind+url.EcommerceOrder — normalized cache: external_id, number, status, financial_status, customer email/name, totals in cents, currency, line_items JSONB, ordered_at, raw payload.EcommerceProduct — normalized cache: sku, title, status, price_cents, inventory_qty, image_url, url./api/ecommerce/connections — list / create. POST validates the connection live before saving./api/ecommerce/connections/[id] — toggle active, disconnect, manual sync./api/ecommerce/metrics — aggregated KPIs across active connections./api/ecommerce/products?q=&limit= — searchable cached product list./api/ecommerce/orders?limit= — cached order list./api/webhooks/woocommerce?connection={id} — HMAC-SHA256 signature verification, upserts orders on order.created / order.updated events./ecommerce1. Spin up WordPress + WooCommerce + Stripe gateway on Hetzner VPS
2. Create REST API key (Read/Write) at WooCommerce → Settings → Advanced → REST API
3. Connect at /ecommerce → Connections → WooCommerce, paste base_url + ck + cs
4. Run both Shopify and WooCommerce connections in parallel during migration; /ecommerce shows combined revenue
5. Once WooCommerce catches up, disconnect Shopify, cancel the Basic plan
A drop-in replacement for Calendly that lets each user create unlimited paid or free meeting types — Calendly free plan is capped at 1, which was the deal-breaker for selling multiple service tiers.
BookingMeetingType — name/slug/duration/buffers/location/price/color/active/min-notice/max-per-day/questions per userBookingAvailability — weekly schedule (per-day time ranges) + timezone + blackout datesBooking — meetingType FK, attendee details, ICS uid, status (confirmed/cancelled), Google Calendar event id placeholderCalendlyConnection — optional personal-access-token bridge for users who want to keep Calendly alongside MC20GET/POST /api/bookings/meeting-types — list/create typesGET/PATCH/DELETE /api/bookings/meeting-types/[id]GET /api/bookings/slots?meetingTypeId&from&to — public slot computation respecting weekly schedule, buffers, min-notice, daily cap, existing bookingsPOST /api/bookings/book — public booking endpoint; validates slot, creates Booking, sends Resend confirmation emails (host + attendee) with ICS calendar invite attachedGET /api/bookings?filter=upcoming|past — list my bookingsGET/PUT /api/bookings/availability — weekly schedule + timezone editorGET/POST/DELETE /api/bookings/calendly — connect Calendly via personal access token, fetch + cache event typesGET /api/bookings/public/[handle] — public lookup (returns active types + Calendly fallback URL if connected)GET /api/bookings/public/[handle]/[slug] — single meeting type detail/bookings — manager dashboard with 5 tabs: Meeting Types · Upcoming · Past · Availability · Calendly bridge/bookings/types/[id] — full meeting-type editor (name/desc/duration/buffers/location/price/color/cap/notice/active toggle)/book/[handle] — public meeting-types listing for a host (e.g. /book/morilloedilberto)/book/[handle]/[slug] — calendar picker + slot selector + booking form. Auto-detects attendee timezone. After confirm: success state + ICS-attached email sent./bookings → Calendly tab. Validates token via users/me and event_types, caches event types in DB./book/, /share/, /f/ added to public-paths list in middleware./api/bookings/public/, /api/bookings/slots, /api/bookings/book are public (no auth)./book/morilloedilberto returns 200 (no auth gate)/leads/kanban — drag-between-status board, 6 columns (new / contacted / qualified / proposal / won / lost). Drag a card → PATCH /api/contacts with new status./leads/gallery — card-grid view with inline search filter, status badge, lead-score star, category tag./cfo/customers — new top-level customers list page (was previously only [id] profile). Two-mode toggle: sortable Table view + Gallery card view. Search + sort by name/email/monthly_value/createdAt./leads header (Table | Kanban | Gallery), matches the PM Kanban/List/Calendar pattern.mc20-yjs PM2 process (id 23) — WebSocket server on 127.0.0.1:1234 using y-websocket@1.5.4 (bin/utils.setupWSConnection). Each entity gets its own room (e.g. Doc:cuid123)./y-ws reverse proxy — wss://mc20.needawebnow.com/y-ws → ws://127.0.0.1:1234. Upgrade headers, 24h read/send timeouts, public-side TLS termination.roomId + userName + userColor props. When present, swaps StarterKit history for @tiptap/extension-collaboration (Yjs-backed undo/redo) + @tiptap/extension-collaboration-cursor (live cursors with name labels). Awareness-driven collaborator avatar strip in toolbar./docs mounted with collab — roomId={Doc:${active.id}} userName={active.title}. Two browsers on the same doc see each others edits + cursors live.Doc.body HTML written by client autosave every 2s. If everyone disconnects, room state is GCd; next connect re-seeds from Doc.body via the sync event handler.roomId is omitted, RichTextEditor works exactly as before (single-user, local history). Used everywhere else (comments, future invoice notes, etc.) without change.| Tier | Item | Status |
| P0 | Rich-text editor | done (TipTap) |
| P0 | Comments / threads | done (mentions + embeds + presence) |
| P0 | Templates system | done |
| P1 | PM kanban/list/calendar | done |
| P1 | Public share links | done |
| P1 | Custom fields | done |
| P1 | Client portal | done (/portal) |
| P1 | File uploads + preview | done |
| P2 | Wiki/Docs module | done |
| P2 | AI doc Q&A semantic RAG | done (pgvector + sentence-transformers) |
| P2 | Web clipper | done |
| P2 | Database views on Leads/Customers/Campaigns | done (Kanban + Gallery + Table) |
| P3 | Slash commands | done |
| P3 | Block-level drag | done |
| P3 | Real-time collab (Yjs) | done |
| P3 | Embeds | done |
| P3 | Page version history + restore | done |
postgresql-18-pgvector). Extension enabled in mc20 DB. No restart required.Doc, RecordComment, PmTask (vector(384), cosine ops). Plus embedded_at timestamps and an EmbedQueue table with status/attempts tracking.all-MiniLM-L6-v2 (384-dim, ~80MB, $0/month).mc20-embed-worker (id 21): polls EmbedQueue every 5s, batches 16 at a time, writes vectors back to entity tables. Auto-seeds backfill for any rows missing embeddings on startup.
- PM2 process mc20-embed-server (id 22): HTTP sidecar on 127.0.0.1:7777 for live query encoding. POST /embed returns 384-dim normalized vector. ~10ms latency.
enqueueEmbed("Doc"|"RecordComment"|"PmTask", id) fires fire-and-forget on Doc create + Comment create + Task create. reembed(...) fires on Doc PATCH (when title/body changed) and PmTask PATCH. lib/embed.ts provides both helpers + semanticSearch(entityType, query, projectId, limit)./api/ai-qa/ask rewrite — now does semantic search first (pgvector cosine HNSW), then a keyword fallback that excludes already-matched IDs to widen recall. Response includes matchInfo: { semantic, keyword } counts. Similarity percentages cited inline.src/components/RichTextEditor.tsx) — replaces plain textareas. Bold/italic/strike/code, h1-h3, bullet/ordered/task lists, blockquote, code block, link, image, hr, undo/redo. Slash commands (/) menu. Drag-handle (block reorder). Embed-on-paste for YouTube and image URLs. Mounted in /docs.DocVersion snapshots already auto-write on PATCH; new /api/docs/[id]/versions (GET list + POST { versionId } restore). /docs page now has History drawer with timeline, preview, and Restore (snapshots current first). Auto-save every 2s./portal — simplified receiver-side dashboard: balance due tile, open quotes tile, shared docs tile + invoice/quote tables + shared docs list + agency contact card. Auto-resolves the right project via session.pid or CfoCustomer.email match.@tiptap/extension-drag-handle-react wired into RichTextEditor; grip-vertical handle appears next to focused block./ opens a 9-item insert menu (h1/h2/h3, bullet/ordered/task list, quote, code, divider). Arrow-key nav, Enter/Esc close.| Tier | Item | Status |
| P0 | Rich-text editor | ✅ shipped (TipTap) |
| P0 | Comments / threads | ✅ shipped (with @mentions, embeds, presence) |
| P0 | Templates system | ✅ shipped |
| P1 | PM kanban / list / calendar | ✅ shipped |
| P1 | Public share links | ✅ shipped |
| P1 | Custom fields | ✅ shipped |
| P1 | Client portal simplified view | ✅ shipped (/portal) |
| P1 | File uploads + preview | ✅ shipped |
| P2 | Wiki/Docs module | ✅ shipped |
| P2 | AI doc Q&A (RAG) | 🟡 keyword RAG shipped; semantic pgvector deferred (extension not installed system-wide) |
| P2 | Web clipper | ✅ shipped |
| P2 | Database views on Leads/Customers/Campaigns | 🟡 PM has 3 views; other lists single-view (refactor risk too high to lump) |
| P3 | Slash commands in editor | ✅ shipped |
| P3 | Block-level drag-reordering | ✅ shipped |
| P3 | Real-time collaborative editing (Yjs) | ❌ deferred (heavy: ~12 hr, separate session) |
| P3 | Embeds (Loom/Figma/YouTube/Twitter/Calendly) | ✅ shipped (in comments + paste-to-embed in editor) |
| P3 | Page version history + restore | ✅ shipped |
/api/ai-qa/ask now keyword-RAGs over project Docs, RecordComments, and PmTasks in addition to PLATFORM_UPDATES + brand kit. (src/app/api/ai-qa/ask/route.ts)/pm/list adds a sortable/filterable table view alongside Kanban + Calendar. View-tab navigation consistent across all three PM surfaces./api/mentions/search?q= endpoint. Arrow-key nav, Enter/Tab insert, Esc dismiss. Body renders mentions and URLs styled./cfo/customers/[id] now has comments alongside invoices/quotes.draft_invoice, summarize_emails, run_audit, create_task, search_knowledge via src/lib/channels/specialized-tools.ts.This single session push added 30+ new tables/endpoints/pages across MC20.
RecordComment table, <CommentThread> component drops onto any entity page (mounted on /cfo/invoices/[slug])CustomField + CustomFieldValue; <CustomFields> component auto-renders configured fields per entity (text/number/date/boolean/select/url)PmTask table + /pm page with HTML5 drag-drop + 5 status columns (todo/in_progress/review/blocked/done)/pm/calendar month grid with priority-coded task chipsDoc + DocVersion tables + /docs page with markdown editor, live preview, Cmd+S save, automatic version snapshots on >10-char diffTag + TagAssignment; <TagPicker> component with inline create + assignReaction table + <Reactions> component with 8 quick emoji + customForm + FormSubmission tables + /api/forms + /f/[slug] public page; auto-upserts to Contact when email field present/api/bulk/import for tasks + contacts (5000 row cap)Template table for doc/email/task/invoice/quote/comment templates per projectMention table; auto-creates Notification on mentionSavedView per user/project/page (ready for table-view filters)Bookmark table + /clip page with drag-to-bookmark-bar JS/api/ui-agent/audit with project brand kit context + MC20 design tokens; floating 🟣 "Polish" button on every page/api/ai-qa/ask RAG over PLATFORM_UPDATES.md + project brand kits; 💬 floating chat widget bottom-right/api/docs/assist (improve/summarize/expand/outline/translate/shorter/formal/friendly)/api/email/compose matches project brand voice + tone selector/api/pm/suggest proposes tasks based on inbox drafts + open invoices + uncategorized transactions/api/email/to-task extracts title/description/priority/dueDate from raw emailfast (DeepSeek)/cfo/review AI transaction approval page with category dropdown + bulk approveautoReplyEnabled kill switch (per agent) — single source of truth via existing Auto-Reply tab togglesystemPrompt persona — replaces hardcoded NWN template; brand-specific tone for NWN/Bachaco/Edilberto MorilloautoReplyBlocklist — 35+ keywords for personal-LLC projects (funding/lender/debt/Pawnee/Valentine/Duber/Daniel emails)leadGate — replied_to (canonical default) / any_inbound / high_scorereengagementHooks — domain-specific topics for re-engaging old leads (Bachaco: festivals/sync/press; NWN: SEO/AI/audits; Edilberto: personal)labelTaxonomy per agent — overrides hardcoded NWN labelslastChecked updates every cycle so dashboard shows live freshness/lib/anthropic-batch.ts — Anthropic Batch API wrapper for 50%-off pro tier (24h SLA)/api/channels/[id]/stream replaces 5s polling with EventSource (polling fallback on error)/lib/ui-agent.ts — shared design-token + brand-context loader/pm/pm/calendar/docs/cfo/bank-connections/clip/activity_count_replies_to Gmail helper for canonical lead detectionautoReplyEnabled=false, ready to flip per project after persona review)---
CfoCreditMemo + CfoCreditMemoApplication. FKs on CfoCustomer.creditMemos and CfoInvoice.creditMemosFromOverpayment + creditMemoApplications.src/lib/cfo/credit-memo-number.ts — race-safe sequential numbering via pg advisory lock. Format CM-{shortcode}-{YYYY}-{NNN}. Friendly URL resolver included.GET /api/cfo/credit-memos — list with customerId/status/sourceInvoiceId/projectId filters; returns totalOutstanding
- POST /api/cfo/credit-memos — manual create for refund / settlement / manual adjustment (overpayment is reserved for auto-create)
- GET /api/cfo/credit-memos/[slug] — id or CM-NWN-2026-001, with application history
- PATCH /api/cfo/credit-memos/[slug] — notes only (amount immutable post-issuance)
- POST /api/cfo/credit-memos/[slug]/apply — atomic transaction; same-customer + same-project + balance guards
POST /[slug]/void, POST /[slug]/refund, POST /applications/[id]/reversePOST /api/cfo/invoices/[slug]/mark-paid v2Accepts an optional travelReimbursement (or generic alias nonCreditExcess) so the agent or human marking paid can split the overpayment at point of payment:
creditAmount = overage - travelReimbursement → only this becomes the credit memotravelReimbursement → recorded as a CfoTransaction row (category: "Travel Reimbursement", direction: "income", linked to the source invoice via invoiceId)metadata.travelReimbursement = {amount, category, note, recordedAt, recordedBy} annotationprisma.$transactiontravelReimbursement > overagescripts/backfill-credit-memos.ts — idempotent. Detected and created credit memos for 2 historical Valentine overpayments. Subsequently corrected (see below).Eddy clarified that what looked like overpayments were partly travel reimbursements:
CM-NWN-2026-001 ($203.50 on INV-NWN-2026-007) — was actually a travel reimbursement, NOT credit. Deleted. Recorded as a CfoTransaction category="Travel Reimbursement" linked to INV-007.CM-NWN-2026-002 ($454.50 on INV-NWN-2026-009) — should be $354.50 because $100 was travel prepayment. Updated to $354.50 and renumbered to CM-NWN-2026-001. $100 recorded as CfoTransaction category="Travel Reimbursement - Prepaid" linked to INV-009.metadata.travelReimbursement / metadata.travelPrepayment annotations with full edit history.CM-NWN-2026-001, balance $354.50.Big sweep across the CFO module:
lib/modules.ts: removed "powerx" from FULL_ACCESS_SLUGS; removed "cfo" + "legal" from FREE_MODULES. Now both modules require explicit opt-in via Project.settings.enabledModules.lib/auth/has-module-access.ts: reordered so enabledModules allowlist gates even platform admin when configured.api/auth/me/route.ts: reads settings.enabledModules first; admin bypass only applies when no list is configured.edilberto-morillo (CEO root) left unset → admin bypass intentional for aggregator.Fixes 11+ tenancy leaks identified this session:
/api/cfo/dashboard-stats was summing across all 11 tenants (PowerX saw $528,441 cash on hand that was really Eddy's other businesses). Re-fixed twice — final version uses getCfoProjectScope with dual-ID resolution: cfoProjectIds for invoice/quote (FK → CfoProject), mc20ProjectIds for transaction/bank (FK → Project)./api/cfo/reports/cash-runway + /api/cfo/reports/recurring-vs-actual: same dual-ID scope fix./api/cfo/api-keys GET — was listing every tenant's keys → scoped to ownerUserEmail./api/cfo/entities GET — was listing every tenant's LLCs/EINs → scoped to ownerUserEmail./api/cfo/ingest/jobs GET — was listing every tenant's CSV upload jobs → scoped to ownerUserEmail./api/cfo/customers/[id], /api/cfo/documents/[id], /api/cfo/entities/[id], /api/cfo/time-entries/[id] — all findFirst({where:{id}}) calls now also require ownerUserEmail match.getCfoProjectScope: extended to return mc20ProjectIds: string[] | null for filtering Project-referencing models./cfo/global/page.tsx: was hardcoded to show NWN entities + had no CEO-root gate. Now redirects when not isCeoRoot && not admin; entity list resolved dynamically from session.sub's owned projects; internal API fetches now forward session cookie./cfo/page.tsx sub-nav: "🌍 Global CEO View" link conditional on proj.isCeoRoot./legal/layout.tsx NEW — was missing entirely. Now gates /legal/* via hasModuleAccess./legal/litigation/page.tsx: hardcoded KNOWN_ENTITIES array (with 4 of Eddy's businesses) removed; entity chips now derived from prisma-returned matters only.PATCH /api/cfo/customers/[id] notes field silently fails (returns 200 with {updated:true} but doesn't persist).POST /api/cfo/transactions returns 500 with empty body. Suspected same root cause as the previously-fixed customer-create 500.POST /api/cfo/customers — verify it works before creating new customers (rumored regression).CFO_CLAUDE.md (NEW) — full operating instructions for the CFO Advisor Claude agent. Includes data model, all endpoints, the mark-paid v2 contract (with travelReimbursement split), the Valentine correction history, tenancy rules, and the proactive offer triggers. Read this when picking up CFO work.---
For CFO Agent: Valentine credit is $354.50, not $454.50. CM-NWN-2026-001 (renumbered). Mark-paid v2 contract changed — must ask Eddy for travel-reimbursement split when amountPaid > amountDue.
Changes shipped:
1. Section: Active Customer Credit Balances
- Modify: line currently reading "$454.50 credit owed to Valentine from INV-NWN-2026-009 overpayment"
- New value: "Valentine — $354.50 active credit memo CM-NWN-2026-001 (renumbered from -002). Source: INV-NWN-2026-009 overpayment. Of the $454.50 paid above amountDue, $100 was travel prepayment (now CfoTransaction \"Travel Reimbursement - Prepaid\" linked to INV-009), $354.50 is the credit. INV-NWN-2026-007's $203.50 was NOT a credit either — it was travel reimbursement, now CfoTransaction \"Travel Reimbursement\" linked to INV-007."
2. Section: Known MC20 API Bugs
- Add: "POST /api/cfo/transactions returns 500 with empty body (discovered 2026-06-07). Status: unresolved. Workaround: insert via SSH + psql; document the workaround in the affected ticket. Same suspected root cause as the prior customer-create 500."
- Add: "PATCH /api/cfo/customers/[id] silently fails for the notes field (discovered 2026-06-07). Returns 200 with {updated:true} but the new notes value does not persist. Other fields persist correctly. Workaround: follow up every notes-PATCH with a GET to confirm the value stuck; re-warn Eddy if it didn't."
- Add: "Mark-paid v2 contract (2026-06-07): /api/cfo/invoices/[slug]/mark-paid now accepts optional travelReimbursement (or alias nonCreditExcess) in body. When amountPaid > amountDue, the agent MUST stop and ask Eddy the split between credit and reimbursement before calling. Calling without the field assumes the full overage is credit, which is rarely correct in practice."
3. Section: Recently confirmed working endpoints
- Add:
- GET /api/cfo/credit-memos?customerId=&status=&sourceInvoiceId= — list. Returns {creditMemos, count, totalOutstanding}. Use CfoApiKey bearer.
- POST /api/cfo/credit-memos — manual. Body {customerId, amount, source: refund|manual_adjustment|settlement, notes?, issuedDate?}. NOT overpayment (auto-only via mark-paid).
- GET /api/cfo/credit-memos/[slug] — id or CM-NWN-2026-001.
- PATCH /api/cfo/credit-memos/[slug] — notes only.
- POST /api/cfo/credit-memos/[slug]/apply — atomic. Body {invoiceId, amount}. Validates customer/project match, balance, invoice editable.
- POST /api/cfo/invoices/[slug]/mark-paid (v2) — body now accepts travelReimbursement / travelReimbursementNote / travelReimbursementCategory in addition to amountPaid / paidDate.
4. Section: CFO Responsibilities
- Modify: any item that mentions "mark invoice paid" — append "(always ask Eddy for the travel-reimbursement split when amountPaid > amountDue; the v2 endpoint has a travelReimbursement field that records the pass-through portion as a CfoTransaction instead of inflating a credit memo)"
- Add: "When customer pays anything above amountDue: stop, ask Eddy how much of the excess is true credit vs travel/reimbursement vs other pass-through. Never assume full overage = credit. See Section 4 (Customer Credit Balances) for the Valentine pattern that motivated this rule."
5. Section: Critical Reference Files
- Add: "/Users/edilbertomorillo/Desktop/Projects/CFO/dev-credit-memo-build-prompt.md (saved 2026-06-07) — the build spec the agent authored for credit memo support. Useful when reviewing what shipped vs what is still Phase 2/3."
Phase 2 + 3 not yet built (so the agent gives accurate answers):