Back to The Times of Claw

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.

Mark Rachapoom
Mark Rachapoom
·6 min read
Build a CRM Dashboard with Chart.js and DenchClaw

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_deals and v_people views 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.app

Create 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: tab

The 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: 300000

In 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.fetch to 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 →

Mark Rachapoom

Written by

Mark Rachapoom

Building the future of AI CRM software.

Continue reading

DENCH

© 2026 DenchHQ · San Francisco, CA