From ab51b151ed728d1b06001a1b24f7851b5b670e82 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Thu, 25 Jun 2026 11:19:35 +0600 Subject: [PATCH] [2026-06-25] eagle: family/how-to/htpc-emulators-setup.md family/how-to/htpc-gaming-plans.md family/how-to/kraken-access.md family/how-to/openmediavault-rpi5.md family/how-to/time-machine.md family/how-to/wireguard-vpn.md personal/documents/todo-list.md personal/plans/extract-stable-prompt-blocks.md personal/plans/hermes-whale-system-prompt.md personal/plans/thread-scoped-memory.md --- family/how-to/htpc-emulators-setup.md | 165 ++++---- family/how-to/htpc-gaming-plans.md | 3 + family/how-to/kraken-access.md | 114 ++---- family/how-to/openmediavault-rpi5.md | 70 +++- family/how-to/time-machine.md | 36 +- family/how-to/wireguard-vpn.md | 3 +- personal/documents/todo-list.md | 1 + .../plans/extract-stable-prompt-blocks.md | 133 +++++++ personal/plans/hermes-whale-system-prompt.md | 104 +++++ personal/plans/thread-scoped-memory.md | 122 ++++++ personal/projects/balda/balda-valera.md | 332 ++++++++-------- personal/projects/balda/setup.md | 6 +- .../budget-app/bank-statement-import.md | 355 ++++++++++++++++++ .../projects/budget-app/excel-balances.md | 70 ++++ .../budget-app/financial-strategy-and-plan.md | 264 +++++++++++++ .../fire-investment-strategies-2026.md | 246 ++++++++++++ personal/projects/budget-app/index.md | 199 +++++++++- .../projects/personal-os/eagle-dashboard.md | 116 +++++- .../projects/personal-os/hermes-cron-jobs.md | 88 +++-- .../personal-os/obsidian-mcp-setup.md | 84 +++++ .../personal-os/obsidian-mcp-wrapper.md | 125 +----- personal/tech/docker-mac-disk-issues.md | 78 ++++ personal/tech/docker_colima_setup.md | 11 + personal/tech/hermes-memory-architecture.md | 112 ++++++ .../tech/hermes-self-improvement-review.md | 222 +++++++++++ personal/tech/obsidian-mcp-ecosystem.md | 104 +++++ .../ashby-reviews/EP2-Candidate-848-review.md | 70 ++++ work/ashby-reviews/EP2-prompt.md | 25 ++ .../opaque-url-fragment-detection.md | 233 ++++++++++++ .../bugs-absolutestring-scan.md | 160 ++++++++ 30 files changed, 3141 insertions(+), 510 deletions(-) create mode 100644 personal/plans/extract-stable-prompt-blocks.md create mode 100644 personal/plans/hermes-whale-system-prompt.md create mode 100644 personal/plans/thread-scoped-memory.md create mode 100644 personal/projects/budget-app/bank-statement-import.md create mode 100644 personal/projects/budget-app/excel-balances.md create mode 100644 personal/projects/budget-app/financial-strategy-and-plan.md create mode 100644 personal/projects/budget-app/fire-investment-strategies-2026.md create mode 100644 personal/projects/personal-os/obsidian-mcp-setup.md create mode 100644 personal/tech/docker-mac-disk-issues.md create mode 100644 personal/tech/hermes-memory-architecture.md create mode 100644 personal/tech/hermes-self-improvement-review.md create mode 100644 personal/tech/obsidian-mcp-ecosystem.md create mode 100644 work/ashby-reviews/EP2-Candidate-848-review.md create mode 100644 work/ashby-reviews/EP2-prompt.md create mode 100644 work/tech-design/opaque-url-fragment-detection.md create mode 100644 work/wiki/apple-browsers/bugs-absolutestring-scan.md diff --git a/family/how-to/htpc-emulators-setup.md b/family/how-to/htpc-emulators-setup.md index e1c3917b..2fa2437e 100644 --- a/family/how-to/htpc-emulators-setup.md +++ b/family/how-to/htpc-emulators-setup.md @@ -393,116 +393,83 @@ CACHE="$HOME/.var/app/org.libretro.RetroArch/config/retroarch/system/Mupen64plus ## Ryujinx (Nintendo Switch) -**Версия:** 1.3.3 (Ryubing fork, Flatpak `io.github.ryubing.Ryujinx`) -**Статус:** ✅ Работает (10.06.2026) - -**Компоненты:** -| Компонент | Статус | Путь | -|-----------|--------|------| -| Keys | ✅ `prod.keys` (12KB) | `~/.var/app/io.github.ryubing.Ryujinx/data/Ryujinx/system/` | -| Firmware | ✅ 19.0.1 (229 .nca) | установлен через GUI | -| ROMs на диске | ✅ 27 игр | `/run/media/system/Data/Switch Roms/` | -| Symlinks (плоские, без пробелов) | ✅ | `~/Emulation/roms/switch/` | -| В Steam (shortcuts.vdf) | ✅ 24 игры | launchoptions → плоские симлинки | -| Постеры (SteamGridDB) | ✅ wide + poster для всех | `~/.steam/steam/userdata/147839491/config/grid/` | - -**36 игр в Steam (Switch) — статус на 21.06.2026:** -Всего symlinks в `~/Emulation/roms/switch/`: 31. Из них в Steam добавлено 38 (shortcuts.vdf), все с artwork. -Эмулятор: Ryujinx 1.3.3 (flatpak), firmware 19.0.1, prod.keys (12KB), title.keys отсутствует. - -| Игра | ROM slug | Статус | -|------|----------|--------| -| Animal Crossing New Horizons | `animal_crossing.nsp` | ✅ работает | -| Donkey Kong Country Tropical Freeze | `donkey_kong.nsp` | ✅ работает | -| Super Mario Odyssey | `mario_odyssey.nsp` | ✅ работает | -| Mario Kart 8 Deluxe | `mario_kart_8.nsp` | ✅ работает | -| The Legend of Zelda: Breath of the Wild | `zelda_botw.nsp` | ✅ работает | -| Spyro Reignited Trilogy | `spyro_reignited_trilogy.nsp` | ✅ работает | -| Majora's Mask (2Ship2Harkinian) | `majoras_mask.nsp` | ❌ PAL_SEHException (GPU crash) | -| Donkey Kong Country Returns HD | `donkey_kong_country_returns_hd.nsp` | ⏳ NSZ | -| Hollow Knight | `hollow_knight.nsp` | ❌ битый | -| Hollow Knight: Silksong | `hollow_knight__silksong.nsp` | ⏳ NSZ | -| It Takes Two | `it_takes_two.nsp` | ❓ | -| Little Nightmares | `little_nightmares.nsp` | ❓ | -| Little Nightmares III | `little_nightmares_iii.nsp` | ❓ | -| Luigi Mansion 3 | `luigi_mansion_3.nsp` | ❓ | -| Ori and the Will of the Wisps | `ori_and_the_will_of_the_wisps.nsp` | ⏳ NSZ | -| Plants vs. Zombies: Replanted | `plants_vs__zombies__replanted.nsp` | ⏳ NSZ | -| Super Mario 3D All-Stars | `super_mario_3d_all_stars.nsp` | ❓ | -| Super Mario 3D World + Bowser Fury | `super_mario_3d_world___bowser_fury.nsp` | ⏳ NSZ | -| Super Mario Bros. Wonder | `super_mario_bros__wonder.nsp` | ❓ | -| Super Mario Party | `super_mario_party.nsp` | ❓ | -| Super Smash Bros. Ultimate | `smash_ultimate.nsp` | ⏳ NSZ | -| Sonic Mania | `sonic_mania.nsp` | ❓ | -| The Disney Afternoon Collection | `the_disney_afternoon_collection.nsp` | ✅ | -| Unravel Two | `unravel_two.nsp` | ⏳ NSZ | -| Sam & Max Save the World | `sam_and_max_save_the_world.nsp` | ❓ | -| Vasilisa and Baba Yaga | `vasilisa_and_baba_yaga.nsp` | ⏳ NSZ | -| Constance | `constance.nsp` | ❓ | -| Hunting Simulator 2 | `hunting_simulator_2.nsp` | ⏳ NSZ | -| Carnivores Dinosaur Hunt | `carnivores_dinosaur_hunt.nsp` | ⏳ NSZ | - -**Symlink схема:** плоские симлинки без пробелов в `~/Emulation/roms/switch/` → `/run/media/system/Data/Switch Roms/...` Оригинальные симлинки на папки сохранены для совместимости. - -**Transmission:** `/data/Switch Roms` (case-sensitive, с пробелом) мапится в `/run/media/system/Data/Switch Roms/` через mount `/data` → `/run/media/system/Data` - +**Версия:** 1.3.3 (Ryubing fork, Flatpak `io.github.ryubing.Ryujinx`) +**Статус:** ✅ Работает (22.06.2026) → Подробно: [[switch-emulation-rom-infra]] -### ⚠️ Текущий статус — 21.06.2026 +### Компоненты -**Используется:** Flatpak `io.github.ryubing.Ryujinx` 1.3.3 stable. -**AppImage Canary 1.3.287** (`~/Applications/publish/Ryujinx`) — переименован в `Ryujinx.sh.bak`, чтобы launcher не подхватывал его (в 1.3.287 баг `ArgumentOutOfRangeException` в `CheckLaunchState()`, PR #7123/#7116). +| Компонент | Статус | Путь | +|-----------|--------|------| +| prod.keys | ✅ 12KB (22.1.0) | `~/.var/app/io.github.ryubing.Ryujinx/data/Ryujinx/system/` | +| title.keys | ❌ отсутствует | — | +| Firmware | ✅ 19.0.1 (229 .nca) | установлен через GUI | +| ROMs на диске | ✅ | `/run/media/system/Data/Switch Roms/` | +| Symlinks (плоские) | ✅ | `~/Emulation/roms/switch/` | +| В Steam (shortcuts.vdf) | ✅ 38 игр | launchoptions → плоские симлинки | +| Постеры SteamGridDB | ✅ все | `~/.steam/steam/userdata/147839491/config/grid/` | -**Проблема:** после копирования конфигов из AppImage → flatpak, Ryujinx просит prod.keys (хотя файл лежит в `data/Ryujinx/system/prod.keys`, 12KB). firmware 19.0.1 есть (229 .nca). `title.keys` отсутствует. -**Статус:** выясняется — возможно mismatch версий prod.keys (22.1.0) и firmware (19.0.1), либо flatpak sandbox не видит файлы. +### Launcher -**Launcher:** `~/Emulation/tools/launchers/ryujinx.sh` — ищет `~/Applications/publish/Ryujinx.sh` → не находит (`.bak`) → запускает `flatpak run io.github.ryubing.Ryujinx`. -**Логирование:** `~/ryujinx-launch.log` — PWD, ARGS, EXE, EXIT_CODE. +`~/Emulation/tools/launchers/ryujinx.sh` → ищет `~/Applications/publish/Ryujinx.sh` → не находит (`.bak`) → `flatpak run io.github.ryubing.Ryujinx`. Лог: `~/ryujinx-launch.log`. -**Ryujinx не поддерживает NSZ нативно** — только NSP и XCI. NSZ нужно конвертировать в NSP. +AppImage Canary 1.3.287 (`~/Applications/publish/Ryujinx`) переименован в `.bak` — баг `ArgumentOutOfRangeException` в `CheckLaunchState()`. -**NSZ → NSP конвертация (22.06.2026):** -Установлен `nsz` CLI (v4.6) на HTPC через `pip3 install nsz`. -**Правильная команда:** `nsz -D file.nsz` (decompress mode). +### NSZ конвертация -Проверено на The Disney Afternoon Collection — `nsz -D` верифицировал все NCA хеши (`[VERIFIED]`), файл 407MB PFS0 (7 files), работает в Ryujinx ✅. +**Ryujinx не поддерживает NSZ** — только NSP и XCI. Установлен `nsz` CLI (v4.6) на HTPC: `pip3 install nsz`. +Команда: `nsz -D file.nsz`. NSP создаётся рядом с NSZ. После: +1. Переименовать NSP в slug (короткое имя без пробелов/скобок) +2. Создать симлинк: `ln -sf "/run/media/system/Data/Switch Roms/…/slug.nsp" ~/Emulation/roms/switch/slug.nsp` -NSP создаётся рядом с NSZ, с тем же длинным именем (со скобками/размером). После декомпрессии: -1. Переименовать NSP в slug (короткое имя без пробелов/скобок) — `dk_country_returns_hd.nsp` и т.д. -2. Создать симлинк: `ln -sf /run/media/system/Data/Switch\ Roms/…/slug.nsp ~/Emulation/roms/switch/slug.nsp` +### Игры -**Статус:** -- The Disney Afternoon Collection — ✅ конвертирован, работает -- **NSZ на конвертацию (23 игры):** +| Игра | ROM slug | Статус | Формат | Steam | +|------|----------|--------|--------|-------| +| Animal Crossing New Horizons | `animal_crossing.nsp` | ✅ работает | NSP | ✅ | +| Donkey Kong Country Tropical Freeze | `donkey_kong.nsp` | ✅ работает | NSP | ✅ | +| Super Mario Odyssey | `mario_odyssey.nsp` | ✅ работает | NSP | ✅ | +| Mario Kart 8 Deluxe | `mario_kart_8.nsp` | ✅ работает | NSP | ✅ | +| The Legend of Zelda: Breath of the Wild | `zelda_botw.nsp` | ✅ работает | NSP | ✅ | +| Spyro Reignited Trilogy | `spyro_reignited_trilogy.nsp` | ✅ работает | NSP | ✅ | +| The Disney Afternoon Collection | `the_disney_afternoon_collection.nsp` | ✅ работает | NSP | ✅ | +| Sam & Max Save the World | `sam_and_max_save_the_world.nsp` | ✅ работает | NSP | ✅ | +| It Takes Two | `it_takes_two.nsp` | ⚠️ работает, но как Friends Pass (требуется полная версия) | NSP | ✅ | +| Little Nightmares | `little_nightmares.nsp` | ✅ работает | NSP | ✅ | +| Little Nightmares III | `little_nightmares_iii.nsp` | ✅ работает | NSP | ✅ | +| Luigi Mansion 3 | `luigi_mansion_3.nsp` | ❓ не тестировалась | NSP | ✅ | +| Sonic Mania | `sonic_mania.nsp` | ❓ не тестировалась | NSP | ✅ | +| Super Mario 3D All-Stars | `super_mario_3d_all_stars.nsp` | ❓ не тестировалась | NSP | ✅ | +| Super Mario Bros. Wonder | `super_mario_bros__wonder.nsp` | ❓ не тестировалась | NSP | ✅ | +| Super Mario Party | `super_mario_party.nsp` | ❓ не тестировалась | NSP | ✅ | +| Majora's Mask (2Ship2Harkinian) | `majoras_mask.nsp` | ❌ PAL_SEHException (GPU crash) | NSP | ✅ | +| Constance | `constance.nsp` | ❌ Application not found | NSP | — | +| Hollow Knight | `hollow_knight.nsp` | ❌ битый (hash error) | NSP | ✅ | +| Donkey Kong Country Returns HD | `donkey_kong_country_returns_hd.nsp` | ✅ NSP готов | NSP | ✅ | +| Hollow Knight: Silksong | `hollow_knight__silksong.nsp` | ✅ NSP готов | NSP | ✅ | +| Ori and the Will of the Wisps | `ori_and_the_will_of_the_wisps.nsp` | ✅ NSP готов | NSP | ✅ | +| Plants vs. Zombies: Replanted | `plants_vs__zombies__replanted.nsp` | ✅ NSP готов | NSP | ✅ | +| Super Mario 3D World + Bowser Fury | `super_mario_3d_world___bowser_fury.nsp` | ✅ NSP готов | NSP | ✅ | +| Super Smash Bros. Ultimate | `smash_ultimate.nsp` | ✅ NSP готов | NSP | ✅ | +| Unravel Two | `unravel_two.nsp` | ✅ NSP готов | NSP | ✅ | +| Vasilisa and Baba Yaga | `vasilisa_and_baba_yaga.nsp` | ✅ NSP готов | NSP | ✅ | +| Hunting Simulator 2 | `hunting_simulator_2.nsp` | ✅ NSP готов | NSP | ✅ | +| Carnivores Dinosaur Hunt | `carnivores_dinosaur_hunt.nsp` | ✅ работает | NSP | ✅ | +| America Wild Hunting | `america_wild_hunting.nsp` | ✅ NSP готов | NSP | — | +| Animal Hunting 3D | `animal_hunting_3d.nsp` | ✅ NSP готов | NSP | — | +| Best Sniper Legacy Dino Hunt Shooter 3D | `best_sniper_legacy.nsp` | ✅ NSP готов | NSP | — | +| Big Buck Hunter Arcade | `big_buck_hunter_arcade.nsp` | ✅ NSP готов | NSP | — | +| Big Buck Hunter Ultimate Trophy | `big_buck_hunter_ultimate_trophy.nsp` | ✅ NSP готов | NSP | — | +| Cabela's The Hunt Championship Edition | `cabelas_the_hunt.nsp` | ✅ NSP готов | NSP | — | +| Deer Drive Legends | `deer_drive_legends.nsp` | ✅ NSP готов | NSP | — | +| Duck Hunting Challenge | `duck_hunting_challenge.nsp` | ✅ NSP готов | NSP | — | +| Hunt | `hunt.nsp` | ✅ NSP готов | NSP | — | +| Hunter Simulator Wild Hunting | `hunter_simulator_wild_hunting.nsp` | ✅ NSP готов | NSP | — | +| Hunting Simulator | `hunting_simulator.nsp` | ✅ NSP готов | NSP | — | +| Werewolf Hunter Survive the Howl | `werewolf_hunter.nsp` | ✅ NSP готов | NSP | — | -**В Steam (10):** -1. Donkey Kong Country Returns HD -2. Hollow Knight: Silksong -3. Ori and the Will of the Wisps -4. Plants vs. Zombies: Replanted -5. Super Mario 3D World + Bowser Fury -6. Super Smash Bros. Ultimate -7. Unravel Two -8. Vasilisa and Baba Yaga -9. Hunting Simulator 2 -10. Carnivores Dinosaur Hunt - -**Не в Steam, в 14 Hunting games (12):** -11. America Wild Hunting -12. Animal Hunting 3D -13. Best Sniper Legacy Dino Hunt Shooter 3D -14. Big Buck Hunter Arcade -15. Big Buck Hunter Ultimate Trophy -16. Cabela's The Hunt Championship Edition -17. Deer Drive Legends -18. Duck Hunting Challenge -19. Hunt -20. Hunter Simulator Wild Hunting -21. Hunting Simulator -22. Werewolf Hunter Survive the Howl - -**Битый (Hash error):** -- Hollow Knight — ❌ +**Symlink схема:** плоские симлинки без пробелов в `~/Emulation/roms/switch/` → `/run/media/system/Data/Switch Roms/...` +**Transmission:** `/data/Switch Roms` мапится в `/run/media/system/Data/Switch Roms/` через mount `/data` → `/run/media/system/Data` --- diff --git a/family/how-to/htpc-gaming-plans.md b/family/how-to/htpc-gaming-plans.md index 35b383ab..97db46df 100644 --- a/family/how-to/htpc-gaming-plans.md +++ b/family/how-to/htpc-gaming-plans.md @@ -55,6 +55,9 @@ → **Полное research + план действий: [[switch-emulation-rom-infra]]** Приоритет 1: **Switch (DKC: Tropical Freeze)** — Ryujinx (GreemDev fork) через EmuDeck + +### TODO: +- [ ] It Takes Two — смержить DLC Full Game (`010092A0172E5001`) с base NSP (сейчас работает как Friends Pass) Приоритет 2: **Wii (DKC Returns оригинал)** — Dolphin через EmuDeck (проще, без keys) **Важно:** оригинальный Ryujinx закрылся (окт. 2024). Актуальный — GreemDev fork. Eden — для Android. diff --git a/family/how-to/kraken-access.md b/family/how-to/kraken-access.md index b4074f34..2c897695 100644 --- a/family/how-to/kraken-access.md +++ b/family/how-to/kraken-access.md @@ -1,99 +1,51 @@ # Kraken — Внешний доступ -> Обновлено: 2026-05-13 +> Обновлено: 2026-06-24 -## SSH с любой машины +## Как зайти -**Контейнер hermes-kraken:** -- Маппинг `/home/kraken/.ssh:/opt/data/.ssh:ro` в docker-compose.yml -- Кастомный `/etc/passwd` с пользователем `kraken:x:1000:1000` (файл `passwd-with-kraken` рядом с compose) -- `HERMES_UID=1000 HERMES_GID=1000` чтоб entrypoint ремапил пользователя -- SSH из контейнера: `ssh -i /opt/data/.ssh/id_ed25519 kraken@localhost` -- Ключ `kraken` добавлен в `authorized_keys` на хосте +Везде `ssh kraken`. Резолвится через `~/.ssh/config` на Eagle: -`~/.ssh/config` на Eagle: -``` -Host kraken - HostName kraken - User kraken - Port 22 - IdentityFile ~/.ssh/id_rsa - StrictHostKeyChecking no - ProxyCommand bash -c 'if ifconfig | grep -q 10.99.0.2; then nc 10.99.1.2 22; else nc kraken 22; fi' -``` +- **Дома** — напрямую по LAN (`192.168.1.15`) +- **Снаружи** — через WireGuard (`10.99.1.2`, VPN поднимается автоматически `wg-auto.sh`) -**SSH напрямую по LAN:** -```bash -ssh kraken@kraken -``` +WireGuard split-tunnel: Eagle (10.99.0.2) ↔ VPS ↔ Kraken (10.99.1.2). Подробнее: [[wireguard-vpn]]. -**SSH через VPS** (reverse tunnel, порт 2223): -```bash -ssh -J root@91.207.28.205 kraken@127.0.0.1 -p 2223 -``` +## Portainer (локально) -`kraken.qentra.top` — Cloudflare Tunnel, только HTTP (Hermes API), SSH не работает. - -`kraken.qentra.top` — Cloudflare Tunnel, только HTTP (Hermes API), SSH не работает. - ---- - -## Как устроен туннель - -Кракен держит постоянный **reverse SSH tunnel** на VPS через autossh: - -``` -Кракен (RPi 5, 192.168.1.15) - └── autossh → ssh -R 2223:localhost:22 root@91.207.28.205 - ↓ - VPS (91.207.28.205) - слушает :2223 → перенаправляет на Кракен:22 -``` - -Аналогично Тайге (у неё порт 2222). - -### Управление туннелем на Кракене - -```bash -sudo systemctl status kraken-tunnel -sudo systemctl restart kraken-tunnel -sudo journalctl -u kraken-tunnel -n 20 -``` - -Файл сервиса: `/etc/systemd/system/kraken-tunnel.service` -SSH-ключ Кракена: `/home/kraken/.ssh/id_ed25519` -Публичный ключ добавлен в `/root/.ssh/authorized_keys` на VPS. - -### Проверить с VPS - -```bash -ssh root@91.207.28.205 "ss -tlnp | grep 2223" -``` - ---- - -## Cloudflared (для будущих сервисов) - -Контейнер `cloudflared` запущен на Кракене (`unless-stopped`), туннель `kraken` активен в CF Zero Trust. -Hostname: `kraken.qentra.top → HTTP → localhost:8642` (Hermes API). - ---- - -## Portainer API (локально) +`http://kraken:9000` +API: ```bash BASE=http://localhost:9000 KEY="ptr_AJY+Ba9A7f6pcAHZfDD5koU4stkKgJCdbTEXLDLxn0g=" EP=3 # endpoint ID на Кракене = 3, не 1! - -curl -s -H "X-API-Key: $KEY" "$BASE/api/endpoints/$EP/docker/containers/json?all=true" \ - | jq '[.[] | {name: .Names[0], status: .Status}]' ``` ---- +## Hermes API + +`http://kraken:8642/v1/chat/completions` — OpenAI-compatible endpoint. + +## Docker контейнеры (2026-06-24) + +| Имя | Заметки | +|-----|---------| +| flaresolverr | | +| hermes-kraken | | +| homeassistant | | +| jellyfin | | +| portainer | | +| prowlarr | | +| radarr | | +| rclone | | +| sonarr | | +| transmission | | +| cloudflared | | +| watchtower | | ## Связанные заметки -- [[kraken-portainer-access]] — полная инструкция по деплою cloudflared через Portainer -- [[openmediavault-rpi5]] — настройка Кракена (Docker, Hermes, структура папок) -- [[truenas-remote-access-reverse-proxy]] — аналогичный туннель для Тайги +- [[kraken-network]] — SSH, WG топология +- [[wireguard-vpn]] — полное описание WG +- [[kraken-portainer-access]] +- [[openmediavault-rpi5]] diff --git a/family/how-to/openmediavault-rpi5.md b/family/how-to/openmediavault-rpi5.md index 859fc56e..d84437e4 100644 --- a/family/how-to/openmediavault-rpi5.md +++ b/family/how-to/openmediavault-rpi5.md @@ -5,7 +5,7 @@ tags: - openmediavault - how-to created: '2026-05-11' -updated: '2026-05-12' +updated: '2026-06-24' status: hermes-pending ip: 192.168.1.15 --- @@ -26,7 +26,7 @@ ip: 192.168.1.15 | 6 — USB HDD | ✅ | 12TB Seagate ST12000NT001, EXT4, UUID `49e8f586-3839-4c5d-a1e1-58bfc3579ade`, смонтирован через OMV в `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade` | | 7 — omv-extras + Docker | ✅ | omv-extras 7.0.6, Docker 29.4.3, kraken в группе docker | | 8 — Контейнеры | ✅ | Все через отдельные docker compose, конфиги на HDD (см. ниже) | -| 9 — Time Machine share | ✅ | `mbentley/timemachine:smb`, расписание 10:00-22:00 GMT+6, HDD/timemachine | +| 9 — Time Machine share | ✅ | Samba на хосте (не Docker), шара TimeMachine, квота 2.8 TiB | | 10 — Перенос данных | N/A | не входит в этот гайд | | 11 — Hermes агент Кракен | ✅ | Запущен, Telegram @kraken_htpc_bot живой, mcpvault ✅, obsidian RW, SOUL.md, browser/Playwright ✅, Spotify ✅ | | 11а — Docker на HDD | ✅ | data-root + containerd перенесены на HDD через symlink, SD освободилась до 22% | @@ -276,11 +276,14 @@ read only = no inherit acls = yes fruit:time machine = yes vfs objects = catia fruit streams_xattr +fruit:time machine max size = 3000000000000 # квота ~2.8 TiB ``` +> **TM квота:** установлена 2026-06-24 — `3000000000000` байт (~2.8 TiB). Раньше было `0` (безлимит), из-за чего TM занял 2.2T из 11T HDD. При достижении квоты Mac начнёт удалять старые снепшоты. + **Credentials:** - Пользователь: `timemachine` -- Пароль: сохранён в bitwarden (или уточнить у Alex) +- Пароль: `timemachine` **Подключение с Mac:** 1. Finder → `⌘K` → `smb://kraken` @@ -462,6 +465,67 @@ docker inspect hermes-kraken --format 'Status={{.State.Status}} Restarts={{.Rest --- +## Перенаправление записи с SD на HDD (2026-06-24) + +**Проблема (2026-06-24):** Kraken крашился — SD-карта забита под 99% (14G/14G, 40MB свободно). folder2ram держит `/var/log` в tmpfs, но при ребуте/shutdown OMV сбрасывает tmpfs на SD. + +**Решение:** bind-mount всех пишущихся каталогов на HDD. + +| Каталог | Цель на HDD | Метод | +|---------|-------------|-------| +| `/var/folder2ram/var/log` | `.../var-log` | fstab bind | +| `/var/cache/apt` | `.../apt-cache` | fstab bind | +| `/var/log/journal` | `.../journal` | fstab bind | +| Docker data-root | `.../docker-data` | daemon.json | +| containerd | `.../containerd` | symlink | + +### Команды + +```bash +HDD="/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade" + +# folder2ram/var/log +sudo mkdir -p "$HDD/var-log" +sudo rsync -a /var/folder2ram/var/log/ "$HDD/var-log/" +sudo mount --bind "$HDD/var-log" /var/folder2ram/var/log +echo "$HDD/var-log /var/folder2ram/var/log none bind,nofail 0 0" | sudo tee -a /etc/fstab + +# Apt-кэш +sudo mkdir -p "$HDD/apt-cache" +sudo rsync -a /var/cache/apt/ "$HDD/apt-cache/" +sudo mount --bind "$HDD/apt-cache" /var/cache/apt +echo "$HDD/apt-cache /var/cache/apt none bind,nofail 0 0" | sudo tee -a /etc/fstab + +# Journald +sudo mkdir -p "$HDD/journal" +sudo rsync -a /var/log/journal/ "$HDD/journal/" 2>/dev/null +sudo mount --bind "$HDD/journal" /var/log/journal +echo "$HDD/journal /var/log/journal none bind,nofail 0 0" | sudo tee -a /etc/fstab +sudo systemctl restart systemd-journald +``` + +Проверка: +```bash +df -h /var/folder2ram/var/log /var/log/journal /var/cache/apt +# Все должны показывать /dev/sda1 (HDD), не mmcblk0 (SD) +``` + +> ⚠️ OMV может перезаписывать `/etc/fstab` при `omv-salt deploy run`. Если bind пропадёт — перепроверить. + +### df vs du — расхождение на SD + +`df` = 13G/14G, `du -shx /` = 3.6G. Разница ~10G — **блоки ext4, занятые физически на NAND но не привязанные к файлам** (эффект отсутствия TRIM). На SD это норма. + +Остаточная запись на SD после переносов: `/etc`, `/var/lib/samba` (8 MB), `/home/kraken` (uv кэш), dpkg — стабильно. + +### SD backup + +Скрипт: `/usr/local/bin/sd-backup.sh` — образ SD на HDD, хранит 3 последних копии. +```bash +sudo /usr/local/bin/sd-backup.sh # ~11G, ~11 мин +``` +Хранится: `$HDD/backup/sd-card/kraken-sd-*.img.gz`. Ротация: auto-removes oldest. + ## Шаг 11а — Docker и containerd на HDD **Статус: ✅ (2026-05-13)** diff --git a/family/how-to/time-machine.md b/family/how-to/time-machine.md index 521cc57a..016ba8f6 100644 --- a/family/how-to/time-machine.md +++ b/family/how-to/time-machine.md @@ -10,16 +10,20 @@ Destination: `smb://timemachine@kraken/TimeMachine` Пароль: `smbpasswd -a timemachine` passdb.tdb персистентен на хосте — пароль не слетает при рестарте -Бандл лежит на TrueNAS, смонтирован по SMB в Кракен. +Бандл лежит на локальном HDD Кракена: `/srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/timemachine/` ## Конфигурация OMV Samba Шара добавлена через `extraoptions` в `conf.service.smb`: ``` [TimeMachine] - path = /mnt/timemachine - valid users = timemachine - ... + path = /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/timemachine + browseable = yes + read only = no + valid users = timemachine + vfs objects = fruit streams_xattr + fruit:time machine = yes + fruit:time machine max size = 3000000000000 ``` Применить конфиг: @@ -45,9 +49,33 @@ sudo tmutil setdestination -p smb://timemachine@kraken/TimeMachine ## Связанные заметки - [[family/how-to/wireguard-vpn|WireGuard VPN]] — туннель для бэкапа вне дома +- [[family/how-to/kraken-network|Kraken Network]] — ssh, сетевые адреса + +## Важно: связка WireGuard + SMB + +Когда mbp-black не дома (IP не `192.168.1.x`), `kraken` резолвится через VPS dnsmasq +в `10.99.1.2` (WG адрес). SMB работает через туннель, но может быть медленнее. + +Проверка доступа через WG с mbp-black: +```bash +smbutil view //timemachine@kraken +``` + +Если дома — лучше через LAN (`smb://192.168.1.15/TimeMachine`), быстрее. ## History - **До 2026-06-02**: использовался Docker-контейнер `mbentley/timemachine:smb`. Контейнер снесён, Samba перенесена на хост OMV из-за проблем с persisting паролей (tdbsam не сохранялся, `ntlm auth = no` ломал backupd при reconnect). +- **2026-06-24**: проверена доступность smbd и бандла. + - **Проблема:** пароль `timemachine` не проходит при подключении через WireGuard (mbp-black + вне дома, IP `10.120.1.143`, Kraken резолвится в WG `10.99.1.2`). `mount_smbfs` и `smbutil` + выдают `Authentication error`. + - **Проверено на Kraken:** smbd слушает на `0.0.0.0:445`, TCP/445 через WG открыт, + `smbstatus` показывает активных сессий 0. Samba конфигурация: `disable netbios = yes`, + `log level = 0` (логи в /var/log/samba/log.smbd пустые — запросы аутентификации даже + не доходят до smbd, значит проблема на клиентской стороне). + - **Диагностика не завершена** — причина Authentication error не выяснена. + - **Доки обновлены:** убрано упоминание TrueNAS, прописан прямой пароль `timemachine` + в credentials в обоих документах (`time-machine.md` и `openmediavault-rpi5.md`). diff --git a/family/how-to/wireguard-vpn.md b/family/how-to/wireguard-vpn.md index 40536ab6..007417a7 100644 --- a/family/how-to/wireguard-vpn.md +++ b/family/how-to/wireguard-vpn.md @@ -55,7 +55,8 @@ Destination обновлён: - Старый: `smb://timemachine@kraken._smb._tcp.local./TimeMachine` (mDNS, не работает) - Новый: `smb://timemachine@kraken/TimeMachine` (VPN DNS ✅) -Контейнер: `mbentley/timemachine:smb` на Кракене, работает постоянно (`restart: unless-stopped`). +Samba работает на **хосте OMV** (не в Docker-контейнере). См. [[family/how-to/time-machine]]. +Контейнер `mbentley/timemachine:smb` больше не используется. --- diff --git a/personal/documents/todo-list.md b/personal/documents/todo-list.md index fc3ae568..9e0ed6e3 100644 --- a/personal/documents/todo-list.md +++ b/personal/documents/todo-list.md @@ -7,6 +7,7 @@ related: - "[[personal/documents/deferred-tasks]]" - "[[personal/documents/russia-tasks]]" --- +- [ ] Cleanup cloud.mail.ru - [ ] https://x.com/i/status/2056077614113325373 Claude code free - [x] жене SketchUp установить - [ ] Eagle/Whale/Balda - в docker контейнеры diff --git a/personal/plans/extract-stable-prompt-blocks.md b/personal/plans/extract-stable-prompt-blocks.md new file mode 100644 index 00000000..030d7a66 --- /dev/null +++ b/personal/plans/extract-stable-prompt-blocks.md @@ -0,0 +1,133 @@ +# План: Вынести хардкод stable-блоков system prompt в файлы + +**Статус:** Реализовано ✅ (ждёт перезапуска webhook) + +## Мотивация + +В Hermes Whale все stable-блоки system prompt (identity, guidance, enforcement) захардкожены в `agent/prompt_builder.py` как Python-константы. Невозможно изменить их без редактирования исходного кода Hermes Agent. + +## Решение + +Вынести каждый блок в отдельный `.md` файл, добавить маппинг в `config.yaml: agent.prompt_overrides`, и модифицировать `agent/system_prompt.py`, чтобы он читал файлы вместо констант. + +## Изменяемые файлы + +### 1. `agent/system_prompt.py` — замена констант на file-load + +Добавлена функция `_load_prompt_block(agent, block_name, default_text)` — строки 51-71 в `/Users/admin/.hermes/hermes-agent/agent/system_prompt.py`: +- Читает `agent._prompt_overrides` (берётся из конфига на старте) +- Если для `block_name` указан путь — читает файл, возвращает его содержимое +- Иначе возвращает `default_text` + +Заменены все 7 прямых ссылок на константы в `build_system_prompt_parts()` на вызовы `_load_prompt_block()`. + +Добавлены импорты: `import logging`, `import os`, `from pathlib import Path`, `logger = logging.getLogger(__name__)`. + +**Константы, которые заменяются (7 блоков):** + +| Блок | Константа | Вставляется при условии | +|------|-----------|------------------------| +| `hermes_help` | `HERMES_AGENT_HELP_GUIDANCE` | всегда | +| `task_completion` | `TASK_COMPLETION_GUIDANCE` | всегда | +| `memory_guidance` | `MEMORY_GUIDANCE` | когда есть tool "memory" | +| `session_search_guidance` | `SESSION_SEARCH_GUIDANCE` | когда есть tool "session_search" | +| `skills_guidance` | `SKILLS_GUIDANCE` | когда есть tool "skill_manage" | +| `tool_use_enforcement` | `TOOL_USE_ENFORCEMENT_GUIDANCE` | зависит от модели | +| `execution_discipline` | `OPENAI_MODEL_EXECUTION_GUIDANCE` | зависит от модели | + +**Не заменяется (остаётся в коде):** +- `DEFAULT_AGENT_IDENTITY` — это fallback когда нет SOUL.md (у нас есть SOUL.md, не нужно) +- `GOOGLE_MODEL_OPERATIONAL_GUIDANCE` — Google-specific, неактуально для Whale +- `COMPUTER_USE_GUIDANCE` — нет toolset +- `KANBAN_GUIDANCE` — нет kanban +- `PLATFORM_HINTS` — platform-specific, другая логика + +### 2. `agent/system_prompt.py` — добавить функцию загрузки + +```python +def _load_prompt_block(agent, block_name: str, default: str) -> str: + """Load a prompt block from a file if configured, else return default.""" + overrides = getattr(agent, "_prompt_overrides", None) or {} + path = overrides.get(block_name) + if path: + try: + resolved = os.path.expanduser(path) + content = Path(resolved).read_text(encoding="utf-8").strip() + if content: + return content + except Exception: + logger.debug("Could not load prompt override '%s' from %s", block_name, path) + return default +``` + +### 3. `agent/agent_init.py` — пробросить конфиг + +После загрузки `_agent_cfg` (строка ~1058) добавлено чтение `agent.prompt_overrides`: + +```python +agent._prompt_overrides = {} +try: + _po = _agent_cfg.get("agent", {}).get("prompt_overrides", {}) + if isinstance(_po, dict): + agent._prompt_overrides = _po +except Exception: + pass +``` + +### 4. Конфиг Whale — `config.yaml` + +Добавить секцию: + +```yaml +agent: + prompt_overrides: + hermes_help: ~/.hermes/hermes-whale/review/hermes_help.md + task_completion: ~/.hermes/hermes-whale/review/task_completion.md + memory_guidance: ~/.hermes/hermes-whale/review/memory_guidance.md + session_search_guidance: ~/.hermes/hermes-whale/review/session_search_guidance.md + skills_guidance: ~/.hermes/hermes-whale/review/skills_guidance.md + tool_use_enforcement: ~/.hermes/hermes-whale/review/tool_use_enforcement.md + execution_discipline: ~/.hermes/hermes-whale/review/execution_discipline.md +``` + +### 5. Файлы блоков + +Создать 7 файлов в `~/.hermes/hermes-whale/review/`: + +- `hermes_help.md` — содержимое константы `HERMES_AGENT_HELP_GUIDANCE` +- `task_completion.md` — содержимое `TASK_COMPLETION_GUIDANCE` +- `memory_guidance.md` — содержимое `MEMORY_GUIDANCE` +- `session_search_guidance.md` — содержимое `SESSION_SEARCH_GUIDANCE` +- `skills_guidance.md` — содержимое `SKILLS_GUIDANCE` +- `tool_use_enforcement.md` — содержимое `TOOL_USE_ENFORCEMENT_GUIDANCE` +- `execution_discipline.md` — содержимое `OPENAI_MODEL_EXECUTION_GUIDANCE` + +### 6. Обратная совместимость + +Если `prompt_overrides` не задан или файл не найден — используется хардкод. Никакой код не ломается для других профилей/пользователей. + +### 7. Документация + +- Обновить `personal/plans/thread-scoped-memory.md` → переименовать или создать отдельный doc +- Создать `personal/plans/extract-stable-prompt-blocks.md` (этот) + +## Порядок выполнения + +1. ✅ Создать 7 файлов блоков в `~/.hermes/hermes-whale/review/` +2. ✅ Пропатчить `agent/system_prompt.py` — добавить `_load_prompt_block()` и заменить 7 констант +3. ✅ Пропатчить `agent/agent_init.py` — пробросить `prompt_overrides` из конфига +4. ✅ Обновить `config.yaml` — добавить `agent.prompt_overrides` (через копию, т.к. patch блокирован TIRITH) +5. ⏸️ Перезапустить webhook (ждёт команды) +6. ⏸️ Обновить Obsidian docs (этот шаг) + +## Проверка + +После изменений system prompt должен содержать те же блоки, что и раньше, но загруженные из файлов. При изменении файла и рестарте сессии — новый текст. При удалении файла — fallback на хардкод. + +## Pitfalls + +- **TIRITH блокирует patch/config.yaml** — `config.yaml` под защитой TIRITH, patch и write_file отказываются писать в него. Решение: `cp` в `/tmp/`, отредактировать там, `cp` обратно (с аппрувом). +- **sed не подходит для YAML конфигов** — сложные многострочные замены с вложенными отступами и escape-символами (`~`, `/`) ломаются в sed. Лучше patch. +- **Конфиг всё равно просит аппрув** — при `cp` обратно TIRITH запрашивает подтверждение (overwrite project env/config file). Это нормально, нужно подтвердить. +- **SOUL.md уже существует** — в Whale он лежит в `/Users/admin/.hermes/hermes-whale/SOUL.md`. Он НЕ выносится через prompt_overrides, т.к. уже является файлом и загружается отдельно через `load_soul_md()`. +- **webhook не в PLATFORM_HINTS** — webhook нет в словаре `PLATFORM_HINTS` в `prompt_builder.py`, поэтому блок platform hints пустой. Это не менялось. diff --git a/personal/plans/hermes-whale-system-prompt.md b/personal/plans/hermes-whale-system-prompt.md new file mode 100644 index 00000000..54c37e97 --- /dev/null +++ b/personal/plans/hermes-whale-system-prompt.md @@ -0,0 +1,104 @@ +# Состав system prompt Hermes Whale + +**Дата анализа:** 2026-06-24 +**Версия:** Текущее состояние на момент разговора + +System prompt собирается в `agent/system_prompt.py` (Hermes Agent) из трёх слоёв: **stable**, **context**, **volatile**. + +--- + +## Полный состав + +### СТАБИЛЬНЫЙ СЛОЙ (stable) — кешируется на всю сессию + +Этот слой не меняется между поворотами. Всё, кроме SOUL.md, захардкожено в `agent/prompt_builder.py`. + +| # | Блок | Источник | Описание | +|---|------|----------|----------| +| 1 | **SOUL.md** | `~/.hermes/hermes-whale/SOUL.md` | Кастомная идентичность: «Кит (Whale) — Alex's personal agent». Правила: Obsidian MCP, jq, backup before edit, plan first, stop on стоп. | +| 2 | **Hermes Agent help guidance** | `prompt_builder.py`: `HERMES_AGENT_HELP_GUIDANCE` | docs.hermes-agent.nousresearch.com — source of truth | +| 3 | **Task completion guidance** | `prompt_builder.py`: `TASK_COMPLETION_GUIDANCE` | «Finishing the job» — доводить до конца, не фабриковать | +| 4 | **Memory guidance** | `prompt_builder.py`: `MEMORY_GUIDANCE` | Как сохранять memory (declarative facts, не task progress) | +| 5 | **Session search guidance** | `prompt_builder.py`: `SESSION_SEARCH_GUIDANCE` | session_search для кросс-сессионного контекста | +| 6 | **Skills guidance** | `prompt_builder.py`: `SKILLS_GUIDANCE` | Сохранять сложные подходы как skills, патчить устаревшие | +| 7 | **Tool-use enforcement** | `prompt_builder.py`: `TOOL_USE_ENFORCEMENT_GUIDANCE` | Ты ДОЛЖЕН вызывать инструменты, не описывать планы | +| 8 | **Execution discipline** | `prompt_builder.py`: `OPENAI_MODEL_EXECUTION_GUIDANCE` | Расширенные правила: tool persistence, prerequisite checks, verification | +| 9 | **Skills prompt** | `prompt_builder.py`: `build_skills_system_prompt()` | Список всех доступных skills (автогенерируется) | +| 10 | **Environment hints** | `prompt_builder.py`: `build_environment_hints()` | macOS, home dir, cwd | +| 11 | **Active profile hint** | `system_prompt.py` runtime | «default» + guard не лезть в чужие профили | +| 12 | **Platform hints** | `prompt_builder.py`: `PLATFORM_HINTS` | webhook **нет** в словаре → пусто | + +**Note #7–8:** Т.к. модель `deepseek-chat` попадает под `TOOL_USE_ENFORCEMENT_MODELS = ("gpt", "codex", "gemini", "gemma", "grok", "glm", "qwen", "deepseek")`, то enforcement применяется. DeepSeek НЕ входит в OpenAI-специфичную часть (GPT/Codex/Grok), поэтому блок `OPENAI_MODEL_EXECUTION_GUIDANCE` не вставляется — только `TOOL_USE_ENFORCEMENT_GUIDANCE`. + +### КОНТЕКСТНЫЙ СЛОЙ (context) — зависит от cwd + +| # | Блок | Что приходит | +|---|------|-------------| +| 13 | **AGENTS.md / CLAUDE.md / .cursorrules / .hermes.md** | Из `TERMINAL_CWD`. В этой сессии `TERMINAL_CWD=/Users/admin` — файлов нет → пусто | +| 14 | **system_message** | Caller-supplied. Webhook не передаёт → пусто | + +Поиск контекстных файлов (функция `build_context_files_prompt()`): +1. `.hermes.md` / `HERMES.md` (walk to git root) +2. `AGENTS.md` / `agents.md` (cwd only) +3. `CLAUDE.md` / `claude.md` (cwd only) +4. `.cursorrules` + `.cursor/rules/*.mdc` (cwd only) +— Первый найденный wins, только один проект-контекст загружается. + +SOUL.md из HERMES_HOME независим и не участвует в этой очереди — он загружается отдельно через `load_soul_md()`. + +### ВОЛАТИЛЬНЫЙ СЛОЙ (volatile) — пересобирается каждый раз + +| # | Блок | Источник | Описание | +|---|------|----------|----------| +| 15 | **MEMORY** | `memories/threads//MEMORY.md` | Thread-scoped memory (включено конфигом) | +| 16 | **USER profile** | `memories/USER.md` | Глобальный профиль пользователя | +| 17 | **Memory instruction** | `~/.hermes/hermes-whale/review/memory_prompt.md` | Кастомная инструкция что запоминать | +| 18 | **Timestamp/model/provider** | Runtime | Дата старта, Model, Provider | + +--- + +## Ключевые файлы + +### SOUL.md +- Путь: `~/.hermes/hermes-whale/SOUL.md` (HERMES_HOME/SOUL.md) +- Полностью заменяет `DEFAULT_AGENT_IDENTITY` +- Правила: Obsidian MCP first, jq, backup before edit, plan first, stop on стоп +- Загружается `load_soul_md()` в `agent/prompt_builder.py` + +### memory_prompt.md +- Путь: `~/.hermes/hermes-whale/review/memory_prompt.md` +- Конфиг: `memory.prompt_path` в config.yaml +- Загружается в `agent/agent_init.py`, вставляется в volatile слой + +### config.yaml (Whale) +- Путь: `~/.hermes/hermes-whale/config.yaml` +- Релевантные секции: + ```yaml + memory: + thread_scoped: true + prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md + ``` +- Webhook route: `whale` на порту 8645, deliver в zulip +- Webhook платформа **не имеет** platform hint в `PLATFORM_HINTS` + +--- + +## Отсутствующие возможности (opportunity) + +Алекс предложил сделать стабильный слой конфигурируемым через файлы так же, как `memory_prompt.md`: +- Вынести каждый хардкод-блок (hermes help, task completion, memory guidance, session search, skills guidance и т.д.) в отдельный `.md` файл +- Добавить `prompt_overrides` секцию в config.yaml, где указывать пути к файлам +- Если файла нет — fallback на хардкод + +Связанные файлы Hermes Agent: +- `agent/system_prompt.py` — сборка system prompt, управляющая логика +- `agent/prompt_builder.py` — константы и функции загрузки +- `agent/agent_init.py` — чтение конфига и проброс + +--- + +## Текущие ограничения + +- webhook не имеет platform hint → может быть полезно добавить +- Всё хардкоженое в `prompt_builder.py` нельзя переопределить без редактирования кода +- Нет `.hermes.md` / `AGENTS.md` в `/Users/admin/` — контекстный слой пуст diff --git a/personal/plans/thread-scoped-memory.md b/personal/plans/thread-scoped-memory.md new file mode 100644 index 00000000..06357fa9 --- /dev/null +++ b/personal/plans/thread-scoped-memory.md @@ -0,0 +1,122 @@ +# Реализация: Thread-scoped memory + custom memory prompt + +**Статус:** Реализовано ✅ + +## Изменённые файлы + +### 1. `tools/memory_tool.py` — MemoryStore с thread_key + +**`__init__`** — новый параметр `thread_key: Optional[str] = None`, сохраняется как `self.thread_key`. + +**`_path_for(target)`** — теперь instance method (был static): +- `target == "user"` → всегда `memories/USER.md` +- `target == "memory"` и `self.thread_key` задан → `memories/threads//MEMORY.md` +- `target == "memory"` без thread_key → `memories/MEMORY.md` (глобальный fallback) + +**`load_from_disk()`** — использует `self._path_for("memory")` и `self._path_for("user")` вместо хардкода. + +### 2. `agent/agent_init.py` — конфиг + проброс + +Новые поля на агенте: +- `_memory_thread_scoped` — читается из `config.yaml: memory.thread_scoped` +- `_memory_instruction` — читается из `config.yaml: memory.prompt_path` (.md файл) + +MemoryStore создаётся с `thread_key=_gateway_session_key` если `thread_scoped: true`. + +Кастомная memory instruction логируется: `Loaded custom memory instruction from ...`. + +### 3. `agent/system_prompt.py` — вставка в system prompt + +В блок MEMORY добавляется: +- Заголовок `MEMORY for thread: ` (вместо `MEMORY (your personal notes)`) когда thread_scoped включён и есть ключ. + +После memory блока вставляется отдельный блок `MEMORY INSTRUCTION` (с опциональным `for thread: `), содержащий кастомную инструкцию из .md файла. + +### 4. `gateway/run.py` — уже пробрасывает + +`gateway_session_key` уже передаётся в `AIAgent.__init__` на строке ~17828. Никаких изменений не потребовалось. + +### 5. `run_agent.py` — уже принимает + +Параметр `gateway_session_key` уже есть в `AIAgent.__init__`. Пробрасывается в `init_agent()` где записывается как `agent._gateway_session_key`. + +## Конфиг (Whale) + +```yaml +memory: + ... + thread_scoped: true + prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md +``` + +## Файлы + +- **`~/.hermes/hermes-whale/review/memory_prompt.md`** — инструкция что запоминать (документы, команды, конфиги, статус проекта, решения). + - После добавления нового пункта в список patch() не перенумеровывает — нужен второй clean patch. + - **2026-06-24:** Добавлен пункт 2 — после загрузки Obsidian docs (skill_view, mcp_obsidian_read_note) извлекать ключевые факты в memory. + +## Файловая структура на диске + +``` +~/.hermes/hermes-whale/memories/ +├── MEMORY.md # глобальная (fallback для CLI/старых сессий) +├── USER.md # глобальная (всегда) +└── threads/ + ├── agent:main:webhook:webhook:webhook:whale/ + │ └── MEMORY.md # память Whale + ├── agent:main:zulip:stream:general:thread:123/ + │ └── MEMORY.md # память конкретного треда + └── ... +``` + +## Коммиты + +- `hermes-agent`: `98cb69b50` — feat: thread-scoped memory + configurable memory instruction +- `hermes-agent`: `4ebad4f69` — test: thread-scoped memory persistence, drift guard, snapshot, sanitization (+9 тестов, 142 строки) +- `hermes-whale`: `736f8f3` — whale: enable thread-scoped memory and custom memory instruction + +## Тесты + +9 тестов в `tests/tools/test_memory_tool.py` (всего 76 в файле, 76/76 passed): + +**Persistence:** +- `test_thread_scoped_memory_writes_separate_file` — global и thread пишутся в разные файлы +- `test_user_stays_global_with_thread_key` — USER.md всегда глобальный, не залезает в `threads/` +- `test_thread_and_global_are_independent_on_load` — загрузка thread не видит global entries и vice versa +- `test_thread_key_none_falls_back_to_global` — backward compat: без thread_key пишет в `memories/MEMORY.md` + +**Snapshot:** +- `test_snapshot_reflects_thread_scoped_path` — `format_for_system_prompt` берёт данные из thread-файла +- `test_snapshot_from_thread_and_global_are_independent` — thread snapshot изолирован от global + +**Drift guard:** +- `test_drift_guard_with_thread_key` — `_detect_external_drift` работает на thread-scoped MEMORY.md + +**Sanitization:** +- `test_load_time_sanitization_with_thread_key` — poisoned entry в thread блокируется на уровне snapshot + +**Pitfalls:** +- `pytest-timeout` плагин не установлен, но `pyproject.toml` содержит `addopts = "--timeout=30"`. Запуск падает с `unrecognized arguments`. Используй `-o "addopts="` для override. +- Drift guard на thread: нужен блок > `memory_char_limit` (дефолт 2200), иначе `_detect_external_drift` не находит entry-size overflow. В тесте `"x" * 2300`. + +## Тесты + +- **76/76 passed** (из них 9 новых для thread_key, добавлены в `4ebad4f69`) +- **9 новых тестов:** + - `test_thread_scoped_memory_writes_separate_file` — разные файлы для global/thread + - `test_user_stays_global_with_thread_key` — USER.md не уходит в threads/ + - `test_thread_and_global_are_independent_on_load` — не пересекаются при чтении + - `test_thread_key_none_falls_back_to_global` — backward compat + - `test_snapshot_reflects_thread_scoped_path` — форматирует snapshot из thread файла + - `test_snapshot_from_thread_and_global_are_independent` — не подхватывает global entry + - `test_drift_guard_with_thread_key` — детекция внешней модификации на thread файле + - `test_load_time_sanitization_with_thread_key` — poisoned entry блокируется в thread snapshot + - `test_already_blocked_entry_passes_through` — no double-wrap (расширен) +- Запуск: `cd ~/.hermes/hermes-agent && source venv/bin/activate && python -m pytest tests/tools/test_memory_tool.py -v -o "addopts="` + +## Неизменённое + +- `run_conversation` / `conversation_loop.py` — не трогали +- `background_review.py` — наследует `_memory_store` от родителя, thread_key приходит автоматически +- `tools/memory_tool.py` schema/MEMORY_SCHEMA — не меняли, кастомная инструкция в system prompt +- External memory providers (honcho/mem0) — не трогали diff --git a/personal/projects/balda/balda-valera.md b/personal/projects/balda/balda-valera.md index 643a14ce..eaf0803e 100644 --- a/personal/projects/balda/balda-valera.md +++ b/personal/projects/balda/balda-valera.md @@ -1,197 +1,185 @@ -# Балда / Валера — эксплуатация +# Балда / Валера — эксплуатация & MCP debug ## Расположение -- Repo: `~/Developer/balda/` (ветка `main`) -- Code: upstream `normahq/balda` + `feat/zulip-transport` влит, `normahq/norma` v0.0.10 -- **Рабочий compose**: `~/Docker/balda-agent/docker-compose.yaml` → контейнер `balda-agent-valera-1` -- Config (persistent): `~/Docker/balda-agent/.config/balda/config.yaml` -- Env: `~/Docker/balda-agent/.env` -- Owner: `allowed_owners` в config.yaml (статически, `/start owner=` не нужен) +- **Валера** = `balda-agent-valera-1`, конфиг: `~/Docker/balda-agent/.config/balda/config.yaml` +- **Клавдий** = `claudio-agent-claudio-1`, конфиг: `~/Docker/claudio-agent/.config/balda/config.yaml` +- **Общий образ**: собирается из `~/Docker/claudio-agent/Dockerfile.claudio`, контекст `/Users/admin/Developer` +- **Norma-local** (форк с MCP tools): `~/Developer/norma-local/` + - `go.mod` replace: `github.com/normahq/norma => ./norma-local` (в `~/Developer/balda/go.mod`) + - Модифицирован `pkg/runtime/hostedagent/openai.go` + `pkg/runtime/agentfactory/agentfactory.go` +- **Dockerfile**: `~/Docker/claudio-agent/Dockerfile.claudio` + - Копирует `norma-local/` в `/src/norma-local/` + - `go mod edit -replace github.com/normahq/norma=./norma-local` перед `go mod download` -## Запуск / рестарт +## Архитектура +- **Валера** = DeepSeek (`provider: deepseek`, модель `deepseek-chat`) +- **Клавдий** = Claude через прокси (`provider: claude`, `claude-sonnet-4-6`) +- Валера использует **Balda runtime** — OpenAIModel из openai.go вызывается (hostedagent провайдер). +- `agentfactory.go` используется обоими — там лог версии на старте. +## Версионный лог (norma-tools) + +При старте Валеры в логах: +``` +norma-tools version=v0.0.10 build=whale-YYYYMMDD-N +``` + +Где: +- `version` — номер версии нормы (из `openAIVersion`) +- `build` — тег сборки (из `buildTag`) + +**Перед каждым билдом апать `buildTag`** в `openai.go`: +```go +const openAIVersion = "v0.0.10" +const buildTag = "whale-YYYYMMDD-N" // ← менять! +``` + +Файлы где апать: +- `~/Developer/norma-local/pkg/runtime/hostedagent/openai.go` — `buildTag` константа + +## Статус: DeepSeek MCP tools — РАБОТАЕТ + +### Что сделано (openai.go) +Добавлена полная поддержка OpenAI tool_calls: +- `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` — структуры +- `Tools []openAIToolDefinition` в `openAIChatRequest` +- `openAIToolsFromConfig()` — конвертация genai.Tool[] → OpenAI definitions +- `parseChatResponse()` — парсинг tool_calls из ответа DeepSeek (ID сохраняется) +- `contentToOpenAI()` — конвертация FunctionCall/FunctionResponse в историю +- `generate()` — TurnComplete=false при tool_calls (ADK делает второй раунд) + +### Исправленные проблемы + +#### 1. DeepSeek возвращает аргументы с двойной сериализацией ✓ 22.06.2026 +**Симптом:** `read_note`/`write_note` падали — Obsidian MCP возвращал `"Cannot read properties of undefined (reading 'replace')"`. + +**Корень:** DeepSeek возвращает `function.arguments` как JSON-строку (экранированную), а не как JSON-объект. + +**Фикс в `parseChatResponse`:** сперва пробуем распарсить Arguments как строку (`json.Unmarshal(&argsStr)`), потом эту строку как объект (`json.Unmarshal([]byte(argsStr), &args)`). + +#### 2. Obsidian MCP: get_vault_stats работает, read/write/delete нет ✓ 22.06.2026 +**Корень:** двойная сериализация аргументов (см. проблему 1). После её фикса всё работает. + +#### 3. Key `output` не проверялся в contentToOpenAI ✓ 22.06.2026 +Obsidian MCP возвращает response как `{"output":"..."}` — добавлена проверка на ключ `output`. + +### Текущий билд +- buildTag: `whale-20260622-7` +- Все фиксы закоммичены в `master` (норма-локаль) +- OPENAI-DEBUG стэш дропнут — логи в рабочей копии (не коммитятся) +- Replace на норму работает через `go.mod` + Dockerfile +- `norma-tools` лог подтверждён: `time=2026-06-22T13:00:37.170Z level=INFO msg=norma-tools version=v0.0.10 build=whale-20260622-7` + +## Промежуточные статусы — ПОЧИНЕНО (23.06.2026) +**Статус:** работает. +- `RunSessionTurnPayload` (zulip_handler.go) — публикует промежуточный текст и ⚙️ function call статусы для `!ev.TurnComplete` ивентов через `sendPlain`. +- `balda.go` (Telegram) — те же промежуточные публикации. +- `handleAutoClaimMention` / `handleMessage` / `enqueueTurn` — передают `messageID` для точного Zulip threading. +- Коммит: `05159d1` (main), запушен. + +## Стэши debug-логов (23.06.2026) + +Дебаг логи не коммитятся, хранятся в стэшах. + +**balda** (`~/Developer/balda`): +- `stash@{0}`: `debug: whale-20260623 — TASK-ACTOR logging in swarm_task_actor, event count & response_len debug in zulip_handler` + - `internal/apps/balda/actors/swarm_task_actor.go` — TASK-ACTOR: dispatching/dispatch OK/FAILED + - `internal/apps/balda/handlers/zulip_handler.go` — eventCount, response_len, running session turn log +- `stash@{1}`: WIP on feat/zulip-events-polling + +**Восстановление balda стэша:** ```bash -cd ~/Docker/balda-agent -docker-compose restart # без пересборки, только конфиг/env не менялись -docker-compose up -d # пересоздать контейнер, перечитать .env - -# Rebuild после изменений в коде: -cd ~/Docker/claudio-agent && docker-compose build -docker tag claudio-agent-claudio:latest balda-agent-valera:latest -cd ~/Docker/claudio-agent && docker-compose up -d -cd ~/Docker/balda-agent && docker-compose up -d +cd ~/Developer/balda && git stash pop stash@{0} ``` -> Валера и Клавдий — **один образ** (`balda-agent-valera`), собирается из одного Dockerfile (`Dockerfile.claudio`). Разница только в конфиге (`config.yaml`) и `.env`, которые монтируются volumes. Сборка идёт из `~/Docker/claudio-agent/` — там есть build секция. +**norma-local** (`~/Developer/norma-local`): +- OPENAI-DEBUG стэш дропнут — 9x fmt.Fprintf(os.Stderr, "OPENAI-DEBUG:...") в рабочей копии openai.go -## Архитектура получения сообщений -Валера — **outgoing webhook bot** (bot_type=3). Zulip отправляет webhook только для @mention и DM. +## Бранчи (23.06.2026) -- `events_polling.enabled: false` в config.yaml -- Сообщения приходят только через **outgoing webhook** (webhook_token в .env) -- После @mention создаётся сессия; последующие сообщения без @mention обрабатываются +**norma-local:** +- `feat/hostedagent-mcp-tools` — коммит `56a6e1e` (DeepSeek tool_calls фикс), чистая бранча от `origin/master` +- `master` — коммит `56a6e1e` (тот же, DeepSeek фиксы) -**После перезапуска**: написать `@Валера <текст>` чтобы создать сессию. +**balda:** +- `main` — `05159d1` (intermediate status фикс запушен) +- `backup/our-main-before-upstream` — старый main +- `feat/zulip-transport-intermediate` — от коммита `bc5fd03` -## MCP Obsidian — конфиг -Balda через SSE подключается к obsidian-mcp. **URL обязательно с `/sse`**: +## Сборка -```yaml -runtime: - mcp_servers: - obsidian: - type: sse - url: http://obsidian-mcp:3101/sse # <-- без /sse → 404 Not Found - name: obsidian -``` - -## Контекст (история сообщений) -**Работает** (на upstream `normahq/balda` с поддержкой hosted LLM agent sessions). ADK inject'ит историю корректно для OpenAI-совместимых провайдеров. - -Раньше не работало — исправлено в upstream `32e33a8 fix: support hosted MCP toolsets` / `d774c78 fix: use hosted llmagent sessions`. - -## Проблема: MCP не работают у Валеры (provider: type=openai) - -### Коренная причина -hostedagent (`pkg/runtime/hostedagent/`) не поддерживает MCP инструменты. -`openAIConstructor` в `agentfactory.go` получает `resolvedMCP map[string]agentconfig.MCPServerConfig`, -но не передаёт их в `hostedagent.Config` — у Config нет поля для MCP. -`openAIConstructor` вызывает `newHostedAgent(hostedagent.Config{...})` без MCPServers. - -Фикс: в `hostedagent.Config` добавить `MCPServers map[string]agentconfig.MCPServerConfig`, -а в `openAIConstructor` передавать `toRuntimeMCPServers(resolvedMCP)`. - -Важно: hostedagent использует OpenAI-compatible API (не ACP). MCP инструменты нужно -интегрировать через OpenAI tool_calls — модель шлёт tool_call, код выполняет MCP вызов. - -## Проблема: MCP не работают у Валеры — попытка фикса - -### Что было сделано (2026-06-19) - -#### 1. go.mod -Добавлен `replace github.com/normahq/norma => ../norma-local` — для локальной разработки. - -#### 2. hostedagent — добавлена поддержка MCP -- `pkg/runtime/hostedagent/agent.go`: - - В Config добавлено `MCPServers map[string]acpagent.MCPServerConfig` - - Сохранено в `runtimeAgent` - - В `run()`: после создания модели вызывается `model.SetTools(openAIToolsFromMCPServers(r.MCPServers))` - -- `pkg/runtime/hostedagent/openai.go`: - - В `openAIChatRequest` добавлено `Tools []openAIToolDefinition` - - В `OpenAIModel` добавлены `tools []openAIToolDefinition + `SetTools()` - - Добавлен `tool_calls` в парсинг ответа (`tool_calls` → content string) - -#### 3. agentfactory — передача MCP -- В `openAIConstructor` добавлено `MCPServers: toRuntimeMCPServers(resolvedMCP)` в вызов `newHostedAgent()` - -#### 4. Dockerfile -В `Dockerfile.claudio` добавлен `COPY norma-local/ ./norma-local/` — чтобы `replace` работал в контейнере. - -#### 5. Сборка -Образ собран и запущен как `balda-agent-valera:latest`. - -#### 6. Диагностика -- `mcpServerIDs` приходят корректно: `[balda obsidian fast-rlm]` -- Добавлен stderr-лог в `agentfactory.go` в `Build()`: - ```go - fmt.Fprintf(os.Stderr, "BALDA-DEBUG: mcpServerIDs=%v resolvedMCP=%v\n", mcpServerIDs, resolvedMCP) - ``` -- Для просмотра логов нужен `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG` - -### Почему не заработало -Точная причина не установлена — нужно увидеть `resolvedMCP` в логах контейнера. Возможные варианты: -- `resolveMCPServers` возвращает пустой map -- `toRuntimeMCPServers` неправильно конвертирует -- `SetTools` не влияет на уже созданные сообщения в сессии - -### Что осталось -1. Прочитать stderr из контейнера: `docker logs balda-agent-valera-1 2>&1 | grep BALDA-DEBUG` -2. Если `resolvedMCP` пустой — исправлять цепочку resolveMCPServers -3. Если не пустой — тестировать response с tool_calls в DeepSeek ответе - -## Диагностика молчания - -### 1. MCP ошибка: `failed to list MCP tools: failed to connect: Not Found` -**Причина**: в config.yaml url без `/sse` на конце. -**Фикс**: `url: http://obsidian-mcp:3101/sse` → `docker-compose restart`. - -### 2. В логах `command running → command handled` за секунду, без `received provider event` -**Причина**: стухший embedded NATS. Swarm внутри процесса не доставляет команду до session/task actor'ов. -**Фикс**: `docker-compose restart` (если не помогло → `up -d`). - -### 3. Удалён / пустой state.db -После удаления balda не может зарегистрировать swarm акторы. -**Фикс**: `rm state.db` и `up -d` (balda создаст заново). - -### 4. Нет owner -`handleMessage` выходит при `getOwnerID() == 0`. -**Фикс**: прописать `allowed_owners` в config.yaml (не нужен `/start owner=`). - -### 5. stream_only_with_session -Сообщение в теме без сессии молча дропается. -**Фикс**: написать `@Валера <текст>` чтобы создать сессию. - -### 6. Проверить webhook со стороны +### Полный цикл сборки ```bash -curl -X POST http://localhost:8091/zulip/webhook \ - -H "Content-Type: application/json" \ - -d '{"type":"test"}' +# 1. Инкремент buildTag в openai.go (строка buildTag) +# ~/Developer/norma-local/pkg/runtime/hostedagent/openai.go +# buildTag = "whale-YYYYMMDD-N" + +# 2. Применить OPENAI-DEBUG стэш (если нужно дебажить) +cd ~/Developer/norma-local && git stash pop stash@{0} + +# 3. Сборка +docker build --no-cache \ + -f ~/Docker/claudio-agent/Dockerfile.claudio \ + -t balda-agent-valera:latest \ + /Users/admin/Developer + +# 4. Деплой +cd ~/Docker/balda-agent && docker-compose up -d --force-recreate + +# 5. Проверка версии в логах +docker logs balda-agent-valera-1 2>&1 | grep "norma-tools" ``` -## Ключевые грабли +### Особенности сборки +- Dockerfile копирует `norma-local/` в `/src/norma-local/` +- `go.mod` replace: `github.com/normahq/norma => ./norma-local` +- `go mod edit -replace` применяется **до** `go mod download` +- `--no-cache` обязателен при изменении norma-local +- Образ надо таргетировать под нужное имя: `balda-agent-valera` для Валеры, `claudio-agent` для Клавдия +- После сборки `docker-compose up -d --force-recreate` чтобы подхватить новый образ + +## Диагностика молчания / проблем + +### NORMA-TOOLS не появляется в логах +Причина: агент использует не hostedagent провайдер. openai.go задействован только при provider=deepseek через ADK. + +### MCP ошибка: `failed to list MCP tools: failed to connect: Not Found` +Фикс: в config.yaml url должен заканчиваться на `/sse`: `url: http://obsidian-mcp:3101/sse` + +### В логах `command running → command handled` за секунду, без `received provider event` +Причина: стухший embedded NATS. Фикс: `docker-compose restart` или `up -d`. + +### Удалён / пустой state.db +Фикс: `rm state.db` и `up -d` (balda создаст заново). + +### Нет owner +Фикс: прописать `allowed_owners` в config.yaml. + +### stream_only_with_session +Фикс: написать `@Валера <текст>` чтобы создать сессию. + +## Известные грабли ### OPENAI_BASE_URL — обязательная env var -Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env `OPENAI_BASE_URL`. -Без неё запросы уходят на `api.openai.com` (401). - -**В `.env` обязательно:** -``` -OPENAI_BASE_URL=https://api.deepseek.com/v1 -``` +Norma не читает `base_url` из config.yaml для `provider: deepseek`. Читает env. ### DOCKER_OPTS — пустая строка убивает старт -Должен быть валидный JSON: -``` -DOCKER_OPTS={"max_tokens": 2048, "temperature": 0.7} -``` +Должен быть валидный JSON: `DOCKER_OPTS={"max_tokens": 2048}` -### composerestart vs compose up -d -- `restart` — не перечитывает `.env` -- `up -d` — пересоздаёт контейнер с обновлённым `.env` +### docker compose up -d не работает +На этом хосте `docker compose` (без дефиса) не принимает `-d`. Использовать `docker-compose up -d`. +### Docker кеш COPY norma-local/ +Слой копирования кешируется. `--no-cache` обязателен при изменении norma-local. ---- +### go.mod replace +`replace github.com/normahq/norma => ./norma-local` — путь относительно `/src` в контейнере, куда копируется `norma-local/`. -## Диагноз: MCP не работают — инструменты не передаются модели +### zulip-router owner-форвард падал с 400 (23.06.2026) +**Симптом:** Валера не отвечал на сообщения в закреплённых тредах без @mention. Router лог: `forward failed (owner) error="post ... status 400 body=bad request"`. Валера лог: `invalid zulip webhook payload error="unsupported message.type \"\""`. -**Статус (2026-06-21):** MCP резолвятся, но не вызываются. +**Корень:** В `zulip-router/config.go` структура `Message` не имела поля `Type`. Zulip Events API присылает `message.type` ("stream"/"private"), но роутер его не парсил — в вебхук приходил пустой `""`. Balda-agent валидирует message.type. -**Установлено через BALDA-DEBUG:** -``` -BALDA-DEBUG: mcpServerIDs=[balda obsidian fast-rlm] -resolvedMCP=map[balda:{http http://127.0.0.1:33545/mcp} - fast-rlm:{http http://fast-rlm-mcp:3333/mcp} - obsidian:{sse http://obsidian-mcp:3101/sse}] -``` -MCP серверы зарезолвлены, тулсеты переданы в `hostedagent.Config`. - -**Почему не работают:** -`OpenAIModel.generate()` → `buildChatRequest()` не читает `req.Config.Tools` (поле `genai.GenerateContentConfig`). ADK кладёт инструменты туда, но `buildChatRequest` сериализует только `model`, `messages`, `temperature`, `top_p`, `max_tokens`, `stop`. DeepSeek получает запрос без `tools` → отвечает текстом. - -**Логи подтверждают:** `function_call_part_count=0` в ответе DeepSeek. - -**Фикс:** добавить передачу tools в OpenAI запрос и парсинг tool_calls из ответа. -**Сделано (2026-06-21):** в `openai.go` добавлены: -- `openAIToolDefinition`, `openAIFunction`, `openAIToolCall` — структуры для API -- Поле `Tools` в `openAIChatRequest` -- Функция `openAIToolsFromConfig()` — конвертирует `genai.Tool[]` → OpenAI tool definitions -- Функция `contentToOpenAI()` — обрабатывает FunctionCall/FunctionResponse из ADK -- Парсинг `tool_calls` в `parseChatResponse()` -- **Тесты:** 17 тестов на new функциональность + 3 существующих = все проходят - -1. Добавить поле `Tools` в `openAIChatRequest` + `openAIToolDefinition` -2. В `buildChatRequest` читать `req.Config.Tools` → конвертировать в OpenAI tool definitions -3. В `parseChatResponse` парсить `tool_calls` из ответа (ADK сам выполнит MCP) - -**Важно:** upstream `norma` v0.0.10 (`bf2b25d`) уже корректно передаёт MCP тулсеты через `agentfactory.hostedToolsets()`. Проблема только в том, что `OpenAIModel` — кастомная реализация, не умеющая прокидывать tools. Если бы использовался Gemini/ACP provider — MCP бы работали. +**Фикс:** Добавлен `Type string \`json:"type,omitempty"\`` в `Message struct`. Коммит `803ae68` (локальный, без remote). docker-compose up -d --build. +Фикс: messageID int в сигнатуру handleAutoClaimMention, передаётся payload.Message.ID из processMessage. +Файл: `~/Developer/balda/internal/apps/balda/handlers/zulip_handler.go` (в коммите `05159d1`) \ No newline at end of file diff --git a/personal/projects/balda/setup.md b/personal/projects/balda/setup.md index 4d847af3..d7773cdf 100644 --- a/personal/projects/balda/setup.md +++ b/personal/projects/balda/setup.md @@ -1,10 +1,12 @@ # Balda — Setup & Config -_Последнее обновление: 2026-06-16_ +_Последнее обновление: 2026-06-24_ ## Хост -Mac (Eagle), Docker Desktop (arm64 native). Контейнеры собираются из исходников в `~/Developer/balda/`. +Mac (Eagle), **Colima** (arm64, 8 CPU, 24 GB RAM, 100 GB sparse disk). Контейнеры собираются из исходников в `~/Developer/balda/`. + +**Важно:** `buildx_buildkit_arm64builder0_state` volume — основная причина зависаний Zulip при сборке. См. [[tech/docker-mac-disk-issues]]. ## Компоненты diff --git a/personal/projects/budget-app/bank-statement-import.md b/personal/projects/budget-app/bank-statement-import.md new file mode 100644 index 00000000..27bb37a2 --- /dev/null +++ b/personal/projects/budget-app/bank-statement-import.md @@ -0,0 +1,355 @@ +# Импорт банковских выписок за последний год + +**Создано:** 2026-06-23 +**Цель:** Получить все транзакции за последний год (середина 2025 — июнь 2026) в Budget App, автоматизируя импорт банковских выписок как можно полнее. + +## Проблема + +В `Budget.xlsx` данные заканчиваются в **середине 2025** (май-июнь 2025, в зависимости от счёта). Последние ~12 месяцев транзакций не внесены в Excel. Вручную вспомнить каждую трату за год — нереалистично. + +## Существующий инструмент: budget-bank-statement-converter + +**Путь:** `~/Developer/budget-bank-statement-converter/` +**Язык:** Swift (macOS command-line tool) +**Формат вывода:** CSV с колонками `[дата, сумма, дебет, кредит, категория, комментарий, курс]` — совпадает с форматом Excel. + +### Поддерживаемые банки + +| Банк | Формат входа | Конфиг | Статус | +|------|-------------|--------|--------| +| Demir | CSV (из PDF → Adobe Extract → CSV) | `demir-config.json` | ✅ Работает | +| Сбер | CSV (выгрузка из СберБизнес) | `sber-config.json` | ✅ Работает | +| Тинькофф | CSV (выгрузка из Тинькофф) | `tinkoff-config.json` | ✅ Работает | +| ВТБ | CSV (из PDF → Adobe Extract → CSV) | `vtb-config.json` | ✅ Работает | +| Альфа | — | — | ❌ `fatalError("Alfa not implemented")` | + +### Как работает + +1. **PDF → CSV**: использует Adobe PDF Extract API (`pdfservices-api-credentials.json`) — загружает PDF, получает ZIP с CSV-таблицами. +2. **CSV → формат App**: разбирает CSV, маппит категории через regex-конфиг, нормализует double-entry (дебет/кредит/конверсии). +3. Если нужно — автоматически конкатенирует `fileoutpart0001.csv`… файлы. +4. Использует OpenAI GPT-3.5-turbo для AI-категоризации (закомментировано, `aiMaxTokens = 80`). + +### Что нужно для использования + +- Xcode (для сборки Swift-проекта) +- `OPENAI_API_KEY` в env (не обязательно, выключено) +- `pdfservices-api-credentials.json` для Adobe Extract +- JSON config для каждого банка: `Сбер config`, `Tinkoff config`, `Demir config`, `VTB config` + +## План импорта + +### Шаг 1: Получить выписки из банков + +**Что нужно выгрузить за июнь 2025 — июнь 2026:** + +| Счёт | Банк | Как получить выписку | +|------|------|---------------------| +| Нал RUB | Наличные | Ручной ввод (см. ниже про наличные) | +| Нал KGS | Наличные | Ручной ввод | +| Нал USD | Наличные | Ручной ввод | +| Нал KZT | Наличные | Ручной ввод | +| Demir ИП | Demir | CSV (интернет-банк/моб. приложение) | +| Demir ИП USD | Demir | CSV (интернет-банк/моб. приложение) | +| Demir KGS | Demir | CSV (интернет-банк/моб. приложение) | +| Demir USD | Demir | CSV (интернет-банк/моб. приложение) | +| Тинькофф Black | Тинькофф | CSV (выгрузка из Тинькофф) | +| Тинькофф Кредитка | Тинькофф | CSV (выгрузка из Тинькофф) | +| Сбер | Сбер | CSV (СберБизнес / PDF) | +| Сбер Кредитка | Сбер | CSV (СберБизнес / PDF) | +| Альфа | Альфа | CSV — но конвертер Альфу не поддерживает | +| Альфа Кредитка | Альфа | — | +| ВТБ | ВТБ | PDF → Adobe Extract → CSV | +| ВТБ Кредитка | ВТБ | PDF → Adobe Extract → CSV | + +### Шаг 2: Конвертировать выписки в CSV формата App + +Запуск для каждого банка: +```bash +./budget-bank-statement-converter --bank --account +``` + +Выход: `_processed.csv` с колонками `дата, сумма, дебет, кредит, категория, комментарий, курс`. + +### Шаг 3: Написать Python импортёр CSV → Budget App DB + +Существующий `xlsx_import.py` читает из Excel. Нужен новый: `csv_bank_import.py`, который: + +- Читает CSV в формате App (колонки из `csvHeaders` в `Common.swift`) +- Привязывает `дебет`/`кредит` к существующим счетам в БД (по имени) +- Маппит категории из CSV на существующие категории в БД (по имени подкатегории) +- Игнорирует дубликаты (hash по `date + amount + source + dest + comment`) +- Поддерживает несколько CSV-файлов за раз (много выписок) +- Выводит отчёт: сколько добавлено, сколько пропущено (дубликаты), какие категории не найдены + +### Шаг 4: Импортировать в Budget App + +```bash +cd ~/Developer/budget-app +uv run python src/budget/importers/csv_bank_import.py ... +``` + +### Шаг 5: Дописать недостающее в Swift-конвертере + +- **AlfaToCSV**: реализовать парсер для Альфа-банка (CSV выгрузка из моб. банка/СберБизнес) +- **AI-категоризация**: раскомментировать и обновить (GPT-3.5 → DeepSeek/local LLM?) + +## Наличные расходы — проблема и решение + +### Проблема +Наличные траты не трекались последний год. У нас есть конечный остаток налички на руках сейчас, но нет истории по категориям. + +### Подходы + +#### A. Снять остаток наличных сейчас → счёт в БД (простой) +- Посчитать физическую наличку сейчас → записать как `initial_balance` для `Нал RUB`, `Нал KGS`, `Нал USD`, `Нал KZT`. +- Все траты наличными за год никогда не будут зафиксированы. +- **Минус:** дыра в данных большого объёма (вероятно значительная часть расходов). + +#### B. Экстраполяция по историческим трендам (средний) +- Взять помесячные тренды наличных трат по категориям за 2023–первую половину 2025. +- Экстраполировать на июнь 2025 — июнь 2026 с учётом сезонности. +- Создать транзакции-плейсхолдеры с пометкой `import_id = 'cash_estimate'`. +- **Минус:** неточность, может не отражать реальные изменения. + +#### C. Ретроспектива через месяц-два + экстраполяция (предпочтительный) +- **Сейчас:** начать трекать наличные расходы (вручную или через мобильный интерфейс Budget App). +- **Через 1–2 месяца:** по собранным данным наличных трат вычислить реальные помесячные паттерны. +- Экстраполировать на пропущенный год с этими паттернами. +- **Плюс:** база для экстраполяции будет основана на реальных данных, а не на исторических. + +#### D. None of the above — принять дыру +- Сделать только безналичный импорт. Наличные начинаем трекать с сегодня. +- В аналитике отмечать периоды как "без наличных". +- **Плюс:** не надо ничего выдумывать. + +### Рекомендация: C+D combined +1. Трекать наличку вручную через UI Budget App начиная с сегодня. +2. Через 2 месяца посчитать реальные тренды и решить, стоит ли экстраполировать на прошлый год. +3. Если нет — просто принять дыру и жить с хорошей аналитикой начиная с 2026-06. + +## Что уже реализовано в Budget App для импорта + +- ✅ `xlsx_import.py` — полный импорт из Budget.xlsx (34k строк) +- ✅ Все счета, категории, курсы, транзакции — в БД +- ✅ Идемпотентный UPSERT для счетов и курсов +- ✅ Транзакции добавляются обычным insert (без import_hash после фикса) + +## Реализованный скрипт: bank_scraper + +**Путь:** `~/Developer/budget-app/scripts/bank_scraper/` + +Структура: +``` +scripts/bank_scraper/ +├── __init__.py +├── .gitignore # config.yaml + data/imports/ не коммитятся +├── config.example.yaml # шаблон для копирования в config.yaml +├── base_driver.py # base class BankDriver + load_config() +├── orchestrator.py # entry point (н.п.) +└── drivers/ + └── demir.py # Demir IB драйвер (н.п.) +``` + +**Статус:** ✅ base + Demir driver написаны, Playwright установлен. **НО — Demir требует QR-логин через мобильное приложение**, не логин/пароль на сайте. + +## Реальность Demir IB + +Сайт `93.171.215.109` (и `apps.demirbank.kg/ib/`) — **Flutter web SPA** с QR-аутентификацией. Нет формы логина с паролем — нужно сканировать QR мобильным приложением Demir. + +**Варианты решения:** + +### A. Продолжить с Playwright + session persistence +- Один раз залогиниться руками (QR → моб. приложение) +- Сохранить session cookies/storage в persistent context +- Дальше переиспользовать сессию для выгрузок (пока не протухнет) +- **Плюс:** минимум кода +- **Минус:** сессия рано или поздно протухнет, нужен ручной ре-логин + +### B. Appium / ADB — эмуляция мобильного приложения +- Демонстратор Android/iOS эмулятора с мобильным приложением Demir +- Appium для UI automation внутри приложения +- **Плюс:** полный контроль +- **Минус:** сложно, накладно + +### C. Заменить Demir на первый банк с логином/паролем +- Тинькофф имеет API для разработчиков (OAuth) +- Сбер — есть API SberBusinessAPI (хотя для юрлиц) +- Можно начать с Тинькофф: Tinkoff API → выписка без браузера +- **Плюс:** самый простой tech-wise +- **Минус:** Demir пока под вопросом + +### D. Парсить CSV выписки, которые уже есть в mobile/email +- Возможно Demir присылает выписки на email +- Или можно скачать через мобильное приложение → экспорт → AirDrop/email себе +- Это полу-ручной подход (но быстрее чем QR scraping) + +## Решение + +**Рекомендация: A + D** +1. Самый ценный банк — **Тинькофф** (есть API) — начинаем с него +2. Demir — разово выгрузить через мобильное приложение (Export CSV/email) +3. Если сессия Demir долго живёт — Playwright persistent context отработает + +### Новый порядок разработки + +1. ✅ Demir driver (написан, но упирается в QR) +2. **Tinkoff API driver** — следующий приоритет (без браузера, REST API) +3. **Сбер / ВТБ / Альфа** — Playwright или Tinkoff-style API +4. **Parse Demir CSV** — Python-версия DemirToCSV для уже скачанных файлов + +## Файл вывода Swift-конвертера + +``` +csvHeaders = ["дата", "сумма", "дебет", "кредит", "категория", "комментарий", "курс"] +``` + +- `дата` — `dd.MM.yyyy HH:mm` (формат EUR) +- `сумма` — строка с суммой (±знак) +- `дебет` — имя счёта-источника (пусто = доход извне) +- `кредит` — имя счёта-получателя (пусто = расход вовне) +- `категория` — имя подкатегории +- `комментарий` — очищенный текст +- `курс` — кросс-курс при внутреннем переводе между валютами + +## Автоматизация выгрузки выписок из банков + +### 1. Browser automation libraries (CV-driven) + +| Библиотека | Язык | Браузеры | CV | 2FA/SMS | +|-----------|------|----------|----|---------| +| **Playwright** (MS) | Python, JS, Java, .NET ⭐ | Chromium, Firefox, WebKit | Есть (locator screenshots) | `page.wait_for_selector` на поле ввода кода | +| **Puppeteer** (Google) | JS (Python через pyppeteer) | Chromium | Есть | — | +| **Selenium** | Python, Java, JS и др. | Все major | Через сторонние утилиты | — | + +**Рекомендация: Playwright Python** — де-факто стандарт в 2025, cross-browser, async, видит элементы даже в SPA, встроенные ожидания. Подходит и для РФ-банков (Сбер, Тинькофф, Альфа-клик — все на SPA). + +### 2. Готовые решения на GitHub + +**AploBankParsers** ([github.com/Zaurrex1/AploBankParsers](https://github.com/Zaurrex1/AploBankParsers)): +- Парсер выписок **СберБизнес** (production-ready) — читает xlsx/сsv из уже выгруженного файла +- Заглушки для Альфа, ВТБ, Тинькофф +- Это парсер **уже скачанных файлов**, не скрапер + +**bank_scrapers** ([github.com/eebette/bank_scrapers](https://github.com/eebette/bank_scrapers)): +- Playwright-based для scraping bank websites +- Generic, не специфичен под РФ-банки + +**Sber API** — официальный REST API Сбера: +- `developers.sber.ru/docs/ru/sber-api/specifications/statement/transactions` +- Получение выписки по счёту за 5 лет +- **Требует** корпоративного доступа (SberBusinessAPI / ДБО), не подойдёт для личного СберБанк + +**Готового решения "под ключ" для РФ-банков** (Playwright → bank login → 2FA → CSV выписка) **нет** в открытом доступе. Каждый банк — свой уникальный UI и flow. Придётся писать самим. + +### 3. Архитектура скрипта + +``` +┌─────────────────────────────────┐ +│ Telegram Bot (Hermes/кит) │ ← запрашивает SMS-код +├─────────────────────────────────┤ +│ Orchestrator (Python) │ ← запускает по крону / кнопке +│ ┌─────────────────────────┐ │ +│ │ Playwright browser │ │ ← drives bank login page +│ │ - headless=false │ │ (visible для отладки) +│ │ - persistent context │ │ (сессия не слетает) +│ └─────────────────────────┘ │ +│ ┌─────────────────────────┐ │ +│ │ Bank drivers: │ │ +│ │ - tinkoff.py │ │ +│ │ - sber.py │ │ +│ │ - alfa.py │ │ +│ │ - demir.py │ │ +│ │ - vtb.py │ │ +│ └─────────────────────────┘ │ +│ ┌─────────────────────────┐ │ +│ │ Output: CSV в формате │ │ +│ │ budget-bank-statement- │ │ +│ │ converter │ │ +│ └─────────────────────────┘ │ +└─────────────────────────────────┘ +``` + +### 4. Flow для каждого банка + +``` +1. Запустить headless Playwright (или visible=False для отладки) +2. Открыть страницу логина банка +3. Ввести credentials (из конфига, НЕ скрипта) +4. Если запрошен SMS-код: + → отправить в Telegram: "Код из смс для {bank}:" + → ждать ответа (polling/async) + → ввести полученный код +5. Дождаться загрузки дашборда +6. Перейти на страницу выписок/истории +7. Указать период: 2025-06-01 — 2026-06-23 +8. Скачать CSV/Excel +9. Сохранить в ~/Developer/budget-app/data/imports/{bank}/{date}.csv +10. Конвертировать через budget-bank-statement-converter (или Python-версию) +11. Импортировать в БД +12. Закрыть браузер +``` + +### 5. Обработка SMS-кодов (Telegram) + +Скрипт не должен хранить сессию банка, каждый запуск — новая авторизация. + +**Варианты:** +1. **Telegram Bot (inline keyboard)**: скрипт ждёт сообщение, когда нужен код — присылает кнопку "Отправить код для {bank}", пользователь вводит → скрипт вставляет +2. **Hermes-агент**: крон-джоб спрашивает в Telegram нужный код, ждёт ответа через webhook +3. **Простой stdin**: скрипт пишет "Введите код для Тинькофф:" и ждёт ввод (если запуск из терминала) + +**Рекомендация: вариант 1** — TG bot минимальная зависимость, полный контроль. + +Для реализации: существующий Hermes/Zulip может служить relay. Или простой скрипт на Python + python-telegram-bot с `await incoming_message`. + +### 6. Чувствительность данных — ограничения + +Скрипт будет: +- Знать **логины/пароли** банков (хранятся в локальном конфиге, НЕ в коде) +- Открывать **браузер на машине Алекса** (никаких VPN/прокси) +- Передавать только SMS-коды через TG — пароли не передаются +- Работать **локально**, без LLM/агентов в browser automation + +Код пишем так, чтобы ни одна строка credentials не была в скрипте: +```python +# config.yaml (chmod 600) +banks: + tinkoff: + login: "7999..." + password: "..." + phone: "7999..." + sber: + login: "..." + password: "..." +``` + +### 7. Альтернатива: API банков (без browser) + +| Банк | REST API для личных счетов | Комментарий | +|------|---------------------------|-------------| +| Тинькофф | Есть (Tinkoff API для разработчиков) | Требует регистрации приложения, OAuth | +| Сбер | Sber API для юрлиц, нет для личных | Не подходит | +| Альфа | Альфа-Бизнес API (юрлица) | Не подходит | +| Demir | Нет публичного API | — | +| ВТБ | Нет публичного API | — | + +Тинькофф — единственный из списка, у кого есть адекватный API для физлиц (Tinkoff API / Tinkoff Invest API). Можно получить выписку через API, без browser. Остальные — только SPA scraping. + +**Код:** 10 swift-файлов, ~2 400 строк. + +**Что хорошо:** +- Хорошая архитектура: каждый банк = отдельный struct с чётким интерфейсом +- Конфиги вынесены из кода (JSON) +- Regex-маппинг категорий гибкий +- Умеет объединять multi-part CSV и извлекать из PDF через Adobe API +- Формат вывода совпадает со структурой Excel/Budget App + +**Чего не хватает:** +- Парсер Альфа-банка (только заглушка) +- AI-категоризация закомментирована (GPT-3.5, устарела) +- Нет интеграции с Budget App (только → CSV, не → БД) +- Нет обработки для Demir ИП USD / Demir USD / Demir KGS отдельно (DemirToCSV один конфиг на все) +- PDF-парсер привязан к Adobe PDF Extract API (платный сервис, credentials нужны) +- Нет обработки для Сбер Кредитка как отдельного счёта (SberToCSV один конфиг) +- Нет автоматического определения новых форматов CSV от банков diff --git a/personal/projects/budget-app/excel-balances.md b/personal/projects/budget-app/excel-balances.md new file mode 100644 index 00000000..5cefb38f --- /dev/null +++ b/personal/projects/budget-app/excel-balances.md @@ -0,0 +1,70 @@ +# Остатки счетов из Excel (Budget.xlsx) + +Файл: `~/Downloads/Budget.xlsx` +Лист: `транзакции` + +Балансы **совпадают** с API после фиксов (2026-06-23). + +| Счёт | Баланс | Валюта | Последняя операция | +|---|---|---|---| +| Нал RUB | 815 555.70 | RUB | 2025-05-16 (R6029, 60 000 deb) | +| Нал KGS | 1 794.00 | KGS | 2025-06-17 (R6042, deb) | +| Нал KZT | -22 820.00 | KZT | 2024-12-15 (R5505, deb) | +| Нал USD | 1.00 | USD | 2025-02-04 (R5976, deb) | +| Нал AED | 0.00 | AED | 2023-11-16 (R2335, deb) | +| Нал EUR | — | EUR | нет операций | +| Нал UZS | 18 000.35 | UZS | 2024-03-31 (R3337, deb) | +| Demir ИП | 192 308.54 | KGS | 2025-01-31 (R5943, deb) | +| Demir ИП USD | 284 842.96 | USD | 2026-05-29 (R6053, cred) | +| Demir KGS | 96 013.12 | KGS | 2025-01-31 (R5947, deb) | +| Demir USD | 3 394.29 | USD | 2025-01-31 (R5950, cred) | +| Тинькофф Black | 521 263.65 | RUB | 2025-01-31 (R5959, deb) | +| Тинькофф Кредитка | 7 170.17 | RUB | 2025-01-28 (R5901, deb) | +| Сбер | 266 905.98 | RUB | 2025-01-28 (R5898, deb) | +| Сбер Кредитка | 3 059.00 | RUB | 2025-01-31 (R5946, deb) | +| Альфа | 78 030.00 | RUB | 2025-01-28 (R5925, cred) | +| Альфа Кредитка | 0.50 | RUB | 2025-01-28 (R5924, deb) | +| ВТБ | 265 178.95 | RUB | 2025-01-27 (R5887, deb) | +| ВТБ Кредитка | 297 189.00 | RUB | 2025-01-31 (R5952, deb) | +| Райффайзен | — | RUB | нет операций | + +## Формула API + +`balance = incoming_transfer + incoming_income - outgoing + initial_balance` + +Где: +- `incoming_transfer` — `SUM(cross_rate * amount)` где source IS NOT NULL (cross_rate конвертирует валюту source в валюту dest) +- `incoming_income` — `SUM(amount)` где source IS NULL (amount уже в валюте счёта, cross_rate — только для отчёта) +- `outgoing` — `SUM(amount)` (всегда в валюте счёта-источника) + +## Фиксы (2026-06-23) + +### 1. incoming для income и transfer — разный расчёт + +Было: `incoming = SUM(COALESCE(cross_rate, 1.0) * amount)` — cross_rate применялся ко всем включая income (где amount уже в валюте счёта). Для KGS-счетов с income-пополнениями (например "Конвертация USD по курсу 87") incoming умножался на 87, давая баланс ×87. + +Стало: incoming разделён на две части: +- `incoming_transfer (source IS NOT NULL)`: cross_rate применяется (конвертирует валюту source→dest) +- `incoming_income (source IS NULL)`: просто amount (cross_rate — только для отчётной валюты) + +### 2. import_hash и on_conflict_do_nothing — удалены + +Было: дедупликация по `import_hash = SHA256(date|amount|source|dest|comment)`. В Excel есть 141 дублирующаяся строка с одинаковыми этими полями (реальные повторные списания). `on_conflict_do_nothing` молча пропускал их, но `tx_count` врал что импортировал. + +Стало: обычный `session.add(tx)`. Колонка `import_hash` и уникальный индекс дропнуты из таблицы. Функция `_make_import_hash` удалена. + +### 3. Accounts — идемпотентность + +Было: каждый запуск импорта создавал 20 новых аккаунтов (session.add). После нескольких запусков — 40+ аккаунтов с разными UUID, транзакции привязаны к разным наборам. + +Стало: UPSERT — ищет существующий account по `name + user_id`, переиспользует. + +### 4. UserSettings — UPSERT + +Было: `session.add(UserSettings(...))` — падало с UniqueViolation при повторном запуске. + +Стало: `pg_insert(...).on_conflict_do_update(...)`. + +### 5. Нюанс Excel col I/L + +Для некоторых строк Excel не кеширует вычисленные значения col I (остаток деб) — показывает None. Это не баг, а особенность data_only=True — если Excel не пересчитал формулы перед сохранением, кеш пуст. В таких случаях последний корректный баланс берётся из предыдущей строки минус amount. diff --git a/personal/projects/budget-app/financial-strategy-and-plan.md b/personal/projects/budget-app/financial-strategy-and-plan.md new file mode 100644 index 00000000..e3425a9d --- /dev/null +++ b/personal/projects/budget-app/financial-strategy-and-plan.md @@ -0,0 +1,264 @@ +--- +aliases: + - Freedom Strategy + - Budget app FIRE + - Budget app strategy +related: + - '[[personal/projects/budget-app/index]]' + - '[[personal/projects/budget-app/fire-investment-strategies-2026]]' +tags: + - personal + - budget-app + - finance + - FIRE + - freedom + - roadmap +title: Финансовая стратегия + FIRE-адаптация для Budget App +updated: '2026-06-23T00:00:00.000Z' +--- +# Финансовая стратегия + FIRE-адаптация для Budget App + +**Обновлено:** 2026-06-23 +**Основание:** анализ данных budget-app + личные вводные Alex + +--- + +## 1. Текущая позиция (June 2026) + +### 1.1 Балансы счетов + +| Счёт | Валюта | Баланс | Статус | +|------|--------|--------|--------| +| Нал USD | USD | $925,511 | **Накопления** | +| Demir ИП USD | USD | $284,843 | **Накопления** | +| Demir USD | USD | $3,394 | Остаток | +| Нал KGS | KGS | 1,794 | Остаток (текущие) | +| Demir ИП | KGS | -359,861 | На расход | +| Demir KGS | KGS | -3,542,775 | Ушёл в минус | +| RUB счета (Альфа, Сбер, ВТБ, Тинькофф, Нал) | RUB | ~-29.5M | Кредитки + овердрафты | +| Нал RUB | RUB | -12,709,478 | Долг | +| Альфа | RUB | -18,187,976 | Долг | + +**Итого накопления:** ~$1,210,000 USD ≈ **112.5M KGS** (по курсу 93) +**Реально свободные:** ~$100k (нал USD), остальное — предпринимательские счета + оборотка + +### 1.2 Расходы (из БД, 2024 — июнь 2025, 17 мес) + +| Показатель | Значение | +|------------|----------| +| Средние расходы/мес | ~1,075,000 KGS | +| Средний доход/мес | ~851,000 KGS | +| Норма сбережений (по БД) | -26% (данные неполные — часть трат не проведена) | + +Категоризация в БД сломана: все расходы свалены в "📉 Расходы" (11.4M), остальное — депозиты, кэшбэк, аренда (~378k). Данные после июня 2025 не вносились, последняя транзакция май 2026 — видимо разовый импорт. + +### 1.3 Внешние активы и доходы + +| Статья | Цифра | +|--------|-------| +| **Накопления (ликвид)** | ~$100k (Нал USD) | +| **Аренда 2 квартир** | 60-80k KGS/мес | +| **Образование старшей** | $8-10k/год = 65-77k KGS/мес | +| **Младшая (дистант)** | Запуск в этом году — точные цифры появятся | +| **Частный дом** | Текущее содержание | +| **Indie dev** | Проекта пока нет | + +--- + +## 2. Стратегия (Freedom, не FIRE) + +Классический FIRE (накопить 25x и сидеть без дела) **тебе не подходит** по нескольким причинам: + +1. **Валютный риск** — живёшь в KGS, доход в RUB/USD. KGS волатильна. FIRE-расчёт в KGS ненадёжен +2. **Образование детей** — крупный обязательный платёж на ~10 лет вперёд. Это не "сократить", это фиксированная статья +3. **Ты не хочешь "не работать"** — инди-дев показывает что хочешь заниматься проектами, а не сидеть на пляже +4. **Две квартиры** — актив, который уже почти покрывает образование старшей + +### 2.1 Твоя цель: "Freedom Gap" + +Не FIRE number, а **Freedom Gap** — разница между расходами и пассивным/полупассивным доходом, которую нужно закрыть капиталом. + +``` +Freedom Gap = (расходы/мес) − (аренда + дивиденды + проектный доход) +``` + +| Сценарий | Расходы/мес | Аренда | Проект | Gap/мес | Капитал для 4% | +|----------|------------|--------|--------|---------|----------------| +| **Сейчас** | ~900k KGS | 70k | 0 | **830k KGS** | **249M KGS** ($2.7M) | +| **Аренда → образование** | 900k | 70k→на образование | 0 | 830k | 249M KGS ($2.7M) | +| **+Проект $2k/мес** | 900k | 70k | 186k | **644k KGS** | **193M KGS** ($2.1M) | +| **+Проект $5k/мес** | 900k | 70k | 465k | **365k KGS** | **110M KGS** ($1.2M) | + +### 2.2 Реалистичные вехи + +| Веха | Условие | Цифра | +|------|---------|-------| +| **🟢 1ая: аренда = образование** | Аренда 80k покрывает старшую ~77k | ✅ **УЖЕ почти** | +| **🟢 2ая: проект = жизнь** | Проект $3k/мес покрывает ~280k KGS | Нужен работающий проект | +| **🟡 3ая: капитал + аренда = всё** | Накопить 110M KGS ($1.2M) при проекте $5k | 10-15 лет | +| **🔴 4ая: полная свобода** | Накопить 249M KGS ($2.7M) | Долгий горизонт | + +--- + +## 3. Инвестиционная стратегия для Alex + +### 3.1 Принципы + +1. **Core-Satellite** — 70% широкий рынок (VT / VWRA), 30% дивиденды + защита +2. **Мультивалютность** — портфель в USD (защита от девальвации KGS) +3. **Дивиденды как income stream** — снижают sequence-of-returns risk, частично покрывают расходы +4. **Буфер 12 мес** — в USD, не трогать +5. **Ребаланс раз в год** — не дёргаться + +### 3.2 Рекомендуемая аллокация + +| Класс | % | Инструмент | Назначение | +|-------|---|-----------|------------| +| **Global Equity ETF** | 60% | VWRA (VT) — IRSH / LSE | Рост капитала, мультивалютная диверсификация | +| **Dividend ETF** | 15% | VIG, SCHD, или HDV | Стабильный дивидендный поток | +| **Bonds (TIPS)** | 10% | TIP (iShares TIPS) | Защита от инфляции | +| **Cash USD** | 10% | HYSA / money market | Буфер, ~12 мес расходов | +| **Real Estate (REIT)** | 5% | VNQ / O | Доход от недвижимости без управления | + +**Почему не BND:** для тебя облигации в USD не дают премии, а TIPS защищают от инфляции которая в KGS выше номинальной. + +### 3.3 Ребаланс + +- **Раз в год** в декабре +- **Автоматический триггер:** любая позиция отклонилась >5% от цели +- **Новые деньги:** направляются в самый отстающий класс (автоматический buy-low) + +--- + +## 4. Что внедрить в budget-app (Phase 6 — Roadmap) + +### 4.1 Приоритеты (по ценности) + +| # | Фича | Зачем | Оценка сложности | Статус | +|---|------|-------|-----------------|--------| +| **P0** | **Savings Rate Dashboard** | Увидеть реальную норму сбережений. Сейчас -26% — надо понять куда уходят деньги | Medium | ❌ Не начато | +| **P0** | **Multi-Currency Net Worth** | Общий капитал в USD: наличка + счета + квартиры + пассивы | Low | ❌ Не начато | +| **P1** | **Investment Snapshots** | Раз в месяц вбить "сколько на Нал USD / IBKR / крипте" — увидеть динамику | Low | ❌ Не начато | +| **P1** | **Freedom Gap Dashboard** | Доход (аренда+дивиденды+проект) - расходы = gap | Medium | ❌ Не начато | +| **P1** | **Passive Income Tracker** | Сколько приносят аренда, дивиденды, депозиты в месяц | Low | ❌ Не начато | +| **P2** | **Projection Engine** | "Если докладываю X/мес и проекты дают Y/мес — через N лет свобода" | Medium+ | ❌ Не начато | +| **P2** | **What-if Simulator** | "Что если аренда упадёт / курс изменится / проект взлетит" | Medium+ | ❌ Не начато | +| **P3** | **Monte Carlo FI Calculator** | Классический 4% vs 3.5% vs variable, probability of success | High | ❌ Не начато | +| **P3** | **Withdrawal Strategy Planner** | Бакетная / guardrails / dividend | High | ❌ Не начато | + +### 4.2 Схема расширения БД + +```sql +-- Инвестиционные снапшоты (ручной ввод раз в месяц) +CREATE TABLE investment_snapshot ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES "user"(id), + account_id UUID REFERENCES account(id), -- NULL для внешних брокеров + name VARCHAR(128) NOT NULL, -- "Нал USD", "Interactive Brokers", "Крипта" + portfolio_value NUMERIC(16,2) NOT NULL, + currency_code VARCHAR(5) NOT NULL REFERENCES currency(code), + snapshot_date DATE NOT NULL, + asset_class VARCHAR(32), -- cash, bonds, stocks, real_estate, crypto, business + notes TEXT, + UNIQUE(user_id, name, snapshot_date) +); + +-- Цели свободы +CREATE TABLE freedom_goal ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES "user"(id), + name VARCHAR(128) NOT NULL, -- "Свобода", "Образование детей", "Ремонт дома" + target_amount NUMERIC(16,2) NOT NULL, + target_currency_code VARCHAR(5) NOT NULL REFERENCES currency(code), + current_amount NUMERIC(16,2) DEFAULT 0, -- ручной или вычисляемый + category VARCHAR(32), -- freedom, education, major_purchase + monthly_contribution NUMERIC(16,2), -- план пополнения + target_date DATE, + expected_return_rate NUMERIC(5,4), -- 0.07 = 7% + is_active BOOLEAN DEFAULT true +); +``` + +### 4.3 UI макет (какие страницы добавить) + +``` +/dashboard +├── Savings Rate (график за 12 мес + норма) +├── Net Worth (USD, KGS — два числа) +├── Freedom Gap (прогресс-бар: 0% → 100%) +└── Cash Reserve (мес жизни / норма 12) + +/freedom +├── Goals (список целей с прогресс-барами) +├── Investment Snapshots (таблица + график) +├── Projection (график: сегодня → свобода) +└── What-if (слайдеры: аренда, проект, курс) + +/finances +├── Passive Income (аренда, дивиденды, депозиты за месяц) +└── Allocations (pie chart портфеля) +``` + +### 4.4 API эндпоинты (новые) + +``` +GET /api/freedom/goals — список целей свободы +POST /api/freedom/goals — создать цель +PUT /api/freedom/goals/:id — обновить +GET /api/freedom/goals/:id/projection — проекция к цели + +GET /api/investments/snapshots — список снапшотов (с пагинацией) +POST /api/investments/snapshots — добавить снапшот +GET /api/investments/snapshots/latest — последний по каждому инструменту + +GET /api/dashboard/net-worth — общий капитал (USD + KGS) +GET /api/dashboard/freedom-gap — gap месяца +GET /api/dashboard/passive-income — аренда + дивиденды за период +``` + +--- + +## 5. Первый шаг (что сделать прямо сейчас) + +### Step 0: Заполнить данные в budget-app +Без актуальных расходов любой расчёт — гадание. Нужно: +1. Импортировать выписки за июнь 2025 — июнь 2026 (Альфа, Сбер, Demir) +2. Разнести по категориям (хотя бы крупные статьи: дети, дом, стройка, еда) +3. Вбить балансы квартир и других активов как investment_snapshot + +### Step 1: Net Worth Dashboard +Показать общий капитал в USD без лишних действий. +Уже можно сделать — данные по счетам есть в БД, квартиры добавляются как manual entry. + +### Step 2: Investment Snapshots +Форма: дата, инструмент, сумма, валюта → график роста капитала с проекцией. +Без автоматизации — раз в месяц ввести руками. + +### Step 3: Freedom Goal +После того как есть: +- реальные расходы (не -26%) +- актуальный капитал с квартирами +- доход от проекта (хоть какой-то) + +→ построить Freedom Gap Dashboard с what-if сценариями. + +--- + +## 6. Что не надо делать (анти-приоритеты) + +| Не надо | Почему | +|---------|--------| +| Monte Carlo Simulation | Сложно, данных мало, толку для твоего случая 0 | +| Withdrawal Strategy Planner | Ты не на пенсии, не нужен | +| Roth Conversion Ladder | Не применимо (не US resident) | +| FIRE Number Calculator в классике | Тебе нужен Freedom Gap, не 25x | +| Авто-импорт курсов валют | Не влияет на решение — достаточно раз в месяц | +| Сложные прогнозы в KGS | Курс KGS непредсказуем, считай в USD | + +--- + +## 7. Связанные заметки + +- [[personal/projects/budget-app/index|Budget App — главный документ]] +- [[personal/projects/budget-app/fire-investment-strategies-2026|Исходное исследование FIRE + инвестстратегий]] +- [[personal/documents/budget-app-features|Список всех идей фич]] diff --git a/personal/projects/budget-app/fire-investment-strategies-2026.md b/personal/projects/budget-app/fire-investment-strategies-2026.md new file mode 100644 index 00000000..4ff3fe19 --- /dev/null +++ b/personal/projects/budget-app/fire-investment-strategies-2026.md @@ -0,0 +1,246 @@ +--- +title: FIRE и инвестиционные стратегии для Budget App +tags: + - personal + - budget-app + - finance + - FIRE + - investments +related: '[[personal/projects/budget-app/index]]' +updated: '2026-06-23T00:00:00.000Z' +--- +# FIRE и инвестиционные стратегии для Budget App + +**Создано:** 2026-06-23 +**Источник:** исследование whale (FIRE guides, Goldman Sachs, PIMCO, Bogleheads, 2026) + +--- + +## 1. FIRE-типы и целевые показатели + +### 1.1 Виды FIRE + +| Тип | Годовые расходы | Целевой портфель | Суть | +|-----|----------------|-------------------|------| +| **Lean FIRE** | < $40,000 | < $1,000,000 | Минимализм, быстрый выход | +| **Chubby FIRE** | $40k–$100k | $1M–$2.5M | Комфорт без излишеств | +| **Fat FIRE** | $100k+ | $2.5M–$6M+ | Высокий уровень жизни | +| **Barista FIRE** | Переменные | 50–80% от полного | Частичная занятость покрывает часть | +| **Coast FIRE** | Любые | Достаточно чтобы дорасти | Перестать пополнять, дать сложному % работать | + +### 1.2 FIRE Number + +**Формула:** `FIRE Number = Annual Expenses × 25` (4% rule) + +На 2026 год рекомендуется **3–3.5%** вместо 4%: +- Ранний выход = 40–60 лет на пенсии (Trinity Study считала 30 лет) +- Повышенные оценки рынка (Shiller CAPE выше исторической нормы) +- Низкая доходность облигаций + +| Норма сбережений | Годовые расходы | FIRE (4%) | FIRE (3.5%) | +|------------------|----------------|-----------|-------------| +| $30,000 | $30,000 | $750,000 | $857,000 | +| $40,000 | $40,000 | $1,000,000 | $1,143,000 | +| $50,000 | $50,000 | $1,250,000 | $1,429,000 | +| $75,000 | $75,000 | $1,875,000 | $2,143,000 | +| $100,000 | $100,000 | $2,500,000 | $2,857,000 | + +### 1.3 Скорость до FIRE от нормы сбережений + +| Норма сбережений | Лет до FIRE | +|-----------------|-------------| +| 10% | 51 лет | +| 20% | 37 лет | +| 30% | 28 лет | +| 40% | 22 года | +| 50% | 17 лет | +| 60% | 12.5 лет | +| 70% | 8.5 лет | +| 80% | 5.5 лет | + +*Assumes 5% real returns, starting from zero* + +--- + +## 2. Стратегии вывода (Withdrawal Strategies) + +### 2.1 4% Rule (Trinity Study) +- 95% success rate для 30 лет с 60/40 портфелем +- **Для FIRE (40+ лет):** риск sequence-of-returns выше → используй 3.5% + +### 2.2 Flexible Spending (Variable Withdrawal) +- В плохие годы режешь дискреционные траты на 10–20% +- В хорошие — тратишь больше +- **Guardrails:** увеличиваешь withdrawal когда портфель выше цели, уменьшаешь когда ниже + +### 2.3 Bucket Strategy +- **Bucket 1 (1–2 года):** кэш/деньги — текущие расходы +- **Bucket 2 (3–7 лет):** облигации +- **Bucket 3 (8+ лет):** акции +- Ребалансируешь только когда акции переросли + +### 2.4 Dividend Investing +- Покрываешь расходы дивидендами (не продавая акции) +- Ниже общая доходность, но выше стабильность + +### 2.5 Roth Conversion Ladder +- Конвертируешь traditional IRA → Roth IRA ежегодно (1 год расходов) +- Через 5 лет конвертированные средства доступны без штрафа +- Нужен 5-летний bridge из taxable счетов + +--- + +## 3. Инвестиционные стратегии на 2026 + +### 3.1 Strategic Asset Allocation (классика) +- Фиксированные цели, ребаланс раз в квартал/год +- **Типичный model:** 60% equities / 30% fixed income / 10% alternatives +- **Историческая доходность 60/40:** ~6.5% annualized (2014–2024) +- **Плюс:** убирает эмоции, дисциплина правил +- **Минус:** не адаптируется к среде + +### 3.2 Three-Fund Portfolio (Bogleheads) +- US Total Stock Market (VTI/VTSAX) +- International Total Stock Market (VXUS/VTIAX) +- US Total Bond Market (BND/VBTLX) +- **Allocation:** 60/20/20 (или агрессивнее для молодых) +- **Expense ratios:** 0.03–0.05% +- **Разница в комиссиях:** 0.03% vs 1.0% на $500k за 20 лет = ~$100k+ + +### 3.3 Tactical Asset Allocation (2026 context) +- Отклонения 5–20% от стратегической аллокации на основе макро +- **Overweight:** энергия (commodities supercycle), floating-rate securities, инфраструктура +- **Underweight:** long-duration bonds, перегретые US tech +- **Добавить:** emerging market debt, inflation-protected assets +- **Alpha:** ~2.1% annually above static (multi-asset, 2024 study) + +### 3.4 Enhanced Passive (Goldman Sachs 2026) +- **Alpha Enhanced:** tracking error 50–200 bps, чуть выше комиссии +- **Зачем 2026:** снижение ожидаемой рыночной доходности, концентрация индексов, неопределённость +- **Systematic factor tilts** — небольшие ставки на value, momentum, quality + +### 3.5 Insured / Dynamic Allocation +- **Insured:** автоматический переход в консерватив при drawdown > X% +- **Dynamic:** 7.8% annualized vs static с меньшими просадками (2024) +- **Рекомендация 2026:** infrastructure, private credit, low-duration fixed income + +### 3.6 Goldman Sachs 2026: активные ETF + alternatives +- **Active ETFs:** AUM растёт 46% CAGR с 2020 +- **Derivative-income ETFs:** $47B inflows (Q1–Q3 2025) +- **Private assets:** Millennials держат ~20%, Boomers ~6% — поколенческий сдвиг +- **Tail-risk hedging:** нужен более широкий набор инструментов (не только bonds/USD) + +--- + +## 4. Тактики для нашего контекста (Alex, мультивалютный, KGS-based) + +### 4.1 Особенности +- **Базовая валюта:** KGS (высокая волатильность, зависимость от переводов РФ) +- **Счета:** RUB, USD, EUR, KGS, KZT, UZS, AED, CNY +- **Доход:** в основном RUB/USD +- **Расходы:** KGS (жизнь), USD (крупные/накопления), RUB (регулярные) + +### 4.2 Что адаптировать в budget-app + +#### Phase 6 — что строить (приоритеты): + +1. **FIRE Number Calculator** + - Поле: годовые расходы (берутся из фактических транзакций за 12 мес) + - Поле: текущий инвестированный капитал + - Поле: expected return (4–7%) + - Вывод: FIRE number (25x / 28.6x / 33x), years to FIRE + - Вывод: сколько нужно докладывать в месяц для выхода через N лет + +2. **Savings Rate Dashboard** + - `Норма сбережений = (Доходы - Расходы) / Доходы` + - График: savings rate по месяцам + - График: накопленный капитал vs FIRE trajectory (projection line) + +3. **FireGoal модель** + - `fire_goal` таблица: user_id, target_amount, target_currency_id, expected_return_rate, monthly_contribution, target_date + - Progress bar: сколько % от цели накоплено + - What-if: "что если увеличу savings rate на 5%?" + +4. **Multi-currency FIRE number** + - Пересчёт цели в base currency (KGS) и в USD + - Проблема: валютный риск при пенсии в KGS — нужно показывать и USD-эквивалент + +5. **Investment Tracking** + - `investment` / `portfolio` таблицы: date, account_id, value, currency_id + - Ввод: ручные snapshots (когда обновляешь брокерский счёт) + - График: капитал по месяцам + проекция 7% CAGR + - Автоматический импорт: нет (ручной ввод раз в месяц) + +6. **Withdrawal Simulator (+FI Calc)** + - Monte Carlo simulation (по историческим данным) + - Поля: начальный капитал, годовые расходы, asset allocation, withdrawal rate + - Результат: probability of success (не остаться без денег) + - Модель 4% vs 3.5% vs variable + +7. **"Буфер" / Cash Reserve Dashboard** + - Сколько месяцев расходов в кэше (по счетам типа cash/debit) + - Целевой буфер: 6–12 месяцев расходов + - Trigger: < 3 мес → alert + +### 4.3 Архитектурные решения для budget-app + +``` +investment_account (таблица): + id, user_id, account_id FK → account, + portfolio_value, currency_id, snapshot_date, + asset_class {cash, bonds, stocks, real_estate, crypto, other}, + notes + +fire_goal (уже есть в схеме): + id, user_id, target_amount, target_currency_id, + expected_return_rate (decimal, 0.07 = 7%), + monthly_contribution (decimal), + target_date NULL, + current_portfolio_value (вычисляется из investment_account snapshot) + +fire_projection: + computed view: по месяцам от текущей даты + columns: month, contribution, return, portfolio_value, is_fire (bool если >= target) +``` + +### 4.4 Какие FIRE-варианты реалистичны для Alex + +| FIRE-тип | Расходы/мес | Год | FIRE Number (4%) | Годовая норма сбережений | Лет* | +|----------|-------------|-----|------------------|-------------------------|------| +| Lean FIRE | Минимальные | ??? | ??? | ??? | ??? | +| Chubby FIRE | Текущие | ??? | ??? | ??? | ??? | +| Barista FIRE | С part-time | ??? | ??? | ??? | ??? | + +*\* — нужно подставить фактические цифры из budget-app* + +**Рекомендуемая стратегия для Alex:** +- Core: Three-Fund Portfolio (VT + BNDW) — глобальная диверсификация, не привязана к KGS +- Satellite: TIPS / real assets (инфляция в KGS выше чем в USD) +- Дивидендная составляющая: покрывает часть расходов (снижает sequence-of-returns risk) +- Буфер: 12+ месяцев расходов в USD (не KGS — защита от девальвации) +- Ребаланс: раз в год, или при drift > 5% + +--- + +## 5. Ключевые метрики на дашборд + +| Метрика | Откуда берётся | Формула | +|---------|---------------|---------| +| **Норма сбережений** | Транзакции за 12 мес | (Доходы - Расходы) / Доходы | +| **FIRE Number** | Средние расходы × 25 | (AvgExpenses × 12) × 25 | +| **FIRE Progress** | Инвестиционный капитал | PortfolioValue / FIRE_Number × 100% | +| **Years to FIRE** | Калькулятор | (ln(FIRE/R) - ln(FIRE/(R - P×12))) / ln(1+r) — сложно | +| **Safe Withdrawal Amount** | Портфель × 4% | PortfolioValue × 0.04 | +| **Runway (мес)** | Кэш / расходы в месяц | CashBalance / MonthlyExpenses | +| **Dollar-Cost Avg Equity** | Инвестиции / куплено единиц | — | + +--- + +## 6. Связанные ресурсы + +- [[personal/projects/budget-app/index|Budget App — главный документ]] +- [[personal/documents/budget-app-features|Исходный список фич]] +- [WealthVieu FIRE Guide 2026](https://wealthvieu.com/retirement/fire/) +- [Goldman Sachs — Portfolio Construction 2026](https://am.gs.com/en-us/advisors/insights/article/investment-outlook/portfolio-construction-2026) +- [Bogleheads Safe Withdrawal Rates](https://www.bogleheads.org/wiki/Safe_withdrawal_rates) +- [PIMCO — Investment Ideas for 2026](https://www.pimco.com/us/en/insights/charting-the-year-ahead-investment-ideas-for-2026) diff --git a/personal/projects/budget-app/index.md b/personal/projects/budget-app/index.md index 0659b76d..535f0fc2 100644 --- a/personal/projects/budget-app/index.md +++ b/personal/projects/budget-app/index.md @@ -81,7 +81,7 @@ - Категоризация — ручная. - Невозможно нормально работать с мобильного. - Анализ ограничен сводными в Excel: нет нормальных графиков по подкатегориям, кросс-фильтрации, прогнозов. -- Нет ничего про FIRE: проекций пенсии, моделирования инвестиций, целевых процентов нормы сбережений. +- Нет ничего про Freedom Gap / инвестиции: нет трекинга портфеля, нормы сбережений, проекции "когда аренда + проекты покроют расходы". - История курсов хранится в строках листа `курсы` — это не нормализованная таблица. - Нет API: нельзя интегрировать с банковскими экспортами, нельзя автоматически тянуть в дашборд. @@ -95,7 +95,7 @@ 2. Считает аналитику и сводки **на лету** через SQL, а не через `VLOOKUP`. 3. Импортирует банковские выписки (Альфа, Сбер, Demir, …) автоматически + AI-резолвер для неоднозначных случаев. 4. Даёт мобильный UI для быстрого ввода трат на ходу. -5. Моделирует **FIRE-сценарии** (отдельная фаза после готового бюджетирования). +5. **Freedom Gap** — разница между расходами и пассивным/полупассивным доходом (аренда, дивиденды, проекты). График прогресса к нулевому gap. What-if сценарии: "если проект даёт X, аренда Y — через N лет свобода". 6. Прогнозирует траты на N месяцев вперёд по сезонной модели (категории `🧱 Стройка`, `🏖️ Отдых` цикличны). ### Доменная модель (первая итерация) @@ -144,13 +144,34 @@ Base currency не хардкодится — выбирается в `Settings` 1. **Phase 0 — Discovery & schema**. Полностью разобрать Excel, утвердить доменную модель. *(в процессе — этот док)* 2. **Phase 1 — Skeleton + import**. Создать `~/Developer/budget-app/` (git init), FastAPI + Vue в Docker Compose в `~/docker/budget-app/` с подключением к существующему Postgres-кластеру на хосте (новая БД `budget_app`). Миграция xlsx → БД. Read-only viewer транзакций + остатков + годовые отчёты. Внешний доступ через `budget.qentra.top` с auth (FastAPI Users + JWT). -3. **Phase 2 — Курсы и настройки**. Cron-джоб для НБ КР с back-fill. Базовая валюта в настройках. Все пересчёты привязаны к ней. -4. **Phase 3 — Ручной ввод**. Форма добавления транзакции, CRUD категорий/счетов, ручная правка курсов. -5. **Phase 4 — Импорт банков + AI-резолвер**. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений. -6. **Phase 5 — Аналитика**. Дашборды: расход по категориям, динамика, бёрндаун по бюджету. Прогнозы. -7. **Phase 6 — Mobile / PWA**. Быстрый ввод с телефона. -8. **Phase 7 — MCP HTTP**. Экспонировать MCP-эндпоинт для Hermes. -9. **Phase 8 — Инвестпортфели + FIRE**. Тикеры/цены/дивы. Калькулятор FIRE. *(отдельное планирование когда бюджетирование готово.)* +3. **Phase 2 — Полноценный Viewer + CRUD**. Довести до уровня Excel по функциональности: + - Валюта у счетов (символ) + - Категории с иерархией (группа → подкатегория) + - Свёртка по годам/месяцам (drill-down как в Excel) + - Dynamic scrolling (infinite scroll вместо кнопок пагинации) + - CRUD транзакций: добавление, редактирование, удаление + - CRUD категорий + - CRUD счетов + - Ручная правка курсов + - Дашборды: расход по категориям, динамика, бёрндаун по бюджету + - Прогнозы (сезонная модель по категориям) + - Multi-currency: отображение балансов в валюте счёта + в base currency + - Сводная таблица по годам (как лист `сводная` в Excel) + - Налоговый учёт (как лист `налоги 22-24` в Excel) +4. **Phase 3 — Импорт банков + AI-резолвер**. Автофетч/парсеры (Альфа, Сбер, Demir). LLM-резолвер. TG-бот для ручных подтверждений. +5. **Phase 4 — Mobile / PWA**. Быстрый ввод с телефона. +6. **Phase 5 — MCP HTTP**. Экспонировать MCP-эндпоинт для Hermes. +7. **Phase 6 — Freedom / Инвестиции**. Замена классического FIRE на Freedom Gap — разница между расходами и пассивным доходом. + - P0: Savings Rate Dashboard (норма сбережений из транзакций) + - P0: Multi-Currency Net Worth (общий капитал USD/KGS) + - P1: Investment Snapshots (ручной ввод раз в месяц, таблица + график) + - P1: Freedom Gap Dashboard (доход-расход-аренда-проект = gap) + - P1: Passive Income Tracker (аренда + дивиденды + депозиты) + - P2: Freedom Goal with Projection Engine (what-if сценарии) + - P3: What-if Simulator (слайдеры: курс, аренда, проект, норма сбережений) + - Новые таблицы: `investment_snapshot`, `freedom_goal` + - Новые страницы: `/freedom`, `/finances` + - *Не делать:* Monte Carlo, Withdrawal Strategy Planner, Roth Conversion Ladder — не применимы ### Multi-tenant readiness (для будущего public SaaS) @@ -340,6 +361,12 @@ CLOUDFLARE_TUNNEL_TOKEN=... - `echarts` (скаффолд под Phase 5, не используем активно) - dev: `vite`, `typescript`, `eslint`, `prettier` +### Правила работы + +1. **Тесты — обязательны** для каждого нового API-роута или изменения. Если код не покрыт тестом — он не готов. +2. **Обновление доку** — после каждой завершённой задачи обновлять таблицу прогресса и Acceptance criteria в этом доке. +3. **Комит** — после каждой логически завершённой задачи (не раз в 10 шагов). + ### Acceptance criteria Phase 1 - [ ] `https://budget.qentra.top` открывается, login работает. @@ -364,4 +391,156 @@ CLOUDFLARE_TUNNEL_TOKEN=... --- **Создано:** 2026-06-21 -**Статус:** Phase 0 закрыт, Phase 1 спланирован +**Статус:** Phase 1 — в работе + +## Phase 1 progress + +| Шаг | Статус | Кем | +| -------------------------------------- | ------ | ---- | +| Init репо | ✅ | Орёл | +| Backend skeleton | ✅ | Орёл | +| Postgres bootstrap — роль + БД | ✅ | Кит | +| Alembic initial migration | ✅ | Кит | +| FastAPI Users + auth routes | ✅ | Кит | +| Bootstrap первого юзера | ✅ | Кит | +| XLSX импортёр (6 045 транзакций) | ✅ | Кит | +| Read-only API (12 тестов) | ✅ | Кит | +| Frontend skeleton + read-only страницы | ✅ | Кит | +| Docker Compose (2 сервиса, работает) | ✅ | Кит | +| Cloudflare Tunnel budget.qentra.top | ✅ | Alex | +| Сверка данных | ✅ | Кит | +| | | | + +## Disaster recovery: CASCADE data loss + +**Сценарий:** Удалён пользователь (A-click → user delete). Из-за `ON DELETE CASCADE` на `transaction_user_id_fkey` все транзакции этого пользователя удалены мгновенно (6 045 строк). Балансы обнулены. + +### Recovery шаги (на будущее) + +```bash +# 1. Пересоздать пользователя с тем же email (admin123) +curl -XPOST .../api/auth/register -H... -d'{"email":"alex@qentra.top","password":"admin123"}' + +# 2. Переимпортировать транзакции из xlsx +cd ~/Developer/budget-app +uv run python src/budget/importers/__init__.py + +# 3. Проверить балансы — все 18 счетов должны совпасть с excel-balances.md +# 4. Пересобрать и передеплоить backend (дата формат) + frontend (любые изменения) +docker-compose build backend && docker-compose up -d backend +cd frontend && npm run build && cd .. && docker-compose build --no-cache frontend && docker-compose up -d frontend +``` + +### Формат даты на фронте (актуальный) + +API возвращает `t.date.isoformat()` → `2026-05-29T17:00:00`. +Фронт режет: `{{ t.date.slice(0, 10) }} {{ t.date.slice(11, 16) }}` → `2026-05-29 17:00`. + +Если время = `00:00` — в Excel не было времени для этой транзакции. Это корректно. + +### Символы валют вместо колонки + +Колонка "Валюта" убрана из таблиц Transactions, Accounts, TaxRecords. +Вместо неё символ валюты показывается непосредственно перед суммой (Transactions, TaxRecords) или в ячейке (Accounts). + +Маппинг на фронте (постоянный, не из БД): + +| Код | Символ | Валюта | +|-----|--------|--------| +| USD | $ | Доллар | +| EUR | € | Евро | +| RUB | ₽ | Рубль | +| KGS | С̲ | Сом (с с нижней чертой) | +| KZT | ₸ | Тенге (уже есть в Unicode) | + +Файлы: `frontend/src/pages/Transactions.vue`, `Accounts.vue`, `TaxRecords.vue` — каждая содержит `CURRENCY_SYMBOLS` маппинг и функцию `getCurrencySymbol`/`currencySymbol`. + +### Пароль + +- `admin123` — совпадает с `ADMIN_PASSWORD` в `.env` + +## Phase 2 progress + +| Шаг | Статус | Кем | +| ---------------------------------------------------------------------------- | ------ | -------- | +| Валюта у счетов (символ) | ✅ | Кит | +| Категории с иерархией (API + фронт) | ✅ | Кит | +| Фронт: формы CRUD (транзакции, категории, счета) | ✅ | Кит | +| Свёртка по годам/месяцам (drill-down) | ✅ | Кит | +| Dynamic scrolling (infinite scroll) | ✅ | Кит | +| CRUD транзакций (API + тесты) | ✅ | Кит | +| CRUD категорий (API + тесты) | ✅ | Кит | +| CRUD счетов (API + тесты) | ✅ | Кит | +| Ручная правка курсов + API | ✅ | Кит | +| Дашборды (расход по категориям, динамика, сводная) | ✅ | Кит | +| Multi-currency отображение (баланс в валюте счёта + base currency) | ✅ | Кит | +| Сводная по годам | ✅ | Кит | +| Налоговый учёт | ✅ | Кит | +| DateTime в транзакциях (date → DateTime, datetime-local на фронте, миграция) | ✅ | Кит | +| Символ валюты вместо колонки (Transactions, Accounts, TaxRecords) | ✅ | Кит | + + +### Phase 2 — что сделано (подробно) + +**API (новые эндпоинты):** +- `GET /api/reports/monthly?year=` — помесячная разбивка доходов/расходов +- `GET /api/reports/summary` — сводная по годам +- `GET /api/reports/category-breakdown?year=&month=` — расходы по группам категорий (данные для дашборда) +- `GET/POST/PUT/DELETE /api/exchange-rates` — CRUD курсов валют +- `GET /api/exchange-rates/summary` — группировка по типу налога +- `GET/POST/PUT/DELETE /api/tax-records` — CRUD налоговых записей +- Accounts API теперь возвращает `balance_in_base` и `base_currency` (мультивалютность) + +**БД:** +- Новая таблица `tax_record` (alembic migration) + +**Фронтенд (новые страницы):** +- `/dashboard` — дашборд с помесячной динамикой (CSS-chart), расходами по категориям, сводной по годам +- `/exchange-rates` — таблица курсов с фильтрами, CRUD через модалку +- `/tax-records` — таблица налогов с фильтрами и сводкой по типам + +**Фронтенд (доработки):** +- `/transactions` — infinite scroll вместо пагинации (scroll-based) +- `/accounts` — отображение баланса в валюте счёта + в базовой валюте +- Навигация обновлена — добавлены ссылки на Дашборд, Курсы, Налоги + +## Тесты + +Запуск всех тестов одной командой (из `backend/`): + +```bash +cd backend && uv run pytest +``` + +Verbose: `cd backend && uv run pytest -v` + +### Фикстуры + +Общий `conftest.py` в `tests/` предоставляет: +- `engine` — Postgres test DB (`budget_app_test`) с `create_all`/`drop_all` на каждый тест + seed валют +- `client` — ASGI клиент с зарегистрированным тестовым юзером +- `auth_headers` — JWT Bearer token + +Все тесты используют **Postgres** (не sqlite). Настройка через `settings.test_database_url`. + +### Покрытие + +**37 тестов + 1 skipped**: + +| Файл | Тестов | Что проверяет | +|------|--------|---------------| +| `test_health.py` | 1 | Health endpoint | +| `test_models.py` | 3 | Импорт моделей, метаданные, create_all в sqlite | +| `test_auth.py` | 2 | Auth flow (register→login→me), unauthorized | +| `test_api.py` | 6 | Транзакции (list, filter, search), accounts, reports, unauthorized | +| `test_crud.py` | 6 | CRUD транзакций | +| `test_categories.py` | 8 | CRUD групп и категорий | +| `test_accounts.py` | 6 | CRUD счетов, удаление с транзакциями | +| `test_account_balances.py` | 6 | **Баланс: доход+расход, переводы, cross_rate, initial_balance** | + +### Вычисление баланса + +`balance = initial_balance + incoming - outgoing` + +- `incoming` = SUM(amount) if cross_rate IS NULL, SUM(amount * cross_rate) если перевод между валютами (для dest_account) +- `outgoing` = SUM(amount) — всегда в валюте источника diff --git a/personal/projects/personal-os/eagle-dashboard.md b/personal/projects/personal-os/eagle-dashboard.md index 25f81990..1dc3c5f0 100644 --- a/personal/projects/personal-os/eagle-dashboard.md +++ b/personal/projects/personal-os/eagle-dashboard.md @@ -39,7 +39,7 @@ FastAPI + HTMX + Tailwind CDN + Alpine.js. No build step, CDN-only frontend. ├── services.yaml # Service definitions (source of truth) ├── .env # EAGLE_TOKEN= (not committed) ├── templates/ -│ └── index.html # Dashboard UI — 3 tabs: Services, Pages, Files +│ └── index.html # Dashboard UI — 4 tabs: Services, Crons, Pages, Files └── com.eagle.dashboard.plist ``` @@ -133,14 +133,102 @@ PID-file-based — survives Dashboard restarts without crashing. --- +## Tabs — заглушка урл + +Каждый таб имеет свой URL через `?tab=` query-параметр. Переключение через Alpine.js `setUrlTab()`, который использует `history.replaceState()`. При загрузке `activeTab` инициализируется из `URLSearchParams` (с fallback на `'services'`). При клике на таб URL обновляется без перезагрузки страницы. Бэкенд также принимает `?tab=` в `GET /` и передаёт его как `initial_tab` в шаблон (пока не используется). + +**Урлы:** +- `http://dashboard.qentra.top/` — Services +- `http://dashboard.qentra.top/?tab=crons` — Crons +- `http://dashboard.qentra.top/?tab=pages` — Pages +- `http://dashboard.qentra.top/?tab=files` — Files + ## Tabs **Services** — health cards with: status badge, PID, uptime, memory (RSS + children), start/stop/restart controls, log tail drawer (last 200 lines). -**Pages** — URL input + iframe for quick local page testing. +**Pages** — URL input + metadata table for `/Library/WebServer/Documents/*.html` test pages. Each row shows filename, title, purpose, contents, modified date, and opens the page in a new browser tab. **Files** — FileBrowser iframe at `http://localhost:8181`. +**Crons** (2-я вкладка) — управление cron jobs из 3 источников: +- **Hermes Eagle** (22 jobs) — read/write напрямую в `~/.hermes/cron/jobs.json` +- **Hermes Whale** (0 jobs) — read/write в `~/.hermes/hermes-whale/cron/jobs.json` +- **Launchd (User)** — launchd plist-ы из `~/Library/LaunchAgents/` с `StartInterval` или `StartCalendarInterval` (редактируемые через launchctl + plistlib) + +Для каждого крона на карточке: name, schedule, enabled/disabled, last run, prompt preview, chips (skills, toolsets, model, workdir). Кнопки: +- **+ Cron** — создать новый крон (выбор source: Hermes Eagle / Hermes Whale / Launchd (User)). При выборе Launchd поля Deliver, Prompt, Model/Provider, Skills/Toolsets динамически скрываются через `x-show="cronModal.source !== 'launchd'"`. +- ✎ — открыть модал редактирования всех полей (name, schedule, deliver, prompt, script, model, provider, skills, toolsets, workdir) +- ⏸/▶ — pause/resume (toggle enabled) +- 🗑 — удалить крон + +|**Launchd create** — `POST /api/crons` с `source=launchd` генерирует plist. Принимает: `name` (→ Label, префикс `com.` если не указан), `schedule` (только `every Ns/m/h/d` → StartInterval в секундах), `script` (→ ProgramArguments, разбивка по пробелам), `workdir` (→ WorkingDirectory). Cron-выражения и ISO-даты для launchd **не поддерживаются** — валидатор на фронтенде их отклоняет с сообщением "Launchd: используйте every 30m / every 2h / every 1d". Поля prompt, deliver, skills, toolsets, model, provider игнорируются. Backend парсит `every N` + суффикс s/m/h/d → множитель 1/60/3600/86400. + +**`_list_launchd_crons()`** теперь показывает **все** plist-ы из `~/Library/LaunchAgents/`, не только с StartInterval/StartCalendarInterval. Для plist без schedule — `schedule: "none (manual)"`. Это фикс: раньше plist без schedule (например созданный с cron-выражением) не отображался в дашборде. + +**Schedule input — формат подсказки и валидация зависят от source:** + +- Для **launchd**: подсказка только `every 30s / 30m / 2h / 1d — интервал`. Плейсхолдер `every 30m / every 2h`. Валидатор reject cron и ISO. +- Для **Eagle/Whale**: полная cron-схема (5 полей), пояснения `*`, `*/N`, `N-M`, `A,B`, `N`, плюс `every 30s / 30m / 2h / 1d` и ISO-дата. + +**Schedule hint** — сворачиваемый multiline блок над полем ввода, открывается по кнопке `? подсказка`. Финальный рабочий текст: + +``` +┌── минута (0-59) +│ ┌── час (0-23) +│ │ ┌── день месяца (1-31) +│ │ │ ┌── месяц (1-12) +│ │ │ │ ┌── день недели (0-6, вс=0) +│ │ │ │ │ +* * * * * + +* — любое значение +*/N — с шагом N (*/30 = каждые 30) +N-M — диапазон (1-5 = пн-пт) +A,B — список (0,6 = вс,сб) +N — точное число (0 = в 0 мин) + +every 30s / 30m / 2h / 1d — человеческий формат +2026-07-01T09:00 — ISO, одноразово (только Eagle/Whale) +``` + +- `scheduleHintOpen` — boolean в `cronModal`, по умолчанию `false` +- При открытии модала сбрасывается в `false` +- ISO-строка скрыта для launchd через `x-if="cronModal.source !== 'launchd'"` + +**API эндпоинты:** +- `GET /` — главная страница, опционально `?tab=crons|pages|files|services` +- `GET /api/crons` — список всех кронов +- `POST /api/crons` — создать новый крон (body: source, name, schedule, prompt, script, deliver, skills, enabled_toolsets, model, provider, workdir). Для source=launchd: только name, schedule, script, workdir используются — генерируется plist и загружается через launchctl +- `PATCH /api/crons/{source}/{job_id}` — редактировать поля +- `POST /api/crons/{source}/{job_id}/toggle` — включить/выключить +- `DELETE /api/crons/{source}/{job_id}` — удалить + +**Формат Hermes cron jobs.json:** +```json +{ + "jobs": [ + { + "id": "531b242c30ad", + "name": "data-pipeline", + "prompt": "[SILENT] bash ...", + "skills": [], + "skill": null, + "model": null, + "provider": null, + "script": null, + "schedule": { "kind": "cron", "expr": "*/30 7-21 * * 1-5", "display": "*/30 7-21 * * 1-5" }, + "schedule_display": "*/30 7-21 * * 1-5", + "enabled": false, + "state": "paused", + "deliver": "local", + "enabled_toolsets": null, + "workdir": null + } + ] +} +``` + --- ## Deployment @@ -171,10 +259,32 @@ launchctl load ~/Library/LaunchAgents/com.eagle.dashboard.plist **Ghost launchctl entries** — after removing plists, use `launchctl remove