Carmen LabsCarmen Labs

Sales Dashboard

A complete MView built from a three-column sales CSV.

Example: Sales Dashboard

A minimal but complete MView, end to end. For a simple web view, the spec recommends the html renderer because it needs no framework or build step. Source file:

Month,Client,Total
Jan,Acme,100
Feb,Globex,250
Mar,Initech,175

mview.json

{
  "$schema": "https://schemas.carmenlabs.com/mview/v1/schema.json",
  "id": "sales-dashboard",
  "name": "Sales Dashboard",
  "description": "Executive dashboard generated from sales.csv.",
  "version": "1.0.0",
  "prompt": "Create an executive dashboard from sales.csv showing monthly sales, top clients, and total sales.",
  "runtime": {
    "environment": "web",
    "renderer": "html"
  },
  "author": "Victor Avila",
  "license": "MIT",
  "icon": "./resources/icon.svg",
  "accepts": ["table"],
  "source": {
    "path": "../sales.csv",
    "adapters": ["csv-table"],
    "contentHash": "sha256:eae7fe950b82445103a8e87a23d6db28ddf019c4127ea509bc0338e19b553d87"
  },
  "entry": {
    "parser": "./parser.js",
    "component": "./view.html"
  },
  "settings": {
    "currency": "USD",
    "topClientsLimit": 3
  },
  "metadata": {
    "createdBy": "llm",
    "model": "gpt-5.5",
    "createdAt": "2026-07-11T05:00:00Z"
  }
}

parser.js

export default async function parse(input, context) {
  if (!input || !Array.isArray(input.rows)) {
    throw new Error("Expected a normalized table with rows.");
  }
 
  const rows = input.rows ?? [];
 
  const totalSales = rows.reduce((sum, row) => {
    return sum + Number(row.Total ?? row.total ?? 0);
  }, 0);
 
  const salesByMonth = rows.map((row) => ({
    month: row.Month ?? row.month,
    total: Number(row.Total ?? row.total ?? 0)
  }));
 
  const topClients = [...rows]
    .sort((a, b) => Number(b.Total ?? b.total ?? 0) - Number(a.Total ?? a.total ?? 0))
    .slice(0, context.settings?.topClientsLimit ?? 3)
    .map((row) => ({
      client: row.Client ?? row.client ?? "Unknown",
      total: Number(row.Total ?? row.total ?? 0)
    }));
 
  return { totalSales, salesByMonth, topClients };
}

The parser only consumes the normalized { columns, rows } input from the table capability. It does not read the source file directly; context.settings contains the manifest settings combined with any runtime overrides.

view.html

<!doctype html>
<html lang="en">
  <body>
    <main id="app"></main>
    <script>
      const { data, settings } = window.mview;
      const currency = settings?.currency ?? "USD";
      const format = (value) => `${currency} ${Number(value).toLocaleString()}`;
 
      document.querySelector("#app").innerHTML = `
        <h1>Sales Dashboard</h1>
        <section>
          <h2>Total Sales</h2>
          <strong>${format(data.totalSales)}</strong>
        </section>
        <section>
          <h2>Sales by Month</h2>
          <ul>${data.salesByMonth.map((item) => `<li>${item.month}: ${format(item.total)}</li>`).join("")}</ul>
        </section>
        <section>
          <h2>Top Clients</h2>
          <table>
            <thead><tr><th>Client</th><th>Total</th></tr></thead>
            <tbody>${data.topClients.map((row) => `<tr><td>${row.client}</td><td>${format(row.total)}</td></tr>`).join("")}</tbody>
          </table>
        </section>
      `;
    </script>
  </body>
</html>

An HTML component receives the same props object as every renderer, but the runtime injects it as window.mview before inline scripts run. It must use that global rather than inventing another one or fetching the source file.

Resulting file tree

sales-dashboard.mview/
├── mview.json
├── parser.js
└── view.html

What happens when sales.csv changes

Add a row, and the runtime re-reads the original file, invokes csv-table, normalizes it to { columns, rows }, re-runs parse, and re-renders view.html with the new data. parser.js and view.html don't change unless the shape of the CSV itself changes (e.g. a renamed column) — in which case only the parser needs regenerating. See Security Model → Regeneration.

Sales Dashboard

A complete MView built from a three-column sales CSV.

Example: Sales Dashboard

A minimal but complete MView, end to end. For a simple web view, the spec recommends the html renderer because it needs no framework or build step. Source file:

Month,Client,Total
Jan,Acme,100
Feb,Globex,250
Mar,Initech,175

mview.json

{
  "$schema": "https://schemas.carmenlabs.com/mview/v1/schema.json",
  "id": "sales-dashboard",
  "name": "Sales Dashboard",
  "description": "Executive dashboard generated from sales.csv.",
  "version": "1.0.0",
  "prompt": "Create an executive dashboard from sales.csv showing monthly sales, top clients, and total sales.",
  "runtime": {
    "environment": "web",
    "renderer": "html"
  },
  "author": "Victor Avila",
  "license": "MIT",
  "icon": "./resources/icon.svg",
  "accepts": ["table"],
  "source": {
    "path": "../sales.csv",
    "adapters": ["csv-table"],
    "contentHash": "sha256:eae7fe950b82445103a8e87a23d6db28ddf019c4127ea509bc0338e19b553d87"
  },
  "entry": {
    "parser": "./parser.js",
    "component": "./view.html"
  },
  "settings": {
    "currency": "USD",
    "topClientsLimit": 3
  },
  "metadata": {
    "createdBy": "llm",
    "model": "gpt-5.5",
    "createdAt": "2026-07-11T05:00:00Z"
  }
}

parser.js

export default async function parse(input, context) {
  if (!input || !Array.isArray(input.rows)) {
    throw new Error("Expected a normalized table with rows.");
  }
 
  const rows = input.rows ?? [];
 
  const totalSales = rows.reduce((sum, row) => {
    return sum + Number(row.Total ?? row.total ?? 0);
  }, 0);
 
  const salesByMonth = rows.map((row) => ({
    month: row.Month ?? row.month,
    total: Number(row.Total ?? row.total ?? 0)
  }));
 
  const topClients = [...rows]
    .sort((a, b) => Number(b.Total ?? b.total ?? 0) - Number(a.Total ?? a.total ?? 0))
    .slice(0, context.settings?.topClientsLimit ?? 3)
    .map((row) => ({
      client: row.Client ?? row.client ?? "Unknown",
      total: Number(row.Total ?? row.total ?? 0)
    }));
 
  return { totalSales, salesByMonth, topClients };
}

The parser only consumes the normalized { columns, rows } input from the table capability. It does not read the source file directly; context.settings contains the manifest settings combined with any runtime overrides.

view.html

<!doctype html>
<html lang="en">
  <body>
    <main id="app"></main>
    <script>
      const { data, settings } = window.mview;
      const currency = settings?.currency ?? "USD";
      const format = (value) => `${currency} ${Number(value).toLocaleString()}`;
 
      document.querySelector("#app").innerHTML = `
        <h1>Sales Dashboard</h1>
        <section>
          <h2>Total Sales</h2>
          <strong>${format(data.totalSales)}</strong>
        </section>
        <section>
          <h2>Sales by Month</h2>
          <ul>${data.salesByMonth.map((item) => `<li>${item.month}: ${format(item.total)}</li>`).join("")}</ul>
        </section>
        <section>
          <h2>Top Clients</h2>
          <table>
            <thead><tr><th>Client</th><th>Total</th></tr></thead>
            <tbody>${data.topClients.map((row) => `<tr><td>${row.client}</td><td>${format(row.total)}</td></tr>`).join("")}</tbody>
          </table>
        </section>
      `;
    </script>
  </body>
</html>

An HTML component receives the same props object as every renderer, but the runtime injects it as window.mview before inline scripts run. It must use that global rather than inventing another one or fetching the source file.

Resulting file tree

sales-dashboard.mview/
├── mview.json
├── parser.js
└── view.html

What happens when sales.csv changes

Add a row, and the runtime re-reads the original file, invokes csv-table, normalizes it to { columns, rows }, re-runs parse, and re-renders view.html with the new data. parser.js and view.html don't change unless the shape of the CSV itself changes (e.g. a renamed column) — in which case only the parser needs regenerating. See Security Model → Regeneration.