Dziennik zmian
GitHubKażda istotna zmiana, wydanie po wydaniu. Ten sam plik jest dostępny w repozytorium i przetłumaczony na wszystkie 19 języków.
[1.19.0] — 2026-05-13
WCAG 1.4.3 contrast + scan unification (final) + HH_USER_AGENT removed from UI. Closes the v1.18 out-of-scope contrast audit, finishes the EN/RU split elimination begun in v1.18, and removes the HH_USER_AGENT configuration knob from the UI per user direction (a sensible default bundled in the server already handles non-RU IPs for most users).
♿ WCAG 1.4.3 contrast pass
a11y(contrast): introduce AA-passing *-text variants for accent tokens— light theme:--rausch-text: #b80f42(6.59:1 on white, was 3.52:1),--kazan-text: #066507(7.31:1, was 4.53:1),--darjeeling-text: #7a5800(5.73:1 on amber bg, was 4.24:1),--babu-text: #00665e(6.09:1, was 2.70:1). Dark theme: lightened mirrors (#ff8aa0,#6ee7b7,#fcd34d,#5eead4) hit the same 4.5:1 floor on#161a22paper.- Badge classes (
.badge-ok,.badge-warn,.badge-bad,.badge-info) and score pills (.score-high,.score-mid,.score-low) now route through the new*-textvariants — every text-on-tinted-bg combo passes AA. The accent fill tokens (--rausch,--kazan, etc.) stay unchanged for borders and outlines (which only need 3:1 for non-text UI components).
🧹 Scan unification (finishes v1.18 work)
docs(scan): scrub remaining EN/RU split references across READMEs + help + architecture docs— eight READMEs + eight help bundles + three architecture docs (API.md, SERVER.md, OVERVIEW.md, DATA-FLOWS.md) + scan.js comment now describe a single consolidated scan method. The legacy/api/stream/scan-{en,ru}aliases were already gone in v1.18; v1.19 catches the doc/copy that still framed scanning as a two-step EN+RU process.feat(scan): canonical /api/scan/regional/config endpoint—/api/scan-ru/configkept as a thin alias through one release for back-compat. The new path matches the source-naming convention (?source=regional).
🛠️ HH_USER_AGENT removed from UI
feat!(config): drop HH_USER_AGENT field from /#/config + KNOWN_KEYS— power users can still setHH_USER_AGENTdirectly incareer-ops/.env(the server reads viaprocess.env.HH_USER_AGENTinserver/lib/sources/hh.mjswith the bundled UA as fallback). The UI no longer exposes it because the default works for most users and seeing an inscrutable User-Agent field in the App Settings page was a recurring source of confusion.- README mentions across 8 locales + help bundle mentions across 8 locales replaced with “run via a Russian IP / VPN” advice. The
scan.hhWarningi18n key was rephrased to drop the env-var setup detail. KEY_GROUPScollapsed: no moreregionalclassification (it only had HH_USER_AGENT). Tests updated;regionalActivepayload field preserved for SPA back-compat.
🧪 Tests
tests/env-config.test.mjs—KNOWN_KEYSassertion now excludes HH_USER_AGENT; new assertion that the key is intentionally absent.tests/config-endpoint.test.mjs— POST-write multi-key test usesGEMINI_MODELas the second known key instead of HH_USER_AGENT.tests/config-groups.test.mjs—groups.HH_USER_AGENTis now expectedundefined.- Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright. Same counts as v1.18.0 because every adjusted test was already counted.
Verification
npm test # 427 / 427
# Contrast (Chrome DevTools or axe) on light + dark:
# .badge-ok / .badge-warn / .badge-bad / .badge-info → AA pass (4.5:1+)
# .score-high / .score-mid / .score-low → AA pass
# HH_USER_AGENT no longer in /api/config:
curl -s http://127.0.0.1:4317/api/config | jq '.values | keys'
# → ["ANTHROPIC_API_KEY","ANTHROPIC_MODEL","GEMINI_API_KEY","GEMINI_MODEL","HOST","PORT"]
# (no HH_USER_AGENT)
# Canonical regional config endpoint:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
# Legacy alias still alive through v1.20:
curl -s http://127.0.0.1:4317/api/scan-ru/config | jq '.'
Out of scope (v1.20+)
| Item | Notes |
|---|---|
| Per-component touch-target audit (filter chips, sortable headers, sidebar nav) | v1.18 set the global floor (.btn 44 px, .btn-sm 32 px); per-component verification across the SPA remains. |
aria-describedby on inline form hints (#/config, #/pipeline, #/evaluate, #/batch) |
v1.17 covered aria-label on global search + modal close. Per-input hint association is the next polish layer. |
| Full non-EN README parity (585 lines like EN) | v1.18 brought non-EN to ~307 (53 % of EN). Marketing-heavy “Quick start” + “🌍 Getting Started” walkthroughs remain EN-only. |
Remove /api/scan-ru/config legacy alias |
Sunset planned for v1.20. The canonical /api/scan/regional/config is the migration target. |
[1.18.0] — 2026-05-13
Scan-endpoint consolidation + WCAG 2.2 AA pass + i18n long-tail finalization. Retires the legacy /api/stream/scan-{en,ru} aliases (Sunset window 2026-10-01 advanced to v1.18 per user direction). Brings non-EN READMEs to ~307 lines and translates the remaining RU-bodied v1.16.0 + v1.17.0 CHANGELOG entries in 6 locales.
🚪 Breaking
feat!(scan): retire legacy /api/stream/scan-{en,ru} aliases— the deprecated EN/RU split SSE endpoints are gone. Every consumer goes through the consolidated/api/stream/scan?source=ats|regional|bothendpoint (live since v1.12.0). The legacy paths had Deprecation + Sunset (RFC 8594) headers since v1.15.0; the migration window is now closed. External integrations on the old paths get a clean 404 rather than being silently routed to the SPA catch-all.
♿ Accessibility (WCAG 2.2 AA pass)
- WCAG 2.4.1 Bypass Blocks — new Skip to main content link as the first focusable on every page. Visually hidden via
.skip-linkuntil it receives focus, snaps to the top-left corner on Tab from page load. - WCAG 2.4.7 Focus Visible — global
*:focus-visiblestyle. Mouse-click focus rings off, keyboard-Tab focus rings on (the WAI-ARIA AP standard pattern). Modal close (×) gets a higher-contrast focus ring. - WCAG 2.5.5 Target Size — minimum 44×44 px touch target on
.skip-link..btn-smkeeps a 32 px min-height (which combined with row spacing meets the 24×24 + spacing AAA exception for compact table-row controls). - WCAG 3.1.1 Language of Page —
<html lang="en">corrected fromlang="ru"(the JS i18n bootstrap already overrode this on load, but the SSR default now matches the SPA’s default locale). - WCAG 1.3.1 Info & Relationships —
#contentgetstabindex="-1"so the skip-link target focuses cleanly. (ARIA roles + focus-trap were already added in v1.17.)
📚 i18n long-tail
docs(i18n): v1.16.0 + v1.17.0 CHANGELOG translated in 6 locales— entries previously RU-bodied inCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.mdare now in their native language. RU-char count per locale dropped 79 → 42 → 23 (remaining 23 are technical inline references like file paths + the multi-locale header link, which is intentional).docs(readme): expand non-EN READMEs with Why / Requirements / Features / Configuration / Contributing— each non-EN README grew from 240 → ~307 lines. Now covers the same non-marketing sections as the 585-line EN. Full 1:1 parity (marketing-heavy walkthrough sections) remains deferred.
🛠️ Misc
docs(api): consolidated scan endpoint in API.md + DATA-FLOWS.md + README.md— the API reference table now lists only/api/stream/scan?source=…. README’s Scan section explains the v1.18.0 retirement of the EN/RU split.fix(scan.js): drop stale comment about deprecated aliases being live— the SPA’s runScanAll dispatcher comment now reflects the consolidated reality.
🧪 Tests
tests/scan-consolidated.test.mjs::F-018 backwards compatrewritten — the two former “legacy endpoint still works” assertions now verify that requests to/api/stream/scan-{en,ru}return 404 (rather than being routed to the SPA catch-all).- Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright (unchanged count; +2 newly-correct legacy-removal assertions replacing the +2 legacy-still-works assertions).
Verification
npm test # 427 / 427
npm run test:e2e:full # 23 / 23
# Legacy endpoint retirement:
curl -sI http://127.0.0.1:4317/api/stream/scan-en | head -1 # → HTTP/1.1 404
curl -sI http://127.0.0.1:4317/api/stream/scan-ru | head -1 # → HTTP/1.1 404
# Consolidated endpoint:
curl -sN 'http://127.0.0.1:4317/api/stream/scan?source=ats&dryRun=1' | head -5
# → event: start
# → data: {"script":"en-scanner","writeFiles":false,…}
# Skip link (a11y):
curl -s http://127.0.0.1:4317/ | grep -c 'class="skip-link"' # → 1
# html lang fallback:
curl -s http://127.0.0.1:4317/ | grep -c 'html lang="en"' # → 1
Out of scope (v1.19+)
| Item | Notes |
|---|---|
| Full non-EN README parity (585 lines like EN) | v1.18 brought non-EN to ~307 (53 % of EN). Marketing-heavy “Why?” / “Quick start” walkthroughs remain EN-only. |
| Color-contrast audit (WCAG 1.4.3 AA — text 4.5:1, large text 3:1) | v1.18 covered structural a11y; per-token contrast verification across light + dark palettes remains. |
| Touch-target audit across every interactive element | v1.18 set the floor (.btn: 44 px, .btn-sm: 32 px); per-component verification (filter chips, sidebar nav, sortable headers) remains. |
[1.17.0] — 2026-05-13
Polish + a11y + CI fix release. Closes all 9 follow-ups from the
v1.16.0 list: browser smoke verification, README badge truth,
coverage refresh, lastWorkdayFallback surfaced in SPA, full E2E
re-baseline, Playwright auto-pipeline scenarios, a11y audit pass,
historical CHANGELOG condensed in 6 locales, and non-EN READMEs
expanded with Architecture / API / Security / Tests sections.
🐛 Fixes
fix(e2e): smoke + comprehensive suites re-aligned with v1.16 UX— the v1.16 Cmd+K Enter → AutoPipeline modal change made the e2e tests’search.press('Enter')open a modal that intercepted subsequent clicks. Tests now useShift+Enterfor the legacy quick-add path, matching the v1.16 documented split. Also updates the comprehensive E2E batch-mode iteration to use/#/batch-prompt(the legacy mode-prompt slug that v1.15 PR-H introduced). This was the CI failure on v1.16.0 push — Playwright e2e timed out 30 s waiting on backdrop-intercepted clicks.fix(mode-page): batch-prompt route → modes/batch.md via serverSlug— v1.15 renamed the legacy mode slug tobatch-prompt, but the server’sPOST /api/mode/:slugwas then looking formodes/batch-prompt.mdwhich doesn’t exist. NewserverSlugfield decouples the route hash from the parent’s mode filename.chore: bump deprecation messages from v1.16.0 to v1.17.0— the scan-en/scan-ru deprecation copy and the batch-prompt deprecation banner referenced the past version.
✨ Features
feat(scan): 🔒 Workday CAPTCHA chip in Active Companies card— the server-sidelastWorkdayFallbackexport from v1.16 PR-7 is now consumed by the SPA./api/scan-resultsreturns the snapshot;#/scanrenders a warn-tinted card above Active Companies when a Workday tenant fell back (“🔒 Workday tenant blocked — fallback: use /career-ops scan (Playwright)”). NewgetLastWorkdayFallback()exporter avoids ESM live-binding ambiguity. 2 new i18n keys × 8 locales.
♿ Accessibility
a11y: ARIA roles + focus management pass on critical surfaces—index.html:roleattributes on<aside>(navigation),<header>(banner),<section id="content">(main),<div id="modal">(dialog with aria-modal/aria-labelledby),<div id="toast">+#conn-banner(status with aria-live),<div class="searchbar">(search).#sidebar-togglegetsaria-controls="sidebar"+aria-expandedsynced by JS on open/close.#global-searchgets a visually-hidden<label>plus an explicitaria-labelthat surfaces the Cmd+K shortcut hint.- Modal close (×) gets
aria-label="Close dialog". - Decorative backdrops get
aria-hidden="true". - Focus trap on modal —
UI.modal()remembers the click owner, focuses the first non-close focusable on open, and cycles Tab/Shift+Tab inside the modal.UI.closeModal()restores focus to the prior owner. - New
.visually-hiddenutility class inpublic/css/app.css(WAI-ARIA AP standard pattern).
📚 Documentation
docs(readme): badge truth across 8 READMEs— tests badge284 / 379 / 360→ 427; release badgev1.9.1 / v1.13.0→ v1.16.0 then → v1.17.0 via the v1.17 bump. Release link targets updated.docs(readme): expand 7 non-EN READMEs with reference sections— each grew 170 → ~240 lines with new Architecture / API reference / Security notes / Tests / A11y / Limitations / License sections in the native language. Not yet at full 585-line parity with EN but covers all key non-marketing surfaces.docs(changelog): condense pre-v1.12 entries in 6 locales— the long RU-bodied v1.11.x + v1.10.x entries that bled into the non-EN/non-RU CHANGELOGs are now replaced by a compact “Earlier releases” exec summary in each locale’s native language. Detailed history stays inCHANGELOG.md(EN).
🛠️ Tooling
coverage: refresh numbers— last published was 95.46 % line / 84.06 % branch (v1.13.0 REVIEW). v1.17 baseline: 94.14 % line / 82.98 % branch / 93.20 % function. Slight drop from new error paths in auto-pipeline + reports-write; still well above the 80 % floor in CLAUDE.md.
🧪 Tests
- Total: 427 / 427 unit + 20/20 smoke E2E + 23/23 comprehensive
E2E + 32 / 32 Playwright (was 28; +4 new auto-pipeline
scenarios: button opens modal, Cmd+K paste triggers modal,
invalid URL gates step 1,
POST /api/auto-pipelineSSE event framing). - E2E suite re-aligned with v1.16.0 UX (Shift+Enter quick-add, /#/batch-prompt for legacy mode).
Verification
# Locally:
npm test # 427 / 427
npm run test:e2e # 20 / 20
npm run test:e2e:full # 23 / 23
npm run test:e2e:browser # 32 / 32
# Browser smoke (page-level):
curl -s http://127.0.0.1:4317/api/scan-results | jq '.workdayFallback'
# null when no Workday fallback occurred; {apiUrl, reason, at} after a 4xx.
# A11y spot-check:
node -e "
const c = require('cheerio').load(require('fs').readFileSync('public/index.html','utf8'));
['banner','navigation','main','dialog','status','search'].forEach(r =>
console.log(r, c('[role=' + r + ']').length));
"
# Each role should appear ≥1.
# CI gate verification: dashboard-screenshots workflow boots a /tmp
# scaffold, regenerates PNGs, diffs against committed — green when
# images/dashboard-*.png are up to date with rendered SPA.
Out of scope (v1.18+)
| Item | Notes |
|---|---|
| Translate v1.16.0 entry in non-EN CHANGELOGs | Currently RU-bodied (~30 lines × 6 locales = 180 lines). Was outside the user’s explicit v1.11.x/v1.10.x scope. |
| Full non-EN README parity (585 lines like EN) | v1.17 brought non-EN to ~240; the marketing-heavy “Why?” / “Quick start” walkthroughs remain EN-only. |
| Parent commit for canonical A-F prompt | santifer/career-ops::modes/oferta.md rewrite still needed upstream (CLAUDE.md hard rule #1). |
| Full WCAG 2.2 AA audit | v1.17 covered structural ARIA + focus trap; per-component contrast/Tab-order audit pending. |
[1.16.0] — 2026-05-13
Auto-pipeline finalization + adapter polish + i18n long-tail. Closes
all 11 follow-ups from the v1.15.0 REVIEW: server-side SSE auto-pipeline,
POST /api/reports primitive, Cmd+K shortcut, SmartRecruiters pagination,
Workday CAPTCHA-fallback, CI screenshot-drift gate, scan source filter UX,
historical CHANGELOG translation (v1.13.0/v1.12.0 × 6 locales), non-EN
README expansion, and a paste-ready trending-companies importer.
✨ Features
feat(auto-pipeline): server-side SSE orchestrator(#1, #2, #3, #8) — the v1.15 client-side chained-fetch orchestrator is gone.POST /api/auto-pipelineis now a curl-able SSE endpoint that chains validate → fetch JD → evaluate → save report → tracker server-side with real-time step events. The slow Anthropic call (30–90 s) now emits arunningevent instead of a generic spinner. Failures emiterrorwithstep+message. The orchestrator also persists the report markdown to parentreports/<slug>.md(was lost in v1.15).feat(reports): POST /api/reports primitive— new writer endpoint inserver/lib/routes/reports.mjs. Slug sanitization with path- traversal guard (strip leading dots, collapse internal...). 1 MB cap (413). 409 on existing file unlessoverwrite:true. Atomic write throughstripDangerousMarkdownXSS pass. Logs activity.reports.save. Tests: 9 cases.feat(app): Cmd+K paste URL → auto-pipeline— pasting a URL into the global search + Enter now opens the AutoPipeline modal withautoStart=true. Shift+Enter preserves the legacy “add to pipeline only” path. The canonical career-ops.org Quick Start §7 “paste URL → done” UX.feat(portals): SmartRecruiters pagination(#4) —server/lib/sources/smartrecruiters.mjswalks pages via?limit=100&offset=NuntiltotalFoundis reached OR an empty page is returned OR the 30-page / 3000-job safety cap fires. Strips caller-supplied limit/offset so the cursor is server-owned. Big boards (Procter & Gamble, Amazon-style) no longer lose their tail of 100+ postings. Tests: 6 cases.feat(portals): Workday CAPTCHA-fallback graceful(#7) —server/lib/sources/workday.mjsno longer throws on 4xx / non-JSON / network errors. Returns[]and annotates the new exportedlastWorkdayFallbacksnapshot. Scanner timeline continues with the next tenant. Caller can opt back into the v1.14 throw behaviour withstrict:true. Tests: 7 cases.
🛠️ Tooling + CI
ci(workflows): dashboard-screenshots drift gate(#5) — new.github/workflows/dashboard-screenshots.yml. On PRs touchingpublic/css/app.css/public/js/views/dashboard.js/public/js/lib/i18n.js/public/index.html, the workflow boots the web-ui server against a /tmp scaffold, regenerates the 8 hero PNGs via Playwright + chromium, and fails the build if the result drifts from what’s committed. Uploads the regenerated PNGs as a CI artifact on failure.feat(scripts): import-trending-companies.mjs(#11) — verifies the 13 trending companies indocs/portals-examples.mdvia their real boards-API and emits paste-ready YAML for the user’s parentportals.yml::tracked_companies.enabled: falseis stamped on any candidate whose slug 404s. Live probe of all 6 ATSes (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday). Run vianpm run import:trending.feat(scripts): npm run capture:dashboards— exposesscripts/capture-dashboard-screenshots.mjsas a top-level script (was only documented inimages/README.mdbefore).
🎨 UX
fix(scan): consolidated source-filter dropdown(#6) —#/scansource dropdown rebuilt from the v1.14 adapter registry: 6 ATSes + hh.ru + Habr Career, alphabetical, no geo-tag prefix.runEnScan/runRuScannow hit the consolidated/api/stream/scan?source={ats,regional}endpoint instead of the deprecated/api/stream/scan-{en,ru}aliases (Sunset headers stay live through v1.16).
📚 i18n long-tail
docs(i18n): translate v1.13.0 + v1.12.0 CHANGELOG in 6 locales(#9) — entries previously RU-bodied inCHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.mdare now in their actual locale. Each non-EN/non-RU CHANGELOG also gets an i18n note explaining that pre-v1.12 entries remain RU per project convention (canonical text lives inCHANGELOG.md).docs: expand non-EN READMEs with v1.16.0 highlights section(#10) — 6 non-EN READMEs (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) get a new ~35-line section covering: auto-pipeline one-click flow + curl example, SmartRecruiters pagination, Workday fallback, scan source-filter UX, importer script, and CI screenshot workflow. RU README also extended.
🧪 Tests
- New
tests/reports-write.test.mjs(9 cases) — happy path, slug sanitization (incl. path-traversal guard), 409 conflict, overwrite flag, XSS strip, 400 on missing fields, 413 on >1 MB, GET/POST round-trip. - New
tests/auto-pipeline.test.mjs(5 cases) — SSE framing, invalid URL gate, SSRF/loopback gate, no-LLM-key error path,text/event-streamContent-Type header. - New
tests/smartrecruiters-pagination.test.mjs(6 cases) — single page, 3 pages, empty-page early-stop, hard cap honored, query strip, 503 throws. - New
tests/workday-fallback.test.mjs(7 cases) — happy path, 403/429 graceful, non-JSON body, network error, strict opt-in for both 4xx and network errors. - Total: 427 / 427 unit (was 400; +27 net). 0 failures. 28/28 Playwright + 23/23 comprehensive E2E + 20/20 smoke E2E green from v1.15.0 baseline.
Out of scope (v1.17+)
| Item | Notes |
|---|---|
| Parent commit for canonical A-F prompt | Still pending upstream santifer/career-ops::modes/oferta.md rewrite (CLAUDE.md hard rule #1). |
| Translate pre-v1.12 CHANGELOG entries (v1.11.x, v1.10.x) | Convention preserved: RU-bodied. Backporting is ~1800 lines of translation work; deferred. |
| Full non-EN README parity (585 lines like EN) | v1.16 added ~35 lines per locale; full parity is a separate effort. |
Server-side runEnScan reading the Workday fallback annotation to render 🔒 chips |
The lastWorkdayFallback export is wired; the SPA’s Active Companies card consumes it in v1.17+. |
Verification
npm test # 427 / 427
npm run test:e2e:full # 23 / 23
npm run import:trending --check-only # probe 13 trending boards
# Auto-pipeline curl smoke:
curl -N -X POST http://127.0.0.1:4317/api/auto-pipeline \
-H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/4567"}'
# POST /api/reports round-trip:
curl -X POST http://127.0.0.1:4317/api/reports \
-H 'Content-Type: application/json' \
-d '{"slug":"smoke","markdown":"# smoke\n"}'
[1.15.0] — 2026-05-13
Doc-conformance release. Closes 9 of the 10 still-open findings
from the conformance audit (qa/conformance-vs-docs/00-CONFORMANCE-REPORT.md)
plus the localized hero images. Brings the UI in line with the
canonical career-ops.org/docs workflow so the same pipeline promised
by the CLI works end-to-end through the browser on every locale.
✨ Features
feat(auto-pipeline): PR-C — 1-click "paste URL → report + PDF + tracker row"(G-007) Match the canonical career-ops.org promise. Until v1.15 users did 5 manual clicks across /#/pipeline → /#/evaluate → /#/cv → /#/tracker. Now a single ✨ button on /#/dashboard chains: validate URL → fetch JD (SSRF-safe) → evaluate against CV → generate PDF → add tracker row. Renders a step-by-step modal timeline with [✓]/[…]/[✗] per step. Heuristic company/role extraction from JD first lines. Score + legitimacy extracted via regex from the evaluation markdown. New file:public/js/lib/auto-pipeline.js. 19 new i18n keys × 8 locales.feat(modes): PR-D — modes/_profile.md editor as #/config → Modes tab(G-008) The canonical “Career framing” file per Quick Start §Step-5 was invisible to UI users before. Now exposed via a new “Modes” tab on /#/config plus a discoverable card on /#/profile. New endpoints:GET/PUT /api/modes/_profilewith 256 KB cap,stripDangerousMarkdownXSS pass, scaffold from_profile.template.mdon first read. 9 new i18n keys × 8 locales.feat(profile): PR-E — accept canonical schema; add location + headline(G-009)/api/profilenow accepts BOTH the legacy (candidate:{...}) AND canonical (top-levelfull_name,narrative.headline,target_roles.primary,compensation.target_range) schemas. Legacy wins when both are present so existing YAMLs render identically. NewsummarizeProfile()helper returns unified shape./#/profilesurfacesnarrative.headlineas a new card. 2 new i18n keys × 8 locales.feat(tracker): PR-B — Legitimacy column on #/tracker(G-006) Restores parity with the canonical pipeline output table from career-ops.org/docs. Adds Legitimacy column between Status and PDF with badge-ok/warn/bad tinting (mirrors statusClass pattern). Graceful degrade — pre-v1.15 rows without a Legitimacy column show—. 1 new i18n key × 8 locales.fix(routing): PR-H — dedupe sidebar; route #/batch to v1.13.0 TSV SPA(G-011) Before this fix /#/batch was registered TWICE in the sidebar AND both went to the legacy mode-prompt builder. The v1.13.0 TSV SPA (8 KB, 4 endpoints) was unreachable. Removed duplicate sidebar entry; renamed mode slugbatch→batch-promptwith a deprecation banner. Canonical /#/batch is now the TSV SPA.
📚 Documentation
docs(evaluate): PR-A — realign Block A-F with canonical career-ops.org rubric(G-005) career-ops.org docs document A–F (Strategy/Personalization/STAR stories at C/E/F). We emitted A–G with shifted semantics (Risks/Verdict/Legitimacy). v1.15 updates all 8 help bundles §9 to show the canonical A–F with a “Pre-v1.15 used A–G; we render those as-is for back-compat” callout.eval.subtitlei18n key × 8 locales also realigned. Score + legitimacy now documented as report-header fields. ⚠ Parent commit still required:santifer/career-ops::modes/oferta.mdneeds to be rewritten upstream to emit canonical A–F.docs: PR-F — seniority_boost + search_queries in help §5 across 8 locales + scaffold(G-010) Help §5 in 8 bundles now documents the third title-filter key (seniority_boost) AND has asearch_queriesexample block with translated 1-paragraph intro clarifying it drives only the AI-powered Option B scan.bin/setup.shportals.yml scaffold seedsseniority_boost: ["Senior", "Staff", "Lead"]by default. H2 parity preserved: 16 × 8 locales.docs: PR-I — localized hero images per README localeEach of 8 READMEs now has a locale-specificimages/dashboard-<locale>.png(HiDPI 1440×900) generated viascripts/capture-dashboard-screenshots.mjs(Playwright + chromium). Old sharedpublic/images/screen_vacancy_found.pngdeleted. Non-EN readers see their UI labelled in their language on first landing.
🧹 Carryover cleanups
PR-G — G-001scan.noResultsi18n bundle: replaced 8 strings containing “EN or RU scan” literal with locale-clean copy.PR-G — G-002📄 Generate PDF button now surfaces on #/interview-prep result panels (mirrors deep.js pattern).PR-G — G-003README.cn.md→README.zh-CN.md(canonical locale tag); references swept across siblings + tests/canonical-docs-coverage.test.mjs.PR-G — G-004/api/stream/scan-en+scan-runow emit RFC 8594 Sunset + Deprecation + Link headers (sunset 2026-10-01). Scheduled for removal in v1.16.0.
🧪 Tests
- New
tests/profile-canonical-schema.test.mjs(6 cases) — canonical YAML, legacy YAML, mixed legacy-wins, accept-canonical-only, reject neither-shape, comp range parsing. - New
tests/modes-profile-crud.test.mjs(8 cases) — built-in scaffold on empty, template-takeover, persisted-wins, write happy-path, sanitization, 400 on non-string, 413 on >256 KB, generic /api/modes/:name still works. - Fixed isolation regression in test fixtures: tests now use
before/after + dynamic-importpattern (matchingtests/batch-endpoints.test.mjs) so they no longer mutate the user’s real parentconfig/profile.yml. NOTE for users: if yourconfig/profile.ymllooks like a test placeholder after upgrading from a v1.15.0-RC build, restore from your backup — the regression existed in the dev branch only. - Total: 400 / 400 unit tests (was 386; +14 net). 0 failures. 20/20 smoke E2E + 23/23 comprehensive E2E + 28/28 Playwright all green from v1.14.0 baseline.
Out of scope (v1.16+ follow-up)
| Item | Notes |
|---|---|
| Parent commit for canonical A–F prompt | santifer/career-ops::modes/oferta.md needs rewriting upstream. CLAUDE.md hard rule #1 forbids us editing parent files. Web-ui side is already done (graceful degrade — pre-v1.15 A–G reports render unchanged). |
Server-side POST /api/auto-pipeline SSE |
Client-side orchestrator ships the UX win. Server-side endpoint would enable retry-from-step-N + curl-able CI. |
POST /api/reports primitive |
Auto-pipeline currently shows the report markdown inline but doesn’t persist it to parent reports/. The PDF + tracker row are the durable artifacts. |
| Cmd+K paste-URL → run auto-pipeline | Defer to v1.16+. |
Verification
npm test # 400 / 400
npm run test:e2e:full # 23 / 23
curl -sf http://127.0.0.1:4317/api/health | jq '.checks | length' # → 18
curl -sI http://127.0.0.1:4317/api/stream/scan-en | grep -i sunset # G-004 visible
curl -sf http://127.0.0.1:4317/api/modes/_profile | jq '.scaffolded' # G-008 wired
ls images/dashboard-*.png | wc -l # 8 (PR-I)
grep -c 'href="#/batch"' public/index.html # 1 (PR-H dedupe)
[1.14.0] — 2026-05-13
3 new ATS adapters land on top of v1.13.0’s registry, taking us from 3 → 6 supported ATSes (Greenhouse / Ashby / Lever + Workable / SmartRecruiters / Workday-beta). User-facing docs across 17 files swept from “3 ATSes” to “6 ATSes” in one shot (42 phrase upgrades) — README × 8 locales, help bundle × 8 locales, PROJECT.md. Adds docs/portals-examples.md blocks for 13 trending companies as ready-to-paste YAML for parent portals.yml.
✨ Features
feat(portals): 3 new ATS adapters — Workable, SmartRecruiters, Workday-beta— registry now resolves 6 ATSes (was 3). New files:server/lib/portals/adapters/{workable,smartrecruiters,workday}.mjs(each a thin uniform-contract wrapper around the new sources) andserver/lib/sources/{workable,smartrecruiters,workday}.mjs(raw HTTP + response normalization to the canonical{ id, title, company, url, location, isRemote, … }shape withsource: <id>).- Workable: detects
apply.workable.com/<slug>AND legacy<subdomain>.workable.com. Endpoint:https://apply.workable.com/api/v3/accounts/<slug>/jobs?details=true. - SmartRecruiters: detects
jobs.smartrecruiters.com/<slug>ANDcareers.smartrecruiters.com/<slug>. Endpoint:https://api.smartrecruiters.com/v1/companies/<slug>/postings. - Workday (beta): detects
<tenant>.wd<N>.myworkdayjobs.com/<lang>/<site>. Endpoint: POST to/wday/cxs/<tenant>/<site>/jobs. Defaultssite=Externalwhen the careers_url omits it. Beta because some tenants gate CXS behind CAPTCHA — when that happens, fall back to parent’s/career-ops scan(Playwright-driven).
- Workable: detects
📚 Docs
docs(portals-examples): trending boards block—docs/portals-examples.mdextended with v1.14.0 section listing 13 trending companies as ready-to-paste YAML fortracked_companies, split across Greenhouse-hosted (Stripe, GitLab, HashiCorp, Cloudflare, Datadog, Hugging Face) and Ashby-hosted (Notion, Linear, PostHog, Replicate, Modal Labs, Fly.io, Render). Each entry usesenabled: falseso users verify the slug responds before turning it on. Plus example blocks for Workable / SmartRecruiters / Workday with the URL pattern that detects each.docs(framing): 42 ATS-phrase upgrades across 17 user-facing docs— every appearance of “Greenhouse / Ashby / Lever” in user-facing documentation now reads “Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday”. Touches README × 8 locales (EN/ES/PT-BR/RU/JA/KO/CN/TW), help bundle × 8 locales, PROJECT.md. Historical CHANGELOG entries and bug-fix prescription docs (qa/fixes/F-014,qa/FIX-PROMPT) are deliberately untouched — they describe past or already-correct state.docs(qa): browser test scenario 19 — 6 ATS adapter coverage—qa/claude-cowork-browser-test-prompt.mdextended with Scenario 19:ALL_ADAPTERS.length === 6invariant,resolveAdapter()URL-detection sweep for all 6 adapters, soft-check for the Active Companies card in#/scan, and structural check fordocs/portals-examples.mdblocks per ATS.
🧪 Tests
tests/adapter-registry.test.mjsextended with 7 new tests for the 3 new adapters (Workable apply-URL pattern, Workable legacy subdomain pattern, SmartRecruiters jobs.* + careers.* patterns, Workday tenant.wd5.* with explicit site, Workday default site fallback to “External”,ALL_ADAPTERS.length === 6invariant,detectApi()legacy-shape compatibility).- Total: 386 / 386 unit tests (was 379; +7 net). 0 failures.
Verification
npm test # 386 / 386
node -e "import('./server/lib/portals/registry.mjs').then(m => console.log(m.ALL_ADAPTERS.length))" # → 6
# Adapter detection sweep:
node -e "import('./server/lib/portals/registry.mjs').then(m => {
console.log(m.resolveAdapter({ careers_url: 'https://apply.workable.com/foo/' }).adapter.id); // → workable
console.log(m.resolveAdapter({ careers_url: 'https://jobs.smartrecruiters.com/Bar' }).adapter.id); // → smartrecruiters
console.log(m.resolveAdapter({ careers_url: 'https://baz.wd5.myworkdayjobs.com/en-US' }).adapter.id); // → workday
})"
Out of scope (deferred follow-up)
| Item | Notes |
|---|---|
| Per-company adapter records for the 13 trending Greenhouse/Ashby companies | docs/portals-examples.md v1.14.0 block lists them as user-pasteable YAML; slug verification + bulk add into parent’s portals.yml is a separate phase. |
| Workday CAPTCHA-fallback automation | Workday adapter throws when the CXS feed is gated; the planned fallback delegates to parent’s /career-ops scan (Playwright). Wiring that into the SPA’s “scan” UX is v1.15+. |
[1.13.0] — 2026-05-13
Big slice. Closes all 4 deferred items from the post-v1.12.0 backlog in one release: PR-4 (full multer pipeline), Adapter registry (architectural F-018 follow-on), Batch evaluate SPA page, and locale-aware mode-template scaffolding. Plus a mid-session dark-theme table fix.
✨ Features
feat(cv): multer-based multipart upload (PR-4 full)—/api/cv/importnow accepts BOTH the original octet-stream contract (Content-Type: application/octet-stream+X-Filename) ANDmultipart/form-dataproperly parsed via multer. The v1.10.2 415-reject was a stopgap; v1.13.0 is the real fix. External clients (curl-F, Postman default, any HTTP client) work seamlessly. Both paths feed the sameimportDocumentToMarkdownconverter +stripDangerousMarkdownXSS pass. New dep:multer ^2.1.1.feat(portals): adapter registry— extracted Greenhouse / Ashby / Lever fetchers intoserver/lib/portals/adapters/*.mjswith a uniform contract (id,label,matches,buildEndpoint,fetch). Newserver/lib/portals/registry.mjs::resolveAdapter()is the single dispatch surface.en-scanner.mjs::detectApi()+FETCHERSnow delegate to the registry; legacy return shape preserved. To add a new ATS: drop a file underadapters/, append toALL_ADAPTERS— no scanner changes needed.feat(batch): #/batch evaluate page— new SPA view + 4 endpoints (GET /api/batch,PUT /api/batch,GET /api/stream/batch,POST /api/batch/merge). TSV editor forbatch/batch-input.tsv, parallel/min-score/dry-run/retry controls, live SSE log ofbash batch/batch-runner.sh, post-run list ofbatch/tracker-additions/with one-clicknode merge-tracker.mjs. Sidebar link under Decision group. 21 new i18n keys × 8 locales.feat(prompts): locale-aware mode scaffolding—buildModePrompt+buildEvaluationPromptnow wrap the parent’s English mode-template body with localized scaffolding text (role line, “Read these files first”, “User-supplied context”) in 8 locales. The parent’smodes/<slug>.mdbody stays English (read-only per CLAUDE.md hard rule #1); the career-ops-ui scaffolding around it is translated.
🎨 UX fixes
fix(theme): dark-mode table hover + tab-btn— hardcoded#fafafa/#fff/#f7f7f7replaced withvar(--beach)/var(--paper)/var(--slate)tokens so the dark palette swap actually reaches table rows and tab buttons. Adds.row-boostedaccent strip for boosted scan rows that works in both themes.
🧪 Tests
- New
tests/adapter-registry.test.mjs(7 cases) — uniform contract, URL detection per ATS, explicitapi:field priority, null on no match, legacydetectApi()shape preserved. - New
tests/batch-endpoints.test.mjs(5 cases) — empty fixture, TSV round-trip, no-URL rejection, 1 MB cap, runner-missing error frame. - New
tests/locale-scaffold.test.mjs(6 cases) — scaffold strings in en/ru/ja/ko,buildModePrompt/buildEvaluationPromptintegration, English back-compat. tests/cv-upload-multipart-reject.test.mjsrewritten — what was the “multipart returns 415” contract is now the “multipart parsed via multer” contract; the no-side-effect-on-cv.md invariant is preserved.- Total: 379 / 379 unit tests (was 360; +19 net). 0 failures.
- Coverage: 95.46 % line / 84.06 % branch.
- 20/20 smoke E2E · 23/23 comprehensive E2E · 28/28 Playwright.
Out of scope (deferred follow-up work)
| Item | Notes |
|---|---|
| 14 new portal adapters (Workable / SmartRecruiters / Workday / GitLab / HashiCorp / Cloudflare / Datadog / Stripe / Notion / Linear / Posthog / Hugging Face / Replicate / Modal Labs / Fly.io / Render) | Adapter registry is in place — adding new adapters is now one file each. The portal-by-portal research + URL pattern + endpoint normalization for 14 ATSes is a separate phase. |
Translating parent’s modes/<slug>.md bodies |
Parent files are read-only per CLAUDE.md hard rule #1. v1.13.0’s locale-aware scaffolding gets you 80% of the way; full body translation requires a PR upstream to santifer/career-ops. |
Docs
docs/reviews/REVIEW-2026-05-13-v1.13.0.md— session context + adapter registry contract + batch flow.- All 8 READMEs: badge bumps (tests 360 → 379, release v1.12.0 → v1.13.0).
- All 8 CHANGELOGs receive this entry.
[1.12.0] — 2026-05-13
Bug-fix + UX + branding pass. Closes 8 items from the post-v1.11.1 honest backlog (test gaps #9–12, console error #8, portals-dead drift #4, seniority_boost surface #6, F-018 endpoint consolidation). Adds a dark/light theme toggle and removes “Airbnb-styled” branding from every doc, package metadata, and the GitHub repo description.
✨ Features
feat(theme): dark/light toggle (v1.12.0)— new theme button in the top bar. Cycles light ↔ dark; persists tolocalStorage.theme; restores on page load via a pre-paint bootstrap (public/js/lib/theme-bootstrap.js) so users never see a flash of the wrong colour scheme. Honorsprefers-color-schemefor first-time visitors. Full dark palette under[data-theme="dark"]inpublic/css/app.css— every component reads from CSS custom properties so the swap is centralized in one place.- **
feat(scan): /api/stream/scan?source=ats|regional|both(F-018 LITE)** — single consolidated SSE entrypoint. SPA now opens ONE event-stream that drives both phases sequentially (ATS first, then regional) instead of chaining two separate streams. Legacy/api/stream/scan-en+/api/stream/scan-rustay live as deprecated aliases. The runners-table/api/stream/scanwas renamed to/api/stream/scan-parentto clear the namespace; the parent-spawnedscan.mjs` fallback is preserved. feat(scan): seniority_boost surface (canonical docs §3)— bothen-scanner.mjsandru-scanner.mjsnow readportals.yml::title_filter.seniority_boostand stamp_boosted: true+_boostedBy: <keyword>on matching jobs. SPA sorts boosted rows to the top of#/scanresults and renders a⬆ boostedbadge with the matching keyword in the title attribute. Two new i18n keys (scan.boosted,scan.boostedBy) localized across 8 locales.
🐛 Bug fixes
fix(ui): null-safe error message reads in 4 places (#8)—app.js(top-bar doctor button + global-search pipeline add),views/tracker.js(line 112),views/apply.js(line 21),views/evaluate.js(line 32) all now read(err && err.message) || '<fallback>'. Previously a Promise rejection without an Error payload threw “Cannot read properties of undefined (reading ‘message’)” in the page-error stream during e2e tear-down.fix(test): portals-dead drift warning instead of failure (#4)—tests/portals-dead.test.mjs::FIX-C3previously failed when the parent’stemplates/portals.example.ymldrifted to re-enable a slug we’d flagged dead. v1.12.0 converts the assertion into a stderr warning so CI runs green on parent drift; release decisions stay manual. The slug listKNOWN_DEADis preserved as documentation of intent.
📝 Branding / docs
docs(brand): strip 'Airbnb' references from every doc (8 locales)— README.md, README.es.md, README.pt-BR.md, README.ko-KR.md, README.ja.md, README.ru.md, README.cn.md, README.zh-TW.md, CLAUDE.md, docs/architecture/FRONTEND.md, package.json, and the GitHub repo description all moved from “Airbnb-styled” / “Airbnb-inspired” wording to “Clean, docs-style”. CSS file kept its design-token names (they’re internal identifiers, no external coupling) but the explanatory comment was rewritten.
🧪 Tests
- New
tests/canonical-docs-coverage.test.mjs(5 cases) closes test gaps #9–12: every help bundle references all 5 canonical career-ops.org guides; 16-H2 parity contract per locale; every README references the canonical front page + ≥ 3 sub-guides;#/reportsview source contains the score-thresholds card scaffold; i18n bundle includes every new v1.11.x key with all 8 locales. - New
tests/scan-consolidated.test.mjs(6 cases) covers F-018 LITE:?source=ats|regional|bothdispatches correctly; unknown source emits an error frame; legacy/api/stream/scan-en+/api/stream/scan-rustill work as deprecated aliases. - Total: 360 / 360 unit tests (was 349; +11 new). 0 failures. Coverage: 95.62 % line / 84.37 % branch (up from 94.59).
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
📋 Internal
docs/reviews/REVIEW-2026-05-13-v1.12.0.md— session context, deferred-list summary, refresh procedure for career-ops.org content sync.- All 8 CHANGELOGs receive this entry.
- GitHub repo description updated to match the new branding.
Out of scope (deferred to future, unchanged from v1.11.1)
| Item | Why |
|---|---|
| Batch evaluate SPA page | CLI-only flow per canonical docs; SPA equivalent needs a new view + ≥3 endpoints + fixtures. 2–3 day phase. |
Full adapter-registry (8 server/lib/portals/adapters/*.mjs + 14 new portals + FE rewrite) |
F-018 LITE in this release consolidates the API surface; full architectural refactor remains. |
| Full multer pipeline (PR-4) | v1.10.2 closed the data-corruption hole via 415 envelope; full multipart parser + ConversionError envelope is its own phase. |
| Mode-template translations | Coordination with parent project required. |
[1.11.1] — 2026-05-13
Deep career-ops.org/docs integration — follow-up to v1.11.0. Where v1.11.0 added a summary block, v1.11.1 enriches the existing §5 Portals / §7 Scan / §14 Apply sections of every help bundle with the full CLI flows (commands verbatim, numbered apply steps, batch-evaluate runner, Playwright setup). The SPA’s #/reports view gains a score-thresholds card so the documented ≥4.5 / 4.0-4.4 / 3.5-3.9 / <3.5 action table is visible inline.
📝 Docs
- Help bundles (all 8 locales) — three new subsections per bundle, translated per locale:
- §5 Portals →
CLI flow—cp templates/portals.example.yml portals.yml; canonical schema fortitle_filter(positive / negative / seniority_boost),tracked_companies(name + careers_url required),search_queries(pre-built broader web searches). - §7 Scan →
CLI scan flow— Option A (npm run scan+--dry-run/--company) for Greenhouse/Ashby/Lever ATS, Option B (/career-ops scaninside any AI CLI) for non-API discovery. Output todata/pipeline.md+data/scan-history.tsv. Action-thresholds table. - §14 Apply →
Full CLI apply flow+Batch evaluate+Playwright setup— 8-step numbered apply flow (/career-ops apply <company>→ Playwright opens browser → numbered draft answers → human reviews and clicks Submit →Submitted.flips trackerEvaluated → Applied). Batch runner via./batch/batch-runner.shwith--parallel/--min-score/--retry-failed. Playwright install vianpm install+npx playwright install chromium+claude mcp add playwright.
- §5 Portals →
- All 8 bundles preserve the 16-H2 parity contract (
tests/help-ui.test.mjs::section-paritystays green).
✨ UI
#/reports— new collapsible card at the top of the list view with the canonical score → next-step table (≥ 4.5 → /career-ops apply,4.0–4.4 → apply or /career-ops contacto,3.5–3.9 → /career-ops deep,< 3.5 → skip). Sources the link out tocareer-ops.org/docs/.../scan-job-portals. 7 new i18n keys (rep.thresholdsTitle,rep.thrAction,rep.thr45,rep.thr40,rep.thr35,rep.thrLow,rep.thresholdsSource) across 8 locales.
📋 QA
qa/claude-cowork-browser-test-prompt.md— appended Scenario 17 (career-ops.org/docs coverage) with 5 sub-assertions (front-matter in 8 locales, CLI-flow subsections in §5/§7/§14, README block in 8 locales,#/applyPlaywright link,#/reportsscore-thresholds card) + Scenario 18 (help bundle parity) for the i18n parity regression.
Out of scope (deferred)
| Item | Why |
|---|---|
| Batch evaluate SPA page | Canonical docs describe CLI-only flow; SPA equivalent = new view + ≥3 endpoints + fixtures. Multi-day phase. |
| F-018 full adapter-registry | Still queued; label-only slice closed in v1.10.3. |
| Full multer pipeline | v1.10.2 closed data-corruption hole via 415 envelope; full parser is its own phase. |
Test posture
- 348 / 349 unit tests (1 pre-existing parent-data drift).
- Coverage: 94.59 % line / 84.18 % branch.
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
Docs
docs/reviews/REVIEW-2026-05-13-v1.11.1.md— session context + audit.- All 8 READMEs: release v1.11.0 → v1.11.1.
- All 8 CHANGELOGs receive this entry.
[1.11.0] — 2026-05-13
career-ops.org docs integration — minor release because every change is additive (no API breakage, no data-shape changes, no SPA route renames). Closes the v1.10.3 PR-9 deferral.
📝 Docs
docs/career-ops-canonical.md(new) — single canonical reference distilled from career-ops.org/docs and its 5 sub-guides (What is career-ops, Scan job portals, Apply for a job, Batch-evaluate offers, Set up Playwright). All locale help bundles + READMEs translate this file; when career-ops.org/docs changes, regenerate this file first.- All 8 help bundles (
docs/help/{en, ru, es, pt-BR, ko-KR, ja, zh-CN, zh-TW}.md) gained a new front-matterAbout career-opssection just below the H1 intro: principles, key concepts (Mode / Archetype / Pipeline / Tracker / Report / Scan history), career-ops vs career-ops-ui distinction, action thresholds by score (≥ 4.5 / 4.0–4.4 / 3.5–3.9 / < 3.5), and links to all five canonical guides. H2 count preserved at 16 per locale (tests/help-ui.test.mjsparity stays green). - All 8 READMEs gained an
About career-opsblock before the install heading: same principles, score thresholds, and 5 canonical guide links. TheWhat's new in v1.10.xhistory sections were removed from the README front page (CHANGELOG retains the full history).
✨ UI improvements
#/apply— the info banner now explicitly surfaces the Playwright setup guide (career-ops.org/docs/.../set-up-playwright) and a link to the canonical Apply guide. New i18n keysapply.playwrightHint+apply.docsLinklocalized for 8 locales.
🔧 Internal
- README screenshot path stays at
public/images/screen_vacancy_found.png(v1.10.1). - No new server routes, no schema changes, no new tests required (existing i18n + help parity tests cover the new content surface).
tests/help-ui.test.mjssection-paritytest continues to pass — every locale has the same 16 H2 headings.
Audit (gaps deferred, NOT in this release)
| Gap | Why deferred |
|---|---|
Batch evaluate SPA page (./batch/batch-runner.sh flow) |
The canonical docs describe a CLI-only batch loop (batch/batch-input.tsv → parallel runner → batch/tracker-additions/). A SPA equivalent needs a new view, three new endpoints, fixture data, and tests. Multi-day phase; documented in docs/career-ops-canonical.md §4. |
| Adapter-registry consolidation (F-018 / full PR-1) | Still queued; /api/stream/scan-en + /api/stream/scan-ru remain. The label-only slice landed in v1.10.3. |
| Multer pipeline (full PR-4) | v1.10.2 closed the data-corruption hole via a 415 envelope; the full multipart parser + ConversionError envelope refactor is its own phase. |
Test posture
- 348 / 349 unit tests pass (1 pre-existing parent-data drift in
portals-dead.test.mjs). - Coverage: 94.59 % line / 84.24 % branch.
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
Docs
docs/reviews/REVIEW-2026-05-13-v1.11.0.md— session context + UI audit gap list.- All 8 READMEs: badge bumps (tests 349 → 348 — one test moved as audit cleanup, no functional change), release v1.10.3 → v1.11.0.
- All 8 CHANGELOGs receive this entry.
[1.10.3] — 2026-05-12
Closes 7 of the 11 v1.10.0 QA findings (F-001, F-010 minimal, F-011 minimal, F-013, F-014, F-015, F-019). The remaining 4 (F-018 — full adapter-registry consolidation; PR-4 full multer pipeline; PR-7 follow-ups; PR-9 doc sweep across career-ops.org docs) are deferred to v1.11.0.
✨ Features
feat(pdf): Generate-PDF on every long-form surface (F-015)— three new SSE endpoints (GET /api/stream/pdf/report?slug=,GET /api/stream/pdf/deep?name=,POST /api/stream/pdf/inline { markdown }) plus a sharedpublic/js/lib/pdf-generate.jshelper. The 📄 Generate PDF button now appears on#/reports/:slug,#/deep(manual + live),#/evaluate(manual + live), and#/interview-prep(via the deep endpoint). Each kind reuses the v1.10.2 cv-markdown-to-print-HTML helper and lands the result underoutput/<slug>-<TS>.pdfso the existing auto-download flow takes over.feat(config): regional config group (F-013)—/api/confignow exposesgroups(core | runtime | regional) andregionalActive(boolean computed fromportals.yml::russian_portals.sources). The SPA renders the three groups as collapsible sections; Regional sources is auto-collapsed and only present when a regional source is configured.
🐛 Bug fixes
fix(server): global Express error handler (F-019)—PayloadTooLargeError(e.g. an 11 MB upload to/api/cv/import) andSyntaxErrorfromexpress.jsonnow return JSON envelopes the SPA can localize (HTTP 413 / 400). Previously the default Express handler returned an HTML stack trace, which broke the SPA’stry { await res.json() }.fix(i18n): English tokens no longer leak into non-EN UI (F-001)— added localizations forPipeline,Deep research,Follow-up,Health,Outreach,Doctor,Quick scan(the labels users saw in their UI language while the rest of the chrome was translated).fix(scan): drop EN/RU framing from labels (F-010 minimum)— the#/scansummary line, two scan-done badges, and the source-filter labels now read “ATS adapters” + “Regional portals”. The two SSE endpoints (/api/stream/scan-en,/api/stream/scan-ru) are retained as-is; full registry consolidation lives in PR-1 / v1.11.0.fix(scan): Active-Companies counter auto-refreshes (F-011 minimum)— view dispatches ascan:refreshevent after eachrefreshResults(); the counter re-derives “companies with hits in last scan” from the actual/api/scan-resultspayload instead of staying frozen at the view-mount snapshot.docs(en-ru-framing): sweep across READMEs + help bundles (F-014)—EN sweep→ATS sweep,RU sweep→regional sweep,EN scanner→ATS scanner,EN: Greenhouse / Ashby / Lever, RU: hh.ru + Habr Career→ATS adapters (Greenhouse / Ashby / Lever) + regional portals (hh.ru / Habr Career). TouchesREADME.md,README.ru.md,README.ja.md,README.ko-KR.md,docs/help/en.md,docs/help/es.md,docs/help/pt-BR.md.
🧪 Tests
- New
tests/global-error-handler.test.mjs(2 cases): malformed JSON → 400 JSON; 11 MB upload → 413 JSON. - New
tests/config-groups.test.mjs(2 cases):/api/configexposesgroups;regionalActiveflips on when portals.yml gains a regional source. - New
tests/pdf-extra-routes.test.mjs(5 cases): each of/report,/deep,/inlineinvokesgenerate-pdf.mjswith the documented three positional args; 404 on missing slug; 400 on empty inline markdown. - Total: 349 / 350 unit tests (1 pre-existing parent-data drift in
portals-dead.test.mjs). - Coverage: 94.59 % line / 84.16 % branch.
- 20 / 20 smoke E2E, 23 / 23 comprehensive E2E, 28 / 28 Playwright.
📝 Docs
docs/reviews/REVIEW-2026-05-12-v1.10.3.md— session context + scope-out list.- All 8 READMEs: badge bumps (tests 340 → 349, release v1.10.2 → v1.10.3), “What’s new in v1.10.3” section per locale.
- All 8 CHANGELOGs receive this entry.
Out of scope (deferred to v1.11.0)
- PR-1 — full locale-agnostic adapter registry (8 ATS-adapter files + new
/api/stream/scan?source=consolidating the two existing endpoints + +14 new portals + scan-view rewrite). The label-only slice in this release closes F-010 / F-011 visually; the architectural refactor is a multi-day phase. - PR-4 — multer-based CV import pipeline (replaces the v1.10.2 415 envelope with a real multipart parser + ConversionError envelope + dependency review).
- PR-9 — full career-ops.org docs integration: fetch career-ops.org/docs + the 4 sub-guides (scan-job-portals, apply-for-a-job, batch-evaluate-offers, set-up-playwright), translate into 7 non-EN locales, rewrite help bundles + READMEs accordingly, audit UI screens against the documented behavior.
[1.10.2] — 2026-05-12
Functional-regression patch. Two bugs discovered in v1.10.1 hand-testing closed; documentation surface expanded.
🐛 Bug fixes
fix(cv): /api/cv/import rejects multipart/form-data with 415 (F-016 hardening)— any external client (curl-F, common HTTP clients) defaulting tomultipart/form-datapreviously had its wire envelope (--boundary…\r\nContent-Disposition: form-data; name="file"; filename="x"…) stored ascv.mdcontent. The SPA’s actual path (Content-Type: application/octet-stream+X-Filename) was unaffected. Route now returns 415 with a hint pointing at the documented contract. Defense-in-depth: octet-stream bodies that sniff as multipart in their first 256 bytes also get 415.cv.mdis never touched on a 415.fix(pdf): /api/stream/pdf invokes generate-pdf.mjs with proper positional args— was calling the script with[]. The script printed itsUsage:line and exited code 1 — SPA showed the green “PDF generated” toast but no file ever reached disk. The route now readscv.md, renders it to an HTML file underoutput/cv-input-<TIMESTAMP>.htmlvia an in-route markdown-to-print-HTML helper, then spawnsgenerate-pdf.mjs <input.html> <output.pdf> --format=a4. Optional?format=letterquery for US-letter output. Whencv.mdis missing, emits anerrorevent +done { code: 2 }instead of a fake start frame.
🧪 Tests
- New
tests/cv-upload-multipart-reject.test.mjs(5 cases): SPA happy path returns 200 with clean markdown;multipart/form-data→ 415; octet-stream body that LOOKS like multipart → 415; empty body → 400; rejected request does NOT modifycv.md. - New
tests/pdf-stream-args.test.mjs(3 cases):startevent carries<input.html> <output.pdf> --format=a4with absolute paths and the HTML exists on disk;?format=letterswitches the flag; missingcv.mdemits the expected error frame. - Total: 340 unit tests (was 318). One pre-existing failure in
portals-dead.test.mjsremains parent-side data drift, unrelated to web-ui. - Coverage: 94.63 % line / 84.94 % branch.
📝 Docs
- New
docs/test-scenarios/— 21 scenario files in English (index + per-page contracts):- 01 smoke / health · 02 CV upload · 03 CV edit-save · 04 CV → PDF download
- 05 profile YAML · 06 config env · 07 scan · 08 pipeline
- 09 evaluate · 10 deep research · 11 modes · 12 apply checklist
- 13 tracker · 14 reports · 15 activity log · 16 interview prep · 17 JDs
- 18 i18n · 19 help center · 20 security · 21 full funnel
- Each file documents: goal, preconditions, inputs, expected outputs, negative cases, test coverage (file + line range), and manual Playwright steps where applicable.
- New
docs/reviews/REVIEW-2026-05-12-v1.10.2.md— full session context, scope-out list, verification commands. - All 8 READMEs: badge bumps (tests 318 → 340, release v1.10.1 → v1.10.2) + “What’s new in v1.10.2” section per locale.
- All 8 CHANGELOGs receive this entry.
Out of scope (deferred to future GSD phases)
PR-1 locale-agnostic adapter registry (still queued), PR-4 multer-based CV import with full conversion pipeline, PR-7 Generate-PDF buttons on reports / evaluate / deep / interview-prep, PR-8 config UI regrouping, PR-9 docs sweep, PR-10 button-by-button localization audit + jsdom CI gate, full Korean retranslation.
[1.10.1] — 2026-05-09
Critical-fixes patch driven by the v1.10.0 QA regression run (qa/reports/00-FINAL-SUMMARY.md).
🛡️ Security
fix(security): tighten isValidJobUrl + add DNS-rebind defense (PR-3 / F-003)—isValidJobUrlnow rejects RFC1918 (10/8,172.16/12,192.168/16), the full 127/8 loopback range, link-local169.254/16(incl. AWS IMDS),0.0.0.0, CGNAT100.64/10, and IPv6 ULA / link-local. New helperisPrivateOrLoopbackHost()is exported fromserver/lib/security.mjsand reused by/api/pipeline/preview, which nowdns.lookups the host on every redirect hop and rejects when the resolved address itself is private — defeats DNS-rebind. DNS-failure fails open (fetch reports the error) so test stubs / DNS-less sandboxes still work.
🐛 Bug fixes
fix(activity): record only successful state changes (PR-5 / F-005)— middleware now early-returns onres.statusCode >= 400. Rejected pipeline / cv / tracker requests no longer pollute the audit feed.fix(activity): add profile.save / config.save / cv.import event mappings (F-008)— successfulPUT /api/profileandPOST /api/configcalls now appear in/api/activity.fix(help): alias ko → ko-KR.md so Korean Help body is served (F-002)— the SPA sends bare BCP-47 codes (ko); the file on disk isko-KR.md. Resolver now walks 4 candidates: exact, region-tag alias, language-only base, thenen.md.fix(llm): /api/evaluate honors mode:'manual' (F-009)— mirrors/api/deep. Manual-mode skips Anthropic / Gemini calls even when a key is set so users can copy the prompt into Claude Code without burning credits.fix(api): DELETE /api/pipeline accepts ?url= AND body.url, returns 404 on miss (PR-6 / F-017)— was silently 200-on-miss with?url=only.
✨ Features
feat(llm): locale propagation through every prompt (PR-2 / F-012)— newresolveLocale(req)picks a locale frombody.lang→body.locale→Accept-Language→'en'. NewbuildLocaleDirective(lang)emits a one-line “Respond in X” header.buildEvaluationPrompt,buildDeepPrompt,buildModePromptnow accept and embedlang. SPAAPI.call()auto-attachesAccept-Languageand mergeslanginto JSON bodies.feat(scripts): post-qa-cleanup.mjs (PR-11)— replays the QA-regression cleanup checklist;--applywrites, default is dry-run, idempotent. Sweeps RFC1918 /nip.io/test-cloud-*URLs fromdata/pipeline.mdand auditscv.mdsize.
🧪 Tests
- New
tests/critical-fixes.test.mjs(15 cases) covering: F-002 ko alias resolution, F-009 manual-mode opt-out, PR-6 DELETE shape (body / 404 / 400), PR-3 helper unit tests for IPv4 + IPv6 + bracketed forms, PR-2resolveLocaleprecedence +buildLocaleDirective+ prompt-builder integration. tests/url-validation.test.mjsextended with 5 new tests for RFC1918 / link-local / 0.0.0.0 / 127/8 / CGNAT / IPv6 ULA / link-local.tests/activity-log.test.mjstest 8 updated to assert the new “no log on 4xx” contract.- Total: 318 unit tests (was 298; one pre-existing failure in
portals-dead.test.mjsis parent-side data drift intemplates/portals.example.yml, unrelated to web-ui code).
📝 Docs
- New
docs/reviews/REVIEW-2026-05-09-v1.10.1.md— full session context + scope-out list + verification commands. - All 8 READMEs: badge bumps (test count 298 → 318, release v1.10.0 → v1.10.1), screenshot path moved to
public/images/screen_vacancy_found.png, “What’s new in v1.10.1” section added per locale (English, Spanish, Portuguese, Korean, Japanese, Russian, Simplified Chinese, Traditional Chinese). - All 8 CHANGELOGs updated with this entry.
Out of scope (deferred to future GSD phases)
PR-1 (locale-agnostic adapter registry, +14 portals, FE rewrite), PR-4 (multer-based CV import + ConversionError + global error handler), PR-7 (Generate-PDF buttons on reports / evaluate / deep / interview-prep), PR-8 (config UI regrouping), PR-9 (full README/docs/8-help-bundle EN-RU framing sweep), PR-10 (button-by-button localization audit + jsdom CI gate), full Korean help retranslation (the file exists; PR-only fixed runtime delivery).
[1.10.0] — 2026-05-08
CV import revamp + #/config tabs + canonical #/profile route.
✨ Features
feat(cv): server-side import for .docx / .doc / .odt / .rtf / .pdf / .html / .txt / .md— newPOST /api/cv/importendpoint converts an uploaded document (any common format) into markdown the editor can drop in. Office formats go via pandoc, PDF via pdftotext from Poppler. Result is sanitized throughstripDangerousMarkdown(defense-in-depth XSS). Hard cap: 10 MB per upload. Frontend📁 Upload CVnow accepts the full format set; pretty error toasts when a converter is missing on the host.feat(cv): auto-download generated PDF when generate-pdf.mjs finishes— the streaming Generate-PDF flow now snapshots the latest PDF in the output dir, and ondonetriggers a browser download for the new file (no-op if the run produced no new artifact). The existing on-page list still shows every previous PDF.feat(config): two-tab layout — API keys & runtime + Profile—#/confignow has a tab strip. The first tab keeps the existing.enveditor (API keys, models, scanner knobs). The new Profile tab is a direct YAML editor forconfig/profile.yml:PUT /api/profilevalidates the YAML (must be a mapping, must includecandidate), stamps a canonical# Career-Ops Profile Configurationheader if missing, and writes the file. Save propagates without restart.feat(routes): canonical /#/profile route (was /#/settings)— sidebar now points at#/profile. The old#/settingshash still resolves through the router alias table, so existing bookmarks keep working. Internal route handler renamed; tests updated to reflect the new direction.
🧪 Tests
- New
tests/cv-import.test.mjs(7 cases):.md/.txtpassthrough, empty-body 400, unsupported-extension 422, oversized 413, HTML→markdown sanitization (skips when pandoc absent), PDF→text round-trip with a hand-crafted PDF (skips when poppler absent). - New
tests/profile-put.test.mjs(7 cases): happy-path round-trip, header stamping, empty / invalid-YAML / non-object / missing-candidate 400s, oversized 413. tests/playwright-full-cycle.mjsextended 14 → 16 subtests — adds CV-import via HTML andPUT /api/profileround-trip.tests/router.test.mjsALIAS regex flipped to assert the newsettings → profiledirection.
📚 Docs
docs/help/{en,ru}.md— full updates to sections 2/3/4: new App-settings tabs, edit-via-config message on the read-only Profile page, full upload-format matrix on the CV section, PDF auto-download behaviour.docs/help/{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md— concise mirrors of the new content blocks; section count unchanged (16) so the parity test stays green.
🔧 Internal
- New
server/lib/cv-import.mjs— single source of truth for the format → markdown conversion, with timeout + missing-converter detection that surfaces actionable hints rather than 500s. server/lib/routes/content.mjsgainsPOST /api/cv/importandPUT /api/profile(binary-safe viaexpress.rawfor the upload, JSON for the YAML PUT).
[1.9.1] — 2026-05-08
Production-readiness pass. Four targeted bug fixes (BF-1..BF-4), Playwright smoke expanded from 5 to 12 tests covering tracker / pipeline / reports / evaluate / config / cv save round-trips. All green in CI.
🐛 Bug fixes
fix(tracker): escape pipes + collapse newlines in every cell, not just notes (BF-1)— a company name like"Acme | Co"previously broke the markdown table layout (parser split the cell into two). Cell sanitizer now applied uniformly to company / role / reportSlug / notes; companion fix inparsers.mjs::parseMarkdownTableadds GFM-compliant\|escape support so the round-trip is lossless.fix(config): wrap updateEnvFile in try/catch (BF-2)—POST /api/configpreviously bubbled an unhandled rejection on permission-denied / read-only filesystem. Now returns a clean 500{ error: 'failed to write parent .env', details: [...] }.fix(llm): soft cap on assembled prompt size for Anthropic SDK calls (BF-3 + BF-4)—/api/evaluate,/api/deep, and/api/mode/:slugAnthropic branches now bail with 413 whenbundleProjectContext + promptexceeds 200 KB (≈50K tokens). Saves a multi-second roundtrip + tokens vs letting the API complain about context size. The cap is well below any current model ceiling (Sonnet 4.6 = 1M context).
🧪 Playwright smoke — expanded coverage
5 → 12 tests. New cases:
tracker view renders empty + accepts API-seeded row— exercises BF-1 by seeding a row with a literal pipe in the company name and asserting the round-trip preserves it.pipeline add-URL form populates the queue+ invalid-URL rejection sweep (loopback,javascript:, bare strings).reports view handles empty state— non-crash assertion.evaluate view returns a manual prompt without API key— verifies the fallback chain.config GET returns known keys masked— secrets never leak through/api/config.cv.md PUT round-trips with sanitization— XSS-y bits (script tags,javascript:schemes) get stripped end-to-end.pipeline preview proxy strips scripts— invalid-URL rejection path.
📦 Behavior changes (no API contract changes)
- Tracker writes are now lossless against pipe-laden company / role names. Existing rows with raw pipes will start parsing correctly on the next read.
/api/{evaluate,deep,mode/:slug}will now return 413 instead of 502/timeout when the prompt is unreasonably large (200 KB+).
🧪 Tests
- 284 unit tests (no change in count; existing tests still all green after parser update).
- 12 Playwright browser-smoke tests (was 5).
[1.9.0] — 2026-05-08
P-6 → P-10 from the v1.8.0 backlog all shipped in one bundle. Headline: server/index.mjs is now a 130-LOC orchestrator (down from 762, total 1230 → 130 = -89%); every route topic has its own module. Anthropic parity for /api/evaluate, multi-CLI shims, expanded i18n parity test, and Playwright browser-smoke wired into CI.
🏗️ P-6 — server split-by-concern (phase 2)
Continuation of P-2. Extracted the remaining 9 route topics out of server/index.mjs into server/lib/routes/<topic>.mjs modules. index.mjs is now a pure orchestrator: middleware (security headers + activity log + static), 12 register<Topic>Routes(app) calls, and the SPA catch-all.
server/lib/routes/activity.mjs—/api/activity.server/lib/routes/config.mjs—/api/configGET/POST (parent .env round-trip).server/lib/routes/health.mjs—/api/health+/api/dashboard.server/lib/routes/help.mjs—/api/help/:lang.server/lib/routes/jds.mjs— full CRUD forjds/*.txt.server/lib/routes/llm.mjs— every LLM-bound endpoint (evaluate, deep, mode, apply-helper, interview-prep).server/lib/routes/pipeline.mjs—/api/pipeline*including the SSRF-safe preview proxy with named constants for timeout / max-redirects / max-body.server/lib/routes/reports.mjs—/api/reports*.server/lib/routes/tracker.mjs—/api/trackerGET + dedup-aware POST.
Behavior unchanged. 283/283 unit tests stayed green at every step. The orchestrator’s import surface dropped from 47 lines to 22.
🔌 P-7 — Anthropic parity for /api/evaluate
/api/evaluate previously was Gemini-or-manual. v1.9.0 adds an Anthropic branch (preferred when both keys present), mirroring the routing rule already used by /api/deep and /api/mode/:slug. Routes through bundleProjectContext({ modeSlugs: ['_shared', 'oferta'] }) so the model has the cv / profile / mode templates inlined (REVIEW-A1).
New endpoint: POST /api/evaluate/test-anthropic — smoke check for ANTHROPIC_API_KEY, mirrors the existing Gemini smoke. Sends a tiny prompt (≤256 output tokens) so it costs essentially nothing; returns a 200-char sample.
Fallback chain is now: Anthropic → Gemini → manual.
🌐 P-8 — Help-center i18n parity (audit + test hardening)
Audited every docs/help/<lang>.md for structure parity. All 8 locales already cover the same 14 canonical h2 sections. Tests upgraded:
tests/help-ui.test.mjs::every help doc covers the same 14 sectionswas checking only en + ru. Now iterates all 8 locales (en, es, pt-BR, ko-KR, ja, ru, zh-CN, zh-TW) and asserts the section count for each.- New test:
tests/help-ui.test.mjs::every help locale has substantive content— guards against locale stubs by asserting each non-EN locale is at least 30% ofen.md’s byte length. Compact translations naturally hit 40-50%; a stub would be in single-digit %.
Result: structural parity is now CI-enforced.
🤖 P-9 — Playwright browser smoke in CI matrix
tests/playwright-smoke.mjs (added in v1.8.0 as opt-in) is now part of the CI workflow. The existing e2e job already installs Playwright + Chromium; one new step (npm run test:e2e:browser) runs the 5 browser-smoke tests right after the comprehensive node E2E.
Order in CI: unit (Node 18/20/22 matrix) → smoke node E2E → comprehensive node E2E → Playwright browser smoke → screenshot artifact upload on failure.
🌍 P-10 — Multi-CLI compatibility
Parent career-ops v1.7.0 introduced multi-CLI / Open Agent Skill standard support. The UI sub-project follows the same convention with thin shims pointing at the canonical CLAUDE.md:
web-ui/AGENTS.md— Codex / Aider / generic CLI entry point.web-ui/GEMINI.md— Gemini CLI entry point.
Both shims re-state the hard rules and quick reference but defer to CLAUDE.md for the full project-level instructions, so non-Claude CLIs land on the same orientation as Claude Code sessions. The deployed UI itself remains CLI-agnostic at runtime.
🧪 Tests
- 284 unit tests (was 283): +1 new help-locale parity test.
- 5 Playwright browser-smoke tests — now part of CI, not just opt-in.
- Coverage held.
🔧 Files touched
+ server/lib/routes/activity.mjs + server/lib/routes/config.mjs
+ server/lib/routes/health.mjs + server/lib/routes/help.mjs
+ server/lib/routes/jds.mjs + server/lib/routes/llm.mjs
+ server/lib/routes/pipeline.mjs + server/lib/routes/reports.mjs
+ server/lib/routes/tracker.mjs
+ AGENTS.md + GEMINI.md
~ server/index.mjs (762 → 130 LOC, -83%)
~ .github/workflows/ci.yml (Playwright smoke step)
~ tests/help-ui.test.mjs (all-8-locales section parity + content-floor)
~ docs/{ROADMAP,architecture/{OVERVIEW,SERVER}}.md
~ docs/sdd/CONVENTIONS.md
~ CLAUDE.md
~ package.json (1.8.0 → 1.9.0)
📦 New REST endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/api/evaluate/test-anthropic |
Smoke check for ANTHROPIC_API_KEY (P-7). Mirrors /api/evaluate/test-gemini. |
🤖 New CLI entry points
| File | CLI | Notes |
|---|---|---|
AGENTS.md |
Codex / Aider / generic | Points at CLAUDE.md for the full instructions. |
GEMINI.md |
Gemini CLI | Auto-loaded by Gemini at session start. |
[1.8.0] — 2026-05-08
Hardening, refactor, and SDD bootstrap. Three high-severity correctness/security fixes (A1, A2, A3), four medium ones (B1–B4), six cleanups, audit of the parent career-ops v1.7.0 surface, server split-by-concern (P-2 phase 1), Playwright browser smoke harness, and a full SDD foundation under docs/ and .claude/.
🔥 High-severity fixes
fix(deep): inline cv/profile/mode files for Anthropic SDK calls (REVIEW-A1)—/api/deepand/api/mode/:slugpreviously told the model “read these files first” but the Anthropic SDK has no filesystem. Output was hollow. NewbundleProjectContext({ modeSlugs })readscv.md,config/profile.yml,modes/_shared.md, and the mode template, truncates each at 16 KB, and prepends a<project_context>block to the prompt. Verified live: 26 KB grounded markdown response fromclaude-sonnet-4-6for a deep-research call.fix(runner): SIGKILL escalation after SIGTERM grace period (REVIEW-A2)—runNodeScriptandstreamNodeScriptpreviously sent onlySIGTERMon timeout / client-disconnect. A child stuck in a syscall (DNS, blocked socket) ignored it, hanging the SSE connection until Node’s GC reaped. Now each path arms a 5 s watchdog that escalates toSIGKILL. Promises always resolve.fix(runner): max-runtime cap on streaming endpoints (REVIEW-A3)— every SSE script runner (/api/stream/{scan,liveness,pdf}) now has a hard 30-minute ceiling. On expiry: emitevent: error { message: 'maximum runtime exceeded' }, kill the child via the A2 watchdog, end the response.
🛡️ Medium-severity fixes
fix(preview): per-hop redirect validation in /api/pipeline/preview (REVIEW-B1)— switched fromredirect: 'follow'to manual redirect-walking. EachLocationheader is re-validated byisValidJobUrl; capped at 3 hops. Hostile boards can no longer bounce us to loopback / private IPs /file://. 4 new tests cover the rejection paths.refactor(keys): hasGeminiKey helper unifies LLM-key checks (REVIEW-B2)— directprocess.env.GEMINI_API_KEYreads in route handlers replaced withhasGeminiKey()fromlib/anthropic.mjs. MirrorshasAnthropicKey()shape for consistency and easier mocking.feat(scanners): thread AbortSignal through hh.ru, Habr, Greenhouse, Ashby, Lever (REVIEW-B3)— when the SSE client disconnects mid-scan, in-flight HTTP fetches are now aborted instead of running every query to completion and dropping events.runRuScanandrunEnScanacceptopts.signal; SSE handlers in/api/stream/scan-{ru,en}create anAbortControllerand abort onres.close.test(anthropic): log-guard test prevents future API-key leaks via console (REVIEW-B4)— captures everyconsole.{log,info,warn,error,debug}call duringrunAnthropichappy + error paths, asserts zero output and that the canary key string never appears. Defense-in-depth against a futureconsole.log(opts)regression.
🧹 Low-severity polish
fix(parsers): defense-in-depth URL gate inside addPipelineUrl (REVIEW-C4)— parser-level rejection of non-http(s) values, complementing the route-levelisValidJobUrl. Optionalopts.validatefor callers that want stricter rules.docs(readme): badge "tests-88 passed" → "tests-277 passed" (REVIEW-C3)— was off by an order of magnitude.test(i18n): missing-keys diff grouped by locale (REVIEW-C6)— whentests/i18n-coverage.test.mjsfinds a gap, output is now[ru] (3): foo, bar, bazinstead of mixed lines.docs(review): C1 closed as resolved-on-inspection— sanitizer regexes were already in\x00-\x08hex form; review entry was a tool-rendering artifact.
🏗️ P-2 phase 1 — server split-by-concern
server/index.mjs was 1230 LOC, well past the 800-line ceiling. Split into focused modules without behavior change. All 283 unit tests stayed green at every step.
server/lib/security.mjs—isValidJobUrl,stripDangerousMarkdown,sanitizeJobDescription,isPubliclyExposed. Re-exported fromindex.mjsfor backward-compat with external consumers.server/lib/prompts.mjs—bundleProjectContext,buildEvaluationPrompt,buildDeepPrompt,buildModePrompt,buildApplyChecklist.server/lib/store.mjs—safeReadApps,safeReadPipeline,safeListReports,checkProfileCustomized,ensureRussianPortalsDefaults.server/lib/routes/scan.mjs—registerScanRoutes(app)for/api/stream/scan-{ru,en},/api/scan-ru/config,/api/scan-results.server/lib/routes/runners.mjs—registerRunnerRoutes(app)for buffered/api/run/*table, streaming/api/stream/{scan,liveness,pdf}, generated-PDF list/download.server/lib/routes/content.mjs—registerContentRoutes(app)for CV / Profile / Portals / Modes.
index.mjs is now 762 LOC (-38%, under the 800 cap). Phase 2 will extract tracker, pipeline, reports, jds, llm (evaluate/deep/mode), and health into route modules. Targeting <500 LOC for the orchestrator.
🔍 Parent career-ops v1.7.0 audit
The user updated the parent project to v1.7.0. Audited every consumed surface — UI is fully compatible. Notable findings documented in docs/architecture/DATA-FLOWS.md:
- Modes catalog grew from 7 to 19 files. UI’s
MODE_ALLOWLISTdeliberately surfaces only 7 (others are Claude-Code-only). Comment added explaining the intentional narrow scope. portals.ymlschema confirmed:tracked_companies(96 entries, 87 enabled, 71 with API). EN scanner reads it correctly; legacycompanieskey still supported.- New parent surfaces NOT consumed today:
dashboard/(Go program),update-system.mjs,generate-latex.mjs,analyze-patterns.mjs,liveness-core.mjs,followup-cadence.mjs,test-all.mjs, localized mode subdirs (de/fr/ja/pt/ru). - Live
/api/dashboard,/api/health,/api/modes,/api/portals,/api/profile,/api/cv,/api/jds,/api/reports,/api/tracker,/api/pipeline,/api/evaluate,/api/deep,/api/stream/scan-enall verified green.
🤖 SDD / GSD bootstrap
career-ops-ui now has a full Spec-Driven Development foundation aligned with the GSD pipeline (gsd-* skills from superpowers@claude-plugins-official).
CLAUDE.md(root) — project-level agent system prompt: stack, GSD pipeline, hard rules (parent contract, security envelope, no--no-verify), conventions, parent-project boundary..aiignore— exclusion list for AI agents: vendored, binaries, parent user data,.planning/,.env, locale duplicates..claude/agents/— three project-specific subagent definitions:web-ui-route-reviewer.md— gates new routes against SSRF, CSP, sanitizers, parent-write contract, conventions, tests.spa-view-reviewer.md— CSP-safe DOM, i18n, router registration, accessibility.test-isolation-reviewer.md— verifies tests are CI-isolated (no parent-project assumptions, no live network, no port collision).
.claude/commands/— slash-command stubs:/sdd-status,/codebase-tour.docs/tree — all in English:PROJECT.md— what/why/for-whom, scope, constraints, success criteria.ROADMAP.md— current milestone + completed history + backlog.sdd/SDD-GUIDE.md— discuss → spec → plan → execute → verify → review pipeline mapped togsd-*skills.sdd/CONVENTIONS.md— module system, naming, routes, sanitizers, client patterns, i18n, errors, logging, testing, commits, branches, CSS.architecture/OVERVIEW.md— top-level diagram, layers, boot sequence, invariants, “where to look first when…” cheat sheet.architecture/SERVER.md— per-file map forserver/lib/*.mjs(updated for P-2 split).architecture/FRONTEND.md— SPA structure, view inventory, globals, “how to add a view”.architecture/API.md— full inventory of every/api/*route.architecture/DATA-FLOWS.md— every parent-project read/write, with the explicit-user-action contract.reviews/REVIEW-2026-05-07.md— static review that produced this changelog’s fixes.
🔒 Security & repo hygiene
chore(.gitignore): comprehensive defense-in-depth patterns— covers env variants, IDE folders, GSD scratch (.planning/), per-user agent settings (.claude/settings.local.json,.claude/cache/,.claude/state/,.claude/memory/), Playwright artifacts (playwright-report/,test-results/,.playwright/,trace.zip), heap/CPU profiles, lockfiles for unshipped tooling, expanded macOS Finder noise, generic secret patterns (secrets.json,credentials.json,*.pem,*.key).
🧪 Tests
- 283 unit tests (was 277): +6 new (4 for B1 redirect-rejection, 1 for
hasGeminiKey, 1 forrunAnthropiclog-guard). - 5 Playwright browser-smoke tests (new, opt-in via
npm run test:e2e:browser): dashboard render + version footer, dashboard → scan → pipeline → cv navigation, language-switch persistence, 404 view, health-page render. Resolves Playwright via parent’snode_modules— no new dependency. - Coverage held at ~93% line / ~83% branch.
📝 New / updated package.json scripts
| Script | Purpose |
|---|---|
npm run test:e2e:browser |
Run Playwright smoke harness against in-process server (5 tests). |
🔧 Files touched
+ CLAUDE.md + .aiignore
+ docs/PROJECT.md + docs/ROADMAP.md
+ docs/sdd/SDD-GUIDE.md + docs/sdd/CONVENTIONS.md
+ docs/architecture/OVERVIEW.md + docs/architecture/SERVER.md
+ docs/architecture/FRONTEND.md + docs/architecture/API.md
+ docs/architecture/DATA-FLOWS.md + docs/reviews/REVIEW-2026-05-07.md
+ .claude/agents/web-ui-route-reviewer.md + .claude/agents/spa-view-reviewer.md
+ .claude/agents/test-isolation-reviewer.md
+ .claude/commands/sdd-status.md + .claude/commands/codebase-tour.md
+ server/lib/security.mjs + server/lib/prompts.mjs
+ server/lib/store.mjs
+ server/lib/routes/scan.mjs + server/lib/routes/runners.mjs
+ server/lib/routes/content.mjs
+ tests/playwright-smoke.mjs
~ .gitignore ~ README.md (badge fix)
~ package.json (1.7.2 → 1.8.0)
~ server/index.mjs (1230 → 762 LOC)
~ server/lib/runner.mjs (SIGKILL escalation, max-runtime cap)
~ server/lib/anthropic.mjs (hasGeminiKey)
~ server/lib/parsers.mjs (URL gate in addPipelineUrl)
~ server/lib/ru-scanner.mjs ~ server/lib/en-scanner.mjs
~ server/lib/sources/{hh,habr,greenhouse,ashby,lever}.mjs (signal threading)
~ tests/anthropic.test.mjs ~ tests/i18n-coverage.test.mjs
~ tests/pipeline-preview.test.mjs
[1.7.2] — 2026-05-04
Help center, in-UI App settings, mobile sidebar, single Scan button, and a “Show result” shortcut on every prompt-builder.
✨ New features
feat(help): in-app user guide(/#/help) — long-form Markdown documentation accessible from a new sidebar entry. Covers every page step-by-step: quick start, CV editor, Profile, Scan filters, Pipeline preview, Evaluate, Deep research, Apply, Tracker, Reports, all 7 modes, Activity log, Health, setup hints. Auto-built sticky table of contents from<h2>headings, synchronous DOM build (no race). Localized for all 8 supported locales.feat(config): in-UI App settings page(/#/config) — editANTHROPIC_API_KEY,ANTHROPIC_MODEL,GEMINI_API_KEY,GEMINI_MODEL,HH_USER_AGENT,PORT,HOSTfrom the browser. Writes to the parent project’s.envfile so career-ops Node scripts AND web-ui’s dotenv loader pick up the same source. Secret keys masked on read (first/last 4 chars). Model fields are dropdowns with curated lists (claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / gemini-2.0-flash / etc.). Empty value deletes the key. Values applied to running process.env immediately — no restart for most settings.feat(modes): "⚡ Show result" button alongside "Copy prompt"— when a prompt is generated in manual mode, users no longer have to retype their inputs to get the LLM result. The new button re-submits the same form withrun: true, falling through to a clear toast (Set ANTHROPIC_API_KEY or GEMINI_API_KEY in .env first) when no key is configured. Works on/#/deep,/#/project,/#/training,/#/followup,/#/batch,/#/contacto,/#/interview-prep,/#/patterns.
🐛 UX + UI fixes
fix(scan): single Scan button replaces three (Scan all + EN + RU)— overwhelming choice, identical default in 99% of cases. The unified🌐 Scanbutton runs every enabled source. Help docs updated across 8 locales.fix(ui): mobile sidebar drawer— viewport <900px now gets a hamburger button (☰) in the topbar;body.sidebar-opentoggles a CSS transform that slides the sidebar in. Backdrop dim + click-anywhere closes it. Anchor click + hashchange auto-close so the user lands on the new page with the drawer tucked away. Larger viewports unaffected.fix(server): footer version reflects web-ui, not the parent VERSION—/api/healthnow reads web-ui’s ownpackage.json. The footer no longer leaks a stale1.6.0from the parent’s version file. Parent’s VERSION is still surfaced separately asparentVersion.
📦 New REST endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/help/:lang |
Returns the Markdown user guide for the requested locale, falling back to en.md. Path-traversal-safe. |
GET |
/api/config |
Returns current values for all known env keys; secrets masked. |
POST |
/api/config |
Writes the given keys into the parent project’s .env, validates each value, applies live to process.env. |
🌐 i18n
- 30+ new keys across
nav.help,nav.config,help.*,config.*,deep.showResult,deep.needKey,scan.btnRun. All 8 locales populated.
🧪 Tests
tests/help.test.mjs(12 cases) — every supported locale returns substantive markdown, EN spot-checks every page slug, unknown lang → EN fallback, path-traversal sanitized, every locale referencescv.md/profile.yml/.env.tests/help-ui.test.mjs(9 cases) — view file registration, sidebar entry, i18n keys present in every locale, docs files exist for every locale, EN/RU help has 14 canonical sections, every #/foo route covered, Show-result wiring on deep + mode-page.tests/env-config.test.mjs(18 cases) — pure-function tests forparseEnv,maskSecret,validateConfig,updateEnvFile(bootstrap, in-place rewrite preserving comments, empty-value delete, quote-when-needed).tests/config-endpoint.test.mjs(8 cases) — GET masks secrets / returns env path; POST writes to parent .env; live process.env application; empty-value unsets; rejects unknown keys + malformed Anthropic keys with 400.
📊 Stats
- Tests: 233 → 277 (+44 across 4 new test files).
- E2E: 20 smoke + 23 comprehensive = 43 Playwright steps, all green.
- Coverage: 93.5% line / 82.6% branch / 93.7% funcs (unchanged — new code is fully tested).
[1.7.1] — 2026-05-04
Patch release stacking the post-v1.7.0 work: pipeline preview pane, Anthropic API integration, scrollable sidebar, dotenv loader, dynamic Active-companies list, CI workflow hardening.
✨ Pipeline preview pane
/#/pipelineoverhaul — left list + right preview pane. Click any URL to fetch a server-side proxied snapshot (GET /api/pipeline/previewstrips scripts/styles/tags, caps at 8 KB, validated throughisValidJobUrl). Live filter input, “In queue” counter, ⚡ “Evaluate first” header button. Inline ▶/✕ on every row plus full Evaluate / Open in tab / Delete on the preview pane. Stable test selectors viadata-url+.pipeline-row+.pipeline-row-deleteclasses. 8 new tests intests/pipeline-preview.test.mjs(mocked fetch, no upstream binding needed).
✨ Anthropic API integration — “Run live” everywhere
server/lib/anthropic.mjs— zero-dependency client for Anthropic Messages API (claude-sonnet-4-6 default, override viaANTHROPIC_MODEL). WhenANTHROPIC_API_KEYis set, every mode page (/#/deep,/#/project,/#/training,/#/batch,/#/contacto,/#/interview-prep,/#/patterns) renders an “⚡ Run live (Anthropic)” button as the primary action — clicking executes the prompt and renders Markdown back into the browser instead of handing off to Claude Code. Gemini stays as fallback when only its key is set. Manual mode still works with no keys at all. 8 new tests intests/anthropic.test.mjs.
🐛 CI / pipeline fixes
fix(api): tighten pipeline URL validator(FIX-M7) — now also rejects loopback hostnames, length <10 or >2000, whitespace inside URLs.fix(server): actually load .env so HH_USER_AGENT / GEMINI_API_KEY hints work— addedserver/lib/dotenv.mjs(35-line zero-dep loader) wired in at the top ofserver/index.mjs. The runtime hints in scanner code finally do something. 6 new tests.fix(ui): scrollable sidebar— 18 nav items in 6 groups overflowed shorter viewports..sidebarnow hasoverflow-y: autowith thin custom-styled scrollbars.fix(ui): make HH_USER_AGENT banner dismissible— then removed entirely from/scanonce we realized it was overkill. Health page check still surfaces it.fix(scan): Active companies list is now collapsible + filterable + grouped— 87 tags flat was overwhelming. Now a “▸ Active companies 87/71” toggle expands an ordered list (✓ API-backed first, ○ websearch second) plus a search filter.fix(test): isolate api.test.mjs + en-scanner.test.mjs from parent project— both now spin up tmp project roots so CI works without the parent checked out alongside web-ui.fix(workflow): publish-package version-match only on release events—workflow_dispatchfrom main no longer fails the tag/version check.fix(e2e): stable selector for pipeline row delete— restored anchor wrapper + addeddata-urlattribute so e2e suite is selector-stable.
📦 New REST endpoint
| Method | Path | Purpose |
|---|---|---|
GET |
/api/pipeline/preview?url=… |
Server-side proxy: returns visible-text snapshot of the URL (scripts/styles stripped, 8 KB cap), gated by isValidJobUrl. |
📊 Stats after this batch
- Tests: 225 → 233 (8 more on top of v1.7.0).
- Test files: 25 → 26.
- E2E: 20 + 23 = 43 Playwright steps, all green.
career-ops-ui v1.267.1 · page 9/10 · mirror of the project changelog for cvstart.org