Что изменилось со времён Firebase
Две эпохи. Бэкенд Финапки прошёл путь от 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 Database | Supabase Postgres (реплицируется PowerSync) |
| Авторизация | Firebase Auth через nuxt-vuefire | Supabase 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/A | RLS-ограниченное удаление строк пользователя + локальная очистка (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() |
| Persist | deepUnref() + немедленный localforage.setItem() | Источник правды - SQLite PowerSync; блоб read-through кеш (useStoreCache.ts) рисует первый кадр |
| Офлайн-операции | Ручные хелперы в trns/helpers.ts, произвольные ключи localStorage | Не нужны - встроенная очередь загрузки PowerSync обрабатывает офлайн-записи |
| Система ID | Firebase 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) пропускается когда не нужна |
| Поиск кошелька в getTotal | walletsIds.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() проходит все записи даже после достижения лимита maxCategories | for цикл с break останавливается на лимите | 200 категорий, лимит 16: ~200 → ~16 итераций |
| Итерация недавних категорий | for...of Object.keys(trnsItems) создает промежуточный массив | for...in trnsItems итерирует напрямую по объекту | Без аллокации промежуточного массива для 10K+ транзакций |
| Переиспользование ID категорий | Object.keys(items.value) вызывается отдельно в categoriesRootIds, favoriteCategoriesIds | Оба переиспользуют computed categoriesIds | Ключи вычисляются один раз, используются всеми зависимыми |
| Переиспользование transactible IDs | categoriesIdsForTrnValues повторно фильтрует все категории с проверкой !hasChildren | Делегирует уже вычисленному transactibleIds + .filter(id !== 'transfer') | Без повторного O(M) сканирования родителей |
| Дедупликация children IDs | getChildrenIdsOrParent дублирует логику .filter() из getChildrenIds | Делегирует getChildrenIds(), проверяет .length | Одна реализация фильтра |
Бандл и рендеринг
| Что | firebase | текущий |
|---|---|---|
| ECharts | 9 компонентов в основном бандле (вкл. PieChart, DataZoom, MarkPoint) | 7 компонентов в lazy-загружаемом async-чанке (неиспользуемые удалены) |
| Плагин темы | 81 строка с SSR инлайн-скриптами и regex-заменами | 32 строки, SPA-only обновление appConfig из localStorage |
| Persist | deepUnref() на каждый localforage.setItem() - полное рекурсивное клонирование | Прямая запись, debounce 300ms, без клонирования |
| Адаптивная вёрстка | JS-слушатель useWindowSize() для sidebar | CSS sm: / md: Tailwind брейкпоинты |
| Container queries | Только media queries | CSS 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 any | usePreferredDark() + захардкоженный neutral, поддержка режима system |
Безопасность
| Что | firebase | текущий |
|---|---|---|
| Парсер калькулятора | new Function(expression) - риск инъекции кода, требует unsafe-eval CSP | Рекурсивный парсер - только +,-,*,/ над числами |
| Валидация ввода | Firebase rules (клиент не может проверить) | Клиентские Zod-схемы + типы колонок Postgres / RLS |
| Токены авторизации | Управляются Firebase | JWT + refresh-токен под управлением Supabase (валидируются PowerSync через JWKS) |
| Время жизни сессии | По умолчанию Firebase | Сессия Supabase Auth с автообновлением токена |
| Каскадные удаления | N/A (Firebase) | RLS-ограниченное удаление строк пользователя (без FK-ограничений; PowerSync чистит локальные данные) |
| Целостность данных | Не проверяется в коде | Transfer - categoryId='transfer', нормализация при смене типа, уникальность имён |
| Защита редиректов | Без валидации | getSafeRedirectPath() - только относительные пути, начинающиеся с / (не //) |
Авторизация
| firebase | текущий | |
|---|---|---|
| Провайдер | Firebase Auth | Supabase Auth (email/password) |
| Плагин | nuxt-vuefire (авто-управление авторизацией) | plugins/powersync.client.ts - подключает PowerSync при появлении сессии |
| Поток токенов | Внутри Firebase SDK | SupabaseConnector.fetchCredentials() отдаёт JWT Supabase в PowerSync; валидация через JWKS |
| Офлайн | Firebase SDK управляет реконнектом | Сохранённая сессия Supabase в localStorage; route guard читает её синхронно (useAuthSession) |
| Подсказка состояния | Нет | Сохранённая сессия читается синхронно на холодном старте (getPersistedSession()), без сети |
| Callback | Firebase redirect | Вход по email/password, без OAuth-попапов |
| CORS | N/A | URL сервисов Supabase + PowerSync из runtime config |
| Логин | Firebase авто-редирект | Сохраняет целевой редирект, вход через useSupabaseAuth() |
Синхронизация и офлайн
| firebase | текущий | |
|---|---|---|
| Механизм | Хелперы в trns/helpers.ts | Встроенная очередь загрузки PowerSync - без рукописной офлайн-очереди |
| Записи | Разрозненные ключи localStorage | upsertRow / 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[]> с ?? fallback | Record<SeriesSlugSelected | StatTabSlug, TrnType[]> - все ключи покрыты, fallback не нужен |
| Ключ средних | key as keyof TotalReturns небезопасный каст + as number | Типизированный key: keyof TotalReturns через явный маппинг 'netIncome' -> 'sum', без кастов |
| Fallback курсов | rates[code] || 1, wallet?.currency || 'USD' - falsy 0/'' триггерит fallback | rates[code] ?? 1, wallet?.currency ?? 'USD' - только null/undefined триггерит fallback |
| Парсинг query-параметров | if (data.rangeOffset) - falsy 0 пропускается | if (data.x !== undefined) - нулевые значения применяются корректно |
| Пропсы total | baseCurrencyCode?: 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 config | 2 проекта 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
Удалены: Item1–4, 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; извлечён computedisIntervalSelected - Очистка
useStatItem: извлечён computedbaseTrnsIdsForSelectionдля устранения дублирования логики выбора интервала; исправлен небезопасный каст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 | текущий |
|---|---|
getTrnsIds | filterTrnsIds |
getCategoriesWithData | computeCategoriesWithData |
getTotalOfTrnsIds | computeTotalForTrnsIds |
getChildsIds | getChildrenIds |
trnFormCreate / Edit / Duplicate | openFormForCreate / Edit / Duplicate |
getIsShowSum | shouldShowSum |
isItTransactible | isTransactible |
editedAt | updatedAt |
TransferType | TransferSide |
Selector2 (categories) | SelectorGrid |
Selector (categories) | SelectorTree |
Checkbox (ui) | SwitchItem |
Tabs1 (ui) | TabsBar |
Toggle3 (ui) | ToggleControlled |
Section (stat) | RoundSection |
Section2 (stat) | DetailedSection |
isNotTransferCategory | shouldShowAmounts |
biggestCatNumber | maxCategoryValues |
addMarkArea | withMarkArea |
toggleOpened | toggleAllCategoriesExpanded |
Модель данных
| firebase | текущий | |
|---|---|---|
| Дочерние категории | Денормализованный массив childIds | Вычисляются динамически из parentId |
| Определение переводов | transferCategoriesIds (computed из данных категорий) | Значение enum TrnType.Transfer |
| Корректировки | Не выделены | categoryId === 'adjustment', исключены из статистики доходов/расходов |
UX-улучшения
- Тактильная вибрация при создании транзакции (вибрация на мобильных)
- Доступность:
<div>-><button>в интерактивных компонентах, i18naria-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, radius | Background color, Rounding, Appearance |
| Опечатки | errorChilds | errorChildren |
Качество кода
Удалено из 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 - Магические числа заменены на
TrnTypeenum (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-onlylog, always-onwarnиerror, с[prefix]форматированием во всех модулях- Захардкоженная локаль
'ru'в форматтерах графиков -> динамическая локаль из настроек Statistics.vueпустые секции групп скрыты черезv-showSelector.vueonSelectRange(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 заменены наisSamePeriodgetIntervalsInRange:rangeOffsetсделан опциональным вIntervalsInRangeProps- не используется вgetIntervalsInRange, только вcalculateIntervalInRangecompareCategoriesByParentAndName:||->??для fallback пустой строки (консистентная nullish-семантика)useCategoriesStore: убраны 5 лишних?? {}defensive-проверок -itemsвсегда инициализирован черезshallowRefuseCategoryLongPress: убран избыточный!после optional chaining (value?.startуже сужает тип)useCategoriesExpanded:forEach->for...of;reduceсasкастом ->Object.fromEntriessortCategoriesByAmount:isP/isNпереименованы вbothPositive/bothNegativeдля читаемости; убран избыточныйcategories ??= [](уже инициализирован при группировке)
Зависимости
(Что изменилось от ветки firebase к текущему приложению. Полный список - в package.json проекта.)
| Удалены (эпоха firebase) | Добавлены (текущие) |
|---|---|
firebase | @powersync/web |
nuxt-vuefire | @supabase/supabase-js |
vue-deepunref | @vueuse/nuxt |
currency.js | zod |
es-toolkit |