diff --git a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json index 756ac497..c0ef4973 100644 --- a/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json +++ b/stacks/monitoring/modules/monitoring/dashboards/uk-payslip.json @@ -352,7 +352,7 @@ { "id": 2, "title": "Monthly cash flow (RSU stripped)", - "description": "Cash-only view: gross pay minus the RSU vest (cash_gross) and the bank-deposited net_pay. Tax and NI are not shown here because UK cumulative PAYE genuinely takes a YTD true-up chunk in vest months on top of the marginal RSU PAYE — see Panel 11 for the full tax breakdown with the band-aware RSU split.", + "description": "Cash-only view: gross pay minus the RSU vest (cash_gross) and the bank-deposited net_pay. Tax and NI are not shown here because UK cumulative PAYE genuinely takes a YTD true-up chunk in vest months on top of the marginal RSU PAYE \u2014 see Panel 11 for the full tax breakdown with the band-aware RSU split.", "type": "timeseries", "datasource": { "type": "grafana-postgresql-datasource", @@ -1929,407 +1929,10 @@ } ] }, - { - "id": 10, - "title": "HMRC Tax Year Reconciliation \u2014 Individual Tax API", - "description": "Latest snapshot from HMRC Individual Tax API v1.1 vs SUM(payslip.income_tax) per tax year. Delta > \u00a310 turns red \u2014 that's parser drift vs HMRC's held figures, the authoritative ground truth. Shown only for years where hmrc-sync has pulled a snapshot.", - "type": "table", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 94 - }, - "fieldConfig": { - "defaults": { - "unit": "currencyGBP", - "custom": { - "align": "right", - "displayMode": "auto" - } - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "^delta_" - }, - "properties": [ - { - "id": "custom.displayMode", - "value": "color-background" - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "green", - "value": -10 - }, - { - "color": "red", - "value": 10 - }, - { - "color": "red", - "value": -10 - } - ] - } - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "tax_year" - }, - "properties": [ - { - "id": "unit", - "value": "string" - }, - { - "id": "custom.align", - "value": "left" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "employer_paye_ref" - }, - "properties": [ - { - "id": "unit", - "value": "string" - }, - { - "id": "custom.align", - "value": "left" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "snapshot_date" - }, - "properties": [ - { - "id": "unit", - "value": "dateTimeAsIso" - } - ] - } - ] - }, - "options": { - "showHeader": true, - "cellHeight": "sm", - "footer": { - "show": false - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "rawQuery": true, - "editorMode": "code", - "format": "table", - "rawSql": "WITH latest AS (SELECT DISTINCT ON (tax_year, employer_paye_ref) tax_year, employer_paye_ref, snapshot_date, gross_pay, income_tax, ni_contributions FROM hmrc_sync.tax_year_snapshot ORDER BY tax_year, employer_paye_ref, snapshot_date DESC), summed AS (SELECT tax_year, COALESCE(SUM(gross_pay), 0) AS sum_gross, COALESCE(SUM(income_tax), 0) AS sum_tax, COALESCE(SUM(national_insurance), 0) AS sum_ni FROM payslip_ingest.payslip GROUP BY tax_year) SELECT l.tax_year, l.employer_paye_ref, l.snapshot_date, l.gross_pay AS hmrc_gross, s.sum_gross AS computed_gross, (l.gross_pay - s.sum_gross) AS delta_gross, l.income_tax AS hmrc_tax, s.sum_tax AS computed_tax, (l.income_tax - s.sum_tax) AS delta_tax, l.ni_contributions AS hmrc_ni, s.sum_ni AS computed_ni, (l.ni_contributions - s.sum_ni) AS delta_ni FROM latest l LEFT JOIN summed s ON s.tax_year = l.tax_year ORDER BY l.tax_year DESC" - } - ] - }, - { - "id": 14, - "title": "Meta payroll: bank deposit vs payslip net pay", - "description": "Cross-check between ActualBudget bank deposits (META/FACEBOOK payee) and payslip net_pay. |delta| > \u00a350 flags likely parser or bank-sync drift. Synced daily 02:00 UTC.", - "type": "timeseries", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "gridPos": { - "h": 9, - "w": 24, - "x": 0, - "y": 160 - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "unit": "currencyGBP", - "custom": { - "axisPlacement": "auto", - "drawStyle": "line", - "fillOpacity": 0, - "lineWidth": 2, - "pointSize": 6, - "showPoints": "always", - "spanNulls": false, - "thresholdsStyle": { - "mode": "off" - } - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "deposit_sum" - }, - "properties": [ - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "green" - } - }, - { - "id": "displayName", - "value": "Bank deposit (ActualBudget)" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "payslip_net_pay" - }, - "properties": [ - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "blue" - } - }, - { - "id": "displayName", - "value": "Payslip net pay" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "delta" - }, - "properties": [ - { - "id": "color", - "value": { - "mode": "fixed", - "fixedColor": "red" - } - }, - { - "id": "displayName", - "value": "Delta (deposit \u2212 payslip)" - }, - { - "id": "custom.drawStyle", - "value": "bars" - } - ] - } - ] - }, - "options": { - "legend": { - "calcs": [ - "last" - ], - "displayMode": "table", - "placement": "bottom" - }, - "tooltip": { - "mode": "multi", - "sort": "desc" - } - }, - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "rawSql": "WITH deposits AS (SELECT DATE_TRUNC('month', deposit_date)::date AS month_start, SUM(amount) AS deposit_sum FROM payslip_ingest.external_meta_deposits GROUP BY 1), payslip_net AS (SELECT DATE_TRUNC('month', pay_date)::date AS month_start, SUM(net_pay) AS payslip_net_pay FROM payslip_ingest.payslip GROUP BY 1) SELECT COALESCE(p.month_start, d.month_start) AS \"time\", d.deposit_sum, p.payslip_net_pay, COALESCE(d.deposit_sum, 0) - COALESCE(p.payslip_net_pay, 0) AS delta FROM deposits d FULL OUTER JOIN payslip_net p ON p.month_start = d.month_start WHERE $__timeFilter(COALESCE(p.month_start, d.month_start)) ORDER BY \"time\"", - "format": "time_series", - "refId": "A", - "rawQuery": true, - "editorMode": "code" - } - ] - }, - { - "id": 15, - "title": "RSU vest reconciliation \u2014 payslip vs Schwab", - "description": "Per-vest-month join between payslip.rsu_vest (what HMRC reporting shows) and Schwab's vest-confirmation email data (what actually happened at the broker). Empty rows until the broker-sync IMAP ingest runs and VestEvents are persisted.", - "type": "table", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "gridPos": { - "h": 10, - "w": 24, - "x": 0, - "y": 169 - }, - "fieldConfig": { - "defaults": { - "custom": { - "align": "right", - "cellOptions": { - "type": "auto" - }, - "inspect": false - }, - "unit": "currencyGBP", - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "vest_month" - }, - "properties": [ - { - "id": "custom.width", - "value": 140 - }, - { - "id": "custom.align", - "value": "left" - }, - { - "id": "unit", - "value": "none" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "ticker" - }, - "properties": [ - { - "id": "custom.width", - "value": 80 - }, - { - "id": "unit", - "value": "none" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "shares_vested" - }, - "properties": [ - { - "id": "unit", - "value": "none" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "tax_delta_pct" - }, - "properties": [ - { - "id": "unit", - "value": "percent" - }, - { - "id": "custom.cellOptions", - "value": { - "type": "color-background", - "mode": "basic" - } - }, - { - "id": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "orange", - "value": 2 - }, - { - "color": "red", - "value": 5 - } - ] - } - } - ] - } - ] - }, - "options": { - "cellHeight": "sm", - "footer": { - "show": false - } - }, - "targets": [ - { - "refId": "A", - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "payslips-pg" - }, - "rawQuery": true, - "editorMode": "code", - "format": "table", - "rawSql": "WITH vest_by_month AS (SELECT DATE_TRUNC('month', vest_date)::date AS vest_month, ticker, SUM(shares_vested) AS shares_vested, SUM(gross_value_gbp) AS broker_gross_gbp, SUM(tax_withheld_gbp) AS broker_tax_gbp FROM payslip_ingest.rsu_vest_events GROUP BY 1, 2), payslip_by_month AS (SELECT DATE_TRUNC('month', pay_date)::date AS vest_month, SUM(rsu_vest) AS payslip_rsu_gbp, SUM(income_tax - COALESCE(cash_income_tax, income_tax)) AS payslip_rsu_tax_gbp FROM payslip_ingest.payslip WHERE rsu_vest > 0 GROUP BY 1) SELECT COALESCE(v.vest_month, p.vest_month) AS vest_month, v.ticker, v.shares_vested, v.broker_gross_gbp, p.payslip_rsu_gbp, (p.payslip_rsu_gbp - v.broker_gross_gbp) AS gross_delta_gbp, v.broker_tax_gbp, p.payslip_rsu_tax_gbp, (p.payslip_rsu_tax_gbp - v.broker_tax_gbp) AS tax_delta_gbp, CASE WHEN v.broker_tax_gbp IS NULL OR v.broker_tax_gbp = 0 THEN NULL ELSE ABS(p.payslip_rsu_tax_gbp - v.broker_tax_gbp) * 100.0 / v.broker_tax_gbp END AS tax_delta_pct FROM vest_by_month v FULL OUTER JOIN payslip_by_month p ON p.vest_month = v.vest_month ORDER BY vest_month DESC" - } - ] - }, { "id": 16, - "title": "Yearly receipt — what I actually earned per tax year", - "description": "One stacked bar per tax year, showing pay components I keep: salary (gross), bonus (gross), and RSU vests AFTER band-aware tax (PAYE+NI withheld via sell-to-cover). Excludes deductions and taxes — this is the take-home view of earnings, not the gross-deductions waterfall. tax_year axis (text), unit GBP. Always shows all years — ignores the time picker.", + "title": "Yearly receipt \u2014 what I actually earned per tax year", + "description": "One stacked bar per tax year, showing pay components I keep: salary (gross), bonus (gross), and RSU vests AFTER band-aware tax (PAYE+NI withheld via sell-to-cover). Excludes deductions and taxes \u2014 this is the take-home view of earnings, not the gross-deductions waterfall. tax_year axis (text), unit GBP. Always shows all years \u2014 ignores the time picker.", "type": "barchart", "datasource": { "type": "grafana-postgresql-datasource", @@ -2462,8 +2065,8 @@ }, { "id": 17, - "title": "YTD gross salary — year-over-year comparison", - "description": "Cumulative gross pay built up month by month within each UK tax year (April → March). One line per tax year. Pay dates are projected onto a sliding 12-month window ending now, so years overlay cleanly without falling outside the dashboard's time range. X-axis shows month-of-tax-year (April first, March last).", + "title": "YTD gross salary \u2014 year-over-year comparison", + "description": "Cumulative gross pay built up month by month within each UK tax year (April \u2192 March). One line per tax year. Pay dates are projected onto a sliding 12-month window ending now, so years overlay cleanly without falling outside the dashboard's time range. X-axis shows month-of-tax-year (April first, March last).", "type": "timeseries", "timeFrom": "13M", "datasource": { @@ -2478,7 +2081,9 @@ }, "fieldConfig": { "defaults": { - "color": {"mode": "palette-classic"}, + "color": { + "mode": "palette-classic" + }, "unit": "currencyGBP", "decimals": 0, "custom": { @@ -2489,14 +2094,27 @@ "showPoints": "auto", "spanNulls": true, "axisPlacement": "auto", - "stacking": {"group": "A", "mode": "none"} + "stacking": { + "group": "A", + "mode": "none" + } } }, "overrides": [] }, "options": { - "legend": {"calcs": ["last", "max"], "displayMode": "table", "placement": "bottom"}, - "tooltip": {"mode": "multi", "sort": "desc"} + "legend": { + "calcs": [ + "last", + "max" + ], + "displayMode": "table", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } }, "targets": [ {