bort
# Борт — персональная система управления проектами
Локальная сводка по всем проектам: деньги, дедлайны, задачи, чаты. Одна SQLite-база,
два потребителя: человек — веб-интерфейс на `http://localhost:8100`, AI-ассистент — MCP-сервер.
Числа в UI и у ассистента всегда совпадают: оба идут через один сервисный слой.
## Старт
```bash
cd ~/bort
uv sync # зависимости, venv на Python 3.13
uv run python scripts/migrate.py # создать/обновить схему БД (идемпотентно)
uv run uvicorn bort.web.app:app --host 127.0.0.1 --port 8100 # вручную
```
Открыть `http://localhost:8100`. API-документация (Swagger): `http://localhost:8100/docs`.
Команда ручного старта слушает `127.0.0.1`. Развёрнутый на Mac mini сервис
слушает `0.0.0.0:8100` для доступа через LAN/Tailscale; не пробрасывайте его
в публичный интернет — в Борта пока нет авторизации.
### Сводка: порядок, канбан и статусы
Проекты в «Открытых» можно расставить вручную: перетащите строку за ручку слева
от названия, поставьте фокус на ручку и нажмите ↑/↓ (на тач-экранах для этого
рядом есть кнопки ↑/↓). Порядок хранится в SQLite (`projects.sort_order`) и
действует на всех устройствах; ссылка «Вернуть автосортировку» возвращает
автоматический порядок «горящее сверху, потом по приоритету». Проекты, не
затронутые перестановкой, остаются в автосортировке.
Переключатель «Список / Канбан» показывает ту же сводку досками по статусам
проекта; статус меняется прямо в карточке, архив закрытых проектов остаётся
свёрнутым. Ручная перестановка задаётся в виде «Список».
Стоимость проекта (сумма сделки) задаётся при создании и меняется в карточке
проекта: кнопка «Изменить» рядом со «Стоимостью» в панели денег открывает поле
суммы («150 000,50»; пустое значение — 0). После сохранения сразу пересчитываются
остаток к оплате, процент оплаты и маржа.
Проект можно удалить: на его странице рядом с «← К сводке» есть кнопка
«Удалить». Подтверждение предупреждает, что задачи, затраты, оплаты и связи
проекта уйдут вместе с ним каскадом и безвозвратно; после удаления браузер
возвращается на сводку, сохраняя её фильтры.
Помимо «Идеи», «В работе», «На паузе» и «Закрыт» есть статусы «На обсуждении»,
«На проверке» и «Ожидаем оплату» (работа сдана, деньги ещё не пришли — суммы
уже входят в общие деньги). Проект «На обсуждении» ещё не взят в работу: он виден
в списке и в счётчике проектов, но его стоимость, оплаты, затраты и маржа
**не входят** в общие деньги, пока статус не сменится на рабочий. В панели
портфеля такие проекты перечислены подписью «На обсуждении — не в деньгах».
Кнопки «+ Задача» в строке проекта и в блоке внимания нет: следующая задача
показана текстом, а задачи ведутся в карточке проекта и через MCP.
### Вкладка AI
Откройте `http://localhost:8100/ai` после применения миграций. Карточки идут
сверху вниз: сначала **подписки** (Claude, GPT · Codex, Qwen Cloud · Token Plan,
Z.AI · GLM Coding Plan), затем **балансы и API** (DeepSeek, OpenRouter). Каждая
карточка обновляется своей кнопкой «Обновить», а кнопка **«Обновить всё»** над
сеткой обновляет все провайдеры разом; сбой одного из них не мешает остальным
(сбойная карточка покажет ошибку, прежние цифры сохранятся).
- **Claude** — окна подписки 5 часов / 7 дней из `GET /usage` локального демона
`kraube serve` (OAuth-подписка Pro/Max/Team, шлюз `ai-gateway` на mini).
Служебный endpoint не делает платных обращений к Anthropic: демон кэширует
лимиты из заголовков последних ответов (до первого запроса — 404, карточка
честно об этом скажет). Нужен `KRAUBE_SERVE_KEY` (serve-ключ демона, не ключ
Anthropic) в `~/.config/bort/env`; адрес переопределяется
`BORT_KRAUBE_USAGE_URL` (по умолчанию `http://127.0.0.1:8787/usage`).
При статусе `allowed_warning`/`rejected` карточка подсвечивает сообщение.
- **DeepSeek** — баланс по `DEEPSEEK_API_KEY` через официальный `/user/balance`.
- **Qwen Cloud · Token Plan** — кредиты читает **снимок консоли**: ключ подписки
остаток не отдаёт, поэтому скрипт `scripts/pull_qwencloud_usage.py` на MacBook
открывает консоль Qwen Cloud в headless Chrome (копия залогиненного профиля),
читает внутренние API и присылает снимок в `~/bort/data/qwencloud-usage.json`
на mini (0600). Карточка показывает остаток кредитов, план, срок и время
сброса; снимок старше 3 часов помечается предупреждением. Кнопка «Обновить»
(или «Обновить всё») на mini ставит заявку `qwen-refresh.request`: sync-агент
MacBook забирает её в течение ~30 секунд, запускает сборщик и присылает свежий
снимок, а карточка подтягивает его раз в минуту. Путь снимка переопределяет
`BORT_QWENCLOUD_FILE`, путь заявки — `BORT_QWEN_REQUEST_FILE`, адрес mini —
те же `BORT_BEADS_SYNC_HOST/_KEY_ALIAS`.
- **OpenRouter** — баланс аккаунта по `OPENROUTER_API_KEY` (`/credits`) и расход
этого ключа (`/key`). Разбивка по моделям (за 7 дней) и ключам работает только
с **management-ключом** (`OPENROUTER_MANAGEMENT_KEY`, endpoint `/activity`) —
обычный ключ её не отдаёт, карточка честно об этом пишет.
- **Z.AI · GLM Coding Plan** — остаток по `ZAI_API_KEY` (или `Z_AI_API_KEY`)
с monitor-эндпоинта панели Z.AI `/api/monitor/usage/quota/limit`. Это endpoint
панели, а не публичный API: формат может измениться. Переопределить URL:
`BORT_ZAI_QUOTA_URL`.
- **GPT · Codex** — лимиты подписки читаются из **официального Codex app-server**
(`account/rateLimits/read`). На машине с Codex CLI (MacBook) они берутся напрямую,
а на mini — из снимка `scripts/pull_codex_limits.py` (`~/bort/data/codex-limits.json`,
путь переопределяет `BORT_CODEX_LIMITS_FILE`). Кнопка «Обновить» на mini создаёт
заявку `codex-refresh.request`: sync-агент MacBook забирает её в течение ~30 секунд,
перечитывает CLI и присылает свежий снимок; сам агент обновляет лимиты и без заявки,
если снимок старше 15 минут. Карточка на mini подтягивает снимок раз в минуту,
а устаревший (>3 часов) помечает предупреждением. Это независимая локальная
авторизация: сверьте учётную запись Codex CLI с GPT-подключением OpenCode.
OpenCode V2 не отдаёт остаток подписки напрямую; если CLI отсутствует и снимка
нет, вкладка покажет «недоступно».
Ключи **не хранятся в базе и репозитории**: положите их в локальный файл
`~/.config/bort/env` (права `600`, строки `KEY=VALUE`; путь переопределяет
`BORT_ENV_FILE`). Файл читается при старте, реальное окружение имеет приоритет:
```bash
mkdir -p ~/.config/bort && chmod 700 ~/.config/bort
printf 'DEEPSEEK_API_KEY=...\nZAI_API_KEY=...\nOPENROUTER_API_KEY=sk-or-...\n' > ~/.config/bort/env
chmod 600 ~/.config/bort/env
```
Журнал получает **только метаданные** через `POST /api/v1/ai/usage`. Он не перехватывает
внешние запросы автоматически: вызывающий клиент должен отправить событие после
AI-запроса. Например:
```json
{"provider":"deepseek","agent":"OpenCode","model":"deepseek-chat","input_tokens":1200,"output_tokens":250,"cost_microusd":3500,"external_request_id":"request-123"}
```
`cost_microusd` — стоимость в миллионных долях USD, которую сообщает клиент;
Борт пока не вычисляет её по тарифам. Промпты и ответы API отклоняет. Повтор
`external_request_id` в пределах провайдера не записывается (409). Все метрики во
вкладке отражают только присланные события; балансы обновляются кнопкой «Обновить».
### Вкладка «Задачи»
После применения миграций откройте `/tasks` (старый `/beads` перенаправляет
сюда). «Борт» показывает проекты Beads: канбан по состояниям, переключаемый список, счётчики,
поиск и фильтры по статусу, приоритету и типу. Проекты, у которых Beads не может
прочитать задачи, **не показываются** и не предлагаются для привязки.
**Задачи можно создавать и менять прямо на странице:** «+ Новая задача»,
смена статуса и приоритета, «Закрыть»/«Вернуть». Запись выполняет `bd` на
машине, где лежат проекты (MacBook), поэтому Борт ставит команды в очередь
(`beads_commands` в SQLite) и показывает их до выполнения; на mini очередь
разбирает sync-агент. Ошибки выполнения видны в блоке команд и снимаются
кнопкой «Снять». Кнопка «Синхронизировать с MacBook» просит агента прислать
свежий снимок, «Проверить снимок» просто перечитывает уже полученный файл.
Пока команды ждут, страница сама обновляется (черновики форм при этом
сохраняются).
Лишние проекты убираются из левого списка кнопкой «×» («Скрытые · N» —
вернуть обратно). Это скрытие **только в Борте**: задачи и файлы на Mac не
удаляются и не меняются.
Из «Сводки» раскройте «Ещё» у проекта и нажмите «Привязать к задачам». Выберите
один доступный проект и сохраните связь; затем из той же строки можно открыть
задачи и при необходимости изменить связь. Один проект Beads привязывается не более
чем к одному проекту Борта. Привязку можно снять на странице задач. Только
связь хранится в SQLite Борта, сами задачи остаются в Beads.
Собственные задачи в карточке проекта Борта сохраняются отдельно.
На машине с `bd` по умолчанию просматриваются каталоги `~/Documents` с
`.beads/config.yaml` на глубине до двух папок, без обхода всего домашнего
каталога и скрытых директорий. Иные корни — `BORT_BEADS_ROOTS` (через `:` на
macOS); нестандартный путь к CLI — `BORT_BEADS_BIN`. Вызовы `bd --readonly`
ограничены четырьмя одновременно и 12 секундами на команду; кеш в памяти —
2 минуты. Проекты с ошибкой чтения скрыты вместе с прежними данными.
На Mac mini, где нет `bd` и каталогов MacBook, страница читает **снимок из
MacBook** `~/bort/data/beads-snapshot.json`. Скрипт
`scripts/push_beads_snapshot.py` на MacBook каждый тик забирает очередь команд
mini (`scripts/beads_remote_queue.py` по SSH), выполняет их локальным `bd`,
отмечает результаты и атомарно отправляет снимок (названия, ID, статусы, типы
и приоритеты — без описаний и путей). Для ручного прогона задайте
`BORT_BEADS_SYNC_HOST=<ssh-адрес-mini>` и выполните
`uv run python scripts/push_beads_snapshot.py`. При доступе по альтернативному
имени SSH можно задать
`BORT_BEADS_SYNC_HOST_KEY_ALIAS` с уже проверенным именем ключа в `known_hosts`.
Для регулярного обновления на MacBook используется LaunchAgent
`com.alluc.bort-beads-sync` с интервалом 30 секунд; если он выключен
или MacBook недоступен, вкладка пометит снимок старше 10 минут как устаревший,
а команды останутся в очереди.
Снимок хранится только локально на mini с правами
`0600`; при локальном запуске Борта без него используется сам `bd`.
Вкладка первоначально подгружается через htmx, чтобы не задерживать открытие
страницы; без JavaScript доступна ссылка «Открыть без JavaScript». Сервер без
аутентификации: не открывайте порт 8100 в публичный интернет. Привязка
`127.0.0.1` из примера старта не переопределяет уже запущенный сервис на mini.
### Вкладка «Работа»
Откройте `/work` — справочник **постоянных проектов** с ежемесячной платой:
подписки, регулярные контракты, частичная занятость. Проекты заводятся вручную
кнопкой «+ Проект»; поле «В месяц» принимает рубли («150 000») и хранится
в копейках. Поля карточки: название, клиент, роль, формат (полная/частичная
занятость, контракт, другое), ежемесячная плата, валюта, статус
(активен/на паузе/завершён), дата начала, ссылка и заметки. Строку можно
изменить, сменить статус прямо в списке и удалить.
Панель итогов главным числом показывает **«Приходит в месяц»** — сумму плат
активных проектов в базовой валюте; рядом **«В паузе»** — потенциал, если вернуть
паузы в работу, и счётчики по статусам. Проекты в других валютах в эти суммы не
подмешиваются и перечислены отдельной строкой «Вне итогов», как и чужие валюты
в «Сводке».
Раздел **независим от денег «Сводки» и от Beads**: у него своя таблица
`work_projects` (миграция `009_work_projects`) и свои агрегаты; проекты «Работы»
не появляются в проектах, оплатах, затратах и марже, а задачи Beads на них не
ссылаются. REST API и MCP-инструменты для «Работы» пока не сделаны — это
возможный следующий шаг; интерфейс работает только через страницу `/work`.
### Запуск на Mac mini
На mini веб-сервис управляется LaunchAgent `com.alluc.bort-web`, а не PM2:
```bash
launchctl print gui/$(id -u)/com.alluc.bort-web
launchctl kickstart -k gui/$(id -u)/com.alluc.bort-web # после обновления кода
```
На MacBook проверить синхронизацию можно командой
`launchctl print gui/$(id -u)/com.alluc.bort-beads-sync`; первый снимок и
миграция `005_project_beads_links` уже применены на mini. Запущенный сервис
берёт БД и снимок из `~/bort/data/`. Не запускайте второй процесс на порту 8100.
Перед запуском новой версии кода на mini примените миграции
(`.venv/bin/python scripts/migrate.py`): сводка использует поле ручного порядка
из `006_project_order_statuses` и без него не откроется, а вкладка «Работа» —
таблицу из `009_work_projects`.
### Альтернативный запуск через PM2
```bash
cd ~/bort
pm2 start ecosystem.config.js # процесс bort-web, логи в ~/bort/logs/
pm2 save # запомнить список процессов
pm2 startup # автозапуск при загрузке (если не используется LaunchAgent)
pm2 restart bort-web # перезапуск
pm2 logs bort-web # смотреть логи
```
Если PM2 не установлен: `npm i -g pm2`. Без него сервис стартует вручную командой
`uv run uvicorn bort.web.app:app --host 127.0.0.1 --port 8100` из `~/bort`.
## Подключение MCP к Hermes
Добавить секцию `mcp_servers` в конфиг Hermes (`~/.hermes/config.yaml`; если секция уже есть —
дописать ключ `bort` внутрь):
```yaml
mcp_servers:
bort:
command: <ПУТЬ К РЕПО>/.venv/bin/python
args: ["-m", "bort.mcp.server"]
env:
BORT_DB: <ПУТЬ К РЕПО>/data/bort.db
BORT_TZ: Europe/Moscow
```
Транспорт — stdio: Hermes сам запускает процесс, открытых портов нет.
Проверка вручную: `echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' | BORT_DB=~/bort/data/bort.db ~/bort/.venv/bin/python -m bort.mcp.server`
— в stdout придёт JSON-ответ с `serverInfo.name == "bort"`.
Ресурс `bort://summary` — markdown-сводка одним чтением.
## Инструменты MCP
| Инструмент | Что делает |
|---|---|
| `bort_project_list` | Список проектов (краткая форма), фильтры статус/приоритет/поиск |
| `bort_project_get` | Проект целиком: задачи, затраты, чаты, люди (по id или названию) |
| `bort_project_create` | Создать проект; сумма в рублях или копейках |
| `bort_project_update` | Обновить поля проекта (по id или названию) |
| `bort_task_create` | Создать задачу в проекте |
| `bort_task_update` | Обновить задачу (статус, приоритет, дедлайн…) |
| `bort_task_close` | Закрыть задачу: `done` или `cancelled`, проставляет `closed_at` |
| `bort_expense_add` | Добавить затрату; в ответе — новая маржа проекта |
| `bort_summary` | Сводка: агрегаты по деньгам + счётчики «просрочено»/«горит». Проекты «на обсуждении» в денежные агрегаты не входят, пока не взяты в работу |
| `bort_project_summary` | Сводка одного проекта: маржа, прогресс задач, разбивка по категориям |
| `bort_chat_attach` | Привязать чат к проекту (существующий или создать новый) |
| `bort_chat_detach` | Отвязать чат от проекта (чат остаётся в справочнике) |
| `bort_chat_list` | Чаты с участниками и привязками к проектам |
| `bort_person_upsert` | Создать/обновить человека, сразу привязать к проекту/чату с ролью |
Все инструменты возвращают `{"ok": true, ...}` либо `{"ok": false, "error": {code, message, details}}`.
Проект ищется по названию регистронезависимо; неоднозначность → `conflict` со списком кандидатов.
## REST API
База: `http://127.0.0.1:8100/api/v1`. Полный список — в Swagger (`/docs`). Основное:
```
GET /api/v1/summary?scope=open|active|all&q= сводка с агрегатами
GET /api/v1/projects список (status, priority, q, limit, offset, sort)
POST /api/v1/projects создать (name*, deal_amount или deal_amount_minor)
GET /api/v1/projects/{id} проект + задачи, затраты, чаты, люди
PATCH /api/v1/projects/{id} частичное обновление
DELETE /api/v1/projects/{id} удалить (каскадом задачи и затраты)
POST /api/v1/projects/{id}/tasks задача · POST /tasks/{id}/close — закрыть
POST /api/v1/projects/{id}/expenses затрата · PATCH/DELETE /expenses/{id}
GET /api/v1/expense-categories категории · POST — добавить
GET/POST /api/v1/chats, /api/v1/people справочники
POST /api/v1/projects/{id}/chats привязать чат · DELETE .../chats/{chat_id}
POST /api/v1/projects/{id}/people привязать человека · DELETE .../people/{person_id}
GET /api/v1/health {status, db_path, schema_version, wal}
GET /api/v1/meta/enums статусы/приоритеты с русскими подписями
```
Деньги: канон — целые копейки (`*_minor`); человекочитаемая строка («150 000,50»)
принимается в полях `deal_amount`/`amount` и отдаётся рядом с `*_minor`.
## Бэкапы
```bash
~/bort/scripts/backup.sh
```
Копия через `sqlite3 .backup` (корректно при WAL) → `~/bort/backups/bort-YYYYMMDD-HHMMSS.db`,
хранятся последние 14 копий. Переменные: `BORT_DB`, `BORT_BACKUP_DIR`. Код возврата — 0/не-0.
Запускается вручную или из cron — отдельного экрана настроек в интерфейсе нет.
Ежедневный бэкап в 21:00 — добавить в crontab (`crontab -e`, НЕ добавлено автоматически):
```
0 21 * * * ~/bort/scripts/backup.sh >> ~/bort/logs/backup.log 2>&1
```
## Демо-данные
```bash
uv run python scripts/seed_demo.py # 6 демо-проектов (если база пуста)
uv run python scripts/seed_demo.py --wipe # убрать ВСЁ: проекты (с задачами и затратами), чаты, люди
```
## Тесты и структура
```bash
uv run pytest tests/ -q
```
Схема БД: `migrations/001_init.sql` (деньги — INTEGER копейки; даты — `YYYY-MM-DD` без TZ;
WAL; foreign_keys=ON на каждом соединении). Логика — в `src/bort/services/`, REST и MCP —
тонкие транспорты над ней, поэтому числа в UI и у ассистента не расходятся.
TDQS
Scored across 14 tools
Each tool targets a distinct resource+action, and the project/task/chat/person prefixes make boundaries clear. The only fuzziness is between bort_summary (cross-project aggregates) and bort_project_summary (single project), plus mild overlap with bort_project_get, which also returns money and tasks, but the descriptions distinguish scopes adequately.
All tools share the bort_ prefix and follow a resource_action pattern (project_list, task_create, chat_attach, expense_add), which is highly predictable. The lone global bort_summary lacking a resource prefix is a minor deviation from an otherwise consistent convention.
14 tools is well within the ideal 3-15 range for a project-management server covering projects, tasks, chats, people, expenses, and summaries. Each tool earns its place without redundancy.
Core lifecycles (project create/update, task create/update/close, chat attach/detach/list, expense add) are covered, but there are notable gaps: no expense update/delete/list, no standalone person list/get, no project delete/archive, and tasks lack a standalone list or reopen. Agents can partly work around via bort_project_get, but the surface is not fully rounded.