மாற்றப் பதிவு
GitHubஒவ்வொரு குறிப்பிடத்தக்க மாற்றமும், வெளியீடு வாரியாக. இதே கோப்பு ரிப்போவில் இருக்கிறது மற்றும் அனைத்து 19 மொழிகளிலும் மொழிபெயர்க்கப்பட்டுள்ளது.
[1.162.0] — 2026-08-13
Fixed (MEDIUM) — the “?” help affordance is now a ≥24×24 pointer target (WCAG 2.5.8). .help-hint measured 18×18 px with padding:0, below the Target Size (Minimum) floor, on every page heading.
Fixed
.help-hintbox is now 24×24 (the measurable pointer target) while the visible ring stays 18px, drawn by a centered::before— so the glyph size and the<h1>baseline/line-height are unchanged (page-title lines are taller than 24px). Hover / active / focus-visible states move with the ring; the margin trims 6→3px so the gap to the title is identical. The v1.158.0document.titleclone-strip (which excludes.help-hint) is untouched.
Notes
- CSS-only; no JS, i18n, route, CSP, SSRF, or parent-write change.
- Tests:
tests/help-hint-target-size.test.mjs(+2). Suite: 2418 (+2).
[1.161.0] — 2026-08-13
Fixed (MEDIUM) — #/reports shows a “score not detected” chip instead of blank space. After the v1.159.0 locale-aware parser, a report that still has no parseable score rendered an empty metadata area — indistinguishable from a failed evaluation, with no recovery affordance.
Fixed
- The score cell now branches: a parsed score → the tone pill; no score → a muted
.score-mutedchip reading “Score not detected” (localized ×17) with an “Open the report to see the score” tooltip. The card stays a keyboard-operablerole="link"that opens the report (where the score lives in the body), and the date still renders (v1.159.0 mtime fallback), so cards never lose their anchor. - Reuses the existing neutral tone token — no new colour (
score-tone.jsalready specifies a no-score row as muted).
Notes
- Client-only; no route/CSP/SSRF/parent-write change. +2 i18n keys ×17 (
rep.scoreUnparsed,rep.scoreUnparsedHint; snapshot 1217 → 1219). - Tests:
tests/reports-unparsed-chip.test.mjs(+3). Suite: 2416 (+3).
[1.160.0] — 2026-08-13
Fixed (HIGH) — provider copy no longer contradicts the 7-provider promise. #/config said the web-ui live eval “uses your Anthropic or Gemini API key” and that the OpenAI key is “not used by the web UI itself”; #/dashboard’s Evaluate card said “Anthropic-first scoring” — all false since the OpenRouter / 7-provider cascade shipped (v1.157.0), and self-contradicted on the same screen (the header chip read Active: OpenRouter).
Fixed
config.providerModelNote(×17) now states the truth: the headless ⚡ live eval runs on any one of your seven provider keys — Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub Models · Hermes — auto-ordered, with fallback when a pinned provider has no key. The false “OpenAI … not used by the web UI itself” sentence is removed.dash.quick.evaluateSub(×17) is now vendor-neutral (“0–5 fit scoring”), not “Anthropic-first scoring”; thedashboard.jsfallback matches.Keys: N / 5→N / 7— the denominator now matches the seven configurable provider key slots (SECRET_KEYS), the same second-order drift.
Notes
- Copy-only; no route, CSP, SSRF, or parent-write change. Two existing i18n keys reworded ×17 (no new keys; snapshot stays 1217).
- Tests:
tests/provider-copy-honesty.test.mjs(+3: ≥5 provider names ×17, EN drops the exclusivity claims, Evaluate subtitle vendor-neutral ×17). Suite: 2413 (+3).
[1.159.0] — 2026-08-13
Fixed (HIGH) — report metadata is no longer language-coupled. Reports generated in a non-English locale rendered a blank metadata strip on #/reports (no score pill, date, or legitimacy chip), because parseReportHeader matched only English **Score:** / **Legitimacy:** / **Date:** bold labels — so the docs’ “Score → next step” table sat above cards that showed no score.
Fixed
parseReportHeadernow parses the language-invariant## Machine SummaryYAML block. Precedence: English bold labels (keeps EN reports byte-identical) → the## Machine Summaryblock’sscore:/legitimacy:/date:keys (the same locale-free sourceauto-pipelinealready reads) → locale-aware prose labels (REPORT_LABELS, all 17 locales) as a last resort. A Russian/Japanese/Arabic report now yields a realscoreNum, date and legitimacy.- Locale-tolerant score parsing —
1.5/5,1.5 / 5,1,5/5(comma decimal),1.5 из 5,4.5 out of 5and bare4.5all resolve; out-of-range (0–5) values reject. - Date never null — falls back to the report file’s mtime (already in the list payload) when the body has no parseable date, so cards keep their chronological anchor.
Notes
- Read/parse only — no route, CSP, SSRF, or parent-write change; parent files stay read-only. No i18n-dict key change.
- Tests:
tests/report-header-locale.test.mjs(+8: EN byte-identity, RU Machine-Summary parse, mtime fallback, unparsed shape, numeric tolerance, 17-locale label coverage, localized-prose fallback) + a liveGET /api/reportsRU-fixture integration case. Suite: 2410 (+8).
[1.158.0] — 2026-08-12
Fixed — two cosmetic display bugs (a leaked “?” in tab titles, a wrong provider count on the landing). Display-only; no behaviour, security, or data-flow change.
Fixed
- The HelpHint
?no longer leaks intodocument.title. Views built viaHelpHint.title(text, key)render their<h1>as[<span>text</span>, <button class="help-hint">?</button>]; the router derived the per-route tab title from the rawh1.textContent, so the tab (and the screen-reader “page changed” announcement) read “Vacancy search?” instead of “Vacancy search”.router.js::focusNewViewnow deep-clones the heading, strips.help-hint, then reads the text — the live heading’s visible “?” is untouched. - cvstart.org showed “17 AI providers” instead of “7”. The landing’s
Features.astrosub()helper eagerly rewrote every{n}to the locale count (17) before the per-card override could set the providers card tofacts.providers(7), so the feature card and the stats banner disagreed on the same page.{n}is now resolved per-card (providers → 7, languages → 17).
Notes
- No server, route, CSP, SSRF, or i18n-key change;
facts.jsonshape unchanged (providers: 7,locales: 17). - Tests:
tests/document-title-per-route.test.mjs(+1, source-static guard that the title is computed from a.help-hint-stripped clone). Suite: 2402 tests (+1); Playwright smoke/full-cycle/forms 62/62.
[1.157.0] — 2026-08-12
Fixed — live evals now run on ANY configured provider, not just Anthropic/Gemini. A user with only OPENROUTER_API_KEY set was wrongly forced into manual mode (“set ANTHROPIC_API_KEY or GEMINI_API_KEY…”). Two independent causes: a stale server-side pin, and stale client-side gating.
Fixed
- Root cause — a keyless
LLM_PROVIDERpin dead-ended. Runninginitwith Claude Code writesLLM_PROVIDER=claude; if you later add only, say, an OpenRouter key, the forced-claude routing found no Anthropic key and fell through to a manual prompt — even though OpenRouter was configured and fully supported. Now a forced provider whose key isn’t set falls back to the auto order among the configured providers (a pin that DOES have its key stays forced). Applied consistently inenv-config.mjs::selectActiveProviderand both dispatch cascades (routes/llm.mjs::_provGate+llm-dispatch.mjs::gate), so/api/status/providersand the actual run always agree. - Client gating was stale.
#/deepand the mode-page views (#/contacto,#/interview-prep,#/project, …) decided “Run live vs manual” by probing/api/healthfor onlyANTHROPIC_API_KEY/GEMINI_API_KEY. They now use the newwindow.ProviderStatushelper, which reads/api/status/providers(activeProvider, honoring all seven + the pin). No more ⚡-button that promises a live run the server would refuse. - Misleading copy.
deep.tipManual,deep.needKey, andeval.manualMode(× 17) no longer name only Anthropic/Gemini — they point at “any provider key … in App settings”.config.llmProviderHint(× 17) explains the new pin-fallback.#/dashboard’s system card now shows a single honest Live evals · ready/manual badge (derived from all seven provider rows) instead of Anthropic/Gemini-only badges.
Added
public/js/lib/provider-status.js—window.ProviderStatus(.live()→{ available, engine, activeProvider, keysConfigured }, plus a 7-provider label map): the single client source of truth for live-eval availability. Loaded afterapi.js.
Notes
- No security-surface change — provider endpoints are still trusted config; no route, CSP, or SSRF change. The fallback only picks among keys the operator already configured.
- Tests:
tests/live-provider-gating.test.mjs(+4, source-static guard against the stale 2-provider probe) + aselectActiveProviderkeyless-pin-fallback case; the old “pin with no key → null” assertion updated to the new fallback. 3 newdash.system.*keys × 17 (snapshot 1214 → 1217). - Suite: 2401 tests (+5); Playwright smoke/full-cycle/forms 62/62.
[1.156.0] — 2026-08-12
Refactor — split scan.js under the file-size limit (P-16), + a CodeQL fix. public/js/views/scan.js was 906 lines, over the 800-line hard limit. Two cohesive, behavior-preserving factories were extracted, bringing it to 648 — completing the view-split pair started with config.js in v1.155.0.
Changed
scan/runner.js(new, 276 lines) —window.createScanRunner(ctx)→{ runScanAll, stopScan }: the scan-execution engine (Scan/Stop run-state, the indeterminate/determinate progress bar, the persistent error banner + Retry, the SSE console stream, and the per-source runners for ATS / regional / both).scan/filters.js(new, 76 lines) —window.createScanFilters(refs, deps)→{ applyFilters, resetFilters, getFilterState, setFilterState }: the result-filter state machine that backs saved searches.scan.js(648) wires both viactx/refsbags (the same pattern aslib/scan-results.js); the live-poll teardown timers stay atscan.jstop level (shared classic-<script>scope). Both new files load beforescan.jsinindex.html.
Fixed
- CodeQL
js/useless-assignment-to-local(#428) inconfig/tab-controller.js—let n = i;wherenis reassigned on every non-returning branch →let n;. Harmless dead-store introduced by the v1.155.0 extraction (moved verbatim from config.js), now cleared.
Notes
- Pure refactor, zero behavior change — moved code is byte-identical (de-indent only); no route, server, i18n, or CSS change. Four source-reading tests were repointed to the new files (the
loadScanSrc()helper now concatenatesscan.js+scan/runner.js+scan/filters.js+lib/scan-results.js); assertions otherwise identical. - Both oversized views (
config.js,scan.js) are now under the 800-line limit; P-15/P-16 complete. - Suite: 2396 tests (unchanged — tests repointed, not added); Playwright scan+smoke 26/26 in-browser.
[1.155.0] — 2026-08-12
Refactor — split config.js under the file-size limit (P-15). public/js/views/config.js was 1030 lines, over the project’s 800-line hard limit. Two cohesive, behavior-preserving modules were extracted, bringing it to 783.
Changed
config/field-specs.js(new) — the pure, read-only field-spec data: the curated per-provider model lists + theFIELDSdescriptor table (API keys / runtime / regional).window.ConfigFieldSpecs, loaded before the view.config/tab-controller.js(new) — the#/configtab-bar controller (ARIA tablist + keyboard nav + panel swapping) as acreateConfigTabController(c, panelHost)factory returning{ tabBtn, activate }.config.jsnow references both; the render logic (fieldRow, profile/section editors, save flow) is unchanged. Both new files are wired intoindex.htmlbeforeconfig.js.
Notes
- Pure refactor, zero behavior change — no route, no server, no i18n key, no CSS. Six source-reading tests were repointed to the new files (they assert on the moved field/model/tab markup); the assertions are otherwise identical.
scan.js(906) is intentionally left as-is. Its results-rendering was already extracted tolib/scan-results.js(v1.132.0); the remaining core is a tightly-coupled event-wiring closure bound to ~18 filter/DOM refs, where a mechanical factory extraction would need an 18-argument signature and worsen coupling — the opposite of the rule’s intent. A proper split needs a filter-state model refactor, deferred rather than force-fit.- Suite: 2396 tests (unchanged — tests repointed, not added).
[1.154.0] — 2026-08-12
New guide — “Running the whole stack in the cloud.” career-ops has no cloud/server story of its own, so this adds one: a step-by-step recipe for putting the parent career-ops pipeline, this career-ops-ui viewer, and the AI engine (a Claude subscription via the Claude Code CLI, a local Hermes gateway, or provider API keys) on a small always-on server — provision, pick your engine, and expose it safely. It ships as in-app Help §31 in all 17 languages, a README section, a wiki page, and (via the help mirror) the site.
Added
- In-app Help §31 “Running the whole stack in the cloud” (× 17 locales) — the three moving parts (pipeline / viewer / engine), provision + install (VPS, Node ≥ 18), pick your engine (Claude subscription / Hermes / API keys, mixable), and expose it safely (HTTPS reverse proxy + auth + the CSP/SSRF/XSS/no-secrets invariants that must survive the move off
127.0.0.1). Help bundle grows to 31 H2 / 112 H3 (was 30/108). - README — a
## Run the whole stack in the cloudsection (× 17) pointing to Help §31,docs/integrations/HERMES.md, and the new wiki page. - Wiki — a dedicated Cloud-Deployment page (the three-parts table, provision, engine choice, and the “must survive the move” invariants), linked from Home.
Notes
- Docs-only — no route, no server, no client-code change; no new i18n key (help is Markdown, not the dict). The four help-bundle gate tests move to the 31 H2 / 112 H3 contract (
canonical-docs-coverage,help-ui,help-ru-config-section,locales-de-it-tr). - Grounded in the existing
docs/integrations/HERMES.md§2 cloud-deployment checklist and the parent’s own docs (which recommend Node 22.5+ and the eight first-class agent CLIs). - Suite: 2396 tests (unchanged — the gate assertions were updated, not added).
[1.153.0] — 2026-08-12
Jobvite scanner migrated to the public XML feed (parent-sync). The parent career-ops retired the Jobvite JSON API (it now 302-redirects and returns zero jobs); web-ui’s jobvite source used that same dead endpoint, so any tracked Jobvite company silently scanned empty. This ports the parent’s fix (#2623) into the web-ui source contract: the source now reads the public per-tenant XML feed on a different host, keyed by an opaque companyEId.
Fixed
- Jobvite returned zero jobs — the source fetched
https://jobs.jobvite.com/api/company/{slug}/jobs(retired). It now fetcheshttps://app.jobvite.com/CompanyJobs/Xml.aspx?c={companyEId}and parses the XML<result><job>…payload (CDATA + entity-decode,detail-urlpreferred overapply-url,http:→https:on display-only per-job URLs).
Changed
- companyEId resolution — the tenant key changed from the vanity slug (
tylertech) to an opaquecompanyEId(q6NaVfwI) not present in the careers URL. Resolution order: (1)company_eid:on the portal entry, (2) thec=param of an explicitapi:URL, (3) board-page discovery (scrapecompanyEIdfrom the inline JS). Prefer (1) inportals.yml— one line, survives board redesigns, skips a request. server/lib/http-json.mjs—fetchTextnow attaches.location/.retryAfterto the thrown non-ok error (read-only; lets jobvite tell an empty board — aNoJobs.htmredirect — from a retired tenant, without ever following the redirect). Backward-compatible: both fields arenullfor a plain error, so existingredirect:'error'callers are unaffected.
CI
- Pages deploy no longer flips the status badge on a superseded build —
deploy-pages.ymlusedconcurrency.cancel-in-progress: true, so when a merge touchingdocs/help/**auto-fired a Pages build and a manual dispatch raced it, the older run was cancelled and surfaced as a red “check” (“Some checks were not successful”) even though the site deployed fine. Switched tocancel-in-progress: false(GitHub’s recommended Pages pattern) so a second run queues instead of cancelling — the status stays green.
Notes
- Security — the source pins two hosts (
jobs.jobvite.comfor discovery,app.jobvite.comfor the feed) viaassertJobviteUrlbefore every fetch: https-only, strict-hostname allowlist, no redirect ever followed (redirect:'error'for discovery,redirect:'manual'for the feed — the 3xx is read but never chased). ThecompanyEIdis only ever a?c=query value; feed/board URLs are rebuilt from the resolved slug/eId, never fetched verbatim from user input. Registry source count unchanged (meta.value='jobvite'preserved). - Parent-sync: this was the only web-ui-relevant change in the 17 parent commits since parentVersion 1.26.0 (the others — an LLM re-ranker,
verify-cv-facts,liveness-core, and twoweb/fixes — have no web-ui mirror). - Suite: 2396 tests (+4:
tests/sources-jobvite.test.mjsrewritten for the XML contract — config/c=/discovery eId resolution, XML parse, the two-host guard, empty-feed handling).
[1.152.0] — 2026-08-12
Hermes provider — completed wiring + docs actualization. A detailed code review of the v1.151.0 Hermes integration surfaced two real gaps and four completeness items; all are fixed here, and the whole app’s LLM-provider roster is brought up to the full seven (Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models → Hermes) across every doc surface and all 17 locales.
Fixed
#/configcould not force Hermes — theLLM_PROVIDERdropdown listed only six providers (GitHub was the last), so a user with both a cloud key and a local Hermes could setHERMES_API_KEYbut had no way to force Hermes from the UI (only by hand-editing.env).hermesis now the 8th option (auto+ 7 providers), and the field hint (config.llmProviderHint× 17 + the JS fallback) names the full seven-provider order. Newprovider-selector.test.mjsguard asserts the dropdown can never drift fromLLM_PROVIDERSagain.- Short self-hosted keys were silently rejected —
isUsableKey’s 20-char floor was calibrated for cloud keys, but ahermes gateway‘sAPI_SERVER_KEYis user-chosen and may be short (the Hermes docs’ own examplechange-me-local-devis 19 chars).hasHermesKeynow uses a relaxed 8-char floor (still rejects empty/placeholder junk), so a legitimate short local key is no longer dropped to manual-mode without a diagnostic.
Changed
hermesChatUrlcompletes a bare host —HERMES_BASE_URL=http://127.0.0.1:8642(a mis-paste that drops the/v1) now resolves to…/v1/chat/completionsinstead of a/v1-less 404.- Manual-fallback copy in
routes/llm.mjsnow names Hermes in the “execute via …” provider list. - Provider-roster actualization — the six-provider chain/force-list strings were normalized to the full seven across README (× 17), the in-app help (× 17), the
config.llmProviderHintdict (× 17), anddocs/sdd.CONVENTIONS.mdcorrected to “all 16 non-EN locales”. Assembled-dict snapshot regenerated (1214 keys).
Notes
- Security unchanged — no new route, no SSRF/CSP/sanitizer change. The relaxed key floor only affects the local-loopback Hermes gateway; the provider endpoint stays trusted config (not a scanned job URL).
HERMES_API_KEYremains aSECRET_KEY. - Health/doctor now carries a
HERMES_API_KEYrow (was omitted in v1.151.0), so#/healthandcareer-ops-ui doctorlist all seven providers. - Suite: 2392 tests (+2: the
isUsableKeyminLen guard + the dropdown-vs-LLM_PROVIDERSparity guard).
[1.151.0] — 2026-08-12
Hermes is now a wired LLM provider (Phase 5) — the Phase 5 scoping spike confirmed that Nous Research’s Hermes ships an OpenAI-compatible API Server (hermes gateway → POST /v1/chat/completions), so career-ops-ui now runs live evaluations through a local Hermes exactly like OpenAI/Qwen. Set HERMES_API_KEY in App settings and it joins the auto provider order (last). This closes the roadmap’s final open item — Phase 5, Shape A.
Added
- Hermes LLM provider (Shape A) —
runHermeson the sharedrunOpenAICompatibleclient (server/lib/openai.mjs), gated in both cascades (llm-dispatch.mjs+routes/llm.mjs), added to the auto provider-order tail + theLLM_PROVIDER=hermespin,/api/status/providers, andserver/lib/llm-pricing.mjs. It reaches a user-configured local base URL (defaulthttp://127.0.0.1:8642/v1) with Bearer auth — a configured provider endpoint (like OpenRouter/Qwen), not a user-supplied job URL, so it does not touch theisValidJobUrlSSRF guard. #/configfields —HERMES_API_KEY(secret) +HERMES_BASE_URL+HERMES_MODEL(defaulthermes-agent), with 6 new i18n keys × 17 locales (assembled-dict snapshot 1208 → 1214).
Changed
- The scoping spike is resolved:
docs/integrations/HERMES.md, the in-app help §30 (× 17), the README teaser (× 14), thehermes-bridgeskill, anddocs/UX-ROADMAP.mdall move from “planned / not-yet-wired” to wired (Shape A). Shape B (a bespoke agent-runtime relay) was not needed.
Notes
- Security: the provider fetch is a configured endpoint, identical in category to the existing OpenAI-compatible providers — no new SSRF surface, no CSP/sanitizer change.
HERMES_API_KEYis aSECRET_KEY(never echoed). - Tests (CI-isolated, stubbed transport):
tests/hermes-provider.test.mjs(+5); the v1.146.0 “no Hermes branch” canary is inverted to assert it IS wired; the provider-surface tests updated to the 7-provider order. - Suite: 2390 tests (+5:
tests/hermes-provider.test.mjs).
[1.150.0] — 2026-08-12
Consistent empty states (Phase 4 polish) — every “nothing here yet” panel now renders through the one shared .empty style instead of a few views re-declaring the look inline with a magic 40px. Small visual-consistency fix; the empty states on #/activity, #/cv-studio, #/stats, and #/usage now match every other one (tokenized 48px padding + the dashed border).
Changed
#/activity,#/cv-studio,#/stats,#/usagedropped their redundant inlinestyle: { padding: '40px', textAlign: 'center', color: 'var(--foggy)' }on empty-state panels — all three properties are already provided by the shared.emptyclass (tokenized--space-7= 48px, centered, muted, dashed border). So those four now render pixel-identical to the ~25 other.emptypanels instead of drifting a few px on a hardcoded number.- Genuine per-view overrides (
#/dashboardwidth:100%,#/pipelineborder:none) are untouched — only the pure-redundant re-declarations were removed.
Notes
- Client CSS-usage cleanup only — no route, no server, no i18n key, no CSS-rule change (the
.emptyclass itself is unchanged); app dict snapshot stays 1208. Browser-verified (the#/usageempty panel computes to 48px padding + dashed border, no inline style, 0 console errors). - New canary
tests/empty-state-consistency.test.mjskeeps.emptythe single source of truth — a view may still add a layout-specific override (width/border) but not re-state padding/centering/colour. - Completes the concrete slice of Phase 4’s “overall visual polish”; broader taste-driven refinement across all pages remains open to direction. Phase 5 (Hermes provider) stays blocked on the API-contract spike.
- Suite: 2385 tests (+2:
tests/empty-state-consistency.test.mjs).
[1.149.0] — 2026-08-12
Portals moved into Settings (Phase 4) — #/portals now lives in the Setup nav group next to App settings, instead of under Sourcing. Since v1.144.0 it’s a settings surface (enable/disable tracked companies + an ATS health probe), not a sourcing action — so this is where it belongs. Nav-only change; the page and its route are untouched.
Changed
#/portalsnav item → Setup group (inpublic/index.html), placed right after App settings. Removed from the Sourcing group (which keeps Scan / Pipeline / Auto-pipeline / Funded companies). The#/portalsroute, the view, and thenav.portalslabel are unchanged — only the sidebar position moved.
Notes
- Nav markup only — no route, no view, no i18n key, no server change. Browser-verified (the Portals item now renders under the Setup header, 0 console errors); guarded by
tests/portals-nav-placement.test.mjs. - Completes the Phase 4 “Portals → Settings” item. The remaining Phase 4 line — a whole-app visual-polish pass — is tracked separately.
- Suite: 2383 tests (+2:
tests/portals-nav-placement.test.mjs).
[1.148.0] — 2026-08-12
Cleaner scan filters (Phase 4) — the result-filter panel is now a tidy grid — the #/scan filter panel moved from a ragged flex-wrap of rigid, variable-width boxes to a responsive grid, and the Apply/Reset actions now sit on their own separated, right-aligned row. Same filters, same behaviour — just easier to read. A senior-designer polish pass (no parent-sync).
Changed
#/scanresult-filter panel → responsive grid —.scan-filtersis nowdisplay: gridwithrepeat(auto-fill, minmax(180px, 1fr))columns and even gutters, so the 11 labelled filters align into tidy columns at every width instead of wrapping into a ragged row of 160–240px boxes.- Apply / Reset actions span the full grid on their own row, separated by a hairline and right-aligned, so they read as the panel’s primary action. Dropped the old hidden-label alignment hack + inner flex wrapper in
scan.js(buttons are now direct children of.scan-filters__actions).
Notes
- CSS + one small DOM cleanup only — every filter input id (
#scan-filter-*,#scan-apply) and theSR.render()wiring are unchanged, so the Playwright scan-filter flow is untouched. No new i18n keys. - Browser-verified (grid renders 4 columns at 1100px, actions row separated + right-aligned, 0 console errors); guarded by
tests/scan-filters-grid.test.mjs. - Remaining Phase 4: the broader “overall visual polish across all pages” pass is a subjective, whole-app design task left for explicit direction; the
#/portals→ settings-nav move is still open. - Suite: 2381 tests (+3:
tests/scan-filters-grid.test.mjs).
[1.147.0] — 2026-08-12
Hermes & Telegram — the in-app help section + cvstart.org surface (Phase 5b, part 2) — the second and final slice of the Hermes docs work: the how-to now lives inside the app’s own help guide, in all 17 languages, and the in-app docs assistant answers Hermes questions from it. Still docs-only — the Hermes LLM-provider path remains planned / not-yet-wired (Phase 5).
Added
- In-app help §30 “Hermes & Telegram” × 17 locales — a new help-guide section (what Hermes is + the two integration shapes; running on a cloud server; Telegram via Hermes + the “what NOT to expose” rule), reachable from
#/help. Thedocs-assistant/ floating “Ask the docs”DocsFabgrounding picks it up automatically — no wiring needed, since both readdocs/help/<lang>.md. - cvstart.org — a short Hermes explainer mirroring the README teaser and deep-linking to the GitHub guide.
Changed
- Help-bundle gate lifted 29 → 30 H2 / 105 → 108 H3 (
canonical-docs-coverage,help-ui,help-ru-config-section); §30 adds 3 H3s.
Notes
- Still nothing calls Hermes. New canary
tests/help-hermes-section.test.mjsasserts every locale carries the §30 section with its language-independent anchors (docs/integrations/HERMES.md,hermes-bridge,#/help,127.0.0.1, Telegram). The provider stays blocked on the Phase 5 API-contract spike. - This closes the Phase 5b docs + skill deliverable; the provider integration (Phase 5) remains a separate, blocked item.
- Suite: 2378 tests (+2:
tests/help-hermes-section.test.mjs).
[1.146.0] — 2026-08-12
Hermes agent + Telegram — the integration guide + a skill (Phase 5b, part 1) — you can run career-ops-ui on a cloud server and bridge its events (a finished scan, a new report, an urgent follow-up) to Telegram through a Nous Research Hermes agent. This release ships the design + deployment docs and a hermes-bridge skill; the Hermes LLM-provider path stays planned / not-yet-wired (blocked on the Phase 5 API-contract spike). Docs-ahead-of-code by design.
Added
docs/integrations/HERMES.md— the deep-dive: the two integration shapes (OpenAI-compatible endpoint vs. agent runtime), cloud-server deployment (reverse proxy + HTTPS + systemd, the read-only parent contract on a headless box), Telegram-via-Hermes, and a threat-model “what NOT to expose” list (no CV / salary / report bodies / keys to the channel).## Hermes agent + TelegramREADME teaser — a short pointer + link, in the EN README and mirrored across the fully-translated locale READMEs.- A
hermes-bridgeskill (.claude/skills/hermes-bridge/) that operationalizes the guide — prerequisite + scoping-gate checks (Node ≥ 18, keys present, endpoint reachability via the SSRF-safe path), never writes secrets to disk/logs, and refuses to invent a Hermes endpoint or claim the provider is wired. - An Integrations section in
docs/architecture/OVERVIEW.mdlinks the guide.
Notes
- Nothing calls Hermes yet. A canary (
tests/hermes-docs.test.mjs) asserts the “planned / not-yet-wired” honesty markers and thatllm-dispatch.mjshas no Hermes/Nous branch — so wiring the provider later must update the docs + roadmap in the same change. - Deferred to v1.147.0 (Phase 5b, part 2): the in-app help “Hermes & Telegram” H2 × 17 and the cvstart.org marketing surface.
- Suite: 2376 tests (+4:
tests/hermes-docs.test.mjs).
[1.145.0] — 2026-08-12
Insightful stats (cont.): a rebuildable chart — the #/stats “Target-role trend” tab now has a Build a chart widget: pick a metric × dimension and it re-renders live. First of the Phase-3 interactive-chart work. A user-reported UX request (no parent-sync).
Added
- Rebuildable metric × dimension chart — choose a metric (Vacancies / Median salary / Average salary) and a dimension (By country / By role), and the bar chart re-renders instantly. Salary metrics honor the existing currency + per-year ⇄ per-month controls; vacancies are a plain count. Built from the same aggregation (
RoleStats.aggregate) already computed for the tab — no new data or endpoint. - 8 new i18n keys × 17 locales (
stats.customChart/metric/dimension/metricVacancies/metricMedian/metricAvg/dimCountry/dimRole); assembled-dict snapshot 1200 → 1208.
Notes
- The widget lives on the Target-role trend tab (which owns the scan-derived role/country/salary aggregation). v1.140.0’s changelog mislabeled this same section as “My pipeline” — it was always
renderTrend. - Browser-verified: the metric + dimension selects render and the chart re-renders on change, 0 console errors.
- Suite: 2372 tests (+2:
tests/stats-custom-chart.test.mjs— the wiring + EN-keys guard).
[1.144.0] — 2026-08-12
Settings & filters (Phase 4, part 1): enable/disable tracked portals — you can now turn a watched company on or off from #/portals, and the scanner honors it. A user-reported UX request (no parent-sync).
Added
POST /api/portals/toggle— an explicit user write that flips a watched company’senabledflag inportals.yml, keyed bycareers_url. The edit is surgical + parse-validated: only the target company’senabled:line is inserted/flipped (comments, ordering, and every other field preserved byte-for-byte, matching thestore.mjswrite pattern), and the result must still parse as YAML or the write is refused. Same write-through contract asPOST /api/tracker/PUT /api/cv.- Enable / Disable toggle per company on
#/portals— one click turns a portal off (the EN scanner already skipsenabled: falsecompanies, so a disabled portal drops out of every future scan) or back on, with an optimistic toast. - 5 new i18n keys × 17 locales (
portals.disable/enable/enabledToast/disabledToast/toggleFailed); assembled-dict snapshot 1195 → 1200.
Notes
- The scanner change was zero —
en-scanner.mjsalready filterscompanies.filter((c) => c.enabled !== false); this release adds the UI + a safe write path to set it. - Deferred to a later Phase-4 slice (subjective / cosmetic — they want your eye): the scan-filter visual redesign and moving
#/portalsfully into a settings nav section.#/portalsremains the portal-settings surface for now. - Suite: 2370 tests (+3:
setEnabledInRawinsert/flip/not-found + the toggle round-trip + unknown-url 404 intests/portals-routes.test.mjs).
[1.143.0] — 2026-08-12
Understandable (cont.): ? hints on the core workflow views — the ? help affordance now covers the nine main workflow/decision pages, in every language. A user-reported UX pass (no parent-sync).
Added
?help hint on 9 more view titles —#/scan,#/evaluate,#/cv-studio,#/tracker,#/config,#/deep,#/batch,#/auto,#/applyeach get an inline title?(viaHelpHint.title) that opens a localized “what this does / how to use it / what result to expect” popover — the same CSP-safe, theme-aware, RTL-mirrored primitive shipped in v1.139.0.- 9 new i18n keys × 17 locales (
help.hint.scan/evaluate/cvStudio/tracker/config/deep/batch/auto/apply); assembled-dict snapshot 1186 → 1195.
Notes
- Browser-verified in EN (scan / evaluate / config / tracker / batch render the
?+ popover, 0 console errors).#/configand#/batcheach have two page-title<h1>s (main render + error state) — both wired. - The remaining lighter views (
#/dashboard,#/cv,#/reports,#/usage,#/pipeline,#/portals,#/activity,#/docs-assistant) are a later wave. - Suite: 2365 tests (+1: the 9-workflow-view wiring guard in
tests/help-hint.test.mjs).
[1.142.0] — 2026-08-12
Correctness: no more “Unknown” career archetype — #/orientation now always ranks from the eight named career vectors instead of occasionally answering “Unknown” and recommending you “double down” on it. A user-reported fix (no parent-sync).
Fixed
#/orientation— the AI prompt now forbids an out-of-set archetype. The model MUST rank the top three from exactly the eight named vectors (Functionalist / Administrator / Communicator / Specialist / Analyst / Innovator / Manager / Entrepreneur) and may never answer “Unknown” / “N/A” / “insufficient data” or invent a new label. When the CV is thin it still names the three closest fits at lower confidence and says what evidence is missing — instead of declining and producing nonsensical “double down on Unknown” advice.
Notes
- Server-side prompt change only (
buildOrientationPrompt); no i18n / schema change. The generated profile stays localized via the existing output-language directive. - Suite: 2364 tests (+1:
tests/orientation-routes.test.mjs— the prompt names the eight vectors and forbids “Unknown”).
[1.141.0] — 2026-08-12
Insightful stats (cont.): funded-company enrichment — #/funded becomes a richer, more visual view: company logos, a funding-amount chart, and per-company round / amount / discovery-score / suggested-action cards. A user-reported UX pass (no parent-sync).
Changed
#/funded— flat table → enriched card grid. Each recently-funded company is now a card with a logo (derived from the company name viaCompanyLogo, letter-avatar fallback), round + amount chips, the parent’s discovery score and suggested action (previously discarded from the relay payload), and the funding-news source link + date.- Funding-amount visualization — a horizontal bar chart of the top companies by disclosed amount; free-text amounts (“$120M” / “€1.5B” / “500K”) are parsed to a magnitude via a new
parseAmount. - 3 new i18n keys × 17 locales (
funded.byAmount/funded.score/funded.action); assembled-dict snapshot 1183 → 1186.
Notes
- Still read-only over
GET /api/company-funded(host-pinned public feeds — TechCrunch / PR Newswire / The Guardian / Hacker News); no writes, no LLM, no new route. The “verify a company independently” caveat stays. Description and salary range aren’t in the funding feed, so they’re out of scope for this source. - Remaining Phase 3 — interactive/rebuildable charts + the “Unknown” archetype fix (orientation prompt-quality) — continues later (
docs/UX-ROADMAP.md). - Suite: 2363 tests (+2:
tests/funded-view.test.mjs—parseAmount+ card-render wiring; the v1.133.1 regression guard updated table→cards).
[1.140.0] — 2026-08-12
Insightful stats: richer salary figures — the #/stats “My pipeline” salary breakdown now shows the average (not just the median), a per-year ⇄ per-month toggle, and a min · avg · median · max table per country. First slice of roadmap Phase 3. A user-reported UX pass (no parent-sync).
Added
- Average (mean) salary —
RoleStats.salaryStatsnow returnsavgUsdalongsideminUsd/medianUsd/maxUsd. The median resists outliers; the average exposes right-skew (a few very-high postings pull it above the median), so the two together read as a distribution rather than a single point. - Per-year ⇄ per-month toggle on the salary section — every figure divides by 12 for the monthly view, respecting the currency selector.
- Min · avg · median · max table per country under the “salary by country” chart, driven by the same currency + period controls. 8 new i18n keys × 17 locales.
Notes
- Salary figures are still derived only from postings/applications with a parseable salary and normalized to USD via approximate FX — indicative, not authoritative (the existing caveat stays on the section).
- Remaining Phase 3 — interactive/rebuildable charts, the “Unknown” archetype fix (orientation), and funded-company enrichment — continues in later releases (
docs/UX-ROADMAP.md). - Suite: 2361 tests (+1: an average-right-skew case in
tests/role-stats.test.mjs).
[1.139.0] — 2026-08-12
Understandable: ? help hints — a reusable, CSP-safe ? affordance that explains “what this does / how it works / what to expect” on demand, in your language. A user-reported UX pass (no parent-sync).
Added
?help-hint popover (public/js/lib/help-hint.js→window.HelpHint): a small round?button next to a heading opens a lightweight, theme-aware, RTL-mirrored popover that renders a localized explanation throughUI.md()(the escape-first XSS boundary). Accessible — a real<button>witharia-expanded, the popover isrole="tooltip", and Escape / outside-click close it with focus returning to the button; CSP-safe (handlers viaaddEventListener, static?glyph); singleton (one popover open at a time), positioned under the icon and clamped to the viewport.?on the 5#/statstabs — the active-tab caption (“Rejection patterns (?)”) now carries a hint explaining each analytics view (Market report, My pipeline, Target-role trend, Rejection patterns, Lifetime) — directly answering “what is this tab for”.?on 8 AI/analytics view titles — career-plan, orientation, two-pager, networking, mock-interview, memory, funded, and the weekly interview digest each get an inline title?(HelpHint.title) so “what is this / how do I use it / what result to expect” is one click away even when the page is empty.- 14 new i18n keys × 17 locales (
help.hint.*+stats.hint.*); assembled-dict snapshot regenerated (1161 → 1175 keys). Guarded bytests/help-hint.test.mjs(CSP-safe, wired, EN-present) + the existing i18n parity/latin-leak/personal-data gates.
Notes
- All 30 views already carried a one-line
page-subtitle; the?hints add the deeper on-demand explanation on top, which also makes empty states (#/career-plan, the weekly digest,#/funded) self-explanatory instead of looking broken. docs/UX-ROADMAP.md: Phase 2b “Understandable” is now shipping — the?-hint system landed here; extending?to the remaining views continues in a later release.- Suite: 2360 tests (+4:
tests/help-hint.test.mjs).
[1.138.0] — 2026-08-12
Generation in your interface language — every AI generation now answers in the language you’ve picked in the UI, plus review-driven test hardening. A user-reported UX pass (no parent-sync).
Changed
- AI generations now respect the UI language. With the interface set to Russian, Spanish, Japanese, … the generated text now comes back in that language instead of always English. The output-language directive is threaded through every generation endpoint — career plan, orientation, market report, mock interview, networking plan, “ask the docs”, the memory-note suggestion, and the two-pager draft. Code and identifiers stay English (e.g. the two-pager YAML keys); only prose, headings, and bullets are localized. Server-side,
resolveLocale(req)+buildLocaleDirective(lang)now also coverPOST /api/memory/suggestandPOST /api/two-pager/draft; the client sends the activelangon all eight generate calls.
Fixed
- CSS colour-role guard (
tests/css-role-tokens.test.mjs) — a source-static canary asserting the v1.137.0 dark-mode alias tokens never invert role: text-role tokens (--fg/--danger/--ok/…) are never used as abackground, and surface-role tokens (--card/--panel/--line/…) never as a textcolor, across all hand-written CSS and the SPA inline styles. Makes the “0 WCAG-AA failures across 29 views” claim machine-checkable (both AI reviews asked for it). UI.md()XSS-loader self-probe — the test-only extractor that vm-loadsmd()fromapi.jsnow probesmd('<script>…')immediately after extraction and throws if the escape is missing, so a future mis-slice of the brace-matched source fails loudly instead of turning the security suite green against a truncated function.#/career-planscroll guard — the post-generatescrollIntoViewnow runs only when the preview is still connected to the document.
Notes
docs/UX-ROADMAP.mdupdated: the?-help-hints + page-descriptions + empty-state pass is now v1.139.0; a Nous Research / Hermes provider — plus a cloud-server + Telegram deployment guide and a Hermes skill — is tracked as Phase 5 / 5b.- Suite: 2356 tests (+5: two generation-language canaries, three colour-role guards).
[1.137.0] — 2026-08-11
Readability & rendering fixes — dark-mode contrast, chart labels, and the career plan. A user-reported UX pass (no parent-sync).
Fixed
- Dark-mode white-on-white / black-on-black across many screens — fifteen CSS custom properties several views referenced (
--fg,--panel,--panel-2,--surface-elev1,--line,--ok,--go,--err,--error,--danger,--warn,--muted,--ink,--card,--border) were never declared in the palette, sovar(--fg, #111)/var(--panel-2, #eef1f6)/var(--ok, #008a05)silently fell back to hardcoded light/black literals: fine in light mode, unreadable in dark (the#/pipelineoverview chips,#/statsactive tab,#/config“Active / Keys” + “✓ set”,#/two-pagersections,#/mock-interviewquestion bubble, error text, and more). They’re now aliased to the real theme-aware tokens (--hof/--paper/--slate/--kazan-text/--rausch-text/…) on:root, so they follow the theme automatically. The#/configactive tab (a solid pink pill with white text, ~2.94:1) became the readable tinted-badge pattern. Verified with an automated alpha-composited contrast auditor: 0 WCAG-AA failures across all 29 views in dark mode (was: pipeline chip ~1→12.2, stats tab ~1→15.8). A regression guard (tests/dark-theme-tokens.test.mjs) keeps every alias declared and mapped to a theme-aware token. #/statschart labels were hard-cut mid-word (“Senior Backend Engineer” → “…Enginee”) — the SVG bar chart now ellipsizes (…) with the full label kept as a hover<title>, and the label column is wider.#/career-planshowed the generated plan as raw Markdown (##,**, table pipes) — it now auto-renders the plan as formatted, readable text the moment it’s generated (the editable Markdown stays in the textarea below; Preview toggles the rendered view).
Notes
#/career-plan,#/two-pager,#/memory,#/stats, and the weekly interview digest are not broken — every endpoint returns 200; they render empty states (no plan generated yet / no interview sessions logged in the selected week). Clearer on-page “what this does / how to use it” guidance and?help hints land in the next release.- The generated career plan renders through
UI.md(), the app’s escape-first client XSS boundary (api.jsHTML-escapes every byte before any tag transform) — same pathreports/orientation/cv-studiouse. A newtests/ui-md-xss.test.mjsfeeds<img onerror>/<script>/javascript:payloads through it and asserts no live tag/handler survives. - Repo tidy:
docs/UX-ROADMAP.mdrecords the Phase 2–4 UX plan; the two perennial QA prompts (qa/UX-AUDIT-PROMPT.md,qa/DESIGNER-EXPORT-PROMPT.md) were actualized to the v1.137.0 baseline (32 route modules · 79 sources · 17 locales); and 59 superseded per-release QA prompts moved toqa/archive/superseded-prompts/.
[1.136.0] — 2026-08-11
Parent career-ops v1.26.x parity (post-v1.26.0 mainline) — one new zero-auth source plus a wave of quality & robustness ports to code web-ui mirrors. Registry now 79 sources = 74 EN + 5 RU (ALL_ADAPTERS 74).
Added
eightfold(Eightfold AI, parent #2684) — talent-acquisition boards via the zero-authhttps://<tenant>.eightfold.ai/api/apply/v2/jobsAPI, host-pinned to*.eightfold.ai(the brandedcareers.<company>.comCNAME is deliberately rejected — the entry must point at the tenant host); paginated with a safety cap, dead-board-throw, url-dedup. Source + adapter + CI-isolated suite; appears in the#/scanSource filter and on the landing.
Fixed
- Unicode-aware dedup & role keys (parent #2569 / #2587 / #2667) — a new shared
server/lib/text-key.mjs::normalizeTextKey(NFKC + lowercase, keeps letters/marks/digits of any script) replaces the ASCII-only keys web-ui mirrored from the core:detect-repostsnow keys the company on it, so width/punctuation/spacing variants ("Acme, Inc."≡"Acme Inc") cluster and a genuine repost is no longer missed, while distinct non-Latin employers («Тинькофф» vs «Яндекс») never collapse to one key.role-matcherfolds full-width titles (SENIOR …≡Senior …) and keeps non-Latin role tokens instead of erasing them to an empty set that could never match.
fetchJsonWithRetryno longer retries a refused redirect (parent #2657) — ourredirect:'error'SSRF guard meeting a 3xx surfaces as a no-statusTypeErrorthat looks transient but is deterministic; it’s now classified non-retryable (via undici’serr.cause.message), so it fails fast instead of burning the whole retry budget.title_filter.positiveAND-groups (parent #2552) — a whitespace-delimited+inside a positive entry ("staff + platform") now requires every term to appear in the title, in any order; plain entries andc++-style terms are unaffected (the separator requires surrounding whitespace).oraclecloudaccepts the numbered tenant apexesoraclecloud1.com … oraclecloud99.com(parent #2683) — some tenants live only on a numbered apex (<t>.fa.ocs.oraclecloud26.com); the host pin now enumerates the bounded family (no leading zero, ≤ 2 digits — never a wildcard apex).workablehardened (parent #2675) — the widget request now goes through the shared retry helper with browser-like headers + a per-account referer and process-wide request serialization against the Cloudflare-fronted host.personiofalls back to an HTML scrape when the XML jobs feed is disabled (404) instead of returning nothing — same host-pinning, dead-board contract preserved.statesFALLBACK aliases resynced with the parent’sstates.yml(#2615) —evaluatedgainscondicional/hold/evaluar/verificar,skipgainsgeo blocker/geo_blocker(the live parent file is still read first; this only backstops a fresh clone).
Notes
- Not ported (not mirrored by web-ui, or CLI-only):
reply-matchercorroboration (#2672),jd-similarityseniority gate (#2661),jd-skill-gapheading recognition (#2686) — web-ui has no email-reply / JD-analysis surface;scanenv-path (#2568) /--flag=value(#2589) parsing and the per-run dedup-read perf (#… ) — web-ui runs the scanners in-process; cover-letter / CV-template / doctor / ollama / generate-pdf changes — CLI-only. The webjs-yaml/nanoidHIGH advisories were already patched in web-ui v1.135.0.
[1.135.0] — 2026-08-11
Parent career-ops v1.26.0 parity — five new zero-auth scan sources plus correctness fixes to four boards web-ui already carries. Registry now 78 sources = 73 EN + 5 RU (ALL_ADAPTERS 73).
Added
- Five new scan sources (parent #825 / #2527 / #2464), each a self-registering source + adapter + CI-isolated suite, appearing in the
#/scanSource filter (FALLBACK + live registry) and on the cvstart.org landing:join(JOIN, parent #2527) — reads a company’s JOIN board from the Next.js__NEXT_DATA__embedded injoin.com/companies/<slug>(host-pinned, page-capped,redirect:'error'); detected from ajoin.comcareers URL.getro(Getro, parent #825) — VC “talent-network” portfolio boards (b2venture, Point Nine, Speedinvest, …) via the publicapi.getro.com/api/v2/collections/{id}/search/jobsPOST API, paginated newest-first with an age-bounded early stop; the numeric board id is set explicitly (getro_collection:), and each job is attributed to the portfolio employer, not the fund.consider(Consider, parent #825) — getconsider.com VC portfolio boards (Founderful, Creandum, Balderton, …) via the same-origin/api-boards/search-jobsPOST; the POST host is config-driven from the board’s careers URL and pinned by a structural SSRF guard (public HTTPS host only — rejects IP-literals, loopback,*.internal).joinup(JOINUP, parent #825) — the Swiss startup board joinup.ch, reading the SSR’d newest page ofjoinup.ch/browse/jobs(__NEXT_DATA__); fail-closed on a scraper break.remotli(Remotli, parent #2464) — remotli.ch, a curated board of remote roles at Swiss companies (salaries in CHF), via the publicremotli.ch/api/jobs?remote=allJSON API; emits the employer’s own ATSapplyUrlas the canonical link (so cross-listings dedup) and the real employer as the company.
Fixed
- a16z Speedrun no longer aborts the whole board on a transient blip (parent #2506) — the feed paginates into the hundreds of pages, so a single mid-sweep 429/5xx/timeout used to abort the provider and return nothing. Page fetches now go through a new shared
fetchJsonWithRetry(bounded retries on transient failures only — a permanent 4xx is never retried), and the page budget is re-sized for the 50-job page (DEFAULT_MAX_PAGES3→6,MAX_PAGES_CAP120→1000, parent #36d0c44). web-ui’s page-0-throw / later-page-keep-partials dead-board contract is preserved. - arbeitsagentur moved to the v6 Jobsuche API (parent #2494) — the old
/pc/v4/jobsendpoint 404s; v6 (/pc/v6/jobs) renames the response shape (ergebnisliste,referenznummer,stellenangebotsTitel,stellenlokationen[]) and drops the detail-endpoint remote verification (v6’s detail endpoint 403s to the public key), soremoteMatch:'filter'now narrows server-side withhomeoffice=nv_true+ the same title check. - The Hub moved to the v2
jobsandfeaturedAPI (parent #6b33fc4) — fromapi/jobstoapi/v2/jobsandfeatured(json.jobs.docs/.pagesenvelope), posting URLs rebuilt from the job id,countryCode=EUsent by default; The Hub rows carry no posted date and are exempt from the age filter (emitdate: ''). - hackernews finds the monthly “Who is hiring?” thread reliably (parent #3aa5e15) — the Algolia lookup filters by the
whoishiringaccount tag (tags=story,author_whoishiring) instead of a free-text query that could rank an unrelated recent story above the real thread once enough time had passed since it posted.
Notes
- Not ported (web-ui already safe, relay-absorbed, or CLI-only):
- The Unicode-aware role-dedup / company-matching keys (parent #2569 / #2587 / #2429 family): web-ui’s repost grouping already keys company on a plain lowercase (
detect-reposts.mjs), so distinct non-Latin employers never collapse to one key — the exact bug those fixed.trust-validator’s ASCII company/hostname heuristic degrades gracefully (returns “no flag” for a non-Latin name, never a silent merge), and role-token matching mirrors the parent’s deliberate “non-Latin role distinction out of scope” decision. - The
followuprejection-latency signal (parent #2014) + newrejection-latency.mjs, and thecompany-fundedtouch-ups: web-ui relays these read-only, and the fail-soft relay absorbs the shape change with no code edit. scanenv-overridable pipeline/history paths (#2568) and--flag=valueparsing (#2589): web-ui runs the scanners in-process with its ownpaths.mjsand has no CLI arg surface.- The UA-consolidation refactor (
oraclecloud/vdab/jobspresso→ shared_http.mjs, #2536): a no-op for web-ui, which already centralizesBROWSER_LIKE_USER_AGENTinhttp-json.mjs. - CLI-only / non-mirrored: the untrusted-content coverage roster (#2521),
oferta/offer-preplawyer routing,doctorsubscription warnings, cover-letter signature block + unresolved-token guard, CV empty-section trimming, YC-seed paging,.gitattributes/.npmignore, and the parent’s own Next.jsweb/changes.
- The Unicode-aware role-dedup / company-matching keys (parent #2569 / #2587 / #2429 family): web-ui’s repost grouping already keys company on a plain lowercase (
[1.134.1] — 2026-08-05
Validation-hardening — fixes surfaced by a full-project audit (all locales + code).
Fixed
successfactorsno longer discards scraped jobs on a mid-scan failure (regression in the v1.134.0 dead-board-throw port) — its RMK pagination loop had notry/catch, so a failure on page 2+ (after page 1 succeeded) threw and dropped everything already collected. Worse, if that mid-pagination failure was a 404 (plausible on an out-of-rangestartrowbehind a WAF),en-scanner’s permanent-failure check would quarantine a live tenant as dead for days. Now mirrorsphenom/radancy: a page-0 failure (nothing fetched) still throws (dead board), but a later-page failure keeps the partials (+1regression test).#/scanfilter chips are now keyboard-operable (WCAG 2.1.1) — the stack/level/dynamic-keyword facet chips (and the “clear” chip) were<span onClick>with notabindex/role, so keyboard and screen-reader users couldn’t reach or toggle them (the.chip:focus-visibleCSS was dead). They now carryrole="button",tabindex="0",aria-pressed(toggles), and Enter/Space activation.- Three hardcoded English strings are now localized — the
#/scantrust-badge tooltip (“Trust …”), the#/scanrelocation column header (“Reloc”), and the#/dashboardscore header (“Score”) were bare literals the i18n parity gate couldn’t see (they were never keys), so they stayed English in all 16 non-English locales. Nowscan.trustTip+scan.col.reloc(2 new keys ×17) and a reuse of the existingtrack.col.score. A source-static guard locks them so the gap can’t silently return.
Security
fast-uri3.1.4 → 3.1.5 in thesite/build lockfile (Dependabot GHSA — high: host confusion via a backslash authority introducer). Transitive dev-only dependency of the cvstart.org Astro toolchain;npm auditnow reports 0 vulnerabilities and the Astro build is unchanged (86 pages, green). Does not touch the SPA or server (express+js-yamlonly).
[1.134.0] — 2026-08-05
Parent career-ops v1.25.0 parity.
Added
- New scan source: getManfred (
manfred, parent #9474ff1) — a board-wide feed of Spanish/EU tech roles with published salaries, fromwww.getmanfred.com/api/v2/public/offers(zero-auth, host-pinned + HTTPS-only, single-request full catalogue, per-company fail-soft). Source + adapter + a CI-isolated suite (tests/sources-manfred.test.mjs); registry now 73 sources = 68 EN + 5 RU (ALL_ADAPTERS68). Appears in the#/scanSource filter (FALLBACK + live registry) and on the cvstart.org landing.
Fixed
- a16z Speedrun feed was silently truncating to 50 jobs (parent #2404) — the feed caps a page at 50, but the source requested
PER_PAGE = 100, so therawCount < PER_PAGEguard stopped after page 1. Corrected to 50 so pagination continues. - Dead boards now throw instead of reading as “live but empty” (parent #2379) —
cryptocurrencyjobs,phenom,radancy, andsuccessfactorssources: a fetch failure where no request ever resolved now throws (so#/portalshealth and the scan record a real failure) instead of swallowing it to[]; a mid-scan failure after ≥1 success keeps partials (proof-of-life).radancyproves life across both transports;successfactorskeeps the RMK-answered carve-out. - workable now uses the public widget API (parent #5ab8425) — switched from the offset/limit-capped
api/v3endpoint toapply.workable.com/api/v1/widget/accounts/<slug>(host-pinned,redirect:'error'), which returns a large account’s full posting list in one request, so big accounts are no longer silently truncated. The adapter endpoint is unchanged (the source derives the account slug and rebuilds the widget URL).
Notes
- Not ported (CLI-only or not mirrored by web-ui): the
detect-reposts#2389 title-bucketing rewrite (an O(N²)→O(N) perf optimization over the parent’s large scan-history CLI; web-ui’s reposts run in-process over the user’s own bounded history, so the pairwise cost is negligible — the match semantics are unchanged); the Unicode company-key fixes (company-history/fingerprint-core/tracker-utils/merge-tracker), which web-ui does not carry — web-ui’s own tracker dedup already compares full lowercased company strings and is non-Latin-safe;scan --since+scan-ats-fullcheckpoints (web-ui runs the scanners in-process and has its own “Posted within” age filter);cv-facts/verify-cv-facts, the CV Awards/Honors template + hiring-manager audit PDF pass,doctor, and the modes untrusted-content directive.
[1.133.1] — 2026-08-02
Fixed
#/funded(Funded companies) now renders results — two bugs made the table always show “no funded companies” even when the parent’scompany-funded.mjsreturned a full list. (1) The view read the results underres.candidates, but the parent emits them undercompanies(each{ company, amount, round, funding: { sources: [{ source, url, observed_date }] } }); the client now reads the correct key and maps the real evidence shape. (2) The results table passed its cells toUI.el('tr', {}, …)as varargs, butUI.el(tag, attrs, children)takeschildrenas a single node or array, so only the first column (Company) rendered — cells are now passed as an array. Verified in a real browser: 11 companies across the four feeds render with Company / Funding signal / Source / Date columns and working evidence links, zero console errors. An empty pass now also surfaces the per-source diagnostics (source: status (fetched/funding-like)) so a quiet news day is distinguishable from a blocked feed.- Regression guards in
tests/parity-routes-v1133.test.mjs: the fake parent script now emits the realcompaniesoutput shape (the original fixture mirrored the wrongcandidatesshape — which is exactly why the bug shipped green), plus source-static canaries thatfunded.jsreadsres.companies(neverres.candidates) and builds table rows with array children (+1 → 2144).
[1.133.0] — 2026-08-01
Added
- Funded-company discovery (
#/funded, parent parity #2117) — a new read-only view relaying the parent career-opscompany-funded.mjsviaGET /api/company-funded: a review-first list of recently funded companies discovered from public, host-pinned funding feeds (TechCrunch, PR Newswire, The Guardian, Hacker News). The relay runs the script with--json --dry-run(JSON to stdout, no file writes), never threads user input into--sources(no SSRF surface beyond the parent’s own fixed feeds), carriesllmRateLimit, and is user-triggered (a Discover button, never on mount). New route moduleserver/lib/routes/funded.mjs(the 32nd) +public/js/views/funded.js, under Sourcing. - Weekly interview digest (
#/interview-digest, parent parity #2129/#2130) — a new read-only view relaying the parent’s zero-LLMweekly-digest.mjsviaGET /api/interview/weekly-digest: a mechanical roll-up ofinterview-prep/sessions/*.md— which companies and rounds you interviewed with this week, recurring competencies, and best-effort open gaps. Optional?from=&to=range is threaded ONLY when BOTH are validYYYY-MM-DD; an empty range is a validavailable:truedigest (not a failure). Added toserver/lib/routes/interview.mjs+public/js/views/interview-digest.js, under Analytics. - Both relays follow the established fail-soft
{ available:false }contract (like/api/stats/lifetime) when the parent script is absent (CI, standalone installs), so each view shows an honest note. 26 new i18n keys ×17 locales; CI-isolated suitetests/parity-routes-v1133.test.mjs(+5 → 2143: success passthrough,--from/--tothreading, the read-only--dry-runguarantee, and the script-error fail-soft path).
Notes
- Parent career-ops advanced past v1.24.0 with the Next.js web/ app’s Follow-up Tracker page (#1422) and backend PDF render (#2182) — not ported: web-ui already has its own follow-up relay (v1.117.0) and PDF runners, and the underlying
followup-cadence.mjshardening (impossible-date rejection, null-safeaddDays, legacy-bullet parsing) arrives for free via the shell-out relay. Theset-status.mjs/tracker-utils.mjschanges are CLI-internal (exit-code / lock helpers) and not mirrored.
[1.132.0] — 2026-07-31
Changed
#/scanresults-rendering subsystem extracted topublic/js/lib/scan-results.js(file-size-contract paydown —public/js/views/scan.jshad grown to ~1254 LOC). The subsystem —renderResults,buildChipRow,getRows, the row/facet builders, the seniority/country option painters, and theFALLBACK_SOURCESregistry mirror — moves into awindow.ScanResults.create(ctx)factory that closes over a context object the view supplies (filter elements, the active-facet Sets, the paginator, two-pager data, and alastResultsgetter). No behaviour change — the functions were moved verbatim and their closure vars mechanically rewired toctx.*;scan.jsis now ~906 LOC. (Still above the 800-LOC target — a second extraction pass is planned; the file-size contract for the two remaining JS-view outliers,scan.jsandconfig.js, is tracked indocs/sdd/CONVENTIONS.md.)- Source-static tests read the two files via
tests/helpers/scan-src.mjs::loadScanSrc()(likeloadAppCss());tests/scan-fallback-sources.test.mjsnow reads the registry mirror fromscan-results.js. - New in-browser regression gate —
tests/playwright-scan-filters.mjsseeds a canneddata/last-scan.jsonand drives every#/scanfilter (Source, Seniority, Remote, Age, text include/exclude), asserting exact row counts, so the extraction is verified against real browser behaviour (run vianpm run test:e2e:browser). Stable filter-control ids (#scan-filter-*,#scan-apply) were added for it.
Housekeeping
- The README “Latest release” banner is slimmed to a one-line summary + a link to the full changelog (the long per-version narrative wall is retired — history lives in
CHANGELOG*.md). Applied across all 17 locales. - CodeQL cleanup — removed the dead
readFileSync/resolve/APP_CSSpath-scaffolding left over from the v1.131.2loadAppCss()migration in four CSS source-guard tests (design-polish-v1115,managed-focus-no-ring,toast-fab-clearance,wcag-target-size); those imports had become unused once the tests switched toloadAppCss(), closing the eight openjs/unused-local-variablecode-scanning alerts. Test-only, no behaviour change.
[1.131.2] — 2026-07-31
Changed
app.csssplit into three ordered stylesheets (file-size-contract debt — the single file had grown to ~1990 LOC, well past the 800-LOC hard target). It is nowapp.css(~672 — a11y, design tokens/theme, sidebar, main, buttons, content-shell),components.css(~595 — cards, grids, paginator, badges, tables, forms, log/console, markdown, language switcher, chip filter, connection banner), andoverlays.css(~737 — toast, notifications drawer, modal, misc/responsive, the[dir="rtl"]mirror, docs-fab, usage-hud), each within the hard limit.- The cut is contiguous and in-order, so the cascade is byte-for-byte identical to the pre-split file;
index.htmlloads the three as ordered<link>s. No behavior, markup, or i18n change. - CSS-asserting tests now read the concatenation via a shared
tests/helpers/css.mjs::loadAppCss()helper (agnostic to which physical file holds a rule). Newtests/css-modularization.test.mjslocks the split (files exist · each ≤ 800 LOC · index.html link order) → suite 2138. Verified in-browser: all three stylesheets parse and their rules apply (0 console errors). scan.js(~1254) andconfig.js(~1010) remain the outstanding file-size debts.
- The cut is contiguous and in-order, so the cascade is byte-for-byte identical to the pre-split file;
[1.131.1] — 2026-07-31
Fixed
- Adapter host-pinning consistency on the two v1.130.0 sources (code-review follow-ups, defense-in-depth; no behavior change for valid inputs):
a16z-speedrun-talentadapter now re-validates theapi:/a16z-speedrun-talent:override atbuildEndpoint(HTTPS + exact hostspeedrun-talent-network.com) and falls back to the canonical feed when it fails — parity with thecryptocurrencyjobsadapter, so an off-host value never reaches the fetch slot (previously it relied solely on the fetch-timeassertSpeedrunUrlguard). The exact-host check is now a single exportedSPEEDRUN_TALENT_HOST_REshared by the guard and the adapter.cryptocurrencyjobsparser —cleanUrlnow uses the same exact-match host guard asassertCryptocurrencyJobsUrland the adapter override (wasendsWith, which accepted subdomains). The parser is never more permissive than the SSRF guard: asub.cryptocurrencyjobs.coitem link is dropped.- +2 tests (
tests/sources-a16z-speedrun-talent.test.mjsoff-host/non-HTTPS/subdomain override → feed;tests/sources-cryptocurrencyjobs.test.mjssubdomain link dropped) → suite 2135.
[1.131.0] — 2026-07-31
Added
#/trackerCRM stage-tab board (ported from the parent web app’s/pipelineview). The tracker’s funnel-chip bar + status dropdown are replaced by a proper stage-tab strip: an All tab plus one tab per canonical status — Evaluated · Applied · Responded · Interview · Offer · Rejected · Discarded · SKIP · Hired — each showing a live whole-history count, including zero-count stages so the full funnel is always visible at a glance (the CRM look). The active tab drives the filter; clicking it again clears back to All. Rows keep their score-tone, legitimacy, PDF and report affordances, and the company cell now shows a brand logo when logos are enabled (off by default → zero extra requests, same contract as#/scan).- New read-only route
GET /api/tracker/stagesreturns the canonical funnel (labels in order) + an alias-fold map, sourced fromserver/lib/states.mjs(templates/states.yml, with the built-in fallback) — so the client never hardcodes the status whitelist (the v1.128.0 doctrine): a parent that renames or reorders a stage flows through with no client change. The legacy no-paramGET /api/trackerresponse is unchanged ({ rows }only). - New pure, unit-tested client lib
public/js/lib/tracker-stages.js(window.TrackerStages—foldStatus,stageCounts) buckets rows against the server’s stages, tolerating stray markdown bold and localized aliases (e.g.aplicado→Applied). Tabs are accessible (role="tablist"/tab,aria-selected, ≥44 px hit area, counts folded into each tab’s accessible name); focus stays on the active tab after a switch. No new i18n keys (stage labels render verbatim like the row badges). +tests/tracker-stages.test.mjs(6) +tests/tracker-stages-endpoint.test.mjs(4); suite 2133.
- New read-only route
[1.130.0] — 2026-07-31
Added
- Two new scan sources ported from parent career-ops v1.24.0 (in-process, no new deps; both appear in the
#/scanSource filter and on the cvstart.org landing):- a16z Speedrun (
a16z-speedrun-talent, #2231) — the a16z Speedrun talent-network board-wide JSON feed. Host-pinned tospeedrun-talent-network.com, HTTPS-only, 0-indexed pagination with a page cap, per-companyq/config threading, fail-soft. +server/lib/sources/a16z-speedrun-talent.mjs+ adapter +tests/sources-a16z-speedrun-talent.test.mjs(16). - Cryptocurrency Jobs (
cryptocurrencyjobs) — the Web3 job boardcryptocurrencyjobs.co, ingested via its public RSS 2.0 feed (zero-auth). Two-pass XML-entity decode, remote-only listings, employer parsed from the"… at <Company>"title tail. +server/lib/sources/cryptocurrencyjobs.mjs+ adapter +tests/sources-cryptocurrencyjobs.test.mjs(14). - Registry total is now 72 sources = 67 English + 5 Russian (
ALL_ADAPTERS= 67 EN portal adapters).
- a16z Speedrun (
Fixed
echojobs— hybrid roles stay distinguishable from remote (mirrors parent #2258). A case-insensitivehybridmarker in the posting’s remote-type now yields"<City> · Hybrid"(or a bareHybridwhen no city is present) andworkplaceType: 'Hybrid', instead of being collapsed intoRemote. +tests/sources-echojobs.test.mjs(7).radancy— legacy TalentBrew markup + JSON results-fragment transport (mirrors parent a3e6df9). The adapter now parses TalentBrew HTML listings and, when available, the JSON results fragment (buildFragmentUrl/readFragmentTotals), gated on an injectableopts.fetchJson. +tests/sources-radancy.test.mjs(13).
Notes
- Not ported — CLI-only parent features. career-ops v1.24.0’s large CLI/mode surface stays out of web-ui, which is a viewer + thin write-through, not a mode host: the compliance/jurisdiction tables (
interview-redflagprotected-grounds,ofertaimmigration-status / jurisdiction-prohibited / agency-licensing,offer-preprestrictive-covenant,check-table-freshness), the contacts phonebook + vCard export +company-history, the interview transcript-debrief / call-platform detection / plan wiring,ledgerset-status,outcomerecording + archiving, two-passtriage,jd-similarityCV-reuse hints, the versioned application-CV artifact schema,doctorPlaywright-MCP detection, andportals/fix-slugs.mjs(web-ui exposes read-onlyPOST /api/portals/health, never auto-writesportals.yml). Scan-orchestration changes that live in the parent’sscan.mjs— the Interamt.de Playwright scanner, iCIMS reverse-ATS full sweep (#2141), country-eligibility remote filter (#2095), DNS-lookup pacing + negative-cache resolver, StepStonerltrdedup (#1982), and the scan-history normalized-company column (#2243) — are not applicable: web-ui runs the EN/RU scanners in-process and does not shell intoscan.mjs. - Already covered. The
role-matcheraccent-folding fix (#2209 — fold accented Latin before tokenizing) was ported in v1.127.0 (normalizeTitleNFD fold inserver/lib/role-matcher.mjs), so parent v1.24.0’s change is a no-op here. Read-only relays (stats.mjs,salary-gap.mjs,analyze-patterns.mjs,followup-*) absorb the parent’s shape tweaks through their fail-soft path — no code change needed.
[1.129.1] — 2026-07-29
Fixed
- AI-review follow-ups on the v1.128.0/v1.129.0 web-ports (all advisory, none blocking; fixed at source):
job-facets.jsseniority precedence — an explicit modifier now wins over a management word:Senior Engineering Manager→senior(waslead),Staff Manager→staff, while a bareEngineering ManagerstaysleadandSenior Staff Engineer→staff. The modifier buckets (staff/senior/junior/intern) are tested before the role-levelleadbucket.server/lib/states.mjsfallback is no longer pinned — a SUCCESSFULtemplates/states.ymlread is still memoized, but the built-in FALLBACK is returned uncached, so a parent whosetemplates/was momentarily unavailable at boot (or updated live) is re-read on the next call instead of being stuck for the process lifetime. A present-but-malformed file now emits aconsole.warn(drift is surfaced to ops) while a genuinely absent file stays quiet.score-tone.js— a not-yet-scored row is neutral, not red — a null/blank score now returnsmuted(.score-muted) instead ofbad; a real low grade (D/F) still readsbad.company-logo.jsdomainFromName()skips non-ASCII slugs — a name like株式会社no longer builds an invalid host handed to/api/logo(whoselooksLikeHostguard would reject it anyway); it goes straight to the letter-avatar, sparing the round-trip.tests/states.test.mjsisolation guard — a first sanity assertion pinsPATHS.statesYmlto the tempCAREER_OPS_ROOT, so if the per-file test isolation ever regresses the fallback assertions fail loudly instead of silently passing against the real parent’s file. +4 tests → 2073.
[1.129.0] — 2026-07-29
Added
#/scanseniority facet + freshness column — thejob-facets.jslibrary shipped in v1.128.0 is now wired into the scan UI (previously logic-only). A new Seniority filter dropdown buckets each posting’s title into lead/staff/senior/mid/junior/intern (JobFacets.seniorityFromTitle) and auto-populates from what’s actually in the results — exactly like the Country facet; titles with no seniority word always pass. It round-trips through saved searches, Reset, and the Apply flow. The results table gains a Seniority badge column and a zero-token Age column showing freshness (today/Nd, fromJobFacets.daysSince(job.date); blank when the posting has no date). 12 new i18n keys ×17 (scan.allSeniority,scan.lblSeniority,scan.col.seniority,scan.col.age,scan.freshToday,scan.dSuffix,scan.sen.{lead,staff,senior,mid,junior,intern}). +tests/scan-seniority-facet-v1129.test.mjs(3) → suite 2069.
[1.128.0] — 2026-07-29
Added
- Four solutions ported from the parent’s own web app (
../web/, Next.js) — re-implemented in vanilla JS / ESM, no new deps:- Canonical states read live from
templates/states.yml(server/lib/states.mjs) — the tracker’s status vocabulary is no longer hardcoded.canonicalLabels()+canonicalizeStatus()read the parent’s single source of truth (with a byte-identical CI fallback), so the whitelist and alias folding stay in sync automatically instead of needing a manual re-sync every parent release (the v1.118.0 ‘Hired’ add was one such sweep).POST /api/trackernow folds any label/id/alias (Spanish/legacy, stray**) to its canonical label; theGET /api/trackerfunnel buckets by canonical status so aliases no longer spawn phantom entries. +tests/states.test.mjs. - Company logos on ATS-hosted rows —
public/js/lib/company-logo.jsgainsdomainFromName()(a curated ~90-entry brand→domain override map + legal-suffix stripping + slug fallback). When the posting URL is a shared ATS host (greenhouse/lever/…) — the majority of rows — the employer domain is now derived from the company name and fed to the existing SSRF-safe/api/logoproxy before dropping to a letter-avatar (now 1–2 initials). +tests/company-logo-domain.test.mjs. - Finer score tone —
public/js/lib/score-tone.js(window.ScoreTone) replaces the coarse ≥4/≥3 split with a 4-tier tone (≥4.2 / ≥3.8 / ≥3.0) + a letter-grade fallback;#/trackerscore cells use it (new neutral.score-mutedtier). +tests/score-tone.test.mjs. - Zero-token job facets —
public/js/lib/job-facets.js(window.JobFacets):seniorityFromTitle(),sourceFromUrl()(dot-boundary-anchored host match),daysSince()— a reusable client lib for cheap filtering. +tests/job-facets.test.mjs.
- Canonical states read live from
Notes
- Not ported (CONCEPT-only). The parent web app’s agentic action layer (
actions/registry.ts+api/assistant/route.ts— one registry both the UI and the AI assistant dispatch,sideEffect-gated with confirm-before-write) is the blueprint ifdocs-fab.jsever grows from a read-only help chat into an acting co-pilot; a large, spec-worthy build left for later. Analytics / cv-quality / logbuf / logo+usage routes are already at or above parity. No new scan sources (registry stays 70); no i18n/help changes.
[1.127.0] — 2026-07-29
Added
- Three new scan sources (parent career-ops v1.23.0 parity) — the registry now ships 70 adapters (65 English + 5 Russian):
- Flowxtra (
flowxtra) — a board-wide, no-auth aggregator (app.flowxtra.com/api/central/jobs) that lists live postings across every hosted company in one paginated call. Source + adapter +tests/sources-flowxtra.test.mjs(25 cases). - VDAB (
vdab) — Flanders’ public employment service; queries the publicvindeenjobJSON API (www.vdab.be) by keyword, recall-first like Arbeitsagentur (filtering happens downstream). +tests/sources-vdab.test.mjs(24 cases). - iCIMS (
icims) — the classic iCIMS hosted-portal search pages (careers-<tenant>.icims.com), auto-detected from any*.icims.comcareers_url. Distinct from the existingjibeapply(iCIMS’s JibeApply product); the per-jobenrichDatehook is omitted (the in-process scanner returns jobs directly, undated). +tests/sources-icims.test.mjs(11 cases).
- Flowxtra (
- Cursor re-added to the supported-CLI roster (parent #2115) — the parent restored Cursor as a first-class host (
.cursor/skills/career-ops/SKILL.md).server/lib/routes/cli-detect.mjsnow probes thecursorbinary too (10 tools reported: 9 first-class + Gemini legacy), and every roster surface — help ×17 (intro / comparison table / provider-setup list / AI-CLI-tools tab), README ×17, the#/configAPI-keys tab (config.providerModelNote, i18n ×17),docs/career-ops-canonical.md, and thecanonical-docs-coveragegate — lists Cursor again.
Fixed
- Agentic Engineering Jobs: HTML scraper → REST API (parent #2167/#2143) —
server/lib/sources/agenticjobs.mjsnow reads the documentedagentic-engineering-jobs.com/api/v1/jobsREST endpoint instead of parsing job cards out of HTML (the scraper broke). Same emitted job shape (salary min/max/currency now surfaced),FEED_URLpoints at the API, host pin unchanged. Test suite rewritten to feed canned JSON. - Greenhouse: recover the city when
location.nameis a work model (parent #2104) — boards that put “Hybrid”/“Distributed” inlocation.nameand keep the real city only in the separate/officesendpoint no longer have every role silently dropped by the location filter. A second/officesrequest (paid only for boards that exhibit the pattern, fail-soft) folds the city back in viabuildOfficeMap/isWorkModelOnly/officesUrlFor. +tests/sources-greenhouse-offices.test.mjs(6 cases). - role-matcher parity (parent #1933 / #2164 / #2009) — the mirrored
server/lib/role-matcher.mjs(feeds the repost detector) now strips the “Member of Technical Staff” boilerplate prefix before tokenizing, treatsproductas a baseline token so PM sibling specialties stay distinct, NFD-folds accents (no phantom “nior” from “Sênior”), and treats a lone sub-baseline qualifier (associate/junior/entry/intern) as a disagreement. +4 assertions blocks intests/detect-reposts.test.mjs.
Notes
- Not ported (CLI-only or relay). The bulk of parent v1.23.0 is CLI/dashboard surface web-ui does not shell into:
batch-tailor.mjs,company-history.mjs,contacts.mjs,discover-ats.mjs,outcome.mjs,pipeline-lock.mjs,skill-extract.mjs,theme-style.mjs,sync-pdf-flags.mjs, the Dutch/Portuguese modes, the CV section-partial HTML system, PDF profile theming, the Go dashboard’s RESPONDED tab, and the updater/doctor fixes. The relayed scripts (analyze-patterns.mjsHired recognition #2145,salary-gap.mjsrange parse #2200,stats.mjs,funnel-velocity.mjs) need no web-ui change — the fail-soft relays absorb their shape. The parent’s own DNS-cache/decodeEntities/scan-timeout hardening lives in provider internals web-ui already guards viasafe-fetch/http-json. Parent VERSION → 1.23.0 (parentVersion).
[1.126.1] — 2026-07-25
Fixed
- Two CLI-roster drift spots the v1.126.0 resync missed — (1) the
#/config→ API keys tab intro (config.providerModelNote, i18n ×17) listed only 7 CLIs (Claude Code · Codex · Gemini · OpenCode · Qwen · Copilot · Kimi) — Antigravity and Grok Build are now inserted after OpenCode; (2) a second comparison-table row in the help guide (×17) and the built-at-CI site help still readInside Claude Code / Codex / Cursor / Gemini CLI— the pre-v1.28 stale set with Cursor — now the fullClaude Code / Codex / OpenCode / Antigravity CLI / Grok Build CLI / Qwen Code / Kimi / GitHub Copilot CLI (Gemini CLI legacy)roster. Both used slash/middot separators the v1.126.0 sweep’s patterns didn’t cover. i18n snapshot regenerated; suite stays 1969.
[1.126.0] — 2026-07-25
Added
- AI CLI tools tab now detects all 8 first-class career-ops CLIs — synced the
#/config“AI CLI tools” roster with the parent’sdocs/SUPPORTED_CLIS.md:server/lib/routes/cli-detect.mjsgains Grok Build CLI (grok) and Kimi CLI (kimi), and the Antigravity entry now probes its canonicalagybinary first (falling back toantigravity). The read-only PATH scan now reports 9 tools (8 first-class + Gemini CLI); it still never executes a found binary.tests/cli-detect-routes.test.mjslength assertions bumped 7→9.
Changed
- Documentation resync with career-ops.org/docs — reconciled every doc surface against the live parent docs (all 31 pages read). The canonical AI-assistant roster (help ×17 intro + comparison table + provider-setup list + AI-CLI-tools tab; README ×17 intro + CLI→provider mapping) now lists the 8 first-class CLIs — Claude Code, Codex, OpenCode, Antigravity CLI, Grok Build CLI, Qwen Code, Kimi, GitHub Copilot CLI — plus Gemini CLI (legacy wrapper, transitioned into Antigravity). The
canonical-docs-coveragegate’s CANON list was widened to enforce the fuller roster; wikiFeatures.mdCLI-detect line updated. Help bundles keep their 29 H2 / 105 H3 structure.
[1.125.4] — 2026-07-23
Changed
- site dependencies (dependabot #151–#153) —
sharp0.34.5→0.35.3,svgo4.0.1→4.0.2,fast-uri(dev) 3.1.3→3.1.4 insite/; Astro build green, no SPA/server impact.
Notes
- Parent parity sweep (career-ops
37d17ec..254764a, post-v1.22.0) — nothing portable: theset-statuswrong-row guard (#2108) is CLI-only (web-ui tracker rows are selected explicitly in the UI and no route shells intoset-status.mjs), the localized-mode Risk Summary mirror (#2109) touchesmodes/<lang>/files web-ui never reads (top-levelmodes/*.mdonly), theupdate-systemmanifest verification (#2111) is updater-only, and the rest is parent docs (Turkish README, SIGNATURES ×4, SCRIPTS.md, es accents). Parent VERSION stays 1.22.0 —parentVersionunchanged.
[1.125.3] — 2026-07-23
Fixed
- Danish and Hindi LLM prompts answered in English (user-reported) —
LOCALE_NAMESand all fiveSCAFFOLD_STRINGSbags inserver/lib/prompts.mjswere never extended fordaorhi(each locale expansion after the 12-locale era skipped this file), soresolveLocale()fell back toenand every AI prompt — deep research (live and manual), mode runs, evaluate, interview, networking, CV Studio — lost its# Output languagedirective in those two locales. Both are now first-class: locale directive + localized scaffolding (readFiles / userContext / modeTemplate / modeRoleLine / evalRoleLine). The regression gate intests/locale-scaffold.test.mjsnow sweeps the canonical 17-locale list fromtests/helpers/i18n-vm.mjsinstead of a hardcoded 12, and a new structural parity gate fails any scaffold key that falls back to English in a non-EN locale — a future locale that missesprompts.mjscan no longer ship (+12 tests, suite now 1969).
[1.125.2] — 2026-07-22
Fixed
- Deep research over Gemini: HTTP 502 (
MALFORMED_FUNCTION_CALL) (#145, contributed by @Alien10140) — the live/api/deepprompt told the model to “Use WebFetch / WebSearch” and to save the brief to a file, but headless API providers have no tool channel; Gemini answered with a function call instead of text, surfacing as an empty HTTP 502.buildDeepPromptandbundleProjectContextnow take aheadlessflag: live runs (Anthropic/Gemini/fallback cascade) get a no-tools prompt that writes the brief from the inlined context, while the copy-paste prompt for Claude Code keeps its tool instructions. +1 test intests/critical-fixes.test.mjs.
Changed
- Gemini defaults bumped past the deprecated
gemini-2.0-flash(#144, contributed by @Alien10140) — the Config dropdown, the server fallback ingemini.mjs(which silently disagreed with the hint), the OpenRouter fallback chain,config.geminiModelHint×17 and the help guide ×17 now all namegemini-3.6-flash. The new drift gatetests/gemini-default-model.test.mjs(+5 tests) pins every surface to the same literal — the suite is now 1957 tests.
[1.125.1] — 2026-07-21
Fixed
- SuccessFactors: multi-brand RMK tenants keep their brand path (parent #2099, post-v1.22.0) — holding companies that run several acquired brands off one shared RMK instance disambiguate them by a path segment (
careers.nemetschek.com/Bluebeam/vs…/Vectorworks/); the adapter used to collapse the configured URL to its origin, silently scanning the parent brand’s postings. The endpoint now preserves the brand prefix, stripping only a trailing/search/or/tile-search-results/segment so nothing ever doubles onto itself; single-domain tenants are byte-for-byte unchanged. New exportedresolveTenantBasehelper + 1 ported test block intests/sources-successfactors.test.mjs.
[1.125.0] — 2026-07-21
Added
- cvstart.org: “Job sources” landing section — a new section between the screenshots and the comparison lists all 67 scanner sources as clickable chips (62 EN boards/ATS + the 5 Russian boards under their own subheading), each linking to the source’s public site. The list is synced from the live adapter registry at build (
sync-assets.mjs→facts.sources), so it can never drift from the app; a curated link map inSources.astrois gated by the newtests/site-sources.test.mjs— a newly ported adapter without a link fails CI instead of silently shipping a linkless chip. Header nav gained a Sources anchor; 4 new site i18n keys ×17. Also fixed the landing JSON-LDinLanguagelist, which was still missinghi.
[1.124.0] — 2026-07-21
Added
- Five scan sources (parent v1.22.0 parity, #1808/#1572/#2024/#2055) — Welcome to the Jungle (board-wide JSON API), Agentic Engineering Jobs (agentic/AI-engineering board), Jobvite (zero-auth per-tenant ATS), Gem (per-tenant ATS), and Alibaba Group (careers JSON API, Meituan/Tencent pattern). Each is a host-pinned, CI-isolated source + adapter pair; the registry now ships 67 adapters (62 EN + 5 RU); the
#/scanSource-dropdown fallback and its drift gate are updated; five new suitestests/sources-{wttj,agenticjobs,jobvite,gem,alibaba}.test.mjs.
Fixed
- Arbeitsagentur: nationwide-remote only when
homeofficetypisVOLLSTAENDIG(parent #1981) — thehomeoffice=nv_truequery also returns hybrid roles, so the remote pass now confirms each hit against the job-details endpoint in small batches and fails closed (a lookup error keeps the job’s real city so location filters still apply). - SmartRecruiters: public job URLs built without
/postings/(parent #2047) — links now land on the public posting page instead of a 404 for tenants whose public site drops the segment.
Notes
- Parent v1.22.0 also shipped CLI-side changes the web UI does not shell into or already covers: the zh-CN CV template + PDF typography,
/expandmode, provider prompt-cache tweaks (Gemini/OpenAI/Ollama), the per-step token breakdown (utils/token-tracker.mjs— the web UI has its owndata/llm-usage.jsonlusage meter), tracker writer-lock serialization (the web UI routes writes throughwithFileLocksince v1.21), the scanvisa_filter+ absolute posted-date CLI flags (the web UI has its own “Posted within” age filter), and the seen-sources dedupe seeding (#2079/#2080 — the web UI scanner keeps its own scan-history dedup).
[1.123.0] — 2026-07-17
Added
- Oracle Recruiting Cloud scan source (parent v1.21.0 parity, #1929) — the zero-auth
recruitingCEJobRequisitionsREST API of Oracle Fusion/ORC career sites (JPMorgan Chase, Oracle, BNY Mellon, American Express, Honeywell, …): host-pinned to*.fa[.<region>][.ocs].oraclecloud.com, the site number resolved from each tracked company’scareers_url, offset pagination with a hard page cap, and WAF-aware browser-like headers. The registry now ships 62 adapters (57 EN + 5 RU); the#/scanSource-dropdown fallback and its drift gate are updated; new CI-isolated suitetests/sources-oraclecloud.test.mjs.
Fixed
- Repost detector: base titles stay distinct from specialized-suffix siblings (parent #1922) — “Senior Analytics Engineer” no longer clusters with “Senior Analytics Engineer, People Analytics”: when one title’s tokens are a strict subset of the other’s and the extra token is a real specialization (not a baseline word), the two are treated as separately-postable openings. Reposting annotations (“(Repost)”, “relisted”) are now stopworded as meta noise. +2 assertions in
tests/detect-reposts.test.mjs.
Notes
- Parent v1.21.0 also shipped CLI-side changes the web UI does not shell into or already covers: the repeat-company reapply warning (the web UI has the re-apply cooldown since v1.84.0), cover-letter
--format/--reportflags, the interview red-flag / panel-intel / no-show e-mail prompt modes, scan trust-signal & portal-health persistence (the web UI runs its own in-process scanner withtrust-validatorand the Portals health page), and the stats/salary-gap extensions (relayed read-only and fail-soft).
[1.122.0] — 2026-07-16
Added
- Hindi (हिन्दी) — the 17th language — full UI dictionary (~1,110 keys), the complete in-app help guide (29 H2 / 105 H3 parity),
README.hi.md, a newCHANGELOG.hi.md(starts at v1.122.0, following the de/it/tr precedent), the cvstart.org landing + Methodology/License/Changelog/Help pages, the language switcher (🇮🇳), browser-language auto-detect, and a localized dashboard screenshot. Every ×16 parity gate now runs ×17: i18n dict parity + snapshot, help H2/H3 gates, CHANGELOG parity, sitecheck-i18n, and the Playwright locale sweep.
[1.121.0] — 2026-07-16
Added
- cvstart.org: Methodology, License and Changelog pages — the landing gained three new sections in all 16 languages, next to the existing Compare block: /methodology/ (the six-dimension 0.0–5.0 scoring rubric, the 4.0 apply threshold, and the never-do rules — a localized summary of career-ops.org/methodology), /license/ (the canonical MIT text with the NOTICE.md pointer), and /changelog/ (this file, rendered per-locale from the repo’s 16 translated CHANGELOGs). New header Methodology entry and footer Resources links;
sync-assets.mjsnow syncs the CHANGELOG ×16 and LICENSE into the site at build time, so the pages can never drift from the repository. - Methodology links across the docs — the README (all 16), the in-app help guide §1 canonical list (all 16), and the wiki now link career-ops.org/methodology (plus the FAQ and glossary) alongside the existing career-ops.org/docs guides.
Changed
- README release banner and badges refreshed (tests 1850, release v1.121.0) — the banner had still announced v1.119.5.
[1.120.0] — 2026-07-16
Added
- The CareerOps Manifesto (parent v1.20.0 parity) — the parent project shipped the CareerOps Manifesto (
MANIFESTO.md· career-ops.org/manifesto) and now surfaces it from its README, updater, and Go dashboard. The web UI follows suit: a new sidebar-footer link opens the manifesto page (newfooter.manifestoi18n key in all 16 locales), the in-app help guide gained §29 “The CareerOps Manifesto” in all 16 languages, the README explains what the manifesto is and how to sign it, and the cvstart.org landing footer links to it too.
Notes
- Parent v1.20.0 also fixed the
upskilltargeted-mode known-skill suppression, quieted dotenv soscan --jsonstdout stays parseable, and fixed the HTML CV template so a role header stays with its bullets — CLI-side surfaces the web UI does not shell into; no web-ui code change was needed.
[1.119.5] — 2026-07-13
Fixed
- Landing language button no longer wraps — with the v1.119.2 flags the header switcher label (e.g. «🇷🇺 Русский») could break onto up to three lines at narrow desktop widths; the switcher label and every option in the dropdown are now
whitespace-nowrap, keeping flag + endonym on one line. The footer language list switched from a rigid two-column grid to a wrapping row of one-line items, so «🇧🇷 Português (Brasil)» never splits mid-name either.
[1.119.4] — 2026-07-13
Changed
- LICENSE names the author — the copyright line now reads Sergei Emelianov (Fighter90) https://sergey-cv.com and career-ops-ui contributors (canonical MIT text untouched). A new NOTICE.md spells the licensing out in detail: who holds copyright, exactly what the MIT grant covers (code, docs, translations, the landing, the wiki), what it does NOT cover (your runtime data, the parent project, job-board content, trademarks), the third-party component table (express/js-yaml MIT; Astro/Tailwind MIT; Figtree & JetBrains Mono under SIL OFL 1.1; sharp Apache-2.0) and an optional attribution line.
[1.119.3] — 2026-07-13
Added
- SECURITY.md — the security policy the CONTRIBUTING guide pointed to now exists: supported versions, private reporting flow (GitHub private vulnerability reporting is now enabled on the repo — Security tab → “Report a vulnerability”), the threat model for a localhost-bound single-user app (XSS via hostile job postings / SSRF / path traversal / secret leakage / CSP weakening in scope; localhost DoS and parent-project issues out of scope) and the hardening baseline for reviewers.
[1.119.2] — 2026-07-13
Added
- CONTRIBUTING.md — the contributor guide the landing and README have linked to all along now exists: setup, project map, the hard security/no-build rules, testing tiers, the two-registry walkthrough for adding a scan source, the ×16 i18n contract, commit/PR conventions and the release process.
- Language flags on the landing — the cvstart.org language switcher, the footer language grid and the “read in your language” banner now show each locale’s flag next to its endonym (same regional-indicator set as the app’s language
<select>; degrades to region letters where flag glyphs are missing). - Landing footer fixes — the dead Discussions link (the feature is not enabled on the repo) now points to the project wiki, and the footer credits the author: Sergei Emelianov (sergey-cv.com · LinkedIn).
[1.119.1] — 2026-07-13
Fixed
#/scansource filter caught up with the registry — the staticFALLBACK_SOURCESlist behind the Source dropdown (used only whenGET /api/scan/sourcesis unreachable) had silently lagged since v1.87.0: 20 providers (Amazon, Avature, SAP SuccessFactors, Get on Board, Dassault Systèmes, beesite, HigherEdJobs, JibeApply (iCIMS), softgarden, Cornerstone, Phenom, Radancy, Deutsche Bahn, EchoJobs, TKMS, Heckler & Koch, Rheinmetall, LaraJobs and the new Meituan / Tencent) were missing from the offline fallback. Synced to all 61 and now gated by a drift test that fails CI whenever the client list diverges from the server registry (values AND labels). +1 test (1845).
[1.119.0] — 2026-07-13
Parent career-ops v1.19.0 parity + cvstart.org landing refresh.
Added
- 2 new scan providers — Meituan (
zhaopin.meituan.com) and Tencent (careers.tencent.com): the Chinese tech boards’ zero-auth public JSON APIs, host-detected or selected via an explicitprovider:, with per-keyword server-side search, paginated fetch and URL dedup — 61 adapters now (56 EN + 5 RU). +20 tests (1844). - Contributors block on the landing — cvstart.org shows the avatars of everyone who landed code (GitHub
/contributorsAPI at build time, bots filtered), localized in all 16 languages, linking to the full contributors graph. - Live GitHub star counter on the landing — the header badge now refreshes client-side from the GitHub API on every visit (build-time snapshot as fallback), and a weekly scheduled Pages rebuild keeps the snapshot + contributors list fresh; CI API calls are token-authenticated.
Fixed
- Workday CXS requests carry browser-like headers (parent #1813) — Cloudflare-gated tenants (seen live: geico) answer 500 to requests missing an ordinary UA/
accept-language/origin/referer; the fetcher now derives origin + site slug from the CXS URL itself. Glints requests gained the same browser-like UA + origin/referer, both sourced from one sharedBROWSER_LIKE_USER_AGENTconstant inhttp-json.mjs.
[1.118.4] — 2026-07-10
Fixed
- hh.ru scans returned 0 hits from a Russian IP (regional-subdomain links) — from a Russian residential IP hh.ru 302-redirects the search to a regional subdomain (
sochi.hh.ru,spb.hh.ru, …) and returns vacancy links on that subdomain. The parser’s title-link matcher was anchored to a hardhttps://hh.ru/vacancy/host, so it matched zero of the regional links and a fully working scan silently recorded 0 hits. It now accepts any*.hh.ruhost (ads onadsrv.hh.ru/click?…are still excluded — they have no/vacancy/<id>path) and canonicalizes each result URL back tohttps://hh.ru/vacancy/<id>. Verified live: 17 real vacancies now parse from asochi.hh.rupage that previously yielded 0. +1 test (1824).
[1.118.3] — 2026-07-10
Fixed
- hh.ru silently returned 0 hits (VPN-check interstitial) — hh.ru now 302-redirects networks it flags as VPN/proxy (datacenter egress IPs) to a
/vpncheeckinterstitial (“VPN мешает работе сайта”) that answers HTTP 200 with zero vacancy cards, so the scan reported 0 with no error at all. The scanner now detects the redirect via the response’s final URL, disables hh.ru for the rest of the run, and prints an honest hint: traffic must really exit via a residential IP — a system-wide VPN/proxy can stay active even when the browser toggle is off. +1 test (1823).
career-ops-ui v1.267.1 · page 6/11 · mirror of the project changelog for cvstart.org