Журнал змін

GitHub

Кожна значуща зміна, реліз за релізом. Той самий файл постачається в репозиторії та перекладений усіма 17 мовами.

Журнал змін

Усі важливі зміни career-ops-ui. Формат — Keep a Changelog, версії за SemVer.

Переклади: 🇬🇧 English · 🇪🇸 Español · 🇧🇷 Português · 🇰🇷 한국어 · 🇯🇵 日本語 · 🇷🇺 Русский · 🇨🇳 简体中文 · 🇹🇼 繁體中文 · 🇫🇷 Français · 🇵🇱 Polski · 🇩🇰 Dansk · 🇸🇦 العربية · 🇩🇪 Deutsch · 🇮🇹 Italiano · 🇹🇷 Türkçe · 🇮🇳 हिन्दी

Примітка щодо перекладу (v1.70.0) — українську додано як одну з трьох нових мов інтерфейсу. Цей файл перекладає найновіші записи; повна історія — в 🇬🇧 англійському CHANGELOG, який є нормативним джерелом.


[1.236.0] — 2026-09-18

Паритет із батьківським репозиторієм — career-ops main @ 6a9c84c (VERSION 1.33.0, 128 комітів). Два нові джерела та три дзеркальні виправлення — два з них тут тихо губили вакансії.

Додано

Два джерела сканера, 92 → 94 (89 EN + 5 RU). Python.org Jobs (pythonorg) читає публічну стрічку дошки; атрибуція роботодавця обов’язкова, тож рядок, чия назва не називає роботодавця, відкидається, а не добудовується назвою запису порталу. Generalist World (generalist-world) скрейпить картки дошки й гучно падає на розмітці, яку не може розпарсити — картка зі зміненою формою кидає виняток, а не читається як порожня дошка, а саме це і є той режим відмови, що робить скрейпнуте джерело ненадійним. Обидва з’являються у фільтрі Source на #/scan, обидва прив’язані до хоста (якірний regex, лише HTTPS), і обидва наскрізь керовані наборами тестів із фейковим транспортом (43 і 36 випадків).

Виправлено

Вакансію Workday, відкриту в кількох містах, відкидав фільтр локації. Список-ендпоінт Workday відповідає на багатолокаційну вакансію лічильником там, де кожна інша вакансія несе місце: "53 Locations". Це не локація, а buildLocationFilter зіставляє за регістронезалежним підрядком — тож вакансія, відкрита в Остіні й ще у 52 інших містах, не збігалася з жодним записом allow: [austin] і відкидалася, тоді як та сама вакансія, вказана поодиноко, проходила. Виміряно на батьківському боці на трьох орендарях: 53 із 291 вакансій були заглушками, тож це звичайний випадок, а не крайовий. Справжні місця вакансії існують лише в детальному документі CXS, тож кожна заглушка тепер коштує один додатковий GET до нього, з обмеженням у 200 на запис — понад ліміт заглушку лишають видимою, а не підробляють, бо невирішена вакансія ніколи не повинна виглядати як вирішена. Невдалий чи безлокаційний деталь лишає вакансію точно такою, якою її повернув список: видиме "53 Locations" чесно повідомляє про те, що не вдалося розв’язати, тоді як порожня локація мовчки пройшла б крізь будь-який фільтр. Той самий GET несе й startDate, тож збагачена вакансія датується точно, а не відносною прозою список-ендпоінта ("Posted 30+ Days Ago"undefined); усе, що не є голим YYYY-MM-DD, лишається без змін, а не проганяється через Date.parse, чия неISO-заглушка залежить від реалізації. Заявлений рівень більше не згортає дві вакансії в один повторний допис. "Insurance Specialist II" — це власне твердження роботодавця, що ця вакансія не є сестринською до іншої: інша оплатна вилка, обсяг і номер заявки під однією базовою назвою. Токенізатор цього не бачив: w.length > 3 відкидає кожну римську цифру аж до VIII і кожну однозначну цифру, тож "Insurance Specialist" і "Insurance Specialist II" токенізувалися однаково, отримували ідеальний коефіцієнт Жаккара, і детектор повторних дописів позначав другу як переоголошення першої. role-matcher.mjs у web-ui взагалі не мав обробки рівнів, тож це переносить увесь механізм батьківського репозиторію: рівні видобуваються зі згортанням римських і арабських цифр в одне число ("Nurse II" і "Nurse 2" — те саме твердження, і саме ця пара раніше хибно різнилася), два заявлені рівні мають збігатися, а рівень лише з одного боку трактується як вільний переказ, якщо тільки кожна назва не несе й слова, якого немає в іншій, — а це вже дві розбіжності, а не прикраса. Дошка Avature, прив’язана до фільтра, усе одно обходила всю глобальну дошку. UI пошуку Avature генерує непрозорі параметри фасетів (?42386=[812132] — це фільтр за країною), і оператор, який закріплює звужену дошку в portals.yml, очікує, що сканування обійде саме її. resolveSearch перебудовував URL як ${origin}${path} і повністю відкидав рядок запиту — тож кожен запит, включно з першим, обходив увесь борд орендаря: більше сторінок, більше запитів і вакансії, які оператор свідомо відфільтрував. Тепер фасети супроводжують кожен пагінований запит, а власний jobOffset застосовується останнім, щоб випадковий однойменний фасет ніколи не міг перехопити пагінацію.

Безпека

devalue 5.8.1 → 5.9.2 (транзитивно через astro), що закриває єдине відкрите сповіщення Dependabot — відмову в обслуговуванні на пошкодженому вводі. Залежність лише сайту; код рантайму не змінився.

Примітки

Не портовано, з причиною для кожного. Батьківський репозиторій дав lever тайм-аут 30с, а manfred — 25с, проти спільного усталеного 10с — це непорт: транспорт сканування web-ui вже дозволяє 60с (DEFAULT_SCAN_TIMEOUT_MS), тож обидві дошки мають тут більший запас, ніж дає те виправлення там. Подвоєний префікс careers- в iCIMS тут теж неможливий: web-ui бере origin орендаря прямо з api/careers_url і ніколи не будує хост з імені орендаря; новий обхід резервного хоста — це фіча для менш строгої конфігурації батьківського репозиторію, а не виправлення дефекту. deutschebahn і manfred отримали повторну спробу при тимчасовому обриві з’єднання, що при стелі web-ui у 60с значно менш нагально — занотовано, а не портовано, тож майбутній тайм-аут довгого обходу вже має перше, що варто спробувати. Усе інше у 128-комітному дельта-наборі — це CLI, верифікатор, дашборд або web/ (власний фронтенд батьківського репозиторію): liveness-browser, check-jd-archive, verify-ats/verify-portals/verify-cv-facts, update-system, set-status, audit-portals, discover-ats, scaffolder, test-all, маніфести плаґінів. Розбіжності форку повторно перевірено після pull: кириличне виправлення \p{L} у providers/telegram-channel.mjs, власний providers/telegram.mjs форку, hermes у web/src/lib/clis.ts. Апстрім career-ops-hq/main повністю змержено (0 пропущених комітів). 3066 → 3164 тести; довідка тепер вказує 94 адаптери (89 EN + 5 RU) в усіх 17 бандлах, а структура заголовків незмінна — 32 H2 / 122 H3.

[1.235.0] — 2026-09-16

Паритет із батьківським репозиторієм — career-ops main @ 68e6b94 (VERSION 1.33.0, 42 коміти). Два дзеркальні виправлення; одне з них тихо губило вакансії MokaHR тут.

Виправлено

Друга вакансія з тієї самої дошки MokaHR зникала замість того, щоб додаватися. url-key.mjs зводить URL оголошення до ключа порівняння і безумовно відкидав фрагмент — цілком розумно, адже майже кожен фрагмент існує лише для показу (#apply, #section-2). MokaHR — виняток у нашому власному реєстрі: її оголошення мають вигляд …/social-recruitment/{tenant}/{id}#/job/{n}, де всі вакансії одного орендаря поділяють один і той самий шлях, а ідентифікатор оголошення існує лише у фрагменті. Тож усі вони нормалізувалися до одного ключа. Виміряно до виправлення: вставте у конвеєр дві різні вакансії MokaHR — і отримаєте один рядок: без помилки, без попередження, нічого, що впадало б в очі; фільтр свіжості сканера (en-scanner.mjs) читає той самий ключ, тож щойно будь-яка вакансія цього орендаря потрапляла в історію сканування, кожну іншу відкидали як «вже бачену». Тепер normalizeUrl перетворює розпізнаний маршрут #/job/{id} чи #/jobs/{id} на ключ query-рядка, призначений лише для порівняння, перед тим як відкинути фрагмент — MokaHR зберігає своє усталене ім’я mokahr_job_id, щоб раніше записані ключі лишалися порівнюваними, а будь-який інший хост отримує внутрішнє _career_ops_fragment_job_id. Перенесений ідентифікатор додається, а не встановлюється: URL, що вже несе цей параметр, містить ідентифікатор іншого оголошення, і перезапис злив би саме ті дві речі, заради розділення яких і існує ключ. Кожен нерозпізнаний фрагмент і далі згортається — це та половина, яка й раніше була правильною. accept-encoding тепер зафіксовано в кожному запиті до джерела. Без явного значення Node узгоджує все, що пропонує його збірка undici; починаючи з Node ≥ 23 це включає zstd, а zstd-відповідь amazon.jobs повертається обрізаною рівно на 1024 байтах зі статусом 200 — тож збій виглядає як Unterminated string in JSON at position 1024, ніби пошкоджений API, а не проблема транспорту. Тепер кожен запит надсилає gzip, deflate, br. На наших рантаймах це запобіжний захід, а не виправлення: Node 18/20/22 — матриця CI та сервер (22.22.1) — надсилають gzip, deflate, що перевірено напряму, а не припущено, тож тут нічого не обрізалося; сьогодні змінюється те, що пропонується br, який ці версії не запитують за замовчуванням, але коректно розпаковують. Усталене значення застосовується регістронезалежно, і саме цю частину варто перевірити уважніше: імена заголовків регістронезалежні, а fetch об’єднує два однойменні записи в одне значення через кому, тож викликач, який передав би Accept-Encoding: identity, інакше отримав би надіслане gzip, deflate, br, identity.

Примітки

Заголовки лишаються звичайним об’єктом на межі fetchImpl, тоді як батьківський репозиторій передає fetch екземпляр Headers. Це свідома розбіжність, а не недогляд: fetch-хелпери web-ui приймають ін’єктований fetchImpl, і 45 наборів тестів джерел читають opts.headers['User-Agent'] із того, що вони перехоплюють — екземпляр Headers не підтримує доступ через дужки, тож достовірно портовано саме поведінку (регістронезалежне перевизначення, перемагає викликач), а не реалізацію. Об’єкт викликача копіюється, а не мутується, оскільки кілька джерел ділять одну спільну константу заголовків. Супутнє виправлення батьківського репозиторію — застосування усталеного user-agent регістронезалежно — це непортована зміна: web-ui взагалі не впроваджує усталений UA, кожне джерело встановлює той, якого вимагає його дошка, тож немає усталеного значення, з яким міг би зіткнутися User-Agent викликача. Два нові набори тестів і три нові випадки закріплюють цю пару: tests/http-accept-encoding.test.mjs (точне зафіксоване значення, кожен із чотирьох fetch-хелперів і перевизначення викликачем у трьох варіантах регістру — перевіряється, що виживає єдиний ключ accept-encoding, тоді як баг злиття породжував два) та tests/parsers-pipeline-hash-route.test.mjs, який працює зі швом, видимим користувачу, а не з ключем: обидві вакансії MokaHR мають дійти до конвеєра, повторна вставка тієї самої вакансії у чотирьох написаннях має і далі згортатися в один рядок, а косметичний фрагмент не повинен ставати ідентифікатором. Усі підтверджено як провальні до виправлення. Сама фіксація живе на рівні транспорту, а не лише в цих хелперах: 25 із 92 джерел викликають упроваджений fetchImpl напряму й через хелпери взагалі не проходять, тож її застосовує makeTimeoutFetch — обгортка, яку обидва сканери справді впроваджують, і єдина точка проходу, спільна для всіх джерел. (Батьківському проєкту еквівалент не потрібен: providers/_http.mjs — його єдиний транспорт.) Структурна перевірка тепер валить CI, якщо нове джерело викликає fetch напряму замість переданого йому транспорту, а поведінковий кейс проганяє greenhouse — одне з тих 25 — через обгортку й доводить, що заголовок доходить до дроту. Не портовано, із причиною: liveness-browser.mjs (запобіжник повторного використання headed-Playwright — web-ui не керує headed-браузером), check-jd-archive.mjs (у web-ui немає такої ретрансляції), test-all.mjs (харнес батьківського репозиторію; розбіжність форку vpFixtureEnv лишається незмінною), web/ (власний фронтенд батьківського репозиторію, не дзеркалиться), маніфести плаґінів і SIGNATURES.md (існують лише в репозиторії). batch/batch-runner.sh (амперсанди екрановано перед підстановкою в sed) і jd-skill-gap.mjs (резолвить cv.md відносно кореня даних) — ретранслюються лише для читання: web-ui звертається через shell до копій батьківського репозиторію, тож обидва потрапляють сюди без жодної зміни коду, а зміна в jd-skill-gap робить ретрансляцію /api/jds/:id/skill-gap коректнішою: cv.md тепер резолвиться від кореня даних, а не від того каталогу, звідки був запущений процес. Розбіжності форку повторно перевірено після pull: кириличне виправлення \p{L} у providers/telegram-channel.mjs, власний providers/telegram.mjs форку, hermes у web/src/lib/clis.ts. Кількість джерел не змінилася — 92 (87 EN + 5 RU), тож довідка лишилася незмінною. 3042 → 3066 тести.

[1.234.0] — 2026-09-15

Паритет із батьківським репозиторієм — career-ops main @ 56cce8f (VERSION усе ще 1.32.0, 58 комітів після 1.32.0). Одне дзеркальне виправлення в 15 джерелах, а також запобіжник, щоб воно не могло повернутися.

Виправлено

Один пошкоджений ідентифікатор вакансії більше не спустошує всю сторінку. encodeURIComponent кидає URIError на самотньому UTF-16-сурогаті, а JSON-payload може його нести — JSON.parse('"\\uD800"') зберігає його. П’ятнадцять джерел формували URL кожного оголошення з ідентифікатора чи slug, контрольованого хостом, усередині циклу парсингу, тож одне погане оголошення переривало цикл і губило всі вакансії на цій сторінці; нічого не потрапляло в лог як збій джерела, бо виняток розгортався ще до того, як catch по кожній компанії встигав щось повідомити. Батьківський репозиторій виправив це в providers/_safe-url.mjs (safeEncodeURIComponentnull при URIError, усе інше прокидається далі) і провів через нього ідентифікатор кожного циклу, відкидаючи лише одне оголошення при null. Цей хелпер тепер — server/lib/sources/_safe-url.mjs, і та сама зміна застосована до тринадцяти дзеркальних джерел — alibaba, arbeitsagentur, bamboohr, feishu-jobs, garena, jibeapply, manfred, meituan, mokahr, phenom, thehub, tkms, vdab — і до двох джерел, наявних лише у web-ui, які новий запобіжник виявив за першого ж запуску, jobstreet і trudvsem, — вони мали ту саму форму циклу й той самий прихований кидок винятку. Обсяг обмежено свідомо: лише ідентифікатори з API проходять через хелпер. Значення, похідні від конфігурації, зберігають строге кодування й падають гучно — office у garena (сегмент portals.yml) лишається на кидаючому urlSegment, тоді як її id з API отримує idUrlSegment, що повертає null; corpName у csod і перевірений через SLUG_RE slug у 4dayweek занесені в allowlist поіменно, з поясненням причини. Резервний варіант заголовка в rheinmetall міг кидати виняток на пошкодженому href. Коли картка не має заголовка, він відновлюється зі slug у URL через decodeURIComponent, яка кидає виняток на поганій відсоток-послідовності у зібраному зі сторінки href — та сама форма «один поганий рядок валить усю сторінку», тільки на боці декодування. Тепер декодування відбувається захищено, а при невдачі використовується сирий slug. Реєстр джерел пропускає файли з префіксом _. Автовиявлення імпортувало кожен .mjs у server/lib/sources/, крім самого себе, і попереджало про будь-що без export const meta — тож хелпер породжував попередження під час запуску. Прийнято конвенцію батьківського репозиторію: провідне підкреслення означає не джерело; такі файли не імпортуються, і про них не попереджають. Незадекларований файл без meta й далі викликає попередження, а тест виявлення тепер перевіряє обидві половини (підтверджено як червоний на старому фільтрі). Скрипт збірки сайту sync-assets.mjs рахує файли адаптерів на диску за тим самим правилом, тож хелпер більше не спрацьовує на його захист від тихої втрати джерела.

Примітки

tests/sources-url-encoding-surrogate.test.mjs дзеркалить набір батьківського репозиторію у трьох частинах: контракт хелпера (парний сурогат — звичайний емодзі — і далі кодується; кидок винятку із власного toString значення — це вина викликача, і він поширюється далі), поведінку кожного з п’ятнадцяти джерел (один поганий ідентифікатор + один чистий → без винятку, чистий лишається, поганий відкидається, кожен прогнаний через чистий парсер, який експортує джерело, або, для thehub, через його fetcher із фейковим транспортом), і запобіжник на рівні джерела: кожен конвертований файл повинен імпортувати хелпер і викликати його, жоден файл не може посилатися на нього без імпорту, і жоден непереглянутий файл не може будувати URL вакансії голим encodeURIComponent — нове джерело зі старою формою провалює CI з номером рядка-порушника. Усі п’ятнадцять окремих для кожного джерела випадків підтверджено як провальні до порту. Усе інше з батьківської дельти не портовано, з причиною: batch/batch-runner.sh --cli — це прапорець лише для CLI (web-ui не звертається до batch-runner через shell); invite-match.mjs тепер резолвить set-status.mjs з кореня коду — web-ui це не ретранслює; company-funded.mjs отримав ту саму зміну safe-url усередині, а форма його виводу --json, яку парсить /api/company-funded, не змінилася; test-all.mjs лише переніс бюджет таймауту (розбіжність форку vpFixtureEnv лишається незмінною); web/ — це власний фронтенд батьківського репозиторію, і він не дзеркалиться; AGENTS.md/SIGNATURES.md існують лише в репозиторії. Розбіжності форку повторно перевірено після pull: кириличне виправлення \p{L} у providers/telegram-channel.mjs, власний providers/telegram.mjs форку, hermes у web/src/lib/clis.ts. Кількість джерел не змінилася — 92 (87 EN + 5 RU), тож довідка лишилася незмінною. 3022 → 3042 тести.

[1.233.2] — 2026-09-10

Виправлено — дві знахідки CodeQL js/remote-property-injection.

Виправлено

Цикл, що переносить збережені значення в process.env, обходив Object.entries(safe) — об’єкт, зібраний із тіла запиту — із захистом від прототипних ключів усередині. Код був безпечним, бо safe наповнюється лише з KNOWN_KEYS, але аналізатор не може перенести це обмеження через два цикли. Тепер цикл іде прямо константним масивом, тож ім’я властивості доказово є літералом модуля. Поведінка ідентична.

Примітки

Тест, що закріплював стару перевірку, був грепом по тексту вихідника. Замінено структурним і поведінковим — другий шле __proto__ сирим JSON, бо літерал об’єкта задав би прототип, а не властивість. 3021 → 3022 тести.

[1.233.1] — 2026-09-10

Виправлено — посібник не описував елемент, доданий у v1.233.0, а той самий розділ ніс застарілі вказівки.

Виправлено

DOC-1 — пункт «Використовувати усталене (…)» виражає стан «ключ не задано», і відмінність від явного вибору того самого значення невидима та істотна: перше нічого не пише й далі віддає усталене проєкту, друге закріплює значення в .env. Три пункти додано до наявного списку ### Behavior в усіх 17 бандлах. DOC-2 — рядок таблиці не називав ключа: у колонці стояло (server uses default UA), що описувало HH_USER_AGENT, вилучений у v1.19.0, і радило реєструватися на dev.hh.ru — марно з v1.65.0. Усі три застарілі згадки прибрано; рядок troubleshooting переписано, бо частина його досі правдива: з липня 2026 hh.ru віддає HTTP 451 не-російським адресам. Таблиця ключів перелічує 6 із 45, тоді як заголовок обіцяє реєстр. Одна фраза ×17 називає її вибіркою й вказує на #/config.

Примітки

Новий гейт help-recognized-keys.test.mjs звіряє першу колонку ключових таблиць із живим KNOWN_KEYS. 3018 → 3021 тестів; довідка лишається 32 H2 / 122 H3.

[1.233.0] — 2026-09-10

Виправлено — три знахідки в шляху збереження #/config; CONFIG-3 породжений виправленням v1.232.1.

Виправлено

CONFIG-3 — свідомий вибір, що збігся із засіяним значенням, відкидався як «не чіпали». initial змішує два походження: засів із .env (рівність означає, що поле не редагували) і засів із defaultValue (рівність неоднозначна). Виправлено прапорцем seededFromFile: там, де засів із файлу, нічого не змінюється; там, де з умовчання, другою ознакою стає dirty. Зняття ключа показувалося як «· 0» — сервер повертав лише записане. Тепер POST /api/config віддає ще й removed, а тост рахує обидва. Select не можна було очистити — жоден із 18 не мав порожнього пункту. Тепер кожен починається з явного «Використовувати усталене (…)», і незаданий ключ обирає саме його. Один новий ключ i18n ×17.

Примітки

База юніт-тестів незмінна — 3018; браузерних 111 → 116, усі п’ять нових підтверджено падали на старому коді.

[1.232.1] — 2026-09-10

Виправлено — збереження на #/config записувало поля, яких користувач не торкався.

Виправлено

CONFIG-2. v1.57.1 засіває незадані контроли їхнім defaultValue, щоб поле показувало те, що сервер справді використовує, а save() питав dirty лише для секретів і надсилав решту полів завжди. Тож значення, підставлене для ПОКАЗУ, ішло на сервер як вибір користувача: зміна одного дропдауна надсилала 28 ключів і закріплювала 18. Налаштування переходило з «не задане — слідуй умовчанню проєкту» в «закріплене в .env», і наступна зміна умовчання мовчки не дійшла б до тих, хто хоч раз натиснув Save.

Примітки

Порівняння з cfg.values недостатньо: незаданий select показує свій defaultValue, а збережене значення порожнє — вони чесно різняться. Тепер порівнюється з тим, чим контрол було проініціалізовано. Також число записаних ключів знову видно в тості. 105 → 111 браузерних тестів.

[1.232.0] — 2026-09-09

Виправлено — два дефекти, знайдені браузерним прогоном; жоден із них не походить із v1.231.5.

Виправлено

PROFILE-1 — #/profile прокручував убік усю сторінку. Значення поля було безіменним <div> лише з інлайновими шрифтовими стилями, тож overflow-wrap лишався normal. URL LinkedIn не дає CSS жодної допустимої точки переносу, отже min-content дорівнює всьому рядку, а min-width: auto у grid-елементів не дозволяє стиснутися нижче — картка розпирала колонку, рядок і врешті документ (23 px реального скролу на 1280 px). Виправлено спільним класом .card-value з overflow-wrap: anywhere та підстраховкою .card-row > * { min-width: 0 }. Саме anywhere, а не break-word: лише anywhere враховується під час обчислення min-content. CONFIG-1 — POST /api/config приймав будь-яке значення для select-поля. {"LLM_PROVIDER":"not-a-provider"} повертав 200 і записувався в .env, після чого резолвер мовчки йшов у фолбек. LLM_PROVIDERS увесь час експортувався з env-config.mjs — валідатор просто до нього не звертався.

Примітки

Перевіряється лише LLM_PROVIDER: справжній домен списків моделей належить вендорові й змінюється між нашими релізами. Уже збережене значення отримує поблажку — форма надсилає всі несекретні поля щоразу, тож сувора перевірка заморозила б сторінку. Також додано дескриптор OLLAMA_API_KEY. 3013 → 3018 тестів.

[1.231.5] — 2026-09-09

Безпека — закрито сім сповіщень, одне з них — критичне віддалене виконання коду. Жоден файл джерел не змінено.

Безпека

Змержено з Dependabot: multer 2.2.0 → 2.3.0 (high — відмова в обслуговуванні через витік файлових дескрипторів при обриві) і svgo 4.0.2 → 4.1.0 у сайті (high і medium — removeScripts пропускав виконувані посилання). Потім ще п’ять, які виявило пересканування. Кожен зачеплений діапазон уже допускався наявним ^, тож потрібне було оновлення, а не правка маніфесту: astro 7.1.0 → 7.3.2критично, віддалене виконання коду через оптимізацію зображень AVIF (виправлено в 7.2.8) та обхід авторизації (7.2.4); sharp 0.35.3 → 0.35.4high, вразливості libheif; js-yaml 4.3.1 → 4.3.2 в обох маніфестах — high.

Примітки

«Лише залежності» тут не дорівнює «без ризику». js-yaml розбирає кожен конфіг, multer приймає кожне завантаження, а Astro змістився на мінорну версію. Перевірено: test:ci 3013/3013, браузерний набір 4/4, повна пересборка сайту — ті самі 86 сторінок і всі 17 дзеркал. npm audit: 0 вразливостей в обох маніфестах.

[1.231.4] — 2026-09-07

Виправлено — v1.231.3 полагодив наслідок кліку по Doctor, а не сам момент. Повідомлено знову, з телефона і з десктопа.

Виправлено

withSpinner усе ще переписував кнопку на час запиту. v1.231.3 навчив його зберігати вузли-дітей і повертати їх у finally — і вони справді поверталися, але індикатор зайнятості й далі виконував replaceChildren з текстом '⏳ ' + original. Тому на весь час роботи doctor.mjs квадрат 36 px тримав текст ⏳ 🩺Doctor і ставав широкою «пігулкою», що ламала рядок. Відновлення дітей лагодило наслідок, а не момент. Кнопка, що має дітей-елементів, тепер не переписується взагалі: її індикатор — клас .is-loading, який тепер підмінює піктограму пісковим годинником засобами CSS, тож геометрія змінитися не може: 36 px у спокої, 36 px під час, 36 px після.

Примітки

Урок про перевірку, а не про код. Гейт v1.231.3 чекав зняття aria-busy і лише тоді мірив — тобто бачив лише те, що finally вже виправив. Нова браузерна перевірка сама тримає проміс відкритим і знімає замір усередині польоту. 3012 → 3013 тестів.

[1.231.3] — 2026-09-07

Виправлено — клік по Doctor ламав верхню панель на телефоні. Повідомлено з телефона в день виходу v1.231.2.

Виправлено

UI.withSpinner знищував дітей тієї кнопки, яку оформлював. Індикатор зайнятості працював через textContent: зберегти button.textContent, записати '⏳ ' + original, записати назад. Присвоєння textContent замінює всіх дітей одним текстовим вузлом — а v1.231.2 якраз розділив кожну кнопку верхньої панелі на .btn-ico і .btn-label, сховавши підпис під max-width: 900px. Тому перший же дотик до Doctor знищував обидва <span> назавжди: мобільним правилам не стало чого зіставляти, підпис повертався голим текстом, а квадрат 36 px розростався в «пігулку» 🩺Doctor. Само не відновлювалося — лише перезавантаження сторінки повертало розмітку. Виправлення зберігає вузли-дітей і повертає їх через replaceChildren; кнопки з одним текстом, а це більшість із 24 місць виклику, поводяться точно як раніше.

  • FIND-3 — поле пошуку схлопувалося до 8 px на екрані 320 px. Звівши панель до одного рядка, я лишив searchbar єдиним гнучким елементом — і минулий випуск прямо це записав: він «може стиснутися до нуля». Він і стиснувся: 8 px вводу на 320 px, 28 px на 340 px — один символ із двадцяти одного в підказці. Жоден min-width тут не зарадить: на 320 px панель тримає 32 px відступів, 40 px меню і 162 px дій. Тому нижче 420 px поле ховається за лупу і по дотику розгортається на всю панель: 182 px на 320 px, 222 px на 360 px, 252 px на 390 px, без переповнення. Кнопка-розкриття зберігає одне доступне ім’я і повідомляє стан через aria-expanded; заміна 🔍/✕ зроблена чистим CSS. Вище 420 px не змінюється нічого. Знайдено браузерним регресом по живій збірці.

Примітки

Це той самий клас дефекту, що й пастка з applyI18n(), зазначена у v1.231.2 — присвоєння textContent елементу, у якого тепер є діти-елементи, — але через іншого викликача. Урок узагальнюється: розділивши вміст елемента на дочірні <span>, ви перетворюєте кожного наявного писача textContent по цьому елементу на прихований баг. Закрито двома замками — node:vm над справжнім withSpinner і клік у справжньому браузері на 320 px. Обидва підтверджено падали на старому коді. 3009 → 3012 тестів.

[1.231.2] — 2026-09-06

Виправлено — мобільна верхня панель переносилася на другий рядок, сервер працював на старому батьківському проєкті, а редирект cvstart.ru давав 404 на будь-якому шляху, де вже вказано мову.

Виправлено

  • Мобільна панель знову в один рядок, кнопки — піктограмами. Кнопки дій примусово йшли на окремий рядок на всю ширину. Тепер «Діагностика» і «Відкрити Scan» — квадрати 36 px поруч із дзвіночком і перемикачем теми, і все вміщується в один рядок при 320 px. Виміряно: ширина документа дорівнює вьюпорту при 320, 360, 390 і 430 px та на смузі 560–760 px. Локаль більше не впливає на мобільну розкладку, бо підпис прихований; вище 900 px підписи повертаються.
  • Сервер працював на батьківському 1.31.0, хоча реліз зібрано під 1.32.0. Викладено 95 файлів рантайму, два видалено. Знайдено браузерним регресом на живій поверхні.
  • cvstart.ru приклеював /ru/ до будь-якого шляху. /ru/help ставав /ru/ru/help, /en/help/ru/en/help; обидва 404. Тепер шлях, що вже називає одну з 16 локалей, проходить як є, en відкидається (англійська живе в корені), решта отримує /ru/.

Змінено

  • 98 застарілих QA-промптів перенесено до qa/archive/superseded-prompts/ — правило записане у qa/README.md з v1.137.0 і не застосовувалося 95 релізів.
  • З верхнього рівня пішли ще п’ять промптів — вони називали себе актуальними, не будучи ними. REGRESSION-FINAL.md оголошував себе «єдиним авторитетним, версійно-незалежним регресійним промптом», але його §§11–15 — п’ять закритих журналів циклів (v1.55.x → v1.59.7), а заявлена база хелпу — 28 H2 / 103 H3 на 16 локалей проти 32 / 122 і 17 сьогодні. У QA-REGRESSION-PROMPT.md ім’я без версії, а вміст від v1.226.0. QA-REGRESSION-PROMPT-v1.76.0-FULL.md закріплений на 155 релізів назад, REGRESSION-PROMPT-FINAL.md закриває цикл v1.58.52 → v1.59.10, а QA-FULL-REGRESSION.md дублював їх усіх на базі v1.131.1. Туди ж пішов PARENT-SYNC-WORKLIST-v1.26.0.md, а датований UX-AUDIT-2026-07-06.md переїхав до reports/. Лишилося чотири драйвери, чиї бази оновлено до фактичних значень v1.231.2. qa/README.md переписано під ту теку, яка існує.

Примітки

  • Дві пастки специфічності описано в app.css: десктопні правила оголошені нижче блоку max-width: 900px і при рівній специфічності виграють.
  • applyI18n() присвоює el.textContent, тож ключ i18n переїхав на внутрішній <span>.
  • Перший rsync батьківського проєкту обірвався на півдорозі й відрапортував успіх — тепер перевіряється контрольними сумами.

[1.231.1] — 2026-09-06

Виправлено — свіп лічильників із v1.231.0 переписав імʼя акаунта в усіх посиланнях.

Виправлено

  • Кожне посилання Fighter90 стало Fighter92. Свіп лічильника джерел у v1.231.0 замінював 90 на 92 під захистом цифрової межі, яка правильно пропускає 290 і 901, але збігається з 90 всередині Fighter90. 562 посилання в усіх 17 README, усіх 17 наборах довідки та дзеркалах довідки на сайті, плюс 138 у вікі, вели на неіснуючий акаунт. Помітив користувач — за зниклою картинкою.
  • Пʼять історичних згадок релізів зсунулися. Свіп 85 → 87 перетворив v1.85.0 на v1.87.0, вигадавши версію, якої не випускали.

Примітки

  • Цифрової межі замало: вона захищає число від сусідніх цифр, але не від того, що воно частина ідентифікатора чи номера версії. Самі лічильники правильні: 92 джерела, 87 EN-адаптерів, 3009 тестів.
  • Батьківський репозиторій не постраждав.

[1.231.0] — 2026-09-05

Додано — два джерела сканування з батьківського career-ops v1.32.0 (Collage, Telegram суворий), режим REST у Gem і один дефект, який порт виявив у коді самого батька.

Додано

  • Два нові джерела, 90 → 92 (87 EN + 5 RU), ALL_ADAPTERS 85 → 87. Обидва без токенів.
    • Collage (provider: collage) — публічний API джоб-сайтів Collage HR. Сегмент шляху — це адреса джоб-сайту, яку орендар обрав сам, а не слаг із назви компанії, тож він читається з api: або з careers-адреси secure.collage.co/jobs/<адреса> і ніколи не вгадується: вгадана адреса призвела б до сканування чужої дошки.
    • Telegram (суворий) (provider: telegram-channel) — читає ті самі попередні перегляди t.me/s/<канал>, що й наявне джерело telegram, але з протилежним розміном: допис стає рядком лише якщо називає роботодавця Й посилається на сторінку вакансії. Дайджести, дописи з прихованим роботодавцем і дописи «пишіть у личку» відкидаються. Низька повнота за задумом — вище за течією виміряно 32–77 % дописів, що проходять, — але кожен рядок несе справжнього роботодавця і справжнє посилання. Обидва лишаються, як і в батька.
  • Gem отримав необовʼязковий режим REST поряд зі шляхом GraphQL (api: https://api.gem.com/job_board/v0/<board>/job_posts). Він ніколи не виводиться з ідентифікатора дошки, а шлях GraphQL лишається типовим. Перенесено з батьківського #3783.

Виправлено

  • Фільтр локацій у telegram-channel не бачив кирилиці й називав міста роботодавцями. У батьківському LOCATIONISH_RE стояла \b — межа лише за ASCII, поряд із кирилицею вона не спрацьовує ніколи. Тож усі російські варіанти в ньому були недосяжні, і Senior Engineer | Москва повертав «Москва» як роботодавця. Це саме та підміна неправильного поля, заради запобігання якій уся політика атрибуції й написана. У порті web-ui використано юнікодні перевірки — те саме, що батько вже робить рядком нижче в ROLE_WORD_RE. Закріплено тестом, який падає на ASCII-формі. Вище за течією ще не виправлено.

Примітки

  • Робота батька з відновлення Workday через фасети (#3851, #3874) переносу не потребувала — web-ui взагалі не гортає Workday, один POST при offset: 0.
  • providers/_types.js змінився лише в документації.
  • Тести: 2962 → 3009 (+47).

[1.230.0] — 2026-09-05

Змінено — три виправлення провайдерів із батьківського career-ops v1.32.0 і один дефект, який виявив сам перенос.

Додано

  • Welcome to the Jungle приймає серверний вираз filters для Algolia (wttj: { filters: "offices.country_code:FR AND contract_type:full_time" }). Саме лише ключове слово глобальну дошку не звужує: які саме max_hits результатів повернуться, вирішує ранжування Algolia, а не фільтри сканера — вони спрацьовують пізніше, вже по тому, що прийшло. Фільтр скорочує сам набір, а не переупорядковує його, тож ліміт на запит зростає з 200 до 1000. За наявності фільтра queries стає необов’язковим: порожній запит означає «усе, що проходить фільтр». Перенесено з батьківського #3757.

Виправлено

  • Radancy більше не пише «truncated», коли орендар просто має менше вакансій, ніж заявляє його ж банер. Батьківський проєкт звірив data-total-results із дев’ятьма живими орендарями: п’ять збіглися, а чотири — і маленький, і дуже великий — віддали на 10–56 % менше унікальних вакансій, ніж оголошено, без жодного дубля. Банер завищує те, що насправді віддає пошуковий індекс орендаря, тож порівняння з накопиченим лічильником хибно спрацьовувало на більшості дощок. Тепер попередження виникає лише коли walk зупинив наш власний max_jobs/max_pages; totalResults лишається в тексті як контекст і нічого не вирішує. Перенесено з батьківського #3839.
  • Radancy обходить кеш на кожному JSON-запиті фрагмента. Кешувальний шар перед цим маршрутом у частини орендарів віддає застарілі відповіді: та сама вакансія з’являється сторінками пізніше, а інша не приходить зовсім — відтворено вище за течією дев’ять прогонів із дев’яти. Заголовки Cache-Control/Pragma не змінили нічого, випадковий параметр на запит — виправив. Він мусить бути унікальним на виклик, а не виводитися з номера сторінки. Перенесено з батьківського #3839.
  • Списки офісів Greenhouse впорядковано детерміновано. Greenhouse не обіцяє порядок offices[] між відповідями, а набір, відновлений із дерева /offices, іде в порядку обходу. Обидва потрапляють у поле location, тож перестановка офісів переписувала цей рядок — і вакансія без змін читалася як нова. Батьківський проєкт виправив набір (#3839); у web-ui знайшовся другий екземпляр того самого дефекту, якого в батька немає — запасний варіант offices[0], — і відсортовано обидва.

Примітки

  • Виправлення iCIMS (#3728) переносити не довелося. Воно читає всі записи jobLocation із JSON-LD сторінки вакансії; web-ui бере локацію з HTML сторінки списку і сторінку вакансії не відкриває — такого шляху коду тут немає.
  • Зміна _http.mjs переносу не потребувала. Вона виносить isRefusedRedirectError заради другого споживача, якого в web-ui немає; поведінка isRetryableError не змінюється.
  • Дедуплікація за локацією (#3751) не дзеркалиться — вона в конвеєрі scan.mjs CLI батьківського проєкту.
  • Реєстр без змін: 90 джерел (85 EN + 5 RU). Тести: 2956 → 2962 (+6).

[1.229.0] — 2026-09-03

Додано — чотири джерела сканування з батьківського career-ops v1.31.0: Built In, Feishu Jobs, Garena і MokaHR.

Додано

  • Чотири нові джерела, 86 → 90 (85 EN + 5 RU), ALL_ADAPTERS 81 → 85, усі без токенів. Built In охоплює дев’ять ринків американської технічної дошки й не несе типового запиту: запис без queries: та categories: не сканує нічого і прямо про це каже, бо спільне джерело не має нести чиїсь пошукові слова; ринок поза списком відхиляється, а не підміняється тихцем національною дошкою. Feishu Jobs приймає рівно дві форми хоста, і вони мають різні шляхи до сторінки вакансії. Garena — одна компанія: office формує посилання, а не список. MokaHR повертає зашифровану відповідь (конверт AES-128-CBC, ключ поруч) — це обфускація, не захист, але без розшифрування нічого не прочитати.

Виправлено

  • htmlToText міг пропустити в текст незакритий відкривальний тег. safe &lt;img src=x onerror=1 виходило як safe <img src=x onerror=1. Тепер за кожним декодуванням іде очищення, а кінцевий відкривач втрачає кутову дужку. Портовано від батьківського проєкту (#3491).

Примітки

  • Із виправлення Workday не портовано нічого. Воно рятує тенантів, у яких бекенд зупиняє пагінацію на offset 2000 — web-ui Workday взагалі не пагінує (один POST на offset: 0, 100 рядків).
  • Виправлення verify-cv-facts і merge-tracker порту не потребували: web-ui запускає ці скрипти з чекауту батьківського проєкту.
  • Тести: 2909 → 2956 (+47).

[1.228.5] — 2026-08-30

Виправлено — повідомлена версія описувала файл на диску, а не виконуваний код.

Виправлено

  • /api/health перечитував package.json на кожен запит, тож застарілий процес повідомляв версію, якої не виконував. QA знайшов локальний інстанс, що відповідав version: 1.228.4 і водночас давав 404 на /api/ping — маршрут, доданий у 1.228.3: запущений до розгортання, обслуговував старий код і повідомляв версію нового файлу. Саме так виглядає успішним розгортання, яке скопіювало файли й ніколи не перезапустило процес: єдиний рядок, що його перевіряє оператор, — той, який не здатен побачити проблему. Той самий клас, що й символьне посилання з v1.228.1, де rsync уривався посеред передачі й усе одно звітував про успіх.

    Версія тепер фіксується під час завантаження модуля й описує виконуваний код. Застарілий процес повідомить стару версію — правду, видну одразу. parentVersion лишається почитаним на запит: він описує чекаут батьківського проєкту, який цей процес не завантажує і який законно змінюється під працюючим сервером.

[1.228.4] — 2026-08-30

Виправлено — неавтентифікована проба живості робила дискову роботу на кожен запит.

Виправлено

  • GET /api/ping читав і розбирав package.json на кожне звернення. Це єдина точка, досяжна без облікових даних, тож читання з диска плюс розбір JSON на запит — це важіль відмови в обслуговуванні, вручений будь-кому; CodeQL позначив це як брак обмеження частоти. Версія тепер читається один раз під час реєстрації маршруту; без перезапуску вона й так не зміниться, тож обробник не робить введення-виведення взагалі. Обмеження частоти вкоротило б шкоду; прибирання роботи прибирає важіль.
  • Маскування справжнього імені власника профілю поза loopback тепер закріплено тестом. /api/health уже замінював його на hidden — але лише тому, що цей рядок ділить охоронця hidden ?? value з коренем проєкту. Ніщо цього не перевіряло: правка, яка дала б рядку власне значення, злила б ім’я реальної людини в локальну мережу непоміченою.

[1.228.3] — 2026-08-30

Fixed — the docs assistant could not answer in English, Telegram company names arrived truncated, and a monitor had no way to see the service was up.

Fixed

  • The docs assistant answered “no matching help sections were found” in English while the same question worked in Russian. Retrieval was fine; the context builder was not. buildAskPrompt did if (ctx.length + sec.body.length > MAX_CONTEXT) break — a break, so one oversized section ended the loop and left the context empty, and the assistant truthfully reported it had nothing. The English §5 had grown to 16 081 bytes against a 14 336-byte budget (partly because v1.228.0 added telegram_channels to it); the Russian §5 was 13 009 and fit. Same ranking in both languages — §5 was first — only the size differed.

    Two changes: a section past 6 KB is now split at its ### boundaries, so telegram_channels is its own retrievable chunk (1 340 bytes) that ranks first in both languages; and an oversized section is skipped or truncated rather than ending the loop. The guide went from 32 chunks to 75.

  • A Telegram company label yielded the sentence after it, not the name. Компания: FinCore Technology (продуктовая разработка, высоконагруженные системы) became FinCore Technology (продуктовая разработка, высоконагруженны — capped at 60 characters, cut mid-word. Three of 141 rows. The name now ends at the first separator that introduces a description — a spaced dash (so Coca-Cola survives), a comma, semicolon, pipe or opening parenthesis — and any remaining cap falls on a word boundary. This source attributes a company as fact; a truncated sentence is as wrong as a guessed name, only less obviously so.

Added

  • GET /api/ping — a liveness probe that leaks nothing. { ok, version } and no more. /api/health reports absolute paths, the profile owner’s real name and which API keys are set: legitimate for an authenticated operator, never for the public internet. A monitor behind a Basic-auth gate previously had no way to see the service was up at all.

[1.228.2] — 2026-08-30

Fixed — a two-word search in /api/scan-results matched the phrase, not the words, and under-reported badly.

Fixed

  • ?q= now matches every term independently instead of the literal phrase. The query was a single includes(q) over title + company + location, so продакт менеджер found only rows carrying those two words in that exact adjacency — 32 of the 162 that carry both. Менеджер продукта and Менеджер по развитию продукта were invisible to it: the same words, reordered or with filler between them.

    This is how a narrow match becomes a wrong answer. The Telegram assistant asked for all Продуктовый менеджер reported 9 — faithfully, because 9 was what the endpoint said. The count it quoted was honest; the endpoint’s was not. Word order and the words between are not something a person typing two words intended to pin.

    Matching is AND across terms, so widening precision costs nothing: a row must still carry every term. менеджер разработчик returns zero rather than the union.

[1.228.1] — 2026-08-29

Fixed — a dangling symlink in the repo that silently truncated deploys.

Fixed

  • .claude/skills/refero-design was a tracked symlink to a path that has never existed here. It pointed at ../../.agents/skills/refero-design, which resolves to .agents/ at the repo root; there is no such directory, and the link arrived in the v1.118.0 parity pack from a machine where the surrounding folders nested differently. Anyone cloning the repo has been getting a broken link ever since.

    It was not inert. rsync aborts on it mid-transfer — a deploy today reported success while the server kept running the previous version, and the mismatch only surfaced because the version was checked afterwards. The parent project’s node test-all.mjs also crashes outright when a web-ui checkout sits inside its tree, because its fixture copy walks the working directory and cannot stat the link. Nothing referenced it: the only refero matches in the repo are the Polish word preferowany.

[1.228.0] — 2026-08-29

Додано — Telegram-канали як джерело сканування та посторінковий режим запиту в /api/scan-results.

Додано

  • Telegram-канали стали скановним джерелом. Блок telegram_channels: у portals.yml їх перелічує; кожен читається з публічного вебпрев’ю https://t.me/s/<канал>. Реєстр: 85 → 86 джерел (81 EN + 5 RU), ALL_ADAPTERS 80 → 81. Telegram не публікує RSS, а Bot API не читає канал, яким бот не адмініструє — обидва очевидні шляхи закриті. Прев’ю /s/ — звичайний HTML із сервера: без входу, без JS, без кук. Перевірено на 16 каналах: 15 віддали по 20 дописів на голий GET.
  • GET /api/scan-results отримав посторінковий режим запиту. ?q=<текст>&limit=50[&region=ru|en][&set=filtered|fresh][&offset=N] повертає { total, returned, offset, limit, set, rows }, де total — кількість ДО пагінації. Без параметрів маршрут і далі віддає знімок цілком — таблиця #/scan залежить від цієї форми.

Виправлено

  • Telegram-асистент відповідав із частки даних. Його навичка вже вказувала на /api/scan-results, але маршрут умів віддати лише весь знімок — близько 2 МБ — що не вміщується в контекст агента. На «знайди всіх Продуктових менеджерів» він повідомив 9 збігів там, де у знімку їх були сотні, і сказав, що «працює з обмеженим локальним індексом». Це була правда: ендпоінт він бачив, а спожити не міг. З посторінковим режимом те саме питання коштує 2 КБ і повертає чесний total. Навичку теж переписано: вона просила підсумувати нові дописи, тобто вказувала на fresh замість filtered.

Примітки

  • Допис каналу — це проза, а не запис про вакансію. Структурованих полів немає: заголовок із першого змістовного рядка, компанія/місто/вилка — з явних міток. Відділяти справжні вакансії від реклами й дайджестів залишено наявному title_filter — тому самому, що вже налаштований для інших джерел. Це підказки, а не список.
  • Компанію ніколи не вгадують. Без мітки рядок приписується каналу (@rabotaphp): хибний роботодавець потрапляє до трекера як факт.
  • Порожній розбір — це помилка, а не «вакансій немає». На закритий чи неіснуючий канал t.me відповідає перенаправленням; повернути нуль рядків читалося б як спокійний день і назавжди сховало б одрук у конфігу. jobGeeks робить саме це (HTTP 302) і навмисно не налаштований.
  • Визначення віддаленої роботи використовує межу слова Unicode замість \b, який працює лише з ASCII — та сама пастка, що й у фільтрі заголовків у v1.227.3, тут спіймана тестом до релізу.
  • 15 каналів налаштовано та перевірено наживо: 299 дописів, 15/15 каналів. 30 нових тестів. Тести: 2865 → 2895.
  • The README test counts jumped by hundreds, and no, this release did not add them. The localized READMEs carried unit-test totals frozen at 1856 / 1945 / 1955 / 549 / 419 / 284 / 2527 depending on the file — each stale since a different release, and several contradicting the badge at the top of the same page. They now all read 2895, verified by running the suite rather than by copying a badge. Playwright (70 / 90 / 12 → 101), e2e smoke (20 → 21), route modules (12 / 32 → 37), smoke flows (12 → 22) and the test-file count (218 → 329) were stale in the same way. The Total row also repeated the unit count, so it under-summed the three suites below it; it now reads the real sum (3040).

[1.227.5] — 2026-08-29

Виправлено — регіональний скан вичерпував пам’ять сервера, а один Playwright-сьют падав на власному teardown.

Виправлено

  • Регіональний скан убивав сервер через OOM. ru-scanner накопичував усі сирі влучання з усіх запитів і дедуплікував та фільтрував лише наприкінці. Список запитів навмисно складається з близьких синонімів (Golang, Go разработчик, Golang разработчик, Senior Go…), тож та сама вакансія повертається раз на запит, і масив тримав кратно більше об’єктів, ніж унікальних — разом з описами, які становлять основну масу об’єкта і які фільтри майже повністю відкидають. Виміряно: 742 МБ купи на реальному прогоні з 21 запиту. Сервер обмежує купу Node 490 МБ (V8 рахує ліміт від 956 МБ RAM машини), тож скан валив сервіс із FATAL ERROR: Reached heap limitчотири рази за добу. Тепер дедуплікація і фільтрація йдуть щозапиту, і той самий прогін уклався в 177 МБ, мінус 76%. Звітні числа не змінилися: унікальний підсумок несе Set з URL-рядків.
  • net::ERR_SOCKET_NOT_CONNECTED випадково валила CI. Три Playwright-сьюти викликають window.stop(), щоб обірвати незавершені запити перед перевіркою «консольних помилок немає». Chromium повідомляє про цю одну навмисну дію трьома іменами залежно від стану сокета: ERR_ABORTED при скасуванні, ERR_EMPTY_RESPONSE у польоті та ERR_SOCKET_NOT_CONNECTED, коли сокет уже зник. Спільний фільтр знав лише перші два. Виявилося на прогоні по локалях у tr, а playwright-forms і playwright-smoke були за один невдалий прогін від того самого.

Примітки

  • Чому саме тепер. Список виріс із 14 до 21 запиту після додавання пошуків продакт-менеджера. Пам’ять старої реалізації зростала із загальною кількістю влучань, тож +50% запитів пробили стелю машини.
  • Зміна дедупу зберігає поведінку, і це перевірено, а не заявлено. tests/ru-scanner-dedup-order.test.mjs фіксує незручний напрямок — пізніший дублікат, що НЕ проходить фільтри, зобов’язаний витіснити раніший, який пройшов — і виконує 200 рандомізованих звірок зі старою реалізацією.
  • Фільтр консолі лишається вузьким. Безпечна лише сім’я скасованих запитів; відмова у з’єднанні, збій DNS, скидання, будь-який 5xx і неперехоплені JS-винятки й далі валять перевірку.
  • Дві гіпотези перевірено й відкинуто, а не відправлено в реліз: таймаут Caddy (конфіг чистий, flush_interval -1 для SSE правильний) і типовий 5-хвилинний requestTimeout Node (зменшена репродукція показала, що він не рве довгу стрімінгову відповідь). Справжнім доказом став Reached heap limit у журналі. Тести: 2860 → 2865.

[1.227.4] — 2026-08-29

Виправлено — word: / stem: усе ще ігнорувалися в content_filter; v1.227.3 полагодила лише фільтр заголовків.

Виправлено

  • content_filter не враховував префікси. v1.227.3 навчила title_filter батьківських префіксів word: / stem:, але content_filter лишився на власному сирому lower.includes(k), тож запис із префіксом там і далі зіставлявся як буквальний текст "word:java" — той самий мовчазний no-op, на один фільтр далі. content_filter читає ОПИС вакансії, і його замовчуванням завжди був простий підрядок: саме тому голий негатив java відкидає все, де лише згадано «JavaScript»; префікс — це спосіб вивести один запис з-під цього правила. Портовано батьківський compileContentKeyword: він використовує ту саму механіку префіксів, але навмисно не прив’язує короткі ключі — фільтр заголовків прив’язує абревіатури з 2–3 літер, бо «COO» всередині «Coordinator» завжди помилка, тоді як коротка послідовність усередині абзацу прози зазвичай навмисна (aws, gcp, sql, go).

Примітки

  • Знайдено в AI-рев’ю PR v1.227.3, яке помітило, що код не покриває того, що заявляє його ж коментар — там було «title_filter / content_filter», хоча підключено було лише шлях заголовків. Тепер коментар явно розділяє: префікси працюють в обох фільтрах, автоприв’язка абревіатур — лише в заголовках.
  • Так само перевірено за тим рев’ю і не змінилося: перехід правила для абревіатур 2–3 літер з \b на межу Unicode поводиться однаково для ASCII і суворіший лише там, де треба — COO_lead і coo1 і далі зберігаються (підкреслення та цифра — символи слова в обох), Coordinator зберігається, «Директор COO» і «VP «Продукт»» коректно відкидаються. Перевірено 14 межових випадків.
  • Записи content_filter без префікса зберігають попереднє зіставлення байт у байт, порожній або відсутній опис і далі проходить, а голий word: / stem: не збігається ні з чим замість усього. 6 нових кейсів у tests/content-filter.test.mjs. Тести: 2852 → 2858.

[1.227.3] — 2026-08-29

Виправлено — префікси word: / stem: у фільтрі заголовків мовчки ігнорувалися, через що ці рядки фільтра нічого не робили.

Виправлено

  • web-ui не реалізовував батьківські префікси word: / stem:. title_filtercontent_filter) за замовчуванням зіставляють підрядок без урахування регістру — саме тому голий негатив intern відкидає і «International Product Manager», і «Internal Tools Engineer». Замість зміни цього замовчування (що зламало б усі налаштовані встановлення) батьківський проєкт зробив точність опційною для кожного запису: word:intern збігається лише з цілим словом, а stem:agent має починати слово й може продовжуватися (відділяючи «Agentforce» від «Reagents»). У web-ui не було ні того, ні іншого, тож запис зіставлявся як буквальний текст "word:intern", якого немає в жодному заголовку вакансії. Рядок ставав мовчазним no-op: той самий portals.yml коректно фільтрував через CLI і зовсім не фільтрував тут, тож усі стажерські вакансії, які користувач виключив, усе одно потрапляли до таблиці #/scan. Портовано з батьківського title-keywords.mjs, включно з межею слова з підтримкою Unicode (\p{L}\p{M}\p{N}_) замість \b, який працює лише з ASCII і збігався всередині слова в заголовках з діакритикою та кирилицею.

Примітки

  • Знайдено під час перевірки живого portals.yml, куди щойно додали word:intern саме для того, щоб голий intern перестав відкидати «International Product Manager». У CLI префікс працював; web-ui тихо лишав усі стажування. Тепер поведінка ідентична.
  • Замовчування не змінилися. Запис без префікса зберігає попереднє зіставлення: підрядок для фраз і всього, що містить не-літери (.NET, SAP , L&D), і прив’язка до меж слова для абревіатур із 2–3 літер. Голий word: / stem: вважається одруком і не збігається ні з чим — як негатив порожній шаблон збігся б із будь-яким заголовком і скасував би весь скан через одну випадкову двокрапку.
  • Правило для абревіатур 2–3 літер переведено на ту саму межу Unicode: раніше воно використовувало \b, через що coo і word:coo розходилися на не-ASCII заголовках — два написання одного правила.
  • 6 нових кейсів у tests/title-filter.test.mjs. Тести: 2846 → 2852.

[1.227.2] — 2026-08-29

Виправлено — #/config з’їжджав убік на вузьких телефонах.

Виправлено

  • MOBILE-1: #/config виходив за екран горизонтально на в’юпортах ≤ 352 px. Кожне поле-селект несло інлайновий min-width: 300px. Інлайновий стиль перемагає таблицю стилів, тож контрол не міг стиснутися разом із контейнером: на в’юпорті 320 px правий край селекта опинявся на 353 px, сторінка отримувала горизонтальну прокрутку, а контроли виїжджали за правий край. Зачепило всі 18 селектів провайдера/моделі (cfg-llm-provider і всі cfg-*-model). Тепер min-width: min(300px, 100%) + max-width: 100%: комфортні 300 px діють лише коли є місце. Пристрої на порозі й нижче: iPhone SE (1-ше покоління, 320 px), старі Android, зовнішній екран Galaxy Fold.

Примітки

  • Чому всі попередні мобільні прогони це проґавили. На 360 і 375 px запасу рівно вистачає, щоб приховати переповнення, а набори Playwright працюють у вікні 1280 px. Жоден тест у CI ніколи не міряв в’юпорт вужчий за 375 px. Новий tests/playwright-narrow-viewport.mjs проходить маршрути з формами на 320 px і падає на будь-якому горизонтальному переповненні, називаючи в повідомленні найширший елемент; другий кейс фіксує, що жоден селект #/config не виходить за в’юпорт. Спершу перевірено, що він відтворює баг (scrollWidth 353 > clientWidth 320, 18 селектів із правим краєм 353). Playwright: 99 → 101.
  • Відомо і свідомо поки без гейта: 280 px (зовнішній екран Galaxy Fold) усе ще переповнюється приблизно на 5 px, але з іншої причини — довгий контент усередині блоків <details> картки конфігурації та сайдбар 256 px проти в’юпорта 280 px, а не інлайновий min-width, який лагодить цей реліз. Переверстка під ширину, якої не заявляє жоден масовий телефон, — окрема робота; список WIDTHS у сьюті до неї готовий.
  • Також виправлено QA-промт v1.227.1, де маршрут фоллоу-апу названо #/modes/followup. Mode-сторінки реєструють «голий» слаг (Router.register(cfg.slug)), тож маршрут — #/followup, а #/modes/followup коректно віддає 404. Лише документація.
  • Юніт-сьют без змін — 2846 (нове покриття браузерного рівня).

[1.227.1] — 2026-08-29

Виправлено — розбивка джерел у довідці не сходилася сама з собою, а GitHub-банер друкувався в застосунку сирою розміткою.

Виправлено

  • §17 давав підсумок і розбивку, які не додавалися. Усі бандли довідки писали «реєстр server/lib/sources/ постачає 85 адаптерів — 78 англомовних + 5 російськомовних». 78 + 5 = 83, а не 85; у ja/ko дрейф сягав 77 (= 82). Кожне нове джерело піднімало підсумок і не чіпало доданок, тож обидва числа розходилися вже кілька релізів. Тепер розбивка — 80 EN + 5 RU в усіх 17 локалях. Це важливіше за косметику: docs/help/<lang>.md — ЄДИНИЙ корпус, на якому ґрунтується «Ask the docs», тож на питання про кількість джерел користувач отримував два суперечливі числа з одного речення.
  • Те саме речення фіксувало застарілий якір версії. «Станом на v1.213.0 реєстр …» дожило в 16 локалях, поки лічильник рухався двічі (83 у v1.219.0, 85 у v1.226.0). Якір уже лагодили вручну у v1.210.1 — і він знову поплив. Його прибрано на користь формулювання без версії: сам лічильник перевіряється проти живого реєстру, який не може застаріти.
  • Банер провайдерів друкувався екранованою розміткою вгорі #/help. У v1.225.0/v1.225.1 записано, що його «вирізає UI.md() (немає підтримки зображень)». Це змішує дві різні речі: UI.md() екранує-першим — він екранує кожен байт джерела до будь-яких markdown-перетворень, і це правильна XSS-межа, але протилежність вирізанню. Ніхто не прибирав банер, бо ніхто й не намагався, і всі 17 локалей відкривали #/help 268 символами буквального <p align="center"><img src="https://…. Новий спільний вхід (server/lib/help-markdown.mjs::stripGithubOnlyBlocks) відкидає при читанні окремі HTML-блоки із зображенням — і для GET /api/help/:lang, і для корпусу docs-assistant. У файлах банер лишається, тож на GitHub вітрина рендериться, як і раніше.
  • Колонка «#» на дошці каденції була порожня з v1.117.0. Вона читала entry.appNum, тоді як followup-cadence.mjs віддає num — поля appNum у payload не було ніколи. Тепер читаються обидва. Рядок «Next up» із v1.227.0 успадкував ту саму помилку й виправлений разом із нею.

Примітки

  • Усі чотири лишалися непоміченими, бо їх ніщо не перевіряло. Гейти довідки (canonical-docs-coverage, help-ui, help-ru-config-section) рахують ЗАГОЛОВКИ — 32 H2 / 121 H3 — і жоден тест ніколи не читав число всередині абзацу й не дивився, що насправді віддає вхід довідки. Це закривають два нові сьюти: tests/help-source-counts.test.mjs (заявлений підсумок дорівнює SOURCES.length; доданки дорівнюють живому EN/RU-поділу й дають у сумі підсумок; абзац не фіксує версію) та tests/help-banner-strip.test.mjs (банер лишається на диску для GitHub, ніколи не переживає strip, перший відрендерений рядок кожного бандла — його заголовок, strip лишається вузьким, і обидва споживачі йдуть через нього). Обидва перевірені на падіння саме проти цих багів до коміту. Тести: 2837 → 2846.
  • Реєстр, сканер і контракти роутів не змінювалися; джерел і далі 85 (80 EN + 5 RU), ALL_ADAPTERS 80.

[1.227.0] — 2026-08-28

Додано — SEEK Гонконг (JobsDB), фільтр «лише термінові» для фоллоу-апів і нерозкриті роботодавці тепер показують посередника.

Додано

  • JobsDB Гонконг через наявне джерело Jobstreet/SEEK. Гонконг працює під брендом JobsDB, але стоїть на тій самій платформі SEEK за тим самим ендпоінтом v5, тож окреме джерело не потрібне — достатньо hk.jobsdb.com у списку дозволених хостів і siteKey: HK-Main. Реєстр без змін: 85 джерел (80 EN + 5 RU), ALL_ADAPTERS 80 — це новий РИНОК на наявному адаптері, а не новий адаптер.
  • Фільтр «лише термінові» на дошці каденції (#/modes/followup). Новий чекбокс звужує дошку до того, що потребує дії просто зараз. Терміновість читається з urgency, яку вже обчислив батьківський followup-cadence.mjs (urgent / overdue), і ніколи зі status трекера — там applied/responded/interview, що не збіглося б ні з чим. Коли термінового немає, дошка називає найближчий запланований фоллоу-ап замість порожнього екрана. Логіка живе в чистому модулі public/js/lib/followup-view.js — дзеркалі батьківського followup-view.mjs.
  • Нерозкриті роботодавці атрибутуються агенції-посереднику. Вакансія з прихованим роботодавцем записується в трекер як ?, а рекрутер потрапляє в колонку Via. #/tracker показував голий ?, тож кілька нерозкритих рядків були нерозрізненні, а резолверу логотипів не було за що зачепитися. Тепер такий рядок читається як «Не розкрито · через <агенція>», логотип береться за назвою агенції, і рядок знаходиться за цією назвою в пошуку. Це лише відображення — канонічне поле company ніколи не перезаписується рекрутером. Новий чистий модуль public/js/lib/company-presentation.js — дзеркало батьківського company-presentation.mjs.

Виправлено

  • Jobstreet/SEEK звертався до застарілого ендпоінта. Джерело досі вказувало на chalice-search v4 (/api/chalice-search/v4/search), який SEEK вимкнув; батько перейшов на /api/jobsearch/v5/search, а web-ui за ним не пішов. Тепер будь-який запис Jobstreet/SEEK запитує v5. Форма елемента v5 читається нативно (URL будується з id, locations[0].label, advertiser.description, salaryLabel), проєкційний параметр solrFields з v4 прибрано, а запис, у якого в portals.yml усе ще зафіксовано мертвий шлях v4, зберігає хост свого ринку, а шлях перезбирається на v5 — правок від користувача не потрібно.

Примітки

  • Паритет із career-ops 1.30.0 (HEAD батька 8d64f656cc46a4; його VERSION усе ще 1.30.0 — release-please відстає від main). Не портовано, з причинами: заповнення локації з JSON-LD в iCIMS — у джерела iCIMS у web-ui взагалі немає хука enrichDate() (лише сторінки списку), тож лагодити нічого; нефатальний stderr CLI (#1974) — раннер web-ui визначає збій лише за кодом виходу і ніколи не мав цієї евристики; трасування шляхів Turbopack — у web-ui немає збирача; фікси cv-sync-check — ретранслюються лише на читання і fail-soft; послідовний Playwright у scan.mjs, відповіді Block H, запобіжники updater/doctor/update-system, рекурсивна перевірка синтаксису, дашборд на Go та додані самотести jd-skill-gap/verify-cv-facts — лише-CLI поверхні, до яких web-ui не заходить, або самотести без зміни поведінки. Тести: 2818 → 2837.

[1.226.0] — 2026-08-27

Додано — два в’єтнамські джерела вакансій: ITviec і CareerViet.

Додано

  • ITviec + CareerViet (В’єтнам). Дві в’єтнамські дошки з прив’язкою хоста підключаються до сканера записами provider: itviec / provider: careerviet: ITviec (itviec.com, техно) і CareerViet (careerviet.vn, широке покриття). Обидва — HTML-скрапери без токенів із SSRF-прив’язкою хоста, лише-HTTPS, fail-soft по компаніях і лімітами сторінок. Реєстр тепер має 85 джерел (80 EN + 5 RU), ALL_ADAPTERS 80 — обидва видно у фільтрі «Джерело» на #/scan.

Примітки

  • Паритет із career-ops 1.30.0 (parentVersion 1.29.0 → 1.30.0). Не портовано (лише CLI/спільнота або вже є): Hired Wall, /calibrate, clean-markers, паки шаблонів, doctor, pdf/LaTeX і CLI eval/tracker/scan, до яких web-ui не звертається; фікс _html-to-text і roleFuzzyMatch вже були; префікс title-filter stem: відкладено. Тести: 2818.

[1.225.1] — 2026-08-27

Виправлено — банер логотипів провайдерів тепер відображається в README і довіднику.

Виправлено

  • Банер v1.225.0 не показувався на GitHub: у окремого images/providers.svg був некоректний font-family (вкладені подвійні лапки), який GitHub відхиляє; <img> використовував відносний src (raw HTML <img> не розв’язує відносні шляхи на GitHub); а блок <p> не мав завершального порожнього рядка. Тепер він указує на відрендерений images/providers.png за абсолютним raw-URL, з розділенням порожніми рядками; SVG також виправлено до валідного XML.

Примітки

  • Лише доки; без змін коду й тестів. Вітрина cvstart.org і плитки в застосунку вже були коректні (вбудований SVG, не цей файл). 2784

[1.225.0] — 2026-08-27

Додано — банер логотипів провайдерів у README та довіднику.

Додано

  • Банер логотипів провайдерів. Єдиний самодостатній images/providers.svg18 LLM-провайдерів: справжній логотип бренду (simple-icons, CC0) для 11, що публікують іконку, і монограма в кольорі бренду для 7 інших — тепер очолює README (×17) та вбудований довідник. (Застосунок сам малює плитки зі справжніми логотипами; банер відображається на GitHub, а XSS-безпечний рендерер застосунку його прибирає.)

Примітки

  • Лише доки — без змін коду й тестів; SVG вбудований (без віддалених ресурсів). Джерела сканування без змін: 83. Тести: 2784.

[1.224.0] — 2026-08-26

Змінено — плитки LLM-провайдерів тепер показують справжні логотипи брендів.

Змінено

  • Справжні логотипи провайдерів. Плитки провайдера — поля API-ключів у Налаштуваннях, рядки Використання, чип дашборда, зведення «Активний» у Налаштуваннях, результат ⚡-оцінки та привітальний банер — тепер показують справжній логотип бренду для 11 провайдерів, що публікують open-source-іконку (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub · DeepSeek · Kimi · MiniMax · Mistral · Ollama; одношляхові SVG з simple-icons, CC0). Семеро без опублікованої іконки (Hermes · GLM/Z.ai · Grok · Together · Fireworks · BytePlus Ark · Volcengine Ark) зберігають монограму в кольорі бренду. Досі безпечно для CSP за побудовою: шлях логотипа — вбудована статична константа, без віддалених ресурсів і без innerHTML.

Примітки

  • Без змін маршрутів чи поведінки; той самий API ProviderLogo.el(slug). provider-logo.js виріс (вбудовані шляхи логотипів). Джерела сканування без змін: 83. Тести: 2784.

[1.223.0] — 2026-08-26

Виправлено — успішна ⚡ live-оцінка фарбувалася як помилка; крім того, встановлення coworker OpenWorker прямо з його GitHub-URL або .zip.

Виправлено

  • Тон бейджа результату ⚡-оцінки. Успішна live-оцінка більше не фарбується як провал. In-process провайдери (OpenRouter, DeepSeek, …) не повертають код виходу підпроцесу, тож суворе code === 0 було хибним і будь-який успіх виходив червоним (показуючи «exit 0»). Тон тепер слідує за результатом — червоним фарбує лише підпроцес із ненульовим кодом; live-результат без коду — badge-ok, а суфікс «· exit N» показується лише за наявності справжнього коду виходу.

Змінено

  • Встановлення coworker OpenWorker з його GitHub-URL / .zip. Документація coworker тепер описує інсталятор однією командою (curl … | bash, ідемпотентний — повторно використовує наявні career-ops / web-ui) та три шляхи встановлення в OpenWorker (GitHub URL · .zip · імпорт одного файлу) у docs/integrations/openworker.md, вбудованій довідці §32 (×17) і README.

Примітки

  • Закрито давню прогалину в покритті: у джерела Recruitee з’явився наскрізний тест на фікс v1.214.2 з лапками навколо кутової дужки (> усередині атрибута тега не має витікати в опис). Джерела сканування без змін: 83. Тести: 2783.

[1.222.0] — 2026-08-26

Змінено — підказки полів розширених провайдерів (DeepSeek … Volcengine Ark) тепер локалізовані всіма 17 мовами.

Змінено

  • Локалізація підказок полів провайдерів. Кожен config.<slug>Hint для 11 OpenAI-сумісних провайдерів, доданих у v1.216.0–v1.217.0 (DeepSeek · GLM/Z.ai · Kimi · MiniMax · Mistral · Grok · Together · Fireworks · Ollama · BytePlus Ark · Volcengine Ark) — підказки API-ключа, моделі та базового URL у Налаштування → Ключі API — тепер перекладено всіма 17 мовами замість відкату до англійської. URL реєстрації, id моделей і маркер ⚡ live-eval збережено дослівно.

Примітки

  • Поведінка не змінюється — англійський hintFallback у field-specs.js досі страхує відсутній ключ, а новий контрактний тест перевіряє, що кожен hintKey дескрипторів полів резолвиться в усіх 17 мовах. Джерела сканування без змін: 83. Тести: 2779.

[1.221.0] — 2026-08-26

Безпека — ешелонований захист від DNS-rebinding на шляху fetch сканера.

Безпека

  • HTTP-ядро сканера (fetchJson / fetchText у server/lib/http-json.mjs) тепер резолвить хост кожного джерела на реальному мережевому шляху і відмовляється підключатися, якщо він резолвиться у приватну, loopback, link-local, CGNAT або адресу хмарних метаданих — закриваючи вектор DNS-rebinding. Хости сканера вже привʼязані до публічних доменів, тож це ешелонований захист; шлях користувацьких URL уже мав сильнішу привʼязку зʼєднання (safe-fetch.mjs). Guard працює лише на реальному транспорті fetch — впроваджений у тестах fetchImpl ніколи не резолвиться.

Примітки

  • Для нормального сканування поведінка не змінюється. Джерела сканування без змін: 83. Тести: 2775.

[1.220.0] — 2026-08-26

Додано — скануйте кілька категорій Get on Board з одного запису.

Додано

  • Запис getonbrd тепер може задати categories: [programming, operations-management, machine-learning-ai] (або один category:) замість лише каналу programming; вакансії дедуплікуються за URL між категоріями, ліміт — 12. Наявні записи не змінюються (типово досі programming).

Примітки

  • Лише сервер; SSRF-прив’язка хоста (www.getonbrd.com) і redirect:'error' незмінні, а неправильний слаг категорії відхиляється до запиту. Джерела сканування без змін: 83. Тести: 2771.

[1.219.0] — 2026-08-26

Додано — Torre долучається до сканера, і тепер кожна мітка провайдера коректна.

Додано

  • Torre (provider: torre) — пан-латиноамериканський маркетплейс талантів torre.ai, публічний пошук без автентифікації з віддаленими вакансіями, яких немає в Greenhouse/Lever/Ashby; один обмежений запит на запис. Реєстр тепер містить 83 джерела (78 англійських + 5 російських). Також монограма активного провайдера на банері привітання.

Виправлено

  • Сканування a16z Speedrun завершувалося зарано на дошці з ротацією (коротку сторінку в середині обходу вважали останньою, обрізаючи до 149 із ~300); тепер довіряє total_pages стрічки. А банер привітання визначав провайдера з карти на 4 записи (показуючи сирий слаг для решти 14) — тепер використовує мітку+монограму з 18; примітка про провайдерів у Налаштуваннях змінюється з 7 на 18.

Примітки

  • Опція кількох категорій на запис у Get on Board ще не підключена (потрібен рефакторинг на рівні джерела). Тести: 2768.

[1.218.0] — 2026-08-26

Додано — значки провайдерів (і правильні назви) скрізь, де показано провайдера.

Додано

  • Монограми провайдерів на чипі дашборда, у зведенні «Активний» налаштувань і в заголовку результату ⚡ оцінювання.

Виправлено

  • Застарілі мітки: три екрани визначали назву з карти на 5 записів (результат оцінювання позначав будь-якого не-Anthropic провайдера як «Gemini»); тепер використовують спільний ProviderStatus.label (18). Лічильник ключів показував «/ 7» → 18. До списку слагів підказки LLM_PROVIDER додано ark / arkcn в усіх 17 мовах.

Примітки

  • Лише клієнт, безпечно для CSP. Без змін сервера. Джерела сканування: 82. Тести: 2758.

[1.217.1] — 2026-08-26

Посилення тестів — покриття ендпоінтів для всього списку провайдерів.

Додано

  • Параметризований тест run<Provider>, що перевіряє ендпоінт, модель за замовчуванням і Bearer-автентифікацію для Kimi, MiniMax, Mistral і Fireworks.

[1.217.0] — 2026-08-26

Додано — Ark долучається до списку LLM (18 провайдерів). BytePlus Ark і Volcengine Ark — моделі Doubao від ByteDance — тепер провайдери ⚡ живого оцінювання через той самий OpenAI-сумісний код і базовий URL за регіоном.

Додано

  • BytePlus Ark (ARK_API_KEY, міжнародний) і Volcengine Ark (ARK_CN_API_KEY, Китай) — OpenAI-сумісний Chat Completions через runOpenAICompatible(). Задайте ARK_MODEL / ARK_CN_MODEL (назву моделі Doubao або ваш id ендпоінта ep-…); ARK_BASE_URL / ARK_CN_BASE_URL перемикають регіон. Обидва стають у порядок auto (останніми), з монограмою в Налаштуваннях і «Використанні». Вебінтерфейс охоплює тепер 18 провайдерів.

Примітки

  • Той самий шлях довіреної конфігурації + перевірки схеми http(s), що й в інших (ніколи не SSRF-валідатор URL вакансій). Джерела сканування без змін: 82. Тести: 2755.

[1.216.0] — 2026-08-26

Додано — ще девʼять LLM-провайдерів, у кожного власна фірмова плитка. ⚡ Живе оцінювання тепер можна запускати через DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together, Fireworks або повністю локальний Ollama — достатньо одного ключа, поряд із полем кожного в Налаштуваннях є монограма.

Додано

  • Девʼять OpenAI-сумісних провайдерів. DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together AI, Fireworks AI та Ollama (повністю локально, без ключа). Задайте ключ — або OLLAMA_BASE_URL для Ollama — у Налаштуваннях, і провайдер стане в порядок auto після Hermes; закріпіть будь-який через LLM_PROVIDER. Together також хостить Inkling від Thinking Machines (thinkingmachines/Inkling). Вебінтерфейс охоплює тепер 16 провайдерів живого оцінювання.
  • Монограми провайдерів. CSP-безпечна плитка з ініціалом у фірмовому кольорі позначає кожного провайдера поряд із полем ключа в Налаштуваннях і в рядку сторінки «Використання» — вбудований SVG, без зовнішніх логотипів, ніщо не залишає ваш пристрій.

Примітки

  • Базові URL провайдерів — довірена конфігурація, що проходить через runOpenAICompatible() з перевіркою схеми http(s) (петля Ollama дозволена), а не через SSRF-валідатор URL вакансій. Джерела сканування без змін: 82. Тести: 2752.

[1.215.0] — 2026-08-26

Додано — керуйте всім пошуком роботи з OpenWorker. Новий coworker керує цим пайплайном з open-source-застосунку AI-coworker від Andrew Ng, і вбудована довідка тепер його охоплює, тож асистент «Запитати документацію» може провести вас.

Додано

  • Coworker для OpenWorker. Coworker без коду для OpenWorkerFighter90/career-ops-coworker — виконує весь пошук роботи (скан, оцінка, підгонка, трекінг, follow-up) і може запустити цей дашборд, усе на вашому робочому столі. Це персона з одного файлу 17 мовами, встановлення якої перевірено власним завантажувачем OpenWorker.
  • Довідка §32 «Run it from OpenWorker» (усі 17 локалей) документує це, щоб вбудований асистент «Запитати документацію» та плаваюча кнопка довідки відповідали на питання про нього. Також додано посилання у футері лендингу, README, вікі та docs/integrations/openworker.md.

Примітки

  • Лише документація + сайт — без змін коду застосунку. Джерела сканування без змін — 82. Набір тестів: 2742.

[1.214.2] — 2026-08-25

Виправлено — спільний конвеєр HTML→текст (описи Greenhouse / Recruitee) більше не протягує атрибути тегу в опис, коли значення атрибута містить символ >.

Виправлено

  • HTML-описи з > усередині атрибута очищуються коректно. Засіб видалення тегів за описами Greenhouse і Recruitee використовував наївне співпадіння, що зупинялося на першому >, тож тег на кшталт <a title="salary > 100k"> залишав хвіст атрибута (100k">) в описі. Тепер він поважає значення атрибутів у лапках (одинарних чи подвійних), видаляє розмітку до декодування сутностей (щоб закодована лапка всередині атрибута не стала хибним роздільником) і досі зберігає літеральний <> у простому тексті.

Примітки

  • Набір тестів: 2742. Джерела сканування без змін — 82. Цього разу нічого нового від батька: решта змін ретранслюється лише на читання (виправлення follow-up / weekly-digest / rejection-latency / salary-gap не потребують змін у web-ui) або не є поверхнями web-ui (зовнішній X/Xquik BYO-key плагін, виправлення CLI/verify). Опціональні пункти — детальні описи SmartRecruiters, пропуск агрегаторів у detect-reposts, префікси заголовків stem: / word:, захист від DNS-rebinding — лишаються в черзі.

[1.214.1] — 2026-08-25

Виправлено — описи Ashby тепер обрізаються до тієї ж довжини, що й Greenhouse та Recruitee, тож контент-фільтр поводиться однаково на трьох дошках, і одна дошка не може роздути кеш сканування.

Виправлено

  • Описи Ashby тепер дотримуються того ж ліміту довжини, що й інші дошки. v1.214.0 подавала тіло вакансії Ashby в контент-фільтр, але — на відміну від Greenhouse і Recruitee, які ріжуть до 4000 символів — у Ashby воно мапилося напряму без ліміту (деякі вчетверо довші). Тож те саме слово content_filter могло збігтися на Ashby, але не на Greenhouse — лише залежно від позиції в тексті, а одна дошка могла додати сотні КБ до кешу сканування, який вантажить #/scan. Тепер Ashby ріжеться до тих самих 4000 символів (його текст уже простий, тож він усікається, а не очищується від тегів).

Примітки

  • Набір тестів: 2738. Джерела сканування без змін — 82.

[1.214.0] — 2026-08-25

Виправлено — описи вакансій Greenhouse, Ashby і Recruitee тепер потрапляють до контент-фільтра, тож фільтри за словами / країною / візою реально збігаються з цими дошками; а “Associate <старший титул>” (включно з академічними званнями) більше не відносять до entry-level.

Виправлено

  • Контент-фільтри тепер працюють на Greenhouse, Ashby і Recruitee. Ці дошки тепер несуть повний текст кожної вакансії як її опис, тож content_filter (або фільтр за словами країни / візи) збігається з тілом вакансії, а не пропускає дошку наосліп. Ashby і Recruitee віддають тіло в списковому payload безкоштовно; Greenhouse уже це робив. Усі три очищаються одним спільним конвеєром HTML→текст.
  • “Associate Director” / “Associate Professor” — це senior, а не entry-level. Класифікатор рівня (використовує skip_tiers:) вважав слово “associate” міткою entry-level, навіть коли воно передує старшому титулу. Тепер він читає “Associate <старший іменник>” — включно з академічними званнями (Associate Professor, Associate Dean) — як senior, зберігаючи справжні junior-варіанти (Associate Attorney, Associate Editor) як entry-level, і досі відносить провідний “Intern, Associate Dean” до стажування.

Примітки

  • Набір тестів: 2736. Джерела сканування без змін — 82. У черзі на потім: опціональне збагачення описів SmartRecruiters (запит деталі на кожну вакансію), пропуск дощок aggregator: true у detect-reposts і префікси заголовків із якірним словом stem: / word: — кожне опціональне налаштування, а не зміна поведінки за замовчуванням. Захист від DNS-rebinding досі в черзі на окремий реліз безпеки.

[1.213.0] — 2026-08-22

Додано — MyCareersFuture, національний банк вакансій Сінгапуру, як джерело сканування. Виправлено — вакансії Greenhouse тепер несуть повний текст, щоб контент-фільтри працювали, а віддалені вакансії Ashby більше не ховаються за локацією «лише місто».

Додано

  • MyCareersFuture (Сінгапур) (mycareersfuture.gov.sg) — нове джерело сканування без токенів для національного державного банку вакансій Сінгапуру (Workforce Singapore). Виберіть його у фільтрі Джерело на #/scan або додайте компанію з provider: mycareersfuture та необовʼязковим списком keywords (інакше беруться цільові ролі з профілю, як у Job Bank). Читає публічний пошуковий API, з привʼязкою до хоста, без ключа.

Виправлено

  • Вакансії Greenhouse тепер можна фільтрувати за контентом. Дошки Greenhouse запитуються з повним тілом вакансії, декодованим у простий текст як опис — тож content_filter (або фільтр за словами країни/візи), що читає опис, тепер справді збігається з вакансіями Greenhouse, а не пропускає їх наосліп.
  • Віддалені вакансії Ashby більше не відсікаються фільтром міста. Ashby тримає модель роботи (Remote/Hybrid/Onsite) окремо від міста офісу, тож повністю віддалена вакансія все ще читалася як, наприклад, «San Francisco» — і фільтр локації, що блокує це місто, приховував доступну вакансію. Тепер за віддаленої роботи до локації додається «Remote», а workplaceType важливіший за застарілий прапорець isRemote, щоб офісно-прив’язану гібридну вакансію не позначили хибно.

Примітки

  • Джерела сканування: 82 (77 англійських + 5 російських). Набір тестів: 2724. Захист від DNS-rebinding (перевірка розвʼязаної адреси хоста перед підключенням) відкладено в окремий реліз — йому потрібен власний дизайн для web-ui, а не прямий порт.

[1.212.1] — 2026-08-21

Виправлено — лендинг cvstart.org недорахував джерела сканера (показував 80 і губив Job Bank (Канада)); тепер знову збігається з 81 у застосунку, а збірка сайту гучно падає, якщо ці числа розходяться.

Виправлено

  • Лічильник «Джерела вакансій» на лендингу знову синхронний із застосунком. Після v1.212.0 cvstart.org показував 80 дощок, і бракувало нового чипа Job Bank (Канада), тоді як застосунок, випадний список сканування та довідка перелічували 81. Лендинг будує список, завантажуючи живий реєстр сканера, і одне джерело не завантажилося в тій збірці через те, як воно підтягувало YAML-залежність — тож було мовчки відкинуте. Тепер Job Bank завантажує цю залежність ліниво, так само як решта застосунку робить під час скану, тому він завжди присутній.
  • Збірка сайту тепер відмовляється публікувати неузгоджений лічильник джерел. Якщо реєстр перелічить менше джерел, ніж є на диску (ознака джерела, що не завантажилося), збірка падає з чітким повідомленням замість тихої публікації хибного числа.

Примітки

  • Поведінка застосунку не змінилася — у сканері завжди були всі 81 джерело; зачепило лише лендинг. Джерела сканування: 81 (76 англійських + 5 російських) — без змін. Набір тестів: 2687.

[1.212.0] — 2026-08-21

Додано — Job Bank (Канада), федеральна національна дошка вакансій. Вилучено — EchoJobs (його фід тепер за бот-захистом). Виправлено — дошки на Consider знову повертають результати, а мульти-локаційні вакансії Lever більше не приховують половину локацій.

Додано

  • Job Bank (Канада) (jobbank.gc.ca) — нове джерело сканування без токенів для федеральної національної служби зайнятості Канади, високонавантаженої дошки, яку погано покриває будь-який агрегатор. Виберіть його у фільтрі Джерело на #/scan або додайте компанію з provider: jobbankca та необовʼязковим списком keywords (інакше беруться цільові ролі з вашого профілю). Читає публічний ATOM-фід, з привʼязкою до хоста, без ключа.

Вилучено

  • EchoJobs — виведено. Його публічний фід тепер за бот-захистом і нічого не повертає, тож тримати його — лише марнувати слот сканування.

Виправлено

  • Дошки на Consider знову повертають результати. Consider тепер вимагає анонімний handshake (GET, що задає сесійну куку + CSRF-токен) перед прийняттям пошукового запиту; без нього запит мовчки відхилявся, і дошка виглядала порожньою.
  • Мульти-локаційні вакансії Lever більше не приховують половину локацій. Lever кладе одне основне місто в location, а решту — у allLocations; читання лише основного робило вакансію, відкриту в Барселоні І Монтевідео, «лише-Барселона» (і її помилково відсікав фільтр локацій). Тепер обидва обʼєднуються.

Примітки

  • Мʼякший темп між сторінками (250 мс замість 150) на посторінкових дошках — із ввічливості до одногостових careers-сайтів. Джерела сканування: 81 (76 англійських + 5 російських) — без змін (Job Bank додано, EchoJobs вилучено). Набір тестів: 2685.

[1.211.0] — 2026-08-19

Додано — Yourator, тайванська tech-дошка вакансій. Виправлено — акцентовані сутності в заголовках/назвах компаній тепер декодуються всюди, а компанія з акцентом у назві більше не позначається помилково.

Додано

  • Yourator (yourator.co) — нове джерело сканування без токенів для tech- та digital-ринку вакансій Тайваню. Виберіть його у фільтрі Джерело на #/scan або додайте компанію з provider: yourator. Воно читає публічний JSON API (без ключа, без браузера), обходить усі сторінки дошки й видає справжнє посилання роботодавця (його власний ATS) з вирізаними трекінг-параметрами.

Виправлено

  • Акцентовані іменовані сутності тепер декодуються всюди. Спільний HTML-декодер отримав латинські літери Latin-1 (&eacute; → é, &ccedil; → ç, …), тож європейська дошка, що пише D&eacute;veloppeur чи Fran&ccedil;ais, більше не лишає цей літерал у заголовку, трекері чи згенерованому документі. (Великі лишаються великими — &Eacute; це É, а не é, — а пошук на кшталт &constructor; тепер розвʼязується в себе.)
  • Компанія з акцентом у назві більше не позначається помилково за перебування на власному домені. «Işık» тепер згортається в «isik» і збігається з isik.com.tr; «Société Générale» збігається з societegenerale.com. Стара перевірка видаляла акцентовані літери замість згортати їх до ASCII-основи.

Примітки

  • Джерела сканування: 81 (76 англійських + 5 російських). Набір тестів: 2667.

[1.210.1] — 2026-08-19

Виправлено — заголовки вакансій і назви компаній з «&» чи лапками з Habr Career більше не надходять спотвореними.

Виправлено

  • Джерело Habr Career тепер декодує HTML-сутності в заголовку та назві компанії, перш ніж вони підуть далі. Серверні картки надходять екранованими («Changellenge >>», «Demand Forecasting & Inventory Optimization», «ООО "М-ТЕХ"»), тож недекодований «&» тихо не проходив ваш же фільтр заголовка з «&» — саме той симптом, який минулий реліз закрив на п’яти інших дошках, — а назви компаній доїжджали до трекера й звітів кривими. Декодування сутностей тепер завершене на всіх шести уражених джерелах.

Примітки

  • Набір тестів: 2644.

[1.210.0] — 2026-08-19

Додано — Senjob, перша африканська дошка вакансій сканера (Сенегал); точніше зіставлення заголовків ще на п’яти дошках.

Додано

  • Senjob (senjob.com) — нове джерело сканування без токенів для Сенегалу, перша африканська дошка сканера. Виберіть його у фільтрі Джерело на #/scan або додайте компанію з provider: senjob. Воно читає публічний список звичайним HTTP (без ключа, без браузера), прив’язує кожен запит до senjob.com і — оскільки парсить HTML — вважає список, що раптом дав порожньо, зламаною дошкою (видимою помилкою), а не країною без вакансій.

Виправлено

  • Заголовки з «&» більше не втрачають вакансії на п’яти дошках — на beesite, Cornerstone (csod), Hacker News «Who is hiring», Phenom і TKMS заголовки надходять з HTML-екрануванням, тож екранований «&» у посаді на кшталт “R&D Engineer” не проходив ваше ж ключове слово “r&d”, і вакансія тихо зникала (вето “sales & marketing” теж не спрацьовувало). Тепер заголовки — і локації Phenom — декодуються перед фільтруванням.

Примітки

  • Джерела сканування: 80 (75 англійських + 5 російських). Набір тестів: 2643.

[1.209.0] — 2026-08-17

Додано — вбудована довідка тепер описує, як записати підсумок відгуку, а «Запитати довідку» підкаже, де це.

Додано

  • У довідці Трекера (§11) усіма 17 мовами зʼявився розділ «Записати підсумок», що проводить по кнопці Підсумок: оберіть, що сталося (відмова / оффер / найнято / відхилено / ігнор / пройшов на співбесіду), перегляньте, що буде зроблено, і запишіть — це фіксує результат, архівує надіслані вами резюме та супровідний лист і синхронізує Статус рядка за вас. Плаваючий помічник «Запитати довідку» читає цей посібник, тож тепер веде вас до цієї кнопки, а не лише пропонує правити Статус вручну.

Примітки

  • Кожен довідковий бандл тепер 31 H2 / 119 H3 (було 118); вартові паритету оновлено. Лише документація — без змін коду й поведінки. Набір: 2625.

[1.208.2] — 2026-08-16

Виправлено — на телефоні кнопки сповіщень і теми більше не лежать поверх поля пошуку.

Виправлено

  • У v1.208.1 кнопки верхньої панелі перестали накривати заголовок сторінки, але на вузькому — хоч і не найвужчому — екрані, особливо в мовах із довшими підписами, уся панель усе одно втискалася в один рядок, тож кнопки 🔔 і 🌙 могли лягти поверх поля пошуку. Тепер кнопки дій (сповіщення, тема, Діагностика, Відкрити Scan) на телефоні завжди переходять на власний другий рядок на всю ширину, тож поле пошуку видно повністю і ніщо не перекривається.

Примітки

  • На телефоні кнопки дій панелі переходять на другий рядок на всю ширину, прибираючи крихку смугу «майже повного рядка», де розкладка розподіляла залишок відʼємного місця як накладання. Playwright-вартовий тепер відтворює точний тригер — локаль із довгими підписами в смузі 565–640px — і перевіряє, що елементи панелі не ділять пікселі. Набір: 2621.

[1.208.1] — 2026-08-16

Виправлено — на телефоні кнопки верхньої панелі більше не накривають сторінку.

Виправлено

  • У v1.208.0 кнопки верхньої панелі (Діагностика, Відкрити Scan, сповіщення, тема) переносилися на другий рядок на вузьких екранах, але панель зберігала фіксовану висоту, тож перенесений рядок вилазив і лягав поверх заголовка сторінки. Тепер панель росте під свої рядки, а контент іде під нею.

Примітки

  • Фіксована height панелі стала min-height, тож вона росте під контент на будь-якій ширині (десктоп без змін). Playwright-вартовий тепер також перевіряє, що панель не вилазить поверх сторінки. Набір: 2621.

[1.208.0] — 2026-08-16

Виправлено — застосунок тепер вміщається на екрані телефона: горизонтальне прокручування зникло.

Виправлено

  • На вузькому екрані весь застосунок їхав убік — верхня панель, таблиці, статті довідки й вкладки налаштувань вилазили за правий край. Тепер кожна сторінка вміщається на будь-якій ширині: кнопки верхньої панелі переходять на другий рядок, широкі таблиці й блоки коду прокручуються всередині своєї рамки, довідка ставить зміст над статтею, ряди кнопок і вкладок переносяться, а довгі шляхи чи URL переносяться, а не розтягують сторінку.

Примітки

  • Причина — класична пастка flex/grid min-width: auto плюс кілька необгорнутих широких елементів; виправлено через min-width: 0 на grid-елементах, overflow-wrap на markdown/заголовках, прокручувану markdown-таблицю та перебудову сітки довідки на мобільному брейкпоінті. Playwright-вартовий перевіряє 0 горизонтального переповнення на 375 px на основних маршрутах. tests/playwright-smoke.mjs. Набір: 2621.

[1.207.2] — 2026-08-16

Виправлено — AI-плани та профілі карʼєрної орієнтації більше не показуються як сирий дамп коду.

Виправлено

  • Деякі моделі загортають усю відповідь в код-огорожу markdown … . Коли так ставалося, план розвитку і профіль орієнтації показувалися моноширинним блоком коду замість документа із заголовками та списками. Тепер обгортальну огорожу знято — лише якщо вона охоплює всю відповідь і мова явно markdown/md, тож справжня відповідь на python/js/без мови ``` лишається недоторканою.

Примітки

  • Оброблено один раз у спільному кроці очищення LLM (cleanLlmMarkdown), тож виграють усі AI-маршрути, а вкладені блоки коду всередині відповіді зберігаються. tests/llm-output.test.mjs (+3). Набір: 2621.

[1.207.1] — 2026-08-16

Виправлено — на маленьких телефонах лендинг більше не вилазить убік.

Виправлено

  • На вузькому телефоні перший екран — заголовок, вступний рядок і термінал встановлення — міг обрізатися по правому краю, бо довга команда встановлення та колонки верстки не стискалися під екран. Тепер усе вміщається на будь-якій ширині; команда встановлення прокручується всередині свого термінала.

Примітки

  • Також зміцнено нестабільну E2E-перевірку, яка могла падати через тимчасовий 404 ресурсу: тепер вона ігнорує нешкідливий мережевий шум (favicon / зʼєднання / невдалий ресурс), як і сусідні перевірки, але й далі ловить справжні помилки скриптів. Поведінка застосунку не змінилася. Набір: 2618.

[1.207.0] — 2026-08-15

Додано — записуйте підсумок відгуку прямо з трекера.

Додано

  • У кожному рядку трекера зʼявилася дія Підсумок: оберіть, що сталося (відмова, офер отримано, найнятий, офер відхилено, немає відповіді, пройшов на співбесіду), додайте необовʼязкову примітку, перегляньте підсумковий статус і запишіть. Запис архівує надіслані артефакти CV та супровідного листа й синхронізує трекер до канонічного стану — одна детермінована дія замість ручного редагування.

Примітки

  • Нова POST /api/outcome ретранслює CLI підсумків: dryRun:true — перегляд лише для читання (знаходить рядок, повідомляє підсумковий стан, нічого не пише); реальний виклик записує. Безпека запису: тип підсумку обмежено відомим набором, а кожне текстове поле відхиляється за керуючих символів до виклику (аргументи-масив, spawn — без оболонки). tests/outcome-route.test.mjs. Набір: 2618.

[1.206.0] — 2026-08-15

Документація — вбудований довідник тепер описує п’ять найновіших функцій усіма 17 мовами.

Додано

  • Вбудований довідник — і асистент «Запитати довідку», що відповідає на його основі — тепер описує п’ять нещодавно доданих функцій: Доктор налаштування (Налаштування — перевіряє CV і профіль на прогалини та залишені демодані), Пошук ATS-дощок (Портали — автоматично знаходить карʼєрну дошку компанії), перевірку «ще відкрита?» (Трекер — чи вакансія ще відкрита), підказку «повторно використати попереднє CV?» (CV Studio — сигналізує, коли вже адаптоване CV пасує до нової вакансії) та Журнал навичок (Аналітика — запис оцінок самоперевірки). П’ять нових підрозділів, перекладених усіма 17 мовами.

Примітки

  • Структура довідника зросла до 31 H2 / 118 H3, паритет гарантовано для кожної мови. Оновлено довідкову документацію: docs/architecture/API.md описує п’ять маршрутів цих функцій, а лічильники маршрутів/версії у CLAUDE.md та docs/sdd/CONVENTIONS.md актуальні (36 модулів маршрутів). Набір: 2610.

[1.205.0] — 2026-08-15

Додано — журнал навичок для запису результатів тестів/оцінювань.

Додано

  • Новий Журнал навичок (Аналітика → Журнал навичок) дає змогу записати самооцінку — компанія, платформа, навичка, оцінка % та необовʼязкова примітка — додається до data/assessments.tsv, зі списком минулих записів (нові згори). Без токенів, детерміновано; формат файлу веде CLI батьківського проєкту.

Примітки

  • Новий GET /api/assessments (ретранслює типовий список JSON з assessment-log.mjs; мʼяка відмова {available:false}) + POST /api/assessments (явний запис: поля передаються як аргументи-масив до assessment-log.mjs add). Безпека запису: будь-яке текстове поле з керуючими символами відхиляється (TAB зламав би стовпець, перенесення рядка вставило б рядок) → 400 до запису; оцінка/поріг 0–100, довжини обмежені. tests/assessments-route.test.mjs. Набір: 2610.

[1.204.0] — 2026-08-15

Додано — панель «Доктор налаштування» в Налаштуваннях, що позначає незаповнені чи демо-дані CV/профілю.

Додано

  • Налаштування → Доктор налаштування тепер без токенів перевіряє ваші cv.md і config/profile.yml та перелічує блокуючі проблеми (відсутні файли/поля) і попередження (залишені демо/шаблонні дані, зашиті метрики) — щоб виявити неповне налаштування, перш ніж воно послабить скани й адаптацію. Лише читання; перезапуск одним кліком.

Примітки

  • Новий route лише для читання GET /api/cv-sync-check ретранслює cv-sync-check.mjs з батьківського проєкту, який друкує текст + код виходу (без --json); route легко структурує його стабільні рядки ERROR: / WARN: у {ok, errors[], warnings[]} — успіх визначає банер, а не код виходу. М’яка відмова {available:false} в автономних установках. tests/cv-sync-check-route.test.mjs. Набір: 2602.

[1.203.0] — 2026-08-15

Додано — підказку «повторно використати попереднє резюме?» у CV Studio.

Додано

  • Коли ви відкриваєте збережену вакансію в CV Studio, застосунок тепер порівнює її з іншими вашими збереженими вакансіями (детерміноване перекриття слів, без токенів) і підказує, чи найближча схожа настільки, щоб повторно використати те адаптоване резюме, використати його з правками чи готувати нове — щоб не створювати з нуля для ролі, над якою ви вже працювали.

Примітки

  • Новий route лише для читання GET /api/jds/:name/reuse ретранслює jd-similarity.mjs з батьківського проєкту (перекриття Жаккара + перевірка сеніорності; JSON {decision, score, reason}) по разу на кожну попередню вакансію (fan-out обмежено 25, береться найкраще); м’яка відмова {available:false}, якщо скрипта чи попередніх вакансій немає. tests/jd-similarity-reuse-route.test.mjs. Набір: 2594.

[1.202.0] — 2026-08-15

Додано — знайдіть ATS-дошку компанії просто на #/portals і почніть відстежувати її.

Додано

  • На #/portals введіть назву компанії — застосунок перевірить Greenhouse, Ashby і Lever на її публічну дошку (без LLM, без браузера) і покаже дошки, які існують і зараз мають ≥1 вакансію. Один клік додає обрану дошку до компаній, за якими стежить сканер. Перевірка лише для читання; запис у portals.yml відбувається тільки після натискання Додати.

Примітки

  • Новий server/lib/discover-ats.mjs (проба слага з фіксованим хостом і перевіркою charset через safeGet із закріпленим DNS, ≤12 проб на запит) + POST /api/portals/discover (лише читання) та POST /api/portals/track (явний запис: withFileLock + текстова вставка + перевірка повторного парсингу + атомарна заміна; лише відомі ATS-хости, ідемпотентно). Повторно використовує реєстр адаптерів сканера. i18n ×17. tests/discover-ats-resolver.test.mjs + tests/discover-ats-route.test.mjs. Набір: 2588.

[1.201.0] — 2026-08-15

Виправлено — трекер із локалізованими або альтернативними заголовками стовпців більше не відображається порожнім.

Виправлено

  • Якщо ваш data/applications.md використовує неанглійські чи альтернативні заголовки — іспанські empresa / puesto / estado / fecha / enlace, або position / stage / link — трекер читав їх за неправильними ключами й показував порожні стовпці Компанія / Роль / Статус / Дата / Посилання. Тепер такі заголовки згортаються до канонічних імен полів, і трекер відображається правильно. Англомовний трекер обробляється точно як раніше.

Примітки

  • Нова карта HEADER_ALIASES і згортка нормалізації в parseApplications (server/lib/parsers.mjs); невідомі або вже канонічні заголовки проходять без змін. tests/tracker-header-aliases.test.mjs. Набір: 2563.

[1.200.0] — 2026-08-15

Додано — перевірка «ще активна?» для вакансій на ATS одним кліком у трекері.

Додано

  • На #/tracker відгук, чий URL — вакансія Greenhouse / Lever / Ashby / Workday / SmartRecruiters, тепер має кнопку «Ще активна?». Один клік запитує публічний JSON самого ATS — без токенів, без браузера — і показує Активна / Закрита / Невідомо, щоб знаходити мертві вакансії, не відкриваючи кожну. Консервативно за замовчуванням: лише явний 404/410 читається як Закрита; усе неоднозначне лишається Невідомо (ніколи хибної Закрита).

Примітки

  • Нові server/lib/liveness-core.mjs + liveness-api.mjs і route лише для читання GET /api/liveness?url= (без записів, без LLM). Безпечно щодо SSRF: URL проходить isValidJobUrl, далі до API ATS звертаємось лише через safeGet (із закріпленим DNS), з фіксованим хостом і перевіреними сегментами. tests/liveness-core.test.mjs + tests/liveness-route.test.mjs. Набір: 2557.

[1.199.0] — 2026-08-15

Виправлено — широкі таблиці тепер прокручуються вбік, а не обрізаються.

Виправлено

  • На сторінці Scan (та в усіх інших таблицях — Трекер, Статистика, Використання, Дашборд) таблиця, ширша за вікно, обрізалася без смуги прокрутки, і до правих стовпців було не дістатися. Тепер широкі таблиці показують горизонтальну смугу прокрутки за потреби, тож будь-який стовпець доступний за будь-якої ширини.

Примітки

  • .table-wrap у public/css/components.css переключено з overflow: hidden на overflow-x: auto (як у наявного контейнера .reports-scroll); заокруглену рамку збережено. tests/table-wrap-scroll.test.mjs. Набір: 2540.

[1.198.0] — 2026-08-15

Додано — повтори сканування тепер використовують експоненційну затримку, джитер і враховують Retry-After від обмежувача частоти.

Додано

  • Коли дошка вакансій ненадовго обмежує частоту або віддає помилку (HTTP 429 / 5xx) під час сканування, повтор тепер чекає з експоненційною затримкою + джитером замість фіксованої короткої паузи — зайняту дошку не гатять з тим самим інтервалом, а одночасні повтори не стикаються синхронно. Надісланий дошкою Retry-After враховується (але з обмеженням, щоб ворожий Retry-After: 86400 не застопорив усе сканування). Постійні помилки (404, відхилені перенаправлення) досі падають одразу — без змін.

Примітки

  • Нові parseRetryAfterMs() і чиста computeRetryDelayMs() у server/lib/http-json.mjs; fetchJson тепер захоплює .retryAfter на не-ok відповіді, а fetchJsonWithRetry приймає необов’язковий maxDelayMs (типово 8000). tests/http-json.test.mjs (+9). Набір: 2536.

[1.197.0] — 2026-08-14

Додано — відстежуйте дошку вакансій VC-фонду Getro лише за її careers_url; id колекції визначається автоматично.

Додано

  • Відстежуваній дошці Getro (b2venture, Earlybird, Point Nine, …) більше не потрібен знайдений вручну числовий getro_collection. Вкажіть власний careers_url дошки, і id визначиться автоматично з цієї сторінки під час першого сканування — один захищений від SSRF GET читає числовий network.id прямо з вбудованих даних сторінки. Явний getro_collection досі має пріоритет і повністю пропускає завантаження.

Примітки

  • Нові httpsCareersUrl(), extractCollectionId() та асинхронний resolveCollectionId() у server/lib/sources/getro.mjs; сторінка дошки завантажується через safeGet (закріплений DNS, обмежений розмір), а визначений id досі прив’язаний до хоста api.getro.com через assertGetroUrl. Адаптер тепер збігається із записом provider: getro, що несе https careers_url, навіть без id. tests/sources-getro.test.mjs (+8). Набір: 2527.

[1.196.0] — 2026-08-14

Виправлено (безпека) — адаптер Workday перевіряє ендпоінт api за іменем хоста, а не за підрядком.

Виправлено

  • Значення api: Workday у portals.yml тепер приймається лише коли його ім’я хостаmyworkdayjobs.com (або піддомен .myworkdayjobs.com). Стара перевірка була пошуком підрядка, тож будь-який URL, що просто містив рядок — напр. https://example.com/?x=myworkdayjobs.com — проходив і міг бути використаний як ендпоінт. Справжні ендпоінти Workday не зачеплені. (Повідомлено CodeQL, #443.)

Примітки

  • Новий isWorkdayApi() парсить URL і перевіряє хост (server/lib/portals/adapters/workday.mjs). tests/workday-adapter-endpoint.test.mjs (+1). Набір: 2522.

[1.195.0] — 2026-08-14

Продуктивність (сканер) — виявлення переоприлюднень лишається швидким на великих історіях сканів.

Продуктивність

  • Виявлення дублів більше не деградує в O(N²) на великому scan-history.tsv. Групування заголовків за компанією було вкладеним циклом, що платив повний roleFuzzyMatch на кожну пару; тепер це інвертований індекс — за один прохід бакетуємо рядки за точним заголовком, потім нечітке порівняння лише між РІЗНИМИ бакетами, що ділять розрізнювальний (небазовий) токен. Вивід ідентичний — ті самі кластери — доведено диференційним тестом проти старого алгоритму на 200+ випадкових історіях.

Примітки

  • groupRowsByTitle у server/lib/detect-reposts.mjs (експортовано для диф-тесту). tests/detect-reposts-grouping.test.mjs (+2). Набір: 2521.

[1.194.0] — 2026-08-14

Виправлено (сканер) — кар’єрні сторінки Workday з URL з одного сегмента тепер скануються коректно.

Виправлено

  • Адаптер Workday тепер розбирає кар’єрні URL, шлях яких — один сегмент — напр. https://parsons.wd5.myworkdayjobs.com/Search, .../KBR_Careers, .../Careers. Раніше сайт падав у External, адаптер бив не в той CXS-ендпоінт, і проба могла виглядати справною, нічого не повертаючи. Тепер береться перший непорожній сегмент шляху як сайт (відкидаючи мовний префікс на кшталт en-US); документований випадок /en-US/External не змінюється. (Повідомлено в #255.)

Примітки

  • Структурний розбір шляху в server/lib/portals/adapters/workday.mjs. tests/workday-adapter-endpoint.test.mjs (+7). Набір: 2519.

[1.193.0] — 2026-08-14

Додано (статистика) — вкладка «Тиша після співбесіди», що показує співбесіди, яким варто нагадати.

Додано

  • Вкладка Тиша після співбесіди в #/stats: співбесіди, що замовкли після «ввічливого» вікна (типово 30 днів), поєднуючи активні співбесіди й трекер — із тим, як довго кожна мовчить, датою останньої співбесіди та причиною. М’який список нагадувань/закриття; тільки рекомендації, ніколи не заява про відмову. Без токенів.

Примітки

  • Новий релей GET /api/stats/rejection-latency (fail-soft {available:false}). tests/stats-rejection-latency-route.test.mjs (+2). +10 ключів i18n ×17; help-hint #/stats з 7→8 вкладок. Набір: 2510.

[1.192.0] — 2026-08-14

Додано (cv-studio) — запобіжник «Перевірка фактів у CV», що ловить числа, яких у вас не було.

Додано

  • Картка Перевірка фактів у CV в #/cv-studio: вставте адаптоване CV чи лист і звірте кожну метрику й факт із вашими справжніми CV, профілем та two-pager. Ви отримуєте вердикт pass / warn / block плюс точні вигадані метрики, необґрунтовані факти та заборонені / попереджувальні фрази. Без LLM; нічого не пишеться.

Примітки

  • Новий релей POST /api/cv-studio/verify-facts: пише текст у одноразовий тимчасовий файл і запускає verify-cv-facts.mjs, довіряючи JSON-вердикту, хоча скрипт виходить з 1 на block. tests/cv-studio-verify-facts-route.test.mjs (+4). +15 ключів i18n ×17. Набір: 2508.

[1.191.0] — 2026-08-14

Додано (статистика) — вкладка «Що вчити далі», що ранжує навички для вивчення.

Додано

  • Вкладка Що вчити далі в #/stats: зведення по всьому трекеру — відсутні навички, що найчастіше топили низьку відповідність, зважені (за 5−оцінкою відповідності в кожному звіті) і за грейдами Critical / High / Medium — плюс ті, що вже покриті вашим CV/профілем. Лише читання, тільки рекомендації, без токенів.

Примітки

  • Новий релей GET /api/stats/upskill (поле { error } за браку даних; fail-soft {available:false}). tests/stats-upskill-route.test.mjs (+3). +15 ключів i18n ×17. Набір: 2504.

[1.190.0] — 2026-08-14

Додано (трекер) — панель «Історія компанії», що показує, які компанії реально вам відповідають.

Додано

  • Картка Історія компанії на #/tracker: виберіть компанію й отримайте свідчення лише для читання — наскільки вона відповідала вам (мовчить у відповідь / змішано / відповідала раніше) і чи переоприлюднюється та сама вакансія — поєднуючи трекер, follow-up та історію сканів. Без токенів; сканер не викликається.

Примітки

  • Новий релей GET /api/stats/company-history[?company=] (fail-soft {available:false}). tests/stats-company-history-route.test.mjs (+3). +18 ключів i18n ×17. Набір: 2501.

[1.189.0] — 2026-08-14

Виправлено (сканер) — рівні грейду, записані римськими цифрами, тепер враховуються й у нелатинських заголовках.

Виправлено

  • Класифікатор грейду за skip_tiers тепер читає суфікс рівня римськими цифрами (I / II / III / IV / V) після слова посади в будь-якій писемності — «Инженер III», «エンジニア I», «Ingénieur IV» — не лише після ASCII-слів. Раніше цифра рівня після нелатинського слова ігнорувалася й вакансія падала до mid, тож skip_tiers: [senior] чи [entry] їх пропускав.

Примітки

  • Незалежний від писемності lookbehind у server/lib/classify-tier.mjs; видалено мертвий дубль-матчер Sr.. tests/classify-tier.test.mjs (+1). Набір: 2498.

[1.188.0] — 2026-08-14

Виправлено (UI) — основні кнопки дій більше не притиснуті впритул до підзаголовка сторінки.

Виправлено

  • Рядок основної дії / керування на сторінках Щотижневий дайджест співбесід, Профінансовані компанії, Портали, Кар’єрний план та Кар’єрна орієнтація тепер має верхній відступ, тож кнопка «дихає» під підзаголовком, а не впирається в нього.

Примітки

  • Регрес-гард tests/lead-row-top-margin.test.mjs (+5). Набір: 2497.

[1.187.0] — 2026-08-14

Виправлено (сканер) — налаштування skip_tiers знову працює: вакансії, які ви просили пропускати за грейдом, відкидаються.

Виправлено

  • Список skip_tiers: у portals.yml (напр. skip_tiers: [intern, entry]) тепер враховується скануванням. Заголовок кожної вакансії класифікується за грейдом (intern / entry / mid / senior) і відкидається, якщо його грейд у списку. Раніше сканування застосовувало фільтри заголовка / локації / контенту / довіри, але не мало фільтра за грейдом, тож skip_tiers мовчки ігнорувався. Заголовки без слова рівня потрапляють у mid (тож skip_tiers: [mid] відкидає й більшість звичайних вакансій), а класифікатор читає НАЙЛІВІШЕ слово рівня.

Примітки

  • Новий чистий модуль server/lib/classify-tier.mjs (classifyTier + buildTierFilter), підключений до ланцюгів фільтрів EN- та RU-сканерів. tests/classify-tier.test.mjs (+7). Набір: 2492.

[1.186.0] — 2026-08-14

Додано (CV-студія) — панель «Розрив навичок»: які з потрібних навичок вакансії ваше CV називає, натякає або яких немає.

Додано

  • Нова панель Розрив навичок у CV-студії. Виберіть збережену вакансію — і кожен потрібний навик розподіляється на названо у вашому CV, мається на увазі в CV або відсутнє — порівняння слів без LLM, нічого не записується. Якщо у вакансії не було чіткого розділу вимог, з’являється позначка про низьку впевненість.

Примітки

  • Новий GET /api/jds/:name/skill-gap (ім’я вакансії проходить path-санітизацію та перевірку наявності під jds/, перш ніж стати аргументом; м’який відкат до {available:false} без скрипта). +13 ключів i18n ×17. Тести: tests/jds-skill-gap-route.test.mjs (+4, включно з відмовою на path-traversal). Набір: 2485.

[1.185.0] — 2026-08-14

Додано (статистика) — вкладка «Лійка та швидкість»: як ваша лійка співвідноситься з ринком і як швидко ви рухаєтесь між етапами.

Додано

  • Нова вкладка Лійка та швидкість у Статистиці показує вашу частку відповідей і частку інтерв’ю поряд із ринковими орієнтирами (із застереженнями про малу вибірку та зміщення відбору), список очікування активних відгуків поза типовим вікном першої відповіді та медіанні дні за етапами (Відгук → Відповідь → Інтерв’ю → Оффер) — повільні рядки цензуруються справа, щоб не зміщувати медіани. Лише читання, нуль токенів; читає лише ваш трекер.

Примітки

  • Новий GET /api/stats/funnel (м’який відкат до {available:false} без скрипта). +18 ключів i18n ×17. Тести: tests/stats-funnel-route.test.mjs (+2). Набір: 2481.

[1.184.0] — 2026-08-14

Виправлено (UI) — плитки швидких дій на Дашборді тепер вишиковуються в рівну сітку.

Виправлено

  • На Дашборді (Командному центрі) група з 3 плиток малювалася ширше, ніж із 4, через що секції мали рваний правий край. Тепер кожна група використовує колонки однакової ширини (4 на широкому екрані, зі зниженням до 3 / 2 / 1 у міру звуження вікна), тож усі плитки однакового розміру, а їхні праві краї вирівняні.

Примітки

  • Лише CSS (.qa-grid: фіксоване repeat(N, minmax(0,1fr)) замість auto-fill). Захищено tests/dashboard-grid-align.test.mjs (+2). Набір: 2479.

[1.183.0] — 2026-08-14

Додано (сканер) — розумніше виявлення дублікатів: та сама вакансія, перевикладена з посиланням-трекером, більше не показується двічі.

Додано

  • Сканер тепер розпізнає вакансію за канонічним URL-ключем, тож та сама вакансія, перевикладена з параметром-трекером (?utm_…, gclid, …), через http vs https, або з кінцевим слешем / #фрагментом, вважається однією вакансією — без дубля в результатах скану чи пайплайні й без марної оцінки вже баченої вакансії. Справді різні вакансії (збережений функціональний id на кшталт gh_jid) досі рахуються окремо.

Примітки

  • Новий server/lib/url-key.mjs, підключений до дедупу обох сканерів і до запису пайплайна. Навмисно недо-нормалізує — ніколи не зливає дві різні вакансії. Тести: tests/url-key.test.mjs (+5), tests/parsers.test.mjs (+1). Набір: 2477 (+6).

[1.182.0] — 2026-08-14

Виправлено (сканер) — діапазони зарплат тепер однаково читаються всіма мовами.

Виправлено

  • Суми зарплат у рядках скану та трекера використовують локале-нейтральні символи і (напр. ≥ 120000 EUR, ≤ 90000) замість англійських слів “from” / “up to”, які потрапляли без перекладу в неанглійський інтерфейс. Стосується всіх дощок з одностороннім діапазоном (Getro, Remotli, Manfred, Agentic Jobs, JustJoin, Jobicy); двосторонні діапазони (100000–150000 USD) і так були нейтральні.

Примітки

  • Лише відображення — клієнтський фільтр зарплати розбирає числа незалежно від префікса, тож фільтрація не змінюється. Набір: 2471.

[1.181.0] — 2026-08-14

Додано (сканер) — дошки Getro тепер показують зарплату, усі локації та віддалені вакансії.

Додано

  • Сканер Getro (дошки венчурних talent-мереж) тепер показує зарплату за кожною вакансією (річний діапазон + валюта), перелічує всі локації замість лише першої та позначає віддалені вакансії. Вакансія Getro у скані та трекері тепер несе ті самі дані про зарплату й локацію, що й інші дошки.

Примітки

  • Лише сканер; без нових залежностей, без змін маршрутів / CSP / SSRF. Тести: tests/sources-getro.test.mjs (+5). Набір: 2470 (+5).

[1.180.0] — 2026-08-14

Виправлено (СЕРЕДНЄ, звіти) — список #/reports тепер таблиця, і відновлено справжню оцінку, яку приховував заповнювач Machine Summary.

Виправлено

  • Список #/reports — це таблиця (Звіт · Дата · Легітимність · Оцінка), а не сітка з 4 карток. Довгий чип «Оцінку не виявлено» стискав колонку заголовка майже до нуля, а overflow-wrap: anywhere у заголовку картки переносив назву звіту по одному символу. Тепер кожне поле має власну колонку, клітинка назви переноситься за словами, а на вузькому екрані таблиця прокручується горизонтально (новий контейнер .reports-scroll). Новий ключ i18n rep.colReport ×17.
  • Справжня оцінка в тілі (**Итоговый балл:** 1.8 / 5) більше не приховується заповнювачем Machine Summary (score: —). Коли блок ## Machine Summary містив нечислову або поза діапазоном оцінку, вона займала слот розібраної оцінки й блокувала запасний розбір за формою-значенням у жирному, тож звіт показував «Оцінку не виявлено», попри справжній X / 5 у тілі. Тепер parseReportHeader відновлює форму-значення з тіла, якщо придатного числа не залишилося (крок 4.5).

Примітки

  • Лише клієнт + парсер; без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/reports-table.test.mjs (+5), tests/report-header-locale.test.mjs (+2). Набір: 2465 (+7).

[1.179.0] — 2026-08-13

Змінено (LOW, сканер) — 20 дубльованих декодерів HTML-сутностей зведено до спільного модуля (наслідок паритету, закриває worklist).

Змінено

  • 20 скрейпінг-джерел мали кожне свій decodeEntities/decodeXmlEntities (+ хелпер fromCodePoint) — копії, що дрейфували (три могли кинути RangeError, виправлено в v1.172.0; інші пропускали NUL/C0 або неправильно розбирали &#1a2;). Тепер усі проходять через єдиний server/lib/html-entities.mjs (декодер, безпечний за XML 1.0 Char), прибрано ~237 рядків дублювання. 8 RSS-подібних джерел отримали декодування &nbsp; (раніше знали лише 5 сутностей); навмисне подвійне декодування cryptocurrencyjobs збережено через аліас. hh лишає свій декодер (обробляє &mdash;/&ndash;, поза спільними 6). Новий гард-тест падає, якщо джерело знову заведе локальний декодер.

Примітки

  • Рефакторинг зі збереженням поведінки; без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/decoder-consolidation.test.mjs (+2). Набір: 2458 (+2).

[1.178.0] — 2026-08-13

Виправлено (LOW, паритет з батьком) — дві застарілі константи оновлено під батька (PARENT-SYNC GAP #4 + #5).

Виправлено

  • User-Agent браузера (GAP #4)BROWSER_LIKE_USER_AGENT (його шлють workable/workday/oraclecloud/a16z/eightfold для проходження WAF/бот-фільтрів) піднято з Chrome 131 до 151 під батьківський user-agent.mjs; застарілу збірку частіше блокують. Захищено тестом Chrome major ≥ 151.
  • FALLBACK станів трекера (GAP #5) — резервний FALLBACK у states.mjs (використовується лише коли живий templates/states.yml нечитабельний — свіжий клон / ізольований CI-корінь) отримав турецькі аліаси станів із батька (#2615): değerlendirildi, başvuruldu, yanıt verildi, mülakat, teklif, reddedildi, iptal edildi, uygun değil, kabul edildi/işe alındı. У проді живий файл уже їх давав.

Примітки

  • Лише дві константи; без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/http-json.test.mjs (+1) + tests/states.test.mjs (+1). Набір: 2456 (+2).

[1.177.0] — 2026-08-13

Виправлено (MEDIUM, сканер) — csod (Cornerstone) повертав 0 вакансій в орендарів, які закривають search-API сесійними куками (parent #2769, PARENT-SYNC GAP #1).

Виправлено

  • Деякі орендарі Cornerstone виставляють сесійні куки на bootstrap-сторінці кар’єрного сайту й відповідають 401 CSOD Unauthorized на search-API, поки ці куки не повернуться разом з анонімним bearer-токеном. sources/csod.mjs тепер читає bootstrap через новий хелпер fetchResponse, будує заголовок Cookie зі значень Set-Cookie (cookieHeaderFrom — лише name=value, семантика jar) і повторює його в POST пошуку. Лише той самий origin (хост закріплено + redirect:'error'), тож сесійні куки не потраплять третій стороні; орендар без кук поводиться як раніше.

Примітки

  • Новий server/lib/http-json.mjs::fetchResponse (аддитивно; наявні джерела не зачеплено). Без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/sources-parity-v1118a.test.mjs (+1). Набір: 2454 (+1).

[1.176.0] — 2026-08-13

Виправлено (MEDIUM, звіти) — бал під жирною міткою, якої немає в RU-таблиці, все ще показував «Score not detected» (FIND-5).

Виправлено

  • Два RU-звіти записували бал як **Итоговый балл:** 1.8 / 5 / **Скор:** 1.8 / 5 — жирні мітки, яких REPORT_LABELS.ru не перелічує (знає лише «Оценка»/«Балл»), тож бал не розбирався. Замість нарощування списку синонімів parseReportHeader тепер відкочується до форми значення: дробу за рубрикою /5 під БУДЬ-ЯКОЮ жирною міткою. Це незалежно від мови, несприйнятливо до заголовка (немає **, немає значення /5) і відхиляє дату на кшталт 5/5/2026 (негативний перегляд уперед за знаменником).

Примітки

  • Лише парсер на сервері; без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/report-header-locale.test.mjs (+2). Набір: 2453 (+2).

[1.175.0] — 2026-08-13

Виправлено (LOW, зміцнення) — регрес-гард для SEO-опису з FIND-3 + null-безпечне очищення легітимності (за наслідками AI-рев’ю).

Виправлено

  • Гард паритету SEO-опису — правка v1.174.0, що замінила жорстко зашите «~55» у meta.desc кожної мови на плейсхолдер {adapters} з реєстру, не мала тесту й могла тихо регресувати при наступному редагуванні локалі. Новий CI-ізольований tests/site-meta-desc-parity.test.mjs падає, якщо будь-який із 17 site/src/i18n/*.json втрачає плейсхолдер або знову зашиває число, або якщо Landing.astro перестає підставляти його в усі три meta-описи.
  • Null-безпечне очищення легітимностіstripEmphasis повертає '' для nullish-входу замість рядка «undefined» (поля ініціалізуються рядком, тож це захист углиб).

Примітки

  • Тест + однорядковий гард у парсері; без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/site-meta-desc-parity.test.mjs (+3). Набір: 2451 (+3).

[1.174.0] — 2026-08-13

Виправлено (HIGH, звіти) — локалізовані звіти показували «Score not detected»; SEO-опис застарів.

Виправлено

  • Розбір оцінки (FIND-1) — звіт не англійською, чий H1 містить слово-мітку оцінки (# Оценка вакансии: <назва>), більше не приймає цю назву за оцінку. parseReportHeader тепер якориться на локалізованій жирній мітці (**Оценка:** 1.5 / 5), пропускає рядки-заголовки і вимагає, щоб мітка стояла впритул до двокрапки — тож RU-звіти, що показували «Score not detected», відображають справжню оцінку.
  • Чип легітимності (FIND-2) — зі значення прибираються markdown-зірочки, тож чип показує «High Confidence», а не «** High Confidence».
  • Переповнення оцінки — рядок оцінки з хвостовим статусним текстом («1.8, Status: Evaluated, …») стискається до самої оцінки; у .score-pill з’являється обмеження без переносу/overflow, а колонка заголовка може стискатися — тож кольоровий чип не вилазить за край картки.
  • SEO-опис (FIND-3) — meta / OG / Twitter-описи cvstart.org (усі 17 мов) жорстко містили «Scan ~55 job boards», тоді як тіло сторінки рахувало реальний реєстр («~75»). Тепер опис підставляє число з реєстру, тож розсинхрон не повториться.

Примітки

  • Парсер на сервері + рендер/CSS клієнта + i18n сайту; без змін маршрутів / CSP / SSRF / запису в батьківський проєкт. Тести: tests/report-header-locale.test.mjs (+4). Набір: 2448 (+4).

[1.173.0] — 2026-08-13

Додано (LOW, налаштування) — Hermes додано до списку виявлюваних AI-CLI (паритет із career-ops).

Додано

  • Вкладка #/config → «Інструменти AI CLI» тепер виявляє Hermes (Nous Research) — нове середовище виконання агента, яке підтримує батьківський проєкт (бінарник hermes). Фіксований список у server/lib/routes/cli-detect.mjs зростає з 10 до 11 інструментів; виявлення лишається скануванням PATH лише для читання (жоден бінарник не запускається).

Примітки

  • Без змін i18n / маршрутів / CSP / SSRF / запису в батьківський проєкт; список фіксований, не ввід. Набір: 2444 (канарка cli-detect оновлена 10 → 11).

[1.172.0] — 2026-08-13

Виправлено (MEDIUM, сканер) — некоректна HTML-сутність могла обвалити джерело сканування (паритет із career-ops #2150).

Виправлено

  • Джерела oraclecloud, gem і dassault декодували числові HTML-сутності зі слабкою перевіркою Number.isFinite перед String.fromCodePoint — посилання понад 0x10FFFF (наприклад, &#99999999; з некоректного чи ворожого фіду) кидало неперехоплений RangeError і переривало весь розбір цього джерела. Спільний модуль server/lib/html-entities.mjs (дзеркало _html-entities.mjs батьківського проєкту) тепер обмежує числові посилання набором Char XML 1.0 §2.2, тож String.fromCodePoint не може кинути виняток, і зіставляє шістнадцяткові та десяткові окремо, тому &#1a2; більше не розбирається неправильно. Три джерела його імпортують.

Примітки

  • Для коректних фідів поведінка не змінюється; без змін JS / i18n / маршрутів / CSP / SSRF / запису в батьківський проєкт. Консолідація решти ~20 копій декодера у джерелах відстежується в qa/PARENT-SYNC-WORKLIST-v1.26.0.md.
  • Тести: tests/html-entities.test.mjs (+7). Набір: 2444 (+7).

[1.171.0] — 2026-08-13

Змінено (НИЗЬКЕ, дизайн-система) — токени типографічної шкали + шари z-index (D-4, перший крок). Розміри та накладання були літералами в кожному компоненті.

Змінено

  • Шари z-index — додано токени --z-* (--z-topbar--z-skiplink) і мігровано кожен літерал z-index. Значення збережені, порядок накладання ідентичний; новий канарковий тест забороняє нові «магічні» числа.
  • Типографічна шкала — рампа --font-size-* (xs 112xl 28, base = Inter 15px); мігровано основні розміри (без візуальних змін). Значення поза рампою мігрують поступово (docs/UX-ROADMAP.md).

Примітки

  • Лише CSS-токен; без змін поведінки/JS/i18n/маршруту/CSP/SSRF/запису. Без піксельних змін. tests/design-tokens-scale.test.mjs (+3). Набір: 2437 (+3).

[1.170.0] — 2026-08-13

Додано (НИЗЬКЕ) — чесні підказки ETA для довгих AI-генерацій (P4-ETA). Важкі генерації (кар’єрний план ~40 с, орієнтація / ринок / нетворкінг ~30 с, two-pager ~20 с) показували лише «Генерація…» без відчуття тривалості.

Додано

  • Поруч із кожною кнопкою довгої генерації тепер показується приглушена підказка ⏱ ~N с (як ETA на #/auto). Спільний стиль .eta-hint + два узагальнені ключі (common.eta ~{n}s, common.etaTitle).

Примітки

  • Лише клієнт; без змін маршруту/CSP/SSRF/запису. +2 ключі i18n ×17 (знімок 1219 → 1221). tests/generation-eta-hint.test.mjs (+2). Набір: 2434 (+2).

[1.169.0] — 2026-08-13

Додано (НИЗЬКЕ) — inline-перегляд PDF (D-5). GET /api/output/pdfs/:name примусово ставив Content-Disposition: attachment, тож навіть кнопка «Відкрити» на #/cv завантажувала файл, а не показувала його.

Додано

  • ?inline=1 віддає ТОЙ САМИЙ перевірений файл із Content-Disposition: inline, для 👁 Перегляду в новій вкладці; за замовчуванням — досі завантаження. Без нових маршрутів; ті самі перевірки імені.
  • Перша кнопка списку PDF на #/cv тепер 👁 Перегляд поруч із ⬇ Завантажити. cv.openPdf «Відкрити» → «Перегляд» ×17.

Примітки

  • Без змін CSP/SSRF — той самий sanitizePathName. Один наявний ключ i18n переписано ×17 (знімок 1219). tests/output-pdfs.test.mjs (+3). Набір: 2432 (+3).

[1.168.0] — 2026-08-13

Виправлено (НИЗЬКЕ, a11y) — рядки чекбоксів тепер відповідають мінімуму 24×24 WCAG 2.5.8 (D-2). Мітки чекбоксів/радіо на #/scan, #/config, #/evaluate і #/cv-studio були в смузі ~22 px.

Виправлено

  • Обмежене правило label:has(> input[type="checkbox"/"radio"]) { min-height: 24px } гарантує смугу ≥24 px. Лише min-height — мітки вже flex, нічого не зсувається; .apply-checklist (32 px) уже відповідав.

Примітки

  • Лише CSS; без змін поведінки/JS/i18n/маршруту/CSP/SSRF/запису. tests/checkbox-target-size.test.mjs (+1). Набір: 2429 (+1).

[1.167.0] — 2026-08-13

Виправлено (НИЗЬКЕ, дизайн-система) — підняті поверхні тепер відділяються від розділювачів (D-3). Токени --panel-2 / --surface-elev1 розв’язувалися в --slate — те саме значення, що й розділювачі --line / --border — без візуального відділення.

Виправлено

  • Окремий токен з урахуванням теми --elev (світла #eef1f6 / темна #1e232e, відрізняється від --slate в обох темах) тепер стоїть за піднятими поверхнями; розділювачі лишаються на --slate. Решта знахідок (D-2, D-4, D-5, P4-ETA) — у беклозі docs/UX-ROADMAP.md.

Примітки

  • Лише CSS-токен; без змін поведінки/JS/i18n/маршруту/CSP/SSRF/запису. tests/elevation-token.test.mjs (+2). Набір: 2428 (+2).

[1.166.0] — 2026-08-13

Виправлено (НИЗЬКЕ) — термінологія рубрики тепер збігається з канонічною документацією. career-ops.org/docs описує «п’ять вимірів плюс цілісна загальна оцінка», але web-ui, cvstart.org і вікі казали «рубрика з шести вимірів» (5 + 1 = 6, але формулювання розходилося).

Виправлено

  • Прийнято формулювання документації — «п’ять вимірів плюс цілісна загальна оцінка» — узгоджено в README ×17, на сайті cvstart.org ×17, у довідці ×17, у docs/career-ops-canonical.md і у вікі (Home ×17 + Features).

Примітки

  • Лише docs/маркетинг; без змін коду/ключа i18n/маршруту/CSP/SSRF/запису. tests/rubric-terminology.test.mjs (+2). Набір: 2426 (+2).

[1.165.0] — 2026-08-13

Виправлено (НИЗЬКЕ) — термін «Two-pager» тепер узгоджений у межах кожної локалі. В арабській бічна панель показувала латинське «Two-pager», тоді як <h1> був повністю локалізований — єдиний латинський рядок у дзеркальній RTL-навігації.

Виправлено

  • Рішення зафіксовано: для кожної локалі nav.twoPager і twoPager.title узгоджують термін (обидва латиною або обидва локалізовані). Розходилася лише арабська; її пункт навігації тепер локалізований («الصفحتان»). Новий канарковий тест падає, якщо якась локаль знову їх розведе.

Примітки

  • Лише текст; без змін маршруту/CSP/SSRF/запису. Змінено 1 значення i18n (ar); без нових ключів (знімок 1219). tests/two-pager-term-consistency.test.mjs (+2). Набір: 2424 (+2).

[1.164.0] — 2026-08-13

Виправлено (НИЗЬКЕ) — плейсхолдер пошуку у верхній панелі більше не переповнюється жодною мовою. «Find a company, role or URL…» обрізався (nowrap), коли рядок пошуку стискався; половина «…or URL» ніколи не була видима.

Виправлено

  • top.search (×17) тепер короткий «Пошук або вставте URL» (≤24 символи кожною мовою), вміщується навіть у вузькому рядку й зберігає підказку про URL. Резервний плейсхолдер у index.html збігається; aria-label зберігає повний опис.

Примітки

  • Лише текст; без змін маршруту/CSP/SSRF/запису. Один наявний ключ i18n переписано ×17 (нових немає; знімок 1219). tests/search-placeholder-fit.test.mjs (+2). Набір: 2422 (+2).

[1.163.0] — 2026-08-13

Виправлено (НИЗЬКЕ) — вбудований асистент «Запитати документацію» тепер охоплює експорт звіту в PDF. Раніше відповідав, що посібник цього не описує, хоча на #/reports/:slug є робоча кнопка 📄 Generate PDF.

Виправлено

  • До всіх 17 пакетів довідки під §10 «Звіти» додано H3 «Експорт звіту в PDF» (де кнопка, файл потрапляє в output/*.pdf, потрібен Playwright, перевірити перед надсиланням). Пошук асистента тепер показує розділ «Звіти».

Примітки

  • Лише docs/довідка; без змін коду/маршруту/CSP/SSRF/запису. Гейт довідки 112 → 113 H3 (31 H2 без змін). tests/help-reports-pdf-section.test.mjs (+2). Набір: 2420 (+2).

[1.162.0] — 2026-08-13

Виправлено (СЕРЕДНЄ) — «?»-підказка тепер ≥24×24 (WCAG 2.5.8). .help-hint був 18×18 px з padding:0 — нижче мінімуму, на кожному заголовку.

Виправлено

  • Бокс .help-hint тепер 24×24 (вимірна ціль), а видиме кільце лишається 18px — малюється центрованим ::before, тож гліф і базова лінія <h1> незмінні. Стани hover/активний/фокус слідують за кільцем; відступ 6→3px зберігає проміжок.

Примітки

  • Лише CSS; без змін JS/i18n/маршруту/CSP/SSRF/запису. tests/help-hint-target-size.test.mjs (+2). Набір: 2418 (+2).

[1.161.0] — 2026-08-13

Виправлено (СЕРЕДНЄ) — #/reports показує чип «Оцінку не розпізнано» замість порожнього місця. Після мовно-незалежного парсера v1.159.0 звіт без розбірної оцінки показував порожню область — неможливо відрізнити від збою.

Виправлено

  • Клітинка оцінки тепер розгалужується: є оцінка → тональна плашка; немає оцінки → приглушений чип .score-muted («Оцінку не розпізнано», ×17) з підказкою «Відкрийте звіт…». Картка лишається role="link" з клавіатурним керуванням, дата відображається.
  • Повторно використовує наявний нейтральний токен; без нового кольору.

Примітки

  • Лише клієнт; без змін маршруту/CSP/SSRF/запису. +2 ключі i18n ×17 (знімок 1217 → 1219). Набір: 2416 (+3).

[1.160.0] — 2026-08-13

Виправлено (HIGH) — текст про провайдерів більше не суперечить обіцянці 7 провайдерів. #/config казав, що жива оцінка «використовує ключ Anthropic або Gemini», а ключ OpenAI «вебінтерфейс не використовує»; дашборд показував «Anthropic-first scoring» — хибно від каскаду 7 провайдерів (v1.157.0).

Виправлено

  • config.providerModelNote (×17): тепер вказано, що ⚡ жива оцінка headless працює на будь-якому з семи ключів (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub Models · Hermes), з автопорядком і відкатом. Хибну фразу про OpenAI видалено.
  • dash.quick.evaluateSub (×17): нейтрально («Оцінка відповідності 0–5»). Keys: N / 5N / 7.

Примітки

  • Лише текст; без змін маршруту/CSP/SSRF/запису. Без нових ключів i18n (знімок 1217). Набір: 2413 (+3).

[1.159.0] — 2026-08-13

Виправлено (HIGH) — метадані звіту більше не залежать від мови. Звіти, згенеровані не англійською, показували порожню смугу метаданих на #/reports, бо parseReportHeader розпізнавав лише англійські жирні мітки.

Виправлено

  • parseReportHeader тепер розбирає мовно-незалежний YAML-блок ## Machine Summary (score: / legitimacy: / date: — те саме джерело, що вже читає auto-pipeline): англійські мітки → блок Machine Summary → локалізовані мітки (REPORT_LABELS, 17 локалей). Англійські звіти лишаються байт-у-байт ідентичними.
  • Толерантний розбір числа (1.5/5, 1,5/5, 1.5 из 5, 4.5 out of 5); дата відкочується до mtime файлу, якщо її немає в тілі.

Примітки

  • Лише читання/розбір; без змін маршруту, CSP, SSRF чи запису в батьківський проєкт. Без нових ключів i18n. Набір: 2410 (+8).

[1.158.0] — 2026-08-12

Виправлено — два косметичні баги відображення («?» протікала в заголовок вкладки та неправильна кількість провайдерів на лендингу). Лише відображення; без змін поведінки, безпеки чи потоку даних.

Виправлено

  • «?» з HelpHint більше не протікає в document.title. Роутер брав заголовок вкладки з необробленого h1.textContent, тож вкладка показувала «Vacancy search?» замість «Vacancy search». Тепер router.js::focusNewView клонує заголовок, видаляє .help-hint і лише потім читає текст; видима «?» лишається недоторканою.
  • cvstart.org показував «17 AI providers» замість «7». Хелпер sub() у Features.astro замінював усі {n} кількістю локалей (17) до підстановки за карткою; тепер {n} розв’язується за карткою (провайдери → 7, мови → 17).

Примітки

  • Без змін сервера, маршруту, CSP, SSRF чи ключів i18n; форма facts.json без змін. Набір: 2402 тести (+1).

[1.157.0] — 2026-08-12

Виправлено — живі оцінки тепер виконуються на БУДЬ-ЯКОМУ налаштованому провайдері, не лише Anthropic/Gemini. Користувач лише з OPENROUTER_API_KEY помилково примусово переводився в ручний режим.

Виправлено

  • Першопричина: пін LLM_PROVIDER без ключа (напр., LLM_PROVIDER=claude з init) заходив у глухий кут; тепер відбувається відкат до auto-порядку серед налаштованих провайдерів (у selectActiveProvider + обох каскадах диспетчеризації).
  • Клієнтський гейтинг (#/deep + вью mode-page) тепер використовує window.ProviderStatus (/api/status/providers, усі 7) замість застарілої перевірки Anthropic/Gemini; переписані підказки (deep/eval × 17) + значок «Живі оцінки» на дашборді + config.llmProviderHint.

Примітки

  • Без змін безпеки. Набір: 2401 тестів (+5).

[1.156.0] — 2026-08-12

Рефакторинг — розбити scan.js під ліміт розміру (P-16) + фікс CodeQL. scan.js мав 906 рядків; вилучено дві фабрики, що зберігають поведінку → 648. Завершує пару розбиттів вью P-15/P-16.

Змінено

  • Нові scan/runner.js (рушій виконання скану) і scan/filters.js (машина станів фільтрів) через bag-обʼєкти ctx/refs; scan.js звʼязує обидва.

Виправлено

  • CodeQL js/useless-assignment-to-local (#428) у config/tab-controller.js: let n = i;let n;.

Примітки

  • Чистий рефакторинг, без зміни поведінки; 4 тести, що читають джерело, перенаправлено. Обидва великі вью тепер під 800 (P-15/P-16 готово). Набір: 2396 тестів.

[1.155.0] — 2026-08-12

Рефакторинг — розбити config.js під ліміт розміру (P-15). config.js мав 1030 рядків (понад ліміт 800); вилучено два модулі, що зберігають поведінку, — тепер 783.

Змінено

  • Нові config/field-specs.js (дані полів + списки моделей) і config/tab-controller.js (фабрика панелі вкладок); config.js посилається на них, логіка рендеру не змінюється.

Примітки

  • Чистий рефакторинг, без зміни поведінки; 6 тестів, що читають джерело, перенаправлено. scan.js (906) лишено як є (вже частково розбитий; ядро надто зв’язане для чистого механічного поділу). Набір: 2396 тестів.

[1.154.0] — 2026-08-12

Новий гайд — «Запуск усього стеку в хмарі». У career-ops немає власної розповіді про хмару/сервер, тож її додано: покроковий рецепт, як розмістити батьківський пайплайн career-ops, цей переглядач career-ops-ui і рушій ШІ (підписка Claude через Claude Code, локальний Hermes, або ключі API) на невеликому завжди увімкненому сервері. Постачається як Довідка §31 17 мовами, розділ README і вікі-сторінка.

Додано

  • Довідка §31 «Запуск усього стеку в хмарі» (× 17) — три частини, підготовка + встановлення, вибір рушія, безпечна публікація (HTTPS зворотний проксі + автентифікація + інваріанти CSP/SSRF/XSS/без-секретів). Бандл довідки зростає до 31 H2 / 112 H3.
  • README — розділ «Запуск усього стеку в хмарі» (× 17) + вікі-сторінка Cloud-Deployment.

Примітки

  • Лише документація — без маршрутів, сервера й змін клієнта; без нових i18n-ключів. 4 тести довідки переведено на контракт 31 H2 / 112 H3. Набір: 2396 тестів (без змін).

[1.153.0] — 2026-08-12

Сканер Jobvite переведено на публічний XML-фід (синк із батьком). Батько вивів з експлуатації JSON API Jobvite (тепер нуль вакансій); source у web-ui використовував той самий мертвий ендпоінт, тож будь-яка відстежувана компанія Jobvite мовчки сканувалася порожньою. Порт батьківського фіксу (#2623): тепер читається публічний по-тенантний XML-фід із ключем companyEId.

Виправлено

  • source звертався до виведеного JSON API і повертав нуль вакансій; тепер він звертається до https://app.jobvite.com/CompanyJobs/Xml.aspx?c={companyEId} і парсить XML <result><job>… (CDATA + сутності, detail-url перед apply-url).

Змінено

  • Розвʼязання companyEId: (1) company_eid: у порталі, (2) параметр c= явного api:, (3) дискавері зі сторінки борду. fetchText (http-json.mjs) додає .location/.retryAfter до non-ok помилки (лише читання, зворотно сумісно).

Примітки

  • Безпека — два хости (jobs.jobvite.com, app.jobvite.com) закріплені через assertJobviteUrl: лише https, суворий allowlist, редиректи не переслідуються. companyEId — лише значення ?c=; кількість sources не змінилася.
  • Набір: 2396 тестів (+4).

[1.152.0] — 2026-08-12

Провайдер Hermes — розведення завершено + актуалізація документації. Код-рев’ю інтеграції Hermes із v1.151.0 виявило дві реальні прогалини й чотири пункти повноти; усе виправлено тут, а перелік LLM-провайдерів по всьому застосунку доведено до повних семи на всіх поверхнях документації та в 17 мовах.

Виправлено

  • #/config не міг форсувати Hermes — випадний список LLM_PROVIDER перелічував лише шість провайдерів, тож HERMES_API_KEY можна було задати, але не форсувати Hermes з UI. Тепер hermes — 8-й варіант, а новий тест паритету не дасть списку знову розійтися з LLM_PROVIDERS.
  • Короткі self-hosted ключі мовчки відхилялися — поріг у 20 символів в isUsableKey розраховано на хмарні ключі; hasHermesKey тепер використовує пом’якшений поріг у 8 (приклад у документації Hermes — 19 символів).

Змінено

  • Перелік провайдерів нормалізовано до повних семи в README (× 17), вбудованій довідці (× 17), словнику config.llmProviderHint (× 17) і docs/sdd; hermesChatUrl добудовує хост без шляху; текст ручного фолбеку називає Hermes.

Примітки

  • Безпека без змін — нових маршрутів і правок SSRF/CSP немає; у health/doctor додано рядок HERMES_API_KEY.
  • Набір: 2392 тестів (+2).

[1.151.0] — 2026-08-12

Hermes тепер підключений LLM-провайдер (Phase 5) — спайк Phase 5 підтвердив, що Hermes від Nous Research має OpenAI-сумісний API Server (hermes gatewayPOST /v1/chat/completions), тож career-ops-ui тепер виконує живі оцінки через локальний Hermes так само, як OpenAI/Qwen. Задайте HERMES_API_KEY у Налаштуваннях застосунку — він входить в auto-порядок (останнім). Закриває останній відкритий пункт роадмапу — Phase 5, Shape A.

Додано

  • LLM-провайдер Hermes (Shape A)runHermes поверх спільного клієнта runOpenAICompatible (server/lib/openai.mjs), у обох каскадах (llm-dispatch.mjs + routes/llm.mjs), у хвості auto-порядку + пін LLM_PROVIDER=hermes, /api/status/providers і llm-pricing.mjs. Звертається до налаштовуваного локального base URL (типово http://127.0.0.1:8642/v1) з Bearer-автентифікацією — це НАЛАШТОВАНИЙ ендпойнт провайдера (як OpenRouter/Qwen), а не URL вакансії від користувача, тож SSRF-guard не задіюється.
  • Поля #/configHERMES_API_KEY (секрет) + HERMES_BASE_URL + HERMES_MODEL (типово hermes-agent), 6 нових ключів i18n × 17 мов (snapshot 1208 → 1214).

Змінено

  • Спайк вирішено: docs/integrations/HERMES.md, вбудована довідка §30 (× 17), тизер README (× 14), навичка hermes-bridge і roadmap переходять з «заплановано / ще не підключено» на підключено (Shape A). Shape B (кастомний relay агентного runtime) не знадобився.

Примітки

  • Безпека: запит провайдера — це налаштований ендпойнт, тієї ж категорії, що й інші OpenAI-сумісні провайдери — без нової SSRF-поверхні, без змін CSP/санітайзера. HERMES_API_KEY — це SECRET_KEY (ніколи не відображається).
  • Тести (ізольовані в CI, заглушка транспорту): tests/hermes-provider.test.mjs (+5); канарку «немає гілки Hermes» з v1.146.0 інвертовано, щоб стверджувати, що вона Є; тести набору провайдерів оновлено до порядку з 7 провайдерів.
  • Набір: 2390 тестів (+5).

[1.150.0] — 2026-08-12

Узгоджені порожні стани (шліфування Phase 4) — кожна панель «поки нічого немає» тепер малюється єдиним спільним стилем .empty, замість того щоб деякі подання переоголошували вигляд інлайн із магічним 40px. Невелике виправлення візуальної узгодженості; порожні стани #/activity, #/cv-studio, #/stats і #/usage тепер збігаються з усіма іншими (токенізований відступ 48px + пунктирна рамка).

Змінено

  • #/activity, #/cv-studio, #/stats, #/usage прибрали свій інлайн style: { padding: '40px', textAlign: 'center', color: 'var(--foggy)' } на порожніх панелях — усі три властивості вже дає спільний клас .empty (--space-7 = 48px, по центру, приглушено, пунктирна рамка). Тож ці чотири малюються ідентично до ~25 інших панелей .empty.
  • Правомірні перевизначення по поданню (#/dashboard width:100%, #/pipeline border:none) не чіпали — прибрано лише суто надлишкові переоголошення.

Примітки

  • Лише чистка використання CSS на клієнті — без зміни маршруту, сервера, ключа i18n чи правил CSS (сам клас .empty без змін); snapshot словника 1208. Перевірено в браузері (порожня панель #/usage обчислює 48px відступу + пунктирну рамку, 0 помилок консолі).
  • Новий канарковий тест tests/empty-state-consistency.test.mjs тримає .empty єдиним джерелом істини. Phase 5 (провайдер Hermes) лишається заблокованою.
  • Набір: 2385 тестів (+2: tests/empty-state-consistency.test.mjs).

[1.149.0] — 2026-08-12

Портали перенесено до Налаштувань (Phase 4)#/portals тепер у групі навігації Setup, поруч із Налаштуваннями застосунку, а не під Sourcing. Із v1.144.0 це екран налаштувань (увімкнення/вимкнення відстежуваних компаній + перевірка стану ATS), а не дія сорсингу — тож саме тут його місце. Зміна лише навігації; сторінка та її маршрут без змін.

Змінено

  • Елемент навігації #/portals → група Setuppublic/index.html), розміщено одразу після Налаштувань застосунку. Прибрано з групи Sourcing (у ній лишаються Scan / Pipeline / Auto-pipeline / Профінансовані компанії). Маршрут #/portals, подання та підпис nav.portals без змін — зсунулася лише позиція на бічній панелі.

Примітки

  • Лише розмітка навігації — без зміни маршруту, подання, ключа i18n чи сервера. Перевірено в браузері (0 помилок консолі); захищено tests/portals-nav-placement.test.mjs.
  • Набір: 2383 тестів (+2: tests/portals-nav-placement.test.mjs).

[1.148.0] — 2026-08-12

Охайніші фільтри сканування (Phase 4) — панель фільтрів тепер акуратна сітка — панель фільтрів #/scan перейшла з рваного flex-wrap із жорстких блоків змінної ширини на адаптивну сітку, а дії «Застосувати / Скинути» тепер займають власний окремий рядок із вирівнюванням праворуч. Фільтри ті самі, поведінка та сама — просто читати зручніше. Дизайнерське шліфування (без parent-sync).

Змінено

  • Панель фільтрів #/scan → адаптивна сітка.scan-filters тепер display: grid зі стовпцями repeat(auto-fill, minmax(180px, 1fr)) та рівномірними проміжками, тож 11 підписаних фільтрів вишиковуються в охайні колонки за будь-якої ширини, а не рвано переносяться в рядок.
  • Дії «Застосувати / Скинути» охоплюють усю сітку у власному рядку, відокремлені тонкою лінією та вирівняні праворуч. Прибрано старий трюк із прихованою міткою + внутрішній flex-обгортку в scan.js.

Примітки

  • Лише CSS + невелике прибирання DOM — усі id фільтрів (#scan-filter-*, #scan-apply) та зв’язку SR.render() не змінено, тож потік Playwright не зачеплено. Нових ключів i18n немає.
  • Перевірено в браузері (0 помилок консолі); захищено tests/scan-filters-grid.test.mjs.
  • Набір: 2381 тестів (+3: tests/scan-filters-grid.test.mjs).

[1.147.0] — 2026-08-12

Hermes & Telegram — розділ вбудованої довідки + поверхня на cvstart.org (Phase 5b, частина 2) — друга й остання частина роботи над документацією Hermes: інструкція тепер живе всередині власної довідки застосунку всіма 17 мовами, а вбудований асистент документації відповідає на її основі на запитання про Hermes. Це й досі лише документація — шлях LLM-провайдера Hermes залишається запланованим / ще не підключеним (Phase 5).

Додано

  • Вбудована довідка §30 «Hermes & Telegram» × 17 мов — новий розділ (що таке Hermes + дві форми інтеграції; запуск на хмарному сервері; Telegram через Hermes + правило «що НЕ показувати»), доступний з #/help. Grounding у docs-assistant / DocsFab підхоплює його автоматично, оскільки обидва читають docs/help/<lang>.md.
  • cvstart.org — посилання на посібник з Hermes з переходом на документ у GitHub.

Змінено

  • Поріг пакета довідки піднято 29 → 30 H2 / 105 → 108 H3 (canonical-docs-coverage, help-ui, help-ru-config-section); §30 додає 3 H3.

Примітки

  • Hermes і досі ніщо не викликає. Новий канарковий тест tests/help-hermes-section.test.mjs перевіряє, що всі мови містять §30 з її незалежними від мови якорями (docs/integrations/HERMES.md, hermes-bridge, #/help, 127.0.0.1, Telegram). Провайдер залишається заблокованим до контракту API з Phase 5.
  • Це завершує постачання документація + skill для Phase 5b; інтеграція провайдера (Phase 5) залишається окремим заблокованим пунктом.
  • Набір: 2378 тестів (+2: tests/help-hermes-section.test.mjs).

[1.146.0] — 2026-08-12

Агент Hermes + Telegram — посібник з інтеграції + skill (Phase 5b, частина 1) — можна запустити career-ops-ui на хмарному сервері та передавати його події (завершений скан, новий звіт, термінове нагадування) у Telegram через агента Hermes від Nous Research. Цей реліз додає документацію з дизайну + розгортання та skill hermes-bridge; шлях провайдера LLM Hermes залишається запланованим / ще не підключеним (заблокований спайком щодо контракту API Phase 5). Документація свідомо йде попереду коду.

Додано

  • docs/integrations/HERMES.md — докладний розбір: дві форми інтеграції (endpoint, сумісний з OpenAI, проти агентного runtime), розгортання на хмарному сервері (reverse proxy + HTTPS + systemd, контракт «лише читання» з parent на headless-машині), Telegram через Hermes, і список моделі загроз «що НЕ можна показувати» (жодних CV / зарплат / тіл звітів / ключів у канал).
  • Тизер ## Hermes agent + Telegram у README — короткий вказівник-посилання в англійському README, відображений у всіх повністю перекладених локалізованих README.
  • Skill hermes-bridge (.claude/skills/hermes-bridge/), що реалізує посібник на практиці — перевірки передумов і межі охоплення (Node ≥ 18, наявність ключів, доступність endpoint через безпечний щодо SSRF шлях), ніколи не записує секрети на диск/у логи й відмовляється вигадувати endpoint Hermes або стверджувати, що провайдер підключений.
  • Розділ Integrations у docs/architecture/OVERVIEW.md посилається на посібник.

Примітки

  • Наразі ніщо не викликає Hermes. Канарковий тест (tests/hermes-docs.test.mjs) перевіряє маркери чесності «заплановано / ще не підключено» і те, що в llm-dispatch.mjs немає жодної гілки Hermes/Nous — тож підключення провайдера пізніше має оновити документацію + roadmap в тій самій зміні.
  • Відкладено до v1.147.0 (Phase 5b, частина 2): розділ довідки в застосунку «Hermes & Telegram» H2 × 17 мов та маркетингова сторінка cvstart.org.
  • Набір: 2376 тестів (+4: tests/hermes-docs.test.mjs).

[1.145.0] — 2026-08-12

Проникливі статистики (продовження): перебудовуваний графік — вкладка «Тренд цільових ролей» на #/stats тепер має віджет Побудувати графік: виберіть метрику × вимір, і він перемальовується наживо. Запит UX від користувача (без parent-sync).

Додано

  • Перебудовуваний графік метрика × вимір — виберіть метрику (Вакансії / Медіанна зарплата / Середня зарплата) і вимір (За країнами / За ролями), і стовпчиковий графік миттєво перемальовується. Зарплатні метрики враховують валюту + перемикач рік ⇄ місяць; вакансії — простий підрахунок.
  • 8 нових ключів i18n × 17 локалей; snapshot 1200 → 1208.

Примітки

  • Перевірено в браузері (0 помилок консолі). Набір: 2372 тестів (+2).

[1.144.0] — 2026-08-12

Налаштування та фільтри (Фаза 4, частина 1): увімкнення/вимкнення відстежуваних порталів — тепер можна вмикати чи вимикати відстежувану компанію на #/portals, і сканер це враховує. Запит UX від користувача (без parent-sync).

Додано

  • Перемикач Увімкнути/Вимкнути для компанії на #/portals — один клік вимикає портал (EN-сканер уже пропускає компанії enabled: false, тож вимкнений портал випадає з усіх майбутніх сканів) або вмикає назад, з оптимістичним тостом.
  • POST /api/portals/toggle — явний запис користувача, який хірургічно та з перевіркою парсингу перемикає прапорець enabled компанії в portals.yml (коментарі, порядок та інші поля збережено). 5 нових ключів i18n × 17 локалей; snapshot 1195 → 1200.

Примітки

  • Зміна сканера — нуль: en-scanner.mjs уже фільтрує enabled !== false. Набір: 2370 тестів (+3).

[1.143.0] — 2026-08-12

Зрозумілість (продовження): підказки ? на основних робочих сторінках — довідкове ? тепер охоплює дев’ять головних сторінок дій, усіма мовами. Правка UX за репортом користувача (без parent-sync).

Додано

  • Підказка ? ще на 9 заголовках#/scan, #/evaluate, #/cv-studio, #/tracker, #/config, #/deep, #/batch, #/auto, #/apply отримали вбудоване ? (через HelpHint.title), що відкриває локалізоване «що це / як використовувати / який результат» — той самий CSP-безпечний компонент із v1.139.0.
  • 9 нових ключів i18n × 17 локалей (help.hint.scan/…/apply); snapshot 1186 → 1195.

Примітки

  • Перевірено в браузері (0 помилок консолі). Набір: 2365 тестів (+1).

[1.142.0] — 2026-08-12

Виправлення: більше немає «Unknown» кар’єрного архетипу#/orientation тепер завжди ранжує з восьми названих кар’єрних векторів, а не відповідає інколи «Unknown» і не радить «подвоїти ставку» на ньому. Правка за репортом користувача (без parent-sync).

Виправлено

  • #/orientation — промпт ШІ тепер забороняє архетип поза набором. Модель МУСИТЬ ранжувати топ-3 рівно з восьми названих векторів і ніколи не відповідати «Unknown»/«N/A»/«недостатньо даних» та не вигадувати мітку. За бідного резюме вона все одно називає три найближчі з меншою впевненістю і вказує, яких доказів бракує.

Примітки

  • Лише зміна серверного промпту (buildOrientationPrompt); без змін i18n/схеми. Набір: 2364 тестів (+1).

[1.141.0] — 2026-08-12

Проникливі статистики (продовження): збагачення профінансованих компаній#/funded став наочнішим: логотипи компаній, графік за сумою фінансування та картки з раундом / сумою / оцінкою знахідки / рекомендованою дією. Правка UX за репортом користувача (без parent-sync).

Змінено

  • #/funded — плоска таблиця → сітка карток. Кожна нещодавно профінансована компанія тепер картка з логотипом (з назви через CompanyLogo, запасний варіант — літерний аватар), чипами раунду + суми, оцінкою знахідки та рекомендованою дією від батьківського проєкту, а також посиланням на новину і датою.
  • Візуалізація суми фінансування — горизонтальний стовпчиковий графік найбільших компаній за розкритою сумою; вільний текст сум (“$120M”/“€1.5B”) парситься у величину новим parseAmount. 3 нових ключі i18n × 17 локалей.

Примітки

  • Досі лише для читання через GET /api/company-funded; опису й вилки зарплат у стрічці фінансування немає. Набір: 2363 тестів (+2).

[1.140.0] — 2026-08-12

Проникливі статистики: багатші дані про зарплати — розбивка зарплат у «Мій пайплайн» на #/stats тепер показує середнє (не лише медіану), перемикач на рік ⇄ на місяць і таблицю мін · сер · медіана · макс за країнами. Перша частина Фази 3. Правка UX за репортом користувача (без parent-sync).

Додано

  • Середня зарплатаRoleStats.salaryStats тепер повертає avgUsd поряд із minUsd/medianUsd/maxUsd. Медіана стійка до викидів, середнє показує перекіс — разом вони читаються як розподіл.
  • Перемикач на рік ⇄ на місяць і таблиця мін · сер · медіана · макс за країнами в розділі зарплат, з урахуванням валюти й періоду. 8 нових ключів i18n × 17 локалей.

Примітки

  • Цифри й далі беруться лише з вакансій із розпізнаваною зарплатою і нормалізуються до USD (орієнтовно). Набір: 2361 тестів (+1).

[1.139.0] — 2026-08-12

Зрозумілість: підказки ? — багаторазова, безпечна для CSP кнопка ?, яка за кліком пояснює «що це / як працює / чого очікувати» вашою мовою. Правка UX за репортом користувача (без parent-sync).

Додано

  • Спливна підказка ? (window.HelpHint) — кругла ? біля заголовка відкриває легке, залежне від теми та дзеркалене в RTL спливне вікно, що відображає локалізоване пояснення через UI.md(); доступно (role="tooltip", aria-expanded, закриття Escape/кліком поза, повернення фокуса) та безпечно для CSP.
  • ? на 5 вкладках #/stats і на 8 заголовках ШІ/аналітичних екранів (career-plan, орієнтація, two-pager, нетворкінг, мок-інтерв’ю, пам’ять, funded, щотижневий підсумок) — 14 нових ключів i18n × 17 локалей.

Примітки

  • Усі екрани вже мали однорядковий підзаголовок; ? додає глибше пояснення на вимогу й робить порожні стани самоочевидними. Набір: 2360 тестів (+4).

[1.138.0] — 2026-08-12

Генерація мовою інтерфейсу — кожна AI-генерація тепер відповідає мовою, обраною в інтерфейсі, плюс посилення тестів за підсумками рев’ю. Правка UX за репортом користувача (без parent-sync).

Змінено

  • AI-генерації тепер враховують мову інтерфейсу. Якщо інтерфейс російською, іспанською, японською тощо — згенерований текст повертається цією мовою, а не завжди англійською. Директиву мови виводу проброшено в усі ендпоінти генерації — кар’єрний план, орієнтацію, звіт по ринку, мок-інтерв’ю, план нетворкінгу, «запитати документацію», пропозицію нотатки пам’яті та чернетку two-pager. Код та ідентифікатори лишаються англійською (напр., ключі YAML у two-pager); локалізуються лише проза, заголовки та пункти.

Виправлено

  • Вартовий CSS-ролей кольору (tests/css-role-tokens.test.mjs) — статичний канарок, що аліас-токени темної теми з v1.137.0 ніколи не змінюють роль: текстові токени (--fg/--danger/--ok/…) ніколи як background, а токени поверхонь (--card/--panel/--line/…) — як текстовий color, у всьому CSS та inline-стилях SPA.
  • Самоперевірка XSS-завантажувача UI.md() — тест, що через vm вантажить md() з api.js, тепер одразу після витягання перевіряє md('<script>…') і кидає помилку, якщо екранування зникло, — тож майбутній збій нарізки падає гучно, а не лишає security-набір зеленим на усіченій функції.
  • Вартовий прокрутки на #/career-planscrollIntoView після генерації виконується лише якщо прев’ю ще приєднане до документа.

Примітки

  • docs/UX-ROADMAP.md оновлено: підказки ? + описи сторінок + порожні стани тепер v1.139.0; провайдер Nous Research / Hermes — з гайдом розгортання на хмарному сервері + Telegram і скілом Hermes — відстежується як Фаза 5 / 5b.
  • Набір: 2356 тестів (+5).

[1.137.0] — 2026-08-11

Виправлення читабельності та рендерингу — контраст темної теми, підписи графіків та кар’єрний план. UX-правка за повідомленням користувача (без parent-sync).

Виправлено

  • Білий на білому / чорний на чорному в темній темі на багатьох екранах — п’ятнадцять кастомних властивостей CSS, на які посилалися кілька вкладок (--fg, --panel, --panel-2, --ok, --danger, --card, …), ніколи не були оголошені, тож вони відкочувалися до захардкоджених світлих/чорних значень: у світлій темі це не заважало, але в темній робило текст нечитабельним (чіпи огляду #/pipeline, активна вкладка #/stats, «Активний / Ключі» + «✓ встановлено» на #/config, розділи #/two-pager, бульбашка питання #/mock-interview, текст помилок). Тепер вони прив’язані псевдонімами до справжніх токенів, що враховують тему, тож автоматично слідують за нею — 0 порушень контрасту WCAG-AA на всіх 29 вкладках, підтверджено автоматизованим аудитором; активна вкладка #/config отримала читабельний тонований стиль. Регресійний бар’єр (tests/dark-theme-tokens.test.mjs) утримує цю прив’язку псевдонімами.
  • Підписи графіків #/stats обрізалися посередині слова («Senior Backend Engineer» → «…Enginee») — тепер вони скорочуються трьома крапками, а повний підпис зберігається як підказка при наведенні.
  • #/career-plan показував згенерований план як необроблений Markdown — тепер він автоматично рендериться як форматований, читабельний текст (редагований Markdown залишається в текстовому полі; кнопка «Перегляд» перемикає режим).

Примітки

  • #/career-plan, #/two-pager, #/stats та щотижневий дайджест інтерв’ю не зламані — вони показують порожні стани, доки ви не згенеруєте план / не матимете даних. Наступним кроком запланована зрозуміліша підказка на сторінці та довідкові підказки ? (docs/UX-ROADMAP.md).

[1.136.0] — 2026-08-11

Паритет із батьківським career-ops v1.26.x (після мейнлайну v1.26.0) — одне нове джерело без авторизації плюс хвиля портів якості та надійності для дзеркал web-ui. Реєстр тепер налічує 79 джерел = 74 англомовних + 5 російськомовних (ALL_ADAPTERS 74).

Додано

  • eightfold (Eightfold AI, #2684) — дошки з підбору персоналу через API без авторизації https://<tenant>.eightfold.ai/api/apply/v2/jobs, прив’язане до хоста *.eightfold.ai (брендований CNAME careers.<company>.com свідомо відхиляється); пагіноване з обмеженням безпеки, відмовою для неактивної дошки, дедуплікацією URL. Джерело + адаптер + CI-ізольований набір тестів; з’являється у фільтрі джерел #/scan та на лендінгу.

Виправлено

  • Unicode-орієнтована дедуплікація та ключі ролей (#2569 / #2587 / #2667) — нова спільна normalizeTextKey (NFKC, зберігає літери/знаки/цифри будь-якого письма) замінює ASCII-лише ключі: detect-reposts тепер групує варіанти назв компаній з різною шириною символів/пунктуацією («Acme, Inc.» ≡ «Acme Inc») і ніколи не зливає різних неЛатинських роботодавців, а role-matcher згортає повноширинні назви посад і зберігає неЛатинські токени ролей, а не стирає їх.
  • fetchJsonWithRetry більше не повторює відхилене перенаправлення (#2657) — захист redirect:'error', що натрапляє на 3xx, є детермінованим, тож тепер він неповторюваний і завершується швидким провалом, замість витрачання бюджету повторних спроб.
  • AND-групи title_filter.positive (#2552) — розділений пробілами + усередині позитивного запису тепер вимагає, щоб усі терміни зустрічалися в назві, у будь-якому порядку.
  • oraclecloud приймає нумеровані апекси орендаря oraclecloud1.com … oraclecloud99.com (#2683) — обмежене сімейство (без провідного нуля, ≤ 2 цифри), ніколи не символ підстановки апекса.
  • workable посилено (#2675) — повторні спроби, схожі на браузерні заголовки та серіалізація запитів проти хоста за Cloudflare.
  • personio переходить на HTML-скрейп, коли XML-стрічка вимкнена, замість повернення нічого.
  • Резервні псевдоніми states FALLBACK ресинхронізовано з батьком (#2615).

Примітки

  • Не портовано (не дзеркалюється web-ui, або лише для CLI): reply-matcher (#2672), jd-similarity (#2661), jd-skill-gap (#2686), розбір env-шляху сканування (#2568) / --flag=value (#2589), а також зміни cover-letter / шаблону CV / doctor / ollama / generate-pdf. HIGH-рекомендації web js-yaml/nanoid уже виправлено в web-ui v1.135.0.

[1.135.0] — 2026-08-11

Паритет із батьківським career-ops v1.26.0 — п’ять нових джерел сканування без авторизації плюс виправлення коректності для чотирьох дощок, які web-ui вже підтримує. Реєстр тепер налічує 78 джерел = 73 англомовних + 5 російськомовних (ALL_ADAPTERS 73).

Додано

  • П’ять нових джерел сканування (кожне — джерело + адаптер + CI-ізольований набір тестів; з’являються у фільтрі джерел #/scan та на лендінгу cvstart.org):
    • join (JOIN) — дошка вакансій компанії JOIN зі сторінки join.com/companies/<slug>, зчитана з Next.js __NEXT_DATA__ (прив’язка до хоста, обмеження за кількістю сторінок).
    • getro (Getro) — портфельні дошки «talent-network» венчурних фондів через публічний POST API api.getro.com, з пагінацією від найновіших; кожна вакансія приписується компанії-портфелю, а не самому фонду.
    • consider (Consider) — портфельні дошки венчурних фондів getconsider.com через POST-запит того ж походження; конфігурований хост прив’язаний структурним захистом від SSRF (лише публічний HTTPS-хост).
    • joinup (JOINUP) — швейцарська дошка joinup.ch, зчитування SSR-рендерованої найновішої сторінки; при поломці скрапера — відмова закритого типу (fail-closed).
    • remotli (Remotli) — remotli.ch, віддалені ролі у швейцарських компаніях (зарплати в CHF); видає власне посилання подання (apply URL) ATS роботодавця, тож перехресні лістинги дедуплікуються.

Виправлено

  • a16z Speedrun більше не переривав весь борд через тимчасовий збій — запити сторінок тепер проходять через спільну fetchJsonWithRetry (обмежені повторні спроби лише на тимчасових 429/5xx/таймаут, ніколи на постійних 4xx), а бюджет сторінки перерахований під сторінку з 50 вакансіями.
  • arbeitsagentur перенесено на API Jobsuche v6 (/pc/v6/jobs) — старий v4-ендпоінт видає 404; форму відповіді перейменовано, а фільтрація «remote» тепер звужується на боці сервера.
  • thehub перенесено на API v2 jobsandfeatured; рядки не містять дати публікації і звільнені від фільтра за віком.
  • hackernews тепер надійно знаходить щомісячну гілку «Who is hiring?», фільтруючи пошук у Algolia за тегом облікового запису author_whoishiring замість довільнотекстового запиту.

Примітки

  • Не портовано (web-ui вже безпечний, поглинутий через relay, або лише для CLI): ключі Unicode-дедуплікації ролей / зіставлення компаній (групування репостів у web-ui вже ключує компанію за простим нижнім регістром, тож різні неЛатинські роботодавці ніколи не зливаються); сигнал затримки відмов followup + доопрацювання company-funded (реле лише для читання, з м’яким відмовленням); середовищно-перевизначувані шляхи сканування та розбір --flag=value (web-ui запускає сканери in-process); рефакторинг консолідації User-Agent (web-ui вже централізує його); а також елементи лише для CLI (реєстр недовіреного вмісту, oferta/offer-prep, doctor, зміни шаблонів супровідного листа/резюме).

[1.134.1] — 2026-08-05

Посилення валідації — виправлення, виявлені під час повного аудиту проєкту.

Виправлено

  • successfactors більше не відкидає зібрані вакансії при збої в середині сканування (регресія в портуванні «неактивна дошка = помилка» з v1.134.0) — цикл пагінації не мав try/catch, тож збій на сторінці 2+ (після успішної сторінки 1) призводив до винятку й губив усе вже зібране; а якщо цей збій був 404 (вихід startrow за межі діапазону), en-scanner міг на кілька днів помилково позначити живого орендаря як неактивного. Тепер дзеркалює phenom/radancy: збій на сторінці 0 досі видає помилку (неактивна дошка), а збій на пізнішій сторінці зберігає часткові результати.
  • Фільтр-чипи #/scan тепер керуються з клавіатури (WCAG 2.1.1) — чипи фасетів (і чип «очистити») були елементами span з обробником кліку, але без tabindex/ролі, тож користувачі клавіатури та програм читання з екрана не могли до них дістатися чи перемикати їх. Тепер вони мають role="button", tabindex="0", aria-pressed і активацію по Enter/Space.
  • Три захардкоджені англійські рядки тепер локалізовано — підказка бейджа довіри #/scan, заголовок колонки релокації #/scan та заголовок колонки балу #/dashboard були голими літералами, яких не бачив паритет-контроль i18n (вони ніколи не були ключами), тож вони залишалися англійськими в кожній неанглійській локалі. Тепер scan.trustTip + scan.col.reloc (2 нових ключі) + повторне використання track.col.score, зі статичною перевіркою на рівні коду.

[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 англомовних + 5 російськомовних (ALL_ADAPTERS 68). З’являється у фільтрі джерел #/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): переробка продуктивності групування заголовків для виявлення репостів #2389; виправлення Unicode-ключів компаній (власна дедуплікація трекера web-ui вже безпечна для не-латиниці); scan --since; cv-facts; аудит-прохід шаблону CV / PDF; doctor; директива щодо недовіреного вмісту в режимах.

[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 — саме тому баг і потрапив у реліз із зеленими тестами), плюс статичні канарки джерела, що funded.js читає res.companies (ніколи не res.candidates) і будує рядки таблиці з дочірніми елементами-масивом (+1 → 2144).

[1.133.0] — 2026-08-01

Додано

  • Виявлення профінансованих компаній (#/funded, паритет із батьком #2117) — новий тільки-для-читання вигляд, що ретранслює батьківський company-funded.mjs career-ops через GET /api/company-funded: список нещодавно профінансованих компаній «спершу на перегляд», знайдених у публічних стрічках про фінансування з прив’язкою до хоста (TechCrunch, PR Newswire, The Guardian, Hacker News). Ретрансляція запускає скрипт з --json --dry-run (JSON у stdout, без запису файлів), ніколи не передає користувацький ввід у --sources, має обмеження частоти запитів і запускається лише користувачем (кнопка «Знайти», ніколи при монтуванні). Новий модуль маршруту server/lib/routes/funded.mjs + public/js/views/funded.js, у розділі «Пошук».
  • Щотижневий дайджест співбесід (#/interview-digest, паритет із батьком #2129/#2130) — новий тільки-для-читання вигляд, що ретранслює батьківський weekly-digest.mjs (без ШІ) через GET /api/interview/weekly-digest: механічне зведення нотаток сесій співбесід — з якими компаніями та на яких раундах ви проходили співбесіди цього тижня, повторювані компетенції та відкриті прогалини за принципом «найкраще зусилля». Необов’язковий діапазон ?from=&to= передається, лише якщо ОБИДВА параметри — валідні YYYY-MM-DD; порожній діапазон — це коректний дайджест available:true. Додано до server/lib/routes/interview.mjs + public/js/views/interview-digest.js, у розділі «Аналітика».
  • Обидві ретрансляції дотримуються усталеного контракту fail-soft available:false, коли батьківський скрипт відсутній (CI, автономні інсталяції). 26 нових i18n-ключів ×17 локалей; CI-ізольований набір tests/parity-routes-v1133.test.mjs (+5 → 2143).

Примітки

  • Батьківський career-ops просунувся далі за v1.24.0 зі сторінкою «Follow-up Tracker» застосунку Next.js web/ (#1422) та бекенд-рендером PDF (#2182) — не портовано: web-ui вже має власну ретрансляцію нагадувань і власні PDF-раннери, а посилення followup-cadence.mjs надходить безкоштовно через 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, getRows, конструктори рядків/фасетів, малювальники опцій і дзеркало реєстру FALLBACK_SOURCES) перенесена у фабрику window.ScanResults.create(ctx), що замикається на об’єкт контексту, який надає вигляд. Без зміни поведінки — функції перенесено дослівно, змінні замикання переприв’язано до ctx.*; scan.js тепер має ~906 рядків (другий прохід виділення до цілі в 800 рядків заплановано).
  • Тести, що читають вихідний код статично, тепер читають обидва файли через tests/helpers/scan-src.mjs::loadScanSrc(); tests/scan-fallback-sources.test.mjs тепер читає дзеркало реєстру з scan-results.js.
  • Новий регресійний бар’єр у браузеріtests/playwright-scan-filters.mjs засіває заготовлений data/last-scan.json і проганяє кожен фільтр #/scan, перевіряючи точну кількість рядків (npm run test:e2e:browser); для цього додано стабільні ідентифікатори фільтрів (#scan-filter-*, #scan-apply).
  • Банер «Останній реліз» у README скорочено до однорядкового підсумку + посилання на повний журнал змін (довгу багатоверсійну стіну опису прибрано). Застосовано в усіх 17 локалях.

[1.131.2] — 2026-07-31

Змінено

  • Поділ app.css на три впорядковані таблиці стилів (борг за контрактом розміру файлу — єдиний файл розрісся приблизно до 1990 рядків, значно перевищивши жорсткий ліміт у 800 рядків). Тепер це app.css (~672 — a11y, дизайн-токени/тема, бічна панель, основний контент, кнопки, оболонка вмісту), components.css (~595 — картки, сітки, пагінатор, бейджі, таблиці, форми, лог/консоль, markdown, перемикач мов, фільтр-чіпи, банер з’єднання) і overlays.css (~737 — тост, шухляда сповіщень, модальне вікно, різне/адаптивність, дзеркало [dir="rtl"], docs-fab, usage-hud) — кожен у межах жорсткого ліміту.
    • Розріз суцільний і в тому ж порядку, тож каскад побайтово ідентичний файлу до поділу; index.html завантажує всі три як впорядковані <link>. Без зміни поведінки, розмітки чи i18n.
    • Тести, що перевіряють CSS, тепер читають конкатенацію через спільний хелпер tests/helpers/css.mjs::loadAppCss(). Новий tests/css-modularization.test.mjs фіксує поділ (файли існують · кожен ≤ 800 рядків · порядок <link> в index.html) → набір 2138. Перевірено в браузері: усі три таблиці стилів парсяться і їхні правила застосовуються.

[1.131.1] — 2026-07-31

Виправлено

  • Узгодженість прив’язки хоста в адаптерах для двох джерел v1.130.0 (доопрацювання за код-рев’ю, багаторівневий захист; без зміни поведінки для коректних вхідних даних):
    • Адаптер a16z-speedrun-talent тепер повторно перевіряє перевизначення api: / a16z-speedrun-talent: у buildEndpoint (HTTPS + точний хост speedrun-talent-network.com) і в разі невдачі відкочується до канонічної стрічки — паритет з адаптером cryptocurrencyjobs, тож значення з іншим хостом ніколи не потрапляє у слот запиту (раніше це покладалося виключно на перевірку assertSpeedrunUrl під час запиту). Перевірка точного хоста тепер — єдиний експортований SPEEDRUN_TALENT_HOST_RE, спільний для перевірки й адаптера.
    • Парсер cryptocurrencyjobscleanUrl тепер використовує ту саму перевірку точної відповідності хоста, що й assertCryptocurrencyJobsUrl та перевизначення адаптера (було endsWith, яке приймало субдомени). Парсер ніколи не є поблажливішим за SSRF-перевірку: посилання елемента sub.cryptocurrencyjobs.co відкидається.
    • +2 тести → 2135.

[1.131.0] — 2026-07-31

Додано

  • Дошка стадій-вкладок CRM #/tracker (портовано з вигляду /pipeline батьківського веб-застосунку). Панель чіпів воронки трекера + випадний список статусу замінено на смугу вкладок стадій: вкладка Усі плюс по одній вкладці на кожен канонічний статус — Evaluated · Applied · Responded · Interview · Offer · Rejected · Discarded · SKIP · Hired — кожна показує живий лічильник за всю історію, включно зі стадіями з нульовим лічильником, тож уся воронка завжди видима (вигляд CRM). Активна вкладка керує фільтром; повторний клік на неї скидає назад до «Усі». Рядки зберігають тональність оцінки, легітимність, можливості PDF і звіту, а комірка компанії тепер показує бренд-логотип, коли логотипи увімкнено (вимкнено за замовчуванням → без додаткових запитів).
    • Новий тільки-для-читання маршрут GET /api/tracker/stages повертає канонічну воронку (мітки за порядком) + карту згортання псевдонімів, отримані з server/lib/states.mjs (templates/states.yml, із вбудованим фолбеком) — тож клієнт ніколи не хардкодить whitelist статусів. Застаріла відповідь GET /api/tracker без параметрів лишається незмінною (лише { rows }).
    • Нова чиста, покрита юніт-тестами клієнтська бібліотека public/js/lib/tracker-stages.js розкладає рядки за стадіями сервера, толеруючи випадковий markdown bold і локалізовані псевдоніми (наприклад, aplicadoApplied). Вкладки доступні (role tablist/tab, aria-selected, зона кліку ≥44 px, лічильники в доступному імені кожної вкладки). Нових i18n-ключів немає. Набір тестів 2133.

[1.130.0] — 2026-07-31

Додано

  • Два нові джерела сканування, портовані з батьківського career-ops v1.24.0 (in-process, без нових залежностей; обидва з’являються у фільтрі джерел #/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) — дошка вакансій Web3 cryptocurrencyjobs.co, отримується через публічну стрічку RSS 2.0 (без авторизації). Дворазове декодування XML-сутностей, лише віддалені вакансії, роботодавець розбирається з хвоста заголовка "… at <Company>".
    • Загальна кількість у реєстрі тепер 72 джерела = 67 англомовних + 5 російськомовних (ALL_ADAPTERS = 67 англомовних адаптерів порталів).

Виправлено

  • echojobs — гібридні ролі лишаються відмінними від віддалених (віддзеркалює батьківський #2258). Регістронезалежний маркер hybrid тепер дає "<Місто> · Гібрид" (або просто Гібрид, якщо міста немає) і workplaceType: 'Hybrid', замість згортання в Remote.
  • radancy — застаріла розмітка TalentBrew + транспорт JSON-фрагмента результатів (віддзеркалює батьківський a3e6df9), під контролем ін’єктованого opts.fetchJson.

Примітки

  • Не портовано — CLI-специфічні можливості батька. Великий CLI/режимний шар career-ops v1.24.0 лишається поза web-ui, який є переглядачем + тонким наскрізним записом, а не хостом режимів: таблиці комплаєнсу/юрисдикції, телефонна книга контактів + vCard, транскрипт-дебриф інтерв’ю / визначення платформи дзвінків, встановлення статусу в реєстрі, запис результату, двопрохідний трайдж, jd-подібність, версійована схема артефакту CV заявки, виявлення Playwright-MCP у doctor та portals/fix-slugs.mjs. Зміни оркестрації сканування, що живуть у батьківському scan.mjs — сканер Interamt.de на Playwright, повне сканування iCIMS зворотним ATS, фільтр віддаленості за прийнятністю країни, темп DNS-пошуку, дедуп StepStone rltr і нормалізована колонка компанії в історії сканування — не застосовуються: 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 Managersenior, було 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 + літерний fallback); (4) job-facets.js — фасети seniority/source/days. +21 тест.

Примітки

  • Не портовано (лише концепт): агентський шар дій батька (actions/registry.ts + api/assistant/route.ts) — креслення на випадок, коли docs-fab стане копайлотом. Нових джерел немає (реєстр 70), змін i18n/help немає.

[1.127.0] — 2026-07-29

Додано

  • Три нові джерела сканування (паритет career-ops v1.23.0) — у реєстрі тепер 70 адаптерів (65 EN + 5 RU): Flowxtra (глобальний агрегатор без авторизації), 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) другий рядок порівняльної таблиці в довідці (×17) і зібраний у CI help сайту все ще містили Inside Claude Code / Codex / Cursor / Gemini CLI — застарілий набір із Cursor — тепер повний ростер. Обидва рядки використовували роздільники слеш/середня крапка, які патерни свіпу v1.126.0 не покривали. i18n-снапшот перегенеровано; сьют лишається 1969.

[1.126.0] — 2026-07-25

Додано

  • Вкладка інструментів AI CLI тепер виявляє всі 8 першокласних CLI career-ops — ростер #/config синхронізовано з батьківським docs/SUPPORTED_CLIS.md: у server/lib/routes/cli-detect.mjs додано Grok Build CLI (grok) і Kimi CLI (kimi), а Antigravity тепер спершу шукає канонічний бінарник agy. Скан 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 (застаріла обгортка). Help-бандли зберігають структуру 29 H2 / 105 H3.

[1.125.4] — 2026-07-23

Змінено

  • залежності site (dependabot #151–#153) — sharp 0.34.5→0.35.3, svgo 4.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.0parentVersion без змін.

[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.mjsfacts.sources), тож він ніколи не може розійтися із застосунком; курований довідник посилань у Sources.astro захищений новим тестом tests/site-sources.test.mjs. У навігації шапки з’явився якір Sources; додано 4 нові ключі i18n сайту ×17. Також виправлено список inLanguage у JSON-LD лендінгу, в якому досі бракувало hi.

[1.124.0] — 2026-07-21

Додано

  • П’ять джерел сканування (паритет із батьківським v1.22.0, #1808/#1572/#2024/#2055) — Welcome to the Jungle (JSON API для всієї дошки), Agentic Engineering Jobs (дошка агентного/AI-інжинірингу), Jobvite (ATS на тенант без автентифікації), Gem (ATS на тенант) і Alibaba Group (JSON API кар’єрної сторінки, шаблон Meituan/Tencent). Кожне джерело — прив’язана до хоста, CI-ізольована пара джерело + адаптер; реєстр тепер постачається з 67 адаптерами (62 англомовних + 5 російськомовних); резервний список випадаючого списку 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), розбивка токенів за кроками (у веб-інтерфейсі є власний лічильник використання), серіалізація блокування запису трекера (веб-інтерфейс скеровує записи через withFileLock ще з v1.21), прапорці CLI visa_filter та абсолютної дати публікації для сканування (у веб-інтерфейсі є власний фільтр віку «Опубліковано протягом») та посів дедуплікації відомих джерел (сканер веб-інтерфейсу веде власну дедуплікацію історії сканування).

[1.123.0] — 2026-07-17

Додано

  • Джерело сканування Oracle Recruiting Cloud (паритет із батьківським 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 кожної відстежуваної компанії, пагінація за зсувом із жорсткою межею кількості сторінок і WAF-обізнані заголовки на кшталт браузерних. Реєстр тепер постачається з 62 адаптерами (57 англомовних + 5 російськомовних); резервний список випадаючого списку 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 / панельної розвідки / no-show листа, персистентність довіри до джерел сканування та здоров’я порталів (веб-інтерфейс запускає власний внутрішньопроцесний сканер із trust-validator і сторінкою здоров’я порталів) і розширення статистики/розриву в зарплаті (транслюються лише для читання та безвідмовно).

[1.122.0] — 2026-07-16

Додано

  • Гінді (हिन्दी) — 17-та мова — повний словник інтерфейсу (~1 110 ключів), повний вбудований посібник довідки (паритет 29 H2 / 105 H3), README.hi.md, новий CHANGELOG.hi.md (починається з v1.122.0, за прецедентом de/it/tr), лендинг cvstart.org + сторінки Методологія/Ліцензія/Журнал змін/Довідка, перемикач мов (🇮🇳), автовизначення мови браузера та локалізований знімок дашборда. Кожен гейт паритету ×16 тепер працює ×17: паритет словника i18n + знімок, гейти H2/H3 довідки, паритет CHANGELOG, check-i18n сайту та перевірка локалей Playwright.

[1.121.0] — 2026-07-16

Додано

  • Сторінки Methodology, License і Changelog на cvstart.org — лендинг отримав три нові розділи всіма 16 мовами, поруч із наявним блоком Порівняння: /methodology/ (шкала оцінювання за шістьма вимірами 0.0–5.0, поріг подання заявки 4.0 і правила «ніколи не робимо» — локалізований стислий виклад career-ops.org/methodology), /license/ (канонічний текст ліцензії MIT із посиланням на NOTICE.md) і /changelog/ (цей файл, відображений для кожної локалі з 16 перекладених CHANGELOG репозиторію). Новий пункт Methodology у шапці та посилання в блоці ресурсів футера; 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-дашборді. Веб-інтерфейс наслідує це: нове посилання у футері бічної панелі відкриває сторінку маніфесту (новий ключ i18n footer.manifesto у всіх 16 локалях), вбудований посібник довідки отримав §29 «Маніфест CareerOps» усіма 16 мовами, README пояснює, що таке маніфест і як його підписати, а футер лендингу cvstart.org теж посилається на нього.

Примітки

  • Батько v1.20.0 також виправив придушення відомих навичок у цільовому режимі upskill, приглушив dotenv, щоб stdout scan --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, HigherEdJobs (RSS), JibeApply (iCIMS), softgarden — реєстр тепер налічує 50 адаптерів (45 EN + 5 RU), усі в списку Scan. (5) Крок передперевірки нокаут-питань у чек-листі Apply. (6) Раннер reconcile (/api/run/reconcile). Shell-out-маршрути чесно деградують без батьківських скриптів.

  • Новий модуль server/lib/routes/followup.mjs (31-й) + нові маршрути + 8 файлів source/adapter. Тести: 6 + 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.

  • Лише 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, завантажується з 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, монтується глобально з index.html; безпечний для CSP; стилі з урахуванням теми та дзеркалом RTL у app.css. Без нового маршруту сервера. Тести: tests/docs-fab.test.mjs (8). 6 нових ключів i18n ×16. Довідка §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.mjsGET /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.mjsGET /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.mjsGET /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.mjsPOST /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, спільний каскад провайдерів, ручний промпт без ключа, з обмеженням частоти, без записів. Результат експортується у 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 та авто-пайплайну для великих сторінок). Логування активності SSE stream.* знову досяжне (перевірку /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 місяці та поради щодо перемовин. Кожна цифра позначена як орієнтовна оцінка зі знань моделі, і ніколи не подається як зібрані дані. Нова вкладка Мій пайплайн будує графіки за вашим власним трекером: розподіл оцінок, воронка статусів, провідні компанії та ролі, заявки в часі та коефіцієнти конверсії. Початковий вигляд цільових ролей (вакансії/зарплати за країнами + тренд збережених знімків) переходить у третю вкладку, тепер із вибором валюти та оглядом вакансій за роллю.

  • Експортуйте будь-який звіт у 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-запиту (оцінка, пробна співбесіда, нетворкінг, CV Studio) в усіх провайдерах. Напишіть її раз — вона керує всім.
  • Керування, а не факти — вона фіксує ваші вподобання й те, як вам зручно працювати (тон, формат, стоп-фактори, ритм), і ніколи не нові фактичні твердження про ваш досвід — ті й далі живуть лише у вашому резюме, профілі та 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, нічого вигаданого). Зберегти план зберігає готовий план у користувацький шар (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 (виправлення після v1.78.1). Доступна назва логотипа тепер локалізована всіма 13 мовами (nav.logoHome). Enter у глобальному пошуку, коли ви вже на #/scan, примусово ре-рендерить, щоб не втратити введений термін (guard тієї ж маршрутизації). health.title тепер перекладено польською (Kondycja) та данською (Systemtilstand) — раніше англійською. Тести 1235 → 1238.

[1.78.1] — 2026-06-27

Виправлення UX на Scan. Таблиця результатів #/scan тепер автоматично оновлюється під час сканування та ще раз після завершення — без перезавантаження. Глобальний пошук у шапці показує підказку Enter і за не-URL-запитом переходить на #/scan із заповненим полем (раніше #/tracker). Логотип тепер веде на дашборд (головну).

[1.78.0] — 2026-06-27

Гео-фільтр на сторінці Scan — фільтр вакансій за країною, з прапорцями. Новий список Країна у #/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:); кожен хост закріплено якірним 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, filter (homeoffice=nv_true на сервері + пагінація) або off.

Змінено

  • Немає ліміту результатів. MAX_STORED_RESULTS (2000) прибрано — зберігаються всі збіги, таблиця #/scan гортає їх (200/стор.).
  • Надійність фільтра заголовків — короткі абревіатури (COO, SDR…) збігаються за межами слів; некоректний title_filter більше не ламає скан. Обидва сканери.

Тести

  • +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 запускає власні внутрішньопроцесні сканери (він не викликає shell out до батьківського 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), а провідні = + - @ нейтралізуються проти ін’єкції формул електронних таблиць.
  • secondaryLocations Ashby (батьківський #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 out): резервні шрифти 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 (та застереження про неофіційний інтерфейс) не змінилося — перемістилися лише 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/inlinegenerate-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.