{"id":49482274,"url":"https://github.com/milouk/personal-portfolio-tracker","last_synced_at":"2026-05-04T09:03:13.787Z","repository":{"id":354876147,"uuid":"1225744999","full_name":"milouk/personal-portfolio-tracker","owner":"milouk","description":"Self-hosted dashboard for tracking your net worth across brokers, banks, T-bills, crypto, and cash. Live prices, automated sync, full privacy — all data stays on your machine.","archived":false,"fork":false,"pushed_at":"2026-04-30T21:56:23.000Z","size":269,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-30T23:19:57.490Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://milouk.github.io/personal-portfolio-tracker","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/milouk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-30T15:38:26.000Z","updated_at":"2026-04-30T21:56:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/milouk/personal-portfolio-tracker","commit_stats":null,"previous_names":["milouk/personal-portfolio-tracker"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/milouk/personal-portfolio-tracker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milouk%2Fpersonal-portfolio-tracker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milouk%2Fpersonal-portfolio-tracker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milouk%2Fpersonal-portfolio-tracker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milouk%2Fpersonal-portfolio-tracker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/milouk","download_url":"https://codeload.github.com/milouk/personal-portfolio-tracker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/milouk%2Fpersonal-portfolio-tracker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32600968,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-30T23:03:02.294Z","updated_at":"2026-05-04T09:03:13.781Z","avatar_url":"https://github.com/milouk.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Personal Portfolio Tracker\n\n[![Next.js](https://img.shields.io/badge/Next.js-16-black?logo=nextdotjs)](https://nextjs.org)\n[![React](https://img.shields.io/badge/React-19-61dafb?logo=react)](https://react.dev)\n[![Tailwind](https://img.shields.io/badge/Tailwind-4-38bdf8?logo=tailwindcss)](https://tailwindcss.com)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript)](https://www.typescriptlang.org)\n[![Deploy demo](https://github.com/milouk/personal-portfolio-tracker/actions/workflows/deploy-demo.yml/badge.svg)](https://github.com/milouk/personal-portfolio-tracker/actions/workflows/deploy-demo.yml)\n[![Release](https://github.com/milouk/personal-portfolio-tracker/actions/workflows/release.yml/badge.svg)](https://github.com/milouk/personal-portfolio-tracker/actions/workflows/release.yml)\n[![GHCR](https://img.shields.io/badge/ghcr.io-milouk%2Fpersonal--portfolio--tracker-2188ff?logo=github)](https://github.com/milouk/personal-portfolio-tracker/pkgs/container/personal-portfolio-tracker)\n[![Latest release](https://img.shields.io/github/v/release/milouk/personal-portfolio-tracker?logo=github)](https://github.com/milouk/personal-portfolio-tracker/releases/latest)\n[![License](https://img.shields.io/badge/license-personal--use-blue)](#license)\n\nA self-hosted dashboard for tracking your net worth across brokers, banks,\nT-bills, crypto, and cash — with live prices, automated sync, and per-position\nP/L. **All data stays on your own machine.**\n\n\u003e **Live demo:** [milouk.github.io/personal-portfolio-tracker](https://milouk.github.io/personal-portfolio-tracker)\n\n## Features\n\n- **Live valuations** — ETFs, stocks, crypto and FX update on every page load.\n- **Multi-broker sync** — pull positions and cash directly from your accounts.\n- **Bond / T-bill ladder** — maturity dates, YTM, accrued profit, blended yield.\n- **Greek T-bill auctions** — upcoming auctions and a profit calculator.\n- **Calendar** — bond maturities, ex-dividend dates, dividend payments.\n- **Email + push reminders** — N days before each event.\n- **Privacy mode** — one click blurs every number on screen for screenshots.\n- **JSON API** — drop-in widget for self-hosted dashboards (Glance, Homepage…).\n- **Dark / light theme** — Nova preset, readable in both modes.\n\n## Quick start\n\n```bash\ngit clone https://github.com/\u003cyou\u003e/personal-portfolio-tracker\ncd personal-portfolio-tracker\nnpm install\ncp .env.example .env.local        # add the credentials you want\nnpm run dev                       # → http://localhost:3000\n```\n\nThat's enough to see the empty dashboard. Add assets with the **+ Add asset**\nbutton or by editing `data/portfolio.json` directly.\n\n## Data layout\n\nEverything lives in `data/`:\n\n```text\ndata/\n├── portfolio.json          your assets (single source of truth)\n├── events.jsonl            append-only change log\n├── prices.json             price cache\n├── fx.json                 EUR/USD cache\n├── ecb.json                ECB Deposit Facility Rate cache\n└── …\n```\n\n`data/` is git-ignored. Backup = copy the folder.\n\n## Live data sources\n\n| Asset class             | Source           | Refresh        |\n| ----------------------- | ---------------- | -------------- |\n| ETFs / stocks (EUR)     | Yahoo Finance    | 60 s           |\n| ETFs / stocks (LSE/USD) | Stooq (fallback) | 60 s           |\n| Crypto                  | CoinGecko        | 60 s           |\n| EUR/USD FX              | ECB / Frankfurter| 1 h            |\n| ECB rate                | ECB SDW          | 24 h           |\n| Greek T-bill auctions   | PDMA             | 24 h           |\n| Dividend calendar       | Yahoo            | 24 h           |\n| Broker positions / cash | per-broker sync  | on demand      |\n\n## Broker sync\n\nSome sources have no public API, so this project drives them with the same\ntools you'd use yourself — a Python helper for Trade Republic and a headless\nbrowser for NBG. Both run **only on your machine**, only when you ask them to.\n\n```bash\nnpm run sync:tr      # Trade Republic (push / SMS code on first login)\nnpm run sync:nbg     # National Bank of Greece (Viber OTP each login)\nnpm run sync:all     # both, sequentially\n```\n\nOr click **Sync** in the header. When an OTP is needed, a modal pops up — paste\nthe code and submit.\n\n## Calendar reminders\n\nThe dashboard surfaces upcoming events automatically. To get **email or\nntfy.sh push notifications** N days ahead, add this to a cron job:\n\n```cron\n0 9 * * *  cd /path/to/personal-portfolio-tracker \u0026\u0026 npm run notify:calendar\n```\n\nConfigure SMTP / ntfy in `.env.local` (see `.env.example`).\n\n## JSON API\n\nRead-only summary + sync triggers — drop into Glance, Homepage, or any custom\ndashboard:\n\n| Endpoint           | Method | Purpose                              |\n| ------------------ | ------ | ------------------------------------ |\n| `/api/summary`     | GET    | Net worth, P/L, allocation, FX, ECB  |\n| `/api/prices`      | GET    | Live prices + FX (cached)            |\n| `/api/calendar`    | GET    | Upcoming maturities + dividends      |\n| `/api/tbills`      | GET    | Greek T-bill auction calendar        |\n| `/api/sync`        | GET    | Current sync status                  |\n| `/api/sync`        | POST   | Trigger a sync (`tr` / `nbg` / `all`)|\n\nSet `PORTFOLIO_API_TOKEN` in `.env.local` and pass it via `?token=…`,\n`x-api-token: …`, or `Authorization: Bearer …`. With no token configured the\nendpoints are open — keep the dashboard on localhost or behind a tunnel.\n\n### Glance widget recipe\n\n```yaml\n- type: custom-api\n  title: Net worth\n  cache: 5m\n  url: http://portfolio-tracker:3000/api/summary?token=YOUR_TOKEN\n  template: |\n    \u003cdiv style=\"font-size: 28px; font-weight: 600;\"\u003e\n      €{{ printf \"%.0f\" (.JSON.Float \"totalEur\") }}\n    \u003c/div\u003e\n    \u003cdiv style=\"opacity: 0.7;\"\u003e\n      P/L {{ if gt (.JSON.Float \"gainEur\") 0.0 }}+{{ end }}€{{ printf \"%.0f\" (.JSON.Float \"gainEur\") }}\n      ({{ printf \"%.1f\" (multiply (.JSON.Float \"gainPct\") 100.0) }}%)\n    \u003c/div\u003e\n```\n\n## Docker\n\nMulti-arch images (`linux/amd64`, `linux/arm64`) are published on every\nrelease to **GitHub Container Registry**:\n\n```text\nghcr.io/milouk/personal-portfolio-tracker\n```\n\nTags follow semver: each release gets `vX.Y.Z`, `vX.Y`, `vX`, plus `latest`.\nImages are public — no `docker login` needed to pull.\n\n### One-line up (pulls from GHCR)\n\n```bash\ncp .env.example .env.local       # fill in the vars you actually want\ndocker compose up -d             # dashboard at http://localhost:3000\n```\n\n`compose.yaml` defaults to `ghcr.io/milouk/personal-portfolio-tracker:latest`.\nPin a specific version with `APP_TAG`:\n\n```bash\nAPP_TAG=v1.2.3 docker compose up -d\n```\n\n### Upgrading\n\nWhenever a new release is cut, the new image is on Docker Hub within minutes.\nOn your server:\n\n```bash\ndocker compose pull \u0026\u0026 docker compose up -d\n```\n\nFor unattended updates, drop in [Watchtower](https://containrrr.dev/watchtower/)\nor run the above as a daily cron job.\n\n### Building locally\n\nIf you've patched the source and want to run your local copy:\n\n```bash\ndocker compose up -d --build      # rebuilds from ./Dockerfile, ignores registry\n```\n\n### Passing environment variables\n\nCompose looks at variables in this priority order, so you can mix and match:\n\n1. **Shell** when invoking compose:\n\n   ```bash\n   PORTFOLIO_API_TOKEN=xxx docker compose up -d\n   ```\n\n2. **`.env`** next to `compose.yaml` (auto-loaded by Docker Compose).\n3. **`.env.local`** (loaded into the container via `env_file`, optional).\n4. **`environment:`** block in `compose.yaml` (explicit overrides).\n\nAll vars from [`.env.example`](.env.example) work in any of those — NBG creds,\nTR phone/PIN, SMTP, ntfy, `PORTFOLIO_API_TOKEN`. Anything left unset is treated\nas empty (no defaults are baked into the image).\n\n### One-time broker logins\n\n```bash\n# Trade Republic — interactive, sends SMS or push to your phone\ndocker compose run --rm app pytr login\n\n# NBG — interactive, prompts for the Viber OTP\ndocker compose exec app npm run sync:nbg\n```\n\nSessions are persisted in the named `pytr-home` volume and your local `./data`\ndirectory, so subsequent syncs run without prompts (until the broker invalidates\nthe session, typically every few weeks).\n\n### Periodic sync (host crontab)\n\n```cron\n0 8 * * *  docker compose -f /path/to/compose.yaml exec -T app npm run sync:all\n0 9 * * *  docker compose -f /path/to/compose.yaml exec -T app npm run notify:calendar\n```\n\n### Headless OTP (no TTY)\n\nWhen running without an interactive terminal, set `NBG_OTP_SOURCE=webhook`,\nexpose port 4848 in `compose.yaml`, then deliver the OTP from any phone:\n\n```bash\ncurl -d 123456 http://your-server:4848/otp\n```\n\n## Privacy\n\n- Hide-numbers toggle (eye icon, `⌘/Ctrl + .`) blurs every number on screen.\n- All credentials and balances stay local — no cloud, no telemetry.\n- `data/` is git-ignored by default.\n\n## Demo\n\nA static-export build with seeded dummy data lives in `demo/`:\n\n```bash\nnpm run demo:dev      # local preview on :3000\nnpm run demo:build    # build for GitHub Pages → out/\n```\n\n## Stack\n\nNext.js 16 · React 19 · Tailwind v4 · shadcn/ui · Recharts · Motion ·\nPlaywright · pytr\n\n## License\n\nPersonal use. No warranties. PRs welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmilouk%2Fpersonal-portfolio-tracker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmilouk%2Fpersonal-portfolio-tracker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmilouk%2Fpersonal-portfolio-tracker/lists"}