From 563308a08b0e4335bb82ac432698f53298405e45 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Fri, 18 Sep 2026 13:43:17 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=BC=D0=BE=D0=BD=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=BE,=20dev-=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=20=D0=B4=D0=B5=D0=BF=D0=BB=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nix flake на два шелла (backend и + Flutter), justfile как единая точка входа, локальный Postgres в ./.pgdata на порту 54329, docker-compose из пяти сервисов (db, api, worker, caddy, pg-backup) и CI на ruff/pyright/pytest + дрейф OpenAPI. Российские корневые сертификаты лежат в репозитории: они публичные, но нужны и Caddy, и gRPC-каналу T-Invest, а глобальный gitignore отбрасывает *.pem. --- .env.example | 25 +++++ .github/workflows/ci.yml | 42 +++++++++ .gitignore | 38 ++++++++ README.md | 11 +++ backend/.dockerignore | 6 ++ backend/Dockerfile | 27 ++++++ backend/README.md | 3 + backend/certs/russian_ca.pem | 76 ++++++++++++++++ deploy/Caddyfile | 14 +++ deploy/certs/russian_ca.pem | 76 ++++++++++++++++ deploy/certs/russian_trusted_root_ca.pem | 34 +++++++ deploy/certs/russian_trusted_sub_ca.pem | 42 +++++++++ docker-compose.yml | 81 +++++++++++++++++ flake.lock | 27 ++++++ flake.nix | 57 ++++++++++++ justfile | 111 +++++++++++++++++++++++ 16 files changed, 670 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/README.md create mode 100644 backend/certs/russian_ca.pem create mode 100644 deploy/Caddyfile create mode 100644 deploy/certs/russian_ca.pem create mode 100644 deploy/certs/russian_trusted_root_ca.pem create mode 100644 deploy/certs/russian_trusted_sub_ca.pem create mode 100644 docker-compose.yml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 justfile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..93acb79 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Copy to .env (never committed). On the VPS, docker compose reads this file. + +# --- database --- +POSTGRES_DB=fintracker +POSTGRES_USER=fintracker +POSTGRES_PASSWORD=change-me +# used by api/worker; inside compose the host is `db` +DATABASE_URL=postgresql+asyncpg://fintracker:change-me@db:5432/fintracker + +# --- auth --- +# 32+ random bytes: `openssl rand -hex 32` +JWT_SECRET=change-me +ACCESS_TOKEN_TTL_SECONDS=3600 +REFRESH_TOKEN_TTL_SECONDS=2592000 + +# --- web --- +DOMAIN=fin.example.com +# comma-separated; leave empty when the Flutter web build is served by Caddy from the same origin +CORS_ORIGINS= +# dev only: serve a Flutter web build from the API process (prod uses Caddy) +WEB_DIR= + +# --- sources (filled in later phases) --- +ZENMONEY_TOKEN= +TINVEST_TOKEN= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..57acf22 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + backend: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:17-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: ["5432:5432"] + options: >- + --health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 3s --health-retries 20 + defaults: + run: + working-directory: backend + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - run: uv python install 3.12 + - run: uv sync --frozen + - run: uv run ruff check . + - run: uv run ruff format --check . + - run: uv run pyright + - name: tests + run: uv run pytest -q + env: + # tests spawn their own postgres via pg_ctl; the client tools come from the OS package + PATH: /usr/lib/postgresql/17/bin:${{ env.PATH }} + - name: openapi drift + run: | + uv run fintracker openapi /tmp/openapi.json + diff -u ../openapi/openapi.json /tmp/openapi.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d01d733 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# secrets and local state +.env +.env.* +!.env.example +.direnv/ +.pgdata/ +backups/ + +# python +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +dist/ +*.egg-info/ + +# real broker reports and other personal inputs — never committed +backend/tests/fixtures/reports/raw/* +!backend/tests/fixtures/reports/raw/.gitkeep + +# flutter / dart +app/.dart_tool/ +app/build/ +app/.flutter-plugins* +app/packages/api_client/.dart_tool/ +app/packages/api_client/build/ + +# editor / os +.DS_Store +.idea/ +.vscode/ +result + +# public Russian trusted CA bundle (not a secret) — the global gitignore drops *.pem +!deploy/certs/*.pem +!backend/certs/*.pem diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9b0aa0 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# fin-tracker + +Личная аналитика финансов и инвестиций: ZenMoney (повседневные деньги) + брокеры +(T-Invest API, отчёты Сбера и ВТБ) в одной базе, с XIRR/TWR, календарём дивидендов и +купонов, аллокацией и net worth. Замена Snowball Income. + +- `backend/` — Python 3.12, FastAPI, PostgreSQL, worker с расписанием синков. +- `app/` — Flutter-клиент (web, Linux, Windows, Android) поверх REST/OpenAPI. +- `deploy/` — Caddy и сертификаты; `docker-compose.yml` — прод на VPS. + +Для агентов: [AGENTS.md](AGENTS.md). Команды: `just --list`. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..0651816 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,6 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ +.ruff_cache/ +tests/fixtures/reports/raw/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..9d815f2 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,27 @@ +# syntax=docker/dockerfile:1.7 +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never +WORKDIR /app +COPY pyproject.toml uv.lock ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev --no-install-project +COPY . . +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev + +FROM docker.io/library/python:3.12-slim-bookworm +ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 TZ=Europe/Moscow +# Russian trusted CA bundle for T-Invest gRPC +ENV GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/app/certs/russian_ca.pem +RUN apt-get update && apt-get install -y --no-install-recommends tzdata ca-certificates \ + && rm -rf /var/lib/apt/lists/* && useradd -r -u 10001 app +WORKDIR /app +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/src /app/src +COPY --from=builder /app/alembic.ini /app/alembic.ini +COPY --from=builder /app/alembic /app/alembic +COPY certs /app/certs +RUN mkdir -p /data/uploads && chown -R app /data +USER app +EXPOSE 8000 +CMD ["fintracker", "serve", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..ad4b52c --- /dev/null +++ b/backend/README.md @@ -0,0 +1,3 @@ +# fintracker backend + +FastAPI + PostgreSQL. См. корневой `AGENTS.md` и `justfile`. diff --git a/backend/certs/russian_ca.pem b/backend/certs/russian_ca.pem new file mode 100644 index 0000000..410d506 --- /dev/null +++ b/backend/certs/russian_ca.pem @@ -0,0 +1,76 @@ +-----BEGIN CERTIFICATE----- +MIIG6DCCBNCgAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx +PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu +ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg +Q0EwHhcNMjQwNzE1MTI1MDQxWhcNMjkwNzE5MTI1MDQxWjBvMQswCQYDVQQGEwJS +VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg +YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1j0rkZECOt1S8o7I +JY+4YKAxuEa5xaHKHXT2EpkuC/0krqMOjUy2oPIRNgR5g8X0Jl6jamxeGLc4Q1tf +ju6or9oSRYThIUhRsFDQNBiBBEXoBgWxTfiKB2eyT97+pz5TBtBiRCPaLGRHYLRb +9Jz2HkJlxbtNPjtDrF5DPHym+mZ1M1z3hIQYAqJwLpsEBnsw/VxWMlxqHoeewd0h +uJMd71KQ5vOKlz7KrIZ6EobNNa6wItuvsfj3kYCK7O78uLHGXXFxdr8Hae9lMUmC +8F7AFwa+bO1LRlTlqW7rE3rLf+jj70N01N8T3o22v14YBaFBWQWncAVYD2JuL3tH +252+kdNOERf1fLbLRigJAbd+hOhWYlNf963TFDgnNPliHNIW72SygVBnI2V3JwO1 +dp1hVKpK/zt8ziGdHW4gmOLTsH50YKdR4jNqUgQv4wASlKn9OpN6zHYc5G8h86fY +BM+zxE5ikGI+I/vIqBuI0eaDU92AWN/YjFLpu8tMu9kLRSCf1vug6FIfDPWVo7iP +ac/SI2v8jnnpaW7ph/Pz3WkzaG7ZZJsfFs+8dploWc6LOoDtbFBhMdGMxu024msC +0PSjZb5ODXPIaO2NsA7fMiAtZcoK6anTUJh4zOP/stA9qsJGNxdrEmiPXSmBZY/N +Y0wkZgZ6JTDhw7038bPvctkblJkCAwEAAaOCAYswggGHMB0GA1UdDgQWBBR3Pdk5 +r0K93FvKduru/c4+YSkwXzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qws +hzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADCBmAYIKwYBBQUH +AQEEgYswgYgwQAYIKwYBBQUHMAKGNGh0dHA6Ly9udWMtY2RwLnZvc2tob2QucnUv +Y2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQwRAYIKwYBBQUHMAKGOGh0dHA6Ly9u +dWMtY2RwLmRpZ2l0YWwuZ292LnJ1L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3J0 +MIGFBgNVHR8EfjB8MDqgOKA2hjRodHRwOi8vbnVjLWNkcC52b3NraG9kLnJ1L2Nk +cC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMD6gPKA6hjhodHRwOi8vbnVjLWNkcC5k +aWdpdGFsLmdvdi5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG +9w0BAQsFAAOCAgEAmsINXtQ7wwUWvIeOr80MdJS/5G4xhyZOVEmeUorThquT672y +cCg3XCxc4fwbiZqSSbBqntQ7RtiTAKMYMvBageKoVHbzz+R4jX01tKcTx8cDePrz +dJ73bLNUorE7RU9QsW4KyiUeRmjMDV23AUlEvuQFTwgkHXvbac1BBdPn9CrssQuF +5EGohZKcQPFiAAc4SHbRNhlr7uAwgpc/erzI9EAcvA6BVAXcVKoeGpV01uexUgZ6 +St5RP9UmDWNA7T4yVXWJ233N0Q8bl+6AswINQ3PosPu6yQQHQjr65YS06epK+AeI +6j+oGR4xI7EhTQhQvaobnGmX/8QQ7XDRYCP2HXYxiffnn/CfZ/BVyKLYeY1ZipjE +nzqdQIC2+Q3WtY8jsVRQMP38WFRmtsIt5snehnPTs5bKGVIcYzj3o3Ex/K7agEz0 +zAJ0JR5ivXZOvNkT0g9x1v+S1IkU3e/nX1a+tpRquMtnHX0L2lXArNHUbaOO9EJt +d57WaIpofV5cVhhwShOgAuBc9UMJF3/n4t4RKiPxtsK8P67gcmphMhslj7AMYrYM +ej2NvQZY4m3ub3CPC/PrTjDONvb+8g5xrKtxBjYqC74HSB4dg9G3WimSDUuP2Su6 +G2y2TUeyJuCvCLz289VoO0vg7cNdMobE3KCqAiiNhN2VBFxHAUKmUoRcRdw= +-----END CERTIFICATE----- + +subject=C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA +issuer=C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA +-----BEGIN CERTIFICATE----- +MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx +PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu +ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg +Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS +VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg +YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n +qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q +XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U +zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX +YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y +Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD +U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD +4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9 +G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH +BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX +ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa +OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf +BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS +BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH +tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq +W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+ +/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS +AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj +C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV +4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d +WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ +D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC +EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq +391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4= +-----END CERTIFICATE----- + diff --git a/deploy/Caddyfile b/deploy/Caddyfile new file mode 100644 index 0000000..ca5e292 --- /dev/null +++ b/deploy/Caddyfile @@ -0,0 +1,14 @@ +{$DOMAIN} { + encode zstd gzip + + handle /api/* { + reverse_proxy api:8000 + } + + # Flutter web build (app/build/web), SPA fallback to index.html + handle { + root * /srv/app + try_files {path} /index.html + file_server + } +} diff --git a/deploy/certs/russian_ca.pem b/deploy/certs/russian_ca.pem new file mode 100644 index 0000000..410d506 --- /dev/null +++ b/deploy/certs/russian_ca.pem @@ -0,0 +1,76 @@ +-----BEGIN CERTIFICATE----- +MIIG6DCCBNCgAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx +PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu +ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg +Q0EwHhcNMjQwNzE1MTI1MDQxWhcNMjkwNzE5MTI1MDQxWjBvMQswCQYDVQQGEwJS +VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg +YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1j0rkZECOt1S8o7I +JY+4YKAxuEa5xaHKHXT2EpkuC/0krqMOjUy2oPIRNgR5g8X0Jl6jamxeGLc4Q1tf +ju6or9oSRYThIUhRsFDQNBiBBEXoBgWxTfiKB2eyT97+pz5TBtBiRCPaLGRHYLRb +9Jz2HkJlxbtNPjtDrF5DPHym+mZ1M1z3hIQYAqJwLpsEBnsw/VxWMlxqHoeewd0h +uJMd71KQ5vOKlz7KrIZ6EobNNa6wItuvsfj3kYCK7O78uLHGXXFxdr8Hae9lMUmC +8F7AFwa+bO1LRlTlqW7rE3rLf+jj70N01N8T3o22v14YBaFBWQWncAVYD2JuL3tH +252+kdNOERf1fLbLRigJAbd+hOhWYlNf963TFDgnNPliHNIW72SygVBnI2V3JwO1 +dp1hVKpK/zt8ziGdHW4gmOLTsH50YKdR4jNqUgQv4wASlKn9OpN6zHYc5G8h86fY +BM+zxE5ikGI+I/vIqBuI0eaDU92AWN/YjFLpu8tMu9kLRSCf1vug6FIfDPWVo7iP +ac/SI2v8jnnpaW7ph/Pz3WkzaG7ZZJsfFs+8dploWc6LOoDtbFBhMdGMxu024msC +0PSjZb5ODXPIaO2NsA7fMiAtZcoK6anTUJh4zOP/stA9qsJGNxdrEmiPXSmBZY/N +Y0wkZgZ6JTDhw7038bPvctkblJkCAwEAAaOCAYswggGHMB0GA1UdDgQWBBR3Pdk5 +r0K93FvKduru/c4+YSkwXzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qws +hzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADCBmAYIKwYBBQUH +AQEEgYswgYgwQAYIKwYBBQUHMAKGNGh0dHA6Ly9udWMtY2RwLnZvc2tob2QucnUv +Y2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQwRAYIKwYBBQUHMAKGOGh0dHA6Ly9u +dWMtY2RwLmRpZ2l0YWwuZ292LnJ1L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3J0 +MIGFBgNVHR8EfjB8MDqgOKA2hjRodHRwOi8vbnVjLWNkcC52b3NraG9kLnJ1L2Nk +cC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMD6gPKA6hjhodHRwOi8vbnVjLWNkcC5k +aWdpdGFsLmdvdi5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG +9w0BAQsFAAOCAgEAmsINXtQ7wwUWvIeOr80MdJS/5G4xhyZOVEmeUorThquT672y +cCg3XCxc4fwbiZqSSbBqntQ7RtiTAKMYMvBageKoVHbzz+R4jX01tKcTx8cDePrz +dJ73bLNUorE7RU9QsW4KyiUeRmjMDV23AUlEvuQFTwgkHXvbac1BBdPn9CrssQuF +5EGohZKcQPFiAAc4SHbRNhlr7uAwgpc/erzI9EAcvA6BVAXcVKoeGpV01uexUgZ6 +St5RP9UmDWNA7T4yVXWJ233N0Q8bl+6AswINQ3PosPu6yQQHQjr65YS06epK+AeI +6j+oGR4xI7EhTQhQvaobnGmX/8QQ7XDRYCP2HXYxiffnn/CfZ/BVyKLYeY1ZipjE +nzqdQIC2+Q3WtY8jsVRQMP38WFRmtsIt5snehnPTs5bKGVIcYzj3o3Ex/K7agEz0 +zAJ0JR5ivXZOvNkT0g9x1v+S1IkU3e/nX1a+tpRquMtnHX0L2lXArNHUbaOO9EJt +d57WaIpofV5cVhhwShOgAuBc9UMJF3/n4t4RKiPxtsK8P67gcmphMhslj7AMYrYM +ej2NvQZY4m3ub3CPC/PrTjDONvb+8g5xrKtxBjYqC74HSB4dg9G3WimSDUuP2Su6 +G2y2TUeyJuCvCLz289VoO0vg7cNdMobE3KCqAiiNhN2VBFxHAUKmUoRcRdw= +-----END CERTIFICATE----- + +subject=C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA +issuer=C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA +-----BEGIN CERTIFICATE----- +MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx +PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu +ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg +Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS +VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg +YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n +qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q +XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U +zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX +YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y +Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD +U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD +4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9 +G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH +BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX +ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa +OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf +BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS +BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH +tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq +W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+ +/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS +AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj +C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV +4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d +WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ +D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC +EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq +391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4= +-----END CERTIFICATE----- + diff --git a/deploy/certs/russian_trusted_root_ca.pem b/deploy/certs/russian_trusted_root_ca.pem new file mode 100644 index 0000000..d167532 --- /dev/null +++ b/deploy/certs/russian_trusted_root_ca.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx +PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu +ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg +Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS +VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg +YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n +qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q +XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U +zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX +YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y +Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD +U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD +4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9 +G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH +BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX +ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa +OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf +BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS +BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF +AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH +tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq +W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+ +/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS +AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj +C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV +4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d +WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ +D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC +EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq +391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4= +-----END CERTIFICATE----- + diff --git a/deploy/certs/russian_trusted_sub_ca.pem b/deploy/certs/russian_trusted_sub_ca.pem new file mode 100644 index 0000000..eb906d5 --- /dev/null +++ b/deploy/certs/russian_trusted_sub_ca.pem @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIG6DCCBNCgAwIBAgICEAUwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx +PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu +ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg +Q0EwHhcNMjQwNzE1MTI1MDQxWhcNMjkwNzE5MTI1MDQxWjBvMQswCQYDVQQGEwJS +VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg +YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1j0rkZECOt1S8o7I +JY+4YKAxuEa5xaHKHXT2EpkuC/0krqMOjUy2oPIRNgR5g8X0Jl6jamxeGLc4Q1tf +ju6or9oSRYThIUhRsFDQNBiBBEXoBgWxTfiKB2eyT97+pz5TBtBiRCPaLGRHYLRb +9Jz2HkJlxbtNPjtDrF5DPHym+mZ1M1z3hIQYAqJwLpsEBnsw/VxWMlxqHoeewd0h +uJMd71KQ5vOKlz7KrIZ6EobNNa6wItuvsfj3kYCK7O78uLHGXXFxdr8Hae9lMUmC +8F7AFwa+bO1LRlTlqW7rE3rLf+jj70N01N8T3o22v14YBaFBWQWncAVYD2JuL3tH +252+kdNOERf1fLbLRigJAbd+hOhWYlNf963TFDgnNPliHNIW72SygVBnI2V3JwO1 +dp1hVKpK/zt8ziGdHW4gmOLTsH50YKdR4jNqUgQv4wASlKn9OpN6zHYc5G8h86fY +BM+zxE5ikGI+I/vIqBuI0eaDU92AWN/YjFLpu8tMu9kLRSCf1vug6FIfDPWVo7iP +ac/SI2v8jnnpaW7ph/Pz3WkzaG7ZZJsfFs+8dploWc6LOoDtbFBhMdGMxu024msC +0PSjZb5ODXPIaO2NsA7fMiAtZcoK6anTUJh4zOP/stA9qsJGNxdrEmiPXSmBZY/N +Y0wkZgZ6JTDhw7038bPvctkblJkCAwEAAaOCAYswggGHMB0GA1UdDgQWBBR3Pdk5 +r0K93FvKduru/c4+YSkwXzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qws +hzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADCBmAYIKwYBBQUH +AQEEgYswgYgwQAYIKwYBBQUHMAKGNGh0dHA6Ly9udWMtY2RwLnZvc2tob2QucnUv +Y2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQwRAYIKwYBBQUHMAKGOGh0dHA6Ly9u +dWMtY2RwLmRpZ2l0YWwuZ292LnJ1L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3J0 +MIGFBgNVHR8EfjB8MDqgOKA2hjRodHRwOi8vbnVjLWNkcC52b3NraG9kLnJ1L2Nk +cC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMD6gPKA6hjhodHRwOi8vbnVjLWNkcC5k +aWdpdGFsLmdvdi5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG +9w0BAQsFAAOCAgEAmsINXtQ7wwUWvIeOr80MdJS/5G4xhyZOVEmeUorThquT672y +cCg3XCxc4fwbiZqSSbBqntQ7RtiTAKMYMvBageKoVHbzz+R4jX01tKcTx8cDePrz +dJ73bLNUorE7RU9QsW4KyiUeRmjMDV23AUlEvuQFTwgkHXvbac1BBdPn9CrssQuF +5EGohZKcQPFiAAc4SHbRNhlr7uAwgpc/erzI9EAcvA6BVAXcVKoeGpV01uexUgZ6 +St5RP9UmDWNA7T4yVXWJ233N0Q8bl+6AswINQ3PosPu6yQQHQjr65YS06epK+AeI +6j+oGR4xI7EhTQhQvaobnGmX/8QQ7XDRYCP2HXYxiffnn/CfZ/BVyKLYeY1ZipjE +nzqdQIC2+Q3WtY8jsVRQMP38WFRmtsIt5snehnPTs5bKGVIcYzj3o3Ex/K7agEz0 +zAJ0JR5ivXZOvNkT0g9x1v+S1IkU3e/nX1a+tpRquMtnHX0L2lXArNHUbaOO9EJt +d57WaIpofV5cVhhwShOgAuBc9UMJF3/n4t4RKiPxtsK8P67gcmphMhslj7AMYrYM +ej2NvQZY4m3ub3CPC/PrTjDONvb+8g5xrKtxBjYqC74HSB4dg9G3WimSDUuP2Su6 +G2y2TUeyJuCvCLz289VoO0vg7cNdMobE3KCqAiiNhN2VBFxHAUKmUoRcRdw= +-----END CERTIFICATE----- + +subject=C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA +issuer=C=RU, O=The Ministry of Digital Development and Communications, CN=Russian Trusted Root CA diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..386fc98 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,81 @@ +services: + db: + image: docker.io/library/postgres:17-alpine + restart: unless-stopped + env_file: .env + environment: + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --no-locale" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 3s + retries: 20 + + api: + build: ./backend + restart: unless-stopped + env_file: .env + command: ["sh", "-c", "alembic upgrade head && exec fintracker serve --host 0.0.0.0 --port 8000"] + depends_on: + db: + condition: service_healthy + volumes: + - uploads:/data/uploads + expose: + - "8000" + + worker: + build: ./backend + restart: unless-stopped + env_file: .env + command: ["fintracker", "worker"] + depends_on: + db: + condition: service_healthy + volumes: + - uploads:/data/uploads + + caddy: + image: docker.io/library/caddy:2-alpine + restart: unless-stopped + env_file: .env + ports: + - "80:80" + - "443:443" + volumes: + - ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro + - ./app/build/web:/srv/app:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - api + + pg-backup: + image: docker.io/library/postgres:17-alpine + restart: unless-stopped + env_file: .env + entrypoint: ["sh", "-c"] + # nightly logical dump, keep 14 days; copy ./backups off-site with rclone/cron on the host + command: + - | + while true; do + ts=$(date +%F_%H%M) + pg_dump -h db -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" -Fc -f "/backups/fintracker_$$ts.dump" && \ + find /backups -name 'fintracker_*.dump' -mtime +14 -delete + sleep 86400 + done + environment: + PGPASSWORD: ${POSTGRES_PASSWORD} + volumes: + - ./backups:/backups + depends_on: + db: + condition: service_healthy + +volumes: + pgdata: + uploads: + caddy_data: + caddy_config: diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a3a9b5f --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1789546076, + "narHash": "sha256-zVxLZiSnmaaPLwnhj7pwmqe3axBg/C6nG5JZsJMh2g4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b1b875982b17dabde9b4a37f3e229e74913e6db3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..4d32f38 --- /dev/null +++ b/flake.nix @@ -0,0 +1,57 @@ +{ + description = "fin-tracker — personal finance + investment analytics"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + python = pkgs.python312; + + # manylinux wheels pulled by uv (grpcio for T-Invest, asyncpg, polars, …) + # dlopen these at runtime. nix-ld is enabled system-wide; we only extend + # its search path (never overwrite — keeps direnv diffs sane). + runtimeLibs = pkgs.lib.makeLibraryPath [ + pkgs.stdenv.cc.cc.lib # libstdc++.so.6 + pkgs.zlib + pkgs.openssl + ]; + + backendTools = [ + python + pkgs.uv + pkgs.just + pkgs.postgresql_17 # local dev cluster (`just db-start`) and psql + pkgs.openapi-generator-cli # `just gen-client` + ]; + + commonHook = '' + export UV_PYTHON="${python}/bin/python3" + export UV_PYTHON_DOWNLOADS=never + export NIX_LD_LIBRARY_PATH="${runtimeLibs}''${NIX_LD_LIBRARY_PATH:+:$NIX_LD_LIBRARY_PATH}" + export LD_LIBRARY_PATH="${runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + # T-Invest gRPC must not go through a local SOCKS/HTTP proxy + export no_proxy="invest-public-api.tbank.ru,.tbank.ru,invest-public-api.tinkoff.ru,.tinkoff.ru,localhost,127.0.0.1''${no_proxy:+,$no_proxy}" + export NO_PROXY="$no_proxy" + export PGDATA="$PWD/.pgdata" + export PGHOST="$PWD/.pgdata" + export PGPORT=54329 + ''; + in { + devShells.${system} = { + # backend only — fast to enter + default = pkgs.mkShell { + packages = backendTools; + shellHook = commonHook + '' + echo "fin-tracker: 'just --list' for recipes; 'nix develop .#app' adds Flutter" + ''; + }; + # backend + Flutter toolchain (large download on first use) + app = pkgs.mkShell { + packages = backendTools ++ [ pkgs.flutter ]; + shellHook = commonHook; + }; + }; + }; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..04a82fd --- /dev/null +++ b/justfile @@ -0,0 +1,111 @@ +set shell := ["bash", "-cu"] + +backend := "backend" +db_url := "postgresql+asyncpg://fintracker:fintracker@localhost:54329/fintracker" + +default: + @just --list + +# --- python --- + +# install backend deps (uv) +sync: + cd {{backend}} && uv sync + +# run API with reload against the local dev DB +api: + cd {{backend}} && DATABASE_URL={{db_url}} uv run fintracker serve --reload + +# API + Flutter web build from one origin (run `just build-web` first) +web: + cd {{backend}} && DATABASE_URL={{db_url}} WEB_DIR=../app/build/web uv run fintracker serve --port 8000 + +# --- flutter --- + +build-web: + cd app && flutter pub get && flutter build web --release + +app-check: + cd app && flutter analyze && flutter test + +# one-off source sync against the local dev DB (zenmoney | cbr) +pull SOURCE: + cd {{backend}} && DATABASE_URL={{db_url}} uv run fintracker sync {{SOURCE}} + +# run the sync worker against the local dev DB +worker: + cd {{backend}} && DATABASE_URL={{db_url}} uv run fintracker worker + +lint: + cd {{backend}} && uv run ruff check . && uv run ruff format --check . + +fmt: + cd {{backend}} && uv run ruff format . && uv run ruff check --fix . + +typecheck: + cd {{backend}} && uv run pyright + +test *ARGS: + cd {{backend}} && uv run pytest {{ARGS}} + +check: lint typecheck test openapi-check app-check + +# --- database (local dev cluster in ./.pgdata, port 54329) --- + +db-init: + test -d .pgdata || initdb -D .pgdata -U postgres --auth=trust --no-locale --encoding=UTF8 >/dev/null + grep -q "unix_socket_directories = '$PWD/.pgdata'" .pgdata/postgresql.conf || \ + printf "\nport = 54329\nunix_socket_directories = '%s/.pgdata'\nlisten_addresses = 'localhost'\n" "$PWD" >> .pgdata/postgresql.conf + +db-start: db-init + pg_ctl -D .pgdata status >/dev/null 2>&1 || pg_ctl -D .pgdata -l .pgdata/log start -w + psql -h localhost -p 54329 -U postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='fintracker'" | grep -q 1 || \ + psql -h localhost -p 54329 -U postgres -c "CREATE ROLE fintracker LOGIN PASSWORD 'fintracker' CREATEDB" + psql -h localhost -p 54329 -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='fintracker'" | grep -q 1 || \ + psql -h localhost -p 54329 -U postgres -c "CREATE DATABASE fintracker OWNER fintracker" + +db-stop: + pg_ctl -D .pgdata status >/dev/null 2>&1 && pg_ctl -D .pgdata stop -m fast || true + +db-psql: + psql -h localhost -p 54329 -U fintracker fintracker + +migrate *ARGS="upgrade head": + cd {{backend}} && DATABASE_URL={{db_url}} uv run alembic {{ARGS}} + +# create a new migration from model changes +revision MSG: + cd {{backend}} && DATABASE_URL={{db_url}} uv run alembic revision --autogenerate -m "{{MSG}}" + +create-user EMAIL: + cd {{backend}} && DATABASE_URL={{db_url}} uv run fintracker create-user {{EMAIL}} + +# --- openapi / dart client --- + +# export openapi/openapi.json from the FastAPI app +openapi: + cd {{backend}} && uv run fintracker openapi ../openapi/openapi.json + +# fail if the committed spec drifted from the code +openapi-check: + cd {{backend}} && uv run fintracker openapi /tmp/fin-tracker-openapi.json + diff -q openapi/openapi.json /tmp/fin-tracker-openapi.json + +# regenerate app/packages/api_client from openapi/openapi.json +gen-client: openapi + rm -rf app/packages/api_client/lib app/packages/api_client/doc + openapi-generator-cli generate -g dart-dio -i openapi/openapi.json -o app/packages/api_client \ + -c app/packages/api_client/openapi-config.yaml + # the generator pins an old language version; null-aware elements need >= 3.8 + sed -i "s/sdk: '>=3.5.0 <4.0.0'/sdk: '>=3.8.0 <4.0.0'/" app/packages/api_client/pubspec.yaml + cd app/packages/api_client && dart pub get && dart run build_runner build --delete-conflicting-outputs + +# --- deploy (VPS) --- + +# build without the host proxy vars: podman/buildah bakes HTTP_PROXY into the image otherwise +up: + env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY -u all_proxy \ + docker compose up -d --build + +logs *ARGS: + docker compose logs -f {{ARGS}}