- TLS через подключаемый overlay docker-compose.tls.yml + Caddyfile:
Caddy сам получает и продлевает сертификат Let's Encrypt, HSTS
добавляется на уровне прокси — намеренно там, а не в nginx.conf,
иначе установки без TLS заперли бы сами себя;
- проверка заголовка Host против DNS rebinding: AURUM_ALLOWED_HOSTS,
по умолчанию localhost и 127.0.0.1, чужой Host получает 421 с
подсказкой, какую переменную поправить;
- «запомнить меня на 7 дней» вернулось в безопасном виде: пароль
шифруется AES-GCM ключом с extractable: false в IndexedDB, вместо
прежнего base64 в localStorage, который разворачивался обратно в
пароль одним atob. Требует secure context, поэтому по обычному HTTP
к LAN-адресу чекбокс не показывается, а не притворяется рабочим;
- аудит-лог необратимых операций (восстановление бэкапа, массовое
создание транзакций, удаление счёта) в лог контейнера. Не в БД
сознательно: таблица аудита исчезла бы ровно в том событии, которое
должна объяснять;
- README: раздел про TLS и таблица двух переменных, решающих, кто
может достучаться до инстанса.
Конфигурация и эксплуатационный слой:
- nginx: client_max_body_size 64m — восстановление из бэкапа не работало
вовсе, дефолтный лимит в 1 МБ резал дампы (реальный экспорт ~3.4 МБ)
с 413 ещё до backend;
- nginx: заголовки безопасности (nosniff, Referrer-Policy,
Permissions-Policy) и полный Content-Security-Policy. Ради него
inline-скрипт темы вынесен в public/theme-init.js;
- nginx: limit_req на /api/ — перебор пароля Basic Auth шёл со
скоростью сети, ни блокировки, ни задержки не было;
- nginx: server_tokens off;
- порт web привязан к 127.0.0.1, адрес настраивается через
AURUM_BIND_ADDRESS (раньше слушался на всех интерфейсах);
- backend работает от непривилегированного пользователя,
no-new-privileges для backend и web;
- дефолты пароля Postgres сведены к одному значению в обоих сервисах,
в .env.example описана ловушка с инициализацией тома.
Приложение:
- версия убрана из неавторизованного /api/health и отдаётся как
app_version в GET /api/settings;
- /api/docs, /api/redoc и /api/openapi.json отключаются через
AURUM_ENABLE_DOCS;
- текст исключения при неудачном восстановлении уходит в лог, а не
клиенту;
- max_length для notes в схемах транзакций, активов и регулярных
платежей (и в соответствующих полях UI);
- срок «запомнить меня» сокращён с 30 дней до 7;
- pytest обновлён до 9.0.3 (PYSEC-2026-1845), pytest-asyncio до 1.4.0;
- CI: permissions contents:read для GITHUB_TOKEN.
- Кэш проверки дублей не сбрасывался после импорта: повторный импорт
того же файла в течение 30 секунд (staleTime) сверялся со списком
транзакций, полученным ДО первого импорта, и не находил ни одного
дубля — ровно тот случай, ради которого проверка и делалась.
- Кнопку импорта можно было нажать, пока проверка дублей ещё шла или
шёл фоновый рефетч: дубли проходили мимо фильтра. Теперь кнопка
заблокирована на время запроса, а его ошибка больше не проглатывается
молча, а показывается предупреждением.
- Ручная смена кодировки ломала уже выбранные колонки: заголовки
перечитывались, старый выбор указывал на несуществующее имя,
indexOf давал -1, и все строки молча отбрасывались как «не удалось
распознать дату». Маппинг переугадывается при изменении состава
заголовков.
- Текст «Они не будут импортированы» противоречил включённой галочке
«Всё равно импортировать их».
Пять доработок импорта транзакций из CSV:
1. Проверка дублей при повторном импорте — сверка с уже существующими
транзакциями на счёте за диапазон дат файла (по дате+типу+сумме+
описанию), похожие исключаются из импорта по умолчанию с опцией
всё равно их добавить.
2. Автоопределение кодировки файла (UTF-8 с откатом на Windows-1251)
вместо жёстко зашитого UTF-8 — раньше кириллица из банковских
выгрузок превращалась в кракозябры. Есть ручной выбор на случай
неверного автоопределения.
3. Исправлен баг в parseAmount: суммы с несколькими разделителями
тысяч и без дробной части (например "1,234,567") теряли цифры и
отбрасывались как нераспознанные — теперь распознаются верно.
4. Добавлен явный выбор разделителя дробной части (точка/запятая) на
случай, когда автоопределение не может разрешить неоднозначность.
5. Пример значения из файла показывается прямо под каждым выбором
колонки при сопоставлении — не нужно доходить до превью, чтобы
понять, что в колонке.
Раньше при отсутствии .env docker compose тихо стартовал на дефолтах
из docker-compose.yml (без пароля, дефолтные креды Postgres), никак
не сигнализируя об этом. Добавлен start.sh — обёртка над
docker compose up -d --build, которая при отсутствии .env создаёт его
из .env.example и один раз громко предупреждает об используемых
дефолтах перед стартом контейнеров. README обновлён на использование
этого скрипта.
Дизайн-баннер был легко закрываем и появлялся заново каждую сессию.
Теперь показывается блокирующее модальное окно без кнопки закрытия/
Escape/клика по фону — пройти дальше можно только явным подтверждением
'Я понимаю риски'. Согласие сохраняется в localStorage и не запрашивается повторно.
Раньше предупреждение об отсутствии AURUM_BASIC_AUTH_USER/PASSWORD
было видно только в логах docker-контейнера при старте и терялось,
если пользователь забыл настроить .env. Теперь LoginGate различает
'авторизован' и 'auth вообще не настроен' и во втором случае
показывает заметный баннер прямо в интерфейсе.
На Firefox (Linux) программный .click() по input[type=file] со стилем
display:none может не открывать нативный диалог выбора файла. Теперь
инпут скрыт визуально (opacity + нулевой размер), но остаётся в layout.
Все minor/patch-апдейты одного пакетного менеджера теперь идут одним
PR, а major-обновления — отдельным, чтобы не терялись среди рутинных
и не сыпало десятком PR одновременно.
Создание с дефолтным/кастомным risk_level, правка через PATCH
/assets/{id}, создание/правка/сброс network через PATCH
/crypto/holdings/{id}, partial-update и 404 на неизвестный id.
Ссылается на чек-лист из CONTRIBUTING.md вместо дублирования, плюс
пункт про реальную проверку (не просто "тесты прошли" на словах) и
про отсутствие личных данных/ключей в дифе.
Vitest для чистой логики: lib/format.ts (заодно поймал баг в
trimTrailingZeros на "-0.000") и расчёт разбивки по риску/монете/сети
на вкладке Crypto — вынес их из компонентов в отдельные тестируемые
функции. Тесты подключены в CI. Версия приложения обновлена до 1.1.6.
Свободное текстовое поле на холдинге — какой сети принадлежит конкретная
монета (Ethereum, Tron и т.д.), с редактированием прямо в таблице и
разбивкой % по сетям на отдельной вкладке. Помогает увидеть, что часть
стейблкоинов лежит на рискованных сетях и стоит перевести на безопасные.
Риск (низкий/средний/высокий) задаётся при добавлении монеты и
редактируется прямо в таблице холдингов. Добавлена вкладка "Уровни
риска" в карточке портфеля с разбивкой % по риск-тирам, только для
крипты. Заодно поправлено округление процентов в риск-карточках.
Монеты в таблице активов шли в алфавитном порядке (сортировка по
имени в SQL-запросе) — с импортированными портфелями это выглядело
хаотично. Теперь список отсортирован по cost basis (сколько денег
реально вложено в монету) от большего к меньшему; полностью
проданная позиция уходит в конец списка, а не в начало.
График во вкладке «Криптовалюта» теперь по умолчанию показывает всю
историю, а не 30 дней — короткое окно у портфеля, растущего годами,
часто попадало на локальную просадку и выглядело демотивирующе.
Плитки «Лучший рост» и «Худшее падение» раньше всегда считались за
24 часа. Теперь они следуют за диапазоном, выбранным у графика
(7д/30д/90д/всё время): для 7д/30д используются закэшированные данные
CoinGecko, для «всё время» — метрика за 1 год (дальше бесплатный
тариф CoinGecko историю не отдаёт), а для 90д — отдельный запрос по
каждой монете, который выполняется только когда пользователь реально
выбрал этот период.
Криптовалюту теперь можно разделять на отдельные портфели (например,
долгосрочные инвестиции, накопления на недвижимость, спекулятивные
монеты) вместо одного общего списка — по тому же принципу, что уже
есть у счетов (Account): у портфеля есть имя, цветная метка и
архивация, каждая монета обязательно принадлежит одному портфелю,
один и тот же токен можно держать в нескольких портфелях сразу.
Тип счёта investment не входил в CASH_ACCOUNT_TYPES, из-за чего деньги
на инвестсчёте не попадали ни в блок "Наличные", ни в класс "Инвестиции"
раздела Капитал — просто выпадали из расчёта net worth (issue #14).
Раньше пул-реквесты никак не проверялись автоматически — сломанный PR
можно было смержить, ничего бы не показало проблему. Теперь на каждый PR
и пуш в main гоняются 115 backend-тестов (реальный Postgres в сервис-
контейнере, та же схема, что и в tests/README.md) и сборка фронтенда
(tsc + vite build).