Журнал изменений
GitHubКаждое значимое изменение, релиз за релизом. Тот же файл публикуется в репозитории и переведён на все 17 языков.
Журнал изменений
Все значимые изменения career-ops-ui задокументированы здесь. Формат — Keep a Changelog, версии по Semantic Versioning.
Переводы: 🇬🇧 English · 🇪🇸 Español · 🇧🇷 Português · 🇰🇷 한국어 · 🇯🇵 日本語 · 🇨🇳 简体中文 · 🇹🇼 繁體中文 · 🇫🇷 Français · 🇵🇱 Polski · 🇺🇦 Українська · 🇩🇰 Dansk · 🇸🇦 العربية · 🇩🇪 Deutsch · 🇮🇹 Italiano · 🇹🇷 Türkçe · 🇮🇳 हिन्दी
Примечание о переводе. Файл полностью переведён на русский: тела всех записей переписаны на технический русский без англоязычных заглушек.
[1.134.1] — 2026-08-05
Усиление валидации — исправления, выявленные полным аудитом проекта.
Исправлено
successfactorsбольше не отбрасывает уже собранные вакансии при сбое в середине скана (регрессия из портирования dead-board-throw в v1.134.0) — в цикле его пагинации не былоtry/catch, поэтому сбой на странице 2 и далее (после успешной первой страницы) выбрасывал ошибку и терял всё уже собранное; а если этим сбоем был404(вышедший за диапазонstartrow),en-scannerна несколько дней помечал живого тенанта как мёртвого. Теперь поведение зеркалируетphenom/radancy: сбой на странице 0 всё ещё выбрасывает ошибку (мёртвая доска), но сбой на более поздней странице сохраняет частичные результаты.- Фильтр-чипы
#/scanтеперь управляются с клавиатуры (WCAG 2.1.1) — чипы фасетов (и чип «очистить») были элементамиspanс обработчиком клика, но безtabindex/роли, поэтому пользователи клавиатуры и скринридеров не могли до них добраться или переключить их. Теперь у них естьrole="button",tabindex="0",aria-pressedи активация по Enter/пробелу. - Три хардкоженные англоязычные строки теперь локализованы — подсказка бейджа доверия на
#/scan, заголовок столбца релокации на#/scanи заголовок столбца оценки на#/dashboardбыли обычными строковыми литералами, которые гейт паритета i18n не мог увидеть (они никогда не были ключами), поэтому во всех неанглийских локалях они оставались на английском. Теперь этоscan.trustTip+scan.col.reloc(2 новых ключа) и повторное использованиеtrack.col.score, со статической source-гардой.
[1.134.0] — 2026-08-05
Паритет с родительским career-ops v1.25.0.
Добавлено
- Новый источник сканирования: getManfred (
manfred) — общедоступный по всей доске фид испанских/европейских технических вакансий с публикуемыми зарплатами, изwww.getmanfred.com/api/v2/public/offers(без авторизации, закреплён по хосту + только HTTPS, полный каталог одним запросом). Источник + адаптер + CI-изолированный сьют (tests/sources-manfred.test.mjs); реестр теперь насчитывает 73 источника = 68 EN + 5 RU (ALL_ADAPTERS68). Появляется в фильтре Source на#/scanи на лендинге cvstart.org.
Исправлено
- Фид a16z Speedrun незаметно урезался до 50 вакансий (#2404) — фид ограничивает страницу 50 записями, но источник запрашивал
PER_PAGE = 100, поэтому пагинация останавливалась после первой страницы. Исправлено на 50. - Мёртвые доски теперь выбрасывают ошибку вместо того, чтобы читаться как «живые, но пустые» (#2379) —
cryptocurrencyjobs,phenom,radancy,successfactors: сбой запроса, при котором ни один запрос так и не завершился успешно, теперь выбрасывает ошибку (так что здоровье#/portalsи скан фиксируют реальный сбой), вместо того чтобы проглатывать его как пустой список; сбой в середине скана после хотя бы одного успеха сохраняет частичные результаты. - workable теперь использует публичный widget API (#5ab8425) — переключено на
apply.workable.com/api/v1/widget/accounts/<slug>, который отдаёт полный список вакансий крупного аккаунта за один запрос, поэтому крупные аккаунты больше не урезаются.
Примечания
- Не портировано (только для CLI или не зеркалируется в web-ui): перф-переписывание группировки заголовков в detect-reposts #2389; исправления Unicode-ключей компаний (собственная дедупликация трекера web-ui уже безопасна для не-латиницы);
scan --since;cv-facts; проход аудита шаблона CV / PDF;doctor; директива о недоверенном контенте в modes.
[1.133.1] — 2026-08-02
Исправлено
#/funded(Профинансированные компании) теперь отображает результаты — две ошибки приводили к тому, что таблица всегда показывала «нет профинансированных компаний», даже когда родительскийcompany-funded.mjsвозвращал полный список. (1) Вид читал результаты по ключуres.candidates, но родитель отдаёт их под ключомcompanies(каждый элемент —{ company, amount, round, funding: { sources: [{ source, url, observed_date }] } }); теперь клиент читает правильный ключ и сопоставляет реальную форму доказательств. (2) Таблица результатов передавала свои ячейки вUI.el('tr', {}, …)как varargs, ноUI.el(tag, attrs, children)принимаетchildrenкак один узел или массив, поэтому отрисовывался только первый столбец (Компания) — теперь ячейки передаются как массив. Проверено вживую в браузере: 11 компаний из четырёх фидов отрисовываются со столбцами Компания / Сигнал финансирования / Источник / Дата и рабочими ссылками на доказательства, ноль ошибок в консоли. Пустой проход теперь также показывает диагностику по каждому источнику, так что тихий новостной день можно отличить от заблокированного фида.- Регрессионные гарды в
tests/parity-routes-v1133.test.mjs: поддельный родительский скрипт теперь отдаёт реальную форму выводаcompanies(исходная фикстура зеркалировала неверную формуcandidates— именно поэтому баг и прошёл в релиз), плюс статические source-канарейки, проверяющие, чтоfunded.jsчитаетres.companies(никогдаres.candidates) и строит строки таблицы с дочерними элементами-массивом (+1 → 2144).
[1.133.0] — 2026-08-01
Добавлено
- Обнаружение профинансированных компаний (
#/funded, паритет с родителем #2117) — новый read-only вид, транслирующий родительский career-opscompany-funded.mjsчерезGET /api/company-funded: предварительно проверяемый (review-first) список недавно профинансированных компаний, обнаруженных из публичных, закреплённых по хосту фидов о финансировании (TechCrunch, PR Newswire, The Guardian, Hacker News). Релей запускает скрипт с--json --dry-run(JSON в stdout, без записи файлов), никогда не прокидывает пользовательский ввод в--sources(нет SSRF-поверхности сверх собственных фиксированных фидов родителя), несётllmRateLimitи запускается пользователем (кнопка Discover, никогда при монтировании). Новый модуль маршрутаserver/lib/routes/funded.mjs(32-й) +public/js/views/funded.js, в разделе Sourcing. - Еженедельный дайджест интервью (
#/interview-digest, паритет с родителем #2129/#2130) — новый read-only вид, транслирующий родительский zero-LLMweekly-digest.mjsчерезGET /api/interview/weekly-digest: механическая сводкаinterview-prep/sessions/*.md— с какими компаниями и на каких раундах вы проходили интервью на этой неделе, повторяющиеся компетенции и оценочные (best-effort) открытые пробелы. Опциональный диапазон?from=&to=прокидывается ТОЛЬКО если ОБА значения — валидныеYYYY-MM-DD; пустой диапазон — валидный дайджестavailable:true(не сбой). Добавлено вserver/lib/routes/interview.mjs+public/js/views/interview-digest.js, в разделе Analytics. - Оба релея следуют устоявшемуся fail-soft-контракту
{ available:false }(как/api/stats/lifetime), когда родительский скрипт отсутствует (CI, автономные установки), так что каждый вид показывает честную заметку. 26 новых i18n-ключей ×17 локалей; CI-изолированный сьютtests/parity-routes-v1133.test.mjs(+5 → 2143: успешный passthrough, прокидывание--from/--to, гарантия read-only--dry-runи путь fail-soft при ошибке скрипта).
Примечания
- Родительский career-ops продвинулся дальше v1.24.0: страница Follow-up Tracker Next.js-приложения web/ (#1422) и бэкенд-рендер PDF (#2182) — не портированы: у web-ui уже есть собственный релей follow-up (v1.117.0) и раннеры PDF, а лежащее в основе укрепление
followup-cadence.mjs(отклонение невозможных дат, null-safeaddDays, разбор legacy-буллетов) достаётся бесплатно через shell-out релей. Измененияset-status.mjs/tracker-utils.mjs— внутренние для CLI (хелперы кода выхода / блокировки) и не зеркалируются.
[1.132.0] — 2026-07-31
Изменено
- Подсистема рендеринга результатов
#/scanвынесена вpublic/js/lib/scan-results.js(погашение техдолга по контракту размера файла —public/js/views/scan.jsразросся до ~1254 строк). Подсистема —renderResults,buildChipRow, билдеры строк/фасетов, отрисовщики опций и зеркало реестраFALLBACK_SOURCES— переезжает в фабрикуwindow.ScanResults.create(ctx), замыкающуюся на объект контекста, который передаёт вид. Без изменений поведения — функции перенесены дословно, а их переменные замыкания перепривязаны наctx.*;scan.jsтеперь ~906 строк (запланирован второй проход извлечения к цели в 800 строк). - Новый браузерный регрессионный гейт —
tests/playwright-scan-filters.mjsзасевает эталонныйdata/last-scan.jsonи прогоняет каждый фильтр#/scan, проверяя точные счётчики строк, так что извлечение верифицировано на реальном поведении браузера. - Баннер README облегчён — длинная построчная narrative-стена «Последний релиз» по версиям упраздняется в пользу однострочного резюме + ссылки на полный журнал изменений (
CHANGELOG.ru.md).
[1.131.2] — 2026-07-31
Изменено
app.cssразделён на три упорядоченных таблицы стилей (техдолг по контракту размера файла — единый файл разросся до ~1990 строк, далеко за жёсткий лимит в 800 строк). Теперь этоapp.css(~672 — a11y, дизайн-токены/тема, sidebar, main, кнопки, content-shell),components.css(~595 — карточки, гриды, пагинатор, бейджи, таблицы, формы, лог/консоль, markdown, переключатель языка, chip-фильтр, баннер соединения) иoverlays.css(~737 — toast, шторка уведомлений, модалка, разное/адаптивность, RTL-зеркалирование[dir="rtl"], docs-fab, usage-hud), каждый в пределах жёсткого лимита.- Разрез непрерывный и в исходном порядке, поэтому каскад побайтово идентичен файлу до разделения;
index.htmlподключает все три как упорядоченные<link>. Без изменений поведения, разметки или i18n. - Тесты, проверяющие CSS, теперь читают конкатенацию через общий хелпер
tests/helpers/css.mjs::loadAppCss(). Новыйtests/css-modularization.test.mjsфиксирует разделение (файлы существуют · каждый ≤ 800 строк · порядок ссылок в index.html) → сьют 2138. Проверено вживую в браузере: все три таблицы стилей парсятся и их правила применяются.
- Разрез непрерывный и в исходном порядке, поэтому каскад побайтово идентичен файлу до разделения;
[1.131.1] — 2026-07-31
Исправлено
- Согласованность host-pinning адаптеров на двух источниках v1.130.0 (доработки по code-review, defense-in-depth; поведение для валидных входных данных не меняется):
- Адаптер
a16z-speedrun-talentтеперь повторно валидирует overrideapi:/a16z-speedrun-talent:вbuildEndpoint(HTTPS + точное совпадение хостаspeedrun-talent-network.com) и откатывается на канонический фид при сбое проверки — паритет с адаптеромcryptocurrencyjobs, так что значение с чужим хостом никогда не попадает в слот запроса (ранее полагался исключительно на гардassertSpeedrunUrlна момент запроса). Проверка точного хоста теперь единый экспортируемыйSPEEDRUN_TALENT_HOST_RE, общий для гарда и адаптера. - Парсер
cryptocurrencyjobs—cleanUrlтеперь использует тот же гард точного совпадения хоста, что иassertCryptocurrencyJobsUrlи override адаптера (былоendsWith, который принимал поддомены). Парсер никогда не более permissive, чем SSRF-гард: ссылка на элемент сsub.cryptocurrencyjobs.coотбрасывается. - +2 теста → сьют 2135.
- Адаптер
[1.131.0] — 2026-07-31
Добавлено
- CRM-доска со вкладками стадий на
#/tracker(портировано из вида/pipelineвеб-приложения родителя). Полоска funnel-чипов трекера и дропдаун статуса заменены на полосу вкладок стадий: вкладка All плюс по одной вкладке на каждый канонический статус — Evaluated · Applied · Responded · Interview · Offer · Rejected · Discarded · SKIP · Hired — каждая показывает живой счётчик за всю историю, включая стадии с нулевым счётчиком, чтобы всегда была видна вся воронка (вид CRM). Активная вкладка управляет фильтром; повторный клик по ней сбрасывает обратно на All. Строки сохраняют тон оценки, легитимность, возможности PDF и отчёта, а ячейка компании теперь показывает брендовый логотип, если логотипы включены (по умолчанию выключены → ноль дополнительных запросов).- Новый read-only маршрут
GET /api/tracker/stagesвозвращает канонический перечень стадий воронки (метки по порядку) + карту свёртки алиасов, источник —server/lib/states.mjs(templates/states.yml, со встроенным fallback) — так что клиент никогда не хардкодит whitelist статусов. Легаси-ответGET /api/trackerбез параметров не изменился (только{ rows }). - Новая чистая, покрытая юнит-тестами клиентская библиотека
public/js/lib/tracker-stages.jsраскладывает строки по стадиям сервера, устойчива к случайному markdown-жирному начертанию и локализованным алиасам (например,aplicado→Applied). Вкладки доступны (role tablist/tab, aria-selected, зона клика ≥44 px, счётчики в accessible name каждой вкладки). Новых i18n-ключей нет. Сьют — 2133 теста.
- Новый read-only маршрут
[1.130.0] — 2026-07-31
Добавлено
- Два новых источника сканирования, портированных из родительского career-ops v1.24.0 (in-process, без новых зависимостей; оба появляются в фильтре Source на
#/scanи на лендинге cvstart.org):- a16z Speedrun (
a16z-speedrun-talent, #2231) — общедоступный JSON-фид всей доски talent-network a16z Speedrun. Закреплён по хостуspeedrun-talent-network.com, только HTTPS, постраничная выборка с 0-индексацией и лимитом страниц, сквозная передачаq/конфигурации по компании, fail-soft. - Cryptocurrency Jobs (
cryptocurrencyjobs) — доска вакансий Web3cryptocurrencyjobs.co, забираемая через её публичный RSS 2.0-фид (без авторизации). Двухпроходное декодирование XML-сущностей, только удалённые вакансии, работодатель извлекается из хвоста заголовка"… at <Компания>". - Реестр теперь поставляется с 72 источниками = 67 английских + 5 русских (
ALL_ADAPTERS= 67 адаптеров англоязычных порталов).
- a16z Speedrun (
Исправлено
echojobs— гибридные вакансии остаются отличимы от удалённых (зеркалит родительский #2258). Регистронезависимый маркерhybridтеперь даёт"<Город> · Hybrid"(или простоHybrid, если города нет) иworkplaceType: 'Hybrid', вместо схлопывания вRemote.radancy— устаревшая разметка TalentBrew + транспорт JSON results-фрагмента (зеркалит родительский a3e6df9), под инжектируемымopts.fetchJson.
Примечания
- Не портировано — CLI-специфичные функции родителя. Обширная CLI/mode-поверхность career-ops v1.24.0 остаётся вне web-ui, который является вьюером + тонким write-through, а не хостом режимов: таблицы соответствия/юрисдикции, телефонная книга контактов + vCard, транскрипт-дебриф интервью / определение платформы звонка, установка статуса в ledger, запись исхода, двухпроходный триаж, jd-similarity, версионированная схема артефакта CV заявки, детектирование Playwright-MCP в doctor и
portals/fix-slugs.mjs. Изменения оркестрации скана, которые живут в родительскомscan.mjs— Playwright-сканер Interamt.de, полный проход iCIMS в режиме reverse-ATS, фильтр допустимости страны для удалённых вакансий, троттлинг DNS-lookup, дедупликация StepStonerltrи нормализованная колонка компании в истории сканирований — не применяются: web-ui запускает сканеры EN/RU in-process и не шеллится вscan.mjs. - Уже покрыто. Фикс свёртки акцентов в
role-matcher(#2209) был портирован в v1.127.0, так что здесь это no-op.
[1.129.1] — 2026-07-29
Исправлено
- Доработки по AI-ревью портов web из v1.128/v1.129 (все адвайзори, исправлено в корне): порядок грейда в
job-facets.js(явный модификатор теперь побеждает управленческое слово —Senior Engineering Manager→senior, былоlead); fallback вstates.mjsбольше не пиннится (успешное чтение мемоизируется, fallback возвращается без кэша — родитель, временно недоступный на буте, перечитывается при следующем вызове) +console.warnпри присутствующем, но битом файле;score-tone.js— строка без оценки нейтральна (muted), не красная;domainFromName()пропускает не-ASCII slug до/api/logo; +sanity-гард изоляции вtests/states.test.mjs. +4 теста → 2073.
[1.129.0] — 2026-07-29
Добавлено
- Фасет «Грейд» + колонка «Свежесть» в
#/scan— библиотекаjob-facets.jsиз v1.128.0 теперь вплетена в UI сканирования (раньше была только логика). Новый дропдаун Грейд раскладывает заголовок каждой вакансии на lead/staff/senior/mid/junior/intern (JobFacets.seniorityFromTitle) и автозаполняется тем, что реально есть в результатах (как фасет «Страна»); заголовки без слова-грейда всегда проходят. Сохраняется в сохранённых поисках, Сбросе и Применить. В таблице появляются колонка Грейд (бейдж) и zero-token колонка Свежесть (сегодня/Nдн, изJobFacets.daysSince). 12 i18n-ключей ×17, +3 теста → 2069.
[1.128.0] — 2026-07-29
Добавлено
- Четыре решения, портированные из собственного веб-приложения родителя (
../web/, Next.js) — переписаны на ванильном JS/ESM, без новых зависимостей: (1)server/lib/states.mjsчитаетtemplates/states.ymlвживую как канонический словарь статусов трекера (с CI-fallback) — устраняет ручной ре-синк whitelist каждый релиз; POST сворачивает алиасы (испанские/legacy) в канонический ярлык, funnel в GET группирует по каноническому статусу; (2) логотипы компаний на ATS-хостованных строках черезdomainFromName()(~90 бренд→домен) перед буквой-аватаром; (3)score-tone.js— 4-уровневый тон оценки (≥4.2/3.8/3.0 + letter-fallback); (4)job-facets.js— фасеты seniority/source/days. +21 тест.
Примечания
- Не портировано (только концепт): агентский слой действий родителя (
actions/registry.ts+api/assistant/route.ts) — чертёж на случай, когдаdocs-fabвырастет из help-чата в копайлот. Новых источников нет (реестр 70), изменений i18n/help нет.
[1.127.0] — 2026-07-29
Добавлено
- Три новых источника сканирования (паритет career-ops v1.23.0) — в реестре теперь 70 адаптеров (65 EN + 5 RU): Flowxtra (board-wide агрегатор без авторизации), VDAB (keyword-API фламандской службы занятости) и iCIMS (порталы
careers-<tenant>.icims.com, отдельно отjibeapply). Плюс Cursor вернулся в ростер CLI (parent #2115):cli-detectтеперь детектитcursor(10 инструментов), ростер восстановлен в help/README/config ×17.
Исправлено
- agenticjobs переведён с HTML-скрейпинга на REST API (#2167); Greenhouse восстанавливает город из
/offices, когдаlocation.name— только модель работы (#2104); паритет role-matcher (#1933/#2164/#2009: префикс MTS, базовый токенproduct, свёртка акцентов, несогласие sub-baseline).
Примечания
- Не портировано. Основная часть v1.23.0 — CLI/dashboard-поверхность, в которую web-ui не шеллится (batch-tailor, discover-ats, режимы NL/PT, темы PDF, Go-дашборд, updater/doctor); релейные скрипты не требуют правок. VERSION родителя → 1.23.0.
[1.126.1] — 2026-07-25
Исправлено
- Две точки дрейфа ростера CLI, которые пропустил ресинк v1.126.0 — (1) интро вкладки API keys в
#/config(config.providerModelNote, i18n ×17) перечисляло только 7 CLI — теперь Antigravity и Grok Build вставлены после OpenCode; (2) вторая строка сравнительной таблицы в help-гайде (×17) и собираемый в CI help сайта всё ещё содержалиInside Claude Code / Codex / Cursor / Gemini CLI— устаревший набор с Cursor — теперь полный ростер. Обе строки использовали разделители слэш/средняя точка, которые паттерны свипа v1.126.0 не покрывали. i18n-снапшот перегенерирован; сьют остаётся 1969.
[1.126.0] — 2026-07-25
Добавлено
- Вкладка AI CLI tools теперь детектит все 8 первоклассных CLI career-ops — ростер
#/configсинхронизирован с родительскимdocs/SUPPORTED_CLIS.md: вserver/lib/routes/cli-detect.mjsдобавлены Grok Build CLI (grok) и Kimi CLI (kimi), а Antigravity теперь сначала ищет канонический бинарникagy. Read-only-скан PATH теперь сообщает 9 инструментов; найденный бинарник по-прежнему никогда не запускается.
Изменено
- Ресинк документации с career-ops.org/docs — сверил каждую поверхность доков с живыми страницами родителя (прочитаны все 31). Канонический ростер AI-ассистентов (help ×17 + README ×17) теперь перечисляет 8 первоклассных CLI — Claude Code, Codex, OpenCode, Antigravity CLI, Grok Build CLI, Qwen Code, Kimi, GitHub Copilot CLI — плюс Gemini CLI (legacy-обёртка). Help-бандлы сохраняют структуру 29 H2 / 105 H3.
[1.125.4] — 2026-07-23
Изменено
- зависимости site (dependabot #151–#153) —
sharp0.34.5→0.35.3,svgo4.0.1→4.0.2,fast-uri(dev) 3.1.3→3.1.4 вsite/; сборка Astro зелёная, SPA/сервер не затронуты.
Примечания
- Парити-свип родителя (career-ops
37d17ec..254764a, после v1.22.0) — портировать нечего: guard неверной строки вset-status(#2108) — только CLI (в web-ui строка трекера выбирается явно в UI, и ни один маршрут не вызываетset-status.mjs); Risk Summary в локализованных режимах (#2109) — файлыmodes/<lang>/, которые web-ui не читает (только верхнеуровневыеmodes/*.md); проверка манифеста вupdate-system(#2111) — только апдейтер; остальное — доки родителя (турецкий README, SIGNATURES ×4, SCRIPTS.md, акценты es). VERSION родителя остаётся 1.22.0 —parentVersionбез изменений.
[1.125.3] — 2026-07-23
Исправлено
- LLM-промпты на датском и хинди отвечали по-английски (сообщено пользователем) —
LOCALE_NAMESи все пять наборовSCAFFOLD_STRINGSвserver/lib/prompts.mjsтак и не были расширены дляdaиhi, поэтомуresolveLocale()откатывался вen, и каждый AI-промпт — deep research (живой и ручной), режимы, оценка, интервью, нетворкинг, CV Studio — терял директиву# Output languageв этих двух локалях. Теперь обе — полноправные: языковая директива + локализованная обвязка. Регрессионный гейт вtests/locale-scaffold.test.mjsтеперь обходит канонический список из 17 локалей вместо захардкоженных 12, а новый структурный гейт роняет любой ключ обвязки, падающий в английский — локаль, пропустившаяprompts.mjs, больше не выйдет (+12 тестов, всего 1969).
[1.125.2] — 2026-07-22
Исправлено
- Deep research через Gemini: HTTP 502 (
MALFORMED_FUNCTION_CALL) (#145, вклад @Alien10140) — живой промпт/api/deepвелел модели «Use WebFetch / WebSearch» и сохранить бриф в файл, но у безинструментальных API-провайдеров нет канала инструментов; Gemini отвечал вызовом функции вместо текста, что проявлялось как пустой HTTP 502.buildDeepPromptиbundleProjectContextполучили флагheadless: живые запуски (Anthropic/Gemini/каскад фолбэков) получают промпт без инструментов, который пишет бриф из встроенного контекста, а copy-paste-промпт для Claude Code сохраняет инструкции по инструментам. +1 тест вtests/critical-fixes.test.mjs.
Изменено
- Дефолтные модели Gemini обновлены с устаревшей
gemini-2.0-flash(#144, вклад @Alien10140) — выпадающий список в Конфигурации, серверный фолбэк вgemini.mjs(молча расходившийся с подсказкой), цепочка фолбэков OpenRouter,config.geminiModelHint×17 и справка ×17 теперь единообразно указываютgemini-3.6-flash. Новый анти-дрейф гейтtests/gemini-default-model.test.mjs(+5 тестов) фиксирует все поверхности на одном литерале — в сьюте теперь 1957 тестов.
[1.125.1] — 2026-07-21
Исправлено
- SuccessFactors: мультибрендовые тенанты RMK сохраняют свой путь бренда (родитель #2099, после v1.22.0) — холдинговые компании, которые ведут несколько приобретённых брендов на одном общем инстансе RMK, различают их по сегменту пути (
careers.nemetschek.com/Bluebeam/против…/Vectorworks/); адаптер ранее схлопывал настроенный URL до его origin, незаметно сканируя вакансии родительского бренда. Теперь эндпоинт сохраняет префикс бренда, отбрасывая только завершающий сегмент/search/или/tile-search-results/, чтобы ничего никогда не задваивалось само на себя; тенанты с одним доменом остаются побайтово неизменными. Новый экспортируемый хелперresolveTenantBase+ 1 перенесённый тестовый блок вtests/sources-successfactors.test.mjs.
[1.125.0] — 2026-07-21
Добавлено
- cvstart.org: раздел лендинга «Источники вакансий» — новый раздел между скриншотами и сравнением, который перечисляет все 67 источников сканирования в виде кликабельных чипов (62 англоязычные доски/ATS + 5 российских досок под отдельным подзаголовком), каждый из которых ведёт на публичный сайт источника. Список синхронизируется с живым реестром адаптеров при сборке (
sync-assets.mjs→facts.sources), поэтому он никогда не может разойтись с приложением; курируемая карта ссылок вSources.astroзащищена новым тестомtests/site-sources.test.mjs. В навигации шапки появился якорь Sources; добавлено 4 новых ключа i18n сайта ×17. Также исправлен списокinLanguageв JSON-LD лендинга, в котором всё ещё не хваталоhi.
[1.124.0] — 2026-07-21
Добавлено
- Пять источников сканирования (паритет с родительским career-ops v1.22.0, #1808/#1572/#2024/#2055) — Welcome to the Jungle (JSON API по всей доске вакансий), Agentic Engineering Jobs (доска вакансий по agentic/AI-инженерии), Jobvite (ATS без авторизации, на тенант), Gem (ATS на тенант) и Alibaba Group (JSON API карьерной страницы, паттерн Meituan/Tencent). Каждый источник — это закреплённая по хосту, CI-изолированная пара «источник + адаптер»; реестр теперь поставляется с 67 адаптерами (62 EN + 5 RU); фолбэк выпадающего списка Source на
#/scanи его дрифт-гейт обновлены; пять новых наборов тестовtests/sources-{wttj,agenticjobs,jobvite,gem,alibaba}.test.mjs.
Исправлено
- Arbeitsagentur: общенациональная удалённая работа определяется только при
homeofficetypравномVOLLSTAENDIG(родитель #1981) — запросhomeoffice=nv_trueтакже возвращает гибридные вакансии, поэтому проверка удалённого формата теперь подтверждает каждое совпадение через эндпоинт деталей вакансии небольшими пакетами и при ошибке выборки закрывается безопасно (оставляет реальный город вакансии, чтобы фильтры по локации по-прежнему применялись). - SmartRecruiters: публичные URL вакансий формировались без
/postings/(родитель #2047) — ссылки теперь ведут на публичную страницу вакансии вместо 404 для тенантов, чей публичный сайт опускает этот сегмент.
Примечания
- Родитель v1.22.0 также выпустил изменения на стороне CLI, в которые веб-интерфейс не заходит через shell или которые уже покрыты иначе: шаблон CV для zh-CN и типографика PDF, режим
/expand, доработки кеширования промптов у провайдеров (Gemini/OpenAI/Ollama), пошаговая разбивка токенов (у веб-интерфейса свой счётчик использования), сериализация записи трекера через writer-lock (веб-интерфейс проводит записи черезwithFileLockс версии v1.21), флаги CLIvisa_filterи абсолютной даты публикации для скана (у веб-интерфейса свой фильтр «Опубликовано за» по возрасту), а также посев дедупликации по увиденным источникам (сканер веб-интерфейса ведёт собственную дедупликацию истории сканирований).
[1.123.0] — 2026-07-17
Добавлено
- Источник сканирования Oracle Recruiting Cloud (паритет с родительским career-ops v1.21.0, #1929) — REST API
recruitingCEJobRequisitionsкарьерных сайтов Oracle Fusion/ORC без авторизации (JPMorgan Chase, Oracle, BNY Mellon, American Express, Honeywell и др.): хост закреплён по паттерну*.fa[.<region>][.ocs].oraclecloud.com, номер сайта определяется изcareers_urlкаждой отслеживаемой компании, постраничная выборка по offset с жёстким лимитом числа страниц и WAF-совместимые «браузерные» заголовки. Реестр теперь поставляется с 62 адаптерами (57 EN + 5 RU); фолбэк выпадающего списка Source на#/scanи его дрифт-гейт обновлены; новый CI-изолированный наборtests/sources-oraclecloud.test.mjs.
Исправлено
- Детектор повторных публикаций: базовые заголовки больше не смешиваются со «специализированными» вариантами-соседями (родитель #1922) — «Senior Analytics Engineer» больше не группируется с «Senior Analytics Engineer, People Analytics»: если токены одного заголовка являются строгим подмножеством токенов другого, а лишний токен — настоящая специализация (а не базовое слово), обе вакансии считаются отдельно публикуемыми позициями. Пометки о повторных публикациях («(Repost)», «relisted») теперь исключаются как метамусор (стоп-слова). +2 проверки в
tests/detect-reposts.test.mjs.
Примечания
- Родитель v1.21.0 также выпустил изменения на стороне CLI, в которые веб-интерфейс не заходит через shell или которые уже покрыты иначе: предупреждение о повторной подаче в ту же компанию (у веб-интерфейса своя логика кулдауна повторной подачи с v1.84.0), флаги
--format/--reportдля сопроводительного письма, режимы промптов для интервью (red-flag / panel-intel / письмо о неявке), сохранение trust-сигналов сканирования и здоровья порталов (веб-интерфейс использует собственный in-process сканер сtrust-validatorи страницу здоровья порталов), а также расширения статистики/salary-gap (транслируются только для чтения, в fail-soft режиме).
[1.122.0] — 2026-07-16
Добавлено
- Хинди (हिन्दी) — 17-й язык — полный словарь интерфейса (~1110 ключей), полное встроенное руководство справки (паритет 29 H2 / 105 H3),
README.hi.md, новыйCHANGELOG.hi.md(начинается с v1.122.0, по образцу de/it/tr), лендинг cvstart.org и страницы Methodology/License/Changelog/Help, переключатель языка (🇮🇳), автоопределение языка браузера и локализованный скриншот дашборда. Теперь каждый паритетный гейт «×16» проверяется как «×17»: паритет словаря i18n и снапшот, гейты H2/H3 справки, паритет CHANGELOG,check-i18nсайта и локальный прогон Playwright.
[1.121.0] — 2026-07-16
Добавлено
- cvstart.org: страницы Methodology, License и Changelog — лендинг получил три новых раздела на всех 16 языках рядом с существующим блоком Compare: /methodology/ (шкала оценки из шести параметров 0.0–5.0, порог отклика 4.0 и правила «никогда не делает» — локализованное резюме career-ops.org/methodology), /license/ (канонический текст MIT со ссылкой на NOTICE.md) и /changelog/ (этот файл, отрисованный по локали из 16 переведённых CHANGELOG репозитория). Новый пункт «Методология» в шапке и ссылки в футере «Ресурсы»;
sync-assets.mjsтеперь синхронизирует CHANGELOG ×16 и LICENSE в сайт на этапе сборки, чтобы страницы никогда не расходились с репозиторием. - Ссылки на методологию по всей документации — README (все 16), канонический список §1 встроенного гида справки (все 16) и вики теперь ссылаются на career-ops.org/methodology (а также FAQ и глоссарий) наряду с существующими руководствами career-ops.org/docs.
Изменено
- Обновлены баннер релиза и бейджи в README (1850 тестов, релиз v1.121.0) — баннер всё ещё анонсировал v1.119.5.
[1.120.0] — 2026-07-16
Добавлено
- Манифест CareerOps (паритет с родителем v1.20.0) — родительский проект выпустил Манифест CareerOps (
MANIFESTO.md· career-ops.org/manifesto) и теперь показывает его в своём README, апдейтере и Go-дашборде. Веб-интерфейс следует за ним: новая ссылка в футере боковой панели открывает страницу манифеста (новый ключ i18nfooter.manifestoво всех 16 локалях), встроенный гид справки получил §29 «Манифест CareerOps» на всех 16 языках, README объясняет, что такое манифест и как его подписать, а футер лендинга cvstart.org тоже ссылается на него.
Примечания
- Родитель v1.20.0 также исправил подавление известных навыков в целевом режиме
upskill, заглушил dotenv, чтобы stdoutscan --jsonоставался парсируемым, и исправил HTML-шаблон CV, чтобы заголовок роли не отрывался от своих пунктов — это поверхности на стороне CLI, в которые веб-интерфейс не заходит через shell; изменений кода веб-интерфейса не потребовалось.
[1.119.5] — 2026-07-13
Исправлено
- Кнопка языка на лендинге больше не переносится — после появления флагов в v1.119.2 подпись переключателя в шапке (например «🇷🇺 Русский») могла разбиваться до трёх строк на узких десктопных ширинах; подпись переключателя и все пункты выпадающего списка теперь
whitespace-nowrap— флаг + эндоним всегда в одну строку. Список языков в футере переведён с жёсткой двухколоночной сетки на переносимый ряд однострочных пунктов — «🇧🇷 Português (Brasil)» тоже больше не рвётся посреди названия.
[1.119.4] — 2026-07-13
Изменено
- LICENSE называет автора — строка копирайта теперь: Sergei Emelianov (Fighter90) https://sergey-cv.com and career-ops-ui contributors (канонический текст MIT не тронут). Новый NOTICE.md подробно расписывает лицензирование: кто держатель копирайта, что именно покрывает грант MIT (код, доки, переводы, лендинг, вики), что НЕ покрывает (ваши данные в рантайме, родительский проект, контент джоб-бордов, товарные знаки), таблицу сторонних компонентов (express/js-yaml — MIT; Astro/Tailwind — MIT; шрифты Figtree и JetBrains Mono — SIL OFL 1.1; sharp — Apache-2.0) и необязательную строку атрибуции.
[1.119.3] — 2026-07-13
Добавлено
- SECURITY.md — политика безопасности, на которую ссылался CONTRIBUTING, теперь существует: поддерживаемые версии, приватный процесс репортов (в репозитории включён private vulnerability reporting GitHub — вкладка Security → «Report a vulnerability»), модель угроз для localhost-приложения одного пользователя (в скоупе: XSS через враждебные вакансии / SSRF / path traversal / утечки секретов / ослабление CSP; вне скоупа: DoS собственного localhost и проблемы родительского проекта) и базовая линия харденинга для ревьюеров.
[1.119.2] — 2026-07-13
Добавлено
- CONTRIBUTING.md — гид контрибьютора, на который лендинг и README ссылались всё это время, теперь существует: установка, карта проекта, жёсткие правила безопасности/no-build, уровни тестирования, walkthrough «двух реестров» для добавления скан-источника, контракт i18n ×16, конвенции коммитов/PR и процесс релиза.
- Флаги языков на лендинге — переключатель языков cvstart.org, сетка языков в футере и баннер «читайте на своём языке» теперь показывают флаг каждой локали рядом с её эндонимом (тот же набор региональных индикаторов, что и в языковом
<select>приложения; деградирует до букв региона там, где нет глифов флагов). - Правки футера лендинга — нерабочая ссылка Discussions (функция не включена в репозитории) теперь ведёт на вики проекта, а в футере указан автор: Sergei Emelianov (sergey-cv.com · LinkedIn).
[1.119.1] — 2026-07-13
Исправлено
- Фильтр источников на
#/scanдогнал реестр — статический списокFALLBACK_SOURCESза выпадающим списком Source (используется только когдаGET /api/scan/sourcesнедоступен) молча отставал с v1.87.0: в офлайн-фолбэке не хватало 20 провайдеров (Amazon, Avature, SAP SuccessFactors, Get on Board, Dassault Systèmes, beesite, HigherEdJobs, JibeApply (iCIMS), softgarden, Cornerstone, Phenom, Radancy, Deutsche Bahn, EchoJobs, TKMS, Heckler & Koch, Rheinmetall, LaraJobs и новые Meituan / Tencent). Синхронизирован со всеми 61 и теперь закрыт дрифт-тестом, который валит CI при расхождении клиентского списка с серверным реестром (значения И лейблы). +1 тест (1845).
[1.119.0] — 2026-07-13
Паритет с родительским career-ops v1.19.0 + обновление лендинга cvstart.org.
Добавлено
- 2 новых сканер-провайдера — Meituan (
zhaopin.meituan.com) и Tencent (careers.tencent.com): публичные JSON-API китайских тех-досок без авторизации, определяются по хосту или явнымprovider:, серверный поиск по ключевым словам с пагинацией и дедупликацией по URL — теперь 61 адаптер (56 EN + 5 RU). +20 тестов (1844). - Блок контрибьюторов на лендинге — cvstart.org показывает аватары всех, кто внёс код (GitHub API
/contributorsна этапе сборки, боты отфильтрованы), локализовано на все 16 языков, со ссылкой на полный граф контрибьюторов. - Живой счётчик звёзд GitHub на лендинге — бейдж в шапке теперь обновляется на клиенте из GitHub API при каждом визите (снимок этапа сборки — как fallback), а еженедельная пересборка Pages по расписанию не даёт устареть снимку и списку контрибьюторов; API-вызовы в CI аутентифицированы токеном.
Исправлено
- Запросы Workday CXS несут браузерные заголовки (родительский #1813) — тенанты за Cloudflare (наблюдалось вживую: geico) отвечают 500 на запросы без обычных UA/
accept-language/origin/referer; фетчер теперь выводит origin и слаг сайта из самого CXS-URL. Запросы Glints получили тот же браузерный UA + origin/referer — оба из общей константыBROWSER_LIKE_USER_AGENTвhttp-json.mjs.
[1.118.4] — 2026-07-10
Исправлено
- Скан hh.ru давал 0 вакансий с российского IP (ссылки на региональном поддомене) — с российского резидентного IP hh.ru редиректит поиск на региональный поддомен (
sochi.hh.ru,spb.hh.ru, …) и отдаёт ссылки на вакансии на этом поддомене. Парсер искал ссылку заголовка по жёсткому хостуhttps://hh.ru/vacancy/и не находил ни одной региональной — полностью рабочий скан молча записывал 0. Теперь принимается любой хост*.hh.ru(рекламаadsrv.hh.ru/click?…по-прежнему отсекается — у неё нет пути/vacancy/<id>), а URL результата канонизируется вhttps://hh.ru/vacancy/<id>. Проверено вживую: 17 реальных вакансий парсятся со страницыsochi.hh.ru, где раньше было 0. +1 тест (1824).
[1.118.3] — 2026-07-10
Исправлено
- hh.ru молча возвращал 0 вакансий (заглушка VPN-проверки) — hh.ru теперь отвечает 302-редиректом на заглушку
/vpncheeck(«VPN мешает работе сайта») сетям, которые считает VPN/прокси (датацентровые IP); заглушка отдаёт HTTP 200 без единой карточки вакансии, и скан сообщал 0 без какой-либо ошибки. Сканер теперь распознаёт редирект по финальному URL ответа, отключает hh.ru до конца прогона и пишет честную подсказку: трафик должен реально выходить через резидентный IP — системный VPN/прокси может оставаться включённым, даже когда переключатель в браузере выключен. +1 тест (1823).
[1.118.2] — 2026-07-10
Обслуживание
- Доводка лендинга (#118) —
site/README.mdприведён к Astro 7 (security-апгрейд из #116), убран неиспользуемый импорт, добавлены 4 исполняемых гарда для билд-скриптов лендинга: гейт i18n-паритета доказуемо падает на сломанном словаре, аsync-assetsникогда не пишет за пределыsite/— набор 1822. Разобраны два алерта CodeQL (один исправлен в коде, один задисмиссен как намеренное поведение билда).
[1.118.1] — 2026-07-10
Исправлено
- Скан hh.ru из-за пределов России — hh.ru теперь отдаёт HTTP 451 (региональная юридическая блокировка) не-российским IP на публичных страницах поиска. Сканер обрабатывает 451 как 403: после первой блокировки hh.ru отключается до конца прогона с честной строкой в логе про российский IP / VPN-выход, так что оставшиеся запросы и другие RU-источники не тратятся впустую. Справка §7 обновлена на всех 16 языках. +1 тест (1818).
[1.118.0] — 2026-07-09
Пакет паритета с родительским career-ops v1.18.0.
Добавлено
- 9 новых сканер-провайдеров — Cornerstone OnDemand (
csod), Phenom (phenom), Radancy (radancy), Deutsche Bahn (deutschebahn), EchoJobs (echojobs), TKMS (tkms), Heckler & Koch (hecklerkoch), Rheinmetall (rheinmetall), LaraJobs (larajobs) — теперь 54 адаптера. Адаптер Lever дополнительно распознаёт доски EU-тенанта (jobs.eu.lever.co). - Статус
Hiredв трекере (паритет соstates.ymlродителя): принятые офферы получают собственный канонический статус, праздничный бейдж и баннер «работа найдена» на#/tracker; воронка и конверсии считают его прошедшим все этапы. - Вкладка «За всё время» в
#/stats— ретрансляция только для чтения родительскогоstats.mjs(сводка трекера за всё время, накопленные доли воронки, итоги сканера, покрытие порталов) плюс наблюдения по компенсации изsalary-gap.mjs(желаемая vs заявленная vs фактическая, по каждой заявке). Новые роутыGET /api/stats/lifetimeиGET /api/stats/salary-gap— shell-out с нулевой стоимостью токенов, мягкая деградация{available:false}без родительского проекта. - 28 новых i18n-ключей во всех 16 локалях; справка §14/§26 обновлена на всех языках.
Тесты
- +38 юнит-тестов (три сьюта паритета провайдеров + роуты релея/статуса) — всего 1817.
[1.117.2] — 2026-07-06
Фикс пустого трекера для shell-out паритета. Родительские скрипты каденции/паттернов выходят с кодом 1 и структурированным {error}-JSON, когда в трекере ещё нет заявок; доска фоллоу-апов и вкладка «Паттерны отказов» показывали это как «script-error». Оба маршрута теперь передают это как здоровое пустое состояние (available:true, empty:true), и UI показывает честное «пока пусто». Проверено вживую на реальном родителе.
Новое: нет.
[1.117.1] — 2026-07-06
Донастройка v1.117.0 (разбор CodeQL). Три shell-out эндпоинта (GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns) теперь несут общий per-IP лимитер (каждый запрос порождает дочерний процесс; на loopback — no-op). Извлечение текста по URL в «Добавить в CV» вычищает теги до неподвижной точки и затем удаляет все оставшиеся </> — доказуемо полная зачистка для текста LLM-промпта. Поведение на корректном вводе не изменилось.
Новое: нет.
[1.117.0] — 2026-07-06
Пакет паритета с родителем — шесть возможностей родительского career-ops теперь в UI. (1) Доска каденции фоллоу-апов: страница #/followup открывается срочностью по каждой заявке (🔴 срочно / 🟠 просрочено / 🟡 ожидание / 🔵 остыло) из родительского followup-cadence.mjs, плюс кнопка «Проставить даты фоллоу-апов» (followup-seed.mjs --backfill). (2) Паттерны отказов: четвёртая вкладка Статистики запускает analyze-patterns.mjs (только чтение) — распределение исходов, рекомендации и доля продвижения по каждому ATS-вендору. (3) Добавить в CV: карточка CV Studio превращает URL проекта/публикации или вставленный текст в ATS-пункты, основанные ТОЛЬКО на этом источнике (только предложения, без записей; загрузка URL защищена от SSRF). (4) 4 новых сканер-провайдера — beesite (бэкенд jobs.mercedes-benz.com), HigherEdJobs (RSS), JibeApply (iCIMS), softgarden — реестр теперь насчитывает 50 адаптеров (45 EN + 5 RU), все в выпадающем списке Scan. (5) Шаг предпроверки нокаут-вопросов в чек-листе Apply (виза/степень/зарплатные пороги — до заполнения формы). (6) Раннер reconcile (/api/run/reconcile → reconcile-pipeline.mjs). Shell-out-маршруты деградируют честно: без родительских скриптов UI показывает «недоступно».
- Новый модуль маршрутов
server/lib/routes/followup.mjs(31-й) +GET /api/stats/patterns+POST /api/cv-studio/add-entry+ 8 файлов source/adapter + счётчики реестра 41→45 EN. Тесты:tests/sources-parity-v1117.test.mjs(6) +tests/parity-routes-v1117.test.mjs(7); сьют 1737 → 1750. 41 новый ключ i18n ×16. Справка §13/§17/§24/§26 расширена ×16.
Новое: GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns, POST /api/cv-studio/add-entry, POST /api/run/reconcile.
[1.116.0] — 2026-07-06
Переработан счётчик расхода + первый сквозной тест виджетов. Счётчик расхода ИИ (v1.114.0) закреплён правильно: теперь он прижат к низу левой боковой панели (на всю ширину панели, с той же поверхностью) и резервирует внизу место, равное своей высоте, чтобы меню никогда не перекрывалось — навигация и подвал версии всегда прокручиваются свободно над ним. Он обновляется вживую (каждые 15 с, плюс при фокусе вкладки и смене маршрута), а каждая строка окна теперь показывает реальные <токены> · <оценка стоимости> (полосы масштабируются относительно 30-дневного окна) вместо всегда-100% «доли». Кроме того: устойчивый барьер typeof в импортёре CV закрывает в первопричине повторяющийся ложноположительный type-confusion CodeQL, а новый сквозной тест Playwright прогоняет оба постоянных виджета в реальном браузере.
public/js/lib/usage-hud.js+app.css,server/lib/cv-import.mjs. Тесты:tests/playwright-widgets.mjs(2 E2E) +tests/usage-hud.test.mjs(10). Справка §6 расширена ×16.
Новое: нет.
[1.115.0] — 2026-07-06
Шлифовка дизайна (консервативная, коралловый бренд сохранён). Лёгкий проход по общей дизайн-системе — без перестройки, без смены палитры. Карточки метрик на дашборде теперь слегка приподнимаются и получают коралловую рамку при наведении (как плитки быстрых действий); карточки контента приподнимаются чуть-чуть; кнопки primary / dark / danger получают тень в покое и мягкий подъём при наведении для глубины; крупные числа выравниваются через tabular-nums; интерактивные элементы получают мягкий коралловый ореол за чётким 2px-кольцом клавиатуры. Вся анимация уважает prefers-reduced-motion, а ореол намеренно ограничен элементами управления — не глобальный *:focus-visible (иначе он перерисовал бы кольцо на заголовках маршрутов; урок v1.58.x).
- Только CSS (
public/css/app.css); без изменений разметки, i18n, маршрутов или CSP. Тесты:tests/design-polish-v1115.test.mjs(5). Проверено вживую через Playwright.
Новое: нет.
[1.114.0] — 2026-07-06
Счётчик использования и стоимости ИИ в боковой панели (левый нижний угол). Компактный раздел ИСПОЛЬЗОВАНИЕ теперь находится внизу боковой панели (фиксированная карточка в левом нижнем углу, если панели нет; в правом нижнем для RTL) на каждой странице. Он показывает использование токенов LLM по окнам 24ч / 7д / 30д — каждое как <токены> · <доля%> с зелёной полосой (доля от всего времени) — плюс подвал с оценкой стоимости за 24ч. Данные — доступный только для чтения свод GET /api/usage из data/llm-usage.jsonl (только локально), тот же источник, что и страница #/usage; стоимость оценочная, а запуски в ручном режиме бесплатны и не учитываются. Сворачивается — заголовок переключает, состояние сохраняется.
- Новый клиентский виджет
public/js/lib/usage-hud.js(window.UsageHud), загружается изindex.html, монтируется в боковую панель над подвалом версии (запасной вариант — фиксированный угол). Безопасен для CSP; с учётом темы и зеркалом RTL. Без нового маршрута сервера. Тесты:tests/usage-hud.test.mjs(8). 3 новых ключа i18n ×16.
Новое: нет.
[1.113.0] — 2026-07-06
Плавающий помощник «Спросить справку» на каждой странице. Кнопка-робот с чатом и градиентом теперь плавает в правом нижнем углу (в левом нижнем для RTL) каждой страницы. Нажмите, чтобы открыть компактный чат, который отвечает на вопросы по использованию, опираясь ТОЛЬКО на встроенную справку на вашем языке — тот же эндпоинт, что и страница #/docs-assistant (POST /api/docs-assistant/ask), так что он никогда не читает ваше резюме, профиль или трекер. Вживую с LLM-ключом; без ключа → готовый к запуску промпт. В шапке — аватар-робот и статус онлайн; чипы подставляют частые вопросы; Escape или клик снаружи закрывает; на самой странице #/docs-assistant кнопка скрыта.
- Новый клиентский виджет
public/js/lib/docs-fab.js(window.DocsFab), монтируется глобально изindex.html; безопасен для CSP; стили с учётом темы и зеркалом RTL вapp.css. Без нового маршрута сервера — переиспользует эндпоинт справки. Тесты:tests/docs-fab.test.mjs(8). 6 новых ключей i18n ×16 (fab.*+docs.err). Справка §1 расширена на месте.
Новое: нет.
[1.112.0] — 2026-07-06
Консолидация документации и QA. Без видимых пользователю изменений кода. Документ соглашений SDD (docs/sdd/CONVENTIONS.md) обновлён до текущих 30 модулей маршрутов (было 24) и текущей базовой линии тестов; определяющий общепроектный QA-промпт (qa/QA-REGRESSION-PROMPT.md) консолидирован — механика релиза актуализирована (v1.111, parentVersion 1.17.0, публикация по событию релиза), таблица дополнений §14 исправлена («Исключить» в Scan переразмечено как v1.109.0) и дополнена закрытием CodeQL из v1.111 — так что он самодостаточен как единый регрессионный промпт для всей функциональности. Добавлен один тест покрытия для ветки слишком большой загрузки.
Новое: нет.
[1.111.0] — 2026-07-06
Безопасность — закрытие бэклога CodeQL. Три усиления эшелонированной защиты, которые закрывают оставшиеся замечания статического анализа в первопричине, а не через dismiss. stripDangerousMarkdown теперь экранирует < у любого усечённого открывающего опасного тега (полезная нагрузка, заканчивающаяся на <script/<iframe/…), так что в выводе доказуемо не остаётся живого опасного тега. Импорт CV читает размер загруженного буфера через явное приведение Number() — барьер против путаницы типов. Строки роли режимов теперь являются шаблонными строками, интерполируемыми через String.replace, а не хранимыми функциями, что полностью убирает вызов динамической диспетчеризации. Видимого для пользователя изменения поведения нет.
server/lib/security.mjs,server/lib/cv-import.mjs,server/lib/prompts.mjs. Тесты:tests/security-hardening-v1111.test.mjs(7) + обновлён тест-страж v1108. Без изменений i18n/справки/маршрутов.
Новое: нет.
[1.110.0] — 2026-07-06
Обновление документации и QA (все языки). Без изменений кода. Полнопроектный QA-промпт обновлён до v1.109.0 с новым §14 (v1.98→v1.109), а вечнозелёные промпты UX-аудита и design-export получили текущий набор экранов. Каждый абзац справки, добавленный в v1.100–v1.109, теперь переведён на все 16 языков.
Новое: нет.
[1.109.0] — 2026-07-06
Фильтр «Исключить» в Scan + обзор пайплайна (паритет с веб-версией). На #/scan поле Поиск теперь трактует запятые как ИЛИ («роли для поиска»), а новое поле Исключить скрывает строки, где компания/роль/локация содержат любое из слов через запятую (напр. senior, staff); оба запоминаются в сохранённых поисках. На #/pipeline компактная полоса обзора показывает пайплайн с одного взгляда — N во входящих, N отслеживается и счётчики Applied / Responded / Interview / Offer из трекера, каждый чип ведёт в #/tracker.
- Только клиент (без нового маршрута/записей).
public/js/views/scan.js+public/js/views/pipeline.js. Тесты:tests/scan-pipeline-ui-v1109.test.mjs(2). 4 новых ключа i18n ×16. Справка §7 + §8 расширены на месте.
Новое: нет.
[1.108.0] — 2026-07-06
Усиление безопасности (разбор CodeQL, раунд 2). Исправлены ещё три находки низкой серьёзности: сборщик промптов разрешает строку роли локали по собственному ключу + typeof === function, чтобы подделанная локаль не могла вызвать метод прототипа (unvalidated-dynamic-method-call); slug имени PDF-файла ограничен 200 символами до регулярки, чтобы вход из одних дефисов не откатывался (полиномиальный ReDoS); импорт документа приводит массивный filename (повторный заголовок) к строке (type-confusion). Без изменения поведения для корректного ввода.
server/lib/prompts.mjs,server/lib/routes/runners.mjs,server/lib/cv-import.mjs+tests/security-hardening-v1108.test.mjs(3). За v1.106–v1.108 бэклог статического анализа сократился со 167 до ~14: все действительно значимые для безопасности находки исправлены, остальное (защищённые/санированные ложные срабатывания + lint уровня note) отклонено с обоснованием.
Новое: нет.
[1.107.0] — 2026-07-06
Усиление санитайзера (эшелонированная защита от XSS в состоянии покоя). stripDangerousMarkdown — нейтрализует опасный HTML в сохранённом markdown резюме/вакансии, чтобы любой потребитель, минующий клиент с экранированием при рендере, оставался в безопасности — теперь выполняет удаление тегов до неподвижной точки (повторять до стабилизации), чтобы удаление, переформирующее полезную нагрузку (напр. <scr<script></script>ipt>), было поймано, совпадает с закрывающими тегами script/style и т. п. с мусором в конце (</script foo>) и удаляет незакрытый исполняемый открывающий тег (<script …>). Поведение для корректного markdown не меняется — удаляет только больше.
server/lib/security.mjs: цикл неподвижной точки (ограничен 8 проходами) + шаблоны закрывающих тегов[^>]*>+ удаление незакрытого открывающего тега. +3 регрессионных случая вtests/cv-xss-bypasses.test.mjs. Авторитетная граница XSS остаётся экранированием на выводе (UI.md); это усиливает гарантию в покое и закрывает соответствующие находки CodeQL.
Новое: нет.
[1.106.0] — 2026-07-06
Усиление безопасности (разбор CodeQL). Исправлены три реальных (пусть и низкой серьёзности) находки из бэклога статического анализа: путь ошибки рендера маршрута теперь экранирует сообщение об ошибке до попадания в DOM (ошибка сервера может отразить пользовательский ввод, поэтому считается недоверенной — граница XSS), а записи свойств профиля/конфига отклоняют ключи __proto__ / constructor / prototype (защита от загрязнения прототипа на всякий случай — ключи берутся из фиксированных спецификаций полей, а не из сырого ввода). Большинство оставшихся предупреждений — ложные срабатывания на легитимных чтениях/записях сканера в data/* и на маршрутах, уже несущих собственный ограничитель; отклонены с обоснованием.
public/js/router.jsэкранируетerr.messageчерезUI.escapeHtmlпередinnerHTML;server/lib/routes/content.mjsиserver/lib/routes/config.mjsзащищают ключи прототипа. Без изменения поведения для корректного ввода. Тесты:tests/security-hardening-v1106.test.mjs(3). Новых ключей i18n нет.
Новое: нет.
[1.105.0] — 2026-07-06
Страница расхода и стоимости ИИ. Новая страница Расход ИИ (боковая панель, рядом со «Здоровьем») показывает, сколько токенов вы потратили на живые генерации ИИ — оценки, отчёты, чаты — с разбивкой по провайдерам за последние 24 часа, 7 дней, 30 дней и всё время, с оценкой стоимости в USD. Каждый живой вызов добавляет небольшую запись {provider, in, out} в data/llm-usage.jsonl (никуда ничего не отправляется); запуски без ключа (ручной режим) ничего не стоят и не записываются.
- Новый модуль маршрута (30-й)
server/lib/routes/usage.mjs—GET /api/usage(агрегаты только для чтения) +server/lib/llm-usage.mjs(recordUsageнормализует форматы usage у Anthropic/OpenAI/Gemini и добавляет best-effort;readUsage/aggregateагрегируют по окнам 24ч/7д/30д/всё × провайдер) +server/lib/llm-pricing.mjs(редактируемая таблица цен по провайдерам$/1Mтокенов — токены точны, доллары — приблизительные прайс-цены, которые можно исправить под тариф; не выставляются к оплате). Запись подключена в точках диспетчеризации (runActiveProvider+routes/llm.mjs). - Новое представление
public/js/views/usage.js(#/usage, вкладки окон). Тесты:tests/usage-routes.test.mjs. 17 новых ключей i18n ×16 (usage.*+nav.usage). Справка §6 расширена на месте.
Новое: server/lib/routes/usage.mjs; server/lib/llm-usage.mjs; server/lib/llm-pricing.mjs; public/js/views/usage.js.
[1.104.0] — 2026-07-06
Логотипы компаний в таблице сканирования (без утечки приватности). Новый переключатель Внешний вид в Настройках — Показывать логотипы компаний в таблице сканирования (по умолчанию выключен) — рисует логотип каждой компании рядом с названием на #/scan. Логотип — это favicon компании, взятый с её собственного домена и проксируемый на сервере (GET /api/logo), поэтому ни один сторонний сервис логотипов не узнаёт, каких работодателей вы смотрите. Вакансии на общем портале (Greenhouse, Lever, Ashby, …) показывают цветной буквенный значок вместо иконки портала, и любой не загрузившийся логотип откатывается на тот же значок.
- Новый модуль маршрута (29-й)
server/lib/routes/logos.mjs—GET /api/logo?domain=. Он валидирует домен (без схемы/пути/loopback), берёт/favicon.icoчерез SSRF-безопасныйsafeGet(новый режимbinaryвозвращает сырые байты + content-type; DNS-пиннинг, проверка редиректов и лимит размера не изменены), выполняет сниффинг сигнатуры изображения, чтобы никогда не отдать HTML-страницу ошибки как картинку, кэширует попадания и промахи в LRU в памяти и ничего не пишет на диск. - Новая клиентская библиотека
public/js/lib/company-logo.js(window.CompanyLogo): по умолчанию выключена через флаг в localStorage; пропускает общие ATS-хосты в пользу детерминированного буквенного аватара; CSP-безопасный откатimg.onerror. Тесты:tests/logo-routes.test.mjs. 5 новых ключей i18n ×16 (appear.*). Справка §2 расширена на месте.
Новое: server/lib/routes/logos.mjs; public/js/lib/company-logo.js.
[1.103.0] — 2026-07-06
Настройки: «Инструменты CLI для ИИ» — какие установлены. career-ops работает на Claude Code, но совместим с любым агентским CLI открытого стандарта skills. Новая вкладка Инструменты CLI для ИИ в Настройках (#/config) показывает, какие из них — Claude Code, Codex, Gemini CLI, OpenCode, GitHub Copilot CLI, Qwen, Antigravity — установлены на машине, где запущен сервер, и их пути. Это сканирование PATH только для чтения: проверяется лишь наличие каждого бинарника, и он никогда не запускается (без --version, без выполнения), ничего не пишется и данные пользователя не затрагиваются.
- Новый модуль маршрута (28-й)
server/lib/routes/cli-detect.mjs—GET /api/cli-detect. Определение резолвит путь бинарника из фиксированного списка из 7 записей поprocess.env.PATH(шимы.cmd/.exe/.batв Windows; бит выполнения в POSIX); враждебный файл на PATH никогда не будет выполнен этим маршрутом. - Новая вкладка «Инструменты CLI для ИИ» в
public/js/views/config.js(ленивая загрузка, диплинк через#/config?tab=cli). Тесты:tests/cli-detect-routes.test.mjs. 8 новых ключей i18n ×16 (cli.*+config.tabCli). Справка §2 расширена на месте.
Новое: server/lib/routes/cli-detect.mjs.
[1.102.0] — 2026-07-05
«Спросить справку» — чат с опорой на встроенную справку. Новая страница Спросить справку 💬 (боковая панель, под «Справкой»): введите вопрос вроде «Как сканировать порталы вакансий?» и получите ответ только из встроенной справки приложения на вашем языке — показывает, какие разделы использованы, и никогда не читает ваше резюме, профиль или поиск работы. Это о том, как пользоваться приложением, а не о вас. С ключом LLM отвечает вживую; без ключа выдаёт готовый промпт, уже заполненный нужными разделами справки.
- Новый модуль маршрута (27-й)
server/lib/routes/docs-assistant.mjs—POST /api/docs-assistant/ask. Поиск без зависимостей: справка на вашем языке разбивается на разделы##и оценивается по совпадению ключевых слов с вопросом; лучшие включаются, и модель должна отвечать из них или сказать, что справка это не покрывает (без выдуманных функций/маршрутов). Общий каскад провайдеров, ручной откат, ограничение частоты, без записей, не читает данные пользователя. - Новое представление
public/js/views/docs-assistant.js. Тесты:tests/docs-assistant-routes.test.mjs. 14 новых ключей i18n ×16 (docs.*+nav.docsAssistant). Справка §1 расширена на месте.
Новое: server/lib/routes/docs-assistant.mjs; public/js/views/docs-assistant.js.
[1.101.0] — 2026-07-05
CV Studio: адаптируйте резюме и напишите сопроводительное письмо под конкретную вакансию, с рекрутерским чек-листом. Новая карточка Адаптировать под вакансию на #/cv-studio: вставьте описание вакансии (и, по желанию, целевую роль/заголовок), и CV Studio создаёт адаптированное под эту вакансию резюме и подходящее сопроводительное письмо, а затем прогоняет оба через чек-лист-гейт перед выдачей — error блокирует (исправляется до того, как вы увидите результат), warn советует. Механика — выжимка из практики карьерного консультирования в общие правила: рекрутер читает за секунды, поэтому релевантное идёт наверх, заголовок совпадает с ролью вакансии, в результатах — конкретные цифры, а письмо остаётся коротким тизером с единственной «смычкой» «требование ↔ ваш подходящий факт». Основано только на вашем резюме, профиле и two-pager и ничего не выдумывает — без зашитых компаний, ролей или истории.
- Новый эндпоинт
POST /api/cv-studio/tailor(расширяет существующий модуль cv-studio — без 27-го модуля):buildTailorPrompt+ общий гейтTAILOR_INSTRUCTIONS, на основеbundleProjectContext(резюме + профиль + two-pager), общий каскад провайдеров, ручной промпт без ключа, с ограничением частоты, без записей. Результат экспортируется в Markdown / PDF / DOCX через общую панельreport-export.js. - Тесты: +3 в
tests/cv-studio-routes.test.mjs. 10 новых ключей i18n ×16 (cvs.tailor*). Общий референсdocs/prompts/resume-cover.md. Справка §24 расширена на месте.
Новое: docs/prompts/resume-cover.md.
[1.100.0] — 2026-07-05
Two-pager: ИИ-автозаполнение из вашего резюме + Просмотр + экспорт в PDF/DOCX/Markdown. Two-pager (#/two-pager) фиксирует то, чего вы на самом деле хотите от следующей роли, но раньше каждое поле приходилось писать вручную или копировать промпт в другой инструмент. Теперь ✨ ИИ-помощник заполнения работает вживую с настроенным провайдером — читает только ваше резюме + профиль (через bundleProjectContext, ничего не выдумывая), составляет все поля (кто я / что нравится / обязательное / что ненавижу / стоп-факторы / непреложное / целевая среда) и заполняет форму, чтобы вы проверили, отредактировали и сохранили. Без API-ключа возвращается к модалке «скопировать промпт», как раньше. Новая кнопка 👁 Просмотр и экспорт отображает two-pager как форматированный документ с панелью Скачать .md / Сохранить как PDF / Сохранить как DOCX / Копировать.
- Экспорт
.docxбез зависимостей. Новыйserver/lib/docx.mjsформирует минимальный, но валидный Office Open XML.docx(ZIP с DEFLATE из четырёх частей OOXML, CRC-32 на запись) — без новой зависимости (зависимости остаютсяexpress+js-yaml). Новый маршрутPOST /api/export/docx(server/lib/routes/export.mjs, 26-й модуль маршрутов; без состояния, лимит 200 КБ, без записей / без LLM / без fetch по URL). Подключён к общемуpublic/js/lib/report-export.js, поэтому отчёт по рынку, карьерный план и профориентация тоже получают экспорт в DOCX. - Живое автозаполнение использует общий каскад провайдеров (
runActiveProvider/providerAvailable); возвращённый YAML парсится и приводится к ограниченной форме two-pager (parseYamlFields+normalizeTwoPager) — неизвестные ключи отбрасываются, массивы/строки ограничиваются. Ручной режим сохранён. - Тесты:
tests/export-routes.test.mjs. 4 новых ключа i18n ×16 (export.saveDocx,twoPager.preview,twoPager.aiFilling,twoPager.aiFilled).
Новое: server/lib/docx.mjs; server/lib/routes/export.mjs.
[1.99.0] — 2026-07-05
Страница здоровья порталов (#/portals). Сканер следит за набором компаний в portals.yml; slug ATS может тихо сломаться, и этот работодатель исчезает из всех будущих сканирований. Новая страница Portals перечисляет каждую отслеживаемую компанию и по кнопке Check portal health проверяет каждый careers_url через safeGet с закреплённым DNS (защита от SSRF), помечая мёртвые (404 = тихо отброшена) — только чтение. Также усиливает репортёр багов v1.98.0 по итогам ревью: кольцевой буфер ошибок теперь ловит сетевые сбои fetch, а скраббер вычищает непомеченные ключи провайдеров.
Новое: server/lib/routes/portals.mjs; public/js/views/portals.js.
[1.98.0] — 2026-07-05
Встроенный репортёр багов (паритет с веб-частью web-v0.2.0 родительского проекта). Кнопка 🐞 Report a bug в панели уведомлений собирает диагностический снимок с приоритетом приватности — версии, ваш экран, браузер, сводку проверок /api/health и последние 20 ошибок из нового клиентского кольцевого буфера — плюс детерминированный отпечаток для дедупликации (co-web-<base36>), даёт просмотреть точный Markdown и затем открывает предзаполненный issue на GitHub. Ничего не отправляется автоматически; он никогда не несёт ваше CV, профиль, ответы, URL вакансий или ключи. Новые библиотеки logbuf.js + bug-report.js; 11 ключей i18n ×16; tests/bug-report.test.mjs.
Новое: public/js/lib/logbuf.js; public/js/lib/bug-report.js.
[1.97.1] — 2026-07-05
Усиление по итогам ревью и паритет документации (продолжение v1.97.0). Разбор логов AI-ревью выявил реальные исправления:
fit-score.js(значок соответствия◎при сканировании).salaryFloor()больше не превращает ставку меньше годовой в фиктивный годовой минимум — «at least 500 EUR/day», «$80/hr», «6000 monthly» теперь возвращаютnullвместо дилбрейкера в 500k/80k. Сопоставление стран теперь идёт по целому слову (\b…\b), поэтому «Germany» больше не совпадает с прилагательным «German» (равно как «Nigeria» внутри «Nigerian») и не срабатывает ложным нарушением «обязательно-в-другом-месте». +3 теста вtests/fit-score.test.mjs.- Паритет документации. Каждый локализованный README теперь единообразно заявляет 16 локалей — счётчик/список в строке Help (×13) и текст раздела локализации плюс примечание «добавьте ключ во все N файлов» (×8) всё ещё держались на счётчиках до v1.85 (8/9). Счётчик адаптеров §17 во встроенной справке исправлен на 46 адаптеров — 41 английский + 5 русских во всех 16 бандлах.
Никаких изменений поведения помимо эвристики значка соответствия; ни новых маршрутов, ни ключей, ни добавлений i18n.
[1.97.0] — 2026-07-05
Источник сканера Dassault Systèmes + фронт улучшений качества по трём направлениям.
- Новый источник сканирования — Dassault Systèmes (паритет с родительским career-ops, #1498).
server/lib/sources/dassault.mjs+server/lib/portals/adapters/dassault.mjsвоспроизводят zero-token-провайдер «карточного поиска» Exalead из родительского проекта (публичная лента за3ds.com/careers/jobs). Это единственная глобальная конечная точка, поэтому она выбирается по провайдеру (provider: dassault) или автоматически определяется по хосту3ds.com, с защитой от SSRF, привязкой хоста кwww.3ds.comиredirect:'error'. XML разбирается без DOM (карты<Meta>для каждого<Hit>), город/страна извлекаются из локализованной строки категории, а вакансии сохраняются только тогда, когда их публичный URL находится на*.3ds.com. Реестр теперь поставляет 46 адаптеров (41 EN + 5 RU); счётчикALL_ADAPTERS, проверки сортированных id и EN-набора/api/scan/sourcesподняты с 40 → 41. Наборtests/sources-dassault.test.mjs(10 случаев). - Перенесённые из родительского проекта исправления надёжности. Парсер Avature теперь допускает два живых варианта разметки арендаторов (
article--resultс суффиксом позиционного индекса + безклассовый якорь заголовка JobDetail, #1541); Get on Board защищается от0/отрицательногоpublished_at(больше никаких ложных дат 1970 года); SuccessFactors ограничивает последнюю страницу, чтобы она не могла превыситьMAX_JOBS(#1528). - Исправления серверного аудита.
safe-fetchбольше не зависает при ответе, превышающем лимит размера — путь ограничения размера теперь напрямую завершает промис вместо ожидания события'end', которое разрушенный поток никогда не испустит (исправляет запросы/api/pipeline/previewи авто-пайплайна для больших страниц). Логирование активности SSEstream.*снова достижимо (проверка/api/stream/перемещена выше общей защиты «пропускать GET»). - Исправления аудита SPA. Переключатель вкладок
#/statsзащищён от гонки при асинхронном рендеринге — результат медленной вкладки больше не может затереть более новую вкладку, на которую пользователь уже переключился. Подтверждения удаления в mock interview и нетворкинге теперь передают корректные заголовок + тело (больше никаких диалогов с пустым телом). - Исправления перевода. Исправлены непереведённые значения словаря — украинские
config.modes*(Adaptive Framing / Exit Narrative / Location Policy), русскийeval.jdLbl(«Job Description»), итальянскийdash.quick.contactoSub(«referral» → «segnalazione») — а также английская заготовка 16 локалей локализована в CHANGELOG для ru/uk/ja/ko/zh-CN/zh-TW.
Новое: server/lib/sources/dassault.mjs; server/lib/portals/adapters/dassault.mjs.
[1.96.0] — 2026-07-04
Карьерная ориентация (Epic 27). Новая страница #/orientation отвечает на вопрос «какие направления мне действительно подходят?» — такой ответ вы получили бы от профориентационного теста, но выведенный из вашего собственного резюме и профиля, а не из анкеты. Нажмите Сгенерировать профиль, и модель вернёт ваши наиболее подходящие карьерные векторы (какие из восьми архетипов — Функционалист, Администратор, Коммуникатор, Специалист, Аналитик, Новатор, Управленец, Предприниматель — подходят, с подтверждениями), склонность к карьерному типу, рекомендуемые роли, профессиональные сильные стороны, привязанные к вашему резюме, особенности стиля работы и рекомендации по развитию. Это ИИ-размышление о том, как читается ваше резюме, — а не психометрический тест: оно никогда не выдумывает достижения и не приводит числовые оценки так, будто они были измерены. Экспортируйте профиль в Markdown или PDF; на диск ничего не записывается.
- Новый маршрут
server/lib/routes/orientation.mjs(24-й модуль маршрутов) —POST /api/orientation/generateстроит промпт профиля из CV+профиля+two-pager+памяти через общий каскад провайдеров, с ручным резервным вариантом для копирования и вставки и без записи файлов. - Переиспользует
report-export.jsдля Markdown/PDF/копирования, в навигационной группе Развитие. - Тесты:
tests/orientation-routes.test.mjs(формулировка «размышление» / отсутствие сфабрикованных оценок, ручной режим на основе CV/профиля). 7 новых i18n-ключей ×16 локалей, справка §28 ×16.
Новое: #/orientation; server/lib/routes/orientation.mjs.
[1.95.0] — 2026-07-04
План карьеры (Epic 26). Новая страница #/career-plan превращает ваше резюме и профиль в конкретный, персонализированный план развития. Выберите горизонт (6/12/24 месяца) и, при желании, фокус, и модель — читая ваше CV, профиль, two-pager и заметку памяти — пишет снимок отправной точки, SWOT сильных сторон и роста, цели в формате SMART / OKR / WOOP, альтернативные траектории, план hard/soft-навыков, помесячную дорожную карту, методы отслеживания прогресса, подводные камни и поддерживающие шаги. Она планирует вперёд от того, что действительно показывают ваши материалы, и никогда не выдумывает факты о вашей истории. Редактируйте её на месте, Сохраните её в пользовательский слой (config/career-plan.md) и экспортируйте её в Markdown или PDF.
- Новый маршрут
server/lib/routes/career-plan.mjs(23-й модуль маршрутов) —GET/PUT /api/career-plan(пишетconfig/career-plan.md) +POST /api/career-plan/generate(общий каскад провайдеров, ручной откат, без фабрикации).PATHS.careerPlan. - Переиспользует общий
report-export.js(v1.94.0) для Markdown/PDF/копирования и новую навигационную группу Рост. - Тесты:
tests/career-plan-routes.test.mjs(ограничение, круговой обход GET/PUT, промпт с учётом горизонта, засеянный из CV/профиля). 20 новых ключей i18n во всех 16 локалей, Справка §27 во всех 16.
Новое: #/career-plan; server/lib/routes/career-plan.mjs; PATHS.careerPlan.
[1.94.0] — 2026-07-04
Статистика, переработана (Epic 25). Страница #/stats теперь представляет собой раздел Статистика с тремя вкладками, с настоящими графиками и куда большим объёмом данных. Новая вкладка Отчёт по рынку запрашивает у модели анализ зарплат и рынка труда по вашим целевым ролям в выбранных вами регионе и валюте — резюме для руководства, зарплата по грейдам с процентилями P10/P25/P75/P90, ведущие работодатели, таблица востребованных навыков, частота бенефитов, распределение офис/гибрид/удалёнка, тренды на 12–24 месяца и рекомендации по переговорам. Каждая цифра помечается как ориентировочная оценка на основе знаний модели и никогда не подаётся как собранные данные. Новая вкладка Мой pipeline строит графики по вашему собственному трекеру: распределение баллов, воронка статусов, ведущие компании и роли, отклики во времени и коэффициенты конверсии. Исходное представление целевых ролей (вакансии/зарплата по странам + тренд сохранённых снимков) переезжает на третью вкладку, теперь с выбором валюты и обзором вакансий по ролям.
- Экспортируйте любой отчёт в Markdown или PDF либо скопируйте его — через общий помощник
report-export.js(загрузка Markdown-blob; PDF через существующий встроенный PDF-раннер). - Новый маршрут
server/lib/routes/market.mjs(22-й модуль маршрутов) —POST /api/stats/marketстроит промпт анализа рынка на основе вашего CV/профиля (чтобы он знал ваши целевые роли), региона и валюты, прогоняет его через общий каскад провайдеров и откатывается к промпту для копирования и вставки при отсутствии ключа. Без записи на диск. - Тесты:
tests/market-routes.test.mjs(ограничение региона/валюты, промпт с пометкой о честности, ручной режим с посевом из CV/профиля). 36 новых ключей i18n во всех 16 локалей, Справка §26 во всех 16.
Новое: #/stats переработана во вкладки; server/lib/routes/market.mjs; public/js/lib/report-export.js.
[1.93.0] — 2026-07-04
Слой памяти (Epic 24). Новая страница #/memory хранит короткую редактируемую заметку «запомни это обо мне», которую ассистент держит в уме при каждой задаче:
- Одна заметка везде — поскольку она встраивается в
bundleProjectContext, заметка автоматически доходит до каждого AI-запроса (оценка, mock interview, нетворкинг, CV Studio) по всем провайдерам. Напишите её один раз — она направляет всё. - Направление, а не факты — она фиксирует ваши предпочтения и то, как вам нравится работать (тон, формат, deal-breakers, ритм), и никогда не вводит новые фактические утверждения о вашем опыте — они по-прежнему живут только в вашем CV, профиле и two-pager. Сохраняется в пользовательский слой в
config/memory.mdи никогда не перезаписывается обновлениями. - Предложить из ваших данных —
POST /api/memory/suggestанализирует ваш собственный трекер откликов на предмет поведенческих паттернов и набрасывает пункты, которые вы можете просмотреть и отредактировать. Он читает ваш трекер; он никогда не выдумывает факты и не делает живых вызовов.
Новое: server/lib/routes/memory.mjs (21-й модуль маршрутов — GET/PUT /api/memory + POST /api/memory/suggest), public/js/views/memory.js, PATHS.memory и блок config/memory.md, добавленный в bundleProjectContext. 11 новых ключей i18n во всех 16 локалей. Тесты: tests/memory-routes.test.mjs.
[1.92.0] — 2026-07-04
CV Studio (Epic 21). Новая страница #/cv-studio даёт вашему резюме три честных, в основном локальных инструмента:
- Диагностика резюме — детерминированная оценка 0–100 с пояснениями по каждой проверке (количественно измеримое влияние, слабые глаголы, штампы, длина, ключевые разделы, контактные данные). Полностью на стороне клиента (
window.CvDiagnostics) — без LLM, ничего выдуманного, каждая находка объяснена, так что вы решаете, что менять. - Маска приватности — скрывает PII (email, телефон, ссылки/ники, домашний адрес и, по желанию, ваше имя → инициалы) перед тем, как поделиться резюме в виде образца или скриншота. Работает целиком в браузере (
window.CvPrivacy); сообщает ровно то, что было скрыто, и никогда не хранит оригинал. - Make it human / подгонка под голос — вставьте сухую строку или абзац, и он перепишет её вашим голосом, обоснованно на стороне сервера через
voice-dna.mdиwriting-samples/. Жёсткое ограничение: он может переставлять, ужимать и перезвучивать, но никогда не вводит факт, метрику или достижение, которых ещё нет в тексте. Выполняется вживую через общий каскад провайдеров либо возвращает готовый к копированию промпт без ключа.
Новое: server/lib/routes/cv-studio.mjs (20-й модуль маршрутов — POST /api/cv-studio/humanize), public/js/views/cv-studio.js, public/js/lib/cv-diagnostics.js, public/js/lib/cv-privacy.js, PATHS.voiceDna + PATHS.writingSamplesDir. 29 новых ключей i18n во всех 16 локалей. Тесты: tests/cv-diagnostics.test.mjs, tests/cv-studio-routes.test.mjs. (Галерея шаблонов, экспорт в Word и архив PDF вакансий отслеживаются как последующая работа над CV Studio.)
[1.91.0] — 2026-07-04
Нетворкинг и глубокое исследование компаний (Epic 16). Новая страница #/networking превращает компанию в действенный план по получению собеседования, обоснованный вашим резюме, профилем и two-pager:
- Досье компании — сжатая справка о том, чем занимается компания, недавние сигналы, достойные упоминания, и зацепки «почему я подхожу», взятые из вашего реального опыта.
- С кем связаться — 3–5 целевых персон (нанимающий менеджер, внутренний рекрутер, старший IC из команды, тёплый контакт/выпускник того же вуза) с конкретной поисковой строкой LinkedIn для нахождения каждого. Реальные имена никогда не выдумываются.
- Самый тёплый путь для представления — единственный наиболее реалистичный тёплый вход именно для вашего опыта (общий работодатель/учебное заведение/сообщество, путь второго уровня или сильный по сигналу холодный DM) и почему.
- Черновики для контакта — короткие, конкретные сообщения для главных персон, обоснованные вашими реальными точками доказательства.
- Вживую или вручную — выполняется вживую через общий каскад провайдеров с любым ключом либо возвращает готовый к копированию промпт (честный fallback, ничего выдуманного). Save plan сохраняет готовый план в пользовательский слой (
networking/net-{company}-{role}-{date}.md); страница показывает список, открывает и удаляет сохранённые планы.
Новое: server/lib/routes/networking.mjs (19-й модуль маршрутов), public/js/views/networking.js, PATHS.networkingDir. Переиспользует каскад server/lib/llm-dispatch.mjs из v1.90.0. 24 новых ключа i18n во всех 16 локалей. Тесты: tests/networking-routes.test.mjs.
[1.90.0] — 2026-07-04
Mock Interview 2.0 (Epic 15). Новая страница #/mock-interview превращает ваше резюме, профиль, two-pager и банк историй в пошаговую репетицию собеседования:
- Диалоговая практика — задайте целевую роль (+ по желанию компанию / JD), и интервьюер начинает с точечного вопроса. На каждый отправленный ответ вы получаете структурированную реплику: Feedback (сильные стороны + пробел по STAR+R), Score (
N/5) и Next question, который прощупывает самое слабое место вашего последнего ответа. Обосновано на стороне сервера вашими реальными материалами — оно никогда не выдумывает опыт, которого у вас нет. - Учитывает банк историй —
interview-prep/story-bank.mdвстраивается в промпт (тот же уровень доверия, что иcv.md), поэтому обратная связь может указать вам на ваши же лучшие истории. - Вживую или вручную — при наличии ключа провайдера ход выполняется вживую через общий каскад провайдеров (Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models); без ключа вы получаете готовый к запуску промпт для копирования и вставки (честный fallback, без выдуманных ответов).
- Сохранённые сессии — нажмите Save transcript, чтобы сохранить завершённое собеседование в пользовательский слой (
interview-prep/mock-{company}-{role}-{date}.md); страница показывает список, открывает и удаляет сохранённые сессии.
Новое: server/lib/routes/interview.mjs (18-й модуль маршрутов), public/js/views/mock-interview.js, server/lib/llm-dispatch.mjs (общий каскад провайдеров), PATHS.storyBank, bundleProjectContext({ extraFiles }). 30 новых ключей i18n во всех 16 локалей. Тесты: tests/interview-routes.test.mjs.
[1.89.0] — 2026-07-04
Соответствие кандидата и рынка — two-pager (Epic 14). Новая страница #/two-pager позволяет зафиксировать то, что вы на самом деле хотите от следующей роли, по образцу «Mnookin two-pager» из книги Never Search Alone:
- Пошаговый конструктор — повествование от первого лица «Кто я», заметка «Целевая среда» и пять редакторов списков-чипов: loves, must-haves, hates, deal-breakers и non-negotiables. Сохраняется в пользовательский слой родительского проекта (
config/two-pager.yml) черезPUT /api/two-pager— никогда не перезаписывается системными обновлениями. - ИИ-помощник заполнения (
POST /api/two-pager/draft) — собирает готовый к запуску Mnookin-промпт с вашим CV + профилем внутри, чтобы вы запустили его в любом LLM и вставили результат обратно. Он использует только ваши собственные материалы; ничего не выдумывается. - Бейдж соответствия-тому-что-вы-хотите — каждая вакансия на
#/scanтеперь показывает оценку соответствия◎ N(на стороне клиента, черезwindow.FitScore), сопоставляющую формат работы, страну, минимальную зарплату и релокацию вакансии с вашим two-pager. Честный по замыслу: если вакансия не даёт сопоставимого сигнала, бейдж не показывается (никогда не выдуманное число). Нарушения deal-breakers весят больше, чем лёгкие неприязни. - Питает каждую оценку — сохранённый two-pager встраивается в
bundleProjectContext, поэтому все последующие оценки LLM сочетают заявленные вами предпочтения с соответствием CV-против-JD.
Новое: server/lib/routes/two-pager.mjs, public/js/views/two-pager.js, public/js/lib/fit-score.js, PATHS.twoPager. 27 новых ключей i18n во всех 16 локалях. Тесты: tests/two-pager-routes.test.mjs, tests/fit-score.test.mjs.
[1.88.0] — 2026-07-04
Доводка issue #29 — пробелы i18n в Сканировании + гигиена API.
- Локализованы последние захардкоженные строки Сканирования (дорожная карта v1.69.4): плашки сводки по источнику (
N новых / M совпадений), тостыN новых вакансийи бейджrelocтеперь проходят черезt()— 4 новых ключа (scan.pillNew,scan.pillMatching,scan.newOffers,scan.relocBadge) во всех 16 локалях. Неанглоязычные пользователи больше не видят случайного английского в основном потоке сканирования. - Отключён заголовок
X-Powered-By(дорожная карта v1.69.5):app.disable('x-powered-by')вcreateApp()— сервер больше не сообщает о том, что работает на Express. (Остальная часть этого эпика уже была выпущена:parentVersionвырезает свой комментарий release-please, переключатель светлой темы, закрытие модальных окон при смене маршрута и локализация «Score» (rep.score) в Отчётах.)
Тесты: tests/scan-i18n-gaps.test.mjs + проверка отсутствия X-Powered-By в tests/security-headers.test.mjs.
[1.87.0] — 2026-07-04
4 новых провайдера сканирования без авторизации (паритет с родительским career-ops v1.16.0). Реестр сканера вырастает с 41 → 45 адаптеров (40 EN + 5 RU) — все публичные, без авторизации, с закреплённым хостом, redirect:'error' (защита от SSRF), каждый с изолированным для CI тестом:
- Get on Board (
getonbrd) — публичный JSON:API по всему порталу (LATAM/удалённые технические роли), выбирается по провайдеру, с пагинацией.server/lib/sources/getonbrd.mjs. - Amazon (
amazon) — публичный JSON поискаamazon.jobs, определяется по хосту илиprovider: amazon, пагинация по смещению.server/lib/sources/amazon.mjs. - Avature (
avature) — ATS*.avature.netдля каждого арендатора, парсинг HTML, определяется по хосту илиprovider: avature.server/lib/sources/avature.mjs. - SAP SuccessFactors (
successfactors) — список плиток RMK для каждого арендатора (*.successfactors.eu/.com,jobs2web.com), парсинг HTML.server/lib/sources/successfactors.mjs.
Каждый поставляет sources/<slug>.mjs (автообнаруживаемый meta → выпадающий список #/scan) и portals/adapters/<slug>.mjs в ALL_ADAPTERS (правило двух реестров) + tests/sources-<slug>.test.mjs. Счётчик ALL_ADAPTERS, а также проверки сортированных id и EN-набора /api/scan/sources выросли с 36→40; GET /api/scan/sources теперь перечисляет 45.
[1.86.0] — 2026-07-03
Статистика по целевым ролям (#/stats) — рыночная статистика вакансий и зарплат по ВАШИМ целевым ролям. Новая страница аналитики читает ваши целевые роли из профиля (config/profile.yml → не захардкожены) и вакансии из последнего сканирования, а затем показывает по каждой роли и стране:
- Вакансии по странам и медианную зарплату по странам (USD) — агрегируются на стороне клиента (
public/js/lib/role-stats.js, повторно используяwindow.Countries) из разрозненных данных, которые сканеры уже собирают. Зарплаты в любой валюте нормализуются в USD через явно приблизительную таблицу FX, с оговоркой о размере выборки — никогда не выдумываются. - Фильтры по роли и стране и самодельные встроенные SVG-графики: столбчатые и трендовые (без новых зависимостей, безопасно для CSP — только
addEventListener). - Сохранить снимок (
POST /api/stats/snapshot) сохраняет текущий агрегат вdata/role-stats.jsonl; график тренда (GET /api/stats/trend) отслеживает количество вакансий со временем — представление «динамики». Честный гибрид: снимки формируются из локальных данных сканирования и обновляются по запросу. - Полностью локализовано во всех 16 локалях (26 новых ключей i18n).
Новое: server/lib/routes/stats.mjs (16-й модуль маршрутов), public/js/lib/role-stats.js, public/js/views/stats.js, PATHS.roleStats; тесты role-stats.test.mjs (7) + stats-routes.test.mjs (5).
[1.85.0] — 2026-07-03
Локали немецкого, итальянского и турецкого (паритет локалей с родительским career-ops v1.16.0). Интерфейс теперь поставляется на 16 языках — de 🇩🇪, it 🇮🇹 и tr 🇹🇷 добавляются к уже имеющимся 13.
- Полный перевод интерфейса — все 730 ключей i18n переведены в
public/js/lib/locales/i18n-dict.{de,it,tr}.js; переключатель языков показывает Deutsch / Italiano / Türkçe, а автоопределение языка браузера распознаётde/it/tr(public/js/lib/i18n.js). - Встроенное руководство помощи —
docs/help/{de,it,tr}.mdпереведены (полная структура из 19 H2 / 75 H3), отдаются черезGET /api/help/:lang. - Документация — добавлены
README.{de,it,tr}.mdиCHANGELOG.{de,it,tr}.md; проверка паритета локалей CHANGELOG теперь охватывает 15 не-EN локалей. - Каркас промптов —
server/lib/prompts.mjs(LOCALE_NAMES+SCAFFOLD_STRINGS) локализован для трёх новых локалей, так что вывод LLM следует языку интерфейса.
Все проверки паритета (i18n-locale-files, i18n-coverage, check-changelog-parity, lang-switcher-rtl) расширены на набор из 16 локалей.
[1.84.0] — 2026-06-30
Кулдаун повторных заявок + компенсация в pipeline.md (паритет с родительским career-ops v1.15.0). Два улучшения сканера:
- Кулдаун повторных заявок (#1201): EN-скан теперь пропускает роли в компаниях, в которые вы уже подавали заявку недавно, — результаты остаются сфокусированы на НОВЫХ вакансиях. Настройте временны́е окна на уровне компании в
config/profile.ymlв секцииre_apply_windows:(last_apply_date,same_role_days,applied_to: [roles], необязательныйcross_role_bucket); сопоставление компаний нечувствительно к пунктуации и работает по границам слов (server/lib/cooldown.mjs). Не действует, если ключ отсутствует; в лог сканирования выводитсяCooldown skipped: N. - Компенсация в pipeline.md (#1017): найденные при сканировании вакансии теперь сохраняют зарплату в виде необязательного завершающего столбца (
url | <salary>) вdata/pipeline.md. URL остаётся ключом дедупликации (столбец| compубирается при чтении), значение ячейки санируется (нет инъекции строки/столбца, формульный префикс нейтрализуется), а pipeline в формате «просто URL» остаётся обратно совместимым.
Поставляется с tests/cooldown.test.mjs и тестами компенсации для pipeline. Количество источников не изменилось — 41 (оба улучшения относятся к логике сканирования, а не к новым доскам).
[1.83.0] — 2026-06-30
Детектор повторных публикаций / фантомных вакансий (паритет с родительским career-ops v1.15.0). На странице #/scan появилась панель 🔁 Повторные публикации / фантомные вакансии, которая помечает кластеры «компания + должность», повторно размещённые под разными URL в скользящем 90-дневном окне — сигнал устаревших воронок и фантомных объявлений. Работает на основе нечёткого сравнения заголовков (server/lib/role-matcher.mjs) и read-only детектора (server/lib/detect-reposts.mjs) по data/scan-history.tsv, отдаётся через GET /api/scan/reposts. Также: parentVersion в /api/health теперь возвращает только semver (комментарий # x-release-please-version из release-please убирается). Поставляется с tests/detect-reposts.test.mjs. Количество источников не изменилось — 41: повторные публикации — аналитическая функция, не новая доска.
[1.82.0] — 2026-06-30
Источник сканирования NoDesk (паритет с родительским career-ops v1.15.0). Общедосочная RSS-лента удалённой работы NoDesk теперь полноценный источник — добавьте запись provider: nodesk, и она появится в выпадающем списке Source на #/scan (всего 41 адаптер: 36 EN + 5 RU). Хост закреплён за nodesk.co с redirect:'error' (защита от SSRF); заголовки делятся по Role at Company (у NoDesk нет тега локации, поле местоположения остаётся пустым); все вакансии — удалённые. Поставляется с CI-изолированным набором тестов tests/sources-nodesk.test.mjs; полный набор юнит-тестов зелёный, 1523.
[1.81.0] — 2026-06-29
Паритет с родительским career-ops — 13 новых источников сканирования вакансий. Переносит последнюю партию провайдеров из main Fighter90/career-ops во встроенный сканер. Общедоступные API досок (выбор по provider:): Arbeitnow, Himalayas, Jobicy, Landing.jobs, 4 Day Week, The Muse, The Hub, Jobspresso (RSS) и Hacker News “Who is hiring?” (Algolia, два шага). Польские доски (определяются по хосту или provider:): JustJoin.it и NoFluffJobs (POST-поиск). Per-tenant ATS (автоопределение по careers_url): Pinpoint (<slug>.pinpointhq.com/postings.json) и Rippling (ats.rippling.com/<slug> → api.rippling.com board). Каждый источник закреплён по хосту с redirect:'error' (защита от SSRF) и доступен в выпадающем списке Source на #/scan — реестр теперь содержит 40 адаптеров сканера (35 EN + 5 RU). Добавлены 13 изолированных CI-наборов тестов на источник; полный набор юнит-тестов зелёный, 1513.
[1.80.0] — 2026-06-28
Пять улучшений сканера (идеи из job-crawler, переписаны). (1) Источник Teamtailor — карьерные сайты <slug>.teamtailor.com через публичную ленту /jobs.rss, автоопределение по careers_url (хост закреплён + redirect:'error'); в реестре теперь 27 адаптеров. (2) Карантин источников — источник с постоянной 404/410 записывается в data/scan-quarantine.json и пропускается в следующих сканах (самовосстановление: повтор через 14 дней). (3) Макс. на источник — необязательное поле на #/scan, ограничивающее число вакансий с каждой доски (∞ по умолчанию). (4) Опубликовано за — клиентский фильтр по возрасту (24ч / 7д / 30д). (5) Сохранённые поиски + ★ избранное — называйте и переиспользуйте наборы фильтров и отмечайте вакансии; хранится в localStorage с защитной валидацией (повреждённый кэш сбрасывается чисто); кэш результатов сбрасывается перед сканом и наполняется вживую.
[1.79.0] — 2026-06-28
Источник сканирования WeWorkRemotely (паритет с career-ops v1.14.0). Общедосочная RSS-лента удалёнки We Work Remotely теперь полноценный источник — добавьте запись provider: weworkremotely, и она появится в списке Source на #/scan (26 адаптеров всего). Хост закреплён за weworkremotely.com с redirect:'error' (защита от SSRF); заголовки делятся по Company: Role. Также: ключевые слова title_filter теперь обрезаются до проверки длины (parent #1261).
[1.78.2] — 2026-06-27
Доработки i18n и UX (follow-up к v1.78.1). Доступное имя логотипа теперь локализовано во всех 13 языках (nav.logoHome). Enter в глобальном поиске, когда вы уже на #/scan, делает принудительный ре-рендер, чтобы введённый запрос не терялся (same-route guard). health.title переведён на польский (Kondycja) и датский (Systemtilstand) — раньше оставался по-английски. Тесты 1235 → 1238.
[1.78.1] — 2026-06-27
Исправления UX на странице сканирования. Таблица результатов #/scan теперь автоматически обновляется во время сканирования и ещё раз после его завершения — без перезагрузки. Глобальный поиск в шапке показывает подсказку Enter и при не-URL-запросе переходит на #/scan с заполненным полем поиска (раньше — #/tracker). Логотип теперь ведёт на дашборд (главную).
[1.78.0] — 2026-06-27
Гео-фильтр на странице сканирования — фильтр вакансий по стране, с флагами. Новый список Страна в #/scan показывает каждую страну, найденную в результатах (эмодзи-флаг + счётчик), чтобы оставить только вакансии, привязанные к конкретной стране — рядом с фильтром Remote/Hybrid/Onsite, так что можно искать как с привязкой к стране, так и удалёнку. Реализовано новым помощником countries.js, который сопоставляет свободный текст локации (названия стран, алиасы и ~100 крупных городов рынка труда) со страной ISO + флагом; детекция консервативна и не угадывает.
[1.77.0] — 2026-06-27
Добавлен датский (Dansk) — 13-й язык интерфейса. Полный перевод UI, встроенного руководства Help (19 H2 / 75 H3), README и CHANGELOG. Датский добавлен в переключатель языков с флагами; механика i18n (ассемблер, аудит, проверки паритета, снапшот) теперь охватывает 13 локалей.
[1.76.0] — 2026-06-26
Паритет с родительским career-ops v1.13.0 — шесть новых источников вакансий, усиление сканера и таблица результатов без лимита.
Добавлено
- Шесть ATS-источников на тенант — BambooHR, Breezy HR, Comeet, Personio, Recruitee, SolidJobs. Определяются по хосту
careers_url(для Comeet нужен полныйapi:-URL), хост закреплён якорным regex +redirect:'error'(защита от SSRF). Все доступны в выпадающем списке Source на#/scan— реестр теперь содержит 25 адаптеров (20 EN + 5 RU). Добавлен помощникfetchTextдля XML-ленты Personio. trust_filter— опциональная оценка доверия (0–100, уровень high/medium/low, флаги), только аннотирует. Записи нижеhighполучают нейтральный значок ⚠ на#/scan; вакансии никогда не отбрасываются.- Arbeitsagentur
remoteMatch+remoteMaxPages— определение удалёнки из конфига:title(regex),filter(серверныйhomeoffice=nv_true+ пагинация) илиoff.
Изменено
- Нет лимита результатов скана. Лимит
MAX_STORED_RESULTS(2000) убран — хранятся все совпавшие вакансии, таблица#/scanлистает их (200/стр.). Большие прогоны больше не теряют хвост. - Надёжность фильтра по заголовку — короткие аббревиатуры (COO, SDR…) теперь сопоставляются по границам слов; некорректный
title_filterбольше не роняет скан. Оба сканера (ATS и региональный).
Тесты
- +32 кейса (1190 → 1222):
sources-ats-providers,title-filter,arbeitsagentur-remote,trust-validatorи переписанный стражscan-result-cap(«без лимита»).
[1.75.2] — 2026-06-19
docs: полный паритет документации для агрегаторов сканера из v1.75.0 во всех 12 локалях. Без изменения кода — приводит пользовательскую документацию в соответствие с семью источниками, появившимися в v1.75.0:
- Справочное руководство (12 локалей). В §5 добавлен блок
content_filter(гейтинг по ключевым словам описания/сниппета, аналогlocation_filter) и примечание об агрегаторах; §7 перечисляет семь новых источников в проходе сканирования в один клик и в полном перечислении выпадающего списка Source; счётчик адаптеров в §17 исправлен с устаревших «11 adapters» на «19 adapters — 14 English + 5 Russian». Заголовков##/###не добавлялось, поэтому закреплённая структура 19 H2 / 75 H3 осталась неизменной. - README (9 полных локалей). Новый пункт «Aggregator boards (v1.75.0)» под источниками сканирования, плюс значок релиза, обновлённый до v1.75.2. (У сокращённых README pl/uk/ar нет списка по источникам, и там они намеренно не тронуты.)
- Справочные документы. В
docs/portals-examples.mdдобавлен готовый к копированию раздел «Aggregator boards» с точными блоками конфигурацииprovider:/<provider>:для всех семи;docs/PROJECT.mdобновлён до 19 adapters;docs/sdd/CONVENTIONS.mdдокументирует различие двух реестров (sources/registry.mjsдля выпадающего списка противportals/registry.mjsдля выборки), выбор агрегатора на основеprovider:, прокидываемый какopts.company, санитайзер записи сканирования (scan-sanitize.mjs) и число тестов v1.75.1 (1190). - QA. Добавлен
qa/QA-REGRESSION-PROMPT-v1.75.2-FULL.md— драйвер релизного гейта по полной поверхности, обновлённый под цикл агрегаторов сканирования v1.75.x.
[1.75.1] — 2026-06-19
fix(scan): доводка надёжности источников на основе конфигурации из v1.75.0. Три небольших усиливающих исправления по итогам послерелизного разбора (без изменения поведения при штатном сканировании):
- Паузы пагинации с учётом отмены. Межстраничные вежливые паузы Glints (300 мс) и Jobstreet/SEEK (200 мс) теперь немедленно разрешаются при срабатывании
AbortSignalсканирования — через новый помощникdelay(ms, signal)вserver/lib/http-json.mjs, так что отключившийся клиент не может удерживать пагинируемое сканирование открытым ещё на одну паузу. - Описательная ошибка для не-JSON ответа.
fetchJsonтеперь оборачивает не-JSON тело с кодом2xx(например, HTML-страницу обслуживания, отданную со статусом 200) какnon-JSON 2xx response from <url>вместо того, чтобы выдавать голыйSyntaxError, так что журнал ошибок сканера по каждому источнику называет некорректно ведущий себя эндпоинт. - Усиленная нормализация записи сканирования.
normalizeScanScalarтеперь сворачивает вертикальную табуляцию, перевод страницы и разделители строки/абзаца Unicode (\v \f U+2028 U+2029) в дополнение к\r \n \t— строгое надмножество, так что ни один разделитель записи/строки, который могла бы учесть таблица или просмотрщик, не попадает вscan-history.tsv.
[1.75.0] — 2026-06-19
feat(scan): переносим паритет с родительским career-ops v1.12.0 — семь новых источников вакансий, фильтрация контента и исправления безопасности/качества. web-ui запускает собственные внутрипроцессные сканеры (он не вызывает родительский scan.mjs), поэтому изменения провайдеров и сканирования из родительской v1.12.0 не проходят автоматически — этот релиз переносит применимые из них в контракт адаптеров web-ui.
- Семь новых источников сканера. Три глобальных агрегатора удалённых вакансий — RemoteOK, Remotive, Working Nomads — встраиваются в автоматически обнаруживаемый паттерн
server/lib/sources/*.mjs(выбираются черезprovider: remoteok/remotive/workingnomads). Четыре региональных агрегатора, управляемых конфигурацией — careers IBM, Arbeitsagentur (Федеральное агентство занятости Германии), Glints (Юго-Восточная Азия), Jobstreet / SEEK — читают поэлементный конфигурационный блок<provider>:; en-scanner теперь прокидывает разрешённую запись компании в каждый фетчер, чтобы они могли её прочитать. Все семь автоматически появляются в выпадающем списке источников#/scan. content_filter(родительский #974). Необязательный блокportals.yml(списки ключевых словpositive/negative), который фильтрует вакансию по тексту её описания/сниппета — повторяет семантикуlocation_filter; вакансии без описания всегда проходят. Подключён в оба сканера — EN и RU.- Усиление записи при сканировании (родительский #1098). Метаданные внешних фидов теперь санируются перед попаданием в
data/scan-history.tsvиdata/pipeline.md: управляющие символы схлопываются (перенос строки в названии компании/вакансии больше не может внедрить строку TSV), а ведущие= + - @нейтрализуются против инъекции формул в электронные таблицы. secondaryLocationsAshby (родительский #1073). Источник Ashby теперь сворачивает метку региона каждой вторичной локации плюс почтовыеaddressLocality/addressCountryв строку локации (с дедупликацией), так что роль с правом работы в ЕС, чья основная метка читается, например, как “Canada”, всплывает дляlocation_filter.- Валидация формы отчёта об оценке (родительский #819). Внутрипроцессные провайдеры
/api/evaluate(Anthropic / OpenAI / Qwen / OpenRouter / GitHub Models) теперь помечают некорректный отчёт A–G /SCORE_SUMMARYкак нефатальный массивwarnings; путь оценки Gemini уже наследует эту защиту из родительскогоgemini-eval.mjs. - docs: Antigravity CLI добавлен в списки поддерживаемых ассистентов во всех 12 README (сопоставляется с провайдером Gemini).
Унаследовано бесплатно из git pull родителя (web-ui вызывает их через shell): запасной набор CJK-шрифтов для японского PDF (#1053), ATS-совместимые PDF-шрифты (#1074), защита CJK для LaTeX (#1054), исправления tracker/merge/followup/dashboard и китайские режимы modes/zh (web-ui перечисляет режимы динамически).
[1.74.3] — 2026-06-18
docs(parent-source): нацеливаем родительский репозиторий career-ops на форк Fighter90/career-ops. web-ui теперь ссылается на форк мейнтейнера как на родительский проект везде, где это живой источник: значение по умолчанию CAREER_OPS_REPO в установщике bin/setup.sh, каждая ссылка git clone / «поверх» / онбординга во всех 12 README и документация агентов (CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, docs/). Указание авторства santifer (и дисклеймер о неофициальном UI) не изменилось — переместились только URL источника/клонирования. tests/sh-files.test.mjs теперь проверяет, что установщик клонирует форк.
[1.74.2] — 2026-06-17
fix(health): отображение GITHUB_MODELS_API_KEY как необязательной проверки на #/health и в /api/status/providers. Провайдер GitHub Models из v1.74.0 настраивался в #/config, но не имел строки на странице Health и отсутствовал в провайдерской поверхности keysConfigured. Добавлены необязательная проверка (с той же формулировкой “set / unset (manual mode)”, что и у остальных пяти провайдеров живой оценки) и github (+ его GITHUB_MODELS_MODEL) в /api/status/providers, поэтому маршрутизация активного провайдера и страница Health теперь отражают все шесть. Тест строки health в tests/api.test.mjs расширен на все шесть провайдеров.
[1.74.1] — 2026-06-17
docs + test: раздел README «Установка ИИ-ассистента»; полное покрытие ветвей для коннектора Gemini. В README добавлена таблица установки/входа — ссылки на установку Claude Code / Gemini CLI / Codex / Qwen Code / OpenCode / GitHub Copilot CLI + привязка провайдера #/config для каждого + «войдите в систему перед продолжением» (соответствует разделу быстрого старта career-ops.org/docs; поясняет, что web-ui является самостоятельной альтернативой, не требующей CLI). Новый tests/gemini-connector.test.mjs (8 сценариев) охватывает каждую ветку runGemini — отсутствие ключа, успех, ошибка API, пустое/заблокированное завершение, некорректное тело, тайм-аут, сетевая ошибка, hasGeminiKey — доводя server/lib/gemini.mjs до 100% по инструкциям. Общее покрытие: 96% строк / 88% ветвей / 96% функций. Набор тестов 1126 → 1134.
[1.74.0] — 2026-06-17
feat(llm): GitHub Models (Copilot) как 6-й провайдер + каноническое выравнивание 6 ассистентов. career-ops.org/docs перечисляет шесть ИИ-ассистентов для написания кода — Claude Code, Gemini CLI, Codex, Qwen Code, OpenCode, GitHub Copilot CLI. Теперь web-ui поддерживает все шесть: пять сопоставлены с существующими активными провайдерами (Anthropic / Gemini / OpenAI / Qwen / OpenRouter), а GitHub Copilot CLI получает выделенный коннектор GitHub Models — runGitHubModels (OpenAI-compatible; PAT GitHub с областью models), настраиваемый в #/config (GITHUB_MODELS_API_KEY + GITHUB_MODELS_MODEL) и выбираемый через LLM_PROVIDER=github; 6-й в порядке auto. Пакеты справки и README теперь перечисляют канонические шесть (переименован Qwen CLI→Qwen Code; добавлены Gemini CLI + GitHub Copilot CLI), а README добавляет полную таблицу ссылок на режимы и адаптеры порталов на career-ops.org/docs, чтобы каждая функция отслеживалась до родительского проекта. tests/llm-provider-context.test.mjs расширяет матрицу границ получения до всех шести провайдеров (cv.md + profile.yml встроены + возвращаемый артефакт); новые ключи GITHUB_MODELS_* добавлены во все 12 словарей локалей. Набор тестов 1125 → 1126.
[1.73.0] — 2026-06-17
feat(llm): универсальный коннектор Gemini + верифицированный контекст CV/профиля для всех провайдеров. Добавлен server/lib/gemini.mjs (runGemini) — клиент Gemini generateContent без внешних зависимостей, возвращающий ту же форму {markdown, usage, error}, что и клиенты, совместимые с Anthropic / OpenAI. Исправление: /api/mode/:slug и /api/deep ранее пропускали промпты через gemini-eval.mjs, предназначенный исключительно для оценки вакансий, из-за чего Gemini Run live возвращал оценку вместо запрошенного артефакта (сопроводительное письмо, аутрич, бриф). Теперь они вызывают runGemini с bundleProjectContext, поэтому cv.md + config/profile.yml встраиваются инлайн для Gemini точно так же, как и для всех остальных провайдеров — письма и брифы становятся подробными и персонализированными. Новый tests/llm-provider-context.test.mjs мокирует HTTP-границу каждого провайдера и проверяет, что все пять (Anthropic / Gemini / OpenAI / Qwen / OpenRouter) встраивают cv.md + profile.yml инлайн и возвращают артефакт (матрица mode + deep + evaluate, 9 случаев). /api/evaluate сохраняет свой gemini-eval.mjs, настроенный под вакансии. Suite 1116 → 1125.
[1.72.0] — 2026-06-17
feat(modes): Run live теперь возвращает итоговый артефакт напрямую (контракт вывода за один запрос). Родительские шаблоны modes/<slug>.md написаны для интерактивных сессий Claude Code — некоторые (cover, contacto и др.) делают паузу, чтобы задать уточняющие вопросы перед формированием результата, из-за чего Run live в веб-интерфейсе выдавал анкету вместо артефакта. buildModePrompt теперь оборачивает каждый режим в неинтерактивный контракт вывода: выполняет анализ (разбор вакансии, заметки о компании, ключевые слова ATS, расхождения профиля↔вакансии, выбор тона/подачи) в фоновом режиме, выбирает разумные значения по умолчанию из cv.md / config/profile.yml для всего, о чём шаблон обычно спрашивал, и выводит только итоговый артефакт — с завершающим напоминанием для каждого режима «output ONLY {the cover letter / outreach message / …}». Теперь клик на Run live в #/cover возвращает само сопроводительное письмо; то же исправление применяется ко всем универсальным режимам (cover, contacto, interview-prep, project, training, followup, patterns) во всех 12 локалях (артефакт создаётся на языке интерфейса через директиву локали). Suite 1103 → 1116.
[1.71.2] — 2026-06-17
docs(i18n): публикует проход по согласованности документации. Блок “Translations of this guide” в каждом README теперь перечисляет все 11 языков-«сестёр» (ранее в некоторых пропускались English/Français или присутствовала ссылка на самого себя), восстановлена пустая строка перед разделителем секций. Полный QA-промпт регрессионного тестирования переименован в текущую версию, а документация (CLAUDE.md, CONVENTIONS, LOCALIZATION, PROJECT-CONTEXT) синхронизирована с текущей версией и количеством тестов (1103). Изменений в коде или поведении нет — только документация, поэтому переводы справки/UI и все функции версий 1.70.0–1.71.1 остаются без изменений.
[1.71.1] — 2026-06-17
fix(i18n): встроенное руководство по помощи теперь полностью переведено на все 12 языков. Добавлены docs/help/{pl,uk,ar}.md (каждый содержит проверенную структуру 19 H2 / 75 H3), благодаря чему #/help отдаёт нативный пакет на польском, украинском и арабском языках вместо перехода на английский — GET /api/help/{pl,uk,ar} теперь возвращают собственную локаль. Подключено ко всем проверкам помощи (help-ui, help.test, help-ru-config-section, canonical-docs-coverage). Также завершены все списки переводов на 12 языков: блок «Translations of this guide» в README (9 README), заголовки «Translations:» в локализованных CHANGELOG (8 файлов), а также обновлены устаревшие счётчики документации. Suite 1100 → 1103.
[1.71.0] — 2026-06-16
feat(cover): генерация PDF сопроводительного письма прямо из #/cover. Режим cover (добавлен в v1.70.0) формирует текст письма; теперь в результате появляется кнопка Generate PDF, которая рендерит его через общий встроенный конвейер markdown→PDF (POST /api/stream/pdf/inline → generate-pdf.mjs) — тот же путь, что использует interview-prep. Теперь можно создать письмо и отправить PDF, не покидая SPA.
test/docs: усиление ревью v1.70.0. Добавлено CI-изолированное покрытие для режима cover (список разрешений + сборка промпта), переключателя <select> флагов + арабского RTL (dirFor/<html dir>), top.langLabel в каждой локали, подключения PDF сопроводительного письма, а также директивы локали prompts.mjs + скаффолдинга для fr/pl/uk/ar. Обновлены устаревшие упоминания «всех 8» → 12 локалей в docs/sdd/CONVENTIONS.md и в промпте регрессионного QA всего проекта.
[1.70.0] — 2026-06-16
feat(i18n): три новых языка интерфейса — польский (pl), украинский (uk) и арабский (ar, с полной поддержкой RTL) — расширяют SPA до 12 локалей, охватывая все языки из README родительского проекта career-ops. Каждая новая локаль поставляется с полным словарём из 697 ключей (public/js/lib/locales/i18n-dict.{pl,uk,ar}.js), проверенным существующими наборами тестов на паритет / покрытие / отсутствие латинских утечек / отсутствие персональных данных. Арабский язык добавляет полноценную поддержку письма справа налево: i18n.js устанавливает <html dir="rtl"> для RTL-локалей, а блок [dir="rtl"] в app.css отражает хром (боковая панель, ящик уведомлений, таблицы и блок-цитаты в markdown, встроенные отступы) — LTR-локали остаются байт в байт без изменений. Новый ключ top.langLabel (×12) именует переключатель для программ чтения с экрана.
feat(ui): выпадающий список <select> с иконками флагов заменяет ряд переносящихся кнопок. С 12 локалями прежний ряд .lang-btn переносился на три строки в боковой панели; нативный <select> (каждый пункт с префиксом в виде эмодзи флага) компактно масштабируется, изначально поддерживает клавиатуру и экранные считыватели, а также безопасен для CSP (обработчик изменений через addEventListener, без встроенного JS). Там, где платформа не поддерживает глифы флагов, они заменяются буквами региона, поэтому языковая метка всегда остаётся основным идентификатором.
feat(cover): перенос режима сопроводительного письма из родительского проекта (career-ops v1.10.0 + приветствие v1.11.0) в SPA. Новая страница #/cover в группе навигации «Заявки», построенная на базовом исполнителе режимов: описание вакансии + компания/роль + необязательное приветствие → персонализированное письмо, сгенерированное из cv.md / modes/_profile.md. Добавлен cover в серверный MODE_ALLOWLIST и блок i18n cover.* (×12 локалей).
chore(compat): отслеживание родительского career-ops v1.11.0. Подтверждено, что контракт чтения/записи остаётся в силе — data/applications.md по-прежнему является markdown-источником истины (SQLite-индекс трекера в v1.11.0 — производный кэш), столбцы трекера по-прежнему сопоставляются по заголовку. parentVersion теперь сообщает 1.11.0.
fix(i18n): устранена скрытая проблема, при которой французский язык (добавленный в v1.61.0) отсутствовал в LOCALE_NAMES и SCAFFOLD_STRINGS в server/lib/prompts.mjs — LLM-вызовы на французском языке молча переходили на английский вывод и английские заготовки. fr/pl/uk/ar теперь все подключены к пути локали подсказок.
Известные доработки: встроенный справочник (
docs/help/) откатывается на английский для pl/uk/ar (сам UI-хром полностью локализован); интерактивное собеседование-онбординг родительского проекта, обратный поиск ATS и новые провайдеры сканирования пока не представлены в SPA.
[1.69.2] — 2026-06-12
fix(test): закрыта утечка тест-изоляции, из-за которой npm test перезаписывал реальные config/profile.yml и data/scan-history.tsv. tests/critical-fixes.test.mjs импортировал prompts.mjs (→ paths.mjs) в начале файла, поэтому PROJECT_ROOT разрешался в реальный родительский каталог до того, как before() задавал CAREER_OPS_ROOT на временный — и PUT /api/profile на каждом прогоне записывал фикстуру «Acceptance Test» в реальный профиль. Исправление: загружать prompts.mjs через динамический import() внутри before(). Новый tests/test-root-isolation.test.mjs (2 кейса) защищает весь сьют от этого паттерна. Без изменений production-кода. Сьют 1084 → 1086.
[1.69.1] — 2026-06-12
fix(scan): #/scan больше не обрезает молча крупные региональные сборы. Отображаемый набор по региону был жёстко ограничен 500 (реальный RU-скан из 1352 подходящих вакансий показывал только 500; 852 скрыты — симптом «просканировано 2000, показано ~600»). Оба сканера теперь используют общую, переопределяемую через окружение константу MAX_STORED_RESULTS (по умолчанию 2000, переопределение через SCAN_MAX_RESULTS). Только отображение — добавление в pipeline.md / scan-history.tsv и так использовало необрезанный набор. fix(health/ui): карточки проверок #/health больше не вылезают за пределы. Длинное имя/значение сталкивалось с кнопкой Fix → и бейджем статуса; строка теперь сжимается и переносится через .health-check-row. Новые тесты scan-result-cap + health-card-overflow. Набор 1079 → 1084.
[1.69.0] — 2026-06-12
feat(scan): автообнаружение адаптеров сканера (P-14) — достаточно положить .mjs в server/lib/sources/, чтобы зарегистрировать новый источник. До v1.69 список источников в server/lib/sources/registry.mjs был статическим массивом, поддерживаемым вручную: добавление адаптера требовало правок и в <id>.mjs, и в registry.mjs. Закрывает оставшуюся половину пункта P-14 дорожной карты (docs/ROADMAP.md). Теперь каждый *.mjs из server/lib/sources/ подгружается динамически при загрузке модуля; каждый адаптер заявляет о себе самоописательным блоком export const meta = { value, label, region, configKey? }. Все 12 поставляемых адаптеров (ashby / greenhouse / lever / rss / smartrecruiters / workable / workday + geekjob / getmatch / habr / hh / trudvsem) получили meta; registry.mjs использует readdirSync + динамический import() с top-level await (стандарт ESM Node 18+). Публичный API (SOURCES, SOURCES_BY_REGION, RU_CONFIG_KEYS, getRegionalSources) не меняется: все существующие импорты продолжают работать. Валидация отбрасывает некорректные meta и пишет console.warn на каждый сбойный файл. Новый tests/sources-registry-discovery.test.mjs с 14 кейсами. Сьют 1065 → 1079.
[1.68.2] — 2026-06-07
fix(bin): команды CLI через npx / npm link были сломаны — путь к bin теперь резолвится через симлинки. npm и npx публикуют career-ops-ui как симлинк в node_modules/.bin/, и старый dirname "${BASH_SOURCE[0]}" указывал на .bin, а не на корень пакета — поэтому npx career-ops-ui init запускал node node_modules/scripts/init.mjs и падал с MODULE_NOT_FOUND (локальный запуск после npm install работал, что и скрывало баг). Теперь bin/career-ops-ui.sh и bin/start.sh канонизируют SCRIPT_DIR по цепочке симлинков (цикл readlink + cd -P), так что любая команда работает из репозитория, через npm link и через npx. Добавлен регресс-замок в tests/sh-files.test.mjs, прогоняющий команду через симлинк в стиле .bin. Сьют 1065/1065.
[1.68.1] — 2026-05-29
fix(scan): таймаут запроса к источнику 10s → 60s. Fail-fast в 10s (v1.67.1) обрезал и «медленные, но живые» доски Ashby, которым просто нужно было больше времени. Поднимаем дефолт до одной минуты, чтобы они успевали ответить. Компромисс: реально мёртвый/зависший источник теперь держит слот параллелизма все 60s (худший случай скана медленнее), а хронически висящие (Perplexity, Supabase, Resend, …), скорее всего, всё равно отвалятся по таймауту — по-настоящему это решит фикс по источнику / снижение параллелизма для Ashby. Переопределяется SCAN_FETCH_TIMEOUT_MS. Сьют 1063/1063.
[1.68.0] — 2026-05-29
feat(scan): переработана панель фильтров результатов — поля с подписями, кнопка «Применить», вариант «В офисе» и работающий фильтр по зарплате. Каждый фильтр на #/scan теперь поле с подписью (подпись над контролом, а не плейсхолдер): Поиск · Формат · Зарплата от · Зарплата до · Источник · Объём. Явная кнопка «Применить» (плюс «Сбросить» и Enter в любом поле) перезапускает фильтр; подсказка на странице объясняет, как это работает. Фильтр по зарплате теперь действительно фильтрует — как только задано значение от/до, вакансии с оплатой вне диапазона и вакансии без указанной зарплаты убираются (пересечение диапазонов; валюта не учитывается). В фильтр формата добавлен вариант «В офисе» рядом с Remote / Hybrid / Релокация. Новые ключи i18n ×9; salaryInRange сделан строгим; сьют 1063/1063.
[1.67.1] — 2026-05-29
fix(scan): таймаут запроса к источнику 30s → 10s (fail-fast). Поднятие до 30s в v1.67.0 вернуло лишь ~половину медленных досок Ashby; остальные (Perplexity, Supabase, Resend, DeepL, Ramp, …) висят независимо от дедлайна, поэтому больший таймаут лишь тормозил весь скан, ожидая мёртвые слоты. 10s быстро отбраковывает хронически висящие источники и сохраняет отзывчивость скана. Переопределяется SCAN_FETCH_TIMEOUT_MS. Сьют 1060/1060.
[1.67.0] — 2026-05-29
feat(scan): фильтр по зарплате (от / до) на #/scan и увеличенный таймаут запросов к источникам. В таблице результатов рядом с фильтрами по тексту и remote появились два числовых поля — зарплата от / до. Зарплата каждой строки в свободном тексте (от 100 000 до 200 000 ₽, 120000-150000 USD, $120K–$150K, …) парсится в числовой диапазон и сопоставляется по пересечению диапазонов; строки без указанной зарплаты сохраняются, поэтому фильтр сужает список, а не обнуляет его (сравнение без учёта валюты — без конвертации курса). Также поднят таймаут запроса к источнику с 15s → 30s (переопределяется SCAN_FETCH_TIMEOUT_MS): payload Ashby с includeCompensation под параллелизмом ×8 регулярно занимал >15s, из-за чего ~30 досок Ashby отваливались по таймауту в каждом скане. Новые window.Skills.parseSalaryRange/salaryInRange + i18n ×9; 13 новых тестов; сьют 1060/1060.
[1.66.0] — 2026-05-28
feat(scan): РФ-источники теперь обходят ВСЕ страницы, а не только первую. hh.ru, Habr Career и Trudvsem брали только первые ~50 вакансий на запрос; теперь идут по пагинации до конца — &page=N для hh.ru/Habr, offset/meta.total для Trudvsem — с дедупом между страницами и остановкой, когда страница не даёт ничего нового (или по предохранителю в 50 страниц). Запрос вроде «Backend разработчик» теперь возвращает весь результат, а не одну страницу (напр. hh.ru PHP 17 → 55+ за 3 страницы; Trudvsem отдаёт все 72). Постраничные запросы сохраняют существующий таймаут + AbortSignal. 4 новых теста; сьют 1045/1045.
[1.65.0] — 2026-05-28
feat(scan): hh.ru теперь парсится с публичного сайта вместо JSON-API — работает с любого IP, без прокси. api.hh.ru стал отдавать голый 403 forbidden любому программному клиенту независимо от IP (US, РФ-датацентр, РФ-residential, РФ-мобильный) и User-Agent — это анти-бот на периметре, а не задокументированная ошибка API. Зато сайт (hh.ru/search/vacancy) отдаёт полные серверно-отрендеренные результаты любому браузероподобному клиенту, поэтому адаптер теперь парсит его HTML (как Habr Career). Удаляет переменную HH_PROXY из 1.64.0 и зависимость undici — больше не нужны ни прокси, ни ключ, ни настройка User-Agent. Тесты переписаны под HTML-парсер; сьют 1041/1041.
[1.64.0] — 2026-05-27
feat(scan): запрос к hh.ru через российский прокси по HH_PROXY. hh.ru блокирует свой API по IP, а не по User-Agent — поэтому один HH_USER_AGENT никогда не снимал 403 с зарубежного выходного узла. Укажи в HH_PROXY URL российского HTTP/HTTPS-прокси (напр. http://user:pass@ru-host:port) — через него пойдёт только запрос к hh.ru, остальные источники сохраняют прямое соединение. Реализовано на ProxyAgent из undici (новая прод-зависимость); при пустом HH_PROXY диспетчер не добавляется вовсе, изменённое значение подхватывается после перезапуска. 3 новых теста; сьют 1041/1041.
[1.63.2] — 2026-05-27
feat(scan): живой % прогресса + детали по источникам в консоли #/scan. Прогресс-бар стал детерминированным — сканеры шлют события прогресса (EN: по компаниям; RU: по запросам) через SSE, бар заполняется с живой подписью «Scanning… NN%» (анимированная полоса — только до первого события). Первая ошибка каждого источника (таймаут / 403 / сеть) теперь детально логируется в консоль (напр. ⚠ hh timed out: …), повторы подавляются. 1 новый тест; сьют 1040/1040.
[1.63.1] — 2026-05-27
style(scan): прогресс-бар на #/scan стал заметнее. Индикатор обёрнут видимой подписью «Scanning…», высота бара увеличена до 8px (было тонкие 4px) — теперь явно виден во время скана. Поведение не изменилось.
[1.63.0] — 2026-05-27
feat(scan): таймаут на запрос + прогресс-бар на #/scan. У запросов источников не было дедлайна, поэтому зависший источник (например, api.hh.ru с заблокированного IP) мог подвесить весь скан. Новый server/lib/fetch-timeout.mjs оборачивает fetchImpl сканеров (makeTimeoutFetch, по умолчанию 15с, переопределяется SCAN_FETCH_TIMEOUT_MS) — у каждого запроса жёсткий дедлайн; истёкший по таймауту источник записывается как некритичная ошибка, скан продолжается (Habr работает, даже когда hh.ru недоступен). На #/scan во время скана показывается индикатор прогресса (ключ scan.progress во всех 9 локалях). 7 новых тестов; сьют 1039/1039.
[1.62.3] — 2026-05-27
docs: прояснена установка (career-ops-ui работает внутри career-ops/web-ui/) + troubleshooting init, во всех 9 локалях. Install-секция переписана на Option 1 (один curl) / Option 2 (клонировать UI внутрь существующего career-ops как web-ui) + CLI-команды + настройка провайдера + блок Troubleshooting init — устранена частая путаница: клонируют career-ops-ui отдельно и запускают init без родительского проекта. Заметка о вложенной структуре добавлена и в /help §1 Setup; в README highlight — сводка всей линейки v1.62.*. Только документация; код не менялся.
[1.62.2] — 2026-05-27
fix(help): фильтр на #/help теперь полнотекстовый (находит H3-подсекции вроде RSS). Поиск/TOC-фильтр страницы help раньше искал только по заголовкам H2, поэтому документация по RSS из v1.62.x (H3 под §5 Portals & sources) не находилась. Теперь в фильтр индексируется и тело каждой секции, так что поиск, например, «RSS» показывает §5. Чисто клиентская правка, без изменений API.
[1.62.1] — 2026-05-27
feat(scan): RSS в фильтре источников + фикс локации RSS. Выпадающий фильтр источников на #/scan теперь содержит RSS (добавлен в server/lib/sources/registry.mjs + fallback-список SPA), поэтому результаты RSS-бордов (LaraJobs, WeWorkRemotely, …) фильтруются как любой ATS-источник. RSS-адаптер больше не кладёт тег <category> фида в location — не-локационные теги там заставляли location_filter ошибочно отбрасывать remote-вакансии; теперь location пустой и фиды проходят фильтр локации. Подписи/тултипы кнопки скана и строка списка источников (dash.quick.scanSub) обновлены во всех 9 локалях (Workable / SmartRecruiters / Workday / RSS). Обновлены i18n-снэпшот и тест эндпоинта источников (6 → 7 EN).
[1.62.0] — 2026-05-27
feat(scan): универсальный RSS-адаптер для не-ATS джоб-бордов. Новый адаптер rss (server/lib/portals/adapters/rss.mjs + server/lib/sources/rss.mjs) позволяет сканеру собирать вакансии из любого RSS-фида — LaraJobs, WeWorkRemotely, RemoteOK, golangprojects и других бордов вне Greenhouse/Ashby/Lever. Без новых зависимостей: парсинг фида на регулярках с поддержкой CDATA и HTML-сущностей (заголовки/компании очищаются от тегов, astral-символы декодируются безопасно). Включается для компании через provider: rss / rss: / feed_url: в portals.yml, поэтому не перехватывает компании, уже сопоставленные с ATS. ALL_ADAPTERS растёт 6 → 7. 29 новых тестов; описано во всех 9 локалях README.
[1.61.1] — 2026-05-22
fix(i18n): локализована подсказка (title) и aria-label кнопки переключения темы во всех 9 локалях (MINOR-001). Кнопка светлой/тёмной темы (#theme-toggle) имела захардкоженные title="Toggle theme" и aria-label="Toggle theme" в index.html — всплывающая подсказка и текст для скринридеров не переводились ни в одной локали. Новый ключ top.themeToggle + обработчик data-i18n-title в applyI18n() (по образцу исправления aria-label поиска в v1.58.15) локализуют оба атрибута при загрузке и при каждой смене языка. Заблокировано tests/playwright-theme-toggle-i18n.mjs (9 локалей + смена в рантайме) и двумя статическими guard-тестами. Единственная находка уровня LOW из sign-off v1.61.0. (MINOR-001)
[1.61.0] — 2026-05-22
feat(i18n): добавлен французский как 9-й язык интерфейса. Новый словарь по локали public/js/lib/locales/i18n-dict.fr.js (window.__I18N_DICT_FR) с полным паритетом 668 ключей с английским; новый пакет справки docs/help/fr.md (19 H2 / 73 H3, точный структурный паритет с en). fr зарегистрирован в переключателе языка и автоопределении браузера (i18n.js), в ассемблере (i18n-dict.js), в index.html (тег <script> перед ассемблером), в снапшоте тестов и во всех списках локалей тестов. Исходная таблица перевода — из PR #9 (вклад сообщества). Без изменений логики: t() и все представления неизменны. 1001 / 1001 модульных тестов; прогон локалей Playwright расширен до 9 подтестов. (FR-LOCALE)
[1.60.0] — 2026-05-22
refactor(i18n): разбили 8-язычный мегафайл на файлы по локалям (I18N-SPLIT). Словарь переводов лежал в одном public/js/lib/i18n-dict.js; теперь это по одному файлу на язык в public/js/lib/locales/ плюс общий i18n-dict.aliases.js, чтобы переводчик правил один язык изолированно (раскладка как в i18next / OpenWA). i18n-dict.js стал небольшим сборщиком, который собирает тот же самый window.__I18N_DICT, поэтому t() и все вью не изменились. Загрузка синхронная через <script src> — без сборки и без fetch. Снимок (snapshot) доказывает, что миграция без потерь (678 ключей). Инструменты и ~25 тестов переведены на сплит через общий tests/helpers/i18n-vm.mjs; новые tests/i18n-locale-files.test.mjs и tests/playwright-locale-sweep.mjs (каждая страница × 8 локалей в реальном Chromium). 994 → 1000 модульных · 62 → 70 Playwright. Поведение не изменилось. (I18N-SPLIT)
[1.59.13] — 2026-05-21
fix(i18n): схлопывание ИСТИННЫХ дубликатов через @alias + финальная чистка персданных. Реальное имя мейнтейнера убрано из тест-фикстур (tests/health-doctor-unify, tests/llm-output) и архивных QA-отчётов (→ Jane Doe); LICENSE + package.json author переведены с настоящего юр. имени на публичный хэндл Fighter90. Добавлен механизм @alias в i18n.t(): ключ { '@alias': 'x.y' } резолвится в канонический. Схлопнуты 10 ключей, идентичных во всех 8 локалях (nav.help → help.title и т.д.). Пример контрибьютора nav.config/config.title НАМЕРЕННО не схлопнут — в испанском они расходятся (короткий Configuración в сайдбаре vs полный Configuración de la aplicación в заголовке). Coverage-тест, CI-чек и tools/i18n-audit.mjs стали alias-aware. Новый tests/i18n-alias.test.mjs. 991 → 994 теста. (I18N-CL3)
[1.59.12] — 2026-05-21
fix(i18n): чистка i18n-dict.js — перед fr-локалью (I18N-CL1, I18N-CL2, I18N-CL4). Аудит от внешнего контрибьютора французской локали. I18N-CL1 (приватность): в training.coursePh во всех 8 локалях лежало конкретное название сертификата вендора — заменено нейтральным плейсхолдером (Сертификация по облачной архитектуре и эквиваленты). I18N-CL2 (гигиена): fallback followup.lastPh был замороженным литералом 2026-04-21 — заменён локализованной подсказкой формата (YYYY-MM-DD / ГГГГ-ММ-ДД); живой динамический плейсхолдер today − 14d (U-3) в mode-page.js уже работал и теперь покрыт lock-тестом. I18N-CL4 (аудит): новый tools/i18n-audit.mjs + npm run audit:i18n в CI — жёстко падает на персданных, пробелах в паритете локалей, пустых значениях и голых датах. I18N-CL3 (решение): ~50 групп дубль-значений (напр. nav.scan / scan.btnRun / scan.col.company) намеренны — это разные роли UI, которые в неанглийских локалях переводятся по-разному; дедуп убрал бы гибкость, поэтому они — информационные предупреждения, не ошибки. 3 новых теста (988 → 991). Разблокирует PR французской локали. (I18N-CL1, I18N-CL2, I18N-CL4)
[1.59.11] — 2026-05-21
fix(test): v1.59.11 — e2e-comprehensive 12 падений в CI разобраны и закрыты. Двенадцать кейсов (Pipeline · Activity · Health · 7 mode-страниц · 404 · Profile) падали в каждом CI run начиная с v1.58.x, потому что page.goto(baseUrl + '/#/X') в Playwright — no-op для изменения только хэша. После того как CV-step ставил хэш #/cv, все следующие goto молча оставляли страницу на #/cv. Фикс: новый helper goRoute(hash) в tests/e2e-comprehensive.mjs делает bounce через about:blank перед каждым goto, форсируя реальную навигацию и ребутстрап SPA. Все 17 вызовов page.goto(${baseUrl}/#/…) заменены. Результат: 23 / 23 кейса зелёные · 988 / 988 unit · 20 / 20 smoke e2e. (e2e-harness-r1)
[1.59.10] — 2026-05-21
fix(api): NEW-F1-sub-r1 (v1.59.10) — middleware для сырого ../ поднят над всеми регистрациями /api-роутов. В v1.59.8 он стоял ПОСЛЕ app.all('/api/*') И после регистрации хендлеров, поэтому Express успевал нормализовать URL и middleware никогда не срабатывал. v1.59.10 поднимает guard в начало createApp() (выше любого register*Routes) — он смотрит на req.originalUrl до нормализации. Регекс: /^\/api(\/|$)/.test && /\.\.\//.test. Новый tests/api-path-traversal.test.mjs — 6 кейсов через сырой http.request (fetch нормализует .. на клиенте). 982 → 988 юнит-тестов. (NEW-F1-sub-r1)
[1.59.9] — 2026-05-21
fix(ux): UX-A5-r4 (v1.59.9) — debug-маркер data-toc-spy="active" + поведенческий lock-test scroll-spy на Help TOC. Шестой цикл закрытия: предыдущие 5 закрытий проходили статические тесты, но баг возвращался. v1.59.9 добавляет маркер (любой тестер проверит document.body.dataset.tocSpy === 'active'), синхронный initial paint, double-rAF re-compute, resize listener и полную очистку на hashchange. Новый tests/help-toc-spy-behavior.test.mjs гоняет алгоритм против 6 сценариев синтетической геометрии + 1 algorithm-parity check против help.js. 973 → 982 юнит-тестов. (UX-A5-r4)
[1.59.8] — 2026-05-21
fix(ux+api): v1.59.8 — UX-A5-r3 + NEW-F1-sub (HIGH + LOW в одном релизе). Исключение из правила «одна правка на релиз» — разрешено отчётом FINAL-REGRESSION-v1.59.7, чтобы закрыть 5-й цикл регрессии UX-A5. UX-A5-r3: #/help заменяет IntersectionObserver на простой scroll-listener c rAF-троттлингом — после 4 неудачных циклов с IO scroll-listener вычисляет активный заголовок по абсолютным координатам каждый кадр. NEW-F1-sub: middleware отклоняет сырые .. в /api/* через 404 JSON {error:'invalid path'}. 971 → 973 юнит-тестов. (UX-A5-r3 · NEW-F1-sub)
[1.59.7] — 2026-05-20
fix(api): NEW-D3-cache (v1.59.7) — GET /api/cv теперь шлёт Cache-Control: no-store. CV — главный артефакт пользователя; кеш браузера/прокси никогда не должен показывать вчерашний текст. (NEW-D3-cache)
[1.59.6] — 2026-05-20
feat(a11y): NEW-D2-motion (v1.59.6) — поддержка prefers-reduced-motion: reduce. Новый блок @media обнуляет анимации, переходы и scroll-behavior. WCAG 2.3.3 (AAA). (NEW-D2-motion)
[1.59.5] — 2026-05-20
fix(api): NEW-F1 (v1.59.5) — неизвестный /api/* возвращает JSON 404 на любом HTTP-методе. Замена app.get на app.all — устранён фолбэк на HTML SPA shell для POST/PUT/DELETE. (NEW-F1)
[1.59.4] — 2026-05-20
fix(ui): NEW-OR1 (v1.59.4) — устранена гонка обновления чипа Active/Keys на #/config. Атомарный replaceChildren + in-flight токен + кэш последнего валидного состояния. Закрывает пользовательский отчёт о вспышке 0/5 во время Save. (NEW-OR1)
[1.59.3] — 2026-05-20
fix(ux): UX-A5-r2 (v1.59.3) — усиление scroll-spy на #/help. rootMargin расширен с 10 % до 25 % видимой полосы + initial-state на mount. (UX-A5-r2)
[1.59.2] — 2026-05-20
fix(ui): v1.59.2 — чип Active/Keys: корректный счёт, капитализация имени провайдера, нет наложения на другие элементы. (post-v1.59.1 hotfix)
[1.59.1] — 2026-05-20
fix(test): v1.59.1 — NEW-D1 guard теперь принимает отполированный UX-A11 ES-копирайт. Регулярка расширена на candidaturas. (v1.59.1)
[1.59.0] — 2026-05-20
feat(ui): UX-A14 (v1.59.0) — мобильный (≤ 420 px) аудит-проход. Пять точечных правок в новом блоке @media (max-width: 420px). (UX-A14)
[1.58.65] — 2026-05-20
test(ui): UX-A2 (v1.58.65) — регресс-замок на структурный field-form Modes. Новый тест защищает реализацию v1.54.3 от регрессий. (UX-A2)
[1.58.64] — 2026-05-20
fix(i18n): UX-A11 (v1.58.64) — полировка es/pt-BR копирайта. Английские заимствования заменены родными эквивалентами. (UX-A11)
[1.58.63] — 2026-05-20
fix(ui): UX-A15 (v1.58.63) — плитка Pipeline на Dashboard получила приоритетный визуальный вес. Плитка Pipeline теперь выделяется акцентной рамкой, увеличенной иконкой и жирной подписью. (UX-A15)
[1.58.62] — 2026-05-20
feat(ui): UX-A9 (v1.58.62) — sticky-чип-сводка во вкладке API keys. В верхней части вкладки #/config → API keys появился sticky-чип с активным провайдером и количеством настроенных ключей. (UX-A9)
[1.58.61] — 2026-05-20
docs(readme): UX-A8 (v1.58.61) — раздел очистки при первом запуске добавлен во все 8 READMEs. Теперь документирован шаг make clean-test-fixtures, очищающий два QA-фикстурных URL перед первым сканом. (UX-A8)
[1.58.60] — 2026-05-20
feat(ui): UX-A12 (v1.58.60) — drawer уведомлений: Очистить все + закрыть отдельную запись. Добавлена кнопка глобальной очистки и × у каждой записи в панели уведомлений. (UX-A12)
[1.58.59] — 2026-05-20
feat(ui): UX-A13 (v1.58.59) — действенный CTA «Fix →» на падающих строках #/health. Строки FAIL/OPTIONAL теперь показывают ghost-кнопку, которая ведёт сразу к нужной вкладке настроек. (UX-A13)
[1.58.58] — 2026-05-20
fix(ux): UX-A10 (v1.58.58) — защита #/cv от потери несохранённой правки. Теперь при закрытии вкладки (beforeunload) и при внутренней SPA-навигации (hashchange) показывается локализованное подтверждение, если буфер изменён. (UX-A10)
[1.58.57] — 2026-05-20
test(ui): UX-A7 (v1.58.57) — регресс-замок на контракт авто-обновления cost-line. Новый статический тест гарантирует диспатч события providers-changed, подписку и вызов UI.providerCostHint во всех advisor-вьюхах. (UX-A7)
[1.58.56] — 2026-05-20
fix(a11y): UX-A4 (v1.58.56) — .lang-btn соответствует минимальному размеру цели нажатия WCAG 2.5.8. Раньше кнопки языка были высотой 23–25 px и не дотягивали до порога 24×24 px. Теперь min-height: 28px + min-width: 28px обеспечивают соответствие WCAG 2.2 AA. (UX-A4)
[1.58.55] — 2026-05-20
feat(ui): UX-A3 (v1.58.55) — чип активного провайдера на Dashboard. В герое #/dashboard теперь видно, какой LLM-провайдер активен (⚡ Live evals: Anthropic claude-sonnet-4-6 либо 📋 Manual prompt mode). Автоматически обновляется при смене LLM_PROVIDER в #/config и при возврате фокуса во вкладку. (UX-A3)
[1.58.54] — 2026-05-20
fix(ux): UX-A1 (v1.58.54) — защитное предупреждение о структуре Deep-брифа. Если в сохранённом брифе отсутствует не менее 3 из 6 канонических разделов (Company snapshot / Engineering culture / Recent news / Glassdoor / Interview process / Negotiation leverage), public/js/views/deep.js показывает над контентом неблокирующее предупреждение со ссылкой на канон. Это UI-страховка; корневая правка промпта живёт в родительском проекте. (UX-A1)
[1.58.53] — 2026-05-20
fix(ux): UX-A6 (NEW-M4-r1) — каждая saved-research карточка проходит через единый helper renderSavedCard(). Верификационный регресс v1.58.51 показал: одна карточка рендерилась без структурных детей (title+date как один text node — software-engineer-generalyesterday), а другая с правильной <span> + <time> структурой. Правка в public/js/views/deep.js: извлечён renderSavedCard(f), всегда выдающий .saved-card__title + .saved-card__date datetime=…. Любой render-путь (page-load renderArchive, post-Run live, будущие точки) проходит через единый helper — flex-gap layout M-4 v1.58.11 работает только с правильными структурными детьми. 948 → 949 модульных. (UX-A6)
[1.58.52] — 2026-05-20
fix(ux): UX-A5 (NEW-K1) — scroll-spy для TOC на #/help снова работает (регресс из v1.58.45). Верификационный регресс v1.58.51 показал: 18 H2-элементов с id, 18 TOC <a> ссылок, но 0 ссылок получают .toc-current при скролле. Причина: в v1.58.45 observer был обернут в setTimeout(0), который срабатывал ДО того, как роутер монтировал view в #content; document.querySelectorAll(".help-article h2[id]") не находил ничего. Правка в public/js/views/help.js: новый mountTocSpy() наблюдает за синхронно построенными refs headings (без re-query документа); deferred через двойной requestAnimationFrame — наблюдатель привязывается после первого paint, который включает смонтированный view. 947 → 948 модульных. (UX-A5)
[1.58.51] — 2026-05-20
chore(docs): v1.58.51 — финальный housekeeping цикла v1.58.37 → v1.58.50 (14 single-fix релизов). Без изменений кода; консолидация qa/, валюты документов, и две lessons captured из этого цикла. (1) Реорганизация qa/: все version-locked документы — FIX-PROMPT-CONSOLIDATED.md, FIX-PROMPT-FINAL-EXHAUSTIVE.md, FIX-PROMPT-v1.58.37_and_beyond.md, плюс 6 session-снимков из корня — перемещены в qa/archive/v158-cycle/. В qa/ корне теперь ровно 6 канонических perennial документов. (2) qa/REGRESSION-FINAL.md §13 документирует каждый инвариант v1.58.37→.50 с его lock-тестом, организованный по классам + 2 новые «lessons captured» (markdown-bold regex pitfall; Publish-runs-tests-against-tagged-ref pitfall). (3) qa/UX-AUDIT-PROMPT.md baseline таблица расширена 14 новыми closed-in строками. (4-7) Currency CLAUDE.md / .claude/PROJECT-CONTEXT.md / docs/ROADMAP.md / README ×8. Test baseline на v1.58.51: 947 unit (структурно без изменений) · 62 Playwright · 20 smoke · 23 comprehensive E2E. Закрывает pattern v1.58.48 / v1.58.50 Publish-failure — тегируем на коммите, где ВСЕ тесты проходят, а не на коммите с follow-up патчами только на main. (housekeeping)
[1.58.50] — 2026-05-20
docs: DOC-1 — qa/REGRESSION-FINAL.md получает §5a, документирующую тело ошибок сервера как English-by-policy. v1.58.36 аудит поднял NEW-D4: server 4xx body на любой локали английский. Два пути: (A) confirm by-design, задокументировать контракт; (B) читать Accept-Language и локализовать. Spec рекомендовал A — закрытие NEW-D4 как not-a-finding минимально disruptive и сохраняет тесты стабильными. Новая секция §5a в qa/REGRESSION-FINAL.md объясняет: JSON-тела ошибок сервера остаются английскими (debuggability boundary — bug-репорты копируются чисто между локалями, CI-фикстуры стабильны, server-tests не нуждаются в parallel locale strings). SPA оборачивает ответы в локализованный chrome (цвет toast, U-4 Details summary). Accept-Language намеренно не читается; SPA-side lang стрипается перед validateConfig (инвариант v1.57.2). v1.59 option B (локализованные server errors через { error, error_en, code }) — future gate. 946 → 947 модульных. Закрывает очередь v1.58.37 → v1.58.50 из FIX-PROMPT-FINAL-EXHAUSTIVE.md — 14 single-fix релизов отгружены, каждый CI-green + AI-review LGTM. (DOC-1)
[1.58.49] — 2026-05-20
chore(tooling): TOOL-1 — make clean-test-fixtures + scripts/clean-test-fixtures.mjs. v1.58.36 аудит: ${CAREER_OPS_ROOT}/data/pipeline.md накопил 1252+ строки с example.com/job/<n> от регрессионных прогонов. Чистка вручную утомительна. Новый scripts/clean-test-fixtures.mjs читает ${CAREER_OPS_ROOT or ..}/data/pipeline.md, удаляет каждую строку с example.com (case-insensitive), сохраняет реальные ATS URL без изменений, печатает счёт и завершается с кодом 0. Флаг --dry-run печатает результат в stdout, не трогая файл. Новый Makefile с целями make clean-test-fixtures и make clean-test-fixtures-dry-run. 4 новых CI-isolated теста в tests/clean-test-fixtures.test.mjs (через mkdtempSync — никаких реальных file-writes). 942 → 946 модульных (+4 новых TOOL-1 кейса). (TOOL-1)
[1.58.48] — 2026-05-20
fix(ux/onboarding): UX-D-B — на #/dashboard появляется глобальный предупреждающий баннер, если пользователь всё ещё на дефолтном fixture-профиле. v1.58.36 аудит: /api/health уже возвращает { name: "Profile customized", ok: false } (server/lib/store.mjs проверяет Acceptance Test / Jane Smith / прочие шаблонные имена), но пользователь видел это только зайдя на #/health. При этом каждое advisor-output (Apply / Followup / Contacto / Deep) обращалось к fixture-имени — испорченное первое впечатление. Новый profileFixtureBanner() в public/js/views/dashboard.js рендерит .hero-banner.hero-banner--warning в начале маршрута, когда чек fail, с локализованным сообщением (onboarding.fixtureWarning × 8) + CTA-кнопкой на #/config (onboarding.fixProfile × 8). Когда пользователь меняет профиль, /api/health возвращает ok: true и следующий mount dashboard молча убирает баннер. Новые CSS правила .hero-banner + .hero-banner--warning. 941 → 942 модульных. (UX-D-B)
[1.58.47] — 2026-05-20
fix(ux/naming): UX-D-C — кнопка Quick scan в шапке переименована в Открыть Scan, чтобы метка соответствовала поведению. v1.58.36 аудит: «Quick scan» намекало на мгновенный скан с дефолтными настройками, но обработчик на public/js/app.js только навигирует на #/scan — сам скан не запускается. Переименовано через ключ top.quickscan во всех 8 локалях (en Open Scan / es Abrir Scan / pt-BR Abrir Scan / ko Scan 열기 / ja Scan を開く / ru Открыть Scan / zh-CN 打开 Scan / zh-TW 開啟 Scan). Дефолтный текст в HTML также обновлён. Поведение без изменений; только честность метки. 940 → 941 модульных. (UX-D-C)
[1.58.46] — 2026-05-20
fix(ux): UX-D-D — checklist на #/apply подставляет {company}-{role} слагами, вычисленными из URL/JD. v1.58.36 аудит: item 5 чеклиста читался как Save filled answers to interview-prep/{company}-{role}.md before submitting. — литералы {company}-{role} отрисовывались дословно, пользователь должен был мысленно подставлять значения (или, хуже, оставлял их как есть). Новые extractSlugs(url, jd) + substitutePlaceholders(text, url, jd) в public/js/views/apply.js: white-list хостов (greenhouse / lever / ashby / workable / smartrecruiters / workday) выделяет company из path или поддомена, затем role из последнего сегмента пути (с обрезкой trailing numeric id) — или, как fallback, из первой строки JD. Если извлечение не удалось (unknown host / нет JD), плейсхолдеры становятся [company] / [role] (square-bracket convention для «заполни сам»). Substitution выполняется один раз перед parseChecklist, поэтому live checklist и Copy-unchecked output остаются согласованными. 939 → 940 модульных. (UX-D-D)
[1.58.45] — 2026-05-20
fix(ux): UX-D-K — scroll-spy в TOC #/help подсвечивает текущую секцию. v1.58.36 аудит: при прокрутке тела help страница TOC (sticky, ~92 H2-секций) не показывал, какую секцию читает пользователь. Новый IntersectionObserver в public/js/views/help.js наблюдает за каждым .help-article h2[id] и применяет .toc-current к соответствующей <a> ссылке TOC, когда H2 попадает в верхнюю треть viewport (rootMargin: "-30% 0% -60% 0%"). CSS public/css/app.css задаёт .toc-current левый бренд-бордер + цвет var(--rausch) + font-weight: 600. При hashchange observer отключается рядом с существующим scroll-listener — никаких утечек. 938 → 939 модульных. (UX-D-K)
[1.58.44] — 2026-05-20
fix(ux): UX-D-L — на #/deep у открытого Saved-research brief появилась inline-кнопка × для закрытия. v1.58.36 аудит: клик по карточке Saved-research отрисовывал тело brief’а внутри страницы (Copy / Download / Open in tab / Generate PDF в шапке), но закрыть brief можно было только прокруткой или навигацией. Новая кнопка × в public/js/views/deep.js в шапке showResult() — очищает out.innerHTML, повторяет паттерн закрытия модалки (api.js UI.modal × button), доступна с клавиатуры, aria-label + title из нового ключа deep.closeBrief × 8 локалей. 937 → 938 модульных. (UX-D-L)
[1.58.43] — 2026-05-20
fix(ux): UX-D-F — пустой submit на #/evaluate теперь показывает отдельный локализованный toast (раньше показывал «too short»). До правки клик Evaluate при пустой textarea проваливался в существующую проверку <50 chars и выдавал JD too short (min 50 chars) — точно, но бесполезно: реальная проблема — «вы ничего не ввели», а не «вы ввели мало». В public/js/views/evaluate.js теперь первый чек !jd показывает новый eval.emptyJd («JD обязателен — вставьте полное описание вакансии», локализовано × 8) и фокусирует textarea, чтобы пользователь сразу начал печатать. Существующий eval.shortJd всё ещё срабатывает для 1–49 символов. 936 → 937 модульных. (UX-D-F)
[1.58.42] — 2026-05-20
fix(ux): UX-D-J — паритет ETA-чипа на всех advisor-страницах с #/auto (UX-6 v1.55.4). v1.58.36 аудит: только #/auto показывал честный «⏱ ~1–2 min» рядом с Run; остальные 7 LLM-страниц (#/evaluate, #/deep, 5 mode-страниц — project/training/followup/contacto/interview-prep/patterns) совершали похожий 10-60 с вызов и не давали подсказку про время. Добавлен <span class="advisor-eta">⏱ ~30s</span> (локализация через новый ключ advisor.eta × 8) рядом с UI.providerCostHint(t) в evaluate.js, deep.js, mode-page.js. CSS правило .auto-eta расширено на .advisor-eta с тем же стилем. Оригинальный auto.eta остаётся «~1–2 min» — это единственный multi-step SSE-пайплайн. 935 → 936 модульных. (UX-D-J)
[1.58.41] — 2026-05-20
fix(ux/truthfulness): UX-D-I — cost-hint теперь перечитывает /api/status/providers при возврате фокуса на вкладку и по событию providers-changed (последствие M-7 v1.58.12). v1.58.12 связал UI.providerCostHint(t) с /api/status/providers, но запрос делался ОДИН раз при создании ноды. Если пользователь открывал #/config в другой вкладке, менял провайдера и возвращался — cost-line молча отображал старое значение, пока не происходил переход с маршрута и обратно. Правка в public/js/api.js: извлечена именованная функция refreshCostLine() и привязана к document.visibilitychange (фокус вернулся на вкладку) + новому CustomEvent("providers-changed"). Обработчик Save в public/js/views/config.js диспатчит этот event после успешного POST, и cost-line на #/auto, #/deep, #/evaluate, mode-страницах обновляется без перезагрузки страницы или re-mount маршрута. 934 → 935 модульных. (UX-D-I)
[1.58.40] — 2026-05-20
fix(ux/docs): UX-D-H — regression-lock: каждая видимая ссылка career-ops.org/docs/<path> обязана быть кликабельной. v1.58.36 аудит подтвердил живьём: каждая такая ссылка в public/js/views/*.js уже находится внутри c("a", { href, target: "_blank", rel: "noopener noreferrer" }, …) (apply.js / batch.js / config.js / reports.js), и каждая ссылка в docs/help/*.md использует markdown [text](https://github.com/Fighter90/career-ops-ui/blob/main/url). Поэтому в этом релизе только regression lock: новый tests/external-doc-links.test.mjs парсит каждый views/*.js и docs/help/*.md и валит сборку, если career-ops.org/docs/<path> URL рендерится как plain child text. Бренд-упоминания career-ops.org без /docs/ пути допускаются (например, «career-ops.org schema» в прозе). 932 → 934 модульных. (UX-D-H)
[1.58.39] — 2026-05-20
fix(ux): NEW-D2 — на #/dashboard появилась кнопка ↻ Refresh в шапке с явным toast-фидбэком (отличная от Refresh из connection-banner — M-9 v1.58.14). До правки на #/dashboard не было in-place обновления — пользователь использовал banner Refresh, который делает полный location.reload() и теряет позицию скролла. Новая кнопка в public/js/views/dashboard.js перечитывает /api/dashboard и перерисовывает view через Router.go("/dashboard"). Toast-пайплайн: «Обновление…» → «Панель обновлена» (success) / локализованная ошибка. Два новых i18n-ключа × 8 локалей: dash.refreshAria, dash.refreshed. 931 → 932 модульных. (NEW-D2)
[1.58.38] — 2026-05-20
fix(a11y): NEW-D3 (WCAG 4.1.2) — поле поиска на #/tracker получает локализованный aria-label, отличный от placeholder. v1.58.36 аудит: у поля поиска был только placeholder="Search by company / role…" и не было aria-label — скрин-ридеры озвучивали generic-роль «edit text» без описания назначения. По WCAG 4.1.2 (Name, Role, Value) одиночное search-поле без <label> ОБЯЗАНО иметь явный accessible name. Правка в public/js/views/tracker.js: filterText теперь декларирует type="search" + aria-label: t(track.searchAria, …). Новый ключ track.searchAria во всех 8 локалях — значения явно отличаются от placeholder («Поиск заявок по компании или роли» vs «Поиск по компании / роли…»), чтобы SR не повторял ту же строку дважды. 930 → 931 модульных. (NEW-D3)
[1.58.37] — 2026-05-20
fix(i18n): NEW-D1 — H1 на #/pipeline локализован в es/pt-BR/ru + закрыты 2 случайные RU-утечки в title-ключах. v1.58.36 аудит показал, что заголовок #/pipeline отрисовывался как литеральное Pipeline на es/pt-BR/ru, тогда как сайдбар уже был локализован (Vacantes / Vagas / Воронка) — нарушение паритета с закрытым I18N-011 (v1.58.18). Обновлены значения pipe.title: es → Pipeline de vacantes, pt-BR → Pipeline de vagas, ru → Воронка вакансий. Новый статический guard tests/i18n-no-latin-leaks.test.mjs парсит DICT и валит сборку, если значение *.title на ru/ja/ko/zh-CN/zh-TW состоит только из латиницы вне небольшого whitelist (собственные имена, акронимы, названия продуктов). Этот же guard сразу поймал 2 дополнительные RU-утечки: contacto.title (LinkedIn outreach → Касания через LinkedIn) и health.title (Health → Состояние) — обе исправлены в том же релизе. 928 → 930 модульных. (NEW-D1)
[1.58.36] — 2026-05-20
chore(docs): v1.58.36 — полный housekeeping sweep на закрытии цикла v1.58.x. Без изменений кода — синхронизация документации + структуры qa + базовых тестовых счётчиков после 32 single-fix релизов. (1) Реорганизация qa/: три снимка регрессии конкретных версий (REGRESSION-END-TO-END-v1.58.16/33/35.md) перемещены в новый qa/archive/v158-cycle/ вместе с pre-cycle MASTER regression и FIX-PROMPT-v1.58.3.md; qa/v158-regression/ теперь содержит только активный FIX-PROMPT-v1.58.4_and_beyond.md (все строки ✅). (2) qa/REGRESSION-FINAL.md получил §12: каждый инвариант цикла v1.58.x (CSP безусловный, 5 provider rows, :focus-visible ring, поведение drawer, help §18 и т.д.) с lock-тестом из tests/qa-report-fixes.test.mjs. (3) qa/UX-AUDIT-PROMPT.md базовая таблица расширена 30 закрытыми строками v1.58.x. (4) docs/architecture/: в FRONTEND.md добавлен раздел drawer уведомлений + заметки v1.58.24 toast-postfix + v1.58.10 modal-drain; OVERVIEW.md ссылается на drawer; TESTING.md итоги подняты до 928 unit / 117 files / 62 Playwright / 20 smoke / 23 comprehensive E2E. (5) CLAUDE.md получил раздел «Hard-won lessons (v1.58.x cycle)» с 10 запомненными ловушками. (6) .github/copilot-instructions.md baseline поднят до 928 unit / 62 Playwright. (7) README ×8 локалей: новая строка «Уведомления 🔔» в page-table + устаревший счётчик тестов исправлен 284 unit + 12 Playwright → 928 + 62 + 23 + 20 (baseline @ v1.58.35). Тестовая база без изменений: 928 unit · 62 Playwright · 20 smoke · 23 comprehensive E2E. (housekeeping)
[1.58.35] — 2026-05-20
fix(ui): v1.58.35 — drawer уведомлений больше не открывается сам по себе + новая §18 «Уведомления» в справке (отчёт пользователя). Две связанные проблемы. (1) Авто-открытие / нерабочее закрытие: .notif-drawer { display: flex } и .notif-badge { display: inline-flex } из v1.58.34 имели тот же author-level специфичность, что и UA правило [hidden] { display: none }, поэтому явное display: flex/inline-flex побеждало, и атрибут hidden ничего не делал — drawer был виден на каждой загрузке, а клик по закрытию ничего не давал. Исправлено в public/css/app.css: добавлены явные .notif-drawer[hidden] { display: none } и .notif-badge[hidden] { display: none }. Теперь drawer открывается ТОЛЬКО по клику на колокольчик (или Enter/Space при клавиатурном фокусе); статический guard в тестах фиксирует ровно одну точку вызова open() в app.js (тернарник клика по колокольчику), чтобы будущие правки случайно не добавили ещё одну авто-точку. (2) Категории не задокументированы: новая §18 «Уведомления» во всех 8 локалях справки (docs/help/{en,es,ja,ko-KR,pt-BR,ru,zh-CN,zh-TW}.md) — 3 H3-подсекции (Категории уведомлений / Что НЕ является уведомлением / Клавиатура) описывают каждый источник toast (Success / Error / Info-progress), что значит каждая визуальная пометка, что НЕ попадает в drawer (модалки Doctor/verify, SSE log-линии, spinner-only) и клавиатурный контракт. Help-section parity baseline 17 → 18; H3 baseline 70 → 73. 927 → 928 модульных. (отчёт пользователя)
[1.58.34] — 2026-05-20
feat(ui): v1.58.34 — Drawer уведомлений (полностью закрывает U-13). v1.58.33 положил данные (in-memory toastHistory cap 50 + UI.getToastHistory()) и явно отложил UI-обвязку. Сейчас она здесь. UI.onToast(fn) — pub/sub поверх capture в public/js/api.js; подписчики получают только что записанный entry, и каждый коллбек ловится в try/catch — баг в drawer-е никогда не порвёт toast-конвейер. Колокольчик 🔔 в шапке с красным badge непрочитанных, aria-haspopup="dialog" + aria-controls + aria-expanded. Right-slide <aside role="dialog"> с локализованным заголовком (notif.title), пустым состоянием (notif.empty) и элементами: локализованный toLocaleTimeString + сообщение + (если есть) технический хвост из U-4. Новейшие сверху; самые старые выпадают по cap 50. Esc + кнопка закрыть + повторный клик по колокольчику — закрывают drawer; фокус возвращается на колокольчик (WAI-ARIA APG drawer pattern). 4 новых ключа i18n × 8 локалей. CSS: .notif-bell, .notif-badge, .notif-drawer, .notif-drawer__head, .notif-item. 926 → 927 модульных; Playwright 61/61 без изменений. (U-13 follow-up)
[1.58.33] — 2026-05-20
fix(ux): U-13 + U-14 + U-15 — журнал toast-уведомлений (кеп 50 + UI.getToastHistory()) + предохранительное CSS-правило .page-header h1 + p + индикатор несохранённых изменений в #/cv. Финальный релиз цикла v1.58.x. U-13: каждый вызов UI.toast() пушит { ts, type, message, detail } в in-memory toastHistory с лимитом 50; экспонирован через UI.getToastHistory() — будущая drawer-UI отложена. U-14: новое правило .page-header h1 + p { margin-block-start: var(--space-2); color: var(--foggy); } для страниц с сырым <h1>+<p> без класса .page-subtitle. U-15: кнопка Save в #/cv получает класс .btn-dirty + локализованный tooltip cv.unsaved × 8, когда текстарея расходится с последним сохранённым baseline; успешный Save переустанавливает baseline. Загрузка CV диспатчит синтетический input-event, чтобы программное ta.value = … тоже включало dirty. CSS .btn.btn-dirty рисует кольцо --rausch-dark + ● префикс. 925 → 926 модульных. (U-13/U-14/U-15)
[1.58.32] — 2026-05-20
fix(ux): U-12 — поле фильтра TOC справки получает min-width: 16ch чтобы KO/JA плейсхолдеры не обрезались. Корейский 섹션 필터 и японский セクションをフィルター шире EN Filter sections на 5-10%. Поле уже было width: 100% внутри .help-toc, но при сжатии карточки placeholder клиппинговался. Новый класс .help-toc__filter с min-width: 16ch гарантирует, что плейсхолдер вмещается во всех 8 локалях. 924 → 925 модульных. (U-12)
[1.58.31] — 2026-05-20
fix(ux): U-11 — заголовок столбца Legitimacy в трекере получил локализованный info-чип ⓘ с tooltip про шкалу High/Caution/Suspicious. До правки бейдж в строке читался как High / Caution / Suspicious без подсказки в шапке, что они значат. В public/js/views/tracker.js <th> теперь рендерит лейбл + <span class="th-info" tabindex="0" role="img">ⓘ</span>; title + aria-label берутся из нового ключа track.col.legitimacy.help × 8 локалей: «Достоверность вакансии (высокая / осторожно / подозрительная).» CSS .th-info:focus-visible даёт фокусное кольцо — чип доступен с клавиатуры (WCAG 2.4.7). Параллельно поправлен lock-тест #25 destructive buttons have a title hint, который после U-10 проверял только track.fixHint, но теперь tooltip ветвится между track.fixHint и track.fixEmpty. 923 → 924 модульных. (U-11 + U-10 follow-up)
[1.58.30] — 2026-05-20
fix(ux): U-10 — кнопки Normalize / Dedup / Merge на трекере отключаются, когда data/applications.md пуст. v1.58.3 QA: до правки клик по ним на пустом трекере всё равно дёргал эндпоинты переписывания parent-проекта — холостая операция, о которой пользователь не знал. Теперь в public/js/views/tracker.js каждая кнопка получает disabled + aria-disabled + локализованный tooltip (track.fixEmpty × 8 локалей): «Сначала добавьте строку в трекер — это переписывает data/applications.md, а переписывать пока нечего.» При появлении строк кнопки возвращаются к привычному track.fixHint. 922 → 923 модульных. (U-10)
[1.58.29] — 2026-05-20
fix(ux): U-9 — строка «счётчик ↔ фильтр» на #/pipeline теперь складывается вертикально на узких экранах. До правки при ≤ 720 px чип «In queue: N» и поле фильтра спорили за горизонталь и поле уезжало в узкую колонку. Новый класс .pipeline-controls + CSS под @media (max-width: 720px) ставит flex-direction: column и тянет #pipe-filter на 100% ширины. Десктоп без изменений. 921 → 922 модульных. (U-9)
[1.58.28] — 2026-05-20
fix(ux): U-8 — блок сгенерированного prompt по умолчанию свёрнут на 7 mode-страницах. Раньше после клика «Generate prompt» inline <pre> уходил на 1200+ px и ронял Copy + Run-live ниже сгиба. В public/js/views/mode-page.js showPrompt() теперь оборачивает <pre> в <details class="prompt-block"> (по умолчанию закрыт); summary показывает «Показать prompt (N строк)» через новые ключи prompt.show / prompt.lines × 8 локалей. Copy + Run-live сразу видны. Параллельно исправлена регрессия теста BUG-007/008: после U-7 (v1.58.27) между UI.dismissToast() и UI.modal(t(…)) появился промежуточный const stripped = …; жёсткое требование «соседних строк» ослаблено до «dismissToast вызывается перед modal в окне ≤ 1200 символов». 920 → 921 модульных. (U-8)
[1.58.27] — 2026-05-20
fix(ux): U-7 — ASCII-разделители === от verify-pipeline.mjs убраны из модалки результата. Скрипт печатает ========= (≥ 50 символов) между секциями; в 14 px моноширинной модалке такая полоса визуально ломала ширину. Теперь в public/js/views/health.js перед рендером применяется .replace(/^={10,}$/gm, "") — секции по-прежнему разделяются пустыми строками. 919 → 920 модульных. (U-7)
[1.58.26] — 2026-05-20
fix(ux): U-6 — чип «Активные компании N/M» на #/scan теперь объясняет N и M через tooltip + aria-label. До правки чип читался как «▸ Активные компании 96/80» без подсказки, что значит 96 и 80. Теперь <button> несёт title и aria-label из нового ключа scan.activeCo.help во всех 8 локалях: «Активные: компании, по которым сейчас есть результаты. Всего: настроено в portals.yml.» 918 → 919 модульных. (U-6)
[1.58.25] — 2026-05-20
fix(ux/ia): U-5 — дедуп CTA на Dashboard (убраны кнопка Open Pipeline в шапке и плитка Scan all sources в Quick actions). v1.58.3 QA нашла 4× Pipeline / 4× Scan точек входа на Dashboard. Hero уже даёт ✨ Auto-pipeline a URL + 🌐 Scan now, сайдбар уже ведёт к /pipeline — двойники в шапке и Quick actions были чистым шумом. Удалены, hero + sidebar остаются каноническими entry-points. 917 → 918 модульных. (U-5)
[1.58.24] — 2026-05-20
fix(ux): U-4 — toast-уведомления об ошибках прячут хвост “(METHOD /path · HTTP NNN)” в свернутый <details>. v1.57.1 добавил what/where/why postfix к каждой ошибке API; технический хвост обязан оставаться в DOM (инвариант BUG-006), но в заголовке тоста он конкурирует с человеческой фразой. Теперь UI.toast() парсит хвост через TOAST_ENDPOINT_RE и кладёт его в <details class="toast-detail"> с локализованным <summary> (toast.details × 8). Заголовок чистый, технический детал — в одном клике. Инвариант BUG-006 сохраняется. 916 → 917 модульных. (U-4)
[1.58.23] — 2026-05-20
fix(ux): U-3 — плейсхолдер lastContact в #/followup теперь = сегодня − 14 дней (был замороженный ISO 2026-04-21). Статичный плейсхолдер устаревает: пользователь читает его и как формат, и как “недавнюю дату” — последнее уплывает в прошлое. Теперь считаем в момент рендера: new Date() → setDate(getDate() - 14) → ISO YYYY-MM-DD. Спец-кейс только для followup.lastContact; остальные поля по-прежнему из i18n. 915 → 916 модульных. (U-3)
[1.58.22] — 2026-05-20
fix(ux): U-2 — заголовок #/auto больше не переносится на 2 строки из-за ведущего ✨. Эмодзи вынесен из i18n-значения auto.title в отдельный <span class="page-icon" aria-hidden="true">; .page-header--icon использует CSS grid с отдельной колонкой под иконку. Во всех 8 локалях. 914 → 915 модульных. (U-2)
[1.58.21] — 2026-05-20
fix(ux): U-1 — H1 + подзаголовок #/cv теперь как у остальных страниц (отменяет UX-9 из v1.56.0 by design). Чип .cv-breadcrumb удалён; заголовок снова <h1 class="page-title"> + <p class="page-subtitle">. Инвариант одного <h1> сохраняется (F-V54-A продолжает сдвигать пользовательский # Имя в h2 в превью). 913 → 914 модульных. (U-1)
[1.58.20] — 2026-05-20
fix(i18n/platform): I-6 — подсказка хоткея в подвале сайдбара показывает ⌘K на Mac и Ctrl+K в остальных ОС, с локализованным глаголом. До правки буквальное CTRL+K — search отображалось на любой платформе и любом языке. top.langhint теперь использует шаблон {hotkey} — поиск; applyFooterHotkey() подставляет {hotkey} платформенной комбинацией через navigator.platform. 915 → 916 модульных. (I-6)
[1.58.19] — 2026-05-20
fix(i18n): I-4 — RU-строки #/followup без латиницы cadence / follow-up. Заголовок H1 переписан с «Советник по cadence follow-up» → «Советник по ритму касаний»; подсказка «ISO-дата (YYYY-MM-DD) — основа для cadence.» → «ISO-дата (ГГГГ-ММ-ДД) — основа для расчёта ритма касаний.»; «timeline, internal hold» → «сроки, внутренняя пауза»; companyHint «scope» → «область». 914 → 915 модульных. (I-4)
[1.58.18] — 2026-05-20
fix(i18n): I-3 — пункты 2/5/13/14 в TOC справки без английских остатков в нелатинских локалях. До правки в части help-бандлов оставались ## 2. App settings & API keys, ## 5. Portals & Sources, ## 13. Mode prompts, ## 14. Apply checklist. Теперь полностью локализованы во всех 8 локалях (RU Подсказки режимов / Чек-лист отклика). 913 → 914 модульных. (I-3)
[1.58.17] — 2026-05-20
fix(i18n): I-2 — даты на карточках Saved-research теперь используют Intl.RelativeTimeFormat для активной локали. В public/js/views/deep.js formatRelative() возвращал «today» / «1d ago» / «Nd ago» на любом языке. Заменено на Intl.RelativeTimeFormat(I18n.getLang(), { numeric: 'auto' }) — нативная локализованная строка («сегодня/вчера/N дней назад», «今日/昨日», и т.д.). Даты старше 7 дней — Intl.DateTimeFormat(locale, { dateStyle: 'medium' }). 912 → 913 модульных. (I-2)
[1.58.16] — 2026-05-20
fix(ui): мерцание hover на брендовых кнопках (отчёт пользователя). Причина: дефолтный фон .btn-primary / .btn-danger — linear-gradient(...), в :hover менялся на сплошной var(--rausch-dark). CSS не умеет интерполировать градиент↔сплошной цвет, поэтому 180 мс transition: background «защёлкивался» и пользователь видел белую/розовую вспышку. Правка в public/css/app.css: сохраняем градиент при hover и затемняем через filter: brightness(0.92) — filter плавно интерполируется во всех браузерах. В список transition для .btn добавлен filter var(--transition), чтобы затемнение анимировалось. 911 → 912 модульных. (отчёт пользователя)
[1.58.15] — 2026-05-20
fix(a11y/i18n): I-1 — aria-label и визуально-скрытый <label> верхнего поиска теперь локализуются. До этого скрин-ридер на любом языке слышал английский aria-label. В public/js/app.js добавлен универсальный хук data-i18n-aria-label — applyI18n() обновляет aria-label при каждой смене языка, симметрично уже работающим data-i18n / data-i18n-placeholder. Добавлены два новых ключа i18n (top.search.aria, top.search.label) во все 8 локалей. Хук переиспользуем для любых будущих элементов. 910 → 911 модульных. (I-1)
[1.58.14] — 2026-05-20
fix(ux): M-9 — кнопка «Обновить» в connection-баннере теперь даёт обратную связь (раньше — молчаливый reload). До v1.58.13 обработчик сразу вызывал location.reload(). Теперь сначала показывается тост «Обновление…», ставится sessionStorage['refreshedToast'], кнопка переводится в disabled (защита от двойных кликов), и location.reload() откладывается на 200 мс, чтобы тост успел отрисоваться. На следующей загрузке app.js видит флаг и выводит успешный тост «Обновлено». Добавлены 2 новых ключа i18n (common.refreshing, common.refreshed) во все 8 локалей. 909 → 910 модульных. (M-9)
[1.58.13] — 2026-05-20
fix(ux): M-8 — чек-лист на #/apply стал интерактивным. До v1.58.13 «▶ Сформировать чек-лист» выдавал пункты 0…7 в моноширинном <pre>-блоке без возможности отмечать. Теперь каждый пункт — настоящий <input type="checkbox">, обёрнутый в <label> (область клика — вся строка, WCAG 2.5.5). Состояние сохраняется по URL в localStorage['applyChecklist:'+slug] — отметили 3 пункта → перезагрузка → 3 остались отмеченными. Кнопки: Скопировать невыполненные (выводит оставшиеся пункты в виде - markdown-списка) и Сбросить. Добавлено 5 новых ключей i18n (apply.checklist.copyUnchecked, resetBtn, copied, copyFailed, reset) во всех 8 локалях. Защитный fallback, если парсер не нашёл пунктов. 908 → 909 модульных. (M-8)
[1.58.12] — 2026-05-20
fix(ux): M-7 — строка стоимости теперь следит за активным провайдером (OpenRouter больше не падает на придуманное число). UI.providerCostHint() уже шёл через /api/status/providers и был provider-aware, но мапы в public/js/api.js включали только anthropic/gemini/openai/qwen. После v1.57.0 с OpenRouter (5-й провайдер) он попадал на общий fallback 0,03 и выводил строчный литерал openrouter как имя. Теперь EST содержит openrouter: null (роутер сам выбирает модель — стоимость варьируется), а ветка === null выводит локализованное «cost varies (router picks)» вместо ложного ~$0.03/eval. В NAME добавлен openrouter: 'OpenRouter'. Новый ключ i18n cost.varies во всех 8 локалях. 907 → 908 модульных. (M-7)
[1.58.11] — 2026-05-20
fix(ux): M-4 — отступ между заголовком и датой на карточке сохранённого исследования теперь структурный CSS (раньше — inline margin). MASTER-регрессия v1.58.3 показала, что на части карточек выводилось software-engineer-generaltoday (без пробела между заголовком и датой), на других — нормально. Раньше код опирался на style="margin-left: 8px" между двумя голыми <span>, и на некоторых записях этот отступ схлопывался. Правка в public/js/views/deep.js — два <span> заменены на .saved-card__title + семантический <time class="saved-card__date" datetime="…">, обёрнутые во flex-контейнер .saved-card. Отступ теперь задаёт gap: var(--space-2, 8px), схлопнуться не может, плюс получаем a11y/SEO-семантику через <time>. 906 → 907 модульных. (M-4)
[1.58.10] — 2026-05-20
fix(ux): M-2 — гасить прогресс-тост перед открытием любого result-модала. Клик по sync-check на #/cv оставлял тост «Running cv-sync-check.mjs…» в правом нижнем углу, пока открывался модал с результатом — оба тянули внимание, а на узких экранах визуально перекрывались. Кнопки Doctor / verify-pipeline на странице Health уже явно вызывали UI.dismissToast() перед UI.modal(); sync-check в cv.js был единственной точкой входа, где этого не делали. Правка в public/js/api.js — UI.modal() теперь вызывает dismissToast() первым исполняемым оператором (эшелонированная защита на границе). Хардкоженые английские строки в cv.js локализованы через t('cv.syncCheckRunning') / t('cv.syncCheck') (инвариант BUG-008: заголовок модала == локализованная подпись кнопки). Два новых ключа i18n добавлены во все 8 локалей. 905 → 906 модульных. (M-2)
[1.58.9] — 2026-05-20
fix(a11y): M-1 — восстановлено видимое кольцо :focus-visible на полях форм (WCAG 2.4.7 Уровень AA). MASTER-регрессия v1.58.3 показала, что getComputedStyle(focusedInput) возвращал outline: rgb(255,255,255) none 1.5px — ключевое слово none стягивало ширину кольца до 0 px на каждом поле. Корень: базовые правила .input, .textarea, .select { outline: none } и .searchbar input { outline: none } имели более высокую специфичность, чем глобальное *:focus-visible, и тихо убивали кольцо клавиатурного фокуса на 88 элементах страницы. Правка в public/css/app.css — добавлены явные правила .input:focus-visible/.textarea:focus-visible/.select:focus-visible и .searchbar input:focus-visible с outline: 2px solid var(--rausch) + полупрозрачный box-shadow; мышиный фокус (:focus) остаётся чистым. 904 → 905 модульных (статический контрактный гард); Playwright 60 → 61 (Tab-обход). (M-1)
[1.58.8] — 2026-05-20
feat(health): показать OPENAI_API_KEY / QWEN_API_KEY / OPENROUTER_API_KEY на #/health (по аналогии с GEMINI_API_KEY). В v1.57.0 OpenRouter добавлен как 5-й headless live-eval провайдер, в v1.55.3 (UX-2) появился on-screen-онбординг для 4 провайдеров, но страница #/health всё ещё показывала только GEMINI_API_KEY и ANTHROPIC_API_KEY — остальные три ключа были невидимы, хотя /api/status/providers уже маршрутизировал на них evals. По запросу пользователя расширил тот же паттерн строк «set / unset (manual mode)» на все headless-провайдеры. server/lib/routes/health.mjs теперь добавляет три необязательных строки чека через тот же isUsableKey-гейт, что и /api/status/providers. Хелперы hasOpenAIKey() / hasQwenKey() / hasOpenRouterKey() были уже импортированы, но не использовались. Формулировка «manual mode» совпадает со строкой GEMINI; локализация не требуется — Health-вью итерирует body.checks. 903 → 904 модульных. (Запрос пользователя)
[1.58.7] — 2026-05-20
fix(security): NEW-2 — isValidJobUrl теперь отклоняет парные синтаксисы плейсхолдеров шаблонов (${…}, {{…}}) для соответствия сообщению об ошибке. Ответ 400 у POST /api/pipeline декларирует «contain no script or template characters», однако MASTER-регрессия v1.58.3 показала, что фактически блокировалась только ASP/EJS-форма <%…%> (побочный эффект [<>"'\\s]). JS-шаблонные литералы ${TEST}и Mustache/Handlebars{{TEST}}проходили — расхождение regex↔сообщение. Вариант A из fix-prompt (затягиваем regex под сообщение): новый массивTEMPLATE_PATTERNSв [server/lib/security.mjs](https://github.com/Fighter90/career-ops-ui/blob/main/server/lib/security.mjs), вызываемый черезhasTemplatePlaceholder(url)передnew URL(…). Отклоняются только **парные** плейсхолдеры ({normal}` как легитимный ATS-токен остаётся принятым). 901 → 903 модульных. (NEW-2)
[1.58.6] — 2026-05-20
fix(a11y/i18n): BUG-008-tb — заголовок модалки кнопки Doctor в верхней панели теперь совпадает с локализованной подписью кнопки. Правило BUG-008 (закрытое в v1.58.0) требует «заголовок модалки == локализованная подпись кнопки». MASTER-регрессия v1.58.3 обнаружила, что точка входа из верхней панели всё ещё нарушала инвариант: независимо от языка UI клик по Doctor открывал модалку с заголовком doctor (английский, строчные). Правка в public/js/app.js:118 — литерал 'doctor' заменён на I18n.t('top.doctor', 'Doctor'). Ключ top.doctor уже есть во всех 8 локалях (EN Doctor · ES/pt-BR Diagnóstico · KO 진단 · JA 診断 · RU Диагностика · zh-CN 诊断 · zh-TW 診斷) и совпадает с ключом кнопки data-i18n="top.doctor". В tests/qa-report-fixes.test.mjs добавлен статический контрактный гард. 900 → 901 модульных; Playwright 60/60. (BUG-008-tb)
[1.58.5] — 2026-05-20
fix(ui): NEW-3 — двойной POST по Run live на #/followup отсортирован как невоспроизводимый; зафиксировано Playwright-гардом. В MASTER-регрессии v1.58.3 (через monkey-patched window.fetch) после одного клика по Run live на #/followup фиксировались два идентичных POST на /api/mode/followup в окне ~2 с (компания/роль/заметки заполнены, дата намеренно пустая). По доктрине fix-prompt «сначала воспроизведи» провёл аудит public/js/views/mode-page.js::submit(): (а) Run live и Generate prompt — это обычные <button>, у каждого один onClick, родительского <form> и addEventListener('submit') нет, то есть путь двойного срабатывания отсутствует; (б) UI.withSpinner() (FIX-L1) ставит button.disabled = true на время запроса, блокируя второй физический клик у источника. В tests/playwright-smoke.mjs добавлен тест, повторяющий точный сценарий регрессии — заполняет компанию/роль/заметки, оставляет дату пустой, кликает по «ручной» кнопке (использующей тот же submit(), что и Run live), и проверяет, что за 3 с произошёл ровно один POST /api/mode/followup. Селектор устойчив к локали (глиф ▶ одинаков во всех 8 языках), а addInitScript заранее задаёт career-ops-ui:lang=en, чтобы предыдущий тест языка в том же контексте браузера не перебивал селекторы полей. Playwright 59 → 60. Исходное наблюдение QA фиксируется как рецепт-регресс; правки производственного кода не требуются. (NEW-3)
[1.58.4] — 2026-05-19
fix(security): NEW-1 — отдавать Content-Security-Policy в каждом ответе (раньше только при привязке вне loopback). До v1.58.4 заголовок CSP добавлялся только когда isPubliclyExposed() истинно (HOST привязан за пределами loopback); на 127.0.0.1 и /, и /api/health отвечали без CSP, и единственной защитой от XSS оставался escape-first-контракт UI.md(). MASTER-регрессия v1.58.3 (§5) отметила это как stop-ship-инвариант. Теперь CSP безусловный и одинаков во всех ответах независимо от адреса привязки: default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'. script-src никогда не допускает 'unsafe-inline'/'unsafe-eval'. Набор директив не изменился относительно прежней «только для внешнего доступа» политики (уже корректен для SPA — Google Fonts в allowlist для Inter), визуальных и функциональных регрессий нет. Файл tests/security-headers.test.mjs переписан; обход маршрутов Playwright (en/ru/ja/zh-TW × 7 маршрутов) подтверждает 0 нарушений CSP. 900 модульных · Playwright 58→59 · e2e 20/20+23/23. Следующие пункты fix-prompt выходят отдельными one-fix-релизами по доктрине проекта. (NEW-1)
[1.58.3] — 2026-05-19
fix(deep): R-2 / FIX-C1 — вычистить ОРФАННЫЕ / несбалансированные теги agent-скаффолдинга из вывода исследований. В v1.58.0 cleanLlmMarkdown убирал только парные <tool_call>…</tool_call> и висячий открывающий тег. Глубокий регресс v1.58.2 нашёл модель с перекошенной трассой — одиночный закрывающий </tool_response> (и </thinking>) без открывающего — он выживал и рендерился буквально в сохранённом брифинге #/deep. Финальный консервативный проход теперь убирает любой одиночный scaffold-токен (open/close, сбалансирован или нет): tool_call|tool_response|tool_use|function_call|function_results|thinking, Anthropic tool XML (<invoke …>/</invoke>/<parameter …>/<function_calls> и antml:-формы) и fenced tool_*. Чисто + идемпотентно; реальные <https://…> автоссылки и `<code>` сохраняются. FIX-C2 (<html lang> не обновляется) — триаж не воспроизводится: i18n.js уже ставит document.documentElement.lang в setLang И при загрузке и детектит navigator.language; репро QA — артефакт устаревшего localStorage/до-редеплой-сборки. Оба зафиксированы регресс-гардами. 896 → 900 юнит · Playwright 58/58 · e2e 20/20+23/23. Остальные пункты fix-prompt v1.58.3 (M-1 focus-ring, M-4..M-9, I-1..I-6, U-1..U-15) — в очереди как отдельные one-fix-релизы по доктрине (не батчим). См. qa/v158-regression/.
[1.58.2] — 2026-05-19
fix(i18n): I18N-011 — локализация оглавления #/help во всех 7 не-EN локалях. TOC «На этой странице» строится из заголовков ## файла docs/help/<lang>.md. Разделы 3/4/6/7/8/9/10/11/12 (Profile, CV, Health, Scan, Pipeline, Evaluate, Reports, Tracker, Deep research) всё ещё несли английские названия в es/pt-BR/ko/ja/ru/zh-CN/zh-TW, поэтому TOC был на английском, а сайдбар — переведён. Каждый такой заголовок теперь локализован тем же термином, что и ключ сайдбара nav.* (единый источник истины — TOC ↔ сайдбар совпадают), номер раздела и (#/route …) сохранены дословно. EN не менялся. 7–9 заголовков на локаль. Закрывает единственный открытый i18n-пункт бэклога v1.58. Только документация; 896/896 юнит · 33/33 help · Playwright 58/58. См. qa/v158-regression/.
[1.58.1] — 2026-05-19
fix(test): CI-изолированный guard checkProfileCustomized (патч поверх v1.58.0). v1.58.0 прошёл advisory pre-commit, но упал в ci.yml (Node 18/20/22): тест BUG-002/UX-032 использовал cache-bust динамический импорт + подмену PATHS, но server/lib/paths.mjs резолвит корень проекта один раз на процесс, поэтому временный корень не применялся под общим раннером npm test (локально проходило случайно). Заменено на надёжный статический guard: проверяет, что allow-list в store.mjs содержит фикстурные имена И что регэксп ^(…)$/i строго заякорен (реальное имя со словом “test”, напр. María Testanova, доказуемо не помечается). Прод-код не менялся; также разблокирует publish-package.yml (он гоняет тесты перед публикацией). 896/896 юнит · Playwright 58/58. См. qa/v158-regression/.
[1.58.0] — 2026-05-19
fix(qa): прогон багов из внешнего QA-отчёта + чистый форматированный вывод исследований. Исправлено: BUG-001 — #/followup валидирует необязательное поле даты как ISO YYYY-MM-DD на клиенте (мусор больше не уходит в LLM); BUG-003 — **bold**/`code`/ссылки теперь рендерятся внутри блок-цитат в UI.md() (на всех страницах Help); BUG-005 — повторный URL в pipeline даёт честный тост «Уже в очереди — пропущено»; BUG-006 — человеческое сообщение о неверном URL (контекст (POST /api/pipeline · HTTP 400) сохранён намеренно); BUG-007/008 — прогресс-тост «Running doctor.mjs…» убирается перед модалкой (новый UI.dismissToast()), заголовок модалки = локализованная подпись кнопки; BUG-010 — у пустого состояния #/reports появился подзаголовок; BUG-002/UX-032 — checkProfileCustomized() помечает тест-фикстуры (Acceptance Test и др.) как «не настроено» (родительские profile.yml/cv.md не трогаем — hard rule #1); I18N-012/013 — русский Deep research реально переведён. Новое: server/lib/llm-output.mjs::cleanLlmMarkdown() убирает echo агентного скаффолдинга (<tool_call>{…}</tool_call>, <tool_response>, <tool_use>, <function_call>, <thinking>, [TOOL_CALL]…) из #/deep и Сохранённых исследований — на всех провайдерах и при отдаче уже сохранённых файлов; алиас #/outreach→#/contacto (BUG-004); сетевая ошибка клиента локализована через I18n.t() (api.netError/api.netHint, 8 локалей; серверные details намеренно английские — диагностические). Тесты: новые tests/qa-report-fixes.test.mjs (10) и tests/llm-output.test.mjs (5); 881 → 896 юнит; Playwright 58/58; e2e 20/20 + 23/23. Не менялось (с обоснованием): BUG-009 (H1 #/cv — by-design, WCAG single-h1), родительские данные (parent-owned), длинный хвост minor i18n/UX — в бэклоге. Подробно — CHANGELOG.md.
[1.57.2] — 2026-05-19
fix(config): НАСТОЯЩАЯ причина «validation failed» на /#/config — внедряемое SPA поле lang. public/js/api.js авто-добавляет поле lang в каждое JSON-POST тело (чтобы LLM-маршруты подхватывали локаль UI). /api/config — не LLM-маршрут, и lang не является ключом конфигурации, поэтому (корректная, важная для безопасности) проверка неизвестных ключей в validateConfig отвечала 400 на каждое Сохранение: validation failed — lang: not a known config key. Это проявлялось только в браузере: репро через curl/in-process не слали lang, поэтому v1.57.0/.1 (триминг пробелов, описательные ошибки) улучшили сообщение, но не устранили причину. Маршрут конфигурации теперь убирает транспортное поле lang до валидации; фильтр записи по KNOWN_KEYS по-прежнему отбрасывает любой действительно неизвестный ключ — защита от инъекции не изменилась. Найдено новым Playwright-обходом форм, который кликает реальную кнопку Save (а не синтетические fetch-репро, маскировавшие баг). Тесты: новый tests/playwright-forms.mjs (26, включён в npm run test:e2e:browser) — реальный Chromium по всем формам: нет console-ошибок по всему сайту, тост невалидного сохранения показывает поле+причину+контекст запроса, секретные поля не эхо-выводят введённое, валидные сохранения проходят, PORT/HOST показывают предзаполненные значения по умолчанию; config-endpoint — паритет с браузером (POST с lang проходит и lang не попадает в .env; реальный неизвестный ключ при наличии lang по-прежнему отклоняется). 879 → 881 юнит; Playwright 32 → 58. Подробно — CHANGELOG.md.
[1.57.1] — 2026-05-19
fix(ux): каждая ошибка API теперь говорит ЧТО упало, ГДЕ и ПОЧЕМУ; текст ошибки ввода — максимально подробный. Сервер и раньше возвращал { error, details: ["ПОЛЕ: причина", …] }, но все формы показывали в тосте только верхнюю строку («validation failed»), и на /#/config (и везде) было непонятно, какое поле не так. api.js теперь подмешивает пофайловые details в сообщение по всему сайту (одна правка — выигрывают все формы), добавляет контекст запроса (МЕТОД /путь · HTTP NNN) (ГДЕ), при не-JSON ответе показывает обрезанный сырой фрагмент, а сетевые ошибки несут метод+путь; err.details доступен для inline-рендеринга. Сообщения validateConfig стали максимально описательными (что не так и как исправить), напр. PORT: must be 1-65535 — целое число, только цифры (по умолчанию 4317); вы ввели "abc". Секретные ключи никогда не эхо-выводят введённое значение (только длину в символах) — опечатка реального ключа не утечёт в тост/лог. Диапазон PORT теперь действительно проверяется (5-значное >65535, напр. 99999, раньше принималось — теперь отклоняется с понятной причиной). На /#/config поля PORT и HOST предзаполнены реальными значениями по умолчанию (4317 / 127.0.0.1). Тосты ошибок держатся на экране дольше (9–20 с в зависимости от длины) и переносятся/скроллятся, а не обрезаются. Тесты: новый tests/config-validation-detail.test.mjs (12); 874 → 879. Подробно — CHANGELOG.md.
[1.57.0] — 2026-05-19
feat(provider): OpenRouter — 5-й headless-провайдер живой оценки + fix(config): «validation failed» при сохранении любого API-ключа. Вставленные ключи часто приходят с хвостовым переносом строки или пробелами (буфер ОС, кнопки «копировать» в консолях провайдеров) — до 1.57 это роняло проверку переноса строки для каждого провайдера, а $-якорная регулярка ANTHROPIC_API_KEY (/^sk-ant-[A-Za-z0-9_-]{20,}$/) ложно отвергала настоящие ключи Anthropic. Теперь validateConfig нормализует (тримит) каждое значение до проверки, маршрут сохраняет уже обрезанное значение (аутентифицируется в рантайме, без порчи .env кавычками из-за \n), а проверка Anthropic — устойчивый префикс sk-ant- + длина (общий порог isUsableKey() ≥ 20 остаётся реальным «настоящий ли это ключ»). Внутренние переносы строки по-прежнему отвергаются (защита от инъекции в .env). OpenRouter теперь полноценный провайдер: OPENROUTER_API_KEY на /#/config — один ключ открывает 300+ моделей (Anthropic, OpenAI, Google, Meta, Qwen, DeepSeek …). Он последний в порядке auto (Anthropic → Gemini → OpenAI → Qwen → OpenRouter), поэтому существующая настройка никогда молча не перенаправляется; LLM_PROVIDER=openrouter фиксирует его. Подключён в тот же путь _tailProvider(), что OpenAI/Qwen, в /api/evaluate, /api/deep, /api/mode/:slug; виден в /api/status/providers + на дашборде Health. Клиент OpenAI-совместимый (без новых зависимостей — прямой fetch, таймаут AbortController, ключ не логируется) с рекомендованными заголовками HTTP-Referer/X-Title. Выпадающий список моделей — живой: OPENROUTER_MODEL заполняется из GET /api/openrouter/models (серверный прокси публичного каталога OpenRouter — сохраняет CSP connect-src 'self'; без запроса браузер→третья сторона), с кураторским fallback при недоступности каталога и 10-минутным in-memory кэшем. Новые i18n-ключи (config.openrouter*) во всех 8 локалях; config.llmProviderHint обновлён. Тесты: новые CI-изолированные наборы tests/openrouter-route.test.mjs и tests/openrouter-model-selector.test.mjs; расширены наборы env-config/openai/provider-selector; два прежде слабых кейса validateConfig (использовали более не известный HH_USER_AGENT) теперь проверяют настоящий ключ. 831 → 855. Подробно — CHANGELOG.md.
[1.56.4] — 2026-05-19
feat(ui): UX-N2 — видимая платформозависимая подсказка ⌘K / Ctrl K на поле глобального поиска. Шорткат Cmd/Ctrl+K («фокус на поиск») жил только в aria-label/исходнике — зрячие пользователи его не находили, и приложение казалось медленнее, чем есть. Теперь приглушённый <kbd class="kbd-shortcut"> стоит в конце строки поиска и заполняется при загрузке из data-mac/data-other по проверке платформы (navigator.platform/userAgent): ⌘K на macOS/iOS, Ctrl K в остальных. Он aria-hidden="true" (существующий aria-label уже озвучивает шорткат скринридеру — бейдж не должен дублировать) и pointer-events:none (декоративный). Существующая привязка Cmd/Ctrl+K не изменена. Новых i18n-ключей нет (глифы универсальны); бейдж — flex-потомок существующего .searchbar (без обёртки/абсолютного позиционирования — input уже flex:1). Тесты: новый CI-изолированный статичный-по-исходнику набор tests/cmdk-hint-visible.test.mjs (5): <kbd class="kbd-shortcut"> внутри .searchbar; aria-hidden="true" + оба варианта data-mac/data-other; app.js заполняет его по проверке navigator; привязка (e.ctrlKey||e.metaKey)&&e.key==='k' → search.focus() цела (регресс-гард); app.css стилизует .kbd-shortcut и не ставит display:none. 826 → 831. feat(ui) · test: tests/cmdk-hint-visible.test.mjs. Подробно — CHANGELOG.md.
[1.56.3] — 2026-05-19
fix(reliability): определение ключей провайдеров отвергает плейсхолдеры / слишком короткие значения, а не только пустую строку. Плейсхолдер GEMINI_API_KEY в родительском .env показывался как «✓ set» И выбирался активным провайдером вместо валидного ANTHROPIC_API_KEY. effectiveEnv() отвергал лишь undefined/'', поэтому 10-символьный мусор считался настоящим ключом: баннер онбординга показывал GEMINI ✓ set, GET /api/status/providers возвращал activeProvider: "gemini", и каждый живой ⚡-прогон молча падал бы на мёртвом ключе, игнорируя рабочий 108-символьный Anthropic. Новая чистая isUsableKey() (env-config.mjs) считает секрет настроенным только если он ≥ 20 символов (ни один реальный ключ не короче — Gemini AIza… ≈ 39, Anthropic sk-ant-… ≈ 100+, OpenAI ≥ 40, Qwen ≈ 35) и не известный плейсхолдер (your_*_here, changeme, placeholder, <…>, один повторённый символ…). Применено единообразно к hasAnthropicKey()/hasGeminiKey() (anthropic.mjs), hasOpenAIKey()/hasQwenKey() (openai.mjs) и строкам GEMINI_API_KEY/ANTHROPIC_API_KEY в GET /api/health (переведены с сырого process.env на тот же effective+plausible-взгляд) — теперь health-страница, эндпоинт провайдеров и OR-роутер всегда согласованы. selectActiveProvider() не менялся — получает корректный keysConfigured. Тесты: новый CI-изолированный набор tests/key-detection-rejects-placeholder.test.mjs (5): юнит-кейсы isUsableKey + in-process createApp() воспроизведение точного сценария (10-символьный GEMINI_API_KEY + реальный ANTHROPIC_API_KEY во временном .env) — gemini НЕ в keysConfigured, activeProvider === "anthropic", строки /api/health согласованы. Четырём существующим тестам слоёв effective-env удлинили слишком короткие заглушки (контракт не изменился). 821 → 826. fix(reliability) · test: tests/key-detection-rejects-placeholder.test.mjs. Подробно — CHANGELOG.md.
[1.56.2] — 2026-05-19
feat(a11y): UX-N1 — по-маршрутный локализованный document.title (ориентация в мультитабах + объявление смены страницы скринридером). До фикса все 24 маршрута сохраняли статический <title> из index.html («career-ops — command center») — одинаковые подписи вкладок, общие закладки, одинаковое объявление «страница изменилась». public/js/router.js focusNewView() теперь берёт заголовок из собственного локализованного <h1 class="page-title"> вида — «Вид — career-ops» — поэтому заголовки автоматически переведены (без новых i18n-ключей) и различны для каждого маршрута. Устанавливается до guard’а первого пейнта, чтобы стартовая вкладка тоже была озаглавлена (тот же порядок, что и установка tabindex в v1.56.0 UX-12). Фолбэк — career-ops — command center, если у вида нет заголовка. Тесты: новый CI-изолированный статичный-по-исходнику набор tests/document-title-per-route.test.mjs (4): focusNewView присваивает document.title; заголовок берётся из <h1> (по-маршрутный + локализованный, не один литерал); присваивание идёт до !firstPaintDone; присутствует продуктовый дефолт. 817 → 821. feat(a11y) · test: tests/document-title-per-route.test.mjs. Подробно — CHANGELOG.md.
[1.56.1] — 2026-05-19
fix(a11y): убрана ложная фирменная рамка фокуса вокруг заголовка при программном фокусе роутера на tabindex="-1". public/js/router.js ставит каждому заголовку целевого вида tabindex="-1" и вызывает .focus() при каждой клиентской навигации (озвучивание новой страницы скринридером). Элемент tabindex="-1" недостижим с клавиатуры, но эвристика :focus-visible в Chromium всё равно рисовала глобальную фирменную рамку (*:focus-visible { outline: 2px solid var(--rausch) }) — красный прямоугольник вокруг заголовка страницы (например, «Command Center» на #/dashboard) при каждой навигации; та же рамка попала в hero-скриншоты images/dashboard-*.png. Исправление — одно точечное правило [tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none } (паттерн управляемого фокуса WAI-ARIA APG). Настоящий клавиатурный фокус на интерактивных элементах сохраняет глобальную рамку *:focus-visible (WCAG 2.4.7 цел); рамка skip-link не затронута (это <a>, не tabindex="-1", с более высокой специфичностью). Все 8 images/dashboard-*.png перегенерированы — красной рамки больше нет. Тесты: новый CI-изолированный статичный-по-исходнику набор tests/managed-focus-no-ring.test.mjs (4): глобальная рамка *:focus-visible на месте (WCAG 2.4.7 не нарушен); [tabindex="-1"]:focus,:focus-visible ⇒ outline:none; правило-подавление идёт после глобального (безопасность каскада); фикс точечный (нет общего *:focus{outline:none}). В паре с tests/dashboard-initial-focus.test.mjs. 813 → 817. fix(a11y) · test: tests/managed-focus-no-ring.test.mjs. Подробно — CHANGELOG.md.
[1.56.0] — 2026-05-19
feat(ux): пакет LOW-полировки — UX-9 / UX-10 / UX-11 / UX-12 (один сгруппированный минорный релиз). UX-9 #/cv: заголовок страницы понижен до тихого чипа-хлебной-крошки .cv-breadcrumb, шумный подзаголовок убран в title-тултип <h1> — резюме пользователя (его имя в превью) получает визуальный приоритет. Инвариант F-V54-A цел — по-прежнему ровно один <h1>, по-прежнему .page-title. UX-10 новый общий хелпер UI.providerCostHint(t) рядом с ⚡ Запуск вживую на #/auto, #/evaluate, #/deep и каждом #/<mode>; переиспользует GET /api/status/providers (v1.55.3): с ключом — «Оценочная стоимость: OpenAI gpt-5-codex · ~$0.04/eval» (порядок величины, “~”); без ключа — что ⚡ копирует ручной промпт без затрат API; fail-soft. UX-11 #/help: когда фильтр TOC сужается ровно до одной секции, страница прокручивается туда после 300мс простоя (debounce; никогда при 0 или >1). UX-12 #/dashboard: на первом пейнте <h1> делается фокусируемым (tabindex="-1"), #content остаётся aria-live="polite" (озвучивается при загрузке) — без кражи фокуса (не воюет со skip-link, решение v1.41.0). Новые i18n-ключи cost.estimate, cost.manual ×8; новый CSS .cv-breadcrumb/.cost-hint. Тесты: 4 новых статичных-по-исходнику CI-изолированных набора (cv-breadcrumb 3, run-cost-line 4, help-toc-autoscroll 4, dashboard-initial-focus 3); обновлены прежние локи cv-single-h1/help-nav-a11y (инварианты сохранены). 800 → 813. Живой Playwright-зонд всех 4, 0 ошибок консоли. feat(ux) · 4 test suites. Подробно — CHANGELOG.md.
[1.55.8] — 2026-05-19
feat(tracker): серверная пагинация + кликабельные чипы воронки (UX-8). Сервер: GET /api/tracker получает необязательные query-параметры ?page / ?pageSize / ?status. Без них ответ байт-в-байт равен прежнему { rows: [...] } (все существующие вызовы/тесты не затронуты). С ними возвращает { rows: slice, total, page, pageSize, funnel } — pageSize ограничен [1,500], page ≥1, status фильтрует rows+total, а funnel — разбивка статус→количество по всей истории (независимо от страницы/фильтра, чтобы чипы всегда были точны). #/tracker: новая кликабельная панель чипов воронки сверху — «все статусы · N · Applied · N · Interview · N …» (порядок Applied → Responded → Interview → Offer → Rejected → Discarded → Evaluated → SKIP). Клик по чипу задаёт фильтр Status (повторный клик по активному — сброс); активный чип aria-pressed и подсвечен. Новый i18n-ключ track.funnelAria ×8; новый CSS .tracker-funnel/.tracker-chip/.tracker-chip--active. test: tests/tracker-server-paged.test.mjs (новый, 7 кейсов, CI-изолирован, in-process Express на эфемерном порту + временный CAREER_OPS_ROOT applications.md — CLAUDE.md #2/#8): back-compat (без параметров ⇒ ровно {rows}); ?page&pageSize срез + total/page/pageSize/funnel с суммой N; последняя неполная страница без перекрытия; страница вне диапазона ⇒ пустые rows + валидный total; ?status= фильтрует total/rows, funnel по всей истории; кап pageSize; + статичный-по-исходнику лок панели чипов. 793 → 800. feat(tracker) · test: tests/tracker-server-paged.test.mjs. Подробно — CHANGELOG.md.
[1.55.7] — 2026-05-19
feat(pipeline): vanilla-JS виртуализация строк при >1000 строках (UX-7). #/pipeline рендерил каждую строку (filtered.forEach(list.appendChild(urlRow))) — скан наполняет очередь тысячами URL, поэтому тысячи узлов строк (каждый — flex-div + <a> + две кнопки) строились синхронно на каждое нажатие в фильтре, переполняя DOM и дерево доступности. Новая vanilla-JS виртуализация (эквивалент react-window, без зависимостей): выше VIRTUALIZE_THRESHOLD = 1000 #/pipeline становится скролл-вьюпортом фиксированной высоты (70vh) с несжимаемым спейсером (flex:0 0 auto, height = строк × 56px), который сохраняет реальный скроллбар для всего списка, а rAF-троттлинговый scroll-листенер рендерит только вьюпорт ± буфер в 5 строк (~16–19 узлов вместо N). На/ниже порога исходный простой рендер сохранён байт в байт, поэтому типичные пайплайны и все существующие тесты/e2e не затронуты. Каждая виртуализированная строка сохраняет URL-различающий ▶/✕ aria-label (F-V54-B зафиксирован регрессией). Расчёт окна — чистый хелпер computeWindow(). test: tests/pipeline-virtualize.test.mjs (новый, 5 кейсов, CI-изолирован, статичный-по-исходнику): числовой порог ~1000; ветка ≤порога сохраняет forEach→appendChild; ветка >порога рендерит slice(start,end) с rAF scroll-листенером + спейсером; computeWindow() клампит [0,total] с ± буфером; строки сохраняют ▶/✕ aria-label. 788 → 793. Живой Playwright-зонд (фикстура 1200 URL): scrollHeight≈67248, в DOM только ~16–19 узлов, окно следует за скроллом из конца в конец, 0 ошибок консоли. feat(pipeline) · test: tests/pipeline-virtualize.test.mjs. Подробно — CHANGELOG.md.
[1.55.6] — 2026-05-19
feat(scan): убрать вторичные фильтры за раскрывашку «Расширенные фильтры» (UX-4). #/scan складывал все фильтры — свободный текст, удалёнка/гибрид/офис, охват, источник и пост-сканные фасетные чипы stack/level/dynamic — равным весом, стена контролов. Теперь повседневные фильтры остаются видимыми (свободный текст + Удалёнка/Гибрид/Офис; кнопка 🌐 Скан уже отдельно в карточке контролов), а вторичные сворачиваются за <details class="scan-advanced"><summary>Расширенные фильтры</summary>: селекты Охват + Источник и — отдельно — кластер фасетных чипов (теперь свежий набор начинается с таблицы, а не стены чипов, и рендерится только при ≥1 строке чипов). Новый i18n-ключ scan.advancedFilters во всех 8 локалях; новый стиль .scan-advanced (тихая ⚙-аффорданс, без маркера, жирный при раскрытии). test: tests/scan-advanced-disclosure.test.mjs (новый, 6 кейсов, CI-изолирован, статичный-по-исходнику): <details>/<summary> с хуком .scan-advanced и меткой scan.advancedFilters; свободный текст + удалёнка остаются видимыми; охват + источник внутри раскрывашки; chipsContainer — <details>; .scan-advanced summary стилизован; scan.advancedFilters ×8. 782 → 788. feat(scan) · test: tests/scan-advanced-disclosure.test.mjs. Подробно — CHANGELOG.md.
[1.55.5] — 2026-05-19
feat(dashboard): hero с 2 P0-CTA + фокусной подсказкой недавней активности (UX-3). #/dashboard открывался ~30 узлами равного веса — без ясного «что дальше». Новый блок .dash-hero теперь сразу под шапкой страницы: два P0-сценария — ✨ Auto-pipeline по URL и 🌐 Запустить скан — повышены до крупных кнопок .btn-hero, а единственная фокусная подсказка недавней активности («Последняя оценка: <балл> — <заголовок>», со ссылкой на отчёт; на холодном старте — направляющее пустое состояние через dash.heroNoEval) показывает вернувшемуся пользователю, где он остановился, а новому — единственное важное действие. Две основные кнопки убраны из шапки (остаётся только вторичная «📋 Открыть pipeline»), чтобы не дублировать действие. Счётчики статусов понижены с заметных .badge до тихих пилюль .dash-chip. Новые i18n-ключи dash.lastEval, dash.heroNoEval во всех 8 локалях; новый CSS .dash-hero/.btn-hero/.dash-chip. test: tests/dashboard-hero.test.mjs (новый, 5 кейсов, CI-изолирован, статичный-по-исходнику): .dash-hero существует и предшествует сетке Quick-actions; оба P0-CTA — .btn-hero с маршрутами /auto+/scan; фокусная dash.lastEval + пустое состояние dash.heroNoEval; бакеты используют .dash-chip; CSS существует; dash.lastEval+dash.heroNoEval ×8. 777 → 782. feat(dashboard) · test: tests/dashboard-hero.test.mjs. Подробно — CHANGELOG.md.
[1.55.4] — 2026-05-19
feat(ux): честный ETA auto-pipeline рядом с Run + заметный Stop во время сканирования (UX-6). #/auto: новая подсказка .auto-eta — “⏱ ~1–2 мин” (ключ auto.eta, title через auto.etaTitle) — теперь рядом с кнопкой Run, чтобы обещание «в один клик» было честным о длительности до того, как пользователь решится; формулировка совпадает с career-ops.org/docs («вставьте URL → полный отчёт за 1–2 минуты»). #/scan: пока многоминутный обход выполняется (aria-busy), Stop повышается из малоконтрастной призрачной кнопки в заметную деструктивную (новый .btn-danger — заливка, высококонтрастный белый на коралловом, насыщенность 600). setScanRunning(running) переключает scan-stop-btn между btn-danger (выполняется) и btn-ghost (простой, всё равно скрыт), чтобы пользователь находил и доверял Stop под нагрузкой. Новые i18n-ключи auto.eta, auto.etaTitle во всех 8 локалях; новый CSS .btn-danger/.auto-eta. test: tests/auto-eta-stop.test.mjs (новый, 4 кейса, CI-изолирован, статичный-по-исходнику): #/auto рендерит t('auto.eta') с классом .auto-eta рядом с runBtn; auto.eta ×8; setScanRunning(running) повышает Stop до btn-danger; .btn-danger существует с высококонтрастным белым текстом. 773 → 777. feat(ux) · test: tests/auto-eta-stop.test.mjs. Подробно — CHANGELOG.md.
[1.55.3] — 2026-05-19
feat(onboarding): экранный статус OR из 4 провайдеров — баннер холодного старта + чип активного провайдера (UX-2, HIGH). Новый read-only эндпоинт GET /api/status/providers → { activeProvider, activeModel, keysConfigured }. keysConfigured использует тот же effective-env обзор, что и gate sites llm.mjs (process.env ∨ родительский .env); activeProvider — то, что реально выберет OR-роутер: selectActiveProvider(), новый чистый хелпер в env-config.mjs, обходит providerOrder() (пин LLM_PROVIDER без подходящего ключа → null). Секреты не возвращаются — только имена провайдеров + id модели. Оболочка SPA рендерит глобальную область онбординга (#onboarding-banner, заполняет app.js, только CSP-безопасный DOM): 0 ключей → красный баннер + CTA на #/config?tab=api-keys; ≥1 ключ → ненавязчивый чип с активным провайдером+моделью. Делает ключевое отличие («один из Anthropic / Gemini / OpenAI / Qwen, авто-порядок») обнаружимым на экране, а не методом проб. Новые i18n-ключи onboarding.* во всех 8 локалях; новый CSS .onboarding-warn/.onboarding-ok. test: tests/onboarding-key-banner.test.mjs (новый, 9 кейсов, CI-изолирован): семантика selectActiveProvider; GET /api/status/providers in-process (эфемерный порт + временный .env в CAREER_OPS_ROOT, чтобы никогда не читать реальный родительский ключ — CLAUDE.md #2/#8); статичная SPA-обвязка + покрытие onboarding.* ×8. 764 → 773. feat(onboarding) · test: tests/onboarding-key-banner.test.mjs. Подробно — CHANGELOG.md.
[1.55.2] — 2026-05-18
fix(cv): дать markdown-редактору #/cv описательное, самодостаточное доступное имя (F-V55-H / UX-5). <textarea id="cv-editor"> основного редактора #/cv теперь несёт описательный aria-label через новый ключ cv.editorAria — “Редактор markdown резюме — ваше профессиональное резюме в формате markdown” — вместо краткого имени, унаследованного от видимого заголовка раздела “Markdown”. Примечание: вопреки симптому F-V55-H (проверявшему лишь aria-label/labels), поле не было безымянным — v1.47.0 (WS2 #16) уже связал его через aria-labelledby → <h3 id="cv-md-heading">Markdown</h3>, так что скринридер озвучивал “Markdown, редактирование, многострочное”. v1.55.2 улучшает это краткое “Markdown” до самодостаточной метки. Избыточный aria-labelledby удалён (иначе мёртвая разметка — aria-label побеждает по приоритету ARIA); видимый <h3>Markdown</h3> остаётся для зрячих пользователей. WCAG 1.3.1 + 4.1.2; параллель к правке batch-tsv v1.54.5 (F-V54-C). test: tests/cv-editor-a11y.test.mjs (новый, 3 кейса, CI-изолирован, статичный-по-исходнику как auto-stepper-prerender.test.mjs): #cv-editor именует себя через t('cv.editorAria', …) с непустым фолбэком; cv.editorAria присутствует и непуст во всех 8 локалях; на элементе нет избыточного aria-labelledby. 761 → 764. fix(cv) · test: tests/cv-editor-a11y.test.mjs. Подробно — CHANGELOG.md.
[1.55.1] — 2026-05-18
fix(auto): пререндер 5-этапного степпера пайплайна при монтировании #/auto (F-V55-E / UX-1, senior-наблюдение S-4 переоткрыто). #/auto теперь показывает документированную схему из пяти этапов — валидация → загрузка → оценка → сохранение отчёта → добавление в трекер — в момент монтирования экрана, а не остаётся пустым до первого события SSE. Раньше <ol class="auto-stepper"> создавался с display:none, а renderStepper() достигался только из setStep() / run(), поэтому пользователь при холодном старте никогда не видел обещанный документацией пайплайн до нажатия Run. Степпер теперь виден при монтировании со всеми пятью этапами в состоянии pending и несёт aria-label (auto.stepperAria), чтобы вспомогательные технологии озвучивали регион. Закрывает F-V55-E (линза a11y/статической гарантии) и UX-1 (линза верности обещанию) — одна правка, обе линзы. test: tests/auto-stepper-prerender.test.mjs (новый, 4 кейса, CI-изолирован, статичный-по-исходнику как router.test.mjs): массив STEPS — ровно 5 канонических этапов по порядку; stepperEl не display:none при монтировании и несёт auto.stepperAria; вызов renderStepper() уровня монтирования предшествует function setStep(; auto.stepperAria присутствует во всех 8 локалях. 757 → 761. fix(auto) · test: tests/auto-stepper-prerender.test.mjs. Подробно — CHANGELOG.md.
[1.55.0] — 2026-05-18
feat(llm): headless live-eval работает через “OR” — Anthropic | Gemini | OpenAI | Qwen, автовыбор по тому, какой ключ задан. По запросу пользователя ⚡ live-eval веб-UI теперь работает с любым заданным ключом API, а не только Anthropic/Gemini. LLM_PROVIDER получает openai и qwen; auto (по умолчанию) использует первого провайдера, чей ключ присутствует, предпочитая Anthropic → Gemini → OpenAI → Qwen. Явное значение фиксирует одного; форсированный провайдер без ключа всё равно откатывается к пути ручного промпта. Новый server/lib/openai.mjs — клиент Chat Completions, совместимый с OpenAI, без зависимостей (тот же безопасный паттерн прямого HTTPS, что у anthropic.mjs: таймаут AbortController, ключ никогда не логируется, разрешение ключа через effectiveEnv(), чтобы ключ из родительского .env работал без перезапуска). Одно ядро (runOpenAICompatible) обслуживает runOpenAI (api.openai.com) и runQwen (OpenAI-совместимый режим Alibaba DashScope; переопределите endpoint через QWEN_BASE_URL в сыром .env для хоста материкового Китая). Без SDK, без произвольного исполнения CLI — родительский проект остаётся CLI-агностичным (Claude Code · Codex · Gemini · OpenCode · Qwen · Copilot · Kimi); это лишь расширяет headless путь по ключу API. Хвост OpenAI/Qwen подключён ко всем поверхностям eval: /api/evaluate, /api/deep, /api/mode/:slug и SSE /api/auto-pipeline — консультируется после ветвей Anthropic (inline) + Gemini (подпроцесс), чтобы сохранить auto-предпочтение, с тем же инлайнингом упакованного контекста, что использует Anthropic. env-config.mjs: QWEN_API_KEY (секрет) + QWEN_MODEL (не секрет) добавлены в KNOWN_KEYS/KEY_GROUPS.core; LLM_PROVIDERS и providerOrder() расширены; OPENAI_API_KEY теперь полноправный headless-ключ провайдера (раньше только хранимый). Вкладка ключей #/config: селект LLM_PROVIDER получает openai/qwen; новые поля QWEN_API_KEY + QWEN_MODEL (курируемый список qwen-max/qwen-plus/qwen-turbo/qwen2.5-*); новая заметка вверху вкладки объясняет CLI-агностичного родителя vs headless-eval веб-UI и порядок OR. Новые i18n-ключи во всех 8 локалях. test: tests/openai.test.mjs (новый, 9 кейсов, CI-изолирован): успех OpenAI/Qwen + контент-массив блоков, Bearer-аутентификация, дефолтный и переопределённый QWEN_BASE_URL endpoint’ы, 4xx/5xx/некорректный, кламп max_tokens, таймаут, детекция ключа effectiveEnv, канарейка на отсутствие утечки ключа. tests/provider-selector.test.mjs обновлён под поверхность providerOrder/LLM_PROVIDERS/SECRET v1.55.0 + подключение хвоста OpenAI/Qwen. 748 → 757. feat(llm) · test: tests/openai.test.mjs · test: tests/provider-selector.test.mjs. Подробно — CHANGELOG.md.
[1.54.10] — 2026-05-18
fix(auto-pipeline): гигиена отключения SSE-клиента — устранить нестабильный Playwright e2e-job. Playwright e2e-job периодически краснел (32/32 индивидуальных теста проходят, но not ok 2 - tests/playwright-smoke.mjs): закрытие страницы, пока SSE-стрим #/auto был в процессе, приводило к тому, что следующий res.write() сервера отклонялся с EPIPE/"aborted", и — без слушателя 'error' на ответе — Node эскалировал это в uncaughtException, который node:test сообщал как “asynchronous activity after the test ended”. openSse() в auto-pipeline.mjs теперь регистрирует no-op res.on('error') и защищает send() через res.writableEnded || res.destroyed (обёрнуто в try/catch) — исчезнувший клиент ожидаем, а не исключителен. Это корректная продакшен-гигиена SSE, а не просто починка теста. tests/playwright-smoke.mjs: тест Cmd+K использовал реальный исходящий URL (https://example.com/jobs/123), но только ждал появления модалки, поэтому closePage() обрывал выполняющийся safeGet() сервера после завершения теста. Теперь он ждёт, пока пайплайн достигнет терминального состояния (чтобы fetch разрешился нормально до закрытия). Общий хелпер closePage() (window.stop(), затем закрыть) и хук after с server.closeAllConnections() остаются как защита в глубину. Проверено: 8/8 подряд зелёных прогонов (6× node --test + 2× browser-smoke), ранее ~1 из 2 красных. tests/auto-pipeline.test.mjs +1 статический кейс, фиксирующий контракт гигиены отключения openSse (слушатель res.on('error') + гард writableEnded||destroyed + обёрнутые в try записи). 747 → 748. fix(auto-pipeline) · test: tests/auto-pipeline.test.mjs. Подробно — CHANGELOG.md.
[1.54.9] — 2026-05-18
fix(llm): уважать LLM-ключи из родительского .env во время запроса — прекратить ошибочную маршрутизацию на устаревшего/невалидного провайдера. Живая оценка могла падать с “Gemini API error: API key not valid” даже когда настроенным провайдером был ANTHROPIC_API_KEY. Первопричина: hasAnthropicKey() / hasGeminiKey() (и поиск ключа/модели в runAnthropic) читали только снимок process.env на момент старта. Если ключ Anthropic добавляли в родительский .env после запуска сервера, работающий процесс его никогда не видел → детекция Anthropic была ложной, и оценка откатывалась к любому устаревшему ключу, который был в process.env (часто старый, невалидный GEMINI_API_KEY). Путь исполнения Gemini (родительский Node-подпроцесс) уже читал живой родительский .env, поэтому оба провайдера разрешали ключи несогласованно. Новый effectiveEnv(key, envFilePath) в env-config.mjs: непустое значение process.env побеждает (покрывает shell-экспорты и live-apply в POST /api/config); иначе консультируется текущий родительский файл .env. anthropic.mjs теперь разрешает ANTHROPIC_API_KEY, ANTHROPIC_MODEL и проверку Gemini-ключа через него, так что ключ, заданный в родительском .env, уважается без перезапуска сервера и ДЕТЕКЦИЯ ключа всегда совпадает с ключом, который запрос реально ОТПРАВЛЯЕТ. Порядок провайдеров не изменён (auto → Anthropic-потом-Gemini); это чинит только детекцию. Ключи никогда не логируются и не возвращаются (тест на отсутствие утечки REVIEW-B4 по-прежнему проходит). tests/anthropic.test.mjs переписан CI-изолированным (temp CAREER_OPS_ROOT, динамический import) с 2 новыми кейсами, воспроизводящими точный баг (ключ только в родительском .env → детектируется; runAnthropic отправляет ключ + модель из родительского .env, когда process.env не задан). tests/env-config.test.mjs +3 кейса effectiveEnv (приоритет process.env, фолбэк на .env вкл. пустую-строку-как-незаданную, отсутствующий-файл / отсутствующий-ключ / без-пути → undefined) — 100% новой ветки. 742 → 747. fix(llm) · test: tests/anthropic.test.mjs · test: tests/env-config.test.mjs. Подробно — CHANGELOG.md.
[1.54.8] — 2026-05-18
feat(config): форма по полям вкладки Modes всегда рендерит каноническую схему (даже на пустом/заглушечном файле) с подсказками по полям из career-ops.org. Форма по полям Modes из v1.54.3 рендерила поля только для уже существующих секций ## — поэтому на свежем, пустом или несхемном modes/_profile.md (например, обычная однострочная заглушка) она откатывалась к “No ## sections found — use the raw editor below.” и пользователь не получал полей. По запросу пользователя (“разбей по полям … описание полей возьми из career-ops.org/docs”) форма теперь всегда рендерит 5 канонических полей в документированном порядке (Target Roles, Adaptive Framing, Exit Narrative, Comp Targets, Location Policy), предзаполненных из файла при наличии и пустых-но-редактируемых при отсутствии — так что совершенно новый профиль можно заполнить целиком через форму. Каждое поле показывает описание из канонического Quick Start §Step-5 на career-ops.org (что класть в Target Roles / Adaptive Framing / Exit Narrative / Comp Targets / Location Policy), привязанное через aria-describedby для скринридеров. Толерантность к вариантам заголовка: ## Your Target Roles (и т. п.) из шаблона маппится в то же каноническое поле, что и ## Target Roles, так что ни шаблон, ни конвенция серверного скаффолда не ломают форму. collect() теперь — тегированный payload: неразрушающее слияние { sections }, когда отрендеренные заголовки точно совпадают с имеющимися в файле (преамбула + нетронутые + кастомные секции переживают байт-стабильно), либо полная пересборка файла { markdown }, которая бутстрапит/нормализует схемно-согласованный документ, когда в файле не было схемы. Путь пересборки защищён подтверждением в config.js (он заменяет родительский файл — инвариант разрушающего сохранения WS2 #4), сохраняет имеющуюся преамбулу (или документированный дефолт) и держит неканонические секции verbatim. 6 новых i18n-ключей (config.modesDescTargetRoles … config.modesDescLocationPolicy + config.modesFormRebuildBody) во всех 8 локалях. tests/modes-form.test.mjs переписан под контракт v1.54.8: схема + канонический порядок, проводка payload/подтверждения в config.js, наличие описания каждого поля из документации во всех 8 локалях, толерантность canonicalKey “Your X”, стабильность round-trip списков, гарантия bootstrap-всегда-рендерит, и тегированный sections-vs-markdown collect() с безопасностью данных. Проверено вживую против реального родительского заглушечного файла (5 полей + описания появляются, 0 ошибок консоли) и изолированного заглушечного фикстура (заполнить → защищённое подтверждением сохранение → все 5 канонических секций сохранены). feat(config) · test: tests/modes-form.test.mjs. Подробно — CHANGELOG.md.
[1.54.7] — 2026-05-18
fix: W-001 — ассеты кода/стилей + оболочка SPA отдавались с Cache-Control: no-store (гигиена деплоя). SPA загружает api.js / router.js / каждое представление через обычный <script src> без версионной query-строки, и шага сборки нет (без content hashing), поэтому после деплоя браузер мог часами отдавать кэшированный старый бандл → stale-cache 404 на маршрутах с query-строкой (наблюдалось вживую во время регрессии v1.29.2; прогон регрессии W-001). server/index.mjs теперь ставит Cache-Control: no-store на .js / .mjs / .css / .html через хук setHeaders у express.static, и явно на catch-all оболочки SPA (который использует sendFile и обходит setHeaders), так что браузер всегда перепроверяет код, управляющий маршрутизацией. Не-кодовые статические ассеты сохраняют кэширование express.static по умолчанию. Заголовки безопасности (CSP / nosniff / frame-deny / referrer-policy) не изменены — проверено существующей сюитой security-headers (8 случаев), идущей зелёной рядом с новым тестом. +1 файл тестов tests/asset-cache-control.test.mjs — 4 случая (JS-ассеты no-store, CSS no-store, статический index.html no-store, оболочка глубокого маршрута catch-all SPA no-store), с загрузкой реального приложения против изолированного CAREER_OPS_ROOT. Плюс правка флакающего teardown в tests/playwright-smoke.mjs (отдельный коммит test(e2e)): дымовой SSE-тест авто-пайплайна теперь отменяет reader + прерывает fetch в finally, а хук after принудительно закрывает зависшие сокеты, устраняя пост-teardown «Error: aborted», который краснил Playwright e2e-джоб v1.54.6. 738 → 742. fix · test: tests/asset-cache-control.test.mjs. Подробно — CHANGELOG.md.
[1.54.6] — 2026-05-18
fix(a11y): S-7 — кнопка back-to-top на #/help несёт канонический класс-селектор back-to-top. Плавающая кнопка back-to-top на #/help работала корректно (проверено вживую), но её список классов (btn btn-primary help-back-top) находился вне конвенции селектора .back-to-top, на который нацелен тест spec §2 #28 — ужесточённый селектор флакнул бы (прогон регрессии S-7, «лёгкая победа»). Теперь кнопка несёт также канонический класс back-to-top. Чисто аддитивно и CSS-no-op: help-back-top (существующий CSS-хук) не изменён, а у back-to-top нет CSS-правила — это лишь стабильная ручка для тестов/автоматизации. Проверено вживую: document.querySelector('.back-to-top') разрешает кнопку, aria-label цел, 0 ошибок консоли. Существующий случай #12 в tests/help-nav-a11y.test.mjs расширен утверждением, что список классов кнопки back-to-top включает канонический селектор back-to-top (без нового файла). fix(a11y) · test: tests/help-nav-a11y.test.mjs. Подробно — CHANGELOG.md.
[1.54.5] — 2026-05-18
fix(a11y): F-V54-C — редактор TSV на #/batch имеет доступное имя. TSV <textarea> на #/batch имел подсказку, подключённую через aria-describedby, но никакого доступного имени — ни <label htmlFor>, ни aria-label/aria-labelledby (прогон регрессии F-V54-C; WCAG 1.3.1 Info & Relationships / 4.1.2 Name, Role, Value). aria-describedby поставляет описание, а не имя, поэтому скринридер озвучивал «edit text» без метки. Теперь textarea несёт aria-label через новый ключ i18n batch.tsvAria, согласованно с соседними полями управления прогоном, которые уже используют ключи *Aria; существующая подсказка describedby сохранена. Проверено вживую: aria-label присутствует + локализован, aria-describedby цел, 0 ошибок консоли. Новый ключ i18n batch.tsvAria во всех 8 локалях. +1 файл тестов tests/batch-tsv-accessible-name.test.mjs (2 случая: блок batch-tsv имеет aria-label через t(batch.tsvAria), сохраняя подсказку describedby; batch.tsvAria определён во всех 8 локалях); 736 → 738. fix(a11y) · test: tests/batch-tsv-accessible-name.test.mjs. Подробно — CHANGELOG.md.
[1.54.4] — 2026-05-18
fix(a11y): F-V54-B — кнопки действий строк на #/pipeline имеют доступные имена. Кнопки ▶ (оценить) и ✕ (удалить) для каждой строки на #/pipeline были только иконками с одним лишь атрибутом title (прогон регрессии F-V54-B; WCAG 4.1.2 Name, Role, Value). title не является надёжным доступным именем, поэтому пользователь скринридера слышал длинную череду неразличимых «button» и не мог понять, к какой строке относится удаление. Обе кнопки теперь несут явный aria-label, разрешённый по неоднозначности компактным URL через новый хелпер shortUrl() (host + …/ + последние 2 сегмента пути; фолбэк с обрезкой хвоста для непарсируемого ввода), так что дерево a11y читается как, например, «Delete: hh.ru/…/vacancy/12345». Без новых ключей i18n — переиспользует common.delete / pipe.evaluateBtn + URL. Проверено вживую: 1385 строк, имя каждой кнопки уникально для строки, 0 ошибок консоли. +1 файл тестов tests/pipeline-row-action-names.test.mjs (4 случая: обе кнопки подключены через shortUrl(url) + ровно две такие метки, shortUrl объявлен до использования, URL одного хоста, но разных вакансий не схлопываются, фолбэки голого хоста / непарсируемого / пустого ввода); 732 → 736. fix(a11y) · test: tests/pipeline-row-action-names.test.mjs. Подробно — CHANGELOG.md.
[1.54.3] — 2026-05-18
feat(config): структурированная форма полей для вкладки “Modes” в #/config (больше никакого сырого markdown). Вкладка “Modes” редактировала modes/_profile.md как один сырой <textarea> на каждую секцию ## (гранулярность на уровне секции из v1.36.0). По запросу пользователя — «собери данные по полям, разбей из документации, определи набор полей и реализуй поля именно, а не сырой» — теперь она отрисовывает структурированную форму полей, выведенную из задокументированной схемы (career-ops.org Quick Start §Step-5): Target Roles / Adaptive Framing / Comp Targets → повторяемые поля строк с метками и кнопками добавить/удалить (одна строка роли/угла/comp на поле, + Add line / ✕ на каждую строку с aria-label); Exit Narrative / Location Policy → один <textarea> с меткой для прозы. Каждое поле — реальный контрол, привязанный через <label htmlFor> с i18n-именем секции. Новый public/js/lib/modes-form.js (window.ModesForm) владеет логикой parse → render → collect(); он питает существующий путь слияния PUT /api/modes/_profile { sections }, поэтому преамбула, порядок и любая секция, которую форма не трогает, сохраняются байт-стабильно (слияние-а-не-замена, обеспечивается сервером). Безопасность данных: каноническая списковая секция, тело которой не является чистым маркированным списком (пользователь поместил туда прозу), и любая неканоническая секция ## откатываются к дословному <textarea> с меткой и пояснительной заметкой — произвольное содержимое проходит round-trip нетронутым, никогда не переформируется и не теряется молча. Стабильность round-trip доказана: serialise(parse(body)) ре-парсится идентично. Редактор сырого markdown всего файла остаётся в виде раскрытия Advanced с подтверждением для добавления/удаления секции и правки преамбулы (деструктивный гейт сохранения WS2 #4 без изменений). 10 новых ключей i18n (config.modesTargetRoles … config.modesUnknownNote) во всех 8 локалях. +1 файл тестов tests/modes-form.test.mjs (7 случаев); 725 → 732. Проверено вживую на изолированной фикстуре CAREER_OPS_ROOT: 5 канонических секций отрисованы как поля + 1 пользовательская секция как фолбэк с меткой, round-trip «отредактировать-и-сохранить» сохранил преамбулу + пользовательскую секцию, 0 ошибок консоли. feat(config) · test: tests/modes-form.test.mjs. Подробно — CHANGELOG.md.
[1.54.2] — 2026-05-18
feat(config): селектор модели OpenAI / Codex в #/config. В #/config не было способа выбрать модель OpenAI / Codex — выпадающие списки были только у ANTHROPIC_MODEL и GEMINI_MODEL, хотя OPENAI_API_KEY уже была доступна для мульти-CLI потока родительского проекта (Codex / OpenCode). Теперь OPENAI_MODEL — полноценный ключ окружения: добавлен в KNOWN_KEYS в env-config.mjs (упорядочен сразу после OPENAI_API_KEY) и в группу ключей core, и намеренно не в SECRET_KEYS — это id модели, а не учётные данные, поэтому он никогда не маскируется. config.js получает курируемый список OPENAI_MODELS (gpt-5-codex по умолчанию, далее gpt-5 / gpt-5-mini / gpt-4.1 / o4-mini / o3) и поле <select> OPENAI_MODEL, отрисовываемое сразу после ключа OpenAI, в точности повторяя поля моделей Anthropic/Gemini. Новые ключи i18n config.openaiModel + config.openaiModelHint во всех 8 локалях. +1 файл тестов tests/openai-model-selector.test.mjs (4 случая); 721 → 725. Проверено вживую: #/config → select OPENAI_MODEL с 6 вариантами, по умолчанию gpt-5-codex, привязан к метке, 0 ошибок консоли. feat(config) · test: tests/openai-model-selector.test.mjs. Подробно — CHANGELOG.md.
[1.54.1] — 2026-05-18
fix(a11y): F-V54-A — единственный <h1> на #/cv. Собственный # Name из markdown резюме рендерился как второй <h1> верхнего уровня рядом с <h1>CV</h1> заголовка страницы (прогон регрессии F-V54-A; WCAG 1.3.1 Информация и взаимосвязи / 2.4.6 Заголовки). Теперь cv.js пропускает каждую точку внедрения превью резюме (первичный рендер, обновление при импорте файла, живую синхронизацию редактора) через cvMd() с ограниченной областью действия, который сдвигает заголовки на уровень ниже (h1→h2 … h6→role="heading" aria-level="7"), так что страница сохраняет ровно один <h1>. Ограничено cv.js намеренно — UI.md используется совместно help/reports/deep/evaluate, которые управляют своими заголовками по-своему. +1 файл тестов tests/cv-single-h1.test.mjs (4 случая); 717 → 721. Проверено вживую: #/cv → 1 <h1>, пользовательский # Name теперь <h2>, 0 ошибок консоли. fix(a11y): F-V54-A · test: tests/cv-single-h1.test.mjs. Подробно — CHANGELOG.md.
[1.54.0] — 2026-05-18
WS10 — повторная валидация canonical-docs + паритет H3 help-бандла (финальный релиз конвергенции). CI-гейт CHANGELOG/структуры проверял только H2, поэтому docs/help/en.md молча сполз до 70 H3-подразделов, тогда как 7 локализованных бандлов остались на 68 — разрыв был в §17 (таблица «Reference adapters» + список «Common pitfalls», только по-английски). Оба теперь переведены на все 7 языков (имена файлов / ссылки / идентификаторы адаптеров сохранены байт в байт идентичными); все 8 бандлов теперь 17 H2 / 70 H3. Новый гейт паритета H3 в help-ru-config-section.test.mjs фиксирует это (716 → 717). canonical-docs-coverage.test.mjs 7/7 подтверждает, что help по-прежнему отражает все 5 гайдов career-ops.org/docs; UX-аудит WS2 (40 замечаний v1.41→v1.52) сверил каждый экран с docs — расхождений нет. docs/sdd/CONVENTIONS.md обновлён до v1.54.0 (итоги тестов, гейт паритета H3, файлы-выбросы по размеру, новый раздел соглашений по Доступности). WS0–WS10 завершены; остаётся только WS11. fix(docs): WS10 canonical re-validation + H3 parity · test(help): H3-parity gate. Подробно — CHANGELOG.md.
[1.53.0] — 2026-05-18
WS9 — пирамида тестов shell-поверхности (последний непокрытый слой). 4 скрипта bin/*.sh и хук .githooks/pre-commit имели нулевое покрытие; новый tests/sh-files.test.mjs добавляет 10 кейсов, фиксирующих синтаксис bash -n/sh -n, shebang + бит исполнения и поведенческие контракты, от которых зависят другие workstream’ы: career-ops-ui.sh — help завершается с 0 без утечки shell-source (страж регрессии v1.40.0), неизвестный verb завершается с 2, а usage() — это heredoc; start.sh — уважает NO_OPEN, требует Node ≥ 18 и делегирует подъём браузера в scripts/open-dashboard.mjs (страж v1.43.0); setup.sh — строгий режим, SKIP_START, клонирует оба репозитория; run_all.sh — разбор --quick/--no-e2e и 4 сьюта; .githooks/pre-commit исполняет ревьюера WS7, и ни один shell-файл не вызывает git --no-verify (страж жёсткого правила #7 CLAUDE.md); install-hooks.mjs прокладывает core.hooksPath. docs/architecture/TESTING.md — в диаграмму пирамиды добавлен базовый слой shell-поверхности + заметка с итогами v1.53.0 (716 кейсов node --test / 90 файлов + 4 E2E-поверхности). 706 → 716. Подробно — CHANGELOG.md.
[1.52.0] — 2026-05-18
WS2 LOWs #33–#40 — пакетная зачистка-полировка (закрывает очередь UX-аудита). Восемь замечаний низкой важности. fix(a11y/i18n): WS2 LOW batch — #33: #/dashboard — 3 CTA в шапке были несогласованы (лишь у 2 был ведущий значок); «Open Pipeline» теперь несёт 📋, и все три совпадают. #34: #/profile — fit/level архетипа рисовались двумя неоднозначными чипами; теперь с префиксом (Fit: / Level:) и соответствующим aria-label. #35: #/health — тосты Run-doctor / verify показывали сырые строки из doctor.mjs; теперь с i18n-ключом. #36: #/health — результаты проверок были плоским рядом <div>; теперь role=list <ul>/<li>, а бейдж статуса несёт aria-label="<check>: <status>". #37: #/reports — карточки отчётов были <div onClick> только под мышь; теперь role=link + tabindex + обработчик Enter/Space + aria-label. #38: #/activity — комментарий пагинатора гласил «200», тогда как код запрашивал 500; согласовано с константой CAP, и уведомление role=note всплывает, когда лимит 500 усекает старую историю. #39: #/batch — текстовые плейсхолдеры были захардкожены по-английски, тогда как их aria-label были localized; четыре теперь с i18n-ключом. #40: страницы режимов молча переименовывали главную кнопку после асинхронного зонда; теперь вежливая область role=status объявляет это. 10 новых ключей i18n × 8 локалей ({n} сохранён); +9 тестов: test: tests/low-sweep.test.mjs. 697 → 706. Закрывает очередь UX-аудита WS2 (#1–#40 от v1.41→v1.52); далее WS9 → WS10 → WS11. Подробно — CHANGELOG.md.
[1.51.0] — 2026-05-18
WS2 #13 + #14 + #18 + #19 + #20 — зачистка feedback/i18n на #/auto и #/evaluate. Пять замечаний UX-аудита. fix(a11y/ux): auto+evaluate — busy state, actionable HTTP errors, clipboard fallback, aria-live result, spinner-guarded submit — #13: кнопка Run на #/auto теперь показывает состояние занятости (is-loading + aria-busy + «Running…») вместо простого отключения. #14: неуспешный HTTP-запрос теперь выдаёт действенное i18n-сообщение на шаге И тост (auto.httpFail с {n}), вместо голого «HTTP 500». #18: «Copy prompt» ручного режима теперь использует асинхронный Clipboard API с запасным execCommand и тостит реальную ошибку вместо ложного «Copied». #19: контейнер результата evaluate теперь role=status aria-live=polite, так что долгий вызов LLM объявляется программам чтения с экрана. #20: кнопка Evaluate обёрнута в UI.withSpinner (был простой onClick: run, допускавший повторные отправки). 3 новых ключа i18n × 8 локалей; +6 тестов: 691 → 697. Также правка только тестов (коммит 7f8e250): teardown e2e pipeline-delete был на пути нативного confirm до v1.48; переведён на DELETE по API (fix(test): … — CI Playwright-e2e был красным; не регрессия продукта). Подробно — CHANGELOG.md.
[1.50.0] — 2026-05-18
WS2 #12 + #27 + #28 — доступность навигации справки. Три замечания UX-аудита на #/help для руководства из 17 секций и 90+ заголовков, исправлены в help.js. fix(a11y): help — single h1, labelled+filterable TOC, focus-on-anchor, back-to-top — #28: markdown документа открывался собственным # Title, порождая ВТОРОЙ <h1> на странице, чей хедер уже даёт канонический h1; теперь каждый <h1> статьи удаляется, так что h1 ровно один, а иерархия чисто начинается с секций <h2>. #27: <nav> оглавления был безымянным ориентиром (два <nav> без метки на странице); теперь у него есть aria-label (help.toc), и при клике по записи оглавления фокус перемещается на заголовок секции (tabindex=-1 + focus()), а не только прокрутка вьюпорта. #12: не было способа что-либо найти в длинном документе; фильтр type=search над оглавлением сужает записи по тексту заголовка вживую, а плавающая кнопка с aria-label «Back to top» появляется после прокрутки, возвращает к началу и возвращает фокус на <h1> страницы; её scroll-слушатель снимается на hashchange при уходе с #/help. 2 новых ключа i18n × 8 локалей — help.tocFilter, help.backToTop; +6 тестов: test: tests/help-nav-a11y.test.mjs. 685 → 691. Подробно — CHANGELOG.md.
[1.49.0] — 2026-05-18
WS2 #10 + #11 + #25 + #26 — доступность и сортировка таблицы tracker. Четыре замечания UX-аудита на #/tracker, исправлены в tracker.js. fix(a11y): tracker headers, sortable table, localized fix labels, empty state — #10: заголовок столбца действия был пустой строкой, а построчная кнопка Report не имела контекста; теперь у каждого <th> есть scope=col, заголовок действия и заголовки Score/PDF переведены на ключи i18n (были пустыми или захардкоженным английским), а кнопка Report получает aria-label с названием компании (<report> — <company>). #11: tracker без способа сортировки; заголовки Date / Score / Status теперь — управляемые с клавиатуры кнопки сортировки внутри <th> с aria-sort (none/ascending/descending); компаратор sorted() (числовой для score, locale-сравнение для date/status) выполняется до пагинации, а клик переключает направление и сбрасывает пагинатор. #25: track.normalize/dedup/merge были идентичным английским во всех 8 локалях, несмотря на то что это самые рискованные деструктивные элементы (переписывают data/applications.md на месте) — теперь корректно локализованы, плюс подсказка title. #26: первый запуск с нулём строк показывал то же сообщение «no match», что и переотфильтрованный список; rows.length === 0 теперь рендерит отдельное пустое состояние (заголовок + тело + CTA «Open pipeline»). 7 новых ключей i18n × 8 локалей + 3 перелокализованы; +6 тестов: test: tests/tracker-a11y-sort.test.mjs. 677 → 683. Подробно — CHANGELOG.md.
[1.48.0] — 2026-05-18
WS2 #8 + #22 — pipeline: подтверждение с захватом фокуса + доступность превью. Два замечания UX-аудита на #/pipeline, исправлены в pipeline.js. fix(a11y): pipeline UI.confirm() + live preview region — #8: три действия #/pipeline использовали нативный confirm() (фокус не захватывался): Delete в панели превью, построчное удаление ✕ и «Evaluate first»; теперь все проходят через UI.confirm() с захватом фокуса (инфраструктура v1.44.0) — два удаления danger:true (Cancel по умолчанию), «Evaluate first» danger:false; нативного confirm() в pipeline.js больше не осталось. #22: у previewPane не было live-роли, а сбой fetch запихивался в previewBody и рендерился как вводящий в заблуждение <pre> «preview»; теперь это role=region aria-live=polite с aria-label, а сбои устанавливают отдельный previewError, рендерящийся как отдельный блок role=alert (очищается при (пере)выборе и при удалении активной строки). 4 новых ключа i18n × 8 локалей; +5 тестов: test: tests/pipeline-confirm-preview.test.mjs. 672 → 677. Подробно — CHANGELOG.md.
[1.47.0] — 2026-05-18
WS2 #7 + #30 + #31 + #16 — зачистка доступности непривязанных меток. Четыре замечания UX-аудита, где у элементов формы не было программной метки (WCAG 1.3.1 / 3.3.2 / 4.1.2), теперь привязаны. fix(a11y): bind every swept form control to an accessible name — #7 scan.js: у чекбокса dry-run и выпадающего списка company-select были метки без for; добавлен htmlFor (с существующими id). #30 deep.js: у инпутов company / role были непривязанные метки; добавлены id + htmlFor (deep-company, deep-role). #31 apply.js: у url / jd были непривязанные метки; добавлены id + htmlFor (apply-url, apply-jd). #16 cv.js: у основного markdown-<textarea> не было доступного имени; привязан через aria-labelledby к видимому заголовку «Markdown» — имя для скринридера совпадает с заголовком на экране, без нового ключа i18n. Использует явный паттерн label[for]↔control[id], уже стандартный в batch.js / mode-page.js; без новых ключей i18n; ноль изменений поведения. +5 тестов: test: 667 → 672. Подробно — CHANGELOG.md.
[1.46.0] — 2026-05-18
WS2 #5 + #6 + #21 + #24 — доступность scan SSE. Четыре замечания UX-аудита на #/scan, исправлены в scan.js. fix(a11y): scan SSE — live-log region, Stop, run-state, error banner — #5: потоковая консоль теперь role=log aria-live=polite (+ aria-label, tabindex=0, прокрутка с клавиатуры), с отдельной визуально скрытой assertive-областью role=status, объявляющей терминальные события (завершено / провалено / остановлено). #6: кнопка Stop закрывает выполняющийся EventSource (es.close()), отменяет опрос результата и сбрасывает состояние; показывается только пока идёт scan. #21: кнопка Scan отключается + получает aria-busy во время выполнения, а Stop показывается — на обоих путях стрима (однофазном streamTo и многофазном runScanAll — последний завершает прогон только на терминальном done, final !== false). #24: сбой SSE больше не только тост на 3,5 с; теперь постоянный баннер role=alert показывает ошибку с действием повтора (повторно вызывает последнюю функцию прогона), очищается при следующем прогоне. 8 новых ключей i18n × 8 локалей; +7 тестов: test: tests/scan-sse-a11y.test.mjs. 660 → 667. Подробно — CHANGELOG.md.
[1.45.0] — 2026-05-18
WS2 #3 — вкладки #/config: полный паттерн WAI-ARIA Tabs. Три вкладки #/config (API keys / Profile / Modes) были обычными <button class="tab-btn"> с активацией только по клику: ни role, ни aria-selected, ни клавиатурной модели (UX-аудит HIGH #3, WCAG 4.1.2 / 2.1.1). fix(a11y): config.js tabs implement role=tablist/tab/tabpanel — теперь контейнер role=tablist с aria-label; каждая вкладка role=tab + id + aria-controls + aria-selected (синхронизируется в activate()) + блуждающий tabindex (0 у активной / -1 у остальных); панель role=tabpanel + tabindex=0 + aria-labelledby, отслеживающий активную вкладку. Полная клавиатурная навигация: ←/→/↑/↓ (с зацикливанием) + Home/End перемещают фокус И активируют. Легаси CSS-хук .tab-btn.is-active сохранён. +1 ключ i18n × 8 локалей (config.tablistLabel); +7 тестов: test: tests/config-tabs-aria.test.mjs. Также правка только для тестов: fix(test): retarget 2 stale auto-pipeline smoke tests — два smoke-теста Playwright-e2e эпохи до v1.34 проверяли переходную модалку, которую кнопка «Auto-pipeline» на дашборде перестала открывать в v1.34.0 (→ Router.go('/auto')); они оставались красными в отдельном CI-джобе Playwright-e2e. Перенацелены на экран #/auto. 653 → 660. Подробно — CHANGELOG.md.
[1.44.0] — 2026-05-18
WS2 #4 + #9 — подтверждение с захватом фокуса перед деструктивной перезаписью файлов родительского проекта. Два HIGH из UX-аудита, оба с потерей данных: (#4) config.js saveProfileRaw/saveModesRaw заменял весь родительский config/profile.yml / _profile.md без подтверждения; (#9) tracker.js Normalize/Dedup/Merge переписывал родительский data/applications.md на месте без подтверждения. fix(a11y/safety): UI.confirm() gate before whole-file parent overwrites — новый UI.confirm() в public/js/api.js, диалог с захватом фокуса, переиспользующий существующую WAI-ARIA-инфраструктуру модалок (хук _onClose приводит к тому, что Esc / backdrop / × / Cancel — все пути закрытия резолвят false; фокус по умолчанию на Cancel; возвращает Promise<boolean>; НЕ нативный confirm()). Все три деструктивных вызова теперь защищены перед записью. 8 новых ключей i18n × 8 локалей (плейсхолдер {op} сохранён дословно); +8 тестов: test: tests/confirm-gate.test.mjs, 644 → 652. Подробно — CHANGELOG.md.
[1.43.0] — 2026-05-18
По запросу пользователя — career-ops-ui open + autostart с подъёмом окна браузера. После setup/run голый open/xdg-open оставлял вкладку дашборда на заднем плане, когда браузер уже был запущен, и пользователю приходилось её искать. feat(cli): career-ops-ui open — open AND raise the dashboard tab — новый scripts/open-dashboard.mjs строит URL из HOST/PORT (переписывая бинд 0.0.0.0 на loopback), при необходимости ждёт /api/health, открывает браузер по умолчанию и затем принудительно поднимает его на передний план — macOS через osascript активирует тот из Chrome/Brave/Edge/Safari/Arc/Firefox, что запущен, Linux через xdg-open+wmctrl, Windows через start. Представлено как глагол career-ops-ui open (алиасы dash, focus). Autostart в bin/start.sh теперь делегирует ему, так что вкладка поднимается автоматически; NO_OPEN=1 отключает auto-open для headless/CI-запусков. README ×8 + help §1 ×8 обновлены; +8 тестов: test: tests/open-dashboard.test.mjs, 636 → 644. Подробно — CHANGELOG.md.
[1.42.0] — 2026-05-18
WS2 исправление №2 — мёртвый маршрут #/portals → глубокая ссылка на config. #/portals был незарегистрированным маршрутом и рендерил 404-вьюшку, хотя это вполне правдоподобный URL для закладки/ручного ввода при управлении источниками порталов (HIGH №2 из UX-аудита). fix(router): #/portals 404 → alias to config + Regional-sources deep-link — в ALIASES файла router.js добавлено portals: 'config' (тот же паттерн стабильности закладок, что и settings→profile), так что теперь это резолвится во вьюшку config с активным пунктом навигации config. Когда существует группа Regional-sources, вьюшка (config.js) обнаруживает хеш #/portals, принудительно раскрывает эту <details>-группу, прокручивает её в видимую область и переводит фокус на её summary (переопределяя фокус по умолчанию на h1), так что пользователь попадает ровно на элементы управления источниками порталов; никогда не рендерит пустую региональную группу из-за одного лишь алиаса. help-bundle §5 × 8 получил заметку о шорткате; +1 тест роутера: test(router): portals→config alias guarantee в router.test.mjs, 635 → 636. Подробно — CHANGELOG.md.
[1.41.0] — 2026-05-18
WS2 — старший аудит UX/юзабилити + сквозное исправление управления фокусом. Эвристический аудит с опытом 10+ лет (Nielsen × WCAG 2.2 AA × конвенции проекта) всех 17 маршрутов дал очередь из 40 находок, ранжированную по серьёзности (.planning/.../UX-AUDIT.md); HIGH→MEDIUM→LOW теперь поставляются по одному исправлению на релиз. Этот релиз закрывает сквозной HIGH №1. Исправления: fix(a11y): move focus to the new view on every route change — router.js render() заменял #content на каждом hashchange, но никогда не перемещал фокус, поэтому пользователи клавиатуры / скринридера оставались на уничтоженном узле и теряли своё место (WCAG 2.4.3 Focus Order / 4.1.3 Status Messages — сквозное, затрагивало все 17 экранов); новый focusNewView(content) фокусирует первый h1/.page-title новой вьюшки (лаконичный анонс для SR + правильный порядок фокуса), делая заголовок фокусируемым (tabindex=-1) при необходимости и откатываясь на #content; самая первая отрисовка пропускается, чтобы не конфликтовать со skip-link; подключено и на успешном, и на ошибочном путях рендера; проверено вживую: после навигации document.activeElement — это H1.page-title новой вьюшки. Тесты: test(router): focus-management static guarantees — 4 кейса в router.test.mjs (хелпер определён, цель-заголовок + фолбэк на content, страж пропуска первой отрисовки, ≥2 места вызова); 631 → 635. Подробно — CHANGELOG.md.
[1.40.0] — 2026-05-18
WS8.3 — проход актуализации docs + исправление career-ops-ui help + усиление askSecret. Исправления: fix(cli): career-ops-ui help no longer leaks shell source — диспетчер печатал свой заголовочный комментарий через sed -n '2,12p', но строка 12 (set -euo pipefail) — это код, а не комментарий, поэтому career-ops-ui help (и текст использования при неизвестном глаголе) заканчивался лишней строкой set -euo pipefail; сужено до 2,11p (блок комментария) в обоих случаях — help и *); help завершается с кодом 0, неизвестный глагол — с кодом 2 — проверено. fix(cli): scripts/init.mjs key entry never echoes — доработка v1.39.0 заменила косметическую readline-маску реальным ридером в raw-режиме: setRawMode(true) + буферизованная строка, так что байты введённого/вставленного ключа вообще не доходят до терминала (нет утечки в scrollback / tmux / при демонстрации экрана); полный FSM VT-escape поглощает каждую последовательность CSI/SS3/OSC/DCS/SOS/PM/APC, так что стрелки и функциональные клавиши не портят секрет; stdin внедряется как зависимость, поэтому не-TTY fallback покрыт юнит-тестом без правки глобала; итерации до чистого LGTM от AI-ревью. Документация: README ×8 — старый раздел «установка одной командой» заменён заметным разделом «Запуск и инициализация одной командой» (curl-однострочник плюс явная цепочка CLI career-ops-ui: clone → npm link → setup → init → doctor → run → help, объяснение мастера провайдера, CI-форма --provider --anthropic-key --yes и примечание о LLM_PROVIDER); все 8 README-бейджей актуализированы с v1.22–v1.24 / tests-461–474 до v1.40.0 / tests-631 (e2e-бейдж сделан нечисловым, чтобы не выдумывать счётчик); help-bundle ×8 §1 — врезка-callout «Запуск и init одной командой» добавлена в начало руководства быстрого старта (перед «A. Setup») во всех 8 локалях; паритет H2-секций сохранён (по 17 — CI-гейт зелёный). Тесты: test(init): non-TTY askSecret fallback — provider-selector.test.mjs получает DI-stdin кейс, проверяющий, что askSecret делегирует обычному ask() (паритет trim) вне TTY без мутации общего глобала; 629 → 631. Подробно — CHANGELOG.md.
[1.39.0] — 2026-05-18
WS8.2 — селектор LLM-провайдера + ключ OpenAI/Codex + интерактивный мастер init. В env-config LLM_PROVIDER (auto|claude|gemini) + OPENAI_API_KEY (секрет). providerOrder() используется всеми 6 gate-site llm.mjs через _provGate(); для auto поведение неизменно. select + поле в #/config. scripts/init.mjs теперь реальный мастер (пишет parent .env через validated-path). 7 тестов. 622 → 629. README ×8 / fold канон-доков = WS8.3/WS10. Подробно — CHANGELOG.md.
[1.38.0] — 2026-05-17
WS8.1 — единый CLI-диспетчер + verb doctor. bin/career-ops-ui.sh маршрутизирует setup/run/doctor/init/help. scripts/doctor.mjs переиспользует точный движок /api/health (createApp in-process → терминальный отчёт); exit 0 только если все ОБЯЗАТЕЛЬНЫЕ проверки зелёные. docs/sdd + help §1 ×8. 6 тестов. 616 → 622. README quickstart ×8 = WS8.3 (финальный шаг). Подробно — CHANGELOG.md.
[1.37.0] — 2026-05-17
WS7 — pre-commit AI-ревью в git-workflow. Детерминированный floor (fail-HARD): блокирует staged .env/секреты, паттерны ключей в diff, .also( в staged-вьюхах, провал node --check. AI-слой (fail-SOFT): claude -p по diff, если CLI есть и AI_REVIEW != off. .githooks/pre-commit + prepare прописывает core.hooksPath. Никогда --no-verify. docs/sdd. 6 тестов. 610 → 616. Подробно — CHANGELOG.md.
[1.36.0] — 2026-05-17
WS6.3 — вкладка Modes: сырой блоб → посекционный редактор. WS6 завершён. modes/_profile.md правится по ##-секциям (свой сворачиваемый textarea на заголовок). Серверный splitProfileSections байт-точен; PUT { sections } мёржит только названные секции — преамбула + чужие секции + порядок сохраняются байт-в-байт. Неизвестный заголовок → 400. Raw-путь без изменений. i18n 5 ключей ×8. help §2 ×8. +6 тестов. 604 → 610. WS6 закрыт (API-keys/Profile-скаляры/Profile-массивы/Modes-секции — всё структурировано). Подробно — CHANGELOG.md.
[1.35.0] — 2026-05-17
WS6.4 — редакторы массивов Profile + аудит WS6.2 API-keys. PUT /api/profile принимает { arrays } (комбинируется с { fields }): Target roles/Superpowers (списки), Archetypes (name/level/fit), Proof points (name/url/hero-metric). Та же гарантия merge-not-replace; пустые строки отброшены; пустой список удаляет ключ. 4 add/remove-редактора в #/config. i18n 6 ключей ×8. Аудит: KNOWN_KEYS ≡ FIELDS, гэпа нет. +7 тестов. 597 → 604. Подробно — CHANGELOG.md.
[1.34.0] — 2026-05-17
WS5 — экран Auto-pipeline в один клик (#/auto). Модалка повышена до отдельной линкуемой страницы. Один клик: валидация→загрузка→оценка→сохранение отчёта→трекер (SSE). Доступный stepper (aria-current, live-region), deep-link на отчёт/трекер, ручной режим без ключа, линкуется #/auto?url=…&go=1. Пункт в сайдбаре; кнопка ✨ дашборда ведёт сюда. i18n 14 ключей ×8. help §1 ×8 + README ×8. +8 тестов. 589 → 597. Подробно — CHANGELOG.md.
[1.33.0] — 2026-05-17
WS4 — аудит паритета с career-ops 1.8.0 + location_filter. В родительском scan.mjs появился location_filter (#570); in-process сканеры web-ui не делегируют ему, поэтому не флоу. Новый server/lib/location-filter.mjs повторяет семантику дословно; подключён в оба сканера. help §5 ×8. +8 тестов. 581 → 589. Остальной дельта классифицирован в PARENT-PARITY.md. Подробно — CHANGELOG.md.
[1.32.0] — 2026-05-17
Вкладка Profile в #/config — сырой YAML-блоб → форма по полям (WS1). 3 сворачиваемые секции (Кандидат / Нарратив / Компенсация), 14 скалярных путей. Сохранение по полям мёржит в config/profile.yml: архетипы, proof points и кастомные ключи сохраняются нетронутыми. Raw-YAML escape-hatch сохранён в Advanced (хранит комментарии). 23 i18n-ключа ×8. +7 тестов (вкл. инвариант выживания неизвестных ключей). 574 → 581. Подробно — CHANGELOG.md.
[1.31.0] — 2026-05-17
Синхронизация с career-ops 1.8.0 — #/batch отдаёт --model + --start-from. Родительский проект 1.7.1 → 1.8.0; в batch-runner.sh добавлены --model NAME (#504) и --start-from N. web-ui выводит их в #/batch (поля Модель и Старт с #) с серверной defense-in-depth валидацией (charset для model, целое 1..100000 для start-from). i18n ×8. +7 тестов. 567 → 574. Полностью — в CHANGELOG.md.
[1.30.0] — 2026-05-14
Paginator на #/scan — заменяет truncation «первые 200 из N» из v1.12.
До v1.30 таблица результатов скана жёстко обрезалась на первых 200 отфильтрованных строках, внизу шла подпись «Показаны первые 200 из N». Строки 201..N были недоступны из UI — приходилось руками сужать title_filter.positive в portals.yml. v1.30.0 заменяет cap на UI.paginate (тот же хелпер что в #/tracker / #/reports / #/activity). PAGE_SIZE = 200 сохраняет прежнюю плотность; сортировка boost-to-top стабильна между страницами (сортируем ПОЛНЫЙ набор и только потом режем на страницы); автореsет на page 1 при смене любого фильтра. Устаревший i18n-ключ scan.shownTop удалён (×8 локалей). 9 новых кейсов в tests/scan-paginator.test.mjs (7 статических канарий + 1 логическая таблица с 6 граничными кейсами + 1 расчёт summary). 558 → 567 unit + acceptance (+9 новых). Полные подробности в CHANGELOG.md.
[1.29.2] — 2026-05-14
Hot-fix: 🌐 Scan с source=both запускал только EN-фазу. RU-фаза тихо терялась.
SSE-клиент (public/js/api.js:156) закрывал EventSource на ПЕРВОМ событии done, но сервер в source=both шлёт по одному done на фазу. RU-фаза стартовала и сразу же отменялась. Фикс: сервер маркирует каждый done полем final: true|false; клиент закрывает поток только когда final !== false. Обратная совместимость — продюсеры одиночных фаз без final закрываются как раньше. 547 → 558 unit + acceptance (+11 новых). Полные подробности в CHANGELOG.md.
[1.29.1] — 2026-05-14
Подробный пользовательский гайд по настройке 5 русских порталов в help-bundle §5, во всех 8 локалях.
Новая ### подсекция «Настройка русских порталов — подробное руководство» внутри §5 (Portals & sources): инвентарная таблица 5 источников с auth и гео-ограничениями, пошаговый поиск и редактирование portals.yml, полный YAML-пример с 5 источниками, коллизия с negative-list’ом и пример исправления, как временно отключить отдельный источник, как проверить через 🌐 Scan + SSE-лог. §17 (релиз v1.29.0) описывает девелоперский флоу; §5 в v1.29.1 — пользовательский флоу. 540 → 547 unit + acceptance (+7 новых). Полные подробности в CHANGELOG.md.
[1.29.0] — 2026-05-14
Сканер русских порталов расширен с 2 до 5 источников. Registry + динамический dropdown. Новая секция §17 «Как добавить новый источник».
- 3 новых RU-адаптера:
Trudvsem(open-data API «Работа в России», без auth, без гео-гейта),GetMatchиGeekJob(HTML-скрейп с defensive-парсером —[]при неудачном парсе, никогда не throw на здоровом 200). - Source registry в
server/lib/sources/registry.mjs— единый источник правды, который читают dispatcher + endpoint + dropdown. До v1.29 список жил жёстко-захардкоженным в трёх местах. - Новый endpoint
GET /api/scan/sourcesсCache-Control: max-age=60— SPA при mount’е#/scanдинамически перестраивает dropdown. - Новая §17 во всех 8 локалях: «Как добавить новый источник для скана» (template адаптера, запись в registry, dispatcher, мокированный тест,
portals.yml). - Дефолт
russian_portals.sourcesменяется с["hh", "habr"]на 5 источников; если у тебя вportals.ymlуже явно перечисленыsources:, добавь три новых руками. - Тесты: 520 → 540 (+20). Полные подробности в
CHANGELOG.md.
[1.28.1] — 2026-05-14
Hot-fix: router 404 на хэшах с ?query. Строку HH_USER_AGENT убрали из health.
До v1.28.1 Router.go('/evaluate?url=…') производил hash, чей первый сегмент по split('/') был литералом "evaluate?url=…", который никогда не совпадал с зарегистрированной ветвью → __not_found__ (404). Однострочный фикс: hash.split('?')[0] перед разбиением имени. Закрывает оба известных клика: #/pipeline → ▶ и «App settings → Modes». Опциональная строка HH_USER_AGENT убрана из /api/health (подсказка про 403-вне-РФ остаётся в help-bundle §16 и в stderr самого ru-scanner). 515 → 520 unit + acceptance (+5 новых). Полные подробности в CHANGELOG.md.
[1.28.0] — 2026-05-14
Выравнивание документации + контрол --max-retries N на #/batch. Закрывает два открытых issue, поднятых в qa/QA-PROMPT-docs-vs-app.md.
- Issue #2 — на
#/batchпоявился числовой input «Max retries» (1–10), активный только когда отмечен «Retry failed». Сервер парсит черезparseIntи валидирует 1≤N≤10; значения вне диапазона тихо отбрасываются, без--retry-failedфлаг не пробрасывается вовсе. 7 кейсов вtests/batch-max-retries.test.mjs. 2 новых i18n-ключа × 8 локалей. - Issue #1 — список AI-CLI в 8 help-bundle и 8 README выровнен под канон career-ops.org/docs (Claude Code · Codex · OpenCode · Qwen CLI), с локализованной строкой: «другие Claude-совместимые CLI работают через тот же интерфейс slash-команд». Бюллет «Multi-CLI» в README про shim-файлы web-ui сохранён без изменений (это другая поверхность). 2 новых канария в
tests/canonical-docs-coverage.test.mjs. - 506 → 515 unit + acceptance (+ 9 новых). Playwright 32/32 без изменений. Полные подробности в
CHANGELOG.md.
[1.27.0] — 2026-05-14
Косметика + a11y: устранён дубль #/dashboard в сайдбаре.
В сайдбаре логотип бренда (<a class="logo" href="#/dashboard">) и первый пункт навигации указывали на один и тот же маршрут. Скринридеры дважды произносили «Dashboard», а у клавиатурных пользователей появлялся лишний tab-stop. Блок логотипа теперь — обычный <div class="logo">; пункт навигации остаётся единственной ссылкой на #/dashboard. 506 / 506 unit + 32 / 32 Playwright — без изменений. Подробности в CHANGELOG.md.
[1.26.1] — 2026-05-14
Hot-fix WCAG 2.5.5 — восстановлен минимум min-height: 44px на .btn.
В v1.26.0 декларация min-height: 44px в .btn отсутствовала, header-кнопки рендерились на 39-41 px (нарушение WCAG 2.5.5). v1.26.1 восстанавливает 44-пиксельный пол + flex-shrink: 0 + line-height: 1.2. 502 → 506 unit, Playwright 32/32 без изменений. Детали в CHANGELOG.md.
[1.26.0] — 2026-05-14
Пирамида тестов + line-покрытие ≥ 93 %.
Внедрена 4-уровневая пирамида тестов (unit → functional → acceptance → e2e) согласно backlog’у v1.25. Добавлены 22 новых теста, покрывающие крупнейшие пробелы из v1.25 (jds.mjs 61.64 % → 100 %, ветки отказа в auto-pipeline). Появилась директория tests/acceptance/ для тестов пользовательских сценариев через несколько endpoint’ов. 480 → 502 unit + acceptance, Playwright 32/32 без изменений. Полные детали в CHANGELOG.md и docs/architecture/TESTING.md.
[1.25.0] — 2026-05-14
Короткое замыкание ручного режима авто-пайплайна + косметика дашборда + добивка паритета CHANGELOG. Релиз закрывает G-014 (авто-пайплайн игнорировал mode: 'manual'), G-012 (рассинхрон CHANGELOG — 6 локалей отставали на два релиза) и косметический баг с двойным глифом ✨ ✨ на дашборде. G-003 (переименование README.cn.md) де-факто уже закрыт — в репозитории присутствует только README.zh-CN.md. G-005 (перестройка блоков отчётов A-G → A-F) требует согласованного коммита в родительском проекте и переносится дальше.
🛡️ G-014 — короткое замыкание mode: 'manual' в авто-пайплайне
fix(auto-pipeline): G-014 — honour mode:'manual' short-circuit(server/lib/routes/auto-pipeline.mjs:158-195) — до v1.25 маршрут всегда обращался к LLM. Передачаmode: 'manual'(по аналогии с/api/evaluateначиная с v1.10.2) тихо игнорировалась, запрос висел 1–3 минуты на Anthropic. Теперь обработчик:- Принимает
modeИevalModeради обратной совместимости. Любое из значений'manual'запускает короткое замыкание. - Эмитит все 5 этапов SSE со
status: 'done'/status: 'skipped'. Без fetch. Без вызова LLM. Без $0.05 за запрос. - Полезная нагрузка
doneнесёт{ mode: 'manual', prompt: <buildEvaluationPrompt scaffold>, message }— SPA может отрендерить её так же, как уже существующую карточку ручного промпта/api/evaluate.
- Принимает
- Закрывает риск DoS на
HOST=0.0.0.0: ранее, даже когдаllmRateLimitограничивал 10 запросов в 60 с с IP, 10 атакующих × 10 запросов = $50/мин расхода Anthropic. Короткое замыкание срабатывает до того, как декремент лимита запросов учтётся для реального вызова. - Тесты —
tests/auto-pipeline-manual-mode.test.mjs: 3 теста подтверждают (1)mode: 'manual'возвращает ответ менее чем за 2 с со всеми 5 ключами этапов, (2) даже при заданномANTHROPIC_API_KEYкороткое замыкание всё равно срабатывает (исходный симптом), (3) старые вызовы сevalMode: 'manual'продолжают работать.
📝 G-012 — добивка паритета CHANGELOG (6 локалей × 2 пропущенных релиза)
docs(changelog): backfill v1.23.0, v1.24.0, v1.24.1, v1.25.0 in 6 lagging locales— до v1.25 только EN содержал v1.23–v1.24; RU отставал на один релиз, остальные 6 — на два. v1.25 запускает параллельных переводческих агентов (по образцу цикла v1.23), которые добавляют все четыре записи вCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md. RU получает v1.24.0 + v1.24.1 + v1.25.0 (v1.23.0 уже присутствовал с цикла v1.23).feat(ci): scripts/check-changelog-parity.mjs gate— падает сборка, если самая свежая запись CHANGELOG любой локали старше канонического EN. Подключено вnpm run test:ci. Текущий рассинхрон G-012 поймал бы себя сам в момент пересечения границы EN.
✨ Косметика — устранение двойного глифа на дашборде
fix(dashboard): dedup ✨ glyph in auto-pipeline button label(public/js/lib/i18n-dict.js:219) —dash.autoPipelineнёс ведущий✨в строке каждой локали, Иpublic/js/views/dashboard.js:58добавлял ещё один✨во вьюхе. Результат: кнопка рендерилась как✨ ✨ Auto-pipeline …. v1.25 убирает ведущий глиф из значения DICT каждой локали; префикс вьюхи остаётся единственным источником. Тот же проход аудита прошёлся по остальному i18n-бандлу — других случаев двойного глифа не обнаружено.
🚫 Перенесено на следующий релиз
- G-005 — перестройка блоков отчёта A-G → A-F согласно каноническому career-ops.org/docs — требует согласованного коммита в родительском проекте
santifer/career-ops(переписатьmodes/oferta.mdтак, чтобы он эмитил A=Role, B=CV-match, C=Strategy, D=Comp, E=Personalization, F=STAR — отказаться от C-Risks/G-Legitimacy как отдельных блоков). v1.25.0 поставляет сторону web-ui, готовую к новой схеме (reports.jsпринимает произвольные буквы блоков с v1.13). Закладывается в следующее релизное окно, когда родитель и потомок смогут выйти вместе. - G-003 — переименование
README.cn.md→README.zh-CN.md— проверено при подготовке v1.25: в репозитории уже присутствуетREADME.zh-CN.md(никакого осиротевшегоREADME.cn.mdнигде в worktree). Находка G-003 устарела.
🧪 Тесты
- 477 → 480 unit (+3 из PR-B
auto-pipeline-manual-mode.test.mjs). - 32/32 Playwright без изменений.
npm run test:ciтеперь прогоняетnpm test+check-no-also-leftovers.mjs+check-changelog-parity.mjs.
Верификация
$ npm run test:ci
# 480 / 480
# ✓ no .also( leftovers in views/
# ✓ CHANGELOG parity: all 8 locales at v1.25.0
# G-014 — manual mode returns < 2 s even with ANTHROPIC_API_KEY set:
$ ANTHROPIC_API_KEY=sk-ant-test PORT=4317 npm start &
$ sleep 3
$ time curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/x","mode":"manual"}' \
http://127.0.0.1:4317/api/auto-pipeline | head -20
# real 0m0.1xx s (was 1-3 min)
# event: start … event: step (×5) … event: done {"mode":"manual","prompt":"…"}
# G-012 — every locale CHANGELOG carries the v1.25.0 entry:
$ grep -c '^## \[1.25.0\]' CHANGELOG*.md
# 8 files, each → 1
# Cosmetic — dashboard glyph:
$ grep "dash.autoPipeline" public/js/lib/i18n-dict.js
# No leading ✨ in any locale value (view supplies the single glyph)
Несовместимые изменения
Нет. mode: 'manual' подключается опционально; старые вызовы с evalMode: 'manual' продолжают работать без изменений.
Вне рамок (v1.26+)
| Пункт | Примечания |
|---|---|
| G-005 — перестройка блоков отчёта A-F | Требует согласованного коммита в родительском проекте (santifer/career-ops переписывает modes/oferta.md). |
| Живое выполнение визуальных подтестов QA-сценария 31 | Требуют браузерного агента (Claude Cowork). Частично покрыты Playwright smoke. |
Превышение i18n-dict.js целевого лимита в 400 LOC |
Фикстура переводов — освобождена политикой. Разделение добавило бы HTTP-запросов без сборщика. |
[1.24.1] — 2026-05-14
Hot-fix: падение #/config во всех 8 локалях (G-015).
🚑 Критический hot-fix
fix(config): G-015 — replace removed Element.prototype.also call in config.js(public/js/views/config.js:371) — v1.22.0 N-2 удалил глобальный monkey-patchElement.prototype.alsoи перевёлcv.jsна шаблон со свободными выражениями, но пропустилconfig.js. В итоге#/configпадал при первом обращении в каждой локали с ошибкойc(...).also is not a function. v1.24.1 применяет тот же шаблон миграции, что и вcv.js:188-201— вынести дерево вconst root = c(...), выполнить блок активации отдельно и затемreturn root;.
🛡️ Шлюз CI
feat(ci): scripts/check-no-also-leftovers.mjs sweep— обходит каждый файл подpublic/js/views/и валит сборку на любом вызове.also((ссылки в комментариях допустимы). Подключено в новый скриптnpm run test:ci. Будущий откат удаления monkey-patch не сможет тихо вернуть ту же регрессию.
🧪 Тесты
test: tests/config-view-syntax.test.mjs— три страховки:- парсинг
config.jsчерезnode:vm.Script(ловит регрессии уровня синтаксиса без Playwright) - проверка, что
.also(не выживает вне комментариев - проверка, что якоря миграции
const root = c(...)/return root;на месте
- парсинг
- 474 → 477 unit (+3) + 32/32 Playwright без изменений.
Верификация
$ npm run test:ci
# 477 / 477
# ✓ no .also( leftovers in views/
# Browser smoke:
$ open http://127.0.0.1:4317/#/config
# → renders normally, no "is not a function" card. Every locale equivalent.
Вне рамок (перенесено на v1.25)
- G-014, G-012, G-005, G-003 — см. запись v1.25.0 ниже единым пакетом.
[1.24.0] — 2026-05-14
Обновление глубины содержимого справочного бандла + живое выполнение QA-сценария 31 + сквозной перевод RU CHANGELOG. Релиз закрывает оба пункта, перенесённых таблицей «Out of scope» из v1.23.0 на v1.24: полное обновление глубины содержимого всех 8 справочных бандлов из 5 канонических URL career-ops.org/docs (с v1.11.x присутствовало только покрытие URL) и живое выполнение QA-сценария 31 на запущенном сервере (числилось как «требует браузерного агента + LLM-учётных данных» — оказалось, что 6/6 подтестов достижимы через curl + grep, и только визуальные подтесты требуют браузера).
📖 Обновление глубины содержимого справочного бандла
docs(help): refresh en.md from 5 canonical career-ops.org/docs URLs(docs/help/en.md) — до v1.24 EN-бандл содержал 1113 строк и перечислял 5 канонических URL во front-matter, но не раскрывал их в теле. v1.24 загружает все 5 URL через WebFetch и углубляет соответствующие H2-разделы:- About career-ops (front-matter) — добавлены принципы (суверенитет данных, AI-agnostic, контроль со стороны человека), блок «What career-ops is NOT», расширена инвентаризация концепций с 6 до 10 строк (добавлены Proof points, JD store, Interview-prep, Batch additions).
- §5 Portals — добавлен канонический bootstrap
cp templates/portals.example.yml portals.yml, уточнены обязательные и опциональные поля каждой записиtracked_companies. - §7 Scan — добавлено примечание «no AI tokens consumed» для варианта A, перечень последующих команд (
apply/contacto/deep/tracker). - §14 Apply checklist — разделён на режим SPA-чеклиста, режим Manual-vs-Playwright-assisted и полный CLI-сценарий (канонические 8 пронумерованных шагов от
/career-ops apply <company>доSubmitted.с авто-переходомEvaluated → Applied); подсекция batch evaluate получила таблицу схемы TSV + документацию всех 4 флагов +merge-tracker.mjs --dry-run; подсекция Playwright Setup перечисляет команды установки, регистрацию MCP, альтернативу.claude/settings.local.json, примечание о headless-режиме по умолчанию.
- Сохранён паритет 16 H2-разделов (CI-тест
help-ui.test.mjs::section-parityутверждает ровно 16 H2-разделов во всех 8 локалях). - Каждый из 5 канонических URL встречается ≥ 2 раз в бандле (CI-тест
canonical-docs-coverage.test.mjsэто обеспечивает). Счётчики на URL после v1.24:what-is-career-ops× 4,scan-job-portals× 5,apply-for-a-job× 3,batch-evaluate-offers× 5,set-up-playwright× 3. docs(help): translate the v1.24 deepening to 7 non-EN locales— запущены 7 параллельных переводческих агентов. Каждая целевая локаль (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) получает обновлённый бандл, который зеркалит структуру EN раздел в раздел, сохраняет дословно блоки кода, URL, пути к файлам и метки кнопок (📁 Upload CV / 🌐 Scan now / ▶ Evaluate / 📄 Generate PDF / 💾 Save), а также английские аббревиатуры (CSP, SSRF, TOCTOU, WCAG, ATS, JD, SSE, REST, API), и переводит углубление на нативный технический стиль публикационного уровня на целевом языке.
🧪 QA-сценарий 31 — живое выполнение (6/6 PASS)
-
docs(qa): append last-verified live-execution log to qa/claude-cowork-browser-test-prompt.md— до v1.24 сценарий 31 был задокументирован, но ни разу не прогонялся на живом сервере (был отложен как «требует браузерного агента + LLM-учётных данных»). v1.24 прогнал все 6 подтестов наhttp://127.0.0.1:4317:Sub Описание Статус 31.1 Пороги баллов в справочных бандлах ✅ PASS (4.5 × 3, 4.0 × 9, 3.5 × 6 упоминаний в docs/help/en.md)31.2 Эндпоинты сценария сканирования ✅ PASS ( /api/stream/scan-{en,ru}+/api/scan-ru/config→ 404;/api/scan/regional/config→ 200)31.3 Чеклист /api/apply-helper✅ PASS (тело содержит career-ops apply+ предупреждениеauto-submit)31.4 Эндпоинт /api/batch✅ PASS (ключи [exists, runnerExists, raw, rows, additions])31.5 Доступность Playwright ✅ PASS ( /api/healthсообщаетPlaywright (parent node_modules) ok: true, value: installed)31.6 Покрытие URL справочного бандла (5 URL × 8 локалей) ✅ PASS (40 / 40 ✓) Чисто визуальные подтесты (требуют браузер) помечены отдельно в QA-промпте — они остаются выполнимыми через Claude Cowork или
npm run test:e2e:browser.
🌐 RU CHANGELOG end-to-end (продолжение M-9)
docs(translate): CHANGELOG.ru.md retry agent — full body translation(CHANGELOG.ru.md) — релиз v1.23.0 вышел с переводческим агентом для RU CHANGELOG ещё в работе (он один раз упал с ошибкой сокета и был перезапущен). v1.24 подхватывает 1542-строчный полный перевод агента: каждая запись от v1.23.0 до v1.6.0 получает русское тело публикационного уровня, больше никаких EN-телесных заглушек. Стилевая дисциплина соответствует обновлению качества README в v1.22.0: «функциональность» / «возможности» / «поведение» вместо неуклюжего «функционал»; «через» / «с помощью» вместо «при помощи»; активный залог над пассивным; «эндпоинт», «лимит запросов», «состояние гонки», «санитайзинг» как канонические термины; английские аббревиатуры (TOCTOU, CSP, SSRF, WCAG, ATS, JD, SSE, REST, API) сохранены.
🧪 Тесты
- 474 / 474 unit + 20 / 20 smoke E2E + 32 / 32 Playwright. Нулевые поведенческие дельты тестов; каждая CI-проверка справочного бандла (16 H2-разделов × 8 локалей, 5 URL × ≥ 2 упоминания, минимум по содержанию) по-прежнему зелёная.
Верификация
$ npm test # 474 / 474
# Help-bundle deepening:
$ wc -l docs/help/en.md
# ~1270 lines (was 1113 — deepened, not bloated)
$ for url in what-is-career-ops scan-job-portals apply-for-a-job \
batch-evaluate-offers set-up-playwright; do
echo -n "$url: "
grep -c "$url" docs/help/en.md
done
# what-is-career-ops: 4
# scan-job-portals: 5
# apply-for-a-job: 3
# batch-evaluate-offers: 5
# set-up-playwright: 3
# Scenario 31.6 — 40/40 URL coverage:
$ for lang in en es pt-BR ko ja ru zh-CN zh-TW; do
echo -n "$lang: "
for url in what-is-career-ops scan-job-portals apply-for-a-job \
batch-evaluate-offers set-up-playwright; do
curl -sS "http://127.0.0.1:4317/api/help/$lang" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('markdown',''))" \
| grep -q "$url" && echo -n "✓ " || echo -n "✗ "
done
echo
done
Несовместимые изменения
Нет.
Вне рамок (v1.25+)
| Пункт | Примечания |
|---|---|
| Живое выполнение визуальных подтестов сценария 31 | Требуют браузерного агента (Claude Cowork или npm run test:e2e:browser). Вне рамок выполнения только через curl; покрыты существующим Playwright smoke. |
| Перевод тел RU CHANGELOG для более старых записей (v1.5.x и ниже) | Перезапущенный агент покрыл только v1.6.0 и далее. Записи до v1.6 (v1.5.x и т. п.) — если они когда-либо существовали — остаются ранее существовавшим содержимым. |
| Визуальная регрессия скриншотов дашборда после будущих изменений SPA | scripts/capture-dashboard-screenshots.mjs перегенерирует PNG по локалям; автоматического diff сейчас нет. |
[1.23.0] — 2026-05-14
Разделение i18n + исправление CI для баннера соединения + локализованные скриншоты дашборда + закрытие всех заглушек бэклога. Релиз закрывает три пункта, отмеченные в таблице «Out of scope» версии v1.22.0 для v1.23 (M-9 — переводы тел локальных CHANGELOG, N-1 — разделение i18n.js по LOC, аудит содержимого справочного бандла), и добавляет hot-fix для smoke E2E, который после v1.22.0 окрашивал CI на ветке main в красный.
🚑 Hot-fix CI — восстановление баннера соединения
-
fix(client): reset health-poll cadence + visibilitychange eager re-check(public/js/api.js:21-91) — экспоненциальный backoff из M-6 в v1.22.0 был корректен (3 с → 6 с → 12 с → потолок 15 с вместо прежнего потолка 60 с), но активныйsetTimeoutоставался привязан к ранее установленной задержке. Сервер, остановленный в t=0.1 при первом пинге в t=3, проваливал проверку, удваивал задержку до 6 с, а следующая попытка восстановления срабатывала только в t=9. Smoke E2E «Flow 2a: баннер соединения появляется при остановке сервера и скрывается при восстановлении» ждал лишь 4 с и падал наmain.v1.23.0 переделывает цикл опроса:
_healthHandleтеперь отслеживается, поэтомуsetConnectionState(lost=true)может вызватьclearTimeoutи перепланировать опрос с_HEALTH_MIN. Первая проба восстановления срабатывает в течение 3 с после потери соединения независимо от ранее запланированной задержки._HEALTH_MAXснижен с 60 с до 15 с. Свёрнутая вкладка против остановленного сервера всё равно восстанавливается за один цикл опроса при возврате пользователя, а экономия трафика остаётся значительной.document.addEventListener('visibilitychange')инициирует немедленную перепроверку при возврате фокуса вкладке, еслиconnectionLost === true. Переключение Cmd-Tab не ждёт следующего тика backoff.
🧹 N-1 — разделение i18n.js (превышение цели в 400 LOC)
refactor(client): split DICT into i18n-dict.js (data) + i18n.js (logic)— до v1.23 файлpublic/js/lib/i18n.jsсодержал 639 строк. Основная часть (строки 23-586) — таблица переводовDICT, чистые структурированные данные. v1.23.0 выносит её вpublic/js/lib/i18n-dict.js(578 LOC, освобождён от лимита LOC по правилу CLAUDE.md «Exempt from these limits: generated files, migrations, test fixtures, lock files, vendored code» — таблица переводов квалифицируется как фикстура), оставляя вpublic/js/lib/i18n.js86 LOC чистой логики модуля (значительно ниже целевого лимита в 400 LOC).- Контракт загрузки:
i18n-dict.jsзаполняетwindow.__I18N_DICT = { … }, после чегоi18n.jsчитает его внутри существующего IIFE.public/index.htmlзагружает их по порядку —i18n-dict.jsпередi18n.js— так что IIFE видит полностью заполненный DICT во время инициализации. Запасной вариант при отсутствии словаря: каждый вызовt()возвращает встроенный fallback или сам ключ, что громко сигнализирует о неправильной конфигурации без падения SPA. - Обновление тестовой инфраструктуры:
tests/i18n-coverage.test.mjs,tests/help-ui.test.mjs,tests/canonical-docs-coverage.test.mjsтеперь прогоняют оба файла через тестовый VM-контекст (или конкатенируют их исходники для regex-обхода), сохраняя все существующие проверки.
🌐 M-9 — переводы тел локальных CHANGELOG
docs(translate): 7 non-EN CHANGELOG files end-to-end— до v1.23 файлыCHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.mdсодержали англоязычные заглушки для всех записей начиная с v1.13.0 и подвал, отправлявший читателя к каноническому EN. v1.23.0 запускает 7 параллельных агентов перевода (по одному на локаль), которые переписывают каждое тело в публикационном техническом стиле целевого языка. Заглушки удалены. Блоки кода, пути к файлам, URL, строки сообщений коммитов (fix(security): B-1 — …), переменные окружения и метки ссылок сохранены дословно во всех локалях.
🖼️ Локализованные скриншоты дашборда в каждом README
docs(readme): wire each locale README at its locale-specific PNG— до v1.23 толькоREADME.pt-BR.mdссылался наdashboard-pt-BR.png; остальные 6 не-EN README продолжали указывать наdashboard-en.png. Скриншоты (уже снятые в цикле v1.22.0 скриптомscripts/capture-dashboard-screenshots.mjs) лежали вimages/, но не использовались. v1.23.0 обновляет 14-ю строку каждогоREADME.{es,ja,ko-KR,ru,zh-CN,zh-TW}.mdна собственныйdashboard-<locale>.png.
🧪 Тесты
- Те же 474 / 474 unit + 32 / 32 Playwright, что и в v1.22.0. Smoke E2E теперь 20 / 20 (было 19 / 1 fail на
mainпосле v1.22.0 из-за регрессии восстановления баннера; исправление перепланирования в v1.23.0 закрывает её). - Три существующих теста перенастроены под разделение i18n. Ноль новых тестовых файлов, ноль удалённых проверок.
Верификация
$ npm test
# 474 / 474
$ npm run test:e2e
# passed: 20 failed: 0 (было 19/1 на v1.22.0 main)
$ wc -l public/js/lib/i18n.js public/js/lib/i18n-dict.js
# 86 public/js/lib/i18n.js ← логика, под целевым лимитом
# 578 public/js/lib/i18n-dict.js ← фикстура данных, исключение
$ grep -h 'dashboard-' README*.md | sed -E 's/.*(dashboard-[^)]+).*/\1/' | sort -u
# dashboard-en.png (только README.md)
# dashboard-es.png dashboard-ja.png
# dashboard-ko-KR.png dashboard-pt-BR.png
# dashboard-ru.png dashboard-zh-CN.png dashboard-zh-TW.png
# Проверка перевода CHANGELOG: каждый файл локали > 200 строк собственного содержимого
$ wc -l CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md | grep -v total
Несовместимые изменения
Нет. public/index.html теперь загружает два скрипта вместо одного — все, кто раздаёт SPA через CDN, должны подхватить i18n-dict.js; порядок загрузки скриптов определяется порядком тегов <script src> в index.html. Runtime-fallback (пустой DICT → t() возвращает встроенный EN-fallback) предотвращает жёсткие падения при отсутствии нового файла.
Вне рамок (v1.24+)
| Пункт | Примечания |
|---|---|
| Обновление СОДЕРЖИМОГО справочного бандла из career-ops.org/docs (а не только покрытия URL) | Пять канонических URL уже присутствуют в справочном бандле каждой локали с v1.11.x, а сценарий 31.6 QA-промпта проверяет покрытие. Обновление глубины содержимого — кандидат на v1.24+. |
| Живое выполнение QA-сценария 31 на запущенном сервере | Требует браузерного агента и реальных LLM-учётных данных. Кандидат на v1.24. |
| Покомпонентный аудит touch-target для новых блоков подсказок на mode-page | M-1 из v1.22.0 добавил элементы <p class="field-hint">, которые не были проверены на соответствие WCAG 2.5.5 min-height во всех 8 локалях. |
[1.22.0] — 2026-05-14
Закрытие бэклога M/L/N + выравнивание документации + проход по качеству переводов. Весь средний и младший ярус из v1.20.1-BACKLOG.md отгружен одним релизом: девять M-пунктов, пять L-пунктов, две мелочи. Плюс аудит документации против пяти канонических руководств career-ops.org/docs, обновлённые системные промпты под .claude/ и .github/, а также прошедшие проверку качества README во всех 7 не-английских локалях.
🛡️ Усиление безопасности (defense-in-depth)
-
fix(security): M-4 — entity-aware stripDangerousMarkdown(server/lib/security.mjs) — regex до v1.22 сопоставлял<script>,javascript:,on*=как литеральные подстроки.<script>,javascript:и<img src="data:image/svg+xml,<svg onload=…>">проходили насквозь. Очистка теперь декодирует<,>,&,", числовые (&#NN;) и шестнадцатеричные (&#xHH;) сущности до запуска regex-очистки. Подтверждено 11 тестами вtests/cv-xss-bypasses.test.mjs. Основная защита по-прежнему лежит на клиентском конвейереUI.mdс приоритетом escape; этот патч закрывает файл на диске. -
fix(security): L-2 — bash --noprofile --norc on the batch runner(server/lib/routes/batch.mjs:108) —spawn('bash', [PATHS.batchRunner, ...])ранее подхватывал пользовательский~/.bashrc. Враждебный rc-файл мог влиять на выполнение. Теперьspawn('bash', ['--noprofile', '--norc', PATHS.batchRunner, ...]).
🔒 Устойчивость
-
fix(client): M-6 — exponential backoff on health ping(public/js/api.js:22-48) — поллер состояния «нет соединения» совершал 28 800 запросов к мёртвому серверу за ночь. Теперь 3 с → 6 с → 12 с → 24 с → 60 с, сброс к 3 с при первом успешном восстановлении (2xx). Реализация на цепочкеsetTimeout(неsetInterval), поэтому каждый шаг подхватывает новую задержку. -
fix(client): M-5 — Safari private-mode localStorage guard(public/js/lib/i18n.js:572-583) — Safari в приватном режиме выбрасываетSecurityErrorна каждый вызовlocalStorage.getItem/setItem. IIFE во время загрузки роняла весь модуль i18n, и SPA рендерил сырые ключи. Оба вызова обёрнуты в try/catch, в качестве запасного варианта используетсяdetect()по языку браузера. -
fix(server): M-2 — body-size cap on outbound preview fetches (test + verify)— функцияsafeGetиз v1.21.0 уже стримила чанки и ограничивала размерopts.maxBytes. v1.22 добавляет явный регрессионный тест вtests/ssrf-redirect-rebind.test.mjs, фиксирующий контракт: 100 КБ вверх по потоку + лимит 4 КБ → ответ ≤ 4 КБ. -
fix(client): L-5 — clear setTimeout on hashchange in scan.js(public/js/views/scan.js:6-22, :113-120) — таймерrefreshResults()на 300 мс после завершения утекал, если пользователь уходил с#/scanв это окно. Дескриптор теперь захватывается и очищается в__cancelActiveScanPoll. -
fix(client): L-4 — multi-line SSE data: joiner(public/js/lib/auto-pipeline.js:158-176) — парсер SSE использовалmatch()(одна строка). По спецификации событие может нести несколько строкdata:, которые потребитель должен соединить через\n. Сейчас сервер шлёт однострочный JSON, и старый код работал — но был хрупким к любой будущей многострочной полезной нагрузке.
♿ Доступность
-
feat(a11y): M-3 — WCAG 1.4.1 redundant cues on score pills + connection banner(public/css/app.css:602-625, :812-822) —score-high/score-mid/score-lowранее передавали состояние только оттенком (красный/жёлтый/зелёный). Пользователи без цветовосприятия не имели альтернативы. Каждый уровень теперь получает дополнительный глиф через::before(✓ / ◐ / ○). Баннер соединения получает префиксный глиф⚠в офлайн-состоянии. Места рендеринга не тронуты — чисто CSS-усиление. -
feat(a11y): M-1 — inline hint paragraphs for every mode-page field(public/js/views/mode-page.js,public/js/lib/i18n.js) — v1.20.0 связалhtmlFor → idдля каждого поля mode-page, но не добавил встроенных подсказок; назначение поля документировалось только в README-проходах. v1.22.0 добавляет 19 ключей подсказок i18n × 8 локалей = 152 новых перевода, а построительfield()теперь рендерит<p id="…-hint">с привязкой черезaria-describedbyдля каждого поля. Пользователи скринридеров слышат подсказку при фокусе на инпуте. -
fix(a11y): M-7 — null-guard on UI.el() htmlFor alias(public/js/api.js:194-198) —htmlFor: nullранее рендерил литеральныйfor="null". Однострочное зеркало защитыv != null && v !== falseиз соседней ветки.
🧹 Качество / переносимость
-
fix(server): L-1 — parseInt radix in health.mjs + bin/start.sh + bin/setup.sh—parseInt(process.versions.node)без основания вызывает предупреждение линтера и хрупок, если Node однажды начнёт отдавать шестнадцатеричные версии. Везде добавлено10. -
fix(server): L-3 — Windows-safe entrypoint check(server/index.mjs:159-163) —import.meta.url === \file://${process.argv[1]}`неправильно обрабатывает буквы дисков и обратные слэши в Windows. Заменено наfileURLToPath(import.meta.url) === path.resolve(process.argv[1])`. -
refactor(client): N-2 — drop Element.prototype.also monkey-patch(public/js/views/cv.js:188-201) — глобальное загрязнение прототипа DOM. Заменено локальной переменной для корня дерева. -
test(canary): M-8 — 404 regression test for retired /api/scan-ru/config(tests/scan-consolidated.test.mjs) — v1.20.0 убрал alias, но не добавил канарейку. Трёхстрочное добавление по образцу тестов вывода из эксплуатации в v1.18.
📚 Документация и системные промпты
-
docs(architecture): refresh OVERVIEW + DATA-FLOWS for v1.21+ surface— в OVERVIEW.md добавленыsafe-fetch.mjs(GET с привязкой к DNS),file-lock.mjs(мьютекс на путь),rate-limit.mjs(троттлинг LLM) иsanitizePathName. В DATA-FLOWS.md появились два новых раздела: «Outbound URL fetches (DNS-rebind-safe)» и «LLM endpoint rate-limiting». -
docs(readme): security envelope section refresh— раздел «Security notes» README.md теперь документирует все хелперы из envelope безопасности v1.21+ (sanitizePathName, safeGet, withFileLock, llmRateLimit, entity-aware stripDangerousMarkdown). -
docs(qa): scenario 31 — career-ops.org/docs alignment(qa/claude-cowork-browser-test-prompt.md) — шесть новых под-тестов (31.1–31.6), проверяющих соответствие UI поведению из пяти канонических руководств career-ops.org/docs: пороги оценок, поток сканирования (одна кнопка), поток подачи (чек-лист, не автоотправка), batch-поток (TSV-редактор), установка Playwright (мягкое падение), покрытие справочного бандла (5 URL × 8 локалей). -
docs(translate): README quality refresh × 7 non-EN locales— каждый не-EN README переписан в публикационном техническом стиле родного языка. Распространённые корявые кальки заменены; добавлены упоминания envelope безопасности v1.21/v1.22; обновлены бейджи релиза и тестов. -
docs(system): .claude/PROJECT-CONTEXT.md + .github/copilot-instructions.md— однофайловая ориентация для агентов, подключающихся к сессии. Сжатый CLAUDE.md, перечень хелперов v1.21+, список типичных подводных камней. -
docs(bin): actualize start.sh / setup.sh / run_all.sh comments— «two deps» → «three deps» (express + js-yaml + multer); «298 tests» → «474+ tests»; добавлено основаниеparseInt.
🧪 Тесты
- 461 → 474 unit (+13) + 32/32 Playwright без изменений.
- Новые файлы тестов:
cv-xss-bypasses.test.mjs(M-4, 11 тестов). - Расширены:
ssrf-redirect-rebind.test.mjs(+1 для лимита размера тела M-2),scan-consolidated.test.mjs(+1 для канарейки alias M-8). - Ноль поведенческих изменений в существующих наборах — каждый патч аддитивен или покрыт новой канарейкой.
Верификация
npm test # 474 / 474
npm run test:e2e:browser # 32 / 32
# Очистка XSS с entity-кодированием:
node -e "import('./server/lib/security.mjs').then(({stripDangerousMarkdown}) => console.log(stripDangerousMarkdown('<script>alert(1)</script>')))"
# → '' (ни один <script> не выживает)
# Backoff health-ping (открыть devtools, остановить сервер, наблюдать панель network):
# 3 с → 6 с → 12 с → 24 с → 60 с, сброс при первом успешном пинге
# Глиф score-pill (открыть #/reports в светлой и тёмной теме):
# .score-high показывает ✓ + числовую оценку
# .score-mid показывает ◐ + числовую оценку
# .score-low показывает ○ + числовую оценку
# Подсказки mode-page (#/contacto и т. п.):
# <input aria-describedby="mode-contacto-recipient-hint"> ← указывает на <p id="…">
# Снятый с эксплуатации alias:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404
Несовместимые изменения
Нет. Каждый патч аддитивен либо сохраняет контракты существующих эндпоинтов.
Вне рамок (v1.23+)
| Пункт | Примечания |
|---|---|
| M-9 — переводы тел локальных CHANGELOG | Все записи v1.13+ в CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md — англоязычные заглушки. Кандидат на массовый перевод после замедления каденции релизов. |
N-1 — public/js/lib/i18n.js превышает целевой лимит в 400 LOC |
Разделение по локалям увеличивает стоимость HTTP без сборщика. Отложено до решения по этапу сборки. |
| Обновление содержимого справочного бандла из career-ops.org/docs | Пять канонических URL уже присутствуют в каждом локальном бандле (с v1.11.x). Сценарий 31.6 QA-промпта проверяет покрытие. Обновление глубины содержимого — кандидат на v1.23. |
[1.21.0] — 2026-05-14
Безопасность + параллелизм + полировка a11y по результатам двух независимых код-ревью. Семь находок из docs/specs/V1.20.1-BACKLOG.md отгружены одним релизом: один блокер (TOCTOU при DNS-rebind), шесть багов высокой степени серьёзности (распыление санитайзинга при path-traversal, отсутствие лимита запросов на LAN-деплое, состояние гонки при конкурентной записи, провал в покрытии i18n, висячий aria-describedby, отсутствие связей с метками). 34 новых теста; baseline вырос с 427 → 461 unit + 32/32 Playwright. Каждый патч закрыт именованным регрессионным тестом.
🛡️ Безопасность
-
fix(security): B-1 — close DNS-rebind TOCTOU via safe-fetch.mjs(server/lib/safe-fetch.mjs) — прежний паттерн делал один явныйdnsLookupдля валидации, а затемfetch()выполнял свой собственный независимый lookup. Атакующий через DNS-rebind с TTL=0 мог вернуть публичный IP на первом lookup и127.0.0.1/169.254.169.254/ адрес LAN на втором, обходяisPrivateOrLoopbackHost. Новая функцияsafeGetрезолвит ОДИН раз, привязывает TCP-соединение к конкретному IP через node:http(s) и выставляет SNI/Host так, чтобы проверка сертификата по-прежнему шла против исходного hostname. Используется/api/pipeline/previewи/api/auto-pipeline. Fail-CLOSED при ошибке lookup (обратно к прежнемуtry { … } catch { /* fall through */ }). Подтверждено 8 новыми тестами вtests/ssrf-redirect-rebind.test.mjs. -
fix(security): H-4 — consolidate sanitizePathName across 10 routes(server/lib/security.mjs) — голый regexreplace(/[^\w\-.]/g, '')дублировался вjds.mjs,content.mjs,reports.mjs,llm.mjs,runners.mjsи сохранял символы., из-за чего..pdf,....md, имена с ведущей точкой выживали. Толькоreports.mjs::sanitizeSlugделал это правильно. v1.21.0 поднимает корректную версию (sanitizePathName) вsecurity.mjs, удаляет 10 сломанных копий и отвергает пустые результаты с кодом 400. Подтверждено 12 тестами вtests/path-traversal.test.mjs. -
fix(security): H-5 — rate-limit LLM endpoints on public bind(server/lib/rate-limit.mjs) — у/api/evaluate,/api/deep,/api/mode/:slug,/api/auto-pipelineранее не было троттлинга по IP. Loopback-пользователей это не затрагивает; LAN-деплои (HOST=0.0.0.0) получают 10 запросов/мин/IP с заголовкамиRetry-AfterиX-RateLimit-*при превышении. Настраивается черезLLM_RATE_LIMIT="N/Ws". Дешёвая промежуточная защита до auth-шлюза P-12 в v2.0. Подтверждено 6 тестами вtests/rate-limit.test.mjs.
🔒 Параллелизм
fix(data): H-6 — per-file mutex on applications.md / pipeline.md(server/lib/file-lock.mjs) — конкурентныеPOST /api/tracker(или auto-pipeline в гонке с ручным добавлением) ранее оба читалиnum=42, оба писалиnum=43, и более ранняя строка тихо терялась.withFileLock(path, fn)сериализует read-modify-write на путь; независимые пути по-прежнему выполняются параллельно. Подключено вtracker.mjs,pipeline.mjs(POST + DELETE) и в шаге трекераauto-pipeline.mjs. Подтверждено 5 тестами вtests/concurrent-tracker-write.test.mjs, включая интеграционную проверку 20 одновременных POST с утверждением, что строки 001..020 встают последовательно.
♿ Доступность
-
fix(a11y): H-1 — id="batch-tsv-hint" on the batch.js hint paragraph(public/js/views/batch.js) — в v1.20.0 был добавленaria-describedby="batch-tsv-hint"на textarea TSV, но соответствующийidу блока<p>не появился. Скринридерам нечего было озвучивать. Исправлено. -
fix(a11y): H-2 — htmlFor on batch-parallel / batch-min-score labels(public/js/views/batch.js) — четыре инпута из v1.20.0 получили новые id, но их метки не были программно связаны. WCAG 3.3.2 теперь удовлетворён. -
Новая канарейка статического анализа в
tests/a11y-form-wires.test.mjs— обходит каждый view-файл и утверждает, что каждая IDREF вaria-describedby/htmlForуказывает на соседнее объявлениеid:. Ловит регрессии класса опечаток в CI.
🌐 i18n
fix(i18n): H-3 — 13 keys from v1.20.0 silently fell through to EN for 7 locales(public/js/lib/i18n.js) —pipe.filter,pipe.count,pipe.preview*,pipe.openTab,pipe.evaluateAll*,eval.jdHint,batch.parallelAria,batch.minScoreAria, плюсcommon.delete,config.group{Core,Runtime,Regional},config.profileEmpty,config.viewProfile,scan.atsBadge,scan.regionalBadgeупоминались какt('key', 'EN fallback'), но никогда не добавлялись в DICT. Пользователи скринридеров на русском, японском и китайском слышали английскиеaria-label, что прямо обнуляло выигрыш по WCAG 3.3.2, заявленный в v1.20.0. v1.21.0 добавляет все 19 ключей × 8 локалей (≈ 150 новых переводов) и расширяетtests/i18n-coverage.test.mjsпроходом статического анализа, который сканирует каждый вызовt('key', …)вpublic/js/**/*.jsи проверяет наличие ключа в DICT. Будущий дрейф ловится на этапе CI.
🧪 Тесты
- 427 → 461 unit (+34) + 32/32 Playwright без изменений.
- Новые файлы:
ssrf-redirect-rebind,path-traversal,concurrent-tracker-write,rate-limit,a11y-form-wires. - Существующий
pipeline-preview.test.mjsпереподключён с мокаglobalThis.fetchк новой точке инъекции_setTransportвsafe-fetch.mjs— путь SSRF больше не идёт через fetch, и старый мок незаметно обходился.
Верификация
npm test # 461 / 461
npm run test:e2e:browser # 32 / 32
node --test tests/ssrf-redirect-rebind.test.mjs tests/path-traversal.test.mjs \
tests/concurrent-tracker-write.test.mjs tests/rate-limit.test.mjs \
tests/a11y-form-wires.test.mjs # 34 новых теста, все зелёные
# Path-traversal: каждое имя в стиле traversal возвращает 400 / 404
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/jds/..pdf
# → 400
# Лимит запросов при публичной привязке:
HOST=0.0.0.0 LLM_RATE_LIMIT=3/60s npm start &
for i in 1 2 3 4; do
curl -sS -o /dev/null -w '%{http_code} ' -X POST -H 'Content-Type: application/json' \
-d '{"jd":"…"}' http://0.0.0.0:4317/api/evaluate
done
# → 200 200 200 429
# Конкурентные записи в трекер: 20 параллельных POST, 20 строк встают:
node tests/concurrent-tracker-write.test.mjs
# 20 последовательных строк 001..020
# Проверка связей aria:
grep -r 'aria-describedby' public/js/views/ | wc -l
# совпадающие `id:` все резолвятся (канарейка a11y-form-wires.test.mjs)
Вне рамок (v1.22+)
| Пункт | Примечания |
|---|---|
Потоковый лимит размера тела в pipeline-preview (M-2) |
await upstream.text() читает тело целиком перед срезом в 8 КБ; вредоносный поток в 1 ГБ может исчерпать память. Поток-чтение с байтовым счётчиком + abort. |
WCAG 1.4.1 — состояние только цветом на .connection-banner + score pills (M-3) |
Оттенок единолично сигнализирует состояние; добавить префиксную иконку (✓ / ◐ / ○) или текстовый суффикс. |
Обходы stripDangerousMarkdown через HTML-сущности (M-4) |
<script>, javascript:, <img src="data:image/svg+xml,<svg onload=…>"> выживают regex. Defense-in-depth через UI.md ещё держится; зафиксировать и закрыть обходы пакетом тестов. |
Доступ к localStorage в приватном режиме Safari без try/catch (M-5) |
i18n.js:544/571 бросает исключение → SPA рендерит сырые ключи. Обернуть в try/catch с 'en' по умолчанию. |
setInterval(checkHealth, 3000) опрашивает вечно без backoff (M-6) |
Экспоненциальный 3 с → 6 с → 12 с → потолок 60 с. |
Отсутствует null-guard для алиаса htmlFor (M-7) |
Однострочная защита if (v != null && v !== false). |
Канарейка 404 для снятого с эксплуатации /api/scan-ru/config (M-8) |
Трёхстрочный тест по образцу v1.18. |
| Переводы тел локальных CHANGELOG (M-9) | Кандидат на массовый перевод после замедления каденции релизов. |
| Inline-блоки подсказок для каждого поля mode-page (M-1) | ~168 ключей i18n × 8 локалей; отложено как полировка. |
| Мелочи L-1 — L-5 | Основание parseInt, bash –noprofile, Windows-safe fileURLToPath, многострочный SSE, очистка таймера scan.js. |
[1.20.0] — 2026-05-13
Покомпонентная полировка a11y + паритет не-EN README + снятие алиаса /api/scan-ru/config. Закрывает четыре пункта, помеченные таблицей «Out of scope» в v1.19.0 для v1.20.
♿ WCAG 2.5.5 / 2.5.8 — покомпонентный аудит touch-target
a11y(touch-target): chip min-height 28 px + 8 px gap (2.5.8 spaced-target exception)—.chipбыл 24 × ~50 px (вертикаль 24 не дотягивала до пола 24 px из 2.5.5 для кластерных контролов); исключение spaced-target в 2.5.8 требует либо ≥ 24 × 24 px, либо 24 px зазора..chipподнят доmin-height: 28px; padding: 6px 12px;, а обёртка.chip-row— доgap: 8px;, чтобы выполнялись оба условия.a11y(touch-target): sidebar nav-item min-height 44 px— у.nav-itemбыл padding всего10px 14px, вычисленная высота ~36 px на большинстве viewport. Теперьpadding: 12px 14px; min-height: 44px; box-sizing: border-box;. Совпадает с полом.btn.a11y(touch-target): tab-btn min-height 44 px— то же лечение для Sortable Headers / вкладок категорий в Reports, Tracker, Scan results.
♿ WCAG 1.3.1 / 3.3.2 — aria-describedby на встроенных подсказках формы
Каждый элемент управления формой в SPA теперь обладает стабильным id, его <label> указывает на него через htmlFor, а любой блок подсказки связан через aria-describedby. Перенастроены пять view-файлов:
a11y(forms): config.js—idна каждый ключ + привязка подсказки (cfg-<key>/cfg-<key>-hint).a11y(forms): evaluate.js— textareaeval-jd+ блокeval-jd-hint, документирующий минимум в 50 символов после санитайзинга.a11y(forms): batch.js—batch-tsv/batch-tsv-hint, плюсaria-labelнаbatch-parallel,batch-min-score,batch-dry-run,batch-retry.a11y(forms): pipeline.js—pipe-filter+pipe-new-url/pipe-new-url-hint.a11y(forms): mode-page.js— каждое поле в 7 универсальных режимах (project,training,followup,batch-prompt,contacto,interview-prep,patterns) получает id видаmode-<slug>-<name>и меткиhtmlFor.
UI.el() научился алиасу htmlFor в стиле React, чтобы код view оставался декларативным — алиас выставляет атрибут for (зарезервированное в JS имя свойства).
🌍 Паритет не-EN README
docs(readme): translate 7 locales to 585-line parity with EN master—README.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.mdимели 306–316 строк (заголовки покрывались, но маркетинговые проходы и большая часть API-справочника пропускались). Все семь теперь зеркалируют EN-структуру end-to-end: About → One-command install → Why? → Quick start (3 пронумерованных шага) → Requirements → What you get table → Scan → Architecture (полное дерево директорий) → API reference (каждая таблица маршрутов) → Tests → Configuration → Security notes → Limitations → Contributing → 🌍 Getting Started — 5-шаговый проход → License.
🧹 Снятие алиаса /api/scan-ru/config
feat!(scan): remove /api/scan-ru/config legacy alias (sunset v1.20)— сохранялся одним релизом как alias в v1.19 для обратной совместимости. Канонический/api/scan/regional/config— теперь единственный путь. Удалены: регистрация маршрута вserver/lib/routes/scan.mjs, упоминания в README.md,docs/architecture/{OVERVIEW,SERVER,API}.md. Тесты уже покрывали канонический путь — изменения в тестах не требуются.
🧪 Тесты
- Тот же набор, что и v1.19. 427 / 427 unit + 20/20 smoke + 23/23 comprehensive + 32/32 Playwright. Вся обвязка a11y аддитивна (больше
id/for/aria-describedby) — поведенческих изменений нет, дельты в тестах нет.
Верификация
npm test # 427 / 427
npm run test:e2e:browser # 32 / 32
# Touch-target — каждый chip / nav-item / tab-btn ≥ 28 / 44 / 44 px:
# Chrome DevTools → Computed → height/min-height на .chip, .nav-item, .tab-btn
# Метки формы — у каждого input есть связь label[for=…]:
# document.querySelectorAll('input,textarea,select').forEach(el =>
# console.assert(el.labels?.length || el.getAttribute('aria-label'), el))
# Alias снят:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404
# Канонический работает:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
Несовместимые изменения
DELETE /api/scan-ru/config— удалён. Используйте/api/scan/regional/config. Снятие было анонсировано в CHANGELOG v1.19.0 и в верификационном скрипте.
Вне рамок (v1.21+)
| Пункт | Примечания |
|---|---|
| Inline-блоки подсказок для каждого поля mode-page | Сейчас на месте только связь <label for=…>; видимые подсказки на поле в SPA остаются только на английском. Проходы README документируют назначение полей в каждой локали, поэтому это полировка, а не блокер. |
Сигналы состояния только цветом в .connection-banner и pill оценок дашборда (WCAG 1.4.1) |
Баннер опирается на красный/жёлтый/зелёный; нужен иконочный или текстовый суффикс для пользователей без цветовосприятия. |
| Переводы тел локальных CHANGELOG | Англоязычные заглушки остаются в CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md. Перевод будет выполнен после замедления каденции релизов v1.x. |
[1.19.0] — 2026-05-13
Контраст WCAG 1.4.3 + унификация сканера (финал) + удаление HH_USER_AGENT из UI. Закрывает отложенный в v1.18 аудит контраста, завершает устранение разделения EN/RU, начатое в v1.18, и убирает настройку HH_USER_AGENT из UI по указанию пользователя (разумный дефолт уже встроен в сервер и подходит большинству не-RU IP).
♿ Проход контраста WCAG 1.4.3
a11y(contrast): introduce AA-passing *-text variants for accent tokens— светлая тема:--rausch-text: #b80f42(6.59:1 на белом, было 3.52:1),--kazan-text: #066507(7.31:1, было 4.53:1),--darjeeling-text: #7a5800(5.73:1 на янтарном фоне, было 4.24:1),--babu-text: #00665e(6.09:1, было 2.70:1). Тёмная тема: осветлённые зеркала (#ff8aa0,#6ee7b7,#fcd34d,#5eead4) держат тот же пол 4.5:1 на бумаге#161a22.- Классы бейджей (
.badge-ok,.badge-warn,.badge-bad,.badge-info) и pill оценок (.score-high,.score-mid,.score-low) теперь идут через новые*-textварианты — каждая комбинация «текст на тонированном фоне» проходит AA. Токены заливки акцентов (--rausch,--kazanи т. д.) не тронуты для рамок и контуров (для нетекстовых компонентов UI достаточно 3:1).
🧹 Унификация сканера (завершение работы v1.18)
docs(scan): scrub remaining EN/RU split references across READMEs + help + architecture docs— восемь README + восемь справочных бандлов + три архитектурных документа (API.md, SERVER.md, OVERVIEW.md, DATA-FLOWS.md) + комментарий в scan.js теперь описывают единый консолидированный метод сканирования. Унаследованные алиасы/api/stream/scan-{en,ru}уже отсутствовали в v1.18; v1.19 ловит документацию и копию, всё ещё представлявшую сканирование как двухшаговый процесс EN+RU.feat(scan): canonical /api/scan/regional/config endpoint—/api/scan-ru/configсохраняется как тонкий alias на один релиз для обратной совместимости. Новый путь соответствует соглашению об именовании источников (?source=regional).
🛠️ HH_USER_AGENT удалён из UI
feat!(config): drop HH_USER_AGENT field from /#/config + KNOWN_KEYS— продвинутые пользователи по-прежнему могут задатьHH_USER_AGENTнапрямую вcareer-ops/.env(сервер читает черезprocess.env.HH_USER_AGENTвserver/lib/sources/hh.mjsс встроенным UA в качестве запасного варианта). UI больше не показывает этот ключ: дефолт работает у большинства, а наличие непонятного поля User-Agent в App Settings регулярно вводило в заблуждение.- Упоминания в README в 8 локалях + упоминания в справочных бандлах в 8 локалях заменены советом «запустить через российский IP / VPN». Ключ i18n
scan.hhWarningперефразирован без подробностей про настройку env-переменной. KEY_GROUPSсхлопнут: классификацииregionalбольше нет (она содержала только HH_USER_AGENT). Тесты обновлены; полеregionalActiveв payload сохранено для обратной совместимости SPA.
🧪 Тесты
tests/env-config.test.mjs— проверкаKNOWN_KEYSтеперь исключает HH_USER_AGENT; добавлено утверждение, что ключ намеренно отсутствует.tests/config-endpoint.test.mjs— тест POST-записи нескольких ключей используетGEMINI_MODELв качестве второго известного ключа вместо HH_USER_AGENT.tests/config-groups.test.mjs—groups.HH_USER_AGENTтеперь ожидаетсяundefined.- Итого: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright. Те же цифры, что и в v1.18.0, потому что все скорректированные тесты уже учтены.
Верификация
npm test # 427 / 427
# Контраст (Chrome DevTools или axe) на светлой и тёмной:
# .badge-ok / .badge-warn / .badge-bad / .badge-info → AA pass (4.5:1+)
# .score-high / .score-mid / .score-low → AA pass
# HH_USER_AGENT больше не в /api/config:
curl -s http://127.0.0.1:4317/api/config | jq '.values | keys'
# → ["ANTHROPIC_API_KEY","ANTHROPIC_MODEL","GEMINI_API_KEY","GEMINI_MODEL","HOST","PORT"]
# (без HH_USER_AGENT)
# Канонический эндпоинт regional config:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
# Унаследованный alias жив до v1.20:
curl -s http://127.0.0.1:4317/api/scan-ru/config | jq '.'
Вне рамок (v1.20+)
| Пункт | Примечания |
|---|---|
| Покомпонентный аудит touch-target (chip-фильтры, sortable headers, навигация сайдбара) | v1.18 установил глобальный пол (.btn 44 px, .btn-sm 32 px); покомпонентная верификация по всему SPA остаётся. |
aria-describedby на встроенных подсказках формы (#/config, #/pipeline, #/evaluate, #/batch) |
v1.17 покрыл aria-label на глобальном поиске и закрытии модалки. Связывание подсказок на input — следующий слой полировки. |
| Полный паритет не-EN README (585 строк как у EN) | v1.18 поднял не-EN до ~307 (53 % от EN). Маркетинговые «Quick start» + «🌍 Getting Started» проходы остаются только на английском. |
Удалить унаследованный alias /api/scan-ru/config |
Снятие запланировано на v1.20. Канонический /api/scan/regional/config — целевая миграция. |
[1.18.0] — 2026-05-13
Консолидация эндпоинтов сканирования + проход WCAG 2.2 AA + завершение длинного хвоста i18n. Снимает с эксплуатации унаследованные алиасы /api/stream/scan-{en,ru} (окно Sunset 2026-10-01 перенесено на v1.18 по указанию пользователя). Доводит не-EN README до ~307 строк и переводит оставшиеся RU-телесные записи v1.16.0 + v1.17.0 в CHANGELOG для 6 локалей.
🚪 Несовместимые изменения
feat!(scan): retire legacy /api/stream/scan-{en,ru} aliases— устаревшие SSE-эндпоинты EN/RU удалены. Каждый потребитель идёт через консолидированный/api/stream/scan?source=ats|regional|both(живёт с v1.12.0). У унаследованных путей с v1.15.0 были заголовки Deprecation + Sunset (RFC 8594); окно миграции закрыто. Внешние интеграции на старых путях получают чистый 404 вместо тихой маршрутизации в catch-all SPA.
♿ Доступность (проход WCAG 2.2 AA)
- WCAG 2.4.1 Bypass Blocks — новая ссылка Skip to main content как первый фокусируемый элемент на каждой странице. Визуально скрыта через
.skip-linkдо получения фокуса, при Tab от загрузки страницы вытаскивается в верхний-левый угол. - WCAG 2.4.7 Focus Visible — глобальный стиль
*:focus-visible. Фокусные кольца на клик мыши выключены, на нажатие Tab — включены (стандартный паттерн WAI-ARIA AP). Кнопка закрытия модалки (×) получает кольцо фокуса повышенной контрастности. - WCAG 2.5.5 Target Size — минимум 44×44 px touch-target на
.skip-link..btn-smсохраняетmin-height: 32px(в сочетании с межстрочным интервалом удовлетворяет исключению AAA «24×24 + spacing» для компактных контролов строк таблиц). - WCAG 3.1.1 Language of Page —
<html lang="en">исправлен сlang="ru"(JS-бутстрап i18n уже переопределял это при загрузке, но SSR-дефолт теперь совпадает с дефолтной локалью SPA). - WCAG 1.3.1 Info & Relationships —
#contentполучаетtabindex="-1", чтобы цель skip-link фокусировалась чисто. (Роли ARIA + ловушка фокуса были добавлены ещё в v1.17.)
📚 Длинный хвост i18n
docs(i18n): v1.16.0 + v1.17.0 CHANGELOG translated in 6 locales— записи, ранее остававшиеся с русским телом вCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md, теперь на родном языке. Счётчик кириллических символов по локали упал 79 → 42 → 23 (оставшиеся 23 — технические inline-ссылки вроде путей к файлам и заголовок мультиязычных ссылок, намеренно).docs(readme): expand non-EN READMEs with Why / Requirements / Features / Configuration / Contributing— каждый не-EN README вырос с 240 → ~307 строк. Теперь покрывает те же немаркетинговые разделы, что и 585-строчный EN. Полный паритет 1:1 (маркетинговые проходы) пока отложен.
🛠️ Прочее
docs(api): consolidated scan endpoint in API.md + DATA-FLOWS.md + README.md— таблица API-справки теперь перечисляет только/api/stream/scan?source=…. Раздел Scan в README объясняет удаление разделения EN/RU в v1.18.0.fix(scan.js): drop stale comment about deprecated aliases being live— комментарий диспетчераrunScanAllв SPA теперь отражает консолидированную реальность.
🧪 Тесты
tests/scan-consolidated.test.mjs::F-018 backwards compatпереписан — два прежних утверждения «унаследованный эндпоинт ещё работает» теперь проверяют, что запросы к/api/stream/scan-{en,ru}возвращают 404 (а не маршрутизируются в catch-all SPA).- Итого: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright (счётчик не изменился; +2 новых корректных утверждения о удалении legacy заменяют +2 утверждений о «legacy ещё работает»).
Верификация
npm test # 427 / 427
npm run test:e2e:full # 23 / 23
# Снятие legacy-эндпоинтов:
curl -sI http://127.0.0.1:4317/api/stream/scan-en | head -1 # → HTTP/1.1 404
curl -sI http://127.0.0.1:4317/api/stream/scan-ru | head -1 # → HTTP/1.1 404
# Консолидированный эндпоинт:
curl -sN 'http://127.0.0.1:4317/api/stream/scan?source=ats&dryRun=1' | head -5
# → event: start
# → data: {"script":"en-scanner","writeFiles":false,…}
# Skip-link (a11y):
curl -s http://127.0.0.1:4317/ | grep -c 'class="skip-link"' # → 1
# Запасной html lang:
curl -s http://127.0.0.1:4317/ | grep -c 'html lang="en"' # → 1
Вне рамок (v1.19+)
| Пункт | Примечания |
|---|---|
| Полный паритет не-EN README (585 строк как у EN) | v1.18 поднял не-EN до ~307 (53 % от EN). Маркетинговые «Why?» / «Quick start» проходы остаются только на английском. |
| Аудит цветового контраста (WCAG 1.4.3 AA — текст 4.5:1, крупный текст 3:1) | v1.18 закрыл структурную a11y; токенный контраст в светлой и тёмной палитрах остаётся к проверке. |
| Аудит touch-target по каждому интерактивному элементу | v1.18 установил пол (.btn: 44 px, .btn-sm: 32 px); покомпонентная проверка (chip-фильтры, навигация сайдбара, sortable headers) остаётся. |
[1.17.0] — 2026-05-13
Релиз полировки + a11y + исправления CI. Закрывает все 9 follow-up из списка
v1.16.0: проверка smoke в браузере, правда бейджей README,
обновление покрытия, проявление lastWorkdayFallback в SPA, полный пересчёт E2E,
сценарии Playwright для auto-pipeline, проход аудита a11y,
сжатие исторических CHANGELOG в 6 локалях и расширение
не-EN README разделами Architecture / API / Security / Tests.
🐛 Исправления
fix(e2e): smoke + comprehensive suites re-aligned with v1.16 UX— изменение v1.16 Cmd+K Enter → AutoPipeline modal приводило к тому, чтоsearch.press('Enter')в e2e-тестах открывал модалку, перехватывавшую последующие клики. Тесты теперь используютShift+Enterдля устаревшего пути быстрого добавления, что соответствует документированному в v1.16 разделению. Также обновляет итерацию batch-режима в comprehensive E2E для использования/#/batch-prompt(устаревший slug, введённый PR-H в v1.15). Это был провал CI на push v1.16.0 — Playwright e2e таймаутил 30 с на ожидании кликов, перехваченных backdrop.fix(mode-page): batch-prompt route → modes/batch.md via serverSlug— v1.15 переименовала legacy mode slug вbatch-prompt, но серверныйPOST /api/mode/:slugпосле этого искалmodes/batch-prompt.md, которого не существует. Новое полеserverSlugразводит хеш маршрута и имя файла режима родителя.chore: bump deprecation messages from v1.16.0 to v1.17.0— тексты deprecation для scan-en/scan-ru и баннер deprecation batch-prompt ссылались на прошлую версию.
✨ Возможности
feat(scan): 🔒 Workday CAPTCHA chip in Active Companies card— серверный экспортlastWorkdayFallbackиз PR-7 v1.16 теперь потребляется SPA./api/scan-resultsвозвращает снапшот;#/scanрендерит warn-карточку над Active Companies, когда тенант Workday сорвался в fallback («🔒 Workday tenant blocked — fallback: use /career-ops scan (Playwright)»). Новый экспортерgetLastWorkdayFallback()исключает неоднозначность live-binding ESM. 2 новых ключа i18n × 8 локалей.
♿ Доступность
a11y: ARIA roles + focus management pass on critical surfaces—index.html: атрибутыroleна<aside>(navigation),<header>(banner),<section id="content">(main),<div id="modal">(dialog с aria-modal/aria-labelledby),<div id="toast">+#conn-banner(status с aria-live),<div class="searchbar">(search).#sidebar-toggleполучаетaria-controls="sidebar"+aria-expanded, синхронизируемый JS при open/close.#global-searchполучает визуально-скрытую<label>и явныйaria-label, проявляющий подсказку шортката Cmd+K.- Закрытие модалки (×) получает
aria-label="Close dialog". - Декоративные backdrop получают
aria-hidden="true". - Ловушка фокуса в модалке —
UI.modal()запоминает владельца клика, фокусирует первый не-close фокусируемый элемент при открытии и циклит Tab/Shift+Tab внутри модалки.UI.closeModal()возвращает фокус предыдущему владельцу. - Новый утилитарный класс
.visually-hiddenвpublic/css/app.css(стандартный паттерн WAI-ARIA AP).
📚 Документация
docs(readme): badge truth across 8 READMEs— бейдж тестов284 / 379 / 360→ 427; бейдж релизаv1.9.1 / v1.13.0→ v1.16.0, затем → v1.17.0 через v1.17 bump. Цели ссылок релиза обновлены.docs(readme): expand 7 non-EN READMEs with reference sections— каждый вырос с 170 → ~240 строк за счёт новых разделов Architecture / API reference / Security notes / Tests / A11y / Limitations / License на родном языке. Пока не на полном паритете 585 строк с EN, но покрывает все ключевые немаркетинговые поверхности.docs(changelog): condense pre-v1.12 entries in 6 locales— длинные RU-телесные записи v1.11.x + v1.10.x, протекавшие в не-EN/не-RU CHANGELOG, заменены на компактную сводку «Earlier releases» на родном языке каждой локали. Детальная история остаётся вCHANGELOG.md(EN).
🛠️ Инструментарий
coverage: refresh numbers— последняя опубликованная цифра — 95.46 % line / 84.06 % branch (REVIEW v1.13.0). Baseline v1.17: 94.14 % line / 82.98 % branch / 93.20 % function. Лёгкое снижение от новых путей ошибок в auto-pipeline + reports-write; по-прежнему значительно выше пола 80 % из CLAUDE.md.
🧪 Тесты
- Итого: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive
E2E + 32 / 32 Playwright (было 28; +4 новых сценария auto-pipeline:
кнопка открывает модалку, Cmd+K paste запускает модалку,
невалидный URL блокирует шаг 1, обрамление SSE-событий
POST /api/auto-pipeline). - E2E-сет перенастроен под UX v1.16.0 (Shift+Enter quick-add, /#/batch-prompt для устаревшего режима).
Верификация
# Локально:
npm test # 427 / 427
npm run test:e2e # 20 / 20
npm run test:e2e:full # 23 / 23
npm run test:e2e:browser # 32 / 32
# Smoke в браузере (на уровне страницы):
curl -s http://127.0.0.1:4317/api/scan-results | jq '.workdayFallback'
# null, когда fallback Workday не происходил; {apiUrl, reason, at} после 4xx.
# Точечная проверка a11y:
node -e "
const c = require('cheerio').load(require('fs').readFileSync('public/index.html','utf8'));
['banner','navigation','main','dialog','status','search'].forEach(r =>
console.log(r, c('[role=' + r + ']').length));
"
# Каждая роль должна появляться ≥1.
# Проверка шлюза CI: workflow dashboard-screenshots поднимает /tmp
# скаффолд, регенерирует PNG, сравнивает с закоммиченными — зелёный, когда
# images/dashboard-*.png актуальны для отрендеренного SPA.
Вне рамок (v1.18+)
| Пункт | Примечания |
|---|---|
| Перевести запись v1.16.0 в не-EN CHANGELOG | Сейчас с русским телом (~30 строк × 6 локалей = 180 строк). Было вне явных рамок пользователя для v1.11.x/v1.10.x. |
| Полный паритет не-EN README (585 строк как у EN) | v1.17 поднял не-EN до ~240; маркетинговые «Why?» / «Quick start» проходы остаются только на английском. |
| Родительский коммит для канонического промпта A-F | Переписывание santifer/career-ops::modes/oferta.md всё ещё требуется upstream (жёсткое правило CLAUDE.md #1). |
| Полный аудит WCAG 2.2 AA | v1.17 покрыл структурный ARIA + ловушку фокуса; покомпонентный контраст / порядок Tab остаётся к проверке. |
[1.16.0] — 2026-05-13
Финализация auto-pipeline + полировка адаптеров + длинный хвост i18n. Закрывает
все 11 follow-up из REVIEW v1.15.0: серверный SSE auto-pipeline,
примитив POST /api/reports, шорткат Cmd+K, пагинация SmartRecruiters,
CAPTCHA-fallback Workday, шлюз CI на дрейф скриншотов, UX фильтра источников сканирования,
перевод исторического CHANGELOG (v1.13.0/v1.12.0 × 6 локалей), расширение
не-EN README и готовый к вставке импортёр трендовых компаний.
✨ Возможности
feat(auto-pipeline): server-side SSE orchestrator(#1, #2, #3, #8) — клиентский цепочный fetch-оркестратор v1.15 удалён.POST /api/auto-pipelineтеперь curl-доступный SSE-эндпоинт, который выстраивает validate → fetch JD → evaluate → save report → tracker на сервере с событиями шагов в реальном времени. Медленный вызов Anthropic (30–90 с) теперь шлёт событиеrunningвместо общего спиннера. Падения шлютerrorсstep+message. Оркестратор также сохраняет markdown отчёта в родительскийreports/<slug>.md(терялся в v1.15).feat(reports): POST /api/reports primitive— новый эндпоинт записи вserver/lib/routes/reports.mjs. Санитайзинг slug со страховкой от path-traversal (убрать ведущие точки, схлопнуть внутренние...). Лимит 1 МБ (413). 409 на существующий файл, если не указаноoverwrite:true. Атомарная запись через очистку XSSstripDangerousMarkdown. Логирует activity.reports.save. Тесты: 9 кейсов.feat(app): Cmd+K paste URL → auto-pipeline— вставка URL в глобальный поиск + Enter теперь открывает модалку AutoPipeline сautoStart=true. Shift+Enter сохраняет устаревший путь «добавить только в pipeline». Каноничный UX career-ops.org Quick Start §7 «paste URL → done».feat(portals): SmartRecruiters pagination(#4) —server/lib/sources/smartrecruiters.mjsобходит страницы через?limit=100&offset=Nдо достиженияtotalFound, ИЛИ возврата пустой страницы, ИЛИ срабатывания страховочного лимита 30 страниц / 3000 вакансий. Подставленные вызывающим limit/offset стираются — курсор принадлежит серверу. Крупные борды (Procter & Gamble, Amazon-style) больше не теряют хвост 100+ объявлений. Тесты: 6 кейсов.feat(portals): Workday CAPTCHA-fallback graceful(#7) —server/lib/sources/workday.mjsбольше не бросает на 4xx / не-JSON / сетевой ошибке. Возвращает[]и аннотирует новый экспортируемый снапшотlastWorkdayFallback. Таймлайн сканера продолжается со следующего тенанта. Вызывающий может вернуть прежнее поведение v1.14 с исключениями черезstrict:true. Тесты: 7 кейсов.
🛠️ Инструменты + CI
ci(workflows): dashboard-screenshots drift gate(#5) — новый.github/workflows/dashboard-screenshots.yml. На PR, затрагивающихpublic/css/app.css/public/js/views/dashboard.js/public/js/lib/i18n.js/public/index.html, workflow поднимает сервер web-ui против /tmp-скаффолда, регенерирует 8 hero PNG через Playwright + chromium и валит сборку, если результат расходится с закоммиченными. На падении заливает регенерированные PNG как артефакт CI.feat(scripts): import-trending-companies.mjs(#11) — проверяет 13 трендовых компаний изdocs/portals-examples.mdчерез их реальный boards-API и эмитит готовый к вставке YAML для родительскогоportals.yml::tracked_companies.enabled: falseставится на любого кандидата, чей slug отдаёт 404. Живой пробинг всех 6 ATS (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday). Запуск черезnpm run import:trending.feat(scripts): npm run capture:dashboards— выноситscripts/capture-dashboard-screenshots.mjsкак верхнеуровневый скрипт (раньше упоминался только вimages/README.md).
🎨 UX
fix(scan): consolidated source-filter dropdown(#6) — выпадающий список источников на#/scanпересобран из реестра адаптеров v1.14: 6 ATS + hh.ru + Habr Career, по алфавиту, без префикса гео-тега.runEnScan/runRuScanтеперь идут на консолидированный эндпоинт/api/stream/scan?source={ats,regional}вместо устаревших алиасов/api/stream/scan-{en,ru}(заголовки Sunset остаются живыми до v1.16).
📚 Длинный хвост i18n
docs(i18n): translate v1.13.0 + v1.12.0 CHANGELOG in 6 locales(#9) — записи, ранее с русским телом вCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md, теперь на реальной локали. Каждый не-EN/не-RU CHANGELOG также получает заметку i18n, объясняющую, что записи pre-v1.12 остаются на русском по соглашению проекта (канонический текст живёт вCHANGELOG.md).docs: expand non-EN READMEs with v1.16.0 highlights section(#10) — 6 не-EN README (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) получают новый ~35-строчный раздел, покрывающий: одно-клик поток auto-pipeline + пример curl, пагинацию SmartRecruiters, fallback Workday, UX фильтра источников сканирования, импортёр и workflow CI скриншотов. RU README также расширен.
🧪 Тесты
- Новый
tests/reports-write.test.mjs(9 кейсов) — happy path, санитайзинг slug (включая защиту от path-traversal), конфликт 409, флаг overwrite, очистка XSS, 400 на пропущенных полях, 413 на >1 МБ, round-trip GET/POST. - Новый
tests/auto-pipeline.test.mjs(5 кейсов) — обрамление SSE, блок на невалидный URL, блок на SSRF/loopback, путь ошибки без LLM-ключа, заголовок Content-Typetext/event-stream. - Новый
tests/smartrecruiters-pagination.test.mjs(6 кейсов) — одна страница, 3 страницы, ранняя остановка на пустой странице, соблюдение жёсткого лимита, очистка query, бросок 503. - Новый
tests/workday-fallback.test.mjs(7 кейсов) — happy path, graceful 403/429, не-JSON тело, сетевая ошибка, strict-opt-in для 4xx и сетевых ошибок. - Итого: 427 / 427 unit (было 400; +27 чистых). 0 падений. 28/28 Playwright + 23/23 comprehensive E2E + 20/20 smoke E2E зелёные от baseline v1.15.0.
Вне рамок (v1.17+)
| Пункт | Примечания |
|---|---|
| Родительский коммит для канонического промпта A-F | По-прежнему ждёт upstream-переписывания santifer/career-ops::modes/oferta.md (жёсткое правило CLAUDE.md #1). |
| Перевести записи pre-v1.12 CHANGELOG (v1.11.x, v1.10.x) | Соглашение сохранено: с русским телом. Бэкпорт — ~1800 строк перевода; отложено. |
| Полный паритет не-EN README (585 строк как у EN) | v1.16 добавил ~35 строк на локаль; полный паритет — отдельная работа. |
Серверный runEnScan, читающий аннотацию Workday fallback для рендера 🔒 чипов |
Экспорт lastWorkdayFallback подключён; карточка Active Companies в SPA потребляет его в v1.17+. |
Верификация
npm test # 427 / 427
npm run test:e2e:full # 23 / 23
npm run import:trending --check-only # пробинг 13 трендовых бордов
# Smoke auto-pipeline через curl:
curl -N -X POST http://127.0.0.1:4317/api/auto-pipeline \
-H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/4567"}'
# Round-trip POST /api/reports:
curl -X POST http://127.0.0.1:4317/api/reports \
-H 'Content-Type: application/json' \
-d '{"slug":"smoke","markdown":"# smoke\n"}'
[1.15.0] — 2026-05-13
Релиз соответствия документации. Закрывает 9 из 10 ещё открытых находок
из аудита соответствия (qa/conformance-vs-docs/00-CONFORMANCE-REPORT.md)
плюс локализованные hero-картинки. Приводит UI в соответствие с
каноническим workflow career-ops.org/docs, так что тот же конвейер, что обещает
CLI, работает end-to-end через браузер в каждой локали.
✨ Возможности
feat(auto-pipeline): PR-C — 1-click "paste URL → report + PDF + tracker row"(G-007) Соответствует обещанию canonical career-ops.org. До v1.15 пользователи делали 5 ручных кликов по /#/pipeline → /#/evaluate → /#/cv → /#/tracker. Теперь одна кнопка ✨ на /#/dashboard выстраивает цепочку: validate URL → fetch JD (SSRF-safe) → evaluate against CV → generate PDF → add tracker row. Рендерит пошаговый таймлайн в модалке с [✓]/[…]/[✗] по каждому шагу. Эвристическая экстракция компании/роли из первых строк JD. Оценка и легитимность извлекаются regex из markdown оценки. Новый файл:public/js/lib/auto-pipeline.js. 19 новых ключей i18n × 8 локалей.feat(modes): PR-D — modes/_profile.md editor as #/config → Modes tab(G-008) Канонический файл «Career framing» из Quick Start §Step-5 ранее был невидим UI-пользователям. Теперь он доступен через новую вкладку «Modes» на /#/config плюс заметную карточку на /#/profile. Новые эндпоинты:GET/PUT /api/modes/_profileс лимитом 256 КБ, прогоном черезstripDangerousMarkdown(XSS) и скаффолдом из_profile.template.mdпри первом чтении. 9 новых ключей i18n × 8 локалей.feat(profile): PR-E — accept canonical schema; add location + headline(G-009)/api/profileтеперь принимает И legacy (candidate:{...}), И canonical (top-levelfull_name,narrative.headline,target_roles.primary,compensation.target_range) схему. Legacy побеждает при наличии обоих, чтобы существующие YAML рендерились идентично. Новый хелперsummarizeProfile()возвращает унифицированную форму./#/profileвыводитnarrative.headlineновой карточкой. 2 новых ключа i18n × 8 локалей.feat(tracker): PR-B — Legitimacy column on #/tracker(G-006) Восстанавливает паритет с канонической выходной таблицей конвейера из career-ops.org/docs. Добавляет колонку Legitimacy между Status и PDF с тонировкой badge-ok/warn/bad (зеркало паттерна statusClass). Грациозная деградация — строки pre-v1.15 без колонки Legitimacy показывают—. 1 новый ключ i18n × 8 локалей.fix(routing): PR-H — dedupe sidebar; route #/batch to v1.13.0 TSV SPA(G-011) До этого фикса /#/batch регистрировался в сайдбаре ДВАЖДЫ, и оба пути вели в устаревший построитель mode-prompt. TSV SPA из v1.13.0 (8 КБ, 4 эндпоинта) была недоступна. Дубликат в сайдбаре удалён; mode slugbatch→batch-promptс баннером deprecation. Канонический /#/batch теперь — TSV SPA.
📚 Документация
docs(evaluate): PR-A — realign Block A-F with canonical career-ops.org rubric(G-005) Документация career-ops.org описывает A–F (Strategy/Personalization/STAR-истории в C/E/F). Мы эмитили A–G со сдвинутой семантикой (Risks/Verdict/Legitimacy). v1.15 обновляет все 8 справочных бандлов §9, чтобы показывать каноничные A–F с врезкой «Pre-v1.15 used A–G; we render those as-is for back-compat». Ключ i18neval.subtitle× 8 локалей тоже выровнен. Score + legitimacy теперь задокументированы как поля заголовка отчёта. ⚠ Родительский коммит ещё нужен:santifer/career-ops::modes/oferta.mdтребует upstream-переписывания для эмиссии каноничных A–F.docs: PR-F — seniority_boost + search_queries in help §5 across 8 locales + scaffold(G-010) Help §5 в 8 бандлах теперь документирует третий ключ title-фильтра (seniority_boost) И имеет пример блокаsearch_queriesс переведённым однопараграфным вступлением, поясняющим, что он влияет только на AI-сканирование Option B. Скаффолдbin/setup.shдля portals.yml по умолчанию засеваетseniority_boost: ["Senior", "Staff", "Lead"]. Паритет H2 сохранён: 16 × 8 локалей.docs: PR-I — localized hero images per README localeКаждый из 8 README теперь имеет специфическую локалевуюimages/dashboard-<locale>.png(HiDPI 1440×900), сгенерированную черезscripts/capture-dashboard-screenshots.mjs(Playwright + chromium). Старый общийpublic/images/screen_vacancy_found.pngудалён. Не-EN читатели сразу видят свой UI с метками на их языке.
🧹 Хвостовые чистки
PR-G — G-001Бандл i18nscan.noResults: 8 строк, содержащих литеральное «EN or RU scan», заменены на локально-чистую копию.PR-G — G-002Кнопка 📄 Generate PDF теперь появляется на result-панелях #/interview-prep (зеркало паттерна deep.js).PR-G — G-003README.cn.md→README.zh-CN.md(канонический тег локали); ссылки обновлены в соседях + tests/canonical-docs-coverage.test.mjs.PR-G — G-004/api/stream/scan-en+scan-ruтеперь шлют заголовки Sunset + Deprecation + Link по RFC 8594 (sunset 2026-10-01). Запланировано к удалению в v1.16.0.
🧪 Тесты
- Новый
tests/profile-canonical-schema.test.mjs(6 кейсов) — canonical YAML, legacy YAML, mixed legacy-wins, accept-canonical-only, reject neither-shape, парсинг comp range. - Новый
tests/modes-profile-crud.test.mjs(8 кейсов) — встроенный скаффолд при пустом, takeover шаблона, persisted-wins, write happy-path, санитайзинг, 400 на не-строку, 413 на >256 КБ, generic /api/modes/:name по-прежнему работает. - Исправлена регрессия изоляции в тестовых фикстурах: тесты теперь используют паттерн
before/after + dynamic-import(по образцуtests/batch-endpoints.test.mjs), чтобы больше не мутировать реальный родительскийconfig/profile.ymlпользователя. ПРИМЕЧАНИЕ для пользователей: если вашconfig/profile.ymlпосле обновления с dev-сборки v1.15.0-RC выглядит как тестовая заглушка, восстановите из бэкапа — регрессия существовала только в dev-ветке. - Итого: 400 / 400 unit (было 386; +14 чистых). 0 падений. 20/20 smoke E2E + 23/23 comprehensive E2E + 28/28 Playwright зелёные от baseline v1.14.0.
Вне рамок (follow-up v1.16+)
| Пункт | Примечания |
|---|---|
| Родительский коммит для канонического промпта A–F | santifer/career-ops::modes/oferta.md требует upstream-переписывания. Жёсткое правило CLAUDE.md #1 запрещает нам править родительские файлы. На стороне web-ui уже сделано (graceful degrade — отчёты pre-v1.15 A–G рендерятся без изменений). |
Серверный POST /api/auto-pipeline SSE |
Клиентский оркестратор отгружает UX-выигрыш. Серверный эндпоинт включит retry-from-step-N + curl-доступный CI. |
Примитив POST /api/reports |
Auto-pipeline сейчас показывает markdown отчёта inline, но не сохраняет его в родительский reports/. PDF + строка трекера — устойчивые артефакты. |
| Cmd+K paste-URL → запуск auto-pipeline | Отложено до v1.16+. |
Верификация
npm test # 400 / 400
npm run test:e2e:full # 23 / 23
curl -sf http://127.0.0.1:4317/api/health | jq '.checks | length' # → 18
curl -sI http://127.0.0.1:4317/api/stream/scan-en | grep -i sunset # G-004 видимо
curl -sf http://127.0.0.1:4317/api/modes/_profile | jq '.scaffolded' # G-008 подключено
ls images/dashboard-*.png | wc -l # 8 (PR-I)
grep -c 'href="#/batch"' public/index.html # 1 (дедупликация PR-H)
[1.14.0] — 2026-05-13
Поверх реестра v1.13.0 приземляются 3 новых ATS-адаптера, поднимая поддержку с 3 → 6 ATS (Greenhouse / Ashby / Lever + Workable / SmartRecruiters / Workday-beta). Пользовательская документация в 17 файлах подметена с «3 ATSes» на «6 ATSes» одним проходом (42 фразовых апгрейда) — README × 8 локалей, справочный бандл × 8 локалей, PROJECT.md. Добавляет блоки docs/portals-examples.md для 13 трендовых компаний как готовый к вставке YAML для родительского portals.yml.
✨ Возможности
feat(portals): 3 new ATS adapters — Workable, SmartRecruiters, Workday-beta— реестр теперь резолвит 6 ATS (было 3). Новые файлы:server/lib/portals/adapters/{workable,smartrecruiters,workday}.mjs(тонкие обёртки uniform-contract вокруг новых источников) иserver/lib/sources/{workable,smartrecruiters,workday}.mjs(сырой HTTP + нормализация ответа к канонической форме{ id, title, company, url, location, isRemote, … }сsource: <id>).- Workable: распознаёт
apply.workable.com/<slug>И legacy<subdomain>.workable.com. Эндпоинт:https://apply.workable.com/api/v3/accounts/<slug>/jobs?details=true. - SmartRecruiters: распознаёт
jobs.smartrecruiters.com/<slug>Иcareers.smartrecruiters.com/<slug>. Эндпоинт:https://api.smartrecruiters.com/v1/companies/<slug>/postings. - Workday (beta): распознаёт
<tenant>.wd<N>.myworkdayjobs.com/<lang>/<site>. Эндпоинт: POST в/wday/cxs/<tenant>/<site>/jobs. По умолчаниюsite=External, когда careers_url его опускает. Beta, потому что некоторые тенанты гейтят CXS за CAPTCHA — в этом случае откат к родительскому/career-ops scan(на базе Playwright).
- Workable: распознаёт
📚 Документация
docs(portals-examples): trending boards block—docs/portals-examples.mdрасширен разделом v1.14.0, перечисляющим 13 трендовых компаний как готовый к вставке YAML дляtracked_companies, разбитый на Greenhouse-хостовые (Stripe, GitLab, HashiCorp, Cloudflare, Datadog, Hugging Face) и Ashby-хостовые (Notion, Linear, PostHog, Replicate, Modal Labs, Fly.io, Render). У каждой записиenabled: false, чтобы пользователи проверили отклик slug перед включением. Плюс примерные блоки для Workable / SmartRecruiters / Workday с URL-паттерном, который распознаёт каждый.docs(framing): 42 ATS-phrase upgrades across 17 user-facing docs— каждое появление «Greenhouse / Ashby / Lever» в пользовательской документации теперь читается как «Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday». Затрагивает README × 8 локалей (EN/ES/PT-BR/RU/JA/KO/CN/TW), справочный бандл × 8 локалей, PROJECT.md. Исторические записи CHANGELOG и рецепты bug-fix (qa/fixes/F-014,qa/FIX-PROMPT) намеренно не тронуты — они описывают прошлое или уже корректное состояние.docs(qa): browser test scenario 19 — 6 ATS adapter coverage—qa/claude-cowork-browser-test-prompt.mdрасширен сценарием 19: инвариантALL_ADAPTERS.length === 6, проверкаresolveAdapter()для определения URL по всем 6 адаптерам, мягкая проверка карточки Active Companies в#/scanи структурная проверка блоковdocs/portals-examples.mdна ATS.
🧪 Тесты
tests/adapter-registry.test.mjsрасширен 7 новыми тестами для 3 новых адаптеров (паттерн apply-URL Workable, legacy-паттерн subdomain Workable, паттерны jobs.* + careers.* SmartRecruiters, тенант Workdaytenant.wd5.*с явным site, fallback к умолчаниюExternalдля Workday, инвариантALL_ADAPTERS.length === 6, обратная совместимость формыdetectApi()).- Итого: 386 / 386 unit (было 379; +7 чистых). 0 падений.
Верификация
npm test # 386 / 386
node -e "import('./server/lib/portals/registry.mjs').then(m => console.log(m.ALL_ADAPTERS.length))" # → 6
# Проход определения адаптера:
node -e "import('./server/lib/portals/registry.mjs').then(m => {
console.log(m.resolveAdapter({ careers_url: 'https://apply.workable.com/foo/' }).adapter.id); // → workable
console.log(m.resolveAdapter({ careers_url: 'https://jobs.smartrecruiters.com/Bar' }).adapter.id); // → smartrecruiters
console.log(m.resolveAdapter({ careers_url: 'https://baz.wd5.myworkdayjobs.com/en-US' }).adapter.id); // → workday
})"
Вне рамок (отложенные follow-up)
| Пункт | Примечания |
|---|---|
| Адаптерные записи на компанию для 13 трендовых Greenhouse/Ashby | Блок v1.14.0 docs/portals-examples.md перечисляет их как готовый к вставке YAML; верификация slug + массовое добавление в родительский portals.yml — отдельная фаза. |
| Автоматизация CAPTCHA-fallback Workday | Адаптер Workday бросает, когда CXS-фид зашлюзован; планируемый fallback делегирует родительскому /career-ops scan (Playwright). Подключение этого в UX «scan» SPA — v1.15+. |
[1.13.0] — 2026-05-13
Крупный срез. Закрывает все 4 отложенных пункта из бэклога post-v1.12.0 одним релизом: PR-4 (полный конвейер multer), реестр адаптеров (архитектурный follow-on F-018), SPA-страница batch evaluate и локалезависимый скаффолд шаблонов режимов. Плюс правка таблицы dark-темы посреди сессии.
✨ Возможности
feat(cv): multer-based multipart upload (PR-4 full)—/api/cv/importтеперь принимает И оригинальный контракт octet-stream (Content-Type: application/octet-stream+X-Filename), Иmultipart/form-data, правильно разбираемый через multer. 415-отбрасывание из v1.10.2 было заглушкой; v1.13.0 — настоящая правка. Внешние клиенты (curl-F, Postman по умолчанию, любой HTTP-клиент) работают штатно. Оба пути идут через тот же конвертерimportDocumentToMarkdown+ XSS-очисткуstripDangerousMarkdown. Новая зависимость:multer ^2.1.1.feat(portals): adapter registry— фетчеры Greenhouse / Ashby / Lever вынесены вserver/lib/portals/adapters/*.mjsс единым контрактом (id,label,matches,buildEndpoint,fetch). Новая поверхность диспетчеризации —server/lib/portals/registry.mjs::resolveAdapter().en-scanner.mjs::detectApi()+FETCHERSтеперь делегируют реестру; legacy-форма возврата сохранена. Чтобы добавить новый ATS: положить файл вadapters/, добавить вALL_ADAPTERS— никаких правок сканера.feat(batch): #/batch evaluate page— новый view SPA + 4 эндпоинта (GET /api/batch,PUT /api/batch,GET /api/stream/batch,POST /api/batch/merge). TSV-редактор дляbatch/batch-input.tsv, контролы parallel/min-score/dry-run/retry, живой SSE-логbash batch/batch-runner.sh, пост-ран списокbatch/tracker-additions/с однокликовымnode merge-tracker.mjs. Ссылка в сайдбаре под группой Decision. 21 новый ключ i18n × 8 локалей.feat(prompts): locale-aware mode scaffolding—buildModePrompt+buildEvaluationPromptтеперь оборачивают английское тело шаблона режима родителя локализованным скаффолд-текстом (строка роли, «Read these files first», «User-supplied context») на 8 локалях. Телоmodes/<slug>.mdродителя остаётся английским (read-only по жёсткому правилу CLAUDE.md #1); скаффолд career-ops-ui вокруг него — переведён.
🎨 UX-исправления
fix(theme): dark-mode table hover + tab-btn— захардкоженные#fafafa/#fff/#f7f7f7заменены на токеныvar(--beach)/var(--paper)/var(--slate), чтобы своп тёмной палитры действительно доходил до строк таблиц и кнопок вкладок. Добавляет акцентную полоску.row-boostedдля усиленных строк сканирования, работающую в обеих темах.
🧪 Тесты
- Новый
tests/adapter-registry.test.mjs(7 кейсов) — единый контракт, определение URL на ATS, приоритет явного поляapi:, null при отсутствии совпадения, сохранение формы legacydetectApi(). - Новый
tests/batch-endpoints.test.mjs(5 кейсов) — пустая фикстура, round-trip TSV, отказ при отсутствии URL, лимит 1 МБ, кадр ошибки «runner отсутствует». - Новый
tests/locale-scaffold.test.mjs(6 кейсов) — скаффолд-строки на en/ru/ja/ko, интеграцияbuildModePrompt/buildEvaluationPrompt, обратная совместимость English. tests/cv-upload-multipart-reject.test.mjsпереписан — то, что было контрактом «multipart returns 415», теперь контракт «multipart parsed via multer»; инвариант «без побочного эффекта на cv.md» сохранён.- Итого: 379 / 379 unit (было 360; +19 чистых). 0 падений.
- Покрытие: 95.46 % line / 84.06 % branch.
- 20/20 smoke E2E · 23/23 comprehensive E2E · 28/28 Playwright.
Вне рамок (отложенный follow-up)
| Пункт | Примечания |
|---|---|
| 14 новых портальных адаптеров (Workable / SmartRecruiters / Workday / GitLab / HashiCorp / Cloudflare / Datadog / Stripe / Notion / Linear / Posthog / Hugging Face / Replicate / Modal Labs / Fly.io / Render) | Реестр адаптеров на месте — добавление новых адаптеров теперь по одному файлу. Поиск URL-паттернов + нормализация эндпоинтов для 14 ATS — отдельная фаза. |
Перевод тел modes/<slug>.md родителя |
Родительские файлы read-only по жёсткому правилу CLAUDE.md #1. Локалезависимый скаффолд v1.13.0 даёт 80 % выигрыша; полный перевод тел требует upstream-PR в santifer/career-ops. |
Документация
docs/reviews/REVIEW-2026-05-13-v1.13.0.md— контекст сессии + контракт реестра адаптеров + поток batch.- Все 8 README: bump бейджей (tests 360 → 379, release v1.12.0 → v1.13.0).
- Все 8 CHANGELOG получают эту запись.
[1.12.0] — 2026-05-13
Проход bug-fix + UX + брендинг. Закрывает 8 пунктов из честного бэклога post-v1.11.1 (провалы тестов #9–12, ошибка консоли #8, дрейф portals-dead #4, поверхность seniority_boost #6, консолидация эндпоинтов F-018). Добавляет переключатель тёмной/светлой темы и удаляет брендинг «Airbnb-styled» из каждой документации, метаданных пакета и описания GitHub-репо.
✨ Возможности
feat(theme): dark/light toggle (v1.12.0)— новая кнопка темы в топ-баре. Циклит светлую ↔ тёмную; сохраняется вlocalStorage.theme; восстанавливается при загрузке страницы через pre-paint-бутстрап (public/js/lib/theme-bootstrap.js), чтобы пользователи никогда не видели вспышку неверной цветовой схемы. Учитываетprefers-color-schemeдля первых посетителей. Полная тёмная палитра под[data-theme="dark"]вpublic/css/app.css— каждый компонент читает из CSS-переменных, так что своп централизован в одном месте.- **
feat(scan): /api/stream/scan?source=ats|regional|both(F-018 LITE)** — единая консолидированная SSE-точка входа. SPA теперь открывает ОДИН event-stream, который последовательно ведёт обе фазы (сначала ATS, затем regional), вместо цепочки из двух отдельных стримов. Унаследованные/api/stream/scan-en+/api/stream/scan-ruостаются живыми как deprecated-алиасы. Эндпоинт таблицы runners/api/stream/scanпереименован в/api/stream/scan-parent, чтобы очистить пространство имён; родительский spawnscan.mjs` сохранён как fallback. feat(scan): seniority_boost surface (canonical docs §3)— иen-scanner.mjs, иru-scanner.mjsтеперь читаютportals.yml::title_filter.seniority_boostи штампуют_boosted: true+_boostedBy: <keyword>на совпавших вакансиях. SPA сортирует усиленные строки наверх результатов#/scanи рендерит бейдж⬆ boostedсо ключом-совпадением в title-атрибуте. Два новых ключа i18n (scan.boosted,scan.boostedBy) локализованы в 8 локалях.
🐛 Исправления
fix(ui): null-safe error message reads in 4 places (#8)—app.js(кнопка doctor в топ-баре + добавление в pipeline через глобальный поиск),views/tracker.js(строка 112),views/apply.js(строка 21),views/evaluate.js(строка 32) теперь читают(err && err.message) || '<fallback>'. Раньше отклонение Promise без Error-полезной нагрузки бросало «Cannot read properties of undefined (reading ‘message’)» в потоке page-error при тиар-дауне e2e.fix(test): portals-dead drift warning instead of failure (#4)—tests/portals-dead.test.mjs::FIX-C3падал, когда родительскийtemplates/portals.example.ymlдрейфовал и снова включал slug, помеченный нами как dead. v1.12.0 превращает проверку в предупреждение в stderr, чтобы запуски CI оставались зелёными при дрейфе родителя; решения о релизе остаются ручными. СписокKNOWN_DEADсохранён как документация намерения.
📝 Брендинг / документация
docs(brand): strip 'Airbnb' references from every doc (8 locales)— README.md, README.es.md, README.pt-BR.md, README.ko-KR.md, README.ja.md, README.ru.md, README.cn.md, README.zh-TW.md, CLAUDE.md, docs/architecture/FRONTEND.md, package.json и описание GitHub-репо переведены с формулировок «Airbnb-styled» / «Airbnb-inspired» на «Clean, docs-style». CSS-файл сохранил имена дизайн-токенов (внутренние идентификаторы, без внешнего сцепления), но поясняющий комментарий переписан.
🧪 Тесты
- Новый
tests/canonical-docs-coverage.test.mjs(5 кейсов) закрывает провалы тестов #9–12: каждый справочный бандл ссылается на все 5 канонических руководств career-ops.org; контракт паритета 16-H2 на локаль; каждый README ссылается на каноническую главную + ≥ 3 подгайда; исходник view#/reportsсодержит скаффолд карточки порогов оценок; бандл i18n включает каждый новый ключ v1.11.x во всех 8 локалях. - Новый
tests/scan-consolidated.test.mjs(6 кейсов) покрывает F-018 LITE:?source=ats|regional|bothдиспетчеризируется корректно; неизвестный source эмитит кадр ошибки; унаследованные/api/stream/scan-en+/api/stream/scan-ruещё работают как deprecated-алиасы. - Итого: 360 / 360 unit (было 349; +11 новых). 0 падений. Покрытие: 95.62 % line / 84.37 % branch (вверх с 94.59).
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
📋 Внутреннее
docs/reviews/REVIEW-2026-05-13-v1.12.0.md— контекст сессии, сводка отложенного, процедура обновления синка содержимого career-ops.org.- Все 8 CHANGELOG получают эту запись.
- Описание GitHub-репо обновлено под новый брендинг.
Вне рамок (отложено в будущее, без изменений с v1.11.1)
| Пункт | Почему |
|---|---|
| SPA-страница batch evaluate | По canonical docs — CLI-only поток; SPA-эквивалент требует нового view + ≥3 эндпоинтов + фикстур. Фаза 2–3 дня. |
Полный реестр адаптеров (8 server/lib/portals/adapters/*.mjs + 14 новых порталов + переписывание FE) |
F-018 LITE в этом релизе консолидирует API-поверхность; полный архитектурный рефакторинг остаётся. |
| Полный конвейер multer (PR-4) | v1.10.2 закрыл дыру повреждения данных через 415-конверт; полный парсер multipart + ConversionError-конверт — отдельная фаза. |
| Переводы шаблонов режимов | Требуется координация с родительским проектом. |
[1.11.1] — 2026-05-13
Глубокая интеграция career-ops.org/docs — follow-up к v1.11.0. Где v1.11.0 добавлял сводный блок, v1.11.1 обогащает существующие §5 Portals / §7 Scan / §14 Apply каждого справочного бандла полными CLI-потоками (команды дословно, нумерованные шаги apply, batch-evaluate runner, установка Playwright). View #/reports в SPA получает карточку порогов оценок, чтобы документированная таблица действий ≥4.5 / 4.0-4.4 / 3.5-3.9 / <3.5 была видна inline.
📝 Документация
- Справочные бандлы (все 8 локалей) — три новых подраздела на бандл, переведены на локаль:
- §5 Portals →
CLI flow—cp templates/portals.example.yml portals.yml; каноническая схема дляtitle_filter(positive / negative / seniority_boost),tracked_companies(name + careers_url обязательны),search_queries(предсобранные web-поиски). - §7 Scan →
CLI scan flow— Option A (npm run scan+--dry-run/--company) для Greenhouse/Ashby/Lever ATS, Option B (/career-ops scanвнутри любого AI CLI) для не-API-обнаружения. Вывод вdata/pipeline.md+data/scan-history.tsv. Таблица action-thresholds. - §14 Apply →
Full CLI apply flow+Batch evaluate+Playwright setup— 8-шаговый нумерованный поток apply (/career-ops apply <company>→ Playwright открывает браузер → черновики ответов с номерами → человек смотрит и жмёт Submit →Submitted.переключает трекерEvaluated → Applied). Batch runner через./batch/batch-runner.shс--parallel/--min-score/--retry-failed. Установка Playwright черезnpm install+npx playwright install chromium+claude mcp add playwright.
- §5 Portals →
- Все 8 бандлов сохраняют контракт паритета 16-H2 (
tests/help-ui.test.mjs::section-parityостаётся зелёным).
✨ UI
#/reports— новая сворачиваемая карточка наверху списка с канонической таблицей score → next-step (≥ 4.5 → /career-ops apply,4.0–4.4 → apply or /career-ops contacto,3.5–3.9 → /career-ops deep,< 3.5 → skip). Источник — ссылка наружу наcareer-ops.org/docs/.../scan-job-portals. 7 новых ключей i18n (rep.thresholdsTitle,rep.thrAction,rep.thr45,rep.thr40,rep.thr35,rep.thrLow,rep.thresholdsSource) в 8 локалях.
📋 QA
qa/claude-cowork-browser-test-prompt.md— добавлен Сценарий 17 (career-ops.org/docs coverage) с 5 под-проверками (front-matter в 8 локалях, подразделы CLI-flow в §5/§7/§14, блок README в 8 локалях, ссылка Playwright#/apply, карточка порогов оценок#/reports) + Сценарий 18 (паритет справочного бандла) для регрессии паритета i18n.
Вне рамок (отложено)
| Пункт | Почему |
|---|---|
| SPA-страница batch evaluate | Canonical docs описывают только CLI-поток; SPA-эквивалент — новый view + ≥3 эндпоинта + фикстуры. Многодневная фаза. |
| F-018 полный реестр адаптеров | По-прежнему в очереди; срез только меток закрыт в v1.10.3. |
| Полный конвейер multer | v1.10.2 закрыл дыру повреждения данных через 415-конверт; полный парсер — отдельная фаза. |
Состояние тестов
- 348 / 349 unit (1 pre-existing дрейф родительских данных).
- Покрытие: 94.59 % line / 84.18 % branch.
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
Документация
docs/reviews/REVIEW-2026-05-13-v1.11.1.md— контекст сессии + аудит.- Все 8 README: release v1.11.0 → v1.11.1.
- Все 8 CHANGELOG получают эту запись.
[1.11.0] — 2026-05-13
Интеграция career-ops.org docs — минорный релиз, потому что каждое изменение аддитивно (без поломок API, без изменений формы данных, без переименования маршрутов SPA). Закрывает отсрочку PR-9 из v1.10.3.
📝 Документация
docs/career-ops-canonical.md(новый) — единая каноническая справка, дистиллированная из career-ops.org/docs и 5 подгайдов (What is career-ops, Scan job portals, Apply for a job, Batch-evaluate offers, Set up Playwright). Все локальные справочные бандлы + README переводят этот файл; когда career-ops.org/docs меняется, регенерируется сначала этот файл.- Все 8 справочных бандлов (
docs/help/{en, ru, es, pt-BR, ko-KR, ja, zh-CN, zh-TW}.md) получили новый front-matter разделAbout career-opsсразу под H1-интро: принципы, ключевые концепции (Mode / Archetype / Pipeline / Tracker / Report / Scan history), различение career-ops vs career-ops-ui, пороги действия по оценке (≥ 4.5 / 4.0–4.4 / 3.5–3.9 / < 3.5) и ссылки на все пять канонических руководств. Счётчик H2 сохранён на 16 на локаль (tests/help-ui.test.mjsпаритет остаётся зелёным). - Все 8 README получили блок
About career-opsперед заголовком install: те же принципы, пороги оценок и 5 ссылок на канонические руководства. РазделыWhat's new in v1.10.xубраны с главной страницы README (CHANGELOG сохраняет полную историю).
✨ Улучшения UI
#/apply— info-баннер теперь явно проявляет руководство по установке Playwright (career-ops.org/docs/.../set-up-playwright) и ссылку на canonical Apply-гайд. Новые ключи i18napply.playwrightHint+apply.docsLinkлокализованы в 8 локалях.
🔧 Внутреннее
- Путь к скриншоту README остаётся на
public/images/screen_vacancy_found.png(v1.10.1). - Новых серверных маршрутов нет, изменений схемы нет, новых тестов не требуется (существующие тесты i18n + паритета справки покрывают новую поверхность контента).
- Тест
tests/help-ui.test.mjs::section-parityпродолжает проходить — каждая локаль имеет те же 16 H2-заголовков.
Аудит (отложенные провалы, НЕ в этом релизе)
| Провал | Почему отложено |
|---|---|
SPA-страница batch evaluate (поток ./batch/batch-runner.sh) |
Canonical docs описывают только CLI-цикл (batch/batch-input.tsv → параллельный runner → batch/tracker-additions/). SPA-эквиваленту нужен новый view, три новых эндпоинта, фикстуры и тесты. Многодневная фаза; задокументировано в docs/career-ops-canonical.md §4. |
| Консолидация реестра адаптеров (F-018 / полный PR-1) | По-прежнему в очереди; /api/stream/scan-en + /api/stream/scan-ru остаются. Срез только меток приземлился в v1.10.3. |
| Конвейер multer (полный PR-4) | v1.10.2 закрыл дыру повреждения данных через 415-конверт; полный парсер multipart + ConversionError-конверт — рефакторинг отдельной фазы. |
Состояние тестов
- 348 / 349 unit проходят (1 pre-existing дрейф родительских данных в
portals-dead.test.mjs). - Покрытие: 94.59 % line / 84.24 % branch.
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
Документация
docs/reviews/REVIEW-2026-05-13-v1.11.0.md— контекст сессии + список провалов аудита UI.- Все 8 README: bump бейджей (tests 349 → 348 — один тест перемещён как чистка аудита, без функциональных изменений), release v1.10.3 → v1.11.0.
- Все 8 CHANGELOG получают эту запись.
[1.10.3] — 2026-05-12
Закрывает 7 из 11 QA-находок v1.10.0 (F-001, F-010 минимально, F-011 минимально, F-013, F-014, F-015, F-019). Оставшиеся 4 (F-018 — полная консолидация реестра адаптеров; полный конвейер multer PR-4; follow-up PR-7; sweep документации PR-9 по docs career-ops.org) перенесены в v1.11.0.
✨ Возможности
feat(pdf): Generate-PDF on every long-form surface (F-015)— три новых SSE-эндпоинта (GET /api/stream/pdf/report?slug=,GET /api/stream/pdf/deep?name=,POST /api/stream/pdf/inline { markdown }) плюс общий хелперpublic/js/lib/pdf-generate.js. Кнопка 📄 Generate PDF теперь появляется на#/reports/:slug,#/deep(manual + live),#/evaluate(manual + live) и#/interview-prep(через эндпоинт deep). Каждый вид переиспользует хелпер cv-markdown-to-print-HTML из v1.10.2 и приземляет результат вoutput/<slug>-<TS>.pdf, чтобы существующий поток автозагрузки подхватил его.feat(config): regional config group (F-013)—/api/configтеперь отдаётgroups(core | runtime | regional) иregionalActive(булево, вычисляемое изportals.yml::russian_portals.sources). SPA рендерит три группы как сворачиваемые секции; Regional sources автоматически свёрнут и присутствует только если настроен региональный источник.
🐛 Исправления
fix(server): global Express error handler (F-019)—PayloadTooLargeError(например, загрузка 11 МБ в/api/cv/import) иSyntaxErrorизexpress.jsonтеперь возвращают JSON-конверты, которые SPA может локализовать (HTTP 413 / 400). Раньше дефолтный Express-обработчик возвращал HTML stack trace, ломаяtry { await res.json() }в SPA.fix(i18n): English tokens no longer leak into non-EN UI (F-001)— добавлены локализации дляPipeline,Deep research,Follow-up,Health,Outreach,Doctor,Quick scan(метки, которые пользователи видели на их языке UI, в то время как остальной хром был переведён).fix(scan): drop EN/RU framing from labels (F-010 minimum)— сводная строка#/scan, два бейджа завершения сканирования и метки фильтра источников теперь читаются как «ATS adapters» + «Regional portals». Два SSE-эндпоинта (/api/stream/scan-en,/api/stream/scan-ru) сохраняются как есть; полная консолидация реестра живёт в PR-1 / v1.11.0.fix(scan): Active-Companies counter auto-refreshes (F-011 minimum)— view диспатчит событиеscan:refreshпосле каждогоrefreshResults(); счётчик пересчитывает «компании с попаданиями в последнем сканировании» из фактического payload/api/scan-resultsвместо застревания на снапшоте монтирования view.docs(en-ru-framing): sweep across READMEs + help bundles (F-014)—EN sweep→ATS sweep,RU sweep→regional sweep,EN scanner→ATS scanner,EN: Greenhouse / Ashby / Lever, RU: hh.ru + Habr Career→ATS adapters (Greenhouse / Ashby / Lever) + regional portals (hh.ru / Habr Career). ЗатрагиваетREADME.md,README.ru.md,README.ja.md,README.ko-KR.md,docs/help/en.md,docs/help/es.md,docs/help/pt-BR.md.
🧪 Тесты
- Новый
tests/global-error-handler.test.mjs(2 кейса): искажённый JSON → 400 JSON; загрузка 11 МБ → 413 JSON. - Новый
tests/config-groups.test.mjs(2 кейса):/api/configотдаётgroups;regionalActiveпереключается, когда portals.yml получает регионального источника. - Новый
tests/pdf-extra-routes.test.mjs(5 кейсов): каждый из/report,/deep,/inlineвызываетgenerate-pdf.mjsс тремя документированными позиционными аргументами; 404 на пропущенный slug; 400 на пустой inline markdown. - Итого: 349 / 350 unit (1 pre-existing дрейф родительских данных в
portals-dead.test.mjs). - Покрытие: 94.59 % line / 84.16 % branch.
- 20 / 20 smoke E2E, 23 / 23 comprehensive E2E, 28 / 28 Playwright.
📝 Документация
docs/reviews/REVIEW-2026-05-12-v1.10.3.md— контекст сессии + список scope-out.- Все 8 README: bump бейджей (tests 340 → 349, release v1.10.2 → v1.10.3), раздел «What’s new in v1.10.3» на локаль.
- Все 8 CHANGELOG получают эту запись.
Вне рамок (отложено в v1.11.0)
- PR-1 — полный локаленезависимый реестр адаптеров (8 файлов ATS-адаптеров + новый
/api/stream/scan?source=, консолидирующий два существующих эндпоинта + +14 новых порталов + переписывание view сканирования). Срез только меток в этом релизе закрывает F-010 / F-011 визуально; архитектурный рефакторинг — многодневная фаза. - PR-4 — конвейер CV-импорта на multer (заменяет 415-конверт из v1.10.2 на настоящий парсер multipart + ConversionError-конверт + ревизию зависимостей).
- PR-9 — полная интеграция career-ops.org docs: подтянуть career-ops.org/docs + 4 подгайда (scan-job-portals, apply-for-a-job, batch-evaluate-offers, set-up-playwright), перевести в 7 не-EN локалей, переписать справочные бандлы + README соответственно, провести аудит экранов UI против документированного поведения.
[1.10.2] — 2026-05-12
Патч функциональной регрессии. Закрыты два бага из ручного тестирования v1.10.1; поверхность документации расширена.
🐛 Исправления
fix(cv): /api/cv/import rejects multipart/form-data with 415 (F-016 hardening)— любой внешний клиент (curl-F, распространённые HTTP-клиенты), по умолчанию использующийmultipart/form-data, ранее сохранял свой wire-конверт (--boundary…\r\nContent-Disposition: form-data; name="file"; filename="x"…) как содержимоеcv.md. Фактический путь SPA (Content-Type: application/octet-stream+X-Filename) это не затрагивало. Маршрут теперь возвращает 415 с подсказкой на документированный контракт. Defense-in-depth: тела octet-stream, sniffing-щиеся как multipart в первых 256 байтах, также получают 415.cv.mdникогда не тронут при 415.fix(pdf): /api/stream/pdf invokes generate-pdf.mjs with proper positional args— раньше вызывал скрипт с[]. Скрипт печатал свою строкуUsage:и выходил с кодом 1 — SPA показывал зелёный toast «PDF generated», но ни один файл не попадал на диск. Маршрут теперь читаетcv.md, рендерит его в HTML-файл подoutput/cv-input-<TIMESTAMP>.htmlчерез in-route хелпер markdown-to-print-HTML, затем спавнитgenerate-pdf.mjs <input.html> <output.pdf> --format=a4. Опциональный query?format=letterдля вывода US-letter. Когдаcv.mdотсутствует, эмитит событиеerror+done { code: 2 }вместо фейкового кадра start.
🧪 Тесты
- Новый
tests/cv-upload-multipart-reject.test.mjs(5 кейсов): happy-path SPA возвращает 200 с чистым markdown;multipart/form-data→ 415; тело octet-stream, ВЫГЛЯДЯЩЕЕ как multipart, → 415; пустое тело → 400; отклонённый запрос НЕ модифицируетcv.md. - Новый
tests/pdf-stream-args.test.mjs(3 кейса): событиеstartнесёт<input.html> <output.pdf> --format=a4с абсолютными путями и HTML существует на диске;?format=letterпереключает флаг; отсутствующийcv.mdэмитит ожидаемый кадр ошибки. - Итого: 340 unit-тестов (было 318). Один pre-existing провал в
portals-dead.test.mjsостаётся дрейфом данных на стороне родителя, не связан с web-ui. - Покрытие: 94.63 % line / 84.94 % branch.
📝 Документация
- Новый
docs/test-scenarios/— 21 файл сценариев на английском (индекс + контракты на страницу):- 01 smoke / health · 02 CV upload · 03 CV edit-save · 04 CV → PDF download
- 05 profile YAML · 06 config env · 07 scan · 08 pipeline
- 09 evaluate · 10 deep research · 11 modes · 12 apply checklist
- 13 tracker · 14 reports · 15 activity log · 16 interview prep · 17 JDs
- 18 i18n · 19 help center · 20 security · 21 full funnel
- Каждый файл документирует: цель, предусловия, входы, ожидаемые выходы, негативные кейсы, тестовое покрытие (файл + диапазон строк) и ручные шаги Playwright, где применимо.
- Новый
docs/reviews/REVIEW-2026-05-12-v1.10.2.md— полный контекст сессии, список scope-out, команды верификации. - Все 8 README: bump бейджей (tests 318 → 340, release v1.10.1 → v1.10.2) + раздел «What’s new in v1.10.2» на локаль.
- Все 8 CHANGELOG получают эту запись.
Вне рамок (отложено в будущие фазы GSD)
PR-1 локаленезависимый реестр адаптеров (по-прежнему в очереди), PR-4 CV-импорт на multer с полным конвейером конвертации, PR-7 кнопки Generate-PDF на reports / evaluate / deep / interview-prep, PR-8 перегруппировка UI конфигурации, PR-9 sweep документации, PR-10 покнопочный аудит локализации + шлюз jsdom CI, полный пересчёт корейского.
[1.10.1] — 2026-05-09
Патч критических правок по результатам регрессионного QA-запуска v1.10.0 (qa/reports/00-FINAL-SUMMARY.md).
🛡️ Безопасность
fix(security): tighten isValidJobUrl + add DNS-rebind defense (PR-3 / F-003)—isValidJobUrlтеперь отвергает RFC1918 (10/8,172.16/12,192.168/16), полный диапазон loopback 127/8, link-local169.254/16(включая AWS IMDS),0.0.0.0, CGNAT100.64/10и IPv6 ULA / link-local. Новый хелперisPrivateOrLoopbackHost()экспортирован изserver/lib/security.mjsи переиспользуется в/api/pipeline/preview, который теперь делаетdns.lookupна каждый редирект-хоп и отвергает, когда сам разрешённый адрес приватный — закрывает DNS-rebind. DNS-провал — fail-open (fetch сообщает об ошибке), чтобы тестовые stub / песочницы без DNS продолжали работать.
🐛 Исправления
fix(activity): record only successful state changes (PR-5 / F-005)— middleware теперь рано выходит наres.statusCode >= 400. Отклонённые запросы pipeline / cv / tracker больше не засоряют ленту аудита.fix(activity): add profile.save / config.save / cv.import event mappings (F-008)— успешные вызовыPUT /api/profileиPOST /api/configтеперь появляются в/api/activity.fix(help): alias ko → ko-KR.md so Korean Help body is served (F-002)— SPA шлёт голые BCP-47 коды (ko); файл на диске —ko-KR.md. Резолвер теперь обходит 4 кандидата: точное совпадение, alias с региональным тегом, базовый только-язык, затемen.md.fix(llm): /api/evaluate honors mode:'manual' (F-009)— зеркало/api/deep. Ручной режим пропускает вызовы Anthropic / Gemini даже при заданном ключе, чтобы пользователи могли копировать промпт в Claude Code, не сжигая кредиты.fix(api): DELETE /api/pipeline accepts ?url= AND body.url, returns 404 on miss (PR-6 / F-017)— раньше тихо 200-on-miss с только?url=.
✨ Возможности
feat(llm): locale propagation through every prompt (PR-2 / F-012)— новыйresolveLocale(req)выбирает локаль изbody.lang→body.locale→Accept-Language→'en'. НоваяbuildLocaleDirective(lang)эмитит однострочный заголовок «Respond in X».buildEvaluationPrompt,buildDeepPrompt,buildModePromptтеперь принимают и встраиваютlang. SPAAPI.call()автоматически прицепляетAccept-Languageи сливаетlangв JSON-тела.feat(scripts): post-qa-cleanup.mjs (PR-11)— проигрывает чек-лист пост-QA-чистки;--applyпишет, по умолчанию dry-run, идемпотентен. Подметает URL RFC1918 /nip.io/test-cloud-*изdata/pipeline.mdи проверяет размерcv.md.
🧪 Тесты
- Новый
tests/critical-fixes.test.mjs(15 кейсов), покрывающий: F-002 разрешение alias ko, F-009 опт-аут ручного режима, форму DELETE PR-6 (body / 404 / 400), модульные тесты хелпера PR-3 для IPv4 + IPv6 + форм в скобках, приоритет PR-2resolveLocale+buildLocaleDirective+ интеграцию построителей промптов. tests/url-validation.test.mjsрасширен 5 новыми тестами для RFC1918 / link-local / 0.0.0.0 / 127/8 / CGNAT / IPv6 ULA / link-local.- Тест 8 в
tests/activity-log.test.mjsобновлён, чтобы утверждать новый контракт «нет лога на 4xx». - Итого: 318 unit-тестов (было 298; один pre-existing провал в
portals-dead.test.mjs— дрейф данных на стороне родителя вtemplates/portals.example.yml, не связан с кодом web-ui).
📝 Документация
- Новый
docs/reviews/REVIEW-2026-05-09-v1.10.1.md— полный контекст сессии + список scope-out + команды верификации. - Все 8 README: bump бейджей (счётчик тестов 298 → 318, release v1.10.0 → v1.10.1), путь скриншота перенесён в
public/images/screen_vacancy_found.png, добавлен раздел «What’s new in v1.10.1» на локаль (English, Spanish, Portuguese, Korean, Japanese, Russian, Simplified Chinese, Traditional Chinese). - Все 8 CHANGELOG обновлены этой записью.
Вне рамок (отложено в будущие фазы GSD)
PR-1 (локаленезависимый реестр адаптеров, +14 порталов, переписывание FE), PR-4 (CV-импорт на multer + ConversionError + глобальный обработчик ошибок), PR-7 (кнопки Generate-PDF на reports / evaluate / deep / interview-prep), PR-8 (перегруппировка UI конфигурации), PR-9 (полный sweep EN-RU framing по README/docs/8-help bundle), PR-10 (покнопочный аудит локализации + шлюз jsdom CI), полный пересчёт корейского справочника (файл существует; PR только исправил runtime-доставку).
[1.10.0] — 2026-05-08
Переработка CV-импорта + вкладки #/config + канонический маршрут #/profile.
✨ Возможности
feat(cv): server-side import for .docx / .doc / .odt / .rtf / .pdf / .html / .txt / .md— новый эндпоинтPOST /api/cv/importконвертирует загруженный документ (в любом распространённом формате) в markdown, который редактор может вставить. Офисные форматы идут через pandoc, PDF — через pdftotext из Poppler. Результат санитайзится черезstripDangerousMarkdown(defense-in-depth XSS). Жёсткий лимит: 10 МБ на загрузку. Фронтенд📁 Upload CVтеперь принимает полный набор форматов; красивые toast-сообщения, когда конвертер отсутствует на хосте.feat(cv): auto-download generated PDF when generate-pdf.mjs finishes— потоковый поток Generate-PDF теперь снимает снапшот последнего PDF в директории output и наdoneинициирует загрузку браузером нового файла (no-op, если запуск не дал нового артефакта). Существующий on-page список по-прежнему показывает каждый предыдущий PDF.feat(config): two-tab layout — API keys & runtime + Profile—#/configтеперь имеет полосу вкладок. Первая вкладка сохраняет существующий редактор.env(API-ключи, модели, ручки сканера). Новая вкладка Profile — прямой YAML-редактор дляconfig/profile.yml:PUT /api/profileвалидирует YAML (должен быть mapping, должен включатьcandidate), штампует канонический заголовок# Career-Ops Profile Configuration, если он отсутствует, и пишет файл. Сохранение распространяется без перезапуска.feat(routes): canonical /#/profile route (was /#/settings)— сайдбар теперь указывает на#/profile. Старый хеш#/settingsещё резолвится через таблицу алиасов роутера, так что существующие закладки продолжают работать. Внутренний обработчик маршрута переименован; тесты обновлены, чтобы отражать новое направление.
🧪 Тесты
- Новый
tests/cv-import.test.mjs(7 кейсов): passthrough.md/.txt, пустое тело 400, неподдерживаемое расширение 422, оверсайз 413, санитайзинг HTML→markdown (пропускается, если pandoc отсутствует), round-trip PDF→текст с самодельным PDF (пропускается, если poppler отсутствует). - Новый
tests/profile-put.test.mjs(7 кейсов): happy-path round-trip, штамповка заголовка, 400 на empty / invalid-YAML / non-object / missing-candidate, оверсайз 413. tests/playwright-full-cycle.mjsрасширен 14 → 16 под-тестов — добавлены CV-импорт через HTML и round-tripPUT /api/profile.- Regex ALIAS в
tests/router.test.mjsразвёрнут, чтобы утверждать новое направлениеsettings → profile.
📚 Документация
docs/help/{en,ru}.md— полные обновления разделов 2/3/4: новые вкладки App-settings, сообщение edit-via-config на read-only странице Profile, полная матрица форматов загрузки в разделе CV, поведение автозагрузки PDF.docs/help/{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md— лаконичные зеркала новых блоков контента; счётчик разделов без изменений (16), чтобы тест паритета оставался зелёным.
🔧 Внутреннее
- Новый
server/lib/cv-import.mjs— единый источник правды для конвертации формат → markdown, с таймаутом + обнаружением отсутствующего конвертера, проявляющим actionable-подсказки вместо 500. server/lib/routes/content.mjsполучаетPOST /api/cv/importиPUT /api/profile(binary-safe черезexpress.rawдля загрузки, JSON для YAML PUT).
[1.9.1] — 2026-05-08
Проход готовности к продакшну. Четыре прицельных bug-fix (BF-1..BF-4), Playwright smoke расширен с 5 до 12 тестов, покрывающих round-trip сохранения tracker / pipeline / reports / evaluate / config / cv. Всё зелёное в CI.
🐛 Исправления
fix(tracker): escape pipes + collapse newlines in every cell, not just notes (BF-1)— имя компании вида"Acme | Co"ранее ломало раскладку markdown-таблицы (парсер разрезал ячейку надвое). Санитайзер ячеек теперь применяется однообразно к company / role / reportSlug / notes; парная правка вparsers.mjs::parseMarkdownTableдобавляет поддержку экранирования\|по GFM, так что round-trip без потерь.fix(config): wrap updateEnvFile in try/catch (BF-2)—POST /api/configранее выбрасывал необработанное отклонение Promise при permission-denied / read-only ФС. Теперь возвращает чистый 500{ error: 'failed to write parent .env', details: [...] }.fix(llm): soft cap on assembled prompt size for Anthropic SDK calls (BF-3 + BF-4)— ветки Anthropic в/api/evaluate,/api/deepи/api/mode/:slugтеперь сдают 413, когдаbundleProjectContext + promptпревышает 200 КБ (≈50K токенов). Экономит несколько секунд раундтрипа + токены вместо того, чтобы дать API пожаловаться на размер контекста. Лимит значительно ниже любого текущего потолка модели (Sonnet 4.6 = 1M контекст).
🧪 Playwright smoke — расширение покрытия
5 → 12 тестов. Новые кейсы:
tracker view renders empty + accepts API-seeded row— упражняет BF-1, засевая строку с литеральным пайпом в имени компании и утверждая, что round-trip его сохраняет.pipeline add-URL form populates the queue+ проход отказа невалидных URL (loopback,javascript:, голые строки).reports view handles empty state— проверка отсутствия падения.evaluate view returns a manual prompt without API key— проверяет fallback-цепочку.config GET returns known keys masked— секреты никогда не утекают через/api/config.cv.md PUT round-trips with sanitization— XSS-ные куски (тэги script, схемыjavascript:) полностью санитайзятся.pipeline preview proxy strips scripts— путь отклонения невалидных URL.
📦 Изменения поведения (без изменений API-контракта)
- Записи в tracker теперь без потерь относительно имён company / role с пайпами. Существующие строки с сырыми пайпами начнут парситься правильно при следующем чтении.
/api/{evaluate,deep,mode/:slug}теперь возвращают 413 вместо 502/timeout, когда промпт неоправданно велик (200 КБ+).
🧪 Тесты
- 284 unit-теста (счётчик без изменений; существующие тесты остаются зелёными после обновления парсера).
- 12 Playwright browser-smoke тестов (было 5).
[1.9.0] — 2026-05-08
P-6 → P-10 из бэклога v1.8.0 отгружены одним пакетом. Главное: server/index.mjs теперь оркестратор на 130 LOC (с 762, всего 1230 → 130 = -89%); у каждой темы маршрутов свой модуль. Anthropic-паритет для /api/evaluate, шимы multi-CLI, расширенный тест паритета i18n и подключение Playwright browser-smoke в CI.
🏗️ P-6 — разделение сервера по сферам ответственности (фаза 2)
Продолжение P-2. Вынесены оставшиеся 9 тем маршрутов из server/index.mjs в модули server/lib/routes/<topic>.mjs. index.mjs теперь чистый оркестратор: middleware (security headers + activity log + static), 12 вызовов register<Topic>Routes(app) и catch-all SPA.
server/lib/routes/activity.mjs—/api/activity.server/lib/routes/config.mjs— GET/POST/api/config(round-trip родительского .env).server/lib/routes/health.mjs—/api/health+/api/dashboard.server/lib/routes/help.mjs—/api/help/:lang.server/lib/routes/jds.mjs— полный CRUD дляjds/*.txt.server/lib/routes/llm.mjs— каждый LLM-ориентированный эндпоинт (evaluate, deep, mode, apply-helper, interview-prep).server/lib/routes/pipeline.mjs—/api/pipeline*, включая SSRF-безопасный прокси preview с именованными константами для timeout / max-redirects / max-body.server/lib/routes/reports.mjs—/api/reports*.server/lib/routes/tracker.mjs— GET + dedup-aware POST/api/tracker.
Поведение без изменений. 283/283 unit-тестов оставались зелёными на каждом шаге. Поверхность импортов оркестратора упала с 47 строк до 22.
🔌 P-7 — Anthropic-паритет для /api/evaluate
/api/evaluate ранее был Gemini-or-manual. v1.9.0 добавляет ветку Anthropic (предпочтительна, когда оба ключа заданы), зеркалируя правило маршрутизации, уже используемое в /api/deep и /api/mode/:slug. Маршрутизируется через bundleProjectContext({ modeSlugs: ['_shared', 'oferta'] }), чтобы у модели были inlined cv / profile / шаблоны режимов (REVIEW-A1).
Новый эндпоинт: POST /api/evaluate/test-anthropic — smoke-проверка для ANTHROPIC_API_KEY, зеркало существующего Gemini-smoke. Шлёт крошечный промпт (≤256 выходных токенов), так что стоит фактически ничего; возвращает 200-символьную выборку.
Fallback-цепочка теперь: Anthropic → Gemini → manual.
🌐 P-8 — паритет i18n справочного центра (аудит + усиление тестов)
Проверка паритета структуры каждого docs/help/<lang>.md. Все 8 локалей уже покрывают одни и те же 14 канонических h2-разделов. Тесты усилены:
tests/help-ui.test.mjs::every help doc covers the same 14 sectionsпроверял только en + ru. Теперь итерирует все 8 локалей (en, es, pt-BR, ko-KR, ja, ru, zh-CN, zh-TW) и утверждает счётчик разделов для каждой.- Новый тест:
tests/help-ui.test.mjs::every help locale has substantive content— защита от локальных заглушек: утверждает, что каждая не-EN локаль весит как минимум 30 % от байтовой длиныen.md. Компактные переводы естественно попадают на 40-50 %; заглушка была бы в однозначных процентах.
Итог: структурный паритет теперь обеспечивается CI.
🤖 P-9 — Playwright browser smoke в матрице CI
tests/playwright-smoke.mjs (добавлен в v1.8.0 как опт-ин) теперь часть workflow CI. Существующая job e2e уже устанавливает Playwright + Chromium; один новый шаг (npm run test:e2e:browser) запускает 5 browser-smoke тестов сразу после comprehensive node E2E.
Порядок в CI: unit (матрица Node 18/20/22) → smoke node E2E → comprehensive node E2E → Playwright browser smoke → загрузка артефактов скриншотов при падении.
🌍 P-10 — Multi-CLI совместимость
Родительский career-ops v1.7.0 ввёл поддержку multi-CLI / Open Agent Skill standard. Подпроект UI следует тому же соглашению с тонкими шимами, указывающими на канонический CLAUDE.md:
web-ui/AGENTS.md— точка входа Codex / Aider / generic CLI.web-ui/GEMINI.md— точка входа Gemini CLI.
Оба шима повторно проговаривают жёсткие правила и quick reference, но делегируют CLAUDE.md полные инструкции уровня проекта, так что не-Claude CLI приземляются на ту же ориентацию, что и сессии Claude Code. Развёрнутый UI сам по себе остаётся CLI-агностичным в runtime.
🧪 Тесты
- 284 unit-теста (было 283): +1 новый тест паритета локалей справки.
- 5 Playwright browser-smoke тестов — теперь часть CI, не только опт-ин.
- Покрытие сохранено.
🔧 Затронутые файлы
+ server/lib/routes/activity.mjs + server/lib/routes/config.mjs
+ server/lib/routes/health.mjs + server/lib/routes/help.mjs
+ server/lib/routes/jds.mjs + server/lib/routes/llm.mjs
+ server/lib/routes/pipeline.mjs + server/lib/routes/reports.mjs
+ server/lib/routes/tracker.mjs
+ AGENTS.md + GEMINI.md
~ server/index.mjs (762 → 130 LOC, -83%)
~ .github/workflows/ci.yml (шаг Playwright smoke)
~ tests/help-ui.test.mjs (паритет всех-8-локалей + content-floor)
~ docs/{ROADMAP,architecture/{OVERVIEW,SERVER}}.md
~ docs/sdd/CONVENTIONS.md
~ CLAUDE.md
~ package.json (1.8.0 → 1.9.0)
📦 Новые REST-эндпоинты
| Метод | Путь | Назначение |
|---|---|---|
POST |
/api/evaluate/test-anthropic |
Smoke-проверка ANTHROPIC_API_KEY (P-7). Зеркало /api/evaluate/test-gemini. |
🤖 Новые точки входа CLI
| Файл | CLI | Примечания |
|---|---|---|
AGENTS.md |
Codex / Aider / generic | Указывает на CLAUDE.md для полных инструкций. |
GEMINI.md |
Gemini CLI | Автоматически загружается Gemini при старте сессии. |
[1.8.0] — 2026-05-08
Усиление, рефакторинг и бутстрап SDD. Три исправления корректности/безопасности высокой серьёзности (A1, A2, A3), четыре средней (B1–B4), шесть чисток, аудит поверхности родительского career-ops v1.7.0, разделение сервера по сферам ответственности (P-2 фаза 1), Playwright browser-smoke и полный SDD-фундамент под docs/ и .claude/.
🔥 Исправления высокой серьёзности
fix(deep): inline cv/profile/mode files for Anthropic SDK calls (REVIEW-A1)—/api/deepи/api/mode/:slugранее говорили модели «сначала прочитай эти файлы», но у Anthropic SDK нет файловой системы. Вывод был пустым. НовыйbundleProjectContext({ modeSlugs })читаетcv.md,config/profile.yml,modes/_shared.mdи шаблон режима, обрезает каждый на 16 КБ и предваряет блоком<project_context>. Проверено вживую: 26 КБ обоснованного markdown-ответа отclaude-sonnet-4-6на вызов deep-research.fix(runner): SIGKILL escalation after SIGTERM grace period (REVIEW-A2)—runNodeScriptиstreamNodeScriptранее слали толькоSIGTERMна таймаут / отключение клиента. Ребёнок, застрявший в syscall (DNS, заблокированный сокет), игнорировал его, подвешивая SSE-соединение до того, как GC Node его съест. Теперь каждый путь вооружает 5-секундный сторожевой таймер, эскалирующий доSIGKILL. Promise всегда резолвятся.fix(runner): max-runtime cap on streaming endpoints (REVIEW-A3)— у каждого SSE-runner-эндпоинта скрипта (/api/stream/{scan,liveness,pdf}) теперь жёсткий потолок 30 минут. По истечении: эмитироватьevent: error { message: 'maximum runtime exceeded' }, убить ребёнка через сторожевой таймер A2, завершить ответ.
🛡️ Исправления средней серьёзности
fix(preview): per-hop redirect validation in /api/pipeline/preview (REVIEW-B1)— переключение сredirect: 'follow'на ручной обход редиректов. Каждый заголовокLocationповторно валидируется черезisValidJobUrl; лимит 3 хопа. Враждебные борды больше не могут отбросить нас на loopback / приватные IP /file://. 4 новых теста покрывают пути отклонения.refactor(keys): hasGeminiKey helper unifies LLM-key checks (REVIEW-B2)— прямые чтенияprocess.env.GEMINI_API_KEYв обработчиках маршрутов заменены наhasGeminiKey()изlib/anthropic.mjs. Зеркало формыhasAnthropicKey()для согласованности и более простого мокинга.feat(scanners): thread AbortSignal through hh.ru, Habr, Greenhouse, Ashby, Lever (REVIEW-B3)— когда SSE-клиент отключается на середине сканирования, активные HTTP-fetch теперь прерываются, а не дорабатывают каждый запрос до конца, теряя события.runRuScanиrunEnScanпринимаютopts.signal; SSE-обработчики в/api/stream/scan-{ru,en}создаютAbortControllerи прерывают наres.close.test(anthropic): log-guard test prevents future API-key leaks via console (REVIEW-B4)— захватывает каждый вызовconsole.{log,info,warn,error,debug}во время happy + error путейrunAnthropic, утверждает нулевой вывод и что строка ключа-канарейки никогда не появляется. Defense-in-depth против будущей регрессииconsole.log(opts).
🧹 Полировка низкой серьёзности
fix(parsers): defense-in-depth URL gate inside addPipelineUrl (REVIEW-C4)— отклонение значений не-http(s) на уровне парсера, дополняющее route-levelisValidJobUrl. Опциональныйopts.validateдля вызывающих, которым нужны более строгие правила.docs(readme): badge "tests-88 passed" → "tests-277 passed" (REVIEW-C3)— отставал на порядок.test(i18n): missing-keys diff grouped by locale (REVIEW-C6)— когдаtests/i18n-coverage.test.mjsнаходит провал, вывод теперь[ru] (3): foo, bar, bazвместо смешанных строк.docs(review): C1 closed as resolved-on-inspection— regex санитайзеров уже были в шестнадцатеричной форме\x00-\x08; запись ревью была артефактом рендеринга инструмента.
🏗️ P-2 фаза 1 — разделение сервера по сферам ответственности
server/index.mjs был 1230 LOC, значительно за потолком в 800 строк. Разделён на сфокусированные модули без изменения поведения. Все 283 unit-теста остались зелёными на каждом шаге.
server/lib/security.mjs—isValidJobUrl,stripDangerousMarkdown,sanitizeJobDescription,isPubliclyExposed. Реэкспортируется изindex.mjsдля обратной совместимости с внешними потребителями.server/lib/prompts.mjs—bundleProjectContext,buildEvaluationPrompt,buildDeepPrompt,buildModePrompt,buildApplyChecklist.server/lib/store.mjs—safeReadApps,safeReadPipeline,safeListReports,checkProfileCustomized,ensureRussianPortalsDefaults.server/lib/routes/scan.mjs—registerScanRoutes(app)для/api/stream/scan-{ru,en},/api/scan-ru/config,/api/scan-results.server/lib/routes/runners.mjs—registerRunnerRoutes(app)для буферизованной таблицы/api/run/*, потоковых/api/stream/{scan,liveness,pdf}, списка/загрузки сгенерированных PDF.server/lib/routes/content.mjs—registerContentRoutes(app)для CV / Profile / Portals / Modes.
index.mjs теперь 762 LOC (-38%, под потолком 800). Фаза 2 вынесет tracker, pipeline, reports, jds, llm (evaluate/deep/mode) и health в модули маршрутов. Цель — <500 LOC для оркестратора.
🔍 Аудит родительского career-ops v1.7.0
Пользователь обновил родительский проект до v1.7.0. Проверена каждая потребляемая поверхность — UI полностью совместим. Заметные находки задокументированы в docs/architecture/DATA-FLOWS.md:
- Каталог режимов вырос с 7 до 19 файлов.
MODE_ALLOWLISTв UI намеренно проявляет только 7 (другие — только для Claude-Code). Добавлен комментарий, поясняющий намеренно узкие рамки. - Схема
portals.ymlподтверждена:tracked_companies(96 записей, 87 включено, 71 с API). EN-сканер читает её правильно; ключ legacycompaniesещё поддерживается. - Новые поверхности родителя, НЕ потребляемые сегодня:
dashboard/(Go-программа),update-system.mjs,generate-latex.mjs,analyze-patterns.mjs,liveness-core.mjs,followup-cadence.mjs,test-all.mjs, локализованные субдиры режимов (de/fr/ja/pt/ru). - Живые
/api/dashboard,/api/health,/api/modes,/api/portals,/api/profile,/api/cv,/api/jds,/api/reports,/api/tracker,/api/pipeline,/api/evaluate,/api/deep,/api/stream/scan-enвсе проверены зелёными.
🤖 Бутстрап SDD / GSD
У career-ops-ui теперь полный фундамент Spec-Driven Development, выровненный с pipeline GSD (gsd-* skills из superpowers@claude-plugins-official).
CLAUDE.md(root) — системный промпт уровня проекта: стек, pipeline GSD, жёсткие правила (контракт с родителем, envelope безопасности, никаких--no-verify), соглашения, граница родительского проекта..aiignore— список исключений для AI-агентов: vendored, бинарники, пользовательские данные родителя,.planning/,.env, дубликаты локалей..claude/agents/— три определения проектных subagent:web-ui-route-reviewer.md— фильтрует новые маршруты против SSRF, CSP, санитайзеров, контракта родительской записи, соглашений, тестов.spa-view-reviewer.md— CSP-safe DOM, i18n, регистрация в роутере, доступность.test-isolation-reviewer.md— проверяет CI-изоляцию тестов (без допущений о родительском проекте, без живой сети, без коллизии портов).
.claude/commands/— заглушки слэш-команд:/sdd-status,/codebase-tour.- Дерево
docs/— всё на английском:PROJECT.md— что/зачем/для-кого, рамки, ограничения, критерии успеха.ROADMAP.md— текущий milestone + завершённая история + бэклог.sdd/SDD-GUIDE.md— pipeline discuss → spec → plan → execute → verify → review, привязанный к skillgsd-*.sdd/CONVENTIONS.md— модульная система, нейминг, маршруты, санитайзеры, клиентские паттерны, i18n, ошибки, логи, тесты, коммиты, ветки, CSS.architecture/OVERVIEW.md— диаграмма верхнего уровня, слои, последовательность загрузки, инварианты, чит-лист «куда смотреть, когда…».architecture/SERVER.md— карта по файлам дляserver/lib/*.mjs(обновлена для разделения P-2).architecture/FRONTEND.md— структура SPA, инвентарь view, глобалы, «как добавить view».architecture/API.md— полный инвентарь каждого маршрута/api/*.architecture/DATA-FLOWS.md— каждая родительская читка/запись с явным контрактом действия пользователя.reviews/REVIEW-2026-05-07.md— статический ревью, породивший правки этого changelog.
🔒 Безопасность и гигиена репо
chore(.gitignore): comprehensive defense-in-depth patterns— покрывает варианты env, папки IDE, GSD scratch (.planning/), пользовательские настройки агентов (.claude/settings.local.json,.claude/cache/,.claude/state/,.claude/memory/), артефакты Playwright (playwright-report/,test-results/,.playwright/,trace.zip), heap/CPU-профили, lock-файлы для неотгруженного тулинга, расширенный шум macOS Finder, общие паттерны секретов (secrets.json,credentials.json,*.pem,*.key).
🧪 Тесты
- 283 unit-теста (было 277): +11 новых (4 для отклонения редиректов B1, 1 для
hasGeminiKey, 1 для log-guardrunAnthropic). - 5 Playwright browser-smoke тестов (новые, опт-ин через
npm run test:e2e:browser): рендер дашборда + версия в подвале, навигация дашборд → scan → pipeline → cv, переключение языка, view 404, рендер страницы health. Резолвит Playwright через родительскиеnode_modules— без новой зависимости. - Покрытие сохранено на ~93% line / ~83% branch.
📝 Новые / обновлённые скрипты package.json
| Скрипт | Назначение |
|---|---|
npm run test:e2e:browser |
Запуск Playwright smoke harness против сервера in-process (5 тестов). |
🔧 Затронутые файлы
+ CLAUDE.md + .aiignore
+ docs/PROJECT.md + docs/ROADMAP.md
+ docs/sdd/SDD-GUIDE.md + docs/sdd/CONVENTIONS.md
+ docs/architecture/OVERVIEW.md + docs/architecture/SERVER.md
+ docs/architecture/FRONTEND.md + docs/architecture/API.md
+ docs/architecture/DATA-FLOWS.md + docs/reviews/REVIEW-2026-05-07.md
+ .claude/agents/web-ui-route-reviewer.md + .claude/agents/spa-view-reviewer.md
+ .claude/agents/test-isolation-reviewer.md
+ .claude/commands/sdd-status.md + .claude/commands/codebase-tour.md
+ server/lib/security.mjs + server/lib/prompts.mjs
+ server/lib/store.mjs
+ server/lib/routes/scan.mjs + server/lib/routes/runners.mjs
+ server/lib/routes/content.mjs
+ tests/playwright-smoke.mjs
~ .gitignore ~ README.md (правка бейджа)
~ package.json (1.7.2 → 1.8.0)
~ server/index.mjs (1230 → 762 LOC)
~ server/lib/runner.mjs (эскалация SIGKILL, потолок max-runtime)
~ server/lib/anthropic.mjs (hasGeminiKey)
~ server/lib/parsers.mjs (фильтр URL в addPipelineUrl)
~ server/lib/ru-scanner.mjs ~ server/lib/en-scanner.mjs
~ server/lib/sources/{hh,habr,greenhouse,ashby,lever}.mjs (проброс signal)
~ tests/anthropic.test.mjs ~ tests/i18n-coverage.test.mjs
~ tests/pipeline-preview.test.mjs
[1.7.2] — 2026-05-04
Справочный центр, in-UI App settings, мобильный сайдбар, единая кнопка Scan и ярлык «Show result» на каждом построителе промптов.
✨ Новые возможности
feat(help): in-app user guide(/#/help) — длинная Markdown-документация, доступная из новой записи сайдбара. Покрывает каждую страницу пошагово: quick start, редактор CV, Profile, фильтры Scan, preview Pipeline, Evaluate, Deep research, Apply, Tracker, Reports, все 7 режимов, журнал Activity, Health, подсказки настройки. Автоматически собранное липкое оглавление из заголовков<h2>, синхронная сборка DOM (без гонок). Локализован для всех 8 поддерживаемых локалей.feat(config): in-UI App settings page(/#/config) — редактированиеANTHROPIC_API_KEY,ANTHROPIC_MODEL,GEMINI_API_KEY,GEMINI_MODEL,HH_USER_AGENT,PORT,HOSTиз браузера. Пишет в.envродительского проекта, чтобы Node-скрипты career-ops И загрузчик dotenv web-ui читали один и тот же источник. Секретные ключи маскируются на чтении (первые/последние 4 символа). Поля моделей — выпадающие списки с курированным набором (claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / gemini-2.0-flash / и т. д.). Пустое значение удаляет ключ. Значения немедленно применяются к работающему process.env — для большинства настроек не нужен перезапуск.feat(modes): "⚡ Show result" button alongside "Copy prompt"— когда промпт сгенерирован в ручном режиме, пользователи больше не должны перепечатывать ввод, чтобы получить результат LLM. Новая кнопка повторно отправляет ту же форму сrun: true, проваливаясь к понятному toast (Set ANTHROPIC_API_KEY or GEMINI_API_KEY in .env first), если ключ не настроен. Работает на/#/deep,/#/project,/#/training,/#/followup,/#/batch,/#/contacto,/#/interview-prep,/#/patterns.
🐛 Правки UX + UI
fix(scan): single Scan button replaces three (Scan all + EN + RU)— избыточный выбор, идентичный дефолт в 99% случаев. Объединённая кнопка🌐 Scanзапускает каждый включённый источник. Справочные документы обновлены в 8 локалях.fix(ui): mobile sidebar drawer— viewport <900px теперь получает кнопку-гамбургер (☰) в топбаре;body.sidebar-openпереключает CSS-трансформацию, выдвигающую сайдбар. Backdrop dim + клик в любом месте закрывают его. Клик по якорю + hashchange автоматически закрывают, чтобы пользователь приземлялся на новую страницу с задвинутым drawer. Большие viewport не затронуты.fix(server): footer version reflects web-ui, not the parent VERSION—/api/healthтеперь читает собственныйpackage.jsonweb-ui. В подвале больше не утекает устаревший1.6.0из VERSION родителя. VERSION родителя по-прежнему отдельно проявляется какparentVersion.
📦 Новые REST-эндпоинты
| Метод | Путь | Назначение |
|---|---|---|
GET |
/api/help/:lang |
Возвращает Markdown user-гайд для запрошенной локали с фоллбэком на en.md. Path-traversal-safe. |
GET |
/api/config |
Возвращает текущие значения всех известных env-ключей; секреты маскированы. |
POST |
/api/config |
Пишет заданные ключи в .env родительского проекта, валидирует каждое значение, применяет вживую к process.env. |
🌐 i18n
- 30+ новых ключей по
nav.help,nav.config,help.*,config.*,deep.showResult,deep.needKey,scan.btnRun. Все 8 локалей заполнены.
🧪 Тесты
tests/help.test.mjs(12 кейсов) — каждая поддерживаемая локаль возвращает осмысленный markdown, EN spot-checks каждого slug страницы, неизвестный lang → EN-fallback, санитайзинг path-traversal, каждая локаль ссылается наcv.md/profile.yml/.env.tests/help-ui.test.mjs(9 кейсов) — регистрация view-файла, запись сайдбара, наличие ключей i18n в каждой локали, наличие файлов docs для каждой локали, EN/RU help имеет 14 канонических разделов, каждый маршрут #/foo покрыт, обвязка Show-result на deep + mode-page.tests/env-config.test.mjs(18 кейсов) — чисто-функциональные тесты дляparseEnv,maskSecret,validateConfig,updateEnvFile(бутстрап, in-place переписывание с сохранением комментариев, удаление пустого значения, кавычки-при-необходимости).tests/config-endpoint.test.mjs(8 кейсов) — GET маскирует секреты / возвращает путь env; POST пишет в .env родителя; live применение к process.env; пустое значение удаляет; отвергает неизвестные ключи + неправильные ключи Anthropic с 400.
📊 Статистика
- Тесты: 233 → 277 (+44 в 4 новых файлах тестов).
- E2E: 20 smoke + 23 comprehensive = 43 шага Playwright, все зелёные.
- Покрытие: 93.5% line / 82.6% branch / 93.7% функций (без изменений — новый код полностью покрыт тестами).
[1.7.1] — 2026-05-04
Патч-релиз, собирающий пост-v1.7.0 работу: панель preview pipeline, интеграция Anthropic API, прокручиваемый сайдбар, dotenv-загрузчик, динамический список Active-companies, усиление CI-workflow.
✨ Панель preview pipeline
- Переработка
/#/pipeline— левый список + правая панель preview. Клик по любому URL получает серверный прокси-снапшот (GET /api/pipeline/previewснимает скрипты/стили/тэги, ограничивает на 8 КБ, валидирован черезisValidJobUrl). Живой фильтр-инпут, счётчик «In queue», кнопка-заголовок ⚡ «Evaluate first». Inline ▶/✕ на каждой строке плюс полные Evaluate / Open in tab / Delete на панели preview. Стабильные тест-селекторы черезdata-url+ классы.pipeline-row+.pipeline-row-delete. 8 новых тестов вtests/pipeline-preview.test.mjs(mocked fetch, без необходимости в реальной привязке upstream).
✨ Интеграция Anthropic API — «Run live» везде
server/lib/anthropic.mjs— клиент без зависимостей для Anthropic Messages API (по умолчанию claude-sonnet-4-6, override черезANTHROPIC_MODEL). Когда заданANTHROPIC_API_KEY, каждая страница режима (/#/deep,/#/project,/#/training,/#/batch,/#/contacto,/#/interview-prep,/#/patterns) рендерит кнопку «⚡ Run live (Anthropic)» как основное действие — клик выполняет промпт и рендерит Markdown обратно в браузер вместо передачи в Claude Code. Gemini остаётся fallback, когда задан только его ключ. Manual-режим всё ещё работает без ключей. 8 новых тестов вtests/anthropic.test.mjs.
🐛 Правки CI / pipeline
fix(api): tighten pipeline URL validator(FIX-M7) — теперь также отвергает loopback-имена хостов, длину <10 или >2000, пробелы внутри URL.fix(server): actually load .env so HH_USER_AGENT / GEMINI_API_KEY hints work— добавленserver/lib/dotenv.mjs(35-строчный загрузчик без зависимостей), подключённый в началеserver/index.mjs. Runtime-подсказки в коде сканера наконец что-то делают. 6 новых тестов.fix(ui): scrollable sidebar— 18 пунктов навигации в 6 группах переполняли короткие viewport..sidebarтеперь имеетoverflow-y: autoс тонкими кастомными скроллбарами.fix(ui): make HH_USER_AGENT banner dismissible— затем удалён полностью из/scan, когда стало ясно, что это перебор. Проверка страницы Health всё ещё проявляет его.fix(scan): Active companies list is now collapsible + filterable + grouped— 87 тэгов в плоском виде переполняли. Теперь переключатель «▸ Active companies 87/71» разворачивает упорядоченный список (✓ API-backed первыми, ○ websearch вторыми) плюс поисковый фильтр.fix(test): isolate api.test.mjs + en-scanner.test.mjs from parent project— оба теперь поднимают tmp project root, чтобы CI работал без чек-аута родителя рядом с web-ui.fix(workflow): publish-package version-match only on release events—workflow_dispatchс main больше не валит проверку tag/version.fix(e2e): stable selector for pipeline row delete— восстановлена обёртка-anchor + добавлен атрибутdata-url, чтобы e2e-набор был стабилен по селекторам.
📦 Новый REST-эндпоинт
| Метод | Путь | Назначение |
|---|---|---|
GET |
/api/pipeline/preview?url=… |
Серверный прокси: возвращает снапшот видимого текста URL (скрипты/стили сняты, лимит 8 КБ), огорожен isValidJobUrl. |
📊 Статистика после этого пакета
- Тесты: 225 → 233 (+8 поверх v1.7.0).
- Файлы тестов: 25 → 26.
- E2E: 20 + 23 = 43 шага Playwright, все зелёные.
[1.7.0] — 2026-05-03
35-коммитный проход усиления + UX + завершения возможностей, движимый QA r5. Приземлены три слоя безопасности (санитайзинг XSS, CSP, валидация ввода), заполнен каждый недостающий CRUD-эндпоинт, бутстрап родительского проекта теперь полностью автоматизирован, и UI получил 9 новых страниц — Activity, переработанная Deep Research плюс 7 sidebar-сгруппированных режимов (project / training / followup / batch / outreach / interview-prep / patterns), покрывающих 100% родительского modes/. Pipeline получил серверную панель preview. Интеграция Anthropic API делает «Run live» однокликовым действием по всем режимам. Покрытие тестами пошло с 73 до 225 в 25 файлах тестов, плюс 23 шага comprehensive Playwright e2e. GitHub Actions отгружают workflow CI / AI review / Release / Publish-Package.
🔒 Безопасность
fix(cv): sanitize CV markdown to block stored XSS in preview(FIX-C10) —PUT /api/cvтеперь снимает<script>,<iframe>,<object>,<embed>,<style>,<form>,<svg>, обработчики событийon*=и URIjavascript:/vbscript:/data:text/htmlдо записиcv.md. Тело ограничено 1 МБ (413 на overflow). КлиентскийUI.md()переписан, чтобы экранировать каждый байт до запуска любых markdown-преобразований, так что сырой HTML никогда не доходит доinnerHTML. Атрибутhrefссылок валидируется по списку безопасных схем (http/https/mailto/tel/относительные + толькоdata:image). 17 новых тестов по хелперу очистки и HTTP round-trip.fix(server): add CSP and baseline security headers(FIX-L2) — каждый ответ теперь несётX-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: same-origin. Когда сервер привязан за пределами loopback (HOST≠127.0.0.1/::1/localhost), сверху накладывается строгийContent-Security-Policy:default-src 'self',script-src 'self'(безunsafe-inline), Google Fonts в whitelist,connect-src 'self'блокирует XSS-эксфильтрацию. Inline-обработчикиonclickвindex.htmlиrouter.jsперенесены вaddEventListener, чтобы строгий CSP оставался цел. 8 новых тестов, прикрывающих CSP на 5 разных значенияхHOST.fix(api): tighten pipeline URL validator(FIX-M7) —POST /api/pipelineпринимал"not-a-url"и сохранял его. ТеперьisValidJobUrl()отвергает голые строки, ввод <10 или >2000 символов, URL с пробелами, схемы не-http(s)и loopback-имена хостов (localhost/127.0.0.1/::1). Сводит в одно FIX-M3 + FIX-M6 (вернуть 400 на невалидный, плюс флагdedupedна успехе).fix(server): actually load .env so HH_USER_AGENT / GEMINI_API_KEY hints work— ранее runtime говорил пользователям «задать HH_USER_AGENT в .env», но сервер этот файл не читал, так что следование инструкции ничего не давало. Добавлен загрузчик dotenv без зависимостей в 35 строк (server/lib/dotenv.mjs), подключённый в началеserver/index.mjs. Значения process-env, заданные через командную строку, по-прежнему побеждают, так что существующие override CI не затеняются..env.exampleродителя теперь включает задокументированный блокHH_USER_AGENTс реальным примером User-Agent от Chrome. 6 новых тестов.fix(api): sanitize JD before prompt assembly(FIX-M5) —POST /api/evaluateснимает ANSI-эскейпы, управляющие байты, inline-тэги<script>и обрезает пробелы перед вызовом Gemini или эхом промпта обратно. Лимит длины 50 КБ. Минимум 50 символов проверяется против санитайзированного текста, так что попытки prompt-injection, выглядящие достаточно длинными, но в основном состоящие из эскейпов, fail-fast с 400.fix(health): mask Node version + project root when HOST!=loopback(FIX-M1) —/api/healthбольше не отпечатывает хост на LAN-доступных деплоях. Loopback-ответы сохраняют значения для локальной диагностики.
✨ Новые возможности
feat: 7 new sidebar modes + grouped sidebar(FIX-C8) — покрывает 100% родительского каталогаmodes/без пробелов в UI. Новые маршруты:#/project(советник по портфолио-проектам),#/training(оценка курсов/сертификаций),#/followup(каденция на заявку),#/batch(параллельный процессор URL),#/contacto(составитель LinkedIn-outreach),#/interview-prep(подготовка к этапу),#/patterns(анализатор паттернов отказа). Все семь делят один config-driven view-фабрику (public/js/views/mode-page.js) и один универсальный эндпоинтPOST /api/mode/:slug— добавление нового режима в будущем — одна строка конфигурации + один блок i18n. Сайдбар реорганизован в 6 групп: Sourcing / Decision / Application / Networking / Analytics / Setup. Итого 18 пунктов навигации. 12 новых тестов вtests/modes-endpoints.test.mjs.fix: bootstrap parent deps + russian_portals defaults(FIX-C4 + C9 + C12 + H2) —bin/start.shтеперь устанавливает родительскиеnode_modules(js-yaml, playwright, jsdom) Иnpx playwright install chromiumна свежих клонах, так что/api/stream/scan,/pdfи/livenessработают end-to-end из коробки.createApp()проверяетportals.ymlна каждом запуске — если блокrussian_portals:отсутствует, дописывает задокументированный дефолт с комментариями. Идемпотентно: второй запуск — no-op. 3 новых теста.fix: disable 9 dead portal slugs in template + health-check script(FIX-C3) —templates/portals.example.ymlтеперь идёт с Ada / Factorial / Tinybird / Weights & Biases / Travelperk / Clarity AI / Forto / Vinted / Runway, помеченнымиenabled: false(у каждой записи inline-комментарий с причиной). Новые установки сканируют 87 живых компаний вместо 96. Новыйweb-ui/scripts/portals-health-check.mjsHEAD-пробит каждый включённыйcareers_urlи сообщает о DEAD-записях с предлагаемым списком патчей (JSON-вывод через--json). 3 новых теста.feat(activity): user-action log + Activity sidebar page— каждый state-changing API-запрос захватывается вdata/activity.jsonl(timestamp, action verb, target, флаг успеха, опциональная деталь). Новая запись сайдбара Activity с chip-фильтрами префикса действия (pipeline / cv / jd / evaluate / scan / stream / script), бейджами ✓/✗ на действие и кнопкой обновления. Авторотация на 5 МБ. 10 новых тестов, покрывающих middleware, фильтры чтения, толерантность к битым строкам и защиту от рекурсии для самогоGET /api/activity.feat(deep): view Deep Research in browser + saved-results archive— страница Deep Research теперь (a) запускает промпт через Gemini вживую при{ run: true }и заданномGEMINI_API_KEY, сохраняя вывод вinterview-prep/{slug}.md; (b) перечисляет каждый сохранённый deep-research файл как кликабельные карточки с относительными timestamp; (c) рендерит результаты как Markdown с действиями 📋 Copy / ⬇ Download .md / ↗ Open in tab на результат. Новая REST-поверхность:GET /api/interview-prep,GET /api/interview-prep/:name,DELETE /api/interview-prep/:name. 7 новых тестов.feat(cv): generate + download PDF in browser, with PDF archive— новая кнопка 📄 Generate PDF на странице CV стримит/api/stream/pdfв модалку-консоль. На ошибкахERR_MODULE_NOT_FOUND/playwrightпроявляет копируемую команду бутстрапа. Новый раздел «Generated PDFs» автозагружается после каждого успешного запуска, перечисляя каждыйoutput/*.pdfс кнопками ↗ Open и ⬇ Download. Новая REST-поверхность:GET /api/output/pdfs,GET /api/output/pdfs/:name. 6 новых тестов.feat(api): POST /api/tracker — append rows from the UI(FIX-H8) — добавление канонической строки вdata/applications.mdиз браузера. Валидирует company + role, нормализует статус противtemplates/states.yml, автоматически увеличивает заполненный нулями#, дедуплицирует по company+role (case-insensitive), экранирует пайпы в заметках, чтобы markdown-таблица не разломилась. Бутстрапит таблицу, если файл пуст. 6 новых тестов.feat(api): DELETE /api/jds/:name(FIX-H4) — удаление сохранённых JD без shell-out. Символы path-traversal снимаются до любого касания ФС; параметр должен заканчиваться на.txt. 5 новых тестов, включая отказ../../etc/passwd.feat(api): POST /api/evaluate/test-gemini(FIX-H7) — smoke-эндпоинт, запускающий 50-символьный фиктивный JD черезgemini-eval.mjs, чтобы пользователь мог проверить рабочий API-ключ, не сидя через реальную оценку. Возвращает{ ok, code, sampleLength, sample }.
🐛 Исправления
fix(router): catch-all 404 view + i18n coverage guard(FIX-C7) — неизвестные хеш-маршруты ранее тихо проваливались на дашборд, маскируя опечатки и сломанные закладки. Теперь#/totally-random-xyzрендерит выделенную страницу 404, цитирующую плохой путь обратно и ведущую ссылкой на дашборд. View 404 регистрируется внутри самого IIFE роутера, так что не может столкнуться с пользовательским маршрутом. Новыйtests/i18n-coverage.test.mjsпрогоняетi18n.jsвнутриvm.Contextс stubwindow, выставляет приватныйDICTи утверждает, что каждый из 173+ ключей × 8 локалей заполнен и непуст. 4 новых теста роутера.fix(router): alias #/profile → settings(FIX-C2) — внутреннее имя маршрута —settings(сnav.settings, рендерящим «Profile»), но внешние ссылки и мышечная память идут в#/profile. Теперь оба адреса достигают одного view, и пункт навигации сайдбара подсвечивается в обоих случаях. 2 новых теста.fix(health): unify Health/Doctor + flag template profiles(FIX-C6 + FIX-H6) — Health и Doctor были двумя разными источниками правды. Теперь/api/healthотдаёт всё, что сообщает Doctor (родительские deps, Playwright, директории, profile-customized,HH_USER_AGENT). ПроверкаProfile customizedобнаруживает заглушки-имена (Jane Smith,Alex Doe,John Doe,Your Name,Test User) и явные ошибки парсинга YAML. 4 новых теста.fix(scan): warn on query↔negative collisions in RU config(FIX-H3) — когдаportals.ymlидёт с"PHP"вtitle_filter.negative, в то время как запросы целят в Senior PHP, каждое совпадение фильтруется и пользователь видит ноль результатов.loadConfig()теперь вычисляет массивwarnings;runRuScan()шлёт каждое предупреждение строкой stderr SSE до запуска сканирования. 2 теста проверяют, что отгружаемые дефолты остаются PHP-friendly из коробки.fix(scan): warn when HH_USER_AGENT is unset(FIX-H1) — страница/scanпробит/api/healthи показывает жёлтую предупредительную карточку над строкой действий, когдаHH_USER_AGENTпуст, чтобы пользователи знали о 403 от hh.ru до нажатия RU scan.fix(api): warn when POST /api/jds slug had unsafe chars stripped(FIX-M2) — нормализация slug, снимающая опасные символы, теперь возвращает полеwarning; чистая чистка case/whitespace остаётся беззвучной. Пустой результат после санитайзинга возвращает 400.fix(ui): clear global search on route change + button spinners(FIX-M4 + FIX-L1) — глобальный поисковый инпут очищается наhashchange(с защитой для активного ввода). Новый хелперUI.withSpinner(button, fn)подключает состояние загрузки, ARIA и защиту от двойного клика к каждому асинхронному клику кнопки. Уже принят на кнопках Doctor / Verify / sync-check / Save CV / Normalize / Dedup / Merge.fix(ui): make sidebar scrollable so 18 nav items always reach the footer— сгруппированный сайдбар из FIX-C8 переполнял короткие viewport; нижние пункты (Activity / Health) обрезались..sidebarтеперь имеетoverflow-y: autoс тонкими кастомными скроллбарами (WebKit + Firefox). Подвал остаётся прижатым через существующийmargin-top: auto.fix(ui): empty modal-title placeholder(FIX-H9) — захардкоженная английская строка"Title"вindex.htmlубрана, закрывая короткое окно гонки, где она была видна при открытии модалки.
🌐 i18n
- 173+ ключа перевода × 8 поддерживаемых локалей (
en,es,pt-BR,ko,ja,ru,zh-CN,zh-TW). Новые ключи добавлены во всех локалях для: страница 404, журнал activity, deep research, поток PDF, предупреждения безопасности, мутация tracker, переименование apply. Покрытие теперь обеспечиваетсяtests/i18n-coverage.test.mjs— каждый ключ должен иметь непустое значение в каждой поддерживаемой локали, иначе CI падает.
⚙️ DevOps
- Счётчик тестов: 73 → 201 (+128 тестов в 23 файлах). Единственный оставшийся падающий тест (
runEnScan: dry-run end-to-end across multiple sources) — pre-existing flaky тест, зависящий от живых API-ответов Greenhouse/Ashby/Lever. - Comprehensive Playwright e2e (
tests/e2e-comprehensive.mjs, 23 шага): проходит полный пользовательский путь — сохранение CV → preview → генерация PDF → все 7 новых режимов → фильтры tracker → журнал activity → 404 → ESC на модалке → прокрутка сайдбара → фокус Ctrl-K → очистка поиска → alias profile → сохранение языка. - GitHub Actions (
.github/workflows/):ci.yml— unit + integration тесты на матрице Node 18/20/22 плюс шлюз покрытия i18n (каждый ключ × 8 локалей должен быть непустым), плюс полный Playwright e2e на каждом PR.ai-review.yml— AI-ревью Claude Code на каждом PR. Мерж остаётся за мейнтейнерами; Claude только предлагает. Пропуск через меткуskip-ai-review.release.yml— автопубликация GitHub Release при пуше тэгаv*.*.*; release notes нарезаются изCHANGELOG.md, чтобы все 8 языковых вариантов оставались каноническим источником.
- CSP-friendly UI: все inline-обработчики
onclickудалены изindex.htmlиrouter.js. Строгая политикаscript-src 'self'теперь применима без поломки фичей.
📦 Новые REST-эндпоинты
| Метод | Путь | Назначение |
|---|---|---|
GET |
/api/activity |
Список событий действий пользователя, новейшие первыми |
GET |
/api/interview-prep |
Список сохранённых файлов Deep Research |
GET |
/api/interview-prep/:name |
Чтение одного файла Deep Research |
DELETE |
/api/interview-prep/:name |
Удаление файла Deep Research |
GET |
/api/output/pdfs |
Список сгенерированных PDF |
GET |
/api/output/pdfs/:name |
Стрим PDF как attachment |
POST |
/api/tracker |
Добавление строки в applications.md |
DELETE |
/api/jds/:name |
Удаление сохранённого JD |
POST |
/api/evaluate/test-gemini |
Smoke-проверка ключа Gemini API |
POST |
/api/mode/:slug |
Универсальный построитель промптов для 7 новых режимов (project / training / followup / batch / contacto / interview-prep / patterns) |
[1.6.0] — 2026-05-02
Первоначальный публичный релиз веб-UI. Инвентарь возможностей этого baseline — в README.md.