$npx -y skills add ya5huk/findash --skill render-finance-dashboardUse when the user says "render dashboard", "show my finances", "build the dashboard", or any morning-summary equivalent. Reads SQLite, fetches live prices/FX from Yahoo Finance, fills the HTML template, and writes a self-contained dashboard to output/dashboard.html.
| 1 | # render-finance-dashboard |
| 2 | |
| 3 | You render the booky finance dashboard from the current state of `data/finance.db`. **The visual design is fixed by the template; you only fill it with data.** The mechanical part (SQL → HTML, live-price fetch, asset inlining) lives in `scripts/render_dashboard.py` — invoke it, don't re-derive it. |
| 4 | |
| 5 | ## Where things live |
| 6 | |
| 7 | - Canonical renderer: `scripts/render_dashboard.py` |
| 8 | - HTML template (placeholders in `{{NAME}}` form): `templates/dashboard.html.tpl` |
| 9 | - CSS: `templates/styles.css` — inlined into the HTML at render time |
| 10 | - Chart constructors: `templates/charts.js` — inlined into the HTML |
| 11 | - Vendored offline assets (gitignored, produced by `scripts/bundle-assets.py`): |
| 12 | - `templates/vendor/chart.umd.min.js` |
| 13 | - `templates/vendor/chartjs-adapter-date-fns.bundle.min.js` |
| 14 | - `templates/vendor/fonts-inline.css` (EB Garamond + Cormorant Garamond, base64 woff2) |
| 15 | - Telegram credentials: the `[telegram]` section of `.secrets/findash` (`bot_token=…` and `chat_id=…`) |
| 16 | - Local DB: `data/finance.db` |
| 17 | - Output: a single self-contained file at `output/dashboard.html` |
| 18 | - SQL schema and example queries: [`docs/sqlite-schema.md`](../../docs/sqlite-schema.md) |
| 19 | - Visual rules: [`docs/design-system.md`](../../docs/design-system.md) |
| 20 | |
| 21 | ## Flow |
| 22 | |
| 23 | 1. **Run the renderer.** From the project root: |
| 24 | |
| 25 | ```bash |
| 26 | python3 scripts/render_dashboard.py |
| 27 | ``` |
| 28 | |
| 29 | The script handles everything mechanical: |
| 30 | - Live-fetches prices for currently-held securities and `USD/ILS`, `GBP/ILS` FX rates from Yahoo Finance (`query1.finance.yahoo.com/v8/finance/chart/{symbol}`). User-Agent header required. |
| 31 | - Yahoo symbol translation: `RR.LSE` → `RR.L`. Quote units: USD direct; LSE pence (`÷100` → GBP); TASE agorot (`÷100` → ILS). |
| 32 | - Persists fetched prices into `prices` and the new FX into `fx_rates` so a history accumulates day-by-day. |
| 33 | - Computes net worth in ILS, brokerage market value, unrealized P/L per position. |
| 34 | - Excludes accounts with `closed_on IS NOT NULL` from Cash and from the headline (they still show in SQLite-data). |
| 35 | - Sorts the positions table by ILS-equivalent market value, descending (so SPY ranks above RR.LSE despite fewer shares). |
| 36 | - Orders brokerage deposits newest first. |
| 37 | - Builds the SQLite-data debug tabs (first 50 rows + count per table). |
| 38 | - Inlines CSS, fonts, Chart.js, and the chart-app JS into a single HTML file. |
| 39 | - Writes `output/dashboard.html` and a small meta file at `/tmp/dashboard_meta.json` (used by the Telegram step). |
| 40 | |
| 41 | If any vendor file is missing, the script stops with a clear message. Run `python3 scripts/bundle-assets.py` once to fix. |
| 42 | |
| 43 | 2. **Send to Telegram.** Delivery lives in one committed, allowlisted command so the |
| 44 | unattended daily `claude -p` run can run it without per-call approval (it can't build |
| 45 | the read-secret + `curl` send inline — the permission analyzer won't auto-approve a |
| 46 | command that sources `.secrets` and expands `${bot_token}`, and the `Write` tool isn't |
| 47 | granted unattended). The token is read at runtime and never printed. |
| 48 | |
| 49 | ```bash |
| 50 | scripts/send_telegram.sh # add: --note "<line>" to append a caption line |
| 51 | ``` |
| 52 | |
| 53 | What it does (= the old steps 2+3, now deterministic): |
| 54 | - Sends `output/dashboard.html` as a document, caption `Finance — <AS_OF_DATE> · <net worth>` |
| 55 | (literal `·` U+00B7), read from `/tmp/dashboard_meta.json`. |
| 56 | - If `data/last_sync_summary.md` exists, sends it as a second `sendMessage` (HTML parse |
| 57 | mode; bold title `Sync · <AS_OF_DATE>`; split into ≤3900-char parts on `\n` boundaries; |
| 58 | `## Ingested`/`## Triaged` headers render as bold **New**/**Filed**, any other `##` |
| 59 | header — e.g. `## ⚠️ Warnings` — passes through as its own bold line), then deletes it on |
| 60 | success. The summary may lead with a `⚠️ Warnings` block — that's expected on degraded |
| 61 | runs, not an error. Absent → prints `Sync summary: Skipped (no summary)`. |
| 62 | - Telegram not configured (no `[telegram]` in `.secrets/findash`) → prints `Telegram delivery skipped: …` and exits 0 (local |
| 63 | file still useful). Never deletes the local dashboard on a Telegram failure. |
| 64 | |
| 65 | Pass `--note "<line>"` to append a caption line — the morning flow uses this to surface a |
| 66 | best-effort **fetch** failure. The script prints `Sent to chat <id>` / |
| 67 | `Telegram delivery failed: <description>` / the `Sync summary: …` line — relay them in step 3. |
| 68 | |
| 69 | 3. **Report.** Print: |
| 70 | - The absolute path of `output/dashboard.html`. |
| 71 | - The Telegram delivery status: `Sent to chat <chat_id>`, `Telegram delivery skipped: …`, or `Telegram delivery failed: <description>`. |
| 72 | - The sync summary delivery status: `Sync summary: Sent (<N> bullets)`, `Sync sum |