A Google Apps Script dashboard that reads your Cashew CSV export from Google Drive and serves a dark-themed financial report with 18 widgets, interactive charts, and PDF export. No hosting cost, no external services -- runs entirely on Google's infrastructure.
The dashboard is live: it re-reads your CSV and re-computes everything on every page load. There is no deploy step when your data changes -- you visit the URL and see the latest numbers.
- Cashew exports transactions to CSV. The Outbox auto-syncs
outbox.csvto a/Cashew/folder on your Google Drive. - You deploy two files to Google Apps Script.
Code.gs(server-side metrics + widget renderers) andDashboard.html(CSS shell + Chart.js). - Visit the web app URL. GAS reads the CSV, computes all metrics, renders 18 widgets server-side, and serves the complete HTML page. Takes 2-5 seconds.
- Navigate and explore. Use the month picker to move between months. Click doughnut segments to drill down. Expand tables to see all subcategories. Hit "Save as PDF" to archive a snapshot to Drive.
flowchart LR
subgraph Cashew["Cashew App"]
EXP["Export Outbox"]
end
subgraph Drive["Google Drive"]
CSV["/Cashew/outbox.csv"]
end
subgraph GAS["GAS Web App"]
READ["readCSVFromDrive_()"]
PARSE["parseCSV_()"]
COMPUTE["computeMetrics_()"]
RENDER["WIDGET_RENDERERS\n(18 functions)"]
SHELL["Dashboard.html\n(3-line for-loop)"]
end
BROWSER["Your browser / phone"]
EXP --> CSV
CSV --> READ --> PARSE --> COMPUTE --> RENDER --> SHELL --> BROWSER
Every page load triggers the full pipeline. There is no caching or background job -- the dashboard is stateless and always fresh.
sequenceDiagram
participant U as Browser
participant G as doGet()
participant D as Google Drive
participant R as WIDGET_RENDERERS
participant T as Dashboard.html
U->>G: GET ?month=2026-03
G->>D: readCSVFromDrive_()
D-->>G: CSV content
G->>G: parseCSV_() → filterActual_() / filterUpcoming_()
G->>G: computeMetrics_()
loop each visible widget
G->>R: renderer(metrics, widgetConfig)
R-->>G: HTML string
end
G->>T: template.renderedWidgets = [...]
T->>T: for-loop injects widget HTML
T-->>U: Complete page with CSS + Chart.js
- Cashew app with the Outbox export enabled (syncs to Google Drive)
- A Google account (Drive + Apps Script)
- Categories and subcategories configured in Cashew -- see Category Taxonomy for the expected structure
- Upload your Cashew
outbox.csvto a folder namedCashewon Google Drive (or use the includedsample_outbox.csvfor testing) - Go to script.google.com → New project → name it "Cashew Reports"
- Replace the default
Code.gscontent with theCode.gsfrom this repo - Create a new HTML file named
Dashboard→ paste theDashboard.htmlcontent - Edit the CONFIG section in
Code.gsto match your Cashew categories (see Category Taxonomy) - Deploy → New deployment → Web app → "Only myself" → Deploy → bookmark the URL
Full step-by-step with troubleshooting: Setup Guide
The dashboard has 6 sections containing 18 widgets. Each section answers a different question about your finances.
| Section | Widgets | What it answers |
|---|---|---|
| Monthly Summary | burn_rate, money_received, savings_rate, data_quality | How did this month go? |
| Where Did It Go | cat_breakdown, disc_split, lifestyle_tax | Where is the money going? |
| Spending Patterns | utility_bills, grocery_trend | Are recurring expenses on track? |
| Trends | burn_trend, biggest_increases, top_subcats | Are things getting better or worse? |
| Income & Savings | investment, rewards_refunds, rent_trend | What came in? |
| Structural Costs | household_cost, cab_transit, people_payments | What are the non-negotiable baselines? |
The dashboard is built on a pattern that makes it easy to add, remove, or modify widgets without touching the HTML template.
The contract: every widget is a function in the WIDGET_RENDERERS object. It receives the full metrics object and its own config entry, and returns an HTML string.
WIDGET_RENDERERS.my_widget = function(metrics, w) {
var d = metrics.myWidgetData;
return `<div class="panel ws-${w.size}" data-widget="my_widget">
<div class="panel-title">My Widget</div>
<div class="kpi-value">${fmt_(d.value)}</div>
</div>`;
};The shell: Dashboard.html has zero knowledge of individual widgets. The entire widget injection is a 3-line loop:
<div class="widget-grid">
<? for (var i = 0; i < renderedWidgets.length; i++) { ?>
<?!= renderedWidgets[i].html ?>
<? } ?>
</div>Chart data bridge: since GAS serves pre-rendered HTML, chart data passes from server to client via <script type="text/json" id="..."> blocks embedded in widget HTML. Client-side IIFEs read them with getElementById + JSON.parse. Each IIFE is guarded (if (!el) return) so it silently skips when the widget is hidden.
flowchart TD
A["New widget idea"] --> B{"Needs a Chart.js canvas?"}
B -->|No| C{"Needs click interactions?"}
B -->|Yes| D["<b>Chart.js widget</b>\nCode.gs: 3 changes\nDashboard.html: 1 IIFE"]
C -->|No| E["<b>Static widget</b>\nCode.gs: 3 changes\nDashboard.html: 0 changes"]
C -->|Yes| F{"Reuses existing toggle?"}
F -->|Yes| E
F -->|No| G["<b>Interactive widget</b>\nCode.gs: 3 changes\nDashboard.html: 1 JS function"]
style E fill:#1a3a2a,stroke:#34d399,color:#e1e4ed
style D fill:#1a2a3a,stroke:#60a5fa,color:#e1e4ed
style G fill:#2a2a1a,stroke:#fbbf24,color:#e1e4ed
Widget config is declared in CONFIG.WIDGETS -- an array that controls render order, visibility, and sizing:
{ id: "section_title", visible: true, title: "Monthly Summary — {month}" },
{ id: "burn_rate", visible: true, size: "small" },Reorder = move entries in the array. Hide = set visible: false. Resize = change size ("small" 3-col, "third" 4-col, "medium" 6-col, "large" 12-col).
Full details: Widget Guide -- config reference, chart data bridge examples, step-by-step instructions for adding each widget type.
These 7 widgets iterate dynamically over whatever categories and subcategories exist in your CSV. They auto-discover your data and produce useful output regardless of how you have set up your Cashew categories.
| Widget | Type | What it shows |
|---|---|---|
| burn_rate | KPI card | Total monthly expenses (excl. configurable exclusion list) with MoM % delta and top 3 subcategories |
| money_received | KPI card | Total income with MoM % delta and per-category breakdown |
| savings_rate | KPI card | (Core Income - Burn) / Core Income, trailing 3 months, with pp delta vs previous quarter |
| cat_breakdown | Full table | Every expense category with colored bar, amount, and % of burn -- sorted by amount |
| burn_trend | Line/area chart | 6-month burn rate trend with red highlight when current month is near the max |
| top_subcats | Expandable table | Top 5 subcategories by rolling 12-month spend with QoQ trend arrows (expandable to all) |
| biggest_increases | Expandable table | Subcategories with the largest quarter-over-quarter spend increases |
These widgets only reference configurable exclusion lists (
BURN_RATE_EXCLUDED,CORE_INCOME_CATEGORIES) -- not hardcoded category names. They will work with any Cashew export.
These 11 widgets are built for a specific category structure. They ship pre-configured and work out of the box if you use the same categories. If your categories differ, you have three options:
- Adopt this taxonomy in Cashew (recommended if starting fresh)
- Edit the CONFIG lists in Code.gs to match your own categories
- Set
visible: falseto hide widgets that don't apply to your setup
| Widget | Type | What it shows | CONFIG dependency |
|---|---|---|---|
| disc_split | Doughnut + drill-down | Non-Disc / Semi / Discretionary split with per-category breakdown | DISC_CATEGORIES, NON_DISC_CATEGORIES, SEMI_DISC_CATEGORIES |
| lifestyle_tax | KPI card | Eating Out + Personal Purchases as % of burn | Hardcoded: Food > Eating Out, Personal Purchases |
| data_quality | Badge panel | Uncategorized + Untraced as % of burn (target: under 2%) | Hardcoded: Misc Expense > Uncategorized/Untraced Expense |
| utility_bills | Checklist | Paid / upcoming / missing for each expected monthly bill | EXPECTED_UTILITY_BILLS |
| grocery_trend | Bar chart | 6-month grocery spend with average and deviation highlight | Hardcoded: Home Essentials > Groceries |
| investment | Card + breakdown | Monthly investment total with subcategory breakdown | INVESTMENT_SUBCATEGORIES |
| rewards_refunds | Card + breakdown | Cashback + refunds recovered as % of expenses | REWARDS_SUBCATEGORIES |
| household_cost | Card + breakdown | 12-month running cost of essentials | HOUSEHOLD_RUNNING_CATEGORIES |
| cab_transit | Doughnut + insight | Cab vs Metro + Public Transport with savings estimate | Hardcoded: Transit > Cab / Metro / Public Transport |
| people_payments | Card + breakdown | Household Staff + Family Transfer + Gifts + Donations | PEOPLE_PAYMENT_FILTERS |
| rent_trend | Dual-line chart | 6-month rent income trend split by property | Hardcoded: Passive Income > Rent |
Full calculation logic for all 18 widgets: Widget Catalog
Category taxonomy and CONFIG reference: Category Taxonomy
Common tasks at a glance:
| What you want to do | Where |
|---|---|
| Hide a widget | Set visible: false in CONFIG.WIDGETS |
| Reorder or resize widgets | Move entries in CONFIG.WIDGETS, change size field |
| Change category classifications | Edit CONFIG arrays in Code.gs (DISC_CATEGORIES, etc.) |
| Change expected utility bills | Edit EXPECTED_UTILITY_BILLS in CONFIG |
| Add a new widget | See the Widget Guide |
| Change currency symbol | Edit fmt_() and fK_() in Code.gs |
Full customization guide with code examples: Customization Guide
| File | Purpose |
|---|---|
Code.gs |
Server-side GAS script: CONFIG, CSV reader/parser, metric computation (15 functions), rendering helpers, 18 widget renderers (~1300 lines) |
Dashboard.html |
Client-side shell: CSS (dark theme, 12-col grid, responsive), 3-line widget loop, Chart.js inits (5 charts), interaction JS (~430 lines) |
WIDGET_CATALOG.md |
All 18 widgets with calculation logic, formulas, and data model notes |
sample_outbox.csv |
Test dataset (26 rows, single month) for verifying the setup |
resources/SETUP_GUIDE.md |
Step-by-step deployment with troubleshooting |
resources/WIDGET_GUIDE.md |
Widget renderer pattern deep-dive, config reference, adding new widgets |
resources/CATEGORY_TAXONOMY.md |
Full category/subcategory tree and CONFIG reference table |
resources/CUSTOMIZATION.md |
All customization tasks with code examples |
This dashboard is the reporting companion to Mail2Cashew-TPS, which automates transaction entry into Cashew from bank emails and SMS.
flowchart LR
subgraph INPUT["Transaction Entry"]
TPS["Mail2Cashew-TPS\n(emails/SMS → Cashew)"]
end
subgraph CASHEW["Cashew App"]
APP["Budget Tracker\n(categories, accounts)"]
end
subgraph REPORTING["Reporting"]
DASH["Cashew-Reports\n(CSV → dashboard)"]
end
TPS -->|"auto-categorized\ntransactions"| APP
APP -->|"Outbox CSV\nexport"| DASH
You do not need TPS to use this dashboard -- any Cashew CSV export works. But if you use both, your TPS categorySubcategoryKeywordMap and this dashboard's CONFIG should use the same category and subcategory names so that transactions categorized by TPS render correctly in the dashboard widgets.

