Skip to content

feat: a backendless demo build of the console, for shipvoice.dev/demo - #7

Merged
mahimairaja merged 1 commit into
mainfrom
feat/console-demo-build
Aug 12, 2026
Merged

feat: a backendless demo build of the console, for shipvoice.dev/demo#7
mahimairaja merged 1 commit into
mainfrom
feat/console-demo-build

Conversation

@mahimairaja

@mahimairaja mahimairaja commented Aug 12, 2026

Copy link
Copy Markdown
Member

Pairs with mahimairaja/shipvoice-web, which hosts the output at shipvoice.dev/demo.

What

pnpm build:demo produces the console running on fixtures. It is the real console: vite.config resolves src/api.ts to src/demo/fixtures.ts for that build only. No page carries an if (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

  • No string unique to api.ts survives into dist-demo (localhost:8000: 2 hits in the normal bundle, 0 in the demo one).
  • 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 entire no-network guarantee rested on that one 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 it contradicted the rows underneath for every visitor outside UTC. Its test asserted UTC too, so the test agreed with the bug.
  • The test call's footer still claimed in the present tense that this is a real call, 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, marked by a red dot you have to hover, which is not a disclosure on the first screen a stranger sees.

Two leaks the build itself caught and fixed: Rail.tsx hardcoded /logo-boat.svg (404s under any base), and useSession fires a token POST on mount via Room.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.ts at compile time. Both pnpm build and pnpm build:demo work; the normal build is unchanged.

Docs navbar now links to the demo.

Summary by CodeRabbit

  • New Features
    • Added a public Demo link and a static console preview.
    • Added sample agents, calls, transcripts, summaries, and deployment data.
    • Added a persistent preview banner explaining sample-only behavior.
  • Bug Fixes
    • Improved error messages when preview actions cannot reach a backend.
    • Improved asset and routing support for deployments under custom paths.
  • Documentation
    • Documented how to build and use the read-only demo preview.
  • Tests
    • Added coverage for demo data, filtering, metrics, and no-network behavior.

'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.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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.

Changes

Demo console preview

Layer / File(s) Summary
Demo build and deployment wiring
docs/docs.json, frontend/.env.demo, frontend/vite.config.ts, frontend/package.json, frontend/README.md, frontend/.gitignore, frontend/.dockerignore, frontend/src/vite-env.d.ts
Adds the demo build command, environment settings, /demo/console/ base path, dist-demo output, API fixture aliasing, and deployment documentation.
Fixture data and API contract
frontend/src/demo/data.ts, frontend/src/demo/fixtures.ts, frontend/src/demo/fixtures.test.ts, frontend/src/api-error.ts, frontend/src/api.ts, frontend/src/types.ts
Adds deterministic agents, deployments, calls, transcripts, and metrics. Read APIs use fixtures, while write and token APIs reject with ApiError. Tests cover filtering, pagination, metrics, errors, and network isolation.
Preview runtime and interface
frontend/src/demo/flag.ts, frontend/src/demo/router.ts, frontend/src/main.tsx, frontend/src/lib/token-source.ts, frontend/src/components/*, frontend/src/console.css, frontend/src/pages/CallDetail.tsx, frontend/src/pages/Deployment.tsx
Adds demo routing, blocked test calls, the preview banner, base-aware logos, and status-0 error messages.

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
Loading

Poem

A rabbit hops through demo land,
With fixture calls close at hand.
No network knocks, no writes take flight,
Hash routes guide the preview right.
A banner waves: “Sample delight!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a backendless demo build of the console for shipvoice.dev/demo.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/console-demo-build

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
frontend/src/components/console.test.tsx (1)

63-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise a non-root BASE_URL in this test.

If Vitest uses /, a regression to src="/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 test from frontend/ after updating the test. As per coding guidelines: “frontend/**/*.{test,spec}.{ts,tsx,js,jsx}: Run frontend tests with pnpm 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

📥 Commits

Reviewing files that changed from the base of the PR and between 598b8cc and 8f63e90.

📒 Files selected for processing (26)
  • docs/docs.json
  • frontend/.dockerignore
  • frontend/.env.demo
  • frontend/.gitignore
  • frontend/README.md
  • frontend/package.json
  • frontend/src/api-error.ts
  • frontend/src/api.ts
  • frontend/src/components/AppShell.tsx
  • frontend/src/components/Rail.tsx
  • frontend/src/components/TestCall.tsx
  • frontend/src/components/console.test.tsx
  • frontend/src/console.css
  • frontend/src/demo/DemoBar.tsx
  • frontend/src/demo/data.ts
  • frontend/src/demo/fixtures.test.ts
  • frontend/src/demo/fixtures.ts
  • frontend/src/demo/flag.ts
  • frontend/src/demo/router.ts
  • frontend/src/lib/token-source.ts
  • frontend/src/main.tsx
  • frontend/src/pages/CallDetail.tsx
  • frontend/src/pages/Deployment.tsx
  • frontend/src/types.ts
  • frontend/src/vite-env.d.ts
  • frontend/vite.config.ts

Comment thread frontend/README.md
Comment on lines +46 to +56
`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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread frontend/src/demo/data.ts
Comment on lines +130 to +492
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,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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-L197
  • frontend/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

Comment on lines 135 to +143
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.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 demo ApiError.message before the generic delete fallback.
  • frontend/src/pages/Deployment.tsx#L63-L71: Render the demo ApiError.message before the real-backend isForbidden fallback.
📍 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.

@mahimairaja
mahimairaja merged commit 6077d1c into main Aug 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant