From 6f190c72457e9f193372b1334bd9e49aa9cf1f53 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Fri, 3 Jul 2026 12:03:58 +0300 Subject: [PATCH] docs: tabbed settings, auto-update toggle and diagnostics --- ARCHITECTURE.md | 109 +++++++++++++++++++++++++++++++++++------- ARCHITECTURE.ru.md | 115 ++++++++++++++++++++++++++++++++++++++------- README.md | 21 ++++++--- README.ru.md | 21 ++++++--- 4 files changed, 220 insertions(+), 46 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 88027ec..ad6017c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -21,6 +21,8 @@ src/ index.ts SumEngine facade + extension API + sheet totals ui/ editor.ts CodeMirror 6 wiring: highlight, results overlay, copy + search.ts cross-sheet search overlay (text + result-value queries) + export.ts renders the sheet + results to a PNG (clipboard/file) app.css themes (light/dark via CSS variables) main.ts app bootstrap: documents, settings, status bar, dnd storage.ts Tauri commands / localStorage fallback @@ -29,8 +31,8 @@ src/ updater.ts checks tauri-plugin-updater, installs + relaunches src-tauri/ src/main.rs tray, hide-to-tray, storage commands, rates fetching - (open.er-api.com + CoinGecko + Yahoo Finance) with - cache, historical rates (frankfurter.app, permanent + (open.er-api.com + CoinGecko + Yahoo Finance v8 chart) + with cache, historical rates (frankfurter.dev, permanent per-date cache), backups, data-folder migration, file drops, plugin registration (autostart, global-shortcut, single-instance, opener, dialog, @@ -111,7 +113,49 @@ snapshot price) plus the mirror list in `src-tauri/src/main.rs`. bypasses `formatValue` — the results overlay renders an SVG sparkline instead of a text string. - The registry is rebuilt never; extensions mutate it (add phrases/functions) - at startup. + at startup. `buildRegistry()` (`registry.ts`) is a thin orchestrator over + `registerCoreVocab` / `registerUnits` / `registerAreaVolume` / + `registerCurrencies` / `registerExtraUnits` / `registerCrypto` / + `registerCompletions` — **call order matters**, since `Registry.addPhrase` + keeps the *first* registration of an identical phrase and drops later ones + (core vocab must win over unit/currency phrases that happen to collide). +- Multiplying two same-dimension quantities (`2 kg * 500 g`) converts the + right operand into the left operand's unit before multiplying, so the + result keeps a sane unit (`1 kg`, not `1000` in a `kg` label). Cross-dimension + multiplication (`$12 * 3`, `5 hours * 2`) is unaffected — the right side is + treated as a plain scale factor there. +- A `//` only starts a comment at the start of a line or after whitespace + (`findCommentStart` in `index.ts`), so a URL like `see https://example.com` + isn't truncated at the double slash. +- **Date literals and formats.** Besides ISO (`2024-01-01`), the lexer accepts + `D.M.YYYY` / `M/D/YYYY` (`31.12.2024`, `12/31/2024`) with the same separator + on both sides and a 4-digit year. Day/month order is disambiguated per + literal: a component `>12` fixes which side is the day; if both are `≤12`, + the engine's `dateFormat` setting decides. All three literal forms reject + out-of-range components via a round-trip check (`validDateMs` in `lexer.ts` + — `new Date(y, mo-1, d)` silently *rolls over* invalid dates like + `2026-13-45` instead of erroring, so the constructed date's year/month/day + are compared back against the input). `SumEngine` resolves `dateFormat: + "system"` to an actual day/month order once via `detectDateOrder()` + (`datetime.ts`, probes `Intl.DateTimeFormat` on a `2000-12-31` fixture) and + passes it into `tokenize()` → `lexLine()` on every call; `"dmy"`/`"mdy"` + settings skip detection. Display works the same way in reverse: `formatter.ts` + either uses the OS-locale `Intl` formatting (`"system"`) or builds a fixed + `YYYY-MM-DD` / `DD.MM.YYYY` / `MM/DD/YYYY` string via + `Intl.DateTimeFormat(...).formatToParts()` (needed to respect the value's + own IANA timezone, which a hand-rolled `ms`-based formatter can't do). + `Intl.DateTimeFormat` instances are cached by options key in `formatter.ts` + (`dtf()`) since constructing one is comparatively expensive and every dated + line rebuilds its format on every render. +- Timezone name parsing (`time in Europe/Berlin`) has two independent traps. + First, `parseLine`'s noise-filter only keeps word tokens that immediately + follow a `conv` token (`in`/`as`/...); any other token type resets that + state — a bare `/` between two timezone words would normally drop the + second word before the parser ever sees it, so the filter special-cases a + `/` that appears while collecting post-conv words. Second, `parseTarget()`'s + word-collecting loop mirrors that same allowance so it keeps consuming past + the `/` instead of stopping at the first word. Both places need the + exception — fixing only one still drops `Europe/Berlin` down to `Europe`. ## Cross-sheet references @@ -147,10 +191,19 @@ recursing forever. scroll/geometry changes. Click-to-copy reads `data-value`. - Engine evaluation is synchronous on every keystroke — a full sheet parse is well under a millisecond, so there's no debounce or worker. -- Search-all-sheets (`Ctrl+F`) is a plain line scan over `data.contents` in - `main.ts`, not a CodeMirror search extension — it has to cross documents, - so it never touches the active editor until a result is clicked - (`editor.goToLine`). +- Search-all-sheets (`Ctrl+F`) is a plain line scan over every open sheet's + text, not a CodeMirror search extension — it has to cross documents, so it + never touches the active editor until a result is clicked + (`editor.goToLine`). It lives in `src/ui/search.ts` (`initSearch()`), which + owns the `#search-overlay` DOM and its own 150 ms input debounce — a + result-value query (`>1000`) re-evaluates every open sheet, so debouncing + matters once there are more than a couple of sheets open. +- The settings popover is split into four tabs (General / Format / Data / + Updates) in `index.html`, each a `.settings-tabpanel` toggled by a + `#settings-tabs .tab` button in `bindSettingsUI()` (`main.ts`) — plain + `classList.toggle`, no router. Tab styling reuses the sidebar's + `doc-item.active` idiom (`background: var(--sel)` for the active pill) + rather than introducing a new visual pattern. ## Storage @@ -165,7 +218,28 @@ recursing forever. - `updater.ts` calls `tauri-plugin-updater`'s `check()` on boot; if a signed update is available the user is prompted, then `downloadAndInstall()` + - `tauri-plugin-process`'s `relaunch()`. + `tauri-plugin-process`'s `relaunch()`. `checkForUpdate()` returns + `AvailableUpdate | null | "error"` — `null` means the check succeeded and + found nothing newer, `"error"` means the check itself failed (offline, + signature mismatch, etc.); the distinction only matters for the manual path + below. Every outcome is now visible in devtools: failures via + `console.warn`, and a successful "no update" result logs the running + version alongside "that's the latest published release" via `console.info` + — the most common false alarm is simply that the installed build already + *is* the newest release (e.g. right after installing it), which used to be + indistinguishable from a silently broken check. +- Closing the window hides it to the tray instead of exiting the process (see + `on_window_event` in `main.rs`), so a boot-time-only check can go unnoticed + for a long time on a machine that isn't restarted often. `main.ts` also + reruns `checkUpdate()` on a `setInterval` (every 6 hours) so a long-lived + tray process still notices new releases, and Settings → Updates has a + manual "Check for updates" button (`checkUpdate(true)`) that reports + "up to date" / "check failed" via toast — those toasts are suppressed on + the automatic background checks so the app doesn't nag every few hours. + A `SettingsData.autoUpdateEnabled` toggle turns off both the boot check and + the periodic recheck (re-read from `settings` on every interval tick, so + flipping it while the app is running takes effect immediately); the manual + button ignores the toggle and always works. - The update endpoint is `latest.json` published alongside each GitHub Release. `release.yml` signs both architectures with a minisign keypair: the public half is embedded in `tauri.conf.json`; the private half + its @@ -181,16 +255,19 @@ All files below live in `%APPDATA%/app.summarum.calc` (or the user-chosen folder |------|----------|-----| | `settings.json` | app settings | persisted | | `documents.json` | all sheets | persisted (400 ms debounce) | -| `rates.json` | live exchange rates | 1 hour | -| `market.json` | stock/commodity prices | 15 minutes | -| `rates-YYYY-MM-DD.json` | historical ECB rates for one date | permanent | +| `rates.json` | live exchange rates (open.er-api.com + CoinGecko) | 1 hour | +| `market.json` | stock/commodity prices (Yahoo Finance v8 chart, one request per symbol) | 15 minutes | +| `rates-YYYY-MM-DD.json` | historical ECB rates for one date (frankfurter.dev) | permanent | | `backups/documents-YYYY-MM-DD.json` | daily snapshots | 14 days | | `backups/deleted/*.numi` | soft-deleted sheets | configurable | ## Tests -`npm test` runs 113 vitest cases over the engine (`src/engine/*.test.ts`): -every expression class, both languages, deterministic injected rates, -goal seek, historical rates (injected), plus a regression suite covering -all known-fixed bugs. UI is exercised manually; the engine is where the -complexity lives. +`npm test` runs 152 vitest cases: `src/engine/*.test.ts` covers every +expression class, both languages, deterministic injected rates, goal seek, +historical rates (injected), date-format literals/display (dedicated engines +with an explicit `dateFormat`, since the default "system" format depends on +the CI machine's locale), plus a regression suite covering all known-fixed +bugs; `src/workspace.test.ts` covers cross-sheet references, caching and +cycle detection. UI is exercised manually; the engine is where the complexity +lives. diff --git a/ARCHITECTURE.ru.md b/ARCHITECTURE.ru.md index f6bb973..a32e64e 100644 --- a/ARCHITECTURE.ru.md +++ b/ARCHITECTURE.ru.md @@ -22,6 +22,8 @@ src/ index.ts фасад SumEngine + API расширений + итоги листа ui/ editor.ts обвязка CodeMirror 6: подсветка, оверлей результатов, копирование + search.ts оверлей поиска по всем листам (текст + запросы по значению) + export.ts рендерит лист + результаты в PNG (буфер обмена/файл) app.css темы (светлая/тёмная через CSS-переменные) main.ts запуск приложения: документы, настройки, строка статуса, dnd storage.ts команды Tauri / запасной вариант на localStorage @@ -30,10 +32,10 @@ src/ updater.ts проверка tauri-plugin-updater, установка + перезапуск src-tauri/ src/main.rs трей, скрытие в трей, команды хранилища, загрузка курсов - (open.er-api.com + CoinGecko + Yahoo Finance) с кэшем, - исторические курсы (frankfurter.app, постоянный кэш - на каждую дату), резервные копии, миграция папки данных, - открытие файлов, регистрация плагинов (autostart, + (open.er-api.com + CoinGecko + Yahoo Finance v8 chart) + с кэшем, исторические курсы (frankfurter.dev, постоянный + кэш на каждую дату), резервные копии, миграция папки + данных, открытие файлов, регистрация плагинов (autostart, global-shortcut, single-instance, opener, dialog, updater, process) ``` @@ -109,7 +111,53 @@ src-tauri/ - `ChartValue` (`kind: "chart"`) возвращается из `evalAgg("chart", …)` и минует `formatValue` — оверлей результатов рисует SVG-спарклайн вместо текста. - Реестр не пересобирается никогда; расширения мутируют его (добавляют фразы/функции) - при запуске. + при запуске. `buildRegistry()` (`registry.ts`) — тонкий оркестратор над + `registerCoreVocab` / `registerUnits` / `registerAreaVolume` / + `registerCurrencies` / `registerExtraUnits` / `registerCrypto` / + `registerCompletions` — **порядок вызовов важен**: `Registry.addPhrase` + оставляет *первую* регистрацию одинаковой фразы и отбрасывает последующие + (базовый словарь обязан побеждать фразы единиц/валют, если они случайно + совпадают). +- Умножение двух величин одного измерения (`2 kg * 500 g`) конвертирует + правый операнд в единицу левого перед умножением, чтобы результат имел + осмысленную единицу (`1 kg`, а не `1000` в подписи `kg`). Умножение разных + измерений (`$12 * 3`, `5 hours * 2`) не затронуто — там правая часть + трактуется как простой числовой множитель. +- `//` начинает комментарий только в начале строки или после пробела + (`findCommentStart` в `index.ts`), поэтому URL вида `see https://example.com` + не обрезается на двойном слэше. +- **Литералы и форматы дат.** Помимо ISO (`2024-01-01`), лексер понимает + `D.M.YYYY` / `M/D/YYYY` (`31.12.2024`, `12/31/2024`) с одинаковым + разделителем с обеих сторон и 4-значным годом. Порядок день/месяц + разрешается для каждого литерала отдельно: компонент `>12` однозначно + определяет, где день; если оба `≤12`, решает настройка движка `dateFormat`. + Все три формы литералов отклоняют компоненты вне диапазона через + round-trip проверку (`validDateMs` в `lexer.ts` — `new Date(y, mo-1, d)` + молча *перекатывает* невалидные даты вроде `2026-13-45` вместо ошибки, + поэтому построенные год/месяц/день сравниваются обратно со входом). + `SumEngine` разрешает `dateFormat: "system"` в реальный порядок день/месяц + один раз через `detectDateOrder()` (`datetime.ts`, пробует + `Intl.DateTimeFormat` на фикстуре `2000-12-31`) и передаёт его в + `tokenize()` → `lexLine()` при каждом вызове; настройки `"dmy"`/`"mdy"` + пропускают детекцию. Отображение работает так же в обратную сторону: + `formatter.ts` либо использует форматирование `Intl` по локали ОС + (`"system"`), либо строит фиксированную строку `YYYY-MM-DD` / `DD.MM.YYYY` / + `MM/DD/YYYY` через `Intl.DateTimeFormat(...).formatToParts()` (это нужно, + чтобы уважать собственный IANA-часовой пояс значения, чего форматтер на + основе голого `ms` сделать не может). Экземпляры `Intl.DateTimeFormat` + кэшируются по ключу опций в `formatter.ts` (`dtf()`), так как их создание + сравнительно дорого, а каждая строка с датой пересобирает формат при + каждой отрисовке. +- У разбора названий часовых поясов (`time in Europe/Berlin`) есть две + независимые ловушки. Во-первых, шумовой фильтр `parseLine` оставляет + word-токены только сразу после `conv`-токена (`in`/`as`/...); любой другой + тип токена сбрасывает это состояние — голый `/` между двумя словами + часового пояса обычно отбросил бы второе слово раньше, чем его увидит + парсер, поэтому фильтр отдельно разрешает `/`, встреченный при сборе + post-conv слов. Во-вторых, цикл сбора слов в `parseTarget()` зеркалит то же + исключение, чтобы продолжать читать после `/`, а не останавливаться на + первом слове. Нужны оба места — исправление только одного всё равно + урезает `Europe/Berlin` до `Europe`. ## Ссылки между листами @@ -144,10 +192,20 @@ src-tauri/ прокрутке/изменении геометрии. Клик для копирования читает `data-value`. - Оценка движка синхронная на каждое нажатие клавиши — полный разбор листа занимает менее миллисекунды, поэтому нет дебаунса и воркеров. -- Поиск по всем листам (`Ctrl+F`) — простое построчное сканирование - `data.contents` в `main.ts`, а не расширение поиска CodeMirror — ему нужно +- Поиск по всем листам (`Ctrl+F`) — простое построчное сканирование текста + каждого открытого листа, а не расширение поиска CodeMirror — ему нужно пересекать документы, поэтому он не трогает активный редактор, пока не - кликнут результат (`editor.goToLine`). + кликнут результат (`editor.goToLine`). Живёт в `src/ui/search.ts` + (`initSearch()`), который владеет DOM `#search-overlay` и собственным + дебаунсом ввода на 150 мс — запрос по значению (`>1000`) пересчитывает + каждый открытый лист, так что дебаунс важен, как только листов становится + больше пары. +- Поповер настроек разбит на четыре вкладки (Общие / Формат / Данные / + Обновления) в `index.html`, каждая — `.settings-tabpanel`, переключаемая + кнопкой `#settings-tabs .tab` в `bindSettingsUI()` (`main.ts`) — обычный + `classList.toggle`, без роутера. Стиль вкладок повторяет идиому сайдбара + `doc-item.active` (`background: var(--sel)` для активной таблетки) вместо + нового визуального паттерна. ## Хранилище @@ -163,6 +221,28 @@ src-tauri/ - `updater.ts` вызывает `check()` из `tauri-plugin-updater` при запуске; если подписанное обновление доступно, пользователю предлагают установить, затем `downloadAndInstall()` + `relaunch()` из `tauri-plugin-process`. + `checkForUpdate()` возвращает `AvailableUpdate | null | "error"` — `null` + значит проверка прошла успешно и ничего новее нет, `"error"` значит сама + проверка не удалась (офлайн, рассинхрон подписи и т.п.); различие важно + только для ручного пути ниже. Теперь виден любой исход: сбои через + `console.warn`, а успешный результат «нет обновления» логирует текущую + версию рядом с «это последний опубликованный релиз» через `console.info` — + самый частый ложный сигнал тревоги это когда установленная сборка И ЕСТЬ + последний релиз (например, сразу после его установки), что раньше было + неотличимо от молча сломанной проверки. +- Закрытие окна прячет его в трей вместо завершения процесса (см. + `on_window_event` в `main.rs`), поэтому проверка только при старте может + долго оставаться незамеченной на машине, которую редко перезагружают. + `main.ts` также перезапускает `checkUpdate()` через `setInterval` (каждые + 6 часов), чтобы долгоживущий трей-процесс всё равно замечал новые релизы, + а в настройках (вкладка «Обновления») есть кнопка «Проверить обновления» + (`checkUpdate(true)`), которая сообщает «последняя версия» / «не удалось + проверить» через toast — эти уведомления подавлены на автоматических + фоновых проверках, чтобы приложение не напоминало о себе каждые несколько + часов. Тумблер `SettingsData.autoUpdateEnabled` отключает и проверку при + запуске, и периодическую (значение перечитывается из `settings` на каждом + тике интервала, так что переключение прямо во время работы срабатывает + сразу); ручная кнопка тумблер игнорирует и работает всегда. - Эндпоинт обновлений — `latest.json`, публикуемый вместе с каждым GitHub Release. `release.yml` подписывает обе архитектуры парой ключей minisign: публичная половина зашита в `tauri.conf.json`; приватная половина + пароль @@ -179,16 +259,19 @@ src-tauri/ |------|-----------|-----| | `settings.json` | настройки | постоянно | | `documents.json` | все листы | постоянно (дебаунс 400 мс) | -| `rates.json` | живые курсы валют | 1 час | -| `market.json` | котировки акций/сырья | 15 минут | -| `rates-YYYY-MM-DD.json` | исторические курсы ЕЦБ за одну дату | навсегда | +| `rates.json` | живые курсы валют (open.er-api.com + CoinGecko) | 1 час | +| `market.json` | котировки акций/сырья (Yahoo Finance v8 chart, по запросу на символ) | 15 минут | +| `rates-YYYY-MM-DD.json` | исторические курсы ЕЦБ за одну дату (frankfurter.dev) | навсегда | | `backups/documents-YYYY-MM-DD.json` | ежедневные снимки | 14 дней | | `backups/deleted/*.numi` | мягко удалённые листы | настраивается | ## Тесты -`npm test` запускает 113 vitest-кейсов по движку (`src/engine/*.test.ts`): -все классы выражений, оба языка, детерминированные инжектированные курсы, -поиск неизвестного, исторические курсы (инжектированные), плюс набор регрессий -по всем известным исправленным багам. UI проверяется вручную; движок — там -где живёт вся сложность. +`npm test` запускает 152 vitest-кейса: `src/engine/*.test.ts` покрывает все +классы выражений, оба языка, детерминированные инжектированные курсы, поиск +неизвестного, исторические курсы (инжектированные), литералы и отображение +форматов дат (отдельные движки с явным `dateFormat` — формат «system» по +умолчанию зависит от локали машины CI), плюс набор регрессий по всем известным +исправленным багам; `src/workspace.test.ts` покрывает межлистовые ссылки, +кэширование и обнаружение циклов. UI проверяется вручную; движок — там где +живёт вся сложность. diff --git a/README.md b/README.md index f015c1a..be5ffd5 100644 --- a/README.md +++ b/README.md @@ -67,13 +67,16 @@ the math is ignored, so `spent 20 on pizza and 5 on coffee` just gives 25. `100 AAPL in USD`, `2 GC=F in USD` (gold futures), `MSFT + GOOGL`. **Historical exchange rates** — `1000 USD in EUR on 2024-01-01` looks up the -ECB rate for that exact date (via frankfurter.app) and caches it permanently. +ECB rate for that exact date (via frankfurter.dev) and caches it permanently. **Percents the human way** — `20% of 80`, `20% off 1500`, `5 as a % of 25`, `20% of what is 30`. -**Dates and time** — `today + 2 weeks`, `tomorrow - today`, `time in New York`, -`now in unix`, `1750000000 as date`. ISO date literals: `2024-01-01 + 30 days`, +**Dates and time** — `today + 2 weeks`, `tomorrow - today`, `time in New York` +(also works with a raw IANA id: `time in Europe/Berlin`), `now in unix`, +`1750000000 as date`. Date literals: ISO (`2024-01-01 + 30 days`), or +dot/slash with a 4-digit year — `31.12.2024`, `12/31/2024` — where the +day/month order for ambiguous cases follows the Date format setting. `days until 2027-01-01`, `days since 2020-03-15`. **Goal seek** — put `?` where the unknown is and an `=` with the target: @@ -117,9 +120,13 @@ auto-derived one) so the reference doesn't break when its first line changes. - **Data folder is configurable** — point it at OneDrive/Dropbox and your sheets follow you between machines. - **Always on top** (Settings) pins the window above everything else. -- The app checks for updates on launch and offers to install them — no - manual download-and-reinstall. -- Light/dark theme, en/ru interface, configurable precision and separators. +- The app checks for updates on launch and every few hours after that, and + offers to install them — no manual download-and-reinstall. Settings → + Updates → "Check for updates" runs one on demand, or turn automatic + checking off entirely with the toggle next to it. +- Light/dark theme, en/ru interface, configurable precision, separators, and + date format (system locale, `31.12.2024`, `12/31/2024`, or `2024-12-31`). + Settings is split into General / Format / Data / Updates tabs. ## Extensions @@ -149,7 +156,7 @@ see [ARCHITECTURE.md](ARCHITECTURE.md). ```bash npm install -npm test # engine tests (113 cases) +npm test # engine tests (152 cases) npm run tauri dev # run the app npm run tauri build # build the installer ``` diff --git a/README.ru.md b/README.ru.md index c9c3260..547d8e8 100644 --- a/README.ru.md +++ b/README.ru.md @@ -63,13 +63,16 @@ AAPL в USD → $213.55 `100 AAPL в USD`, `2 GC=F в USD` (фьючерс на золото), `MSFT + GOOGL`. **Исторические курсы** — `1000 USD в EUR на 2024-01-01` запрашивает курс ЕЦБ -за этот день (через frankfurter.app) и кэширует его навсегда. +за этот день (через frankfurter.dev) и кэширует его навсегда. **Проценты по-человечески** — `20% от 80`, `скидка 20% от 1500`, `5 это сколько % от 25`, `20% от какого числа равно 30`. -**Даты и время** — `сегодня + 2 недели`, `завтра - сегодня`, `время в Нью-Йорке`, -`сейчас в unix`, `1750000000 как дата`. ISO-даты: `2024-01-01 + 30 дней`, +**Даты и время** — `сегодня + 2 недели`, `завтра - сегодня`, `время в Нью-Йорке` +(работает и с прямым IANA-id: `время в Europe/Berlin`), `сейчас в unix`, +`1750000000 как дата`. Форматы дат: ISO (`2024-01-01 + 30 дней`), или через +точку/слэш с 4-значным годом — `31.12.2024`, `12/31/2024` — порядок день/месяц +для неоднозначных случаев берётся из настройки «Формат даты». `дней до 2027-01-01`, `дней с 2020-03-15`. **Поиск неизвестного** — поставь `?` на место неизвестного и `=` с нужным результатом: @@ -112,9 +115,13 @@ AAPL в USD → $213.55 - **Папка данных настраивается** — укажи OneDrive/Dropbox и листы синхронизируются между машинами. - **Поверх всех окон** (в настройках) закрепляет окно над остальными. -- Приложение само проверяет обновления при запуске и предлагает установить — - вручную скачивать и переустанавливать не нужно. -- Светлая/тёмная тема, интерфейс en/ru, настраиваемая точность и разделители. +- Приложение проверяет обновления при запуске и потом каждые несколько + часов, предлагая установить — вручную скачивать и переустанавливать не + нужно. Настройки → Обновления → «Проверить обновления» запускает проверку + вручную, либо отключи автопроверку целиком тумблером рядом. +- Светлая/тёмная тема, интерфейс en/ru, настраиваемая точность, разделители + и формат даты (системный, `31.12.2024`, `12/31/2024` или `2024-12-31`). + Настройки разбиты на вкладки: Общие / Формат / Данные / Обновления. ## Расширения @@ -144,7 +151,7 @@ numi.addFunction({ id: "гипот", phrases: "гипот" }, ```bash npm install -npm test # тесты движка (113 кейсов) +npm test # тесты движка (152 кейса) npm run tauri dev # запустить приложение npm run tauri build # собрать установщик ```