மாற்றப் பதிவு

GitHub

ஒவ்வொரு குறிப்பிடத்தக்க மாற்றமும், வெளியீடு வாரியாக. இதே கோப்பு ரிப்போவில் இருக்கிறது மற்றும் அனைத்து 19 மொழிகளிலும் மொழிபெயர்க்கப்பட்டுள்ளது.

[1.227.5] — 2026-08-29

Fixed — the regional scan ran the server out of memory, and a Playwright suite flaked on its own teardown.

Fixed

  • The regional scan OOM-killed the server. ru-scanner accumulated every raw hit from every query and only deduplicated and filtered at the very end. The query list is deliberately full of near-synonyms (Golang, Go разработчик, Golang разработчик, Senior Go…), so the same vacancy comes back once per query and the array held several times the unique count — with descriptions attached, which are the bulk of a job object and are almost all discarded by the filters anyway. Measured at 742 MB of heap on a real 21-query run. The server caps Node’s heap at 490 MB (V8 sizes it from the box’s 956 MB of RAM), so the scan crashed the service with FATAL ERROR: Reached heap limit — four times in one day. Dedup and filtering now happen per query, which took the same run to 177 MB, a 76% reduction. Reported counts are unchanged: the unique total is carried by a Set of URL strings, so Total found still means what it always did.
  • net::ERR_SOCKET_NOT_CONNECTED failed CI at random. Three Playwright suites call window.stop() to halt in-flight requests before asserting no console errors fired. Chromium reports that single deliberate act under three names depending on where the socket was: ERR_ABORTED when it cancels the request, ERR_EMPTY_RESPONSE mid-flight, and ERR_SOCKET_NOT_CONNECTED when the socket was already gone. The shared benign-console filter listed only the first two, so the same teardown failed a run at random. It surfaced on the locale sweep in tr — the suite with the most navigations, hence the most chances to lose the race — while playwright-forms and playwright-smoke, which also call window.stop(), were one unlucky run away from the identical failure.

Notes

  • Why the scan only started failing now. The query list grew from 14 to 21 when product-manager searches were added. The old implementation’s memory scaled with total hits, so +50% queries pushed it past the box’s ceiling. Nothing was wrong with the new queries — they exposed an allocation pattern that had been one config change away from failing for a long time.
  • The dedup change is behaviour-preserving, and that is tested rather than asserted. tests/ru-scanner-dedup-order.test.mjs pins the awkward direction — a later duplicate that FAILS the filters must evict an earlier one that passed, because the old end-of-run pass kept only the last copy and then filtered it — and runs a 200-trial randomised equivalence check against the legacy accumulate-then-filter implementation.
  • The console filter stays narrow. Only the cancelled-request family is benign. A refused connection, DNS failure, connection reset, any 5xx and every uncaught JS exception still fail the assertion, pinned by new tests so the widening cannot creep.
  • Two earlier hypotheses were tested and discarded rather than shipped: a Caddy timeout (its config is clean, flush_interval -1 is correct for SSE) and Node’s 5-minute default requestTimeout (a scaled-down reproduction showed it does not terminate a long streaming response). The journal’s Reached heap limit was the actual evidence. Test suite: 2860 → 2865.

[1.227.4] — 2026-08-29

Fixed — word: / stem: were still ignored by content_filter; v1.227.3 only fixed the title filter.

Fixed

  • content_filter did not honour the keyword prefixes. v1.227.3 taught title_filter the parent’s word: / stem: prefixes but left content_filter on its own raw lower.includes(k), so a prefixed entry there was still matched as the literal text "word:java" — the same silent no-op, one filter over. content_filter reads the job DESCRIPTION and its default has always been a plain substring, which is why a bare negative java rejects every posting that merely mentions “JavaScript”; the prefix is how you opt one entry out of that. Ported the parent’s compileContentKeyword, which shares the prefix machinery but deliberately does not auto-anchor short keywords: the title filter anchors 2–3 letter acronyms because “COO” inside “Coordinator” is always wrong, whereas a short run inside a paragraph of prose is routinely intended (aws, gcp, sql, go).

Notes

  • Caught in AI review of the v1.227.3 PR, which noticed the code did not cover what its own comment claimed — the comment said “title_filter / content_filter” while only the title path was wired. The comment is now explicit about what each filter does: both honour the prefixes, only the title filter auto-anchors acronyms.
  • Also verified from that review, and unchanged: switching the 2–3 letter acronym rule from \b to the Unicode boundary is behaviourally identical for ASCII and stricter only where it should be — COO_lead and coo1 are still kept (underscore and digit are word characters under both), Coordinator kept, Директор COO and VP «Продукт» correctly dropped. 14 edge cases checked.
  • Unprefixed content_filter entries keep their previous matching byte-for-byte, a blank/absent description still passes untouched, and a bare word: / stem: matches nothing rather than everything. 6 new cases in tests/content-filter.test.mjs. Test suite: 2852 → 2858.

[1.227.3] — 2026-08-29

Fixed — word: / stem: title-filter prefixes were silently ignored, turning those filter lines into no-ops.

Fixed

  • web-ui did not implement the parent’s word: / stem: keyword prefixes. title_filter (and content_filter) default to case-insensitive substring matching, which is why a bare negative intern also rejects “International Product Manager” and “Internal Tools Engineer”. Rather than flip that default — a breaking change for every configured install — the parent made precision opt-in per entry: word:intern matches the whole word only, stem:agent must start a word and may continue (separating “Agentforce” from “Reagents”). web-ui had neither, so it matched a prefixed entry as the literal text "word:intern" — which appears in no job title. The line became a silent no-op: the same portals.yml filtered correctly through the CLI and not at all here, so every intern posting the user had excluded still reached the #/scan table. Ported from the parent’s title-keywords.mjs, including the Unicode-aware boundary (\p{L}\p{M}\p{N}_) rather than \b, which is ASCII-only and matched mid-word in accented or Cyrillic titles.
  • The 2–3 letter acronym rule now uses that same Unicode boundary. It previously used \b, so coo and word:coo disagreed on non-ASCII titles — two spellings of one rule.

Notes

  • Found while checking a live portals.yml that had just adopted word:intern specifically to stop “International Product Manager” being dropped by a bare intern. The prefix worked in the CLI; web-ui quietly kept every intern posting. Behaviour is now identical in both.
  • Defaults are unchanged. An unprefixed keyword keeps exactly its previous matching: substring for phrases and anything containing non-letters (.NET, SAP , L&D), boundary-anchored for 2–3 letter acronyms. A bare word: / stem: with nothing after it is treated as a typo and matches nothing — as a negative, an empty pattern would match every title and veto an entire scan from one stray colon.
  • 6 new cases in tests/title-filter.test.mjs covering both prefixes, the Unicode boundary, the bare-prefix typo, positive-side use, and that unprefixed defaults are untouched. Test suite: 2846 → 2852.

[1.227.2] — 2026-08-29

Fixed — #/config scrolled sideways on narrow phones.

Fixed

  • MOBILE-1: #/config overflowed horizontally on viewports ≤ 352 px. Every select-kind field carried an inline min-width: 300px. An inline style beats the stylesheet, so the control could not shrink with its container: at a 320 px viewport the select’s right edge landed at 353 px, the page grew a horizontal scrollbar, and controls were pushed off the right edge. Affected all 18 provider/model selects (cfg-llm-provider and every cfg-*-model). Now min-width: min(300px, 100%) + max-width: 100%, so the 300 px comfort width applies only when there is room. Devices at or below the threshold: iPhone SE (1st gen, 320 px), older Android, Galaxy Fold cover screen.

Notes

  • Why every earlier mobile pass missed it. 360 px and 375 px have just enough slack to hide the overflow, and the Playwright suites drive a 1280-wide window. Nothing in CI had ever measured a viewport narrower than 375 px. New tests/playwright-narrow-viewport.mjs sweeps the form-bearing routes at 320 px and fails on any horizontal overflow, naming the widest offending element in the assertion message; a second case pins that no #/config select extends past the viewport. It was verified to reproduce the bug first (scrollWidth 353 > clientWidth 320, 18 selects at right edge 353). Playwright: 99 → 101.
  • Known, deliberately not gated yet: 280 px (Galaxy Fold cover screen) still overflows by ~5 px, from an unrelated cause — long content inside the config card’s <details> blocks and the 256 px sidebar against a 280 px viewport, not the inline min-width this release fixes. Reflowing at a width no mainstream phone reports is separate work; the suite’s WIDTHS list is ready for it.
  • Also corrected the v1.227.1 QA prompt, which named the follow-up route as #/modes/followup. Mode pages register bare (Router.register(cfg.slug)), so the route is #/followup; #/modes/followup correctly 404s. Documentation only.
  • Unit suite unchanged at 2846 (the new coverage is browser-level).

[1.227.1] — 2026-08-29

Fixed — the help guide’s source count contradicted itself, and its GitHub banner was printed as raw markup in the app.

Fixed

  • §17 stated a total and a breakdown that did not add up. Every help bundle read “the server/lib/sources/ registry ships 85 adapters — 78 English + 5 Russian”. 78 + 5 = 83, not 85; ja/ko had drifted further, to 77 (= 82). Each new source bumped the total and left the breakdown alone, and the two numbers had been diverging for several releases. The breakdown is now 80 EN + 5 RU in all 17 locales. This mattered beyond cosmetics: docs/help/<lang>.md is the ONLY grounding corpus for the “Ask the docs” assistant, so a user asking how many sources are supported got two contradictory numbers out of one sentence.
  • The same sentence pinned a stale version anchor. “As of v1.213.0 the registry ships …” survived in 16 locales while the count moved twice (83 in v1.219.0, 85 in v1.226.0). The anchor was already repaired once by hand in v1.210.1 and simply drifted again. It is now removed in favour of version-free wording — the count itself is asserted against the live registry, which cannot go stale.
  • The provider-logo banner printed as escaped markup at the top of #/help. v1.225.0/v1.225.1 recorded it as “stripped in-app by UI.md() (no image support)”. That conflated two things: UI.md() is escape-first — it HTML-escapes every source byte before any markdown transform, which is the correct XSS boundary but the opposite of removal. Nothing removed the banner, because no code ever tried, so all 17 locales opened #/help with 268 characters of literal <p align="center"><img src="https://…. A new shared ingress (server/lib/help-markdown.mjs::stripGithubOnlyBlocks) now drops standalone HTML image blocks on read, for both GET /api/help/:lang and the docs-assistant retrieval corpus. The bundles keep the banner on disk, so GitHub still renders the showcase.
  • The cadence board’s “#” column had been blank since v1.117.0. It read entry.appNum, but followup-cadence.mjs emits num — a field name that never existed in the payload. Both are now read. The v1.227.0 “Next up” line inherited the same mistake and is fixed with it.

Notes

  • These four went undetected because nothing gated them. The help gates (canonical-docs-coverage, help-ui, help-ru-config-section) count HEADINGS — 32 H2 / 121 H3 — and no test had ever read a number inside a paragraph or looked at what the help ingress actually serves. Two new suites close that: tests/help-source-counts.test.mjs (the stated total equals SOURCES.length; the breakdown parts equal the live EN/RU split and sum to the total; the paragraph pins no version) and tests/help-banner-strip.test.mjs (the banner stays on disk for GitHub, never survives the strip, every bundle’s first rendered line is its heading, the strip stays narrow, and both consumers route through it). Both were verified to fail against the exact bugs before being committed. Test suite: 2837 → 2846.
  • No registry, scanner or route-contract change; source count remains 85 (80 EN + 5 RU), ALL_ADAPTERS 80.

[1.227.0] — 2026-08-28

Added — SEEK Hong Kong (JobsDB), a due-only follow-up lens, and confidential employers now show who fronted them.

Added

  • JobsDB Hong Kong via the existing Jobstreet/SEEK source. Hong Kong runs under the JobsDB brand but sits on the same SEEK platform behind the same v5 endpoint, so it needs no separate source — only hk.jobsdb.com in the host allowlist and siteKey: HK-Main. Registry is unchanged at 85 sources (80 EN + 5 RU) and ALL_ADAPTERS 80: this is a new MARKET on an existing adapter, not a new one.
  • Due-only lens on the follow-up cadence board (#/modes/followup). A new checkbox narrows the board to what is actionable right now. Due-ness is read from the urgency that the parent’s followup-cadence.mjs already computed (urgent / overdue) — never from the tracker status, which is applied/responded/interview and would match nothing. When nothing is due, the board names the nearest scheduled follow-up instead of showing a bare empty state. Logic lives in the pure public/js/lib/followup-view.js, mirroring the parent’s followup-view.mjs.
  • Confidential employers are attributed to the agency that fronted them. A posting whose employer is withheld is recorded as ? in the tracker with the recruiter in the Via column. #/tracker showed a bare ?, so several confidential rows were indistinguishable and the logo resolver had nothing to match. Such a row now reads “Confidential · via ”, resolves its logo from the agency name, and is findable by that name in the search box. Presentation only — the canonical company field is never overwritten with the recruiter. New pure public/js/lib/company-presentation.js, mirroring the parent’s company-presentation.mjs.

Fixed

  • Jobstreet/SEEK was calling a deprecated endpoint. The source still pointed at chalice-search v4 (/api/chalice-search/v4/search), which SEEK retired; the parent migrated to /api/jobsearch/v5/search and web-ui never followed. Every Jobstreet/SEEK entry now queries v5. The v5 item shape is read natively (job URL built from id, locations[0].label, advertiser.description, salaryLabel), the v4-only solrFields projection parameter is gone, and an entry whose portals.yml still pins the dead v4 path keeps its market hostname while the path is rebuilt on v5 — no user edit required.

Notes

  • career-ops 1.30.0 parity (parent HEAD 8d64f65 → 6cc46a4; the parent’s VERSION still reads 1.30.0 — release-please lags its main). Not ported, with reasons: iCIMS JSON-LD location fill — web-ui’s iCIMS source has no enrichDate() detail-page hook at all (list-pages only), so there is no code path to fix; nonfatal CLI stderr on clean exit (#1974) — web-ui’s runner decides failure from the exit code alone and never had the stderr-keyword heuristic that bug lived in; Turbopack path tracing — web-ui has no bundler; cv-sync-check crash/CODE_ROOT fixes — relayed read-only and fail-soft, so parent-side fixes land without a web-ui change; Playwright sequencing in scan.mjs, Block H application answers, the updater/doctor/update-system guards, the recursive syntax gate, the Go dashboard, and the jd-skill-gap/verify-cv-facts self-test additions — CLI-only surfaces web-ui does not shell into, or self-tests with no behavior change. Test suite: 2818 → 2837.

[1.226.0] — 2026-08-27

Added — two Vietnamese job sources: ITviec and CareerViet.

Added

  • ITviec + CareerViet (Vietnam). Two host-pinned Vietnamese boards join the scanner via provider: itviec / provider: careerviet entries: ITviec (itviec.com, tech-focused) and CareerViet (careerviet.vn, broad coverage). Both are zero-token HTML scrapers with SSRF host-pinning, HTTPS-only, per-company fail-soft, and page caps. Registry now ships 85 sources (80 EN + 5 RU), ALL_ADAPTERS 80 — both appear in the #/scan Source filter.

Notes

  • career-ops 1.30.0 parity (parentVersion 1.29.0 → 1.30.0). Not ported — CLI/community-only or already covered: the Hired Wall, /calibrate, clean-markers, template packs, doctor, pdf/LaTeX, and the eval/tracker/scan CLIs web-ui does not shell into; the _html-to-text quoted-angle fix and detect-reposts roleFuzzyMatch were already in web-ui; the title-filter stem: prefix is deferred (web-ui filtering lives in en-scanner). Test suite: 2818.

[1.225.1] — 2026-08-27

Fixed — the provider-logo banner now renders in the README and help.

Fixed

  • The v1.225.0 banner did not display on GitHub: the standalone images/providers.svg had an invalid font-family (nested double-quotes) that GitHub rejects, the <img> used a repo-relative src (raw HTML <img> does not resolve relative paths on GitHub), and the <p> block had no trailing blank line so it merged with the next line. It now points at a rendered images/providers.png via an absolute raw URL, with blank-line separation; the SVG is also corrected to valid XML.

Notes

  • Docs-only; no code or test change. The cvstart.org showcase and the app’s own provider tiles were already correct (inline SVG, not this file). Test suite: 2784.

[1.225.0] — 2026-08-27

Added — a provider-logo banner in the README and help guide.

Added

  • Provider-logo banner. A single self-contained images/providers.svg — the 18 LLM providers with the real brand logo (simple-icons, CC0) for the 11 that publish one and a brand-colored monogram for the 7 that don’t — now heads the README (×17) and the in-app help guide. (The app renders the real logo tiles itself; the banner is GitHub-rendered and stripped in-app by the XSS-safe markdown renderer.)

Notes

  • Docs-only — no code or test change; the SVG is inline (no remote asset). Scan sources unchanged at 83. Test suite: 2784.

[1.224.0] — 2026-08-26

Changed — the LLM provider tiles now show real brand logos.

Changed

  • Real provider logos. The provider tiles — Settings API-key fields, the Usage rows, the dashboard chip, the Settings “Active” summary, the ⚡ eval result, and the onboarding banner — now render the real brand logo for the 11 providers that publish an open-source icon (Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub · DeepSeek · Kimi · MiniMax · Mistral · Ollama; single-path SVGs from simple-icons, CC0). The 7 without a published icon (Hermes · GLM/Z.ai · Grok · Together · Fireworks · BytePlus Ark · Volcengine Ark) keep the brand-colored monogram. Still CSP-safe by construction: the logo path is an inlined static constant — no remote asset, no innerHTML — exactly like the monogram.

Notes

  • No route or behaviour change; the same ProviderLogo.el(slug) API. provider-logo.js grew (the inlined logo paths). Scan sources unchanged at 83. Test suite: 2784.

[1.223.0] — 2026-08-26

Fixed — a successful ⚡ live eval was painted as an error; plus install the OpenWorker coworker straight from its GitHub URL or .zip.

Fixed

  • ⚡ eval result badge tone. A successful live evaluation is no longer coloured as a failure. The in-process live providers (OpenRouter, DeepSeek, …) return no subprocess exit code, so the badge’s strict code === 0 test was false and every success rendered red (while still reading “exit 0”). The tone now follows the outcome — only a subprocess that exits non-zero paints it red; a code-less live result is badge-ok, and the “· exit N” suffix shows only when there is a real exit code.

Changed

  • Install the OpenWorker coworker from its GitHub URL / .zip. The coworker’s docs now describe the one-command installer (curl … | bash, idempotent — it reuses an existing career-ops / web-ui) and the three OpenWorker install paths (GitHub URL · .zip · single-file import) across docs/integrations/openworker.md, in-app help §32 (×17), and the README.

Notes

  • Closed a long-standing coverage gap: the Recruitee source now has an end-to-end test for the v1.214.2 quoted-angle fix (a > inside a tag attribute must not leak into the description). Scan sources unchanged at 83. Test suite: 2783.

[1.222.0] — 2026-08-26

Changed — the extended-provider field hints (DeepSeek … Volcengine Ark) are now localized in all 17 languages.

Changed

  • Localized provider field hints. Every config.<slug>Hint for the 11 OpenAI-compatible providers added in v1.216.0–v1.217.0 (DeepSeek · GLM/Z.ai · Kimi · MiniMax · Mistral · Grok · Together · Fireworks · Ollama · BytePlus Ark · Volcengine Ark) — the API-key, model, and base-URL hints under Settings → API keys — is now translated across all 17 locales instead of falling back to English. Signup URLs, model ids, and the ⚡ live-eval marker are preserved verbatim.

Notes

  • No behaviour change — the English hintFallback in field-specs.js still guards a missing key, and a new contract test asserts every field-descriptor hintKey resolves in all 17 locales. Scan sources unchanged at 83. Test suite: 2779.

[1.221.0] — 2026-08-26

Security — DNS-rebinding defence-in-depth on the scanner fetch path.

Security

  • The scanner HTTP core (fetchJson / fetchText in server/lib/http-json.mjs) now resolves each source’s hostname on the real network path and refuses to connect if it resolves to a private, loopback, link-local, CGNAT, or cloud-metadata address (e.g. 169.254.169.254) — closing a DNS-rebinding vector where a public hostname is served a private A record. Scanner hosts are already pinned to public registrable domains, so this is defence-in-depth; the user-supplied-URL path already had stronger connection-pinning (safe-fetch.mjs, since v1.20.1). The guard runs only on the real fetch transport — an injected test fetchImpl is never resolved, so the mocked source suites are unaffected.

Notes

  • No behaviour change for a healthy scan (public boards resolve to public IPs). Scan sources unchanged at 83. Test suite: 2775.

[1.220.0] — 2026-08-26

Added — scan several Get on Board categories from one entry. The board splits leadership and ML/data roles out of programming, so an EM or data search now covers them without a second portal entry.

Added

  • Get on Board: multiple categories per entry. A getonbrd portal entry can now set categories: [programming, operations-management, machine-learning-ai] (or a single category:) instead of only the default programming feed — postings are deduped by URL across categories, and the list is capped at 12. Existing entries are byte-identical (the default is still programming).

Notes

  • Server-only; the SSRF host-pin (www.getonbrd.com) + redirect:'error' are unchanged, and a bad category slug is rejected before any fetch. Scan sources unchanged at 83. Test suite: 2771.

[1.219.0] — 2026-08-26

Added — Torre joins the scanner, and every provider label is now correct. A new LatAm-heavy remote board, a fix that stopped the a16z sweep short, and the last places that named providers from a stale short list now use the full 18-provider roster.

Added

  • Torre (provider: torre) — the pan-LatAm talent marketplace behind torre.ai, a public zero-auth opportunity search carrying remote roles that never reach Greenhouse/Lever/Ashby. Configure search (+ experience) and optional remote_only; one capped request per entry. The scanner registry now ships 83 sources (78 English + 5 Russian).
  • Provider monogram on the onboarding banner — the top-of-page “Live eval” chip now shows the active provider’s brand tile (joining Settings, Usage, the dashboard chip, and the ⚡ result).

Fixed

  • The a16z Speedrun scan stopped early on a rotating board. When a listing was deleted between page requests, the feed returned a 49/50 page mid-sweep; that short page was treated as the last one, silently truncating the scan (149 of ~300 jobs). It now trusts the feed’s own total_pages and only falls back to a short page when the feed doesn’t report one — an empty page always ends iteration, so an over-reporting feed can’t run away.
  • Stale provider names. The global onboarding banner resolved the active provider from a 4-entry map (Anthropic/Gemini/OpenAI/Qwen) and showed a raw slug for the other 14; it now uses the shared 18-provider label + monogram. The Settings “which providers” note (“seven providers …”) now lists the full 18.

Notes

  • Get on Board’s new multiple-categories-per-entry option isn’t wired here yet — the web-ui adapter builds the category into the endpoint URL, so it needs a source-level refactor; queued. Test suite: 2768.

[1.218.0] — 2026-08-26

Added — provider brand tiles (and correct names) everywhere a provider is shown. The dashboard “live evals” chip, the Settings active-provider summary, and the ⚡ evaluation result now each carry the provider’s monogram and name it correctly across all 18.

Added

  • Provider monograms on the dashboard chip, the Settings “Active” summary, and the ⚡ evaluation result header — matching the tiles already in the Settings key fields and the Usage page.

Fixed

  • Stale provider labels. Three surfaces resolved the name from a 5-entry map (Anthropic/Gemini/OpenAI/Qwen/OpenRouter) and fell back to the raw slug — the ⚡ evaluation result even mislabeled every non-Anthropic provider as “Gemini”. All three now use the shared ProviderStatus.label (18 providers).
  • Settings key count showed “/ 7”; it now reflects the real provider total (18).
  • The LLM_PROVIDER hint’s slug list (… / ollama) gained ark / arkcn in all 17 locales, matching the auto-order arrow list.

Notes

  • Client-only, CSP-safe (the same inline-SVG monograms). No server change. Scan sources unchanged at 82. Test suite: 2758.

[1.217.1] — 2026-08-26

Test hardening — endpoint coverage for the full provider roster.

Added

  • A parametrized run<Provider> test asserting the endpoint, default model, and Bearer auth for Kimi, MiniMax, Mistral, and Fireworks, so every OpenAI-compatible wrapper now has a direct endpoint check (not just structural coverage).

Notes

  • No behavior change. Scan sources unchanged at 82. Test suite: 2756.

[1.217.0] — 2026-08-26

Added — Ark joins the LLM roster (18 providers). BytePlus Ark and Volcengine Ark — ByteDance’s Doubao models — are now ⚡ live-eval providers, wired through the same OpenAI-compatible core with a region base-URL override and their own brand tiles.

Added

  • BytePlus Ark (ARK_API_KEY, international) and Volcengine Ark (ARK_CN_API_KEY, China) — OpenAI-compatible Chat Completions via the shared runOpenAICompatible() core. Set ARK_MODEL / ARK_CN_MODEL (a Doubao model name or your ep-… endpoint id); ARK_BASE_URL / ARK_CN_BASE_URL switch region. Both join the auto order (last) and each shows its monogram in Settings + Usage. The web UI now spans 18 live-eval providers.

Notes

  • Same trusted-config + http(s) scheme-guard path as the rest of the roster (never the SSRF job-URL validator). Scan sources unchanged at 82. Test suite: 2755.

[1.216.0] — 2026-08-26

Added — nine more LLM providers, each with its own brand tile. Your ⚡ live evaluations can now run through DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together, Fireworks, or a fully local Ollama — one key away, each marked by a monogram beside its field in Settings.

Added

  • Nine OpenAI-compatible providers. DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together AI, Fireworks AI, and Ollama (fully local, no key). Set a key — or OLLAMA_BASE_URL for Ollama — in App settings and it joins the auto order after Hermes; pin any one with LLM_PROVIDER. Together also hosts Thinking Machines’ Inkling (thinkingmachines/Inkling). The web UI now spans 16 live-eval providers.
  • Provider monogram tiles. A CSP-safe, brand-colored initial tile marks every provider beside its key field in Settings and its row on the Usage page — built with inline SVG, no external logos, nothing leaves your machine.
  • Each provider surfaces on #/config (key + model dropdown; China-endpoint base-URL fields for GLM and Kimi), /api/status/providers, /api/health, and the Usage cost roll-up (editable list prices in server/lib/llm-pricing.mjs).

Notes

  • Provider base URLs are trusted config reached through runOpenAICompatible() with an http(s) scheme guard (Ollama’s loopback is allowed) — never the SSRF job-URL validator. Scan sources unchanged at 82. Test suite: 2752.

[1.215.0] — 2026-08-26

Added — run your whole job search from OpenWorker. A new coworker drives this pipeline from Andrew Ng’s open-source AI coworker app, and the in-app help now covers it so the Ask-the-docs assistant can walk you through it.

Added

  • OpenWorker coworker. A code-free OpenWorker coworker — Fighter90/career-ops-coworker — runs the full job search (scan, score, tailor, track, follow up) and can launch this dashboard, all from your desktop. It’s a one-file persona shipped in 17 languages, verified installable against OpenWorker’s own loader.
  • Help §32 “Run it from OpenWorker” (all 17 locales) documents it, so the in-app Ask the docs assistant and the floating docs button can answer questions about it. Also linked from the landing footer, the README, the wiki, and docs/integrations/openworker.md.

Notes

  • Docs + site only — no app-code change. Scan sources unchanged at 82. Test suite: 2742.

[1.214.2] — 2026-08-25

Fixed — the shared HTML→text pipeline (Greenhouse / Recruitee descriptions) no longer leaks tag attributes into the description when an attribute value contains a > character.

Fixed

  • HTML descriptions with > inside an attribute are stripped cleanly. The tag stripper behind the Greenhouse and Recruitee descriptions used a naive match that stopped at the first >, so a tag like <a title="salary > 100k"> left the trailing attribute text (100k">) in the description. It now respects quoted attribute values (single or double), strips markup before decoding entities (so an encoded quote inside an attribute can’t become a false delimiter), and still preserves a literal <> in plain text.

Notes

  • Test suite: 2742. Scan sources unchanged at 82. Nothing else new from the parent this round: the remaining changes are relayed read-only (follow-up / weekly-digest / rejection-latency / salary-gap fixes need no web-ui change) or aren’t web-ui surfaces (an external X/Xquik BYO-key plugin, CLI/verify fixes). The opt-in items — SmartRecruiters detail descriptions, detect-reposts aggregator skip, stem: / word: title prefixes, DNS-rebinding guard — stay queued.

[1.214.1] — 2026-08-25

Fixed — Ashby job descriptions are now capped at the same length as Greenhouse and Recruitee, so a content filter behaves identically across all three boards and one board can’t bloat the scan cache.

Fixed

  • Ashby descriptions now honour the same length cap as the other boards. v1.214.0 fed Ashby’s posting body to the content filter, but — unlike Greenhouse and Recruitee, which cap at 4000 characters — Ashby’s was mapped straight through uncapped (some ran 4× longer). So the same content_filter keyword could match on Ashby but not Greenhouse depending only on where it fell in the text, and one board could add hundreds of KB to the scan cache the #/scan page loads. Ashby is now capped to the same 4000 characters (its text is already plain, so it’s truncated, never tag-stripped).

Notes

  • Test suite: 2738. Scan sources unchanged at 82.

[1.214.0] — 2026-08-25

Fixed — job descriptions from Greenhouse, Ashby, and Recruitee now feed the content filter, so keyword / country / visa filters actually match those boards; and “Associate ” (including academic ranks) is no longer misfiled as entry-level.

Fixed

  • Content filters now work on Greenhouse, Ashby, and Recruitee. These boards now carry each posting’s full text as its description, so a content_filter (or a country / visa keyword filter) matches the job body instead of passing the board through blind. Ashby and Recruitee ship the body in their list payload for free; Greenhouse already did. All three strip through one shared HTML→text pipeline.
  • “Associate Director” / “Associate Professor” is senior, not entry-level. The seniority classifier (used by skip_tiers:) treated the word “associate” as an entry-level marker even when it prefixes a senior title. It now reads “Associate ” — including academic ranks (Associate Professor, Associate Dean) — as senior, while keeping genuine junior variants (Associate Attorney, Associate Editor) as entry-level, and still filing a leading “Intern, Associate Dean” as an internship.

Notes

  • Test suite: 2736. Scan sources unchanged at 82. Queued for a follow-up: opt-in SmartRecruiters description enrichment (a per-posting detail fetch), detect-reposts’ aggregator: true board skip, and the anchored-keyword stem: / word: title prefixes — each an opt-in config surface rather than a default-behaviour change. The DNS-rebinding hardening remains queued for its own security release.

[1.213.0] — 2026-08-22

Added — MyCareersFuture, Singapore’s national job bank, as a scan source. Fixed — Greenhouse postings now carry their full text so content filters work, and remote Ashby roles are no longer hidden behind a city-only location.

Added

  • MyCareersFuture (Singapore) (mycareersfuture.gov.sg) — a new zero-token scan source for Singapore’s national public job bank, run by Workforce Singapore. Pick it in the #/scan Source filter, or add a provider: mycareersfuture company with an optional keywords list (it falls back to your profile’s target roles, like Job Bank). Reads the public search API, host-pinned, no key.

Fixed

  • Greenhouse postings can now be content-filtered. Greenhouse boards are fetched with their full posting body, decoded to plain text as the posting’s description — so a content_filter (or a country/visa keyword filter) that reads the description now actually matches Greenhouse roles instead of passing them through blind.
  • Remote Ashby roles are no longer dropped by a city filter. Ashby keeps the work model (Remote/Hybrid/Onsite) separate from the office city, so a fully-remote role still read as e.g. “San Francisco” — and a location filter blocking that city hid a role you could take. “Remote” is now appended to the location when the role is remote, and workplaceType wins over a stale isRemote flag so an office-anchored Hybrid role isn’t mislabeled.

Notes

  • Scan sources: 82 (77 English + 5 Russian). Test suite: 2724. A DNS-rebinding hardening (validating a hostname’s resolved address before connecting) is queued for a dedicated release — it needs a web-ui-specific design rather than a direct port.

[1.212.1] — 2026-08-21

Fixed — the cvstart.org landing under-counted the scanner’s job sources (it showed 80 and omitted Job Bank (Canada)); it now matches the app’s 81 again, and the site build fails loudly if the two ever disagree.

Fixed

  • The landing’s “Job sources” count is back in sync with the app. After v1.212.0, cvstart.org showed 80 boards and was missing the new Job Bank (Canada) chip, while the app, the scan dropdown, and the help guide all listed 81. The landing builds its list by loading the live scanner registry, and one source failed to load in that build because of how it pulled in a YAML dependency — so it was silently dropped. Job Bank now loads that dependency lazily, the same way the rest of the app does at scan time, so it always appears.
  • The site build now refuses to ship a mismatched source count. If the registry ever enumerates fewer sources than exist on disk (the signature of a source that failed to load), the build fails with a clear message instead of quietly publishing the wrong number.

Notes

  • App behaviour is unchanged — the scanner always had all 81 sources; only the landing page was affected. Scan sources: 81 (76 English + 5 Russian) — unchanged. Test suite: 2687.

[1.212.0] — 2026-08-21

Added — Job Bank (Canada), the federal national job board. Removed — EchoJobs (its feed is now bot-blocked). Fixed — Consider boards return results again, and multi-location Lever roles no longer hide half their locations.

Added

  • Job Bank (Canada) (jobbank.gc.ca) — a new zero-token scan source for Canada’s federal national employment service, a high-volume board no aggregator covers well. Pick it in the #/scan Source filter, or add a provider: jobbankca company with an optional keywords list (it falls back to your profile’s target roles). Reads the public ATOM feed, host-pinned, no key.

Removed

  • EchoJobs — retired. Its public feed is now behind bot protection and returns nothing, so keeping it only wasted a scan slot.

Fixed

  • Consider-powered boards return results again. Consider now requires an anonymous handshake (a GET that seeds a session cookie + CSRF token) before it accepts the search request; without it the request was silently rejected and the board looked empty.
  • Multi-location Lever roles no longer hide half their locations. Lever puts one primary city in location and the rest in allLocations; reading only the primary made a role open in Barcelona AND Montevideo look Barcelona-only (and wrongly dropped by a location filter). Both are now merged.

Notes

  • Gentler 250 ms inter-page pacing (up from 150) on the paginated boards, to stay polite to single-host careers sites. Scan sources: 81 (76 English + 5 Russian) — unchanged (Job Bank in, EchoJobs out). Test suite: 2685.

[1.211.0] — 2026-08-19

Added — Yourator, a Taiwan tech job board. Fixed — accented company/title entities now decode everywhere, and a company with an accent in its name is no longer wrongly flagged.

Added

  • Yourator (yourator.co) — a new zero-token scan source for the Taiwan tech & digital job market. Pick it in the #/scan Source filter, or add a company with provider: yourator. It reads the public JSON API (no key, no browser), walks every page of the board, and emits each posting’s real employer link (its own ATS) with tracking params stripped.

Fixed

  • Accented named entities now decode everywhere. The shared HTML decoder gained the Latin-1 letters (&eacute; → é, &ccedil; → ç, …), so a European board that writes D&eacute;veloppeur or Fran&ccedil;ais no longer lands that literal in a job title, the tracker, or a generated document. (Uppercase names stay uppercase — &Eacute; is É, not é — and a lookup like &constructor; now resolves to itself.)
  • A company with an accent in its name is no longer wrongly flagged for being on its own domain. “Işık” now folds to “isik” and matches isik.com.tr; “Société Générale” matches societegenerale.com. The old check deleted accented letters instead of folding them to their ASCII base.

Notes

  • Scan sources: 81 (76 English + 5 Russian). Test suite: 2667.

[1.210.1] — 2026-08-19

Fixed — Habr Career vacancy titles and company names with “&” or quotes no longer arrive garbled.

Fixed

  • The Habr Career source now decodes HTML entities in the title and company name before they flow on. The server-rendered cards arrive escaped (“Changellenge >>”, “Demand Forecasting & Inventory Optimization”, “ООО "М-ТЕХ"”), so an undecoded “&” silently failed a user’s own “&” title filter — the exact symptom the previous release closed on five other boards — and company names reached the tracker and reports mangled. Entity-decoding is now complete across all six affected sources.

Notes

  • Test suite: 2644.

[1.210.0] — 2026-08-19

Added — Senjob, the scanner’s first African job board (Senegal); sharper title matching on five more boards.

Added

  • Senjob (senjob.com) — a new zero-token scan source for Senegal, the scanner’s first African board. Select it in the #/scan Source filter, or add a company with provider: senjob. It reads the public listing over plain HTTP (no key, no browser), pins every request to senjob.com, and — parsing HTML — treats a listing that suddenly yields nothing as a broken board (a surfaced error) rather than a silently empty country.

Fixed

  • Titles with an ampersand no longer drop jobs on five boards — on beesite, Cornerstone (csod), Hacker News “Who is hiring”, Phenom and TKMS, titles arrive HTML-escaped, so an escaped “&” in a role like “R&D Engineer” failed your own “r&d” keyword and the posting vanished silently (a “sales & marketing” veto never fired either). Titles — and Phenom locations — are now decoded before filtering.

Notes

  • Scan sources: 80 (75 English + 5 Russian). Test suite: 2643.

[1.209.0] — 2026-08-17

Added — the in-app help now covers recording an application outcome, and “Ask the docs” can point you to it.

Added

  • The Tracker help (§11) gained a “Record an outcome” section in all 17 languages, walking through the Outcome button: pick what happened (rejected / offer / hired / declined / ghosted / advanced to interview), preview what it will do, then record — which logs the result, archives the CV and cover letter you submitted, and syncs the row’s Status for you. The floating “Ask the docs” assistant reads the help guide, so it now guides you to that button instead of only suggesting you edit the Status by hand.

Notes

  • Each help bundle is now 31 H2 / 119 H3 (was 118); the parity gates were bumped to match. Docs-only — no code or behaviour change. Suite: 2625.

[1.208.2] — 2026-08-16

Fixed — on a phone the notification and theme buttons no longer sit on top of the search box.

Fixed

  • v1.208.1 stopped the top-bar buttons from overlapping the page heading, but on a narrow-but-not-narrowest phone — especially in languages with longer button labels — the whole bar still crammed onto one row, so the 🔔 and 🌙 buttons could land on top of the search box. The action buttons (notifications, theme, Diagnostics, Open Scan) now always drop onto their own full-width second row on a phone, so the search box stays fully readable and nothing overlaps.

Notes

  • On a phone the top-bar action buttons move to a full-width second row, removing the fragile “almost-full row” band where the layout distributed leftover negative space as overlap. A Playwright guard now reproduces the exact trigger — a long-label locale across the 565–640px band — and asserts the top-bar controls never share pixels. Suite: 2621.

[1.208.1] — 2026-08-16

Fixed — on a phone the top-bar buttons no longer overlap the page.

Fixed

  • v1.208.0 wrapped the top-bar action buttons (Diagnostics, Open Scan, notifications, theme) onto a second row on narrow screens, but the bar kept a fixed height — so the wrapped row spilled out and sat on top of the page heading. The bar now grows to fit its rows and the content flows below it.

Notes

  • The top bar’s fixed height became a min-height, so it expands with its content on any width (desktop is unchanged — one row always fits). A Playwright guard now also checks the top bar does not spill over the page. Suite: 2621.

[1.208.0] — 2026-08-16

Fixed — the app fits a phone screen now: no more sideways scrolling.

Fixed

  • On a narrow screen the whole app used to slide sideways — the top bar, tables, help articles and settings tabs all pushed past the right edge. Now every page fits any width: the top-bar buttons wrap onto a second row, wide tables and code blocks scroll inside their own box, help stacks its table-of-contents above the article, button/tab rows wrap, and long paths or URLs break instead of stretching the page.

Notes

  • Root cause was the classic flex/grid min-width: auto trap plus a couple of unwrapped wide elements; fixed with min-width: 0 on grid items, overflow-wrap on markdown/titles, a scrollable markdown table, and the help grid stacking at the mobile breakpoint. A Playwright guard asserts 0 horizontal overflow at 375 px across the main routes. tests/playwright-smoke.mjs. Suite: 2621.

[1.207.2] — 2026-08-16

Fixed — AI plans and career-orientation profiles no longer render as a raw code dump.

Fixed

  • Some models wrap their whole answer in a markdown … code fence. When that happened, the development plan and the career-orientation profile showed up as a monospace code block instead of a formatted document with headings and lists. The wrapping fence is now removed — only when it wraps the entire answer and is explicitly markdown/md, so a genuine python/js/bare-``` code answer is left untouched.

Notes

  • Handled once in the shared LLM-declutter step (cleanLlmMarkdown), so every AI route benefits, and inner code blocks inside the wrapped answer survive. tests/llm-output.test.mjs (+3). Suite: 2621.

[1.207.1] — 2026-08-16

Fixed — the landing page no longer overflows sideways on small phones.

Fixed

  • On a narrow phone the hero — the headline, the intro line, and the install terminal — could be clipped off the right edge because a long install command and the layout columns would not shrink to the screen. They now fit any width; the install command scrolls inside its own terminal box.

Notes

  • Also hardened a flaky end-to-end smoke check that could fail on a transient resource 404 — it now ignores benign network noise (favicon / connection / failed-resource) like the sibling checks, while still catching real script errors. No application behavior changed. Suite: 2618.

[1.207.0] — 2026-08-15

Added — record an application’s outcome straight from the tracker.

Added

  • Every tracker row gets an Outcome action: pick what happened (rejected, offer received, hired, offer declined, no response, advanced to interview), add an optional note, preview the resulting status, then record it. Recording archives the submitted CV & cover-letter artifacts and syncs the tracker to the canonical state — one deterministic action instead of hand-editing the tracker.

Notes

  • New POST /api/outcome relays the outcome CLI: dryRun:true is a read-only preview (matches the row, reports the resulting state, writes nothing); a real call records it. Write-safety: the outcome type is whitelisted to the known set and every text field is control-char-rejected before the shell-out (array args, spawn — no shell). tests/outcome-route.test.mjs. Suite: 2618.

[1.206.0] — 2026-08-15

Documentation — the in-app help guide now covers the five newest features, in all 17 languages.

Added

  • The built-in help guide — and the “Ask the docs” assistant that answers from it — now documents five recently shipped features: Setup doctor (Settings — checks your CV & profile for gaps and leftover example data), Discover ATS boards (Portals — find a company’s careers board automatically), the “still live?” check (Tracker — whether a posting is still open), the “reuse a past CV?” hint (CV Studio — flags when a previously tailored CV already fits a new job), and the Skills log (Analytics — record self-assessment scores). Five new subsections, translated across all 17 languages.

Notes

  • Help-guide structure grows to 31 H2 / 118 H3, parity-gated across every locale. Reference docs refreshed: docs/architecture/API.md documents the five routes behind these features, and the route/version counters in CLAUDE.md and docs/sdd/CONVENTIONS.md are current (36 route modules). Suite: 2610.

[1.205.0] — 2026-08-15

Added — a Skills log to record practice-test / assessment results.

Added

  • A new Skills log (Analytics → Skills log) lets you record a skills self-assessment — company, platform, skill, score %, and an optional note — appended to data/assessments.tsv, with a newest-first list of past entries. Zero-token, deterministic; the parent CLI owns the file format.

Notes

  • New GET /api/assessments (relays assessment-log.mjs’s default JSON list; fail-soft {available:false}) + POST /api/assessments (explicit write: fields passed as array args to assessment-log.mjs add, so the parent owns the TSV). Write-safety: every text field is rejected if it contains a control character (a TAB would break a column, a newline would inject a row) → 400 before any write; score/threshold whitelisted to 0–100; lengths bounded. tests/assessments-route.test.mjs. Suite: 2610.

[1.204.0] — 2026-08-15

Added — a “Setup doctor” panel in Settings that flags an incomplete or example-data CV/profile.

Added

  • Settings → Setup doctor now runs a zero-token check of your cv.md and config/profile.yml and lists any blocking issues (missing files/fields) and warnings (leftover example/placeholder data, hardcoded metrics) — so you catch an incomplete setup before it weakens your scans and tailoring. Read-only; re-run with one click.

Notes

  • New read-only GET /api/cv-sync-check relays the parent’s cv-sync-check.mjs, which prints human text + an exit code (no --json); the route does light structured parsing of its stable ERROR: / WARN: lines into {ok, errors[], warnings[]} — the banner, not the exit code, decides success. Fail-soft {available:false} on standalone installs. tests/cv-sync-check-route.test.mjs. Suite: 2602.

[1.203.0] — 2026-08-15

Added — a “reuse a past CV?” hint in CV Studio.

Added

  • When you open a saved job description in CV Studio, the app now compares it against your other saved JDs (deterministic word overlap, zero tokens) and tells you whether the closest match is similar enough to reuse that tailored CV, reuse it with edits, or tailor a fresh one — so you don’t regenerate from scratch for a role you’ve already targeted.

Notes

  • New read-only GET /api/jds/:name/reuse relays the parent’s jd-similarity.mjs (Jaccard overlap + a seniority guard; JSON {decision, score, reason}) once per prior JD (fan-out capped at 25, best match wins); fail-soft {available:false} when the script or prior JDs are absent. tests/jd-similarity-reuse-route.test.mjs. Suite: 2594.

[1.202.0] — 2026-08-15

Added — discover a company’s ATS job board from #/portals and start tracking it.

Added

  • On #/portals, type a company name and the app probes Greenhouse, Ashby, and Lever for its public job board — zero LLM, zero browser — and shows the boards that exist and currently list ≥1 job. One click adds a chosen board to the companies your scanner watches. All probing is read-only; the write to portals.yml happens only when you click Add.

Notes

  • New server/lib/discover-ats.mjs (fixed-host, charset-validated slug probe through the DNS-pinned safeGet, ≤12 probes/request, cross-origin redirect → not resolved) + POST /api/portals/discover (read-only) and POST /api/portals/track (explicit write: withFileLock + surgical text splice + re-parse guard + atomic rename; only known ATS hosts, idempotent). Reuses the scanner’s own adapter registry to confirm each board and count jobs. i18n ×17. tests/discover-ats-resolver.test.mjs + tests/discover-ats-route.test.mjs. Suite: 2588.

[1.201.0] — 2026-08-15

Fixed — a tracker with localized or variant column headers no longer renders blank.

Fixed

  • If your data/applications.md uses non-English or variant column headers — Spanish empresa / puesto / estado / fecha / enlace, or position / stage / link — the tracker read them under the wrong keys and showed blank Company / Role / Status / Date / Link columns. Those headers now fold onto the canonical field names, so the tracker renders correctly. An all-English tracker parses exactly as before.

Notes

  • New HEADER_ALIASES map + a normalization fold in parseApplications (server/lib/parsers.mjs); unknown or already-canonical headers pass through unchanged. tests/tracker-header-aliases.test.mjs. Suite: 2563.

[1.200.0] — 2026-08-15

Added — a one-click “still live?” check for ATS-hosted jobs in your tracker.

Added

  • On #/tracker, an application whose URL is a Greenhouse / Lever / Ashby / Workday / SmartRecruiters posting now shows a “Still live?” button. One click asks the ATS’s own public JSON — zero tokens, no browser — and shows Live / Expired / Unknown, so you can spot dead postings without opening each one. Conservative by design: only a definitive 404/410 reads as Expired; anything ambiguous stays Unknown (never a false Expired).

Notes

  • New server/lib/liveness-core.mjs + liveness-api.mjs and a read-only GET /api/liveness?url= (no writes, no LLM). SSRF-safe: the URL is isValidJobUrl-gated, then the ATS API is reached only through the DNS-pinned safeGet with a fixed host + charset-validated path segments. tests/liveness-core.test.mjs + tests/liveness-route.test.mjs. Suite: 2557.

[1.199.0] — 2026-08-15

Fixed — wide tables now scroll sideways instead of being cut off.

Fixed

  • On the Scan page (and every other table — Tracker, Statistics, Usage, Dashboard) a table wider than the window was clipped with no scrollbar, leaving the rightmost columns unreachable. Wide tables now show a horizontal scrollbar on demand, so every column stays reachable at any width.

Notes

  • .table-wrap in public/css/components.css switched from overflow: hidden to overflow-x: auto (mirrors the existing .reports-scroll container); the rounded border is preserved. tests/table-wrap-scroll.test.mjs. Suite: 2540.

[1.198.0] — 2026-08-15

Added — scan retries now use exponential backoff, jitter, and honour a rate-limiter’s Retry-After.

Added

  • When a job board briefly rate-limits or errors (HTTP 429 / 5xx) mid-scan, the retry now waits with exponential backoff + jitter instead of a fixed short delay — so a busy board isn’t re-hammered at a fixed cadence and concurrent retries don’t re-collide in lockstep. A Retry-After from the board is honoured (but clamped, so a hostile Retry-After: 86400 can’t stall a whole sweep). Permanent errors (404, refused redirects) still fail fast — unchanged.

Notes

  • New parseRetryAfterMs() + the pure computeRetryDelayMs() in server/lib/http-json.mjs; fetchJson now captures .retryAfter on a non-ok response and fetchJsonWithRetry takes an optional maxDelayMs (default 8000). tests/http-json.test.mjs (+9). Suite: 2536.

[1.197.0] — 2026-08-14

Added — track a Getro VC job board by its careers_url alone; the collection id auto-resolves.

Added

  • A tracked Getro board (b2venture, Earlybird, Point Nine, …) no longer needs a hand-looked-up numeric getro_collection. Give it the board’s own careers_url and the id auto-resolves from that page on the first scan — a single SSRF-safe GET reads the numeric network.id straight out of the board’s embedded page data. An explicit getro_collection still wins and skips the fetch entirely.

Notes

  • New httpsCareersUrl(), extractCollectionId(), and async resolveCollectionId() in server/lib/sources/getro.mjs; the board page is fetched through the DNS-pinned, size-capped safeGet, and the resolved id is still host-pinned to api.getro.com by assertGetroUrl. The adapter now matches a provider: getro entry carrying an https careers_url even without an id. tests/sources-getro.test.mjs (+8). Suite: 2527.

[1.196.0] — 2026-08-14

Fixed (security) — the Workday adapter validates an api endpoint by its hostname, not a substring.

Fixed

  • A portals.yml Workday api: value is now accepted only when its hostname is myworkdayjobs.com (or a .myworkdayjobs.com subdomain). The old check was a substring match, so any URL that merely contained the string — e.g. https://example.com/?x=myworkdayjobs.com or https://myworkdayjobs.com.example.com/… — passed and would have been handed back as the fetchable endpoint. Real Workday endpoints are unaffected. (Reported by CodeQL, #443.)

Notes

  • New isWorkdayApi() parses the URL and checks the host in server/lib/portals/adapters/workday.mjs; used by both matches() and buildEndpoint(). tests/workday-adapter-endpoint.test.mjs (+1). Suite: 2522.

[1.195.0] — 2026-08-14

Performance (scanner) — repost detection stays fast on large scan histories.

Performance

  • Duplicate-posting detection no longer degrades to O(N²) on a big scan-history.tsv. The per-company title grouping was a nested loop that paid a full roleFuzzyMatch (re-tokenizing both titles) on every pair; it is now an inverted index — bucket rows by exact title in one pass, then fuzzy-match only over DISTINCT buckets that share a discriminating (non-baseline) token. Output is identical — the exact same repost clusters — proven by a differential test against the old algorithm across 200+ randomised histories.

Notes

  • groupRowsByTitle in server/lib/detect-reposts.mjs (exported for the differential test). tests/detect-reposts-grouping.test.mjs (+2). Suite: 2521.

[1.194.0] — 2026-08-14

Fixed (scanner) — Workday career pages with a single-segment URL now scan correctly.

Fixed

  • The Workday adapter now parses careers URLs whose path is a single segment — e.g. https://parsons.wd5.myworkdayjobs.com/Search, .../KBR_Careers, .../Careers. Before, the site fell back to External, so the adapter hit the wrong CXS endpoint (/wday/cxs/<tenant>/External/jobs) and a probe could look healthy while returning nothing. It now takes the first non-empty path segment (after an optional locale) as the site (dropping a locale prefix like en-US); the documented /en-US/External case is unchanged. (Reported in #255.)

Notes

  • Structural path parse in server/lib/portals/adapters/workday.mjs (replaces the two-segment regex). tests/workday-adapter-endpoint.test.mjs (+7: single-segment, two-segment, deep-link, locale-only default, uppercase host, api pass-through, matches). Suite: 2519.

[1.193.0] — 2026-08-14

Added (stats) — a “Silent after interview” tab that surfaces interviews worth a nudge.

Added

  • A Silent after interview tab in #/stats: interviews that have gone quiet past a courtesy window (default 30 days), joining your active interviews and tracker — with how long each has been silent, the last interview date, and the reason. A gentle nudge / closure list; suggestion-only, never a rejection claim. Zero-token.

Notes

  • New GET /api/stats/rejection-latency relay (fail-soft {available:false} when the parent script is absent). tests/stats-rejection-latency-route.test.mjs (+2). +10 i18n keys ×17; #/stats help-hint bumped 7→8 tabs. Suite: 2510.

[1.192.0] — 2026-08-14

Added (cv-studio) — a “Fact-check your CV” gate that catches numbers you never actually had.

Added

  • A Fact-check your CV card in #/cv-studio: paste a tailored CV or cover letter and check every asserted metric and fact against your real CV, profile, and two-pager. You get a pass / warn / block verdict plus the exact invented metrics, unsupported facts, and forbidden / advisory phrases — so a generated résumé can’t quietly claim a number that isn’t yours. Zero-LLM; nothing is written.

Notes

  • New POST /api/cv-studio/verify-facts relay: writes the text to a throwaway temp file (never your files) and runs verify-cv-facts.mjs against it, trusting the JSON verdict even though the script exits 1 on a block. tests/cv-studio-verify-facts-route.test.mjs (+4). +15 i18n keys ×17. Suite: 2508.

[1.191.0] — 2026-08-14

Added (stats) — a “What to learn next” tab that ranks the skills worth learning first.

Added

  • A What to learn next tab in #/stats: a tracker-wide skill-gap roll-up — the missing skills that most often sank a low-fit match, weighted (by 5−fit-score across every evaluated report) and tiered Critical / High / Medium — plus the skills already covered by your CV/profile. Read-only, suggestions only; zero-token.

Notes

  • New GET /api/stats/upskill relay (carries an { error } field when there is too little data; fail-soft {available:false} when the parent script is absent). tests/stats-upskill-route.test.mjs (+3). +15 i18n keys ×17. Suite: 2504.

[1.190.0] — 2026-08-14

Added (tracker) — a “Company history” panel that tells you which companies actually answer you.

Added

  • A Company history card on #/tracker: pick a company and get read-only evidence — how responsive it has been to you (silent on you / mixed / responded before) and whether the same role keeps getting reposted — joined from your tracker, follow-ups, and scan history. Zero-token; the parent scanner is never called.

Notes

  • New GET /api/stats/company-history[?company=] relay (fail-soft {available:false} when the parent script is absent). tests/stats-company-history-route.test.mjs (+3). +18 i18n keys ×17. Suite: 2501.

[1.189.0] — 2026-08-14

Fixed (scanner) — seniority levels written as roman numerals now count on non-Latin titles too.

Fixed

  • The tier classifier behind skip_tiers now reads a roman-numeral level suffix (I / II / III / IV / V) after a role word in any script — “Инженер III”, “エンジニア I”, “Ingénieur IV” — not only after ASCII words. Before, a level numeral following a non-Latin word was ignored and the posting fell back to mid, so skip_tiers: [senior] or [entry] missed those listings. The match now also lands on the numeral itself, keeping leftmost-marker classification honest.

Notes

  • Script-agnostic lookbehind in server/lib/classify-tier.mjs; removed a dead duplicate Sr. matcher. tests/classify-tier.test.mjs (+1: non-Latin numerals). Suite: 2498.

[1.188.0] — 2026-08-14

Fixed (UI) — lead action buttons no longer sit flush against the page subtitle.

Fixed

  • The primary action / control row on Weekly interview digest, Funded companies, Portals, Career plan, and Career orientation now has a proper top margin, so the button breathes below the page subtitle instead of butting against it.

Notes

  • Regression guard tests/lead-row-top-margin.test.mjs (+5). Suite: 2497.

[1.187.0] — 2026-08-14

Fixed (scanner) — the skip_tiers setting works again: postings you asked to skip by seniority are dropped.

Fixed

  • A skip_tiers: list in portals.yml (e.g. skip_tiers: [intern, entry]) is now honoured by the scan. Each posting’s title is classified into a seniority tier (intern / entry / mid / senior) and dropped if its tier is in your list. Previously the scan ran the title / location / content / trust filters but had no tier filter, so skip_tiers was silently ignored. Titles with no explicit level word fall back to mid (so skip_tiers: [mid] also drops most plain listings), and the classifier reads the LEFTMOST level word — “Summer Intern, Director of Product” is correctly an internship, not a directorship.

Notes

  • New pure server/lib/classify-tier.mjs (classifyTier + buildTierFilter), wired into both the EN and RU scanner filter chains. tests/classify-tier.test.mjs (+7). Suite: 2492.

[1.186.0] — 2026-08-14

Added (CV Studio) — a “Skill gap” panel: which of a job’s required skills your CV names, implies, or is missing.

Added

  • A new Skill gap panel in CV Studio. Pick a saved job description and it sorts each required skill into named in your CV, implied in your CV, or missing — zero-LLM word matching, nothing written. A low-confidence note appears when the job posting had no clear requirements section.

Notes

  • New GET /api/jds/:name/skill-gap (the JD name is path-sanitized and confirmed under jds/ before it becomes a script arg; fail-soft {available:false} without the script). +13 i18n keys ×17. Tests: tests/jds-skill-gap-route.test.mjs (+4, incl. path-traversal rejection). Suite: 2485.

[1.185.0] — 2026-08-14

Added (stats) — a “Funnel & velocity” tab: how your funnel compares to the market and how fast you move between stages.

Added

  • A new Funnel & velocity tab in Statistics shows your response and interview rates next to candidate-side market benchmark ranges (with the small-sample and selection-bias caveats kept intact), a waiting list of in-flight applications past the typical first-response window, and median days per stage (Applied → Responded → Interview → Offer) — with slow-moving rows right-censored so they don’t bias the medians. Read-only and zero-token; it reads only your own tracker.

Notes

  • New GET /api/stats/funnel (fail-soft {available:false} when the script isn’t present). +18 i18n keys ×17. Tests: tests/stats-funnel-route.test.mjs (+2). Suite: 2481.

[1.184.0] — 2026-08-14

Fixed (UI) — the Dashboard quick-action tiles now line up in an even grid.

Fixed

  • On the Dashboard (Command Center), a group of 3 action tiles used to render wider than a group of 4, so the sections stacked with a ragged right edge. Every group now uses equal-width columns (4 on a wide screen, stepping down to 3 / 2 / 1 as the window narrows), so all tiles are the same size and their right edges align.

Notes

  • CSS only (.qa-grid: fixed repeat(N, minmax(0,1fr)) instead of auto-fill). Guarded by tests/dashboard-grid-align.test.mjs (+2). Suite: 2479.

[1.183.0] — 2026-08-14

Added (scanner) — smarter duplicate detection: the same job re-listed with a tracking link no longer shows up twice.

Added

  • The scanner now recognises a posting by a canonical URL key, so the same role re-listed with a tracking parameter (?utm_…, gclid, …), over http vs https, or with a trailing slash / #fragment is treated as the one posting it is — no duplicate row in your scan results or pipeline, and no wasted evaluation on a job you have already seen. Genuinely different postings (a kept functional id like gh_jid) still count separately.

Notes

  • New server/lib/url-key.mjs, wired into both scanners’ dedup and the pipeline writer. It deliberately under-strips — it never merges two distinct postings. Tests: tests/url-key.test.mjs (+5), tests/parsers.test.mjs (+1). Suite: 2477 (+6).

[1.182.0] — 2026-08-14

Fixed (scanner) — salary ranges now read the same in every language.

Fixed

  • Salary figures in scan and tracker rows use the locale-neutral symbols ≥ and ≤ (e.g. ≥ 120000 EUR, ≤ 90000) instead of the English words “from” / “up to”, which leaked untranslated into non-English UIs. Applies to every board that reports a one-sided pay range (Getro, Remotli, Manfred, Agentic Jobs, JustJoin, Jobicy); two-sided ranges (100000–150000 USD) were already language-neutral.

Notes

  • Display only — the client salary filter parses the numbers regardless of prefix, so filtering is unchanged. Suite: 2471.

[1.181.0] — 2026-08-14

Added (scanner) — Getro job boards now show salary, every location, and remote roles.

Added

  • The Getro scanner (VC talent-network boards) now surfaces a salary figure on each role (annual pay range + currency), lists all of a role’s locations instead of just the first, and tags remote roles. A Getro posting in your scan and tracker now carries the same salary + location detail as every other board.

Notes

  • Scanner-only; no new dependency, no route / CSP / SSRF change. Tests: tests/sources-getro.test.mjs (+5). Suite: 2470 (+5).

[1.180.0] — 2026-08-14

Fixed (MEDIUM, reports) — the #/reports list is now a table, and a real score a Machine Summary placeholder was hiding is recovered.

Fixed

  • The #/reports list is a table (Report · Date · Legitimacy · Score), not a 4-card grid. A long “Score not detected” chip used to squeeze the title column to near-zero, and the card title’s overflow-wrap: anywhere then broke the report name one character per line. Every field now has its own column, the report-name cell wraps at word boundaries, and the table scrolls horizontally on a narrow viewport (new .reports-scroll container). New i18n key rep.colReport ×17.
  • A real body score (**Итоговый балл:** 1.8 / 5) is no longer hidden by a Machine Summary placeholder (score: —). When the ## Machine Summary block carried a non-numeric or out-of-range score it filled the parsed score slot and blocked the bold-value-form fallback, so the report showed “Score not detected” despite a real X / 5 in the body. parseReportHeader now recovers the body value form whenever no usable number survived (step 4.5).

Notes

  • Client + parser only; no route / CSP / SSRF / parent-write change. Tests: tests/reports-table.test.mjs (+5), tests/report-header-locale.test.mjs (+2). Suite: 2465 (+7).

[1.179.0] — 2026-08-13

Changed (LOW, scanner) — consolidated 20 duplicate HTML-entity decoders onto the shared module (parent-sync follow-up, closes the worklist).

Changed

  • 20 scraping scan sources each carried their own decodeEntities/decodeXmlEntities (+ a fromCodePoint helper) — copies that had drifted (three could throw a RangeError, fixed in v1.172.0; others admitted NUL/C0 or mis-parsed &#1a2;). All now route through the single server/lib/html-entities.mjs (the XML-1.0-Char-safe decoder), removing ~237 lines of duplication. The 8 RSS-style sources gained &nbsp; decoding (they handled only 5 named entities before); cryptocurrencyjobs’s deliberate double-decode is preserved via an alias. hh keeps its own decoder (it handles &mdash;/&ndash;, outside the shared 6). A new guard test fails if any source re-grows a local decoder.

Notes

  • Behaviour-preserving refactor; no route / CSP / SSRF / parent-write change. Tests: tests/decoder-consolidation.test.mjs (+2). Suite: 2458 (+2).

[1.178.0] — 2026-08-13

Fixed (LOW, parent-parity) — refreshed two stale constants to match the parent (PARENT-SYNC GAP #4 + #5).

Fixed

  • Browser User-Agent (GAP #4) — BROWSER_LIKE_USER_AGENT (sent by workable/workday/oraclecloud/a16z/eightfold to clear WAF/bot gates) bumped Chrome 131 → 151, matching the parent’s user-agent.mjs; a stale build is likelier to be challenged. Guarded by a Chrome major ≥ 151 test.
  • Tracker states FALLBACK (GAP #5) — states.mjs’s last-resort FALLBACK (used only when the live templates/states.yml is unreadable — a fresh clone / CI-isolated root) gained the parent’s Turkish status aliases (#2615): değerlendirildi, başvuruldu, yanıt verildi, mülakat, teklif, reddedildi, iptal edildi, uygun değil, kabul edildi/işe alındı. In production the live file already provided these.

Notes

  • Two constants only; no route / CSP / SSRF / parent-write change. Tests: tests/http-json.test.mjs (+1) + tests/states.test.mjs (+1). Suite: 2456 (+2).

[1.177.0] — 2026-08-13

Fixed (MEDIUM, scanner) — csod (Cornerstone) returned 0 jobs on tenants that gate the search API behind session cookies (parent #2769, PARENT-SYNC GAP #1).

Fixed

  • Some Cornerstone tenants set session cookies on the bootstrap career-site home page and answer 401 CSOD Unauthorized on the search API unless those cookies come back alongside the anonymous bearer token. sources/csod.mjs now reads the bootstrap through a new fetchResponse helper, builds a Cookie header from its Set-Cookie values (cookieHeaderFrom — name=value only, jar semantics), and replays it on the search POST. Same-origin only (host-pinned + redirect:'error'), so session cookies can never reach a third party; a tenant that sets no cookies behaves exactly as before.

Notes

  • New server/lib/http-json.mjs::fetchResponse (additive; existing sources unaffected). No route / CSP / SSRF / parent-write change. Tests: tests/sources-parity-v1118a.test.mjs (+1). Suite: 2454 (+1).

[1.176.0] — 2026-08-13

Fixed (MEDIUM, reports) — a score under a bold label the RU table doesn’t list still read “Score not detected” (FIND-5).

Fixed

  • Two RU reports wrote the score as **Итоговый балл:** 1.8 / 5 / **Скор:** 1.8 / 5 — bold labels REPORT_LABELS.ru doesn’t enumerate (it knows only “Оценка”/“Балл”), so the score stayed unparsed. Rather than grow a synonym list, parseReportHeader now falls back to the value form: a fraction over the /5 rubric under ANY bold label. It’s language-independent, immune to a heading (no **, no /5 value), and rejects a date like 5/5/2026 (negative lookahead on the denominator).

Notes

  • Server parser only; no route / CSP / SSRF / parent-write change. Tests: tests/report-header-locale.test.mjs (+2: the two RU labels + a date-not-a-score guard). Suite: 2453 (+2).

[1.175.0] — 2026-08-13

Fixed (LOW, hardening) — a regression guard for the FIND-3 SEO description + a nullish-safe legitimacy strip (AI-review follow-up).

Fixed

  • SEO description parity guard — the v1.174.0 fix that swapped a hard-coded “~55” in every locale’s meta.desc for a registry-derived {adapters} placeholder had no test, so it could silently regress on the next locale edit. New CI-isolated tests/site-meta-desc-parity.test.mjs fails if any of the 17 site/src/i18n/*.json drops the placeholder or re-hard-codes a count, or if Landing.astro stops interpolating it into all three description metas.
  • Nullish-safe legitimacy strip — stripEmphasis returns '' for a nullish input instead of the string “undefined” (fields are string-initialized, so this is defense-in-depth).

Notes

  • Test + a one-line parser guard; no route / CSP / SSRF / parent-write change. Tests: tests/site-meta-desc-parity.test.mjs (+3). Suite: 2451 (+3).

[1.174.0] — 2026-08-13

Fixed (HIGH, reports) — localized reports read “Score not detected”; the SEO description was stale.

Fixed

  • Score parsing (FIND-1) — a non-English report whose H1 contains the score-label word (# Оценка вакансии: <title>) no longer has that title mistaken for the score. parseReportHeader now anchors on the localized bold label (**Оценка:** 1.5 / 5), skips heading lines, and requires the label adjacent to its colon — so RU reports that showed “Score not detected” render their real score.
  • Legitimacy chip (FIND-2) — markdown emphasis is stripped from the value, so a chip reads “High Confidence”, not “** High Confidence”.
  • Score overflow — a score line with trailing status text (“1.8, Status: Evaluated, …”) is compacted to just the score; .score-pill gains a no-wrap/overflow cap and the card’s title column can shrink, so a coloured chip never spills past the card edge.
  • SEO description (FIND-3) — the cvstart.org meta / OG / Twitter descriptions (all 17 locales) hard-coded “Scan ~55 job boards” while the body counted the live registry (“~75”). The description now interpolates the registry-derived count, so it can’t drift again.

Notes

  • Server parser + client render/CSS + site i18n; no route / CSP / SSRF / parent-write change. Tests: tests/report-header-locale.test.mjs (+4). Suite: 2448 (+4).

[1.173.0] — 2026-08-13

Added (LOW, config) — Hermes joins the detected AI-CLI roster (career-ops parity).

Added

  • The #/config → “AI CLI tools” tab now probes for Hermes (Nous Research), the parent’s newly supported agent runtime (binary hermes). server/lib/routes/cli-detect.mjs grows its fixed allowlist from 10 to 11 tools; detection stays a read-only PATH scan (no binary is ever executed).

Notes

  • No i18n / route / CSP / SSRF / parent-write change; the roster is a fixed allowlist, never input. Suite: 2444 (the cli-detect canary updated 10 → 11).

[1.172.0] — 2026-08-13

Fixed (MEDIUM, scanner) — a malformed HTML entity could crash a scan source (career-ops #2150 parity).

Fixed

  • The oraclecloud, gem and dassault sources decoded numeric HTML entities with a bare Number.isFinite guard before String.fromCodePoint — a reference above 0x10FFFF (e.g. &#99999999; from a malformed or adversarial feed) threw an uncaught RangeError and aborted that source’s entire parse. A shared server/lib/html-entities.mjs (mirroring the parent’s _html-entities.mjs) now restricts numeric references to the XML 1.0 §2.2 Char set so String.fromCodePoint can never throw, and matches hex vs decimal separately so &#1a2; no longer mis-parses. The three sources import it.

Notes

  • No change for valid feeds; no JS / i18n / route / CSP / SSRF / parent-write change. Consolidating the ~20 remaining in-source decoder copies is tracked in qa/PARENT-SYNC-WORKLIST-v1.26.0.md.
  • Tests: tests/html-entities.test.mjs (+7). Suite: 2444 (+7).

[1.171.0] — 2026-08-13

Changed (LOW, design-system) — type-scale + z-index layer tokens (D-4, first step). Sizes and stacking were literal per component; the system couldn’t be reproduced from tokens alone.

Changed

  • z-index layers — introduced named --z-* tokens (--z-topbar … --z-skiplink) and migrated every z-index literal to them. Values are preserved, so stacking is byte-identical; a new canary forbids fresh bare z-index magic numbers.
  • Type scale — introduced a --font-size-* ramp (xs 11 / sm 12 / md 13 / base 15 / lg 18 / xl 22 / 2xl 28, base = Inter 15px) and migrated the core sizes the components already used (value-preserving, zero visual change). Off-ramp one-offs (14 / 16 / 20 / 24 …) and weight/line-height tokens migrate incrementally as components are touched — tracked in docs/UX-ROADMAP.md.

Notes

  • CSS-token only; no behaviour, JS, i18n, route, CSP, SSRF, or parent-write change. No pixel changes (all migrations preserve the exact value).
  • Tests: tests/design-tokens-scale.test.mjs (+3: tokens defined, no bare z-index, base font uses the ramp). Suite: 2437 (+3).

[1.170.0] — 2026-08-13

Added (LOW) — honest ETA hints on long AI generations (P4-ETA). Heavy generations (career-plan ~40 s observed, orientation / market / networking ~30 s, two-pager AI-fill ~20 s) showed a bare “Generating…” with no sense of how long to wait.

Added

  • Each long-generation button now carries a muted ⏱ ~Ns hint next to it, mirroring the #/auto ETA (set expectations before the click). Shared .eta-hint style + two generic i18n keys — common.eta (~{n}s, substituted per page) and common.etaTitle (“Typical generation time”) — so a page passes its typical seconds without a new key each time.

Notes

  • Client-only; no route, CSP, SSRF, or parent-write change. +2 i18n keys ×17 (common.eta, common.etaTitle; snapshot 1219 → 1221).
  • Tests: tests/generation-eta-hint.test.mjs (+2). Suite: 2434 (+2).

[1.169.0] — 2026-08-13

Added (LOW) — inline PDF preview (D-5). GET /api/output/pdfs/:name forced Content-Disposition: attachment, so even the #/cv “Open” link downloaded the file instead of showing it — the docs stress “review it before sending it anywhere”.

Added

  • ?inline=1 on GET /api/output/pdfs/:name serves the SAME sanitized file with Content-Disposition: inline, so the browser renders it in a new tab for a review-before-send 👁 Preview. The default (no param) stays a download (attachment). No new route/surface; the same path-name guards apply.
  • The #/cv generated-PDF list’s first button is now 👁 Preview (opens ?inline=1) next to the unchanged ⬇ Download. cv.openPdf reworded “Open” → “Preview” ×17.

Notes

  • No CSP/SSRF change — same sanitizePathName gate; inline only affects the disposition header on the user’s own generated PDF (served application/pdf). One existing i18n key reworded ×17 (no new keys; snapshot 1219).
  • Tests: tests/output-pdfs.test.mjs (+3: inline header, default-attachment, path guards under ?inline=1). Suite: 2432 (+3).

[1.168.0] — 2026-08-13

Fixed (LOW, a11y) — checkbox rows now meet the WCAG 2.5.8 24×24 target-size floor (D-2). Checkbox/radio labels on #/scan, #/config, #/evaluate and #/cv-studio sat in a ~22 px band — 2 px under the minimum.

Fixed

  • A scoped label:has(> input[type="checkbox"/"radio"]) { min-height: 24px } rule guarantees a ≥24 px clickable band. min-height only — every such label is already flex/inline-flex, so nothing shifts; .apply-checklist (32 px) was already compliant. Completes the design-export a11y target-size items alongside the v1.162.0 ? fix.

Notes

  • CSS-only; no behaviour, JS, i18n, route, CSP, SSRF, or parent-write change.
  • Tests: tests/checkbox-target-size.test.mjs (+1). Suite: 2429 (+1).

[1.167.0] — 2026-08-13

Fixed (LOW, design-system) — elevated surfaces now separate from hairlines (D-3). The raised-surface tokens --panel-2 / --surface-elev1 resolved to --slate — the same value as the --line / --border hairlines — so an elevated panel or chip inside a bordered card had no visual separation.

Fixed

  • A dedicated theme-aware --elev token now backs the raised surfaces (#eef1f6 light / #1e232e dark, distinct from --slate in both themes); the hairlines stay on --slate. Elevated chips (pipeline, two-pager, docs-assistant / mock-interview bubbles, funded bars) and panels now read as raised.
  • The remaining design-export findings (D-2 checkbox target size, D-4 type-scale / z-index tokens, D-5 inline PDF preview, P4 generation-ETA) are restated as tracked backlog in docs/UX-ROADMAP.md — each its own future release (behaviour changes are never bundled into a token ship).

Notes

  • CSS-token only; no behaviour, JS, i18n, route, CSP, SSRF, or parent-write change. Dark-mode contrast guard (tests/dark-theme-tokens.test.mjs) green.
  • Tests: tests/elevation-token.test.mjs (+2). Suite: 2428 (+2).

[1.166.0] — 2026-08-13

Fixed (LOW) — rubric terminology now mirrors the canonical docs. career-ops.org/docs describes the scoring rubric as “five scoring dimensions plus a holistic global score”, but the web-ui, cvstart.org and the wiki all said “six-dimension rubric” — the numbers reconcile (5 + 1 = 6) but the vocabulary didn’t.

Fixed

  • Adopted the docs’ phrasing — “five dimensions plus a holistic global score” — consistently across README ×17, the cvstart.org site copy ×17 (method.* / how.3 / features.2 / meta.methodologyDesc), the in-app help guide ×17, docs/career-ops-canonical.md, and the wiki (Home ×17 + Features). The 6th factor (global fit) is now framed as the holistic global score, matching the docs.

Notes

  • Docs/marketing copy only; no code, i18n-dict key, route, CSP, SSRF, or parent-write change (the SPA UI never used the phrase).
  • Tests: tests/rubric-terminology.test.mjs (+2, guards the English source surfaces against the “six-dimension” drift). Suite: 2426 (+2).

[1.165.0] — 2026-08-13

Fixed (LOW) — the “Two-pager” term is now consistent within every locale. In Arabic the sidebar showed the Latin “Two-pager” while the page <h1> was fully localized (“الصفحتان الخاصتان بك”) — the only Latin string in an otherwise mirrored RTL nav.

Fixed

  • Decision enforced: per locale, nav.twoPager and twoPager.title agree on the term — both keep the Latin product noun, or both localize it. Only Arabic was split; its nav label is now localized (“الصفحتان”) to match its title. A new canary fails if any locale splits them again.

Notes

  • Copy-only; no route, CSP, SSRF, or parent-write change. One i18n value changed (ar); no new keys (snapshot 1219).
  • Tests: tests/two-pager-term-consistency.test.mjs (+2). Suite: 2424 (+2).

[1.164.0] — 2026-08-13

Fixed (LOW) — the top-bar search placeholder no longer overflows in any locale. “Find a company, role or URL…” was clipped (nowrap, scrollWidth > clientWidth) when the searchbar flex-shrinks on a busy top bar — and the “…or URL” half, which teaches the paste-a-URL auto-pipeline flow, was never visible.

Fixed

  • top.search (×17) is now the short “Search or paste a URL” (≤24 chars in every locale), so it fits even in a narrow searchbar while keeping the URL affordance. The hardcoded index.html placeholder fallback matches. The input’s aria-label still conveys the full “Cmd+K … paste a URL and Enter” detail.

Notes

  • Copy-only; no route, CSP, SSRF, or parent-write change. One existing i18n key reworded ×17 (no new keys; snapshot 1219).
  • Tests: tests/search-placeholder-fit.test.mjs (+2). Suite: 2422 (+2).

[1.163.0] — 2026-08-13

Fixed (LOW) — the in-app “Ask the docs” assistant now covers exporting a report to PDF. Asked “How do I export a report to PDF?”, the assistant answered that the guide didn’t cover it — although #/reports/:slug has a working 📄 Generate PDF control.

Fixed

  • Added an “Export a report to PDF” H3 under §10 Reports in all 17 help bundles (where the button is, that the file lands in output/*.pdf, needs Playwright, review-before-send). The docs-assistant retrieval (splitSections/topSections) now surfaces the Reports section for a PDF-export question.

Notes

  • Docs/help only; no code, route, CSP, SSRF, or parent-write change. Help gate moves 112 → 113 H3 (31 H2 unchanged); updated in help-ru-config-section + locales-de-it-tr.
  • Tests: tests/help-reports-pdf-section.test.mjs (+2: guidance present ×17, retrieval surfaces Reports). Suite: 2420 (+2).

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