Compare commits

..

No commits in common. "f2fe6a4829720cd13d0435952a64e1163f618bdb" and "2af2ff32c164bf91791121e9a285591607118ac0" have entirely different histories.

13 changed files with 108008 additions and 242 deletions

2
.gitignore vendored
View File

@ -14,4 +14,4 @@ apps/docs/build/
.playwright-mcp .playwright-mcp
.opencode .opencode
dev.db dev.db
graphify-out/ graphify-out/cache/

View File

@ -83,12 +83,10 @@ export class CacheModule {}
| Позиции T-Bank | `tbankPositionsTtl` | 60s (1 мин) | `CACHE_TBANK_POSITIONS_TTL` | | Позиции T-Bank | `tbankPositionsTtl` | 60s (1 мин) | `CACHE_TBANK_POSITIONS_TTL` |
| Операции T-Bank | `tbankOperationsTtl` | 300s (5 мин) | `CACHE_TBANK_OPERATIONS_TTL` | | Операции T-Bank | `tbankOperationsTtl` | 300s (5 мин) | `CACHE_TBANK_OPERATIONS_TTL` |
| Инструменты T-Bank | `tbankInstrumentTtl` | 86400s (24 ч) | `CACHE_TBANK_INSTRUMENT_TTL` | | Инструменты T-Bank | `tbankInstrumentTtl` | 86400s (24 ч) | `CACHE_TBANK_INSTRUMENT_TTL` |
| События и прогноз выплат T-Bank | `tbankEventsTtl` | 300s (5 мин) | `CACHE_TBANK_EVENTS_TTL` |
## T-Bank cache ## T-Bank cache
`TBankModule` использует те же механики `CacheService`, но с отдельными key prefixes `TBankModule` использует те же механики `CacheService`, но с отдельными key prefixes
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations`, `tbank:events` `tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations` и `tbank:instrument`.
и `tbank:instrument`.
Это позволяет держать агрессивно короткий TTL для текущего портфеля и более длинный TTL для Это позволяет держать агрессивно короткий TTL для текущего портфеля и более длинный TTL для
справочных данных инструментов. справочных данных инструментов.

View File

@ -351,66 +351,6 @@ describe('BrokerEventsPage', () => {
expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0) expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0)
}) })
it('renders corporate events in a separate muted section', () => {
const dataWithOffer = {
...mockData,
summary: { ...mockData.summary, eventCount: 4 },
items: [
...mockData.items,
{
id: 'ev-4',
type: 'offer',
source: 'forecast',
category: 'corporate',
ticker: 'SU26238RMFS5',
name: 'ОФЗ 26238',
eventDate: '2026-07-05',
paymentDate: null,
instrumentUid: 'uid-bond-2',
instrumentType: 'bond',
quantitySnapshot: 1,
payoutPerUnit: null,
estimatedAmount: null,
actualAmount: null,
currency: null,
estimateMode: null,
},
],
}
vi.mocked(useBrokerEvents).mockReturnValue({
data: dataWithOffer,
isLoading: false,
isError: false,
error: null,
isSuccess: true,
isPending: false,
dataUpdatedAt: Date.now(),
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isInitialLoading: false,
isPaused: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
isStale: false,
refetch: vi.fn(),
promise: Promise.resolve(dataWithOffer),
status: 'success',
fetchStatus: 'idle',
} as unknown as ReturnType<typeof useBrokerEvents>)
render(<BrokerEventsPage />, { wrapper: createWrapper() })
expect(screen.getByText('Корпоративные события')).toBeInTheDocument()
expect(screen.getByText('Оферта')).toBeInTheDocument()
})
it('keeps date and type changes as draft until applying filters', async () => { it('keeps date and type changes as draft until applying filters', async () => {
mockSearchParams.set('from', '2026-06-15') mockSearchParams.set('from', '2026-06-15')
mockSearchParams.set('to', '2026-06-29') mockSearchParams.set('to', '2026-06-29')

View File

@ -329,9 +329,7 @@ export function BrokerEventsPage() {
</Box> </Box>
</Box> </Box>
<Box component="tbody"> <Box component="tbody">
{ev.items {ev.items.map((item) => (
.filter((item) => item.category === 'cashflow')
.map((item) => (
<Box component="tr" key={item.id}> <Box component="tr" key={item.id}>
<Box <Box
component="td" component="td"
@ -377,8 +375,7 @@ export function BrokerEventsPage() {
{item.source === 'actual' && item.actualAmount != null ? ( {item.source === 'actual' && item.actualAmount != null ? (
<> <>
<Box sx={{ fontWeight: 700, color: 'success.main' }}> <Box sx={{ fontWeight: 700, color: 'success.main' }}>
+ +{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
</Box> </Box>
<Text variant="caption" tone="secondary"> <Text variant="caption" tone="secondary">
Поступило Поступило
@ -403,103 +400,6 @@ export function BrokerEventsPage() {
</Box> </Box>
</Box> </Box>
))} ))}
{ev.items.filter((item) => item.category === 'corporate').length > 0 && (
<>
<Box
component="tr"
sx={{
'& td': {
borderBottom: 'none',
},
}}
>
<Box
component="td"
colSpan={5}
sx={{
px: 1,
py: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Text variant="caption" tone="secondary">
Корпоративные события
</Text>
</Box>
</Box>
{ev.items
.filter((item) => item.category === 'corporate')
.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{formatBrokerDate(item.eventDate) ?? '-'}
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Box
component="td"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
<Text tone="muted"></Text>
</Box>
</Box>
))}
</>
)}
</Box> </Box>
</Box> </Box>
</Box> </Box>

View File

@ -1,6 +1,6 @@
# Календарь событий и прогноз будущих выплат брокерского счёта — задачи # Календарь событий и прогноз будущих выплат брокерского счёта — задачи
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря завершена Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря в работе
Связанные документы: Связанные документы:
@ -42,9 +42,9 @@
- [x] Добавить entity/hook для чтения broker events. - [x] Добавить entity/hook для чтения broker events.
- [x] Добавить handwritten response types для событий и summary. - [x] Добавить handwritten response types для событий и summary.
- [x] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт. - [ ] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
(Done: quality-gate фича исправила OpenAPI, types.ts сгенерирован через codegen, все типы (Deferred: types.ts не генерируется через codegen, т.к. OpenAPI-artifacts сломан и не является
событий брокера присутствуют.) частью этой фичи. Ответственность — отдельная задача по codegen.)
## 6. Интерфейс overview ## 6. Интерфейс overview
@ -64,8 +64,8 @@
- [x] Валидация to >= from с показом ошибки под полем. - [x] Валидация to >= from с показом ошибки под полем.
- [x] Реализовать summary по выбранному периоду. - [x] Реализовать summary по выбранному периоду.
- [x] Реализовать список событий с признаком `estimate`. - [x] Реализовать список событий с признаком `estimate`.
- [x] Разделить денежные и неденежные события на уровне представления. - [ ] Разделить денежные и неденежные события на уровне представления.
(Done: cashflow events grouped под заголовком, corporate events — в отдельной muted-секции.) (Deferred: all items shown in one table. Visual separation — follow-up.)
## 8. Frontend tests ## 8. Frontend tests
@ -76,15 +76,15 @@
## 9. Документация и quality gates ## 9. Документация и quality gates
- [x] Обновить опубликованную backend documentation по broker endpoints. - [ ] Обновить опубликованную backend documentation по broker endpoints.
(Done: `caching.md` обновлён — добавлен CACHE_TBANK_EVENTS_TTL в таблицу и key prefixes.) (Deferred: docs site update — отдельная задача, т.к. docs build требует дополнительной настройки.)
- [x] Прогнать backend tests (100 passed, 1 pre-existing skip). - [x] Прогнать backend tests (100 passed, 1 pre-existing skip).
- [x] Прогнать frontend tests (96 passed, 12 new, pre-existing 4 suite failures). - [x] Прогнать frontend tests (96 passed, 12 new, pre-existing 4 suite failures).
- [x] Прогнать backend lint. - [x] Прогнать backend lint.
- [x] Прогнать frontend tsc. - [x] Прогнать frontend tsc.
- [x] Прогнать frontend lint (3 pre-existing errors, 0 new). - [x] Прогнать frontend lint (3 pre-existing errors, 0 new).
- [x] Прогнать docs build, если менялась опубликованная документация. - [ ] Прогнать docs build, если менялась опубликованная документация.
(Done: `caching.md` обновлён, docs build проверен.) (Deferred: docs не менялись.)
- [x] Отметить roadmap и связанные SDD-статусы. - [x] Отметить roadmap и связанные SDD-статусы.
## 10. Follow-up: UX фильтров и прошедшие события ## 10. Follow-up: UX фильтров и прошедшие события

View File

@ -92,9 +92,8 @@ Roadmap отражает порядок продуктовой работы, н
## Кандидаты следующих фич ## Кандидаты следующих фич
- [x] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — DividendsTable, - [ ] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — перевести legacy-таблицы
ScreenerTable, SharePositionTable, BondPositionTable мигрированы на `DataTable`. Legacy `shared/ui/Table` на `DataTable` поверх `TanStack Table`.
и `TableSkeleton` удалены.
- [ ] T-Bank data isolation and multi-tenancy (P0/P1) — изолировать данные T-Bank по пользователям, - [ ] T-Bank data isolation and multi-tenancy (P0/P1) — изолировать данные T-Bank по пользователям,
ownership модель ownership модель
- [ ] API envelope runtime contract (P1) — устранить double-wrapping, унифицировать envelope - [ ] API envelope runtime contract (P1) — устранить double-wrapping, унифицировать envelope
@ -106,4 +105,4 @@ Roadmap отражает порядок продуктовой работы, н
- [ ] Frontend delivery optimization (P3) — route-level lazy loading, performance budgets - [ ] Frontend delivery optimization (P3) — route-level lazy loading, performance budgets
- [ ] Аналитика портфеля Phases 23 — дивидендный доход, сравнение с target allocation. - [ ] Аналитика портфеля Phases 23 — дивидендный доход, сравнение с target allocation.
- [ ] Quality gate — завершить оставшиеся AC. - [ ] Quality gate — завершить оставшиеся AC.
- [x] Broker-events — UX доработки и смешанный календарь. - [ ] Broker-events — UX доработки и смешанный календарь.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,286 @@
{
"0": "Community 0",
"1": "Community 1",
"2": "Community 2",
"3": "Community 3",
"4": "Community 4",
"5": "Community 5",
"6": "Community 6",
"7": "Community 7",
"8": "Community 8",
"9": "Community 9",
"10": "Community 10",
"11": "Community 11",
"12": "Community 12",
"13": "Community 13",
"14": "Community 14",
"15": "Community 15",
"16": "Community 16",
"17": "Community 17",
"18": "Community 18",
"19": "Community 19",
"20": "Community 20",
"21": "Community 21",
"22": "Community 22",
"23": "Community 23",
"24": "Community 24",
"25": "Community 25",
"26": "Community 26",
"27": "Community 27",
"28": "Community 28",
"29": "Community 29",
"30": "Community 30",
"31": "Community 31",
"32": "Community 32",
"33": "Community 33",
"34": "Community 34",
"35": "Community 35",
"36": "Community 36",
"37": "Community 37",
"38": "Community 38",
"39": "Community 39",
"40": "Community 40",
"41": "Community 41",
"42": "Community 42",
"43": "Community 43",
"44": "Community 44",
"45": "Community 45",
"46": "Community 46",
"47": "Community 47",
"48": "Community 48",
"49": "Community 49",
"50": "Community 50",
"51": "Community 51",
"52": "Community 52",
"53": "Community 53",
"54": "Community 54",
"55": "Community 55",
"56": "Community 56",
"57": "Community 57",
"58": "Community 58",
"59": "Community 59",
"60": "Community 60",
"61": "Community 61",
"62": "Community 62",
"63": "Community 63",
"64": "Community 64",
"65": "Community 65",
"66": "Community 66",
"67": "Community 67",
"68": "Community 68",
"69": "Community 69",
"70": "Community 70",
"71": "Community 71",
"72": "Community 72",
"73": "Community 73",
"74": "Community 74",
"75": "Community 75",
"76": "Community 76",
"77": "Community 77",
"78": "Community 78",
"79": "Community 79",
"80": "Community 80",
"81": "Community 81",
"82": "Community 82",
"83": "Community 83",
"84": "Community 84",
"85": "Community 85",
"86": "Community 86",
"87": "Community 87",
"88": "Community 88",
"89": "Community 89",
"90": "Community 90",
"91": "Community 91",
"92": "Community 92",
"93": "Community 93",
"94": "Community 94",
"95": "Community 95",
"96": "Community 96",
"97": "Community 97",
"98": "Community 98",
"99": "Community 99",
"100": "Community 100",
"101": "Community 101",
"102": "Community 102",
"103": "Community 103",
"104": "Community 104",
"105": "Community 105",
"106": "Community 106",
"107": "Community 107",
"108": "Community 108",
"109": "Community 109",
"110": "Community 110",
"111": "Community 111",
"112": "Community 112",
"113": "Community 113",
"114": "Community 114",
"115": "Community 115",
"116": "Community 116",
"117": "Community 117",
"118": "Community 118",
"119": "Community 119",
"120": "Community 120",
"121": "Community 121",
"122": "Community 122",
"123": "Community 123",
"124": "Community 124",
"125": "Community 125",
"126": "Community 126",
"127": "Community 127",
"128": "Community 128",
"129": "Community 129",
"130": "Community 130",
"131": "Community 131",
"132": "Community 132",
"133": "Community 133",
"134": "Community 134",
"135": "Community 135",
"136": "Community 136",
"137": "Community 137",
"138": "Community 138",
"139": "Community 139",
"140": "Community 140",
"141": "Community 141",
"142": "Community 142",
"143": "Community 143",
"144": "Community 144",
"145": "Community 145",
"146": "Community 146",
"147": "Community 147",
"148": "Community 148",
"149": "Community 149",
"150": "Community 150",
"151": "Community 151",
"152": "Community 152",
"153": "Community 153",
"154": "Community 154",
"155": "Community 155",
"156": "Community 156",
"157": "Community 157",
"158": "Community 158",
"159": "Community 159",
"160": "Community 160",
"161": "Community 161",
"162": "Community 162",
"163": "Community 163",
"164": "Community 164",
"165": "Community 165",
"166": "Community 166",
"167": "Community 167",
"168": "Community 168",
"169": "Community 169",
"170": "Community 170",
"171": "Community 171",
"172": "Community 172",
"173": "Community 173",
"174": "Community 174",
"175": "Community 175",
"176": "Community 176",
"177": "Community 177",
"178": "Community 178",
"179": "Community 179",
"180": "Community 180",
"181": "Community 181",
"182": "Community 182",
"183": "Community 183",
"184": "Community 184",
"185": "Community 185",
"186": "Community 186",
"187": "Community 187",
"188": "Community 188",
"189": "Community 189",
"190": "Community 190",
"191": "Community 191",
"192": "Community 192",
"193": "Community 193",
"194": "Community 194",
"195": "Community 195",
"196": "Community 196",
"197": "Community 197",
"198": "Community 198",
"199": "Community 199",
"200": "Community 200",
"201": "Community 201",
"202": "Community 202",
"203": "Community 203",
"204": "Community 204",
"205": "Community 205",
"206": "Community 206",
"207": "Community 207",
"208": "Community 208",
"209": "Community 209",
"210": "Community 210",
"211": "Community 211",
"212": "Community 212",
"213": "Community 213",
"214": "Community 214",
"215": "Community 215",
"216": "Community 216",
"217": "Community 217",
"218": "Community 218",
"219": "Community 219",
"220": "Community 220",
"221": "Community 221",
"222": "Community 222",
"223": "Community 223",
"224": "Community 224",
"225": "Community 225",
"226": "Community 226",
"227": "Community 227",
"228": "Community 228",
"229": "Community 229",
"230": "Community 230",
"231": "Community 231",
"232": "Community 232",
"233": "Community 233",
"234": "Community 234",
"235": "Community 235",
"236": "Community 236",
"237": "Community 237",
"238": "Community 238",
"239": "Community 239",
"240": "Community 240",
"241": "Community 241",
"242": "Community 242",
"243": "Community 243",
"244": "Community 244",
"245": "Community 245",
"246": "Community 246",
"247": "Community 247",
"248": "Community 248",
"249": "Community 249",
"250": "Community 250",
"251": "Community 251",
"252": "Community 252",
"253": "Community 253",
"254": "Community 254",
"255": "Community 255",
"256": "Community 256",
"257": "Community 257",
"258": "Community 258",
"259": "Community 259",
"260": "Community 260",
"261": "Community 261",
"262": "Community 262",
"263": "Community 263",
"264": "Community 264",
"265": "Community 265",
"266": "Community 266",
"267": "Community 267",
"268": "Community 268",
"269": "Community 269",
"270": "Community 270",
"271": "Community 271",
"272": "Community 272",
"273": "Community 273",
"274": "Community 274",
"275": "Community 275",
"276": "Community 276",
"277": "Community 277",
"278": "Community 278",
"279": "Community 279",
"280": "Community 280",
"281": "Community 281",
"282": "Community 282",
"283": "Community 283"
}

View File

@ -0,0 +1 @@
.

1381
graphify-out/GRAPH_REPORT.md Normal file

File diff suppressed because it is too large Load Diff

307
graphify-out/graph.html Normal file

File diff suppressed because one or more lines are too long

100147
graphify-out/graph.json Normal file

File diff suppressed because it is too large Load Diff

3437
graphify-out/manifest.json Normal file

File diff suppressed because it is too large Load Diff