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
451 lines
17 KiB
JSON
451 lines
17 KiB
JSON
{
|
||
"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
|
||
}
|