Журнал змін

GitHub

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

[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 (safeEncodeURIComponent → null при 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.4 — high, вразливості 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_filter (і content_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 батька 8d64f65 → 6cc46a4; його 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.svg — 18 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 без коду для OpenWorker — Fighter90/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.

career-ops-ui v1.267.1 · page 4/12 · mirror of the project changelog for cvstart.org