feat: a backendless demo build of the console, for shipvoice.dev/demo - #7
Conversation
'pnpm build:demo' produces the console running on fixtures, for the preview at shipvoice.dev/demo. It is the real console: vite.config resolves src/api.ts to src/demo/fixtures.ts for that build only, so no page carries an 'if (DEMO)' around a fetch, the preview cannot drift from what ships, and the network client is not merely branched around, it is absent from the bundle. Forty seeded calls over about seven days. Every derived figure on the Overview is folded out of that one list by the rules the backend uses, so a reader who counts the rows and compares them with 'Calls today' gets the same answer. Writes are refused rather than faked. The console's success copy is written for a real deployment, so a fake write would print 'Written to agent/prompts/instructions.md. The next call picks it up' over a page with no disk. The test call renders and is disabled: it needs a real LiveKit project and a real microphone, and a scripted transcript would be a conversation that never happened. Proved offline two ways: no string unique to api.ts survives into dist-demo, and driving the built bundle in a browser produces four requests, all for its own assets. Fixed before this shipped: - The alias matched only './api' and '../api'. A later 'from "@/api"' would have shipped the real fetch client into a public demo with every check still green. The whole no-network guarantee rested on that regex. - VITE_DEMO_BASE was '/demo/' while the site serves the bundle from '/demo/console/', so the published preview would have loaded no JavaScript at all. - 'Calls today' counted the UTC day while the log beside it renders local timestamps, so the figure contradicted the rows underneath it for every visitor outside UTC. Its test asserted UTC too, so it agreed with the bug. - The test call's footer still claimed in the present tense that this is a real call, directly under the new copy saying it does not run here. - The preview banner now names the money strip. Those figures are the paid console's and are marked by a red dot you have to hover, which is not a disclosure on the first screen a stranger sees. docs: the docs navbar links to the demo.
WalkthroughThe frontend now supports a static demo build. It uses deterministic fixtures for reads, rejects writes and network calls, selects hash routing, serves assets from a configurable base path, and displays preview-specific UI. ChangesDemo console preview
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant Router
participant AppShell
participant DemoFixtures
Browser->>Router: Load hash route
Router->>AppShell: Render console
AppShell->>DemoFixtures: Request read data
DemoFixtures-->>AppShell: Return deterministic fixtures
AppShell-->>Browser: Render preview console
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
frontend/src/components/console.test.tsx (1)
63-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winExercise a non-root
BASE_URLin this test.If Vitest uses
/, a regression tosrc="/logo-boat.svg"also passes because the expected value is also/logo-boat.svg. Configure the test or build with a non-root base such as/demo/, or add a demo build smoke test that checks the generated asset URL.Run
pnpm testfromfrontend/after updating the test. As per coding guidelines: “frontend/**/*.{test,spec}.{ts,tsx,js,jsx}: Run frontend tests withpnpm test.”🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/console.test.tsx` around lines 63 - 72, Update the test setup around “uses the real ShipVoice mark, resolved against the bundle's base” so import.meta.env.BASE_URL is exercised with a non-root value such as /demo/, ensuring the assertion would fail for a hardcoded /logo-boat.svg; alternatively add a demo-build smoke test that validates the generated asset URL. Run pnpm test from frontend/.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/README.md`:
- Around line 46-56: Update the README’s documented demo URL and any deployment
link in the surrounding documentation to use the configured /demo/console/ base
path, keeping it consistent with VITE_DEMO_BASE in .env.demo. Do not change the
application configuration.
In `@frontend/src/demo/data.ts`:
- Around line 130-492: Remove the prohibited call-recording and metering feature
set: in frontend/src/demo/data.ts lines 130-492, delete seeded transcripts,
caller data, demo call rows, and all derived overview, rollup, and summary
logic; in frontend/src/demo/fixtures.ts lines 91-197, remove the call-log,
call-detail, summary, and rollup fixture API behavior; and in
frontend/src/demo/fixtures.test.ts lines 152-349, remove tests covering those
features. Ensure remaining demo functionality no longer references the removed
symbols or data.
In `@frontend/src/pages/CallDetail.tsx`:
- Around line 135-143: Preserve demo write-refusal messages in both error
handlers: in frontend/src/pages/CallDetail.tsx lines 135-143, render the
applicable ApiError.message before the generic delete fallback; in
frontend/src/pages/Deployment.tsx lines 63-71, render the demo ApiError.message
before the real-backend isForbidden fallback. Keep the existing missing-call and
other specialized handling intact.
---
Nitpick comments:
In `@frontend/src/components/console.test.tsx`:
- Around line 63-72: Update the test setup around “uses the real ShipVoice mark,
resolved against the bundle's base” so import.meta.env.BASE_URL is exercised
with a non-root value such as /demo/, ensuring the assertion would fail for a
hardcoded /logo-boat.svg; alternatively add a demo-build smoke test that
validates the generated asset URL. Run pnpm test from frontend/.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 966c4762-b636-4c2e-9897-92fccba018aa
📒 Files selected for processing (26)
docs/docs.jsonfrontend/.dockerignorefrontend/.env.demofrontend/.gitignorefrontend/README.mdfrontend/package.jsonfrontend/src/api-error.tsfrontend/src/api.tsfrontend/src/components/AppShell.tsxfrontend/src/components/Rail.tsxfrontend/src/components/TestCall.tsxfrontend/src/components/console.test.tsxfrontend/src/console.cssfrontend/src/demo/DemoBar.tsxfrontend/src/demo/data.tsfrontend/src/demo/fixtures.test.tsfrontend/src/demo/fixtures.tsfrontend/src/demo/flag.tsfrontend/src/demo/router.tsfrontend/src/lib/token-source.tsfrontend/src/main.tsxfrontend/src/pages/CallDetail.tsxfrontend/src/pages/Deployment.tsxfrontend/src/types.tsfrontend/src/vite-env.d.tsfrontend/vite.config.ts
| `pnpm build:demo` produces the console at shipvoice.dev/demo. It is this same | ||
| app, with one substitution: `vite.config.ts` resolves `src/api.ts` to | ||
| `src/demo/fixtures.ts`, so every read comes from a fixed sample deployment and | ||
| every write is refused. Nothing in that bundle makes a request. | ||
|
|
||
| Fixtures rather than a live deployment, because the backend has no | ||
| authentication on any route: a public one would let anyone rewrite the agent's | ||
| prompt, repoint the LiveKit project, and spend your provider credits. | ||
|
|
||
| Settings live in `.env.demo`. `VITE_DEMO_BASE` moves it off `/demo/`, and the | ||
| router switches to hashes so a deep link needs no server rewrite. A test call |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the documented preview URL match the configured base path.
Line 46 states that the preview is served at shipvoice.dev/demo. frontend/.env.demo sets VITE_DEMO_BASE=/demo/console/. If the site serves the documented path, the bundle requests assets from a different prefix.
Update the documentation and deployment link to /demo/console/, or change VITE_DEMO_BASE and all deployment configuration together.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/README.md` around lines 46 - 56, Update the README’s documented demo
URL and any deployment link in the surrounding documentation to use the
configured /demo/console/ base path, keeping it consistent with VITE_DEMO_BASE
in .env.demo. Do not change the application configuration.
| const SCRIPTS: readonly (readonly Line[])[] = [ | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "Hey, are you open on Sunday?"], | ||
| ["agent", "We're open Sunday from ten until four. Anything else I can check?"], | ||
| ["user", "No, that's it. Thanks."], | ||
| ["agent", "Great. Thanks for calling, have a good one."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "Can I leave a message for Priya?"], | ||
| ["agent", "Sure. What would you like me to pass on?"], | ||
| ["user", "Tell her the shipment landed and I'll call back after four."], | ||
| [ | ||
| "agent", | ||
| "Got it. The shipment landed, and you'll call back after four. Can I get your name?", | ||
| ], | ||
| ["user", "It's Daniel Okonkwo."], | ||
| ["agent", "Thanks Daniel. I'll pass that to Priya."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "I need someone to call me back about an invoice."], | ||
| ["agent", "I can arrange that. What's the best number for you?"], | ||
| ["user", "Four one six, five five five, oh one nine two."], | ||
| ["agent", "Let me read that back. Four one six, five five five, zero one nine two."], | ||
| ["user", "That's the one."], | ||
| ["agent", "Perfect. Someone will call you back today."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "Can you give me legal advice about a lease?"], | ||
| [ | ||
| "agent", | ||
| "I'm not able to advise on legal questions. I can take your details and have someone get back to you.", | ||
| ], | ||
| ["user", "No, that's alright."], | ||
| ["agent", "No problem at all. Thanks for calling."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "I'm calling about the thing from yesterday."], | ||
| ["agent", "Sorry, could you say that again? It came through quietly."], | ||
| ["user", "The delivery from yesterday. It never showed up."], | ||
| [ | ||
| "agent", | ||
| "Got it, a delivery from yesterday that didn't arrive. Do you have the order number?", | ||
| ], | ||
| ["user", "It's four four two one."], | ||
| ["agent", "Thanks. I'll flag order four four two one and someone will follow up."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "What's your address?"], | ||
| ["agent", "We're at 214 Front Street, unit three. Anything else?"], | ||
| ["user", "Nope, thanks."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "I'd like to book something for Thursday morning."], | ||
| ["agent", "Thursday morning works. Is nine thirty alright?"], | ||
| ["user", "Make it ten if you can."], | ||
| ["agent", "Ten it is. Can I get your name?"], | ||
| ["user", "Marta Lindqvist."], | ||
| ["agent", "Thanks Marta. You're down for Thursday at ten."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "Sorry, I think I've got the wrong number."], | ||
| ["agent", "No trouble at all. Have a good day."], | ||
| ], | ||
| // The two long ones. A log where every call runs under a minute reads as | ||
| // generated, and a caller who has to be talked through something is the case | ||
| // the persona's "one question at a time" rule was written for. | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "I'm trying to sort out an order and I've been passed around twice already."], | ||
| ["agent", "Sorry about that. Let's get it done here. What's the order about?"], | ||
| ["user", "It's a replacement part. I ordered it two weeks ago and nothing's moved."], | ||
| ["agent", "Understood. Do you have the order number in front of you?"], | ||
| ["user", "Hang on. It's, uh, seven one three, then a dash, then oh nine."], | ||
| ["agent", "So seven one three, dash, zero nine. Is that the whole thing?"], | ||
| ["user", "Yeah, that's all of it."], | ||
| ["agent", "Thanks. And is the best number to reach you the one you're calling from?"], | ||
| ["user", "No, use my mobile. It's the same number but ending four four."], | ||
| ["agent", "Got it. I'll put the order and both numbers through, and someone will call you back today."], | ||
| ["user", "Alright. Thank you."], | ||
| ["agent", "Thanks for your patience. Have a good afternoon."], | ||
| ], | ||
| [ | ||
| ["agent", "Hi, this is the assistant. How can I help?"], | ||
| ["user", "I want to move an appointment, but I'm not sure what day it's on."], | ||
| ["agent", "No problem. Can I take your name first?"], | ||
| ["user", "Yusuf Demir."], | ||
| ["agent", "Thanks Yusuf. Do you remember roughly which week it was?"], | ||
| ["user", "Sometime next week I think. Maybe Tuesday."], | ||
| ["agent", "I can't look that up from here, so I'll pass it on rather than guess. What day would suit you instead?"], | ||
| ["user", "Friday afternoon, if there's anything."], | ||
| ["agent", "Friday afternoon. Anything after two, or earlier?"], | ||
| ["user", "After two is better."], | ||
| ["agent", "Noted. Someone will confirm Friday after two, or call you back if it's full."], | ||
| ["user", "That works, thanks."], | ||
| ], | ||
| ]; | ||
|
|
||
| /** The callers. Every number is in the 555 range reserved for fiction. */ | ||
| const CALLERS = [ | ||
| "+1 416 555 0142", | ||
| "+1 415 555 4471", | ||
| "+1 226 555 0198", | ||
| "+1 519 555 0117", | ||
| "+1 647 555 0293", | ||
| "+1 212 555 0164", | ||
| "+1 604 555 0188", | ||
| "+1 902 555 0135", | ||
| "+1 780 555 0126", | ||
| "+1 306 555 0171", | ||
| ]; | ||
|
|
||
| /** | ||
| * A small deterministic generator. | ||
| * | ||
| * Math.random would give every visitor a different deployment and every reload | ||
| * a different one again, which makes the preview impossible to describe, to | ||
| * screenshot, or to test against. | ||
| */ | ||
| function mulberry32(seed: number): () => number { | ||
| let a = seed >>> 0; | ||
| return () => { | ||
| a = (a + 0x6d2b79f5) >>> 0; | ||
| let t = a; | ||
| t = Math.imul(t ^ (t >>> 15), t | 1); | ||
| t ^= t + Math.imul(t ^ (t >>> 7), t | 61); | ||
| return ((t ^ (t >>> 14)) >>> 0) / 4294967296; | ||
| }; | ||
| } | ||
|
|
||
| const random = mulberry32(0x5170ce); | ||
|
|
||
| function between(low: number, high: number): number { | ||
| return low + random() * (high - low); | ||
| } | ||
|
|
||
| function hex(length: number): string { | ||
| let out = ""; | ||
| while (out.length < length) out += Math.floor(random() * 16).toString(16); | ||
| return out.slice(0, length); | ||
| } | ||
|
|
||
| export interface DemoCall { | ||
| call: CallRead; | ||
| transcript: TurnRead[]; | ||
| } | ||
|
|
||
| /** | ||
| * Where each call sits in the week, newest first. | ||
| * | ||
| * Weighted towards the last day rather than spread flat: a console nobody has | ||
| * touched since Tuesday is not what a working deployment looks like, and the | ||
| * Overview's "calls today" is only worth reading when today has calls in it. | ||
| * Sorted after the jitter so the log's order can never contradict its stamps. | ||
| */ | ||
| function ages(): number[] { | ||
| const out: number[] = []; | ||
| for (let i = 0; i < CALL_COUNT; i += 1) { | ||
| const t = i / (CALL_COUNT - 1); | ||
| // The jitter only ever pulls a call forward, so SPAN_MS is a hard ceiling | ||
| // on the age of the oldest row rather than a midpoint it can overshoot. | ||
| out.push(SPAN_MS * Math.pow(t, 2.4) * between(0.86, 1) + between(0, 90_000)); | ||
| } | ||
| return out.sort((a, b) => a - b); | ||
| } | ||
|
|
||
| /** Every failure is at least an hour old, so the newest rows read as healthy. */ | ||
| const FAILED_AT = new Set([3, 9, 20, 33]); | ||
|
|
||
| function build(): DemoCall[] { | ||
| const records: DemoCall[] = []; | ||
| const byAge = ages(); | ||
| let turnId = 1; | ||
|
|
||
| // Built oldest first so the ids climb with the clock, the way a database | ||
| // hands them out. The list the console reads is reversed at the end. | ||
| for (let i = byAge.length - 1; i >= 0; i -= 1) { | ||
| const index = byAge.length - 1 - i; | ||
| const id = index + 1; | ||
| const startedMs = DEMO_NOW - byAge[i]; | ||
| const newest = i === 0; | ||
|
|
||
| const status: CallStatus = newest | ||
| ? "active" | ||
| : FAILED_AT.has(index) | ||
| ? "failed" | ||
| : "completed"; | ||
|
|
||
| // A failed call is one that connected and then dropped, so most of them | ||
| // carry the greeting and nothing after it. Two carry nothing at all. | ||
| const script = | ||
| status === "failed" | ||
| ? SCRIPTS[0].slice(0, index % 2 === 0 ? 0 : 1) | ||
| : newest | ||
| ? SCRIPTS[index % SCRIPTS.length].slice(0, 3) | ||
| : SCRIPTS[index % SCRIPTS.length]; | ||
|
|
||
| // The clock runs from the transcript rather than beside it, so a call is | ||
| // never shorter than the conversation printed inside it. The gap after a | ||
| // line grows with its length, which is roughly how long it takes to say. | ||
| let offset = Math.round(between(600, 1900)); | ||
| let lastSpoken = offset; | ||
| const transcript: TurnRead[] = script.map(([role, text]) => { | ||
| lastSpoken = offset; | ||
| const turn: TurnRead = { | ||
| id: turnId++, | ||
| role, | ||
| text, | ||
| spoken_at: new Date(startedMs + offset).toISOString(), | ||
| }; | ||
| offset += Math.round(between(3200, 11_000) + text.length * 55); | ||
| return turn; | ||
| }); | ||
|
|
||
| const seconds = | ||
| transcript.length === 0 | ||
| ? // Nothing was ever said, so there is no transcript to measure | ||
| // against: a call that dropped on connect is a few seconds long. | ||
| Math.round(between(3, 11)) | ||
| : Math.round((lastSpoken + between(1500, 5200)) / 1000); | ||
|
|
||
| const channel = random() < 0.65 ? "sip" : "web"; | ||
| const durationSeconds = newest ? null : Math.max(2, seconds); | ||
|
|
||
| records.push({ | ||
| call: { | ||
| id, | ||
| room_name: | ||
| channel === "sip" ? `call-${hex(10)}` : `console-${hex(12)}`, | ||
| // A web call reaches the worker through the browser and carries no | ||
| // caller id, which is why the console falls back to the room name. | ||
| caller: channel === "sip" ? CALLERS[index % CALLERS.length] : null, | ||
| channel, | ||
| agent_name: DEMO_AGENT.agent_name, | ||
| business_name: DEMO_AGENT.business_name, | ||
| status, | ||
| started_at: new Date(startedMs).toISOString(), | ||
| ended_at: | ||
| durationSeconds == null | ||
| ? null | ||
| : new Date(startedMs + durationSeconds * 1000).toISOString(), | ||
| duration_seconds: durationSeconds, | ||
| turn_count: transcript.length, | ||
| }, | ||
| transcript, | ||
| }); | ||
| } | ||
|
|
||
| // Newest first, with the id breaking a tie, exactly as the repository pages | ||
| // them (ORDER BY started_at DESC, id DESC). | ||
| return records.reverse(); | ||
| } | ||
|
|
||
| /** Every sample call and its transcript, newest first. */ | ||
| export const DEMO_CALLS: readonly DemoCall[] = build(); | ||
|
|
||
| /** The call rows on their own, in the order the log serves them. */ | ||
| export const DEMO_CALL_ROWS: readonly CallRead[] = DEMO_CALLS.map((r) => r.call); | ||
|
|
||
| // ---------- the figures, folded out of the calls above ---------- | ||
|
|
||
| function startedAt(call: CallRead): number { | ||
| return Date.parse(call.started_at); | ||
| } | ||
|
|
||
| /** | ||
| * The Overview's live numbers, by the backend's own rules. | ||
| * | ||
| * Today is the UTC day, because that is the day the backend counts. Minutes are | ||
| * every call ever recorded; the failure rate and the in-flight count are the | ||
| * trailing 24 hours only. | ||
| */ | ||
| export function deriveOverview( | ||
| calls: readonly CallRead[], | ||
| now: number, | ||
| ): CallOverviewResponse { | ||
| // The visitor's midnight, not UTC's. The real backend counts a UTC day | ||
| // because it serves many readers and cannot know any of their zones, but this | ||
| // runs in one browser and the call log beside it renders local timestamps. A | ||
| // UTC boundary makes "Calls today" disagree with the rows underneath it for | ||
| // everyone outside UTC, which is the one thing this fixture set exists to | ||
| // avoid. | ||
| const dayStart = new Date(now); | ||
| dayStart.setHours(0, 0, 0, 0); | ||
| const windowStart = now - FAILED_WINDOW_HOURS * 3_600_000; | ||
|
|
||
| const inWindow = calls.filter((c) => startedAt(c) >= windowStart); | ||
| const seconds = calls.reduce((sum, c) => sum + (c.duration_seconds ?? 0), 0); | ||
| const newest = calls.reduce<number | null>( | ||
| (max, c) => (max == null || startedAt(c) > max ? startedAt(c) : max), | ||
| null, | ||
| ); | ||
|
|
||
| return { | ||
| calls_today: calls.filter((c) => startedAt(c) >= dayStart.getTime()).length, | ||
| metered_minutes: Math.round((seconds / 60) * 10) / 10, | ||
| active: inWindow.filter((c) => c.status === "active").length, | ||
| failed: { | ||
| count: inWindow.filter((c) => c.status === "failed").length, | ||
| of: inWindow.length, | ||
| window_hours: FAILED_WINDOW_HOURS, | ||
| }, | ||
| last_report: | ||
| newest == null | ||
| ? { at: null, seconds_ago: null } | ||
| : { | ||
| at: new Date(newest).toISOString(), | ||
| seconds_ago: Math.max(0, Math.floor((now - newest) / 1000)), | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function tally(names: readonly (string | null)[]): [string, number][] { | ||
| const totals = new Map<string, number>(); | ||
| for (const raw of names) { | ||
| const key = (raw ?? "").trim() || UNKNOWN_GROUP; | ||
| totals.set(key, (totals.get(key) ?? 0) + 1); | ||
| } | ||
| // Biggest first, name breaking the tie, so the donut never reorders itself | ||
| // between two reads of the same data. | ||
| return [...totals].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])); | ||
| } | ||
|
|
||
| export function deriveRollup( | ||
| calls: readonly CallRead[], | ||
| now: number, | ||
| days: number, | ||
| ): CallRollupResponse { | ||
| const since = now - days * DAY_MS; | ||
| const window = calls.filter((c) => startedAt(c) >= since); | ||
| const byAgent = tally(window.map((c) => c.agent_name)); | ||
|
|
||
| return { | ||
| days, | ||
| // Summed from the rows rather than counted separately, so the total the | ||
| // donut prints in its hole always equals the slices around it. | ||
| total: byAgent.reduce((sum, [, count]) => sum + count, 0), | ||
| by_agent: byAgent.map(([agent_name, count]) => ({ agent_name, calls: count })), | ||
| by_channel: tally(window.map((c) => c.channel)).map(([channel, count]) => ({ | ||
| channel, | ||
| calls: count, | ||
| })), | ||
| }; | ||
| } | ||
|
|
||
| export function deriveSummary( | ||
| calls: readonly CallRead[], | ||
| turns: number, | ||
| ): CallSummaryResponse { | ||
| const seconds = calls.reduce((sum, c) => sum + (c.duration_seconds ?? 0), 0); | ||
| return { | ||
| total_calls: calls.length, | ||
| total_minutes: Math.round((seconds / 60) * 100) / 100, | ||
| total_turns: turns, | ||
| }; | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Remove the prohibited call-record and metering feature set.
This demo slice adds call logging, customer-like records, transcripts, and metering. The starter must not include these features.
frontend/src/demo/data.ts#L130-L492: Remove seeded call transcripts, caller records, call rows, and derived metering data.frontend/src/demo/fixtures.ts#L91-L197: Remove the demo call-log, call-detail, summary, and rollup API behavior.frontend/src/demo/fixtures.test.ts#L152-L349: Remove tests that validate the prohibited functionality.
As per coding guidelines, “Do not build call logging, campaigns, customer records, evaluations, billing, or metering; these features belong to ShipVoice Pro and should remain absent from this starter.”
📍 Affects 3 files
frontend/src/demo/data.ts#L130-L492(this comment)frontend/src/demo/fixtures.ts#L91-L197frontend/src/demo/fixtures.test.ts#L152-L349
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/demo/data.ts` around lines 130 - 492, Remove the prohibited
call-recording and metering feature set: in frontend/src/demo/data.ts lines
130-492, delete seeded transcripts, caller data, demo call rows, and all derived
overview, rollup, and summary logic; in frontend/src/demo/fixtures.ts lines
91-197, remove the call-log, call-detail, summary, and rollup fixture API
behavior; and in frontend/src/demo/fixtures.test.ts lines 152-349, remove tests
covering those features. Ensure remaining demo functionality no longer
references the removed symbols or data.
Source: Coding guidelines
| setDeleteError( | ||
| e instanceof ApiError && e.isMissing | ||
| ? "This call is already gone from the log." | ||
| : "Could not delete this call. Nothing was removed.", | ||
| : // Nobody answered, so the error's own sentence is the only | ||
| // account of why. The line below is true either way, but it does | ||
| // not say what to do about it. | ||
| e instanceof ApiError && e.status === 0 && e.message | ||
| ? e.message | ||
| : "Could not delete this call. Nothing was removed.", |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve demo write-refusal messages.
The demo fixture contract rejects writes. Both handlers discard ApiError.message unless status === 0. This hides the fixture refusal reason. The Deployment fallback can incorrectly instruct a demo user to enable a development backend.
frontend/src/pages/CallDetail.tsx#L135-L143: Render the demoApiError.messagebefore the generic delete fallback.frontend/src/pages/Deployment.tsx#L63-L71: Render the demoApiError.messagebefore the real-backendisForbiddenfallback.
📍 Affects 2 files
frontend/src/pages/CallDetail.tsx#L135-L143(this comment)frontend/src/pages/Deployment.tsx#L63-L71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/pages/CallDetail.tsx` around lines 135 - 143, Preserve demo
write-refusal messages in both error handlers: in
frontend/src/pages/CallDetail.tsx lines 135-143, render the applicable
ApiError.message before the generic delete fallback; in
frontend/src/pages/Deployment.tsx lines 63-71, render the demo ApiError.message
before the real-backend isForbidden fallback. Keep the existing missing-call and
other specialized handling intact.
Pairs with
mahimairaja/shipvoice-web, which hosts the output at shipvoice.dev/demo.What
pnpm build:demoproduces the console running on fixtures. It is the real console:vite.configresolvessrc/api.tstosrc/demo/fixtures.tsfor that build only. No page carries anif (DEMO)around a fetch, so the preview cannot drift from what ships, and the network client is not branched around, it is absent from the bundle.40 seeded calls over about seven days. Every derived Overview figure is folded out of that one list by the rules the backend uses, so a reader who counts the rows and compares them with "Calls today" gets the same answer.
Writes are refused, not faked. The console's success copy is written for a real deployment, so a fake write would print "Written to
agent/prompts/instructions.md. The next call picks it up" over a page with no disk.The test call renders and is disabled. It needs a real LiveKit project and a real microphone, and a scripted transcript would be a conversation that never happened.
Proved offline, two ways
api.tssurvives intodist-demo(localhost:8000: 2 hits in the normal bundle, 0 in the demo one).Fixed before this shipped
./apiand../api. A laterfrom "@/api"would have shipped the real fetch client into a public demo with every check still green. The entire no-network guarantee rested on that one regex.VITE_DEMO_BASEwas/demo/while the site serves the bundle from/demo/console/, so the published preview would have loaded no JavaScript at all.Calls todaycounted the UTC day while the log beside it renders local timestamps, so it contradicted the rows underneath for every visitor outside UTC. Its test asserted UTC too, so the test agreed with the bug.Two leaks the build itself caught and fixed:
Rail.tsxhardcoded/logo-boat.svg(404s under any base), anduseSessionfires a token POST on mount viaRoom.prepareConnection, so merely opening the test call screen put a request on the wire.72 tests pass, including 28 new ones pinning the fixture contract to
api.tsat compile time. Bothpnpm buildandpnpm build:demowork; the normal build is unchanged.Docs navbar now links to the demo.
Summary by CodeRabbit