49 Commits
Author SHA1 Message Date
ksv741 364f6bd8d0 chore(release): 0.1.8
Conventional commits / commits (pull_request) Successful in 10s
Docs security / security (pull_request) Successful in 6s
Docs verify / verify (pull_request) Successful in 9s
Docs security / security (push) Successful in 6s
Docs verify / verify (push) Successful in 10s
2026-11-06 07:47:24 +03:00
codex-bot 6d7d4c7b28 chore(deps): set Renovate release age, PR limit and update groups (WealthLedger/main#6) (#64)
Docs security / security (push) Successful in 6s
Docs verify / verify (push) Successful in 10s
2026-11-03 14:07:41 +03:00
ksv741 40ea47a7c2 chore(release): 0.1.7
Conventional commits / commits (pull_request) Successful in 10s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 12s
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 11s
2026-11-01 19:33:26 +03:00
codex-bot 37430da4c9 docs: describe concentration tab and stored instrument classification (WealthLedger/main#48) (#60)
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
2026-11-01 19:29:01 +03:00
codex-bot abe5b16241 chore(deps): resolve markdownlint audit advisory (WealthLedger/main#49) (#61)
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
2026-11-01 19:13:01 +03:00
codex-bot dcb528fa9f docs: describe all-account row grouping (WealthLedger/main#47) (#59)
Docs security / security (push) Successful in 6s
Docs verify / verify (push) Successful in 9s
2026-10-30 07:28:25 +03:00
ksv741 8c6c2f84ce chore(release): 0.1.6
Conventional commits / commits (pull_request) Successful in 12s
Docs security / security (pull_request) Successful in 8s
Docs verify / verify (pull_request) Successful in 12s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 13s
2026-10-24 19:15:05 +03:00
renovate-bot f9098d8409 chore(deps): update non-major dependencies (WealthLedger/docs#56)
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 12s
Co-authored-by: renovate-bot <renovate-bot@ksv741.ru>
2026-10-24 19:04:54 +03:00
codex-bot 4216eb815f chore: bump Node to 24.22.1 (WealthLedger/backend#122)
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 13s
2026-10-23 16:20:01 +03:00
ksv741 24ad250c02 chore(release): 0.1.5
Conventional commits / commits (pull_request) Successful in 13s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 13s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 13s
2026-10-18 16:24:45 +03:00
codex-bot e8e3ef6285 docs: describe quiet reading and visible source state (#51)
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 13s
ADR 021 фиксирует решение о тихом чтении и видимом состоянии источника; ui-resilience и затронутые domains приведены к AS-IS.
2026-10-17 22:11:49 +03:00
codex-bot 11f819ed31 chore: adopt linear git model in docs instructions (#49)
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 14s
2026-10-17 14:14:16 +03:00
codex-bot 550143aea1 docs: document Nexus delivery with shared account (WealthLedger/docs#18) (#48)
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 14s
2026-10-17 13:01:25 +03:00
codex-bot 56885f450b docs: describe ASCII 8-72 password policy and drop resolved discrepancy (WealthLedger/docs#46) (#47)
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 12s
2026-10-16 10:47:42 +03:00
codex-bot 1c89d321b1 Merge pull request 'chore: synchronize release 0.1.4 into develop (WealthLedger/main#30)' (#45) from chore/30-sync-release into develop
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
2026-10-16 07:48:19 +03:00
codex-bot ad1978c75e Merge pull request 'chore: bring develop into release sync branch (WealthLedger/main#30)' (#44) from develop into chore/30-sync-release
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 6s
Docs verify / verify (pull_request) Successful in 11s
2026-10-16 07:41:04 +03:00
codex-bot 77c1f44726 Merge pull request 'docs: describe events default period and picker bounds (WealthLedger/frontend#63)' (#43) from docs/63-upcoming-events-default-period into develop
Docs security / security (push) Successful in 11s
Docs verify / verify (push) Successful in 15s
Conventional commits / commits (pull_request) Successful in 10s
Docs security / security (pull_request) Successful in 6s
Docs verify / verify (pull_request) Successful in 11s
2026-10-14 21:35:00 +03:00
ksv741 8a2a62c611 docs: describe events default period and picker bounds (WealthLedger/frontend#63)
Conventional commits / commits (pull_request) Successful in 13s
Docs security / security (pull_request) Successful in 9s
Docs verify / verify (pull_request) Successful in 14s
2026-10-14 21:23:33 +03:00
codex-bot 7b61eb8e6b docs: align current pages with hourly freshness model (WealthLedger/docs#34) (#42)
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 13s
2026-10-14 14:03:58 +03:00
codex-bot cad0f31615 Merge pull request 'ci: rerun commits proof on PR title edit (WealthLedger/docs#35)' (#41) from docs/35-commits-workflow-edited into develop
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 13s
2026-10-13 20:33:45 +03:00
ksv741 03110e99ea ci: rerun commits proof on PR title edit (WealthLedger/docs#35)
Conventional commits / commits (pull_request) Successful in 13s
Docs security / security (pull_request) Successful in 9s
Docs verify / verify (pull_request) Successful in 13s
2026-10-13 20:18:59 +03:00
codex-bot 03fb045a9e docs: document income without comparisons and unified payouts (WealthLedger/main#33) (#40)
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 14s
docs: document income without comparisons and unified payouts (WealthLedger/main#33)
2026-10-13 18:16:59 +03:00
ksv741 add9e5f638 ci: validate release PR commits independently (WealthLedger/main#30)
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 12s
2026-10-12 23:39:30 +03:00
ksv741 a818889e54 chore(release): 0.1.4
Conventional commits / commits (pull_request) Failing after 11s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 12s
2026-10-12 23:19:12 +03:00
ksv741 803f0843f8 Merge pull request 'docs: document hourly attempt lifecycle semantics (WealthLedger/backend#56)' (#37) from docs/56-harden-hourly-sync-lifecycle into develop
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 15s
Reviewed-on: #37
2026-10-12 22:29:06 +03:00
ksv741 1378ac9fe2 docs: document hourly attempt lifecycle semantics (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 12s
Docs security / security (pull_request) Successful in 8s
Docs verify / verify (pull_request) Successful in 12s
2026-10-12 20:28:59 +03:00
codex-bot 04dc662b2e Merge pull request 'chore: align docs engineering workflow (WealthLedger/main#27)' (#36) from chore/27-engineering-workflow into develop
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 13s
2026-10-12 11:00:36 +03:00
ksv741 c24e4b990a docs: pin docs workflow reference (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 13s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 12s
2026-10-12 10:22:13 +03:00
ksv741 602eb43294 fix: align docs commitlint workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Canceled after 0s
Docs security / security (pull_request) Canceled after 0s
Docs verify / verify (pull_request) Canceled after 0s
2026-10-12 10:21:16 +03:00
ksv741 eaf7ede841 chore: align docs engineering workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 12s
2026-10-12 09:20:54 +03:00
codex-bot fe99b05e93 docs: document stock screener
Docs security / security (push) Successful in 6s
Docs verify / verify (push) Successful in 11s
2026-10-11 11:45:09 +03:00
ksv741 e6fd4f64b8 ci: skip checks on PR metadata edits
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 6s
Docs verify / verify (pull_request) Successful in 11s
2026-10-11 11:36:51 +03:00
ksv741 4caa5e92b7 ci: avoid duplicate PR checks
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
2026-10-11 11:24:57 +03:00
ksv741 2c34023af8 docs: document stock screener
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 6s
Docs verify / verify (pull_request) Successful in 12s
2026-10-11 11:18:40 +03:00
ksv741 f0cab445d7 docs: document hourly investment reads
Docs security / security (push) Successful in 46s
Docs verify / verify (push) Successful in 13s
2026-10-09 21:13:45 +03:00
ksv741 df6f0109ed docs: clarify card summary totals 2026-10-09 21:13:45 +03:00
codex-bot 0f8bbee10a Merge pull request 'docs: document hourly sync history' (#31) from release/0.1.3 into develop
Docs security / security (push) Successful in 9s
Docs verify / verify (push) Successful in 14s
Merge hourly sync history documentation
2026-10-09 09:19:44 +03:00
ksv741 e0bceea936 docs: document hourly sync history
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 13s
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 13s
2026-10-09 09:05:08 +03:00
ksv741 9a9b0409cc chore(release): 0.1.3
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 10s
Docs verify / verify (pull_request) Successful in 12s
Docs security / security (push) Successful in 9s
Docs verify / verify (push) Successful in 12s
2026-10-07 18:13:58 +03:00
ksv741 256d49b045 fix(deps): refresh docs security baseline
Conventional commits / commits (pull_request) Successful in 12s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 14s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 16s
2026-10-07 15:36:47 +03:00
ksv741 9477e688d1 fix(deps): remove vulnerable docs tooling
Docs security / security (push) Successful in 19s
Docs verify / verify (push) Successful in 13s
Replace CLI2's unpatched braces dependency with markdownlint-cli.

Preserve link checks and CHANGELOG rules; pin patched release and parser dependencies.
2026-10-03 21:32:35 +03:00
ksv741 0ee54ca952 docs: record minimal runtime coordination
Document accepted cross-tab reauthentication, on-demand candles,
native query/logging ownership and the complete deployment path.

Preserve historical ADR context and link amendments through ADR 018.
Related backend change: WealthLedger/backend@d698ed9
2026-10-03 21:32:35 +03:00
codex-bot 6fa62fb8b5 docs: record code quality review and refactoring
Docs security / security (push) Successful in 9s
Docs verify / verify (push) Successful in 14s
2026-09-27 18:26:55 +03:00
ksv741 e4838e1485 chore(release): 0.1.2
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 11s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 12s
2026-09-26 22:10:08 +03:00
ksv741 07de062ebb docs: explain persisted T-Bank reads and synchronization
Conventional commits / commits (pull_request) Successful in 14s
Docs security / security (pull_request) Successful in 8s
Docs verify / verify (pull_request) Successful in 13s
Docs security / security (push) Successful in 8s
Docs verify / verify (push) Successful in 12s
Document local data ownership, coverage and freshness, refresh behavior, failure
recovery and additive migration rollback. Record ADR 017.
2026-09-26 21:22:50 +03:00
ksv741 55610ea2b4 docs: describe local frontend packages and ownership
Docs security / security (pull_request) Successful in 9s
Docs verify / verify (pull_request) Successful in 12s
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (push) Successful in 7s
Docs verify / verify (push) Successful in 11s
OpenSpec change: evolve-frontend-stack
2026-09-22 22:11:04 +03:00
ksv741 15d1d820f5 docs: document versioned deployment bundle
Conventional commits / commits (pull_request) Successful in 15s
Docs security / security (pull_request) Successful in 7s
Docs verify / verify (pull_request) Successful in 14s
Docs security / security (push) Successful in 9s
Docs verify / verify (push) Successful in 13s
2026-09-20 18:49:25 +03:00
ksv741 6fd39cd467 docs: use shared Nexus account #18
Docs security / security (pull_request) Successful in 14s
Docs verify / verify (pull_request) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
Docs security / security (push) Successful in 16s
Docs verify / verify (push) Successful in 11s
2026-09-17 20:53:05 +03:00
ksv741 5de85c296f docs: document Nexus registry migration #18
Docs security / security (push) Successful in 9s
Docs verify / verify (push) Successful in 11s
Docs security / security (pull_request) Successful in 13s
Docs verify / verify (pull_request) Successful in 12s
Conventional commits / commits (pull_request) Failing after 10s
2026-09-17 20:41:24 +03:00
54 changed files with 3170 additions and 1845 deletions
+15
View File
@@ -0,0 +1,15 @@
---
name: Bug Report
about: Неверное поведение
---
# Bug Report
## Проблема и шаги воспроизведения
## Факт / ожидание
## Acceptance Criteria
- [ ]
## Окружение / ссылки (если применимо)
+13
View File
@@ -0,0 +1,13 @@
---
name: Engineering Task
about: CI, тесты, документация, процесс и прочее
---
# Engineering Task
## Контекст и результат
## Scope / связанные Issues (если есть)
## Acceptance Criteria
- [ ]
+15
View File
@@ -0,0 +1,15 @@
---
name: Feature Request
about: Новое наблюдаемое поведение
---
# Feature Request
## Зачем и какой результат
## Ожидаемое поведение
## Acceptance Criteria
- [ ]
## Зависимости / связанные Issues (если есть)
+15
View File
@@ -0,0 +1,15 @@
---
name: Refactoring / Technical Debt
about: Улучшение без нового поведения
---
# Refactoring / Technical Debt
## Проблема и результат
## Сохраняемые инварианты / границы
## Acceptance Criteria
- [ ]
## Зависимости / ссылки (если есть)
+19
View File
@@ -0,0 +1,19 @@
# Pull Request
## Summary
Краткий результат работы описывается здесь.
## Linked Issue
Refs `<owner/repo#number>` (заменить ссылкой на реальную Issue).
## Changes
Затронутые части и изменения.
## Verification
- Команда / результат / ограничения:
## Breaking Changes (если есть)
+9 -1
View File
@@ -4,6 +4,10 @@ on:
pull_request:
types: [opened, synchronize, reopened, edited]
concurrency:
group: docs-commits-${{ gitea.event.pull_request.number }}
cancel-in-progress: true
jobs:
commits:
runs-on: ubuntu-latest
@@ -23,4 +27,8 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
npm run commitlint -- --strict --from "$HEAD_SHA^" --to "$HEAD_SHA"
else
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
fi
+6 -2
View File
@@ -2,12 +2,16 @@ name: Docs security
on:
push:
branches:
- '**'
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '17 5 * * *'
concurrency:
group: docs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs:
security:
runs-on: ubuntu-latest
+6 -1
View File
@@ -2,8 +2,13 @@ name: Docs verify
on:
push:
branches: ['**']
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: docs-verify-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs:
verify:
+6
View File
@@ -0,0 +1,6 @@
{
"extends": ".markdownlint.json",
"MD024": {
"siblings_only": true
}
}
-21
View File
@@ -1,21 +0,0 @@
{
"config": {
"MD013": false
},
"customRules": [
"markdownlint-rule-relative-links"
],
"overrides": [
{
"filter": [
"CHANGELOG.md"
],
"config": {
"MD024": {
"siblings_only": true
}
},
"combine": "merge"
}
]
}
+1
View File
@@ -0,0 +1 @@
module.exports = require('markdownlint-rule-relative-links').default;
+3
View File
@@ -0,0 +1,3 @@
{
"MD013": false
}
+1 -1
View File
@@ -1 +1 @@
24.21.0
24.22.1
+12 -3
View File
@@ -3,6 +3,9 @@
Перед изменением прочитай [README](README.md) и [CONTRIBUTING](CONTRIBUTING.md),
затем только затронутую тему. Docs объясняет систему; требования принадлежат
основным спецификациям, реализация и coding conventions — соседним репозиториям.
До edits найди/создай подходящую Gitea Issue, проверь AC и ветку; чтение
допускается без Issue. Общий маршрут — [Engineering Workflow](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9/.ai/conventions/engineering-workflow.md)
(main#28; после merge — актуальная версия `develop`).
Проверь существенные факты по локальным тематическим страницам и ADR;
внешние источники используй как справочные свидетельства, когда они доступны.
@@ -20,7 +23,13 @@
## Ветки и релизы
Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы).
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`.
Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. PR сливается squash или rebase-merge; merge-коммит не используется.
`main` обновляется только fast-forward из `develop`, поэтому `origin/main`
всегда предок `origin/develop`:
`git merge-base --is-ancestor origin/main origin/develop`. Срочный fix —
обычная ветка `fix/*` от `develop`; пути hotfix от `main` нет.
Релиз — по [README](README.md#релизы).
Commits и заголовки PR — Conventional Commits со ссылкой на Issue (кроме release);
для спецификаций допустим `spec:`.
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
+89
View File
@@ -1,5 +1,94 @@
# Changelog
## [0.1.8](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.7...v0.1.8) (2026-11-06)
### Maintenance
* **deps:** set Renovate release age, PR limit and update groups (WealthLedger/main[#6](https://git.personal.ksv741.ru/WealthLedger/docs/issues/6)) ([#64](https://git.personal.ksv741.ru/WealthLedger/docs/issues/64)) ([6d7d4c7](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6d7d4c7b286535b7338d7e398d2daf7d8137a501))
## [0.1.7](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.6...v0.1.7) (2026-11-01)
### Documentation
* describe all-account row grouping (WealthLedger/main[#47](https://git.personal.ksv741.ru/WealthLedger/docs/issues/47)) ([#59](https://git.personal.ksv741.ru/WealthLedger/docs/issues/59)) ([dcb528f](https://git.personal.ksv741.ru/WealthLedger/docs/commit/dcb528fa9f76f0d7aa98bd827cc7ee39024dbb13))
* describe concentration tab and stored instrument classification (WealthLedger/main[#48](https://git.personal.ksv741.ru/WealthLedger/docs/issues/48)) ([#60](https://git.personal.ksv741.ru/WealthLedger/docs/issues/60)) ([37430da](https://git.personal.ksv741.ru/WealthLedger/docs/commit/37430da4c99cb571ef5bc0676cad7b3a47bcb6f0))
### Maintenance
* **deps:** resolve markdownlint audit advisory (WealthLedger/main[#49](https://git.personal.ksv741.ru/WealthLedger/docs/issues/49)) ([#61](https://git.personal.ksv741.ru/WealthLedger/docs/issues/61)) ([abe5b16](https://git.personal.ksv741.ru/WealthLedger/docs/commit/abe5b16241d3002baa5d39c2a12949f23a75b968))
## [0.1.6](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.5...v0.1.6) (2026-10-24)
### Maintenance
* bump Node to 24.22.1 (WealthLedger/backend[#122](https://git.personal.ksv741.ru/WealthLedger/docs/issues/122)) ([4216eb8](https://git.personal.ksv741.ru/WealthLedger/docs/commit/4216eb815f59b7a284e679ccb1b725438da07882))
* **deps:** update non-major dependencies (WealthLedger/docs[#56](https://git.personal.ksv741.ru/WealthLedger/docs/issues/56)) ([f9098d8](https://git.personal.ksv741.ru/WealthLedger/docs/commit/f9098d84098467e41143bad5044085d6329099d9))
## [0.1.5](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.4...v0.1.5) (2026-10-18)
### Documentation
* align current pages with hourly freshness model (WealthLedger/docs[#34](https://git.personal.ksv741.ru/WealthLedger/docs/issues/34)) ([#42](https://git.personal.ksv741.ru/WealthLedger/docs/issues/42)) ([7b61eb8](https://git.personal.ksv741.ru/WealthLedger/docs/commit/7b61eb8e6ba93a765b1ae27ca31d77b5e422e149))
* describe ASCII 8-72 password policy and drop resolved discrepancy (WealthLedger/docs[#46](https://git.personal.ksv741.ru/WealthLedger/docs/issues/46)) ([#47](https://git.personal.ksv741.ru/WealthLedger/docs/issues/47)) ([56885f4](https://git.personal.ksv741.ru/WealthLedger/docs/commit/56885f450bc1298d383d496174d16e591926e047))
* describe events default period and picker bounds (WealthLedger/frontend[#63](https://git.personal.ksv741.ru/WealthLedger/docs/issues/63)) ([8a2a62c](https://git.personal.ksv741.ru/WealthLedger/docs/commit/8a2a62c611e1acc1c1e69048a88b049bbf0d9087))
* describe quiet reading and visible source state ([#51](https://git.personal.ksv741.ru/WealthLedger/docs/issues/51)) ([e8e3ef6](https://git.personal.ksv741.ru/WealthLedger/docs/commit/e8e3ef62854dae8448deaa66ac26e5b15d5c7d6a))
* document income without comparisons and unified payouts (WealthLedger/main[#33](https://git.personal.ksv741.ru/WealthLedger/docs/issues/33)) ([#40](https://git.personal.ksv741.ru/WealthLedger/docs/issues/40)) ([03fb045](https://git.personal.ksv741.ru/WealthLedger/docs/commit/03fb045a9ee8a36775599bda66e7f6a08362bd84))
* document Nexus delivery with shared account (WealthLedger/docs[#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18)) ([#48](https://git.personal.ksv741.ru/WealthLedger/docs/issues/48)) ([550143a](https://git.personal.ksv741.ru/WealthLedger/docs/commit/550143aea1f0ac90630f326dd927091b0ec4f38e))
### CI
* rerun commits proof on PR title edit (WealthLedger/docs[#35](https://git.personal.ksv741.ru/WealthLedger/docs/issues/35)) ([03110e9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/03110e99ea58600bacd4c091fda8591d374a5a8d))
### Maintenance
* adopt linear git model in docs instructions ([#49](https://git.personal.ksv741.ru/WealthLedger/docs/issues/49)) ([11f819e](https://git.personal.ksv741.ru/WealthLedger/docs/commit/11f819ed31226a05524ac35a893064525713e41b))
## [0.1.4](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.3...v0.1.4) (2026-10-12)
### Bug Fixes
* align docs commitlint workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/docs/issues/27)) ([602eb43](https://git.personal.ksv741.ru/WealthLedger/docs/commit/602eb43294509274cbd9003be31f726b6e275a1e))
### Documentation
* clarify card summary totals ([df6f010](https://git.personal.ksv741.ru/WealthLedger/docs/commit/df6f0109ed696a529cf2622e23fceeab8133f932))
* document hourly attempt lifecycle semantics (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/docs/issues/56)) ([1378ac9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/1378ac9fe2fcfd43bd71421e62b02f00069be928))
* document hourly investment reads ([f0cab44](https://git.personal.ksv741.ru/WealthLedger/docs/commit/f0cab445d7e7076bf92a61f1023b8bf51e094d33))
* document hourly sync history ([e0bceea](https://git.personal.ksv741.ru/WealthLedger/docs/commit/e0bceea936239c08f4717e264be23d7f60552398))
* document stock screener ([2c34023](https://git.personal.ksv741.ru/WealthLedger/docs/commit/2c34023af8593f305f7c81e672b4e179c0992cf3))
* pin docs workflow reference (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/docs/issues/27)) ([c24e4b9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/c24e4b990aaeffd02e05f7de21942579e452cecf))
### CI
* avoid duplicate PR checks ([4caa5e9](https://git.personal.ksv741.ru/WealthLedger/docs/commit/4caa5e92b7d2597e5c7725b2036007b71ece2abd))
* skip checks on PR metadata edits ([e6fd4f6](https://git.personal.ksv741.ru/WealthLedger/docs/commit/e6fd4f64b81dd73b33959b845a1babcfac1891c8))
### Maintenance
* align docs engineering workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/docs/issues/27)) ([eaf7ede](https://git.personal.ksv741.ru/WealthLedger/docs/commit/eaf7ede8414f0df972f9ae7ab9e223a6350b5462))
## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.2...v0.1.3) (2026-10-07)
### Bug Fixes
* **deps:** refresh docs security baseline ([256d49b](https://git.personal.ksv741.ru/WealthLedger/docs/commit/256d49b04591d55d980a22c49b413432c9bda746))
* **deps:** remove vulnerable docs tooling ([9477e68](https://git.personal.ksv741.ru/WealthLedger/docs/commit/9477e688d180783881abc92f8835517715880279))
### Documentation
* record code quality review and refactoring ([6fa62fb](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6fa62fb8b51aba82766fb50b3e5871572af8be27))
* record minimal runtime coordination ([0ee54ca](https://git.personal.ksv741.ru/WealthLedger/docs/commit/0ee54ca952db6ca7264c0d7fe8cf5759b3187c5b))
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.1...v0.1.2) (2026-09-26)
### Documentation
* describe local frontend packages and ownership ([55610ea](https://git.personal.ksv741.ru/WealthLedger/docs/commit/55610ea2b4dc687f76ee9eb37c14d887b1b70100))
* document Nexus registry migration [#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18) ([5de85c2](https://git.personal.ksv741.ru/WealthLedger/docs/commit/5de85c296f7681882f58476343d3da71f1dee28d))
* document versioned deployment bundle ([15d1d82](https://git.personal.ksv741.ru/WealthLedger/docs/commit/15d1d820f5401c25adda23f753b99e894bdcd4d3))
* explain persisted T-Bank reads and synchronization ([07de062](https://git.personal.ksv741.ru/WealthLedger/docs/commit/07de062ebbf043eff50bd7f53b48bff13bb7cb1b))
* use shared Nexus account [#18](https://git.personal.ksv741.ru/WealthLedger/docs/issues/18) ([6fd39cd](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6fd39cd467566e23cca79a1235125dfb80fa99f7))
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.0...v0.1.1) (2026-09-16)
### Documentation
+4 -3
View File
@@ -177,9 +177,10 @@ Markdown-only правки не требуют тестов приложения
`security.yml` при push, PR и ежедневно проверяет всю Git-историю через
Gitleaks 8.30.1 с redacted-выводом и все npm dependencies через
`npm audit --include=dev --audit-level=low`. Override `smol-toml` исправляет
DoS в TOML parser markdownlint-cli2; его можно удалить, когда родительский
пакет примет исправленную версию.
`npm audit --include=dev --audit-level=low`. Overrides `smol-toml` и `js-yaml`
исправляют DoS в parsers markdownlint-cli; overrides для release-it обновляют
уязвимые basic-ftp и undici. Их можно удалить, когда родительские пакеты
примут исправленные версии.
Renovate объединяет minor/patch updates; major требует согласования в
Dependency Dashboard, automerge не включён. Required checks включаются
+43 -18
View File
@@ -30,12 +30,12 @@ WealthLedger помогает частному инвестору понимат
| [Аутентификация](domains/authentication/overview.md) | Регистрация, вход, восстановление и завершение сессии |
| [Счета](domains/accounts/overview.md) | Доступные счета, сводки и состояния каждого счёта |
| [Банковские карты](domains/cards/overview.md) | Ручные остатки, операции, категории, возвраты, месячные сводки и исправления |
| [Портфель](domains/portfolio/overview.md) | Позиции, остатки, суммы, структура и свежесть |
| [Портфель](domains/portfolio/overview.md) | Позиции, остатки, суммы, структура, [концентрация](domains/portfolio/concentration.md) и свежесть |
| [Все счета](domains/portfolio/all-accounts.md) | Общий портфель, происхождение строк и сводные разделы |
| [Операции](domains/operations/overview.md) | Исполненные операции, фильтры и пагинация |
| [Доход](domains/income/overview.md) | Полученные купоны и дивиденды, периоды и состав расчёта |
| [Будущие выплаты](domains/income/upcoming-events.md) | Календарь по текущим позициям и ограничения прогноза |
| [Акции](domains/instruments/stocks.md) и [облигации](domains/instruments/bonds.md) | Поиск выпуска, рыночные показатели и личная история |
| [Скринер акций](domains/instruments/stocks.md) и [облигации](domains/instruments/bonds.md) | Подбор акций по параметрам, поиск выпуска, рыночные показатели и личная история |
## Найти материал по задаче
@@ -72,35 +72,60 @@ WealthLedger помогает частному инвестору понимат
## Релизы
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в `develop`;
общие правила — в Engineering Workflow по ссылке из AGENTS.
PR сливается squash или rebase-merge; merge-коммит не используется. `main`
обновляется только fast-forward из `develop`, поэтому история линейна:
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
`release/hotfix-<name>` и проходит тот же процесс.
```text
<type>/<issue>-<slug> --squash--> develop --ff-only--> main --> tag vX.Y.Z
```
Инвариант: `origin/main` всегда предок `origin/develop`. Проверка:
```bash
git fetch -q origin
git merge-base --is-ancestor origin/main origin/develop && echo ok || echo BROKEN
```
Commits и PR titles — Conventional Commits со ссылкой на Issue (кроме release),
включая `spec:`. Версии вручную не повышай.
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Проверь
инвариант. Создай `release/X.Y.Z` от проверенного `develop`;
`nvm use --silent`, `npm ci`.
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
изменился, согласуй историю и повтори проверки кандидата.
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
открой PR в **`develop`** с заголовком `chore(release): X.Y.Z`. После зелёных
`verify`, `security`, `commits` выполни fast-forward-only merge. Если
`develop` изменился, обнови release-ветку и повтори проверки кандидата.
4. Открой PR `develop → main` с тем же заголовком и слей fast-forward-only.
5. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
существующий кандидат без повторного повышения версии.
Обратная синхронизация не нужна: release-коммит попал в `develop` на шаге 3.
Опубликованные теги не перемещай. При сбое после merge создай отсутствующий тег
без нового bump; до merge исправь и проверь существующий кандидат без
повторного повышения версии.
Freeze релиза не поддерживается: пока открыт релизный PR, `develop` не двигают.
Срочный fix — обычная ветка `fix/*` от `develop` с последующим релизом;
пути от `main` нет, он сделал бы `main` не-предком `develop`. Исключение
требует явного решения: `release/hotfix-<name>` от `main`, PR в `main`, тег,
затем merge `main → develop` для восстановления инварианта.
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены.
администратором, включена блокировка устаревшей ветки, обязательны `verify`,
`security`, `commits`; теги `v*` защищены. Линейность обеспечивают merge styles
репозитория: merge-коммиты и rebase `--no-ff` выключены, доступны squash,
rebase-merge и fast-forward-only; стиль по умолчанию — squash.
Workflow сам не включает эти настройки. Отдельное review approval для одного
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
@@ -2,6 +2,9 @@
**Status:** Accepted; supersedes the refresh-token reuse rule in ADR-006.
Cookie-clearing policy уточнена в [ADR 018](018-minimal-runtime-coordination.md):
неуспешный refresh больше не удаляет cookie. CAS и отказ superseded token сохранены.
## Context
Два одновременных запроса могут предъявить один действительный refresh token. Отзыв Session для проигравшего запроса делает недействительной сессию успешного запроса, а сохранение отдельной истории token или grace-window для различения replay добавляет непропорциональное состояние локальному MVP.
+72
View File
@@ -0,0 +1,72 @@
# 016. Версионируемый комплект развёртывания
**Status:** Accepted
Frontend и backend выпускаются независимо. Выбор их версий непосредственно
на сервере перекладывает подбор совместимой пары на оператора при каждом
обновлении. Принято решение выпускать комплект в репозитории `deploy`:
его релиз закрепляет проверенную пару конкретных версий обоих образов.
Независимые релизы компонентов сохраняются; выпуск компонента сам по себе
не означает готовность нового релиза развёртывания.
Оператор подключается к серверу по SSH и одной командой запускает обновление
до выбранного релиза развёртывания. При обновлении backend команда должна
скачать образы до его остановки, сохранить резервную копию остановленной базы,
выполнить миграции и проверить запуск. Ограничения
[ADR 015](015-single-server-sqlite-deployment.md)
сохраняются: один backend, краткий простой, неуспешная миграция блокирует запуск;
откат образа не восстанавливает базу автоматически.
## Доставка и подготовка сервера
На сервере хранится Git checkout `deploy`. Команда обновления получает
выбранный тег комплекта и загружает закреплённые образы из Nexus;
сборка приложения на сервере не требуется.
Docker с Compose, внешний HTTPS и доступ к registry считаются подготовленными.
README комплекта должен описывать последовательность этой разовой подготовки
и подключения существующей установки. Автоматическое provisioning сервера
в комплект не входит.
Рабочие данные, сертификат доверия T-Bank и секреты остаются вне обновляемого
checkout. При подключении существующей установки сохраняются её БД,
JWT secret и ключ шифрования. Сертификат `certs/tbank-ca.pem` монтируется
в контейнер только для чтения; путь внутри контейнера задаётся через
`TBANK_CA_CERT_PATH`. Этот сертификат не обслуживает входящий HTTPS.
## Резервные копии
Перед обновлением backend создаётся согласованная копия всего каталога данных
при остановленном backend. Копии хранятся в локальном `backups/` вне checkout,
без автоматического удаления; очистку выполняет оператор вручную.
Такие копии позволяют восстановиться после неудачного обновления, но сами
по себе не защищают от потери диска сервера.
## Обновление и сбои
Если релиз меняет только frontend, обновляется только его контейнер:
backend не останавливается, миграции и резервное копирование БД для этого
обновления не выполняются. Этот путь применим только при неизменных образе
и конфигурации backend.
Если миграции прошли, но новый backend не прошёл проверку запуска, обновление
завершается с ошибкой, неисправный backend останавливается. Оператор получает
диагностику, сведения о предыдущей версии и путь к резервной копии.
Автоматического отката нет: прежний образ может быть несовместим с новой
схемой. Откат образа и восстановление базы остаются отдельными явными действиями
по правилам ADR 015.
## Выпуск комплекта
После публикации образов компонентов отдельно готовится релиз `deploy`:
фиксируется совместимая пара версий, выполняется её проверка и выпускается
тег комплекта. Публикация компонента не создаёт релиз комплекта автоматически
и не запускает обновление сервера. Оператор выбирает выпущенный тег при
запуске команды развёртывания.
## Состояние реализации
Комплект и команда реализованы в отдельном репозитории `deploy`.
Публикация первого тега и обновление рабочего сервера — отдельные действия.
[Контейнерная поставка](../architecture/deployment.md) описывает границы,
пошаговая инструкция находится в README комплекта.
@@ -0,0 +1,146 @@
# 017. Локальное чтение данных T-Bank и фоновая синхронизация
**Status:** Accepted — историческое архитектурное направление. Решения ниже о
request-driven запуске, ручном обновлении и отсутствии периодического обхода
заменены [ADR 019](019-hourly-reads-and-online-instrument-reference.md).
Prefetch policy ниже заменена [ADR 018](018-minimal-runtime-coordination.md):
свечи загружаются по запрошенному диапазону, без предварительной загрузки
всей истории бумаг портфеля. Остальные гарантии локального хранения сохранены.
Реализация `persist-tbank-data` добавлена в рабочие ветки приложения;
performance/browser и canonical application gates пройдены. Это не отметка
развёртывания в production. Ниже сохранён контекст
принятия решения, а текущее устройство описывает
[модель свежести](../architecture/cross-cutting/data-freshness.md).
## Context
Инвестиционная история не сохраняется в БД. Серверные кэши находятся в памяти
процесса, а загрузка операций, аналитики дохода и рыночной истории зависит
от повторных обращений к T-Bank. Для журнала операций запрос доступных счетов
выполняется даже перед чтением кэшированной страницы. Сохранение только
ответов без изменения этого пути не устранит ожидание внешнего API.
Нужно ускорить все экраны, использующие T-Bank, и уменьшить повторную загрузку
данных. Текущие механизмы описаны в [политике свежести](../architecture/cross-cutting/data-freshness.md)
и [журнале операций](../domains/operations/overview.md).
## Decision
Для уже загруженных данных обычный запрос frontend обслуживается из постоянного
локального хранилища без ожидания ответа T-Bank. Обновление выполняется в фоне.
Пользователь видит время последнего успешного обновления и может запросить
обновление вручную. Сохранённый портфель может ещё не содержать новую сделку;
эта задержка принята ради быстрого открытия экрана.
Синхронизация запускается только запросами пользовательских экранов и ручным
действием «Обновить». Периодических запусков, ежедневного обновления неактивных
пользователей и плановой сверки истории нет. Фоновое выполнение означает,
что уже запрошенная работа не задерживает выдачу сохранённых данных;
самостоятельное расписание обновлений оно не подразумевает.
Обычный запрос запускает обновление, только если нужных данных нет либо
истёк согласованный срок их свежести:
| Данные | Срок свежести |
| --- | --- |
| Портфель и текущие цены | 1 минута |
| Операции текущего месяца | 5 минут |
| Список доступных счетов | 5 минут |
| Справочник инструментов и календари купонов и дивидендов | 1 сутки |
| Незавершённая дневная свеча | 1 минута |
Эти сроки проверяются при запросе и не задают расписание фоновых запусков.
Аналитика дохода зависит от полноты и свежести исходных операций.
Полностью загруженные предыдущие месяцы операций повторно читаются из T-Bank
только по кнопке «Обновить» для показанного периода. Обычный запрос догружает
отсутствующие интервалы и при необходимости обновляет текущий месяц.
Завершённые дневные свечи сохраняются; ручное обновление перечитывает
показанный диапазон. Автоматическое обнаружение поздних операций и исправлений
в уже полностью загруженных прошлых месяцах не обещается.
Кнопка «Обновить» обходит проверку давности и обновляет данные текущего
экрана: выбранного счёта, доступных счетов в режиме «Все счета» либо
инструмента и показанной личной истории на его странице. Нажатие не означает
повторную загрузку всей накопленной истории всех счетов и инструментов.
Решение охватывает все экраны с данными T-Bank. Исполненные операции и рыночная
история сохраняются; текущие позиции, остатки и цены представлены последним
успешно полученным состоянием. Будущие выплаты остаются изменяемым прогнозом,
а не исполненными операциями. Это не решение о накоплении исторических снимков
стоимости портфеля.
Рыночная история бумаг текущего портфеля загружается заранее в рамках
запрошенной пользователем работы. Для остальных бумаг первая загрузка
начинается при открытии инструмента, результат сохраняется для последующего
чтения. Предварительная загрузка истории всего каталога не требуется.
При первом подключении текущий портфель и недавние операции могут отображаться
до завершения загрузки более ранней истории. Частично загруженный период явно
отличается от полного: неполные итоги нельзя выдавать за окончательные,
а отсутствие загруженных записей — за подтверждённое отсутствие операций.
Смысл полноты определён в [глоссарии](../glossary.md#полнота-истории-счёта).
Первый запрос задаёт приоритет: сначала период, нужный экрану, включая
периоды сравнения дохода, затем вся доступная история счёта. Уже запущенная
догрузка может завершиться после ухода пользователя со страницы; нового
периодического запуска это не создаёт.
Полнота относится к успешно загруженным временным интервалам. Смена месяца
сама по себе не делает завершённым последний неполный интервал прошлого
месяца: его оставшаяся часть должна быть догружена. Ошибка промежуточной
страницы не подтверждает полноту и не удаляет ранее сохранённые данные.
Незавершённая свеча также не становится завершённой только из-за смены дня:
её окончательное состояние должен подтвердить источник.
Целевой ориентир — до 500 мс для обычного серверного запроса к заполненной БД.
Это согласованная цель измерения, а не подтверждённая характеристика системы.
Объём данных и набор запросов для проверки предстоит определить.
Для этого изменения принято допущение: ID брокерской операции не меняется
со временем. Обнаружение сменившихся ID и регулярная сверка старой истории
не входят в объём работы. Это допущение проекта, а не гарантия T-Bank:
используемый protobuf-контракт допускает изменение ID. Допущение относится
к идентичности операции; оно не требует игнорировать новые значения полей,
если операция с прежним ID снова получена от брокера.
Подключение T-Bank считается постоянным в рассматриваемом сценарии.
Архив без подключения, специальная политика хранения после отключения
и работа с закрытыми счетами пока не проектируются. Это ограничение объёма
не отменяет существующие проверки доступа и обработку ошибок интеграции.
## Alternatives
Ожидание свежего ответа T-Bank при каждом открытии экрана сохраняет зависимость
скорости и доступности интерфейса от брокера. Увеличение времени жизни кэша
в памяти уменьшает часть повторных запросов, но не сохраняет историю после
перезапуска и не обеспечивает локальное чтение всех нужных данных.
## Consequences
Интерфейсу нужны различимые состояния первичной загрузки, полноты истории,
свежести и ошибки фонового обновления. Успешная загрузка недавних операций
не подтверждает полноту более ранних периодов или периодов сравнения дохода.
Проверки локальной сессии и принадлежности личных данных сохраняются.
Способ подтверждения брокерского доступа должен позволять обслужить уже
загруженные данные без синхронного ожидания T-Bank.
Повторные запросы одного набора данных должны использовать уже выполняющуюся
загрузку. Завершённые интервалы и прогресс сохраняются в БД; после перезапуска
незавершённый интервал повторяется при следующем запросе или ручном обновлении.
Курсор брокера и максимальная дата полученной операции не доказывают полноту.
Ручное обновление периода согласует состав операций и их поля с полностью
полученным ответом источника. Только успешная загрузка всех страниц позволяет
зафиксировать полное покрытие, успешное обновление и удалить отсутствующие
в этом ответе записи периода. Ошибка не превращает частичный ответ в полный.
Личные операции и позиции сохраняют принадлежность пользователю и счёту.
Общие данные инструмента не должны содержать персональные позиции и расчёты;
поздний результат прежнего подключения не публикуется для нового подключения.
Существующие HTTP-контракты пока не поддерживают все необходимые состояния
загрузки и полноты; их изменение входит в будущую реализацию.
+45
View File
@@ -0,0 +1,45 @@
# 018. Минимальная runtime-координация
**Status:** Accepted. Уточняет cookie policy ADR 009 и prefetch policy ADR 017.
## Context
Изоляция владельцев данных, атомарные финансовые изменения и локальное чтение
не требуют отдельного протокола согласования каждого HTTP-ответа. Дополнительная
координация оправдана только наблюдаемым поведением, которое иначе нарушается.
## Decision
- Refresh сохраняет атомарную ротацию. Неуспешный refresh не меняет cookie:
поздний отказ не должен удалять cookie успешного запроса. Logout очищает cookie.
- Frontend объединяет refresh и упорядочивает cookie-writing requests только
внутри вкладки. Cross-tab auth synchronization не является требованием;
после изменения сессии в другой вкладке допускаются reload или re-login.
- Отменой, сбросом и публикацией query results владеет TanStack Query.
Смена session/connection очищает соответствующий cache; отдельные registries
HTTP-запросов, access epochs и URL-wide latest-read counters не используются.
Локальная auth boundary предотвращает применение старого auth result и
повтор mutation под другой identity.
- Status GET читает только состояние работы. Открытый экран перечитывает
resources через `sync=observe` во время работы и после завершения.
Сверка hashes между status и data GET, публичные progress counters и
невыдаваемые sync states не нужны. Полнота истории остаётся в resource metadata.
- Свечи загружаются для запрошенного диапазона. Предзагрузки всей истории
портфеля и автоматического расширения короткого периода до `all` нет.
- Backend формирует согласованные entries и aggregates из одних исходных
данных; frontend доверяет этому contract и не выполняет вторую сверку групп.
- Request context и HTTP completion принадлежат `nestjs-pino`/`pino-http`.
Проект сохраняет safe field filtering, redaction и route-template logging.
- Deployment всегда выполняет один stop → backup → migration → readiness
цикл. No-op и frontend-only оптимизации не обещаются; короткий простой принят.
## Consequences
Первое открытие нового диапазона свечей может ждать загрузку. Наблюдение активной
работы может перечитать неизменившиеся локальные данные, зато не требует
checksum protocol. Повтор deployment создаёт новый backup и может остановить API.
DB ownership подключения, conditional publication, coverage, staging,
банковские intent fingerprints и версии записей сохраняются. Отмена HTTP
не считается отменой уже выполняющейся server-side transaction.
Сохранённый полный forecast не заменяется candidate с изменившимися dependencies.
@@ -0,0 +1,40 @@
# 019. Часовой обход и онлайн-справочник инструментов
**Status:** Accepted. Заменяет request-driven, manual-refresh и
no-periodic-walk части [ADR 017](017-local-tbank-reads-and-background-sync.md)
и дополняет [ADR 018](018-minimal-runtime-coordination.md). Правило «справочные
сведения запрашиваются онлайн» дополнено исключением для сектора и бренда
актива позиции — [ADR
022](022-stored-instrument-classification.md).
## Decision
- Личные T-Bank данные текущего подключения обновляются владельцами источников
через durable hourly walk; его результат ограничен этой connection boundary.
Обычный GET и retry читают только сохранённый результат и не запускают
personal work.
- Только первое открытие неподтверждённого исторического интервала operations
и первого неоткрытого диапазона candles может получить соответственно этот
интервал или диапазон. Это не manual sync; повторное чтение не расширяет и не
запускает такую работу заново.
- Portfolio, operations, forecasts и события сохраняют собственные complete
результаты и время последнего успешного чтения. Результат ресурса
(freshness, completeness, `lastSuccessfulAt`, availability) независим от
resource-relevant последней часовой попытки: поздняя partial/failed попытка
не заменяет сохранённый complete result. При его отсутствии `data: null`.
- Detail, company, catalog search и ticker lookup получают ответ онлайн через
существующие integration clients. Их ошибки не заменяют сохранённые
финансовые данные или подписи snapshot.
- Старые справочные строки не являются источником актуальности и удаляются
только отдельной совместимой миграцией после проверки FK и свечей.
- Клиент повторяет открытые инвестиционные GET не чаще одного раза в 30 минут;
retry повторяет чтение и не создаёт screen sync work. Наблюдение последней
попытки, включая Accounts `sync`, не является командой.
## Consequences
Карточки инструментов зависят от доступности публичного upstream в момент
чтения. Финансовые экраны сохраняют последний успешный snapshot, а статус
последнего hourly walk показывается отдельно. OpenAPI backend остаётся
единственным исполняемым источником HTTP-контракта; frontend использует
опубликованный snapshot.
@@ -0,0 +1,42 @@
# 020. Evidence for hourly sync read boundaries
**Status:** Accepted. Evidence for [ADR 019](019-hourly-reads-and-online-instrument-reference.md).
## Measurements
The backend persisted-read budget was measured with `npm run test:performance`
using a synthetic SQLite dataset: three accounts, 100,000 operations per
account, 200 instruments, and 5,000 candles per instrument. The run used Node
24.21.0 on Apple M1 Pro with 32 GiB RAM and a failing synthetic upstream after
population. It measured 100 reads per route family, excluding transport
limiter cost.
The generated evidence is written by the test to its temporary
`persistent-budget.json` file. This is a repeatable measurement, not a
production SLA: it does not include network latency, real T-Bank quotas, or
multi-process SQLite contention.
The successful run produced a 617,263,104-byte SQLite database and measured
zero upstream calls during the persisted-read phase. Backend p50/p95 response
times were approximately 1.3/2.3 ms for account and portfolio reads,
3.9/4.5 ms for account operations, 6.7/7.2 ms for aggregate operations,
40.0/41.5 ms for account income analytics, and 128.1/134.2 ms for aggregate
income analytics. These figures are local synthetic measurements only.
## Durability
The migration durability check passes against a separately built repository at
the pre-history migration prefix. It verifies preservation of users, sessions,
T-Bank connection data, bank-card data, operations, categories, archive state,
and replay behavior across migration and restart.
The executed command was `node test/bank-cards-durability.cjs` with a separately
built pre-history repository. It completed with `PASS durability`.
## Limits
The configured T-Bank start limiter is `TBANK_RATE_LIMIT_PER_SECOND`, default
`5` per backend instance. The hourly scheduler therefore remains a single
durable reservation owner; the measurement does not justify increasing this
limit or changing the hourly cadence. Any production tuning requires current
T-Bank documentation and a new measurement with the deployed topology.
@@ -0,0 +1,66 @@
# 021. Тихое чтение и редкие подтверждения
**Status:** Accepted. Заменяет manual-refresh часть
[ADR 019](019-hourly-reads-and-online-instrument-reference.md) на стороне
интерфейса и уточняет клиентскую часть
[ADR 007](007-react-query-fetch-policy.md). Не меняет владельцев обновления
и границы чтения из ADR 019.
## Context
Экраны просмотра перекладывали на пользователя контроль над получением данных:
кнопки «Обновить» и «Повторить», индикатор фонового обновления и сообщение
о неудачной синхронизации повторялись в разных разделах. Пользователь приходит
смотреть финансы, а не обслуживать загрузку; восстановление после временного
сбоя не должно зависеть от нажатия кнопки.
При этом состояние самих данных остаётся значимым: время последнего успешного
результата источника, устаревание и исход часовой попытки влияют на доверие
к показанным суммам и не могут быть просто скрыты.
## Decision
- Автоматическое чтение клиента не сообщает о себе. Экраны просмотра не
показывают кнопок ручного обновления и повтора, индикаторов фонового
перечитывания и технических предупреждений о собственном запросе.
- Временные сбои GET восстанавливаются сами: ограниченный срок ожидания запроса
и не более двух повторов с возрастающими паузами, затем возврат к штатной
работе при появлении результата. Пользователь не инициирует восстановление.
- Состояние источника из `meta` остаётся видимым. Время последнего успешного
результата, признак устаревания, исход последней часовой попытки и неполнота
показываются текстом либо доступно именованным элементом, а не только цветом.
Это свойство данных, а не отчёт о техническом процессе, и оно не превращается
в действие пользователя.
- Доступный результат сохраняется при обновлении и временной ошибке. Неполнота
и подтверждённая пустота остаются различимыми: неизвестная сумма не становится
нулём, незавершённый расчёт не выдаётся за окончательный, незагруженный список
не выдаётся за отсутствие записей.
- Первичное ожидание ограничено по времени и показывает структуру будущего
содержимого. По его истечении экран переходит в спокойную локальную
недоступность вместо бесконечной загрузки и сам заменяет её содержимым после
восстановления. Сбой одного блока не скрывает остальные доступные блоки.
- Ошибки явных действий пользователя сохраняются и остаются видимыми. Решение
касается только чтения; отправка формы, изменение данных и сброс фильтра
по-прежнему сообщают результат, в том числе неуспешный.
## Consequences
Пользователь не может ускорить обновление вручную: нормальным владельцем
обновления остаётся часовой обход из ADR 019, и отсутствие кнопки не меняет его
срок. Если источник недоступен долго, экран показывает прежние данные с
обозначением устаревания, а не приглашение повторить.
Автоматический повтор ограничен: он покрывает короткие сетевые сбои, но не
длительную недоступность. Ошибки доступа, домена и отмены не повторяются,
mutations не повторяются вовсе.
Состояние источника и состояние клиентского чтения описывают разные вещи и
не подменяют друг друга. `Portfolio.isStale` сохраняет смысл возраста снимка;
ошибка клиентского перечитывания его не меняет.
Детали поведения — в
[ожидании и восстановлении интерфейса](../architecture/cross-cutting/ui-resilience.md)
и [свежести данных](../architecture/cross-cutting/data-freshness.md).
Согласованные требования принадлежат workspace-спецификациям
`frontend-user-feedback`, `frontend-data-resilience` и
`frontend-loading-presentation`.
@@ -0,0 +1,56 @@
# 022. Хранимая классификация инструментов T-Bank
**Status:** Accepted. Дополняет [ADR
019](019-hourly-reads-and-online-instrument-reference.md) исключением из
правила «справочные сведения запрашиваются онлайн». Решение утверждено
владельцем в [Issue
WealthLedger/main#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48).
## Context
ADR 019 установил, что справочные сведения инструментов (detail, lookup,
каталоги) запрашиваются онлайн и не сохраняются как долговременный справочник:
«старые справочные строки не являются источником актуальности». Вкладка
«Концентрация» требует для каждой позиции её сектора и эмитента — бренда
актива по данным T-Bank. Сектор есть только в описаниях отдельных типов
инструментов (`share`, `bond`, `etf`), а эмитент — в описании актива по
`asset_uid` инструмента; из названия, тикера, ISIN или `logoName` его вывести
нельзя. Запрашивать эти поля онлайн при каждом открытии экрана нельзя:
ответ Portfolio не ждёт T-Bank, а разрезы должны считаться по уже сохранённому
снимку.
Существующий metadata-кэш (`TbankInstrument`) не подходит: общий ответ
`GetInstrumentBy` не содержит `sector`, а его `brand` — данные логотипа,
а не `{uid, name}` бренда актива.
## Decision
- Сектор и бренд актива позиции хранятся как последнее известное значение в
глобальной таблице `TbankInstrumentClassification` (провайдер, ключ
инструмента, сектор и время его получения, uid актива и uid/название бренда
с временем получения). Данные публичные, без привязки к пользователю или
подключению; ограждение по connection revision к ним не применяется, публикация
Snapshot ограждается как прежде.
- Каждое поле обновляется в часовом обходе при его первой попытке, начавшейся
не ранее чем через 24 часа после последнего успешного получения этого поля;
пока значение не получено, попытка повторяется в каждом обходе. Сектор и
бренд обновляются независимо.
- Неуспешное получение не мешает публикации Portfolio Snapshot и не влияет на
его свежесть; сохранённое значение остаётся доступным. Отказ token
обрабатывается как отказ token при обходе Portfolio. Ответ «инструмент не
найден» сохраняет прежнее значение и перезапускает отсчёт 24 часов.
- Классификация не входит в `meta.dependencies` Portfolio и не меняет
`isStale` и `lastSuccessfulAt`.
## Consequences
- Это первый долговременный справочник T-Bank в системе: ADR 019 теряет
характер «только онлайн» для этой области и дополняется, а не заменяется.
- Разрезы концентрации считаются по снимку без обращений к T-Bank; число
вызовов растёт ограниченно — не более одной попытки на поле в сутки на
позицию, при первом обходе после выката возможен разовый всплеск, сглаженный
общим лимитером и очередью.
- Значение может быть устаревшим до 24 часов; это приемлемо для группировки
позиций, но классификация не является источником актуальных данных брокера.
- Второй потребитель хранимой классификации — условие пересмотра решения
(выделение независимого процесса обновления, отдельные сроки).
+7
View File
@@ -20,3 +20,10 @@
- [013 — НКР как первый источник кредитных рейтингов облигаций](013-nkr-bond-credit-ratings.md)
- [014 — Актуальная OpenAPI-схема как вход frontend](014-trusted-openapi-input.md)
- [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md)
- [016 — Версионируемый комплект развёртывания](016-versioned-deployment-bundle.md)
- [017 — Локальное чтение данных T-Bank и фоновая синхронизация](017-local-tbank-reads-and-background-sync.md)
- [018 — Минимальная runtime-координация](018-minimal-runtime-coordination.md)
- [019 — Часовой обход и онлайн-справочник инструментов](019-hourly-reads-and-online-instrument-reference.md)
- [020 — Evidence for hourly sync read boundaries](020-hourly-sync-verification-evidence.md)
- [021 — Тихое чтение и редкие подтверждения](021-quiet-reading-and-rare-confirmations.md)
- [022 — Хранимая классификация инструментов T-Bank](022-stored-instrument-classification.md)
+44 -26
View File
@@ -76,8 +76,8 @@ prerequisites: локальный smoke не подтверждает registry/s
| Часть | Ответственность |
| --- | --- |
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений и ручной банковской истории |
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и синхронизация сохранённых источников |
| SQLite | Долговременное хранение локальных пользователей, сессий, зашифрованных подключений, ручной банковской истории и инвестиционных источников с coverage/work |
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
Frontend организован слоями `app → pages → features → entities → shared`:
@@ -86,6 +86,20 @@ Frontend организован слоями `app → pages → features → ent
где локальная аутентификация отделена от подключения брокера и инвестиционных
сценариев. Это внутренняя организация приложений, не набор микросервисов.
Маршруты и AppShell принадлежат `app`. Page controller/VM управляет запросами,
формой и восстановлением; presentation получает данные и callbacks, поля
могут использовать контекст React Hook Form. В OperationForm controller
сохраняет intent операции, черновик и явное принятие актуальной версии после
конфликта. В account-income чистые преобразования периода принадлежат модели
страницы и не связывают форму с её запросами.
Локальные packages tokens/ui/icons образуют [визуальную основу](../design/overview.md#общая-основа-и-каталог)
внутри frontend и входят в сборку SPA. Это библиотеки, а не дополнительные
runtime-контейнеры. Провайдеры темы принадлежат приложению и Storybook;
предметные компоненты и API-клиент остаются в приложении. Отдельный API-пакет
и registry-поставка визуальных пакетов не реализованы. Существующие Docker
images приложения не означают независимого npm-выпуска этих библиотек.
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
T-Bank в форме и отправляет его для сохранения, но API не возвращает token
в ответах; SPA не использует его для прямых брокерских запросов. Прямая
@@ -120,28 +134,32 @@ credentials не являются сохраняемым представлен
или перезапуска. Архив обратим и сохраняет историю. Подробности —
[банковские карты](../domains/cards/overview.md).
Сессии и подключение связаны с владельцем. Access token подтверждает
локального пользователя, но `accountId` из URL сам по себе не даёт права
читать брокерский счёт. Backend получает текущее подключение пользователя
и его generation перед использованием финансового кэша. Проверка доступности
счёта у брокера зависит от сценария: журнал выполняет её до чтения страницы
кэша; свежий портфель одного счёта может быть возвращён без нового запроса
списка счетов. При обновлении портфеля доступ проверяется у брокера.
Сессии и подключение связаны с владельцем. Backend локально проверяет
membership текущей revision перед каждым финансовым чтением. Unknown membership
не открывает доступ к произвольному счёту; известный отказ токена блокирует
личные данные. Ротация refresh token остаётся атомарной.
Ротация сессии атомарно заменяет hash текущего refresh token: два
конкурентных запроса не могут оба успешно выполнить одну ротацию.
Вызов брокера и запись в локальную базу не образуют общую транзакцию:
при сохранении подключения сначала проверяется token, затем записывается
шифротекст. Подробности находятся в
[аутентификации](../domains/authentication/overview.md) и
[интеграции T-Bank](../integrations/tbank.md).
SQLite хранит полный последний portfolio, операции и их подтверждённые
диапазоны, private прогнозы, сохранённые точки истории оценки, общие
instrument/market sources, классификацию инструментов (сектор и бренд актива
как последнее известное значение с временем каждого получения,
`TbankInstrumentClassification`) и intent/staging.
Нормализованные финансовые значения сохраняются decimal-строками. Полнота
отделена от свежести и пагинации. Ответ не ждёт внешних RPC; обновлением
личных данных владеет часовой обход в том же backend process через
существующий лимитер, недостающие общие источники он же ставит в фоновую
очередь. На startup обход начинает следующую попытку по обычному часовому
сроку; после нового подключения первая попытка начинается сразу.
Классификация — публичные данные без привязки к пользователю и подключению;
её поля обновляются в том же обходе не чаще раза в сутки
([ADR 022](../adr/022-stored-instrument-classification.md)).
Подробности — [модель свежести](cross-cutting/data-freshness.md).
Брокерские счета, сводки, портфель, страницы журнала и будущие события
не являются долговременной историей в SQLite. Они получаются для текущего
подключения и могут временно кэшироваться. Перезапуск backend не
восстанавливает прежние финансовые снимки и cursor continuations из базы;
[модель свежести](cross-cutting/data-freshness.md) отделяет это от хранения
учётных данных.
PUT подключения сначала проверяет token у T-Bank, затем одной транзакцией
сохраняет encrypted token, новую revision и проверенный membership.
Сетевая операция не удерживает SQLite-транзакцию. Общие источники не содержат
личных позиций, ожидаемых сумм или credentials. Произвольного архива
отключённых счетов нет.
## Сохранение и восстановление данных
@@ -185,9 +203,8 @@ access token и вращаемый refresh token. [Политика query-кэш
Авторизация доступа, недопущение token в публичный ответ и атомарность
ротации сессии — отдельные свойства; наличие типизированного клиента
не доказывает их. Известные расхождения регистрации и деталей валидации
отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход)
и [HTTP API](http-api.md#error-contract).
не доказывает их. Действующий формат ошибок описан в
[HTTP API](http-api.md#error-contract).
Поддерживается локальный Compose и ограниченная single-server topology с SQLite.
Origin браузера, адрес API и cookie policy должны быть согласованы.
@@ -196,7 +213,8 @@ Origin браузера, адрес API и cookie policy должны быть
режимах refresh-cookie имеет Secure flag и требует защищённого соединения.
Single-server deployment не задаёт гарантий HA или zero-downtime.
Кэши и лимитер T-Bank действуют на экземпляр backend. Ротация ключа шифрования
Coordinator и лимитер T-Bank действуют на один экземпляр backend. Сохранённые
данные переживают перезапуск; processRunId не является distributed lease. Ротация ключа шифрования
подключений и согласование изменений подключения между распределёнными
экземплярами не входят в реализованный механизм. Эти ограничения не
позволяют переносить локальную схему на несколько экземпляров без отдельного
+85 -59
View File
@@ -1,72 +1,98 @@
# Свежесть данных и повторное использование
WealthLedger показывает финансовое наблюдение на момент получения данных.
Временный кэш уменьшает повторные обращения к брокеру, но не превращает
снимки в историю портфеля и не обещает рыночные данные в реальном времени.
WealthLedger читает личные инвестиционные данные из SQLite. Обычный GET и его
HTTP retry возвращают сохранённый результат и не запускают личную работу с
T-Bank. Нормальным владельцем обновления служит часовой обход текущего
подключения: после успешного подключения он начинает первую попытку, затем
начинает следующую не ранее чем через 60 минут от начала предыдущей. После
простоя выполняется не более одна актуальная попытка, а перекрывающийся срок
пропускается, не ставится в очередь. Общие справочные данные инструментов
запрашиваются онлайн, когда это требуется карточке; финансовый HTTP-ответ не
ждёт T-Bank. Единственное исключение — сектор и бренд актива позиции, которые
хранятся как последнее известное значение с обновлением не чаще раза в сутки;
свежесть и полнота Portfolio от них не зависят. Модель принята в
[ADR 019](../../adr/019-hourly-reads-and-online-instrument-reference.md),
исключение — в
[ADR 022](../../adr/022-stored-instrument-classification.md).
## Клиентский и серверный снимки
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — последний
успешный ответ, сохранённый TanStack Query в SPA. При обычном фоновом обновлении
того же набора данных уже показанный результат остаётся на экране; ошибка
обновления сообщает о проблеме, не заменяя известные значения пустотой.
Клиентский кэш не является внешней системой или отдельным runtime-приложением.
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — успешный
ответ в TanStack Query. Он сохраняет показанное содержимое при временной ошибке
HTTP-перечитывания в той же границе доступа. Это не серверный fallback и не
состояние часового обхода. SQLite независимо от браузера хранит membership,
последний полный portfolio, исполненные операции, прогнозы, общие сведения об
инструментах и классификацию инструментов (сектор и бренд актива как
последнее известное значение, [ADR
022](../../adr/022-stored-instrument-classification.md)), котировки, свечи и
календари.
[Portfolio Snapshot](../../glossary.md#portfolio-snapshot) — полный результат
загрузки портфеля на backend. В пределах разрешённого сценарием срока он может
служить серверным fallback при временной ошибке upstream. Backend отмечает
такой ответ `isStale`; frontend не выводит это поле из состояния собственного
query-кэша. Серверное предупреждение и ошибка клиентского refetch могут
возникать независимо.
[Полнота периода](../../glossary.md#полнота-истории-счёта) и свежесть различаются.
Успешно проверенный пустой результат полон и доступен; последняя страница
журнала сама по себе полноту не доказывает. Результат каждого личного ресурса
в `{ data, meta }` описывают пять независимых значений: freshness,
completeness, `lastSuccessfulAt`, `lastAttempt` и availability. Первые,
полнота и `lastSuccessfulAt` относятся только к возвращённому подтверждённому
результату. `lastAttempt` описывает относящуюся к ресурсу часовую попытку:
`none`, `running`, `succeeded`, `partial` или `failed`.
Свежесть и допустимость fallback определяются сценарием. Портфельный fallback
не распространяется автоматически на журнал, прогноз событий или страницу
инструмента. Точные правила находятся в
[справочнике портфеля](../../domains/portfolio/data-reference.md),
[журнале операций](../../domains/operations/overview.md) и
[будущих событиях](../../domains/income/upcoming-events.md).
Итог попытки выводится из фактических результатов её шагов: попытка, не
завершившая обязательный шаг, не обозначается полным успехом, а полученные
успешные результаты сохраняются. Ожидание каждого шага ограничено; зависший
шаг учитывается проблемным, а начатое обращение к T-Bank не отменяется.
Попытка, потерявшая выполнение после сбоя сохранения или остановки процесса,
закрывается частичным успехом либо неуспехом и не блокирует следующий обход
дольше обычного часового срока.
По [ADR 007](../../adr/007-react-query-fetch-policy.md) запрос связан с открытием
экрана, а возврат фокуса окна не запускает refetch. Повторное открытие после
`staleTime` может обновить ответ в фоне. Эта политика не является таймером
периодического обновления. Перезапуск backend уничтожает его in-memory
снимки и continuations журнала; SQLite не восстанавливает их.
Поздняя неуспешная или частичная попытка сохраняет прежний полный результат и
его исходное время. Если полного результата нет, `data` равно `null`: состояние
availability объясняет первоначальную загрузку, временную недоступность либо
недоступность подключения. Пустой массив не означает отсутствие результата.
Старый полный portfolio не удаляется из-за возраста. `Portfolio.isStale`
сохраняет смысл возраста его snapshot, а не обозначает неуспех обхода или
клиентскую ошибку перечитывания.
| Источник | Владелец нормального обновления |
| --- | --- |
| Membership, portfolio, изменяемый хвост операций и события | Часовой обход текущего подключения |
| Котировки и изменяемый хвост дневных свечей | Установленная политика источника; не ordinary personal GET |
| Metadata, поиск каталога и company | Онлайн при чтении |
| Полные прошлые интервалы операций и завершённые свечи | Не обновляются часовым обходом или обычным GET |
Два исключения разрешают только первое получение отсутствующего результата:
первое открытие неподтверждённого исторического интервала операций получает
только этот интервал и возвращает локальные строки с неполным покрытием; первое
открытие диапазона свечей без подтверждённого локального результата получает
только этот диапазон. Ни одно исключение не обновляет membership, portfolio,
события, другой интервал или диапазон. Их HTTP retry также только перечитывает
локальное состояние. Подтверждённый полный интервал или диапазон остаётся
локальным.
Часовой обход сохраняет успешные разделы независимо, а failed section не
заменяет прежний complete result. Отменой query lifecycle владеет TanStack
Query. Открытый экран перечитывает ресурс не чаще одного раза в 30 минут;
это локальный HTTP read. Совместимый параметр `sync=observe`, пока он есть в
маршруте, также является только чтением и не представляет команду обходу.
[HTTP API](../http-api.md) описывает исполняемый контракт envelope.
## Смена подключения
Кэш финансовых данных относится к пользователю, счёту и текущему
подключению T-Bank. Backend сначала читает подключение и его generation, затем
проверяет возможность повторного использования ответа. Свежий портфель
одного счёта не требует нового запроса списка счетов у брокера; при обновлении
портфеля доступ проверяется заново. Журнал операций проверяет доступ до чтения
кэшированной страницы. Идентификатор счёта и совпадение параметров запроса
сами по себе недостаточны для выдачи кэша.
Каждое чтение проверяет локальную сессию, текущее подключение, известный отказ
токена и сохранённый состав доступных счетов. Расшифровка token и GetAccounts
для локальной выдачи не нужны. Старый membership допускает выдачу и фоновую
сверку; это задержка обнаружения отзыва доступа, а не live-проверка брокера.
Unknown membership не даёт доступа к произвольному accountId: ответ сообщает
о загрузке доступа. Успешный новый список немедленно ограничивает последующие
чтения. Известный invalid token блокирует личные данные.
При каждом успешном сохранении подключения token шифруется с новым
AES-GCM nonce. Из сохранённого шифротекста backend вычисляет непрозрачную
внутреннюю generation. Она не хранится отдельным полем базы и не входит
в API, журналы или ошибки. Portfolio Snapshot и Ledger Page Cache допускают
повторное использование только в пределах соответствующей generation.
Старый кэш или поздно завершившийся запрос прежней generation не обслуживает
новый запрос после замены подключения, включая сохранение того же plaintext
token. Удаление подключения не оставляет доступного контекста для чтения кэша.
Успешный PUT атомарно сохраняет token, новую случайную внутреннюю revision
и membership из проверки token. Прежние личные datasets и work становятся
недоступны. DELETE удаляет подключение и личные данные; общие неперсональные
источники могут оставаться. Неуспешный PUT сохраняет действующую границу.
После успешного изменения подключения frontend сначала записывает
подтверждённый status, затем сбрасывает generated GET queries в поддеревьях
`/api/v1/brokerage-accounts`, `/api/v1/stocks` и `/api/v1/bonds`. Активные
выборки запрашиваются заново, неактивные — при следующем использовании.
Client Cached Snapshot прежнего подключения больше не служит результатом
новой границы.
Неуспешная операция подключения не меняет статус, generation и доступные
финансовые данные. Обычная ошибка refetch также сохраняет клиентский снимок
текущей границы. Logout — отдельное событие: оно очищает состояние
пользовательской сессии, status подключения и query cache.
Пользовательский сценарий описан в [интеграции T-Bank](../../integrations/tbank.md).
Согласованные правила frontend находятся в workspace-спецификациях
[query coherence](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/frontend-query-coherence/spec.md)
и [data resilience](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/frontend-data-resilience/spec.md);
серверные правила — в
[portfolio](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md) и
[ledger](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
Публикация проверяет revision и attempt в той же SQLite-транзакции; поздняя
работа старого подключения не меняет даже timestamp. Revision не попадает
в API, логи или cursor. Frontend очищает personal queries при смене подключения
и известном отказе доступа; поздний ответ не восстанавливает скрытые данные.
Logout очищает сессию и query cache. Подробнее —
[интеграция T-Bank](../../integrations/tbank.md).
+36 -8
View File
@@ -16,20 +16,48 @@
пока неизвестно, доступен public или protected маршрут. При загрузке
отложенного маршрута уже доступный каркас остаётся видимым.
Фоновое обновление и действие пользователя сохраняют видимые данные или
элемент управления с компактной индикацией процесса. В частности,
Фоновое чтение не сообщает о себе: оно сохраняет видимые данные без индикации
процесса и без технического предупреждения. В частности,
[клиентский снимок](data-freshness.md#клиентский-и-серверный-снимки) счетов или
портфеля остаётся доступным после ошибки refetch; сообщение и повтор
относятся к обновлению, а не к отсутствию инвестиций.
портфеля остаётся доступным после ошибки refetch, и эта ошибка не означает
отсутствие инвестиций. Действие пользователя сохраняет видимые данные или
элемент управления и сообщает свой результат, включая неуспешный.
Первичное ожидание ограничено по времени. По его истечении область переходит
в спокойную локальную недоступность вместо бесконечной загрузки и сама
заменяет её содержимым после восстановления. Решение принято в
[ADR 021](../../adr/021-quiet-reading-and-rare-confirmations.md).
Серверная полнота не выводится из query lifecycle. Missing показывает
skeleton только отсутствующих секций, partial — подтверждённые строки и
сообщение о неполноте. Totals ждут обязательных зависимостей, comparisons
готовятся независимо. Только complete empty означает отсутствие операций.
Время источника не заменяется временем получения HTTP-ответа.
Экраны просмотра не предлагают ручное обновление или повтор чтения: временные
сбои GET восстанавливаются сами ограниченным числом повторов, сохраняя
показанный результат и applied filters. Ошибка transport отличается от
ошибки source work. Status observation прекращается после terminal outcome
или ухода; само истечение TTL не запускает импорт. Известный access loss
очищает personal view, включая защиту от позднего ответа.
Состояние источника при этом видимо: время последнего успешного результата,
устаревание, исход последней часовой попытки и неполнота показаны текстом либо
доступно именованным элементом рядом с данными, к которым относятся. Это
свойство данных, а не отчёт о техническом процессе, и оно не требует действия.
## Известная ошибка сценария
Ошибки счетов, портфеля и подключения T-Bank остаются локальными: рядом
с затронутыми данными доступны предусмотренные сценарием retry и navigation
actions. Устаревший серверный Portfolio Snapshot показывает сохранённый
портфель с предупреждением. Он не превращается в глобальный fallback.
Ошибки счетов, портфеля и подключения T-Bank остаются локальными: затронутый
блок показывает собственное состояние и не скрывает успешно отображённые
соседние блоки. Устаревший серверный Portfolio Snapshot показывает сохранённый
портфель с обозначением устаревания. Он не превращается в глобальный fallback.
Ошибка источника не означает нулевой баланс или отсутствие операций.
Предусмотренные сценарием navigation actions сохраняются: подтверждённая потеря
доступа предлагает восстановить подключение, а `404` — вернуться к списку
счетов. Это навигация и явные действия пользователя, а не повтор чтения.
## Непойманная ошибка маршрута
Непойманная runtime-ошибка при отображении public или protected route
+105 -148
View File
@@ -1,30 +1,41 @@
# Контейнерная поставка
Runbook для одного Docker host, одного backend и краткого простоя.
Поставка для одного Docker host, одного backend и краткого простоя.
[Топология](containers.md#контейнерная-поставка) и
[ADR 015](../adr/015-single-server-sqlite-deployment.md) объясняют границы.
Команды выполняются из checkout `WealthLedger/main`, содержащего `compose.yaml`.
Development дополнительно требует checkout `frontend/` и `backend/`.
Production-комплект находится в отдельном [репозитории deploy](https://git.personal.ksv741.ru/WealthLedger/deploy).
Его README — единственная пошаговая инструкция установки, обновления и recovery;
[ADR 016](../adr/016-versioned-deployment-bundle.md) объясняет выбор релиза комплекта.
Development-команды ниже выполняются из workspace с checkout `frontend/` и `backend/`.
## Предпосылки
- Docker Engine с Compose plugin **не ниже 2.24.4** (`!override` в development),
доступ к registry и достаточно свободного диска для non-root UID 1000.
Локально проверены Engine 29.8.0, Compose 5.5.1, Linux arm64.
Опубликованные images собраны для Linux amd64 (см. README комплекта).
Другую архитектуру проверяют отдельно; multi-platform images не обещаны.
- Gitea Container Registry; проверяемая версия сервера — 1.27.3.
- Nexus Docker hosted repository `docker-hosted`, доступный через
`nexus.personal.ksv741.ru/docker-hosted/`. Docker Bearer Token Realm включён;
reverse proxy передаёт исходные host и HTTPS scheme. Адрес token realm
в `WWW-Authenticate` также должен начинаться с `https://`.
Deployment policy запрещает redeploy существующих release tags.
Runner label — `ubuntu-latest`, runner должен иметь Docker CLI, buildx и daemon.
Наличие label само по себе не доказывает Docker capability.
- В Actions каждого component repo нужны `REGISTRY_USERNAME` и
`REGISTRY_TOKEN` отдельной identity с package write permission. Server
использует другую identity только с package read permission. Built-in
workflow token не заменяет эту настройку.
- В Actions каждого component repo нужны `NEXUS_USERNAME` и
`NEXUS_PASSWORD` пользователя `wealthledger-ci`. Эта же учётная запись
используется сервером для загрузки образов. Для публикации нужны
browse/read/add/edit на `docker-hosted`; фактически выданный набор прав
настраивается в Nexus. Built-in
workflow token не заменяет эту настройку. Отдельной read-only учётной записи
сервера нет: `wealthledger-ci` работает и как CI writer, и как server reader,
поэтому её пароль на сервере даёт право публикации в `docker-hosted`. Это
принятое владельцем решение; разделение ролей — возможное усиление.
- На сервере Caddy уже завершает TLS и передаёт весь домен на
`127.0.0.1:8080` (или выбранный frontend port). Отдельного `/api` upstream нет.
DNS, TLS, firewall и provisioning Caddy выполняются вне этого runbook.
Реальная публикация и server pull-only login требуют отдельной проверки
на вашей инфраструктуре. Локальные images не доказывают registry permissions.
Серверный login и доступ к images проверяются на целевом сервере.
Локальный smoke не доказывает серверные registry permissions.
## Development
@@ -63,7 +74,7 @@ Project `wealthledger-dev` хранит SQLite в `development-data`. Обычн
version, затем выполняет `npm ci`, `npm run verify`; backend дополнительно
выполняет `npm run test:e2e`. У каждого image OCI version/revision labels.
Images публикуются как `git.personal.ksv741.ru/wealthledger/frontend:X.Y.Z`
Images публикуются как `nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:X.Y.Z`
и `.../backend:X.Y.Z`. `latest` не создаётся.
Workflow отказывается перезаписывать существующие release tags;
защита release tags и запрет параллельных publishers обязательны. Если одна
@@ -72,156 +83,102 @@ Workflow отказывается перезаписывать существу
Выберите опубликованные version tags, выполните pull и сравните OCI revision
с release commit. Выполните smoke с disposable data/config до первого server
rollout. В production env хранится пара полных image references, например
`git.personal.ksv741.ru/wealthledger/frontend:0.1.0`. Сохраните versions/revisions
rollout. В `release.env` комплекта хранится пара полных image references, например
`nexus.personal.ksv741.ru/docker-hosted/wealthledger/frontend:1.1.0` и
`.../backend:2.1.0`. Сохраните versions/revisions
в deployment record. Release tags не должны переназначаться другим images.
## Перенос существующих images из Gitea
До переключения CI и server references проверьте HTTPS token realm и настройте
пользователя `wealthledger-ci`. В Actions добавьте `NEXUS_USERNAME`/`NEXUS_PASSWORD`; прежние
`REGISTRY_USERNAME`/`REGISTRY_TOKEN` сохраните для отката старого workflow.
Пароли вводятся интерактивно или через `--password-stdin`, не через аргументы.
Прежние images лежат в [Gitea packages](https://git.personal.ksv741.ru/WealthLedger/-/packages)
(`git.personal.ksv741.ru/wealthledger/{frontend,backend}`); новые публикуются
только в Nexus. Скопируйте каждый опубликованный release без пересборки. Например, для `0.2.0`
с установленным Skopeo и доступом чтения Gitea/записи Nexus:
```bash
skopeo login git.personal.ksv741.ru
skopeo login nexus.personal.ksv741.ru
for component in frontend backend; do
skopeo copy --all --preserve-digests \
"docker://git.personal.ksv741.ru/wealthledger/$component:0.2.0" \
"docker://nexus.personal.ksv741.ru/docker-hosted/wealthledger/$component:0.2.0" || exit 1
done
```
`--all` переносит весь image index, включая платформы и вложенные attestation
manifests; `--preserve-digests` прерывает перенос при изменении digest.
До копирования проверьте наличие destination tag: если он уже существует,
сравните исходный и целевой index digest и вложенные manifests. Совпадающий
artifact повторно не публикуйте; несовпадение требует расследования, не удаления
или перезаписи tag. Частичный перенос не считается завершённым.
После копирования сравните digests через `skopeo inspect --raw` и SHA-256,
выполните `docker pull` обоих images пользователем `wealthledger-ci` на сервере приложения,
проверьте OCI revision и container smoke на disposable data.
Серверные шаги (login, pull, smoke) в репозитории не подтверждены и
выполняются оператором. Только после них меняйте
production image references и запускайте обычную
последовательность обновления ниже. Старые Gitea packages и references сохраните:
откат registry возвращает прежние адреса тех же artifacts, не восстанавливает
SQLite и не меняет encryption keys. Не переносите Git release tags и не
запускайте заново старый tag workflow для копирования существующего image.
## Первая установка
Создайте restricted environment file и data directory:
На подготовленном сервере оператор клонирует `deploy`, настраивает внешний
HTTPS и registry login, размещает server env, сертификат T-Bank и data directory
вне checkout. Для новой пустой базы команда вызывается с `--init`.
Для действующей установки сохраняются прежние данные и ключи, а первый переход
выполняется как полное обновление с backup. Отсутствующая БД без `--init` — ошибка.
Точные команды и разрешения каталогов приведены в README комплекта.
```bash
sudo install -d -m 0700 /srv/wealthledger
sudo install -d -m 0700 -o 1000 -g 1000 /srv/wealthledger/data
sudo install -m 0600 .env.compose.example /srv/wealthledger/production.env
```
Дальнейшие команды выполняет оператор, имеющий доступ к environment file,
backup directory и Docker. Заполните env локальным защищённым редактором:
полные ссылки `FRONTEND_IMAGE` и `BACKEND_IMAGE` с опубликованными version tags,
публичный HTTPS origin и абсолютный data path. Версии в примере — образец
формата; перед запуском выберите реально опубликованные releases.
Пустые required значения блокируют config; некорректные secrets блокируют backend.
Не выводите полный rendered config с настоящими secrets в журналы.
Для новой базы создайте JWT secret длиной хотя бы 32 символа и отдельный
32-byte encryption key в base64 (`openssl rand -hex 32` и
`openssl rand -base64 32` в защищённом терминале). Сохраните ключ отдельно
от data backup. Для существующей установки перенесите прежние JWT secret
и encryption key, а согласованную копию **всего** data directory — при
остановленном прежнем backend. Файл должен называться `wealthledger.db`;
если переименовываете, согласуйте также WAL/SHM companion names до старта.
Не запускайте старый и новый backend на одной базе одновременно.
Войдите в registry server identity без помещения token в командную строку:
```bash
docker login git.personal.ksv741.ru --username SERVER_PULL_USER
export COMPOSE_FILE=compose.yaml
export COMPOSE_PROJECT_NAME=wealthledger
export COMPOSE_ENV_FILES=/srv/wealthledger/production.env
docker compose config --quiet
docker compose pull frontend backend migration
docker compose up -d --wait --wait-timeout 120
docker compose ps -a
```
Migration и backend используют один image и один data mount.
Migration должна завершиться с кодом 0; иначе backend не запускается.
Readiness принимает 401 от существующего `/api/v1/auth/me` без credentials.
Frontend становится healthy после backend. Backend host port отсутствует.
Runtime non-root; root filesystem read-only, nginx temp files находятся в tmpfs.
Проверьте SPA deep link, регистрацию/вход, refresh, существующие карты,
чтение старого зашифрованного подключения и один финансовый запрос.
Production refresh-cookie имеет Secure flag: проверяйте через HTTPS Caddy;
loopback HTTP curl без browser cookie jar не доказывает browser session.
При локальном smoke используйте disposable credentials и явную передачу cookie.
Направляйте Caddy на новый deployment только после успешной проверки.
Прежний runtime удаляйте только после проверки новой установки.
Git-тег `deploy` закрепляет пару images через `release.env`. Версия комплекта
не обязана совпадать с версиями компонентов. Публикация image не выпускает
комплект и не обновляет сервер автоматически. Команда получает выбранный тег,
загружает images до простоя и использует весь snapshot этого commit.
## Backup при остановленном backend
До backend update проверьте совместимость новой версии с текущим frontend
и схемой, а обратной версии — с новой схемой. При неизвестной совместимости
не обещайте image rollback. В выбранном rollout отсутствуют параллельные API.
Сначала останавливаются писатели, затем копируется весь каталог данных,
включая возможные WAL/SHM. Только успешная копия допускает миграции. Локальные
backups находятся рядом с data directory, вне checkout, и автоматически
не удаляются. Незавершённая копия помечается `.partial-` и не подходит для restore.
Ключ шифрования хранится отдельно. Локальная копия не защищает от потери диска.
Ниже data path соответствует примеру env; при другом path замените его.
Сохраните текущую пару image references/revisions и отдельно — ключи.
## Единый цикл обновления
```bash
docker compose stop backend
docker compose ps -a backend
install -d -m 0700 /srv/wealthledger/backups
BACKUP_DIRECTORY="/srv/wealthledger/backups/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -m 0700 "$BACKUP_DIRECTORY"
cp -a /srv/wealthledger/data "$BACKUP_DIRECTORY/data"
```
Каждое обновление, включая повтор того же тега и изменение только frontend,
выполняет stop → backup → migration → readiness. Сравнение hashes контейнеров
и конфигурации не выполняется. Приняты короткий простой API и дополнительный
backup; во время простоя возможен 502, вкладкам может потребоваться reload.
Ошибка migration блокирует старт, ошибка готовности останавливает новый backend;
ошибка frontend не останавливает исправный backend. Успех фиксируется только
после проверок запуска обоих компонентов.
Перед копированием убедитесь, что backend остановлен, migration не выполняется
и никто другой не пишет в каталог. Успех `cp` обязателен перед продолжением.
Не копируйте отдельный `.db` из работающего приложения. Проверяйте backup
на отдельной копии: migration status, `PRAGMA integrity_check`, auth, карты
и расшифровка тестового подключения исходным ключом. Backup без проверяемого
ключа недостаточен для восстановления подключения.
## Независимое обновление frontend
После проверки API compatibility замените только `FRONTEND_IMAGE` в env:
```bash
docker compose pull frontend
docker compose up -d --no-deps --wait --wait-timeout 120 frontend
```
Backend container и SQLite не пересоздаются. Входная точка кратко недоступна.
Проверьте deep link, login/refresh и новую OCI revision. Старым browser tabs
может понадобиться reload: старые hashed chunks не сохраняются между images.
## Независимое обновление backend
Выполните stopped-backend backup выше. Замените только `BACKEND_IMAGE` в env.
Последовательность не продолжается после failed migration:
```bash
docker compose pull backend migration
docker compose up --no-deps --force-recreate --abort-on-container-exit \
--exit-code-from migration migration &&
docker compose up -d --no-deps --wait --wait-timeout 120 backend
```
Frontend не пересоздаётся; nginx повторно разрешает backend DNS. Во время
перерыва возможен 502. При failure оставьте backend остановленным, сохраните
диагностику migration и выберите совместимый image либо отдельный restore.
Не используйте `migrate reset` или автоматический `resolve` как recovery.
Проверьте migration status, health, login/refresh, карты и расшифровку подключения.
После успешного smoke запишите фактически установленную пару image references.
Одна file lock защищает установку от параллельных команд. Маркер незавершённого
обновления блокирует повтор до разбора оператором. Он содержит этап, версии
и путь к backup, но не секреты и не копию данных. После обновления нужен smoke
через HTTPS: healthcheck сам по себе не доказывает работу Secure refresh-cookie.
## Откат image
Для frontend верните предыдущий совместимый version tag и повторите frontend update.
Для backend сначала остановите процесс и подтвердите совместимость старого
image с **текущей** схемой. Верните предыдущий image reference и запустите только backend:
```bash
docker compose pull backend
docker compose up -d --no-deps --wait --wait-timeout 120 backend
```
Данные и encryption key не заменяются. Migration service здесь намеренно
не запускается: старые migrations не являются down-migrations. Прочитайте
новые записи, сделанные после backup, и проверьте auth/cards/connection.
При несовместимости оставьте приложение остановленным до решения о recovery.
Откат версии и восстановление базы — разные операции. Прежний backend можно
запускать только после проверки совместимости с текущей схемой. Автоматического
rollback нет; migration service при ручном image rollback не запускается.
Порядок ручных Compose-команд и проверки recovery находится в README комплекта.
## Explicit restore
Это отдельное решение оператора, **не часть image rollback**. Оператор выбирает
backup timestamp и явно принимает потерю всех изменений после него, включая
сессии, карты и подключения. Сначала воспроизведите restore на isolated project
с отдельным data path/port и совместимым image; не используйте production data
как fixture. Убедитесь в наличии соответствующего encryption key.
После отдельного решения остановите backend и убедитесь, что migration не
работает. Сохраните нынешний каталог под новым именем, ничего не удаляя:
```bash
docker compose stop backend
mv /srv/wealthledger/data /srv/wealthledger/data-before-restore-TIMESTAMP
cp -a /srv/wealthledger/backups/CHOSEN_TIMESTAMP/data /srv/wealthledger/data
```
Проверьте owner 1000:1000, permissions, выбранный compatible image и исходный
ключ. Запустите backend командой rollback с `--no-deps`, затем выполните smoke.
Сверьте наличие записей до backup и отсутствие известных записей потерянного
интервала. Сохраните прежний каталог для расследования. При необходимости
новой migration сначала выполните обычную backup/update последовательность.
Оператор выбирает проверенную копию и явно принимает потерю всех более новых
записей. Сначала восстановление проверяется на isolated данных с прежним ключом
и совместимым image. На целевой установке останавливаются писатели, текущий
каталог сохраняется под новым именем, затем восстанавливается выбранная копия.
Backend пересоздаётся, чтобы заново подключить заменённый bind-каталог:
простой stop/start может сохранить mount прежних данных.
Проверяются целостность БД, auth/cards и расшифровка подключения. Только после
подтверждённого recovery можно снять маркер незавершённой операции.
+58 -11
View File
@@ -2,7 +2,8 @@
## Источник контракта
Backend генерирует OpenAPI из собственного публичного API. Frontend использует
Backend генерирует OpenAPI из собственного публичного API. Этот сгенерированный
OpenAPI — исполняемый источник HTTP-контракта. Frontend использует
предоставленный OpenAPI snapshot как актуальный вход и локально генерирует
типизированный клиент. Backend владеет контрактом, а подготовка и доставка
схемы находятся вне frontend. Это разделение принято в
@@ -20,6 +21,57 @@ artifact у себя и проверяет соответствие generated ty
API имеет prefix `/api/v1`; Swagger публикуется для локальной разработки по
environment policy, а не используется frontend в runtime.
## Скринер акций
`POST /api/v1/stocks/screener/search` всегда создаёт новый результат по
условиям скринера, включая пустой подтверждённый список. Ответ первой страницы
содержит непрозрачный `searchId`, время получения каталога и признак следующей
страницы. `GET /api/v1/stocks/screener/search/{searchId}?page=...` читает
последующую страницу того же результата, не запускает новый поиск и сохраняет
состав и время списка. Идентификатор результата принадлежит текущему пользователю
и его подключению; недоступный или чужой результат возвращает ошибку, а не
пустой список. Точные поля и статусы определяет backend-generated OpenAPI.
## Инвестиционные данные и синхронизация
Затронутые GET инвестиционных ресурсов возвращают сгенерированный contract
`{ data, meta }`. `data` nullable: `null` означает отсутствие complete result,
а подтверждённый пустой массив является available complete result. `meta`
независимо описывает freshness, completeness, `lastSuccessfulAt`,
resource-relevant `lastAttempt` и availability. `lastAttempt` имеет состояния
`none`, `running`, `succeeded`, `partial` и `failed`; его неуспех не заменяет
сохранённый complete result или его время. `Portfolio.isStale` остаётся
специфичным для snapshot полем, а не заменой состояния попытки.
Только Accounts дополнительно возвращает top-level `sync`: `null`, если у
текущего подключения ещё не было часовой попытки, иначе connection-wide
наблюдаемый результат. Это наблюдение, не команда. Обычный GET и retry после
проверки доступа читают локальное состояние и не начинают личную работу.
Устаревший `sync=observe`, пока он сохранён для совместимости маршрута, также
только читает. Request-started screen-sync command/status не является текущей
AS-IS моделью.
Безопасные ошибки завершённой часовой попытки передаются в `lastAttempt` и
Accounts `sync`; HTTP transport/access/input errors сохраняют Problem Details.
Старые данные при временной source failure сохраняют исходное успешное время.
Ни одно поле не раскрывает connection revision. Детали поведения —
[свежесть и полнота](cross-cutting/data-freshness.md).
Envelope несовместим с прежними bare-array ответами: backend/frontend
поставляются согласованной парой. Финальный artifact передаётся явно;
локальная генерация и CI frontend читают только собственный snapshot.
## Income analytics
`GET /api/v1/brokerage-accounts/{accountId}/income-analytics` и общий маршрут
`all` возвращают результат только выбранного периода: итоги по исходным
валютам, точные записи выплат, разбиение по инструментам и временные
интервалы. Данных сравнения с предыдущим периодом или годом ранее в ответе
нет — ни в теле, ни среди зависимостей `meta.dependencies`, роль которых
ограничена значением `selected`. Публичную машинную схему задаёт
backend-generated OpenAPI; frontend получает её обновлённый snapshot отдельно.
Точную семантику поведения описывает [доход](../domains/income/overview.md).
## Validation и identity
Backend преобразует и проверяет входные данные на общей HTTP-границе.
@@ -41,16 +93,11 @@ message/stack. Типичный ответ:
Для input validation контракт требует `400`, `VALIDATION_ERROR` и `errors` как
массив `{ pointer, detail }`, где pointer адресует поле запроса. Это требование
не считается проверенным одним описанием OpenAPI: нужна проверка реального
HTTP-ответа после всех стадий обработки запроса.
**Известное расхождение:** согласованная
[спецификация аутентификации](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/user-authentication/spec.md#requirement-стандартные-ответы-об-ошибках)
требует `pointer` и `detail` для невалидных полей. В текущей конфигурации
валидация формирует строковые сообщения, а фильтр ожидает структурированные
ошибки полей; поэтому корректность деталей `errors` не подтверждена.
Исправление требует отдельного изменения реализации и проверки полного
HTTP-потока. OpenAPI сам по себе этого расхождения не устраняет.
проверяется реальным HTTP-ответом после всех стадий обработки запроса. Глобальная
валидация передаёт фильтру структурированные ошибки без исходных значений и DTO,
а фильтр формирует JSON Pointer, включая вложенные поля. HTTP suites подтверждают
детали ошибок регистрации, банковской карты и синхронизации; OpenAPI отдельно
фиксирует форму ответа.
Ответ не включает автоматически все поля RFC (`type`, `title`, `instance`).
Добавление обязательного поля является изменением публичного контракта.
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -1,4 +1,12 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
defaultIgnores: false,
ignores: [
message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()),
message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()),
],
rules: {
"type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]],
"references-empty": [2, "never"],
},
};
+30 -2
View File
@@ -4,6 +4,34 @@
типографику, интервалы, радиусы, тени и адаптивные размеры.
Поддерживается светлая схема; тёмная тема не заявлена.
## Общая основа и каталог
Внутри frontend используются три private npm workspace-пакета:
| Пакет | Ответственность |
| --- | --- |
| `@wealthledger/tokens` | JSON со значениями палитры, типографики, интервалов и других визуальных параметров |
| `@wealthledger/ui` | Mantine-тема, общие стили, StateView, LoadingSkeleton и EntryCard |
| `@wealthledger/icons` | React-иконки добавления, редактирования и удаления |
EntryCard отвечает за расположение переданных подписей, значения и действий.
Предметный компонент определяет смысл операции, форматирует данные и выбирает
цветовую роль. Денежные расчёты и запросы не входят в общий UI-пакет.
Иконки наследуют цвет через `currentColor`. По умолчанию они декоративные:
доступное имя принадлежит кнопке. Самостоятельная смысловая иконка получает
явные роль изображения и доступное имя. Предметные рисунки и анимации
остаются в приложении; поставки raw SVG и мобильного UI нет.
Storybook объединяет примеры приложения и пакетов. Он показывает синтетические
состояния без API, авторизации и query providers; формы получают локальные
callbacks. Каталог помогает проверять представление, но не заменяет проверки
запросов, денежных ограничений и восстановления после ошибок.
Пакеты используются локально и не опубликованы в registry. Независимый выпуск
не настроен. Потребители импортируют публичные exports пакетов напрямую; прежние
compatibility re-exports `shared/theme` и `shared/ui` удалены.
## Семантические роли
| Роль | Цветовая основа | Применение |
@@ -16,8 +44,8 @@
| border | gray.3 | Граница поверхности |
| primary | blue filled | Основное действие и активная навигация |
| info | blue.6 | Информация |
| positive | green.6 | Положительный результат с текстом или знаком |
| negative | red.6 | Ошибка или отрицательный результат с текстом или знаком |
| positive | green.9 | Положительный результат с текстом или знаком |
| negative | red.9 | Ошибка или отрицательный результат с текстом или знаком |
| warning | yellow.6 | Предупреждение; не мелкий текст на белом |
Цвет не заменяет подпись или знак. Палитра не гарантирует доступный контраст
+19 -36
View File
@@ -7,30 +7,11 @@
только к backend. Счёт не хранится как долговременное локальное владение
независимо от брокерского подключения.
```mermaid
sequenceDiagram
participant U as Пользователь
participant F as Frontend
participant B as Backend
participant D as SQLite
participant T as T-Bank gRPC
U->>F: Открывает /accounts
F->>B: GET /api/v1/brokerage-accounts (Bearer)
B->>D: Читает подключение T-Bank
alt Подключение отсутствует
B-->>F: 409 TBANK_NOT_CONNECTED
F-->>U: Переход к настройкам T-Bank
else Подключение сохранено
B->>T: GetAccounts(OPEN) over TLS
T-->>B: Счета или ошибка
par Для каждого поддерживаемого счета
B->>T: GetPortfolio + GetPositions over TLS
T-->>B: Позиции и деньги либо временная ошибка
end
B-->>F: Счета со сводками либо ошибка
F-->>U: Карточки счетов или объяснение ошибки
end
```
Экран читает локальный список и сохранённые сводки. Обычный запрос запускает
недостающие или устаревшие зависимости в фоне. Список и сводка каждого счёта
имеют независимые timestamps и состояния. Неподтверждённый список показывает
загрузку доступа, а не отсутствие счетов. Поток и границы описаны в
[модели свежести](../../architecture/cross-cutting/data-freshness.md).
## Счёт и его сводка
@@ -69,8 +50,8 @@ sequenceDiagram
сводка не подменяется нулями.
Временная недоступность сводки одного счёта остаётся локальной для него.
Ошибка подключения, отклонение токена или невозможность получить исходный
список не превращаются в частичный успешный список.
Известный отказ доступа скрывает личные данные. Transient failure сохраняет
последний список и его время; отсутствие первого списка остаётся missing.
## Состояния
@@ -81,20 +62,22 @@ sequenceDiagram
- `409 TBANK_NOT_CONNECTED`: список не запрашивается у T-Bank, экран ведёт в настройки.
- `400 INVALID_TBANK_TOKEN`: сохранённый токен не удаляется, экран ведёт в настройки для замены или удаления.
- `503 TBANK_UNAVAILABLE`: экран предлагает повторить загрузку или открыть настройки.
- Неизвестная ошибка: экран предлагает только повторить загрузку.
- `503 TBANK_UNAVAILABLE`: экран показывает спокойное состояние недоступности; чтение возобновляется автоматически.
- Неизвестная ошибка: то же состояние недоступности без действия повтора.
- Успешный пустой ответ: штатное состояние отсутствия инвестиционных счетов.
Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. На широком экране карточки расположены в две колонки, на узком — в одну. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию.
Карточка ведёт на `/accounts/:accountId` по всей области и доступна с клавиатуры. Карточки расположены в одну колонку при любой ширине. Карточка показывает название, локализованный тип, крупный валютный итог и вторую валюту при наличии, число остальных валют, дату открытия без времени и видимое действие открытия. Отдельные цветные блоки «Акции» и «Облигации» показывают количество наименований бумаг. Недоступная или пустая сводка отображается локально; отсутствие сумм не скрывает известные количества бумаг, а недоступная сводка не подменяет их нулями. Наведение и видимый клавиатурный фокус подчёркивают переход; настройка уменьшения движения отключает анимацию.
## Свежесть и границы
Список запрашивается при открытии экрана без ручного обновления и без обновления при возврате фокуса. Frontend использует успешный кэш TanStack Query в течение 60 секунд, поэтому повторный вход в этот интервал не выполняет новый GET. Когда уже показанный список обновляется в фоне, он остаётся на экране: индикатор обновления и ошибка с повторной попыткой отображаются рядом с последним успешным ответом, включая пустой список. Это клиентский сохранённый ответ; серверный запасной список не выдаётся. Backend не хранит кэш и не передаёт метку свежести. Список не заменяет полный [портфель](../portfolio/overview.md),
[журнал операций](../operations/overview.md) или историю закрытых счетов.
Для совокупного результата доступен [режим «Все счета»](../portfolio/all-accounts.md).
Обычное открытие использует query owner и локальный server snapshot.
«Обновить» перечитывает сохранённые membership и сводки доступных счетов;
активная работа наблюдается без нового импорта. Истечение TTL само не
запускает запрос: обновлением личных данных владеет часовой обход.
Membership и portfolio source имеют часовую свежесть. Время списка
не подменяет время каждой сводки. Список не заменяет полный
[портфель](../portfolio/overview.md), [журнал](../operations/overview.md)
или историю закрытых счетов. Для общего результата доступен
[режим «Все счета»](../portfolio/all-accounts.md).
Согласованное поведение описывает [спецификация счетов](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-accounts/spec.md).
Её раздел об экране пока требует одноколоночного списка и не описывает
счётчики акций и облигаций. Текущая реализация и её проверки используют
две колонки на широком экране и показывают эти счётчики. Расхождение требует
отдельного согласования требований; в этой документации отражён наблюдаемый экран.
+19 -12
View File
@@ -11,14 +11,20 @@
Пользователь регистрируется или входит по email и паролю. Регистрация сразу
создаёт первую сессию. Внешние пробелы email удаляются, буквы приводятся
к нижнему регистру. Backend требует пароль не короче 12 символов и не длиннее
72 UTF-8 байтов.
к нижнему регистру.
**Известное расхождение:** сохранённое требование регистрации и проверка
в форме frontend считают нижнюю границу в UTF-8 байтах, а backend — в символах.
Не-ASCII пароль может пройти форму и получить отказ API. Это не принятое
изменение password policy; требуется отдельное согласованное решение
о проверке пароля и подтверждение полного HTTP-потока.
Пароль при регистрации — от 8 до 72 печатных ASCII-символов: латинские
буквы, цифры, спецсимволы и пробел. Кириллица, emoji, другие не-ASCII и
управляющие символы отклоняются. Пароль не обрезается и не нормализуется, а
обязательных классов символов нет. Верхняя граница вытекает из bcrypt, который
учитывает только первые 72 байта: для ASCII символ равен байту, поэтому
длинный пароль не обрезается молча. Frontend и backend проверяют одну и ту же
политику.
При входе проверяется только то, что пароль непустой и не длиннее 72 UTF-8
байтов. Минимум и набор символов не применяются, чтобы пользователь,
зарегистрированный по прежним правилам (в том числе с не-ASCII паролем), мог
войти. Невалидный пароль получает `400` с кодом `VALIDATION_ERROR`.
Access token действует 15 минут и хранится только в памяти браузера.
Refresh token имеет серверный срок 7 дней, заменяется при восстановлении
@@ -55,15 +61,16 @@ sequenceDiagram
B-->>F: Access token, профиль, новая refresh-cookie
F->>B: Protected request с Bearer
else Cookie нет или refresh отклонён
B-->>F: 401 и очистка cookie
B-->>F: 401 без изменения cookie
Note over F: Готовое неавторизованное состояние
end
```
В одном экземпляре frontend одновременные refresh используют один
незавершённый запрос. Между вкладками используется Web Locks, если браузер
его поддерживает. Backend всё равно защищает ротацию атомарно: координация
браузеров не заменяет серверную проверку.
В одной вкладке одновременные refresh используют один незавершённый запрос;
cookie-writing requests упорядочены локально. Межвкладочная синхронизация
не поддерживается: после смены сессии в другой вкладке допустимы reload/re-login.
Backend защищает ротацию атомарно. Неуспешный refresh не удаляет cookie
победившего запроса; недействительный token по-прежнему не восстанавливает сессию.
Обычный защищённый запрос с `401 UNAUTHORIZED` проходит refresh и один повтор
через общий транспорт; auth endpoints из этого цикла исключены. Само
+2 -3
View File
@@ -89,9 +89,8 @@ JavaScript Number. Отдельный ввод ограничен signed64: на
следующие страницы. Баланс при выборе прошлого периода остаётся текущим.
Сводка учитывает все подходящие записи, включая не показанные на странице.
Сводка одной или всех карт показывает одну итоговую сумму расходов с учётом
возвратов и компактные строки категорий по убыванию итоговых расходов. Отдельные показатели «Возвращено»
и «После возвратов» в интерфейсе скрыты; расчёт и операции возврата сохранены. Возврат уменьшает расходы месяца своего
Сводка одной или всех карт отдельно показывает расходы, возвращённую сумму и
итог после возвратов, а также компактные строки категорий по убыванию итоговых расходов. Возврат уменьшает расходы месяца своего
получения: августовская покупка 1 000 ₽ и сентябрьский возврат 600 ₽ дают
августовский итог 1 000 ₽ и сентябрьский −600 ₽ при отсутствии других трат.
Начальный остаток, пополнения и переводы в эти показатели не входят.
+32 -40
View File
@@ -4,7 +4,7 @@
купонами и дивидендами за выбранный период». Пользователь открывает
`/accounts/:accountId/income` из обзора счёта или
[общий доход](../portfolio/all-accounts.md) по доступным счетам.
Экран показывает итоги по валютам, сравнение и отдельную карточку каждой
Экран показывает итоги по валютам и отдельную карточку каждой
включённой выплаты. Источником служат [исполненные операции](../operations/overview.md),
а не изменение цены портфеля и не календарь будущих выплат.
@@ -38,44 +38,36 @@
При первой загрузке или новом периоде без сохранённых данных показан
каркас страницы. Пустой результат объясняет отсутствие исполненных купонов и
дивидендов, сохраняя фильтры, налоговое пояснение и сравнения. Ошибка запроса
или нарушение принадлежности выплат даёт локальное состояние с действием «Повторить».
дивидендов, сохраняя фильтры и налоговое пояснение. Ошибка запроса
или нарушение принадлежности выплат даёт локальное состояние недоступности
без действия повтора; чтение восстанавливается автоматически.
При фоновом обновлении того же периода сохранённый ответ и черновик
остаются видимыми: загрузку обозначает индикатор, неуспешное обновление —
предупреждение с повторной попыткой.
остаются видимыми без индикации обновления и технического предупреждения.
Неполнота применённого периода обозначается состоянием самого результата.
На узком экране поля и действия идут вертикально; все карточки выплат
остаются в общем потоке страницы без раскрытия или внутренних областей прокрутки.
## Какие выплаты входят в результат
- Backend проверяет доступ к счёту, проходит все страницы исполненных операций и не возвращает частичный результат при ошибке страницы или повреждённой цепочке курсоров.
- Backend проверяет доступ локально и читает сохранённые операции. Фоновая загрузка должна завершить все страницы; ошибка страницы или повреждённая цепочка курсоров не подтверждают полноту.
- Только точные типы купона и дивиденда источника формируют результат. Суммы остаются точными десятичными строками в исходной валюте; отрицательные корректировки сохраняют знак. Налоги, комиссии, сделки, переводы, погашения и переводы дивидендов не входят в доход.
- Итоги, сравнения, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. На странице показано только сравнение с годом ранее, когда календарная дата и история счёта позволяют. Предыдущий период остаётся в ответе API, но не отображается.
- Итоги, временные интервалы и разбиение по инструментам не смешивают валюты и не создают общий конвертированный итог. Система не загружает другие периоды ради сравнения полученного дохода с выбранным.
- Временные интервалы по-прежнему приходят с backend: дни для 1–45 дней, недели для 46–180 и месяцы для более длинного периода, включая пустые интервалы. Страница дохода не строит из них график или текстовую динамику; ответ API не меняется.
- Раздел «Выплаты» показывает единый список карточек выплат в исходном порядке `entries`: одна карточка на каждую выплату. Внешних групп по инструментам или валютам, итогов и количества выплат инструмента нет. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой карточке; валюты по-прежнему разделены в итогах и сравнениях. Дополнительный запрос за составом не нужен.
Сравнение с непосредственно предыдущим периодом сохраняется в API:
для полного месяца это предыдущий полный месяц, иначе — непосредственно
предшествующий интервал той же включительной длительности. Текущая страница
его не показывает. Это расходится с
[основным требованием сравнений](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md#requirement-period-comparisons-preserve-calendar-and-currency-meaning),
которое описывает оба сравнения. Требуется отдельно согласовать этот разрыв;
здесь сохранено различие между ответом API и фактическим экраном.
Раздел «Годом ранее» показывает цветные панели без общей белой подложки и
внутренних отступов у внешнего блока. Точные периоды показаны в заголовках двух панелей с русским форматом диапазона
и UTC для календарных дат.
Отдельного заголовка с датами и валютного бейджа нет; валюта остаётся у каждой суммы.
Панель «Разница» сохраняет серверную абсолютную разницу и дополняет итог, купоны
и дивиденды процентом: абсолютная разница / сумма годом ранее × 100.
Это округлённый показатель для отображения, а не пересчёт Money или инвестиционная
доходность. Форматирование сохраняет знак и не более одного десятичного разряда.
При нулевой или отрицательной базе либо непредставимом числовом отношении
процентный бейдж не отображается; абсолютная разница остаётся видимой.
- Раздел «Выплаты» показывает карточки выплат в исходном порядке `entries`. В отдельном счёте каждая выплата — отдельная карточка. В режиме «Все счета» выплаты одного инструмента, дня и точного типа образуют группу, если их две и более ([правила группировки](../operations/overview.md#группировка-в-режиме-все-счета)). Группа стоит на месте первой выплаты; каждая строка счёта показывает название счёта и свою сумму со знаком. Визуальный итог группы — точная сумма по валютам по показанным выплатам; он скрыт, если полнота периода не `complete`, или у выплаты нет суммы, или сумма равна нулю. Это клиентский расчёт и не заменяет серверные итоги аналитики дохода, которые по-прежнему приходят из ответа API и не пересчитываются. Описание, дата, доступные идентификаторы инструмента и сумма со знаком видны в каждой выплате. Дополнительный запрос за составом не нужен.
## Полнота и принадлежность выплат
Доходы читают сохранённые операции и coverage в согласованной транзакции.
Только выбранный период определяет готовность результата. Неполный период
не получает окончательных итогов.
Подтверждённый пустой период отличается от ещё не загруженного. При смене
периода прежний snapshot не показывается под новым названием.
Ручная сверка полного периода сохраняет весь прежний результат до успешной
атомарной публикации всех необходимых интервалов и счетов. История счёта
может ещё догружаться после готовности выбранного месяца.
Каждая запись `income-analytics.entries` содержит обязательный `instrumentKey`,
равный `key` соответствующей строки `instruments`. Backend вычисляет этот ключ
один раз при подготовке записей дохода и использует его же для группировки.
@@ -83,26 +75,26 @@
серверным правилом; клиент воспринимает ключ как непрозрачную строку.
Это поле принадлежит ответу аналитики; общий журнал операций его не требует.
Frontend проверяет принадлежность выплат по полученному ключу до подготовки
плоского списка выплат. Отсутствующий, пустой или
нестроковый ключ, неоднозначные ключи инструментов, неизвестная группа, другая
валюта или несовпадение числа выплат приводят к существующему локальному
состоянию ошибки аналитики с повторной попыткой. Частичный список выплат не показывается как
полный; формула идентификации в клиенте не восстанавливается. Полностью пустой
ответ допустим. Состав и исходный серверный порядок записей и знак сумм
сохраняются, frontend не пересчитывает денежные итоги. Повторяющиеся ID
операций остаются отдельными строками; повторные вхождения учитываются раздельно, а изменение источника логотипа
позволяет заново показать изображение после буквенной заглушки.
Backend обеспечивает принадлежность выплат, валюту и receipt count при
построении entries и instrument groups из одного набора данных. Frontend
доверяет типизированному contract, не восстанавливает формулу идентификации
и не выполняет второй consistency validator. Полностью пустой ответ допустим.
Состав и исходный серверный порядок записей и знак сумм
сохраняются; серверные денежные итоги frontend не пересчитывает (визуальный итог группы в режиме «Все счета» отдельный, см. выше). Стабильный ID в пределах подключения и счёта определяет одну операцию:
повторная сверка обновляет её без двойного учёта. Одинаковые ID разных счетов
остаются разными выплатами. Изменение источника логотипа позволяет заново
показать изображение после буквенной заглушки.
## Будущие события и ограничения
На странице дохода нет блоков «Динамика» и «Ближайший прогноз». Она не запрашивает
На странице дохода нет блоков «Динамика» и «Ближайший прогноз», блоков
сравнения периодов и сообщений об их недоступности. Она не запрашивает
`/events` и не зависит от его доступности. Самостоятельная страница
[«Будущие события»](upcoming-events.md) по-прежнему доступна из обзора
выбранного счёта и сохраняет прежнее поведение. Будущие выплаты
не включаются в итоги полученного дохода или сравнения.
не включаются в итоги полученного дохода.
Аналитика только читает данные, не хранит историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог.
Аналитика читает сохранённую историю операций и не рассчитывает налоги, инвестиционную эффективность, доходность, валютную конвертацию или общий мультивалютный итог.
Основное проверяемое поведение — в [спецификации дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md),
точная форма ответа — в [HTTP API](../../architecture/http-api.md).
+55 -45
View File
@@ -39,66 +39,76 @@
## Период и список
Без фильтра период начинается сегодня в UTC и заканчивается через 30 дней
после начала, обе даты включены. Поля «С» и «По» позволяют изменить период:
«Применить» сохраняет выбранные календарные даты в URL и запрашивает результат,
«Сбросить» возвращает период по умолчанию. Обзор счёта сохраняет прогноз
на текущую дату и следующие 30 дней.
Без фильтра страница применяет период по умолчанию из семи календарных
дат включительно: сегодняшней и шести следующих календарных дней. API при
отсутствии параметров использует своё окно (от текущей даты UTC до `from + 30`
календарных дней), обе даты включены; `from` и `to` принимают только строгие
даты `YYYY-MM-DD`. Календарь фильтра разрешает выбор сегодняшней и будущих дат
и не позволяет выбрать даты раньше сегодняшней. Прямая ссылка с валидным
прошедшим периодом сохраняет указанные даты без замены; некорректный формат,
несуществующая календарная дата или обратный диапазон нормализуются к периоду
по умолчанию с удалением параметров из URL. Поля «С» и «По» сохраняют
выбранные календарные даты в URL, а «Сбросить» возвращает период по умолчанию.
События упорядочены по календарной дате. У каждой строки видны тип,
События упорядочены по календарной дате. Купоны и дивиденды показываются
единым предметным представлением выплаты с явной пометкой прогноза — тем же,
что у исполненных выплат в доходе и журнале; неизвестная сумма остаётся
«сумма неизвестна» и не превращается в ноль или полученные деньги.
Дата дивиденда обозначена как дата реестра. Offer и погашение сохраняют своё
значение без суммы выплаты. У каждой строки видны тип,
инструмент, дата и доступная сумма в исходной валюте. Список не имеет
пагинации и не конвертирует валюты. Пустой результат означает отсутствие
известных поддерживаемых событий в выбранном окне, а не доказательство
отсутствия всех будущих выплат. Инструмент без поддерживаемых данных
не получает искусственную строку «нет данных».
API принимает необязательные `from` и `to` в формате `YYYY-MM-DD` для
отдельного и общего счёта. Без `from` используется сегодня в UTC,
без `to` — 30 дней после начала. Неверный формат или несуществующая дата
дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`.
Сохранённый forecast сохраняет свой `calculatedAt` и исходное включительное
окно. GET для отдельного счёта или `all` возвращает только пересечение этого
окна и запрошенного UTC-периода, не меняя ни `calculatedAt`, ни исходное окно.
Пустое пересечение или отсутствие событий в пересечении — успешный пустой
список с теми же metadata; это не отсутствие forecast. Неверный формат или
несуществующая дата дают `400`; обратный период — `400 INVALID_EVENTS_PERIOD`
без замены периода и без запуска личной работы.
Фильтрация на сервере предшествует сортировке и вычислению
`incomeTotalsByCurrency`, включающего только купоны и дивиденды.
Итоги остаются в ответе API; полный экран показывает список без общего итога.
Итоги остаются в ответе API; полный экран показывает список без общего итога списка.
Текущие controller, сервис и экран поддерживают выбор периода, тогда как
[основная спецификация](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md#requirement-account-page-presents-an-upcoming-events-list)
описывает фиксированные 30 дней и экран без фильтров. Документация отражает
реализованный выбор периода, не изменяя это несогласованное требование.
В режиме «Все счета» события одного инструмента, даты, точного типа и `dateKind` образуют
группу, если их две и более, в том числе одного счёта. Ключ дополнительно включает `dateKind`,
поэтому дата реестра дивиденда и иная дата того же инструмента не объединяются. Группа стоит на месте первого
события. Полный список группирует все полученные события; превью обзора сначала берёт пять
событий и группирует только их. Каждая строка счёта остаётся отдельной и показывает название
счёта и свою прогнозную сумму. Визуальный итог группы — точная сумма по валютам; он показывается
только если полнота прогноза `complete` и у каждого события группы известна прогнозная сумма.
Иначе итог скрыт, а строки остаются. Этот итог не равен итогам ответа API и не относится к полученному доходу.
## Получение и отказы
```mermaid
sequenceDiagram
participant F as Frontend
participant B as Backend
participant T as T-Bank
participant M as MOEX ISS
F->>B: Счёт и выбранный период событий
B->>B: Проверяет сессию и текущее подключение
B->>T: Получает открытые счета и позиции доступного счёта
par События акций
B->>T: GetDividends за выбранный период
T-->>B: Известные дивиденды
and События облигаций
B->>M: Календарь bondization
M-->>B: Купоны, погашения и оферты
end
B->>B: Применяет период и текущее количество бумаг
B-->>F: Полный список известных событий либо ошибка
```
Запрос требует сессии, подключения и локально подтверждённого membership.
Он читает сохранённый forecast и не запускает личную синхронизацию. Часовой
обход строит forecast из полного личного portfolio и общих календарей: T-Bank
для дивидендов, MOEX для bond events. Личные оценки сохраняются отдельно от
общих календарей.
Запрос требует действующей сессии и подключения T-Bank. Получение
необходимых финансовых данных брокера или календаря MOEX завершается ошибкой
`503`, если источник недоступен: частичный список не выдаётся как полный.
Доступные метаданные инструментов дополняют название и логотип; когда
исходного тикера достаточно для календаря, ошибка дополнительного поиска
метаданных не скрывает события.
Последний полный forecast привязан к scope, исходному окну и membership. Если
часовой обход не может получить полный новый forecast, прежний полный прогноз,
его `calculatedAt` и исходное окно остаются читаемыми, в том числе после
рестарта; `meta.lastAttempt` отдельно сообщает об ошибке обновления. Без
полного forecast `data: null` и metadata обозначает initial loading либо
временную недоступность, а не подтверждённое отсутствие выплат. В режиме `all`
результат complete только при полном вкладе каждого включённого счёта. Другой
scope или изменившийся membership не получают старый forecast под новым
названием.
Успешный ответ MOEX без запрошенного блока означает отсутствие поддерживаемых
событий этого источника; это отличается от transport failure и повреждённого
формата ответа.
При первом открытии показан каркас заголовка и строк, без интерактивных
заглушек. Пустой ответ сохраняет возможность изменить период.
При обычном фоновом обновлении уже полученные данные остаются на экране,
ошибка получает предупреждение и повторную попытку. Без выбранного счёта
запрос не выполняется. Смена сессии и подключения следует
При первом открытии без complete forecast показан каркас заголовка и строк,
без интерактивных заглушек. Пустой успешный ответ сохраняет возможность
изменить период. При failed HTTP reread уже полученный Client Cached Snapshot
остаётся на экране; это отличается от server-side failed hourly attempt.
Повторная попытка перечитывает только HTTP-ответ. Без выбранного счёта запрос
не выполняется. Смена сессии и подключения следует
[общим правилам финансовых данных](../../architecture/cross-cutting/data-freshness.md).
Внешние границы объясняют интеграции [T-Bank](../../integrations/tbank.md)
+28 -16
View File
@@ -89,7 +89,11 @@ HTML-источник, общий кэш и поведение при измен
Календарь охватывает пять прошлых лет и следующий год. Будущие купоны
сортируются по дате выплаты, прошедшие и недатированные доступны отдельно.
Показаны сумма на облигацию, номер, тип, длительность периода и дата реестра,
Будущие и прошедшие события купонного графика показываются единым предметным
представлением выплаты с пометкой события по графику — тем же, что у
исполненных выплат и прогноза; исполнение подтверждает только личная история.
Показаны сумма на облигацию, отдельная личная оценка, номер, тип, длительность
периода и дата реестра,
если они известны. Неопределённый будущий купон не заменяется нулём.
Личная оценка доступна только при положительных позициях, известной сумме,
@@ -111,22 +115,24 @@ HTML-источник, общий кэш и поведение при измен
транспортного контракта задаёт [HTTP API](../../architecture/http-api.md).
`BondBy`, `FindInstrument` и `GetBondCoupons` используют способ
декодирования, сохраняющий отсутствие полей. Рыночные RPC проходят
общие TLS, очередь, ограничение частоты, deadline и повторные попытки. Кэш успешных и совместных
незавершённых запросов ограничен LRU и сроком 30 секунд по пользователю,
поколению подключения и параметрам. Ошибки не кэшируются; ошибки авторизации
не подавляются при дополнительной загрузке данных поиска.
Исходный ответ `BondBy` совместно используется поиском, карточкой, котировкой,
графиком, купонами и сводкой показателей в пределах того же пользователя, поколения и TTL.
Одновременная загрузка секций вызывает один `BondBy` на UID. Календарь
и сводка совместно используют один запрос купонов; котировка переиспользуется
при расчёте купонной доходности. MOEX и НКР загружаются только для открытого
выпуска, независимо от поиска. Для 30 новых
результатов поиск выполняет 32 RPC до повторных попыток: один `FindInstrument`,
30 `BondBy` и один пакетный запрос цен. Общий ограничитель backend не заменяет
квоты Т-Банка; при их исчерпании сохраняется `TBANK_UNAVAILABLE`.
декодирования, сохраняющий отсутствие полей. Общие данные выпуска, quotes,
календари, MOEX metrics и необходимые результаты НКР сохраняются в SQLite.
Личные позиции и оценки выплат используют текущий доступ и сохранённый
portfolio; они не попадают в общие источники.
Разделы загружаются независимо, показывают каркас загрузки и локальную повторную попытку.
BondBy переиспользуется зависимостями выпуска. Discovery принадлежит query,
свежесть — конкретному source/range. Missing/stale зависимости ставятся
в фоновую очередь через прежний limiter; обычный HTTP-ответ не ждёт брокера.
Покрытые завершённые свечи не перечитываются после restart. Открытый хвост
остаётся изменяемым; ручной refresh охватывает показанный период.
Последняя полная личная оценка сохраняется при ошибке новых обязательных
зависимостей, вместе с исходным временем. Неполнота не означает отсутствие
выплат. Наблюдение статуса не запускает повторный импорт. Политика источников
и границы — в [модели свежести](../../architecture/cross-cutting/data-freshness.md).
Разделы загружаются независимо, показывают каркас загрузки и локальное
спокойное состояние недоступности без действия повтора.
Ошибка фонового обновления сохраняет последние данные. Смена подключения
сбрасывает поддерево клиентских запросов облигаций вместе с остальными финансовыми запросами;
выход очищает кэш сессии. Общие график, позиция и личные события
@@ -142,3 +148,9 @@ HTML-источник, общий кэш и поведение при измен
и [проверками](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/bonds/application/bond.spec.ts).
Границы источников описаны в [разделе интеграций](../../integrations/README.md),
сходное разделение рыночных и личных данных — в [акциях](stocks.md).
## Online reference
Страница облигации, поиск и переход по тикеру обращаются к онлайн-справочнику
брокера; сохранённые quotes, свечи, события, ratings и метки портфеля
сохраняют собственные границы свежести и ошибок.
+58 -31
View File
@@ -28,24 +28,30 @@
на frontend. Общее для акций и облигаций восстановление справочных данных
принадлежит [загрузке портфеля на backend](../portfolio/flows/portfolio-loading.md).
Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: поиск
по названию или тикеру через каталог Т-Банка, начиная с двух символов.
Результаты разных выпусков, включая обыкновенные и привилегированные акции,
имеют отдельные UID. Карточка результата показывает логотип, последнюю цену
с её временем, тип бумаги, площадку, код режима, ISIN и доступность торгов через
API. Поиск показывает одну карточку на нормализованный ISIN. До ограничения
в 30 результатов выбирается представитель выпуска: доступность через API,
точное совпадение тикера, затем режим TQBR; равные варианты упорядочены по UID.
Записи без ISIN сохраняются отдельно по UID. Обыкновенные и привилегированные
акции с разными ISIN не объединяются. Цена и ссылка принадлежат выбранному
UID; данные разных режимов не смешиваются. Прямые ссылки из портфеля сохраняют
Раздел «Акции» в боковой и мобильной навигации ведёт на `/stocks`: скринер
запускается кнопкой «Найти» с необязательным названием или тикером, сектором,
типом акции, валютой расчётов, площадкой, страной риска и максимальным размером
лота. Он использует полный полученный каталог и показывает только акции, для
которых каталог подтверждает доступность покупки и торговли через API. Это не
гарантирует, что конкретная заявка пользователя будет исполнена прямо сейчас.
Скринер показывает одну карточку на выпуск с нормализованным ISIN, а выпуск
без ISIN остаётся отдельной карточкой точного UID. Обыкновенные и
привилегированные акции с разными ISIN не объединяются. После отбора карточки
упорядочены по тикеру и UID, разделены на страницы по 30 и показывают единое
время получения списка. Изменение условий не заменяет текущий список до
следующего нажатия «Найти». Повторный поиск получает новый каталог; страницы
одного результата не пересчитываются. Подтверждённый пустой результат отличается
от ошибки источника: она сообщается рядом с полем поиска, а повторить запрос
пользователь может обычным «Найти» — отдельного действия повтора чтения нет.
Ссылка карточки принадлежит выбранному UID; данные разных режимов не смешиваются.
Прямые ссылки из портфеля сохраняют
исходный UID, даже если поиск выбрал другой режим этого выпуска; такая страница
сохраняет UUID в адресе. Если один точный тикер относится к разным выпускам,
сервер возвращает `409 STOCK_TICKER_AMBIGUOUS`, а страница предлагает поиск.
Поиск тикера нечувствителен к регистру, но использует точное совпадение:
SBER не заменяется на SBERP. Однобуквенные тикеры также поддерживаются.
сервер возвращает `409 STOCK_TICKER_AMBIGUOUS`.
Прямая ссылка не требует предварительного открытия портфеля. Хлебные крошки
«Акции → название» возвращают к поиску.
«Акции → название» возвращают к скринеру.
## Содержание и состояния
@@ -58,8 +64,9 @@ SBER не заменяется на SBERP. Однобуквенные тикер
рыночные данные и личная история остаются доступны.
Каждый раздел загружается независимо. Первичная загрузка показывает каркас
раздела, ошибка — локальную повторную попытку. При ошибке фонового обновления
сохраняются предыдущие данные и предупреждение. Цена содержит собственное время
раздела, затянувшаяся недоступность — локальное спокойное состояние без
повторной попытки. При ошибке фонового обновления сохраняются предыдущие
данные без технического предупреждения. Цена содержит собственное время
UTC, которое может быть старше времени получения ответа. Статус торгов не
выводится из свежести цены. Отсутствующее значение не заменяется нулём.
@@ -73,7 +80,8 @@ Frontend не получает брокерский токен и не обра
Личная история использует существующий
`GET /api/v1/brokerage-accounts/all/operations?instrumentUid=...`.
Фильтр UID валидируется и передаётся upstream как `instrumentId`; ответ также
Фильтр UID валидируется и применяется к локальной истории. При ручной
сверке бумаги он также передаётся upstream как `instrumentId`; ответ
проверяется по UID. Без `from` такой запрос начинается с самой ранней известной
даты открытия доступных счетов; если даты неполны, нижняя граница — Unix epoch.
Обычный журнал без фильтра инструмента сохраняет начало текущего года.
@@ -118,7 +126,11 @@ UID включён в ключ кэша и подпись общего курс
## Дивиденды и финансовые показатели
События компании отделены от личных зачислений. Известные будущие выплаты
События компании отделены от личных зачислений. Будущие и прошедшие события
дивидендного графика показываются единым предметным представлением выплаты
с пометкой «событие по графику» — тем же, что у исполненных выплат и прогноза;
сумма на акцию, отдельная личная оценка и дата реестра сохраняют свои подписи
и не выдаются за личное зачисление. Известные будущие выплаты
определяются по `paymentDate`, при её отсутствии — по `recordDate`; прошедшие
и недатированные события помещены в отдельную историю компании. Отменённые
события исключаются. Личная оценка умножает сумму на акцию на текущее положительное
@@ -142,18 +154,27 @@ TTM означает последние 12 месяцев, MRQ — послед
## Получение данных и ограничения
Запросы T-Bank проходят общую серверную интеграцию с TLS, лимитом частоты,
очередью, предельным временем ожидания и повторными попытками. Проверка доступа
и расшифровка token выполняются только backend. Успешные ответы и совместные незавершённые запросы кэшируются
в ограниченном кэше LRU на 30 секунд по пользователю, поколению подключения и параметрам.
Для поиска `FindInstrument` дополняется сведениями `ShareBy` через тот же
кэш карточки инструмента и одним пакетным `GetLastPrices` для списка. Ошибка
дополнительной загрузки или исчезнувшая карточка не скрывает исходную запись
каталога; недоступные логотипы и цены имеют заглушки. Ошибки авторизации
не подавляются.
Ошибка целого запроса не кэшируется; отсутствие дополнительного источника
обозначается в ответе. Поддерево клиентских запросов акций сбрасывается вместе с финансовыми
данными при изменении подключения; выход очищает кэш сессии.
Общие metadata, discovery, company, quotes, свечи и календари сохраняются
в SQLite. Скринер получает каталог online и показывает время его получения;
временная недоступность каталога не подтверждает отсутствие подходящих акций.
Отсутствующее enrichment не скрывает найденную бумагу. Сохранённые общие
источники переиспользуются между пользователями, но личные позиции и выплаты
всегда проверяют доступ.
Обычное чтение не ждёт RPC; missing/stale зависимости ставятся в фоновую
очередь через существующий T-Bank limiter. Часовая свежесть применяется
к quotes и открытому хвосту свечей; metadata/company/календари, будучи
сохранёнными, не перечитываются обычным чтением.
Закрытые полные свечные интервалы, включая пустые, сохраняются после restart.
Новый день не делает прежнюю незавершённую свечу полной.
Отдельного действия «Обновить» на странице нет: чтение текущего инструмента,
выбранного графика и personal-history scope выполняется автоматически.
Частичные строки видны с обозначением неполноты; неполные личные оценки
не становятся нулями. Статус и `sync=observe` не создают новый импорт.
Смена подключения и известный access loss очищают личное представление;
поздние ответы не возвращают прежние данные. Детали —
[модель свежести](../../architecture/cross-cutting/data-freshness.md).
Полные даты в интерфейсе имеют формат ДД.ММ.ГГГГ; ISO-формат транспорта и
временные зоны расчётов не меняются. Личная история использует общий с портфелем формат строки операции,
@@ -177,3 +198,9 @@ TTM означает последние 12 месяцев, MRQ — послед
[рыночным данным](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock-market.service.ts),
[выплатам](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock-income.service.ts) и [проверкам](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/instruments/stocks/application/stock.spec.ts).
Сопоставление с облигациями — на [странице облигаций](bonds.md).
## Online reference
Страница акции, поиск и переход по тикеру обращаются к онлайн-справочнику
брокера; сохранённые позиции, quotes, свечи и события остаются независимыми
локальными ресурсами и не подменяются ошибкой справочника.
+50 -42
View File
@@ -13,9 +13,27 @@
Исполненная операция содержит тип события, его категорию, описание,
момент совершения, доступные сведения об инструменте и денежное движение.
Запросы отменённых или ожидающих исполнения поручений не образуют этот журнал.
Суммы остаются точными десятичными строками в исходной валюте; frontend
только форматирует их. Положительное и отрицательное движение сохраняют
знак. Отсутствие `payment` означает «Без движения денег», а не нулевую сумму.
Суммы остаются точными десятичными строками в исходной валюте. Frontend
форматирует их и считает визуальный итог группы по показанным записям
(см. [группировку](#группировка-в-режиме-все-счета)). Положительное и отрицательное
движение сохраняют знак. Отсутствие `payment` означает «Без движения денег», а не нулевую сумму.
## Группировка в режиме «Все счета»
Только в режиме `all` записи журнала и превью обзора, которые относятся к одному
дню экрана, одному инструменту и точному типу операции, объединяются в группу.
Ключом служат день, `instrumentUid` (иначе `figi`) и тип; группа образуется при
двух и более записях, в том числе одного счёта. Группа стоит на месте первой записи.
Каждая запись остаётся отдельной строкой с названием счёта простым текстом и своей суммой.
Одиночный счёт не группируется.
Группировка выполняется после лимита: превью обзора сначала берёт пять записей,
затем группирует только их. Полный журнал группирует страницу, уже загруженную
курсором; группы не переносятся между страницами. Курсор и порядок записей не меняются.
Итог группы — точная сумма по каждой валюте, без конвертации. Он показывается только
если полнота журнала `complete`, у каждой записи группы есть `payment`, и ни одна сумма
не равна нулю. Иначе итог скрыт, а строки счетов остаются.
Категории объединяют разные события для фильтрации: сделка (`trade`),
доход (`income`), налог (`tax`), комиссия (`fee`), перевод (`transfer`)
@@ -46,8 +64,9 @@ ID операций разных счетов не означают дублик
На [страницах акций](../instruments/stocks.md) и
[облигаций](../instruments/bonds.md) этот же журнал становится личной историей
конкретной бумаги. Фильтр `instrumentUid` передаётся брокеру и проверяется
в ответе. Без явного начала периода он охватывает историю с открытия счёта;
конкретной бумаги. Фильтр `instrumentUid` применяется к локальным операциям. Ручная сверка
личной истории может ограничить источниковый запрос этой бумагой, не присваивая
такому subset полноту всех операций счёта. Без явного начала периода он охватывает историю с открытия счёта;
в общем режиме — с самой ранней известной даты открытия доступных счетов.
Если даты открытия неполны, нижняя граница — Unix epoch. Это позволяет
видеть прошлые операции уже проданной бумаги, но не обещает историю
@@ -55,50 +74,38 @@ ID операций разных счетов не означают дублик
## Получение и полнота
```mermaid
sequenceDiagram
participant F as Frontend
participant B as Backend
participant T as T-Bank
F->>B: Запрашивает применённый период и страницу операций
B->>B: Проверяет сессию и текущее подключение
B->>T: Получает открытые счета
T-->>B: Подтверждает доступность счёта
alt Подходящая страница есть в кэше
B-->>F: Сохранённая страница
else Нужна загрузка
B->>T: GetOperationsByCursor, только исполненные операции
T-->>B: Страница операций
B->>B: Нормализует записи и применяет фильтры
B-->>F: Записи и продолжение либо ошибка
end
```
`GET /api/v1/brokerage-accounts/:accountId/operations` читает сохранённые
операции и coverage после локальной проверки доступа. Период, категории,
UID, cursor и limit применяются к этой выборке. Транспорт описан в
[HTTP API](../../architecture/http-api.md).
`GET /api/v1/brokerage-accounts/:accountId/operations` требует Bearer token.
Период, категории, необязательный UID, курсор и размер страницы задают запрос;
ответ содержит записи, время наблюдения и сведения о продолжении.
Точные поля и допустимые значения задаёт [HTTP API](../../architecture/http-api.md).
Стабильность broker operation ID в пределах счёта и подключения принята как
допущение проекта. Повторный ID обновляет ту же запись без двойного учёта.
Страницы источника сначала сохраняются в staging. Полный успешный обход
разрешает upsert, удаление исчезнувших записей в перечитанном predicate
и публикацию coverage одной транзакцией. При повторной сверке полного
показанного периода транзакция охватывает все его окна и счета.
Backend хранит не более 100 страниц в памяти процесса. Свежая страница
живёт до 30 секунд; при переполнении вытесняется давно не использовавшаяся.
Ключ разделяет пользователя, поколение подключения, счёт, нормализованный
период и категории, UID инструмента, курсор и размер страницы.
Отсутствующий конец периода приводится к текущему 30-секундному интервалу.
Текущее подключение и доступ к счёту проверяются до выдачи страницы из кэша.
Полнота относится к UTC-интервалу `[from,to)`, включая подтверждённый пустой.
Категории, размер страницы и `hasNext=false` её не определяют. При первичном
импорте подтверждённые строки видны как partial; итоги ждут полного периода.
Догрузка остальной истории обозначается отдельно. Без известной нижней
границы нельзя заявить полную историю счёта.
После успешного изменения подключения страницы предыдущего поколения,
в том числе поздно завершившиеся запросы, не выдаются журналу или аналитике
дохода. Они удаляются по обычным ограничениям времени и объёма.
Локальной синхронизации и долговременного хранения операций нет.
Продолжения [общего журнала](../portfolio/all-accounts.md#общая-пагинация-операций)
имеют собственные ограничения и восстановление после истечения.
Cursor связан с пользователем, подключением, membership для `all`, фильтрами
и ревизией набора операций. Изменение строк даёт `INVALID_OPERATIONS_CURSOR`;
клиент возвращается на первую страницу, сохраняя применённые фильтры.
Coverage/status без изменения rowset cursor не ломают. Перезапуск сохраняет
опубликованные интервалы; незавершённая работа возобновляется только запросом.
## Представление и ошибки
Полный журнал и превью используют одинаковую компактную строку: логотип
или буквенная заглушка, описание, дата, инструмент и денежное движение.
Для купона и дивиденда денежная метка называет точное событие; для прочих
операций — категорию. Она показывается один раз, без повтора рядом с датой.
Исполненные купоны и дивиденды показываются единым предметным представлением
выплаты — тем же, что на странице дохода, в прогнозе и графиках инструментов;
денежная метка называет точное исполненное событие. Для прочих
операций метка называет категорию. Она показывается один раз, без повтора рядом с датой.
Знак и цвет обозначают направление денег; отсутствие движения нейтрально.
Если описание отсутствует, показывается локализованный тип операции.
@@ -110,7 +117,8 @@ Backend хранит не более 100 страниц в памяти проц
не заменяется каркасом при применении действий или фоновом обновлении.
Ошибки `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступный счёт (`404`)
и `TBANK_UNAVAILABLE` сохраняют [общую семантику API](../../architecture/http-api.md).
Пустой успешный ответ означает отсутствие подходящих операций, а не сбой.
Только полный пустой ответ означает отсутствие подходящих операций.
Partial empty сообщает о неполноте; source failure сохраняет прежний результат.
Согласованное поведение описано в [спецификации журнала](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md).
Фильтр конкретной бумаги и общий режим расширяют исходное описание отдельного
+37 -50
View File
@@ -51,69 +51,56 @@ Backend публикует статические маршруты под `/api/
используют прежнюю карточку. Доля относится к позициям той же валюты
во всём показанном портфеле, без денежных остатков.
- Журнал объединяет операции по убыванию времени, при равном времени использует
порядок идентификаторов счетов, сохраняя порядок внутри одного счёта. ID
порядок идентификаторов счетов и операций. ID
операций разных счетов не считаются дублями. Переводы не схлопываются.
- Доходы собираются из всех страниц операций каждого счёта за общие периоды.
Итоги, сравнения, временные интервалы и принадлежность выплат рассчитывает
сервис дохода.
Для сравнения с годом ранее используется текущий состав счетов; новый счёт
добавляет только реально полученные выплаты. При известных датах открытия всех счетов
доступность сравнения определяется самым ранним открытием в этом составе.
Закрытые счета в историческое сравнение не добавляются.
- Доходы собираются из всех страниц операций каждого счёта за выбранный период.
Итоги, временные интервалы и принадлежность выплат рассчитывает
сервис дохода. Данные сравнения периодов не загружаются и не возвращаются;
состав счетов влияет только на фактические выплаты выбранного периода.
- Будущие события сортируются по дате и сохраняют отдельную строку каждого счёта.
Прогноз не включается в реализованные доходы.
## Полнота, свежесть и доступ
Для каждого общего запроса backend определяет состав доступных счетов
и единое поколение подключения. Получение данных использует эту же границу подключения;
проверки сессии, доступа и существующий лимитер внешних запросов сохраняются.
Финансовые данные не сохраняются в БД; снимки и продолжения хранятся
в памяти процесса.
Общий запрос читает membership и разрешённые snapshots из SQLite в одной
согласованной границе. Время источников может различаться: это не обещание
одновременного наблюдения счетов у брокера. Неполный счёт делает aggregate
неполным; отсутствующие суммы не подменяются нулями. `lastSuccessfulAt`
показывает самое раннее время обязательных источников.
Ошибка источника не превращается в частичный успешный ответ раздела.
Портфель может использовать разрешённый существующей политикой запасной снимок
отдельного счёта. Тогда общий `isStale` равен `true`, а `lastSuccessfulAt`
показывает самое раннее время среди включённых снимков. Для пустого состава
возвращается корректный пустой результат. Общий запрос не является атомарным
наблюдением всех счетов в один момент времени.
Ошибка блока операций, доходов или будущих событий не скрывает основной
портфель. При фоновом обновлении страницы уже полученный результат сохраняется
с индикатором или предупреждением и повторной попыткой. Смена сессии или
подключения сбрасывает общий режим вместе с остальными финансовыми запросами.
Секции портфеля, операций, дохода и событий готовы независимо. Ошибка
обновления сохраняет прежний полный результат того же scope и периода.
Известный отказ доступа и смена подключения очищают personal view.
## Общая пагинация операций
Сервис объединяет потоки страниц каждого счёта и сохраняет непрочитанные строки
между запросами. Фильтрованные пустые страницы не обрывают поток. Повреждённая
цепочка курсоров и нарушенный порядок дат возвращают ошибку без частичной страницы.
Страница строится из локальных строк с порядком occurredAt, accountId и ID
операции. Cursor проверяет пользователя, подключение, membershipRevision,
фильтры, limit и ревизии операций. Изменение rowset возвращает
`400 INVALID_OPERATIONS_CURSOR`; клиент сбрасывает стек страниц, сохраняя
применённые фильтры. Coverage/status без изменения строк не ломают cursor.
Продолжение не зависит от RAM-кэша или перезапуска.
Курсор общего журнала — случайный непрозрачный идентификатор продолжения,
привязанного к пользователю, поколению подключения, составу счетов, фильтрам
и размеру страницы. Верхняя граница периода фиксируется первым запросом.
Состояние ограничено 100 продолжениями на процесс со сроком жизни пять минут;
повторный запрос того же продолжения возвращает тот же успешный результат.
Визуальная группировка строк одного инструмента в общих списках выполняется только
в пределах уже полученного списка: в журнале — на загруженной странице, без переноса группы
на следующую; в полном списке будущих событий и выплатах дохода — по всем полученным строкам
периода. Группировка не меняет порядок, cursor или ответ API. Протокол курсора прежний.
Правила группировки — в [журнале](../operations/overview.md#группировка-в-режиме-все-счета).
Неизвестный, вытесненный, истёкший либо несовместимый курсор возвращает
`400 INVALID_OPERATIONS_CURSOR`. Действие «Повторить» сбрасывает пагинацию,
обновляет первую страницу и сохраняет фильтры. Перезапуск backend также
инвалидирует продолжения. Раздел рассчитан на один экземпляр backend, как
существующий финансовый кэш.
Повторная сверка полного показанного периода публикует все счета и интервалы
одной транзакцией; успешное первое окно не смешивается с прежними остальными.
Подробнее — [журнал](../operations/overview.md#получение-и-полнота).
## Совместимость и границы требований
Общий режим добавляет маршруты, сохраняя маршруты отдельных счетов.
При совместном обновлении сначала нужен backend, затем frontend с
соответствующим сгенерированным контрактом; прежний клиент продолжает
работать с отдельными счетами. При откате сначала возвращается клиент.
Этот механизм не требует изменения схемы SQLite и миграции финансовых данных.
Общий режим использует те же persistent sources и envelope, что и отдельный
счёт. Новый envelope несовместим с прежним bare response: backend и frontend
поставляются согласованной парой по [HTTP contract](../../architecture/http-api.md).
Additive migration сохраняет прежние таблицы; rollback приложения не удаляет
новые данные и не заменяет рабочую базу старым backup. Возврат требует заранее
проверенной совместимой пары; произвольный прежний клиент не считается совместимым.
Процедура backup/restore описана в [runbook](../../architecture/deployment.md).
Основные спецификации [портфеля](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md),
[операций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-ledger/spec.md),
[дохода](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-income-analytics/spec.md)
и [событий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/brokerage-account-upcoming-events/spec.md)
описывают отдельный счёт. Общий режим подтверждён действующими маршрутизаторами,
сервисами и [проверками агрегации](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/all-accounts.spec.ts),
но не выделен в самостоятельную основную спецификацию. Не следует считать
описание общего режима изменением требований к отдельному счёту.
Основные спецификации областей сохраняют исходные понятия отдельного счёта.
Утверждённый change `persist-tbank-data` распространяет полноту, локальный
доступ, атомарную сверку и состояния синхронизации на режим `all`.
+81
View File
@@ -0,0 +1,81 @@
# Концентрация портфеля
Вкладка «Концентрация» на странице счёта `/accounts/:accountId` показывает,
какие акции, облигации, секторы и эмитенты занимают наибольшую часть текущей
оценки Portfolio. Она отвечает на вопрос «где сконцентрированы деньги»,
который не решают карточки позиций и [структура
активов](overview.md#как-читать-экран). Точный смысл полей и долей — в
[справочнике данных](data-reference.md).
## Вкладка и разрезы
Вкладка есть только у отдельного счёта: в режиме [«Все счета»](all-accounts.md)
её нет. Появление вкладки не меняет вкладку, открытую по умолчанию («Позиции»),
и содержание остальных вкладок. Пока Portfolio загружается, при его ошибке или
штатном пустом составе вкладки нет; при Portfolio хотя бы с одной позицией или
денежным остатком вкладка показывается.
Вкладка содержит четыре разреза и показывает один из них одновременно:
| Разрез | Что ранжирует | Типы позиций |
| --- | --- | --- |
| «Акции» | Пять крупнейших позиций акций | `STOCK` |
| «Облигации» | Пять крупнейших позиций облигаций | `BOND` |
| «Секторы» | Пять крупнейших секторов по данным T-Bank | `STOCK`, `BOND`, `FUND` |
| «Эмитенты» | Пять крупнейших эмитентов (брендов актива) | `STOCK`, `BOND` |
Фонды участвуют только в разрезе «Секторы»: их эмитент — управляющая компания,
а не компания внутри фонда. Позиции типов `CURRENCY`, `FUTURE`, `OPTION`,
`STRUCTURED_NOTE`, `DIGITAL_FINANCIAL_ASSET` и `OTHER` во вкладку не входят.
Позиции без сектора образуют явную группу «Сектор не указан», без бренда —
«Эмитент не указан»; обе группы показываются с видимой долей. Государственные
облигации определяются значением сектора T-Bank `government` с подписью
«Государственный сектор»; других собственных классификаций WealthLedger
не вводит. Название элемента в разрезе эмитентов — название бренда; если у
одного бренда разные названия, показывается название позиции с наибольшей
стоимостью.
## Топ-5 и «Прочее»
В каждом разрезе показаны пять крупнейших элементов по текущей денежной оценке,
каждый с суммой и долей. Рядом показан суммарный показатель «Топ-5 · N %».
Если в разрезе семь и более элементов, все остальные объединены в одну строку
«Прочее» с числом элементов, суммой и долей. При шести и меньшем числе
элементов каждый показан своей строкой. По нажатию на «Прочее» раскрывается
полный перечень остальных элементов, каждый со своей суммой и долей; топ-5
и диаграмма при этом не меняются.
Доли считаются от суммы стоимостей позиций разреза в выбранной валюте и
в сумме дают ровно 100 %; округление — до десятой доли процента, очень малые
доли показываются как «< 0,1 %». Отрицательные и нулевые стоимости, а также
позиции без оценки в разрез не входят. Конвертации валют нет: каждая позиция
входит только в валюту своей оценки.
## Валюта
У вкладки свой список валют — валюты оценок позиций акций, облигаций и фондов,
отсортированные по ISO-коду, — и свой выбор, независимый от «Структуры
портфеля». По умолчанию выбрана первая валюта списка. При одной валюте
переключатель не показывается. Общего итога по нескольким валютам нет.
Выбранные разрез и валюта сохраняются, пока пользователь остаётся на странице
счёта, включая переключение её вкладок, и сбрасываются при новом заходе и при
смене счёта.
## Диаграмма и неполные данные
Рядом с перечнем показана кольцевая диаграмма, согласованная с перечнем;
информация передаётся не только цветом, есть текстовый эквивалент. Если ответ
отмечен как неполный (`completeness: "incomplete"` — часть позиций не имеет
оценки), над разрезом появляется предупреждение «Данные неполные», а под
списком — сноска «Не учтено позиций: N» с числом позиций, не вошедших в разрез.
Пустой разрез (нет позиций с положительной оценкой в выбранной валюте)
показывает текстовое состояние вместо диаграммы.
## Откуда берутся сектор и эмитент
Сектор и эмитент — часть данных Portfolio: их получает backend в часовом
обходе и сохраняет в снимке вместе с остальными полями позиций
([поток загрузки](flows/portfolio-loading.md#обновление-сектора-и-эмитента),
[интеграция T-Bank](../../integrations/tbank.md)). Вкладка считается по уже
загруженному снимку и не обращается к T-Bank.
+7 -3
View File
@@ -32,7 +32,9 @@
| Cash Balance | Суммы из `GetPositions.money`, объединённые по валюте | Заблокированные деньги не прибавляются; торгуемый инструмент `CURRENCY` остаётся позицией |
| `totalsByCurrency` | Стоимости позиций + доступные деньги той же валюты | Не включает невычислимую стоимость; не обещает полную оценку портфеля |
| `lastSuccessfulAt` | Время формирования успешного снимка, UTC | Не является временем каждой котировки; запасной снимок сохраняет исходное время |
| `isStale` | Backend вернул предыдущий полный снимок после временной ошибки | Не описывает состояние клиентского кэша или ошибку фонового запроса |
| `isStale` | Истёк часовой срок свежести сохранённого portfolio source | Не описывает состояние клиентского кэша или ошибку фонового запроса |
| `sector` | Сектор инструмента в том виде, как его определяет T-Bank, с обрезанными пробелами | Только для позиций `STOCK`, `BOND` и `FUND`; значение из одних пробел считается отсутствующим. Во всех остальных случаях — `null` |
| `issuer` | Бренд актива инструмента по данным T-Bank: `{ id, name }` | Только для позиций `STOCK`, `BOND` и `FUND`, когда у актива известны uid и название бренда. Во всех остальных случаях — `null`. Не влияет на количество, стоимость и свежесть снимка; используется вкладкой [«Концентрация»](concentration.md) |
Ненулевые денежные остатки одной валюты суммируются. Позиции одинаковых
инструментов в ответе отдельного счёта не объединяются; визуальная группировка
@@ -56,8 +58,10 @@ UID определяет конкретный инструмент брокер
ISIN, тикер и `logoName` могут быть `null`. `logoName` — имя файла бренда,
а не URL: frontend строит ссылку на публичный CDN. Ошибка изображения
или метаданных не меняет количество, суммы и свежесть снимка и не скрывает
финансовые данные. Имя ожидается перед завершением нового снимка;
ошибка поиска оставляет доступный идентификатор.
финансовые данные. Metadata обновляется независимо; отсутствие имени не задерживает выдачу
финансового снимка и оставляет доступный идентификатор.
`logoName` не равен эмитенту из вкладки «Концентрация»: он берётся из metadata
инструмента, а эмитент — из [классификации инструмента](flows/portfolio-loading.md#обновление-сектора-и-эмитента).
## Структура и доли
+51 -143
View File
@@ -1,158 +1,66 @@
# Загрузка портфеля
Поток начинается при открытии `/accounts/:accountId` пользователем
с действующей сессией. Его результат — полный нормализованный снимок
доступного счёта либо объяснимая ошибка. Браузер обращается к backend;
доступ к брокерскому токену и внешние запросы остаются на сервере.
Экран `/accounts/:accountId` читает последний полный snapshot из SQLite после
проверки сессии, подключения и локального membership. Если список счетов ещё
неизвестен, сервер сообщает о загрузке доступа без выдачи личных данных
неизвестного ID. Известный недоступный счёт возвращает `404`.
## Основной обмен
Сначала frontend проверяет свой успешный кэш: в течение 30 секунд повторное
открытие может показать ответ без нового HTTP-запроса. Следующая схема
описывает запрос, который действительно дошёл до backend.
Обычный GET и retry возвращают read-only локальный `{ data, meta }` envelope:
они не ждут и не вызывают T-Bank и не предлагают ручную синхронизацию. Часовой
обход текущего подключения обновляет Portfolio независимо от экрана. Для нового
полного snapshot ему нужны оба финансовых ответа `GetPortfolio` и
`GetPositions`; суммы и позиции публикуются вместе. Metadata инструмента и
классификация (сектор и эмитент) дополняют представление независимо,
с доступным UID/FIGI вместо имени до их загрузки.
```mermaid
sequenceDiagram
participant F as Frontend
participant B as Backend API
participant D as SQLite
participant T as T-Bank
F->>B: Запрос портфеля счёта с Bearer token
B->>B: Проверяет сессию
B->>D: Читает текущее подключение пользователя
D-->>B: Подключение либо отсутствие
alt Подключения нет
B-->>F: 409 TBANK_NOT_CONNECTED
else Подключение доступно
B->>B: Определяет поколение подключения
alt Снимок того же поколения не старше 30 секунд
B-->>F: Полный снимок, isStale=false
else Нужен новый снимок
B->>T: Получает открытые счета
T-->>B: Состав доступных счетов
alt Счёт недоступен
B-->>F: 404
else Счёт доступен
par Финансовые позиции
B->>T: GetPortfolio
T-->>B: Позиции и цены
and Доступные деньги
B->>T: GetPositions
T-->>B: Денежные остатки
end
B->>T: Метаданные отсутствующих инструментов по UID, затем FIGI
T-->>B: Метаданные, отсутствие или ошибка
B->>B: Нормализует состав и точно вычисляет суммы
B-->>F: Полный новый снимок, isStale=false
end
end
end
```
## Обновление сектора и эмитента
Свежий серверный снимок выдаётся после разрешения текущего сохранённого
подключения, но без нового `GetAccounts`. После истечения свежего кэша
проверка доступности счёта предшествует финансовым RPC. SQLite хранит
подключение, а не снимок или право владения счётом вне этого подключения.
Часовой обход также обновляет сектор и эмитента позиций типов `share`, `bond`
и `etf`. Значения хранятся как последнее известное ([ADR
022](../../../adr/022-stored-instrument-classification.md)): сектор — как его
определяет T-Bank, эмитент — бренд актива (`uid` и `name`). Каждое поле
обновляется при первом обходе, начавшемся не ранее чем через 24 часа после его
последнего успешного получения; пока значение не получено, попытка повторяется
в каждом обходе. Сектор и эмитент обновляются независимо.
Для нового снимка требуются оба финансовых ответа. Backend не выдаёт
позиции без денег или деньги без позиций как полный портфель.
Метаданные дополняют имя и идентификаторы, но их временная недоступность
не срывает финансовый результат. Поиск выполняется партиями по пять позиций,
чтобы крупный портфель не переполнял очередь одновременными запросами.
Порядок позиций сохраняется. Backend ищет сведения по UID, затем по FIGI,
если UID не дал пригодного названия или тикера.
Ожидание метаданных ограничено 30 секундами на снимок счёта. После этого
используется доступный кэш либо идентификатор, новые поиски для снимка
не запускаются. Уже начатые запросы могут закончиться позже и обновить общий
справочный кэш, но не меняют выданный финансовый снимок.
Общий серверный клиент обеспечивает TLS, очередь,
ограничение частоты, deadline RPC и разрешённые повторные попытки —
[граница интеграции](../../../integrations/tbank.md).
Неуспешное получение не мешает публикации snapshot и не меняет
`lastSuccessfulAt` и `isStale`; сохранённое значение остаётся доступным.
Исключение — отказ token: он обрабатывается так же, как отказ token при
обходе Portfolio. Инструмент ищется по UID, затем по FIGI; ответ «не найдено»
сохраняет прежнее значение и отсчёт 24 часов начинается заново. Позиции без
полученных значений участвуют в разрезах вкладки
[«Концентрация»](../concentration.md) как «Сектор не указан» и
«Эмитент не указан».
## Время и изменение подключения
Открытый экран выполняет локальное перечитывание не чаще одного раза в
30 минут; retry также повторяет только HTTP GET. `data: null` означает, что
полного snapshot ещё нет, а не нулевой портфель. Пустой подтверждённый portfolio
остаётся available complete result. Секции могут быть готовы независимо.
Подробнее — [свежесть](../../../architecture/cross-cutting/data-freshness.md).
| Слой | Что сохраняется | Условие повторного использования |
| --- | --- | --- |
| Frontend | Последний успешный HTTP-ответ для выбранного счёта и сессии | Свежим считается 30 секунд; при обычном обновлении уже показанный ответ остаётся видимым |
| Backend | Полный снимок пары пользователь–счёт в памяти процесса | До 30 секунд, только в том же поколении текущего подключения |
| Backend при временной ошибке | Тот же полный снимок | Не старше пяти минут с момента успешного сохранения, только в том же поколении |
| Метаданные инструмента | Успешный поиск или подтверждённое отсутствие | Свежими считаются 24 часа; общий кэш на 500 ключей UID/FIGI, ошибки не сохраняются |
## Границы доступа
Это параметры текущей реализации, а не самостоятельная политика этого
документа: [снимки](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/modules/investments/portfolio/application/portfolio.service.ts),
[метаданные](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/integrations/tbank/instrument-metadata.service.ts) и
[клиентский запрос](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/src/entities/portfolio/api/usePortfolio.ts).
Пять минут — предельный возраст успешного снимка, а не дополнительные
пять минут после окончания свежих 30 секунд. Запасной ответ сохраняет
исходное `lastSuccessfulAt` и получает `isStale: true`.
После истечения 24 часов backend обновляет справочные данные. Если обновление
временно недоступно, ранее успешные сведения остаются запасным вариантом,
пока запись есть в ограниченном кэше процесса. Ошибка не продлевает срок
свежести: следующий поиск снова пытается обновить запись. Это относится только
к названию, тикеру, ISIN и логотипу, а не к ценам или денежным значениям.
Перезапуск backend и вытеснение записи из кэша удаляют этот запасной вариант.
Неполный ответ без названия и тикера также не затирает известные сведения
и не обновляет срок их свежести. Подтверждённое отсутствие инструмента
заменяет прежнюю запись отрицательным результатом поиска.
Время снимка фиксируется при получении финансовых ответов, до ожидания
названий. Сроки серверного кэша отсчитываются от этого времени, поэтому
долгая загрузка справочных данных не делает старые суммы новыми.
Успешные справочные данные общие для всех пользователей; выполняющиеся
запросы объединяются только внутри одного брокерского токена, чтобы ошибка
его авторизации не передавалась другому подключению.
Успешное создание, замена или удаление подключения меняет границу
повторного использования финансовых данных. Старое или поздно завершившееся
получение снимка не делает его доступным новым запросам другого поколения.
Явная очистка памяти при изменении подключения дополняет эту проверку.
Frontend сбрасывает всё поддерево финансовых запросов при изменении
подключения и общий кэш сессии при выходе; старый портфель не служит
заглушкой загрузки нового подключения.
В общем режиме сначала определяется состав доступных счетов одного
подключения, затем получаются их снимки. Это
[не атомарное наблюдение нескольких счетов](../all-accounts.md#полнота-свежесть-и-доступ).
Публикация проверяет connection revision и attempt в той же транзакции. Замена
или удаление подключения не допускает позднюю запись прежней работы. Frontend
очищает недоступный personal view и отклоняет запоздалые ответы. В режиме
[«Все счета»](../all-accounts.md#полнота-свежесть-и-доступ) состав accounts и
snapshots читается согласованно, но их source times могут различаться.
## Ошибки и результат для пользователя
| Причина | Ответ backend | Действие на экране |
| --- | --- | --- |
| Нет подключения | `409 TBANK_NOT_CONNECTED` | Открыть настройки T-Bank |
| Брокер отклонил токен | `400 INVALID_TBANK_TOKEN` | Открыть настройки; токен автоматически не удаляется |
| Счёт недоступен через текущее подключение | `404` | Вернуться к списку счетов |
| Временная ошибка, пригодный предыдущий снимок есть | `200`, `isStale: true` | Показать снимок и его исходное время с предупреждением |
| Временная ошибка, пригодного снимка нет | `503 TBANK_UNAVAILABLE` | Повторить загрузку |
| Неизвестная ошибка | Безопасная ошибка API | Повторить загрузку |
Нет подключения — `409 TBANK_NOT_CONNECTED`, известный отказ токена —
`400 INVALID_TBANK_TOKEN`, недоступный счёт — `404`. Они не маскируются
запасным snapshot. Временная ошибка часового обхода оставляет прежний полный
portfolio и его исходное `lastSuccessfulAt`; `meta.lastAttempt` сообщает о
failed или partial попытке отдельно. `Portfolio.isStale` сообщает возраст
snapshot, а Client Cached Snapshot и ошибка браузерного HTTP-перечитывания
имеют независимый смысл. При отсутствии полного snapshot `data: null` и
metadata обозначает initial loading либо временную недоступность, без нулевых
итогов. `503` означает ошибку обслуживания самого HTTP-запроса, а не
завершившуюся фоновую работу.
`INVALID_TBANK_TOKEN` и `404` не заменяются серверным запасным снимком.
Временная ошибка определения доступных счетов также может использовать
пригодный снимок того же поколения. При временном сбое метаданных сначала
используются ранее успешные сведения, а при их отсутствии — идентификатор
инструмента; это не устанавливает `isStale`. Отказ авторизации при поиске
метаданных не маскируется запасным названием.
Для запроса самого портфеля на клиенте нет polling, автоматического повтора или
обновления при возврате фокуса. При обновлении того же уже показанного
результата клиент оставляет его на экране, добавляя индикатор загрузки
или предупреждение с доступной с клавиатуры повторной попыткой.
Это клиентский сохранённый ответ, независимо от серверного `isStale`.
Frontend отображает название и остальные сведения из ответа портфеля.
Повторные попытки поиска для акций, облигаций и других типов инструментов
принадлежат [общему клиенту T-Bank на backend](../../../integrations/tbank.md#конфигурация-и-ограничения).
Первое обращение к крупному портфелю может потребовать много метаданных.
Переполнение общей очереди RPC может произойти при конкурирующих запросах;
поиск метаданных в этом случае повторяется с ограниченными паузами. После
успешного поиска метаданные используются повторно до истечения
кэша. Значение `TBANK_RATE_LIMIT_PER_SECOND` следует соотносить с квотой
источника; его увеличение не является гарантированным исправлением
ограничения самого T-Bank.
Проверяемые правила: [свежесть и запасной снимок](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-свежесть-и-fallback-полного-portfolio-snapshot),
[клиентские состояния](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-состояния-и-ошибки-portfolio-на-клиенте).
Для расследования конкретного значения используйте
[порядок диагностики](../how-to/investigate-discrepancy.md).
Денежные правила находятся в [справочнике](../data-reference.md), проверка
расхождения — в [диагностике](../how-to/investigate-discrepancy.md), RPC/лимиты
— в [интеграции T-Bank](../../../integrations/tbank.md).
@@ -12,12 +12,13 @@
величины. Убедитесь, что сравниваемые значения относятся к одному составу
счетов и инструменту. Название или тикер сами по себе не доказывают
совпадение выпуска; используйте доступные UID, FIGI и ISIN.
2. **Сопоставьте время.** Проверьте `lastSuccessfulAt`, `isStale` и наличие
предупреждения о клиентском обновлении. Серверный снимок не обещает время
2. **Сопоставьте время.** Проверьте `lastSuccessfulAt`, `isStale` и показанный
исход последней часовой попытки. Серверный снимок не обещает время
каждой котировки, а общий портфель может соединять разные моменты.
Если запрос завершился ошибкой, доступное действие «Повторить» должно
обновить результат или оставить объяснимую ошибку. Перезагрузка backend
удаляет кэш и не восстанавливает историю стоимости.
Действия «Повторить» на экране нет: временный сбой чтения восстанавливается
автоматически, поэтому расхождение проверяйте по показанному состоянию
источника, а не по результату ручного обновления. Перезагрузка backend
сохраняет последний полный снимок в SQLite, но не создаёт историю стоимости.
3. **Проверьте состав суммы.** Для одной валюты итог складывается из
вычислимых стоимостей позиций и доступных денег. Не добавляйте деньги
второй раз, не складывайте валюты и не включайте заблокированные средства.
+23 -14
View File
@@ -17,11 +17,11 @@
[справочнике данных](data-reference.md).
Доступность счёта определяется [текущим подключением и списком счетов](../accounts/overview.md).
SQLite не хранит сам портфель: backend получает данные через серверную
[интеграцию T-Bank](../../integrations/tbank.md) и временно сохраняет успешный
снимок в памяти. Frontend получает нормализованный результат через HTTP API.
Backend получает данные через серверную
[интеграцию T-Bank](../../integrations/tbank.md) и постоянно сохраняет последний
полный снимок в SQLite. Frontend получает нормализованный результат через HTTP API.
[Поток загрузки](flows/portfolio-loading.md) объясняет границы доступа,
свежий кэш и восстановление после временной ошибки.
свежесть снимка и восстановление после временной ошибки.
Обзор счёта также показывает пять последних [операций](../operations/overview.md),
[полученный доход](../income/overview.md) текущего месяца и до пяти
@@ -64,6 +64,10 @@ SQLite не хранит сам портфель: backend получает да
без логотипа и с подписью результата. Отсутствующие показатели скрываются,
а не подменяются нулём или заглушкой.
Рядом с вкладкой «Позиции» на странице отдельного счёта есть вкладка
[«Концентрация»](concentration.md): она показывает, где сконцентрированы
деньги — крупнейшие бумаги, секторы и эмитенты. В режиме «Все счета» её нет.
Существует расхождение: [согласованная спецификация представления позиций](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/portfolio-view/spec.md#requirement-представление-карточек-позиций-на-клиенте)
требует доступности всех иных типов в отдельной секции, а текущие frontend
и его проверка исключают `CURRENCY` только из списка карточек. Такие позиции
@@ -79,18 +83,23 @@ SQLite не хранит сам портфель: backend получает да
не содержат данных и не получают фокус. При обычном фоновом обновлении
сохраняются предыдущие суммы и время успешного получения в UTC.
Серверный `isStale` обозначает разрешённый запасной снимок backend.
Клиентское предупреждение означает неудачную попытку обновить уже показанный
ответ. Эти состояния независимы и могут появиться одновременно.
Успешный повторный запрос обновляет суммы и время, снимает клиентскую ошибку;
серверное предупреждение определяется новым `isStale`.
Серверный `isStale` обозначает возраст снимка старше часа: обновлением
владеет часовой обход, а не запрос экрана. Он показывается рядом с суммами
вместе с временем последнего успешного результата и исходом последней часовой
попытки. Неудачная попытка клиента обновить уже показанный ответ о себе не
сообщает: эти состояния независимы, но пользователю видимо только состояние
источника. Успешное автоматическое перечитывание обновляет суммы и время;
обозначение устаревания определяется новым `isStale`.
Правила времени и ошибок приведены в [потоке загрузки](flows/portfolio-loading.md).
В текущем решении нет долговременной истории снимков, графиков стоимости
портфеля, валютной конвертации, учёта виртуальных позиций или заблокированных
средств. Кэш теряется при перезапуске и рассчитан на локальную работу одного
экземпляра backend. Отдельной команды принудительного обновления на экране
нет; повторная попытка доступна после ошибки.
Отдельного чтения истории стоимости нет, но каждый полный снимок
сохраняет точку оценки по счёту и валюте для будущих поддерживаемых
представлений; публичного графика в текущем решении нет. Нет также
валютной конвертации, учёта виртуальных позиций или заблокированных
средств. Последний полный снимок сохраняется после перезапуска;
обновлением личных данных владеет часовой обход, рассчитанный на один
экземпляр backend. Действие «Обновить» выполняет только локальное
перечитывание открытого экрана и не запускает работу с T-Bank.
Для проверки конкретной суммы используйте
[руководство по расхождениям](how-to/investigate-discrepancy.md).
+82 -2
View File
@@ -4,6 +4,14 @@
Связи между ними объясняет [обзор продукта](overview.md), подробные правила —
[предметные области](README.md#предметные-области).
## Релиз развёртывания WealthLedger
Версия комплекта установки WealthLedger, закрепляющая проверенную совместимую
пару релизов frontend и backend. Понятие принято в
[ADR 016](adr/016-versioned-deployment-bundle.md).
_Не использовать_: релиз frontend или backend как синоним релиза всей установки.
## User
Локальный владелец данных WealthLedger, идентифицируемый `id` и
@@ -111,6 +119,46 @@ Instrument. Она остаётся в `positions` и не объединяет
_Не использовать_: денежный остаток, cash balance.
## Концентрация
Вкладка страницы одного Brokerage Account, показывающая, какие акции,
облигации, секторы и эмитенты занимают наибольшую часть текущей оценки
Portfolio, без валютной конвертации. Отличается от [структуры
портфеля](domains/portfolio/data-reference.md#структура-и-доли): в её разрезах
деньги не входят в знаменатель долей.
_Не использовать_: структура портфеля, диверсификация.
## Разрез
Одна из четырёх группировок вкладки «Концентрация»: «Акции», «Облигации»,
«Секторы», «Эмитенты». Элемент разреза — единица ранжирования; сектор
агрегирует позиции `STOCK`, `BOND` и `FUND`, эмитенты — `STOCK` и `BOND`.
_Не использовать_: фильтр, категория структуры.
## Сектор
Значение сектора инструмента в том виде, как его определяет T-Bank, без
перекодирования и собственной классификации WealthLedger. Заполняется для
позиций `STOCK`, `BOND` и `FUND`; государственные облигации определяются
значением `government` с подписью «Государственный сектор». Позиция без
сектора попадает в явную группу «Сектор не указан».
_Не использовать_: отрасль, собственная классификация приложения.
## Эмитент
В разрезе «Эмитенты» вкладки «Концентрация» — бренд актива по данным T-Bank,
объединяющий акции и облигации одной компании; фонды в разрез не входят.
Название эмитента — название бренда. **Оговорка:** в этом разрезе слово
означает бренд актива T-Bank, а не юридического эмитента ценной бумаги;
[рейтинг выпуска](integrations/nkr.md) не подставляется вместо рейтинга
эмитента и наоборот. Позиция без бренда попадает в явную группу
«Эмитент не указан».
_Не использовать_: юридический эмитент, эмитент рейтинга.
## Instrument
Ценная бумага или иной торгуемый актив T-Bank, на который ссылается Portfolio
@@ -120,6 +168,14 @@ Instrument обогащают отображение Position или Event, но
_Не использовать_: бумага, тикер, актив позиции.
## Скринер акций
Подбор выпусков акций по заданным условиям из каталога T-Bank с указанием
времени получения списка. Скринер отличается от текстового поиска конкретного
инструмента, личного портфеля и подтверждения исполнимости сделки.
_Не использовать_: гарантия покупки, личная проверка доступа к сделке.
## Instrument Logo
Имя файла бренда T-Bank для отображения Instrument в интерфейсе;
@@ -144,6 +200,15 @@ _Не использовать_: валютная позиция, заблоки
_Не использовать_: история портфеля, snapshot для графика.
## Точка истории стоимости портфеля
Полная оценка одного Brokerage Account в одной валюте на момент успешного
обновления Portfolio; последовательность таких точек описывает изменение
оценки во времени, включая неторговые дни. Не является Portfolio Snapshot
или инвестиционной доходностью; отсутствие цены позиции не приравнивается к нулю.
_Не использовать_: снимок состава портфеля, доходность.
## Client Cached Snapshot
Технический термин: последний успешно полученный frontend-ответ React Query, который остаётся
@@ -169,6 +234,19 @@ _Не использовать_: история Portfolio, баланс счёт
_Не использовать_: проводка, рассчитанная операция.
## Полнота истории счёта
Подтверждённость того, что за указанный период загружены все доступные
исполненные операции счёта на момент успешной сверки с брокером. Полный период
может не содержать операций; полнота не гарантирует отсутствия последующих
исправлений брокера и отличается от свежести данных.
Понятие принято в [ADR 017](adr/017-local-tbank-reads-and-background-sync.md).
Текущее хранение coverage и независимая свежесть объяснены в
[модели данных](architecture/cross-cutting/data-freshness.md).
_Не использовать_: свежесть истории, отсутствие операций как признак полноты.
## Operation Category
Нормализованная категория Ledger Entry для фильтрации и отображения:
@@ -178,13 +256,15 @@ _Не использовать_: upstream operation type как category.
## Ledger Page Cache
Технический термин: кэш страницы Account Ledger, отделённый от кэша отдельных операций.
Исторический технический термин: RAM-кэш страницы Account Ledger.
Текущее чтение использует сохранённые операции и coverage в SQLite;
страница не является единицей полноты.
_Не использовать_: operations cache, кэш отдельных операций.
## Ledger Page Cache Key
Ключ, разделяющий страницы журнала по пользователю, текущему подключению,
Исторический ключ RAM-кэша, разделявший страницы по пользователю, подключению,
счёту и параметрам запроса, включая необязательный UID инструмента.
Полный состав и правила нормализации описаны в
[журнале операций](domains/operations/overview.md#получение-и-полнота).
+33 -13
View File
@@ -14,12 +14,27 @@ WealthLedger обращается к Invest API на сервере по gRPC ov
| Портфель и денежные остатки | Показать текущий состав инвестиций и итоги по исходным валютам |
| Исполненные операции | Построить журнал и выделить полученные купоны и дивиденды |
| Инструменты и рынок | Имена, идентификаторы, цены, свечи, параметры акций и облигаций |
| Классификация инструментов | Сектор акций, облигаций и фондов и бренд актива для разрезов [концентрации](../domains/portfolio/concentration.md) |
| Купоны и дивиденды | Построить будущие события по текущим позициям и календарь инструмента |
Сектор получается из описаний инструмента (отдельные описания для акций,
облигаций и фондов, поиск по UID, затем по FIGI); эмитент — из описания актива
по `asset_uid` инструмента: его uid и название бренда. Эмитент не выводится из
названия, тикера или ISIN. Классификация хранится как последнее известное
значение с обновлением не чаще раза в сутки ([ADR
022](../adr/022-stored-instrument-classification.md)); все вызовы проходят
через общий лимитер запросов T-Bank.
T-Bank определяет исходные данные и права token. Backend проверяет локальную
сессию, выбирает подключение её пользователя и устанавливает доступ к счёту.
Наличие идентификатора счёта в URL этого доступа не подтверждает.
Скринер акций получает полный online-каталог акций T-Bank для каждого нового
поиска. В него входят только инструменты, для которых каталог подтверждает и
доступность покупки, и торговли через API. Эти признаки описывают полученный
снимок каталога: они не подтверждают личные права пользователя, состояние торгов
или исполнение будущей заявки. Скринер не использует личный портфель как каталог.
[MOEX](moex.md) и [НКР](nkr.md) дополняют публичные сведения об облигациях; они не заменяют
брокера как источник личных позиций и исполненных выплат. Границы источников
показаны в [контексте](../architecture/context.md), смысл дополнений — в
@@ -35,7 +50,9 @@ Backend передаёт имя файла `logoName` из метаданных
Логотипы используются в портфеле, операциях, будущих событиях, поиске и
страницах инструментов. Отсутствующее имя или ошибка изображения оставляют
буквенную заглушку и не меняют финансовые значения. Правило построения URL
буквенную заглушку и не меняют финансовые значения. Логотип — это metadata
инструмента; он не равен эмитенту из [концентрации](../domains/portfolio/concentration.md),
который берётся из бренда актива. Правило построения URL
принадлежит [форматированию frontend](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/src/shared/lib/display.ts).
## Проверка и сохранение подключения
@@ -62,7 +79,7 @@ sequenceDiagram
B->>T: GetAccounts OPEN по gRPC over TLS
alt Проверка успешна, в том числе при пустом списке счетов
T-->>B: Доступные открытые счета
B->>D: Создать или заменить шифротекст AES-256-GCM
B->>D: Атомарно сохранить token, новую revision и membership
B-->>F: Подключено и время сохранения
F->>F: Обновить статус и сбросить финансовые выборки
F-->>U: Показать подтверждённое подключение
@@ -91,9 +108,15 @@ sequenceDiagram
`INVALID_TBANK_TOKEN` означает отказ внешней аутентификации или прав доступа.
`TBANK_UNAVAILABLE` сообщает о временной или иной upstream-ошибке, включая
исчерпание очереди. Ответы и ошибки не содержат token. Удаление подключения
идемпотентно: удаляется локальный секрет и прекращается его использование
для последующих запросов. Оно не описывает отзыв token на стороне T-Bank.
исчерпание очереди. Диагностические причины жизненного цикла часовой попытки
(`INTERRUPTED` — прерывание до нормального завершения, `DEADLINE` — истечение
ограничения ожидания шага) не относятся к ошибкам T-Bank: они остаются
внутренней диагностикой и не раскрываются в hourly-read. Истечение ограничения
ожидания не отменяет уже начатые обращения к T-Bank; их поздние результаты
остаются в строках шагов. Ответы и ошибки не содержат token. Удаление подключения
идемпотентно: удаляются локальный секрет, личные datasets и work; поздняя
публикация прежней revision запрещена. Общие неперсональные источники могут
сохраняться. Оно не описывает отзыв token на стороне T-Bank.
Согласованное поведение сохранено в workspace-спецификации
[T-Bank connection](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/tbank-connection/spec.md).
@@ -106,7 +129,7 @@ sequenceDiagram
использования финансовых данных. Это относится и к сохранению того же самого
token: предыдущее подключение не становится источником данных нового.
Неуспешная операция сохраняет прежнее подключение и его доступные данные.
Механизм generation, поздние ответы и сброс клиентских выборок объясняет
Механизм внутренней revision, поздние ответы и сброс клиентских выборок объясняет
[свежесть данных](../architecture/cross-cutting/data-freshness.md#смена-подключения).
## Конфигурация и ограничения
@@ -129,15 +152,12 @@ Backend хранит token в шифротексте AES-256-GCM с новым n
[validateEnv](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/config/env.validation.ts) (справочный источник).
Общий FIFO-лимитер ограничивает поток RPC и не допускает более 50
ожидающих. Для `UNAVAILABLE`, `DEADLINE_EXCEEDED` и `RESOURCE_EXHAUSTED`
обычный RPC допускает один повтор через секунду. Deadline каждой попытки — 15 секунд,
каждый RPC допускает один повтор через секунду. Deadline каждой попытки — 15 секунд,
а ожидание места в лимитере не является временем этой сетевой попытки.
Для поиска метаданных `GetInstrumentBy` разрешены две повторные попытки
с паузами 2 и 4 секунды после неудачных попыток. Они применяются к тем же
временным кодам и к переполнению локальной очереди; перед каждой попыткой
проверяется её вместимость. Постоянные ошибки и отказ авторизации
Метаданные используют ту же retry policy. Переполненная очередь завершается
ошибкой сразу, без повторов admission. Постоянные ошибки и отказ авторизации
не повторяются. Подтверждённый `NOT_FOUND` при поиске метаданных означает
отсутствие инструмента, а не временный сбой. Остальные RPC при полной
очереди завершаются сразу.
отсутствие инструмента, а не временный сбой.
Параметры попыток принадлежат
[T-Bank client](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/src/integrations/tbank/tbank.client.ts) (справочный источник);
решение об ограниченных повторах — [ADR 005](../adr/005-tbank-upstream-resilience.md).
+2 -1
View File
@@ -20,11 +20,12 @@ WealthLedger — локальное приложение для частного
| Найти свой инвестиционный счёт | [Список счетов](domains/accounts/overview.md), их типы и доступные сводки |
| Вести банковские карты | [Ручной учёт](domains/cards/overview.md): независимые балансы, расходы, пополнения, переводы, возвраты и месячные сводки |
| Понять текущий состав инвестиций | [Портфель](domains/portfolio/overview.md): позиции, денежные остатки и итоги по валютам |
| Увидеть, где сконцентрированы деньги | [Концентрация](domains/portfolio/concentration.md): крупнейшие бумаги, секторы и эмитенты счёта |
| Разобрать уже совершённые движения | [Журнал операций](domains/operations/overview.md): исполненные операции с фильтрами и последовательной загрузкой истории |
| Посчитать полученные выплаты | [Пассивный доход](domains/income/overview.md): исполненные купоны и дивиденды за календарный период |
| Посмотреть ожидаемые события | [Будущие выплаты](domains/income/upcoming-events.md): купоны, дивиденды, погашения и оферты по текущим позициям |
| Увидеть общий состав инвестиций | Режим [«Все счета»](domains/portfolio/all-accounts.md), сохраняющий принадлежность строк к счетам |
| Изучить конкретный инструмент | Поиск и страницы [акций](domains/instruments/stocks.md) и [облигаций](domains/instruments/bonds.md): рынок, личная позиция, история и события |
| Изучить конкретный инструмент | [Скринер акций](domains/instruments/stocks.md) по параметрам каталога, страницы акций и поиск [облигаций](domains/instruments/bonds.md): рынок, личная позиция, история и события |
## Как связаны представления
+431 -1070
View File
File diff suppressed because it is too large Load Diff
+22 -13
View File
@@ -1,29 +1,38 @@
{
"name": "wealth-ledger-docs",
"version": "0.1.1",
"version": "0.1.8",
"private": true,
"engines": {
"node": "24.21.0"
"node": "24.22.1"
},
"scripts": {
"lint": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
"lint": "markdownlint --dot --rules .markdownlint-rule-relative-links.cjs --ignore node_modules --ignore CHANGELOG.md -- \"**/*.md\" && markdownlint --config .markdownlint-changelog.json --rules .markdownlint-rule-relative-links.cjs -- CHANGELOG.md",
"verify": "npm run lint && git diff --check",
"release:preview": "release-it --dry-run --ci",
"release:prepare": "release-it --ci",
"commitlint": "commitlint"
},
"devDependencies": {
"@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2",
"@release-it/conventional-changelog": "12.0.0",
"conventional-changelog-conventionalcommits": "10.4.0",
"markdownlint-cli2": "0.23.2",
"markdownlint-rule-relative-links": "5.1.2",
"release-it": "21.0.2"
"@commitlint/cli": "21.2.4",
"@commitlint/config-conventional": "21.2.4",
"@release-it/conventional-changelog": "12.0.2",
"conventional-changelog-conventionalcommits": "10.4.1",
"markdownlint-cli": "0.49.1",
"markdownlint-rule-relative-links": "5.1.4",
"release-it": "21.1.1"
},
"overrides": {
"markdownlint-cli2@0.23.2": {
"smol-toml": "1.8.0"
}
"fast-uri": "3.1.9",
"katex": "0.19.1",
"markdownlint-cli@0.49.1": {
"js-yaml": "5.4.2",
"minimatch": "11.0.2",
"smol-toml": "1.9.1"
},
"release-it": {
"basic-ftp": "6.2.3",
"undici": "7.30.1"
},
"markdown-it": "15.0.3"
}
}
+30 -1
View File
@@ -2,7 +2,8 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommitTypeAll(chore)"
":semanticCommitTypeAll(chore)",
"security:minimumReleaseAgeNpm"
],
"timezone": "Europe/Moscow",
"labels": [
@@ -10,6 +11,13 @@
],
"dependencyDashboard": true,
"rangeStrategy": "pin",
"prConcurrentLimit": 5,
"lockFileMaintenance": {
"enabled": true,
"schedule": [
"before 4am on the first day of the month"
]
},
"packageRules": [
{
"matchUpdateTypes": [
@@ -18,6 +26,27 @@
],
"groupName": "non-major dependencies"
},
{
"matchManagers": [
"github-actions"
],
"matchUpdateTypes": [
"minor",
"patch"
],
"groupName": "non-major github actions"
},
{
"matchManagers": [
"docker-compose",
"dockerfile"
],
"matchUpdateTypes": [
"minor",
"patch"
],
"groupName": "non-major container images"
},
{
"matchUpdateTypes": [
"major"