job-room's search API returns a short keyword-anchored teaser (always
< 500 chars), not the full posting. The >=6 DE/FR-word threshold was
calibrated against full page text and is unreachable on a couple of
sentences, so a genuinely German teaser (Consult & Pepper's "Senior
Fullstack Software Engineer") could clear it on word count alone despite
jobs.ch itself showing "Deutsch (Fliessend)" on the full posting. Use a
lower threshold for short text; the full-page >=6 threshold stays as-is
for longer descriptions, where it was tuned to avoid false positives.
The >=6 DE_CONTENT_RE word threshold needs a reasonably long snippet to
trip. An Expleo (Bern) posting slipped through because a concurrent fetch
under load returned a truncated description with too few matches, even
though the full text (72 matches) was entirely German.
A bare umlaut or eszett is a near-certain German signal and survives
truncation — one German sentence almost always carries one. Zurich/
Zurcher is excluded as the one umlaut word that shows up constantly in
genuinely English-language Swiss postings, as a place name.
Applied in isViableContent, isEnglishContent, isViableUrl and the
fetch-other.ts sources, so every description path gets the same check.
languageSkills[] (e.g. {languageIsoCode: "de", spokenLevel: "PROFICIENT"})
sits outside jobDescriptions[].description, so requiresGerman()/
isViableDescription() never saw it — the UI renders this as
"Deutsch (Fliessend)", which is exactly what kept reaching Telegram.
Measured: 69 of 1843 ads required PROFICIENT German, prose check caught
only 3.
datacareer.ch had been returning 403 on every page for months. The cause
was our own User-Agent: its nginx inverts the usual bot rule and blocks
browser-like UAs while serving honest ones (no UA, curl, Googlebot).
Give it PLAIN_HEADERS instead of the shared spoofed-Chrome HEADERS.
Basel has 8 jobs on a single page with no pagination, so page 2 returning
0 items is correct rather than truncation.
basel-jobs.ch was documented as un-integrable because DataDome blocks
plain curl. Only half of that was true. Measured: plain fetch 403s on any
UA, headless Chrome over CDP 403s, Playwright's own launch 403s even
headful — but a real Chrome launched normally with --remote-debugging-port
and attached over CDP afterwards gets 200. Clearance then lives entirely
in a datadome cookie that replays fine over plain fetch with a matching
UA, so a browser is touched only to mint, never to scrape. The earlier
"cookie can't be extracted" note was true of the browser extension, not
of CDP.
Minting needs a visible window, so launchd runs can't do it; a stale
cookie makes the source log and yield nothing instead of failing the run.
Refresh by hand with `bun run mint-datadome`, or set LINKME_NO_BROWSER=1.
Three passes, all needed — they cover different sets. The informatik
category is a curated taxonomy, not the whole board, and the two search
modes query different indexes: websearch=1 finds role words and postings
filed outside IT (+147 over the category), while the bare search is the
only mode matching stack terms — react, frontend and backend return zero
under websearch=1. Language checks run against div.plain-content only;
against the whole page the German site chrome alone clears the DE/FR
threshold and drops every job, English ones included.
Also fix a pre-existing double-wrapped Promise in the extraSources type.
jobbasel.ch's search route (/job/suche-{slug}) is server-rendered with
a window.__PRELOADED_STATE__ JSON blob, no headless browser needed.
englishjobsearch.ch works the same way via its ?format=markdown param.
Both swept with the same fixed keyword list job-room.ch already uses.
basel-jobs.ch was evaluated too but left out: it's DataDome-protected,
which blocks plain curl outright, and this project's fetchers are
intentionally browser-free.
While wiring these in, found that isEnglishDescription (now
isViableDescription) only ever ran the DE/FR word-count check against
inline descriptions, never requiresGerman() - so an English-written
posting demanding fluent German (e.g. PRODYNA's "you can communicate
fluently in both German and English") sailed straight through. That
gap existed in itjobs.ch and job-room.ch too, not just the two new
sources, so the fix applies to all four.
Adds the one-off scripts accumulated across recent sessions, removes
temp scripts whose searches now live in config.ts as role-family
sweeps, and updates fetch-other.ts plus the LinkedIn post drafts.
- move temp scripts to scripts/, cv files to cv/, jobs.csv to results/
- update index.ts and recheck.ts CSV path to results/jobs.csv
- fix imports in scripts/ to use ../config.ts and ../fetch.ts
- update CLAUDE.md and README.md to reflect new structure
- add plans/ with LinkedIn post drafts
- update .gitignore: ignore results/, jobs/, cv binaries, .DS_Store