
Decided by MJ, 2026-09-13: the hybrid home from the kickoff, the receptionist is named Bella (a per-business field, Bella is the default), and her open messages roll up into To Do's and the RC Dashboard. The prototype is the spec for layout, copy and behavior; this document is the spec for data and code. When they disagree, the prototype wins on words and layout, this document wins on data.
Every control on the nine tabs survives (section 4.5 maps each). The nine /dashboard/receptionist/*.html routes keep resolving as redirects to their new homes so bookmarks, Academy links and the diagnostics "fix" hrefs keep working.
Wave 1 shows only fields the worker returns today plus one new table. The promise ledger and "said on the record" sources wait for their backend (Wave 2) and are not rendered until then; no placeholder rows.
Illustrative data lives only in the prototype. The v3 build reads the worker; the local seed in section 8 gives QA a realistic day (ids prefixed demo-rc-, removable in one statement).
/sms/send or /sms/conversations (they 404; src/routes/telephony-feature-gates.test.ts:132-136 asserts it). Texting lives under the /dashboard/sms/* hub (threads + send, tables text_chat_threads / text_message_logs); Text back uses that. (2) "Questions she could not answer" is already half built in the worker: the agent's save_question tool writes a knowledge row with status='unanswered', source='ai_call', source_call_id and the caller's question (src/services/mcp/voice-handlers.ts:346-396), so the Today card and the What she knows banner are Wave 1; only the drafted answer and the "answered from your approved answer" stamp are Wave 2. (3) Today's post-call "summary" is not a model call: PostCallProcessor.generateSummary (CallBridge.ts:150) truncates the first caller line to 120 characters, and caller_intent has no writer. Wave 1 shows that line honestly as "what they said first" plus the lead fields the agent saved mid-call; a real one-line "what they wanted" is part of the Wave 2 extraction step.| Wave | Ships | Backend | Frontend | Done when |
|---|---|---|---|---|
| Wave 1 | Today (status line, brief, approvals inline, messages worst-first with owner and due, questions she could not answer, handled collapsed, week line). Message drawer (message, what to do, recording, transcript, details, call back / text back / book / hand to / close, how-did-it-go). All calls (search, date + kind filters). Waiting on your OK (exact change from the stored payload, approve / decline / edit first, everything she did with stamps). Settings (7 sections, every control kept). Rollups (To Do's group, Dashboard card, "Needs you today"). | One table call_followups + 3 routes. A source_kind filter on the activity feed. is_spam / spam_reason / contact_id added to the calls list projection. receptionist_name on the profile. My-work emits source: "receptionist"; workspace-summaries and attention-items include Bella. | 4 tabs, 9 redirects + route ledger, new Today + drawer + calls + OK pages, Settings rail with the existing page bodies as sections, To Do's category, Dashboard card. Deterministic state derivation with unit tests. | A first-time owner opens Today and, without help, knows whether Bella is on, who is waiting on them and for how long, and clears one message end to end (call back, outcome, closed with a stamp). Zero fields shown that the worker did not return. The acceptance checks in section 8 pass on the local seed. |
| Wave 2 | Extraction step (one-line "what they wanted", what she did, commitments). Promise ledger ("Promises made in your name" on Today, the promise section and "said on the record" in the drawer, overdue promises drive due times and the brief). Drafted answers for unanswered questions and the "answered from your approved answer" stamp. Repeat caller line. Hand-off bounce-back. | A sibling of the post-call sentiment call writing call_commitments; a deterministic overdue check; published_by / published_at / use_count / last_used_call_id on knowledge; a prior-calls lookup; a scheduled reassign. | Ledger table, promise pill on messages, sources + improvised chip + hear-it + correct-the-record in the drawer, drafted answer in the question card, stamps in What she knows. | Every commitment Bella spoke on a seeded call appears with its due time within a minute of the call ending; overdue flips without a page reload; an unanswered question becomes a published answer in one click and the next test call uses it (verified by the transcript). |
| Wave 3 | Listen in on a live call. Read it to me (the brief in Bella's voice). Contradiction check. | Live audio stream to the browser (provider dependent). Brief text generator + TTS. Deterministic comparison against approved answers and commitments. | Listen in button live, voice control on the brief, contradiction card. | Owner hears a live call without the caller hearing them; the brief plays; a seeded price conflict shows as a card. |
project-specsheet.md ("Visual QA") is used from day one.A message is a call (PhoneCall as normalized in app/features/receptionist/receptionist-logic.ts) plus, from Wave 1, one optional call_followups row. The state is computed in one pure function, deriveMessageState(call, followup, pendingActions, viewerTeamMemberId), unit-tested, used by Today, All calls, To Do's and the Dashboard so they can never disagree.
calls.status = 'in_progress' (the insert default; also what POST /calls/test writes). Shown in the status line only, never in the pile. Actions: Listen in (Wave 3), Take over (existing transfer, always to contractors.phone).status = 'voicemail', preferred_callback present, urgency high, an unanswered knowledge row for the call, a hang-up, a failed call, or a followup row with state open.status = 'pending' whose customer_id matches the call's contact, or whose args carry the call id (confirm which is populated for receptionist actions). Shown with the approval card inline.receptionist_appointments row with source_call_id = call.id and status not cancelled. Bella sets it.status = 'completed' with nothing owed: answered from knowledge, handled_by = 'forwarded' and accepted, an appointment moved. Bella sets it.is_spam = 1 (the list hides these unless excludeSpam=0). Bella sets it. Actions: unblock.state(call, fu, pending, viewer):
if call.status == "in_progress" -> live
if fu and fu.state == "closed" -> done
if call.is_spam -> spam
if pending.some(p => p.status == "pending" and linksTo(p, call)) -> approval
if fu and fu.owner_team_member_id and fu.owner != viewer -> team
if fu and fu.state == "open" -> needs
if call.status == "voicemail" -> needs (a listen and a call are owed)
if unansweredQuestion(call.id) -> needs (knowledge row status unanswered, source_call_id = call.id)
if call.preferred_callback or call.urgency in ("high","urgent") -> needs
if hungUp(call) -> needs (try once today)
if appointment(call.id) -> booked
if call.status == "completed" -> handled
else -> needs (failed / unknown: someone should look)
hungUp(call) = duration_seconds < 15 and the transcript has no caller line (transcript is JSON or newline text, see normalizeTranscript)
kind(call) = "Vendor" when the vendor skill fired (Wave 1: contact_type; confirm values) ; "Known customer" when contact_id ; "Unknown number" when caller_number is unnamed ; else "New caller"
afterHours = created_at outside contractors.business_hours (profile), business time zone
| Source | Due | Shown as |
|---|---|---|
| Wave 2: a commitment with a time ("within the hour", "Monday", "today") | the commitment's due_at | "Owed a call by 10:14 AM · 3 hours late" |
Voicemail with a stated time ("call me in the morning"); Wave 1 uses preferred_callback when the agent saved one (it is free text: use it only when isIsoLike passes, else the next rule) | parsed time, else 12:00 next business day | "Owed a call this morning · late" |
| Any other needs | end of the current business day (from the profile's business hours; fallback 5:30 PM business time) | "Due by 5:30 PM" |
| How did it go: left a message / no answer | now + 2 hours, editable | "Back on your list at 3:15 PM" |
| Hand to a teammate | chosen in the dialog (default end of day) | "Rosa owns this, due 5:30 PM" |
Overdue is computed on read (due_at < now) everywhere, never stored, so the four surfaces always agree. Wave 2 adds a scheduled check only for the bounce-back and the optional hold-the-line nudge.
| Action | Who | Writes | Result |
|---|---|---|---|
| Take it | viewer with nav.receptionist edit | followup.owner = me (creates the row if none) | needs (mine), shows "You own this" |
| Call back | viewer | followup.owner = me if none; "calling" is client state until an outcome is chosen | dial (tel: link); the message shows "How did it go?" until answered |
| Reached them | viewer | state closed, outcome reached, closed_by, closed_at, note, attempt appended | done; if a promise was overdue the ledger shows "Kept late" (Wave 2) |
| Left a message / No answer | viewer | state open, due_at = now + 2h (editable), note, attempt appended | needs, "Back on your list at 3:15 PM" |
| Hand to… | viewer | owner, due_at, note, handed_by, handed_at; notifyUser to the teammate with a link to index.html?open={callId} | team; appears in the teammate's To Do's; Wave 2 reassigns to handed_by if due passes unclosed |
| Text back | viewer | the /dashboard/sms hub send route (confirm path); logs on the thread | state unchanged; the text shows under What she did with the time |
| Book a visit | viewer with nav.scheduling edit | POST /dashboard/appointments with a picked slot and sourceCallId; followup closed with outcome booked | booked; 409 appointment_conflict reads "A visit already exists for this time" |
| Close it (nothing owed) | viewer | state closed, outcome nothing_owed | done |
| Reopen | viewer | state open, closed_* cleared, reopened_by | needs |
| Approve / Decline / Edit first | viewer with edit | existing POST /dashboard/pending-actions/:id/approve / /reject {reason} / PATCH /:id (editable_payload) | approval resolved; message becomes handled (approved) or needs (declined, if something is still owed) |
Open the prototype next to this. The "Build notes" toggle in its review bar shows the real-vs-sample labels and the "where the old tabs went" map. Column names below are the worker's (src/db/001-consolidated-schema.sql:31-44 for calls); the v3 client normalizes them in normalizePhoneCall.
#/home in the prototype; /dashboard/receptionist/index.html in v3)
| Element | Data | Wave | Rules |
|---|---|---|---|
| Page head "Digital Receptionist" + "Bella answers (616) 555-0100 for Brightwater Plumbing & Heating." | name: new profile column receptionist_name (default "Bella"); number: GET /phone/diagnostics.phone_number; business name: /auth/me contractor.businessName | 1 | "Call me (test call)" is a plain button (not red), opens the existing TestCallDialog (POST /calls/test {toNumber}). No "Config" button in the head. |
| Tabs Today · All calls · Waiting on your OK · Settings | counts: needs (derived), today's calls, pending actions (/summary.pendingActions) | 1 | Replaces the nine-entry RECEPTIONIST_TABS (receptionist-shared.tsx:98-108) rendered through PageMenu. Today = needs count (red when > 0), All calls = calls today, Waiting on your OK = pending count. |
| Status line green dot, "Bella is answering (616) 555-0100 · 10 calls today · [On the line: Jason Okafor · 2:14] Listen in / Take over · All checks pass · 1:05 PM" | GET /phone/diagnostics (src/routes/phone.ts:767): overall_status active / warning / blocked, summary, checks[] with action {label, href}; calls today from GET /summary.callsToday; live call from GET /calls/active (calls/api.ts:87, excludes test/onboarding numbers) | 1 | active = green dot + "All checks pass" in green; warning = amber dot + "1 thing to look at" linking to Settings → Is she working?; blocked = red dot + "Not answering: {summary}" + the first failing check's action button. Listen in is Wave 3 (hide until then). Take over = POST /calls/:id/transfer behind canControlLiveCall (receptionist-logic.ts:187-199); it transfers to the owner's phone, say so in the dialog. |
| The brief "Brief from Bella for Dan · since 7:15 AM · Tue, Sep 15, 1:12 PM · Read the whole brief · Read it to me" + one paragraph | computed client-side from the same lists (section 7 has the sentence grammar). "since" = the viewer's last visit to Today (localStorage per user, fallback midnight business time). "Read the whole brief" opens the long form at brief.html. "Read it to me" is Wave 3. | 1 | The sentence must name the most overdue caller first. With no overdue: "10 calls since 7:15 AM. 3 need you, none past a promise." Counts are bold; overdue counts are red. |
| Waiting on your OK (only when pending > 0) | GET /dashboard/pending-actions?status=pending | 1 | Same card as 4.4. Always in the default scroll, above the messages. Receptionist-only: filter out rows with source_kind = 'meeting' / sourceMeetingId set (Meeting Studio actions live on their own page). |
| Messages chips: Needs you (n) · With the team (n) · Everything today (n) · "All calls and history" | derived states over GET /calls?excludeSpam=0&limit=200 (today + any older call with an open followup) + GET /dashboard/followups?state=open | 1 | Default chip = Needs you. Everything today lists today's calls newest first, excluding live. Empty: "Desk is clear. Nobody is waiting on you." |
| A message row (left: caller, number, kind, open project, "2nd call", time · duration · after hours, owner chip; middle: what they wanted, what Bella did, promise line, due line, note, flags; right: buttons) | caller_name / caller_number / caller_city / caller_state / duration_seconds / created_at / status / handled_by / ai_summary / service_type / service_description / urgency / preferred_callback / estimated_value / is_qualified_lead / contact_id / contact_type; after hours = created_at vs business hours; flags derived (Please call when needs or team, Urgent when urgency high or urgent, Wants an estimate when is_qualified_lead, Left a voicemail when status voicemail). "What they wanted" in Wave 1 = service_type + service_description when the agent saved a lead, else ai_summary (first caller sentence). Open project and "2nd call" are Wave 2. | 1 (promise, prior, project: 2) | Sort: overdue by lateness, then due today, then oldest. Value chip reads "Bella's estimate $180-300" (estimated_value, string) and never a bare number. Buttons by state: needs = Call back (red only when overdue or urgent), Text, Hand to…, Close; team = "{Name} has it" + Open; approval = Approve / Decline / Open; done = "Done · Dan" + Open; booked = the slot + Open. |
| How did it go? strip on a message after Call back | client state until an outcome is chosen; then PUT /dashboard/followups/:callId | 1 | Options: Reached them (closes), Left a message (back at +2h, editable), No answer (same), Could not reach, hand it to someone (opens Hand to). A "What happened?" note box saves with the outcome. Must survive navigation within the session (keep it in a module-level store keyed by call id). |
| Questions she could not answer card ("She could not answer this. Your rule: add an answer asks first." + question + Answer it / We do not answer that) | GET /knowledge-base rows with status = 'unanswered' (written today by saveQuestion, voice-handlers.ts:346: category faq, content '', source ai_call, source_call_id, context = the question); count from GET /knowledge-base/unanswered/count | 1 (draft: 2) | "Answer it" opens the editor with the question as the title and an empty body (Wave 1) or her draft (Wave 2); saving with status answered publishes it (PUT /knowledge-base/:id). "We do not answer that" soft-deletes the row (DELETE /knowledge-base/:id) so it stops surfacing. Link to the call via source_call_id. |
| Promises made in your name table (Who · What she said · Where it stands · Owner · Hear it) | call_commitments | 2 | Sorted overdue, due today, on the calendar, kept late, kept. "Kept · confirmed by Rosa 7:20 AM" under the pill. Not rendered in Wave 1. |
| Handled without you (count, Show / Hide) + This week so far line | derived; week line from /summary (callsThisWeek, qualifiedLeads24h, voicemailsOpen) plus a booked-this-week count (appointments read, confirm) and promises (Wave 2) | 1 | Collapsed by default. The week line omits the promise fragment until Wave 2. |
?open=<callId>)
| Element | Data | Wave | Rules |
|---|---|---|---|
| Header avatar, name, state pill, promise pill, owner chip, number · city · time · duration, Open in CRM, project link, "2nd call" line | GET /calls/:id (SELECT *, so contact_id and the lead fields are present); CRM link = /dashboard/crm/people/detail.html?id={contact_id}; project link and prior calls are Wave 2 | 1 | Owner chip is a button when unassigned ("Nobody yet · take it"). |
| What to do box (bold next step + due line + note) | Wave 1: a template by state ("Call {first name} back" / "Listen, then call back" / "Approve or decline the contact update"); Wave 2: the extracted next step | 1 | Always the first thing under the header for needs / team / approval. |
| From the project fact | PM: the next task on the linked project assigned in the next 7 days (GET /api/pm/tasks, confirm the filter) | 2 | Only when the call matched a project and the caller asked about it. Otherwise omitted. |
| The message (For, Date · time, From, Phone, Address / job site, Message + checked flags + edit, Taken by, Urgency) | followup.owner (For), created_at, caller + kind, caller_number, caller_address or the project, the "what they wanted" line, flags (followup.flags JSON), "Taken by Bella" / "Voicemail (after hours)" / "Block list", urgency | 1 | Show only checked flags; "edit" opens the six checkboxes (Please call, Urgent, Wants an estimate, Left a voicemail, Will call again, Returned your call). |
| What Bella promised in your name (quote, pill, due, "Nobody has called yet." / kept-by, Hear it, Correct the record) | call_commitments (text, due_at, state, kept_by, kept_at, transcript_offset_ms) | 2 | Hear it seeks the recording to transcript_offset_ms and highlights the line. Correct the record appends a correction (never edits) and offers: also text the caller, also fix the answer she reads from, note only. |
| Waiting on your OK card (inline) | the pending action linked to this call | 1 | Same component as 4.4. |
| A question she could not answer card | knowledge unanswered row with source_call_id = this call | 1 | Publishing from here also updates the Today card. |
| Booked line | receptionist_appointments with source_call_id (read route: confirm; the scheduling feature's list endpoint) | 1 | Title, when, who (assignees), where. |
| What she did list | Wave 1: derived lines (matched to a contact when contact_id; forwarded when handled_by = forwarded; took a voicemail; saved a lead when service_type; booked when an appointment exists; sent a text from the SMS thread; took a question when an unanswered row exists). Wave 2: the extracted list. | 1 | Never empty: at minimum "Answered the call" / "Took a voicemail". |
| Said to the caller, on the record (quote, "From: {source}" or "Improvised, not from your answers", Hear it (m:ss), Correct the record) | call_commitments rows of kind statement | 2 | Excludes the promise line (it has its own section). |
| Recording and transcript | GET /calls/:id/recording (calls/api.ts:104, streams with Range passthrough); transcript from the call row (JSON array or newline text; normalizeTranscript) | 1 | Bella's lines labelled with the receptionist name. Wave 2 highlights commitments. |
| Details she captured (Reason, Urgency, Bella's estimate, Lead, Best time to call, Called before) | service_type / service_description, urgency, estimated_value, is_qualified_lead, preferred_callback, prior calls (Wave 2) | 1 | "Bella's estimate" label whenever the value came from the call, never a bare number. |
| Connected (Ask Rippler about this call, Add note to project) | Rippler context = askRippleOpen() in app/features/ask-ripple/ask-ripple-bus.ts; PM note = the project comments route | later | Render only the ones with an API; never dead buttons. "Ask Rippler about this call" can ship in Wave 1 if the bus accepts a context payload (confirm). |
| Action bar | by state (section 3) | 1 | Red is used only for the primary action when the message is overdue or urgent, and for Approve. |
#/calls; v3 history.html → calls.html)| Element | Data | Wave | Rules |
|---|---|---|---|
| Search caller, number, what they wanted | client-side over the loaded list (GET /calls supports limit up to 200, offset, status, urgency, afterDate, excludeSpam) | 1 | Debounced, keeps focus. Server paging later; the list is 200 rows at most. |
| When Any time · Today · This week · Last week; Kind All · Needs someone · Leads · Booked · Handled · Hung up · Voicemail · Spam | derived; Spam requires excludeSpam=0 and is_spam in the projection (add) | 1 | Hung up, Voicemail and Spam are separate filters. |
| Columns When (+ after hours) · Caller (+ "2nd call") · What they wanted (+ estimate) · What Bella did (+ Promised:) · Where it stands (state + promise + due) · Owner · Open | as 4.1 | 1 | Owner shows "Nobody yet" in red for unowned needs. Every row opens the drawer. Footer: block-list link to Settings → Your number. |
| Export | CSV of the filtered rows | 1 | Client-side. |
#/approvals; v3 action-log.html → ok.html)
| Element | Data | Wave | Rules |
|---|---|---|---|
| Rule note "She asks first before: updating a contact, adding or changing an answer, changing how she handles calls. Texts and tags are automatic." | GET /autonomy-policy (src/routes/autonomy-policy.ts:40): the nine receptionist gates the PUT accepts (send_sms, save_skills, update_skills, delete_skills, add_kb_entries, update_kb_entries, update_contacts, tag_contacts + sms_budget_per_day) | 1 | Generated from the policy: gates that are off read "asks first", gates that are on read "automatic". |
| Approval card eyebrow "She wants to update a contact. Your rule: asks first." · title · the exact change verbatim · Why · See the call · "If you do nothing, nothing changes." · Approve / Decline / Edit first | GET /dashboard/pending-actions projection (pending-actions.ts:492-514): actionType, args (the immutable proposed change, JSON), reason, title, subtitle, kind, urgency, customerId, createdAt; editable_payload is the user-edited copy (PATCH /:id, pending-actions.ts:264-298) | 1 | The exact change is rendered from args / editable_payload per action_type by a small FE template map (update_contacts: field, old, new; send_sms: to + full body; add_kb_entries: title + full body; tag_contacts: the tag). Never truncated, never behind a details link. Edit first opens editable_payload, saves with PATCH, then Approve. "See the call" links via customerId (contact) until a call id is projected. |
| Everything she did (auto actions + past decisions, newest first, "Approved by Dan, Sep 1 9:40 AM" / "Declined by Rosa, Sep 10 3:25 PM: reason" / "Automatic, sent 8:11 AM") | GET /dashboard/activity (src/routes/dashboard/activity.ts:35) reads agent_pending_actions only, with limit + cursor; rows carry status, approved_at, rejected_at, executed_at, cancelled_at, error. Add ?source_kind=receptionist (or exclude meeting) so Meeting Studio rows stop appearing, and project approved_by_user_id / rejected_reason so the stamp names a person. | 1 | One stamp per row: actor + time from the record, never a client guess. The rejection reason is shown when present. |
#/setup/<section>)
| Section | Was | Keeps (every control) | Changes |
|---|---|---|---|
| Your number | Phone & SMS (receptionist-phone.tsx, 941 lines) | current number + release (confirm), search by area code + provision, test inbound call, webhook verify + repair, outbound voice configure, sub-account, 10DLC brand + campaign forms, readiness rows | Rows read: number (Active), Text messaging (Registered / the next step), Phone company connection (Connected / Repair), Block list (Manage; the spam list from is_spam rows and the block-list table, confirm). Registration forms render only while a step is incomplete. "10DLC" and "webhook" never appear as headings. |
| Who answers when | Routing (receptionist-routing.tsx) | during / after modes, forward-to, rings, the business-hours builder, partial saves via PUT /phone/settings and PUT /contractors/profile {businessHours} | Modes read "Bella answers right away / Ring my phone first, Bella backs up / Voicemail only". After hours "Then forward to" gains an "On call this week" picker from GET /api/team?fields=picker that fills afterHoursForwardTo with the member's phone. Saturday shows "Emergencies only" when the emergency policy article is published. |
| Greeting and voice | Config (receptionist-setup.tsx) | greeting, language, answer-after seconds, voice + preview, agent, enabled switch, the six capability switches, business identity read-only rows | Adds "Her name" (new profile column receptionist_name, default Bella). "ElevenLabs agent" moves under Advanced. Capability switches read as "What she is allowed to do on a call". |
| What she knows | Knowledge base (receptionist-faq.tsx + receptionist-faq-article.tsx) | categories, articles, answered / unanswered status, reorder, delete, full-page editor with AI usage rules | Adds the questions-she-could-not-answer banner (unanswered rows, Wave 1), an on/off switch per answer (Wave 1: published ↔ empty content is the worker's gate; a real draft status needs a column, Wave 2), "Published by X, date" (needs published_by / published_at, Wave 2), "used N times · last: {call}" (use_count / last_used_call_id, Wave 2). Wave 1 shows "Not used on calls" for empty-body rows. |
| How she handles calls | Customer skills (receptionist-vetting.tsx) | tree, keys, parents, conditions, activation, templates, prompt editor, sort order, load command (/customer-skills, table ai_skills) | Plain name first, key as a grey mono label, condition as words, toggle per row, editor with "What she should do" + "Applies when" and an Advanced toggle for key / parent / sort / load command. |
| What she can do without asking | Autonomy (receptionist-autonomy.tsx) | the eight gates + SMS budget, review changes, save | Each row reads "Automatic" or "Asks first". Pending approvals are not on this page. |
| Is she working? | Diagnostics (receptionist-diagnostics.tsx) | overall status, every check with its fix link, run again, recent issues (recent_errors) | Adds "Things to look at" above the plumbing checks (unanswered questions count). The overall answer also feeds the status line on every page. Check ids today: phone_provisioned, twilio_ownership, webhooks, ai_enabled, business_hours_configured, within_business_hours, during_hours_routing, after_hours_routing, forward_to. |
#/rollup)
| Element | Data | Wave | Rules |
|---|---|---|---|
| Dashboard brief pill "need you" and the sentence | GET /dashboard/attention-items (src/routes/dashboard/attention-items.ts) gains receptionist items from open followups (owner = viewer or unassigned for the tenant owner) | 1 | The sentence names Bella's share: "5 from Bella (3 past a promise)". |
Bella card (the Receptionist section that already exists in command-cockpit.tsx:72 and the v0 tile at home-classic-grid.tsx:32): "Bella is answering · 10 calls today", big number = need you, "3 past a promise", top 3 messages, footer "1 waiting on your OK · 1 booked today · Open Today" | GET /dashboard/workspace-summaries gains a receptionist key (answering, callsToday, needsYou, overduePromises, pendingActions, bookedToday, topMessages[3]); the FE schema dashboardWorkspaceSummariesSchema (api-client-dashboard-home.ts:44-54) gains the key | 1 | Each row opens the message drawer on Today (index.html?open={callId}). The v1 HomeOverview grid (home-overview.tsx:104-124) has no receptionist card today; add a WorkspaceCard there too. |
| Needs you today rows with a "Bella" source chip | attention-items, merged with the existing PM / CRM items, sorted by lateness | 1 | Same row grammar as the existing list (AttentionRow, home-overview.tsx:174). |
| To Do's group "Digital Receptionist" (count, "3 overdue", rows: next-step title, "Bella · Sun 2:15 PM call · promised: …", Open, owner avatar, due / overdue) | GET /dashboard/my-work (src/routes/dashboard/my-work.ts:75) emits items with source: "receptionist", title = next-step template, context = "Bella · {time} call", dueAt, ownerUserId, deepLink = /dashboard/receptionist/index.html?open={callId}, editable true, status open / closed. FE: add "receptionist" to MyWorkSource + myWorkSourceSchema (api-client-productivity.ts:26-35, 50-81) and a row in MY_WORK_CATEGORIES (productivity-my-work-logic.ts:24-41; category order is section order) and to MY_WORK_DATA_SOURCES. | 1 | Unassigned messages show for the business owner with a "?" avatar ("Unassigned, defaults to you"). Hand-off moves the row to the teammate. The "Awaiting your approval" tile counts pending receptionist actions (my-work already has an approval source; confirm whether it includes agent_pending_actions). Checking the box in To Do's calls PUT /dashboard/followups/:callId {state:"closed", outcome:"nothing_owed"}; "Open" goes to the drawer. |
#/files; optional Wave 1 layer)
| Element | Data | Wave | Rules |
|---|---|---|---|
| Entry "Open the pile, one at a time" on Today; the band at the top names the worst overdue promise and opens that folder | the same lists as Today, sorted worst first | 1 (optional) | Pure frontend: no endpoint the list view does not already call. Ship it after the list view works, as a second way in; MJ may make it the default landing. |
| Folder rows (left): one per person, a folder-tab shape in the state color, name, state, time, card count; the open one is navy and pulled out | derived states; card count = which cards exist for that call | 1 | Click a row, or Left / Right, to open that folder. The list scrolls. A legend explains the tab colors. |
| The open folder (desk): a navy tab with avatar, name, state and promise pills, kind, time, duration, owner, and the DataRipple badge; a white body with the card chips and the cards | as the drawer | 1 | Changing folder flies the cards out and the next folder's cards in (staggered 140ms, translate + opacity only). |
| The cards as a column of header bars (title, that card's pills or link, the badge): the message slip, recording and transcript, the promise, the contact, then project fact, open bid, appointment, unanswered question or pending approval; each card keeps its own body style | the drawer's own components, reused; bid and project facts are Wave 2 data | 1 | One card is open at a time and expands in place (max-height transition); the others are 44px header bars that slightly overlap. Hover a bar to peek (it expands over its neighbours), click it to open it, the chips or Up / Down do the same. The open card, Enter, or "Open the full record" opens the message drawer scrolled to that section. Cards for data that does not exist yet are simply absent. |
| Desk bar with the current caller, owner chip, folder count, previous / next and the same action buttons as the message | as the drawer | 1 | An outcome chosen here (reached them, hand to, close) advances to the next folder after the cards fly out. |
ripple-receptionist-worker/src)| Endpoint | Used for | Change needed |
|---|---|---|
GET /calls (routes/calls/api.ts:33) | the message list | Projection (api.ts:66-71, 23 columns) lacks contact_id, is_spam, spam_reason, callback_number. Add those four. Keep excludeSpam default; the dashboard passes excludeSpam=0 for the Spam filter. |
GET /calls/:callId (api.ts:164), GET /calls/active (api.ts:87), GET /calls/:callId/recording (api.ts:104) | the drawer, the live call, the player | none |
POST /calls/:callId/transfer (api.ts:181), POST /calls/:callId/cancel (api.ts:233), POST /calls/test (calls/test.ts:25) | Take over, End call, Call me (test) | none. Transfer always targets contractors.phone; the dialog says "to your phone". |
GET /summary (routes/summary.ts:30) | status line counts, week line | returns callsToday, callsThisWeek, voicemailsOpen, pendingActions, qualifiedLeads24h. Add needsYou (open followups + derived needs), bookedToday, unansweredQuestions; Wave 2 adds overduePromises. |
GET /phone/diagnostics (routes/phone.ts:767) | status line, Is she working? | none (overall_status active / warning / blocked; checks with action.href; the hrefs must point at the new Settings sections after the route change, see 6.1) |
GET /dashboard/pending-actions (routes/dashboard/pending-actions.ts:44), POST …/:id/approve (:113), POST …/:id/reject {reason} (:450), PATCH …/:id (:264) | Waiting on your OK; Edit first | the projection already carries args (immutable proposed change), title, subtitle, kind, urgency, customerId, sourceMeetingId. Add editable_payload, approved_by_user_id, rejected_reason, source_kind, action_class to the projection (columns exist, 001-consolidated-schema.sql:321-338). Confirm how a receptionist action links to its call (args? add source_call_id if not). |
GET /dashboard/activity (routes/dashboard/activity.ts:35) | Everything she did | reads agent_pending_actions only, no source filter, so Meeting Studio rows appear on the receptionist page. Add ?source_kind= and ?exclude_source_kind=meeting; project the actor columns above. |
GET / PUT /autonomy-policy (routes/autonomy-policy.ts:40, 61) | What she can do without asking; the rule note | none (PUT accepts the nine receptionist fields in BOOLEAN_FIELDS + sms_budget_per_day) |
POST /dashboard/appointments (routes/dashboard/appointments.ts:69) | Book a visit | body {title, contactId, attendeeContactIds, assigneeUserIds, serviceType, notes, status, startsAt, endsAt, sourceCallId}, gate nav.scheduling:edit humanOnly + idempotency, 409 appointment_conflict. Add a free-slot read (GET /dashboard/appointments/slots?days=7&assignee=) reusing whatever the voice booking tool calls; without it the dialog takes a typed time. |
/dashboard/sms/* hub (threads + send; tables text_chat_threads, text_message_logs; per the worker's .claude/CLAUDE.md) | Text back; the text thread on the message | Confirm the exact send and thread paths. POST /sms/send and GET /sms/conversations do not exist (404, asserted by routes/telephony-feature-gates.test.ts:132-136); routes/sms.ts holds only the Twilio inbound/status webhooks. |
GET /knowledge-base (routes/knowledge-base.ts:23), GET /knowledge-base/unanswered/count (:45), POST (:74), PUT /:id (:124), DELETE /:id (:186), POST /reorder (:226) | What she knows; the question loop | table knowledge_base (001-consolidated-schema.sql:100): status answered / unanswered, source manual / ai_call, source_call_id, context. The agent writes unanswered rows today (services/mcp/voice-handlers.ts:346-396, dedupes on title). Wave 2 adds published_by, published_at, use_count, last_used_call_id, draft_answer; the knowledge gate (services/elevenlabs.ts:478-482, answered + non-empty) increments use_count when it loads an article into a call. |
/customer-skills (routes/customer-skills.ts:74-245, table ai_skills) | How she handles calls | none |
/contractors/profile, /voices, /voices/preview, /agents, /phone/* | Greeting and voice, Your number | Add receptionist_name to the contractor profile (ALTER migration, default Bella) and read it in the greeting default ("Thanks for calling {business}, this is {name}. How can I help?"). |
GET /api/team (routes/pm/team.ts:807, ?fields=picker) | Hand to… picker, On call this week | none (returns members[] with id, name, role, phone via rowToApi) |
notifyUser(env, contractorId, kind, payload, {clerkUserId}) (services/push.ts:315) | the hand-off notification | writes user_notifications + push + email under preferences. Add a PushKind member receptionist_handoff with title "Bella: {caller} handed to you by {name}", body = the message line, link = /dashboard/receptionist/index.html?open={callId}. Never INSERT the table directly (push.ts:303-314). |
GET /dashboard/my-work (routes/dashboard/my-work.ts:75), GET /dashboard/attention-items, GET /dashboard/workspace-summaries | To Do's, Dashboard | Add receptionist items / keys as described in 4.6. No personal to-do store exists (the only tasks table is project-scoped, 001-consolidated-schema.sql:802), so the followup row is the to-do. |
call_followupsMigration file src/db/YYYYMMDDHHMMSS-call-followups.sql claimed with pnpm make:migration "call followups" (scripts/make-migration.mjs; numeric prefixes are frozen at 685, INVARIANTS.md:56-58). One CREATE TABLE per table (INVARIANT #2), never edited after apply (INVARIANT #3, migration-immutability-check.yml). contractor_id TEXT NOT NULL REFERENCES contractors(id) as DOCS/architecture/ADDING_A_FEATURE.md:111-113 prescribes. Every mutation goes through appendAuditLog(env, contractorId, 'call_followups', id, row) (services/audit-log.ts).
CREATE TABLE IF NOT EXISTS call_followups (
id TEXT PRIMARY KEY,
contractor_id TEXT NOT NULL REFERENCES contractors(id),
call_id TEXT NOT NULL UNIQUE REFERENCES calls(id),
state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open','closed')),
owner_team_member_id TEXT, -- team_members.id; NULL = unassigned (shows for the owner)
due_at TEXT, -- ISO 8601; NULL = end-of-business-day rule on read
outcome TEXT CHECK (outcome IN ('reached','left_message','no_answer','booked','nothing_owed','handed')),
note TEXT, -- latest "what happened?" text
flags TEXT, -- JSON array of the six slip checkboxes
handed_by_user_id TEXT, handed_at TEXT, -- last hand-off (Clerk user id)
closed_by_user_id TEXT, closed_at TEXT, -- the human stamp; never written by the agent
reopened_by_user_id TEXT, reopened_at TEXT,
attempts TEXT NOT NULL DEFAULT '[]', -- JSON [{at, by, outcome, note}] so "called 1:15, no answer" survives
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_call_followups_open ON call_followups (contractor_id, state, owner_team_member_id, due_at);
Route (new file src/routes/dashboard/followups.ts, mount next to my-work in src/index.ts) | Body / response | Rules |
|---|---|---|
GET /dashboard/followups?state=open&owner=me|all|<id> | rows for the tenant joined with the calls projection (caller_name, caller_number, ai_summary, service_type, status, created_at) so Today, To Do's and the Dashboard need one read | requireFeature('nav.receptionist','view'). Resolve "me" to the caller's team member id the way my-work does. |
PUT /dashboard/followups/:callId | partial { state?, ownerTeamMemberId?, dueAt?, outcome?, note?, flags? }; creates the row if missing; appends to attempts when outcome is reached / left_message / no_answer; stamps closed_by_user_id / closed_at from the Clerk principal when state becomes closed; clears them and sets reopened_* when it becomes open again; rejects any closed_by in the body | requireFeature('nav.receptionist','edit') {humanOnly:true} so the agent's worker JWT can never close a message. Idempotency middleware like appointments. Audit log on every write. |
POST /dashboard/followups/:callId/handoff | { ownerTeamMemberId, dueAt, note } → sets owner + due + handed_by / handed_at, appends an attempt of kind handed, calls notifyUser for the teammate | edit, humanOnly. Wave 2: a scheduled reassign to handed_by when due passes unclosed. |
CI to expect: add the new route dir entries to regression-map.json and tests/perf/smoke-all-features.js (regression-sync.yml), refresh scripts/schema-baseline-migrations.json, run scripts/preflight-migration-sync.mjs, and rebase right before pushing (the immutability check diffs against main's tip). Update DOCS/business-rules/receptionist.md (the tab strip paragraph is now wrong) and DOCS/architecture/SCHEMA.md.
CREATE TABLE IF NOT EXISTS call_commitments (
id TEXT PRIMARY KEY, contractor_id TEXT NOT NULL REFERENCES contractors(id), call_id TEXT NOT NULL REFERENCES calls(id),
kind TEXT NOT NULL CHECK (kind IN ('promise','statement')),
text TEXT NOT NULL, -- the sentence as spoken
due_at TEXT, -- parsed deterministically from the due phrase, business time zone; NULL for statements
state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open','kept','kept_late','released','scheduled')),
kept_by_user_id TEXT, kept_at TEXT,
source_kind TEXT CHECK (source_kind IN ('knowledge','calendar','rule','improvised')),
source_ref TEXT, -- knowledge_base.id, rule key, or receptionist_appointments.id
transcript_offset_ms INTEGER, -- for Hear it
corrections TEXT NOT NULL DEFAULT '[]', -- append only: [{at, by, text, action}]
created_at TEXT NOT NULL
);
analyzeCallSentiment(...) at durable-objects/CallBridge.ts:1225 (defined services/sentiment.ts:125, model resolved by callLLM(env, 'receptionist.sentiment.analyze', ...) at sentiment.ts:134). Add a sibling extractCallOutcome under a new use-case key (for example receptionist.call.extract, swappable on /admin/models) that returns: a one-line "what they wanted", the list of what she did, the next step, and every sentence Bella spoke that commits the business (promise) or states a fact (statement), each with a due phrase and the knowledge article or rule it came from when the agent used one. Write the first three onto calls (new columns wanted, did_json, next_step) and the rest into call_commitments. Deterministic code parses the due phrase and marks improvised when no source was cited.saveQuestion writes an unanswered row, also draft a body from the nearest published article (same use-case key) into draft_answer. Publishing (PUT /knowledge-base/:id with status answered) sets published_by / published_at. The gate in elevenlabs.ts:478 increments use_count and sets last_used_call_id for each article it loads, so the next message can say "answered from your approved answer, Sep 15".prior_call_count_30d to the calls list projection (a correlated count on caller_number) or a GET /calls?number= filter.send_sms and the budget) texts the caller a hold-the-line message when a promise passes due with nobody assigned.Every new table carries contractor_id; every read filters by it; every write goes through appendAuditLog. There is no tenant-export catalog in the repo (the kickoff's memory note on that was wrong); the real requirements are the tenant-scope convention and the audit log (DOCS/architecture/CODE_HYGIENE.md:136).
Read: nav.receptionist view. Take / outcomes / hand / close / reopen / approve: nav.receptionist edit (receptionistCanEdit, fails closed). Take over: receptionist.live-call-control admin. Book: nav.scheduling edit. Release number and delete call keep their gates. All followup writes are humanOnly.
calls.status: in_progress, completed, failed, voicemail (forwarded is a handled_by value, not a status). handled_by: ai, forwarded, voicemail, human. Pending actions: pending, approved, rejected, executed, cancelled. Knowledge: answered, unanswered.
| File | Change |
|---|---|
app/routes.ts:527-585 | Inside the dashboard-shell layout the 14 receptionist entries become: index.html (Today, receptionist-index.tsx), calls.html, ok.html, settings.html + settings/:section.html, brief.html, call.html (kept, full-page fallback), faq-article.html (kept, the editor). The old paths become a target map redirect module modeled on app/features/pm/pm-legacy-html-redirect.tsx (LEGACY_TARGETS at :33-51, loader :53-57): history → calls; action-log → ok; routing → settings/hours; faq → settings/knowledge; vetting → settings/skills; phone → settings/number; autonomy → settings/permissions; setup, config, scheduling → settings/greeting; diagnostics → settings/health. The bare-section redirect at routes.ts:170 stays. |
scripts/parity/route-ledger.json:1219+ | Update every receptionist entry's newPath / status; pnpm check:route-ledger gates the PR. |
app/features/dashboard-shell/dashboard-sidebar-nav-data.ts:180-195 | The receptionist entry's href becomes /dashboard/receptionist/index.html (today it is history.html); subItems become Today, All calls, Waiting on your OK, Settings. Count badges do not exist on sidebar items today (only SettingsShellNavItem.badge does); the prototype's sidebar badges are optional, ship without them in Wave 1. |
| Diagnostics fix hrefs | The worker's checks carry v2 hrefs mapped by reactHref in receptionist-diagnostics.tsx; with the redirects in place they keep working. Update the worker's hrefs to the settings sections when convenient. |
app/features/receptionist/)| File | Change |
|---|---|
receptionist-shared.tsx (404 lines) | RECEPTIONIST_TABS (:98-108) becomes four entries with count badges; ReceptionistPageHead (:119) gains a status-line slot; keep useReceptionistBootstrap (:51), receptionistCanEdit (:85), TestCallDialog (:181), ReceptionistCard (:344), ReceptionistRowLine (:379). Add StatusLine, MessageRow, OwnerChip, DueLine, ApprovalCard, HowDidItGo. |
receptionist-logic.ts (1124 lines) | Add deriveMessageState, messageDue, isOverdue, sortWorstFirst, briefSentence, nextStepTemplate, slipFlags, isHungUp, callerKind, proposedChangeRows(action) (the args template map), and the plain-English label maps. All pure, all covered in receptionist-logic.test.ts (532 lines today; the perms({...}) factory and the permission cases at :159-183 and :287-305 are the pattern). |
api-client-receptionist.ts (699 lines) | Add fetchActiveCalls (/calls/active), fetchFollowups, updateFollowup, handoffFollowup, fetchSlots, sendText + fetchTextThread (the /dashboard/sms paths, confirm), fetchTeam (/api/team?fields=picker&clerk=skip), fetchUnansweredQuestions (filter of fetchKnowledgeArticles), patchPendingAction (PATCH editable_payload); extend fetchPendingActions / fetchActivityPage with the source filter and the new projected fields. |
receptionist-index.tsx (Today) | Rewritten: loads calls, active calls, followups, pending actions, summary, diagnostics and unanswered questions in parallel with 8-second timeouts and partial-failure toasts (the withTimeout + Promise.allSettled pattern in receptionist-action-log.tsx); renders the sections in 4.1; reads ?open= to open the drawer. |
receptionist-message-drawer.tsx (new) | The drawer in 4.2 using the shadcn Sheet; reuses the recording object-URL logic from receptionist-call-detail.tsx (revoke on close). call.html stays as the full-page fallback and keeps Delete call (canDeleteCall) and End call. |
receptionist-history.tsx → All calls | Search, when and kind filters, owner column, rows open the drawer. |
receptionist-action-log.tsx → Waiting on your OK | Replaces the five tiles and "Approval gates represented" with the rule note, approval cards and "Everything she did" (source-filtered feed with stamps). Keep the generation-counter pagination. |
receptionist-settings.tsx (new) | A local rail + panel switch mirroring settings-hub.tsx (settingsPanelFromHash / settingsRailKey pattern in settings-hub-logic.ts) with SettingsCard from app/features/settings/settings-shared.tsx:96-150. Do not reuse SettingsShell (app/components/ui/settings-shell.tsx:222) itself: its props are account / company scoped. Each section renders the existing page body (routing, setup, faq, vetting, phone, autonomy, diagnostics) as a component with the new headings; the existing files stop being routes. |
receptionist-brief.tsx (new, small) | The long-form brief (layout B) at brief.html. |
| File | Change |
|---|---|
app/features/productivity/api-client-productivity.ts:26-35, 50-81 | Add "receptionist" to MyWorkSource and myWorkSourceSchema. |
app/features/productivity/productivity-my-work-logic.ts:24-41, 50 | Add the category row { key: "receptionist", label: "Digital Receptionist", short: "Bella", icon: "headset", accent: "#CC0C1F" } (order = section order; place it after Project Management) and add the key to MY_WORK_DATA_SOURCES. Checking a row calls updateFollowup(callId, {state:"closed", outcome:"nothing_owed"}) through the existing controlForItem path. |
app/features/dashboard-home/api-client-dashboard-home.ts:44-54 | Add the receptionist key to dashboardWorkspaceSummariesSchema. |
app/features/dashboard-home/command-cockpit.tsx:72, home-overview.tsx:104-124, home-classic-grid.tsx:32 | The Receptionist section / tile becomes the Bella card (4.6) in all three templates (v2 has the section, v1 needs a WorkspaceCard, v0 has the tile). "Needs you today" rows come from attention-items unchanged. |
SettingsCard, PageMenu (app/components/ui/page-menu.tsx), shadcn Badge / Button / Dialog / Switch / Sheet, Sonner toasts. No new design tokens: the prototype uses only the shipped palette (red on white, navy rail, Ubuntu).
receptionist-logic.test.ts: deriveMessageState one case per derivation row plus precedence (closed beats everything; approval beats team); sortWorstFirst; briefSentence for zero / one / two / three-plus overdue with singular and plural; messageDue rules; proposedChangeRows per action_type.app/features/dashboard-shell/dashboard-index-redirect.test.ts (the redirectFor() helper) for the nine old paths.// @vitest-environment jsdom + the Clerk vi.mock pattern from dashboard-topbar.test.tsx:1-14; assert Today's chip count equals its rows, the approval card shows the full change, the drawer action bar per state. This will be the first .test.tsx in the receptionist feature.pnpm typecheck then pnpm test serially (repo rule).| Where | String |
|---|---|
| Tabs | Today · All calls · Waiting on your OK · Settings |
| Sub-line | {Name} answers {number} for {business}. |
| Head button | Call me (test call) |
| Status, on | {Name} is answering {number} · {n} calls today · All checks pass · {time} |
| Status, warning | {Name} is answering {number} · 1 thing to look at |
| Status, blocked | {Name} is not answering: {diagnostics summary} · Fix it |
| Status, live | On the line: {caller} · {m:ss} · Listen in · Take over |
| Brief head | Brief from {Name} for {first name} · since {time} · {date, time} · Read the whole brief · Read it to me |
| Brief, overdue | {Caller A} has waited {lateness} for the call I promised {when}, {Caller B} {lateness}. They are {k} of the {n} that need you out of {calls} calls since {time}. {q} question(s) I could not answer. {c} change(s) wait on your OK. {h} caller(s) hung up before saying anything. {b} booked, about {value}. {m} handled without you. |
| Brief, none overdue | {calls} calls since {time}. {n} need you, none past a promise. … |
| Brief, clear | Nobody is waiting on you. Nothing I could not answer. Nothing waits on your OK. |
| Messages head | Messages · Worst first. A person closes each one. |
| Chips | Needs you {n} · With the team {n} · Everything today {n} · All calls and history |
| Empty pile | Desk is clear. Nobody is waiting on you. |
| Owner chips | You own this · {First name} owns this · Nobody yet · take it |
| Due lines | Owed a call by {time} · {lateness} late · Owed a call this morning · late · Due by {time} · Back on your list at {time} |
| Kinds | Known customer · New caller · Vendor · Unknown number · open project · after hours · 2nd call this month |
| Flags | Please call · Urgent · Wants an estimate · Left a voicemail · Will call again · Returned your call |
| Value | Bella's estimate {range} · Bid {amount} |
| Row buttons | Call back · Text · Hand to… · Close · Approve · Decline · Open · Listen in · Take over |
| Where | String |
|---|---|
| How did it go | You called {caller} at {time}. How did it go? · Reached them · Left a message · No answer · Could not reach, hand it to someone · What happened? (e.g. wife says try after 3) |
| Toasts | Dialling {number} from your phone. Tell {Name} how it went when you are done. · {caller}: reached. Closed by {you} at {time}. · {caller}: left a message. Back on your list at {time}. · {caller} handed to {teammate}, due {time}. They were notified. It comes back to you if it is not closed. · Approved by {you} at {time}: {title}. Done now. · Declined by {you} at {time}: {title}. Nothing changed. |
| Hand to | Hand {caller} to a teammate · They get the message, the recording and your note. Their name goes on it until they close it. If it is not closed by the time below, it comes back to you. · Needs to be done by · Note to them · Hand it over |
| Drawer sections | The message · What {Name} promised in your name · Waiting on your OK · A question she could not answer · Booked · What she did · Said to the caller, on the record · Recording and transcript · Details she captured |
| Drawer fields | For · Date · time · From · Phone · Address / job site · Message · Taken by · Urgency · Reason · Bella's estimate · Lead · Best time to call · Called before |
| Sources | From: {article}, published {date} · From: the calendar ({calendar}) · From: your {rule} rule · Improvised, not from your answers · Hear it ({m:ss}) · Correct the record |
| Approval card | She wants to {kind}. Your rule: asks first. · Why: {reason} · See the call · If you do nothing, nothing changes. · Approve · Decline · Edit first |
| Stamps | Approved by {name}, {date time} · Declined by {name}, {date time}: {reason} · Automatic, sent {time} · Done by {name} at {time} · Published by {name}, {date} · Answered by {name}, {date time} |
| Settings sections | Your number · Who answers when · Greeting and voice · What she knows · How she handles calls · What she can do without asking · Is she working? |
| Routing modes | {Name} answers right away · Ring my phone first, {Name} backs up · Voicemail only · On call this week · Rings before she takes over |
| Gates | Automatic · Asks first · Daily text limit |
| Knowledge | Published · Not used on calls · Needs an answer · used {n} times this month · last: {call} · Answer it · We do not answer that · Publish answer |
| Health | {Name} is answering calls. · Things to look at · Everything that must be true for her to answer · Pass · Blocked · Fix · Run again |
| To Do's / Dashboard | Digital Receptionist (group) · {n} overdue · Bella · {time} call · promised: "{promise}" · Needs an answer first · Unassigned, defaults to you · Bella is answering · {n} calls today · need you · {n} past a promise · Open Today |
demo-rc-*)Load into local D1 for the demo tenant (contractor_id = org_3DgLsRxn8CR779N3XcS3saSBYEZ, the same tenant the PM demo seed uses) the eleven calls in the prototype's data.js: one in_progress, one hang-up (duration 6, no caller line), one answered from knowledge, one reschedule with a pending update_contacts action (args carrying the old and new number), one call with an unanswered knowledge row (category faq, source ai_call, source_call_id), one call matched to a project with preferred_callback, one vendor call handed to Rosa (a followup row with owner = Rosa), one booked estimate with a receptionist_appointments row and a text log, one is_spam = 1 robocall, one after-hours forwarded call (handled_by forwarded), one weekend voicemail (status voicemail), one Sunday message for the owner (a followup row owned by Dan, due yesterday). Plus twelve knowledge articles, the five skills, four team members, a receptionist_name of Bella. Remove with DELETE … WHERE id LIKE 'demo-rc-%'. Stop the worker before d1 execute --local (executing while it runs corrupts the sqlite file).
The v3 project-specsheet.md "Visual QA" section: mint a Clerk sign-in ticket for qa+test@dataripple.com inside the frontend container (the secret never leaves it), exchange it in-page with signIn.create + setActive with the DataRipple Local QA org, then drive Playwright. The prototype's click-through script (50 steps, 1440x900, zero console errors) is the template for the v3 pass; it lives in this session's scratchpad as qa.mjs and the same steps are listed below.
| # | Question | Default if nobody objects |
|---|---|---|
| 1 | How does a receptionist pending action link to its call: args, customer_id, or nothing today? | Add source_call_id to agent_pending_actions (ALTER migration) and set it from the voice tools; until then link via customer_id. |
| 2 | The exact /dashboard/sms/* paths for send and thread read, and the daily-budget error shape. | Use them as-is; surface the budget error text verbatim. |
| 3 | Does /dashboard/my-work's approval source already include agent_pending_actions, or only PM approvals? | If only PM, add receptionist pending actions under the same source so the "Awaiting your approval" tile counts them. |
| 4 | Is there a free-slot lookup the voice booking tool uses that can be exposed for the dashboard? | Expose it as GET /dashboard/appointments/slots; otherwise Book a visit takes a typed time in Wave 1. |
| 5 | Business time zone for due times and "after hours": the profile timezone field? | Use the contractor profile timezone; fall back to America/Detroit for the demo tenant. |
| 6 | Vendor detection: contact_type on the call, the CRM company type, or the skill that fired? | contact_type when set; otherwise the vendor skill. |
| 7 | Waiting on your OK: keep as a tab as well as inline on Today, or inline only? | Both (prototype). |
| 8 | Sidebar count badges (Today 5, Waiting on your OK 1): worth adding badge support to dashboard-sidebar-nav.tsx in Wave 1? | No; ship without, add in Wave 2 if MJ misses them. |
| 9 | Can "Ask Rippler about this call" pass a context payload through askRippleOpen() today? | If yes, ship in Wave 1; if not, Wave 3. |