github Fighter90/career-ops-ui v1.235.0

4 hours ago

[1.235.0] — 2026-09-16

Parent parity — career-ops main @ 68e6b94 (VERSION 1.33.0, 42 commits). Two mirrored fixes; one of them was losing MokaHR postings here, silently.

Fixed

A second job from the same MokaHR board vanished instead of being added. url-key.mjs reduces a posting URL to a comparison key, and it dropped the fragment unconditionally — reasonable, since almost every fragment is presentation-only (#apply, #section-2). MokaHR is the exception in our own registry: its postings are …/social-recruitment/{tenant}/{id}#/job/{n}, where every job on a tenant shares the path and the posting id exists only in the fragment. So all of them normalized to one key. Measured before the fix: paste two different MokaHR jobs into the pipeline and you end with one line — no error, no warning, nothing to notice; the scanner's freshness filter (en-scanner.mjs) reads the same key, so once any job from that tenant was in scan-history, every other one was discarded as "already seen". normalizeUrl now promotes a recognized #/job/{id} or #/jobs/{id} route into a comparison-only query key before the fragment is dropped — MokaHR keeps its established mokahr_job_id name so previously-written keys stay comparable, any other host gets an internal _career_ops_fragment_job_id. The promoted id is appended, not set: a URL already carrying that param holds a different posting's id, and overwriting it would merge exactly the two things the key exists to keep apart. Every unrecognized fragment still collapses, which is the half that was already right.
accept-encoding is now pinned on every source request. Left unset, Node negotiates whatever its undici build offers; from Node ≥ 23 that includes zstd, and amazon.jobs' zstd response comes back truncated at exactly 1024 bytes with a 200 status — so the failure reads as Unterminated string in JSON at position 1024, like a malformed API rather than a transport bug. Every request now sends gzip, deflate, br. On our runtimes this is preventive, not a repair: Node 18/20/22 — the CI matrix and the server (22.22.1) — send gzip, deflate, verified directly rather than assumed, so nothing was truncated here; what changes today is that br is offered, which those versions do not request by default but decode correctly. The default is applied case-insensitively, which is the part worth reviewing: header names are case-insensitive and fetch joins two same-named entries into one comma-separated value, so a caller passing Accept-Encoding: identity would otherwise have been sent gzip, deflate, br, identity.

Notes

Headers stay a plain object at the fetchImpl boundary, where the parent hands fetch a Headers instance. That is a deliberate divergence, not an oversight: web-ui's fetch helpers take an injectable fetchImpl, and 45 source suites read opts.headers['User-Agent'] off what they capture — a Headers instance has no bracket access, so the faithful port is of the behaviour (case-insensitive override, caller wins) rather than of the implementation. The caller's object is copied, never mutated, since several sources share one header constant. The parent's companion fix — applying a default user-agent case-insensitively — is a no-port: web-ui injects no default UA at all, each source sets the one its board requires, so there is no default for a caller's User-Agent to collide with.
Two new suites and three new cases pin the pair: tests/http-accept-encoding.test.mjs (the exact pinned value, each of the four fetch helpers, and a caller override under three capitalizations — asserting a single accept-encoding key survives, which is what the merge bug produced two of) and tests/parsers-pipeline-hash-route.test.mjs, which works the user-visible seam rather than the key: two MokaHR jobs must both reach the pipeline, a re-paste of the same one under four spellings must still collapse to one line, and a cosmetic fragment must not become an identity. All were confirmed failing first. The pin itself lives at the transport, not only in those helpers: 25 of the 92 sources call the injected fetchImpl directly and never pass through them, so makeTimeoutFetch — the wrapper both scanners actually inject — applies it, and that is the one chokepoint every source shares. (The parent needs no equivalent: providers/_http.mjs is its only transport.) A structural guard now fails CI if a new source calls fetch directly instead of taking the transport it was handed, and a behavioural case drives greenhouse, one of the 25, through the wrapper to prove the header reaches the wire.
Not ported, with the reason: liveness-browser.mjs (headed-Playwright reuse guard — web-ui does not drive a headed browser), check-jd-archive.mjs (no web-ui relay), test-all.mjs (parent's harness; the fork's vpFixtureEnv divergence is intact), web/ (the parent's own frontend, not mirrored), plugin manifests and SIGNATURES.md (repository-only). batch/batch-runner.sh (ampersands escaped before sed interpolation) and jd-skill-gap.mjs (resolves cv.md against the data root) are relayed read-only — web-ui shells into the parent's copies, so both land with no code change here, and the jd-skill-gap one makes the /api/jds/:id/skill-gap relay more correct: cv.md now resolves from the data root instead of whatever directory the process was spawned in. Fork divergences re-verified after the pull: the Cyrillic \p{L} fix in providers/telegram-channel.mjs, the fork's own providers/telegram.mjs, hermes in web/src/lib/clis.ts. Sources unchanged at 92 (87 EN + 5 RU), so help is untouched. 3042 → 3066 tests.

Don't miss a new career-ops-ui release

NewReleases is sending notifications on new releases.