Skip to content

Latest commit

 

History

History
174 lines (118 loc) · 22.3 KB

File metadata and controls

174 lines (118 loc) · 22.3 KB

Архитектура проекта

CreditCalc — кредитный график — статическое React-приложение с Android-оболочкой. Оно не использует backend, базу данных и внешние API для расчётов. Все данные остаются на устройстве пользователя.

Основной поток данных

Параметры кредита / платежи / льготные периоды
        ↓
Zustand store с persist в localStorage
        ↓
expandRepaymentRules
        ↓
useLoanCalculation → loanCalculationRunner
        ↓
Web Worker или синхронный fallback: buildLoanCalculation / compareScenarios
        ↓
generateBaseSchedule
        ↓
интерфейс, график, печать, экспорт

Планировщик цели → GoalPlannerRunner → отдельный Worker
        ↓
buildGoalPlans → точный поиск в копейках → generateBaseSchedule
        ↓
сводки вариантов / выбранный график / атомарное применение операций

Ключевые части проекта

  • src/App.tsx — общая компоновка приложения, переключение разделов и связывание модулей интерфейса.
  • src/hooks/useLoanCalculation.ts — подготовка единого снимка расчёта, контроль актуальности результата и данные обзора.
  • src/loanCalculationRunner.ts — общий сервис запуска расчёта: Web Worker в production и синхронный fallback в средах без Worker.
  • src/loanCalculation.ts, src/loanCalculation.worker.ts — чистая сборка расчёта и worker-обёртка для тяжёлого пересчёта.
  • src/goalPlanner.ts, src/goalPlannerRunner.ts, src/goalPlanner.worker.ts — чистый подбор целей, Worker-протокол, latest-wins отмена и построение выбранного графика.
  • src/hooks/useLoanImport.ts, src/hooks/useLoanExport.ts, src/hooks/useSharedCalculation.ts, src/hooks/useStorageStatus.ts — прикладные потоки импорта, экспорта, ссылок, onboarding и статуса локального хранилища.
  • src/platform.ts, src/clipboard.ts, src/download.ts — тонкая граница веб- и Android-платформ для печати, системных панелей, буфера обмена, ссылок и экспорта файлов.
  • src/service-worker.ts, src/pwa — scoped offline shell, стратегии кеширования, регистрация, установка, обновления и Storage API.
  • src/store.ts — Zustand-действия и persist-конфигурация для кредитов, активного кредита и настроек интерфейса.
  • src/storageKeys.ts — совместимостные ключи хранилища, включая legacy ключ persisted-состояния.
  • src/storeNormalization.ts, src/storeTypes.ts — нормализация входящих данных, миграции localStorage, лимиты и публичные типы store.
  • src/loanEngine — расчётное ядро: проценты, график, досрочные погашения, льготные периоды, сравнение сценариев и валидация.
  • src/repaymentRules.ts — преобразование регулярных досрочных платежей в обычный массив EarlyRepayment.
  • src/importExport.ts — проверка и нормализация JSON перед загрузкой, включая дефолты для старых файлов без новых полей.
  • src/shareCalculation.ts — сериализация расчёта в сжатую ссылку и восстановление из неё.
  • src/components — разделы и модальные окна интерфейса.
  • src/components/ErrorBoundary.tsx — последний защитный слой от неожиданных runtime-ошибок интерфейса.
  • src/styles — разделённые CSS-файлы по областям интерфейса.
  • CHANGELOG.md — источник страницы “Что изменилось” внутри приложения.
  • vite.config.ts — версия приложения, дата сборки и базовый путь для GitHub Pages.

PWA не меняет основной поток финансовых данных: service worker кеширует только статическую оболочку и не имеет маршрута записи пользовательского state в Cache Storage. Подробная политика описана в PWA.md.

Хранение данных

Данные сохраняются через Zustand persist в localStorage.

Текущий ключ хранилища задан константой PERSISTED_LOAN_STORAGE_KEY:

ipoteka-calculator-v1

Название ключа сохранено для совместимости со старыми установками, несмотря на новое название приложения.

Текущая версия persisted-схемы — 11. До миграции собственный deserializer ограничивает размер входа и сохраняет исходную строку при ошибке JSON. Автосохранение в этом случае блокируется, пока пользователь не скачает backup и явно не удалит повреждённые данные. При загрузке старого состояния приложение нормализует параметры кредита, историю изменения ставки, разовые платежи, регулярные платежи и льготные периоды. Legacy-значение amountMode: "total" канонизируется в amountMode: "totalWithFee". Записи с невозможными датами или повреждёнными обязательными полями отбрасываются. Все массивы обрезаются до безопасных лимитов до map, сортировки и полной валидации, чтобы повреждённое хранилище не подвешивало интерфейс.

Скрытие уведомления карантина меняет только UI-флаг: отчёт и исходные данные остаются до отдельного подтверждённого удаления, а скрытую панель можно открыть снова. Экспорт карантина называется ограниченной копией для восстановления, содержит rawIsComplete: false, применённые лимиты и предупреждение о возможном усечении; он не обозначается как полная копия исходного хранилища.

Persist использует безопасный адаптер localStorage. Если браузер запрещает запись или хранилище переполнено, ошибка не ломает интерфейс: приложение показывает предупреждение, что последние изменения не сохранены, и предлагает сохранить расчёт через JSON-экспорт или повторить запись.

Каждая запись получает epoch хранилища, монотонную revision и случайный writer ID вкладки. В поддерживающих браузерах read-check-write выполняется под эксклюзивным Web Lock; BroadcastChannel ускоряет оповещение вкладок, а событие storage остаётся fallback и отдельно обрабатывает удаление ключа. При гонке, новой revision или внешнем удалении интерфейс переходит в состояние конфликта и больше не показывает «Данные сохранены» до явного выбора пользователя.

До первого ввода onboarding предупреждает, что GitHub Pages использует общий origin. Режим «без сохранения» удаляет persisted-кредит и блокирует дальнейшие записи в localStorage до конца текущей вкладки; повторное включение в настройках сразу сохраняет актуальное состояние. Onboarding-флаги версии не содержат финансовых данных и остаются отдельными ключами.

Во время миграции каждый восстановленный кредит проходит проверку расчётного кандидата. Повреждённые кредиты помещаются в карантин, а пользователь получает отчёт восстановления в интерфейсе.

Каждый кредит хранит собственные:

  • параметры кредита и историю изменения ставки;
  • разовые досрочные платежи;
  • регулярные досрочные платежи;
  • льготные периоды;
  • выбранный сценарий;
  • настройки отображения;
  • тему и акцентный цвет.

Сценарии расчёта

compareScenarios строит несколько вариантов:

  • без досрочных платежей;
  • сокращение срока;
  • снижение платежа;
  • по операциям.

Сценарий “по операциям” использует стратегию, указанную в каждой досрочной операции. В интерфейсе конкретной операции пользователь выбирает только прикладные стратегии: сократить срок, снизить платёж или закрыть кредит полностью.

Импорт, экспорт и ссылки

JSON и ссылки не сохраняют готовый график платежей. Они сохраняют исходные данные, после чего график пересчитывается локально.

Day-count enum не содержит дублирующую базу 365: legacy-значение мигрируется в actual365. Audit каждой строки хранит метод начисления, чтобы UI и печатный отчёт не представляли базу года как часть периодической формулы.

Денежные входы ограничены 1 трлн единиц валюты, а агрегаты сценария — 90 трлн. Последний предел гарантирует, что масштабирование до целых копеек не выходит за Number.MAX_SAFE_INTEGER перед передачей результата из Decimal.js в UI и экспорт.

Первый interest-only платёж хранит отдельный договорный режим: addToTerm расширяет календарь на один период, а withinTerm оставляет календарь в исходном числе периодов и рассчитывает аннуитет/дифференцированное тело по оставшимся амортизирующим датам. Отсутствующее legacy-поле нормализуется в addToTerm.

Это важно по двум причинам:

  1. файл и ссылка остаются меньше;
  2. после обновления расчётного ядра старые данные получают актуальный пересчёт.

Исходный текст JSON или полный compressed payload shared-ссылки передаётся Worker до JSON.parse, распаковки и проверки структуры, дат, чисел, валюты, типов платежей, истории ставки, льготных периодов и регулярных платежей. Worker возвращает типизированный ValidatedLoanData; store принимает этот результат без повторного синхронного расчёта. Поля, которых не было в старых версиях формата, получают документированные defaults, если их отсутствие не делает расчёт неоднозначным. Явно повреждённые финансовые поля отклоняются. Отсутствующая валюта получает RUB, а известный legacy-код RUR преобразуется в RUB; оба случая показывают предупреждение и не конвертируют суммы.

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

Тяжёлые разделы интерфейса загружаются лениво:

  • обзор;
  • настройки;
  • досрочные платежи;
  • график платежей;
  • импорт/экспорт.
  • планировщик цели.

Это уменьшает основной JS-бандл и ускоряет первичную загрузку на статическом хостинге.

Сам расчёт строится через LoanCalculationRunner. В production он использует Web Worker и envelope с requestId, kind, immutable snapshot и revision id, а при новой ревизии завершает предыдущий worker-экземпляр до постановки новой задачи. Ответ проходит runtime-проверку структуры и принадлежности активному запросу; Worker завершается после первого результата. Повреждённый, чужой, ошибочный или отсутствующий дольше 15 секунд ответ запускает однократный синхронный fallback через тот же чистый модуль buildLoanCalculation. После трёх последовательных runtime-сбоев новые Worker временно не создаются. Поэтому latest-wins относится не только к ответам: устаревший тяжёлый расчёт не задерживает актуальный в очереди Worker.

Планировщик использует отдельный GoalPlannerRunner. Каждый подбор или предпросмотр получает собственный Worker; изменение формы, параметров кредита или активного кредита завершает старый экземпляр. Тяжёлого синхронного fallback у этого потока нет: при недоступном Worker раздел сообщает локальную ошибку и не блокирует основной UI. Worker возвращает сводки вариантов, а полный график строится отдельным запросом только для выбранного плана. В PWA оба хешированных worker-asset попадают в общий precache статической оболочки.

Большие UI-коллекции имеют жёсткий DOM-budget: график и исчезнувшие строки исходного графика выводятся по 100 записей, а разовые платежи, регулярные правила и объединённый календарь — по 50. Предупреждения импорта выводятся по 20. Переход между страницами не меняет финансовые данные или итоговые суммы.

Мобильное меню открывается как modal drawer: основной контент получает inert, Tab/Shift+Tab остаются внутри меню, Escape закрывает его, а фокус возвращается кнопке открытия. На desktop sidebar сохраняет обычную навигационную семантику.

Граница ошибок раздела привязана одновременно к текущему разделу и активному кредиту. Смена кредита сбрасывает локальную ошибку предыдущего расчёта и даёт новому кредиту отобразиться без ручного перехода в другой раздел.

Пользовательский акцент нормализуется до контраста не ниже 3:1 с самой светлой и тёмной поверхностями тем. Для текста и содержимого кнопок отдельно выбираются цвета с контрастом не ниже 4.5:1; это правило применяется также к импортированным настройкам.

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

Android-оболочка

Capacitor 8 оборачивает тот же production bundle в Android WebView. Нативная сборка выводится в отдельный dist-android, не регистрирует service worker и не показывает браузерные действия установки или persistent storage. Расчётное ядро, форматы импорта и состояние Zustand остаются общими для веб- и Android-версий. Семантическая версия читается из package.json; Gradle преобразует её в versionName и числовой versionCode.

Тонкий платформенный слой отвечает только за системные границы: экспорт через Capacitor Filesystem/Share, буфер обмена через Clipboard, печать через собственный plugin AndroidPrint, аппаратную кнопку «Назад», оформление системных панелей через встроенный в @capacitor/core plugin SystemBars, публичный адрес ссылок на расчёт и платформенно-точные предупреждения о хранении. Android-проект не содержит рекламных, аналитических или сетевых SDK.

Адаптивный график сохраняет два режима представления. Карточки группируют поля платежа, а табличный режим на узком экране преобразует каждую строку в подписанный многострочный блок; горизонтальная прокрутка для основного графика не требуется.

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

Проект проверяется через TypeScript, ESLint, Vitest, coverage-пороги и production-сборку. Глобальные минимумы составляют 80% строк и выражений, 75% функций и 65% ветвей; для goalPlanner.ts и src/loanEngine действуют отдельные повышенные пороги. ESLint включает правила TypeScript и React Hooks; тесты покрывают расчётное ядро, golden-fixture и property-style сценарии, импорт/экспорт, шаринг ссылкой, миграции store, Worker-протоколы, валидацию экспорта и базовые UI-сценарии.

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

Приложение не отправляет финансовые данные на сервер. Основные риски связаны не с backend, а с действиями пользователя:

  • JSON-файл может содержать конфиденциальные параметры кредита;
  • ссылка на расчёт содержит все исходные данные;
  • любой получатель ссылки сможет восстановить расчёт.

Поэтому в интерфейсе и README отдельно указано предупреждение про ссылки.

Защита от повреждённых данных

Приложение проверяет данные на нескольких уровнях:

  • JSON-импорт отклоняет повреждённые даты, неизвестные enum-значения и слишком большие массивы;
  • расчётное ядро повторно проверяет входные данные перед построением графика;
  • старые данные из localStorage нормализуются при миграции;
  • при недоступном localStorage пользователь видит предупреждение, может скачать JSON и повторить сохранение;
  • Error Boundary показывает понятное сообщение вместо пустого экрана, если в интерфейсе всё же возникла непредвиденная ошибка.
  • Error Boundary также позволяет скачать raw-данные из localStorage или перезапустить приложение без повреждённого хранилища.