Manager Trend View
The Dialpad Manager Trend View is a single-file HTML analytics dashboard for call center managers. It renders six time-series charts — call volume, ASA, abandonment rate, service level, AHT, and CSAT/utilization — across selectable 1, 7, 14, or 30-day ranges. Data comes from the Dialpad Stats API v2 via a three-job async fetch pattern, with a fully functional demo mode requiring no credentials.
This file is a sub-page of the parent platform shell. It loads ../nav.js for the site-wide navigation overlay. A missing nav.js produces a 404 in the browser console but does not break the dashboard — all charts, controls, and modes function fully without it.
- Morning Review: Manager scans 7D view for overnight volume, SL, and CSAT before shift briefing.
- Intra-Day Monitoring: Live + Production Mode (5-min auto-refresh) tracks SLA compliance in real time.
- QBR Prep: Switch to 30D range for portfolio-level trend screenshots for client reporting.
- Agent Efficiency: CSAT + utilization split panel surfaces workload vs. quality patterns.
- Dialpad Analytics portal — requires portal login, no multi-metric view, no date range flexibility.
- Manual CSV exports — the dashboard automates the POST-then-poll export pattern entirely.
- Spreadsheet trend tracking — derived metrics (abandon rate, SL%, utilization) computed automatically.
- Separate CSAT reports — CSAT fetched in the same cycle as call stats, rendered on the same time axis.
renderCharts(stats) pipeline — only the data source differs.
A single self-contained HTML file. No build step. No server-side component. All logic executes in the browser. The rendering pipeline is linear: data in → normalize → charts out.
MODE. If 'demo': calls genDemoData(DAYS) immediately and passes result to renderCharts(stats). If 'live': calls fetchLiveData(). API key is also restored from localStorage('dp_api_key') before this fires — if a saved key is found, MODE is pre-set to 'live'.fetchLiveData() issues three sequential POST /api/v2/stats jobs: stat_type=calls, stat_type=onduty, and stat_type=csat. Each job is polled via GET /api/v2/stats/{id} until the CSV is ready. Total wall time: approximately 60–90 seconds per full load.parseCSV() function, merged into a dataByDate map keyed by YYYY-MM-DD, and all derived metrics are computed exactly once here. Returns a single normalized stats object — or null on empty input, which triggers demo fallback.stats object. Each chart is created via mkC(id, type, data, opts) which destroys any existing chart at that canvas ID before creating a new one (prevents memory leaks on repeated refreshes). Rail stats, KPI health badges, and trend arrows all update in the same synchronous pass.| Variable | Default | Purpose |
|---|---|---|
| CHARTS | {} | Registry of active Chart.js instances keyed by canvas ID. Used by mkC() to destroy before re-create. |
| MODE | 'demo' | Current mode: 'demo' or 'live'. Controls branch taken in loadData(). |
| DAYS | 7 | Range in days. Set by range buttons (1/7/14/30). Passed to genDemoData() and the API POST body. |
| PRODUCTION | false | When true, auto-refresh runs every 5 minutes via _prodTimer interval handle. |
| TARGET_ID | 'off_1a2b' | Dialpad target ID for the API POST body. Changed by the TARGET dropdown. |
| API_KEY | '' or restored | Dialpad API key. Set by saveKey(), persisted base64-encoded in localStorage. |
| _prodTimer | null | setInterval handle for Production Mode. Cleared on mode exit to prevent ghost refreshes. |
Two persistent chrome elements sit above the 2×3 chart grid: the Rail (48px) and the Status Bar (28px). Always visible regardless of range or mode.
| Stat | ID | Color | Value | Good Direction |
|---|---|---|---|---|
| Svc Level | rb-sl | GREEN | Avg service level % over range | ↑ = green |
| ASA Avg | rb-asa | CYAN | Avg speed to answer in seconds | ↓ = green (inverted) |
| Abandon Rate | rb-abn | ORANGE | Avg abandonment % | ↓ = green (inverted) |
| Calls (Nd) | rb-vol | BLUE | Total inbound call volume over range | ↑ = green |
| AHT Avg | rb-aht | PURPLE | Avg handle time in minutes | ↓ = green (inverted) |
| CSAT Avg | rb-csat | YELLOW | Avg CSAT score (1–5) | ↑ = green |
| Agent Util | rb-util | GREEN | Avg agent utilization % | ↑ = green (target band 75–85%) |
setRange(days, btn). Keyboard shortcuts: 1, 2/7, 3, 4.TARGET_ID and TARGET_NAME, then calls loadData(). Replace demo values with real Dialpad callcenter IDs for live use.saveKey() — stores key as base64 in localStorage('dp_api_key'), sets MODE to live, triggers load. Key persists across page reloads.loadData() immediately. Demo re-generates; live triggers a full three-job fetch.PRODUCTION. When on: setInterval(loadData, 300000) (5 min). When off: clears interval via _prodTimer. Requires saved API key.| Element | What It Shows |
|---|---|
| ✓ API | POST /api/v2/stats → GET /api/v2/stats/{id} — confirms the active endpoint pattern |
| stat_type=calls | Available fields: inbound, outbound, missed, abandoned, asa, aht, service_level |
| stat_type=onduty | Available fields: available, occupied, wrapup (agent duty hours) |
| stat_type=csat | Available fields: response (1–5 survey score per row) |
| N/A FCR N/A Disconnect N/A Forecast | Permanently unavailable — see Section 13 for full explanation |
| KPI badges (SL, ASA, ABN, UTIL, CSAT) | Health-colored badge per KPI. Green (↑), yellow (◊), red (↓). Updated on every render. |
| fetch-status | Text: "Ready" / "running…" (yellow) / "Demo data · Nd" (green) / error message (red) |
| last-fetched | Timestamp of last successful data load. Set by setLastFetched(). |
Canvas: c-vol · Panel: cp-vol · Grid: col 1 row 1 (largest panel) · Click to open Detail Pane
Stacked bar chart showing daily call counts by outcome across the selected range. Header displays total period volume. Clicking the panel opens the Detail Pane with full API field reference and the exact POST body used.
| Dataset | API Field | Color | Notes |
|---|---|---|---|
| Inbound | inbound_calls | CYAN | Total inbound calls. Base of stack. |
| Outbound | outbound_calls | GREEN | Agent-placed outbound calls. |
| Missed | missed | ORANGE | Rang but not answered, not queued. |
| Abandoned | abandoned | RED | Entered queue but hung up before answer. Used in abandon rate formula. |
is_today: false for stable completed-day data only.
Canvas: c-asa · Panel: cp-asa · SLA target line at 20 seconds (yellow dashed)
asa from stat_type=calls. Dialpad exports ASA in minutes. Normalization multiplies × 60 for seconds display: d.asa_s = parseFloat(r.asa) * 60. Do not re-convert.asa field is in minutes, not seconds. The × 60 in normalizeStats() is essential. A missing multiply produces ASA of 0.2–0.5, making every day appear green when it may be over threshold.
Canvas: c-abn · Panel: cp-abn · SLA target line at 5% (red dashed)
Derived metric — not a direct API field. Header value and color code red/orange/green vs. the 5% SLA ceiling.
abandonRate = abandoned / inbound_calls × 100. Computed per date in normalizeStats(). Returns null if inbound_calls = 0 (chart shows gap).Canvas: c-sl · Panel: cp-sl · SLA target line at 80% (green dashed)
service_level — a count of calls meeting the SLA threshold, not a percentage. Dashboard computes: service_level / answered × 100.Canvas: c-aht · Panel: cp-aht · SLA band: 5–7 min (two purple dashed lines)
AHT (uppercase) or aht. Normalization checks both: r.AHT || r.aht. Returned in minutes. Contact Center targets only.v < 5 → cyan (too fast), v ≤ 7 → purple (on-target), v > 7 → red (too slow). Null values = transparent.wrapup_duration, avg_wrapup_duration, talk_time — available in the export but not currently displayed.Canvas IDs: c-csat (left) + c-util (right) · Panel: cp-csat · Click to open Detail Pane
The sixth panel is a CSS grid split into two equal halves inside the canvas parent div. Left: CSAT bar chart (1–5 scale). Right: agent utilization filled line chart.
stat_type=csat with export_type=records. Each CSV row is one survey response. The response field = score (1–5).csatByDate, then averaged per day in normalizeStats(). Days with no responses = null (chart gap).≥4.5 → green / ≥4.0 → yellow / else → orange. SLA target line at 4.5 (yellow dashed).stat_type=onduty. Fields: available, occupied, wrapup (all in agent-hours).utilization = (occupied + wrapup) / (available + occupied + wrapup) × 100. Returns null if total = 0.The dashboard uses the Dialpad Stats API v2 — an async job-pattern API. You POST a job, receive a job ID, wait, then poll GET until the CSV is ready. There is no synchronous endpoint that returns data immediately.
| Method | Endpoint | Purpose |
|---|---|---|
| POST | https://dialpad.com/api/v2/stats | Initiate a stats export job. Returns { id: "job_xxx" }. Three calls per load cycle. |
| GET | https://dialpad.com/api/v2/stats/{id} | Poll for job completion. Returns CSV directly (content-type text/csv) or a JSON status with url pointing to the CSV download. |
{
"export_type": "stats", // "stats" for daily aggregates; "records" for CSAT individual rows
"stat_type": "calls", // "calls" | "onduty" | "csat"
"target_type": "callcenter", // "callcenter" | "department" | "office" | "user"
"target_id": "off_1a2b", // Dialpad callcenter / office / user ID
"timezone": "America/Chicago", // IANA timezone — affects day boundary calculation
"is_today": false, // false = completed historical days; true = includes live today (~30min refresh)
"days_ago_start": 7, // How many days back to start — DAYS state variable
"days_ago_end": 0 // 0 = through yesterday; 1 = exclude yesterday
}
"export_type": "records" (individual survey rows). Using "stats" for CSAT returns an aggregate — not per-row responses — and cannot be averaged per day. The dashboard correctly uses records for CSAT only.
Authorization: Bearer <api_key>. Key stored in localStorage('dp_api_key') as base64. Transmitted only to dialpad.com.dp_live_xxxxxxxxxxxx. Generated in Dialpad Admin under API Settings.fetch() from a browser will be blocked. A proxy server is required for production. See Section 17.Implemented in fetchLiveData() and pollStats(id, headers, base, waitStep, getStep). All fetch steps are visualized in the Fetch Overlay modal with a progress bar.
Authorization and Content-Type: application/json. Returns { id: "job_xxx" }. Throws on non-OK response or missing ID.await wait(18000). Polls every 8 seconds, up to 12 attempts. On each GET: checks content-type for CSV, or checks JSON for status: "done" and a url field to fetch the CSV from.A custom RFC-4180 compliant tokenizer that handles quoted fields, embedded commas inside quoted fields, escaped quotes (""), Windows (\r\n) and Unix (\n) line endings. Returns an array of objects keyed by the header row. Returns empty array on blank input. Replaces a naive split(',') which would corrupt fields containing commas (e.g. client names, issue descriptions).
Merges all three CSV datasets into one dataByDate map keyed by YYYY-MM-DD. All derived metrics are computed exactly once here — renderCharts() only consumes pre-computed values.
| Derived Metric | Formula | Returns Null When |
|---|---|---|
| Service Level % | service_level / answered × 100 | answered = 0 |
| Abandon Rate % | abandoned / inbound_calls × 100 | inbound_calls = 0 |
| ASA (seconds) | asa (minutes) × 60 | asa field missing |
| Agent Utilization % | (occupied + wrapup) / (avail + occ + wru) × 100 | total hours = 0 |
| CSAT daily avg | avg(response) per date group | no responses that day |
spanGaps: true) and as transparent bars in bar charts. This correctly communicates missing data rather than falsely showing zero performance.
Dialpad exports include a date, date_started, or Date field — normalization checks all three. Dates are extracted as YYYY-MM-DD by splitting on T. The final dataset is sorted ascending using Object.keys(dataByDate).sort().
The following metrics are displayed as N/A in the status bar because they are not buildable from the Dialpad Stats API v2 in any form. These limitations are also documented inside the Detail Pane for each chart.
| Metric | Why Not Available |
|---|---|
| FCR (First Call Resolution) | No First Call Resolution field in any Dialpad stats or records export. FCR requires call-outcome tagging at the agent level, which Dialpad does not expose via API. |
| Disconnect Rate | No mid-call drop metric. API tracks pre-answer abandonment and unanswered, but not calls that dropped during active conversation. |
| Forecast Accuracy | Dialpad provides no forecast vs. actual comparison. Workforce management forecasting data is not part of the Stats API schema. |
| Escalation Rate | Only transfer counts (answered_transferred, transferred_out) are available. These are raw transfer counts — there is no flag to distinguish a warm transfer from an escalation. |
- Service Level and AHT: Contact Center targets (
target_type=callcenter) only. Null for department, office, and user targets. - CSAT: Requires Dialpad's CSAT feature enabled on the account. Without it, the csat export returns an empty CSV.
- Today data: Refreshes ~30 min in Dialpad. Use
is_today: falsefor stable completed-day data. - Large ranges: Ranges above 90 days may experience significantly longer job processing or timeouts. Not documented by Dialpad; observed behavior.
- CORS: Direct browser requests to the Dialpad Stats API are blocked. A proxy is required for production. See Section 17.
Both genDemoData() and normalizeStats() produce the same stats object. renderCharts() consumes only this shape and does not know or care where data originated.
{
dates: ['YYYY-MM-DD', ...], // N dates ascending — all chart x-axes
calls: {
inbound: [number|null], // inbound_calls per day
outbound: [number|null], // outbound_calls per day
missed: [number|null], // missed calls per day
abandoned:[number|null], // abandoned calls per day
answered: [number|null], // answered calls per day (used in SL% formula)
aht: [number|null] // avg handle time in minutes (CC targets only)
},
service: {
serviceLevel: [number|null], // % — pre-computed: service_level / answered × 100
asa: [number|null], // seconds — pre-computed: asa_minutes × 60
abandonRate: [number|null] // % — pre-computed: abandoned / inbound × 100
},
agents: {
available: [number|null], // agent-hours available
occupied: [number|null], // agent-hours on calls
wrapup: [number|null], // agent-hours in post-call wrap-up
utilization: [number|null] // % — pre-computed: (occ+wru) / total × 100
},
quality: {
csat: [number|null] // 1–5 avg per day — pre-computed from survey records
}
}
All thresholds are hard-coded in renderCharts(). To change a threshold, update both the slaLine() value and the corresponding kpiBadge threshold array.
| Metric | Green (OK) | Yellow (Warn) | Red (Crit) | Reference Line(s) |
|---|---|---|---|---|
| Service Level | ≥ 80% | 70–79% | < 70% | 80% green dashed |
| ASA | < 20s | 20–30s | > 30s | 20s yellow dashed |
| Abandon Rate | < 3% | 3–5% | > 5% | 5% red dashed |
| AHT | 5–7 min | 7–9 min | <5 or >9 min | 5m + 7m purple dashed |
| CSAT | ≥ 4.5/5 | 4.0–4.49 | < 4.0 | 4.5 yellow dashed |
| Agent Util | 75–85% | 60–74% | <60% or >85% | 75% + 85% purple dashed |
type:'line', pointRadius:0, and borderDash set. The slaLine(labels, val, color, dashes) helper generates them. No Chart.js Annotation plugin dependency.
The dashboard ships in demo mode. Data is generated by genDemoData(DAYS) using bounded Math.random() ranges. Weekend days produce ~40% lower call volumes. Every demo refresh regenerates all data — no persistence between refreshes.
- No API calls are made in demo mode. The Dialpad API is never contacted.
- Charts populate instantly — no fetch overlay, no wait period.
- Weekend dips simulated —
dt.getDay()%6===0uses 0.4× base multiplier. - ±20% daily variability produces realistic trend shapes, not flat lines.
- CSAT null days occur — not every demo day generates a CSAT response, mirroring real low-survey-volume days.
localStorage('dp_api_key') as base64. MODE switches to live and a data load fires automatically.POST /api/v2/stats and GET /api/v2/stats/{id} with the Authorization: Bearer header injected server-side. Update var base in fetchLiveData() to your proxy URL.<option value="..."> values with real Dialpad callcenter or office IDs. Update display names to match your clients."timezone": "America/Chicago" in the POST body to the IANA timezone of your call center. This controls day boundary calculations.setInterval. Toggle off before leaving the page to clear the interval handle.#ktc-demo-bar div and its CSS block. Remove body { padding-top: 36px !important; } override. Ensure nav.js is accessible at ../nav.js — or update the script src to an absolute URL. A missing nav.js only affects site navigation, not the dashboard itself.All items below activate when proxy is live. Features in demo mode today that require proxy to become functional are noted.
- Deploy proxy server — Azure Function, Cloudflare Worker, or Express. Must forward
POST /api/v2/statsandGET /api/v2/stats/{id}to Dialpad withAuthorization: Bearerinjected server-side. - Update
basevariable — InfetchLiveData(), changevar base = 'https://dialpad.com/api/v2/'to your proxy base URL (e.g.'/api/proxy/dialpad/'). - Move API key server-side — For shared kiosk/wall displays, store the key as an environment variable in the proxy. Remove the KEY input from the rail and let the proxy inject auth headers silently.
- Replace TARGET_IDs — Update all
<option value="...">values and display names with real Dialpad callcenter / office IDs. - Set correct timezone — Update
"timezone"in POST body to match each target's operational timezone. - Verify CSAT feature enabled — Confirm in Dialpad Admin that CSAT surveys are enabled for the target. Without this, the csat export is empty and CSAT chart shows all gaps.
- Test full async fetch cycle — Run a live load and observe the Fetch Overlay. All steps A–H should complete with green checkmarks. Step C/E/G timeout = increase attempts or reduce DAYS range.
- Test Production Mode — Enable Production, let it run 2 cycles (10 min). Verify 5-min interval fires, charts update, interval clears cleanly on toggle-off.
- Remove demo KTC bar — Delete
#ktc-demo-bardiv and associated CSS for standalone deployment.
loadData() completes first render. If dashes persist after toast appears, open console — likely Chart.js CDN failed to load.target_type=callcenter target. Department and office targets will never populate these charts.normalizeStats() was removed. Restore: d.asa_s = parseFloat(r.asa || 0) * 60. Dialpad exports ASA in minutes.attempt loop limit in pollStats().dialpad.com are blocked. Deploy a proxy and update base in fetchLiveData(). See Section 17.export_type: "stats" instead of "records", or no surveys submitted in range._prodTimer is an in-memory handle — does not persist across reloads. Re-enable Production Mode manually after reload.localStorage — not encrypted. On shared kiosk displays, move the key server-side to the proxy and remove the KEY input from the UI.../nav.js resolves, or update the script src to an absolute URL. Dashboard functions fully without nav.js — it only drives the site navigation overlay.mkC() calls chart.destroy() before recreating. If you add custom charts outside mkC(), call destroy() manually. Long sessions (8+ hours) without this accumulate canvas memory.| What to Change | Where | How |
|---|---|---|
| Date range options | Range buttons in rail HTML | Add <button class="range-btn" onclick="setRange(N,this)">ND</button> |
| Target offices / call centers | <select id="target-sel"> | Replace demo off_xyyy option values with real Dialpad target IDs |
| Timezone | fetchLiveData() → body object | Change "timezone": "America/Chicago" to your IANA timezone |
| SLA thresholds | renderCharts() | Update both slaLine() value and the kpiBadge threshold array for each metric |
| Production auto-refresh interval | toggleProduction() | Change 5 * 60 * 1000 to desired milliseconds |
| Poll wait / retry timing | pollStats() | Change await wait(18000) (initial) and await wait(8000) (retry interval) |
| Chart color palette | :root { } CSS block | Update --green, --accent, --red, --pur, --yellow |
| KTC nav bar | #ktc-demo-bar div + style block | Remove entire div and associated CSS for standalone deployment |