Skip to main content
Every screen in the dashboard, what it shows, and which API endpoints back it. A few screens are still stubs or sample data — this page says which, so you do not mistake either for your own.
The dashboard container runs Next.js in development mode with the source bind-mounted. That is right for local work and wrong for anything user-facing — see Production deployment.

The layout shell

Every signed-in route lives under the (app) route group and shares one layout, frontend/src/app/(app)/layout.tsx. It wraps the page in two things:
  • AuthProvider.tsx — the auth gate. On mount it reads the session from localStorage and calls GET /users/me. No stored session, or a failed call, and it clears storage and redirects to /. Children never render without a session, so no page needs its own auth check. It also merges the organisation name into the stored session so the sidebar can show it.
  • AppSidebar.tsx — a collapsible nav rail, its expanded state persisted in localStorage under voicera_sidebar_expanded. It groups routes into Build (Agents, Numbers, Knowledge Base, Campaigns, History, Analytics, Telemetry) and Workspace (Members, Integrations), with a Walkthrough link and a profile menu in the footer.
Note that the sidebar does not link to /agent-creation, /components, or /account — those are reached from within pages or from the profile menu. PageHeader.tsx is a shared eyebrow-plus-title-plus-description header. Only the UI-kit pages use it; the main dashboard screens write their own headers. Sign-in and signup sit outside the shell at / and /signup, backed by POST /users/login and POST /users/signup.

At a glance

dashboard

The agents home, rendered by AgentsHome.tsx. It lists every agent in your active organisation from GET /agents, and pulls up to 500 recent calls with GET /calls/org/{org_id} to show per-agent activity alongside each card. Each card shows the agent’s name, its purpose (derived from the first sentence of its system prompt by agentPurposeFromApi()), and either its linked phone number or a WebSocket badge. The distinction is read from agent_category directly, never guessed from whether a number field is populated. The card’s test action branches on that same field, and this is the most useful thing to understand about the page:
  • A telephony agent opens TestCallSheet.tsx — you type an E.164 number and it fires POST /calls/outbound, placing a real phone call from the agent’s linked number.
  • A websocket agent opens AgentTestModal.tsx — a live browser microphone call over the runtime WebSocket. See Browser test calls.
The page can also duplicate an agent (GET /agents/{id} then POST /agents with the copied config) and delete one (DELETE /agents/{id}). A History item on the same card menu routes to /history?agent={agent_id}, pre-filtered to that agent’s calls.

agents/[agentId]/edit

Loads one agent with GET /agents/{agent_id}, runs it through agentToForm() to rebuild the wizard’s flat form shape, and renders the same step components the wizard uses — NameStep, LanguageProvidersStep, DeliveryStep, PromptKnowledgeStep, and ReviewStep, with SectionNav down the side. ReviewStep does the saving, calling updateAgent() for an existing agent, which sends PATCH /agents/{agent_id}. Because it shares the wizard’s mapper, it shares the wizard’s gaps. See Agent creation wizard.

agent-creation

The six-step wizard. It has its own page: Agent creation wizard.

numbers

Phone number inventory, rendered by PhoneNumbers.tsx over the usePhoneNumbers hook. Each row carries an audit line built from last_link_action, last_link_by_email, and last_link_at — who attached, detached, or imported the number and when. Detaching keeps the inventory row; it only breaks the agent link and the provider-side binding. Background in Telephony model.

batches

The route is /batches; the sidebar labels it Campaigns. Rendered by Campaigns.tsx over the useCampaigns hook and frontend/src/lib/api/campaigns.ts. It lists GET /campaign/, uploads a contact CSV with POST /campaign/upload (parsed in the browser by d3.csvParse for a preview), creates with POST /campaign/create, and drives each campaign with POST /campaign/{id}/start, /pause, /resume, and /redial. Expanding a campaign fetches its per-contact attempts from GET /campaign/{id}/runs. Agents for the create form come from GET /agents. Because dialling runs server-side, the hook polls the list so progress bars and states stay live without a reload. Background: Running a campaign and Campaigns.

history

The one screen where the dashboard clearly beats the API, rendered by History.tsx. It pages through GET /calls/org/{org_id} twenty rows at a time, joins each call to its agent (GET /agents), and lets you filter by date range (a preset or a custom from/to), call type, status — completed, failed, in_progress, ringing, initiated — and agent. All of it filters the current 20-row page in the browser; none of these are query parameters on the API call, so a filter can only narrow what you already fetched, not search the organisation. Phone numbers are masked in the list by maskPhoneNumber(). Opening History from an agent’s card menu on dashboard routes here with ?agent={agent_id}, which pre-selects that agent in the filter. The Export menu has four options, all built on frontend/src/lib/report.ts: CSV or PDF of the currently filtered page, a CSV of every transcript in the organisation (via listAllOrgCalls(), a hundred rows at a time, plus one GET /calls/{call_id}/transcript per call), and a CSV scoped to whichever agent is selected in the filter. Clicking a row opens CallDetailSheet.tsx, which fetches two things:
  • GET /calls/{call_id}/recording — an authenticated proxy in front of MinIO. It is fetched as a blob because <audio src> cannot send an Authorization header; the sheet builds an object URL for playback and decodes the same blob for the waveform.
  • GET /calls/{call_id}/transcript — parsed by frontend/src/lib/transcript.ts into [timestamp] role: content lines.
Transcript lines are anchored so the first line is t=0 and later lines are offset by their deltas. There is no shared clock between the transcript’s timestamps and the recording’s start, so the alignment between text and audio is a close estimate rather than an exact sync. Browser test calls appear here alongside telephony calls. The browser registers the call_type: web CallLog itself with POST /calls/web before opening the socket, and the call gains a transcript and recording like any other. A View Call Telemetry button in the sheet links to /telemetry?call={call_id} — disabled with a tooltip when the call has no latency data yet, since a call still in progress or one the runtime never wrote metrics for would otherwise deep-link into a 404.

knowledge-base

KnowledgeBase.tsx runs over the useKnowledgeBase hook and frontend/src/lib/api/knowledge.ts: GET /knowledge to list, POST /knowledge/upload to add a document, and DELETE /knowledge/{document_id} to remove one. The client refuses a file over 25 MB before uploading, matching KB_MAX_UPLOAD_BYTES on the API. Each FileCard has an Eye action that fetches GET /knowledge/{document_id}/preview as a blob (previewKnowledgeDocument()) and opens PdfPreviewSheet.tsx, a slide-in viewer built from an object URL. Both components are preview-source-agnostic — FileCard only ever emits onPreview, and the sheet only ever renders whatever blob it is handed — so KnowledgeBase.tsx is the only place that knows the source is this specific endpoint. Ingestion and embedding happen server-side, so the hook polls while any document is still indexing and the Indexed / Indexing / Failed counts reflect real document status. The wizard’s knowledge-base picker calls the same GET /knowledge, so the documents you attach to an agent are your real ones. Background: Managing knowledge documents and Knowledge base (RAG).

members

Fully wired, over the useMembers hook and frontend/src/lib/api/members.ts. Cards sort highest-rank-first: super_admin, then admin, then member. There is no accept-invite step — POST /members/invite creates the account directly in your active organisation with a password you set, so you hand the credentials over yourself. The “add member” link the modal generates points at /add-member/{uid}, a shareable page identifier; the organisation context travels in query parameters. Roles are explained in Multi-tenancy and roles.

integrations

The credential manager, rendered by Integrations.tsx. It is the screen you need before the wizard is usable, because the wizard hides every provider you have not configured here. GET /auth/catalog returns each provider’s field schema — types, descriptions, examples, and which fields are secret. groupProvidersByKind() buckets them into STT, TTS, LLM, and telephony sections, with providers that serve several kinds appearing in each. GET /auth/configured marks which are already connected. A second filter row sits above the list: provider type tabs (cloud, adapter, local), built from whatever catalog.provider_type values are actually present, on top of the existing kind filter. Both narrow the same connected/available/telephony lists together. Saving a provider sends POST /auth with { provider, auth }. GET /auth/{provider} reads a stored entry back and DELETE /auth/{provider} removes it. Secret fields render behind a show/hide toggle. Credentials are encrypted at rest by the API — see Provider credentials (ProviderAuth). Because the form is generated from the catalog rather than hand-written, it always matches what the API accepts. That makes this screen genuinely more reliable than reading credential field names out of documentation.

analytics

Organisation-wide call volume, rendered by Analytics.tsx from a single request: GET /calls/org/{org_id}/analytics. This is what separates the screen from dashboard and telemetry. Those two pull raw call logs with GET /calls/org/{org_id} and aggregate in the browser; this one asks the API for figures already aggregated server-side, so it counts every call in the organisation rather than only the page it fetched — dashboard sees at most 500. The layout is four stat tiles over two panels: Agent Performance is a recharts bar chart of agent_performance with a list toggle — the API caps that array at the ten busiest agents, so it is a leaderboard rather than a full breakdown. Connection Breakdown shows connection_rate as a headline percentage and progress bar, with calls_connected and calls_failed beneath it. Model Usage shows the single busiest STT, TTS, and LLM model from model_usage, each as a bar sized against the others’ call counts — a stage renders blank if the API returned null for it. A refresh button sits next to the Connection Breakdown headline; clicking it re-fetches and stamps an “Updated …” timestamp beside it. Download PDF, top right, builds a real PDF client-side with frontend/src/lib/report.ts (a thin jsPDF wrapper) — vector bars for Agent Performance, Connection Breakdown, and Model Usage, not a screenshot or window.print(). Every exported PDF and CSV in the app opens with the same organisation / downloaded-by / generated-at header from that same module, so a report is traceable back to who pulled it. One caveat worth knowing before reading the numbers: the figures are all-time, with no date-range or per-agent filter anywhere on the screen. And “connected” counts only calls whose call_response is answered, which is also the population the duration figures average over — so Avg Call Duration is per answered call, not per attempt. See GET /calls/org/{org_id}/analytics for the response shape. For per-call pipeline latency rather than volume, use telemetry. Languages.tsx draws a d3 bubble map — scaled circles over a colour ramp — from the static LANGS_GEO array in dashboard-data.ts. Those are sample figures, not your call volumes, and no screen currently renders the component.

telemetry

Per-call pipeline latency, rendered by Telemetry.tsx from the CallMetrics the runtime writes at the end of every call. Pick a call — or deep-link one with ?call={call_id} — and the screen fetches GET /calls/{call_id} and GET /calls/{call_id}/metrics, with GET /calls/org/{org_id} behind the picker. It shows four averages as stat tiles, each tinted against a threshold and compared against the previous call: Below them, a recharts bar chart and a table share one per-turn dataset — hovering either highlights the same turn in both — and Export CSV writes the per-turn rows out, via the same report.ts helper the other two screens use. A call has no metrics until the runtime finishes writing them, so GET .../metrics returns 404 for a call still in progress; the screen treats that as “no data” rather than an error.

account

Your profile and organisation switcher, rendered by Account.tsx. It loads GET /users/me and GET /users/organisations in parallel, and switching organisation calls POST /users/switch-organisation and writes the returned token back to localStorage, so the whole dashboard re-scopes to the new organisation. frontend/src/lib/api/organisations.ts also exposes DELETE /organisations/{org_id}, restricted by the API to a super_admin acting on their own active organisation.

components

Internal. A UI-kit gallery of every shared primitive — buttons, inputs, cards, badges, switches, spinners, progress bars, tooltips, toasts, modals, the stepper, the nav rail — each shown with its variants. It is a developer reference for the design system, not a product feature. Nothing here reads or writes data.

walkthrough

The /walkthrough route itself is still the placeholder stub described above — but the sidebar’s “Walkthrough” footer button does not link to it. It calls restartWalkthrough() and navigates to /dashboard, because the real product tour is WalkthroughOverlay.tsx, a spotlight-and-arrow overlay drawn on top of whichever page it targets, not a route of its own. frontend/src/components/walkthrough/ holds the whole feature:
  • WalkthroughProvider.tsx wraps the (app) layout, holds active/stepIndex state, and decides whether to render the overlay by comparing the current step’s page field against usePathname().
  • steps.ts defines six fixed steps, all on /dashboard: an intro card, the sidebar rail, the “new agent” button, a second intro card about metrics, the search toolbar, and the sidebar profile footer. Each non-intro step spotlights one element tagged data-tour="<id>" in the DOM.
  • WalkthroughOverlay.tsx renders one step at a time — a dimmed backdrop with a cut-out spotlight around the target (or a centered card with no spotlight for intro steps), a curved dashed arrow with animated dots from the card to the target, and Back / Next / Skip controls. Motion respects prefers-reduced-motion.
  • storage.ts persists progress in localStorage under voicera_walkthrough_pending, voicera_walkthrough_active, and voicera_walkthrough_step — nothing is stored server-side or on the user record.
What triggers it: SignInPage (frontend/src/app/page.tsx) and the signup page (frontend/src/app/signup/page.tsx) both call markWalkthroughPending() when the login/signup response has is_first_login: true. That only sets a localStorage flag; the tour itself starts on the next mount of the (app) layout, which WalkthroughProvider picks up in a useEffect that consumes the pending flag and starts the tour from step 0. A page reload mid-tour resumes from whatever step was last written, rather than restarting. Replaying it: the sidebar footer’s “Walkthrough” button (AppSidebar.tsx) calls restart() from useWalkthrough() and routes to /dashboard, which restarts the same six steps from the beginning — this is the mechanism steps.ts’s final card means by “Replay this walkthrough from here.” It is fully skippable (Skip the tour / Skip the rest, depending on step) and steppable (Back once past step 1, Next/Finish otherwise), and skipping or finishing clears the localStorage state the same way. There is no server-side “has seen onboarding” flag — only the client-visible is_first_login response field decides whether it auto-starts, and only localStorage remembers progress or that it was skipped.

Real features versus scaffolding

Being blunt about it, because the sidebar gives all of these equal weight:
  • Genuine, API-backed features: Agents home, agent creation, agent edit, Numbers, History, Members, Integrations, Account, Campaigns, Knowledge Base, Analytics, Telemetry.
  • Genuine, but not API-backed: the onboarding walkthrough (WalkthroughOverlay.tsx) — a real, fully-built six-step tour triggered on first login/signup and replayable from the sidebar. It is client-only: everything it needs lives in localStorage, with no server persistence and no route of its own. See walkthrough above.
  • Screens that look complete but are sample data: the language map (Languages.tsx), which renders a convincing bubble chart from a hardcoded array and is not currently reachable from any screen. Do not read it as your data.
  • Placeholders: the /walkthrough route — a single card admitting the feature is not built at that URL. Do not confuse it with the real overlay above; they share a name and nothing else.
  • Developer scaffolding: /components is a UI-kit page, self-described as such. It is unlinked from the sidebar and safe to ignore. The standalone /library prompt-module browser was removed; the same nine modules are still reachable from the wizard’s prompt library dialog.
One more piece of scaffolding worth knowing about: frontend/src/app/api/ contains three Next.js route handlers (/api/agents, /api/auth/login, /api/auth/signup) whose own comments call them “a UI-kit demo, not wired to a real database” — one keeps agents in a module-level array that resets on restart, the others return a mock user and a fake token. The live dashboard does not call them; real auth goes to the API’s /users/login and /users/signup. Do not mistake them for a backend.