Changelog

GitHub

Every notable change, release by release. The same file ships in the repository and is translated into all 19 languages.

[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-talent adapter now re-validates the api: / a16z-speedrun-talent: override at buildEndpoint (HTTPS + exact host speedrun-talent-network.com) and falls back to the canonical feed when it fails — parity with the cryptocurrencyjobs adapter, so an off-host value never reaches the fetch slot (previously it relied solely on the fetch-time assertSpeedrunUrl guard). The exact-host check is now a single exported SPEEDRUN_TALENT_HOST_RE shared by the guard and the adapter.
    • cryptocurrencyjobs parser — cleanUrl now uses the same exact-match host guard as assertCryptocurrencyJobsUrl and the adapter override (was endsWith, which accepted subdomains). The parser is never more permissive than the SSRF guard: a sub.cryptocurrencyjobs.co item link is dropped.
    • +2 tests (tests/sources-a16z-speedrun-talent.test.mjs off-host/non-HTTPS/subdomain override → feed; tests/sources-cryptocurrencyjobs.test.mjs subdomain link dropped) → suite 2135.

[1.131.0] — 2026-07-31

Added

  • #/tracker CRM stage-tab board (ported from the parent web app’s /pipeline view). 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/stages returns the canonical funnel (labels in order) + an alias-fold map, sourced from server/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-param GET /api/tracker response 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.

[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 #/scan Source filter and on the cvstart.org landing):
    • a16z Speedrun (a16z-speedrun-talent, #2231) — the a16z Speedrun talent-network board-wide JSON feed. Host-pinned to speedrun-talent-network.com, HTTPS-only, 0-indexed pagination with a page cap, per-company q/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 board cryptocurrencyjobs.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).

Fixed

  • echojobs — hybrid roles stay distinguishable from remote (mirrors parent #2258). A case-insensitive hybrid marker in the posting’s remote-type now yields "<City> · Hybrid" (or a bare Hybrid when no city is present) and workplaceType: 'Hybrid', instead of being collapsed into Remote. +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 injectable opts.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-redflag protected-grounds, oferta immigration-status / jurisdiction-prohibited / agency-licensing, offer-prep restrictive-covenant, check-table-freshness), the contacts phonebook + vCard export + company-history, the interview transcript-debrief / call-platform detection / plan wiring, ledger set-status, outcome recording + archiving, two-pass triage, jd-similarity CV-reuse hints, the versioned application-CV artifact schema, doctor Playwright-MCP detection, and portals/fix-slugs.mjs (web-ui exposes read-only POST /api/portals/health, never auto-writes portals.yml). Scan-orchestration changes that live in the parent’s scan.mjs — the Interamt.de Playwright scanner, iCIMS reverse-ATS full sweep (#2141), country-eligibility remote filter (#2095), DNS-lookup pacing + negative-cache resolver, StepStone rltr dedup (#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 into scan.mjs.
  • Already covered. The role-matcher accent-folding fix (#2209 — fold accented Latin before tokenizing) was ported in v1.127.0 (normalizeTitle NFD fold in server/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.js seniority precedence — an explicit modifier now wins over a management word: Senior Engineering Manager → senior (was lead), Staff Manager → staff, while a bare Engineering Manager stays lead and Senior Staff Engineer → staff. The modifier buckets (staff/senior/junior/intern) are tested before the role-level lead bucket.
    • server/lib/states.mjs fallback is no longer pinned — a SUCCESSFUL templates/states.yml read is still memoized, but the built-in FALLBACK is returned uncached, so a parent whose templates/ 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 a console.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 returns muted (.score-muted) instead of bad; a real low grade (D/F) still reads bad.
    • company-logo.js domainFromName() skips non-ASCII slugs — a name like 株式会社 no longer builds an invalid host handed to /api/logo (whose looksLikeHost guard would reject it anyway); it goes straight to the letter-avatar, sparing the round-trip.
    • tests/states.test.mjs isolation guard — a first sanity assertion pins PATHS.statesYml to the temp CAREER_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

  • #/scan seniority facet + freshness column — the job-facets.js library 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, from JobFacets.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/tracker now folds any label/id/alias (Spanish/legacy, stray **) to its canonical label; the GET /api/tracker funnel 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.js gains domainFromName() (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/logo proxy 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; #/tracker score cells use it (new neutral .score-muted tier). +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.

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 if docs-fab.js ever 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 public vindeenjob JSON 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.com careers_url. Distinct from the existing jibeapply (iCIMS’s JibeApply product); the per-job enrichDate hook is omitted (the in-process scanner returns jobs directly, undated). +tests/sources-icims.test.mjs (11 cases).
  • 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.mjs now probes the cursor binary 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 #/config API-keys tab (config.providerModelNote, i18n ×17), docs/career-ops-canonical.md, and the canonical-docs-coverage gate — lists Cursor again.

Fixed

  • Agentic Engineering Jobs: HTML scraper → REST API (parent #2167/#2143) — server/lib/sources/agenticjobs.mjs now reads the documented agentic-engineering-jobs.com/api/v1/jobs REST endpoint instead of parsing job cards out of HTML (the scraper broke). Same emitted job shape (salary min/max/currency now surfaced), FEED_URL points at the API, host pin unchanged. Test suite rewritten to feed canned JSON.
  • Greenhouse: recover the city when location.name is a work model (parent #2104) — boards that put “Hybrid”/“Distributed” in location.name and keep the real city only in the separate /offices endpoint no longer have every role silently dropped by the location filter. A second /offices request (paid only for boards that exhibit the pattern, fail-soft) folds the city back in via buildOfficeMap/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, treats product as 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 in tests/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.mjs Hired recognition #2145, salary-gap.mjs range 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 via safe-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 read Inside Claude Code / Codex / Cursor / Gemini CLI — the pre-v1.28 stale set with Cursor — now the full Claude 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’s docs/SUPPORTED_CLIS.md: server/lib/routes/cli-detect.mjs gains Grok Build CLI (grok) and Kimi CLI (kimi), and the Antigravity entry now probes its canonical agy binary first (falling back to antigravity). 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.mjs length 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-coverage gate’s CANON list was widened to enforce the fuller roster; wiki Features.md CLI-detect line updated. Help bundles keep their 29 H2 / 105 H3 structure.

[1.125.4] — 2026-07-23

Changed

  • site dependencies (dependabot #151–#153) — sharp 0.34.5→0.35.3, svgo 4.0.1→4.0.2, fast-uri (dev) 3.1.3→3.1.4 in site/; Astro build green, no SPA/server impact.

Notes

  • Parent parity sweep (career-ops 37d17ec..254764a, post-v1.22.0) — nothing portable: the set-status wrong-row guard (#2108) is CLI-only (web-ui tracker rows are selected explicitly in the UI and no route shells into set-status.mjs), the localized-mode Risk Summary mirror (#2109) touches modes/<lang>/ files web-ui never reads (top-level modes/*.md only), the update-system manifest 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 — parentVersion unchanged.

[1.125.3] — 2026-07-23

Fixed

  • Danish and Hindi LLM prompts answered in English (user-reported) — LOCALE_NAMES and all five SCAFFOLD_STRINGS bags in server/lib/prompts.mjs were never extended for da or hi (each locale expansion after the 12-locale era skipped this file), so resolveLocale() fell back to en and every AI prompt — deep research (live and manual), mode runs, evaluate, interview, networking, CV Studio — lost its # Output language directive in those two locales. Both are now first-class: locale directive + localized scaffolding (readFiles / userContext / modeTemplate / modeRoleLine / evalRoleLine). The regression gate in tests/locale-scaffold.test.mjs now sweeps the canonical 17-locale list from tests/helpers/i18n-vm.mjs instead 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 misses prompts.mjs can 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/deep prompt 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. buildDeepPrompt and bundleProjectContext now take a headless flag: 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 in tests/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 in gemini.mjs (which silently disagreed with the hint), the OpenRouter fallback chain, config.geminiModelHint ×17 and the help guide ×17 now all name gemini-3.6-flash. The new drift gate tests/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 exported resolveTenantBase helper + 1 ported test block in tests/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 in Sources.astro is gated by the new tests/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-LD inLanguage list, which was still missing hi.

[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 #/scan Source-dropdown fallback and its drift gate are updated; five new suites tests/sources-{wttj,agenticjobs,jobvite,gem,alibaba}.test.mjs.

Fixed

  • Arbeitsagentur: nationwide-remote only when homeofficetyp is VOLLSTAENDIG (parent #1981) — the homeoffice=nv_true query 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, /expand mode, provider prompt-cache tweaks (Gemini/OpenAI/Ollama), the per-step token breakdown (utils/token-tracker.mjs — the web UI has its own data/llm-usage.jsonl usage meter), tracker writer-lock serialization (the web UI routes writes through withFileLock since v1.21), the scan visa_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 recruitingCEJobRequisitions REST 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’s careers_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 #/scan Source-dropdown fallback and its drift gate are updated; new CI-isolated suite tests/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/--report flags, 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 with trust-validator and 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 new CHANGELOG.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, site check-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.mjs now 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 (new footer.manifesto i18n 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 upskill targeted-mode known-skill suppression, quieted dotenv so scan --json stdout 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

  • #/scan source filter caught up with the registry — the static FALLBACK_SOURCES list behind the Source dropdown (used only when GET /api/scan/sources is 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 explicit provider:, 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 /contributors API 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 shared BROWSER_LIKE_USER_AGENT constant in http-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 hard https://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.ru host (ads on adsrv.hh.ru/click?… are still excluded — they have no /vacancy/<id> path) and canonicalizes each result URL back to https://hh.ru/vacancy/<id>. Verified live: 17 real vacancies now parse from a sochi.hh.ru page 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 /vpncheeck interstitial (“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).

[1.118.2] — 2026-07-10

Maintenance

  • Landing follow-up (#118) — site/README.md reconciled to Astro 7 (the security upgrade in #116), unused import removed, and +4 executable guards for the landing build scripts: the i18n parity gate provably fails on a broken dictionary, and sync-assets never writes outside site/ — suite 1822. Two CodeQL alerts resolved (one fixed at source, one dismissed as intended build-time behavior).

[1.118.1] — 2026-07-10

Fixed

  • hh.ru scans from outside Russia — hh.ru now serves HTTP 451 (regional legal block) to non-Russian IPs on its public search pages. The scanner treats 451 like 403: after the first block hh.ru is disabled for the rest of the run with an honest log line pointing to a Russian IP / VPN exit node, so the remaining queries and the other RU sources are not wasted. Help §7 updated in all 16 languages. +1 test (1818).

[1.118.0] — 2026-07-09

Parent career-ops v1.18.0 parity pack.

Added

  • 9 new scan providers — Cornerstone OnDemand (csod), Phenom (phenom), Radancy (radancy), Deutsche Bahn (deutschebahn), EchoJobs (echojobs), TKMS (tkms), Heckler & Koch (hecklerkoch), Rheinmetall (rheinmetall), LaraJobs (larajobs) — 54 adapters now. The Lever adapter additionally detects EU-tenancy boards (jobs.eu.lever.co).
  • Hired tracker status (parent states.yml parity): accepted offers get their own canonical state, a celebratory badge, and a job-landed banner on #/tracker; the funnel and conversion charts count it as having advanced through every stage.
  • Lifetime tab in #/stats — read-only relay of the parent’s stats.mjs (lifetime tracker roll-up, cumulative funnel rates, scanner totals, portal coverage) plus compensation observations from salary-gap.mjs (desired vs advertised vs actual, per application). New routes GET /api/stats/lifetime and GET /api/stats/salary-gap — zero-token shell-outs, fail-soft {available:false} without the parent project.
  • 28 new i18n keys in all 16 locales; help guide §14/§26 updated in every language.

Tests

  • +38 unit tests (three provider parity suites + relay/status routes) — 1817 total.

[1.117.2] — 2026-07-06

Empty-tracker fix for the parity shell-outs. The parent’s cadence/patterns scripts exit 1 with a structured {error} JSON when the tracker has no applications yet; the followup board and the Rejection-patterns tab showed that as “script-error”. Both routes now relay it as a healthy empty state (available:true, empty:true), so the UI shows its honest “nothing yet” message. Verified live against a real parent.

New: none.

[1.117.1] — 2026-07-06

Hardening follow-up to v1.117.0 (CodeQL triage). The three shell-out endpoints (GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns) now carry the shared per-IP rate limiter (they spawn a child process per request; no-op on loopback). The Add-to-CV URL text extraction strips tags to a fixed point and then removes every remaining </> outright — a provably complete sanitization for LLM-prompt text. No behavior change for valid input.

New: none.

[1.117.0] — 2026-07-06

Parent parity pack — six capabilities from the parent career-ops surfaced in the UI. (1) Follow-up cadence board: the #/followup page opens with per-application urgency (🔴 urgent / 🟠 overdue / 🟡 waiting / 🔵 cold) from the parent’s followup-cadence.mjs, plus a Seed follow-up dates button (followup-seed.mjs --backfill). (2) Rejection patterns: a fourth Statistics tab runs analyze-patterns.mjs (read-only) — outcome mix, recommendations, and the per-ATS-vendor advance rate. (3) Add to CV: a CV Studio card turns a project/publication URL or pasted text into ATS bullets grounded ONLY in that source (suggestions only, no writes; URL fetch is SSRF-guarded). (4) 4 new scan providers — beesite (jobs.mercedes-benz.com backend), HigherEdJobs (RSS), JibeApply (iCIMS), softgarden — the registry now ships 50 adapters (45 EN + 5 RU), all in the Scan source dropdown. (5) Knock-out pre-scan step in the Apply checklist (visa/degree/salary disqualifiers flagged before form-filling). (6) Reconcile runner (/api/run/reconcile → reconcile-pipeline.mjs). Shell-out routes are fail-soft: without the parent scripts the UI shows an honest “not available” note.

  • New route module server/lib/routes/followup.mjs (31st) + GET /api/stats/patterns + POST /api/cv-studio/add-entry + 8 source/adapter files + registry counts 41→45 EN. Tests: tests/sources-parity-v1117.test.mjs (6) + tests/parity-routes-v1117.test.mjs (7); suite 1737 → 1750. 41 new i18n keys ×16. Help §13/§17/§24/§26 extended ×16.

New: GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns, POST /api/cv-studio/add-entry, POST /api/run/reconcile.

[1.116.0] — 2026-07-06

Usage meter reworked + first end-to-end widget test. The AI usage meter (v1.114.0) is fixed and pinned properly: it now sits pinned to the bottom of the left sidebar (full sidebar width, matching the sidebar surface) and pads the sidebar’s bottom by its own height so the menu is never covered — the nav + version footer always scroll clear above it. It refreshes live (a 15-second interval plus on tab-focus and route change), and each window row now shows the real <tokens> · <estimated cost> (bars scale against the 30-day window) instead of an always-100% “share”. Also: a durable typeof barrier in the CV importer closes the recurring CodeQL type-confusion false positive at the source, and a new Playwright end-to-end acceptance test drives both persistent widgets (Ask-the-docs launcher open/close/hide + usage meter render/collapse) in a real browser.

  • public/js/lib/usage-hud.js + app.css (sidebar-pinned, syncSidebarPad, real-time), server/lib/cv-import.mjs (typeof … === 'number' barrier). Tests: tests/playwright-widgets.mjs (2 E2E) + updated tests/usage-hud.test.mjs (10). Suite 1735 → 1737 (+2 browser E2E). Help §6 extended ×16.

New: none.

[1.115.0] — 2026-07-06

Design polish (conservative, coral brand kept). A light refinement pass over the shared design system — no restructuring, no palette change. Dashboard metric cards now lift and pick up a coral border on hover (matching the quick-action tiles); content cards lift a hair; primary / dark / danger buttons gain a resting shadow and a gentle hover lift for depth; big numbers align via tabular-nums; and interactive controls get a soft coral focus halo behind the crisp 2px keyboard ring. All motion respects prefers-reduced-motion, and the halo is deliberately scoped to controls — never a global *:focus-visible (which would re-paint the managed-focus route headings; the v1.58.x lesson).

  • CSS-only (public/css/app.css); no markup, i18n, route, or CSP change. Tests: tests/design-polish-v1115.test.mjs (5) guard the polish + the no-global-halo anti-regression. Verified live via Playwright (dashboard intact, zero console errors).

New: none.

[1.114.0] — 2026-07-06

AI usage & cost meter in the sidebar (bottom-left). A compact USAGE section now sits at the bottom of the sidebar (a fixed bottom-left card if there’s no sidebar; bottom-right in RTL) on every page. It shows your LLM token usage across 24h / 7d / 30d windows — each as <tokens> · <share%> with a green meter bar (share of all-time) — plus an estimated 24h-cost footer. Data is the read-only GET /api/usage rollup of data/llm-usage.jsonl (local only), the same source as the #/usage page; cost is an estimate and manual-mode runs are free and uncounted. Collapsible — the header toggles and the state persists.

  • New client widget public/js/lib/usage-hud.js (window.UsageHud) loaded from index.html, mounted into the sidebar above the version footer (fixed-corner fallback). CSP-safe (no inline handlers, static-constant gauge SVG); theme-aware + RTL-mirrored; hidden as a fixed overlay on mobile. No new server route — reuses the read-only usage rollup. Tests: tests/usage-hud.test.mjs (8). 3 new i18n keys ×16 (hud.title/hud.empty/hud.estimate).

New: none.

[1.113.0] — 2026-07-06

Floating “Ask the docs” assistant on every page. A gradient robot chat button now floats in the bottom-right corner (bottom-left in RTL) of every page. Click it to open a compact chat that answers how-to questions grounded ONLY in the in-app help guide in your language — the same endpoint as the #/docs-assistant page (POST /api/docs-assistant/ask), so it never reads your CV, profile, or tracker. Live with an LLM key; no key → a ready-to-run prompt. The header shows a robot avatar + an online status; starter chips seed common questions; Escape or click-outside closes it; it hides itself on the dedicated #/docs-assistant page.

  • New client widget public/js/lib/docs-fab.js (window.DocsFab) mounted globally from index.html; CSP-safe (no inline handlers, UI.md() render boundary, self-contained SVG icons); theme-aware + RTL-mirrored styles in app.css. No new server route — reuses the grounded docs-assistant endpoint. Tests: tests/docs-fab.test.mjs (8). 6 new i18n keys ×16 (fab.* + docs.err). Help §1 extended in place.

New: none.

[1.112.0] — 2026-07-06

Docs & QA consolidation. No user-facing code change. The SDD conventions doc (docs/sdd/CONVENTIONS.md) is refreshed to the current 30 route modules (was 24) and the current test baseline; the definitive whole-project QA prompt (qa/QA-REGRESSION-PROMPT.md) is consolidated — release mechanics destaled (v1.111, parentVersion 1.17.0, release-triggered publish), the §14 additions table corrected (scan Exclude re-labelled v1.109.0) and extended with the v1.111 CodeQL closeout — so it stands alone as the single regression prompt for all functionality. Adds one coverage test for the oversize-upload branch.

New: none.

[1.111.0] — 2026-07-06

Security — CodeQL backlog closeout. Three defense-in-depth hardenings that close the remaining static-analysis findings at the source instead of dismissing them. stripDangerousMarkdown now escapes the < of any truncated dangerous-tag opener (a payload ending in <script/<iframe/…) so its output provably contains no live dangerous tag. CV import reads an uploaded buffer’s size through an explicit Number() coercion — a type-confusion barrier. Mode role-lines are now template strings interpolated with String.replace instead of stored functions, removing the dynamic-dispatch call entirely. No user-facing behavior change.

  • server/lib/security.mjs (final escape belt), server/lib/cv-import.mjs (coerced size), server/lib/prompts.mjs (string role-lines). Tests: tests/security-hardening-v1111.test.mjs (7) + updated v1108 guard test. No i18n/help/route changes.

New: none.

[1.110.0] — 2026-07-06

Docs & QA refresh (all languages). No code change. The definitive whole-project QA prompt (qa/QA-REGRESSION-PROMPT.md) is refreshed to v1.109.0 with a new §14 covering everything shipped v1.98→v1.109, and the perennial UX-audit + design-export prompts gained the current page surface. Every in-app help paragraph added over v1.100–v1.109 (Ask the docs, AI CLI tools + company logos, AI usage & cost, scan Exclude, pipeline overview, two-pager export, CV Doctor) is now translated into all 16 languages.

New: none.

[1.109.0] — 2026-07-06

Scan Exclude filter + pipeline overview (parent-web layout parity). On #/scan, the Search box now treats commas as OR (“roles to find” — a row shows if it matches any term) and a new Exclude field hides any row whose company/role/location contains any comma-separated word (e.g. senior, staff); both are remembered by your saved searches. On #/pipeline, a compact overview strip shows your pipeline at a glance — N in inbox, N tracked, and the Applied / Responded / Interview / Offer counts from the tracker, each chip linking to #/tracker.

  • Client-only (no new route/writes). public/js/views/scan.js (include-OR + Exclude, round-tripped through saved-search state) + public/js/views/pipeline.js (overview strip, degrades to the inbox count if the tracker can’t be read). Tests: tests/scan-pipeline-ui-v1109.test.mjs (2). 4 new i18n keys ×16 (scan.filterExclude/scan.lblExclude + pipe.ovInbox/pipe.ovTracked). Help §7 + §8 extended in place.

New: none.

[1.108.0] — 2026-07-06

Security hardening (CodeQL triage, round 2). Three more low-severity findings fixed: the mode-prompt builder resolves the locale role-line by own key + typeof === function so a tampered locale can’t dispatch to a prototype method (unvalidated-dynamic-method-call); the PDF-filename slug is capped to 200 chars before the dash-trim regex so an all-dash input can’t backtrack (polynomial ReDoS); and document import coerces an array filename (a repeated header) to a string (type-confusion). No behavior change for valid input.

  • server/lib/prompts.mjs, server/lib/routes/runners.mjs, server/lib/cv-import.mjs + tests/security-hardening-v1108.test.mjs (3). Over v1.106–v1.108 the static-analysis backlog went from 167 to ~14, with every genuinely security-relevant finding fixed and the remainder (guarded/sanitized false positives + note-level lint) dismissed with rationale.

New: none.

[1.107.0] — 2026-07-06

Sanitizer hardening (at-rest XSS defense-in-depth). stripDangerousMarkdown — which neutralizes dangerous HTML in stored CV/JD markdown so any consumer that bypasses the escape-on-render client is still safe — now runs its tag strip to a fixed point (repeat-until-stable) so a removal that reforms a payload (e.g. <scr<script></script>ipt>) is caught, matches script/style/etc. end tags with trailing junk (</script foo>), and strips an unclosed executable opener (<script …> with no closing tag). Behavior for valid markdown is unchanged — it only ever removes more.

  • server/lib/security.mjs: fixed-point loop (bounded 8 passes) + [^>]*> end-tag patterns + unclosed-opener strip. +3 regression cases in tests/cv-xss-bypasses.test.mjs (end-tag variants, single-pass-reveal, unclosed). The authoritative XSS boundary remains output-escaping (UI.md); this strengthens the at-rest guarantee and closes the matching CodeQL findings.

New: none.

[1.106.0] — 2026-07-06

Security hardening (CodeQL triage). Fixed three real (if low-severity) findings from a pass over the static-analysis backlog: the route-render error path now escapes the error message before it reaches the DOM (a server error can echo user-supplied input, so it’s treated as untrusted — XSS boundary), and the profile/config property writes reject __proto__ / constructor / prototype keys (belt-and-braces prototype-pollution guards — the keys come from fixed field specs, not raw request input). The bulk of the remaining alerts are false positives on the scanner’s legitimate data/* reads/writes and on routes that already carry the app’s custom rate-limiter, and were dismissed with rationale.

  • public/js/router.js escapes err.message via UI.escapeHtml before innerHTML; server/lib/routes/content.mjs (setArray/setDotted) and server/lib/routes/config.mjs (the env-apply loop) guard prototype keys. No behavior change for valid input. Tests: tests/security-hardening-v1106.test.mjs (3). No new i18n keys.

New: none.

[1.105.0] — 2026-07-06

AI usage & cost page. A new AI usage page (sidebar, next to Health) shows how many tokens you’ve spent on live AI generations — evaluations, reports, chats — broken down per provider over the last 24 hours, 7 days, 30 days, and all-time, with an estimated USD cost. Every live provider call appends a small {provider, in, out} record to data/llm-usage.jsonl (nothing is sent anywhere); runs with no API key (manual mode) cost nothing and aren’t recorded.

  • New route module (30th) server/lib/routes/usage.mjs — GET /api/usage (read-only rollups) + server/lib/llm-usage.mjs (recordUsage normalizes the Anthropic/OpenAI/Gemini usage shapes and appends best-effort; readUsage/aggregate roll up per 24h/7d/30d/all window × provider) + server/lib/llm-pricing.mjs (an editable per-provider $/1M-token table — token counts are exact, dollars are approximate list prices you can correct to your plan; never billed). Recording is hooked at the dispatch chokepoints (runActiveProvider + routes/llm.mjs) so all live calls are captured.
  • New view public/js/views/usage.js (#/usage, window tabs). Tests: tests/usage-routes.test.mjs (normalize / price / record round-trip / aggregate / endpoint). 17 new i18n keys ×16 (usage.* + nav.usage). Help §6 extended in place.

New: server/lib/routes/usage.mjs; server/lib/llm-usage.mjs; server/lib/llm-pricing.mjs; public/js/views/usage.js.

[1.104.0] — 2026-07-06

Company logos in the scan table (privacy-preserving). A new Appearance toggle in App settings — Show company logos in the scan table (off by default) — draws each company’s logo next to its name on #/scan. The logo is the company’s favicon fetched from its own domain and proxied server-side (GET /api/logo), so no third-party logo service ever learns which employers you’re viewing. Postings on a shared job board (Greenhouse, Lever, Ashby, …) show a coloured letter badge instead of the board’s icon, and any logo that fails to load falls back to the same badge.

  • New route module (29th) server/lib/routes/logos.mjs — GET /api/logo?domain=. It validates the domain (no scheme/path/loopback), fetches /favicon.ico through the SSRF-safe safeGet (a new binary mode returns the raw bytes + content-type; DNS-pinning, redirect validation and the size cap are unchanged), image-magic sniffs the result so an HTML error page is never served as an image, caches hits and misses in an in-memory LRU, and writes nothing to disk.
  • New client lib public/js/lib/company-logo.js (window.CompanyLogo): off by default via a localStorage flag; skips shared ATS hosts in favour of a deterministic letter-avatar; CSP-safe img.onerror fallback. Tests: tests/logo-routes.test.mjs (domain guard, image sniff/reject, negative cache, binary safeGet). 5 new i18n keys ×16 (appear.*). Help §2 extended in place.

New: server/lib/routes/logos.mjs; public/js/lib/company-logo.js.

[1.103.0] — 2026-07-06

Settings: “AI CLI tools” — which agent CLIs are installed. career-ops is Claude-Code-driven but works with any agent CLI on the open skill standard. A new AI CLI tools tab in App settings (#/config) shows which of them — Claude Code, Codex, Gemini CLI, OpenCode, GitHub Copilot CLI, Qwen, Antigravity — are installed on the machine running the server, and their paths. It is a read-only PATH scan: it only checks whether each binary exists and never runs it (no --version, no execution), writes nothing, and touches no user data.

  • New route module (28th) server/lib/routes/cli-detect.mjs — GET /api/cli-detect. Detection resolves the path of a binary from a fixed 7-entry allowlist across process.env.PATH (Windows .cmd/.exe/.bat shims; POSIX execute-bit); a hostile file on PATH can never be executed by this route.
  • New “AI CLI tools” tab in public/js/views/config.js (lazy-loaded, deep-linkable via #/config?tab=cli). Tests: tests/cli-detect-routes.test.mjs (PATH resolve with a stub exec that must never run, detect shape, endpoint). 8 new i18n keys ×16 (cli.* + config.tabCli). Help §2 extended in place.

New: server/lib/routes/cli-detect.mjs.

[1.102.0] — 2026-07-05

“Ask the docs” — a grounded chat over the in-app help guide. A new Ask the docs 💬 page (sidebar, under Help): type a how-to question like “How do I scan job portals?” and get an answer drawn only from the app’s own help guide in your language — it shows which sections it used and never reads your CV, profile, or job search. It is about how to use the app, not about you. With an LLM key it answers live; with no key it hands you a ready-to-run prompt already filled with the relevant help sections.

  • New route module (27th) server/lib/routes/docs-assistant.mjs — POST /api/docs-assistant/ask. Dependency-free retrieval: the help doc for your locale (docs/help/<lang>.md) is split into its ## sections and scored by keyword overlap with your question; the top few are inlined and the model must answer from them or say the guide doesn’t cover it (no invented features/routes). Shared provider cascade, manual fallback, rate-limited, no writes, reads no user data.
  • New view public/js/views/docs-assistant.js (a chat under the Help nav). Tests: tests/docs-assistant-routes.test.mjs (section split/rank, grounded prompt, manual seed, empty→400). 14 new i18n keys ×16 (docs.* + nav.docsAssistant). Help §1 extended in place.

New: server/lib/routes/docs-assistant.mjs; public/js/views/docs-assistant.js.

[1.101.0] — 2026-07-05

CV Studio: tailor your résumé + write a cover letter for a specific job, gated by a recruiter checklist. A new Tailor to a job card on #/cv-studio: paste a job description (and, optionally, a target role/headline) and CV Studio produces a résumé tailored to that posting plus a matching cover letter, then runs both through a checklist gate before handing them over — errors block (they’re fixed before you see the result), warns advise. The mechanic is distilled from career-coaching practice into generic rules — recruiter reads in seconds, so relevant experience goes to the top, the headline matches the vacancy’s role, results carry specific numbers, and the cover letter stays a short teaser with a single “requirement ↔ your matching fact” bridge. It is grounded only in your own CV, profile, and two-pager and never fabricates — no hardcoded companies, roles, or history.

  • New endpoint POST /api/cv-studio/tailor (extends the existing cv-studio route module — no 27th module): buildTailorPrompt + a generic TAILOR_INSTRUCTIONS gate, grounded in bundleProjectContext (CV + profile + two-pager), shared provider cascade, manual-prompt fallback with no key, rate-limited, no file writes. Result exports as Markdown / PDF / DOCX via the shared report-export.js bar.
  • Tests: +3 in tests/cv-studio-routes.test.mjs (prompt is generic — gate + bridge + no-fabrication present, no hardcoded employer; manual mode seeded from the candidate materials; too-short JD → 400). 10 new i18n keys ×16 (cvs.tailor*). Generic reference docs/prompts/resume-cover.md. Help §24 extended in place.

New: docs/prompts/resume-cover.md.

[1.100.0] — 2026-07-05

Two-pager: AI auto-fill from your CV + Preview + export to PDF/DOCX/Markdown. The two-pager (#/two-pager) captures what you actually want from your next role, but you had to draft each field by hand or copy a prompt into another tool. Now the ✨ AI fill assistant runs live against your configured provider — it reads only your CV + profile (via bundleProjectContext, nothing invented), drafts every field (who I am / loves / must-haves / hates / deal-breakers / non-negotiables / target environment) and populates the form for you to review, edit, and Save. With no API key it falls back to the copy-a-prompt modal exactly as before. A new 👁 Preview & export button renders the two-pager as a formatted document with a Download .md / Save as PDF / Save as DOCX / Copy bar.

  • Dependency-free .docx export. New server/lib/docx.mjs emits a minimal-but-valid Office Open XML .docx (a DEFLATE ZIP of the four OOXML parts, CRC-32 per entry) — no new runtime dependency (deps stay express + js-yaml). New route POST /api/export/docx (server/lib/routes/export.mjs, the 26th route module; stateless, bounded 200 KB, no writes / no LLM / no URL fetch). Wired into the shared public/js/lib/report-export.js, so the market report, career plan, and career orientation reports gain DOCX export too.
  • Two-pager live auto-fill uses the shared provider cascade (runActiveProvider / providerAvailable); the returned YAML is parsed and coerced back into the bounded two-pager shape (parseYamlFields + normalizeTwoPager) — unknown keys dropped, arrays/strings capped. Manual fallback preserved.
  • Tests: tests/export-routes.test.mjs (valid ZIP/OOXML output, empty-input 400, 4-part package, XML-escaping, YAML-fence parsing + null-on-garbage). 4 new i18n keys ×16 (export.saveDocx, twoPager.preview, twoPager.aiFilling, twoPager.aiFilled).

New: server/lib/docx.mjs; server/lib/routes/export.mjs.

[1.99.0] — 2026-07-05

Portals health page (#/portals). The scanner watches a set of companies in portals.yml (tracked_companies:), and an ATS slug can quietly break — a company renames its board or moves off Greenhouse — after which that employer silently vanishes from every future scan with no error. The new Portals page lists every watched company (provider + enabled state) and, on Check portal health, HEAD/GET-probes each careers_url through the DNS-pinned safeGet (SSRF-safe, chunked concurrency) and flags the dead ones (a 404 = silently dropped). Read-only — it never writes portals.yml.

  • New route server/lib/routes/portals.mjs (POST /api/portals/health; the company list reuses the existing GET /api/portals) + view public/js/views/portals.js, under the Sourcing nav group. #/portals was promoted from a config alias to a real registered view (router alias removed). Suite tests/portals-routes.test.mjs + updated tests/router.test.mjs. 14 new i18n keys ×16.
  • Bug-reporter hardening (v1.98.0 follow-up, per review): logbuf.js’s fetch wrapper now has a .catch so network-layer failures (offline/DNS/abort — the failures a bug reporter most wants) reach the ring buffer; bug-report.js’s scrubber now redacts bare/unlabelled provider keys (sk-ant-, sk-, ghp_, xoxb-, AIza…), not only labelled ones. +3 scrub assertions.

New: server/lib/routes/portals.mjs; public/js/views/portals.js.

[1.98.0] — 2026-07-05

In-app bug reporter (parent career-ops web-v0.2.0 parity). The one substantial feature the parent’s experimental web frontend had that we lacked. A 🐞 Report a bug button in the notifications drawer opens a preview-then-confirm modal that gathers a privacy-floored diagnostic snapshot — app + parent version, your current screen, browser, viewport, a checks OK / FAIL summary from /api/health, and the last 20 errors captured by a new client-side ring buffer (console.error, window.onerror, unhandled rejections, and failed /api/* responses — pathname + status only) — plus a deterministic dedupe fingerprint (co-web-<base36>). You review the exact Markdown, then it opens a pre-filled GitHub issue (or copies the report); nothing is auto-filed. The privacy floor is an invariant: never CV, profile, application answers, job URLs, report content, or API keys — home paths and secret-looking tokens are scrubbed on top.

  • New client libs public/js/lib/logbuf.js (error ring buffer, loads first so it captures from first paint) + public/js/lib/bug-report.js (window.BugReport — scrub / fingerprint / issueBody / issueUrl / openModal). No new server route — reuses /api/health. Wired into the notifications-drawer head.
  • Tests: tests/bug-report.test.mjs (ring-buffer cap, path/token scrub, deterministic + volatile-stable fingerprint, privacy-floor body, repo-targeted issue URL). 11 new i18n keys ×16 locales.

Assessment: our web-ui is otherwise ahead of the parent’s alpha web — we already ship WCAG 2.5.5 44×44 tap-targets, provider cost hints, 24 routes, and 16 locales — so the bug reporter closes the last meaningful gap.

New: public/js/lib/logbuf.js; public/js/lib/bug-report.js.

[1.97.1] — 2026-07-05

Review-driven hardening & documentation parity (follow-up to v1.97.0). A sweep of the AI-review logs surfaced real fixes:

  • fit-score.js (scan ◎ fit badge). salaryFloor() no longer promotes a sub-annual rate into a bogus annual floor — “at least 500 EUR/day”, “$80/hr”, “6000 monthly” now return null instead of a 500k/80k deal-breaker. Country matching is now whole-word (\b…\b) so “Germany” no longer matches the adjective “German” (nor “Nigeria” inside “Nigerian”) and fires a false must-have-elsewhere violation. +3 tests in tests/fit-score.test.mjs.
  • Documentation parity. Every localized README now advertises 16 locales consistently — the Help-row count/list (×13) and the Localization-section prose + “add the key to all N files” note (×8) were still on the pre-v1.85 counts (8/9). The in-app help §17 adapter count is corrected to 46 adapters — 41 English + 5 Russian across all 16 bundles.

No behaviour change beyond the fit-badge heuristic; no new routes, keys, or i18n additions.

[1.97.0] — 2026-07-05

Dassault Systèmes scanner source + a three-front quality sweep.

  • New scan source — Dassault Systèmes (parent career-ops parity, #1498). server/lib/sources/dassault.mjs + server/lib/portals/adapters/dassault.mjs mirror the parent’s zero-token Exalead “card search” provider (the public feed behind 3ds.com/careers/jobs). It’s a single global endpoint, so it’s provider-selected (provider: dassault) or auto-detected from a 3ds.com host, SSRF host-pinned to www.3ds.com with redirect:'error'. The XML is parsed without a DOM (per-<Hit> <Meta> maps), city/country pulled from the localized category string, and postings are kept only when their public URL is on *.3ds.com. The registry now ships 46 adapters (41 EN + 5 RU); ALL_ADAPTERS count, sorted-id and /api/scan/sources EN-set assertions bumped 40 → 41. Suite tests/sources-dassault.test.mjs (10 cases).
  • Ported parent robustness fixes. Avature parser now tolerates two live tenant markup variants (article--result with a position-index suffix + a classless JobDetail title anchor, #1541); Get on Board guards a 0/negative published_at (no more bogus 1970 dates); SuccessFactors caps the last page so it can’t overshoot MAX_JOBS (#1528).
  • Server audit fixes. safe-fetch no longer hangs on an over-cap response — the size-cap path now settles the promise directly instead of waiting for an 'end' event a destroyed stream never emits (fixes large-page /api/pipeline/preview + auto-pipeline fetches). SSE stream.* activity-logging is reachable again (the /api/stream/ check moved above the blanket “skip GET” guard).
  • SPA audit fixes. The #/stats tab switcher guards against an async render race — a slow tab’s result can no longer clobber a newer tab the user already switched to. The mock-interview and networking delete confirms now pass a proper title + body (no more empty-bodied dialog).
  • Translation fixes. Untranslated dictionary values corrected — Ukrainian config.modes* (Adaptive Framing / Exit Narrative / Location Policy), Russian eval.jdLbl (“Job Description”), Italian dash.quick.contactoSub (“referral” → “segnalazione”) — plus the English **16 locales** boilerplate localized in the ru/uk/ja/ko/zh-CN/zh-TW CHANGELOGs.

New: server/lib/sources/dassault.mjs; server/lib/portals/adapters/dassault.mjs.

[1.96.0] — 2026-07-04

Career orientation (Epic 27). A new #/orientation page answers “which directions actually fit me?” — the read you’d get from a vocational test, but inferred from your own CV and profile instead of a questionnaire. Click Generate profile and the model returns your best-fit career vectors (which of the eight archetypes — Functionalist, Administrator, Communicator, Specialist, Analyst, Innovator, Manager, Entrepreneur — fit, with evidence), a career-type leaning, recommended roles, professional strengths tied to your CV, working-style tendencies, and development recommendations. It is an AI reflection of how your CV reads — not a psychometric test: it never invents achievements and never reports numeric scores as if measured. Export it to Markdown or PDF; nothing is written to disk.

  • New route server/lib/routes/orientation.mjs (24th route module) — POST /api/orientation/generate builds the profile prompt from CV+profile+two-pager+memory via the shared provider cascade, with a copy-paste manual fallback and no file writes.
  • Reuses report-export.js for Markdown/PDF/copy, under the Growth nav group.
  • Tests: tests/orientation-routes.test.mjs (reflection framing / no fabricated scores, CV/profile-seeded manual mode). 7 new i18n keys ×16 locales, Help §28 ×16.

New: #/orientation; server/lib/routes/orientation.mjs.

Also in this release — review-driven fixes & hardening. A sweep of the AI-review logs across recent PRs surfaced several real defects, all fixed here:

  • Duplicate i18n keys removed (30 CodeQL js/duplicate-property alerts). The v1.94.0 statistics fan-out had inserted the stats.*/export.* block twice in five locale dicts (ar, ja, ko, zh-CN, zh-TW) — last-wins silently shadowed the first copy. Each doubled block is removed; snapshot regenerated; parity green.
  • cv-diagnostics.js dead code (CodeQL js/useless-comparison). The words === 0 length branch was unreachable after the words < 20 early-return guard — removed.
  • cv-privacy.js false redactions. PHONE_RE now skips date-like runs (a 2018-2022 year range, a 2026-07-04 ISO date) that its ≥7-digit guard would otherwise mask; ADDRESS_RE now requires a real address boundary (comma / ZIP / end-of-line) so a mid-sentence “…Full Stack Dev St building…” isn’t redacted. New tests cover both.
  • i18n-dict.fr.js shipped-bundle noise. Stripped the trailing English // gloss comment from every French entry — consistent with the other 15 locales.
  • AI-review workflow. ai-review.yml’s rubric now names all 16 locales (was a stale 8), and the reviewer diff is ordered code-first (server/, public/, tests/) so the 200 KB cap truncates bulky localized CHANGELOG/README churn rather than the security-relevant code it kept reporting as “not in the diff.”
  • Doc drift. Refreshed stale counts in docs/sdd/CONVENTIONS.md (test total, H3 parity number), CLAUDE.md, and .claude/PROJECT-CONTEXT.md.

[1.95.0] — 2026-07-04

Career plan (Epic 26). A new #/career-plan page turns your CV and profile into a concrete, personalized development plan. Pick a horizon (6/12/24 months) and an optional focus, and the model — reading your CV, profile, two-pager, and memory note — writes a starting-point snapshot, a strengths/growth SWOT, goals as SMART / OKR / WOOP, alternative trajectories, a hard/soft skill plan, a month-by-month roadmap, progress-tracking methods, pitfalls, and support moves. It plans forward from what your materials actually show and never invents facts about your history. Edit it inline, Save it to the user layer (config/career-plan.md), and export it to Markdown or PDF.

  • New route server/lib/routes/career-plan.mjs (23rd route module) — GET/PUT /api/career-plan (writes config/career-plan.md) + POST /api/career-plan/generate (shared provider cascade, manual fallback, no fabrication). PATHS.careerPlan.
  • Reuses the shared report-export.js (v1.94.0) for Markdown/PDF/copy, and a new Growth nav group.
  • Tests: tests/career-plan-routes.test.mjs (bounding, GET/PUT round-trip, horizon-aware CV/profile-seeded prompt). 20 new i18n keys ×16 locales, Help §27 ×16.

New: #/career-plan; server/lib/routes/career-plan.mjs; PATHS.careerPlan.

[1.94.0] — 2026-07-04

Statistics, reworked (Epic 25). The #/stats page is now a three-tab Statistics section, with real graphs and a lot more data. A new Market report tab asks the model for a salary & labour-market analysis of your target roles in a region and currency you choose — executive summary, salary by grade with P10/P25/P75/P90 percentiles, top employers, an in-demand skills table, benefits frequency, the office/hybrid/remote split, 12–24 month trends, and negotiation guidance. Every figure is labelled a directional estimate from the model’s knowledge, never presented as scraped data. A new My pipeline tab charts your own tracker: score distribution, status funnel, top companies and roles, applications over time, and conversion rates. The original target-role view (vacancy/salary by country + saved-snapshot trend) moves under a third tab, now with a currency selector and a postings-by-role overview.

  • Export any report to Markdown or PDF, or copy it — via the shared report-export.js helper (Markdown blob download; PDF through the existing inline-PDF runner).
  • New route server/lib/routes/market.mjs (22nd route module) — POST /api/stats/market builds a market-analysis prompt from your CV/profile (so it knows your target roles), region, and currency, runs it through the shared provider cascade, and falls back to a copy-paste prompt with no key. No file writes.
  • Tests: tests/market-routes.test.mjs (region/currency bounding, honesty-labelled prompt, CV/profile-seeded manual mode). 36 new i18n keys ×16 locales, Help §26 ×16.

New: #/stats reworked into tabs; server/lib/routes/market.mjs; public/js/lib/report-export.js.

[1.93.0] — 2026-07-04

Memory layer (Epic 24). A new #/memory page holds a short, editable “remember this about me” note that the assistant keeps in mind on every task:

  • One note, everywhere — because it’s inlined into bundleProjectContext, the note automatically reaches every AI request (evaluate, mock interview, networking, CV Studio) across all providers. Write it once; it steers everything.
  • Steering, not facts — it captures your preferences and how you like to work (tone, format, deal-breakers, cadence), never new factual claims about your experience — those still live only in your CV, profile, and two-pager. Saved to the user layer at config/memory.md, never overwritten by updates.
  • Suggest from your data — POST /api/memory/suggest mines your own application tracker for behavioural patterns and drafts bullets for you to review and edit. It reads your tracker; it never invents facts, and makes no live call.

New: server/lib/routes/memory.mjs (21st route module — GET/PUT /api/memory + POST /api/memory/suggest), public/js/views/memory.js, PATHS.memory, and a config/memory.md block added to bundleProjectContext. 11 new i18n keys across all 16 locales. Tests: tests/memory-routes.test.mjs.

[1.92.0] — 2026-07-04

CV Studio (Epic 21). A new #/cv-studio page gives your CV three honest, mostly-local tools:

  • Résumé diagnostics — a deterministic 0–100 score with per-check explanations (quantified impact, weak verbs, buzzwords, length, core sections, contact info). Pure client-side (window.CvDiagnostics) — no LLM, nothing fabricated, every finding explained so you decide what to change.
  • Privacy mask — redact PII (email, phone, links/handles, street address, and optionally your name → initials) before sharing your CV as a sample or screenshot. Runs entirely in the browser (window.CvPrivacy); it reports exactly what it redacted and never stores the original.
  • Make it human / voice match — paste a stiff line or paragraph and rewrite it in your voice, grounded server-side in voice-dna.md and writing-samples/. Hard guardrail: it may reorder, tighten, and re-voice, but never introduces a fact, metric, or achievement not already in the text. Runs live through the shared provider cascade, or hands back a copy-paste prompt with no key.

New: server/lib/routes/cv-studio.mjs (20th route module — POST /api/cv-studio/humanize), public/js/views/cv-studio.js, public/js/lib/cv-diagnostics.js, public/js/lib/cv-privacy.js, PATHS.voiceDna + PATHS.writingSamplesDir. 29 new i18n keys across all 16 locales. Tests: tests/cv-diagnostics.test.mjs, tests/cv-studio-routes.test.mjs. (Template gallery, Word export, and posting-PDF archive are tracked as follow-up CV Studio work.)

[1.91.0] — 2026-07-04

Networking & deep company research (Epic 16). A new #/networking page turns a company into an actionable plan to get an interview, grounded in your CV, profile, and two-pager:

  • Company dossier — a tight brief on what the company does, recent signals worth citing, and “why I fit” hooks drawn from your real background.
  • Who to contact — 3–5 target personas (hiring manager, in-house recruiter, a senior IC on the team, a warm/alumni connection) with a concrete LinkedIn search string to find each. It never fabricates real names.
  • Warmest intro path — the single most realistic warm route in for your background (shared employer/school/community, a second-degree path, or a high-signal cold DM) and why.
  • Outreach drafts — short, specific messages for the top personas, grounded in your real proof points.
  • Live or manual — runs live through the shared provider cascade with any key, or hands back a copy-paste prompt (honest fallback, nothing invented). Save plan persists a finished plan to the user layer (networking/net-{company}-{role}-{date}.md); the page lists, opens, and deletes saved plans.

New: server/lib/routes/networking.mjs (19th route module), public/js/views/networking.js, PATHS.networkingDir. Reuses the v1.90.0 server/lib/llm-dispatch.mjs cascade. 24 new i18n keys across all 16 locales. Tests: tests/networking-routes.test.mjs.

[1.90.0] — 2026-07-04

Mock Interview 2.0 (Epic 15). A new #/mock-interview page turns your CV, profile, two-pager, and story bank into a turn-by-turn interview rehearsal:

  • Conversational practice — set a target role (+ optional company / JD) and the interviewer opens with a focused question. Each answer you send gets a structured reply: Feedback (strengths + the STAR+R gap), a Score (N/5), and a Next question that probes the weakest part of your last answer. Grounded server-side in your real materials — it never fabricates experience you don’t have.
  • Story bank aware — interview-prep/story-bank.md is inlined into the prompt (same trust level as cv.md) so feedback can point you at your own best stories.
  • Live or manual — with a provider key the turn runs live through the shared provider cascade (Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models); with no key you get a ready-to-run copy-paste prompt (honest fallback, no fabricated answers).
  • Saved sessions — click Save transcript to persist a finished interview to the user layer (interview-prep/mock-{company}-{role}-{date}.md); the page lists, opens, and deletes saved sessions.

New: server/lib/routes/interview.mjs (18th route module), public/js/views/mock-interview.js, server/lib/llm-dispatch.mjs (shared provider cascade), PATHS.storyBank, bundleProjectContext({ extraFiles }). 30 new i18n keys across all 16 locales. Tests: tests/interview-routes.test.mjs.

[1.89.0] — 2026-07-04

Candidate market fit — the two-pager (Epic 14). A new #/two-pager page lets you capture what you actually want from your next role, modelled on the “Mnookin two-pager” from Never Search Alone:

  • Guided builder — a first-person “Who I am” narrative, a “Target environment” note, and five chip-list editors: loves, must-haves, hates, deal-breakers, and non-negotiables. Saved to the parent project’s user layer (config/two-pager.yml) via PUT /api/two-pager — never overwritten by system updates.
  • AI fill assistant (POST /api/two-pager/draft) — builds a ready-to-run Mnookin prompt with your CV + profile inlined, for you to run in any LLM and paste back. It only ever uses your own materials; nothing is fabricated.
  • Fit-to-what-you-want badge — each posting on #/scan now shows a ◎ N fit score (client-side, via window.FitScore) that matches the job’s work-type, country, salary floor, and relocation against your two-pager. Honest by design: when a posting gives no matchable signal, no badge is shown (never a made-up number). Deal-breaker violations weigh more than soft dislikes.
  • Feeds every evaluation — the saved two-pager is inlined into bundleProjectContext, so all downstream LLM evaluations blend your stated preferences with the CV-vs-JD match.

New: server/lib/routes/two-pager.mjs, public/js/views/two-pager.js, public/js/lib/fit-score.js, PATHS.twoPager. 27 new i18n keys across all 16 locales. Tests: tests/two-pager-routes.test.mjs, tests/fit-score.test.mjs.

[1.88.0] — 2026-07-04

Issue #29 polish — Scan i18n gaps + API hygiene.

  • Localized the last hardcoded Scan strings (roadmap v1.69.4): the source-summary pills (N new / M matching), the N new offers toasts, and the reloc badge now flow through t() — 4 new keys (scan.pillNew, scan.pillMatching, scan.newOffers, scan.relocBadge) across all 16 locales. Non-English users no longer see stray English in the core scan flow.
  • Disabled the X-Powered-By header (roadmap v1.69.5): app.disable('x-powered-by') in createApp() — the server no longer advertises Express. (The rest of that epic was already shipped: parentVersion strips its release-please comment, the light-mode theme toggle, modal-dismiss-on-route-change, and the Reports “Score” localization.)

Tests: tests/scan-i18n-gaps.test.mjs + an X-Powered-By-absence assertion in tests/security-headers.test.mjs.

[1.87.0] — 2026-07-04

4 new zero-auth scan providers (parent career-ops v1.16.0 parity). The scanner registry grows 41 → 45 adapters (40 EN + 5 RU) — all public, no-auth, host-pinned, redirect:'error' (SSRF-safe), each with a CI-isolated test:

  • Get on Board (getonbrd) — board-wide public JSON:API (LATAM/remote tech), provider-selected, paginated. server/lib/sources/getonbrd.mjs.
  • Amazon (amazon) — amazon.jobs public search JSON, host-detected or provider: amazon, offset-paginated. server/lib/sources/amazon.mjs.
  • Avature (avature) — per-tenant *.avature.net ATS, HTML-parsed, host-detected or provider: avature. server/lib/sources/avature.mjs.
  • SAP SuccessFactors (successfactors) — per-tenant RMK tile list (*.successfactors.eu/.com, jobs2web.com), HTML-parsed. server/lib/sources/successfactors.mjs.

Each ships a sources/<slug>.mjs (auto-discovered meta → #/scan dropdown) and a portals/adapters/<slug>.mjs in ALL_ADAPTERS (the two-registry rule) + tests/sources-<slug>.test.mjs. The ALL_ADAPTERS count + sorted-id and /api/scan/sources EN-set assertions bumped 36→40; GET /api/scan/sources now lists 45.

[1.86.0] — 2026-07-03

Statistics by target roles (#/stats) — market vacancy & salary stats for YOUR target roles. A new Analytics page reads your target roles from the profile (config/profile.yml → not hard-coded) and the latest scan’s postings, then shows, per role and country:

  • Vacancies by country and median salary by country (USD) — aggregated client-side (public/js/lib/role-stats.js, reusing window.Countries) from the sparse data the scanners already collect. Salaries in any currency are normalized to USD via an explicitly-approximate FX table, with a sample-size caveat — never fabricated.
  • Role & country filters and hand-rolled inline-SVG bar + trend charts (no new deps, CSP-safe — addEventListener only).
  • Save snapshot (POST /api/stats/snapshot) persists the current aggregate to data/role-stats.jsonl; the trend chart (GET /api/stats/trend) tracks vacancy counts over time — the “dynamics” view. Honest hybrid: snapshots come from local scan data, refreshed on demand.
  • Fully localized in all 16 locales (26 new i18n keys).

New: server/lib/routes/stats.mjs (16th route module), public/js/lib/role-stats.js, public/js/views/stats.js, PATHS.roleStats; tests role-stats.test.mjs (7) + stats-routes.test.mjs (5).

[1.85.0] — 2026-07-03

German, Italian & Turkish locales (parent career-ops v1.16.0 locale parity). The UI now ships in 16 languages — de 🇩🇪, it 🇮🇹 and tr 🇹🇷 join the existing 13.

  • Full UI translation — all 730 i18n keys translated in public/js/lib/locales/i18n-dict.{de,it,tr}.js; the language switcher lists Deutsch / Italiano / Türkçe and browser-language auto-detection recognises de/it/tr (public/js/lib/i18n.js).
  • In-app Help guide — docs/help/{de,it,tr}.md translated (full 19 H2 / 75 H3 structure), served by GET /api/help/:lang.
  • Docs — README.{de,it,tr}.md and CHANGELOG.{de,it,tr}.md added; the CHANGELOG locale-parity gate now covers 15 non-EN locales.
  • Prompt scaffolding — server/lib/prompts.mjs (LOCALE_NAMES + SCAFFOLD_STRINGS) localised for the three new locales, so LLM output follows the UI language.

All parity gates (i18n-locale-files, i18n-coverage, check-changelog-parity, lang-switcher-rtl) extended to the 16-locale set.

[1.84.0] — 2026-06-30

Re-apply cooldown + compensation in pipeline.md (parent career-ops v1.15.0 parity). Two scanner upgrades:

  • Re-apply cooldown (#1201): the EN scan now skips roles at companies you applied to recently, so results stay focused on NEW openings. Configure per-company windows in config/profile.yml under re_apply_windows: (last_apply_date, same_role_days, applied_to: [roles], optional cross_role_bucket); company matching is punctuation-insensitive + word-boundary (server/lib/cooldown.mjs). Off when the key is absent; the scan log shows Cooldown skipped: N.
  • Compensation in pipeline.md (#1017): scanned offers now persist their salary as an optional trailing column (url | <salary>) in data/pipeline.md. The URL stays the dedup key (the | comp column is stripped on read), the cell is sanitized (no row/column injection, formula-leading neutralized), and bare-URL pipelines stay backward-compatible.

Ships tests/cooldown.test.mjs + pipeline compensation tests. Source count unchanged at 41 (both are scan-logic upgrades, not new boards).

[1.83.0] — 2026-06-30

Repost / ghost-posting detector (parent career-ops v1.15.0 parity). A new 🔁 Reposted / ghost roles panel on #/scan flags company+role clusters that were re-listed under different URLs within a rolling 90-day window — a signal of stale pipelines and ghost postings. Backed by a fuzzy role-title matcher (server/lib/role-matcher.mjs) and a read-only detector (server/lib/detect-reposts.mjs) over data/scan-history.tsv, surfaced via GET /api/scan/reposts (window clamped 7–365 days; fail-soft on a malformed history). Also: /api/health parentVersion now reports just the semver — the release-please # x-release-please-version comment is stripped. Ships tests/detect-reposts.test.mjs. (Source count unchanged at 41 — reposts is an analysis feature, not a new board.)

[1.82.0] — 2026-06-30

NoDesk scan source (parent career-ops v1.15.0 parity). The board-wide NoDesk remote-jobs RSS feed is now a first-class scan source — add a provider: nodesk entry and it appears in the #/scan Source dropdown (41 adapters total: 36 EN + 5 RU). Host-pinned to nodesk.co with redirect:'error' (SSRF-safe); titles split on Role at Company (NoDesk has no location tag, so location stays empty); all rows remote. Ships a CI-isolated tests/sources-nodesk.test.mjs suite; full unit suite green at 1523.

[1.81.0] — 2026-06-29

Parent career-ops parity — 13 new job-board scan sources. Ports the latest provider batch from Fighter90/career-ops main into the in-process scanner. Board-wide public APIs (provider-selected): Arbeitnow, Himalayas, Jobicy, Landing.jobs, 4 Day Week, The Muse, The Hub, Jobspresso (RSS), and Hacker News “Who is hiring?” (Algolia two-step). Poland boards (host- or provider:-detected): JustJoin.it and NoFluffJobs (POST search). Per-tenant ATS (auto-detected from careers_url): Pinpoint (<slug>.pinpointhq.com/postings.json) and Rippling (ats.rippling.com/<slug> → api.rippling.com board). Every source is host-pinned with redirect:'error' (SSRF-safe) and selectable in the #/scan Source dropdown — the registry now ships 40 scanner adapters (35 EN + 5 RU). Adds 13 CI-isolated per-source test suites; full unit suite green at 1513.

[1.80.0] — 2026-06-28

Five scan upgrades (ideas from bracketouverte/job-crawler, reimplemented). (1) Teamtailor source — per-tenant <slug>.teamtailor.com career sites via their public /jobs.rss feed, auto-detected from careers_url (host-pinned + redirect:'error'); the registry now ships 27 adapters. (2) Source quarantine — a source that returns a permanent 404/410 is recorded in data/scan-quarantine.json and skipped on later scans (self-healing: retried after 14 days), killing the recurring dead-slug noise. (3) Max per source — an optional #/scan field caps how many jobs each board contributes (∞ by default). (4) Posted within — a client-side age filter (24h / 7d / 30d) on the results table. (5) Saved searches + ★ favorites — name & reuse filter sets and star jobs, persisted in localStorage with defensive validation (a corrupt cache resets cleanly); the results cache is reset before each scan and refilled live.

[1.79.0] — 2026-06-28

WeWorkRemotely scan source (parent career-ops v1.14.0 parity). The board-wide We Work Remotely remote-jobs RSS feed is now a first-class scan source — add a provider: weworkremotely entry and it appears in the #/scan Source dropdown (26 adapters total). Host-pinned to weworkremotely.com with redirect:'error' (SSRF-safe); titles split on Company: Role. Also: title_filter keywords are now trimmed before the length check, so a whitespace-only keyword can’t compile into a match-everything filter (parent #1261).

[1.78.2] — 2026-06-27

i18n + UX hardening (follow-ups to v1.78.1). The brand logo’s accessible name is now localized in all 13 languages (nav.logoHome). Global-search Enter while already on #/scan force-re-renders so the pre-filled term is never lost (same-route guard). health.title is now translated in Polish (Kondycja) and Danish (Systemtilstand) — previously left in English. Tests 1235 → 1238.

[1.78.1] — 2026-06-27

Scan UX fixes. The #/scan results table now auto-refreshes live during a scan and once more after it finishes — no manual reload. The top-bar global search shows an Enter hint and, on a non-URL query, jumps to #/scan with the search box pre-filled (was #/tracker). The brand logo now links to the dashboard (home).

[1.78.0] — 2026-06-27

Geography filter on the Scan page — filter job results by country, with flags. A new Country dropdown in #/scan lists every country detected across your scanned results (flag emoji + count), so you can keep only roles tied to a specific country — alongside the Remote/Hybrid/Onsite work-type filter, so you can search both country-bound and remote work. Backed by a new countries.js helper that maps a posting’s free-text location (country names, aliases, and ~100 major job-market cities) to an ISO country + flag; detection is conservative and never guesses (unmapped/remote locations stay under “All countries”).

[1.77.0] — 2026-06-27

Danish (Dansk) added as the 13th interface language. Full Danish UI translation, in-app Help guide (19 H2 / 75 H3), README, and CHANGELOG. Danish joins the flag language picker; the i18n machinery (assembler, audit, parity gates, snapshot) now spans 13 locales.

[1.76.0] — 2026-06-26

Parent career-ops v1.13.0 parity — six new job sources, scanner hardening, and an uncapped results table.

Added

  • Six per-tenant ATS sources — BambooHR, Breezy HR, Comeet, Personio, Recruitee, SolidJobs. They auto-detect from the careers_url host (Comeet needs the full api: careers-api URL) and each pins its host with an anchored regex + redirect:'error' (SSRF-safe). All selectable in the #/scan Source dropdown — the registry now ships 25 adapters (20 EN + 5 RU). Adds a fetchText helper for Personio’s XML feed.
  • trust_filter — optional, annotate-only trust scoring (0–100, level high/medium/low, flags) for each scanned posting. Sub-high rows get a language-neutral ⚠ badge in #/scan; nothing is ever dropped.
  • Arbeitsagentur remoteMatch + remoteMaxPages — config-driven remote detection: title (regex), filter (server-side homeoffice=nv_true + pagination), or off.

Changed

  • No scan result cap. The MAX_STORED_RESULTS display cap (2000) was removed — every matched posting is stored and the #/scan table pages through them (200/page). Large sweeps no longer lose their tail.
  • Title-filter robustness — short all-letter acronyms (COO, SDR…) now match on word boundaries (no more “COO” in “Coordinator”); malformed title_filter config can no longer crash a scan. Both the ATS and regional scanners.

Tests

  • +32 cases (1190 → 1222): sources-ats-providers, title-filter, arbeitsagentur-remote, trust-validator, and a rewritten scan-result-cap “no cap” guard.

[1.75.2] — 2026-06-19

docs: full documentation parity for the v1.75.0 scanner aggregators across all 12 locales. No code change — brings the user-facing docs in line with the seven sources that landed in v1.75.0:

  • Help guide (12 locales). §5 gains a content_filter block (description/snippet keyword gating, sibling of location_filter) and an aggregators note; §7 lists the seven new sources in the one-click-scan sweep and the full Source dropdown enumeration; §17’s adapter count is corrected from the stale “11 adapters” to “19 adapters — 14 English + 5 Russian”. No ##/### heading was added, so the gated 19 H2 / 75 H3 structure is unchanged.
  • README (9 full locales). New “Aggregator boards (v1.75.0)” bullet under the scan sources, plus the release badge bumped to v1.75.2. (The abbreviated pl/uk/ar READMEs have no per-source list and are intentionally untouched there.)
  • Reference docs. docs/portals-examples.md gains a copy-paste “Aggregator boards” section with accurate provider: / <provider>: config blocks for all seven; docs/PROJECT.md updated to 19 adapters; docs/sdd/CONVENTIONS.md documents the two-registry distinction (sources/registry.mjs for the dropdown vs portals/registry.mjs for fetching), the provider:-based aggregator selection threaded as opts.company, the scan-write sanitizer (scan-sanitize.mjs), and the v1.75.1 test count (1190).
  • QA. Added qa/QA-REGRESSION-PROMPT-v1.75.2-FULL.md — the full-surface release-gate driver, refreshed for the v1.75.x scan-aggregator cycle.

[1.75.1] — 2026-06-19

fix(scan): robustness polish on the v1.75.0 config-driven sources. Three small hardening fixes from the post-release review (no behavior change for a healthy scan):

  • Abort-aware pagination delays. The Glints (300 ms) and Jobstreet/SEEK (200 ms) inter-page courtesy pauses now resolve immediately when the scan’s AbortSignal fires, via a new delay(ms, signal) helper in server/lib/http-json.mjs, so a disconnected client can’t hold a paginating scan open for an extra pause.
  • Descriptive non-JSON error. fetchJson now wraps a non-JSON 2xx body (e.g. an HTML maintenance page served with status 200) as non-JSON 2xx response from <url> instead of surfacing a bare SyntaxError, so the scanner’s per-source error log names the misbehaving endpoint.
  • Stronger scan-write normalization. normalizeScanScalar now collapses the vertical tab, form feed, and the Unicode line/paragraph separators (\v \f U+2028 U+2029) in addition to \r \n \t — a strict superset, so no record/line separator a spreadsheet or viewer might honor survives into scan-history.tsv.

[1.75.0] — 2026-06-19

feat(scan): port parent career-ops v1.12.0 — seven new job sources, content filtering, and security/quality fixes. The web-ui runs its own in-process scanners (it does not shell out to the parent’s scan.mjs), so parent v1.12.0’s provider and scan changes do not flow through automatically — this release reimplements the applicable ones in the web-ui’s adapter contract.

  • Seven new scanner sources. Three board-wide remote aggregators — RemoteOK, Remotive, Working Nomads — drop into the auto-discovered server/lib/sources/*.mjs pattern (select with provider: remoteok / remotive / workingnomads). Four config-driven regional aggregators — IBM careers, Arbeitsagentur (German Federal Labor Agency), Glints (SE Asia), Jobstreet / SEEK — read a per-entry <provider>: config block; en-scanner now threads the resolved company entry through to every fetcher so they can read it. All seven appear in the #/scan source dropdown automatically.
  • content_filter (parent #974). Optional portals.yml block (positive / negative keyword lists) that gates a posting on its description/snippet text — mirrors location_filter semantics; postings without a description always pass. Wired into both EN and RU scanners.
  • Scan-write hardening (parent #1098). External feed metadata is now sanitized before it lands in data/scan-history.tsv and data/pipeline.md: control characters are collapsed (a company/title newline can no longer inject a TSV row) and a leading = + - @ is neutralized against spreadsheet formula injection.
  • Ashby secondaryLocations (parent #1073). The Ashby source now folds each secondary location’s region label plus postal addressLocality / addressCountry into the location string (deduped), so an EU-eligible role whose primary label reads e.g. “Canada” surfaces for the location_filter.
  • Evaluation report-shape validation (parent #819). /api/evaluate’s in-process providers (Anthropic / OpenAI / Qwen / OpenRouter / GitHub Models) now flag a malformed A–G / SCORE_SUMMARY report as a non-fatal warnings array; the Gemini eval path already inherits the guard from the parent’s gemini-eval.mjs.
  • docs: Antigravity CLI added to the supported-assistant lists across all 12 READMEs (maps to the Gemini provider).

Inherited for free from the git pull of the parent (web-ui shells out to these): Japanese CJK PDF font fallback (#1053), ATS-safe PDF fonts (#1074), LaTeX CJK guard (#1054), tracker/merge/followup/dashboard fixes, and the modes/zh Chinese modes (the web-ui lists modes dynamically).



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