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.
Это важно по двум причинам:
- файл и ссылка остаются меньше;
- после обновления расчётного ядра старые данные получают актуальный пересчёт.
Исходный текст 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; это правило применяется также к импортированным настройкам.
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или перезапустить приложение без повреждённого хранилища.