infra/stacks/monitoring/modules/monitoring/dashboards/wealth.json
Viktor Barzin d48e222054 monitoring: lock Finance (Personal) folder to admin + fix cash classification
Folder ACL:
- Move uk-payslip + wealth dashboards to a new "Finance (Personal)"
  folder; job-hunter + fire-planner stay in "Finance" (open).
- New null_resource calls Grafana's folder permissions API after the
  dashboard sidecar materialises the folder, setting an admin-only
  ACL ({Admin: 4}). Default Viewer/Editor inheritance is overridden,
  so anonymous-Viewer (auth.anonymous=true) is denied. Server-admin
  always retains access.
- Verified: anonymous → 403 on uk-payslip + wealth, 200 on
  control dashboards (node-exporter); admin → 200 on all.

Wealth cash fix:
- Wealthfolio dumps WORKPLACE_PENSION wrappers entirely into
  cash_balance because it doesn't track underlying fund holdings.
  Reclassify pension cash as invested in the "Cash vs invested"
  panel so the cash series reflects actual uninvested broker cash
  (~£16k T212 ISA + Schwab) instead of phantom £154k.

  Pre-fix:  cash=£153,789 / invested=£870,282 / total=£1,024,071
  Post-fix: cash=£16,064  / invested=£1,008,008 / total=£1,024,071
2026-04-25 23:11:26 +00:00

451 lines
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {"type": "datasource", "uid": "grafana"},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"description": "Wealth — net worth, contributions, and growth over time. Backed by the wealthfolio_sync PG mirror of Wealthfolio's SQLite, refreshed hourly by the pg-sync sidecar.",
"editable": true,
"fiscalYearStartMonth": 0,
"id": null,
"links": [],
"panels": [
{
"id": 1,
"title": "Net worth (current)",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 6, "x": 0, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "fixed", "fixedColor": "green"},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "SELECT SUM(total_value) AS net_worth FROM daily_account_valuation WHERE valuation_date = (SELECT MAX(valuation_date) FROM daily_account_valuation)"
}
]
},
{
"id": 2,
"title": "Net contribution (cumulative)",
"description": "Total deposits minus withdrawals across all accounts.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 6, "x": 6, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "fixed", "fixedColor": "blue"},
"decimals": 0
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "SELECT SUM(net_contribution) AS contribution FROM daily_account_valuation WHERE valuation_date = (SELECT MAX(valuation_date) FROM daily_account_valuation)"
}
]
},
{
"id": 3,
"title": "Growth (unrealised)",
"description": "Net worth minus net contribution — the gain on everything you've put in.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 6, "x": 12, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "currencyGBP",
"color": {"mode": "thresholds"},
"decimals": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "green", "value": 0}
]
}
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "SELECT (SUM(total_value) - SUM(net_contribution)) AS growth FROM daily_account_valuation WHERE valuation_date = (SELECT MAX(valuation_date) FROM daily_account_valuation)"
}
]
},
{
"id": 4,
"title": "ROI %",
"description": "Growth / net contribution × 100. Excludes accounts with zero/negative contribution (Schwab) to avoid distortion.",
"type": "stat",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 4, "w": 6, "x": 18, "y": 0},
"fieldConfig": {
"defaults": {
"unit": "percent",
"color": {"mode": "thresholds"},
"decimals": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{"color": "red", "value": null},
{"color": "yellow", "value": 0},
{"color": "green", "value": 5}
]
}
},
"overrides": []
},
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false},
"textMode": "auto"
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "WITH latest AS (SELECT * FROM daily_account_valuation WHERE valuation_date = (SELECT MAX(valuation_date) FROM daily_account_valuation) AND net_contribution > 0) SELECT (SUM(total_value - net_contribution) / NULLIF(SUM(net_contribution), 0) * 100) AS roi_pct FROM latest"
}
]
},
{
"id": 5,
"title": "Net worth — total over time",
"description": "Daily total_value summed across all accounts (base GBP).",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 4},
"fieldConfig": {
"defaults": {
"color": {"mode": "fixed", "fixedColor": "green"},
"unit": "currencyGBP",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 20,
"pointSize": 4,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "none"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "net_worth"},
"properties": [{"id": "displayName", "value": "Net worth"}]
}
]
},
"options": {
"legend": {"calcs": ["last", "max"], "displayMode": "table", "placement": "bottom"},
"tooltip": {"mode": "multi", "sort": "desc"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "time_series",
"rawSql": "SELECT valuation_date::timestamp AS \"time\", SUM(total_value) AS net_worth FROM daily_account_valuation WHERE $__timeFilter(valuation_date) GROUP BY valuation_date ORDER BY valuation_date"
}
]
},
{
"id": 6,
"title": "Net contribution vs market value",
"description": "Net contribution = cumulative deposits withdrawals. Market value = total_value (cash + investments). Gap between the two = unrealised growth.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 12, "x": 0, "y": 14},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 0,
"pointSize": 4,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "none"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "market_value"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}},
{"id": "displayName", "value": "Market value"}
]
},
{
"matcher": {"id": "byName", "options": "net_contribution"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}},
{"id": "displayName", "value": "Net contribution"}
]
}
]
},
"options": {
"legend": {"calcs": ["last"], "displayMode": "table", "placement": "bottom"},
"tooltip": {"mode": "multi", "sort": "desc"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "time_series",
"rawSql": "SELECT valuation_date::timestamp AS \"time\", SUM(net_contribution) AS net_contribution, SUM(total_value) AS market_value FROM daily_account_valuation WHERE $__timeFilter(valuation_date) GROUP BY valuation_date ORDER BY valuation_date"
}
]
},
{
"id": 7,
"title": "Growth (market value contribution) over time",
"description": "Unrealised gain across all accounts. Filled area to emphasise the wealth created above the contributed capital.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 12, "x": 12, "y": 14},
"fieldConfig": {
"defaults": {
"color": {"mode": "fixed", "fixedColor": "#56A64B"},
"unit": "currencyGBP",
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 50,
"gradientMode": "opacity",
"pointSize": 4,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "none"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "growth"},
"properties": [{"id": "displayName", "value": "Growth"}]
}
]
},
"options": {
"legend": {"calcs": ["last", "max"], "displayMode": "table", "placement": "bottom"},
"tooltip": {"mode": "multi", "sort": "desc"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "time_series",
"rawSql": "SELECT valuation_date::timestamp AS \"time\", (SUM(total_value) - SUM(net_contribution)) AS growth FROM daily_account_valuation WHERE $__timeFilter(valuation_date) GROUP BY valuation_date ORDER BY valuation_date"
}
]
},
{
"id": 8,
"title": "Per-account stacked — total value",
"description": "Stacked area showing each account's contribution to total net worth over time. Useful for spotting which account drives the trajectory.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 11, "w": 24, "x": 0, "y": 24},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"custom": {
"drawStyle": "line",
"lineWidth": 1,
"fillOpacity": 70,
"pointSize": 3,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "normal"}
}
},
"overrides": []
},
"options": {
"legend": {"calcs": ["last"], "displayMode": "table", "placement": "bottom"},
"tooltip": {"mode": "multi", "sort": "desc"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "time_series",
"rawSql": "SELECT d.valuation_date::timestamp AS \"time\", a.name AS metric, d.total_value AS value FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id WHERE $__timeFilter(d.valuation_date) ORDER BY d.valuation_date, a.name"
}
]
},
{
"id": 9,
"title": "Cash vs invested (stacked)",
"description": "Daily breakdown of uninvested broker cash vs market value of investments. WORKPLACE_PENSION accounts (Fidelity) are reclassified entirely as invested — Wealthfolio dumps pension wrappers into cash_balance because it doesn't track the underlying fund holdings, but they are not actually cash.",
"type": "timeseries",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 10, "w": 24, "x": 0, "y": 35},
"fieldConfig": {
"defaults": {
"color": {"mode": "palette-classic"},
"unit": "currencyGBP",
"custom": {
"drawStyle": "line",
"lineWidth": 1,
"fillOpacity": 70,
"pointSize": 3,
"showPoints": "never",
"spanNulls": true,
"axisPlacement": "auto",
"stacking": {"group": "A", "mode": "normal"}
}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "cash"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "#FADE2A"}},
{"id": "displayName", "value": "Cash"}
]
},
{
"matcher": {"id": "byName", "options": "invested"},
"properties": [
{"id": "color", "value": {"mode": "fixed", "fixedColor": "#56A64B"}},
{"id": "displayName", "value": "Invested"}
]
}
]
},
"options": {
"legend": {"calcs": ["last"], "displayMode": "table", "placement": "bottom"},
"tooltip": {"mode": "multi", "sort": "desc"}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "time_series",
"rawSql": "SELECT d.valuation_date::timestamp AS \"time\", SUM(CASE WHEN a.account_type = 'WORKPLACE_PENSION' THEN 0 ELSE d.cash_balance END) AS cash, SUM(CASE WHEN a.account_type = 'WORKPLACE_PENSION' THEN d.cash_balance + d.investment_market_value ELSE d.investment_market_value END) AS invested FROM daily_account_valuation d JOIN accounts a ON a.id = d.account_id WHERE $__timeFilter(d.valuation_date) GROUP BY d.valuation_date ORDER BY d.valuation_date"
}
]
},
{
"id": 10,
"title": "Activity log",
"description": "Recent activities (BUY / SELL / DEPOSIT / WITHDRAWAL / DIVIDEND / etc.) across all accounts. Limited to 100 most recent.",
"type": "table",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"gridPos": {"h": 14, "w": 24, "x": 0, "y": 45},
"fieldConfig": {
"defaults": {
"custom": {"align": "auto", "displayMode": "auto"}
},
"overrides": [
{
"matcher": {"id": "byName", "options": "amount"},
"properties": [{"id": "unit", "value": "currencyGBP"}]
}
]
},
"options": {
"cellHeight": "sm",
"footer": {"show": false}
},
"targets": [
{
"refId": "A",
"datasource": {"type": "grafana-postgresql-datasource", "uid": "wealth-pg"},
"rawQuery": true,
"editorMode": "code",
"format": "table",
"rawSql": "SELECT a.activity_date AS \"date\", acc.name AS \"account\", a.activity_type AS \"type\", a.asset_id AS \"asset\", a.quantity AS \"qty\", a.unit_price AS \"unit_price\", a.amount AS \"amount\", a.currency AS \"ccy\", a.notes AS \"notes\" FROM activities a LEFT JOIN accounts acc ON acc.id = a.account_id WHERE $__timeFilter(a.activity_date) ORDER BY a.activity_date DESC LIMIT 100"
}
]
}
],
"refresh": "5m",
"schemaVersion": 39,
"tags": ["finance", "personal", "wealth"],
"templating": {"list": []},
"time": {"from": "now-5y", "to": "now"},
"timepicker": {},
"timezone": "browser",
"title": "Wealth",
"uid": "wealth",
"version": 1
}