Журнал змін
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_ADAPTERS85 → 87. Обидва без токенів.- Collage (
provider: collage) — публічний API джоб-сайтів Collage HR. Сегмент шляху — це адреса джоб-сайту, яку орендар обрав сам, а не слаг із назви компанії, тож він читається зapi:або з careers-адресиsecure.collage.co/jobs/<адреса>і ніколи не вгадується: вгадана адреса призвела б до сканування чужої дошки. - Telegram (суворий) (
provider: telegram-channel) — читає ті самі попередні переглядиt.me/s/<канал>, що й наявне джерелоtelegram, але з протилежним розміном: допис стає рядком лише якщо називає роботодавця Й посилається на сторінку вакансії. Дайджести, дописи з прихованим роботодавцем і дописи «пишіть у личку» відкидаються. Низька повнота за задумом — вище за течією виміряно 32–77 % дописів, що проходять, — але кожен рядок несе справжнього роботодавця і справжнє посилання. Обидва лишаються, як і в батька.
- Collage (
- 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.mjsCLI батьківського проєкту. - Реєстр без змін: 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_ADAPTERS81 → 85, усі без токенів. Built In охоплює дев’ять ринків американської технічної дошки й не несе типового запиту: запис безqueries:таcategories:не сканує нічого і прямо про це каже, бо спільне джерело не має нести чиїсь пошукові слова; ринок поза списком відхиляється, а не підміняється тихцем національною дошкою. Feishu Jobs приймає рівно дві форми хоста, і вони мають різні шляхи до сторінки вакансії. Garena — одна компанія:officeформує посилання, а не список. MokaHR повертає зашифровану відповідь (конверт AES-128-CBC, ключ поруч) — це обфускація, не захист, але без розшифрування нічого не прочитати.
Виправлено
htmlToTextміг пропустити в текст незакритий відкривальний тег.safe <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.
buildAskPromptdidif (ctx.length + sec.body.length > MAX_CONTEXT) break— abreak, 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 addedtelegram_channelsto 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, sotelegram_channelsis 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 (продуктовая разработка, высоконагруженные системы)becameFinCore 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 (soCoca-Colasurvives), 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/healthreports 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 singleincludes(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-designwas 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.mjsalso 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 onlyreferomatches 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_ADAPTERS80 → 81. Telegram не публікує RSS, а Bot API не читає канал, яким бот не адмініструє — обидва очевидні шляхи закриті. Прев’ю/s/— звичайний HTML із сервера: без входу, без JS, без кук. Перевірено на 16 каналах: 15 віддали по 20 дописів на голий GET. GET /api/scan-resultsотримав посторінковий режим запиту.?q=<текст>&limit=50[®ion=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
Totalrow 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-хвилиннийrequestTimeoutNode (зменшена репродукція показала, що він не рве довгу стрімінгову відповідь). Справжнім доказом став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 локалей відкривали#/help268 символами буквального<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_ADAPTERS80.
[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_ADAPTERS80 — це новий РИНОК на наявному адаптері, а не новий адаптер. - Фільтр «лише термінові» на дошці каденції (
#/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_ADAPTERS80 — обидва видно у фільтрі «Джерело» на#/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