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.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_URLfor Ollama — in App settings and it joins theautoorder after Hermes; pin any one withLLM_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 inserver/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_filterkeyword 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#/scanpage 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
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: trueboard skip, and the anchored-keywordstem:/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
#/scanSource filter, or add aprovider: mycareersfuturecompany with an optionalkeywordslist (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
workplaceTypewins over a staleisRemoteflag 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
#/scanSource filter, or add aprovider: jobbankcacompany with an optionalkeywordslist (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
locationand the rest inallLocations; 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
#/scanSource filter, or add a company withprovider: 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 (
é→ é,ç→ ç, …), so a European board that writesDéveloppeurorFrançaisno longer lands that literal in a job title, the tracker, or a generated document. (Uppercase names stay uppercase —É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
#/scanSource filter, or add a company withprovider: 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
heightbecame amin-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: 0on grid items,overflow-wrapon 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 explicitlymarkdown/md, so a genuinepython/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/outcomerelays the outcome CLI:dryRun:trueis 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.mddocuments the five routes behind these features, and the route/version counters inCLAUDE.mdanddocs/sdd/CONVENTIONS.mdare 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(relaysassessment-log.mjs’s default JSON list; fail-soft{available:false}) +POST /api/assessments(explicit write: fields passed as array args toassessment-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.mdandconfig/profile.ymland 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-checkrelays the parent’scv-sync-check.mjs, which prints human text + an exit code (no--json); the route does light structured parsing of its stableERROR:/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/reuserelays the parent’sjd-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.ymlhappens only when you click Add.
Notes
- New
server/lib/discover-ats.mjs(fixed-host, charset-validated slug probe through the DNS-pinnedsafeGet, ≤12 probes/request, cross-origin redirect → not resolved) +POST /api/portals/discover(read-only) andPOST /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.mduses non-English or variant column headers — Spanishempresa/puesto/estado/fecha/enlace, orposition/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_ALIASESmap + a normalization fold inparseApplications(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.mjsand a read-onlyGET /api/liveness?url=(no writes, no LLM). SSRF-safe: the URL isisValidJobUrl-gated, then the ATS API is reached only through the DNS-pinnedsafeGetwith 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-wrapinpublic/css/components.cssswitched fromoverflow: hiddentooverflow-x: auto(mirrors the existing.reports-scrollcontainer); 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-Afterfrom the board is honoured (but clamped, so a hostileRetry-After: 86400can’t stall a whole sweep). Permanent errors (404, refused redirects) still fail fast — unchanged.
Notes
- New
parseRetryAfterMs()+ the purecomputeRetryDelayMs()inserver/lib/http-json.mjs;fetchJsonnow captures.retryAfteron a non-ok response andfetchJsonWithRetrytakes an optionalmaxDelayMs(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 owncareers_urland the id auto-resolves from that page on the first scan — a single SSRF-safe GET reads the numericnetwork.idstraight out of the board’s embedded page data. An explicitgetro_collectionstill wins and skips the fetch entirely.
Notes
- New
httpsCareersUrl(),extractCollectionId(), and asyncresolveCollectionId()inserver/lib/sources/getro.mjs; the board page is fetched through the DNS-pinned, size-cappedsafeGet, and the resolved id is still host-pinned toapi.getro.combyassertGetroUrl. The adapter now matches aprovider: getroentry carrying an httpscareers_urleven 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.ymlWorkdayapi:value is now accepted only when its hostname ismyworkdayjobs.com(or a.myworkdayjobs.comsubdomain). The old check was a substring match, so any URL that merely contained the string — e.g.https://example.com/?x=myworkdayjobs.comorhttps://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 inserver/lib/portals/adapters/workday.mjs; used by bothmatches()andbuildEndpoint().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 fullroleFuzzyMatch(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
groupRowsByTitleinserver/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 toExternal, 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 likeen-US); the documented/en-US/Externalcase 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-latencyrelay (fail-soft{available:false}when the parent script is absent).tests/stats-rejection-latency-route.test.mjs(+2). +10 i18n keys ×17;#/statshelp-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-factsrelay: writes the text to a throwaway temp file (never your files) and runsverify-cv-facts.mjsagainst 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/upskillrelay (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_tiersnow 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, soskip_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 duplicateSr.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 inportals.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, soskip_tierswas silently ignored. Titles with no explicit level word fall back to mid (soskip_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 underjds/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: fixedrepeat(N, minmax(0,1fr))instead ofauto-fill). Guarded bytests/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, …), overhttpvshttps, or with a trailing slash /#fragmentis 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 likegh_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
#/reportslist 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’soverflow-wrap: anywherethen 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-scrollcontainer). New i18n keyrep.colReport×17. - A real body score (
**Итоговый балл:** 1.8 / 5) is no longer hidden by a Machine Summary placeholder (score: —). When the## Machine Summaryblock 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 realX / 5in the body.parseReportHeadernow 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(+ afromCodePointhelper) — copies that had drifted (three could throw aRangeError, fixed in v1.172.0; others admitted NUL/C0 or mis-parseda2;). All now route through the singleserver/lib/html-entities.mjs(the XML-1.0-Char-safe decoder), removing ~237 lines of duplication. The 8 RSS-style sources gained decoding (they handled only 5 named entities before); cryptocurrencyjobs’s deliberate double-decode is preserved via an alias.hhkeeps its own decoder (it handles—/–, 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’suser-agent.mjs; a stale build is likelier to be challenged. Guarded by aChrome major ≥ 151test. - Tracker states FALLBACK (GAP #5) —
states.mjs’s last-resortFALLBACK(used only when the livetemplates/states.ymlis 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 Unauthorizedon the search API unless those cookies come back alongside the anonymous bearer token.sources/csod.mjsnow reads the bootstrap through a newfetchResponsehelper, builds aCookieheader from itsSet-Cookievalues (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 labelsREPORT_LABELS.rudoesn’t enumerate (it knows only “Оценка”/“Балл”), so the score stayed unparsed. Rather than grow a synonym list,parseReportHeadernow 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/5value), and rejects a date like5/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.descfor a registry-derived{adapters}placeholder had no test, so it could silently regress on the next locale edit. New CI-isolatedtests/site-meta-desc-parity.test.mjsfails if any of the 17site/src/i18n/*.jsondrops the placeholder or re-hard-codes a count, or ifLanding.astrostops interpolating it into all three description metas. - Nullish-safe legitimacy strip —
stripEmphasisreturns''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.parseReportHeadernow 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-pillgains 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 (binaryhermes).server/lib/routes/cli-detect.mjsgrows 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,gemanddassaultsources decoded numeric HTML entities with a bareNumber.isFiniteguard beforeString.fromCodePoint— a reference above0x10FFFF(e.g.�from a malformed or adversarial feed) threw an uncaughtRangeErrorand aborted that source’s entire parse. A sharedserver/lib/html-entities.mjs(mirroring the parent’s_html-entities.mjs) now restricts numeric references to the XML 1.0 §2.2 Char set soString.fromCodePointcan never throw, and matches hex vs decimal separately soa2;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 indocs/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
⏱ ~Nshint next to it, mirroring the#/autoETA (set expectations before the click). Shared.eta-hintstyle + two generic i18n keys —common.eta(~{n}s, substituted per page) andcommon.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=1onGET /api/output/pdfs/:nameserves the SAME sanitized file withContent-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
#/cvgenerated-PDF list’s first button is now 👁 Preview (opens?inline=1) next to the unchanged ⬇ Download.cv.openPdfreworded “Open” → “Preview” ×17.
Notes
- No CSP/SSRF change — same
sanitizePathNamegate;inlineonly affects the disposition header on the user’s own generated PDF (servedapplication/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-heightonly — 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
--elevtoken now backs the raised surfaces (#eef1f6light /#1e232edark, distinct from--slatein 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.twoPagerandtwoPager.titleagree 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 hardcodedindex.htmlplaceholder fallback matches. The input’saria-labelstill 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).
[1.162.0] — 2026-08-13
Fixed (MEDIUM) — the “?” help affordance is now a ≥24×24 pointer target (WCAG 2.5.8). .help-hint measured 18×18 px with padding:0, below the Target Size (Minimum) floor, on every page heading.
Fixed
.help-hintbox is now 24×24 (the measurable pointer target) while the visible ring stays 18px, drawn by a centered::before— so the glyph size and the<h1>baseline/line-height are unchanged (page-title lines are taller than 24px). Hover / active / focus-visible states move with the ring; the margin trims 6→3px so the gap to the title is identical. The v1.158.0document.titleclone-strip (which excludes.help-hint) is untouched.
Notes
- CSS-only; no JS, i18n, route, CSP, SSRF, or parent-write change.
- Tests:
tests/help-hint-target-size.test.mjs(+2). Suite: 2418 (+2).
[1.161.0] — 2026-08-13
Fixed (MEDIUM) — #/reports shows a “score not detected” chip instead of blank space. After the v1.159.0 locale-aware parser, a report that still has no parseable score rendered an empty metadata area — indistinguishable from a failed evaluation, with no recovery affordance.
Fixed
- The score cell now branches: a parsed score → the tone pill; no score → a muted
.score-mutedchip reading “Score not detected” (localized ×17) with an “Open the report to see the score” tooltip. The card stays a keyboard-operablerole="link"that opens the report (where the score lives in the body), and the date still renders (v1.159.0 mtime fallback), so cards never lose their anchor. - Reuses the existing neutral tone token — no new colour (
score-tone.jsalready specifies a no-score row as muted).
Notes
- Client-only; no route/CSP/SSRF/parent-write change. +2 i18n keys ×17 (
rep.scoreUnparsed,rep.scoreUnparsedHint; snapshot 1217 → 1219). - Tests:
tests/reports-unparsed-chip.test.mjs(+3). Suite: 2416 (+3).
[1.160.0] — 2026-08-13
Fixed (HIGH) — provider copy no longer contradicts the 7-provider promise. #/config said the web-ui live eval “uses your Anthropic or Gemini API key” and that the OpenAI key is “not used by the web UI itself”; #/dashboard’s Evaluate card said “Anthropic-first scoring” — all false since the OpenRouter / 7-provider cascade shipped (v1.157.0), and self-contradicted on the same screen (the header chip read Active: OpenRouter).
Fixed
config.providerModelNote(×17) now states the truth: the headless ⚡ live eval runs on any one of your seven provider keys — Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub Models · Hermes — auto-ordered, with fallback when a pinned provider has no key. The false “OpenAI … not used by the web UI itself” sentence is removed.dash.quick.evaluateSub(×17) is now vendor-neutral (“0–5 fit scoring”), not “Anthropic-first scoring”; thedashboard.jsfallback matches.Keys: N / 5→N / 7— the denominator now matches the seven configurable provider key slots (SECRET_KEYS), the same second-order drift.
Notes
- Copy-only; no route, CSP, SSRF, or parent-write change. Two existing i18n keys reworded ×17 (no new keys; snapshot stays 1217).
- Tests:
tests/provider-copy-honesty.test.mjs(+3: ≥5 provider names ×17, EN drops the exclusivity claims, Evaluate subtitle vendor-neutral ×17). Suite: 2413 (+3).
[1.159.0] — 2026-08-13
Fixed (HIGH) — report metadata is no longer language-coupled. Reports generated in a non-English locale rendered a blank metadata strip on #/reports (no score pill, date, or legitimacy chip), because parseReportHeader matched only English **Score:** / **Legitimacy:** / **Date:** bold labels — so the docs’ “Score → next step” table sat above cards that showed no score.
Fixed
parseReportHeadernow parses the language-invariant## Machine SummaryYAML block. Precedence: English bold labels (keeps EN reports byte-identical) → the## Machine Summaryblock’sscore:/legitimacy:/date:keys (the same locale-free sourceauto-pipelinealready reads) → locale-aware prose labels (REPORT_LABELS, all 17 locales) as a last resort. A Russian/Japanese/Arabic report now yields a realscoreNum, date and legitimacy.- Locale-tolerant score parsing —
1.5/5,1.5 / 5,1,5/5(comma decimal),1.5 из 5,4.5 out of 5and bare4.5all resolve; out-of-range (0–5) values reject. - Date never null — falls back to the report file’s mtime (already in the list payload) when the body has no parseable date, so cards keep their chronological anchor.
Notes
- Read/parse only — no route, CSP, SSRF, or parent-write change; parent files stay read-only. No i18n-dict key change.
- Tests:
tests/report-header-locale.test.mjs(+8: EN byte-identity, RU Machine-Summary parse, mtime fallback, unparsed shape, numeric tolerance, 17-locale label coverage, localized-prose fallback) + a liveGET /api/reportsRU-fixture integration case. Suite: 2410 (+8).
[1.158.0] — 2026-08-12
Fixed — two cosmetic display bugs (a leaked “?” in tab titles, a wrong provider count on the landing). Display-only; no behaviour, security, or data-flow change.
Fixed
- The HelpHint
?no longer leaks intodocument.title. Views built viaHelpHint.title(text, key)render their<h1>as[<span>text</span>, <button class="help-hint">?</button>]; the router derived the per-route tab title from the rawh1.textContent, so the tab (and the screen-reader “page changed” announcement) read “Vacancy search?” instead of “Vacancy search”.router.js::focusNewViewnow deep-clones the heading, strips.help-hint, then reads the text — the live heading’s visible “?” is untouched. - cvstart.org showed “17 AI providers” instead of “7”. The landing’s
Features.astrosub()helper eagerly rewrote every{n}to the locale count (17) before the per-card override could set the providers card tofacts.providers(7), so the feature card and the stats banner disagreed on the same page.{n}is now resolved per-card (providers → 7, languages → 17).
Notes
- No server, route, CSP, SSRF, or i18n-key change;
facts.jsonshape unchanged (providers: 7,locales: 17). - Tests:
tests/document-title-per-route.test.mjs(+1, source-static guard that the title is computed from a.help-hint-stripped clone). Suite: 2402 tests (+1); Playwright smoke/full-cycle/forms 62/62.
[1.157.0] — 2026-08-12
Fixed — live evals now run on ANY configured provider, not just Anthropic/Gemini. A user with only OPENROUTER_API_KEY set was wrongly forced into manual mode (“set ANTHROPIC_API_KEY or GEMINI_API_KEY…”). Two independent causes: a stale server-side pin, and stale client-side gating.
Fixed
- Root cause — a keyless
LLM_PROVIDERpin dead-ended. Runninginitwith Claude Code writesLLM_PROVIDER=claude; if you later add only, say, an OpenRouter key, the forced-claude routing found no Anthropic key and fell through to a manual prompt — even though OpenRouter was configured and fully supported. Now a forced provider whose key isn’t set falls back to the auto order among the configured providers (a pin that DOES have its key stays forced). Applied consistently inenv-config.mjs::selectActiveProviderand both dispatch cascades (routes/llm.mjs::_provGate+llm-dispatch.mjs::gate), so/api/status/providersand the actual run always agree. - Client gating was stale.
#/deepand the mode-page views (#/contacto,#/interview-prep,#/project, …) decided “Run live vs manual” by probing/api/healthfor onlyANTHROPIC_API_KEY/GEMINI_API_KEY. They now use the newwindow.ProviderStatushelper, which reads/api/status/providers(activeProvider, honoring all seven + the pin). No more ⚡-button that promises a live run the server would refuse. - Misleading copy.
deep.tipManual,deep.needKey, andeval.manualMode(× 17) no longer name only Anthropic/Gemini — they point at “any provider key … in App settings”.config.llmProviderHint(× 17) explains the new pin-fallback.#/dashboard’s system card now shows a single honest Live evals · ready/manual badge (derived from all seven provider rows) instead of Anthropic/Gemini-only badges.
Added
public/js/lib/provider-status.js—window.ProviderStatus(.live()→{ available, engine, activeProvider, keysConfigured }, plus a 7-provider label map): the single client source of truth for live-eval availability. Loaded afterapi.js.
Notes
- No security-surface change — provider endpoints are still trusted config; no route, CSP, or SSRF change. The fallback only picks among keys the operator already configured.
- Tests:
tests/live-provider-gating.test.mjs(+4, source-static guard against the stale 2-provider probe) + aselectActiveProviderkeyless-pin-fallback case; the old “pin with no key → null” assertion updated to the new fallback. 3 newdash.system.*keys × 17 (snapshot 1214 → 1217). - Suite: 2401 tests (+5); Playwright smoke/full-cycle/forms 62/62.
[1.156.0] — 2026-08-12
Refactor — split scan.js under the file-size limit (P-16), + a CodeQL fix. public/js/views/scan.js was 906 lines, over the 800-line hard limit. Two cohesive, behavior-preserving factories were extracted, bringing it to 648 — completing the view-split pair started with config.js in v1.155.0.
Changed
scan/runner.js(new, 276 lines) —window.createScanRunner(ctx)→{ runScanAll, stopScan }: the scan-execution engine (Scan/Stop run-state, the indeterminate/determinate progress bar, the persistent error banner + Retry, the SSE console stream, and the per-source runners for ATS / regional / both).scan/filters.js(new, 76 lines) —window.createScanFilters(refs, deps)→{ applyFilters, resetFilters, getFilterState, setFilterState }: the result-filter state machine that backs saved searches.scan.js(648) wires both viactx/refsbags (the same pattern aslib/scan-results.js); the live-poll teardown timers stay atscan.jstop level (shared classic-<script>scope). Both new files load beforescan.jsinindex.html.
Fixed
- CodeQL
js/useless-assignment-to-local(#428) inconfig/tab-controller.js—let n = i;wherenis reassigned on every non-returning branch →let n;. Harmless dead-store introduced by the v1.155.0 extraction (moved verbatim from config.js), now cleared.
Notes
- Pure refactor, zero behavior change — moved code is byte-identical (de-indent only); no route, server, i18n, or CSS change. Four source-reading tests were repointed to the new files (the
loadScanSrc()helper now concatenatesscan.js+scan/runner.js+scan/filters.js+lib/scan-results.js); assertions otherwise identical. - Both oversized views (
config.js,scan.js) are now under the 800-line limit; P-15/P-16 complete. - Suite: 2396 tests (unchanged — tests repointed, not added); Playwright scan+smoke 26/26 in-browser.
[1.155.0] — 2026-08-12
Refactor — split config.js under the file-size limit (P-15). public/js/views/config.js was 1030 lines, over the project’s 800-line hard limit. Two cohesive, behavior-preserving modules were extracted, bringing it to 783.
Changed
config/field-specs.js(new) — the pure, read-only field-spec data: the curated per-provider model lists + theFIELDSdescriptor table (API keys / runtime / regional).window.ConfigFieldSpecs, loaded before the view.config/tab-controller.js(new) — the#/configtab-bar controller (ARIA tablist + keyboard nav + panel swapping) as acreateConfigTabController(c, panelHost)factory returning{ tabBtn, activate }.config.jsnow references both; the render logic (fieldRow, profile/section editors, save flow) is unchanged. Both new files are wired intoindex.htmlbeforeconfig.js.
Notes
- Pure refactor, zero behavior change — no route, no server, no i18n key, no CSS. Six source-reading tests were repointed to the new files (they assert on the moved field/model/tab markup); the assertions are otherwise identical.
scan.js(906) is intentionally left as-is. Its results-rendering was already extracted tolib/scan-results.js(v1.132.0); the remaining core is a tightly-coupled event-wiring closure bound to ~18 filter/DOM refs, where a mechanical factory extraction would need an 18-argument signature and worsen coupling — the opposite of the rule’s intent. A proper split needs a filter-state model refactor, deferred rather than force-fit.- Suite: 2396 tests (unchanged — tests repointed, not added).
[1.154.0] — 2026-08-12
New guide — “Running the whole stack in the cloud.” career-ops has no cloud/server story of its own, so this adds one: a step-by-step recipe for putting the parent career-ops pipeline, this career-ops-ui viewer, and the AI engine (a Claude subscription via the Claude Code CLI, a local Hermes gateway, or provider API keys) on a small always-on server — provision, pick your engine, and expose it safely. It ships as in-app Help §31 in all 17 languages, a README section, a wiki page, and (via the help mirror) the site.
Added
- In-app Help §31 “Running the whole stack in the cloud” (× 17 locales) — the three moving parts (pipeline / viewer / engine), provision + install (VPS, Node ≥ 18), pick your engine (Claude subscription / Hermes / API keys, mixable), and expose it safely (HTTPS reverse proxy + auth + the CSP/SSRF/XSS/no-secrets invariants that must survive the move off
127.0.0.1). Help bundle grows to 31 H2 / 112 H3 (was 30/108). - README — a
## Run the whole stack in the cloudsection (× 17) pointing to Help §31,docs/integrations/HERMES.md, and the new wiki page. - Wiki — a dedicated Cloud-Deployment page (the three-parts table, provision, engine choice, and the “must survive the move” invariants), linked from Home.
Notes
- Docs-only — no route, no server, no client-code change; no new i18n key (help is Markdown, not the dict). The four help-bundle gate tests move to the 31 H2 / 112 H3 contract (
canonical-docs-coverage,help-ui,help-ru-config-section,locales-de-it-tr). - Grounded in the existing
docs/integrations/HERMES.md§2 cloud-deployment checklist and the parent’s own docs (which recommend Node 22.5+ and the eight first-class agent CLIs). - Suite: 2396 tests (unchanged — the gate assertions were updated, not added).
[1.153.0] — 2026-08-12
Jobvite scanner migrated to the public XML feed (parent-sync). The parent career-ops retired the Jobvite JSON API (it now 302-redirects and returns zero jobs); web-ui’s jobvite source used that same dead endpoint, so any tracked Jobvite company silently scanned empty. This ports the parent’s fix (#2623) into the web-ui source contract: the source now reads the public per-tenant XML feed on a different host, keyed by an opaque companyEId.
Fixed
- Jobvite returned zero jobs — the source fetched
https://jobs.jobvite.com/api/company/{slug}/jobs(retired). It now fetcheshttps://app.jobvite.com/CompanyJobs/Xml.aspx?c={companyEId}and parses the XML<result><job>…payload (CDATA + entity-decode,detail-urlpreferred overapply-url,http:→https:on display-only per-job URLs).
Changed
- companyEId resolution — the tenant key changed from the vanity slug (
tylertech) to an opaquecompanyEId(q6NaVfwI) not present in the careers URL. Resolution order: (1)company_eid:on the portal entry, (2) thec=param of an explicitapi:URL, (3) board-page discovery (scrapecompanyEIdfrom the inline JS). Prefer (1) inportals.yml— one line, survives board redesigns, skips a request. server/lib/http-json.mjs—fetchTextnow attaches.location/.retryAfterto the thrown non-ok error (read-only; lets jobvite tell an empty board — aNoJobs.htmredirect — from a retired tenant, without ever following the redirect). Backward-compatible: both fields arenullfor a plain error, so existingredirect:'error'callers are unaffected.
CI
- Pages deploy no longer flips the status badge on a superseded build —
deploy-pages.ymlusedconcurrency.cancel-in-progress: true, so when a merge touchingdocs/help/**auto-fired a Pages build and a manual dispatch raced it, the older run was cancelled and surfaced as a red “check” (“Some checks were not successful”) even though the site deployed fine. Switched tocancel-in-progress: false(GitHub’s recommended Pages pattern) so a second run queues instead of cancelling — the status stays green.
Notes
- Security — the source pins two hosts (
jobs.jobvite.comfor discovery,app.jobvite.comfor the feed) viaassertJobviteUrlbefore every fetch: https-only, strict-hostname allowlist, no redirect ever followed (redirect:'error'for discovery,redirect:'manual'for the feed — the 3xx is read but never chased). ThecompanyEIdis only ever a?c=query value; feed/board URLs are rebuilt from the resolved slug/eId, never fetched verbatim from user input. Registry source count unchanged (meta.value='jobvite'preserved). - Parent-sync: this was the only web-ui-relevant change in the 17 parent commits since parentVersion 1.26.0 (the others — an LLM re-ranker,
verify-cv-facts,liveness-core, and twoweb/fixes — have no web-ui mirror). - Suite: 2396 tests (+4:
tests/sources-jobvite.test.mjsrewritten for the XML contract — config/c=/discovery eId resolution, XML parse, the two-host guard, empty-feed handling).
[1.152.0] — 2026-08-12
Hermes provider — completed wiring + docs actualization. A detailed code review of the v1.151.0 Hermes integration surfaced two real gaps and four completeness items; all are fixed here, and the whole app’s LLM-provider roster is brought up to the full seven (Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models → Hermes) across every doc surface and all 17 locales.
Fixed
#/configcould not force Hermes — theLLM_PROVIDERdropdown listed only six providers (GitHub was the last), so a user with both a cloud key and a local Hermes could setHERMES_API_KEYbut had no way to force Hermes from the UI (only by hand-editing.env).hermesis now the 8th option (auto+ 7 providers), and the field hint (config.llmProviderHint× 17 + the JS fallback) names the full seven-provider order. Newprovider-selector.test.mjsguard asserts the dropdown can never drift fromLLM_PROVIDERSagain.- Short self-hosted keys were silently rejected —
isUsableKey’s 20-char floor was calibrated for cloud keys, but ahermes gateway‘sAPI_SERVER_KEYis user-chosen and may be short (the Hermes docs’ own examplechange-me-local-devis 19 chars).hasHermesKeynow uses a relaxed 8-char floor (still rejects empty/placeholder junk), so a legitimate short local key is no longer dropped to manual-mode without a diagnostic.
Changed
hermesChatUrlcompletes a bare host —HERMES_BASE_URL=http://127.0.0.1:8642(a mis-paste that drops the/v1) now resolves to…/v1/chat/completionsinstead of a/v1-less 404.- Manual-fallback copy in
routes/llm.mjsnow names Hermes in the “execute via …” provider list. - Provider-roster actualization — the six-provider chain/force-list strings were normalized to the full seven across README (× 17), the in-app help (× 17), the
config.llmProviderHintdict (× 17), anddocs/sdd.CONVENTIONS.mdcorrected to “all 16 non-EN locales”. Assembled-dict snapshot regenerated (1214 keys).
Notes
- Security unchanged — no new route, no SSRF/CSP/sanitizer change. The relaxed key floor only affects the local-loopback Hermes gateway; the provider endpoint stays trusted config (not a scanned job URL).
HERMES_API_KEYremains aSECRET_KEY. - Health/doctor now carries a
HERMES_API_KEYrow (was omitted in v1.151.0), so#/healthandcareer-ops-ui doctorlist all seven providers. - Suite: 2392 tests (+2: the
isUsableKeyminLen guard + the dropdown-vs-LLM_PROVIDERSparity guard).
[1.151.0] — 2026-08-12
Hermes is now a wired LLM provider (Phase 5) — the Phase 5 scoping spike confirmed that Nous Research’s Hermes ships an OpenAI-compatible API Server (hermes gateway → POST /v1/chat/completions), so career-ops-ui now runs live evaluations through a local Hermes exactly like OpenAI/Qwen. Set HERMES_API_KEY in App settings and it joins the auto provider order (last). This closes the roadmap’s final open item — Phase 5, Shape A.
Added
- Hermes LLM provider (Shape A) —
runHermeson the sharedrunOpenAICompatibleclient (server/lib/openai.mjs), gated in both cascades (llm-dispatch.mjs+routes/llm.mjs), added to the auto provider-order tail + theLLM_PROVIDER=hermespin,/api/status/providers, andserver/lib/llm-pricing.mjs. It reaches a user-configured local base URL (defaulthttp://127.0.0.1:8642/v1) with Bearer auth — a configured provider endpoint (like OpenRouter/Qwen), not a user-supplied job URL, so it does not touch theisValidJobUrlSSRF guard. #/configfields —HERMES_API_KEY(secret) +HERMES_BASE_URL+HERMES_MODEL(defaulthermes-agent), with 6 new i18n keys × 17 locales (assembled-dict snapshot 1208 → 1214).
Changed
- The scoping spike is resolved:
docs/integrations/HERMES.md, the in-app help §30 (× 17), the README teaser (× 14), thehermes-bridgeskill, anddocs/UX-ROADMAP.mdall move from “planned / not-yet-wired” to wired (Shape A). Shape B (a bespoke agent-runtime relay) was not needed.
Notes
- Security: the provider fetch is a configured endpoint, identical in category to the existing OpenAI-compatible providers — no new SSRF surface, no CSP/sanitizer change.
HERMES_API_KEYis aSECRET_KEY(never echoed). - Tests (CI-isolated, stubbed transport):
tests/hermes-provider.test.mjs(+5); the v1.146.0 “no Hermes branch” canary is inverted to assert it IS wired; the provider-surface tests updated to the 7-provider order. - Suite: 2390 tests (+5:
tests/hermes-provider.test.mjs).
[1.150.0] — 2026-08-12
Consistent empty states (Phase 4 polish) — every “nothing here yet” panel now renders through the one shared .empty style instead of a few views re-declaring the look inline with a magic 40px. Small visual-consistency fix; the empty states on #/activity, #/cv-studio, #/stats, and #/usage now match every other one (tokenized 48px padding + the dashed border).
Changed
#/activity,#/cv-studio,#/stats,#/usagedropped their redundant inlinestyle: { padding: '40px', textAlign: 'center', color: 'var(--foggy)' }on empty-state panels — all three properties are already provided by the shared.emptyclass (tokenized--space-7= 48px, centered, muted, dashed border). So those four now render pixel-identical to the ~25 other.emptypanels instead of drifting a few px on a hardcoded number.- Genuine per-view overrides (
#/dashboardwidth:100%,#/pipelineborder:none) are untouched — only the pure-redundant re-declarations were removed.
Notes
- Client CSS-usage cleanup only — no route, no server, no i18n key, no CSS-rule change (the
.emptyclass itself is unchanged); app dict snapshot stays 1208. Browser-verified (the#/usageempty panel computes to 48px padding + dashed border, no inline style, 0 console errors). - New canary
tests/empty-state-consistency.test.mjskeeps.emptythe single source of truth — a view may still add a layout-specific override (width/border) but not re-state padding/centering/colour. - Completes the concrete slice of Phase 4’s “overall visual polish”; broader taste-driven refinement across all pages remains open to direction. Phase 5 (Hermes provider) stays blocked on the API-contract spike.
- Suite: 2385 tests (+2:
tests/empty-state-consistency.test.mjs).
[1.149.0] — 2026-08-12
Portals moved into Settings (Phase 4) — #/portals now lives in the Setup nav group next to App settings, instead of under Sourcing. Since v1.144.0 it’s a settings surface (enable/disable tracked companies + an ATS health probe), not a sourcing action — so this is where it belongs. Nav-only change; the page and its route are untouched.
Changed
#/portalsnav item → Setup group (inpublic/index.html), placed right after App settings. Removed from the Sourcing group (which keeps Scan / Pipeline / Auto-pipeline / Funded companies). The#/portalsroute, the view, and thenav.portalslabel are unchanged — only the sidebar position moved.
Notes
- Nav markup only — no route, no view, no i18n key, no server change. Browser-verified (the Portals item now renders under the Setup header, 0 console errors); guarded by
tests/portals-nav-placement.test.mjs. - Completes the Phase 4 “Portals → Settings” item. The remaining Phase 4 line — a whole-app visual-polish pass — is tracked separately.
- Suite: 2383 tests (+2:
tests/portals-nav-placement.test.mjs).
[1.148.0] — 2026-08-12
Cleaner scan filters (Phase 4) — the result-filter panel is now a tidy grid — the #/scan filter panel moved from a ragged flex-wrap of rigid, variable-width boxes to a responsive grid, and the Apply/Reset actions now sit on their own separated, right-aligned row. Same filters, same behaviour — just easier to read. A senior-designer polish pass (no parent-sync).
Changed
#/scanresult-filter panel → responsive grid —.scan-filtersis nowdisplay: gridwithrepeat(auto-fill, minmax(180px, 1fr))columns and even gutters, so the 11 labelled filters align into tidy columns at every width instead of wrapping into a ragged row of 160–240px boxes.- Apply / Reset actions span the full grid on their own row, separated by a hairline and right-aligned, so they read as the panel’s primary action. Dropped the old hidden-label alignment hack + inner flex wrapper in
scan.js(buttons are now direct children of.scan-filters__actions).
Notes
- CSS + one small DOM cleanup only — every filter input id (
#scan-filter-*,#scan-apply) and theSR.render()wiring are unchanged, so the Playwright scan-filter flow is untouched. No new i18n keys. - Browser-verified (grid renders 4 columns at 1100px, actions row separated + right-aligned, 0 console errors); guarded by
tests/scan-filters-grid.test.mjs. - Remaining Phase 4: the broader “overall visual polish across all pages” pass is a subjective, whole-app design task left for explicit direction; the
#/portals→ settings-nav move is still open. - Suite: 2381 tests (+3:
tests/scan-filters-grid.test.mjs).
[1.147.0] — 2026-08-12
Hermes & Telegram — the in-app help section + cvstart.org surface (Phase 5b, part 2) — the second and final slice of the Hermes docs work: the how-to now lives inside the app’s own help guide, in all 17 languages, and the in-app docs assistant answers Hermes questions from it. Still docs-only — the Hermes LLM-provider path remains planned / not-yet-wired (Phase 5).
Added
- In-app help §30 “Hermes & Telegram” × 17 locales — a new help-guide section (what Hermes is + the two integration shapes; running on a cloud server; Telegram via Hermes + the “what NOT to expose” rule), reachable from
#/help. Thedocs-assistant/ floating “Ask the docs”DocsFabgrounding picks it up automatically — no wiring needed, since both readdocs/help/<lang>.md. - cvstart.org — a short Hermes explainer mirroring the README teaser and deep-linking to the GitHub guide.
Changed
- Help-bundle gate lifted 29 → 30 H2 / 105 → 108 H3 (
canonical-docs-coverage,help-ui,help-ru-config-section); §30 adds 3 H3s.
Notes
- Still nothing calls Hermes. New canary
tests/help-hermes-section.test.mjsasserts every locale carries the §30 section with its language-independent anchors (docs/integrations/HERMES.md,hermes-bridge,#/help,127.0.0.1, Telegram). The provider stays blocked on the Phase 5 API-contract spike. - This closes the Phase 5b docs + skill deliverable; the provider integration (Phase 5) remains a separate, blocked item.
- Suite: 2378 tests (+2:
tests/help-hermes-section.test.mjs).
[1.146.0] — 2026-08-12
Hermes agent + Telegram — the integration guide + a skill (Phase 5b, part 1) — you can run career-ops-ui on a cloud server and bridge its events (a finished scan, a new report, an urgent follow-up) to Telegram through a Nous Research Hermes agent. This release ships the design + deployment docs and a hermes-bridge skill; the Hermes LLM-provider path stays planned / not-yet-wired (blocked on the Phase 5 API-contract spike). Docs-ahead-of-code by design.
Added
docs/integrations/HERMES.md— the deep-dive: the two integration shapes (OpenAI-compatible endpoint vs. agent runtime), cloud-server deployment (reverse proxy + HTTPS + systemd, the read-only parent contract on a headless box), Telegram-via-Hermes, and a threat-model “what NOT to expose” list (no CV / salary / report bodies / keys to the channel).## Hermes agent + TelegramREADME teaser — a short pointer + link, in the EN README and mirrored across the fully-translated locale READMEs.- A
hermes-bridgeskill (.claude/skills/hermes-bridge/) that operationalizes the guide — prerequisite + scoping-gate checks (Node ≥ 18, keys present, endpoint reachability via the SSRF-safe path), never writes secrets to disk/logs, and refuses to invent a Hermes endpoint or claim the provider is wired. - An Integrations section in
docs/architecture/OVERVIEW.mdlinks the guide.
Notes
- Nothing calls Hermes yet. A canary (
tests/hermes-docs.test.mjs) asserts the “planned / not-yet-wired” honesty markers and thatllm-dispatch.mjshas no Hermes/Nous branch — so wiring the provider later must update the docs + roadmap in the same change. - Deferred to v1.147.0 (Phase 5b, part 2): the in-app help “Hermes & Telegram” H2 × 17 and the cvstart.org marketing surface.
- Suite: 2376 tests (+4:
tests/hermes-docs.test.mjs).
[1.145.0] — 2026-08-12
Insightful stats (cont.): a rebuildable chart — the #/stats “Target-role trend” tab now has a Build a chart widget: pick a metric × dimension and it re-renders live. First of the Phase-3 interactive-chart work. A user-reported UX request (no parent-sync).
Added
- Rebuildable metric × dimension chart — choose a metric (Vacancies / Median salary / Average salary) and a dimension (By country / By role), and the bar chart re-renders instantly. Salary metrics honor the existing currency + per-year ⇄ per-month controls; vacancies are a plain count. Built from the same aggregation (
RoleStats.aggregate) already computed for the tab — no new data or endpoint. - 8 new i18n keys × 17 locales (
stats.customChart/metric/dimension/metricVacancies/metricMedian/metricAvg/dimCountry/dimRole); assembled-dict snapshot 1200 → 1208.
Notes
- The widget lives on the Target-role trend tab (which owns the scan-derived role/country/salary aggregation). v1.140.0’s changelog mislabeled this same section as “My pipeline” — it was always
renderTrend. - Browser-verified: the metric + dimension selects render and the chart re-renders on change, 0 console errors.
- Suite: 2372 tests (+2:
tests/stats-custom-chart.test.mjs— the wiring + EN-keys guard).
career-ops-ui v1.267.1 · page 4/10 · mirror of the project changelog for cvstart.org