Nhật ký thay đổi
GitHubMọi thay đổi đáng chú ý, theo từng bản phát hành. Cùng một tệp đi kèm repository và được dịch sang cả 19 ngôn ngữ.
[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/generatebuilds 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.jsfor 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-propertyalerts). The v1.94.0 statistics fan-out had inserted thestats.*/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.jsdead code (CodeQLjs/useless-comparison). Thewords === 0length branch was unreachable after thewords < 20early-return guard — removed.cv-privacy.jsfalse redactions.PHONE_REnow skips date-like runs (a2018-2022year range, a2026-07-04ISO date) that its ≥7-digit guard would otherwise mask;ADDRESS_REnow 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.jsshipped-bundle noise. Stripped the trailing English// glosscomment 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(writesconfig/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.jshelper (Markdown blob download; PDF through the existing inline-PDF runner). - New route
server/lib/routes/market.mjs(22nd route module) —POST /api/stats/marketbuilds 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/suggestmines 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.mdandwriting-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.mdis inlined into the prompt (same trust level ascv.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) viaPUT /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
#/scannow shows a◎ Nfit score (client-side, viawindow.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), theN new offerstoasts, and therelocbadge now flow throught()— 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-Byheader (roadmap v1.69.5):app.disable('x-powered-by')increateApp()— the server no longer advertises Express. (The rest of that epic was already shipped:parentVersionstrips 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.jobspublic search JSON, host-detected orprovider: amazon, offset-paginated.server/lib/sources/amazon.mjs. - Avature (
avature) — per-tenant*.avature.netATS, HTML-parsed, host-detected orprovider: 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, reusingwindow.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 —
addEventListeneronly). - Save snapshot (
POST /api/stats/snapshot) persists the current aggregate todata/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 recognisesde/it/tr(public/js/lib/i18n.js). - In-app Help guide —
docs/help/{de,it,tr}.mdtranslated (full 19 H2 / 75 H3 structure), served byGET /api/help/:lang. - Docs —
README.{de,it,tr}.mdandCHANGELOG.{de,it,tr}.mdadded; 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.ymlunderre_apply_windows:(last_apply_date,same_role_days,applied_to: [roles], optionalcross_role_bucket); company matching is punctuation-insensitive + word-boundary (server/lib/cooldown.mjs). Off when the key is absent; the scan log showsCooldown skipped: N. - Compensation in pipeline.md (#1017): scanned offers now persist their salary as an optional trailing column (
url | <salary>) indata/pipeline.md. The URL stays the dedup key (the| compcolumn 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_urlhost (Comeet needs the fullapi:careers-api URL) and each pins its host with an anchored regex +redirect:'error'(SSRF-safe). All selectable in the#/scanSource dropdown — the registry now ships 25 adapters (20 EN + 5 RU). Adds afetchTexthelper for Personio’s XML feed. trust_filter— optional, annotate-only trust scoring (0–100, level high/medium/low, flags) for each scanned posting. Sub-highrows get a language-neutral ⚠ badge in#/scan; nothing is ever dropped.- Arbeitsagentur
remoteMatch+remoteMaxPages— config-driven remote detection:title(regex),filter(server-sidehomeoffice=nv_true+ pagination), oroff.
Changed
- No scan result cap. The
MAX_STORED_RESULTSdisplay cap (2000) was removed — every matched posting is stored and the#/scantable 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_filterconfig 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 rewrittenscan-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_filterblock (description/snippet keyword gating, sibling oflocation_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.mdgains a copy-paste “Aggregator boards” section with accurateprovider:/<provider>:config blocks for all seven;docs/PROJECT.mdupdated to 19 adapters;docs/sdd/CONVENTIONS.mddocuments the two-registry distinction (sources/registry.mjsfor the dropdown vsportals/registry.mjsfor fetching), theprovider:-based aggregator selection threaded asopts.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
AbortSignalfires, via a newdelay(ms, signal)helper inserver/lib/http-json.mjs, so a disconnected client can’t hold a paginating scan open for an extra pause. - Descriptive non-JSON error.
fetchJsonnow wraps a non-JSON2xxbody (e.g. an HTML maintenance page served with status 200) asnon-JSON 2xx response from <url>instead of surfacing a bareSyntaxError, so the scanner’s per-source error log names the misbehaving endpoint. - Stronger scan-write normalization.
normalizeScanScalarnow 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 intoscan-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/*.mjspattern (select withprovider: 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#/scansource dropdown automatically. content_filter(parent #974). Optionalportals.ymlblock (positive/negativekeyword lists) that gates a posting on its description/snippet text — mirrorslocation_filtersemantics; 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.tsvanddata/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 postaladdressLocality/addressCountryinto the location string (deduped), so an EU-eligible role whose primary label reads e.g. “Canada” surfaces for thelocation_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_SUMMARYreport as a non-fatalwarningsarray; the Gemini eval path already inherits the guard from the parent’sgemini-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).
[1.74.3] — 2026-06-18
docs(parent-source): point the parent career-ops repo at the Fighter90/career-ops fork. The web-ui now references the maintainer’s fork as the parent project everywhere it is a live source: the bin/setup.sh installer’s CAREER_OPS_REPO clone default, every git clone / “sits on top of” / onboarding link across all 12 READMEs, and the agent docs (CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, docs/). Author credit to santifer (and the unofficial-UI disclaimer) is unchanged — only the source/clone URLs moved. tests/sh-files.test.mjs now asserts the installer clones the fork.
[1.74.2] — 2026-06-17
fix(health): surface GITHUB_MODELS_API_KEY as an optional check on #/health and in /api/status/providers. The v1.74.0 GitHub Models provider was configurable in #/config but had no row on the Health page and was missing from the keysConfigured provider surface. Added the optional check (same “set / unset (manual mode)” wording as the other five live-eval providers) and github (+ its GITHUB_MODELS_MODEL) to /api/status/providers, so the active-provider routing and Health page now reflect all six. tests/api.test.mjs health-row test extended to all six providers.
[1.74.1] — 2026-06-17
docs + test: README “Install an AI assistant” section; full branch coverage for the Gemini connector. Added an install/login table to the README — install links for Claude Code / Gemini CLI / Codex / Qwen Code / OpenCode / GitHub Copilot CLI + each one’s #/config provider mapping + “log in before continuing” (mirrors the career-ops.org/docs Quick Start; clarifies the web-ui is the standalone, no-CLI-needed alternative). New tests/gemini-connector.test.mjs (8 cases) covers every runGemini branch — no-key, success, API error, empty/blocked completion, malformed body, timeout, network error, hasGeminiKey — taking server/lib/gemini.mjs to 100% statements. Overall coverage 96% lines / 88% branch / 96% funcs. Suite 1126 → 1134.
[1.74.0] — 2026-06-17
feat(llm): GitHub Models (Copilot) as the 6th provider + canonical 6-assistant alignment. career-ops.org/docs lists six AI coding assistants — Claude Code, Gemini CLI, Codex, Qwen Code, OpenCode, GitHub Copilot CLI. The web-ui now supports all six: five map to existing live providers (Anthropic / Gemini / OpenAI / Qwen / OpenRouter), and GitHub Copilot CLI gains a dedicated GitHub Models connector — runGitHubModels (OpenAI-compatible; a GitHub PAT with the models scope), configurable in #/config (GITHUB_MODELS_API_KEY + GITHUB_MODELS_MODEL) and selectable via LLM_PROVIDER=github; 6th in the auto order. Help bundles + READMEs now list the canonical six (renamed Qwen CLI→Qwen Code; added Gemini CLI + GitHub Copilot CLI), and the README adds a full mode-reference + portal-adapter link table to career-ops.org/docs so every feature traces back to the parent. tests/llm-provider-context.test.mjs extends the fetch-boundary matrix to all six providers (cv.md + profile.yml inlined + artifact returned); new GITHUB_MODELS_* keys added to all 12 locale dictionaries. Suite 1125 → 1126.
[1.73.0] — 2026-06-17
feat(llm): generic Gemini connector + verified CV/profile context across all providers. Added server/lib/gemini.mjs (runGemini) — a zero-dependency Gemini generateContent client returning the same {markdown, usage, error} shape as the Anthropic / OpenAI-compatible clients. Fix: /api/mode/:slug and /api/deep previously piped their prompts through the oferta-only gemini-eval.mjs, so Gemini Run live returned an evaluation instead of the requested artifact (cover letter, outreach, brief). They now call runGemini with bundleProjectContext, so cv.md + config/profile.yml are inlined for Gemini exactly like every other provider — letters/briefs are detailed and personalized. New tests/llm-provider-context.test.mjs mocks each provider’s HTTP boundary and asserts all five (Anthropic / Gemini / OpenAI / Qwen / OpenRouter) inline cv.md + profile.yml and return the artifact (mode + deep + evaluate matrix, 9 cases). /api/evaluate keeps its oferta-tuned gemini-eval.mjs. Suite 1116 → 1125.
[1.72.0] — 2026-06-17
feat(modes): “Run live” now returns the final artifact directly (single-shot output contract). The parent modes/<slug>.md templates are written for interactive Claude Code sessions — several (cover, contacto, …) pause to ask clarifying questions before producing the result, which made the web-ui’s single-shot Run live emit a questionnaire instead of the artifact. buildModePrompt now wraps every mode in a non-interactive output contract: do the analysis (JD breakdown, company notes, ATS keywords, profile↔JD gaps, tone/angle choices) silently, pick sensible defaults from cv.md / config/profile.yml for anything the template would normally ask, and output only the final artifact — closed with a per-mode “output ONLY {the cover letter / outreach message / …}” reminder. So clicking Run live on #/cover now returns the cover letter itself; the same fix applies to every generic mode (cover, contacto, interview-prep, project, training, followup, patterns) in all 12 locales (the artifact is written in the UI language via the locale directive). Suite 1103 → 1116.
[1.71.2] — 2026-06-17
docs(i18n): publish the documentation-consistency pass. Every README “Translations of this guide” block now lists all 11 sibling languages (previously some omitted English/Français and self-linked), with the blank line before the section break restored. The full QA regression prompt is renamed to the current version, and the docs (CLAUDE.md, CONVENTIONS, LOCALIZATION, PROJECT-CONTEXT) are synced to the current version and test count (1103). No code or behavior change — docs only, so the help/UI translations and all features from 1.70.0–1.71.1 are unchanged.
[1.71.1] — 2026-06-17
fix(i18n): the in-app help guide is now fully translated in all 12 languages. Added docs/help/{pl,uk,ar}.md (each holding the gated 19 H2 / 75 H3 structure) so #/help serves a native bundle for Polish, Ukrainian, and Arabic instead of falling back to English — GET /api/help/{pl,uk,ar} now return their own locale. Wired into every help gate (help-ui, help.test, help-ru-config-section, canonical-docs-coverage). Also completed every 12-language translation list: the README “Translations of this guide” block (9 READMEs), the localized CHANGELOG “Translations:” headers (8 files), and refreshed stale doc counts (CONVENTIONS test count → 1100; help “fall back” notes → 12 translated bundles). Suite 1100 → 1103.
[1.71.0] — 2026-06-16
feat(cover): generate a cover-letter PDF straight from #/cover. The cover mode (added in v1.70.0) produces the letter text; the result now offers a Generate PDF button that renders it through the shared inline markdown→PDF pipeline (POST /api/stream/pdf/inline → generate-pdf.mjs), the same path interview-prep uses. You can now produce the letter and ship a PDF without leaving the SPA.
test/docs: v1.70.0 review hardening. Added CI-isolated coverage for the cover mode (allowlist + prompt assembly, via modes-endpoints.test.mjs), the flag <select> switcher + Arabic RTL (dirFor/<html dir>, new tests/lang-switcher-rtl.test.mjs), top.langLabel in every locale, the cover-letter PDF wiring (tests/cover-letter-pdf.test.mjs), and the prompts.mjs locale directive + scaffolding for fr/pl/uk/ar (tests/locale-scaffold.test.mjs — locking the latent French gap closed in v1.70.0). Refreshed three stale “all 8” → 12-locale / 9-help-bundle references in docs/sdd/CONVENTIONS.md. Refreshed the full-project QA regression prompt (qa/QA-REGRESSION-PROMPT-v1.71.0-FULL.md).
[1.70.0] — 2026-06-16
feat(i18n): three new UI languages — Polish (pl), Ukrainian (uk), and Arabic (ar, with full RTL) — bringing the SPA to 12 locales, matching every language in the parent career-ops README. Each new locale ships a complete 697-key dictionary (public/js/lib/locales/i18n-dict.{pl,uk,ar}.js), gated by the existing parity / coverage / no-latin-leak / no-personal-data suites. Arabic adds genuine right-to-left support: i18n.js sets <html dir="rtl"> for RTL locales and a scoped [dir="rtl"] block in app.css mirrors the chrome (sidebar, notifications drawer, markdown tables/blockquotes, inline spacing) — LTR locales are byte-for-byte unchanged. New top.langLabel key (×12) names the picker for screen readers.
feat(ui): flag-icon <select> language switcher replaces the wrapping button row. With 12 locales the old .lang-btn row wrapped to three lines in the sidebar; a native <select> (each option prefixed with a flag emoji) scales cleanly, is keyboard- and screen-reader-friendly out of the box, and stays CSP-safe (change handler via addEventListener, no inline JS). Flags degrade to region letters where the platform lacks flag glyphs, so the language label is always the load-bearing identifier.
feat(cover): port the parent’s cover-letter mode (career-ops v1.10.0 + v1.11.0 greeting) into the SPA. New #/cover page under the Application nav group, built on the generic mode runner: Job description + Company/Role + an optional salutation → a tailored letter generated from cv.md / modes/_profile.md. Added cover to the server MODE_ALLOWLIST and a cover.* i18n block (×12 locales).
chore(compat): track parent career-ops v1.11.0. Verified the read/write contract is intact — data/applications.md stays the markdown source of truth (the v1.11.0 SQLite tracker index is a derived cache), tracker columns are still header-mapped. parentVersion now reports 1.11.0.
fix(i18n): close a latent gap where French (added v1.61.0) was missing from server/lib/prompts.mjs LOCALE_NAMES and SCAFFOLD_STRINGS — French LLM calls silently defaulted to English output and English scaffolding. fr/pl/uk/ar are now all wired into the prompt-locale path.
Known follow-ups (tracked in
qa/v1.70-regression/): the in-app help guide (docs/help/) falls back to English for pl/uk/ar (the UI chrome itself is fully localized); the parent’s interactive interview onboarding, reverse-ATS discovery, and newer scan providers are not yet surfaced in the SPA.
[1.69.2] — 2026-06-12
fix(test): close a test-isolation leak that let npm test overwrite the user’s real config/profile.yml and data/scan-history.tsv. tests/critical-fixes.test.mjs statically imported prompts.mjs at the top of the file; prompts.mjs transitively imports paths.mjs, which resolves PROJECT_ROOT eagerly at module load (PATHS resolves once per process). Because that import ran before the before() hook set CAREER_OPS_ROOT to a temp dir, PATHS pinned the real parent — so the F-008 PUT /api/profile wrote the “Acceptance Test” fixture into the user’s real profile on every run (and similar writes escaped the temp root). Fix: load prompts.mjs via dynamic import() inside before(), after the env is set (the server was already loaded that way). New tests/test-root-isolation.test.mjs (2 cases) guards every isolation-needing test against statically importing a paths.mjs carrier (server/index.mjs, prompts.mjs, store.mjs, en-scanner.mjs, ru-scanner.mjs, paths.mjs). No production-code change. Suite 1084 → 1086.
[1.69.1] — 2026-06-12
fix(scan): raise the #/scan result display cap 500 → 2000 per region so large regional sweeps are no longer silently truncated. A real RU scan produced 1352 matching jobs, but only the first 500 were stored in data/last-scan.json and rendered in the results table — 852 relevant jobs were hidden (the 2000 scanned → ~600 shown symptom: 139 EN + 500 RU). Both server/lib/en-scanner.mjs and server/lib/ru-scanner.mjs now cap the stored filtered set at a shared, env-overridable constant MAX_STORED_RESULTS (default 2000, override via SCAN_MAX_RESULTS). This is display-only — appending to pipeline.md and scan-history.tsv already used the uncapped fresh (new-since-last-scan) set and was never truncated. New tests/scan-result-cap.test.mjs (3 cases) locks the default, the env override, and that neither scanner hard-codes slice(0, 500).
fix(health/ui): #/health check cards no longer overflow. A long check name/value (e.g. PROFILE CUSTOMIZED · still on template …, PLAYWRIGHT (PARENT NODE_MODULES)) collided with the right-hand Fix → button + status badge and spilled out of the card, because the generic .flex-between flex children default to min-width: auto and never shrink. The row is now tagged .health-check-row with scoped CSS: the left text shrinks + wraps (min-width: 0), the action group keeps its size (flex: 0 0 auto) and wraps below on narrow cards. New tests/health-card-overflow.test.mjs (2 cases). Suite 1079 → 1084.
[1.69.0] — 2026-06-12
feat(scan): P-14 plug-in scanner auto-discovery — drop a .mjs in server/lib/sources/ to register a new source. Pre-v1.69 the source list in server/lib/sources/registry.mjs was a static hand-maintained array — adding a new adapter required editing both <id>.mjs AND registry.mjs. Closes the partial half of the roadmap item P-14 (docs/ROADMAP.md). Now every *.mjs in server/lib/sources/ is auto-loaded at module boot; each adapter contributes its identity via a self-describing export const meta = { value, label, region, configKey? } block. The 12 shipped adapters (ashby / greenhouse / lever / rss / smartrecruiters / workable / workday + geekjob / getmatch / habr / hh / trudvsem) each grew a meta export; registry.mjs now uses readdirSync + dynamic import() resolved at module-eval via top-level await (Node 18+ ESM standard). The public API (SOURCES, SOURCES_BY_REGION, RU_CONFIG_KEYS, getRegionalSources) is unchanged — every existing import keeps working. Validation rejects malformed meta (missing value/label/region, RU without configKey, region outside 'en'|'ru') and logs a single console.warn per offending file so half-migrated branches stay diagnostic-friendly. The bundled registry.mjs is excluded from self-import. New tests/sources-registry-discovery.test.mjs adds 14 cases covering shipped-adapter coverage, drop-in adapter discovery, helper-module skip, malformed-meta rejection, self-import exclusion, missing-directory tolerance, and deterministic ordering. Suite 1065 → 1079.
[1.68.2] — 2026-06-07
fix(bin): npx / npm link CLI verbs were broken — resolve the bin path through symlinks. npm and npx expose career-ops-ui as a symlink under node_modules/.bin/, where the old dirname "${BASH_SOURCE[0]}" resolved to .bin instead of the package root — so npx career-ops-ui init ran node node_modules/scripts/init.mjs and crashed with MODULE_NOT_FOUND (local runs after npm install were unaffected, which hid the bug). Both bin/career-ops-ui.sh and bin/start.sh now canonicalize SCRIPT_DIR through the symlink chain (readlink loop + cd -P), so every verb works from the repo, via npm link, and via npx. Adds a regression lock in tests/sh-files.test.mjs that runs a verb through a .bin-style symlink. Suite 1065/1065.
[1.68.1] — 2026-05-29
fix(scan): per-source fetch timeout 10s → 60s. v1.67.1’s 10s fail-fast also cut off slow-but-alive Ashby boards that just needed more time. Raise the default to one minute so those return. Trade-off: a genuinely dead/hung source now holds a concurrency slot for the full 60s (slower worst-case scan), and the chronic hangers (Perplexity, Supabase, Resend, …) likely still time out — a per-source / lower-Ashby-concurrency fix would address those properly. Override with SCAN_FETCH_TIMEOUT_MS. Suite 1063/1063.
[1.68.0] — 2026-05-29
feat(scan): reworked the result-filter panel — labelled fields, an Apply button, an On-site option, and a working salary filter. Every filter on #/scan is now a labelled field (label above the control, not a placeholder): Search · Work type · Salary from · Salary to · Source · Scope. An explicit Apply button (plus Reset, and Enter in any field) re-runs the filter; an on-page hint explains how it works. The salary range now actually filters — once you set a from/to value, jobs whose listed pay falls outside the range and jobs with no listed salary at all are dropped (overlapping-range match; currency ignored). The Work type filter gains an On-site option alongside Remote / Hybrid / Relocation. New i18n keys ×9; salaryInRange made strict; suite 1063/1063.
[1.67.1] — 2026-05-29
fix(scan): per-source fetch timeout 30s → 10s (fail-fast). v1.67.0’s 30s raise recovered only ~half the slow Ashby boards; the rest (Perplexity, Supabase, Resend, DeepL, Ramp, …) hang regardless of the deadline, so a longer timeout just stalled every scan waiting on dead slots. 10s fails fast on the chronic hangers and keeps scans responsive. Override with SCAN_FETCH_TIMEOUT_MS. Suite 1060/1060.
[1.67.0] — 2026-05-29
feat(scan): salary range filter (from / to) on #/scan, plus a longer per-source fetch timeout. The results table gains two numeric inputs — salary from / to — beside the text and remote filters. Each row’s free-text salary (от 100 000 до 200 000 ₽, 120000-150000 USD, $120K–$150K, …) is parsed to a numeric range and matched with overlapping-range semantics; rows with no published salary are kept, so the filter narrows the list instead of gutting it (comparison is currency-agnostic — no FX conversion). Also raises the per-source scan fetch timeout 15s → 30s (override: SCAN_FETCH_TIMEOUT_MS) — Ashby’s includeCompensation payloads routinely took >15s under 8-way concurrency, so ~30 Ashby boards were timing out every scan. New window.Skills.parseSalaryRange/salaryInRange + i18n ×9; 13 new tests; suite 1060/1060.
[1.66.0] — 2026-05-28
feat(scan): RU sources now walk ALL result pages, not just the first. hh.ru, Habr Career and Trudvsem each only paged the first ~50 hits per query; they now follow pagination to the end — &page=N for hh.ru/Habr, offset/meta.total for Trudvsem — deduping across pages and stopping when a page adds nothing new (or at a 50-page safety cap). A query like “Backend разработчик” now returns the full result set instead of one page (e.g. hh.ru PHP 17 → 55+ across 3 pages; Trudvsem returns all 72). Per-page fetches keep the existing timeout + AbortSignal. 4 new tests; suite 1045/1045.
[1.65.0] — 2026-05-28
feat(scan): hh.ru is now scraped from its public website instead of the JSON API — works from any IP, no proxy. api.hh.ru started returning a bare 403 forbidden to every programmatic client regardless of IP (US, RU datacenter, RU residential, RU mobile) or User-Agent — an edge anti-bot block, not a documented API error. The website (hh.ru/search/vacancy) still serves full server-rendered results to any browser-like client, so the adapter now parses that HTML (like Habr Career). Removes the HH_PROXY env added in 1.64.0 and the undici dependency — no proxy, key, or User-Agent setup needed. Tests rewritten for the HTML parser; suite 1041/1041.
[1.64.0] — 2026-05-27
feat(scan): route the hh.ru request through a Russian proxy via HH_PROXY. hh.ru geo-blocks its API by IP, not User-Agent — so HH_USER_AGENT alone never lifted a 403 from a non-RU exit node. Set HH_PROXY to a Russian HTTP/HTTPS proxy URL (e.g. http://user:pass@ru-host:port) and only the hh.ru request is routed through it; every other source keeps its direct connection. Built on undici’s ProxyAgent (new runtime dep); the dispatcher is omitted entirely when HH_PROXY is unset, and a changed value is picked up on restart. 3 new tests; suite 1041/1041.
[1.63.2] — 2026-05-27
feat(scan): live % progress + per-source detail in the #/scan console. The progress bar is now determinate — scanners emit progress events (EN: per company; RU: per query) forwarded over SSE, and the bar fills with a live “Scanning… NN%” label (animated indeterminate stripe only until the first event). Each source’s first failure (timeout / 403 / network) is now logged to the console in detail (e.g. ⚠ hh timed out: …), then repeats are suppressed. 1 new test; suite 1040/1040.
[1.63.1] — 2026-05-27
style(scan): make the #/scan progress bar more prominent. Wrapped the in-flight indicator with a visible “Scanning…” caption and bumped the bar to 8px (was a thin 4px) so it’s clearly noticeable while a scan runs. No behavior change.
[1.63.0] — 2026-05-27
feat(scan): per-request fetch timeout + #/scan progress bar. Scanner source requests had no deadline, so a stalled upstream (e.g. api.hh.ru from a blocked IP) could hang the whole scan. A new server/lib/fetch-timeout.mjs wraps the scanners’ fetchImpl (makeTimeoutFetch, default 15s, override via SCAN_FETCH_TIMEOUT_MS) so every source request has a hard deadline — a timed-out source is recorded as a non-fatal error and the scan continues (Habr keeps working even when hh.ru is unreachable). The #/scan page also shows an indeterminate progress bar while a scan is in flight (localized scan.progress across all 9 locales). 7 new tests; suite 1039/1039.
[1.62.3] — 2026-05-27
docs: clarify install (career-ops-ui runs inside career-ops/web-ui/) + init troubleshooting, across all 9 locales. Rewrote the install section into Option 1 (one curl) / Option 2 (clone the UI inside an existing career-ops project as web-ui) + CLI verbs + provider setup + a Troubleshooting init block — addressing the common confusion of cloning career-ops-ui standalone and running init without the parent project. Also added the nested-layout note to the /help §1 Setup section and summarized the whole v1.62.* line in the README highlight. Docs-only; no code change.
[1.62.2] — 2026-05-27
fix(help): #/help filter is now full-text (finds H3 subsections like RSS). The help-page search/TOC filter previously matched only H2 section titles, so the v1.62.x RSS docs (an H3 under §5 Portals & sources) weren’t findable. Each section’s body text is now indexed into the filter, so searching e.g. “RSS” surfaces §5. Pure client-side; no API change.
[1.62.1] — 2026-05-27
feat(scan): RSS in the source filter + RSS location fix. The #/scan source-filter dropdown now lists RSS (added to server/lib/sources/registry.mjs + the SPA fallback list), so RSS-board results (LaraJobs, WeWorkRemotely, …) are filterable like any ATS source. The RSS adapter no longer maps the feed <category> tag onto location — non-location tags there made location_filter wrongly drop remote roles; location is now empty so feeds pass location filtering. Scan-button tooltips/labels and the source-list i18n string (dash.quick.scanSub) updated across all 9 locales to include Workable / SmartRecruiters / Workday / RSS. i18n snapshot + scan-sources endpoint test (6 → 7 EN sources) updated.
[1.62.0] — 2026-05-27
feat(scan): generic RSS adapter for non-ATS job boards. A new rss portal adapter (server/lib/portals/adapters/rss.mjs + server/lib/sources/rss.mjs) lets the scanner pull jobs from any RSS feed — LaraJobs, WeWorkRemotely, RemoteOK, golangprojects and other boards outside Greenhouse/Ashby/Lever. Zero new dependencies: feed parsing is regex-based with CDATA + HTML-entity support (titles/companies are tag-stripped, astral code points decoded safely). Activated per-company via provider: rss / rss: / feed_url: in portals.yml, so it never intercepts ATS-matched companies. ALL_ADAPTERS grows 6 → 7. 29 new tests; documented across all 9 README locales.
[1.61.1] — 2026-05-22
fix(i18n): localize the theme-toggle title + aria-label across all 9 locales (MINOR-001). The dark/light theme button (#theme-toggle) hardcoded title="Toggle theme" and aria-label="Toggle theme" in index.html — the tooltip and screen-reader text never translated, on any locale. A new top.themeToggle key + a data-i18n-title handler in applyI18n() (mirroring the v1.58.15 search-aria-label fix) localize both attributes on boot and on every language switch. Locked by tests/playwright-theme-toggle-i18n.mjs (9 locales + runtime-switch) and two static guards in tests/qa-report-fixes.test.mjs. The lone LOW finding from the v1.61.0 French sign-off. (MINOR-001)
[1.61.0] — 2026-05-22
feat(i18n): add French as the 9th UI language. New per-locale dictionary public/js/lib/locales/i18n-dict.fr.js (window.__I18N_DICT_FR), at full 668-key parity with English; new help bundle docs/help/fr.md (19 H2 / 73 H3, exact structural parity with en). fr is registered in the language switcher and browser auto-detect (i18n.js), the assembler (i18n-dict.js), index.html (a <script> tag before the assembler), the test snapshot, and every test locale list. The initial translation table came from PR #9 (community contribution). No logic change: t() and every view are unchanged. 1001 / 1001 unit tests; the Playwright locale-sweep grows to 9 subtests. (FR-LOCALE)
[1.60.0] — 2026-05-22
refactor(i18n): split the 8-column megafile into per-locale files (I18N-SPLIT). The translation dictionary lived in one 849-line public/js/lib/i18n-dict.js (key → { en, es, … } across 8 locales). It is now one file per locale — public/js/lib/locales/i18n-dict.{en,es,pt-BR,ko,ja,ru,zh-CN,zh-TW}.js (each assigns window.__I18N_DICT_<LANG> = { key: string }) plus a shared i18n-dict.aliases.js — so a translator edits a single language in isolation (the i18next / OpenWA layout). i18n-dict.js is now a small assembler that merges the per-locale tables back into the exact same window.__I18N_DICT shape, so t(), every view, and every call-site are byte-for-byte unchanged. Everything loads synchronously via <script src> — no build step, no runtime fetch (both hard rules preserved). A captured snapshot proves the migration is lossless (assembled dict ≡ pre-split dict, 678 keys). Tooling + tests made split-aware via a shared tests/helpers/i18n-vm.mjs: tools/i18n-audit.mjs, the CI inline coverage check (which had silently become a no-op against the post-v1.23 split — it now actually loads the dictionary), and ~25 dictionary tests. New tests/i18n-locale-files.test.mjs (snapshot equality · per-locale key parity · alias integrity · index.html load order) and tests/playwright-locale-sweep.mjs (every page × 8 locales renders + localizes in real Chromium). 994 → 1000 unit · 62 → 70 Playwright. No user-facing behaviour change. (I18N-SPLIT)
[1.59.13] — 2026-05-21
fix(i18n): collapse TRUE duplicate keys via @alias + final personal-data purge (pre-fr, follow-up to v1.59.12). Two parts. (1) Personal data: swept the WHOLE repo, not just the dict — the maintainer’s real name was used as incidental test-fixture data (tests/health-doctor-unify.test.mjs, tests/llm-output.test.mjs) and in archived QA reports; all replaced with neutral examples (Jane Doe). LICENSE copyright + package.json author switched from the real legal name to the public Fighter90 handle. Repo now greps clean for the real name/email everywhere. (2) Duplicate keys (I18N-CL3, properly): added an @alias mechanism to i18n.t() — a key { '@alias': 'x.y' } resolves to a canonical key. Collapsed the 10 keys that are byte-identical across all 8 locales (nav.help/dash.quick.helpCta → help.title; nav.cv → cv.title; nav.health/dash.quick.healthCta → health.title; nav.reports/dash.reports/dash.quick.reportsCta → rep.title; nav.apply → apply.title; nav.interviewPrep → interviewPrep.title). The contributor’s own nav.config/config.title example is deliberately NOT aliased — they diverge in Spanish (short sidebar Configuración vs full title Configuración de la aplicación); aliasing would change a rendered label. Coverage test, CI inline check, and tools/i18n-audit.mjs are all alias-aware (skip alias keys for parity, enforce every target exists, resolve before dup-counting). New tests/i18n-alias.test.mjs (3 cases) locks integrity + behaviour. 991 → 994 unit. Reduces the French translator’s surface — true dupes are now translated once. (I18N-CL3, personal-data-sweep)
[1.59.12] — 2026-05-21
fix(i18n): i18n-dict.js cleanup — pre-fr ship (I18N-CL1, I18N-CL2, I18N-CL4). External French-locale contributor audit. I18N-CL1 (privacy): training.coursePh carried a vendor-specific cert name across all 8 locales — replaced with a neutral generic placeholder (Cloud architecture certification + native equivalents). I18N-CL2 (hygiene): the followup.lastPh dict fallback was a frozen 2026-04-21 literal — replaced with a localized format hint (YYYY-MM-DD / AAAA-MM-DD / ГГГГ-ММ-ДД); the live dynamic today − 14d placeholder (U-3, v1.58.23) in mode-page.js was already working and is now lock-tested. I18N-CL4 (audit): new tools/i18n-audit.mjs + npm run audit:i18n, wired into CI — hard-fails on personal data, locale-parity gaps, empty values, and bare-calendar-date placeholders. I18N-CL3 (decision): the ~50 duplicate-value key groups (e.g. nav.scan / scan.btnRun / scan.col.company) are intentional — distinct UI roles that non-English locales translate differently; deduping them would remove i18n flexibility, so they are reported as informational warnings, not failures (documented in the dict header). 3 new tests (988 → 991 unit). Unblocks the upcoming French locale PR. (I18N-CL1, I18N-CL2, I18N-CL4)
[1.59.11] — 2026-05-21
fix(test): v1.59.11 — e2e-comprehensive 12-case CI failure root-caused and closed. Twelve cases (Pipeline · Activity · Health · 7 Mode pages · 404 · Profile) had been failing on every CI run going back to v1.58.x because page.goto(baseUrl + '/#/X') is a no-op for hash-only URL changes in Playwright. Once the CV-save step set the hash to #/cv, every subsequent goto to a hash route silently kept the page on #/cv — Activity/Health/Mode/Profile selectors never matched, and the 4 ‘pass’ results in between were vacuous (their assertions found CV’s elements). Fix in tests/e2e-comprehensive.mjs: new goRoute(hash) helper that bounces through about:blank before each goto, forcing a real navigation and SPA re-bootstrap. All 17 page.goto(${baseUrl}/#/…) call sites replaced. Diagnostic instrumentation (E2E_DUMP_ON_FAIL=1 env var) added for future investigations. Result: 23 / 23 cases green locally · 988 / 988 unit · 20 / 20 smoke e2e. (e2e-harness-r1)
[1.59.10] — 2026-05-21
fix(api): NEW-F1-sub-r1 (v1.59.10) — un-encoded ../ traversal guard hoisted above all /api route registrations. v1.59.8 added a req.originalUrl.includes('..') middleware, but it was placed AFTER app.all('/api/*', JSON-404) AND AFTER all route handlers — by which time Express had already normalised the URL (collapsing .. segments). /api/jds/../../../etc/passwd was rewritten to /etc/passwd and fell through to the SPA static handler (200 OK on index.html). v1.59.10 hoists the guard to the TOP of server/index.mjs createApp() (above every register*Routes(app) call) so it inspects the verbatim request URL before any normalisation. Pattern: /^\/api(\/|$)/.test(req.originalUrl) && /\.\.\//.test(req.originalUrl). New tests/api-path-traversal.test.mjs — 6 cases driving the real http module (Node’s fetch normalises .. client-side, so we drop to http.request with a verbatim path). Unknown /api/* paths still return {error: 'unknown api'} from the existing app.all fallback. 982 → 988 unit. (NEW-F1-sub-r1)
[1.59.9] — 2026-05-21
fix(ux): UX-A5-r4 (v1.59.9) — Help TOC scroll-spy debug marker + behavioural lock-test. Sixth-cycle closure: previous five attempts (v1.58.45 / v1.58.52 / v1.59.0 / v1.59.3 / v1.59.8) all shipped with passing static tests but the bug stayed open because the tests asserted source-shape, not behaviour. v1.59.9 fixes the gap: (1) <body data-toc-spy="active"> debug marker — single selector any tester can use to answer “is the spy alive?” without needing to scroll; (2) synchronous initial paint at mount tail covers the router-pre-paints-view case; (3) double-rAF re-compute covers the route-handler-returns-before-router-appends case; (4) resize listener subscribed so viewport-flip mid-scroll re-paints; (5) cleanup removes BOTH listeners and the marker on hashchange. public/js/views/help.js algorithm: linear scan with else break; short-circuit (O(active-index) per scroll event). New tests/help-toc-spy-behavior.test.mjs runs the algorithm against 6 synthetic-geometry scenarios + 1 algorithm-parity check against help.js source — the test fails if the algorithm regresses, before any browser run. 973 → 982 unit. (UX-A5-r4)
[1.59.8] — 2026-05-21
fix(ux+api): v1.59.8 — UX-A5-r3 + NEW-F1-sub (HIGH + LOW bundled per FIX-PROMPT-FINAL-CONSOLIDATED). Doctrine-exception release: the 2026-05-20 FINAL REGRESSION-v1.59.7 report explicitly recommended bundling these two. UX-A5-r3 (HIGH) — after 4 ship attempts with IntersectionObserver all failed live verification, public/js/views/help.js replaces the IO entirely with a plain scroll listener. Root cause: with rootMargin -20%/-55% the trigger band ended at 45% of viewport, but scrollIntoView({block:‘center’}) lands the target at 50% — JUST below the band end. The new scroll listener probes absolute heading positions every rAF, computes the heading whose top is at-or-above 30% of viewport, applies .toc-current — no band, no race, no mount-order gotcha. NEW-F1-sub (LOW) — server/index.mjs adds a late middleware that inspects req.originalUrl (not normalised req.url) and bounces any /api-prefixed request whose raw URL contains .. as 404 JSON {error: 'invalid path'}. New UX-A5-r3 lock-test + NEW-F1-sub static + behavioural guards in tests/. 971 → 973 unit. (UX-A5-r3 · NEW-F1-sub)
[1.59.7] — 2026-05-20
fix(api): NEW-D3-cache (v1.59.7) — GET /api/cv sends Cache-Control: no-store. Matches the SPA-shell policy (W-001 / v1.54.7). cv.md is the user-edited primary artifact; a stale browser cache or intermediary proxy could surface yesterday’s text and trick the editor into saving over the live version. No ETag dance — the file is small and the GET is rare. New test in tests/api-404-json.test.mjs (the suite is the in-process server harness; named for its first test). 970 → 971 unit. (NEW-D3-cache)
[1.59.6] — 2026-05-20
feat(a11y): NEW-D2-motion (v1.59.6) — honour prefers-reduced-motion: reduce. New @media (prefers-reduced-motion: reduce) block in public/css/app.css neutralizes animation-duration + transition-duration to 0.01 ms and forces scroll-behavior: auto (so the Help TOC click-to-scroll lands instantly). WCAG 2.3.3 (AAA) — users with vestibular disorders, motion sensitivity, or simply OS-level reduced-motion preference now get an animation-free UI. Static guard in tests/qa-report-fixes.test.mjs. 969 → 970 unit. (NEW-D2-motion)
[1.59.5] — 2026-05-20
fix(api): NEW-F1 (v1.59.5) — unknown /api/* returns JSON 404 on every verb. Pre-fix app.get("/api/*", …) was GET-only; POST / PUT / DELETE to an unknown api path fell through to the SPA catch-all and returned an HTML 404, breaking SPA clients that do try { res.json() } catch {}. Changed to app.all("/api/*", …) in server/index.mjs. New test suite tests/api-404-json.test.mjs probes GET / POST / PUT / DELETE on /api/no-such-endpoint plus an unknown :name under a real handler — 5 new tests, all green. 964 → 969 unit. (NEW-F1)
[1.59.4] — 2026-05-20
fix(ui): NEW-OR1 (v1.59.4) — #/config API-keys summary chip is race-safe. User-reported transient Keys: 0 / 5 flash during Save flows. The previous refreshApiSummary() cleared <span> children before awaiting the network fetch; a concurrent providers-changed event could observe the empty state. New implementation in public/js/views/config.js:\n\n1. Build new <span> nodes before any DOM mutation.\n2. Atomic swap via apiSummary.replaceChildren(newActive, newCount) — chip never blanks mid-update.\n3. inFlight token counter drops stale resolves when a newer refresh starts.\n4. lastGoodSt cache preserves the last known state when fetch returns null (network blip, server reload), so the chip never collapses to 0 / 5 on a transient empty response.\n\nLock-test in tests/qa-report-fixes.test.mjs asserts all four invariants. 963 → 964 unit. (NEW-OR1)
[1.59.3] — 2026-05-20
fix(ux): UX-A5-r2 (v1.59.3) — Help TOC scroll-spy harden (third-pass fix). Two real issues persisted after v1.58.52: (1) rootMargin: "-30% 0% -60% 0%" left only a 10% visible band so fast scroll could skip the trigger zone entirely and no IntersectionObserver entry ever fired; (2) no initial-state computation, so a freshly-loaded #/help with zero scroll showed zero highlights even though section 1 was visibly active. public/js/views/help.js: widened rootMargin to -20% 0% -55% 0% (25% band), made root: null explicit, extracted applyCurrent(id) so the same code path serves both observer callbacks and the initial-state pass, and on mount picks the heading closest to 20% from top of viewport. 962 → 963 unit. (UX-A5-r2)
[1.59.2] — 2026-05-20
fix(ui): v1.59.2 — UX-A9 / UX-A3 chip count + overlap (post-v1.59.1 hotfix). Three real bugs in the v1.58.62 / v1.58.55 chips, user-reported on the staging surface:\n\n1. Always Keys: 0 / 5. /api/status/providers returns keysConfigured as an array of provider names; pre-fix typeof === number was always false. Now uses Array.isArray(st.keysConfigured) ? st.keysConfigured.length : 0.\n2. Lowercase provider name. Server returns anthropic (not claude — LLM_PROVIDER env value vs resolved provider name); the NAME map was keyed by claude: so it always fell through to the lowercase fallback. Re-keyed {anthropic, gemini, openai, qwen, openrouter} in both public/js/views/config.js and public/js/views/dashboard.js.\n3. Sticky chip overlapped other elements. .api-keys__summary had position: sticky + z-index: 5 and created a stacking context that floated above the tablist + page header on scroll. Dropped sticky — the chip is at the top of a short panel already. Lock-test updated to forbid the sticky position. (No version bump alone — counts on existing UX-A3 / UX-A9 lock-tests.) (post-cycle hotfix)
[1.59.1] — 2026-05-20
fix(test): v1.59.1 — NEW-D1 i18n-no-latin-leaks guard accepts UX-A11 polished ES copy. During final verification of the v1.59.0 ship I caught a regression: the v1.58.37 NEW-D1 static guard asserted pipe.title[es] must match /vacant|vaca/i (locking the old Pipeline de vacantes). UX-A11 (v1.58.64) refined that to Pipeline de candidaturas (candidate-side perspective). Fix: relax the regex to /vacant|vaca|candidatur/i so either contextualizing noun passes. No production-code change. 962 unit tests, 0 fail.
[1.59.0] — 2026-05-20
feat(ui): UX-A14 (v1.59.0) — Mobile (≤ 420 px) media-query pass. Closes the long-standing mobile audit. Five focused fixes inside a single @media (max-width: 420px) block in public/css/app.css:\n\n1. .card-row (4 dashboard metric cards) collapses to 1-up below 420 px (was a 900+ px horizontal scroll).\n2. .dash-hero-cta stacks vertically with full-width buttons (was side-by-side wrap-mid-label on iPhone SE).\n3. .page-header stacks H1+subtitle above the action buttons row (was right-side overflow on 360 px).\n4. .qa-grid minmax floor drops from 220 px to 160 px so tiles fit two-up on 360–390 px viewports.\n5. .api-keys__summary chips tighten horizontal padding for breathing room.\n\nRegression-lock test in tests/qa-report-fixes.test.mjs. 961 → 962 unit. (UX-A14)
[1.58.65] — 2026-05-20
test(ui): UX-A2 (v1.58.65) — lock-test on Modes structured field-form. The 5-field structured form for #/config → Modes was already shipped in v1.54.3 (canonical Target Roles / Adaptive Framing / Exit Narrative / Comp Targets / Location Policy + repeatable line-inputs for list-kind fields + × remove + add-row affordance + tagged {mode: sections|markdown} collect()). The UX-A2 audit assumed it was missing because all 5 sections initially appeared as <textarea> (an artifact of the field-form scaffolding pass before file load). This regression-lock test in tests/qa-report-fixes.test.mjs prevents a future PR from collapsing the field-form back to raw markdown. No code change. 960 → 961 unit. (UX-A2)
[1.58.64] — 2026-05-20
fix(i18n): UX-A11 (v1.58.64) — es / pt-BR copy polish: English loanwords replaced. eval.subtitle (es) now uses ajuste del CV / Puntaje / cabecera / informe; pt-BR uses aderência do CV / Pontuação / cabeçalho / relatório. pipe.title (es) refined to Pipeline de candidaturas (candidate-side perspective). pt-BR vagas retained. 959 → 960 unit. (UX-A11)
[1.58.63] — 2026-05-20
fix(ui): UX-A15 (v1.58.63) — Dashboard Pipeline tile gets visual primary weight. The Quick-actions grid previously gave every tile equal weight; the highest-frequency action (Pipeline) didn’t draw the eye. The qa() helper in public/js/views/dashboard.js now accepts a 7th primary flag; the Pipeline tile is the only one passing true so it gets a subtle accent border + larger icon + bolder label via .qa-tile--primary (public/css/app.css). Tile order, copy, click target, and accessibility name are unchanged. Lock-test in tests/qa-report-fixes.test.mjs. 958 → 959 unit. (UX-A15)
[1.58.62] — 2026-05-20
feat(ui): UX-A9 (v1.58.62) — #/config → API keys panel sticky summary chip. Before this release, with 5 provider key sections stacked vertically on the API-keys tab, the user had to scroll to know which provider the OR-fallback resolves to or how many keys were configured. public/js/views/config.js now renders a position: sticky .api-keys__summary at the top of the apiPanel showing Active: <provider> + Keys: <count>/5. Reuses /api/status/providers (no new API surface). Refreshes on providers-changed (after Save). Two new i18n keys (config.activeProvider, config.keysConfiguredPrefix) × 8 locales; one CSS rule (.api-keys__summary); static guard in tests/qa-report-fixes.test.mjs. 957 → 958 unit. (UX-A9)
[1.58.61] — 2026-05-20
docs(readme): UX-A8 (v1.58.61) — first-run cleanup section added across all 8 READMEs. Fresh clones of career-ops include two QA fixture URLs (example.com/qa-fixture-*) in data/pipeline.md so the test suite can run hermetically; the README never documented this so first-time users mistook them for real jobs. New ## First run — clean state section (mirrored in es / pt-BR / ko / ja / ru / zh-CN / zh-TW) instructs make clean-test-fixtures && npm start before the first scan. Lock-test in tests/qa-report-fixes.test.mjs verifies all 8 READMEs reference make clean-test-fixtures and qa-fixture-*. 956 → 957 unit. (UX-A8)
[1.58.60] — 2026-05-20
feat(ui): UX-A12 (v1.58.60) — Notifications drawer Clear all + per-entry dismiss. The v1.58.34 notifications journal capped at 50 entries but offered no manual purge. New UI.clearToastHistory() and UI.dismissToastHistory(ts) in public/js/api.js mutate toastHistory in place and notify subscribers with sentinel events ({cleared: true} / {dismissed: ts}); the drawer subscriber in public/js/app.js detects them, re-renders, and — critically — does NOT bump the unread counter on purges. Drawer head now carries a Clear all button (auto-hidden when the journal is empty); every .notif-item carries a × dismiss button. Three new i18n keys (notif.clearAll, notif.clearAllAria, notif.dismiss) × 8 locales; CSS for both controls; lock-test in tests/qa-report-fixes.test.mjs. 955 → 956 unit. (UX-A12)
[1.58.59] — 2026-05-20
feat(ui): UX-A13 (v1.58.59) — actionable “Fix →” CTA on failing #/health rows. Pre-fix the 21 health rows showed status only; a user with Profile customized: FAIL had to guess where to go. public/js/views/health.js now renders a small ghost-button Fix → next to the badge on failing/optional rows. Mapped targets: Profile customized → #/config?tab=profile, cv.md non-empty → #/cv, portals.yml present → #/config?tab=portals, data/applications.md → #/tracker. Any *_API_KEY or LLM_PROVIDER* row routes to #/config?tab=api-keys via regex fallback. Unmapped failures stay action-less (no dead-end CTAs). Two new i18n keys (health.fix, health.fixAria) × 8 locales. 954 → 955 unit. (UX-A13)
[1.58.58] — 2026-05-20
fix(ux): UX-A10 (v1.58.58) — guard #/cv against leaving with unsaved buffer. Pre-fix, navigating away from #/cv with unsaved edits silently dropped the buffer. public/js/views/cv.js now registers a beforeunload listener (browser-close confirm — generic dialog per modern browser policy) and a hashchange listener (SPA-internal nav prompts via localized window.confirm() and rewinds the hash if the user cancels). cvDirty lives in the save-button IIFE closure; cleanup self-detaches when the hash leaves #/cv (M-1 discipline). One new i18n key (cv.unsavedConfirm) × 8 locales. 953 → 954 unit. (UX-A10)
[1.58.57] — 2026-05-20
test(ui): UX-A7 (v1.58.57) — lock-test on the cost-line auto-refresh contract. v1.58.41 (UX-D-I) wired UI.providerCostHint to refresh when the user changes LLM_PROVIDER mid-session, but the contract had no static guard — any of the three pieces (config.js dispatch, api.js subscribe, advisor-view call site) could regress silently and the cost line would lie. New regression test in tests/qa-report-fixes.test.mjs locks all three: (1) config.js Save handler dispatches providers-changed, (2) UI.providerCostHint subscribes via document.addEventListener, (3) all 4 advisor views (#/deep, #/evaluate, #/auto, #/<mode>) call UI.providerCostHint(t). 952 → 953 unit. (UX-A7)
career-ops-ui v1.267.1 · page 6/10 · mirror of the project changelog for cvstart.org