[{"data":1,"prerenderedAt":1244},["ShallowReactive",2],{"navigation_docs_ru":3,"-ru-reference-architecture":167,"-ru-reference-architecture-surround":1239},[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":108,"body":169,"description":1230,"extension":1231,"links":1232,"meta":1233,"navigation":1234,"path":109,"seo":1235,"stem":110,"__hash__":1238},"docs_ru\u002Fru\u002F3.reference\u002F01.architecture.md",{"type":170,"value":171,"toc":1200},"minimark",[172,177,188,196,201,204,284,288,294,297,348,352,371,377,381,387,400,404,410,435,439,456,460,474,477,481,487,547,575,579,582,631,635,655,659,673,701,705,719,723,729,735,739,789,793,837,841,867,871,896,900,907,911,922,926,1179,1183],[173,174,176],"h2",{"id":175},"инициализация-приложения","Инициализация приложения",[178,179,184],"pre",{"className":180,"code":182,"language":183},[181],"language-text","запуск приложения\n├── Плагины\n│   ├── theme.ts (enforce: 'post')\n│   │   └── Чтение темы из localStorage → обновление appConfig\n│   └── powersync.client.ts\n│       ├── navigator.storage.persist() - защита IndexedDB от вытеснения\n│       ├── сессия сохранена? → ранний getPowerSyncDb().init() (параллельно с загрузкой приложения)\n│       ├── setUploadErrorHandler → авто-согласование фатальных отказов загрузки\n│       └── watch uid (immediate)\n│           ├── uid есть → connectPowerSync(...)\n│           └── auth определён, uid нет → disconnectPowerSync()\n├── app.vue\n│   ├── useHead() - CSS-переменные темы (tagPriority: -2)\n│   ├── useGuard() - логика редиректа авторизации\n│   └── рендер layout + page\n└── useInitApp().initApp() (дефолтный layout, через useAsyncData)\n    ├── primeStoresFromCache() - снимок localforage пользователя → мгновенная первая отрисовка\n    ├── startWatches() - 5 подписок watchTable гидрируют хранилища\n    │   ├── useCurrenciesStore  - watchTable('SELECT * FROM rates', ...)\n    │   ├── useUserStore        - watchTable('SELECT * FROM user_settings', ...)\n    │   ├── useWalletsStore     - watchTable('SELECT * FROM wallets', ...)\n    │   ├── useCategoriesStore  - watchTable('SELECT * FROM categories', ...)\n    │   └── useTrnsStore        - watchTable('SELECT * FROM trns', ...)\n    └── awaitInitialSync() - первая серверная синхронизация завершается в фоне (только онлайн)\n","text",[185,186,182],"code",{"__ignoreMap":187},"",[189,190,191,192,195],"p",{},"Каждый ",[185,193,194],{},"watchTable"," срабатывает немедленно с текущими локальными строками SQLite и повторно при каждом изменении (локальная запись или входящая синхронизация). Отдельного шага «инициализация + подписка» нет.",[197,198,200],"h3",{"id":199},"cache-first-холодный-старт","Cache-first холодный старт",[189,202,203],{},"Загрузка идёт по принципу cache-first - сплеш-экрана нет:",[205,206,207,227,238,259],"ul",{},[208,209,210,214,215,218,219,222,223,226],"li",{},[211,212,213],"strong",{},"Blob-кеш"," (",[185,216,217],{},"app\u002Fapp\u002Fcomposables\u002FuseStoreCache.ts","): один localforage-блоб на пользователя (",[185,220,221],{},"finapp.cache.\u003Cuid>",") со снимком хранилищ из последней сессии. Он читается один раз при загрузке и наполняет все пять хранилищ до завершения ",[185,224,225],{},"db.init"," PowerSync и первых сканов SQLite. Записи - дебаунс-обновления (400 мс) затронутых срезов по схеме read-modify-write. Источником истины остаётся SQLite; блоб - только кеш для чтения.",[208,228,229,232,233,237],{},[211,230,231],{},"Согласование с истиной",": подписки перезаписывают данные из кеша строками SQLite. Пустая ",[234,235,236],"em",{},"первая"," эмиссия сохраняет данные из кеша (на свежем устройстве первая синхронизация просто ещё не пришла); любая последующая пустая эмиссия - настоящая очистка и применяется.",[208,239,240,214,243,246,247,250,251,254,255,258],{},[211,241,242],{},"Состояние загрузки",[185,244,245],{},"useInitApp.ts","): единый computed ",[185,248,249],{},"bootState"," - ",[185,252,253],{},"'ready' | 'onboarding' | 'error'",". Дашборд показывает скелетон, резервирующий место (",[185,256,257],{},"StatPageSkeleton","), пока хранилища гидрируются; онбординг рендерится только когда первая синхронизация действительно завершилась и аккаунт пуст; экран ошибки (с повтором) - только когда синхронизация не удалась и локальных данных для отката нет.",[208,260,261,264,265,268,269,272,273,272,276,279,280,283],{},[211,262,263],{},"Инструментирование",": метки ",[185,266,267],{},"performance.measure"," холодного старта - ",[185,270,271],{},"cache:prime",", ",[185,274,275],{},"ps:watch:\u003Ctable>",[185,277,278],{},"trns:first-transform",". См. ",[281,282,142],"a",{"href":143},".",[173,285,287],{"id":286},"структура-проекта","Структура проекта",[178,289,292],{"className":290,"code":291,"language":183},[181],"app\u002F                        # @finapp\u002Fapp - Nuxt, конфиг Supabase, конфиг PowerSync\n  app\u002F                      # Nuxt-корень (компоненты, composables, middleware, страницы, плагины)\n  services\u002Fpowersync\u002F       # Клиентский слой данных: схема, db-синглтон, коннектор, трансформации, мутации\n  supabase\u002F                 # Конфиг Supabase: миграции, RLS-политики, powersync_setup.sql\n  powersync\u002F                # Self-hosted PowerSync: docker-compose, config\u002F\ndocs\u002F                       # @finapp\u002Fdocs - сайт документации\ni18n\u002Flocales\u002F               # en-US.js, ru-RU.js\n",[185,293,291],{"__ignoreMap":187},[189,295,296],{},"Неочевидные соглашения:",[205,298,299,328,339],{},[208,300,301,304,305,308,309,214,312,315,316,319,320,323,324,327],{},[185,302,303],{},"services\u002Fpowersync\u002F"," - клиентский слой данных: SQLite-схема (",[185,306,307],{},"AppSchema.ts","), синглтон ",[185,310,311],{},"PowerSyncDatabase",[185,313,314],{},"db.ts","), коннектор загрузки (",[185,317,318],{},"connector.ts","), конвертеры строк\u002Fэлементов (",[185,321,322],{},"transforms.ts",") и хелперы записи (",[185,325,326],{},"mutations.ts",").",[208,329,330,331,334,335,338],{},"Каждая фича в ",[185,332,333],{},"app\u002Fapp\u002Fcomponents\u002F\u003Cfeature>\u002F"," содержит свой Pinia-стор, форму, список и типы - отдельной папки ",[185,336,337],{},"stores\u002F"," нет.",[208,340,341,344,345,283],{},[185,342,343],{},"supabase\u002Fmigrations\u002F"," содержит все изменения схемы Postgres. Никогда не редактировать вручную - использовать ",[185,346,347],{},"supabase migration new",[173,349,351],{"id":350},"паттерн-хранилища","Паттерн хранилища",[189,353,354,355,272,358,272,361,272,364,272,367,370],{},"Все Pinia-хранилища (",[185,356,357],{},"useTrnsStore",[185,359,360],{},"useWalletsStore",[185,362,363],{},"useCategoriesStore",[185,365,366],{},"useUserStore",[185,368,369],{},"useCurrenciesStore",") следуют одному паттерну:",[178,372,375],{"className":373,"code":374,"language":183},[181],"items: shallowRef\u003CRecord\u003Cid, item> | null>   # реактивное состояние (shallow для производительности)\nподписка watchTable       # срабатывает при инициализации + каждой записи или синхронизации\nsave({ id, values })      # оптимистичная запись → upsertRow → откат при ошибке\ndelete(id)                # оптимистичная запись → deleteRow → откат при ошибке\n",[185,376,374],{"__ignoreMap":187},[197,378,380],{"id":379},"поток-данных-запись","Поток данных (запись)",[178,382,385],{"className":383,"code":384,"language":183},[181],"действие пользователя\n  → save({ id, values })\n    → prev = снимок текущих items\n    → оптимистично: items.value = { ...items, [id]: values }\n    → await upsertRow(table, id, row)\n      → успех: watchTable срабатывает → хранилище обновляется из SQLite\n      → ошибка: items.value = prev + showErrorToast\n",[185,386,384],{"__ignoreMap":187},[189,388,389,390,393,394,397,398,283],{},"Откат при ошибке записи обрабатывает ошибки локального SQLite. Отказ на стороне сервера (RLS, ограничение) обрабатывается отдельно обработчиком ошибок загрузки, который выполняет авто-согласование: отклонённые INSERT откатываются локально (",[185,391,392],{},"sync.errors.uploadReverted","), при отклонённых UPDATE\u002FDELETE предлагается полная повторная синхронизация (",[185,395,396],{},"sync.errors.uploadDiverged","). См. ",[281,399,122],{"href":123},[197,401,403],{"id":402},"поток-данных-watchtable","Поток данных (watchTable)",[178,405,408],{"className":406,"code":407,"language":183},[181],"watchTable('SELECT * FROM trns', [], onRows, throttleMs)\n  → срабатывает немедленно с текущими локальными строками\n  → срабатывает снова при каждом изменении таблицы (локальная запись ИЛИ входящая синхронизация PowerSync)\n  → onRows: rows → трансформация через rowToTrn() → items.value = новый объект-словарь\n",[185,409,407],{"__ignoreMap":187},[189,411,412,414,415,418,419,422,423,426,427,430,431,434],{},[185,413,357],{}," использует ",[185,416,417],{},"reconcileTrns(prev, rows)"," внутри ",[185,420,421],{},"onRows",": возвращает тот же ",[185,424,425],{},"prev","-ref, если ничего не изменилось (подавляет эхо от собственной оптимистичной записи хранилища), и переиспользует неизменённые объекты строк по ",[185,428,429],{},"updatedAt",", так что ",[185,432,433],{},"rowToTrn()"," выполняется только для изменённых строк.",[197,436,438],{"id":437},"каскадное-удаление","Каскадное удаление",[189,440,441,444,445,448,449,451,452,455],{},[185,442,443],{},"deleteWallet"," и ",[185,446,447],{},"deleteCategory"," также удаляют транзакции сущности из локальной SQLite перед возвратом (иначе подписка ",[185,450,194],{}," на ",[185,453,454],{},"trns"," вернёт их в UI после удаления родителя). Откат при ошибке восстанавливает и саму сущность, и её транзакции.",[197,457,459],{"id":458},"демо-режим","Демо-режим",[189,461,462,463,466,467,470,471,283],{},"Демо-режим полностью обходит PowerSync. Хранилища проверяют ",[185,464,465],{},"isDemo"," и пропускают все вызовы PowerSync. Вместо этого используются данные в памяти + localforage. Демо-данные (1000 транзакций, 18 категорий, 6 кошельков) генерируются в ",[185,468,469],{},"useDemo.ts",". Управляется cookie ",[185,472,473],{},"finapp.isDemo",[173,475,21],{"id":476},"авторизация",[197,478,480],{"id":479},"supabase-auth","Supabase Auth",[189,482,483,486],{},[185,484,485],{},"app\u002Fapp\u002Fcomposables\u002FuseSupabase.ts"," предоставляет:",[205,488,489,504,537],{},[208,490,491,492,214,495,272,498,272,501,327],{},"Синглтон-клиент ",[185,493,494],{},"supabase-js",[185,496,497],{},"autoRefreshToken",[185,499,500],{},"persistSession",[185,502,503],{},"detectSessionInUrl: true",[208,505,506,507,510,511,272,514,272,517,272,520,523,524,527,528,527,531,527,534,283],{},"Composable ",[185,508,509],{},"useSupabaseAuth()"," с реактивными ",[185,512,513],{},"session",[185,515,516],{},"uid",[185,518,519],{},"user",[185,521,522],{},"isAuthReady",", а также ",[185,525,526],{},"signInWithPassword"," \u002F ",[185,529,530],{},"signUp",[185,532,533],{},"signInWithGoogle",[185,535,536],{},"signOut",[208,538,539,540,543,544,546],{},"Сессия сохраняется в localStorage и автоматически обновляется. ",[185,541,542],{},"onAuthStateChange"," поддерживает модульный реактивный ref ",[185,545,513],{}," в актуальном состоянии.",[189,548,549,550,444,553,214,556,559,560,562,563,566,567,570,571,574],{},"Методы авторизации: ",[211,551,552],{},"email\u002Fpassword",[211,554,555],{},"вход через Google",[185,557,558],{},"signInWithOAuth",", PKCE). ",[185,561,503],{}," позволяет supabase-js обменять ",[185,564,565],{},"?code="," при возврате после OAuth: ",[185,568,569],{},"login.vue"," возвращает Google на ",[185,572,573],{},"\u002Flogin",", а после появления сессии переходит к исходному пункту назначения. Сессии Google - это обычные JWT Supabase, поэтому PowerSync (проверка через JWKS) и остальная auth-обвязка не меняются.",[197,576,578],{"id":577},"защита-авторизации","Защита авторизации",[189,580,581],{},"Авторизация использует два слоя:",[583,584,585,619],"ol",{},[208,586,587,214,590,593,594,272,597,600,601,603,604,607,608,610,611,614,615,618],{},[211,588,589],{},"Route middleware",[185,591,592],{},"app\u002Fapp\u002Fmiddleware\u002Fauth.global.ts","): синхронная проверка сохранённой сессии Supabase в localStorage (",[185,595,596],{},"hasPersistedSession()",[185,598,599],{},"app\u002Fapp\u002Fcomposables\u002FuseAuthSession.ts","), без сетевых запросов, работает офлайн. Перенаправляет на ",[185,602,573],{}," (сохраняя ",[185,605,606],{},"?redirect=",") при отсутствии; перенаправляет ",[185,609,573],{}," → ",[185,612,613],{},"\u002Fdashboard"," при наличии сессии. Демо-режим обходит эту проверку. При возврате после Google OAuth сессия ещё не сохранена, поэтому ",[185,616,617],{},"\u002Flogin?...&code="," остаётся без редиректа, пока код обменивается на месте.",[208,620,621,214,624,627,628,630],{},[211,622,623],{},"Плагин PowerSync",[185,625,626],{},"app\u002Fapp\u002Fplugins\u002Fpowersync.client.ts","): следит за Supabase ",[185,629,516],{}," и подключает\u002Fотключает PowerSync по мере появления или сброса сессии. Проверка читает localStorage напрямую, поэтому auth-cookie не записывается.",[197,632,634],{"id":633},"защита-от-редиректов","Защита от редиректов",[189,636,637,214,640,643,644,646,647,650,651,654],{},[185,638,639],{},"getSafeRedirectPath()",[185,641,642],{},"app\u002Futils\u002Fredirect.ts",") валидирует параметры ",[185,645,606],{}," - разрешены только относительные пути, начинающиеся с ",[185,648,649],{},"\u002F"," (не ",[185,652,653],{},"\u002F\u002F","), что предотвращает атаки открытого перенаправления.",[197,656,658],{"id":657},"логика-защиты","Логика защиты",[189,660,661,664,665,668,669,672],{},[185,662,663],{},"useGuard()"," в ",[185,666,667],{},"app.vue"," наблюдает за ",[185,670,671],{},"currentUser",":",[205,674,675,687,694],{},[208,676,677,678,680,681,683,684,686],{},"Авторизован + на ",[185,679,573],{}," → редирект на ",[185,682,613],{}," (или путь из ",[185,685,606],{},")",[208,688,689,690,680,692],{},"Не авторизован + не на ",[185,691,573],{},[185,693,573],{},[208,695,696,697,700],{},"Флаг ",[185,698,699],{},"isSigningOut"," предотвращает цикл редиректов при выходе",[173,702,704],{"id":703},"защита-локальной-бд-от-чужого-пользователя","Защита локальной БД от чужого пользователя",[189,706,707,710,711,714,715,718],{},[185,708,709],{},"connectPowerSync(client, powerSyncUrl, userId)"," проверяет сохранённый маркер владельца (",[185,712,713],{},"finapp.psDbOwnerUid"," в localStorage). Если локальная SQLite принадлежит другому пользователю, база данных ",[211,716,717],{},"стирается перед подключением"," (fail-closed). Это предотвращает утечку данных при входе другой учётной записи на том же устройстве.",[173,720,722],{"id":721},"поток-выхода","Поток выхода",[178,724,727],{"className":725,"code":726,"language":183},[181],"signOut()\n  → isSigningOut = true (предотвращение цикла редиректов)\n  → supabase.auth.signOut()\n  → disconnectPowerSync() - disconnectAndClear() стирает локальную SQLite + очищает маркер владельца\n  → window.location.href = '\u002Flogin' (жёсткая навигация, уничтожение всего JS-состояния)\n",[185,728,726],{"__ignoreMap":187},[189,730,731,732,283],{},"Жёсткая навигация вместо Vue Router - см. ",[281,733,734],{"href":128},"Технические решения",[173,736,738],{"id":737},"курсы-валют","Курсы валют",[205,740,741,758,768,780],{},[208,742,743,746,747,750,751,214,754,757],{},[211,744,745],{},"Источник:"," Coinbase (база, фиат + крипта) + OpenExchangeRates (фиат сверху), сливаются в одну дневную строку ",[185,748,749],{},"source='merged'"," edge-функцией ",[185,752,753],{},"fetch-rates",[185,755,756],{},"pg_cron",", 06:00 UTC)",[208,759,760,763,764,767],{},[211,761,762],{},"Хранение:"," таблица ",[185,765,766],{},"rates"," в Postgres; глобальный поток PowerSync синхронизирует все строки каждому авторизованному пользователю",[208,769,770,773,774,776,777],{},[211,771,772],{},"Фронтенд:"," ",[185,775,369],{}," подписывается через ",[185,778,779],{},"watchTable('SELECT * FROM rates', ...)",[208,781,782,773,785,788],{},[211,783,784],{},"Использование:",[185,786,787],{},"getAmountInRate()"," конвертирует суммы в базовую валюту для статистики",[173,790,792],{"id":791},"pwa","PWA",[205,794,795,806,817,823,826,831],{},[208,796,797,798,801,802,805],{},"Стратегия: ",[185,799,800],{},"generateSW"," из ",[185,803,804],{},"@vite-pwa\u002Fnuxt"," (без кастомного service worker)",[208,807,808,809,812,813,816],{},"Прекеширование: ассеты сборки плюс ровно один WASM-файл - ",[185,810,811],{},"wa-sqlite-async.\u003Chash>.wasm",", единственный вариант, который загружает воркер (остальные эмитируемые варианты - cipher\u002Fsync-сборки, которые приложение не использует); ",[185,814,815],{},"maximumFileSizeToCacheInBytes: 3 МБ",", чтобы он не отбрасывался дефолтным лимитом в 2 МБ",[208,818,819,822],{},[185,820,821],{},"navigateFallback: '\u002F'"," отдаёт SPA-оболочку для всех навигаций (офлайн-поддержка)",[208,824,825],{},"Runtime-кеширование: Google Fonts, иконки Iconify (CacheFirst)",[208,827,828,829],{},"Start URL: ",[185,830,613],{},[208,832,833,834],{},"Манифест: ",[185,835,836],{},"display: 'standalone'",[173,838,840],{"id":839},"логирование","Логирование",[189,842,843,214,846,849,850,853,854,272,857,272,860,863,864,283],{},[185,844,845],{},"createLogger(prefix)",[185,847,848],{},"app\u002Futils\u002Flogger.ts",") - логирование только для разработки. В продакшене работает только ",[185,851,852],{},".error()",". Все операции хранилищ и авторизации инструментированы с префиксами (",[185,855,856],{},"[wallets]",[185,858,859],{},"[trns]",[185,861,862],{},"[auth\u002Fmiddleware]"," и т.д.). Проверяйте консоль браузера при ",[185,865,866],{},"pnpm dev",[173,868,870],{"id":869},"управление-модальными-окнами","Управление модальными окнами",[189,872,873,874,877,878,881,882,885,886,664,888,891,892,895],{},"Модальные окна используют локальные ",[185,875,876],{},"ref"," и паттерн ",[185,879,880],{},"v-if"," + ",[185,883,884],{},"@close"," emit. Видимость меню - модульный ",[185,887,876],{},[185,889,890],{},"useMenuData.ts"," (общий для всех вызовов). Глобального реестра модалок нет - они размонтируются автоматически при размонтировании родительских компонентов (например, при выходе хранилища сбрасываются в ",[185,893,894],{},"null"," → условие layout становится false → модалки исчезают).",[173,897,899],{"id":898},"персистентность-сайдбара","Персистентность сайдбара",[189,901,902,903,906],{},"Состояние показа\u002Fскрытия сайдбара на десктопе хранится в cookie (",[185,904,905],{},"finapp.isShowSidebar","), обеспечивая сохранение состояния между загрузками страниц.",[173,908,910],{"id":909},"bottom-sheet","Bottom Sheet",[189,912,913,914,917,918,921],{},"Мобильная форма транзакции использует кастомный компонент ",[185,915,916],{},"BottomSheet"," с ",[185,919,920],{},"useBottomSheetDrag"," - ручная реализация drag-поддержки touch и mouse. Функции: настраиваемый порог начала закрытия, определение направления, затухание оверлея, учёт скролла (игнорирует drag при прокрутке контента), исключение sort-хэндлов.",[173,923,925],{"id":924},"краткая-справка-по-ключевым-файлам","Краткая справка по ключевым файлам",[927,928,929,942],"table",{},[930,931,932],"thead",{},[933,934,935,939],"tr",{},[936,937,938],"th",{},"Что",[936,940,941],{},"Где",[943,944,945,956,968,976,985,994,1004,1013,1022,1032,1042,1052,1062,1072,1082,1092,1102,1112,1121,1131,1139,1149,1159,1169],"tbody",{},[933,946,947,951],{},[948,949,950],"td",{},"Точка входа",[948,952,953],{},[185,954,955],{},"app\u002Fapp\u002Fapp.vue",[933,957,958,961],{},[948,959,960],{},"Логика темы",[948,962,963,272,966],{},[185,964,965],{},"app\u002Fapp\u002Fplugins\u002Ftheme.ts",[185,967,955],{},[933,969,970,972],{},[948,971,623],{},[948,973,974],{},[185,975,626],{},[933,977,978,981],{},[948,979,980],{},"Supabase-клиент + авторизация",[948,982,983],{},[185,984,485],{},[933,986,987,990],{},[948,988,989],{},"Синхронный гейт авторизации",[948,991,992],{},[185,993,599],{},[933,995,996,999],{},[948,997,998],{},"Состояние загрузки \u002F поток инициализации",[948,1000,1001],{},[185,1002,1003],{},"app\u002Fapp\u002Fcomponents\u002Fapp\u002FuseInitApp.ts",[933,1005,1006,1009],{},[948,1007,1008],{},"Blob-кеш холодного старта",[948,1010,1011],{},[185,1012,217],{},[933,1014,1015,1018],{},[948,1016,1017],{},"Защита авторизации (middleware)",[948,1019,1020],{},[185,1021,592],{},[933,1023,1024,1027],{},[948,1025,1026],{},"Логика защиты (app.vue)",[948,1028,1029],{},[185,1030,1031],{},"app\u002Fapp\u002Fcomponents\u002Fuser\u002FuseGuard.ts",[933,1033,1034,1037],{},[948,1035,1036],{},"Утилиты синхронизации хранилищ",[948,1038,1039],{},[185,1040,1041],{},"app\u002Fapp\u002Fcomposables\u002FuseStoreSync.ts",[933,1043,1044,1047],{},[948,1045,1046],{},"Синглтон PowerSync db",[948,1048,1049],{},[185,1050,1051],{},"app\u002Fservices\u002Fpowersync\u002Fdb.ts",[933,1053,1054,1057],{},[948,1055,1056],{},"SQLite-схема",[948,1058,1059],{},[185,1060,1061],{},"app\u002Fservices\u002Fpowersync\u002FAppSchema.ts",[933,1063,1064,1067],{},[948,1065,1066],{},"Коннектор загрузки",[948,1068,1069],{},[185,1070,1071],{},"app\u002Fservices\u002Fpowersync\u002Fconnector.ts",[933,1073,1074,1077],{},[948,1075,1076],{},"Трансформации строк\u002Fэлементов",[948,1078,1079],{},[185,1080,1081],{},"app\u002Fservices\u002Fpowersync\u002Ftransforms.ts",[933,1083,1084,1087],{},[948,1085,1086],{},"Хелперы записи",[948,1088,1089],{},[185,1090,1091],{},"app\u002Fservices\u002Fpowersync\u002Fmutations.ts",[933,1093,1094,1097],{},[948,1095,1096],{},"Подавление эха",[948,1098,1099],{},[185,1100,1101],{},"app\u002Fapp\u002Fcomponents\u002Ftrns\u002Freconcile.ts",[933,1103,1104,1107],{},[948,1105,1106],{},"Расчёт статистики",[948,1108,1109],{},[185,1110,1111],{},"app\u002Fapp\u002Fcomponents\u002Famount\u002FgetTotal.ts",[933,1113,1114,1117],{},[948,1115,1116],{},"Защита редиректов",[948,1118,1119],{},[185,1120,642],{},[933,1122,1123,1126],{},[948,1124,1125],{},"Утилиты дат",[948,1127,1128],{},[185,1129,1130],{},"app\u002Fapp\u002Fcomponents\u002Fdate\u002Futils.ts",[933,1132,1133,1135],{},[948,1134,840],{},[948,1136,1137],{},[185,1138,848],{},[933,1140,1141,1144],{},[948,1142,1143],{},"Состояние меню",[948,1145,1146],{},[185,1147,1148],{},"app\u002Fapp\u002Fcomponents\u002Flayout\u002FuseMenuData.ts",[933,1150,1151,1154],{},[948,1152,1153],{},"Bottom sheet drag",[948,1155,1156],{},[185,1157,1158],{},"app\u002Fapp\u002Fcomponents\u002FbottomSheet\u002FuseBottomSheetDrag.ts",[933,1160,1161,1164],{},[948,1162,1163],{},"Миграции Supabase",[948,1165,1166],{},[185,1167,1168],{},"app\u002Fsupabase\u002Fmigrations\u002F",[933,1170,1171,1174],{},[948,1172,1173],{},"Правила синхронизации PowerSync",[948,1175,1176],{},[185,1177,1178],{},"app\u002Fpowersync\u002Fconfig\u002Fsync-config.yaml",[173,1180,1182],{"id":1181},"следующие-шаги","Следующие шаги",[205,1184,1185,1190,1195],{},[208,1186,1187,1189],{},[281,1188,117],{"href":118}," - как PowerSync синхронизирует данные между сервером и локальной SQLite",[208,1191,1192,1194],{},[281,1193,122],{"href":123}," - офлайн-записи, очередь загрузки и обработка ошибок",[208,1196,1197,1199],{},[281,1198,142],{"href":143}," - бюджет холодного старта, ленивые чанки и инструментирование",{"title":187,"searchDepth":1201,"depth":1201,"links":1202},2,[1203,1207,1208,1214,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229],{"id":175,"depth":1201,"text":176,"children":1204},[1205],{"id":199,"depth":1206,"text":200},3,{"id":286,"depth":1201,"text":287},{"id":350,"depth":1201,"text":351,"children":1209},[1210,1211,1212,1213],{"id":379,"depth":1206,"text":380},{"id":402,"depth":1206,"text":403},{"id":437,"depth":1206,"text":438},{"id":458,"depth":1206,"text":459},{"id":476,"depth":1201,"text":21,"children":1215},[1216,1217,1218,1219],{"id":479,"depth":1206,"text":480},{"id":577,"depth":1206,"text":578},{"id":633,"depth":1206,"text":634},{"id":657,"depth":1206,"text":658},{"id":703,"depth":1201,"text":704},{"id":721,"depth":1201,"text":722},{"id":737,"depth":1201,"text":738},{"id":791,"depth":1201,"text":792},{"id":839,"depth":1201,"text":840},{"id":869,"depth":1201,"text":870},{"id":898,"depth":1201,"text":899},{"id":909,"depth":1201,"text":910},{"id":924,"depth":1201,"text":925},{"id":1181,"depth":1201,"text":1182},"Инициализация, структура проекта, паттерн хранилищ, авторизация - Supabase + PowerSync.","md",null,{},{"icon":111},{"title":108,"description":1236},{"Архитектура Финапки":1237},"инициализация приложения, паттерн хранилищ, Supabase Auth, офлайн-first через PowerSync и настройка PWA.","tzDBSoLWfbsJKc2I-6gkKILVzsMUN70NP6XetaoRqZ4",[1240,1242],{"title":97,"path":98,"stem":99,"description":1241,"icon":100,"children":-1},"Вычисление диапазонов, форматирование периодов и распределение транзакций по интервалам.",{"title":113,"path":114,"stem":115,"description":1243,"icon":44,"children":-1},"Типы транзакций, логика корректировок, статистика.",1782114340125]