[{"data":1,"prerenderedAt":4246},["ShallowReactive",2],{"navigation_docs_ru":3,"-ru-reference-firebase-migration":167,"-ru-reference-firebase-migration-surround":4241},[4,61,101,146],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":60},"Руководство","i-lucide-book-open","\u002Fru\u002Fguide","ru\u002F1.guide",[10,15,20,25,30,35,40,45,50,55],{"title":11,"path":12,"stem":13,"icon":14},"Введение","\u002Fru\u002Fguide\u002Fintroduction","ru\u002F1.guide\u002F01.introduction","i-lucide-house",{"title":16,"path":17,"stem":18,"icon":19},"Установка","\u002Fru\u002Fguide\u002Finstallation","ru\u002F1.guide\u002F02.installation","i-lucide-smartphone",{"title":21,"path":22,"stem":23,"icon":24},"Авторизация","\u002Fru\u002Fguide\u002Fauth","ru\u002F1.guide\u002F03.auth","i-lucide-lock",{"title":26,"path":27,"stem":28,"icon":29},"Кошельки","\u002Fru\u002Fguide\u002Fwallets","ru\u002F1.guide\u002F04.wallets","i-lucide-wallet",{"title":31,"path":32,"stem":33,"icon":34},"Категории","\u002Fru\u002Fguide\u002Fcategories","ru\u002F1.guide\u002F05.categories","i-lucide-tags",{"title":36,"path":37,"stem":38,"icon":39},"Транзакции","\u002Fru\u002Fguide\u002Ftransactions","ru\u002F1.guide\u002F06.transactions","i-lucide-receipt",{"title":41,"path":42,"stem":43,"icon":44},"Переводы","\u002Fru\u002Fguide\u002Ftransfers","ru\u002F1.guide\u002F07.transfers","i-lucide-arrow-left-right",{"title":46,"path":47,"stem":48,"icon":49},"Статистика","\u002Fru\u002Fguide\u002Fstatistics","ru\u002F1.guide\u002F08.statistics","i-lucide-bar-chart-3",{"title":51,"path":52,"stem":53,"icon":54},"Тема","\u002Fru\u002Fguide\u002Ftheme","ru\u002F1.guide\u002F09.theme","i-lucide-palette",{"title":56,"path":57,"stem":58,"icon":59},"Настройки","\u002Fru\u002Fguide\u002Fsettings","ru\u002F1.guide\u002F10.settings","i-lucide-settings",false,{"title":62,"icon":63,"path":64,"stem":65,"children":66,"page":60},"Разработка","i-lucide-code","\u002Fru\u002Fdevelopment","ru\u002F2.development",[67,71,76,81,86,91,96],{"title":16,"path":68,"stem":69,"icon":70},"\u002Fru\u002Fdevelopment\u002Finstallation","ru\u002F2.development\u002F01.installation","i-lucide-download",{"title":72,"path":73,"stem":74,"icon":75},"Граф кодовой базы","\u002Fru\u002Fdevelopment\u002Funderstand-anything","ru\u002F2.development\u002F02.understand-anything","i-lucide-network",{"title":77,"path":78,"stem":79,"icon":80},"Офлайн и PWA","\u002Fru\u002Fdevelopment\u002Foffline","ru\u002F2.development\u002F03.offline","i-lucide-wifi-off",{"title":82,"path":83,"stem":84,"icon":85},"История миграций данных","\u002Fru\u002Fdevelopment\u002Fmigration","ru\u002F2.development\u002F04.migration","i-lucide-database",{"title":87,"path":88,"stem":89,"icon":90},"Деплой","\u002Fru\u002Fdevelopment\u002Fdeployment","ru\u002F2.development\u002F05.deployment","i-lucide-rocket",{"title":92,"path":93,"stem":94,"icon":95},"Тестирование","\u002Fru\u002Fdevelopment\u002Ftesting","ru\u002F2.development\u002F06.testing","i-lucide-flask-conical",{"title":97,"path":98,"stem":99,"icon":100},"Дата-утилиты","\u002Fru\u002Fdevelopment\u002Fdate-utilities","ru\u002F2.development\u002F07.date-utilities","i-lucide-calendar",{"title":102,"icon":103,"path":104,"stem":105,"children":106,"page":60},"Справочник","i-lucide-file-code","\u002Fru\u002Freference","ru\u002F3.reference",[107,112,116,121,126,131,136,141],{"title":108,"path":109,"stem":110,"icon":111},"Архитектура","\u002Fru\u002Freference\u002Farchitecture","ru\u002F3.reference\u002F01.architecture","i-lucide-boxes",{"title":113,"path":114,"stem":115,"icon":44},"Типы транзакций","\u002Fru\u002Freference\u002Ftransaction-types","ru\u002F3.reference\u002F02.transaction-types",{"title":117,"path":118,"stem":119,"icon":120},"Синхронизация","\u002Fru\u002Freference\u002Fsync","ru\u002F3.reference\u002F03.sync","i-lucide-refresh-cw",{"title":122,"path":123,"stem":124,"icon":125},"Офлайн-first","\u002Fru\u002Freference\u002Foffline-first","ru\u002F3.reference\u002F04.offline-first","i-lucide-list-ordered",{"title":127,"path":128,"stem":129,"icon":130},"Тех. решения","\u002Fru\u002Freference\u002Ftech-decisions","ru\u002F3.reference\u002F05.tech-decisions","i-lucide-lightbulb",{"title":132,"path":133,"stem":134,"icon":135},"Валидация","\u002Fru\u002Freference\u002Fvalidation-strategy","ru\u002F3.reference\u002F06.validation-strategy","i-lucide-shield-check",{"title":137,"path":138,"stem":139,"icon":140},"Что изменилось со времён Firebase","\u002Fru\u002Freference\u002Ffirebase-migration","ru\u002F3.reference\u002F07.firebase-migration","i-lucide-hamburger",{"title":142,"path":143,"stem":144,"icon":145},"Производительность","\u002Fru\u002Freference\u002Fperformance","ru\u002F3.reference\u002F08.performance","i-lucide-gauge",{"title":147,"icon":148,"path":149,"stem":150,"children":151,"page":60},"Премиум","i-lucide-star","\u002Fru\u002Fpremium","ru\u002F4.premium",[152,157,162],{"title":153,"path":154,"stem":155,"icon":156},"Обзор","\u002Fru\u002Fpremium\u002Foverview","ru\u002F4.premium\u002F01.overview","i-lucide-layers",{"title":158,"path":159,"stem":160,"icon":161},"Telegram-бот","\u002Fru\u002Fpremium\u002Ftelegram-bot","ru\u002F4.premium\u002F02.telegram-bot","i-lucide-send",{"title":163,"path":164,"stem":165,"icon":166},"AI Chat","\u002Fru\u002Fpremium\u002Fai-chat","ru\u002F4.premium\u002F03.ai-chat","i-lucide-sparkles",{"id":168,"title":137,"body":169,"description":4233,"extension":4234,"links":4235,"meta":4236,"navigation":4237,"path":138,"seo":4238,"stem":139,"__hash__":4240},"docs_ru\u002Fru\u002F3.reference\u002F07.firebase-migration.md",{"type":170,"value":171,"toc":4199},"minimark",[172,205,213,218,420,425,472,476,603,614,617,621,1444,1448,1615,1619,1757,1760,1897,1901,2033,2037,2118,2122,2433,2437,2527,2531,2535,2538,2641,2645,2698,2764,2768,2802,2806,3167,3171,3430,3434,3498,3502,3619,3622,3747,3751,3755,3814,3818,4116,4119,4132],[173,174,175,199],"blockquote",{},[176,177,178,182,183,186,187,190,191,194,195,198],"p",{},[179,180,181],"strong",{},"Две эпохи."," Бэкенд Финапки прошёл путь от ",[179,184,185],{},"Firebase"," (v6 - Firebase Realtime Database + Firebase Auth) к ",[179,188,189],{},"текущему"," стеку: ",[179,192,193],{},"Supabase Postgres + PowerSync"," (offline-first) + ",[179,196,197],{},"Supabase Auth",".",[176,200,201,202,198],{},"Этот документ - сравнение того, что изменилось. Улучшения на уровне приложения (производительность, рефакторинг, UX, TypeScript, i18n), а также разделы бэкенда, авторизации, синхронизации и зависимостей описывают текущую кодовую базу. Полное описание текущей архитектуры Supabase + PowerSync - в разделе ",[203,204,108],"a",{"href":109},[176,206,207,208,212],{},"Сравнение состояния приложения на ",[209,210,211],"code",{},"firebase","-ветке (v6, Firebase) и в текущем приложении на Supabase + PowerSync.",[214,215,217],"h2",{"id":216},"бэкенд","Бэкенд",[219,220,221,236],"table",{},[222,223,224],"thead",{},[225,226,227,230,233],"tr",{},[228,229],"th",{},[228,231,232],{},"firebase (Firebase)",[228,234,235],{},"текущий",[237,238,239,253,268,296,312,328,345,361,386,405],"tbody",{},[225,240,241,247,250],{},[242,243,244],"td",{},[179,245,246],{},"База данных",[242,248,249],{},"Firebase Realtime Database",[242,251,252],{},"Supabase Postgres (реплицируется PowerSync)",[225,254,255,259,265],{},[242,256,257],{},[179,258,21],{},[242,260,261,262],{},"Firebase Auth через ",[209,263,264],{},"nuxt-vuefire",[242,266,267],{},"Supabase Auth (email\u002Fpassword)",[225,269,270,275,282],{},[242,271,272],{},[179,273,274],{},"Доступ к данным",[242,276,277,278,281],{},"Real-time listeners (",[209,279,280],{},"getDataAndWatch",")",[242,283,284,285,288,289,292,293],{},"Локальный SQLite через ",[209,286,287],{},"db.watch(...)",", запись через ",[209,290,291],{},"upsertRow"," \u002F ",[209,294,295],{},"deleteRow",[225,297,298,302,305],{},[242,299,300],{},[179,301,132],{},[242,303,304],{},"Firebase security rules (JSON)",[242,306,307,308,311],{},"Postgres RLS (",[209,309,310],{},"auth.uid()",") + клиентские Zod-схемы",[225,313,314,319,322],{},[242,315,316],{},[179,317,318],{},"Серверный код",[242,320,321],{},"Нет (клиентский SDK)",[242,323,324,325,281],{},"Миграции Supabase + sync-правила self-hosted PowerSync (по пользователю ",[209,326,327],{},"WHERE \"userId\" = auth.user_id()",[225,329,330,335,338],{},[242,331,332],{},[179,333,334],{},"Курсы валют",[242,336,337],{},"Клиентский запрос",[242,339,340,341,344],{},"Таблица ",[209,342,343],{},"rates",", синхронизируется через PowerSync как любая другая",[225,346,347,352,355],{},[242,348,349],{},[179,350,351],{},"Настройки пользователя",[242,353,354],{},"Только localStorage",[242,356,340,357,360],{},[209,358,359],{},"user_settings"," (baseCurrency, locale), создаётся при регистрации триггером",[225,362,363,368,371],{},[242,364,365],{},[179,366,367],{},"Индексы схемы",[242,369,370],{},"N\u002FA",[242,372,373,374,377,378,381,382,385],{},"Индексы клиентского SQLite в ",[209,375,376],{},"AppSchema.ts"," (",[209,379,380],{},"userDate: ['userId','date']"," + ",[209,383,384],{},"userId"," на каждой таблице)",[225,387,388,393,395],{},[242,389,390],{},[179,391,392],{},"Миграция",[242,394,370],{},[242,396,397,398,401,402,281],{},"Экспорт JSON из Firebase -> трансформация -> импорт через PostgREST скриптом ",[209,399,400],{},"scripts\u002Fimport-firebase.mjs"," (см. ",[203,403,404],{"href":83},"Историю миграций",[225,406,407,412,414],{},[242,408,409],{},[179,410,411],{},"Удаление аккаунта",[242,413,370],{},[242,415,416,417,281],{},"RLS-ограниченное удаление строк пользователя + локальная очистка (",[209,418,419],{},"removeAllUserData",[421,422,424],"h3",{"id":423},"что-даёт-текущий-бэкенд","Что даёт текущий бэкенд",[426,427,428,435,444,456,466],"ul",{},[429,430,431,434],"li",{},[179,432,433],{},"Offline-first"," - все чтения\u002Fзаписи идут в локальный SQLite; PowerSync синхронизирует в фоне, поэтому приложение полностью работает офлайн",[429,436,437,440,441,443],{},[179,438,439],{},"Изоляция по пользователю"," - Postgres RLS (",[209,442,310],{},") + sync-правила PowerSync гарантируют, что каждый пользователь синхронизирует только свои строки",[429,445,446,449,450,452,453],{},[179,447,448],{},"Типизированная клиентская схема"," - ",[209,451,376],{}," описывает таблицы SQLite; трансформации row\u003C->item собраны в ",[209,454,455],{},"transforms.ts",[429,457,458,461,462,465],{},[179,459,460],{},"Автоматическая очередь загрузки"," - локальные мутации ставятся в очередь и выгружаются коннектором (",[209,463,464],{},"uploadData","), с авто-реконциляцией отклонённых операций",[429,467,468,471],{},[179,469,470],{},"Инкрементальная синхронизация"," - PowerSync стримит только изменённые строки после начальной синхронизации",[214,473,475],{"id":474},"паттерн-сторов","Паттерн сторов",[219,477,478,488],{},[222,479,480],{},[225,481,482,484,486],{},[228,483],{},[228,485,211],{},[228,487,235],{},[237,489,490,515,538,555,568,587],{},[225,491,492,497,503],{},[242,493,494],{},[179,495,496],{},"Источник данных",[242,498,499,500],{},"Firebase listener -> ",[209,501,502],{},"setTrns()",[242,504,505,508,509,508,512],{},[209,506,507],{},"db.watch('SELECT * FROM ...')"," -> ",[209,510,511],{},"reconcile",[209,513,514],{},"setX()",[225,516,517,522,531],{},[242,518,519],{},[179,520,521],{},"Persist",[242,523,524,527,528],{},[209,525,526],{},"deepUnref()"," + немедленный ",[209,529,530],{},"localforage.setItem()",[242,532,533,534,537],{},"Источник правды - SQLite PowerSync; блоб read-through кеш (",[209,535,536],{},"useStoreCache.ts",") рисует первый кадр",[225,539,540,545,552],{},[242,541,542],{},[179,543,544],{},"Офлайн-операции",[242,546,547,548,551],{},"Ручные хелперы в ",[209,549,550],{},"trns\u002Fhelpers.ts",", произвольные ключи localStorage",[242,553,554],{},"Не нужны - встроенная очередь загрузки PowerSync обрабатывает офлайн-записи",[225,556,557,562,565],{},[242,558,559],{},[179,560,561],{},"Система ID",[242,563,564],{},"Firebase push ID",[242,566,567],{},"Клиентские UUID, стабильны при синхронизации (без ремапа)",[225,569,570,575,578],{},[242,571,572],{},[179,573,574],{},"Оптимистичный UI",[242,576,577],{},"Частичный (fire-and-forget)",[242,579,580,581,583,584,586],{},"Полный: ",[209,582,291],{},"\u002F",[209,585,295],{}," пишут в локальный SQLite -> watch переэмитит -> коннектор выгружает в фоне",[225,588,589,594,597],{},[242,590,591],{},[179,592,593],{},"Верификация синхронизации",[242,595,596],{},"Нет",[242,598,599,600,281],{},"Чек-суммы PowerSync по бакетам; отклонённые загрузки запускают авто-реконциляцию (",[209,601,602],{},"uploadReconcile.ts",[176,604,605,606,609,610,613],{},"Зависимость ",[209,607,608],{},"vue-deepunref"," удалена. Данные сторов используют ",[209,611,612],{},"shallowRef",", поэтому глубокое клонирование при каждом persist было лишним.",[214,615,142],{"id":616},"производительность",[421,618,620],{"id":619},"алгоритмические-улучшения","Алгоритмические улучшения",[219,622,623,637],{},[222,624,625],{},[225,626,627,630,632,634],{},[228,628,629],{},"Что",[228,631,211],{},[228,633,235],{},[228,635,636],{},"Эффект",[237,638,639,669,697,730,752,782,824,847,880,899,933,969,1002,1024,1043,1068,1094,1124,1151,1167,1192,1227,1258,1285,1305,1333,1355,1384,1412],{},[225,640,641,646,656,666],{},[242,642,643],{},[179,644,645],{},"Итоги кошельков",[242,647,648,651,652,655],{},[209,649,650],{},"getWalletTotal(id)"," вызывается для каждого кошелька в ",[209,653,654],{},"reduce"," - каждый вызов фильтрует все транзакции: O(W×N)",[242,657,658,661,662,665],{},[209,659,660],{},"getWalletsTotals()"," один проход, распределение через ",[209,663,664],{},"Map",": O(N), затем O(1) для каждого кошелька",[242,667,668],{},"5 кошельков × 10K трн: ~50K → ~10K операций",[225,670,671,676,687,694],{},[242,672,673],{},[179,674,675],{},"Разбивка по интервалам",[242,677,678,679,682,683,686],{},"Инлайн в ",[209,680,681],{},"Item.vue",": ",[209,684,685],{},"intervalsInRange.reduce()"," фильтрует все транзакции на каждый интервал: O(N×I)",[242,688,689,690,693],{},"Извлечено в ",[209,691,692],{},"bucketTrnsByIntervals()",": один проход + бинарный поиск: O(N log I)",[242,695,696],{},"Год\u002F365 дней\u002F5K трн: 1.8M → ~43K сравнений",[225,698,699,704,717,727],{},[242,700,701],{},[179,702,703],{},"Фильтрация транзакций",[242,705,706,707,710,711,381,713,716],{},"6 последовательных ",[209,708,709],{},".filter()"," через ",[209,712,654],{},[209,714,715],{},"walletsIds.some(w => includes(w))"," O(W²) + сортировка всегда O(N log N)",[242,718,719,720,722,723,726],{},"Один ",[209,721,709],{}," с ранним выходом + ",[209,724,725],{},"Set.has()"," O(1) + сортировка опциональна",[242,728,729],{},"5 промежуточных массивов + O(W²) → один проход + O(1); сортировка O(N log N) пропускается когда не нужна",[225,731,732,737,743,749],{},[242,733,734],{},[179,735,736],{},"Поиск кошелька в getTotal",[242,738,739,742],{},[209,740,741],{},"walletsIds.includes()",": O(W) на каждый перевод",[242,744,745,748],{},[209,746,747],{},"new Set(walletsIds).has()",": O(1) на каждый перевод",[242,750,751],{},"Линейный → константный на каждый вызов",[225,753,754,759,772,779],{},[242,755,756],{},[179,757,758],{},"Генерация демо-данных",[242,760,761,764,765,381,768,771],{},[209,762,763],{},"{ ...acc }"," spread в ",[209,766,767],{},".reduce()",[209,769,770],{},"getTransactibleIds()"," вызывается дважды на итерацию",[242,773,774,775,778],{},"Прямое ",[209,776,777],{},"acc[i] = ..."," + кеширование перед циклом",[242,780,781],{},"O(N²) → O(N)",[225,783,784,789,807,818],{},[242,785,786],{},[179,787,788],{},"Статистика категорий",[242,790,791,794,795,798,799,802,803,806],{},[209,792,793],{},"categoriesStat"," жадно вычисляет и ",[209,796,797],{},"grouped",", и ",[209,800,801],{},"ungrouped"," на каждое изменение; ",[209,804,805],{},"filter().at(0)"," для наибольшей",[242,808,809,810,813,814,817],{},"Ленивый ",[209,811,812],{},"computed()"," на каждый вариант, выполняется только при обращении; ",[209,815,816],{},".find()"," останавливается на первом совпадении",[242,819,820,821],{},"До 2× меньше вызовов ",[209,822,823],{},"computeCategoriesWithData",[225,825,826,831,837,844],{},[242,827,828],{},[179,829,830],{},"Фильтр вертикальных категорий",[242,832,833,836],{},[209,834,835],{},"verticalCategories.filter(c => c.value !== 0)"," вызывается дважды в шаблоне (заголовок + v-for): пересчитывается на каждый рендер",[242,838,839,840,843],{},"Кеширован в ",[209,841,842],{},"visibleVerticalCategories"," computed: фильтрация один раз, переиспользование",[242,845,846],{},"2 прохода фильтра → 1 кешированный",[225,848,849,854,864,877],{},[242,850,851],{},[179,852,853],{},"Форматирование чисел",[242,855,856,857,381,860,863],{},"Библиотека ",[209,858,859],{},"currency.js",[209,861,862],{},"currencies.find()"," O(N) дважды на вызов",[242,865,866,867,870,871,381,873,876],{},"Нативный ",[209,868,869],{},"Intl.NumberFormat"," с кешем в ",[209,872,664],{},[209,874,875],{},"currencyMap.get()"," O(1)",[242,878,879],{},"Нет зависимости; форматтер создаётся один раз на precision",[225,881,882,887,893,896],{},[242,883,884],{},[179,885,886],{},"Последняя транзакция",[242,888,889,892],{},[209,890,891],{},"Object.keys().sort().find()"," - сортирует все транзакции O(N log N) затем ищет",[242,894,895],{},"Один проход O(N), отслеживая максимальную дату",[242,897,898],{},"10K трн: ~130K ops → ~10K ops",[225,900,901,906,916,930],{},[242,902,903],{},[179,904,905],{},"Проверка родителя категории",[242,907,908,911,912,915],{},[209,909,910],{},"categoriesForBeParent"," вызывает ",[209,913,914],{},"Object.values(trns).some()"," O(N) на каждую корневую категорию: O(M×N)",[242,917,918,919,922,923,926,927,929],{},"Предготовленный ",[209,920,921],{},"usedCategoryIds"," Set через ",[209,924,925],{},"for...in"," O(N) + ",[209,928,725],{}," O(1) на проверку; без промежуточных массивов",[242,931,932],{},"200 категорий × 10K трн: ~2M → ~10K ops",[225,934,935,940,954,966],{},[242,936,937],{},[179,938,939],{},"Недавние категории",[242,941,942,945,946,949,950,953],{},[209,943,944],{},"Object.keys(trns).filter().sort()"," O(N log N) + ",[209,947,948],{},"acc.some(c => c.id)"," O(K²) дедупликация + ",[209,951,952],{},"includes()"," O(W) на категорию",[242,955,956,957,959,960,963,964,876],{},"Один проход O(N) через ",[209,958,664],{}," с последней датой + ",[209,961,962],{},"entries().toSorted()"," O(K log K) + ",[209,965,725],{},[242,967,968],{},"10K трн: ~130K + K² → ~10K + K log K ops",[225,970,971,976,989,999],{},[242,972,973],{},[179,974,975],{},"Транзактируемые категории",[242,977,978,980,981,984,985,988],{},[209,979,767],{}," с ",[209,982,983],{},"acc.includes()"," O(M) + ",[209,986,987],{},"childIds.filter(!acc.includes)"," O(C×M): O(M²)",[242,990,991,994,995,998],{},[209,992,993],{},"Map\u003CparentId, childIds[]>"," за O(M) + ",[209,996,997],{},"Set"," дедупликация O(1) на проверку",[242,1000,1001],{},"200 категорий: ~40K → ~200 ops",[225,1003,1004,1009,1015,1021],{},[242,1005,1006],{},[179,1007,1008],{},"Определение переводов",[242,1010,1011,1014],{},[209,1012,1013],{},"transferCategoriesIds.includes(categoryId)"," O(W) линейный поиск на каждую транзакцию",[242,1016,1017,1020],{},[209,1018,1019],{},"trn.type === TrnType.Transfer"," O(1) сравнение enum",[242,1022,1023],{},"Линейный → константный на проверку",[225,1025,1026,1031,1037,1040],{},[242,1027,1028],{},[179,1029,1030],{},"Подсчёт фильтров типов",[242,1032,1033,1034,1036],{},"3 отдельных ",[209,1035,709],{}," прохода по trnsIds (расход, доход, перевод): O(3N)",[242,1038,1039],{},"Один цикл с объектом счётчиков: O(N)",[242,1041,1042],{},"3 полных прохода → 1 проход",[225,1044,1045,1050,1056,1065],{},[242,1046,1047],{},[179,1048,1049],{},"Кеш элементов в списке",[242,1051,1052,1055],{},[209,1053,1054],{},"computeTrnItem(id)"," вызывается дважды на элемент в шаблоне (элемент + дата): O(2P) вызовов",[242,1057,1058,1059,710,1062,1064],{},"Предвычисленная ",[209,1060,1061],{},"trnItemsMap",[209,1063,664],{},": O(P) вычислений + O(1) обращения",[242,1066,1067],{},"30 элементов: ~60 → ~30 вычислений на рендер",[225,1069,1070,1075,1081,1091],{},[242,1071,1072],{},[179,1073,1074],{},"Кеш итогов групп",[242,1076,1077,1080],{},[209,1078,1079],{},"getTotalOfTrnsIds()"," вызывается дважды на группу в шаблоне (доход + расход): O(2G) вызовов",[242,1082,1058,1083,1086,1087,1090],{},[209,1084,1085],{},"groupTotals"," Map + ",[209,1088,1089],{},"paginatedTotal"," computed: O(G) вычислений + O(1) обращения",[242,1092,1093],{},"15 групп: ~30 → ~15 вычислений итогов на рендер",[225,1095,1096,1101,1114,1121],{},[242,1097,1098],{},[179,1099,1100],{},"Сортировка при группировке",[242,1102,1103,1106,1107,1109,1110,1113],{},[209,1104,1105],{},"categories.sort()"," вызывается внутри ",[209,1108,654],{}," на каждый ",[209,1111,1112],{},".push()"," дочерней категории: O(K² log K) на родителя",[242,1115,1116,1117,1120],{},"Сортировка один раз после цикла, только если ",[209,1118,1119],{},"length > 1",": O(K log K) на родителя",[242,1122,1123],{},"5 детей: ~50 → ~12 сравнений",[225,1125,1126,1131,1137,1144],{},[242,1127,1128],{},[179,1129,1130],{},"Итоги при группировке",[242,1132,1133,1136],{},[209,1134,1135],{},"getTotalOfTrnsIds(cat.trnsIds).sum"," вызывается дважды на дочернюю категорию (для значения + суммы родителя)",[242,1138,1139,1140,1143],{},"Вычисляется один раз в ",[209,1141,1142],{},"catTotal",", переиспользуется: один вызов на дочернюю",[242,1145,1146,1147,1150],{},"2× меньше вызовов ",[209,1148,1149],{},"getTotal"," на дочернюю категорию",[225,1152,1153,1158,1161,1164],{},[242,1154,1155],{},[179,1156,1157],{},"Синхронизация данных",[242,1159,1160],{},"Firebase: полная замена данных на каждое обновление: O(N)",[242,1162,1163],{},"Дельта-синхронизация: запрашиваются только изменённые записи - O(D) где D « N",[242,1165,1166],{},"Не обрабатывает неизменённые транзакции",[225,1168,1169,1174,1180,1189],{},[242,1170,1171],{},[179,1172,1173],{},"Генерация интервалов",[242,1175,1176,1179],{},[209,1177,1178],{},"list.unshift(current)"," в цикле while: O(n) на вызов, O(n²) всего",[242,1181,1182,381,1185,1188],{},[209,1183,1184],{},"list.push(current)",[209,1186,1187],{},"list.reverse()",": O(1) на вызов, O(n) всего",[242,1190,1191],{},"Год\u002F365 дней: ~66K сдвигов → ~365 push + 1 reverse",[225,1193,1194,1199,1214,1224],{},[242,1195,1196],{},[179,1197,1198],{},"Ключи Duration",[242,1200,1201,1202,1205,1206,1209,1210,1213],{},"Динамический ",[209,1203,1204],{},"{ [\\","${period}s`]: value }",[209,1207,1208],{},"- TS видит","Record\u003Cstring, number>",[209,1211,1212],{},", нет проверки типа ","Duration`",[242,1215,1216,1219,1220,1223],{},[209,1217,1218],{},"toDuration(period, value)"," возвращает типизированный ",[209,1221,1222],{},"Duration"," через exhaustive switch",[242,1225,1226],{},"Безопасность на этапе компиляции; новые варианты периодов вызывают ошибки, а не молчаливый fallback",[225,1228,1229,1234,1244,1251],{},[242,1230,1231],{},[179,1232,1233],{},"Форматирование дат",[242,1235,1236,1237,381,1240,1243],{},"Каждая format-функция вызывается дважды (",[209,1238,1239],{},"type: 'start'",[209,1241,1242],{},"type: 'end'","), результаты конкатенируются; 3 IIFE switch-блока для сравнения периодов",[242,1245,1246,1247,1250],{},"Каждая format-функция возвращает полную строку; ",[209,1248,1249],{},"isSamePeriod(a, b, period)"," заменяет все 3 switch-блока",[242,1252,1253,1254,1257],{},"~40% меньше строк в ",[209,1255,1256],{},"useGetDateRange","; без двойных вызовов",[225,1259,1260,1265,1275,1282],{},[242,1261,1262],{},[179,1263,1264],{},"Вычисление средних",[242,1266,1267,1270,1271,1274],{},[209,1268,1269],{},"sum !== 0"," проверяется 3 раза + ",[209,1272,1273],{},"Object.keys(items).length > 0"," гард",[242,1276,1277,1278,1281],{},"Ранний выход при ",[209,1279,1280],{},"sum === 0","; среднее за день всегда присутствует (диапазон ≥ 2 гарантирован)",[242,1283,1284],{},"1 проверка вместо 4",[225,1286,1287,1292,1295,1302],{},[242,1288,1289],{},[179,1290,1291],{},"Сортировка избранных категорий",[242,1293,1294],{},"Инлайн-сортировка дублирует логику parent-name + name (8 строк)",[242,1296,1297,1298,1301],{},"Переиспользует утилиту ",[209,1299,1300],{},"compareCategoriesByParentAndName"," (1 строка)",[242,1303,1304],{},"DRY; одна реализация сортировки",[225,1306,1307,1312,1320,1330],{},[242,1308,1309],{},[179,1310,1311],{},"Цикл недавних категорий",[242,1313,1314,1316,1317],{},[209,1315,767],{}," проходит все записи даже после достижения лимита ",[209,1318,1319],{},"maxCategories",[242,1321,1322,1325,1326,1329],{},[209,1323,1324],{},"for"," цикл с ",[209,1327,1328],{},"break"," останавливается на лимите",[242,1331,1332],{},"200 категорий, лимит 16: ~200 → ~16 итераций",[225,1334,1335,1340,1346,1352],{},[242,1336,1337],{},[179,1338,1339],{},"Итерация недавних категорий",[242,1341,1342,1345],{},[209,1343,1344],{},"for...of Object.keys(trnsItems)"," создает промежуточный массив",[242,1347,1348,1351],{},[209,1349,1350],{},"for...in trnsItems"," итерирует напрямую по объекту",[242,1353,1354],{},"Без аллокации промежуточного массива для 10K+ транзакций",[225,1356,1357,1362,1375,1381],{},[242,1358,1359],{},[179,1360,1361],{},"Переиспользование ID категорий",[242,1363,1364,1367,1368,1371,1372],{},[209,1365,1366],{},"Object.keys(items.value)"," вызывается отдельно в ",[209,1369,1370],{},"categoriesRootIds",", ",[209,1373,1374],{},"favoriteCategoriesIds",[242,1376,1377,1378],{},"Оба переиспользуют computed ",[209,1379,1380],{},"categoriesIds",[242,1382,1383],{},"Ключи вычисляются один раз, используются всеми зависимыми",[225,1385,1386,1391,1400,1409],{},[242,1387,1388],{},[179,1389,1390],{},"Переиспользование transactible IDs",[242,1392,1393,1396,1397],{},[209,1394,1395],{},"categoriesIdsForTrnValues"," повторно фильтрует все категории с проверкой ",[209,1398,1399],{},"!hasChildren",[242,1401,1402,1403,381,1406],{},"Делегирует уже вычисленному ",[209,1404,1405],{},"transactibleIds",[209,1407,1408],{},".filter(id !== 'transfer')",[242,1410,1411],{},"Без повторного O(M) сканирования родителей",[225,1413,1414,1419,1431,1441],{},[242,1415,1416],{},[179,1417,1418],{},"Дедупликация children IDs",[242,1420,1421,1424,1425,1427,1428],{},[209,1422,1423],{},"getChildrenIdsOrParent"," дублирует логику ",[209,1426,709],{}," из ",[209,1429,1430],{},"getChildrenIds",[242,1432,1433,1434,1437,1438],{},"Делегирует ",[209,1435,1436],{},"getChildrenIds()",", проверяет ",[209,1439,1440],{},".length",[242,1442,1443],{},"Одна реализация фильтра",[421,1445,1447],{"id":1446},"бандл-и-рендеринг","Бандл и рендеринг",[219,1449,1450,1460],{},[222,1451,1452],{},[225,1453,1454,1456,1458],{},[228,1455,629],{},[228,1457,211],{},[228,1459,235],{},[237,1461,1462,1475,1492,1508,1532,1552,1569,1590],{},[225,1463,1464,1469,1472],{},[242,1465,1466],{},[179,1467,1468],{},"ECharts",[242,1470,1471],{},"9 компонентов в основном бандле (вкл. PieChart, DataZoom, MarkPoint)",[242,1473,1474],{},"7 компонентов в lazy-загружаемом async-чанке (неиспользуемые удалены)",[225,1476,1477,1482,1485],{},[242,1478,1479],{},[179,1480,1481],{},"Плагин темы",[242,1483,1484],{},"81 строка с SSR инлайн-скриптами и regex-заменами",[242,1486,1487,1488,1491],{},"32 строки, SPA-only обновление ",[209,1489,1490],{},"appConfig"," из localStorage",[225,1493,1494,1498,1505],{},[242,1495,1496],{},[179,1497,521],{},[242,1499,1500,1109,1502,1504],{},[209,1501,526],{},[209,1503,530],{}," - полное рекурсивное клонирование",[242,1506,1507],{},"Прямая запись, debounce 300ms, без клонирования",[225,1509,1510,1515,1522],{},[242,1511,1512],{},[179,1513,1514],{},"Адаптивная вёрстка",[242,1516,1517,1518,1521],{},"JS-слушатель ",[209,1519,1520],{},"useWindowSize()"," для sidebar",[242,1523,1524,1525,292,1528,1531],{},"CSS ",[209,1526,1527],{},"sm:",[209,1529,1530],{},"md:"," Tailwind брейкпоинты",[225,1533,1534,1539,1542],{},[242,1535,1536],{},[179,1537,1538],{},"Container queries",[242,1540,1541],{},"Только media queries",[242,1543,1544,1545,1371,1548,1551],{},"CSS container queries (",[209,1546,1547],{},"@xl\u002Fpage:",[209,1549,1550],{},"@md\u002Fpage:",") для адаптивных размеров относительно контейнера",[225,1553,1554,1559,1564],{},[242,1555,1556],{},[179,1557,1558],{},"Зависимости",[242,1560,1561,1563],{},[209,1562,859],{}," для форматирования чисел",[242,1565,866,1566,1568],{},[209,1567,869],{}," (без зависимости)",[225,1570,1571,1576,1583],{},[242,1572,1573],{},[179,1574,1575],{},"Шрифты",[242,1577,1578,1579,1582],{},"4 семейства (Roboto, Roboto Condensed, Nunito, Unica One), множество весов, только кириллица - Google Fonts ",[209,1580,1581],{},"\u003Clink>"," в head",[242,1584,1585,1586,1589],{},"Те же 4 семейства через ",[209,1587,1588],{},"@nuxt\u002Ffonts",", кириллица + латиница",[225,1591,1592,1597,1606],{},[242,1593,1594],{},[179,1595,1596],{},"Внешний вид",[242,1598,1599,1602,1603],{},[209,1600,1601],{},"import colors from 'tailwindcss\u002Fcolors'"," + каст ",[209,1604,1605],{},"as any",[242,1607,1608,1611,1612],{},[209,1609,1610],{},"usePreferredDark()"," + захардкоженный neutral, поддержка режима ",[209,1613,1614],{},"system",[214,1616,1618],{"id":1617},"безопасность","Безопасность",[219,1620,1621,1631],{},[222,1622,1623],{},[225,1624,1625,1627,1629],{},[228,1626,629],{},[228,1628,211],{},[228,1630,235],{},[237,1632,1633,1666,1679,1692,1705,1718,1735],{},[225,1634,1635,1640,1650],{},[242,1636,1637],{},[179,1638,1639],{},"Парсер калькулятора",[242,1641,1642,1645,1646,1649],{},[209,1643,1644],{},"new Function(expression)"," - риск инъекции кода, требует ",[209,1647,1648],{},"unsafe-eval"," CSP",[242,1651,1652,1653,1656,1657,1656,1660,1656,1663,1665],{},"Рекурсивный парсер - только ",[209,1654,1655],{},"+",",",[209,1658,1659],{},"-",[209,1661,1662],{},"*",[209,1664,583],{}," над числами",[225,1667,1668,1673,1676],{},[242,1669,1670],{},[179,1671,1672],{},"Валидация ввода",[242,1674,1675],{},"Firebase rules (клиент не может проверить)",[242,1677,1678],{},"Клиентские Zod-схемы + типы колонок Postgres \u002F RLS",[225,1680,1681,1686,1689],{},[242,1682,1683],{},[179,1684,1685],{},"Токены авторизации",[242,1687,1688],{},"Управляются Firebase",[242,1690,1691],{},"JWT + refresh-токен под управлением Supabase (валидируются PowerSync через JWKS)",[225,1693,1694,1699,1702],{},[242,1695,1696],{},[179,1697,1698],{},"Время жизни сессии",[242,1700,1701],{},"По умолчанию Firebase",[242,1703,1704],{},"Сессия Supabase Auth с автообновлением токена",[225,1706,1707,1712,1715],{},[242,1708,1709],{},[179,1710,1711],{},"Каскадные удаления",[242,1713,1714],{},"N\u002FA (Firebase)",[242,1716,1717],{},"RLS-ограниченное удаление строк пользователя (без FK-ограничений; PowerSync чистит локальные данные)",[225,1719,1720,1725,1728],{},[242,1721,1722],{},[179,1723,1724],{},"Целостность данных",[242,1726,1727],{},"Не проверяется в коде",[242,1729,1730,1731,1734],{},"Transfer - ",[209,1732,1733],{},"categoryId='transfer'",", нормализация при смене типа, уникальность имён",[225,1736,1737,1742,1745],{},[242,1738,1739],{},[179,1740,1741],{},"Защита редиректов",[242,1743,1744],{},"Без валидации",[242,1746,1747,1750,1751,1753,1754,281],{},[209,1748,1749],{},"getSafeRedirectPath()"," - только относительные пути, начинающиеся с ",[209,1752,583],{}," (не ",[209,1755,1756],{},"\u002F\u002F",[214,1758,21],{"id":1759},"авторизация",[219,1761,1762,1772],{},[222,1763,1764],{},[225,1765,1766,1768,1770],{},[228,1767],{},[228,1769,211],{},[228,1771,235],{},[237,1773,1774,1786,1804,1820,1840,1856,1869,1881],{},[225,1775,1776,1781,1784],{},[242,1777,1778],{},[179,1779,1780],{},"Провайдер",[242,1782,1783],{},"Firebase Auth",[242,1785,267],{},[225,1787,1788,1793,1798],{},[242,1789,1790],{},[179,1791,1792],{},"Плагин",[242,1794,1795,1797],{},[209,1796,264],{}," (авто-управление авторизацией)",[242,1799,1800,1803],{},[209,1801,1802],{},"plugins\u002Fpowersync.client.ts"," - подключает PowerSync при появлении сессии",[225,1805,1806,1811,1814],{},[242,1807,1808],{},[179,1809,1810],{},"Поток токенов",[242,1812,1813],{},"Внутри Firebase SDK",[242,1815,1816,1819],{},[209,1817,1818],{},"SupabaseConnector.fetchCredentials()"," отдаёт JWT Supabase в PowerSync; валидация через JWKS",[225,1821,1822,1827,1830],{},[242,1823,1824],{},[179,1825,1826],{},"Офлайн",[242,1828,1829],{},"Firebase SDK управляет реконнектом",[242,1831,1832,1833,1836,1837,281],{},"Сохранённая сессия Supabase в ",[209,1834,1835],{},"localStorage","; route guard читает её синхронно (",[209,1838,1839],{},"useAuthSession",[225,1841,1842,1847,1849],{},[242,1843,1844],{},[179,1845,1846],{},"Подсказка состояния",[242,1848,596],{},[242,1850,1851,1852,1855],{},"Сохранённая сессия читается синхронно на холодном старте (",[209,1853,1854],{},"getPersistedSession()","), без сети",[225,1857,1858,1863,1866],{},[242,1859,1860],{},[179,1861,1862],{},"Callback",[242,1864,1865],{},"Firebase redirect",[242,1867,1868],{},"Вход по email\u002Fpassword, без OAuth-попапов",[225,1870,1871,1876,1878],{},[242,1872,1873],{},[179,1874,1875],{},"CORS",[242,1877,370],{},[242,1879,1880],{},"URL сервисов Supabase + PowerSync из runtime config",[225,1882,1883,1888,1891],{},[242,1884,1885],{},[179,1886,1887],{},"Логин",[242,1889,1890],{},"Firebase авто-редирект",[242,1892,1893,1894],{},"Сохраняет целевой редирект, вход через ",[209,1895,1896],{},"useSupabaseAuth()",[214,1898,1900],{"id":1899},"синхронизация-и-офлайн","Синхронизация и офлайн",[219,1902,1903,1913],{},[222,1904,1905],{},[225,1906,1907,1909,1911],{},[228,1908],{},[228,1910,211],{},[228,1912,235],{},[237,1914,1915,1930,1950,1971,1986,2007,2018],{},[225,1916,1917,1922,1927],{},[242,1918,1919],{},[179,1920,1921],{},"Механизм",[242,1923,1924,1925],{},"Хелперы в ",[209,1926,550],{},[242,1928,1929],{},"Встроенная очередь загрузки PowerSync - без рукописной офлайн-очереди",[225,1931,1932,1937,1940],{},[242,1933,1934],{},[179,1935,1936],{},"Записи",[242,1938,1939],{},"Разрозненные ключи localStorage",[242,1941,1942,292,1944,1946,1947,281],{},[209,1943,291],{},[209,1945,295],{}," пишут в локальный SQLite (INSERT\u002FUPDATE, никогда ",[209,1948,1949],{},"ON CONFLICT",[225,1951,1952,1957,1965],{},[242,1953,1954],{},[179,1955,1956],{},"Выгрузка",[242,1958,1959,1371,1962],{},[209,1960,1961],{},"saveTrnToAddLaterLocal()",[209,1963,1964],{},"removeTrnToDeleteLaterLocal()",[242,1966,1967,1970],{},[209,1968,1969],{},"SupabaseConnector.uploadData()"," выгружает очередь в Supabase",[225,1972,1973,1978,1981],{},[242,1974,1975],{},[179,1976,1977],{},"Чтения",[242,1979,1980],{},"Ручной при следующей загрузке",[242,1982,1983,1985],{},[209,1984,287],{}," - одна подписка покрывает начальную загрузку + realtime, локальные + синхронизированные",[225,1987,1988,1993,1995],{},[242,1989,1990],{},[179,1991,1992],{},"Разрешение конфликтов",[242,1994,596],{},[242,1996,1997,1998,377,2001,2003,2004],{},"Авто-реконциляция: отклонённые загрузки вычисляют ",[209,1999,2000],{},"divergedOps",[209,2002,602],{},") и запускают ",[209,2005,2006],{},"forceResync",[225,2008,2009,2013,2015],{},[242,2010,2011],{},[179,2012,132],{},[242,2014,596],{},[242,2016,2017],{},"Клиентские Zod-схемы на вводе формы; Postgres RLS на сервере",[225,2019,2020,2025,2027],{},[242,2021,2022],{},[179,2023,2024],{},"Первый кадр",[242,2026,596],{},[242,2028,2029,2030,2032],{},"Блоб read-through кеш (",[209,2031,536],{},") сажает сторы до инициализации PowerSync",[214,2034,2036],{"id":2035},"pwa","PWA",[219,2038,2039,2049],{},[222,2040,2041],{},[225,2042,2043,2045,2047],{},[228,2044],{},[228,2046,211],{},[228,2048,235],{},[237,2050,2051,2073,2089,2102],{},[225,2052,2053,2058,2067],{},[242,2054,2055],{},[179,2056,2057],{},"Стратегия",[242,2059,2060,2063,2064,281],{},[209,2061,2062],{},"injectManifest"," (кастомный SW через env var ",[209,2065,2066],{},"SW",[242,2068,2069,2072],{},[209,2070,2071],{},"generateSW"," (автогенерация Workbox)",[225,2074,2075,2080,2086],{},[242,2076,2077],{},[179,2078,2079],{},"Service worker",[242,2081,2082,2083],{},"Рукописный ",[209,2084,2085],{},"sw.ts",[242,2087,2088],{},"Нет - Workbox генерирует автоматически",[225,2090,2091,2096,2099],{},[242,2092,2093],{},[179,2094,2095],{},"Прекеширование",[242,2097,2098],{},"Ручная конфигурация",[242,2100,2101],{},"Автоматическое для всех ассетов сборки",[225,2103,2104,2109,2112],{},[242,2105,2106],{},[179,2107,2108],{},"Иконки",[242,2110,2111],{},"Загрузка с Iconify API в рантайме",[242,2113,2114,2115],{},"Включены в бандл на этапе сборки через ",[209,2116,2117],{},"clientBundle",[214,2119,2121],{"id":2120},"typescript","TypeScript",[219,2123,2124,2134],{},[222,2125,2126],{},[225,2127,2128,2130,2132],{},[228,2129],{},[228,2131,211],{},[228,2133,235],{},[237,2135,2136,2149,2171,2189,2201,2234,2254,2276,2306,2344,2365,2387,2406],{},[225,2137,2138,2143,2146],{},[242,2139,2140],{},[179,2141,2142],{},"Strict mode",[242,2144,2145],{},"Включён, но есть неразрешённые ошибки",[242,2147,2148],{},"Ноль ошибок TypeScript",[225,2150,2151,2156,2161],{},[242,2152,2153],{},[179,2154,2155],{},"Касты типов",[242,2157,2158,2160],{},[209,2159,1605],{}," по кодовой базе",[242,2162,2163,2164,2166,2167,2170],{},"Ноль ",[209,2165,1605],{}," - type guards, ",[209,2168,2169],{},"@vue-ignore",", узкие касты",[225,2172,2173,2177,2183],{},[242,2174,2175],{},[179,2176,1008],{},[242,2178,2179,2182],{},[209,2180,2181],{},"transferCategoriesIds"," (computed из данных категорий)",[242,2184,2185,2188],{},[209,2186,2187],{},"TrnType.Transfer"," enum + discriminated union типы",[225,2190,2191,2196,2198],{},[242,2192,2193],{},[179,2194,2195],{},"Валидация на рантайме",[242,2197,596],{},[242,2199,2200],{},"Zod-схемы для настроек, курсов, конфига статистики, параметров дат",[225,2202,2203,2208,2220],{},[242,2204,2205],{},[179,2206,2207],{},"Функции дат",[242,2209,2210,1371,2213,449,2216,2219],{},[209,2211,2212],{},"getStartOf(date, string)",[209,2214,2215],{},"getEndOf(date, string)",[209,2217,2218],{},"default"," fallback на day",[242,2221,2222,2223,2226,2227,980,2230,2233],{},"Типизированы как ",[209,2224,2225],{},"Period",", exhaustive ",[209,2228,2229],{},"switch",[209,2231,2232],{},"case 'day'"," - новые варианты вызывают ошибку компиляции",[225,2235,2236,2241,2248],{},[242,2237,2238],{},[179,2239,2240],{},"Конструкция Duration",[242,2242,1201,2243,1205,2245,2247],{},[209,2244,1204],{},[209,2246,1208],{},"Record\u003Cstring, number>`",[242,2249,2250,2253],{},[209,2251,2252],{},"toDuration(period, value): Duration"," - типизированный возврат через exhaustive switch",[225,2255,2256,2261,2270],{},[242,2257,2258],{},[179,2259,2260],{},"Маппинг типов статистики",[242,2262,2263,980,2266,2269],{},[209,2264,2265],{},"Record\u003Cstring, TrnType[]>",[209,2267,2268],{},"??"," fallback",[242,2271,2272,2275],{},[209,2273,2274],{},"Record\u003CSeriesSlugSelected | StatTabSlug, TrnType[]>"," - все ключи покрыты, fallback не нужен",[225,2277,2278,2283,2292],{},[242,2279,2280],{},[179,2281,2282],{},"Ключ средних",[242,2284,2285,2288,2289],{},[209,2286,2287],{},"key as keyof TotalReturns"," небезопасный каст + ",[209,2290,2291],{},"as number",[242,2293,2294,2295,2298,2299,508,2302,2305],{},"Типизированный ",[209,2296,2297],{},"key: keyof TotalReturns"," через явный маппинг ",[209,2300,2301],{},"'netIncome'",[209,2303,2304],{},"'sum'",", без кастов",[225,2307,2308,2313,2329],{},[242,2309,2310],{},[179,2311,2312],{},"Fallback курсов",[242,2314,2315,1371,2318,2321,2322,583,2325,2328],{},[209,2316,2317],{},"rates[code] || 1",[209,2319,2320],{},"wallet?.currency || 'USD'"," - falsy ",[209,2323,2324],{},"0",[209,2326,2327],{},"''"," триггерит fallback",[242,2330,2331,1371,2334,2337,2338,583,2341,2328],{},[209,2332,2333],{},"rates[code] ?? 1",[209,2335,2336],{},"wallet?.currency ?? 'USD'"," - только ",[209,2339,2340],{},"null",[209,2342,2343],{},"undefined",[225,2345,2346,2351,2359],{},[242,2347,2348],{},[179,2349,2350],{},"Парсинг query-параметров",[242,2352,2353,2321,2356,2358],{},[209,2354,2355],{},"if (data.rangeOffset)",[209,2357,2324],{}," пропускается",[242,2360,2361,2364],{},[209,2362,2363],{},"if (data.x !== undefined)"," - нулевые значения применяются корректно",[225,2366,2367,2372,2378],{},[242,2368,2369],{},[179,2370,2371],{},"Пропсы total",[242,2373,2374,2377],{},[209,2375,2376],{},"baseCurrencyCode?: string"," нестрогий тип",[242,2379,2380,2383,2384],{},[209,2381,2382],{},"baseCurrencyCode?: CurrencyCode"," - согласованно с ",[209,2385,2386],{},"getAmountInRate",[225,2388,2389,2394,2400],{},[242,2390,2391],{},[179,2392,2393],{},"Параметры стат. категорий",[242,2395,2396,2399],{},[209,2397,2398],{},"trnsItems: Record\u003CTrnId, { categoryId?: string }>"," инлайн-тип",[242,2401,2402,2405],{},[209,2403,2404],{},"trnsItems: Record\u003CTrnId, Pick\u003CTrnItem, 'categoryId'>>"," - привязан к исходному типу",[225,2407,2408,2413,2423],{},[242,2409,2410],{},[179,2411,2412],{},"Null guard в сортировке",[242,2414,2415,2418,2419,2422],{},[209,2416,2417],{},"sortCategoriesByAmount"," проверяет ",[209,2420,2421],{},"if (!a || !b)"," в рантайме",[242,2424,2425,2426,2429,2430],{},"Гард удален - TS уже гарантирует параметры ",[209,2427,2428],{},"CategoryWithData","; явный возвращаемый тип ",[209,2431,2432],{},": number",[214,2434,2436],{"id":2435},"тесты","Тесты",[219,2438,2439,2449],{},[222,2440,2441],{},[225,2442,2443,2445,2447],{},[228,2444],{},[228,2446,211],{},[228,2448,235],{},[237,2450,2451,2472,2487,2500,2512],{},[225,2452,2453,2458,2461],{},[242,2454,2455],{},[179,2456,2457],{},"Конфиг",[242,2459,2460],{},"Нет vitest config",[242,2462,2463,2464,2467,2468,2471],{},"2 проекта vitest: ",[209,2465,2466],{},"unit"," (node), ",[209,2469,2470],{},"store"," (happy-dom)",[225,2473,2474,2479,2481],{},[242,2475,2476],{},[179,2477,2478],{},"Тесты синхронизации",[242,2480,596],{},[242,2482,2483,2484,281],{},"Трансформации row\u003C->item, upload-реконциляция \u002F планирование дивергенции (",[209,2485,2486],{},"uploadReconcile.test.ts",[225,2488,2489,2494,2497],{},[242,2490,2491],{},[179,2492,2493],{},"Тесты калькулятора",[242,2495,2496],{},"Базовые",[242,2498,2499],{},"46 кейсов (приоритет операторов, десятичные, деление на ноль, инъекции)",[225,2501,2502,2507,2509],{},[242,2503,2504],{},[179,2505,2506],{},"Тесты извлечённой логики",[242,2508,596],{},[242,2510,2511],{},"Фильтрация\u002Fгруппировка\u002Fподсчёты кошельков (исключение архивных, видимость available), группировка категорий, auth-гейт, stat barUtils, useStatItem",[225,2513,2514,2519,2521],{},[242,2515,2516],{},[179,2517,2518],{},"Инфраструктура тестов",[242,2520,596],{},[242,2522,2523,2526],{},[209,2524,2525],{},"setup-store.ts"," (общие моки PowerSync\u002FSupabase)",[214,2528,2530],{"id":2529},"рефакторинг","Рефакторинг",[421,2532,2534],{"id":2533},"извлечение-чистых-функций","Извлечение чистых функций",[176,2536,2537],{},"Бизнес-логика перенесена из Vue-компонентов в тестируемые чистые функции:",[426,2539,2540,2569,2592,2606,2622],{},[429,2541,2542,2545,2546,1371,2549,1371,2552,1371,2554,1371,2557,1371,2560,1371,2563,1371,2566],{},[179,2543,2544],{},"Статистика:"," ",[209,2547,2548],{},"bucketTrnsByIntervals",[209,2550,2551],{},"computeAverageTotal",[209,2553,2417],{},[209,2555,2556],{},"getSelectedType",[209,2558,2559],{},"computeBarStyle",[209,2561,2562],{},"formatCompactAmount",[209,2564,2565],{},"computeSeriesAverage",[209,2567,2568],{},"getTrnTypeByAmount",[429,2570,2571,2545,2574,1371,2577,1371,2580,1371,2583,1371,2586,1371,2589],{},[179,2572,2573],{},"Кошельки:",[209,2575,2576],{},"filterWalletsByCurrency",[209,2578,2579],{},"filterWalletsByViewType",[209,2581,2582],{},"groupWalletsByProperty",[209,2584,2585],{},"computeWalletCounts",[209,2587,2588],{},"sumWalletAmounts",[209,2590,2591],{},"getCreditAvailable",[429,2593,2594,2545,2597,1371,2600,1371,2603],{},[179,2595,2596],{},"Категории:",[209,2598,2599],{},"collectCategoriesByTrns",[209,2601,2602],{},"flattenCategoriesWithValues",[209,2604,2605],{},"groupCategoriesWithValues",[429,2607,2608,2545,2611,2613,2614,2617,2618,2621],{},[179,2609,2610],{},"Синхронизация:",[209,2612,511],{}," (сведение сторов со свежими эмиссиями watch), ",[209,2615,2616],{},"planDivergence"," (upload-реконциляция), ",[209,2619,2620],{},"transforms"," (row\u003C->item)",[429,2623,2624,2627,2628,1371,2631,2634,2635,381,2638],{},[179,2625,2626],{},"Демо:"," упрощён до read-only - удалены CRUD-методы (",[209,2629,2630],{},"addDemoCategory",[209,2632,2633],{},"deleteDemoWallet"," и др.), только ",[209,2636,2637],{},"generateDemoData()",[209,2639,2640],{},"isDemo",[421,2642,2644],{"id":2643},"изменения-компонентов","Изменения компонентов",[176,2646,2647,2545,2650,1371,2653,1371,2656,1371,2659,1371,2662,1371,2665,1371,2668,1371,2671,1371,2674,1371,2677,1371,2680,1371,2683,1371,2686,1371,2689,1371,2692,1371,2695],{},[179,2648,2649],{},"Добавлены:",[209,2651,2652],{},"ActionButton",[209,2654,2655],{},"ChipButton",[209,2657,2658],{},"TabsScroll",[209,2660,2661],{},"TextMuted",[209,2663,2664],{},"SettingsCard",[209,2666,2667],{},"TitleSection",[209,2669,2670],{},"ItemBody",[209,2672,2673],{},"BottomSheetModal",[209,2675,2676],{},"WalletsPageListItem",[209,2678,2679],{},"WalletsPageGroupHeader",[209,2681,2682],{},"Onboarding",[209,2684,2685],{},"CurrenciesPageList",[209,2687,2688],{},"CurrenciesItem",[209,2690,2691],{},"ConfigSwitch",[209,2693,2694],{},"GroupingToggle",[209,2696,2697],{},"SelectorItem",[176,2699,2700,2545,2703,2706,2707,1371,2710,1371,2713,583,2716,583,2719,1371,2721,583,2724,1371,2727,583,2730,583,2733,583,2736,1371,2739,1371,2742,1371,2745,1371,2748,1371,2751,2754,2755,1371,2758,1371,2761],{},[179,2701,2702],{},"Удалены:",[209,2704,2705],{},"Item1","–",[209,2708,2709],{},"4",[209,2711,2712],{},"Tabs2",[209,2714,2715],{},"TabsItem1",[209,2717,2718],{},"3",[209,2720,2709],{},[209,2722,2723],{},"TextSm1",[209,2725,2726],{},"2",[209,2728,2729],{},"Title3",[209,2731,2732],{},"6",[209,2734,2735],{},"7",[209,2737,2738],{},"8",[209,2740,2741],{},"TitleOption",[209,2743,2744],{},"ToggleAction",[209,2746,2747],{},"SwitcherTabs",[209,2749,2750],{},"Welcome",[209,2752,2753],{},"Round"," (график статистики), ",[209,2756,2757],{},"SectionWithCollapse",[209,2759,2760],{},"CurrenciesToggleDep",[209,2762,2763],{},"getStyles.ts",[421,2765,2767],{"id":2766},"паттерны-компонентов","Паттерны компонентов",[426,2769,2770,2784],{},[429,2771,2772,2775,2776,2779,2780,2783],{},[179,2773,2774],{},"Мерж классов:"," утилита ",[209,2777,2778],{},"getStyles()"," удалена - заменена на ",[209,2781,2782],{},"cn()"," (clsx + tailwind-merge)",[429,2785,2786,2789,2790,583,2793,508,2796,292,2799],{},[179,2787,2788],{},"Инпуты форм:"," пропы ",[209,2791,2792],{},"value",[209,2794,2795],{},"updateValue",[209,2797,2798],{},"v-model",[209,2800,2801],{},"defineModel",[421,2803,2805],{"id":2804},"декомпозиция-composables","Декомпозиция composables",[426,2807,2808,2827,2834,2855,2869,2890,2941,2957,2979,3003,3031,3051,3078,3090,3108,3123,3137,3155],{},[429,2809,2810,2545,2812,1371,2815,1371,2818,1371,2821,1371,2824],{},[179,2811,2573],{},[209,2813,2814],{},"useWalletContextMenu",[209,2816,2817],{},"useWalletDelete",[209,2819,2820],{},"useWalletsFilter",[209,2822,2823],{},"useWalletsGrouping",[209,2825,2826],{},"useWalletsCounts",[429,2828,2829,2545,2831],{},[179,2830,2596],{},[209,2832,2833],{},"useCategoryContextMenu",[429,2835,2836,2838,2839,1371,2842,1371,2845,2848,2849,583,2852],{},[179,2837,2544],{}," Symbol-based injection keys (",[209,2840,2841],{},"filterKey",[209,2843,2844],{},"statDateKey",[209,2846,2847],{},"statConfigKey",") для типобезопасного ",[209,2850,2851],{},"provide",[209,2853,2854],{},"inject",[429,2856,2857,2860,2861,2864,2865,2868],{},[179,2858,2859],{},"Страницы статистики:"," composable ",[209,2862,2863],{},"useStatPage"," - дублированная инфраструктура статистики (создание и provide filter, statConfig, statDate, а также activeTab, storageKey, trnsIds, maxRange) извлечена из трёх компонентов-страниц (Dashboard, Category, Wallet) в один переиспользуемый composable. Каждая страница передаёт только свою entity-specific логику фильтрации через коллбэк ",[209,2866,2867],{},"getTrnsFilter",". Скрипт-секция сокращена на 24–53% на трёх страницах",[429,2870,2871,2878,2879,1371,2882,2885,2886,2889],{},[179,2872,2873,2874,2877],{},"Composable ",[209,2875,2876],{},"useCategoryLongPress",":"," извлечён дублированный код long-press (59 строк x 3 файла) из ",[209,2880,2881],{},"Round2lines.vue",[209,2883,2884],{},"Line.vue"," и ",[209,2887,2888],{},"Vertical.vue"," в общий composable. Обрабатывает долгое нажатие для создания транзакции из категории с корректным выбором даты из интервалов графика",[429,2891,2892,2898,2899,1371,2902,1371,2905,1371,2908,1371,2911,2914,2915,2918,2919,2885,2922,2925,2926,377,2928,1371,2931,1371,2934,1371,2937,2940],{},[179,2893,2894,2895,2877],{},"Инкапсуляция ",[209,2896,2897],{},"useStatDate"," навигационные computed-свойства (",[209,2900,2901],{},"shouldShowNav",[209,2903,2904],{},"isAtEnd",[209,2906,2907],{},"isAtStart",[209,2909,2910],{},"isDayToday",[209,2912,2913],{},"shouldShowNavHome",") и прямые мутации ",[209,2916,2917],{},"params.value"," перенесены из ",[209,2920,2921],{},"Navigation.vue",[209,2923,2924],{},"chart\u002FWrap.vue"," в методы ",[209,2927,2897],{},[209,2929,2930],{},"selectInterval()",[209,2932,2933],{},"resetInterval()",[209,2935,2936],{},"setIntervalsBy()",[209,2938,2939],{},"navigate()","). Скрипт Navigation.vue сокращён с ~54 строк до 5",[429,2942,2943,2949,2950,2953,2954],{},[179,2944,2945,2946,2877],{},"Удалён дубликат ",[209,2947,2948],{},"stat\u002Fdate\u002FNav.vue"," удалена идентичная копия ",[209,2951,2952],{},"date\u002FNav.vue"," - остался только канонический компонент ",[209,2955,2956],{},"DateNav",[429,2958,2959,2965,2966,2969,2970,1371,2972,1371,2975,2978],{},[179,2960,2961,2962,2877],{},"Компонент ",[209,2963,2964],{},"UiNumberStepper"," извлечён дублированный паттерн ±\u002Finput из ",[209,2967,2968],{},"ConfigModal.vue"," в переиспользуемый UI-примитив с ",[209,2971,2798],{},[209,2973,2974],{},"min",[209,2976,2977],{},"max"," пропами",[429,2980,2981,2545,2984,2987,2988,2991,2992,2885,2995,2998,2999,3002],{},[179,2982,2983],{},"Автономность stat-компонентов:",[209,2985,2986],{},"StatAverage"," - три условных блока ",[209,2989,2990],{},"Amount"," консолидированы в один через computed; ",[209,2993,2994],{},"StatSumWrap",[209,2996,2997],{},"StatChartWrap"," - убран избыточный проброс пропов в пользу inject; ",[209,3000,3001],{},"StatItem"," - два модальных блока объединены в один через state-machine паттерн",[429,3004,3005,2545,3008,2885,3011,3013,3014,3017,3018,1371,3021,1371,3024,1371,3027,3030],{},[179,3006,3007],{},"Кэширование конфигов:",[209,3009,3010],{},"Section2.vue",[209,3012,2884],{}," кэшируют чтения ",[209,3015,3016],{},"statConfig.config.value.catsList.*"," в computed-свойства (",[209,3019,3020],{},"isItemsBg",[209,3022,3023],{},"isLines",[209,3025,3026],{},"isRoundIcon",[209,3028,3029],{},"isListGrouped"," и др.) для читаемости шаблонов",[429,3032,3033,3039,3040,3043,3044,3047,3048],{},[179,3034,3035,3036,2877],{},"Упрощение ",[209,3037,3038],{},"Range.vue"," ручное индексирование ",[209,3041,3042],{},"intervalsInRange"," заменено на ",[209,3045,3046],{},"statDate.selectedInterval","; извлечён computed ",[209,3049,3050],{},"isIntervalSelected",[429,3052,3053,3059,3060,3063,3064,3067,3068,3071,3072,508,3075,281],{},[179,3054,3055,3056,2877],{},"Очистка ",[209,3057,3058],{},"useStatItem"," извлечён computed ",[209,3061,3062],{},"baseTrnsIdsForSelection"," для устранения дублирования логики выбора интервала; исправлен небезопасный каст ",[209,3065,3066],{},"type.value as keyof TotalReturns"," на ",[209,3069,3070],{},"type.value ?? 'sum'","; исправлен знаменатель среднего в графике - использовался неверный источник данных (",[209,3073,3074],{},"intervalsData",[209,3076,3077],{},"intervalsDataWithFilteredCategories",[429,3079,3080,3085,3086,3089],{},[179,3081,2873,3082,2877],{},[209,3083,3084],{},"useCategoriesExpanded"," извлечена логика expand\u002Fcollapse (52 строки) из ",[209,3087,3088],{},"DetailedSection.vue"," (286->234 строк)",[429,3091,3092,2545,3095,583,3098,508,3101,583,3104,3107],{},[179,3093,3094],{},"Переименование stat-компонентов:",[209,3096,3097],{},"Section",[209,3099,3100],{},"Section2",[209,3102,3103],{},"RoundSection",[209,3105,3106],{},"DetailedSection"," для ясности",[429,3109,3110,2545,3116,2885,3119,3122],{},[179,3111,3112,3113,2877],{},"Извлечение ",[209,3114,3115],{},"barUtils",[209,3117,3118],{},"computeBarStyle()",[209,3120,3121],{},"formatCompactAmount()"," извлечены из инлайн-шаблонов в тестируемые утилиты",[429,3124,3125,3128,3129,3132,3133,3136],{},[179,3126,3127],{},"Консистентность статистики кошельков:"," архивные кошельки полностью исключены из всех подсчётов (итого, суммы по типам, снятие, доступно) через ранний ",[209,3130,3131],{},"continue","; ",[209,3134,3135],{},"available.isShow"," требует наличия и withdrawal, и credit кошельков; авто-сброс фильтра на «Итого» при переключении валюты, если текущий фильтр пуст",[429,3138,3139,2545,3142,583,3145,583,3148,3151,3152],{},[179,3140,3141],{},"Консолидация store sync:",[209,3143,3144],{},"showErrorToast",[209,3146,3147],{},"showSuccessToast",[209,3149,3150],{},"showWarningToast"," объединены в ",[209,3153,3154],{},"showToast(type, key, params?)",[429,3156,3157,2545,3160,3163,3164],{},[179,3158,3159],{},"Константы кошельков:",[209,3161,3162],{},"WALLET_STORAGE_KEYS"," извлечены из инлайн-строк в ",[209,3165,3166],{},"constants.ts",[421,3168,3170],{"id":3169},"семантическое-переименование","Семантическое переименование",[219,3172,3173,3181],{},[222,3174,3175],{},[225,3176,3177,3179],{},[228,3178,211],{},[228,3180,235],{},[237,3182,3183,3195,3206,3218,3229,3251,3263,3275,3287,3299,3312,3324,3337,3349,3361,3372,3382,3394,3406,3418],{},[225,3184,3185,3190],{},[242,3186,3187],{},[209,3188,3189],{},"getTrnsIds",[242,3191,3192],{},[209,3193,3194],{},"filterTrnsIds",[225,3196,3197,3202],{},[242,3198,3199],{},[209,3200,3201],{},"getCategoriesWithData",[242,3203,3204],{},[209,3205,823],{},[225,3207,3208,3213],{},[242,3209,3210],{},[209,3211,3212],{},"getTotalOfTrnsIds",[242,3214,3215],{},[209,3216,3217],{},"computeTotalForTrnsIds",[225,3219,3220,3225],{},[242,3221,3222],{},[209,3223,3224],{},"getChildsIds",[242,3226,3227],{},[209,3228,1430],{},[225,3230,3231,3242],{},[242,3232,3233,292,3236,292,3239],{},[209,3234,3235],{},"trnFormCreate",[209,3237,3238],{},"Edit",[209,3240,3241],{},"Duplicate",[242,3243,3244,292,3247,292,3249],{},[209,3245,3246],{},"openFormForCreate",[209,3248,3238],{},[209,3250,3241],{},[225,3252,3253,3258],{},[242,3254,3255],{},[209,3256,3257],{},"getIsShowSum",[242,3259,3260],{},[209,3261,3262],{},"shouldShowSum",[225,3264,3265,3270],{},[242,3266,3267],{},[209,3268,3269],{},"isItTransactible",[242,3271,3272],{},[209,3273,3274],{},"isTransactible",[225,3276,3277,3282],{},[242,3278,3279],{},[209,3280,3281],{},"editedAt",[242,3283,3284],{},[209,3285,3286],{},"updatedAt",[225,3288,3289,3294],{},[242,3290,3291],{},[209,3292,3293],{},"TransferType",[242,3295,3296],{},[209,3297,3298],{},"TransferSide",[225,3300,3301,3307],{},[242,3302,3303,3306],{},[209,3304,3305],{},"Selector2"," (categories)",[242,3308,3309],{},[209,3310,3311],{},"SelectorGrid",[225,3313,3314,3319],{},[242,3315,3316,3306],{},[209,3317,3318],{},"Selector",[242,3320,3321],{},[209,3322,3323],{},"SelectorTree",[225,3325,3326,3332],{},[242,3327,3328,3331],{},[209,3329,3330],{},"Checkbox"," (ui)",[242,3333,3334],{},[209,3335,3336],{},"SwitchItem",[225,3338,3339,3344],{},[242,3340,3341,3331],{},[209,3342,3343],{},"Tabs1",[242,3345,3346],{},[209,3347,3348],{},"TabsBar",[225,3350,3351,3356],{},[242,3352,3353,3331],{},[209,3354,3355],{},"Toggle3",[242,3357,3358],{},[209,3359,3360],{},"ToggleControlled",[225,3362,3363,3368],{},[242,3364,3365,3367],{},[209,3366,3097],{}," (stat)",[242,3369,3370],{},[209,3371,3103],{},[225,3373,3374,3378],{},[242,3375,3376,3367],{},[209,3377,3100],{},[242,3379,3380],{},[209,3381,3106],{},[225,3383,3384,3389],{},[242,3385,3386],{},[209,3387,3388],{},"isNotTransferCategory",[242,3390,3391],{},[209,3392,3393],{},"shouldShowAmounts",[225,3395,3396,3401],{},[242,3397,3398],{},[209,3399,3400],{},"biggestCatNumber",[242,3402,3403],{},[209,3404,3405],{},"maxCategoryValues",[225,3407,3408,3413],{},[242,3409,3410],{},[209,3411,3412],{},"addMarkArea",[242,3414,3415],{},[209,3416,3417],{},"withMarkArea",[225,3419,3420,3425],{},[242,3421,3422],{},[209,3423,3424],{},"toggleOpened",[242,3426,3427],{},[209,3428,3429],{},"toggleAllCategoriesExpanded",[421,3431,3433],{"id":3432},"модель-данных","Модель данных",[219,3435,3436,3446],{},[222,3437,3438],{},[225,3439,3440,3442,3444],{},[228,3441],{},[228,3443,211],{},[228,3445,235],{},[237,3447,3448,3467,3482],{},[225,3449,3450,3455,3461],{},[242,3451,3452],{},[179,3453,3454],{},"Дочерние категории",[242,3456,3457,3458],{},"Денормализованный массив ",[209,3459,3460],{},"childIds",[242,3462,3463,3464],{},"Вычисляются динамически из ",[209,3465,3466],{},"parentId",[225,3468,3469,3473,3477],{},[242,3470,3471],{},[179,3472,1008],{},[242,3474,3475,2182],{},[209,3476,2181],{},[242,3478,3479,3480],{},"Значение enum ",[209,3481,2187],{},[225,3483,3484,3489,3492],{},[242,3485,3486],{},[179,3487,3488],{},"Корректировки",[242,3490,3491],{},"Не выделены",[242,3493,3494,3497],{},[209,3495,3496],{},"categoryId === 'adjustment'",", исключены из статистики доходов\u002Fрасходов",[214,3499,3501],{"id":3500},"ux-улучшения","UX-улучшения",[426,3503,3504,3510,3526,3536,3542,3557,3566,3572,3578,3588,3594,3604,3613],{},[429,3505,3506,3509],{},[179,3507,3508],{},"Тактильная вибрация"," при создании транзакции (вибрация на мобильных)",[429,3511,3512,2545,3515,508,3518,3521,3522,3525],{},[179,3513,3514],{},"Доступность:",[209,3516,3517],{},"\u003Cdiv>",[209,3519,3520],{},"\u003Cbutton>"," в интерактивных компонентах, i18n ",[209,3523,3524],{},"aria-label"," на кнопках-иконках",[429,3527,3528,3531,3532,3535],{},[179,3529,3530],{},"Калькулятор:"," исправлен бесконечный цикл при нулевом результате, корректный показ ",[209,3533,3534],{},"0.",", округление деления",[429,3537,3538,3541],{},[179,3539,3540],{},"Форма:"," сумма сбрасывается сразу после submit, popover закрывается перед навигацией",[429,3543,3544,3547,3548,3550,3551,3553,3554,281],{},[179,3545,3546],{},"Онбординг:"," компонент ",[209,3549,2750],{}," заменён на ",[209,3552,2682],{}," - улучшённый дизайн, состояние в куке (",[209,3555,3556],{},"finapp.isOnboarded",[429,3558,3559,3562,3563],{},[179,3560,3561],{},"Страница валют:"," два раздела (Используемые \u002F Все) с глобальным поиском, O(1) поиск через ",[209,3564,3565],{},"currencyMap",[429,3567,3568,3571],{},[179,3569,3570],{},"Детали кошелька\u002Fкатегории:"," действия редактирования\u002Fудаления перенесены в меню с тремя точками",[429,3573,3574,3577],{},[179,3575,3576],{},"Статистика кошельков:"," архивные кошельки полностью исключены из всех вкладок-фильтров; вкладка «Доступно» показывается только при наличии и withdrawal, и кредитных кошельков; авто-сброс фильтра на «Итого» при переключении валюты, если фильтр пуст",[429,3579,3580,3583,3584,3587],{},[179,3581,3582],{},"Защита редактирования кошелька:"," редирект на ",[209,3585,3586],{},"\u002Fwallets"," при несуществующем кошельке",[429,3589,3590,3593],{},[179,3591,3592],{},"Закрытие тоста:"," клик по уведомлению закрывает его",[429,3595,3596,3599,3600,3603],{},[179,3597,3598],{},"Layout:"," проп ",[209,3601,3602],{},"keepalive"," из слота layout для сохранения состояния при переходах",[429,3605,3606,3609,3610,3612],{},[179,3607,3608],{},"Страница настроек:"," реорганизована с ",[209,3611,2664],{},", выпадающий выбор языка, модальный выбор валюты, зона опасности с «Удалить все данные»",[429,3614,3615,3618],{},[179,3616,3617],{},"Страница логина:"," отображение версии приложения",[214,3620,3621],{"id":3621},"i18n",[219,3623,3624,3634],{},[222,3625,3626],{},[225,3627,3628,3630,3632],{},[228,3629],{},[228,3631,211],{},[228,3633,235],{},[237,3635,3636,3658,3686,3705,3730],{},[225,3637,3638,3643,3646],{},[242,3639,3640],{},[179,3641,3642],{},"Сообщения об ошибках",[242,3644,3645],{},"Общие или отсутствуют",[242,3647,3648,3649,1371,3652,1371,3655,281],{},"Ключи ошибок по сущностям (",[209,3650,3651],{},"categories.errors.*",[209,3653,3654],{},"wallets.errors.*",[209,3656,3657],{},"trns.errors.*",[225,3659,3660,3665,3674],{},[242,3661,3662],{},[179,3663,3664],{},"Лейблы форм",[242,3666,3667,3668,1371,3671,281],{},"Многословные (",[209,3669,3670],{},"Category name",[209,3672,3673],{},"Category color",[242,3675,3676,3677,1371,3680,1371,3683,281],{},"Краткие (",[209,3678,3679],{},"Name",[209,3681,3682],{},"Color",[209,3684,3685],{},"Icon",[225,3687,3688,3693,3699],{},[242,3689,3690],{},[179,3691,3692],{},"Онбординг",[242,3694,3695,3696],{},"Ключи ",[209,3697,3698],{},"welcome.*",[242,3700,3701,3704],{},[209,3702,3703],{},"onboarding.*"," с пошаговым руководством",[225,3706,3707,3711,3719],{},[242,3708,3709],{},[179,3710,51],{},[242,3712,3713,1371,3716],{},[209,3714,3715],{},"neutral",[209,3717,3718],{},"radius",[242,3720,3721,1371,3724,1371,3727],{},[209,3722,3723],{},"Background color",[209,3725,3726],{},"Rounding",[209,3728,3729],{},"Appearance",[225,3731,3732,3737,3742],{},[242,3733,3734],{},[179,3735,3736],{},"Опечатки",[242,3738,3739],{},[209,3740,3741],{},"errorChilds",[242,3743,3744],{},[209,3745,3746],{},"errorChildren",[214,3748,3750],{"id":3749},"качество-кода","Качество кода",[421,3752,3754],{"id":3753},"удалено-из-firebase","Удалено из firebase",[426,3756,3757,3766,3769,3772,3787,3799,3802],{},[429,3758,3759,3760,1371,3762,1371,3764],{},"Зависимости ",[209,3761,608],{},[209,3763,211],{},[209,3765,264],{},[429,3767,3768],{},"Старые page-specific wallet composables",[429,3770,3771],{},"Мёртвый код: неиспользуемые props, emits, функции, протухшие TODO",[429,3773,3774,3775,1371,3778,1371,3781,1371,3784],{},"Устаревшие типы: ",[209,3776,3777],{},"WalletsDirty",[209,3779,3780],{},"TrnItemDirty",[209,3782,3783],{},"TransferDeprecated",[209,3785,3786],{},"TrnTypeSlug",[429,3788,3789,3790,1371,3793,1371,3796],{},"CSS-утилиты: ",[209,3791,3792],{},"flex-center-col",[209,3794,3795],{},"absolute-center",[209,3797,3798],{},"layoutBase",[429,3800,3801],{},"Старые UI-компоненты (Item1–4, Title3\u002F6\u002F7\u002F8 и др.)",[429,3803,3804,3805,1371,3808,1371,3811,281],{},"Конфиги Firebase (",[209,3806,3807],{},"firebase.json",[209,3809,3810],{},".firebaserc",[209,3812,3813],{},"pnpm-workspace.yaml",[421,3815,3817],{"id":3816},"исправлено","Исправлено",[426,3819,3820,3831,3838,3844,3851,3857,3860,3870,3885,3894,3901,3922,3929,3938,3950,3963,3968,3981,3994,4007,4019,4038,4047,4062,4075,4094],{},[429,3821,3822,3823,3826,3827,3830],{},"Утечки памяти: ",[209,3824,3825],{},"addEventListener"," без очистки, ",[209,3828,3829],{},"ResizeObserver"," без отключения",[429,3832,3833,3834,3837],{},"Null spreading в 3 сторах (отсутствовали ",[209,3835,3836],{},"?? {}"," гарды)",[429,3839,3840,3841],{},"Emit во время рендера в ",[209,3842,3843],{},"SelectionCategoriesFast",[429,3845,3846,3847,3850],{},"Магические числа заменены на ",[209,3848,3849],{},"TrnType"," enum (8 мест)",[429,3852,3853,3856],{},[209,3854,3855],{},"ref\u003Cany>"," заменены на корректные типы (4 файла)",[429,3858,3859],{},"Layout ошибок локализован (хардкод на английском -> i18n-ключи)",[429,3861,3862,3863,508,3866,3869],{},"CSS-синтаксис Tailwind v4: ",[209,3864,3865],{},"[var(--name)]",[209,3867,3868],{},"(--name)"," функциональная нотация",[429,3871,3872,3873,1753,3875,3878,3879,3881,3882],{},"Radius использует ",[209,3874,2268],{},[209,3876,3877],{},"||","), чтобы ",[209,3880,2324],{}," был валидным: ",[209,3883,3884],{},"radius ?? 0.375",[429,3886,3887,3888,508,3891],{},"Опечатка CSS-утилиты: ",[209,3889,3890],{},"bottomSheetDrugClassesCustom",[209,3892,3893],{},"bottomSheetDragClassesCustom",[429,3895,3896,3897,3900],{},"ESLint: исключение ",[209,3898,3899],{},"vue\u002Fno-multiple-template-root"," для layout",[429,3902,3903,3906,3907,3910,3911,2885,3914,3917,3918,3921],{},[209,3904,3905],{},"createLogger(prefix)"," - dev-only ",[209,3908,3909],{},"log",", always-on ",[209,3912,3913],{},"warn",[209,3915,3916],{},"error",", с ",[209,3919,3920],{},"[prefix]"," форматированием во всех модулях",[429,3923,3924,3925,3928],{},"Захардкоженная локаль ",[209,3926,3927],{},"'ru'"," в форматтерах графиков -> динамическая локаль из настроек",[429,3930,3931,3934,3935],{},[209,3932,3933],{},"Statistics.vue"," пустые секции групп скрыты через ",[209,3936,3937],{},"v-show",[429,3939,3940,2545,3943,3946,3947],{},[209,3941,3942],{},"Selector.vue",[209,3944,3945],{},"onSelectRange(value: any)"," -> типизированный ",[209,3948,3949],{},"{ end: unknown, start: unknown }",[429,3951,3952,3953,3956,3957,3067,3960],{},"Тип ",[209,3954,3955],{},"setWalletViewType"," исправлен с ",[209,3958,3959],{},"WalletType | 'total'",[209,3961,3962],{},"WalletViewTypes | 'total'",[429,3964,3965,3966,281],{},"Версия повышена с v6.6.4 (базовая на ветке ",[209,3967,211],{},[429,3969,3970,3973,3974,3977,3978,3980],{},[209,3971,3972],{},"useAmount.getAmountInBaseRate",": удалён мёртвый параметр ",[209,3975,3976],{},"noFormat",", убран лишний унарный ",[209,3979,1655],{}," над числом",[429,3982,3983,3986,3987,2885,3990,3993],{},[209,3984,3985],{},"useFilter",": убраны избыточные ",[209,3988,3989],{},"[...array.filter()]",[209,3991,3992],{},"[...string.split()]"," - оба метода уже возвращают новые массивы",[429,3995,3996,682,3999,4002,4003,4006],{},[209,3997,3998],{},"getUCalendarTimedDate",[209,4000,4001],{},"new Date(string)"," парсился как UTC -> ",[209,4004,4005],{},"date.toDate(getLocalTimeZone())"," для корректной локальной таймзоны",[429,4008,4009,4011,4012,4015,4016],{},[209,4010,1256],{},": паттерн ",[209,4013,4014],{},"type: 'start' | 'end'"," удалён - format-функции возвращают полную строку напрямую, 3 IIFE-switch заменены на ",[209,4017,4018],{},"isSamePeriod",[429,4020,4021,682,4024,4027,4028,4031,4032,4034,4035],{},[209,4022,4023],{},"getIntervalsInRange",[209,4025,4026],{},"rangeOffset"," сделан опциональным в ",[209,4029,4030],{},"IntervalsInRangeProps"," - не используется в ",[209,4033,4023],{},", только в ",[209,4036,4037],{},"calculateIntervalInRange",[429,4039,4040,682,4042,508,4044,4046],{},[209,4041,1300],{},[209,4043,3877],{},[209,4045,2268],{}," для fallback пустой строки (консистентная nullish-семантика)",[429,4048,4049,4052,4053,4055,4056,4059,4060],{},[209,4050,4051],{},"useCategoriesStore",": убраны 5 лишних ",[209,4054,3836],{}," defensive-проверок - ",[209,4057,4058],{},"items"," всегда инициализирован через ",[209,4061,612],{},[429,4063,4064,4066,4067,4070,4071,4074],{},[209,4065,2876],{},": убран избыточный ",[209,4068,4069],{},"!"," после optional chaining (",[209,4072,4073],{},"value?.start"," уже сужает тип)",[429,4076,4077,682,4079,508,4082,3132,4085,980,4087,4090,4091],{},[209,4078,3084],{},[209,4080,4081],{},"forEach",[209,4083,4084],{},"for...of",[209,4086,654],{},[209,4088,4089],{},"as"," кастом -> ",[209,4092,4093],{},"Object.fromEntries",[429,4095,4096,682,4098,583,4101,4104,4105,583,4108,4111,4112,4115],{},[209,4097,2417],{},[209,4099,4100],{},"isP",[209,4102,4103],{},"isN"," переименованы в ",[209,4106,4107],{},"bothPositive",[209,4109,4110],{},"bothNegative"," для читаемости; убран избыточный ",[209,4113,4114],{},"categories ??= []"," (уже инициализирован при группировке)",[421,4117,1558],{"id":4118},"зависимости",[176,4120,4121],{},[4122,4123,4124,4125,4127,4128,4131],"em",{},"(Что изменилось от ветки ",[209,4126,211],{}," к текущему приложению. Полный список - в ",[209,4129,4130],{},"package.json"," проекта.)",[219,4133,4134,4144],{},[222,4135,4136],{},[225,4137,4138,4141],{},[228,4139,4140],{},"Удалены (эпоха firebase)",[228,4142,4143],{},"Добавлены (текущие)",[237,4145,4146,4157,4168,4179,4190],{},[225,4147,4148,4152],{},[242,4149,4150],{},[209,4151,211],{},[242,4153,4154],{},[209,4155,4156],{},"@powersync\u002Fweb",[225,4158,4159,4163],{},[242,4160,4161],{},[209,4162,264],{},[242,4164,4165],{},[209,4166,4167],{},"@supabase\u002Fsupabase-js",[225,4169,4170,4174],{},[242,4171,4172],{},[209,4173,608],{},[242,4175,4176],{},[209,4177,4178],{},"@vueuse\u002Fnuxt",[225,4180,4181,4185],{},[242,4182,4183],{},[209,4184,859],{},[242,4186,4187],{},[209,4188,4189],{},"zod",[225,4191,4192,4194],{},[242,4193],{},[242,4195,4196],{},[209,4197,4198],{},"es-toolkit",{"title":4200,"searchDepth":4201,"depth":4201,"links":4202},"",2,[4203,4207,4208,4212,4213,4214,4215,4216,4217,4218,4226,4227,4228],{"id":216,"depth":4201,"text":217,"children":4204},[4205],{"id":423,"depth":4206,"text":424},3,{"id":474,"depth":4201,"text":475},{"id":616,"depth":4201,"text":142,"children":4209},[4210,4211],{"id":619,"depth":4206,"text":620},{"id":1446,"depth":4206,"text":1447},{"id":1617,"depth":4201,"text":1618},{"id":1759,"depth":4201,"text":21},{"id":1899,"depth":4201,"text":1900},{"id":2035,"depth":4201,"text":2036},{"id":2120,"depth":4201,"text":2121},{"id":2435,"depth":4201,"text":2436},{"id":2529,"depth":4201,"text":2530,"children":4219},[4220,4221,4222,4223,4224,4225],{"id":2533,"depth":4206,"text":2534},{"id":2643,"depth":4206,"text":2644},{"id":2766,"depth":4206,"text":2767},{"id":2804,"depth":4206,"text":2805},{"id":3169,"depth":4206,"text":3170},{"id":3432,"depth":4206,"text":3433},{"id":3500,"depth":4201,"text":3501},{"id":3621,"depth":4201,"text":3621},{"id":3749,"depth":4201,"text":3750,"children":4229},[4230,4231,4232],{"id":3753,"depth":4206,"text":3754},{"id":3816,"depth":4206,"text":3817},{"id":4118,"depth":4206,"text":1558},"Что изменилось между v6 (Firebase) и текущим приложением на Supabase + PowerSync - бэкенд, производительность, безопасность, тесты, архитектура.","md",null,{},{"icon":140},{"title":137,"description":4239},"Подробное сравнение Finapp v6 (Firebase) и текущего приложения на Supabase Postgres + PowerSync - изменения бэкенда, производительность, безопасность, тесты, рефакторинг и архитектура.","vZkXfdD6pJRaqMvx8YcwM7vv3b9YcE8isJjC6lVYJis",[4242,4244],{"title":132,"path":133,"stem":134,"description":4243,"icon":135,"children":-1},"Клиентская валидация и компромиссы.",{"title":142,"path":143,"stem":144,"description":4245,"icon":145,"children":-1},"Бюджет холодного старта, ленивые чанки, бандлинг иконок и встроенные метки инструментирования.",1782114341066]