Open Finapp
Справочник

Что изменилось со времён Firebase

Что изменилось между v6 (Firebase) и текущим приложением на Supabase + PowerSync - бэкенд, производительность, безопасность, тесты, архитектура.

Две эпохи. Бэкенд Финапки прошёл путь от Firebase (v6 - Firebase Realtime Database + Firebase Auth) к текущему стеку: Supabase Postgres + PowerSync (offline-first) + Supabase Auth.

Этот документ - сравнение того, что изменилось. Улучшения на уровне приложения (производительность, рефакторинг, UX, TypeScript, i18n), а также разделы бэкенда, авторизации, синхронизации и зависимостей описывают текущую кодовую базу. Полное описание текущей архитектуры Supabase + PowerSync - в разделе Архитектура.

Сравнение состояния приложения на firebase-ветке (v6, Firebase) и в текущем приложении на Supabase + PowerSync.

Бэкенд

firebase (Firebase)текущий
База данныхFirebase Realtime DatabaseSupabase Postgres (реплицируется PowerSync)
АвторизацияFirebase Auth через nuxt-vuefireSupabase Auth (email/password)
Доступ к даннымReal-time listeners (getDataAndWatch)Локальный SQLite через db.watch(...), запись через upsertRow / deleteRow
ВалидацияFirebase security rules (JSON)Postgres RLS (auth.uid()) + клиентские Zod-схемы
Серверный кодНет (клиентский SDK)Миграции Supabase + sync-правила self-hosted PowerSync (по пользователю WHERE "userId" = auth.user_id())
Курсы валютКлиентский запросТаблица rates, синхронизируется через PowerSync как любая другая
Настройки пользователяТолько localStorageТаблица user_settings (baseCurrency, locale), создаётся при регистрации триггером
Индексы схемыN/AИндексы клиентского SQLite в AppSchema.ts (userDate: ['userId','date'] + userId на каждой таблице)
МиграцияN/AЭкспорт JSON из Firebase -> трансформация -> импорт через PostgREST скриптом scripts/import-firebase.mjs (см. Историю миграций)
Удаление аккаунтаN/ARLS-ограниченное удаление строк пользователя + локальная очистка (removeAllUserData)

Что даёт текущий бэкенд

  • Offline-first - все чтения/записи идут в локальный SQLite; PowerSync синхронизирует в фоне, поэтому приложение полностью работает офлайн
  • Изоляция по пользователю - Postgres RLS (auth.uid()) + sync-правила PowerSync гарантируют, что каждый пользователь синхронизирует только свои строки
  • Типизированная клиентская схема - AppSchema.ts описывает таблицы SQLite; трансформации row<->item собраны в transforms.ts
  • Автоматическая очередь загрузки - локальные мутации ставятся в очередь и выгружаются коннектором (uploadData), с авто-реконциляцией отклонённых операций
  • Инкрементальная синхронизация - PowerSync стримит только изменённые строки после начальной синхронизации

Паттерн сторов

firebaseтекущий
Источник данныхFirebase listener -> setTrns()db.watch('SELECT * FROM ...') -> reconcile -> setX()
PersistdeepUnref() + немедленный localforage.setItem()Источник правды - SQLite PowerSync; блоб read-through кеш (useStoreCache.ts) рисует первый кадр
Офлайн-операцииРучные хелперы в trns/helpers.ts, произвольные ключи localStorageНе нужны - встроенная очередь загрузки PowerSync обрабатывает офлайн-записи
Система IDFirebase push IDКлиентские UUID, стабильны при синхронизации (без ремапа)
Оптимистичный UIЧастичный (fire-and-forget)Полный: upsertRow/deleteRow пишут в локальный SQLite -> watch переэмитит -> коннектор выгружает в фоне
Верификация синхронизацииНетЧек-суммы PowerSync по бакетам; отклонённые загрузки запускают авто-реконциляцию (uploadReconcile.ts)

Зависимость vue-deepunref удалена. Данные сторов используют shallowRef, поэтому глубокое клонирование при каждом persist было лишним.

Производительность

Алгоритмические улучшения

ЧтоfirebaseтекущийЭффект
Итоги кошельковgetWalletTotal(id) вызывается для каждого кошелька в reduce - каждый вызов фильтрует все транзакции: O(W×N)getWalletsTotals() один проход, распределение через Map: O(N), затем O(1) для каждого кошелька5 кошельков × 10K трн: ~50K → ~10K операций
Разбивка по интерваламИнлайн в Item.vue: intervalsInRange.reduce() фильтрует все транзакции на каждый интервал: O(N×I)Извлечено в bucketTrnsByIntervals(): один проход + бинарный поиск: O(N log I)Год/365 дней/5K трн: 1.8M → ~43K сравнений
Фильтрация транзакций6 последовательных .filter() через reduce + walletsIds.some(w => includes(w)) O(W²) + сортировка всегда O(N log N)Один .filter() с ранним выходом + Set.has() O(1) + сортировка опциональна5 промежуточных массивов + O(W²) → один проход + O(1); сортировка O(N log N) пропускается когда не нужна
Поиск кошелька в getTotalwalletsIds.includes(): O(W) на каждый переводnew Set(walletsIds).has(): O(1) на каждый переводЛинейный → константный на каждый вызов
Генерация демо-данных{ ...acc } spread в .reduce() + getTransactibleIds() вызывается дважды на итерациюПрямое acc[i] = ... + кеширование перед цикломO(N²) → O(N)
Статистика категорийcategoriesStat жадно вычисляет и grouped, и ungrouped на каждое изменение; filter().at(0) для наибольшейЛенивый computed() на каждый вариант, выполняется только при обращении; .find() останавливается на первом совпаденииДо 2× меньше вызовов computeCategoriesWithData
Фильтр вертикальных категорийverticalCategories.filter(c => c.value !== 0) вызывается дважды в шаблоне (заголовок + v-for): пересчитывается на каждый рендерКеширован в visibleVerticalCategories computed: фильтрация один раз, переиспользование2 прохода фильтра → 1 кешированный
Форматирование чиселБиблиотека currency.js + currencies.find() O(N) дважды на вызовНативный Intl.NumberFormat с кешем в Map + currencyMap.get() O(1)Нет зависимости; форматтер создаётся один раз на precision
Последняя транзакцияObject.keys().sort().find() - сортирует все транзакции O(N log N) затем ищетОдин проход O(N), отслеживая максимальную дату10K трн: ~130K ops → ~10K ops
Проверка родителя категорииcategoriesForBeParent вызывает Object.values(trns).some() O(N) на каждую корневую категорию: O(M×N)Предготовленный usedCategoryIds Set через for...in O(N) + Set.has() O(1) на проверку; без промежуточных массивов200 категорий × 10K трн: ~2M → ~10K ops
Недавние категорииObject.keys(trns).filter().sort() O(N log N) + acc.some(c => c.id) O(K²) дедупликация + includes() O(W) на категориюОдин проход O(N) через Map с последней датой + entries().toSorted() O(K log K) + Set.has() O(1)10K трн: ~130K + K² → ~10K + K log K ops
Транзактируемые категории.reduce() с acc.includes() O(M) + childIds.filter(!acc.includes) O(C×M): O(M²)Map<parentId, childIds[]> за O(M) + Set дедупликация O(1) на проверку200 категорий: ~40K → ~200 ops
Определение переводовtransferCategoriesIds.includes(categoryId) O(W) линейный поиск на каждую транзакциюtrn.type === TrnType.Transfer O(1) сравнение enumЛинейный → константный на проверку
Подсчёт фильтров типов3 отдельных .filter() прохода по trnsIds (расход, доход, перевод): O(3N)Один цикл с объектом счётчиков: O(N)3 полных прохода → 1 проход
Кеш элементов в спискеcomputeTrnItem(id) вызывается дважды на элемент в шаблоне (элемент + дата): O(2P) вызововПредвычисленная trnItemsMap через Map: O(P) вычислений + O(1) обращения30 элементов: ~60 → ~30 вычислений на рендер
Кеш итогов группgetTotalOfTrnsIds() вызывается дважды на группу в шаблоне (доход + расход): O(2G) вызововПредвычисленная groupTotals Map + paginatedTotal computed: O(G) вычислений + O(1) обращения15 групп: ~30 → ~15 вычислений итогов на рендер
Сортировка при группировкеcategories.sort() вызывается внутри reduce на каждый .push() дочерней категории: O(K² log K) на родителяСортировка один раз после цикла, только если length > 1: O(K log K) на родителя5 детей: ~50 → ~12 сравнений
Итоги при группировкеgetTotalOfTrnsIds(cat.trnsIds).sum вызывается дважды на дочернюю категорию (для значения + суммы родителя)Вычисляется один раз в catTotal, переиспользуется: один вызов на дочернюю2× меньше вызовов getTotal на дочернюю категорию
Синхронизация данныхFirebase: полная замена данных на каждое обновление: O(N)Дельта-синхронизация: запрашиваются только изменённые записи - O(D) где D « NНе обрабатывает неизменённые транзакции
Генерация интерваловlist.unshift(current) в цикле while: O(n) на вызов, O(n²) всегоlist.push(current) + list.reverse(): O(1) на вызов, O(n) всегоГод/365 дней: ~66K сдвигов → ~365 push + 1 reverse
Ключи DurationДинамический { [\${period}s`]: value }- TS видитRecord<string, number>, нет проверки типа Duration`toDuration(period, value) возвращает типизированный Duration через exhaustive switchБезопасность на этапе компиляции; новые варианты периодов вызывают ошибки, а не молчаливый fallback
Форматирование датКаждая format-функция вызывается дважды (type: 'start' + type: 'end'), результаты конкатенируются; 3 IIFE switch-блока для сравнения периодовКаждая format-функция возвращает полную строку; isSamePeriod(a, b, period) заменяет все 3 switch-блока~40% меньше строк в useGetDateRange; без двойных вызовов
Вычисление среднихsum !== 0 проверяется 3 раза + Object.keys(items).length > 0 гардРанний выход при sum === 0; среднее за день всегда присутствует (диапазон ≥ 2 гарантирован)1 проверка вместо 4
Сортировка избранных категорийИнлайн-сортировка дублирует логику parent-name + name (8 строк)Переиспользует утилиту compareCategoriesByParentAndName (1 строка)DRY; одна реализация сортировки
Цикл недавних категорий.reduce() проходит все записи даже после достижения лимита maxCategoriesfor цикл с break останавливается на лимите200 категорий, лимит 16: ~200 → ~16 итераций
Итерация недавних категорийfor...of Object.keys(trnsItems) создает промежуточный массивfor...in trnsItems итерирует напрямую по объектуБез аллокации промежуточного массива для 10K+ транзакций
Переиспользование ID категорийObject.keys(items.value) вызывается отдельно в categoriesRootIds, favoriteCategoriesIdsОба переиспользуют computed categoriesIdsКлючи вычисляются один раз, используются всеми зависимыми
Переиспользование transactible IDscategoriesIdsForTrnValues повторно фильтрует все категории с проверкой !hasChildrenДелегирует уже вычисленному transactibleIds + .filter(id !== 'transfer')Без повторного O(M) сканирования родителей
Дедупликация children IDsgetChildrenIdsOrParent дублирует логику .filter() из getChildrenIdsДелегирует getChildrenIds(), проверяет .lengthОдна реализация фильтра

Бандл и рендеринг

Чтоfirebaseтекущий
ECharts9 компонентов в основном бандле (вкл. PieChart, DataZoom, MarkPoint)7 компонентов в lazy-загружаемом async-чанке (неиспользуемые удалены)
Плагин темы81 строка с SSR инлайн-скриптами и regex-заменами32 строки, SPA-only обновление appConfig из localStorage
PersistdeepUnref() на каждый localforage.setItem() - полное рекурсивное клонированиеПрямая запись, debounce 300ms, без клонирования
Адаптивная вёрсткаJS-слушатель useWindowSize() для sidebarCSS sm: / md: Tailwind брейкпоинты
Container queriesТолько media queriesCSS container queries (@xl/page:, @md/page:) для адаптивных размеров относительно контейнера
Зависимостиcurrency.js для форматирования чиселНативный Intl.NumberFormat (без зависимости)
Шрифты4 семейства (Roboto, Roboto Condensed, Nunito, Unica One), множество весов, только кириллица - Google Fonts <link> в headТе же 4 семейства через @nuxt/fonts, кириллица + латиница
Внешний видimport colors from 'tailwindcss/colors' + каст as anyusePreferredDark() + захардкоженный neutral, поддержка режима system

Безопасность

Чтоfirebaseтекущий
Парсер калькулятораnew Function(expression) - риск инъекции кода, требует unsafe-eval CSPРекурсивный парсер - только +,-,*,/ над числами
Валидация вводаFirebase rules (клиент не может проверить)Клиентские Zod-схемы + типы колонок Postgres / RLS
Токены авторизацииУправляются FirebaseJWT + refresh-токен под управлением Supabase (валидируются PowerSync через JWKS)
Время жизни сессииПо умолчанию FirebaseСессия Supabase Auth с автообновлением токена
Каскадные удаленияN/A (Firebase)RLS-ограниченное удаление строк пользователя (без FK-ограничений; PowerSync чистит локальные данные)
Целостность данныхНе проверяется в кодеTransfer - categoryId='transfer', нормализация при смене типа, уникальность имён
Защита редиректовБез валидацииgetSafeRedirectPath() - только относительные пути, начинающиеся с / (не //)

Авторизация

firebaseтекущий
ПровайдерFirebase AuthSupabase Auth (email/password)
Плагинnuxt-vuefire (авто-управление авторизацией)plugins/powersync.client.ts - подключает PowerSync при появлении сессии
Поток токеновВнутри Firebase SDKSupabaseConnector.fetchCredentials() отдаёт JWT Supabase в PowerSync; валидация через JWKS
ОфлайнFirebase SDK управляет реконнектомСохранённая сессия Supabase в localStorage; route guard читает её синхронно (useAuthSession)
Подсказка состоянияНетСохранённая сессия читается синхронно на холодном старте (getPersistedSession()), без сети
CallbackFirebase redirectВход по email/password, без OAuth-попапов
CORSN/AURL сервисов Supabase + PowerSync из runtime config
ЛогинFirebase авто-редиректСохраняет целевой редирект, вход через useSupabaseAuth()

Синхронизация и офлайн

firebaseтекущий
МеханизмХелперы в trns/helpers.tsВстроенная очередь загрузки PowerSync - без рукописной офлайн-очереди
ЗаписиРазрозненные ключи localStorageupsertRow / deleteRow пишут в локальный SQLite (INSERT/UPDATE, никогда ON CONFLICT)
ВыгрузкаsaveTrnToAddLaterLocal(), removeTrnToDeleteLaterLocal()SupabaseConnector.uploadData() выгружает очередь в Supabase
ЧтенияРучной при следующей загрузкеdb.watch(...) - одна подписка покрывает начальную загрузку + realtime, локальные + синхронизированные
Разрешение конфликтовНетАвто-реконциляция: отклонённые загрузки вычисляют divergedOps (uploadReconcile.ts) и запускают forceResync
ВалидацияНетКлиентские Zod-схемы на вводе формы; Postgres RLS на сервере
Первый кадрНетБлоб read-through кеш (useStoreCache.ts) сажает сторы до инициализации PowerSync

PWA

firebaseтекущий
СтратегияinjectManifest (кастомный SW через env var SW)generateSW (автогенерация Workbox)
Service workerРукописный sw.tsНет - Workbox генерирует автоматически
ПрекешированиеРучная конфигурацияАвтоматическое для всех ассетов сборки
ИконкиЗагрузка с Iconify API в рантаймеВключены в бандл на этапе сборки через clientBundle

TypeScript

firebaseтекущий
Strict modeВключён, но есть неразрешённые ошибкиНоль ошибок TypeScript
Касты типовas any по кодовой базеНоль as any - type guards, @vue-ignore, узкие касты
Определение переводовtransferCategoriesIds (computed из данных категорий)TrnType.Transfer enum + discriminated union типы
Валидация на рантаймеНетZod-схемы для настроек, курсов, конфига статистики, параметров дат
Функции датgetStartOf(date, string), getEndOf(date, string) - default fallback на dayТипизированы как Period, exhaustive switch с case 'day' - новые варианты вызывают ошибку компиляции
Конструкция DurationДинамический { [\${period}s`]: value }- TS видитRecord<string, number>`toDuration(period, value): Duration - типизированный возврат через exhaustive switch
Маппинг типов статистикиRecord<string, TrnType[]> с ?? fallbackRecord<SeriesSlugSelected | StatTabSlug, TrnType[]> - все ключи покрыты, fallback не нужен
Ключ среднихkey as keyof TotalReturns небезопасный каст + as numberТипизированный key: keyof TotalReturns через явный маппинг 'netIncome' -> 'sum', без кастов
Fallback курсовrates[code] || 1, wallet?.currency || 'USD' - falsy 0/'' триггерит fallbackrates[code] ?? 1, wallet?.currency ?? 'USD' - только null/undefined триггерит fallback
Парсинг query-параметровif (data.rangeOffset) - falsy 0 пропускаетсяif (data.x !== undefined) - нулевые значения применяются корректно
Пропсы totalbaseCurrencyCode?: string нестрогий типbaseCurrencyCode?: CurrencyCode - согласованно с getAmountInRate
Параметры стат. категорийtrnsItems: Record<TrnId, { categoryId?: string }> инлайн-типtrnsItems: Record<TrnId, Pick<TrnItem, 'categoryId'>> - привязан к исходному типу
Null guard в сортировкеsortCategoriesByAmount проверяет if (!a || !b) в рантаймеГард удален - TS уже гарантирует параметры CategoryWithData; явный возвращаемый тип : number

Тесты

firebaseтекущий
КонфигНет vitest config2 проекта vitest: unit (node), store (happy-dom)
Тесты синхронизацииНетТрансформации row<->item, upload-реконциляция / планирование дивергенции (uploadReconcile.test.ts)
Тесты калькулятораБазовые46 кейсов (приоритет операторов, десятичные, деление на ноль, инъекции)
Тесты извлечённой логикиНетФильтрация/группировка/подсчёты кошельков (исключение архивных, видимость available), группировка категорий, auth-гейт, stat barUtils, useStatItem
Инфраструктура тестовНетsetup-store.ts (общие моки PowerSync/Supabase)

Рефакторинг

Извлечение чистых функций

Бизнес-логика перенесена из Vue-компонентов в тестируемые чистые функции:

  • Статистика: bucketTrnsByIntervals, computeAverageTotal, sortCategoriesByAmount, getSelectedType, computeBarStyle, formatCompactAmount, computeSeriesAverage, getTrnTypeByAmount
  • Кошельки: filterWalletsByCurrency, filterWalletsByViewType, groupWalletsByProperty, computeWalletCounts, sumWalletAmounts, getCreditAvailable
  • Категории: collectCategoriesByTrns, flattenCategoriesWithValues, groupCategoriesWithValues
  • Синхронизация: reconcile (сведение сторов со свежими эмиссиями watch), planDivergence (upload-реконциляция), transforms (row<->item)
  • Демо: упрощён до read-only - удалены CRUD-методы (addDemoCategory, deleteDemoWallet и др.), только generateDemoData() + isDemo

Изменения компонентов

Добавлены: ActionButton, ChipButton, TabsScroll, TextMuted, SettingsCard, TitleSection, ItemBody, BottomSheetModal, WalletsPageListItem, WalletsPageGroupHeader, Onboarding, CurrenciesPageList, CurrenciesItem, ConfigSwitch, GroupingToggle, SelectorItem

Удалены: Item14, Tabs2, TabsItem1/3/4, TextSm1/2, Title3/6/7/8, TitleOption, ToggleAction, SwitcherTabs, Welcome, Round (график статистики), SectionWithCollapse, CurrenciesToggleDep, getStyles.ts

Паттерны компонентов

  • Мерж классов: утилита getStyles() удалена - заменена на cn() (clsx + tailwind-merge)
  • Инпуты форм: пропы value/updateValue -> v-model / defineModel

Декомпозиция composables

  • Кошельки: useWalletContextMenu, useWalletDelete, useWalletsFilter, useWalletsGrouping, useWalletsCounts
  • Категории: useCategoryContextMenu
  • Статистика: Symbol-based injection keys (filterKey, statDateKey, statConfigKey) для типобезопасного provide/inject
  • Страницы статистики: composable useStatPage - дублированная инфраструктура статистики (создание и provide filter, statConfig, statDate, а также activeTab, storageKey, trnsIds, maxRange) извлечена из трёх компонентов-страниц (Dashboard, Category, Wallet) в один переиспользуемый composable. Каждая страница передаёт только свою entity-specific логику фильтрации через коллбэк getTrnsFilter. Скрипт-секция сокращена на 24–53% на трёх страницах
  • Composable useCategoryLongPress: извлечён дублированный код long-press (59 строк x 3 файла) из Round2lines.vue, Line.vue и Vertical.vue в общий composable. Обрабатывает долгое нажатие для создания транзакции из категории с корректным выбором даты из интервалов графика
  • Инкапсуляция useStatDate: навигационные computed-свойства (shouldShowNav, isAtEnd, isAtStart, isDayToday, shouldShowNavHome) и прямые мутации params.value перенесены из Navigation.vue и chart/Wrap.vue в методы useStatDate (selectInterval(), resetInterval(), setIntervalsBy(), navigate()). Скрипт Navigation.vue сокращён с ~54 строк до 5
  • Удалён дубликат stat/date/Nav.vue: удалена идентичная копия date/Nav.vue - остался только канонический компонент DateNav
  • Компонент UiNumberStepper: извлечён дублированный паттерн ±/input из ConfigModal.vue в переиспользуемый UI-примитив с v-model, min, max пропами
  • Автономность stat-компонентов: StatAverage - три условных блока Amount консолидированы в один через computed; StatSumWrap и StatChartWrap - убран избыточный проброс пропов в пользу inject; StatItem - два модальных блока объединены в один через state-machine паттерн
  • Кэширование конфигов: Section2.vue и Line.vue кэшируют чтения statConfig.config.value.catsList.* в computed-свойства (isItemsBg, isLines, isRoundIcon, isListGrouped и др.) для читаемости шаблонов
  • Упрощение Range.vue: ручное индексирование intervalsInRange заменено на statDate.selectedInterval; извлечён computed isIntervalSelected
  • Очистка useStatItem: извлечён computed baseTrnsIdsForSelection для устранения дублирования логики выбора интервала; исправлен небезопасный каст type.value as keyof TotalReturns на type.value ?? 'sum'; исправлен знаменатель среднего в графике - использовался неверный источник данных (intervalsData -> intervalsDataWithFilteredCategories)
  • Composable useCategoriesExpanded: извлечена логика expand/collapse (52 строки) из DetailedSection.vue (286->234 строк)
  • Переименование stat-компонентов: Section/Section2 -> RoundSection/DetailedSection для ясности
  • Извлечение barUtils: computeBarStyle() и formatCompactAmount() извлечены из инлайн-шаблонов в тестируемые утилиты
  • Консистентность статистики кошельков: архивные кошельки полностью исключены из всех подсчётов (итого, суммы по типам, снятие, доступно) через ранний continue; available.isShow требует наличия и withdrawal, и credit кошельков; авто-сброс фильтра на «Итого» при переключении валюты, если текущий фильтр пуст
  • Консолидация store sync: showErrorToast/showSuccessToast/showWarningToast объединены в showToast(type, key, params?)
  • Константы кошельков: WALLET_STORAGE_KEYS извлечены из инлайн-строк в constants.ts

Семантическое переименование

firebaseтекущий
getTrnsIdsfilterTrnsIds
getCategoriesWithDatacomputeCategoriesWithData
getTotalOfTrnsIdscomputeTotalForTrnsIds
getChildsIdsgetChildrenIds
trnFormCreate / Edit / DuplicateopenFormForCreate / Edit / Duplicate
getIsShowSumshouldShowSum
isItTransactibleisTransactible
editedAtupdatedAt
TransferTypeTransferSide
Selector2 (categories)SelectorGrid
Selector (categories)SelectorTree
Checkbox (ui)SwitchItem
Tabs1 (ui)TabsBar
Toggle3 (ui)ToggleControlled
Section (stat)RoundSection
Section2 (stat)DetailedSection
isNotTransferCategoryshouldShowAmounts
biggestCatNumbermaxCategoryValues
addMarkAreawithMarkArea
toggleOpenedtoggleAllCategoriesExpanded

Модель данных

firebaseтекущий
Дочерние категорииДенормализованный массив childIdsВычисляются динамически из parentId
Определение переводовtransferCategoriesIds (computed из данных категорий)Значение enum TrnType.Transfer
КорректировкиНе выделеныcategoryId === 'adjustment', исключены из статистики доходов/расходов

UX-улучшения

  • Тактильная вибрация при создании транзакции (вибрация на мобильных)
  • Доступность: <div> -> <button> в интерактивных компонентах, i18n aria-label на кнопках-иконках
  • Калькулятор: исправлен бесконечный цикл при нулевом результате, корректный показ 0., округление деления
  • Форма: сумма сбрасывается сразу после submit, popover закрывается перед навигацией
  • Онбординг: компонент Welcome заменён на Onboarding - улучшённый дизайн, состояние в куке (finapp.isOnboarded)
  • Страница валют: два раздела (Используемые / Все) с глобальным поиском, O(1) поиск через currencyMap
  • Детали кошелька/категории: действия редактирования/удаления перенесены в меню с тремя точками
  • Статистика кошельков: архивные кошельки полностью исключены из всех вкладок-фильтров; вкладка «Доступно» показывается только при наличии и withdrawal, и кредитных кошельков; авто-сброс фильтра на «Итого» при переключении валюты, если фильтр пуст
  • Защита редактирования кошелька: редирект на /wallets при несуществующем кошельке
  • Закрытие тоста: клик по уведомлению закрывает его
  • Layout: проп keepalive из слота layout для сохранения состояния при переходах
  • Страница настроек: реорганизована с SettingsCard, выпадающий выбор языка, модальный выбор валюты, зона опасности с «Удалить все данные»
  • Страница логина: отображение версии приложения

i18n

firebaseтекущий
Сообщения об ошибкахОбщие или отсутствуютКлючи ошибок по сущностям (categories.errors.*, wallets.errors.*, trns.errors.*)
Лейблы формМногословные (Category name, Category color)Краткие (Name, Color, Icon)
ОнбордингКлючи welcome.*onboarding.* с пошаговым руководством
Темаneutral, radiusBackground color, Rounding, Appearance
ОпечаткиerrorChildserrorChildren

Качество кода

Удалено из firebase

  • Зависимости vue-deepunref, firebase, nuxt-vuefire
  • Старые page-specific wallet composables
  • Мёртвый код: неиспользуемые props, emits, функции, протухшие TODO
  • Устаревшие типы: WalletsDirty, TrnItemDirty, TransferDeprecated, TrnTypeSlug
  • CSS-утилиты: flex-center-col, absolute-center, layoutBase
  • Старые UI-компоненты (Item1–4, Title3/6/7/8 и др.)
  • Конфиги Firebase (firebase.json, .firebaserc, pnpm-workspace.yaml)

Исправлено

  • Утечки памяти: addEventListener без очистки, ResizeObserver без отключения
  • Null spreading в 3 сторах (отсутствовали ?? {} гарды)
  • Emit во время рендера в SelectionCategoriesFast
  • Магические числа заменены на TrnType enum (8 мест)
  • ref<any> заменены на корректные типы (4 файла)
  • Layout ошибок локализован (хардкод на английском -> i18n-ключи)
  • CSS-синтаксис Tailwind v4: [var(--name)] -> (--name) функциональная нотация
  • Radius использует ?? (не ||), чтобы 0 был валидным: radius ?? 0.375
  • Опечатка CSS-утилиты: bottomSheetDrugClassesCustom -> bottomSheetDragClassesCustom
  • ESLint: исключение vue/no-multiple-template-root для layout
  • createLogger(prefix) - dev-only log, always-on warn и error, с [prefix] форматированием во всех модулях
  • Захардкоженная локаль 'ru' в форматтерах графиков -> динамическая локаль из настроек
  • Statistics.vue пустые секции групп скрыты через v-show
  • Selector.vue onSelectRange(value: any) -> типизированный { end: unknown, start: unknown }
  • Тип setWalletViewType исправлен с WalletType | 'total' на WalletViewTypes | 'total'
  • Версия повышена с v6.6.4 (базовая на ветке firebase)
  • useAmount.getAmountInBaseRate: удалён мёртвый параметр noFormat, убран лишний унарный + над числом
  • useFilter: убраны избыточные [...array.filter()] и [...string.split()] - оба метода уже возвращают новые массивы
  • getUCalendarTimedDate: new Date(string) парсился как UTC -> date.toDate(getLocalTimeZone()) для корректной локальной таймзоны
  • useGetDateRange: паттерн type: 'start' | 'end' удалён - format-функции возвращают полную строку напрямую, 3 IIFE-switch заменены на isSamePeriod
  • getIntervalsInRange: rangeOffset сделан опциональным в IntervalsInRangeProps - не используется в getIntervalsInRange, только в calculateIntervalInRange
  • compareCategoriesByParentAndName: || -> ?? для fallback пустой строки (консистентная nullish-семантика)
  • useCategoriesStore: убраны 5 лишних ?? {} defensive-проверок - items всегда инициализирован через shallowRef
  • useCategoryLongPress: убран избыточный ! после optional chaining (value?.start уже сужает тип)
  • useCategoriesExpanded: forEach -> for...of; reduce с as кастом -> Object.fromEntries
  • sortCategoriesByAmount: isP/isN переименованы в bothPositive/bothNegative для читаемости; убран избыточный categories ??= [] (уже инициализирован при группировке)

Зависимости

(Что изменилось от ветки firebase к текущему приложению. Полный список - в package.json проекта.)

Удалены (эпоха firebase)Добавлены (текущие)
firebase@powersync/web
nuxt-vuefire@supabase/supabase-js
vue-deepunref@vueuse/nuxt
currency.jszod
es-toolkit