Журнал изменений
GitHubКаждое значимое изменение, релиз за релизом. Тот же файл публикуется в репозитории и переведён на все 19 языков.
[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/.
career-ops-ui v1.267.1 · page 9/13 · mirror of the project changelog for cvstart.org