Build a CRM Dashboard with Chart.js and DenchClaw
Step-by-step guide to building a live CRM analytics dashboard using Chart.js and the DenchClaw App Builder with real DuckDB data.
Build a CRM Dashboard with Chart.js and DenchClaw
If you want to visualize your CRM data in real time — pipeline by stage, revenue by month, lead sources — the DenchClaw App Builder gives you a direct path. You write a .dench.app folder with an index.html, get the window.dench bridge API auto-injected, and query your DuckDB database directly from the browser.
This guide walks through building a complete CRM dashboard with Chart.js: bar charts for pipeline stages, a line chart for monthly revenue, and a donut chart for lead sources. Total build time: about 30 minutes.
What You're Building#
A dashboard Dench App that:
- Queries your DuckDB
v_dealsandv_peopleviews in real time - Renders three Chart.js charts (bar, line, donut)
- Auto-refreshes every 5 minutes via
dench.events - Lives in your DenchClaw sidebar as a persistent tab
Prerequisites: DenchClaw installed (npx denchclaw), a working CRM with deals and contacts. See getting started with DenchClaw.
Step 1: Create the App Folder#
Every Dench App lives in ~/.openclaw-dench/workspace/apps/:
mkdir -p ~/.openclaw-dench/workspace/apps/crm-dashboard.dench.app
cd ~/.openclaw-dench/workspace/apps/crm-dashboard.dench.appCreate the manifest file .dench.yaml:
name: CRM Dashboard
description: Real-time pipeline and revenue charts
icon: bar-chart-2
version: 1.0.0
permissions:
- read:crm
display: tabThe read:crm permission grants the app access to your DuckDB views. The display: tab makes it appear as a full tab in the sidebar (as opposed to a compact widget).
Step 2: Load Chart.js and Set Up HTML#
Create index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRM Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
body {
font-family: system-ui, sans-serif;
background: #0f172a;
color: #e2e8f0;
margin: 0;
padding: 24px;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 24px;
}
.card {
background: #1e293b;
border-radius: 12px;
padding: 20px;
}
.card.wide { grid-column: span 2; }
h2 { margin: 0 0 16px; font-size: 14px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }
canvas { width: 100% !important; }
</style>
</head>
<body>
<div class="grid">
<div class="card wide">
<h2>Monthly Revenue</h2>
<canvas id="revenueChart"></canvas>
</div>
<div class="card">
<h2>Pipeline by Stage</h2>
<canvas id="pipelineChart"></canvas>
</div>
<div class="card">
<h2>Lead Sources</h2>
<canvas id="sourcesChart"></canvas>
</div>
</div>
<script src="dashboard.js"></script>
</body>
</html>Step 3: Query DuckDB and Render Charts#
Create dashboard.js. This is where the window.dench bridge API does the work:
async function loadDashboard() {
// Query 1: Monthly revenue from closed deals
const revenueData = await dench.db.query(`
SELECT
strftime(COALESCE("Close Date", CURRENT_DATE), '%Y-%m') AS month,
SUM(CAST("Value" AS DOUBLE)) AS revenue
FROM v_deals
WHERE "Stage" = 'Closed Won'
AND "Close Date" >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY month
ORDER BY month
`);
// Query 2: Deal count by pipeline stage
const pipelineData = await dench.db.query(`
SELECT "Stage", COUNT(*) AS count
FROM v_deals
WHERE "Stage" IS NOT NULL
GROUP BY "Stage"
ORDER BY count DESC
`);
// Query 3: Lead sources
const sourcesData = await dench.db.query(`
SELECT "Source", COUNT(*) AS count
FROM v_people
WHERE "Source" IS NOT NULL
GROUP BY "Source"
ORDER BY count DESC
LIMIT 6
`);
renderCharts(revenueData, pipelineData, sourcesData);
}
function renderCharts(revenue, pipeline, sources) {
const chartDefaults = {
plugins: { legend: { labels: { color: '#94a3b8' } } },
scales: {
x: { ticks: { color: '#64748b' }, grid: { color: '#1e293b' } },
y: { ticks: { color: '#64748b' }, grid: { color: '#334155' } }
}
};
// Revenue line chart
new Chart(document.getElementById('revenueChart'), {
type: 'line',
data: {
labels: revenue.map(r => r.month),
datasets: [{
label: 'Revenue ($)',
data: revenue.map(r => r.revenue),
borderColor: '#6366f1',
backgroundColor: 'rgba(99, 102, 241, 0.1)',
fill: true,
tension: 0.4
}]
},
options: { ...chartDefaults, responsive: true }
});
// Pipeline bar chart
new Chart(document.getElementById('pipelineChart'), {
type: 'bar',
data: {
labels: pipeline.map(p => p.Stage),
datasets: [{
label: 'Deals',
data: pipeline.map(p => p.count),
backgroundColor: ['#6366f1', '#8b5cf6', '#a78bfa', '#c4b5fd', '#ddd6fe']
}]
},
options: { ...chartDefaults, responsive: true, plugins: { legend: { display: false } } }
});
// Sources donut chart
new Chart(document.getElementById('sourcesChart'), {
type: 'doughnut',
data: {
labels: sources.map(s => s.Source),
datasets: [{
data: sources.map(s => s.count),
backgroundColor: ['#6366f1', '#8b5cf6', '#a78bfa', '#34d399', '#f59e0b', '#ef4444']
}]
},
options: { responsive: true, plugins: { legend: { labels: { color: '#94a3b8' } } } }
});
}
// Auto-refresh on CRM updates
dench.events.on('entry:created', loadDashboard);
dench.events.on('entry:updated', loadDashboard);
// Initial load
loadDashboard();Step 4: Test and Iterate#
Reload the DenchClaw frontend. Your new CRM Dashboard app should appear in the sidebar. Click it and you'll see live charts pulling from your actual data.
If you get empty charts, check that your deals have the expected field names (Stage, Value, Close Date). You can inspect your schema by asking DenchClaw: "What fields does my deals object have?"
To adjust query field names, open dashboard.js and update the column names to match your actual DuckDB PIVOT view. Run:
# Check your actual column names
duckdb ~/.openclaw-dench/workspace/workspace.duckdb "SELECT * FROM v_deals LIMIT 1"Step 5: Add Widget Mode#
Want the dashboard to show on a home screen widget grid? Add this to .dench.yaml:
display: widget
widget:
width: 4
height: 3
refreshMs: 300000In widget mode, the app renders in a compact card. You'll want to simplify index.html to show just one key metric — e.g., total pipeline value — rather than all three charts.
Extending the Dashboard#
Once the basics work, here are natural extensions:
- Filters: Add a date range picker that updates the SQL query parameters
- KPI cards: Show total pipeline value, win rate, average deal size above the charts
- Team view: Break revenue down by assigned rep using a
GROUP BY "Owner"query - Export: Add a download button that calls
dench.http.fetchto generate a CSV
The DenchClaw App Builder model keeps everything local — your data never leaves your machine, and the charts update in real time as deals move through your pipeline.
Frequently Asked Questions#
What version of Chart.js should I use?#
Chart.js 4.x works well. The CDN link in this guide (chart.umd.min.js) is the UMD build, which works without a bundler.
Can I use D3.js instead of Chart.js?#
Yes. The window.dench.db.query() API returns plain JSON arrays, so any charting library works. D3.js gives more control for custom visualizations; Chart.js is faster to set up for standard charts.
How do I add authentication to my app?#
Apps run inside DenchClaw's iframe with same-origin access — they inherit the user's DenchClaw session automatically. No separate auth needed for internal apps.
Can I share this dashboard with my team?#
Yes. Share the entire .dench.app folder. Anyone with DenchClaw can drop it in their apps/ directory and it will appear in their sidebar with their own data.
How do I handle missing fields in the DuckDB query?#
Use COALESCE to handle NULLs: COALESCE("Field Name", 'Unknown'). If a field doesn't exist in your schema, DenchClaw will return an error — check field names with dench.db.query("DESCRIBE v_deals").
Ready to try DenchClaw? Install in one command: npx denchclaw. Full setup guide →
