Skip to content

Latest commit

 

History

History
357 lines (235 loc) · 36.7 KB

File metadata and controls

357 lines (235 loc) · 36.7 KB

Ревью кода BibleNote

Дата: 2026-08-07 · Ветка: master · База: cc5ce34 Охват: Api (18 .NET-проектов), Web (Electron/TS + ui/*.js), Site, плюс текущий незакоммиченный дифф.

Статус сборки на момент ревью: npx tsc --noEmit — чисто. npm testкрасный: tests/cache-ui-assets.test.ts:41 падает, потому что правка в ui/pdf-viewer.js изменила строку исходника, которую тест сравнивает буквально. Это не случайность, а прямое следствие проблемы №4.


История обработки замечаний

Статус на 2026-08-11. Исходный текст ревью ниже сохранён без переписывания: номера и формулировки описывают состояние на базе cc5ce34, а этот раздел — результат последующей работы в незакоммиченном рабочем дереве.

# Статус Результат
1 Выполнено в согласованном объёме Удалены старая Angular/ASP.NET-оболочка, Middleware, Persistence, их runtime-регистрации и относящиеся к ним тесты старого UI.
2 Не выполнено как архитектурный рефакторинг Провайдеры не схлопывались, startCacheUi не разбивался на роутер и файлы маршрутов.
3 Частично, точечно В модули вынесены только изолированные части, для которых уже была практическая причина. Массовой миграции ui/*.js нет.
4 Частично, по затронутой функциональности Исправлены мешавшие работе source-scraping тесты и добавлены поведенческие проверки. Остальные такие тесты намеренно не переписывались массово.
5 Выполнено Bridge вынесен в отдельный файл, протокол поиска структурирован, входящие сообщения ограничены источником.
6 Не выполнено, сознательно оставлено Текущий механизм SQLite-миграций не заменялся на PRAGMA user_version.
7 Выполнено Чтение метаданных и тяжёлых тел страницы разделено на отдельные запросы.
8 Выполнено частично 25 копий проверки заменены одним общим guard; политика отсутствующего Origin сохранена.
9 Выполнено Ошибочно закоммиченные Windows cache DB удалены, *.db добавлен в ignore.
10 Частично выполнено Нормализация API URL, документация staging и ignore для DB исправлены; Site/ и CSP не трогались.
Выполнено Три отдельно описанных ниже дефекта PDF/теста исправлены.

Подробности по пунктам

1. Старая .NET/Angular-ветка

Сделано:

  • удалён Api/Application/ClientApp со старой Angular-оболочкой;
  • удалены AnalysisSessionsController, NavigationProvidersController и проект CommandsHandler;
  • из Application.csproj убраны SPA/MSBuild-таргеты, NSwag codegen-таргет и пакеты AutoMapper, ElectronNET, MediatR, SpaServices и NewtonsoftJson, которые обслуживали старую оболочку;
  • из Startup убраны SPA static files/proxy, старый Electron bootstrap, MediatR/AutoMapper wiring и синхронный InitDatabaseAsync при старте;
  • удалены устаревшие electron.manifest.json, nswag.json и статическая копия OpenAPI specification;
  • удалены проекты Api/Middleware и Api/Persistence, ссылки на них из solution, Application и тестов;
  • удалены DB/analyzer/navigation-тесты старого UI и их DbTestsBase, но сохранён 101 актуальный тест парсинга, HTML и OneNote document provider;
  • из ServicesModule убраны DB-зависимые регистрации analyzer/save-processing, а из OneNoteModule — регистрация старого navigation provider;
  • удалён неиспользуемый NSwagSchemaNameGenerator с остаточными ссылками на namespace BibleNote.Middleware;
  • из launch settings удалён профиль Electron.NET App, из-за которого обычный dotnet run пытался запустить отсутствующий electronize;
  • README больше не требует -p:GenerateCode=False для обычной .NET-сборки.

Сначала Middleware/Persistence были оставлены из-за прямых ссылок старого Api/Tests. После уточнения, что эти компоненты обслуживали прежний UI, тесты были разделены по назначению: тесты актуального парсинга сохранены и отвязаны от MediatR/EF, а тесты удалённой DB/analyzer/navigation-ветки удалены вместе с ней. Api/Infrastructure не входит в это удаление: он не имеет потребителей, но его окончательная судьба требует отдельного явного решения.

2. Гранулярность проектов и cache-ui.ts

Предложенное объединение .NET-провайдеров и полное разбиение startCacheUi на router/middleware/routes не выполнялись. Это широкий рефакторинг с большим диффом и высоким риском незаметно изменить маршрутизацию, состояние синхронизации или packaging. Отдельные локальные проблемы из этого блока исправлялись без предварительного архитектурного переворота: общий origin guard и специализированные запросы к кэшу описаны в пунктах 7 и 8.

3–4. UI-модули и тесты исходного текста

Массового перевода классических скриптов из index.html на ES-модули не было. Точечно сделано следующее:

  • чистая логика сопоставления PDF-ссылок вынесена в Web/ui/pdf-link-matching.js и импортируется как обычный модуль;
  • iframe bridge вынесен в Web/ui/page-frame-bridge.js;
  • тесты PDF-сопоставления вызывают экспортированные функции на входных данных вместо вырезания их текста;
  • проверки строкового тела bridge удалены или заменены браузерными сценариями поиска, Ctrl+F, навигации и очистки подсветки;
  • фиксированное ожидание waitForTimeout(1200) в PDF E2E убрано в пользу ожидания наблюдаемого состояния.

Остальные source-scraping тесты сохранены. Принято правило репозитория: не переписывать их массово и не переводить UI на ES-модули только ради тестируемости; заменять конкретный тест поведенческим при изменении соответствующей функции или когда он блокирует полезный рефакторинг.

5. Bridge HTML-frame

Bridge больше не собирается массивом экранированных строк внутри api-page-view.js. Он поставляется как отдельный UI asset page-frame-bridge.js и подключается в srcdoc внешним script.src.

Дополнительно:

  • HTML-frame получает структурированный запрос { query, mode, caseSensitive, terms }, а не произвольные regex source/flags;
  • regex строится внутри frame с экранированием пользовательского текста; raw regex разрешён только для явного режима regex, набор флагов формируется кодом;
  • обработчик сообщений принимает команды только при event.source === parent;
  • event.origin здесь не используется как граница доверия: sandboxed srcdoc без allow-same-origin имеет opaque origin (null), поэтому корректной проверкой является идентичность окна-родителя;
  • добавлен E2E-сценарий, в котором соседний iframe пытается отправить управляющее сообщение и не влияет на целевой frame.

PDF-протокол поиска оставлен прежним, чтобы не смешивать этот рефакторинг с изменением PDF viewer.

6. Версии SQLite-схемы

Переход с проверок колонок на массив миграций и PRAGMA user_version не выполнялся по явному решению оставить текущий механизм. Причины: изменение затрагивает открытие всех существующих пользовательских БД, требует проектирования baseline для уже частично мигрировавших схем и политики downgrade. Польза сейчас в основном диагностическая, а цена ошибки — невозможность открыть или корректно обновить локальный кэш.

Возвращаться к этому пункту стоит только как к отдельной задаче с копиями реальных старых БД, тестами обновления с каждой поддерживаемой схемы и заранее определённым поведением при запуске старой версии приложения на новой БД.

7. Запросы к pages

Универсальный SELECT * FROM pages WHERE id = ? заменён специализированными функциями:

  • getCachedPageMetadata — метаданные без content_html и content_text;
  • getCachedPageContent — запрошенное тело (html, text или оба);
  • getCachedPageStatus и getCachedPageSyncState — узкие статусы.

Вызовы в cache-ui.ts переведены на минимально необходимую форму. Добавлены тесты, проверяющие, что метаданные и статус можно читать без загрузки обоих тяжёлых тел и что обновление одних метаданных не переписывает FTS.

8. Origin guard

Повторяющиеся проверки удалены из отдельных обработчиков. isCrossOriginStateChangingApiRequest вызывается один раз до маршрутизации и по умолчанию покрывает POST, PUT, PATCH и DELETE под /api/. Добавлен отдельный тест на мутирующие методы, GET, не-API пути и fallback-порт.

Семантика намеренно сохранена совместимой: запрос без Origin допускается, запрос с чужим Origin отклоняется. Требование заголовка для всех запросов сломало бы локальные non-browser вызовы (в частности интеграционные/Ribbon сценарии). Таким образом, проблема дублирования и риск забыть guard в новом мутирующем API закрыты, но это не строгая политика «каждый запрос обязан иметь Origin».

9. Мусор репозитория

Четыре файла из Web/%SystemDrive%/ProgramData/Microsoft/Windows/Caches удалены из рабочего дерева, после чего удалён и весь оставшийся пустой каркас Web/%SystemDrive%. В корневой и Web .gitignore добавлен *.db, чтобы такой класс cache-файлов не попадал в Git повторно. Конкретный исторический процесс, создавший путь с буквальным %SystemDrive%, не найден; активных ссылок на этот путь в текущих скриптах не обнаружено.

10. Мелкие замечания

  • bibleParseConfigFromEnv теперь нормализует apiUrl один раз после применения overrides; повторные .replace(/\/+$/, '') в gateway удалены;
  • в Web/README.md описано, что release-команды сами выполняют staging vendor/BibleNote, vendor/node и Ribbon перед сборкой;
  • *.db добавлен в корневой и Web ignore;
  • Site/ остаётся untracked и в Site/_headers по-прежнему нет CSP. Это не исправлялось, потому что не установлено, является ли каталог исходником реального deployment target или локальным результатом публикации. Добавление CSP без проверки сайта также может заблокировать используемые им inline/resource сценарии.

Баги из незакоммиченного изменения

  • Сопоставление PDF occurrence: occurrenceIndex теперь задаёт приоритетного кандидата, после которого остаётся fallback на остальные свободные совпадения. Чистая логика находится в pdf-link-matching.js и покрыта прямыми тестами.
  • Фильтр pageNumber: null/undefined явно означают «страница не задана», а заданное значение сравнивается с текущей страницей без преобразования null в 0.
  • Красный source assertion в cache-ui-assets.test.ts удалён/заменён проверкой поведения. После соответствующей серии правок unit-набор снова был зелёным.

Проверки и ограничения

После серии изменений, относящихся к этому ревью, выполнялись:

  • npm.cmd run build — успешно;
  • npm.cmd test — 188/188;
  • npm.cmd run e2e:search — 5/5;
  • node --check Web/ui/page-frame-bridge.js — успешно;
  • сборка .NET-приложения и HTTP smoke для health/specification — успешно;
  • после удаления Middleware/Persistence: dotnet build BibleNote.sln — 0 ошибок, dotnet test Tests/Tests.csproj --no-build — 101/101, HTTP health — ok, OpenAPI — 13 маршрутов;
  • git diff --check — без ошибок (только предупреждения Git о будущей нормализации LF/CRLF).

Это результаты на момент выполнения соответствующих правок, а не утверждение о состоянии любых более поздних незакоммиченных изменений. Версия приложения не повышалась, installer/portable/release artifact не собирались. Изменения из этого раздела на момент записи истории не закоммичены.


Оглавление

# Проблема Область Приоритет
1 Мёртвая параллельная ветка: .NET + Angular + CQRS + EF Api Высокий
2 Инверсия гранулярности между стеками Api, Web/src Высокий
3 ui/*.js — полмегабайта в одном глобальном скоупе Web/ui Высокий
4 Тесты проверяют текст исходников, а не поведение Web/tests Высокий
5 Bridge-скрипт как массив строковых литералов Web/ui Средний
6 Схема БД: «проверь колонку → ALTER TABLE» вместо версий Web/src Средний
7 SELECT * по таблице с HTML-телами Web/src Средний
8 CSRF-проверка: 25 копий и fail-open Web/src Средний
9 Мусор в репозитории репозиторий Низкий
10 Мелочи разное Низкий
Баги в текущем незакоммиченном изменении Web/ui, Web/tests Высокий

1. Мёртвая параллельная ветка: .NET + Angular + CQRS + EF

Api/Application до сих пор несёт полноценную вторую оболочку продукта, которой никто не пользуется.

  • Api/Application/Startup.cs:41 поднимает AddSpaStaticFiles, UseSpa, прокси на http://localhost:4200, ElectronNET-окно.
  • Api/Application/Application.csproj:41: таргет PublishRunWebpack на каждом dotnet publish делает npm install --legacy-peer-deps и сборку Angular в ClientApp/ (20 .ts-файлов, Angular 22, TypeScript 6). Debug-сборка через DebugEnsureNodeEnv тоже дёргает npm install.
  • Реальный клиент (Web/src/biblenote-gateway.ts) обращается только к VerseParsing и OneNoteIntegration. Значит AnalysisSessionsController, NavigationProvidersController, весь Middleware (10 MediatR-хендлеров), Persistence (EF + миграции) и CommandsHandler — недостижимы из продукта.
  • При этом Api/Application/Startup.cs:117 на каждом старте выполняет dbContext.InitDatabaseAsync().GetAwaiter().GetResult() — синхронно блокирует старт ради БД, которую никто не читает.

Отсюда же MediatR 9.0.0 и FluentValidation 9.2.2 (2020 год) в проекте на .NET 10, и NSwagExe_Core31 в таргете NSwag 14.

Что сделать: удалить ClientApp, оба контроллера, Middleware, Persistence, CommandsHandler, пакеты MediatR / FluentValidation / AutoMapper / SpaServices / ElectronNET и оба MSBuild-таргета SPA. Останется Application (2 контроллера) → ServicesProvidersCommon/Domain. Это убирает ~6 сборок, npm-зависимость из .NET-сборки и необходимость флага -p:GenerateCode=False из README.

Если удалять пока рано — минимум вынести SPA-таргеты под Condition="'$(BuildSpa)'=='true'" и убрать InitDatabaseAsync из горячего старта.


2. Инверсия гранулярности между стеками

.NET переразбит: 18 сборок на ~15k рукописных строк — по проекту на каждый провайдер (Web.DocumentId, FileSystem.DocumentId, Html, Word, Pdf, …), при том что все они ссылаются на один Services. Дробление ради дробления: границы сборок не отражают ни независимость релизов, ни замену реализаций.

Web недоразбит: Web/src/cache-ui.ts — 2271 строка, из которых 533–2271 это одна функция startCacheUi с 64 проверками маршрута в линейной if-цепочке внутри одного замыкания. Плюс cache-search.ts (50KB), sync.ts (59KB), electron-main.ts (43KB). У cache-ui.ts 17 прямых внутренних импортов — это хаб без слоёв.

Что сделать:

  • .NET: схлопнуть провайдеры в один BibleNote.Providers с папками. Держать отдельными только те, у кого действительно чужая нативная зависимость (OneNote — COM-interop, Word/Pdf — тяжёлые пакеты).

  • Web: заменить if-цепочку таблицей маршрутов и разнести по файлам:

    src/http/router.ts          — сопоставление method+pattern → handler
    src/http/middleware.ts      — origin-guard, json-body, error→status, логирование
    src/http/routes/sync.ts     — ~8 маршрутов
    src/http/routes/inductive.ts
    src/http/routes/bible.ts
    src/http/routes/onenote.ts
    src/http/routes/system.ts
    

    Состояние (syncState, microsoftLoginState, SingleFlight) — в явный объект контекста, передаваемый хендлерам, вместо переменных замыкания.


3. ui/*.js — полмегабайта в одном глобальном скоупе

11 файлов подключаются классическими <script> без type="module" (Web/ui/index.html:474). Итог: 707 объявлений верхнего уровня живут в общем window.

  • bootstrap.js объявляет const app = ..., layout.js его использует — связь только через порядок тегов.
  • Отсюда защитные updateTreeScrollbar?.() (Web/ui/layout.js:19) — код страхуется от того, что зависимость ещё не загрузилась.
  • Манифест ассетов продублирован: scriptNames в Web/src/cache-ui-assets.ts:20 и <script src> в index.html. Они уже разошлись (pdf-viewer.js есть только в первом).
  • Ни ESLint, ни checkJstsconfig.json покрывает только src/**/*.ts. 500KB кода вообще без статической проверки.
  • Cache-Control: no-store на все ассеты + чтение всех файлов синхронно на импорте модуля.

Что сделать: перевести на нативные ES-модули (<script type="module" src="/ui/main.js">, import/export между файлами) — бандлер не нужен, Electron 42 это тянет. Дальше: allowJs + checkJs в tsconfig, JSDoc-типы на публичных функциях, ESLint с правилом no-undef. Манифест ассетов вывести из одного источника — либо генерировать список из index.html, либо отдавать директорию через whitelist по расширению.


4. Тесты проверяют текст исходников, а не поведение

Самая дорогая проблема, потому что она маскирует все остальные.

  • Web/tests/cache-ui-assets.test.ts — 1266 строк, 111 вызовов readFileSync, 37 извлечений вида indexOf('function ...') с последующим vm.runInNewContext над вырезанным куском исходника.
  • Ассерты вроде assert.equal(viewerScript.includes("if (activeFindKind === 'main' && mainSearch.key) return;"), true) (:41) — это тест на форматирование строки, а не на поведение. Он и упал.
  • assert.equal(html.includes('const app = document.getElementById'), false) (:16) — тест «строки нет в файле».
  • Та же техника в inductive-markings.test.ts и parallel-translations.test.ts.

Это не выбор, а следствие проблемы №3: раз ui/*.js не модули — импортировать функцию нельзя, остаётся вырезать её текстом. После перевода на ES-модули ~90% этих тестов заменяются обычными import { referencePattern } from '../ui/pdf-viewer.js' и проверками на входах/выходах.

Отдельно: в e2e добавился await page.waitForTimeout(1200) (Web/e2e/specs/notes/pdf-viewer.spec.ts:207), завязанный на новый таймер в 900 мс. Хардкод-сон под конкретную константу продакшн-кода — гарантированная будущая флака. Лучше expect(...).toHaveCount(1) с toPass() / ретраями.


5. Bridge-скрипт как массив строковых литералов

Web/ui/api-page-view.js:267–320 — программа примерно на 50 строк JS, собранная конкатенацией строк, с '<scr' + 'ipt>', экранированием \\\" в четыре слэша и вставкой через insertAdjacentHTML в документ с недоверенным OneNote-HTML.

Ни подсветки, ни синтаксической проверки, ни возможности протестировать, ни стек-трейсов при ошибке. Плюс внутри неё:

  • new RegExp(mainSearchSource, mainSearchFlags) (:290) — паттерн и флаги приходят из postMessage без валидации.
  • Ни одной проверки event.origin: grep event.origin ui/*.js даёт 0 совпадений, при том что все postMessage идут с targetOrigin: '*' (25+ мест).

Настройки Electron при этом сделаны правильно (contextIsolation:true, nodeIntegration:false, sandbox:true, setWindowOpenHandler → deny в Web/src/electron-main.ts:849 и :899), и iframe для обычных страниц идёт с sandbox="allow-scripts" (Web/ui/tree-pages.js:2132) — то есть периметр держится. Но модель «доверяем всему, что прилетело в message» держится только на этом периметре.

Что сделать: вынести мост в обычный файл ui/page-frame-bridge.js, подключать его в srcdoc через <script src="/ui/page-frame-bridge.js">. В обработчике message проверять event.source === parent. Вместо передачи сырых source/flags передавать структурированный запрос (строка + набор булевых опций) и собирать regex на стороне фрейма с экранированием.


6. Схема БД: «проверь колонку → ALTER TABLE» вместо версий

Web/src/cache-schema.ts:236–364 — 25 блоков вида if (!columns.some(c => c.name === 'x')) db.exec('ALTER TABLE ... ADD COLUMN x').

Работает, но:

  • нет номера версии схемы — в диагностике не видно, на какой ревизии база;
  • нет защиты от отката — старая сборка молча откроет новую базу;
  • бэкфилы данных вшиты внутрь проверки на отсутствие колонки (:241), то есть семантика миграции зависит от состояния схемы, а не от версии;
  • на каждом старте выполняется серия PRAGMA table_info.

Что сделать: массив миграций + PRAGMA user_version:

const migrations: Array<(db: Database) => void> = [ /* 1..N */ ];
const current = db.pragma('user_version', { simple: true }) as number;
if (current > migrations.length) throw new Error('Кэш создан более новой версией приложения');
for (let v = current; v < migrations.length; v++) {
  db.transaction(() => { migrations[v](db); db.pragma(`user_version = ${v + 1}`); })();
}

Заодно: в системе сейчас две независимые SQLite-базы с двумя разными механизмами миграций — EF-миграции в Api/Persistence и ручные ALTER в Web. После проблемы №1 останется одна.


7. SELECT * по таблице с HTML-телами

Web/src/cache-search.ts:7: getCachedPage = SELECT * FROM pages WHERE id = ?. Таблица pages хранит content_html и content_text — то есть любой запрос метаданных тянет весь HTML страницы.

Новый код это усугубляет: Web/src/cache-ui.ts:1525 вызывает getCachedPage ради одного поля content_html в маршруте, который до этого работал только с параграфами.

Что сделать: разделить на getPageMeta (явный список колонок без тел) и getPageContent(db, id, 'html' | 'text'). Проверить остальные вызовы getCachedPage — большинству тело не нужно.


8. CSRF-проверка: 25 копий и fail-open

Один и тот же блок скопирован 25 раз (Web/src/cache-ui.ts:856, 863, 875, 886, 922, …):

const ownOrigin = `http://${request.headers.host ?? `127.0.0.1:${options.port}`}`;
if (request.headers.origin && request.headers.origin !== ownOrigin) {
  return json(response, 403, { error: '...' });
}

Сейчас покрыты все 25 мутирующих маршрутов — проверено. Но:

  • 26-й маршрут добавится без неё, и это никак не проявится;
  • проверка fail-open — при отсутствии заголовка Origin запрос проходит.

Что сделать: после введения роутера (проблема №2) — один guard, применяемый ко всем не-GET маршрутам по умолчанию, с явным opt-out. Условие поменять на «Origin отсутствует ИЛИ совпадает с собственным» только если это действительно нужно для non-browser клиентов; иначе требовать заголовок.


9. Мусор в репозитории

Web/%SystemDrive%/ProgramData/Microsoft/Windows/Caches/*.db — 4 файла кэша Windows Shell, закоммичены в 9a48ba0. Какой-то скрипт использовал %SystemDrive% в контексте, где cmd-переменная не раскрывается, и создал каталог с таким именем прямо в репозитории.

git rm -r --cached "Web/%SystemDrive%" && rm -rf "Web/%SystemDrive%"

Найти и починить источник — вероятно, путь собирается в PowerShell или Node, где нужно $env:SystemDrive / process.env.SystemDrive.


10. Мелочи

  • bibleConfig.apiUrl.replace(/\/+$/, '') повторяется 11 раз в Web/src/biblenote-gateway.ts — нормализовать один раз при чтении конфига.
  • vendor/ в .gitignore, но dist:win от него зависит. Сборка из чистого клона невозможна без ручного прогона stage-скриптов — стоит явно описать в README или зафиксировать версии артефактов.
  • Site/ не в git вообще. Если это деплоймент-таргет, его надо закоммитить; в Site/_headers нет Content-Security-Policy.
  • .gitignore игнорирует *.sqlite и *.db-wal / *.db-shm, но не *.db.

Баги в текущем незакоммиченном изменении

ui/pdf-viewer.js:422 — сопоставление occurrence по индексу связывает разные тексты

Сервер (Web/src/html.ts:44) нумерует вхождения по порядку <a> внутри <section data-pdf-page> в кэшированном HTML. Клиент выбирает matches[occurrenceIndex] из совпадений regex по текстовому слою PDF. Это два разных текста: в текстовом слое «Ин 3:16» может встречаться и там, где в HTML ссылки нет. Тогда N-я ссылка ляжет не на своё место.

Плюс регрессия: matches.slice(i, i + 1) даёт ровно одного кандидата, и если он перекрыт диапазоном более длинной ссылки (occupied.some(...)continue), оверлей теряется полностью. До правки перебирались все совпадения и находилось свободное.

Предложение — оставить occurrenceIndex как приоритетную подсказку, но с откатом на прежний перебор:

const preferred = Number.isInteger(occurrenceIndex) && occurrenceIndex >= 0 && matches[occurrenceIndex]
  ? [matches[occurrenceIndex], ...matches.filter((_, i) => i !== occurrenceIndex)]
  : matches;
for (const match of preferred) {
  const start = Number(match.index) || 0;
  const end = start + match[0].length;
  if (occupied.some(range => start < range.end && end > range.start)) continue;
  occupied.push({ start, end });
  addBibleLinkRect(layer, overlay, match, link, mapped.positions);
  break;               // одна ссылка — один прямоугольник
}

ui/pdf-viewer.js:407 — хрупкий фильтр страниц

!Number.isInteger(Number(link.pageNumber)) пропускает undefined (Number(undefined)NaN), но null даёт Number(null) === 0Number.isInteger(0) === true → ссылка отфильтруется на всех страницах. Сейчас сервер всегда шлёт целое, но JSON-null тихо убьёт все оверлеи.

Явнее: link.pageNumber == null || Number(link.pageNumber) === renderedPageNumber.

tests/cache-ui-assets.test.ts:41 — красный тест

Надо либо обновить под новую строку, либо (правильнее) заменить на проверку поведения runTargetSearch с forceTarget.


Предлагаемый порядок работ

  1. Удалить мёртвую .NET/Angular ветку (проблема №1).
  2. Перевести ui/*.js на ES-модули (проблема №3).
  3. Переписать source-scraping тесты на нормальные (проблема №4).
  4. Разбить cache-ui.ts на роутер + маршруты (проблема №2), заодно закрыв №8.

Пункты 3 и 4 без пункта 2 сделать нормально не получится.